use spg_engine::{Engine, QueryResult};
use spg_storage::{DataType, Value};
fn rows(r: QueryResult) -> Vec<Vec<Value<'static>>> {
match r {
QueryResult::Rows { rows, .. } => rows.into_iter().map(|r| r.values).collect(),
_ => panic!("expected Rows"),
}
}
#[test]
fn create_table_interval_column_is_accepted() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id INT NOT NULL, span INTERVAL NOT NULL)")
.unwrap();
let r = e.execute("SELECT span FROM t").unwrap();
let QueryResult::Rows { columns, .. } = r else {
panic!()
};
assert_eq!(columns[0].ty, DataType::Interval);
}
#[test]
fn insert_select_round_trip_days_dimension() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id INT NOT NULL, span INTERVAL NOT NULL)")
.unwrap();
e.execute(
"INSERT INTO t VALUES \
(1, INTERVAL '1 day'), \
(2, INTERVAL '30 days'), \
(3, INTERVAL '2 months')",
)
.unwrap();
let r = rows(e.execute("SELECT span FROM t ORDER BY id").unwrap());
assert_eq!(r.len(), 3);
assert_eq!(
r[0][0],
Value::Interval {
months: 0,
days: 1,
micros: 0,
}
);
assert_eq!(
r[1][0],
Value::Interval {
months: 0,
days: 30,
micros: 0,
}
);
assert_eq!(
r[2][0],
Value::Interval {
months: 2,
days: 0,
micros: 0,
}
);
}
#[test]
fn pg_byte_equal_day_vs_24h_preserved_through_storage() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id INT NOT NULL, span INTERVAL NOT NULL)")
.unwrap();
e.execute(
"INSERT INTO t VALUES \
(1, INTERVAL '1 day'), \
(2, INTERVAL '24 hours')",
)
.unwrap();
let r = rows(e.execute("SELECT span FROM t ORDER BY id").unwrap());
assert_eq!(
r[0][0],
Value::Interval {
months: 0,
days: 1,
micros: 0,
}
);
assert_eq!(
r[1][0],
Value::Interval {
months: 0,
days: 0,
micros: 86_400_000_000,
}
);
assert_ne!(r[0][0], r[1][0]);
}
#[test]
fn compound_interval_round_trips_through_storage() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id INT NOT NULL, span INTERVAL NOT NULL)")
.unwrap();
e.execute(
"INSERT INTO t VALUES (1, INTERVAL '1 year 2 months 3 days 4 hours 5 minutes 6 seconds')",
)
.unwrap();
let r = rows(e.execute("SELECT span FROM t").unwrap());
assert_eq!(
r[0][0],
Value::Interval {
months: 14,
days: 3,
micros: (4_i64 * 3600 + 5 * 60 + 6) * 1_000_000,
}
);
}
#[test]
fn nullable_interval_column_accepts_null() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id INT NOT NULL, span INTERVAL)")
.unwrap();
e.execute("INSERT INTO t VALUES (1, NULL), (2, INTERVAL '1 day')")
.unwrap();
let r = rows(e.execute("SELECT span FROM t ORDER BY id").unwrap());
assert_eq!(r[0][0], Value::Null);
assert_eq!(
r[1][0],
Value::Interval {
months: 0,
days: 1,
micros: 0,
}
);
}
#[test]
fn stored_interval_participates_in_arithmetic() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id INT NOT NULL, span INTERVAL NOT NULL)")
.unwrap();
e.execute("INSERT INTO t VALUES (1, INTERVAL '1 day')")
.unwrap();
let r = rows(
e.execute("SELECT '2024-06-01 00:00:00'::TIMESTAMP + span FROM t")
.unwrap(),
);
let Value::Timestamp(t) = r[0][0] else {
panic!("expected Timestamp, got {:?}", r[0][0]);
};
let one_day_micros: i64 = 86_400_000_000;
let jun1_micros: i64 = {
let y = 2024_i64;
let m = 6_i64;
let d = 1_i64;
let _ = (y, m, d);
t - one_day_micros
};
assert_eq!(t - jun1_micros, one_day_micros);
}
#[test]
fn many_intervals_round_trip_unchanged() {
let mut e = Engine::new();
e.execute("CREATE TABLE t (id INT NOT NULL, span INTERVAL NOT NULL)")
.unwrap();
let cases: &[(i32, i32, i64)] = &[
(0, 0, 0),
(0, 1, 0),
(0, 0, 86_400_000_000),
(1, 0, 0),
(12, 31, 86_399_999_999),
(-1, -1, -1),
(i32::MAX, 0, 0),
(0, i32::MAX, 0),
(0, 0, i64::MAX),
(i32::MIN, i32::MIN, i64::MIN),
];
for (idx, (m, d, us)) in cases.iter().enumerate() {
let lit = format!("INTERVAL '{} mons {} days {} microseconds'", m, d, us);
e.execute(&format!("INSERT INTO t VALUES ({}, {})", idx, lit))
.unwrap();
}
let r = rows(e.execute("SELECT span FROM t ORDER BY id").unwrap());
for (idx, (m, d, us)) in cases.iter().enumerate() {
assert_eq!(
r[idx][0],
Value::Interval {
months: *m,
days: *d,
micros: *us,
},
"round-trip mismatch @ idx {idx}: ({m}, {d}, {us})"
);
}
}