use spg_engine::{Engine, QueryResult};
use spg_storage::{DataType, Value};
fn engine_with(sqls: &[&str]) -> Engine {
let mut eng = Engine::new();
for sql in sqls {
let r = eng
.execute(sql)
.unwrap_or_else(|e| panic!("setup {sql:?}: {e:?}"));
assert!(matches!(r, QueryResult::CommandOk { .. }), "{sql:?}");
}
eng
}
fn select(eng: &mut Engine, sql: &str) -> Vec<Vec<Value<'static>>> {
match eng.execute(sql).unwrap() {
QueryResult::Rows { rows, .. } => rows.into_iter().map(|r| r.values).collect(),
_ => panic!("expected Rows"),
}
}
#[test]
fn ddl_accepts_timetz_keyword() {
let mut eng = Engine::new();
eng.execute("CREATE TABLE t (id INT NOT NULL, started TIMETZ)")
.unwrap();
let bytes = eng.snapshot();
let cat = spg_storage::Catalog::deserialize(&bytes).unwrap();
let schema = cat.get("t").unwrap().schema();
assert!(matches!(schema.columns[1].ty, DataType::TimeTz));
}
#[test]
fn insert_utc_offset_round_trips() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, started TIMETZ)",
"INSERT INTO t VALUES (1, '14:30:45+00')",
]);
let rows = select(&mut eng, "SELECT started FROM t");
let Value::TimeTz { us, offset_secs } = &rows[0][0] else {
panic!("expected Value::TimeTz, got {:?}", rows[0][0]);
};
assert_eq!(*us, 52_245_000_000);
assert_eq!(*offset_secs, 0);
}
#[test]
fn insert_positive_whole_hour_offset() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, started TIMETZ)",
"INSERT INTO t VALUES (1, '09:00:00+09')",
]);
let rows = select(&mut eng, "SELECT started FROM t");
let Value::TimeTz { us, offset_secs } = &rows[0][0] else {
panic!()
};
assert_eq!(*us, 32_400_000_000);
assert_eq!(*offset_secs, 9 * 3600);
}
#[test]
fn insert_negative_sub_hour_offset_newfoundland() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, started TIMETZ)",
"INSERT INTO t VALUES (1, '08:30:00-03:30')",
]);
let rows = select(&mut eng, "SELECT started FROM t");
let Value::TimeTz { us, offset_secs } = &rows[0][0] else {
panic!()
};
assert_eq!(*us, 30_600_000_000);
assert_eq!(*offset_secs, -(3 * 3600 + 30 * 60));
}
#[test]
fn insert_microseconds_with_india_offset() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, started TIMETZ)",
"INSERT INTO t VALUES (1, '14:30:45.123456+05:30')",
]);
let rows = select(&mut eng, "SELECT started FROM t");
let Value::TimeTz { us, offset_secs } = &rows[0][0] else {
panic!()
};
assert_eq!(*us, 52_245_123_456);
assert_eq!(*offset_secs, 5 * 3600 + 30 * 60);
}
#[test]
fn timetz_column_survives_catalog_round_trip() {
let mut eng = engine_with(&[
"CREATE TABLE meetings (id INT NOT NULL, started TIMETZ)",
"INSERT INTO meetings VALUES (1, '09:00:00+00'), (2, '14:30:00-05:00')",
]);
let bytes = eng.snapshot();
let cat = spg_storage::Catalog::deserialize(&bytes).unwrap();
let mut eng2 = Engine::restore(cat);
let rows = select(&mut eng2, "SELECT id, started FROM meetings ORDER BY id");
assert_eq!(rows.len(), 2);
let Value::TimeTz {
us: a_us,
offset_secs: a_off,
} = &rows[0][1]
else {
panic!()
};
let Value::TimeTz {
us: b_us,
offset_secs: b_off,
} = &rows[1][1]
else {
panic!()
};
assert_eq!(*a_us, 32_400_000_000);
assert_eq!(*a_off, 0);
assert_eq!(*b_us, 52_200_000_000);
assert_eq!(*b_off, -5 * 3600);
}
#[test]
fn timetz_null_column() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, started TIMETZ)",
"INSERT INTO t VALUES (1, NULL)",
]);
let rows = select(&mut eng, "SELECT started FROM t");
assert!(matches!(rows[0][0], Value::Null));
}
#[test]
fn timetz_malformed_input_is_error() {
let mut eng = engine_with(&["CREATE TABLE t (id INT NOT NULL, started TIMETZ)"]);
let r = eng.execute("INSERT INTO t VALUES (1, 'not a time')");
assert!(r.is_err(), "garbage TIMETZ literal must error");
}
#[test]
fn timetz_missing_offset_takes_the_session_zone() {
let mut eng = engine_with(&["CREATE TABLE t (id INT NOT NULL, started TIMETZ)"]);
eng.execute("INSERT INTO t VALUES (1, '14:30:45')").unwrap();
let r = eng.execute("SELECT started::text FROM t").unwrap();
let spg_engine::QueryResult::Rows { rows, .. } = r else {
panic!()
};
assert_eq!(
spg_engine::eval::value_to_text(&rows[0].values[0]),
"14:30:45+00"
);
}
#[test]
fn timetz_offset_out_of_range_is_error() {
let mut eng = engine_with(&["CREATE TABLE t (id INT NOT NULL, started TIMETZ)"]);
let r = eng.execute("INSERT INTO t VALUES (1, '14:30:45+15')");
assert!(r.is_err(), "offset > +14h must error");
}