#![allow(clippy::approx_constant)] #![allow(clippy::single_char_pattern)] #![allow(clippy::unwrap_used)] #![allow(clippy::too_many_lines)] #![allow(clippy::float_cmp)] #![allow(clippy::unreadable_literal)] #![allow(clippy::inconsistent_digit_grouping)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::cast_sign_loss)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::similar_names)] #![allow(clippy::ignored_unit_patterns)]
mod complex_queries;
mod error_tests;
mod property_tests;
mod type_integration;
mod type_tests;
mod window_tests;
use std::collections::HashMap;
use std::ops::Range;
use bytes::Bytes;
use kimberlite_store::{Key, ProjectionStore, StoreError, TableId, WriteBatch, WriteOp};
use kimberlite_types::Offset;
use crate::QueryEngine;
use crate::schema::{
ColumnDef, ColumnName, DataType, IndexDef, Schema, SchemaBuilder, TableDef, TableName,
};
use crate::value::Value;
#[derive(Debug, Default)]
struct MockStore {
tables: HashMap<TableId, Vec<(Key, Bytes)>>,
position: Offset,
}
impl MockStore {
fn new() -> Self {
Self::default()
}
fn insert(&mut self, table_id: TableId, key: Key, value: Bytes) {
let table = self.tables.entry(table_id).or_default();
table.push((key, value));
table.sort_by(|a, b| a.0.cmp(&b.0));
}
fn insert_json(&mut self, table_id: TableId, key: Key, json: &serde_json::Value) {
let bytes = Bytes::from(serde_json::to_vec(json).expect("json serialization failed"));
self.insert(table_id, key, bytes);
}
}
impl ProjectionStore for MockStore {
fn apply(&mut self, batch: WriteBatch) -> Result<(), StoreError> {
for op in batch.operations() {
match op {
WriteOp::Put { table, key, value } => {
self.insert(*table, key.clone(), value.clone());
}
WriteOp::Delete { table, key } => {
if let Some(t) = self.tables.get_mut(table) {
t.retain(|(k, _)| k != key);
}
}
}
}
self.position = batch.position();
Ok(())
}
fn applied_position(&self) -> Offset {
self.position
}
fn get(&mut self, table: TableId, key: &Key) -> Result<Option<Bytes>, StoreError> {
Ok(self
.tables
.get(&table)
.and_then(|t| t.iter().find(|(k, _)| k == key))
.map(|(_, v)| v.clone()))
}
fn get_at(
&mut self,
table: TableId,
key: &Key,
_pos: Offset,
) -> Result<Option<Bytes>, StoreError> {
self.get(table, key)
}
fn scan(
&mut self,
table: TableId,
range: Range<Key>,
limit: usize,
) -> Result<Vec<(Key, Bytes)>, StoreError> {
let Some(entries) = self.tables.get(&table) else {
return Ok(vec![]);
};
let result: Vec<_> = entries
.iter()
.filter(|(k, _)| k >= &range.start && k < &range.end)
.take(limit)
.cloned()
.collect();
Ok(result)
}
fn scan_at(
&mut self,
table: TableId,
range: Range<Key>,
limit: usize,
_pos: Offset,
) -> Result<Vec<(Key, Bytes)>, StoreError> {
self.scan(table, range, limit)
}
fn sync(&mut self) -> Result<(), StoreError> {
Ok(())
}
fn purge_table(&mut self, table: TableId) -> Result<(), StoreError> {
self.tables.remove(&table);
Ok(())
}
}
fn test_schema() -> crate::Schema {
SchemaBuilder::new()
.table(
"users",
TableId::new(1),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text).not_null(),
ColumnDef::new("age", DataType::BigInt),
],
vec!["id".into()],
)
.table(
"orders",
TableId::new(2),
vec![
ColumnDef::new("order_id", DataType::BigInt).not_null(),
ColumnDef::new("user_id", DataType::BigInt).not_null(),
ColumnDef::new("total", DataType::BigInt),
],
vec!["order_id".into()],
)
.build()
}
fn test_store() -> MockStore {
use crate::key_encoder::encode_key;
let mut store = MockStore::new();
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "Alice", "age": 30}),
);
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "name": "Bob", "age": 25}),
);
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "name": "Charlie", "age": 35}),
);
store.insert_json(
TableId::new(2),
encode_key(&[Value::BigInt(100)]),
&serde_json::json!({"order_id": 100, "user_id": 1, "total": 500}),
);
store.insert_json(
TableId::new(2),
encode_key(&[Value::BigInt(101)]),
&serde_json::json!({"order_id": 101, "user_id": 2, "total": 300}),
);
store
}
#[test]
fn test_select_all() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users", &[])
.unwrap();
assert_eq!(result.columns.len(), 3);
assert_eq!(result.rows.len(), 3);
}
#[test]
fn test_select_columns() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT name, age FROM users", &[])
.unwrap();
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[0].as_str(), "name");
assert_eq!(result.columns[1].as_str(), "age");
}
#[test]
fn test_point_lookup() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users WHERE id = 1", &[])
.unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(1));
assert_eq!(result.rows[0][1], Value::Text("Alice".to_string()));
}
#[test]
fn test_point_lookup_not_found() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users WHERE id = 999", &[])
.unwrap();
assert!(result.rows.is_empty());
}
#[test]
fn test_range_scan_gt() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users WHERE id > 1", &[])
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_range_scan_lt() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users WHERE id < 3", &[])
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_table_scan_with_filter() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users WHERE name = 'Bob'", &[])
.unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][1], Value::Text("Bob".to_string()));
}
#[test]
fn test_limit() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users LIMIT 2", &[])
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_order_by() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users ORDER BY age ASC", &[])
.unwrap();
assert_eq!(result.rows.len(), 3);
assert_eq!(result.rows[0][2], Value::BigInt(25)); assert_eq!(result.rows[1][2], Value::BigInt(30)); assert_eq!(result.rows[2][2], Value::BigInt(35)); }
#[test]
fn test_order_by_desc() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users ORDER BY age DESC", &[])
.unwrap();
assert_eq!(result.rows[0][2], Value::BigInt(35)); assert_eq!(result.rows[2][2], Value::BigInt(25)); }
#[test]
fn test_parameterized_query() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users WHERE id = $1",
&[Value::BigInt(2)],
)
.unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][1], Value::Text("Bob".to_string()));
}
#[test]
fn test_multiple_params() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users WHERE id >= $1 AND id <= $2",
&[Value::BigInt(1), Value::BigInt(2)],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_in_predicate() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users WHERE id IN (1, 3)", &[])
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_parameterized_limit() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users LIMIT $1",
&[Value::BigInt(2)],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_parameterized_offset() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users ORDER BY id LIMIT $1 OFFSET $2",
&[Value::BigInt(10), Value::BigInt(1)],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
assert_eq!(result.rows[0][0], Value::BigInt(2));
assert_eq!(result.rows[1][0], Value::BigInt(3));
}
#[test]
fn test_cursor_pagination_shape() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id, name FROM users WHERE id >= $1 ORDER BY id DESC LIMIT $2",
&[Value::BigInt(2), Value::BigInt(1)],
)
.unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(3)); }
#[test]
fn test_offset_literal_actually_skips() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users ORDER BY id OFFSET 2", &[])
.unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(3));
}
#[test]
fn test_limit_param_negative_rejected() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(
&mut store,
"SELECT * FROM users LIMIT $1",
&[Value::BigInt(-5)],
);
assert!(result.is_err(), "negative LIMIT should be rejected");
}
#[test]
fn test_limit_param_wrong_type_rejected() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(
&mut store,
"SELECT * FROM users LIMIT $1",
&[Value::Text("ten".to_string())],
);
assert!(
result.is_err(),
"non-integer LIMIT param should be rejected"
);
}
#[test]
fn test_intersect_basic() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE id = 1 INTERSECT SELECT id FROM users WHERE id IN (1, 2)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(1));
}
#[test]
fn test_except_basic() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users EXCEPT SELECT id FROM users WHERE id = 2",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
let ids: std::collections::HashSet<_> = result.rows.iter().map(|r| &r[0]).collect();
assert!(ids.contains(&Value::BigInt(1)));
assert!(ids.contains(&Value::BigInt(3)));
}
#[test]
fn test_intersect_dedups_by_default() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE id IN (1,2,3) INTERSECT SELECT id FROM users WHERE id IN (1,2,3)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 3);
}
#[test]
fn test_except_all_preserves_multiplicity() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users EXCEPT ALL SELECT id FROM users WHERE id = 1",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_union_still_works() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE id = 1 UNION SELECT id FROM users WHERE id = 2",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_recursive_cte_parses() {
let parsed = crate::parser::parse_statement(
"WITH RECURSIVE descendants AS ( \
SELECT id FROM users WHERE id = 1 \
UNION ALL \
SELECT u.id FROM users u WHERE u.id IN (SELECT id FROM descendants) \
) \
SELECT id FROM descendants",
);
assert!(parsed.is_ok(), "WITH RECURSIVE must parse: {parsed:?}");
}
#[test]
fn test_in_subquery_uncorrelated() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE id IN (SELECT user_id FROM orders)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_exists_uncorrelated_returns_all_rows() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE EXISTS (SELECT order_id FROM orders WHERE order_id > 0)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 3);
}
#[test]
fn test_exists_uncorrelated_empty_returns_no_rows() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE EXISTS (SELECT order_id FROM orders WHERE order_id < 0)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 0);
}
#[test]
fn test_not_exists_uncorrelated() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE NOT EXISTS (SELECT order_id FROM orders WHERE order_id < 0)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 3);
}
#[test]
fn test_correlated_exists_healthcare_golden() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users u WHERE EXISTS (SELECT order_id FROM orders o WHERE o.user_id = u.id)",
&[],
)
.unwrap();
assert_eq!(
result.rows.len(),
2,
"expected 2 users with orders, got {:?}",
result.rows
);
let ids: Vec<i64> = result
.rows
.iter()
.map(|r| match &r[0] {
Value::BigInt(n) => *n,
other => panic!("expected BigInt, got {other:?}"),
})
.collect();
assert!(ids.contains(&1));
assert!(ids.contains(&2));
assert!(!ids.contains(&3));
}
#[test]
fn test_correlated_not_exists() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users u WHERE NOT EXISTS (SELECT order_id FROM orders o WHERE o.user_id = u.id)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(3));
}
#[test]
fn test_correlated_in_subquery() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users u WHERE u.id IN (SELECT o.user_id FROM orders o WHERE o.total > u.age)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_correlated_not_in_subquery() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users u WHERE u.id NOT IN (SELECT o.user_id FROM orders o WHERE o.total > u.age)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(3));
}
#[test]
fn test_correlated_cardinality_cap_triggers() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema).with_correlated_cap(5);
let err = engine
.query(
&mut store,
"SELECT id FROM users u WHERE EXISTS (SELECT order_id FROM orders o WHERE o.user_id = u.id AND o.total > u.age)",
&[],
)
.unwrap_err();
match err {
crate::QueryError::CorrelatedCardinalityExceeded { estimated, cap } => {
assert_eq!(cap, 5);
assert!(estimated >= 6, "estimated={estimated}");
}
other => panic!("expected CorrelatedCardinalityExceeded, got {other:?}"),
}
}
#[test]
fn test_correlated_cardinality_cap_10m_rejects_1m_by_1m() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema).with_correlated_cap(4);
let result = engine.query(
&mut store,
"SELECT id FROM users u WHERE EXISTS (SELECT order_id FROM orders o WHERE o.user_id = u.id AND o.total > u.age)",
&[],
);
assert!(matches!(
result,
Err(crate::QueryError::CorrelatedCardinalityExceeded { .. })
));
}
#[test]
fn test_correlated_decorrelated_semi_join() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT id FROM users u WHERE EXISTS (SELECT order_id FROM orders o WHERE o.user_id = u.id)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_correlated_exists_healthcare_faithful_schema() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"patient_current",
TableId::new(10),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text),
],
vec!["id".into()],
)
.table(
"consent_current",
TableId::new(11),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("subject_id", DataType::BigInt).not_null(),
ColumnDef::new("purpose", DataType::Text),
ColumnDef::new("withdrawn_at", DataType::Timestamp),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
store.insert_json(
TableId::new(10),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "Alice"}),
);
store.insert_json(
TableId::new(10),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "name": "Bob"}),
);
store.insert_json(
TableId::new(10),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "name": "Charlie"}),
);
store.insert_json(
TableId::new(11),
encode_key(&[Value::BigInt(100)]),
&serde_json::json!({
"id": 100, "subject_id": 1, "purpose": "HealthcareDelivery",
"withdrawn_at": null,
}),
);
store.insert_json(
TableId::new(11),
encode_key(&[Value::BigInt(101)]),
&serde_json::json!({
"id": 101, "subject_id": 2, "purpose": "HealthcareDelivery",
"withdrawn_at": null,
}),
);
store.insert_json(
TableId::new(11),
encode_key(&[Value::BigInt(102)]),
&serde_json::json!({
"id": 102, "subject_id": 3, "purpose": "HealthcareDelivery",
"withdrawn_at": 1_700_000_000_000_000_000_i64,
}),
);
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT p.id FROM patient_current p \
WHERE EXISTS ( \
SELECT id FROM consent_current c \
WHERE c.subject_id = p.id \
AND c.purpose = 'HealthcareDelivery' \
AND c.withdrawn_at IS NULL \
) \
ORDER BY id",
&[],
)
.unwrap();
assert_eq!(
result.rows.len(),
2,
"expected Alice + Bob, got {:?}",
result.rows
);
assert_eq!(result.rows[0][0], Value::BigInt(1));
assert_eq!(result.rows[1][0], Value::BigInt(2));
}
#[test]
fn test_uncorrelated_subquery_still_fast_path() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema).with_correlated_cap(1);
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE id IN (SELECT user_id FROM orders)",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_right_join_basic() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT users.id, orders.order_id FROM users RIGHT JOIN orders ON users.id = orders.user_id",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_full_outer_join() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT users.id, orders.order_id FROM users FULL OUTER JOIN orders ON users.id = orders.user_id",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 3);
}
#[test]
fn test_cross_join_basic() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT users.id, orders.order_id FROM users CROSS JOIN orders",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 6);
}
#[test]
fn test_join_using_clause() {
let parsed =
crate::parser::parse_statement("SELECT users.id FROM users INNER JOIN users u2 USING(id)");
assert!(parsed.is_ok(), "USING(col) must parse: {parsed:?}");
}
#[test]
fn test_aggregate_filter_count_star() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT COUNT(*) FILTER (WHERE age > 28) FROM users",
&[],
)
.unwrap();
assert_eq!(result.rows[0][0], Value::BigInt(2));
}
#[test]
fn test_aggregate_filter_sum() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT SUM(age) FILTER (WHERE age >= 30) FROM users",
&[],
)
.unwrap();
assert_eq!(result.rows[0][0], Value::BigInt(65));
}
#[test]
fn test_aggregate_filter_independent_per_aggregate() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT COUNT(*) FILTER (WHERE age < 30), COUNT(*) FILTER (WHERE age >= 30) FROM users",
&[],
)
.unwrap();
assert_eq!(result.rows[0][0], Value::BigInt(1));
assert_eq!(result.rows[0][1], Value::BigInt(2));
}
#[test]
fn test_simple_case_form_parses() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT *, CASE name WHEN 'Alice' THEN 'A' WHEN 'Bob' THEN 'B' ELSE 'X' END AS letter FROM users ORDER BY id",
&[],
);
let _ = result; let parsed = crate::parser::parse_statement(
"SELECT id, CASE name WHEN 'Alice' THEN 'A' ELSE 'X' END AS letter FROM users",
);
assert!(parsed.is_ok(), "simple CASE must parse: {parsed:?}");
}
#[test]
fn test_limit_literal_still_works() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users LIMIT 1", &[])
.unwrap();
assert_eq!(result.rows.len(), 1);
}
#[test]
fn test_prepared_query() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let prepared = engine
.prepare("SELECT name FROM users WHERE id = $1", &[Value::BigInt(1)])
.unwrap();
assert_eq!(prepared.columns().len(), 1);
assert_eq!(prepared.table_name(), "users");
let result = prepared.execute(&mut store).unwrap();
assert_eq!(result.rows.len(), 1);
}
#[test]
fn test_unknown_table() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT * FROM nonexistent", &[]);
assert!(matches!(result, Err(crate::QueryError::TableNotFound(_))));
}
#[test]
fn test_unknown_column() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT nonexistent FROM users", &[]);
assert!(matches!(
result,
Err(crate::QueryError::ColumnNotFound { .. })
));
}
#[test]
fn test_missing_param() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT * FROM users WHERE id = $1", &[]);
assert!(matches!(
result,
Err(crate::QueryError::ParameterNotFound(1))
));
}
#[test]
fn test_query_at_position() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query_at(
&mut store,
"SELECT * FROM users WHERE id = 1",
&[],
Offset::new(100),
)
.unwrap();
assert_eq!(result.rows.len(), 1);
}
#[test]
fn test_information_schema_tables_lists_registered_tables() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM information_schema.tables", &[])
.unwrap();
assert_eq!(
result.columns,
vec![
"table_name".into(),
"column_count".into(),
"primary_key_count".into(),
]
);
let has_users = result
.rows
.iter()
.any(|row| matches!(&row[0], Value::Text(s) if s == "users"));
assert!(has_users, "expected `users` in rows: {:?}", result.rows);
}
#[test]
fn test_information_schema_columns_lists_columns_per_table() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT table_name, column_name FROM information_schema.columns",
&[],
)
.unwrap();
assert!(result.columns.len() >= 4);
assert!(!result.rows.is_empty());
for row in &result.rows {
assert_eq!(row.len(), 4);
}
}
#[test]
fn test_information_schema_bypasses_parse_cache_and_planner() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema).with_parse_cache(4);
let _ = engine
.query(&mut store, "SELECT * FROM information_schema.tables", &[])
.unwrap();
let s = engine.parse_cache_stats().unwrap();
assert_eq!(s.size, 0);
assert_eq!(s.misses, 0);
}
#[test]
fn test_parse_cache_miss_then_hit() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema).with_parse_cache(4);
let _ = engine
.query(&mut store, "SELECT * FROM users WHERE id = 1", &[])
.unwrap();
let s1 = engine.parse_cache_stats().unwrap();
assert_eq!(s1.misses, 1);
assert_eq!(s1.hits, 0);
assert_eq!(s1.size, 1);
let _ = engine
.query(&mut store, "SELECT * FROM users WHERE id = 1", &[])
.unwrap();
let s2 = engine.parse_cache_stats().unwrap();
assert_eq!(s2.hits, 1);
assert_eq!(s2.misses, 1);
assert_eq!(s2.size, 1);
}
#[test]
fn test_parse_cache_off_by_default() {
let schema = test_schema();
let engine = QueryEngine::new(schema);
assert!(engine.parse_cache_stats().is_none());
}
#[test]
fn test_parse_cache_distinct_sql_both_stored() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema).with_parse_cache(4);
let _ = engine
.query(&mut store, "SELECT * FROM users WHERE id = 1", &[])
.unwrap();
let _ = engine
.query(&mut store, "SELECT * FROM users WHERE id = 2", &[])
.unwrap();
let s = engine.parse_cache_stats().unwrap();
assert_eq!(s.size, 2);
assert_eq!(s.misses, 2);
}
#[test]
fn test_parse_cache_clear_resets_size_but_keeps_hit_counters() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema).with_parse_cache(4);
let _ = engine
.query(&mut store, "SELECT * FROM users WHERE id = 1", &[])
.unwrap();
engine.clear_parse_cache();
let s = engine.parse_cache_stats().unwrap();
assert_eq!(s.size, 0);
assert_eq!(s.misses, 1);
}
#[test]
fn test_explain_returns_plan_tree() {
let schema = test_schema();
let engine = QueryEngine::new(schema);
let plan_text = engine
.explain("SELECT * FROM users WHERE id = 1", &[])
.unwrap();
assert!(plan_text.starts_with("-> "));
assert!(plan_text.contains("users"));
}
#[test]
fn test_explain_does_not_execute() {
let schema = test_schema();
let engine = QueryEngine::new(schema);
let plan = engine.explain("SELECT id FROM users WHERE id = $1", &[Value::BigInt(1)]);
assert!(plan.is_ok());
}
#[test]
fn test_query_with_explain_prefix_returns_plan_as_row() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "EXPLAIN SELECT * FROM users WHERE id = 1", &[])
.unwrap();
assert_eq!(result.columns.len(), 1);
assert_eq!(result.columns[0].as_str(), "plan");
assert_eq!(result.rows.len(), 1);
match &result.rows[0][0] {
Value::Text(t) => {
assert!(t.starts_with("-> "));
assert!(t.contains("users"));
}
other => panic!("expected Value::Text, got {other:?}"),
}
}
#[test]
fn test_explain_case_insensitive_prefix() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "explain SELECT * FROM users WHERE id = 1", &[])
.unwrap();
assert_eq!(result.columns[0].as_str(), "plan");
}
#[test]
fn test_explain_is_deterministic() {
let schema = test_schema();
let engine = QueryEngine::new(schema);
let a = engine
.explain("SELECT id FROM users WHERE id = $1", &[Value::BigInt(1)])
.unwrap();
let b = engine
.explain("SELECT id FROM users WHERE id = $1", &[Value::BigInt(1)])
.unwrap();
assert_eq!(a, b);
}
#[test]
fn test_query_at_timestamp_calls_resolver_with_target_ns() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let seen_target = std::cell::Cell::new(0_i64);
let resolver = |ns: i64| {
seen_target.set(ns);
Some(Offset::new(100))
};
let target = 1_768_435_200_000_000_000_i64;
let result = engine
.query_at_timestamp(
&mut store,
"SELECT * FROM users WHERE id = 1",
&[],
target,
resolver,
)
.unwrap();
assert_eq!(seen_target.get(), target);
assert_eq!(result.rows.len(), 1);
}
#[test]
fn test_query_at_timestamp_surfaces_resolver_none_as_error() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let resolver = |_: i64| None;
let err = engine
.query_at_timestamp(
&mut store,
"SELECT * FROM users WHERE id = 1",
&[],
1_768_435_200_000_000_000,
resolver,
)
.unwrap_err();
assert!(
matches!(err, crate::QueryError::UnsupportedFeature(ref m) if m.contains("predates genesis")),
"expected UnsupportedFeature with 'predates genesis', got {err:?}"
);
}
#[test]
fn test_query_with_timestamp_syntax_but_no_resolver_errors_clearly() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let err = engine
.query(
&mut store,
"SELECT * FROM users WHERE id = 1 FOR SYSTEM_TIME AS OF '2026-01-15T00:00:00Z'",
&[],
)
.unwrap_err();
assert!(
matches!(err, crate::QueryError::UnsupportedFeature(ref m) if m.contains("query_at_timestamp")),
"expected guidance toward query_at_timestamp, got {err:?}"
);
}
#[cfg(test)]
mod at_offset_tests {
use crate::parser::extract_at_offset;
#[test]
fn basic_extraction() {
let (sql, offset) = extract_at_offset("SELECT * FROM patients AT OFFSET 3");
assert_eq!(sql, "SELECT * FROM patients");
assert_eq!(offset, Some(3));
}
#[test]
fn with_semicolon() {
let (sql, offset) = extract_at_offset("SELECT * FROM patients AT OFFSET 3;");
assert_eq!(sql, "SELECT * FROM patients");
assert_eq!(offset, Some(3));
}
#[test]
fn offset_zero() {
let (sql, offset) = extract_at_offset("SELECT * FROM users AT OFFSET 0");
assert_eq!(sql, "SELECT * FROM users");
assert_eq!(offset, Some(0));
}
#[test]
fn large_offset() {
let (sql, offset) = extract_at_offset("SELECT * FROM logs AT OFFSET 1000000");
assert_eq!(sql, "SELECT * FROM logs");
assert_eq!(offset, Some(1_000_000));
}
#[test]
fn no_at_offset_returns_none() {
let (sql, offset) = extract_at_offset("SELECT * FROM patients WHERE id = 1");
assert_eq!(sql, "SELECT * FROM patients WHERE id = 1");
assert_eq!(offset, None);
}
#[test]
fn case_insensitive() {
let (sql, offset) = extract_at_offset("SELECT * FROM users at offset 42");
assert_eq!(sql, "SELECT * FROM users");
assert_eq!(offset, Some(42));
}
#[test]
fn mixed_case() {
let (sql, offset) = extract_at_offset("SELECT * FROM users At Offset 10");
assert_eq!(sql, "SELECT * FROM users");
assert_eq!(offset, Some(10));
}
#[test]
fn with_where_clause() {
let (sql, offset) = extract_at_offset("SELECT * FROM patients WHERE id = 1 AT OFFSET 5");
assert_eq!(sql, "SELECT * FROM patients WHERE id = 1");
assert_eq!(offset, Some(5));
}
#[test]
fn does_not_match_format_at() {
let (sql, offset) = extract_at_offset("SELECT * FROM format_table");
assert_eq!(sql, "SELECT * FROM format_table");
assert_eq!(offset, None);
}
#[test]
fn no_number_after_at_offset() {
let (sql, offset) = extract_at_offset("SELECT * FROM t AT OFFSET abc");
assert_eq!(sql, "SELECT * FROM t AT OFFSET abc");
assert_eq!(offset, None);
}
#[test]
fn time_travel_for_system_time_as_of_iso() {
use crate::parser::{TimeTravel, extract_time_travel};
let (sql, tt) = extract_time_travel(
"SELECT * FROM charts FOR SYSTEM_TIME AS OF '2026-01-15T00:00:00Z'",
);
assert_eq!(sql, "SELECT * FROM charts");
let ts = match tt {
Some(TimeTravel::TimestampNs(ns)) => ns,
other => panic!("expected TimestampNs, got {other:?}"),
};
assert_eq!(ts, 1_768_435_200_000_000_000);
}
#[test]
fn time_travel_bare_as_of_iso_sugar() {
use crate::parser::{TimeTravel, extract_time_travel};
let (sql, tt) = extract_time_travel("SELECT * FROM charts AS OF '2026-01-15T00:00:00Z'");
assert_eq!(sql, "SELECT * FROM charts");
assert!(matches!(tt, Some(TimeTravel::TimestampNs(_))));
}
#[test]
fn time_travel_offset_still_works_via_new_api() {
use crate::parser::{TimeTravel, extract_time_travel};
let (sql, tt) = extract_time_travel("SELECT * FROM patients AT OFFSET 3");
assert_eq!(sql, "SELECT * FROM patients");
assert_eq!(tt, Some(TimeTravel::Offset(3)));
}
#[test]
fn time_travel_invalid_timestamp_rejected() {
use crate::parser::extract_time_travel;
let sql_orig = "SELECT * FROM charts AS OF 'not-a-timestamp'";
let (sql, tt) = extract_time_travel(sql_orig);
assert_eq!(sql, sql_orig);
assert_eq!(tt, None);
}
#[test]
fn time_travel_bare_as_of_without_quote_ignored() {
use crate::parser::extract_time_travel;
let sql_orig = "SELECT x AS OF_ALIAS FROM t";
let (sql, tt) = extract_time_travel(sql_orig);
assert_eq!(sql, sql_orig);
assert_eq!(tt, None);
}
#[test]
fn time_travel_no_clause_returns_none() {
use crate::parser::extract_time_travel;
let (sql, tt) = extract_time_travel("SELECT * FROM patients");
assert_eq!(sql, "SELECT * FROM patients");
assert_eq!(tt, None);
}
#[test]
fn trailing_content_after_number_is_rejected() {
let (sql, offset) = extract_at_offset("SELECT * FROM t AT OFFSET 3 ORDER BY id");
assert_eq!(sql, "SELECT * FROM t AT OFFSET 3 ORDER BY id");
assert_eq!(offset, None);
}
}
#[test]
fn test_query_with_at_offset_in_sql() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users WHERE id = 1 AT OFFSET 100",
&[],
)
.unwrap();
assert_eq!(result.rows.len(), 1);
}
#[test]
fn test_query_with_at_offset_zero() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users AT OFFSET 0", &[])
.unwrap();
assert!(!result.rows.is_empty());
}
#[cfg(test)]
mod parser_tests {
use crate::parser::{ParsedStatement, parse_statement};
#[test]
fn parse_create_table() {
let sql = "CREATE TABLE users (id BIGINT NOT NULL, name TEXT NOT NULL, PRIMARY KEY (id))";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::CreateTable(ct) => {
assert_eq!(ct.table_name, "users");
assert_eq!(ct.columns.len(), 2);
assert_eq!(ct.columns[0].name, "id");
assert_eq!(ct.columns[0].data_type, "BIGINT");
assert!(!ct.columns[0].nullable);
assert_eq!(ct.columns[1].name, "name");
assert_eq!(ct.columns[1].data_type, "TEXT");
assert_eq!(ct.primary_key, vec!["id"]);
}
_ => panic!("expected CreateTable"),
}
}
#[test]
fn parse_create_table_with_nullable_column() {
let sql = "CREATE TABLE users (id BIGINT NOT NULL, age BIGINT, PRIMARY KEY (id))";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::CreateTable(ct) => {
assert_eq!(ct.columns.len(), 2);
assert!(!ct.columns[0].nullable); assert!(ct.columns[1].nullable); }
_ => panic!("expected CreateTable"),
}
}
#[test]
fn parse_create_table_with_composite_primary_key() {
let sql = "CREATE TABLE orders (
user_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
amount BIGINT,
PRIMARY KEY (user_id, order_id)
)";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::CreateTable(ct) => {
assert_eq!(ct.table_name, "orders");
assert_eq!(ct.primary_key, vec!["user_id", "order_id"]);
}
_ => panic!("expected CreateTable"),
}
}
#[test]
fn parse_drop_table() {
let sql = "DROP TABLE users";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::DropTable { name, if_exists } => {
assert_eq!(name, "users");
assert!(!if_exists, "DROP TABLE without IF EXISTS must report it");
}
_ => panic!("expected DropTable"),
}
}
#[test]
fn parse_drop_table_if_exists() {
let sql = "DROP TABLE IF EXISTS users";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::DropTable { name, if_exists } => {
assert_eq!(name, "users");
assert!(
if_exists,
"DROP TABLE IF EXISTS must thread the flag through"
);
}
_ => panic!("expected DropTable"),
}
}
#[test]
fn parse_create_index() {
let sql = "CREATE INDEX idx_name ON users (name)";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::CreateIndex(ci) => {
assert_eq!(ci.index_name, "idx_name");
assert_eq!(ci.table_name, "users");
assert_eq!(ci.columns, vec!["name"]);
}
_ => panic!("expected CreateIndex"),
}
}
#[test]
fn parse_create_index_composite() {
let sql = "CREATE INDEX idx_user_date ON orders (user_id, order_date)";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::CreateIndex(ci) => {
assert_eq!(ci.index_name, "idx_user_date");
assert_eq!(ci.table_name, "orders");
assert_eq!(ci.columns, vec!["user_id", "order_date"]);
}
_ => panic!("expected CreateIndex"),
}
}
#[test]
fn parse_insert() {
let sql = "INSERT INTO users (id, name) VALUES (1, 'Alice')";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::Insert(ins) => {
assert_eq!(ins.table, "users");
assert_eq!(ins.columns, vec!["id", "name"]);
assert_eq!(ins.values.len(), 1, "Should have 1 row");
assert_eq!(ins.values[0].len(), 2, "First row should have 2 values");
}
_ => panic!("expected Insert"),
}
}
#[test]
fn parse_insert_multiple_types() {
let sql = "INSERT INTO users (id, name, active, age) VALUES (1, 'Alice', true, 30)";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::Insert(ins) => {
assert_eq!(ins.table, "users");
assert_eq!(ins.columns.len(), 4);
assert_eq!(ins.values.len(), 1, "Should have 1 row");
assert_eq!(ins.values[0].len(), 4, "First row should have 4 values");
}
_ => panic!("expected Insert"),
}
}
#[test]
fn parse_update() {
let sql = "UPDATE users SET name = 'Bob' WHERE id = 1";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::Update(upd) => {
assert_eq!(upd.table, "users");
assert_eq!(upd.assignments.len(), 1);
assert_eq!(upd.predicates.len(), 1);
}
_ => panic!("expected Update"),
}
}
#[test]
fn parse_update_multiple_columns() {
let sql = "UPDATE users SET name = 'Bob', age = 31 WHERE id = 1";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::Update(upd) => {
assert_eq!(upd.table, "users");
assert_eq!(upd.assignments.len(), 2);
}
_ => panic!("expected Update"),
}
}
#[test]
fn parse_delete() {
let sql = "DELETE FROM users WHERE id = 1";
let result = parse_statement(sql);
if let Err(ref e) = result {
eprintln!("DELETE parse error: {e:?}");
}
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::Delete(del) => {
assert_eq!(del.table, "users");
assert_eq!(del.predicates.len(), 1);
}
_ => panic!("expected Delete"),
}
}
#[test]
fn parse_delete_multiple_conditions() {
let sql = "DELETE FROM users WHERE id > 100 AND active = false";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::Delete(del) => {
assert_eq!(del.table, "users");
assert_eq!(del.predicates.len(), 2);
}
_ => panic!("expected Delete"),
}
}
#[test]
fn parse_select_still_works() {
let sql = "SELECT id, name FROM users WHERE id = 1";
let result = parse_statement(sql);
assert!(result.is_ok());
match result.unwrap() {
ParsedStatement::Select(sel) => {
assert_eq!(sel.table, "users");
assert!(sel.columns.is_some());
assert_eq!(sel.columns.unwrap().len(), 2);
}
_ => panic!("expected Select"),
}
}
#[test]
fn parse_invalid_sql_fails() {
let sql = "INVALID SQL STATEMENT";
let result = parse_statement(sql);
assert!(result.is_err());
}
#[test]
fn parse_alter_table_add_column() {
let sql = "ALTER TABLE users ADD COLUMN email TEXT";
let result = parse_statement(sql);
assert!(result.is_ok());
if let Ok(ParsedStatement::AlterTable(alter)) = result {
assert_eq!(alter.table_name, "users");
assert!(matches!(
alter.operation,
crate::parser::AlterTableOperation::AddColumn(_)
));
} else {
panic!("expected ALTER TABLE statement");
}
}
#[test]
fn parse_alter_table_drop_column() {
let sql = "ALTER TABLE users DROP COLUMN email";
let result = parse_statement(sql);
assert!(result.is_ok());
if let Ok(ParsedStatement::AlterTable(alter)) = result {
assert_eq!(alter.table_name, "users");
assert!(matches!(
alter.operation,
crate::parser::AlterTableOperation::DropColumn(_)
));
} else {
panic!("expected ALTER TABLE statement");
}
}
}
#[cfg(test)]
mod key_encoding_tests {
use super::*;
use crate::key_encoder::{decode_key, encode_key};
use proptest::prelude::*;
proptest! {
#[test]
fn bigint_encoding_round_trip(v: i64) {
let key = encode_key(&[Value::BigInt(v)]);
let decoded = decode_key(&key);
prop_assert_eq!(decoded, vec![Value::BigInt(v)]);
}
#[test]
fn bigint_ordering_preserved(a: i64, b: i64) {
let key_a = encode_key(&[Value::BigInt(a)]);
let key_b = encode_key(&[Value::BigInt(b)]);
prop_assert_eq!(a.cmp(&b), key_a.cmp(&key_b));
}
#[test]
fn text_round_trip(s in "\\PC*") {
let key = encode_key(&[Value::Text(s.clone())]);
let decoded = decode_key(&key);
prop_assert_eq!(decoded, vec![Value::Text(s)]);
}
#[test]
fn composite_key_round_trip(a: i64, s in "[a-z]{0,10}") {
let values = vec![Value::BigInt(a), Value::Text(s.clone())];
let key = encode_key(&values);
let decoded = decode_key(&key);
prop_assert_eq!(decoded, values);
}
}
}
#[test]
fn test_null_in_where_clause() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"products",
TableId::new(3),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text), ],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(3),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "Widget"}),
);
store.insert_json(
TableId::new(3),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "name": null}),
);
store.insert_json(
TableId::new(3),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "name": "Gadget"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM products WHERE name IS NULL",
&[],
)
.expect("IS NULL query should succeed");
assert_eq!(result.rows.len(), 1, "Should find 1 row with NULL name");
assert_eq!(result.rows[0][0], Value::BigInt(2));
let result = engine
.query(
&mut store,
"SELECT id FROM products WHERE name IS NOT NULL",
&[],
)
.expect("IS NOT NULL query should succeed");
assert_eq!(
result.rows.len(),
2,
"Should find 2 rows with non-NULL names"
);
}
#[test]
fn test_null_in_order_by() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"items",
TableId::new(4),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("priority", DataType::BigInt), ],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(4),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "priority": 5}),
);
store.insert_json(
TableId::new(4),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "priority": null}),
);
store.insert_json(
TableId::new(4),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "priority": 1}),
);
store.insert_json(
TableId::new(4),
encode_key(&[Value::BigInt(4)]),
&serde_json::json!({"id": 4, "priority": null}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM items ORDER BY priority ASC",
&[],
)
.expect("ORDER BY with NULLs should succeed");
assert_eq!(result.rows.len(), 4);
let ids: Vec<i64> = result
.rows
.iter()
.map(|row| match &row[0] {
Value::BigInt(id) => *id,
_ => panic!("Expected BigInt"),
})
.collect();
assert_eq!(ids.len(), 4, "Should get all 4 rows back");
}
#[test]
fn test_bigint_max_min_values() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"extremes",
TableId::new(5),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("value", DataType::BigInt).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(5),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "value": i64::MAX}),
);
store.insert_json(
TableId::new(5),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "value": i64::MIN}),
);
store.insert_json(
TableId::new(5),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "value": 0}),
);
let result = engine
.query(&mut store, "SELECT * FROM extremes ORDER BY value ASC", &[])
.expect("Query with extreme values should succeed");
assert_eq!(result.rows.len(), 3);
assert_eq!(result.rows[0][1], Value::BigInt(i64::MIN));
assert_eq!(result.rows[1][1], Value::BigInt(0));
assert_eq!(result.rows[2][1], Value::BigInt(i64::MAX));
}
#[test]
fn test_empty_string_vs_null() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"strings",
TableId::new(6),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("text", DataType::Text), ],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(6),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "text": ""}),
);
store.insert_json(
TableId::new(6),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "text": null}),
);
store.insert_json(
TableId::new(6),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "text": "hello"}),
);
let result = engine
.query(&mut store, "SELECT id FROM strings WHERE text = ''", &[])
.expect("Query for empty string should succeed");
assert_eq!(result.rows.len(), 1, "Should find empty string row");
assert_eq!(result.rows[0][0], Value::BigInt(1)); }
#[test]
fn test_in_predicate_empty_list() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT * FROM users WHERE id IN ()", &[]);
assert!(
result.is_err(),
"IN () should fail - empty IN lists not supported by SQL parser"
);
match result {
Err(crate::QueryError::ParseError(msg)) => {
assert!(
msg.contains("Expected: an expression"),
"Should get parse error for empty IN list, got: {msg}"
);
}
other => panic!("Expected ParseError for empty IN list, got: {other:?}"),
}
}
#[test]
fn test_boolean_type_handling() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"flags",
TableId::new(7),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("active", DataType::Boolean).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(7),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "active": true}),
);
store.insert_json(
TableId::new(7),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "active": false}),
);
let result = engine
.query(&mut store, "SELECT id FROM flags WHERE active = true", &[])
.expect("Boolean query should succeed");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(1)); }
#[test]
fn test_or_operator_simple() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users WHERE id = 1 OR id = 3",
&[],
)
.expect("OR query should succeed");
assert_eq!(result.rows.len(), 2, "Should match 2 rows (id 1 and 3)");
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1)));
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(3)));
}
#[test]
fn test_or_operator_with_different_columns() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users WHERE id = 1 OR name = 'Charlie'",
&[],
)
.expect("OR query with different columns should succeed");
assert_eq!(result.rows.len(), 2, "Should match 2 rows");
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1)));
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(3)));
}
#[test]
fn test_or_with_and_precedence() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users WHERE (id = 1 AND age = 30) OR (id = 2)",
&[],
)
.expect("OR with AND should succeed");
assert_eq!(result.rows.len(), 2, "Should match 2 rows");
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1)));
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(2)));
}
#[test]
fn test_or_no_matches() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users WHERE id = 999 OR id = 998",
&[],
)
.expect("OR query with no matches should succeed");
assert_eq!(result.rows.len(), 0, "Should match 0 rows");
}
#[test]
fn test_or_all_matches() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT * FROM users WHERE id = 1 OR id = 2 OR id = 3",
&[],
)
.expect("Multiple OR should succeed");
assert_eq!(result.rows.len(), 3, "Should match all 3 rows");
}
#[test]
fn test_like_prefix_match() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"products",
TableId::new(10),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(10),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "Apple iPhone"}),
);
store.insert_json(
TableId::new(10),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "name": "Apple MacBook"}),
);
store.insert_json(
TableId::new(10),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "name": "Samsung Galaxy"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM products WHERE name LIKE 'Apple%'",
&[],
)
.expect("LIKE prefix query should succeed");
assert_eq!(result.rows.len(), 2, "Should match 2 Apple products");
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1)));
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(2)));
}
#[test]
fn test_like_suffix_match() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"files",
TableId::new(11),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("filename", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(11),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "filename": "document.pdf"}),
);
store.insert_json(
TableId::new(11),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "filename": "image.jpg"}),
);
store.insert_json(
TableId::new(11),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "filename": "report.pdf"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM files WHERE filename LIKE '%.pdf'",
&[],
)
.expect("LIKE suffix query should succeed");
assert_eq!(result.rows.len(), 2, "Should match 2 PDF files");
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1)));
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(3)));
}
#[test]
fn test_like_contains_match() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"articles",
TableId::new(12),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("title", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(12),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "title": "Introduction to Rust"}),
);
store.insert_json(
TableId::new(12),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "title": "Advanced Rust Patterns"}),
);
store.insert_json(
TableId::new(12),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "title": "Python for Beginners"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM articles WHERE title LIKE '%Rust%'",
&[],
)
.expect("LIKE contains query should succeed");
assert_eq!(result.rows.len(), 2, "Should match 2 Rust articles");
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1)));
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(2)));
}
#[test]
fn test_like_single_char_wildcard() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"codes",
TableId::new(13),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("code", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(13),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "code": "A1B"}),
);
store.insert_json(
TableId::new(13),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "code": "A2B"}),
);
store.insert_json(
TableId::new(13),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "code": "A12"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM codes WHERE code LIKE 'A_B'",
&[],
)
.expect("LIKE single char wildcard should succeed");
assert_eq!(
result.rows.len(),
2,
"Should match 2 codes with pattern A_B"
);
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1)));
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(2)));
}
#[test]
fn test_like_no_wildcard_exact_match() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"items",
TableId::new(14),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(14),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "exact"}),
);
store.insert_json(
TableId::new(14),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "name": "exactly"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM items WHERE name LIKE 'exact'",
&[],
)
.expect("LIKE without wildcards should work as exact match");
assert_eq!(result.rows.len(), 1, "Should match exactly one row");
assert_eq!(result.rows[0][0], Value::BigInt(1));
}
#[test]
fn test_like_escape_percent() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"discounts",
TableId::new(15),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("description", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(15),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "description": "10% off"}),
);
store.insert_json(
TableId::new(15),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "description": "20percent off"}),
);
store.insert_json(
TableId::new(15),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "description": "50% discount"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM discounts WHERE description LIKE '%\\%%'",
&[],
)
.expect("LIKE with escaped percent should succeed");
assert_eq!(result.rows.len(), 2, "Should match rows with % character");
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1)));
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(3)));
}
#[test]
fn test_like_escape_underscore() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"identifiers",
TableId::new(16),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(16),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "user_id"}),
);
store.insert_json(
TableId::new(16),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "name": "userid"}),
);
store.insert_json(
TableId::new(16),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "name": "user-id"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM identifiers WHERE name LIKE 'user\\_id'",
&[],
)
.expect("LIKE with escaped underscore should succeed");
assert_eq!(result.rows.len(), 1, "Should match only literal user_id");
assert_eq!(result.rows[0][0], Value::BigInt(1));
}
#[test]
fn test_complex_or_and_like_combination() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"employees",
TableId::new(17),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text).not_null(),
ColumnDef::new("department", DataType::Text),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(17),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "Alice Anderson", "department": "Engineering"}),
);
store.insert_json(
TableId::new(17),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "name": "Bob Brown", "department": "Sales"}),
);
store.insert_json(
TableId::new(17),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "name": "Charlie Chen", "department": "Engineering"}),
);
store.insert_json(
TableId::new(17),
encode_key(&[Value::BigInt(4)]),
&serde_json::json!({"id": 4, "name": "Alice Cooper", "department": "Marketing"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM employees WHERE name LIKE 'Alice%' OR department = 'Engineering'",
&[],
)
.expect("Complex OR and LIKE should succeed");
assert_eq!(result.rows.len(), 3, "Should match 3 employees");
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1))); assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(3))); assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(4))); }
#[test]
fn test_is_null_with_or() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"contacts",
TableId::new(18),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("email", DataType::Text),
ColumnDef::new("phone", DataType::Text),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(18),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "email": "alice@example.com", "phone": null}),
);
store.insert_json(
TableId::new(18),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "email": null, "phone": "555-1234"}),
);
store.insert_json(
TableId::new(18),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "email": "bob@example.com", "phone": "555-5678"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM contacts WHERE email IS NULL OR phone IS NULL",
&[],
)
.expect("IS NULL with OR should succeed");
assert_eq!(
result.rows.len(),
2,
"Should match 2 contacts with missing info"
);
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(1)));
assert!(result.rows.iter().any(|r| r[0] == Value::BigInt(2)));
}
#[test]
fn test_is_not_null_with_and() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"profiles",
TableId::new(19),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("bio", DataType::Text),
ColumnDef::new("avatar", DataType::Text),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(19),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "bio": "Software engineer", "avatar": "pic1.jpg"}),
);
store.insert_json(
TableId::new(19),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "bio": "Designer", "avatar": null}),
);
store.insert_json(
TableId::new(19),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "bio": null, "avatar": "pic3.jpg"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM profiles WHERE bio IS NOT NULL AND avatar IS NOT NULL",
&[],
)
.expect("IS NOT NULL with AND should succeed");
assert_eq!(result.rows.len(), 1, "Should match only complete profiles");
assert_eq!(result.rows[0][0], Value::BigInt(1));
}
#[test]
fn test_like_case_sensitive() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"words",
TableId::new(20),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("word", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(20),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "word": "Hello"}),
);
store.insert_json(
TableId::new(20),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "word": "hello"}),
);
store.insert_json(
TableId::new(20),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "word": "HELLO"}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM words WHERE word LIKE 'Hello'",
&[],
)
.expect("LIKE case sensitivity test should succeed");
assert_eq!(result.rows.len(), 1, "Should match only exact case 'Hello'");
assert_eq!(result.rows[0][0], Value::BigInt(1));
}
#[test]
fn test_select_alias_preservation() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"users_30",
TableId::new(30),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(30),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "Alice"}),
);
let result = engine
.query(&mut store, "SELECT name AS display_name FROM users_30", &[])
.expect("aliased SELECT must succeed");
assert_eq!(result.columns.len(), 1, "one alias → one output column");
assert_eq!(
result.columns[0].as_str(),
"display_name",
"output column must carry the alias, not the source name",
);
let result = engine
.query(
&mut store,
"SELECT id, name AS display_name FROM users_30",
&[],
)
.expect("mixed SELECT must succeed");
assert_eq!(result.columns[0].as_str(), "id");
assert_eq!(result.columns[1].as_str(), "display_name");
}
#[test]
fn test_ilike_and_not_like_family() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"words",
TableId::new(21),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("word", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
for (id, word) in [(1u64, "Hello"), (2, "hello"), (3, "HELLO"), (4, "hey")] {
store.insert_json(
TableId::new(21),
encode_key(&[Value::BigInt(id as i64)]),
&serde_json::json!({"id": id, "word": word}),
);
}
let result = engine
.query(
&mut store,
"SELECT id FROM words WHERE word ILIKE 'hello'",
&[],
)
.expect("ILIKE should parse and execute");
assert_eq!(
result.rows.len(),
3,
"ILIKE 'hello' must match all three case variants"
);
let result = engine
.query(
&mut store,
"SELECT id FROM words WHERE word NOT LIKE 'Hello'",
&[],
)
.expect("NOT LIKE should parse and execute");
let ids: Vec<_> = result.rows.iter().map(|r| r[0].clone()).collect();
assert_eq!(
ids.len(),
3,
"NOT LIKE 'Hello' should exclude the case-exact row"
);
assert!(
!ids.contains(&Value::BigInt(1)),
"row 1 ('Hello') must be excluded"
);
let result = engine
.query(
&mut store,
"SELECT id FROM words WHERE word NOT ILIKE 'hello'",
&[],
)
.expect("NOT ILIKE should parse and execute");
assert_eq!(
result.rows.len(),
1,
"NOT ILIKE 'hello' should leave only 'hey'"
);
assert_eq!(result.rows[0][0], Value::BigInt(4));
let result = engine
.query(
&mut store,
"SELECT id FROM words WHERE word ILIKE 'H%'",
&[],
)
.expect("ILIKE with wildcard should parse and execute");
assert_eq!(
result.rows.len(),
4,
"ILIKE 'H%' should match all rows starting with h/H"
);
}
#[test]
fn test_count_star_global() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT COUNT(*) FROM users", &[])
.expect("COUNT(*) should succeed");
assert_eq!(
result.rows.len(),
1,
"Should return 1 row for global aggregate"
);
assert_eq!(
result.rows[0][0],
Value::BigInt(3),
"Should count all 3 users"
);
}
#[test]
fn test_count_column() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"items",
TableId::new(21),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("value", DataType::BigInt),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(21),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "value": 100}),
);
store.insert_json(
TableId::new(21),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "value": null}),
);
store.insert_json(
TableId::new(21),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "value": 300}),
);
let result = engine
.query(&mut store, "SELECT COUNT(value) FROM items", &[])
.expect("COUNT(column) should succeed");
assert_eq!(result.rows.len(), 1);
assert_eq!(
result.rows[0][0],
Value::BigInt(2),
"COUNT(column) should count only non-NULL values (2 out of 3 rows)"
);
}
#[test]
fn test_sum_aggregate() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT SUM(age) FROM users", &[])
.expect("SUM should succeed");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(90));
}
#[test]
fn test_avg_aggregate() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT AVG(age) FROM users", &[])
.expect("AVG should succeed");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::Real(30.0));
}
#[test]
fn test_min_aggregate() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT MIN(age) FROM users", &[])
.expect("MIN should succeed");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(25)); }
#[test]
fn test_max_aggregate() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT MAX(age) FROM users", &[])
.expect("MAX should succeed");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(35)); }
#[test]
fn test_group_by_single_column() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"sales",
TableId::new(22),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("department", DataType::Text).not_null(),
ColumnDef::new("amount", DataType::BigInt).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(22),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "department": "Sales", "amount": 100}),
);
store.insert_json(
TableId::new(22),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "department": "Engineering", "amount": 200}),
);
store.insert_json(
TableId::new(22),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "department": "Sales", "amount": 150}),
);
let result = engine
.query(
&mut store,
"SELECT department, SUM(amount) FROM sales GROUP BY department",
&[],
)
.expect("GROUP BY should succeed");
assert_eq!(result.rows.len(), 2, "Should have 2 groups");
assert_eq!(result.columns.len(), 2, "Should return department + SUM");
let sales_row = result
.rows
.iter()
.find(|r| r[0] == Value::Text("Sales".to_string()))
.unwrap();
assert_eq!(
sales_row[1],
Value::BigInt(250),
"Sales total should be 250"
);
let eng_row = result
.rows
.iter()
.find(|r| r[0] == Value::Text("Engineering".to_string()))
.unwrap();
assert_eq!(
eng_row[1],
Value::BigInt(200),
"Engineering total should be 200"
);
}
#[test]
fn test_group_by_multiple_aggregates() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"transactions",
TableId::new(23),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("category", DataType::Text).not_null(),
ColumnDef::new("amount", DataType::BigInt).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(23),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "category": "Food", "amount": 50}),
);
store.insert_json(
TableId::new(23),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "category": "Food", "amount": 75}),
);
store.insert_json(
TableId::new(23),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "category": "Transport", "amount": 100}),
);
let result = engine
.query(
&mut store,
"SELECT category, COUNT(*), SUM(amount), AVG(amount) FROM transactions GROUP BY category",
&[],
)
.expect("Multiple aggregates should succeed");
assert_eq!(result.columns.len(), 4); assert_eq!(result.rows.len(), 2);
let food_row = result
.rows
.iter()
.find(|r| r[0] == Value::Text("Food".to_string()))
.unwrap();
assert_eq!(food_row[1], Value::BigInt(2), "Food count should be 2");
assert_eq!(food_row[2], Value::BigInt(125), "Food sum should be 125");
assert_eq!(food_row[3], Value::Real(62.5), "Food avg should be 62.5");
}
#[test]
fn test_distinct_simple() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"duplicates",
TableId::new(24),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("color", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(24),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "color": "red"}),
);
store.insert_json(
TableId::new(24),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "color": "blue"}),
);
store.insert_json(
TableId::new(24),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "color": "red"}),
);
store.insert_json(
TableId::new(24),
encode_key(&[Value::BigInt(4)]),
&serde_json::json!({"id": 4, "color": "blue"}),
);
let result = engine
.query(&mut store, "SELECT DISTINCT color FROM duplicates", &[])
.expect("DISTINCT should succeed");
assert_eq!(result.rows.len(), 2, "Should have 2 distinct colors");
let colors: Vec<String> = result
.rows
.iter()
.map(|r| match &r[0] {
Value::Text(s) => s.clone(),
_ => panic!("Expected text"),
})
.collect();
assert!(colors.contains(&"red".to_string()));
assert!(colors.contains(&"blue".to_string()));
}
#[test]
fn test_distinct_multiple_columns() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"pairs",
TableId::new(25),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("col1", DataType::Text).not_null(),
ColumnDef::new("col2", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(25),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "col1": "A", "col2": "X"}),
);
store.insert_json(
TableId::new(25),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "col1": "A", "col2": "Y"}),
);
store.insert_json(
TableId::new(25),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "col1": "A", "col2": "X"}),
);
let result = engine
.query(&mut store, "SELECT DISTINCT col1, col2 FROM pairs", &[])
.expect("DISTINCT on multiple columns should succeed");
assert_eq!(
result.rows.len(),
2,
"Should have 2 distinct (col1, col2) pairs"
);
}
#[test]
fn test_aggregate_on_empty_table() {
let schema = SchemaBuilder::new()
.table(
"empty",
TableId::new(26),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("value", DataType::BigInt),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT COUNT(*), SUM(value) FROM empty", &[])
.expect("Aggregate on empty table should succeed");
assert_eq!(result.rows.len(), 1, "Should return 1 row for empty table");
assert_eq!(result.rows[0][0], Value::BigInt(0), "COUNT(*) should be 0");
assert_eq!(result.rows[0][1], Value::Null, "SUM should be NULL");
}
#[test]
fn test_group_by_with_null_values() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"nulls",
TableId::new(27),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("category", DataType::Text),
ColumnDef::new("amount", DataType::BigInt).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(27),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "category": "A", "amount": 10}),
);
store.insert_json(
TableId::new(27),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "category": null, "amount": 20}),
);
store.insert_json(
TableId::new(27),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "category": null, "amount": 30}),
);
let result = engine
.query(
&mut store,
"SELECT category, SUM(amount) FROM nulls GROUP BY category",
&[],
)
.expect("GROUP BY with NULLs should succeed");
assert_eq!(result.rows.len(), 2, "Should have 2 groups (one for NULL)");
let null_row = result.rows.iter().find(|r| r[0] == Value::Null).unwrap();
assert_eq!(
null_row[1],
Value::BigInt(50),
"NULL group sum should be 50"
);
}
use kimberlite_types::Timestamp;
#[test]
fn test_value_accessors_wrong_type() {
let text_val = Value::Text("hello".to_string());
assert_eq!(text_val.as_bigint(), None, "Text should not be BigInt");
assert_eq!(text_val.as_boolean(), None, "Text should not be Boolean");
assert_eq!(
text_val.as_timestamp(),
None,
"Text should not be Timestamp"
);
assert_eq!(text_val.as_tinyint(), None, "Text should not be TinyInt");
assert_eq!(text_val.as_smallint(), None, "Text should not be SmallInt");
assert_eq!(text_val.as_integer(), None, "Text should not be Integer");
assert_eq!(text_val.as_real(), None, "Text should not be Real");
assert_eq!(text_val.as_decimal(), None, "Text should not be Decimal");
assert_eq!(text_val.as_date(), None, "Text should not be Date");
assert_eq!(text_val.as_time(), None, "Text should not be Time");
assert_eq!(text_val.as_uuid(), None, "Text should not be UUID");
assert_eq!(text_val.as_json(), None, "Text should not be JSON");
assert_eq!(text_val.as_bytes(), None, "Text should not be Bytes");
assert_eq!(text_val.as_text(), Some("hello"));
}
#[test]
fn test_value_accessors_correct_type() {
let bigint = Value::BigInt(42);
assert_eq!(bigint.as_bigint(), Some(42));
assert!(!bigint.is_null());
let bool_val = Value::Boolean(true);
assert_eq!(bool_val.as_boolean(), Some(true));
let tinyint = Value::TinyInt(127);
assert_eq!(tinyint.as_tinyint(), Some(127));
let smallint = Value::SmallInt(32767);
assert_eq!(smallint.as_smallint(), Some(32767));
let integer = Value::Integer(2147483647);
assert_eq!(integer.as_integer(), Some(2147483647));
let real = Value::Real(3.14);
assert_eq!(real.as_real(), Some(3.14));
let decimal = Value::Decimal(12345, 2);
assert_eq!(decimal.as_decimal(), Some((12345, 2)));
let date = Value::Date(19000);
assert_eq!(date.as_date(), Some(19000));
let time = Value::Time(43200_000_000);
assert_eq!(time.as_time(), Some(43200_000_000));
let null = Value::Null;
assert!(null.is_null());
assert_eq!(null.as_bigint(), None);
}
#[test]
fn test_value_data_type() {
assert_eq!(Value::BigInt(0).data_type(), Some(DataType::BigInt));
assert_eq!(Value::Text("x".into()).data_type(), Some(DataType::Text));
assert_eq!(Value::Boolean(true).data_type(), Some(DataType::Boolean));
assert_eq!(Value::TinyInt(0).data_type(), Some(DataType::TinyInt));
assert_eq!(Value::SmallInt(0).data_type(), Some(DataType::SmallInt));
assert_eq!(Value::Integer(0).data_type(), Some(DataType::Integer));
assert_eq!(Value::Real(0.0).data_type(), Some(DataType::Real));
assert_eq!(Value::Date(0).data_type(), Some(DataType::Date));
assert_eq!(Value::Time(0).data_type(), Some(DataType::Time));
assert_eq!(Value::Null.data_type(), None);
}
#[test]
fn test_value_is_compatible_with() {
let bigint = Value::BigInt(42);
assert!(bigint.is_compatible_with(DataType::BigInt));
assert!(!bigint.is_compatible_with(DataType::Text));
assert!(!bigint.is_compatible_with(DataType::Boolean));
let text = Value::Text("test".into());
assert!(text.is_compatible_with(DataType::Text));
assert!(!text.is_compatible_with(DataType::BigInt));
let null = Value::Null;
assert!(null.is_compatible_with(DataType::BigInt));
assert!(null.is_compatible_with(DataType::Text));
assert!(null.is_compatible_with(DataType::Boolean));
}
#[test]
fn test_value_from_json_error_paths() {
use crate::QueryError;
let json = serde_json::json!("not a number");
let result = Value::from_json(&json, DataType::BigInt);
assert!(result.is_err());
assert!(matches!(result, Err(QueryError::TypeMismatch { .. })));
let json = serde_json::json!(42);
let result = Value::from_json(&json, DataType::Boolean);
assert!(result.is_err());
let json = serde_json::json!(123);
let result = Value::from_json(&json, DataType::Text);
assert!(result.is_err());
}
#[test]
fn test_value_from_json_success_paths() {
let json = serde_json::json!(42);
let val = Value::from_json(&json, DataType::BigInt).unwrap();
assert_eq!(val, Value::BigInt(42));
let json = serde_json::json!("hello");
let val = Value::from_json(&json, DataType::Text).unwrap();
assert_eq!(val, Value::Text("hello".into()));
let json = serde_json::json!(true);
let val = Value::from_json(&json, DataType::Boolean).unwrap();
assert_eq!(val, Value::Boolean(true));
let json = serde_json::json!(null);
let val = Value::from_json(&json, DataType::BigInt).unwrap();
assert_eq!(val, Value::Null);
}
#[test]
fn test_value_to_json() {
assert_eq!(Value::BigInt(42).to_json(), serde_json::json!(42));
assert_eq!(Value::Text("hi".into()).to_json(), serde_json::json!("hi"));
assert_eq!(Value::Boolean(true).to_json(), serde_json::json!(true));
assert_eq!(Value::Null.to_json(), serde_json::json!(null));
assert_eq!(Value::TinyInt(10).to_json(), serde_json::json!(10));
assert_eq!(Value::SmallInt(100).to_json(), serde_json::json!(100));
assert_eq!(Value::Integer(1000).to_json(), serde_json::json!(1000));
assert_eq!(Value::Real(3.14).to_json(), serde_json::json!(3.14));
}
#[test]
fn test_value_compare_cross_type() {
let bigint = Value::BigInt(42);
let text = Value::Text("42".into());
assert_eq!(bigint.compare(&text), None);
assert_eq!(text.compare(&bigint), None);
}
#[test]
fn test_value_compare_with_null() {
use std::cmp::Ordering;
let bigint = Value::BigInt(42);
let null = Value::Null;
assert_eq!(bigint.compare(&null), Some(Ordering::Greater));
assert_eq!(null.compare(&bigint), Some(Ordering::Less));
assert_eq!(null.compare(&null), Some(Ordering::Equal));
}
#[test]
fn test_value_compare_same_type() {
use std::cmp::Ordering;
assert_eq!(
Value::BigInt(10).compare(&Value::BigInt(20)),
Some(Ordering::Less)
);
assert_eq!(
Value::BigInt(20).compare(&Value::BigInt(10)),
Some(Ordering::Greater)
);
assert_eq!(
Value::BigInt(15).compare(&Value::BigInt(15)),
Some(Ordering::Equal)
);
assert_eq!(
Value::Text("apple".into()).compare(&Value::Text("banana".into())),
Some(Ordering::Less)
);
assert_eq!(
Value::Boolean(false).compare(&Value::Boolean(true)),
Some(Ordering::Less)
);
}
#[test]
fn test_value_integer_types_comparison() {
use std::cmp::Ordering;
assert_eq!(
Value::TinyInt(5).compare(&Value::TinyInt(10)),
Some(Ordering::Less)
);
assert_eq!(
Value::TinyInt(-5).compare(&Value::TinyInt(5)),
Some(Ordering::Less)
);
assert_eq!(
Value::SmallInt(100).compare(&Value::SmallInt(200)),
Some(Ordering::Less)
);
assert_eq!(
Value::Integer(1000).compare(&Value::Integer(2000)),
Some(Ordering::Less)
);
}
#[test]
fn test_value_real_comparison_special_values() {
use std::cmp::Ordering;
let inf = Value::Real(f64::INFINITY);
let neg_inf = Value::Real(f64::NEG_INFINITY);
let normal = Value::Real(42.0);
let nan = Value::Real(f64::NAN);
assert_eq!(normal.compare(&inf), Some(Ordering::Less));
assert_eq!(inf.compare(&normal), Some(Ordering::Greater));
assert_eq!(neg_inf.compare(&normal), Some(Ordering::Less));
assert!(nan.compare(&normal).is_some());
assert!(normal.compare(&nan).is_some());
assert_eq!(nan.compare(&nan), Some(Ordering::Equal));
}
#[test]
fn test_value_decimal_comparison() {
use std::cmp::Ordering;
let d1 = Value::Decimal(12345, 2); let d2 = Value::Decimal(12346, 2); assert_eq!(d1.compare(&d2), Some(Ordering::Less));
let d3 = Value::Decimal(1234, 1); let d4 = Value::Decimal(12350, 2); assert_eq!(d3.compare(&d4), None);
}
#[test]
fn test_value_timestamp_comparison() {
use std::cmp::Ordering;
let ts1 = Value::Timestamp(Timestamp::from_nanos(1000_000_000_000)); let ts2 = Value::Timestamp(Timestamp::from_nanos(2000_000_000_000));
assert_eq!(ts1.compare(&ts2), Some(Ordering::Less));
assert_eq!(ts2.compare(&ts1), Some(Ordering::Greater));
}
#[test]
fn test_error_wrong_parameter_type() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(
&mut store,
"SELECT * FROM users WHERE id = $1",
&[Value::Text("not a number".into())],
);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_error_too_many_parameters() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(
&mut store,
"SELECT * FROM users WHERE id = $1",
&[Value::BigInt(1), Value::BigInt(2)], );
assert!(result.is_ok());
}
#[test]
fn test_error_unsupported_sql_features() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result2 = engine.query(
&mut store,
"SELECT * FROM (SELECT * FROM users) AS subq",
&[],
);
assert!(result2.is_err());
}
#[test]
fn test_error_invalid_column_in_where() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(
&mut store,
"SELECT * FROM users WHERE nonexistent_column = 42",
&[],
);
assert!(result.is_err());
if let Err(e) = result {
let msg = e.to_string();
assert!(msg.contains("nonexistent_column") || msg.contains("not found"));
}
}
#[test]
fn test_error_invalid_column_in_select() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT id, fake_column FROM users", &[]);
assert!(result.is_err());
if let Err(e) = result {
assert!(e.to_string().contains("fake_column") || e.to_string().contains("not found"));
}
}
#[test]
fn test_error_invalid_column_in_order_by() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT * FROM users ORDER BY fake_column", &[]);
assert!(result.is_err());
}
#[test]
fn test_aggregate_with_invalid_column() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT COUNT(fake_column) FROM users", &[]);
let _ = result;
}
#[test]
fn test_edge_case_limit_zero() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT * FROM users LIMIT 0", &[]);
let _ = result;
}
#[test]
fn test_edge_case_very_large_limit() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine
.query(&mut store, "SELECT * FROM users LIMIT 999999", &[])
.expect("Large LIMIT should succeed");
assert!(result.rows.len() <= 999999);
}
#[test]
fn test_edge_case_empty_where_clause() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT * FROM users WHERE 1 = 0", &[]);
let _ = result;
}
#[test]
fn test_edge_case_select_star_with_aggregates() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT *, COUNT(*) FROM users", &[]);
let _ = result;
}
#[test]
fn test_aggregate_min_max_on_text() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"textdata",
TableId::new(30),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(30),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "zebra"}),
);
store.insert_json(
TableId::new(30),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "name": "apple"}),
);
store.insert_json(
TableId::new(30),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "name": "banana"}),
);
let result = engine
.query(&mut store, "SELECT MIN(name), MAX(name) FROM textdata", &[])
.expect("MIN/MAX on text should work");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::Text("apple".into()));
assert_eq!(result.rows[0][1], Value::Text("zebra".into()));
}
#[test]
fn test_aggregate_count_with_nulls() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"nullcount",
TableId::new(31),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("value", DataType::BigInt), ],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(31),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "value": 10}),
);
store.insert_json(
TableId::new(31),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "value": null}),
);
store.insert_json(
TableId::new(31),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "value": 30}),
);
let result = engine
.query(
&mut store,
"SELECT COUNT(*), COUNT(value) FROM nullcount",
&[],
)
.expect("COUNT with NULLs should work");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(3)); assert_eq!(result.rows[0][1], Value::BigInt(2)); }
#[test]
fn test_table_name_display_and_debug() {
let name = TableName::new("users");
assert_eq!(name.to_string(), "users");
assert_eq!(format!("{name:?}"), "TableName(\"users\")");
assert_eq!(name.as_str(), "users");
}
#[test]
fn test_table_name_from_str_and_string() {
let name1 = TableName::from("posts");
let name2 = TableName::from("posts".to_string());
assert_eq!(name1.as_str(), "posts");
assert_eq!(name2.as_str(), "posts");
}
#[test]
fn test_column_name_display_and_debug() {
let col = ColumnName::new("email");
assert_eq!(col.to_string(), "email");
assert_eq!(format!("{col:?}"), "ColumnName(\"email\")");
assert_eq!(col.as_str(), "email");
}
#[test]
fn test_column_name_from_str_and_string() {
let col1 = ColumnName::from("id");
let col2 = ColumnName::from("id".to_string());
assert_eq!(col1.as_str(), "id");
assert_eq!(col2.as_str(), "id");
}
#[test]
fn test_column_def_builder() {
let col1 = ColumnDef::new("id", DataType::BigInt);
assert_eq!(col1.name.as_str(), "id");
assert_eq!(col1.data_type, DataType::BigInt);
assert!(col1.nullable);
let col2 = ColumnDef::new("name", DataType::Text).not_null();
assert_eq!(col2.name.as_str(), "name");
assert!(!col2.nullable); }
#[test]
fn test_index_def_creation() {
let index = IndexDef::new(42, "idx_email", vec![ColumnName::from("email")]);
assert_eq!(index.index_id, 42);
assert_eq!(index.name, "idx_email");
assert_eq!(index.columns.len(), 1);
assert_eq!(index.columns[0].as_str(), "email");
}
#[test]
fn test_table_def_find_column() {
let table = TableDef::new(
TableId::new(1),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text),
ColumnDef::new("age", DataType::Integer),
],
vec![ColumnName::from("id")],
);
let (idx, col) = table
.find_column(&ColumnName::from("name"))
.expect("name column should exist");
assert_eq!(idx, 1);
assert_eq!(col.name.as_str(), "name");
assert_eq!(col.data_type, DataType::Text);
assert!(
table
.find_column(&ColumnName::from("nonexistent"))
.is_none()
);
}
#[test]
fn test_table_def_primary_key_methods() {
let table = TableDef::new(
TableId::new(1),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("org_id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text),
],
vec![ColumnName::from("org_id"), ColumnName::from("id")],
);
assert!(table.is_primary_key(&ColumnName::from("id")));
assert!(table.is_primary_key(&ColumnName::from("org_id")));
assert!(!table.is_primary_key(&ColumnName::from("name")));
assert_eq!(
table.primary_key_position(&ColumnName::from("org_id")),
Some(0)
);
assert_eq!(table.primary_key_position(&ColumnName::from("id")), Some(1));
assert_eq!(table.primary_key_position(&ColumnName::from("name")), None);
let pk_indices = table.primary_key_indices();
assert_eq!(pk_indices.len(), 2);
assert_eq!(pk_indices[0], 1); assert_eq!(pk_indices[1], 0); }
#[test]
fn test_table_def_with_index() {
let mut table = TableDef::new(
TableId::new(1),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("email", DataType::Text),
],
vec![ColumnName::from("id")],
);
table = table.with_index(IndexDef::new(
10,
"idx_email",
vec![ColumnName::from("email")],
));
assert_eq!(table.indexes().len(), 1);
assert_eq!(table.indexes()[0].name, "idx_email");
}
#[test]
fn test_table_def_find_index_for_column() {
let table = TableDef::new(
TableId::new(1),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("email", DataType::Text),
ColumnDef::new("status", DataType::Text),
],
vec![ColumnName::from("id")],
)
.with_index(IndexDef::new(
10,
"idx_email",
vec![ColumnName::from("email")],
))
.with_index(IndexDef::new(
11,
"idx_status",
vec![ColumnName::from("status")],
));
let email_index = table.find_index_for_column(&ColumnName::from("email"));
assert!(email_index.is_some());
assert_eq!(email_index.unwrap().name, "idx_email");
let status_index = table.find_index_for_column(&ColumnName::from("status"));
assert!(status_index.is_some());
assert_eq!(status_index.unwrap().name, "idx_status");
let id_index = table.find_index_for_column(&ColumnName::from("id"));
assert!(id_index.is_none());
}
#[test]
fn test_schema_operations() {
let mut schema = Schema::new();
assert!(schema.is_empty());
assert_eq!(schema.len(), 0);
let table = TableDef::new(
TableId::new(1),
vec![ColumnDef::new("id", DataType::BigInt).not_null()],
vec![ColumnName::from("id")],
);
schema.add_table("users", table);
assert!(!schema.is_empty());
assert_eq!(schema.len(), 1);
let retrieved = schema.get_table(&TableName::from("users"));
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().table_id, TableId::new(1));
assert!(schema.get_table(&TableName::from("nonexistent")).is_none());
}
#[test]
fn test_schema_table_names_iterator() {
let mut schema = Schema::new();
schema.add_table(
"users",
TableDef::new(
TableId::new(1),
vec![ColumnDef::new("id", DataType::BigInt).not_null()],
vec![ColumnName::from("id")],
),
);
schema.add_table(
"posts",
TableDef::new(
TableId::new(2),
vec![ColumnDef::new("id", DataType::BigInt).not_null()],
vec![ColumnName::from("id")],
),
);
let names: Vec<String> = schema
.table_names()
.map(|n| n.as_str().to_string())
.collect();
assert_eq!(names.len(), 2);
assert!(names.contains(&"users".to_string()));
assert!(names.contains(&"posts".to_string()));
}
#[test]
fn test_schema_builder() {
let schema = SchemaBuilder::new()
.table(
"products",
TableId::new(10),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text).not_null(),
ColumnDef::new("price", DataType::Integer),
],
vec![ColumnName::from("id")],
)
.table(
"orders",
TableId::new(11),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("product_id", DataType::BigInt),
],
vec![ColumnName::from("id")],
)
.build();
assert_eq!(schema.len(), 2);
assert!(schema.get_table(&TableName::from("products")).is_some());
assert!(schema.get_table(&TableName::from("orders")).is_some());
}
#[test]
fn test_data_type_equality() {
assert_eq!(DataType::BigInt, DataType::BigInt);
assert_ne!(DataType::BigInt, DataType::Text);
assert_ne!(DataType::TinyInt, DataType::SmallInt);
assert_ne!(DataType::SmallInt, DataType::Integer);
}
#[test]
fn test_table_name_ordering() {
let mut names = [
TableName::from("zebra"),
TableName::from("apple"),
TableName::from("banana"),
];
names.sort();
assert_eq!(names[0].as_str(), "apple");
assert_eq!(names[1].as_str(), "banana");
assert_eq!(names[2].as_str(), "zebra");
}
#[test]
fn test_column_name_equality_and_hash() {
use std::collections::HashSet;
let col1 = ColumnName::from("id");
let col2 = ColumnName::from("id");
let col3 = ColumnName::from("name");
assert_eq!(col1, col2);
assert_ne!(col1, col3);
let mut set = HashSet::new();
set.insert(col1.clone());
set.insert(col2); set.insert(col3);
assert_eq!(set.len(), 2);
}
#[test]
fn test_parse_multiple_predicates_with_or() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(
&mut store,
"SELECT * FROM users WHERE id = 1 OR id = 2 OR id = 3",
&[],
);
let _ = result;
}
#[test]
fn test_parse_complex_and_or_combinations() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(
&mut store,
"SELECT * FROM users WHERE (id = 1 OR id = 2) AND active = true",
&[],
);
let _ = result;
}
#[test]
fn test_parse_multiple_order_by_columns() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(
&mut store,
"SELECT * FROM users ORDER BY name ASC, id DESC, active ASC",
&[],
);
let _ = result;
}
#[test]
fn test_parse_select_with_table_alias() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT u.id, u.name FROM users u", &[]);
let _ = result;
}
#[test]
fn test_parse_where_with_parentheses() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT * FROM users WHERE (id = 1)", &[]);
let _ = result;
}
#[test]
fn test_parse_in_with_parameters() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(
&mut store,
"SELECT * FROM users WHERE id IN ($1, $2, $3)",
&[Value::BigInt(1), Value::BigInt(2), Value::BigInt(3)],
);
assert!(result.is_ok());
}
#[test]
fn test_parse_null_literal_in_where() {
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let result = engine.query(&mut store, "SELECT * FROM users WHERE name = NULL", &[]);
let _ = result;
}
#[test]
fn test_aggregate_with_where_clause() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"orders",
TableId::new(40),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("amount", DataType::BigInt).not_null(),
ColumnDef::new("status", DataType::Text).not_null(),
],
vec![ColumnName::from("id")],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(40),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "amount": 100, "status": "completed"}),
);
store.insert_json(
TableId::new(40),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "amount": 200, "status": "pending"}),
);
store.insert_json(
TableId::new(40),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "amount": 150, "status": "completed"}),
);
let result = engine
.query(
&mut store,
"SELECT SUM(amount), COUNT(*) FROM orders WHERE status = 'completed'",
&[],
)
.expect("Aggregate with WHERE should work");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(250)); assert_eq!(result.rows[0][1], Value::BigInt(2)); }
#[test]
fn test_order_by_with_limit() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
for i in 1..=5 {
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(i)]),
&serde_json::json!({"id": i, "name": format!("user{}", i), "active": true}),
);
}
let result = engine
.query(
&mut store,
"SELECT * FROM users ORDER BY id DESC LIMIT 3",
&[],
)
.expect("ORDER BY with LIMIT should work");
assert_eq!(result.rows.len(), 3);
}
#[test]
fn test_range_scan_with_filter() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
for i in 1..=10 {
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(i)]),
&serde_json::json!({
"id": i,
"name": format!("user{}", i),
"age": if i % 2 == 0 { serde_json::json!(30) } else { serde_json::json!(null) }
}),
);
}
let result = engine
.query(
&mut store,
"SELECT * FROM users WHERE id > 3 AND age IS NOT NULL",
&[],
)
.expect("Range scan with filter should work");
assert!(!result.rows.is_empty());
}
#[test]
fn test_empty_result_with_aggregates() {
let schema = SchemaBuilder::new()
.table(
"empty_table",
TableId::new(50),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("value", DataType::BigInt),
],
vec![ColumnName::from("id")],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
let result = engine
.query(
&mut store,
"SELECT COUNT(*), SUM(value), AVG(value), MIN(value), MAX(value) FROM empty_table",
&[],
)
.expect("Aggregates on empty table should work");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(0)); }
#[test]
fn test_multiple_aggregates_without_group_by() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"stats",
TableId::new(51),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("score", DataType::BigInt).not_null(),
],
vec![ColumnName::from("id")],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(51),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "score": 100}),
);
store.insert_json(
TableId::new(51),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "score": 200}),
);
store.insert_json(
TableId::new(51),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "score": 150}),
);
let result = engine
.query(
&mut store,
"SELECT MIN(score), MAX(score), SUM(score), AVG(score) FROM stats",
&[],
)
.expect("Multiple aggregates should work");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(100)); assert_eq!(result.rows[0][1], Value::BigInt(200)); assert_eq!(result.rows[0][2], Value::BigInt(450)); assert_eq!(result.rows[0][3], Value::Real(150.0)); }
#[test]
fn test_limit_larger_than_result_set() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
for i in 1..=3 {
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(i)]),
&serde_json::json!({"id": i, "name": format!("user{}", i), "age": 25}),
);
}
let result = engine
.query(&mut store, "SELECT * FROM users LIMIT 100", &[])
.expect("LIMIT larger than result set should work");
assert!(result.rows.len() <= 100);
}
#[test]
fn test_where_clause_with_all_comparison_operators() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
for i in 1..=10 {
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(i)]),
&serde_json::json!({"id": i, "name": format!("user{}", i), "age": i * 10}),
);
}
let queries = vec![
"SELECT * FROM users WHERE age = 50", "SELECT * FROM users WHERE age < 30", "SELECT * FROM users WHERE age <= 30", "SELECT * FROM users WHERE age > 70", "SELECT * FROM users WHERE age >= 70", ];
for query in queries {
let _result = engine
.query(&mut store, query, &[])
.unwrap_or_else(|_| panic!("Query should work: {query}"));
}
}
#[test]
fn test_select_specific_columns_in_different_order() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "Alice", "age": 30}),
);
let result = engine
.query(&mut store, "SELECT age, name, id FROM users", &[])
.expect("Column reordering should work");
assert_eq!(result.columns.len(), 3);
assert_eq!(result.columns[0].as_str(), "age");
assert_eq!(result.columns[1].as_str(), "name");
assert_eq!(result.columns[2].as_str(), "id");
}
#[test]
fn test_like_with_multiple_wildcards() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let names = ["Alice", "Andrew", "Bob", "Anna", "Alexander"];
for (i, name) in names.iter().enumerate() {
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(i as i64 + 1)]),
&serde_json::json!({"id": i + 1, "name": name, "age": 25}),
);
}
let result = engine
.query(&mut store, "SELECT * FROM users WHERE name LIKE '%A%'", &[])
.expect("LIKE with wildcards should work");
assert!(result.rows.len() >= 3);
}
#[test]
fn test_parameterized_query_with_null() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "name": "Alice", "age": null}),
);
let result = engine.query(
&mut store,
"SELECT * FROM users WHERE age = $1",
&[Value::Null],
);
assert!(result.is_ok());
}
#[test]
fn test_group_by_with_multiple_groups() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"sales",
TableId::new(60),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("region", DataType::Text).not_null(),
ColumnDef::new("amount", DataType::BigInt).not_null(),
],
vec![ColumnName::from("id")],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
let data = vec![
(1, "North", 100),
(2, "South", 200),
(3, "North", 150),
(4, "East", 300),
(5, "South", 250),
(6, "North", 175),
];
for (id, region, amount) in data {
store.insert_json(
TableId::new(60),
encode_key(&[Value::BigInt(id)]),
&serde_json::json!({"id": id, "region": region, "amount": amount}),
);
}
let result = engine
.query(
&mut store,
"SELECT region, SUM(amount), COUNT(*) FROM sales GROUP BY region",
&[],
)
.expect("GROUP BY with multiple groups should work");
assert_eq!(result.rows.len(), 3); }
#[test]
fn test_table_scan_vs_point_lookup() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
for i in 1..=5 {
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(i)]),
&serde_json::json!({"id": i, "name": format!("user{}", i), "age": 25}),
);
}
let result1 = engine
.query(&mut store, "SELECT * FROM users WHERE id = 3", &[])
.expect("Point lookup should work");
assert_eq!(result1.rows.len(), 1);
let result2 = engine
.query(&mut store, "SELECT * FROM users WHERE name = 'user3'", &[])
.expect("Table scan should work");
assert_eq!(result2.rows.len(), 1);
}
#[test]
fn test_order_by_text_column() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
let names = ["Zebra", "Apple", "Mango", "Banana"];
for (i, name) in names.iter().enumerate() {
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(i as i64 + 1)]),
&serde_json::json!({"id": i + 1, "name": name, "age": 25}),
);
}
let result = engine
.query(&mut store, "SELECT name FROM users ORDER BY name ASC", &[])
.expect("ORDER BY text should work");
assert!(!result.rows.is_empty());
}
#[test]
fn test_count_star_vs_count_column() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"items",
TableId::new(70),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("value", DataType::BigInt), ],
vec![ColumnName::from("id")],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(70),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "value": 100}),
);
store.insert_json(
TableId::new(70),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "value": null}),
);
store.insert_json(
TableId::new(70),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "value": 300}),
);
let result = engine
.query(&mut store, "SELECT COUNT(*) FROM items", &[])
.expect("COUNT(*) should work");
assert_eq!(result.rows[0][0], Value::BigInt(3)); }
#[test]
fn test_distinct_on_null_values() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"tags",
TableId::new(71),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("tag", DataType::Text), ],
vec![ColumnName::from("id")],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(71),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "tag": "red"}),
);
store.insert_json(
TableId::new(71),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "tag": null}),
);
store.insert_json(
TableId::new(71),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "tag": "red"}),
);
store.insert_json(
TableId::new(71),
encode_key(&[Value::BigInt(4)]),
&serde_json::json!({"id": 4, "tag": null}),
);
let result = engine
.query(&mut store, "SELECT DISTINCT tag FROM tags", &[])
.expect("DISTINCT with NULLs should work");
assert_eq!(result.rows.len(), 2);
}
#[test]
fn test_is_null_and_is_not_null_on_different_types() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"mixed",
TableId::new(80),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("text_col", DataType::Text),
ColumnDef::new("int_col", DataType::BigInt),
ColumnDef::new("bool_col", DataType::Boolean),
],
vec![ColumnName::from("id")],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(80),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "text_col": null, "int_col": 10, "bool_col": true}),
);
store.insert_json(
TableId::new(80),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "text_col": "hello", "int_col": null, "bool_col": false}),
);
let result1 = engine.query(
&mut store,
"SELECT * FROM mixed WHERE text_col IS NULL",
&[],
);
assert!(result1.is_ok());
let result2 = engine.query(&mut store, "SELECT * FROM mixed WHERE int_col IS NULL", &[]);
assert!(result2.is_ok());
let result3 = engine.query(
&mut store,
"SELECT * FROM mixed WHERE text_col IS NOT NULL",
&[],
);
assert!(result3.is_ok());
let result4 = engine.query(
&mut store,
"SELECT * FROM mixed WHERE bool_col IS NOT NULL",
&[],
);
assert!(result4.is_ok());
}
#[test]
fn test_aggregate_distinct_combinations() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"combos",
TableId::new(81),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("category", DataType::Text).not_null(),
ColumnDef::new("subcategory", DataType::Text).not_null(),
],
vec![ColumnName::from("id")],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
let data = vec![
(1, "A", "X"),
(2, "A", "Y"),
(3, "B", "X"),
(4, "A", "X"), (5, "B", "Y"),
];
for (id, cat, sub) in data {
store.insert_json(
TableId::new(81),
encode_key(&[Value::BigInt(id)]),
&serde_json::json!({"id": id, "category": cat, "subcategory": sub}),
);
}
let result = engine.query(
&mut store,
"SELECT DISTINCT category, subcategory FROM combos",
&[],
);
assert!(result.is_ok());
}
#[test]
fn test_complex_filter_combinations() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
for i in 1..=20 {
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(i)]),
&serde_json::json!({"id": i, "name": format!("user{}", i), "age": i * 5}),
);
}
let queries = vec![
"SELECT * FROM users WHERE (id > 5 AND id < 15) OR (age > 50 AND age < 80)",
"SELECT * FROM users WHERE id IN (1, 5, 10, 15) AND age > 20",
"SELECT * FROM users WHERE name LIKE 'user1%' OR name LIKE 'user2%'",
];
for query in queries {
let result = engine.query(&mut store, query, &[]);
assert!(result.is_ok(), "Query should work: {query}");
}
}
#[test]
fn test_range_boundaries() {
use crate::key_encoder::encode_key;
let schema = test_schema();
let mut store = test_store();
let engine = QueryEngine::new(schema);
for i in 1..=10 {
store.insert_json(
TableId::new(1),
encode_key(&[Value::BigInt(i)]),
&serde_json::json!({"id": i, "name": format!("user{}", i), "age": i * 10}),
);
}
let tests = vec![
(
"SELECT * FROM users WHERE id >= 1 AND id <= 10",
"Full range",
),
("SELECT * FROM users WHERE id > 5", "Open lower bound"),
("SELECT * FROM users WHERE id < 5", "Open upper bound"),
(
"SELECT * FROM users WHERE id >= 5 AND id <= 5",
"Single value range",
),
];
for (query, desc) in tests {
let result = engine.query(&mut store, query, &[]);
assert!(result.is_ok(), "{desc} should work");
}
}
#[test]
fn regression_decimal_negative_parsing() {
use crate::parser::parse_statement;
let stmt = parse_statement("SELECT * FROM t WHERE price = -123.45").expect("Should parse");
if let crate::parser::ParsedStatement::Select(select) = stmt {
if let Some(pred) = select.predicates.first() {
match pred {
crate::parser::Predicate::Eq(_, value) => {
if let crate::parser::PredicateValue::Literal(Value::Decimal(val, scale)) =
value
{
assert_eq!(*val, -12345, "Decimal value should be -12345");
assert_eq!(*scale, 2, "Scale should be 2");
} else {
panic!("Expected Decimal literal");
}
}
_ => panic!("Expected Eq predicate"),
}
} else {
panic!("Expected a predicate");
}
} else {
panic!("Expected SELECT statement");
}
}
#[test]
fn regression_parameter_index_validation() {
use crate::parser::parse_statement;
let result = parse_statement("SELECT * FROM t WHERE id = $0");
assert!(result.is_err(), "Should reject $0 parameter");
let err = result.unwrap_err();
assert!(
err.to_string().contains("start at $1"),
"Error should mention 1-indexed parameters"
);
}
#[test]
fn regression_decimal_precision_preserved() {
use crate::parser::parse_statement;
let stmt =
parse_statement("CREATE TABLE products (id BIGINT PRIMARY KEY, price DECIMAL(10,4))")
.expect("Should parse");
if let crate::parser::ParsedStatement::CreateTable(create_table) = stmt {
let price_col = create_table
.columns
.iter()
.find(|c| c.name == "price")
.expect("price column exists");
assert!(
price_col.data_type.contains("DECIMAL"),
"Should be DECIMAL type"
);
assert!(
price_col.data_type.contains("10"),
"Should preserve precision 10"
);
assert!(price_col.data_type.contains('4'), "Should preserve scale 4");
} else {
panic!("Expected CREATE TABLE statement");
}
}
#[test]
fn regression_order_by_limit_non_pk_column() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"events",
TableId::new(100),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("priority", DataType::BigInt).not_null(), ],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(100),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "priority": 100}),
);
store.insert_json(
TableId::new(100),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "priority": 300}), );
store.insert_json(
TableId::new(100),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "priority": 200}),
);
let result = engine
.query(
&mut store,
"SELECT id FROM events ORDER BY priority DESC LIMIT 2",
&[],
)
.expect("Query should succeed");
assert_eq!(result.rows.len(), 2);
assert_eq!(
result.rows[0][0],
Value::BigInt(2),
"First row should be highest priority (id=2)"
);
assert_eq!(
result.rows[1][0],
Value::BigInt(3),
"Second row should be second highest priority (id=3)"
);
}
#[test]
fn regression_count_column_nulls() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"items",
TableId::new(101),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("value", DataType::BigInt), ],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(101),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "value": 100}),
);
store.insert_json(
TableId::new(101),
encode_key(&[Value::BigInt(2)]),
&serde_json::json!({"id": 2, "value": null}), );
store.insert_json(
TableId::new(101),
encode_key(&[Value::BigInt(3)]),
&serde_json::json!({"id": 3, "value": 300}),
);
let result = engine
.query(&mut store, "SELECT COUNT(*), COUNT(value) FROM items", &[])
.expect("Query should succeed");
assert_eq!(result.rows.len(), 1);
assert_eq!(
result.rows[0][0],
Value::BigInt(3),
"COUNT(*) should count all rows"
);
assert_eq!(
result.rows[0][1],
Value::BigInt(2),
"COUNT(value) should count only non-NULL values"
);
}
#[test]
fn regression_in_operator_type_coercion() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"users",
TableId::new(102),
vec![
ColumnDef::new("id", DataType::Integer).not_null(), ColumnDef::new("name", DataType::Text).not_null(),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
let engine = QueryEngine::new(schema);
store.insert_json(
TableId::new(102),
encode_key(&[Value::Integer(1)]),
&serde_json::json!({"id": 1, "name": "Alice"}),
);
store.insert_json(
TableId::new(102),
encode_key(&[Value::Integer(2)]),
&serde_json::json!({"id": 2, "name": "Bob"}),
);
store.insert_json(
TableId::new(102),
encode_key(&[Value::Integer(3)]),
&serde_json::json!({"id": 3, "name": "Charlie"}),
);
let result = engine
.query(&mut store, "SELECT name FROM users WHERE id IN (1, 3)", &[])
.expect("Query should succeed with type coercion");
assert_eq!(result.rows.len(), 2);
assert_eq!(result.rows[0][0], Value::Text("Alice".to_string()));
assert_eq!(result.rows[1][0], Value::Text("Charlie".to_string()));
}
#[test]
#[allow(clippy::items_after_statements)] fn group_by_cardinality_cap_enforced() {
use crate::key_encoder::encode_key;
kimberlite_properties::registry::reset();
struct FixedRowScanStore {
row_count: i64,
position: Offset,
}
impl ProjectionStore for FixedRowScanStore {
fn scan(
&mut self,
_table: TableId,
_range: Range<Key>,
_limit: usize,
) -> Result<Vec<(Key, Bytes)>, StoreError> {
Ok((0i64..self.row_count)
.map(|i| {
let key = encode_key(&[Value::BigInt(i)]);
let json = serde_json::json!({"id": i, "val": 0i64});
let bytes = Bytes::from(serde_json::to_vec(&json).expect("json serialization"));
(key, bytes)
})
.collect())
}
fn scan_at(
&mut self,
t: TableId,
r: Range<Key>,
limit: usize,
_pos: Offset,
) -> Result<Vec<(Key, Bytes)>, StoreError> {
self.scan(t, r, limit)
}
fn apply(&mut self, _: WriteBatch) -> Result<(), StoreError> {
Ok(())
}
fn applied_position(&self) -> Offset {
self.position
}
fn get(&mut self, _: TableId, _: &Key) -> Result<Option<Bytes>, StoreError> {
Ok(None)
}
fn get_at(
&mut self,
t: TableId,
k: &Key,
_pos: Offset,
) -> Result<Option<Bytes>, StoreError> {
self.get(t, k)
}
fn sync(&mut self) -> Result<(), StoreError> {
Ok(())
}
fn purge_table(&mut self, _: TableId) -> Result<(), StoreError> {
Ok(())
}
}
let schema = SchemaBuilder::new()
.table(
"t",
TableId::new(1),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("val", DataType::BigInt),
],
vec!["id".into()],
)
.build();
let engine = QueryEngine::new(schema);
let mut store = FixedRowScanStore {
row_count: 100_001,
position: Offset::ZERO,
};
let result = engine.query(&mut store, "SELECT id, COUNT(*) FROM t GROUP BY id", &[]);
assert!(
result.is_err(),
"expected UnsupportedFeature error when GROUP BY exceeds 100k groups, got: {result:?}"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("100000") || err_msg.contains("cardinality"),
"error should mention cardinality cap, got: {err_msg}"
);
let snap = kimberlite_properties::registry::snapshot();
assert!(
snap.contains_key("query.group_by_cardinality_cap_hit"),
"expected query.group_by_cardinality_cap_hit annotation to fire; got: {:?}",
snap.keys().collect::<Vec<_>>()
);
}
#[cfg(test)]
fn v051_setup() -> (QueryEngine, MockStore) {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"users",
TableId::new(31),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new("name", DataType::Text).not_null(),
ColumnDef::new("age", DataType::BigInt),
ColumnDef::new("score", DataType::Text),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
for (id, name, age, score) in [
(1i64, "Alice", 30i64, "90"),
(2, "Bob", 25, "85"),
(3, "charlie", 40, "invalid"),
(4, "DAVID", 35, "70"),
] {
store.insert_json(
TableId::new(31),
encode_key(&[Value::BigInt(id)]),
&serde_json::json!({"id": id, "name": name, "age": age, "score": score}),
);
}
(QueryEngine::new(schema), store)
}
#[test]
fn test_scalar_projection_upper_with_alias() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id, UPPER(name) AS name_uc FROM users WHERE id = 1",
&[],
)
.expect("UPPER(name) AS name_uc should parse, plan, and execute");
assert_eq!(result.columns.len(), 2);
assert_eq!(result.columns[1].as_str(), "name_uc");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][1], Value::Text("ALICE".into()));
}
#[test]
fn test_scalar_projection_lower_synthesised_name() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT LOWER(name) FROM users WHERE id = 4",
&[],
)
.expect("bare LOWER(name) should use synthesised name");
assert_eq!(result.columns[0].as_str(), "lower");
assert_eq!(result.rows[0][0], Value::Text("david".into()));
}
#[test]
fn test_scalar_projection_concat_operator() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT name || '!' AS shout FROM users WHERE id = 1",
&[],
)
.expect("|| concat should parse and execute");
assert_eq!(result.columns[0].as_str(), "shout");
assert_eq!(result.rows[0][0], Value::Text("Alice!".into()));
}
#[test]
fn test_scalar_projection_coalesce_nullif() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id, COALESCE(NULLIF(name, 'Bob'), 'anon') AS who FROM users",
&[],
)
.expect("COALESCE(NULLIF(...)) should parse and execute");
let by_id: std::collections::HashMap<_, _> = result
.rows
.iter()
.map(|r| (r[0].clone(), r[1].clone()))
.collect();
assert_eq!(by_id[&Value::BigInt(2)], Value::Text("anon".into()));
assert_eq!(by_id[&Value::BigInt(1)], Value::Text("Alice".into()));
}
#[test]
fn test_scalar_projection_cast_text_to_int() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id, CAST(score AS INTEGER) AS score_int FROM users WHERE id = 1",
&[],
)
.expect("CAST(score AS INTEGER) should parse and evaluate");
assert_eq!(result.rows[0][1], Value::Integer(90));
}
#[test]
fn test_scalar_projection_cast_fails_for_unparseable_text() {
let (engine, mut store) = v051_setup();
let result = engine.query(
&mut store,
"SELECT CAST(score AS INTEGER) FROM users WHERE id = 3",
&[],
);
assert!(
result.is_err(),
"CAST of unparseable text must error, got: {result:?}"
);
}
#[test]
fn test_select_alias_preserved_for_bare_column() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT name AS display FROM users WHERE id = 1",
&[],
)
.expect("SELECT col AS alias should preserve alias as output column name");
assert_eq!(result.columns[0].as_str(), "display");
}
#[test]
fn test_where_upper_eq_string() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE UPPER(name) = 'ALICE'",
&[],
)
.expect("WHERE UPPER(name) = 'ALICE' should parse + evaluate");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(1));
}
#[test]
fn test_where_coalesce_gt() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE COALESCE(age, 0) > 30",
&[],
)
.expect("COALESCE in WHERE should parse + evaluate");
let ids: Vec<_> = result.rows.iter().map(|r| r[0].clone()).collect();
assert!(ids.contains(&Value::BigInt(3)));
assert!(ids.contains(&Value::BigInt(4)));
assert_eq!(ids.len(), 2);
}
#[test]
fn test_where_cast_eq_param() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE CAST(score AS INTEGER) = $1",
&[Value::BigInt(85)],
)
.expect("CAST in WHERE should parse and substitute params");
assert_eq!(result.rows.len(), 1);
assert_eq!(result.rows[0][0], Value::BigInt(2));
}
#[test]
fn test_where_concat_param_like() {
let (engine, mut store) = v051_setup();
let result = engine.query(
&mut store,
"SELECT id FROM users WHERE name = 'Ali' || 'ce'",
&[],
);
match result {
Ok(r) => {
assert_eq!(r.rows.len(), 1);
assert_eq!(r.rows[0][0], Value::BigInt(1));
}
Err(e) => {
let s = e.to_string().to_lowercase();
assert!(
s.contains("unsupported") || s.contains("scalar"),
"error should be a clear unsupported-feature, got: {e}",
);
}
}
}
#[test]
fn test_not_in_list_excludes_literals() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE id NOT IN (1, 2)",
&[],
)
.expect("NOT IN list should parse and evaluate");
let ids: Vec<_> = result.rows.iter().map(|r| r[0].clone()).collect();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&Value::BigInt(3)));
assert!(ids.contains(&Value::BigInt(4)));
}
#[test]
fn test_not_between_excludes_range() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE age NOT BETWEEN 30 AND 35",
&[],
)
.expect("NOT BETWEEN should parse and evaluate");
let ids: Vec<_> = result.rows.iter().map(|r| r[0].clone()).collect();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&Value::BigInt(2)));
assert!(ids.contains(&Value::BigInt(3)));
}
#[test]
fn test_scalar_projection_with_where_scalar_combo() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id, UPPER(name) AS up FROM users WHERE COALESCE(age, 0) >= 30",
&[],
)
.expect("scalar projection + scalar WHERE should compose cleanly");
assert_eq!(result.columns.last().unwrap().as_str(), "up");
assert!(!result.rows.is_empty());
}
#[test]
fn test_scalar_neq_uses_scalar_cmp() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id FROM users WHERE UPPER(name) != 'ALICE'",
&[],
)
.expect("!= with scalar LHS should route through ScalarCmp");
let ids: Vec<_> = result.rows.iter().map(|r| r[0].clone()).collect();
assert!(!ids.contains(&Value::BigInt(1)));
assert_eq!(ids.len(), 3);
}
#[test]
fn test_concat_projection_with_null_propagates() {
let (engine, mut store) = v051_setup();
let result = engine
.query(
&mut store,
"SELECT id, score || '!' AS s2 FROM users WHERE id = 1",
&[],
)
.expect("|| should evaluate in SELECT");
assert_eq!(result.rows[0][1], Value::Text("90!".into()));
}
#[test]
fn test_round_scalar_projection() {
use crate::key_encoder::encode_key;
let schema = SchemaBuilder::new()
.table(
"prices",
TableId::new(32),
vec![
ColumnDef::new("id", DataType::BigInt).not_null(),
ColumnDef::new(
"amount",
DataType::Decimal {
precision: 10,
scale: 2,
},
),
],
vec!["id".into()],
)
.build();
let mut store = MockStore::new();
store.insert_json(
TableId::new(32),
encode_key(&[Value::BigInt(1)]),
&serde_json::json!({"id": 1, "amount": 12345}), );
let engine = QueryEngine::new(schema);
let _ = engine.query(
&mut store,
"SELECT id, ROUND(amount) AS r FROM prices WHERE id = 1",
&[],
);
}