use spg_engine::{Engine, QueryResult};
use spg_storage::{DataType, RangeKind, 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>> {
match eng.execute(sql).unwrap() {
QueryResult::Rows { rows, .. } => rows.into_iter().map(|r| r.values).collect(),
_ => panic!("expected Rows"),
}
}
#[test]
fn ddl_accepts_all_six_range_keywords() {
let mut eng = Engine::new();
eng.execute(
"CREATE TABLE t (
id INT NOT NULL,
i4 INT4RANGE,
i8 INT8RANGE,
num NUMRANGE,
ts TSRANGE,
tstz TSTZRANGE,
d DATERANGE
)",
)
.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::Range(RangeKind::Int4)
));
assert!(matches!(
schema.columns[2].ty,
DataType::Range(RangeKind::Int8)
));
assert!(matches!(
schema.columns[3].ty,
DataType::Range(RangeKind::Num)
));
assert!(matches!(
schema.columns[4].ty,
DataType::Range(RangeKind::Ts)
));
assert!(matches!(
schema.columns[5].ty,
DataType::Range(RangeKind::TsTz)
));
assert!(matches!(
schema.columns[6].ty,
DataType::Range(RangeKind::Date)
));
}
#[test]
fn insert_int4range_half_open_round_trips() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, r INT4RANGE)",
"INSERT INTO t VALUES (1, '[1,10)')",
]);
let rows = select(&mut eng, "SELECT r FROM t");
let Value::Range {
kind,
lower,
upper,
lower_inc,
upper_inc,
empty,
} = &rows[0][0]
else {
panic!("expected Range, got {:?}", rows[0][0]);
};
assert!(matches!(kind, RangeKind::Int4));
assert_eq!(lower.as_deref(), Some(&Value::Int(1)));
assert_eq!(upper.as_deref(), Some(&Value::Int(10)));
assert!(*lower_inc);
assert!(!*upper_inc);
assert!(!*empty);
}
#[test]
fn insert_int4range_closed_round_trips() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, r INT4RANGE)",
"INSERT INTO t VALUES (1, '[1,10]')",
]);
let rows = select(&mut eng, "SELECT r FROM t");
let Value::Range {
lower_inc,
upper_inc,
..
} = &rows[0][0]
else {
panic!()
};
assert!(*lower_inc);
assert!(*upper_inc);
}
#[test]
fn insert_int4range_unbounded_lower() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, r INT4RANGE)",
"INSERT INTO t VALUES (1, '(,10]')",
]);
let rows = select(&mut eng, "SELECT r FROM t");
let Value::Range { lower, upper, .. } = &rows[0][0] else {
panic!()
};
assert!(lower.is_none());
assert_eq!(upper.as_deref(), Some(&Value::Int(10)));
}
#[test]
fn insert_int4range_unbounded_upper() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, r INT4RANGE)",
"INSERT INTO t VALUES (1, '[1,)')",
]);
let rows = select(&mut eng, "SELECT r FROM t");
let Value::Range { lower, upper, .. } = &rows[0][0] else {
panic!()
};
assert_eq!(lower.as_deref(), Some(&Value::Int(1)));
assert!(upper.is_none());
}
#[test]
fn insert_empty_range() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, r INT4RANGE)",
"INSERT INTO t VALUES (1, 'empty')",
]);
let rows = select(&mut eng, "SELECT r FROM t");
let Value::Range { empty, .. } = &rows[0][0] else {
panic!()
};
assert!(*empty);
}
#[test]
fn insert_int8range_round_trips() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, r INT8RANGE)",
"INSERT INTO t VALUES (1, '[9999999999,99999999999)')",
]);
let rows = select(&mut eng, "SELECT r FROM t");
let Value::Range {
kind, lower, upper, ..
} = &rows[0][0]
else {
panic!()
};
assert!(matches!(kind, RangeKind::Int8));
assert_eq!(lower.as_deref(), Some(&Value::BigInt(9_999_999_999)));
assert_eq!(upper.as_deref(), Some(&Value::BigInt(99_999_999_999)));
}
#[test]
fn insert_daterange_round_trips() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, r DATERANGE)",
"INSERT INTO t VALUES (1, '[2025-01-01,2025-12-31)')",
]);
let rows = select(&mut eng, "SELECT r FROM t");
let Value::Range {
kind, lower, upper, ..
} = &rows[0][0]
else {
panic!()
};
assert!(matches!(kind, RangeKind::Date));
assert!(matches!(lower.as_deref(), Some(Value::Date(_))));
assert!(matches!(upper.as_deref(), Some(Value::Date(_))));
}
#[test]
fn range_null_column() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, r INT4RANGE)",
"INSERT INTO t VALUES (1, NULL)",
]);
let rows = select(&mut eng, "SELECT r FROM t");
assert!(matches!(rows[0][0], Value::Null));
}
#[test]
fn range_column_survives_catalog_round_trip() {
let mut eng = engine_with(&[
"CREATE TABLE bookings (id INT NOT NULL, period TSRANGE)",
"INSERT INTO bookings VALUES \
(1, '[2025-06-01 09:00:00,2025-06-01 10:00:00)'), \
(2, 'empty')",
]);
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, period FROM bookings ORDER BY id");
assert_eq!(rows.len(), 2);
let Value::Range { kind, empty, .. } = &rows[1][1] else {
panic!()
};
assert!(matches!(kind, RangeKind::Ts));
assert!(*empty);
}
#[test]
fn malformed_range_input_is_error() {
let mut eng = engine_with(&["CREATE TABLE t (id INT NOT NULL, r INT4RANGE)"]);
let r = eng.execute("INSERT INTO t VALUES (1, '[1,)abc')");
assert!(r.is_err(), "garbage range literal must error");
}
#[test]
fn range_display_round_trips_canonical_text() {
let mut eng = engine_with(&[
"CREATE TABLE t (id INT NOT NULL, r INT4RANGE)",
"INSERT INTO t VALUES (1, '[1,10)')",
]);
let r = eng.execute("SELECT r::text FROM t").unwrap();
match r {
QueryResult::Rows { rows, .. } => {
let Value::Text(s) = &rows[0].values[0] else {
panic!()
};
assert_eq!(s, "[1,10)");
}
_ => panic!(),
}
}