use super::*;
use crate::query::ir::{Predicate, PredicateValue, QueryOp, SortKey};
mod phase1_basic_sql {
use super::*;
#[test]
fn test_parse_select_star_from_nodes() {
let query = parse_sql("SELECT * FROM nodes").unwrap();
assert!(!query.ops.is_empty());
assert!(matches!(&query.ops[0], QueryOp::ScanNodes { label: None }));
}
#[test]
fn test_parse_select_star_from_table_as_label() {
let query = parse_sql("SELECT * FROM Person").unwrap();
assert!(matches!(
&query.ops[0],
QueryOp::ScanNodes { label: Some(l) } if l == "person"
));
}
#[test]
fn test_parse_select_with_column_projection() {
let query = parse_sql("SELECT name, age FROM nodes").unwrap();
let project_op = query
.ops
.iter()
.find(|op| matches!(op, QueryOp::Project(_)));
assert!(project_op.is_some());
if let Some(QueryOp::Project(cols)) = project_op {
assert!(cols.contains(&"name".to_string()));
assert!(cols.contains(&"age".to_string()));
}
}
#[test]
fn test_parse_select_with_where_equals() {
let query = parse_sql("SELECT * FROM nodes WHERE name = 'Alice'").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(
pred,
Predicate::Eq { key, value: PredicateValue::String(s) }
if key == "name" && s == "Alice"
));
}
}
#[test]
fn test_parse_select_with_where_greater_than() {
let query = parse_sql("SELECT * FROM nodes WHERE age > 25").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(
pred,
Predicate::Gt { key, value: PredicateValue::Int(25) }
if key == "age"
));
}
}
#[test]
fn test_parse_select_with_where_less_than() {
let query = parse_sql("SELECT * FROM nodes WHERE score < 100").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(
pred,
Predicate::Lt { key, value: PredicateValue::Int(100) }
if key == "score"
));
}
}
#[test]
fn test_parse_select_with_where_and() {
let query = parse_sql("SELECT * FROM nodes WHERE age > 18 AND active = true").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(pred, Predicate::And(_)));
}
}
#[test]
fn test_parse_select_with_where_or() {
let query =
parse_sql("SELECT * FROM nodes WHERE status = 'active' OR status = 'pending'").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(pred, Predicate::Or(_)));
}
}
#[test]
fn test_parse_select_with_where_in_list() {
let query = parse_sql("SELECT * FROM nodes WHERE status IN ('active', 'pending')").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(
matches!(pred, Predicate::In { key, values } if key == "status" && values.len() == 2)
);
}
}
#[test]
fn test_parse_select_with_where_like_contains() {
let query = parse_sql("SELECT * FROM nodes WHERE name LIKE '%test%'").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(
pred,
Predicate::Contains { key, substring }
if key == "name" && substring == "test"
));
}
}
#[test]
fn test_parse_select_with_where_like_starts_with() {
let query = parse_sql("SELECT * FROM nodes WHERE name LIKE 'Al%'").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(
pred,
Predicate::StartsWith { key, prefix }
if key == "name" && prefix == "Al"
));
}
}
#[test]
fn test_parse_select_with_where_like_ends_with() {
let query = parse_sql("SELECT * FROM nodes WHERE email LIKE '%@gmail.com'").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(
pred,
Predicate::EndsWith { key, suffix }
if key == "email" && suffix == "@gmail.com"
));
}
}
#[test]
fn test_parse_select_with_where_is_null() {
let query = parse_sql("SELECT * FROM nodes WHERE deleted_at IS NULL").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(
pred,
Predicate::Eq { key, value: PredicateValue::Null } if key == "deleted_at"
));
}
}
#[test]
fn test_parse_select_with_where_is_not_null() {
let query = parse_sql("SELECT * FROM nodes WHERE email IS NOT NULL").unwrap();
let filter_op = query.ops.iter().find(|op| matches!(op, QueryOp::Filter(_)));
assert!(filter_op.is_some());
if let Some(QueryOp::Filter(pred)) = filter_op {
assert!(matches!(
pred,
Predicate::Ne { key, value: PredicateValue::Null } if key == "email"
));
}
}
#[test]
fn test_parse_select_with_order_by_asc() {
let query = parse_sql("SELECT * FROM nodes ORDER BY name ASC").unwrap();
let sort_op = query
.ops
.iter()
.find(|op| matches!(op, QueryOp::Sort { .. }));
assert!(sort_op.is_some());
if let Some(QueryOp::Sort { key, descending }) = sort_op {
assert!(matches!(key, SortKey::Property(p) if p == "name"));
assert!(!descending);
}
}
#[test]
fn test_parse_select_with_order_by_desc() {
let query = parse_sql("SELECT * FROM nodes ORDER BY age DESC").unwrap();
let sort_op = query
.ops
.iter()
.find(|op| matches!(op, QueryOp::Sort { .. }));
assert!(sort_op.is_some());
if let Some(QueryOp::Sort { key, descending }) = sort_op {
assert!(matches!(key, SortKey::Property(p) if p == "age"));
assert!(descending);
}
}
#[test]
fn test_parse_select_with_limit() {
let query = parse_sql("SELECT * FROM nodes LIMIT 10").unwrap();
let limit_op = query.ops.iter().find(|op| matches!(op, QueryOp::Limit(_)));
assert!(limit_op.is_some());
if let Some(QueryOp::Limit(n)) = limit_op {
assert_eq!(*n, 10);
}
}
#[test]
fn test_parse_select_with_offset() {
let query = parse_sql("SELECT * FROM nodes LIMIT 10 OFFSET 20").unwrap();
let skip_op = query.ops.iter().find(|op| matches!(op, QueryOp::Skip(_)));
assert!(skip_op.is_some());
if let Some(QueryOp::Skip(n)) = skip_op {
assert_eq!(*n, 20);
}
}
#[test]
fn test_parse_complex_query() {
let query = parse_sql(
"SELECT name, age FROM nodes WHERE age > 21 AND active = true ORDER BY age DESC LIMIT 100"
).unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::ScanNodes { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Project(_))));
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::Sort { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Limit(100))));
}
#[test]
fn test_parse_error_invalid_sql() {
let result = parse_sql("SELEC * FORM nodes");
assert!(result.is_err());
}
#[test]
fn test_parse_error_no_from() {
let result = parse_sql("SELECT *");
assert!(result.is_err());
}
}
mod phase2_temporal {
use super::*;
#[test]
fn test_parse_system_time_as_of_basic() {
let query =
parse_sql("SELECT * FROM nodes FOR SYSTEM_TIME AS OF TIMESTAMP '1705315200000000'")
.expect("Should parse FOR SYSTEM_TIME AS OF");
assert!(
query.temporal_context.is_some(),
"Query should have temporal context"
);
let ctx = query.temporal_context.as_ref().unwrap();
assert!(
ctx.as_of_tuple().is_some(),
"Should have as_of temporal context"
);
let (_valid_time, tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(tx_time.wallclock(), 1705315200000000);
}
#[test]
fn test_parse_system_time_as_of_with_where() {
let query = parse_sql(
"SELECT * FROM nodes FOR SYSTEM_TIME AS OF TIMESTAMP '1705315200000000' WHERE age > 21",
)
.expect("Should parse temporal query with WHERE");
assert!(query.temporal_context.is_some());
let has_filter = query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_)));
assert!(has_filter, "Query should have filter operation");
}
#[test]
fn test_parse_system_time_as_of_with_order_limit() {
let query = parse_sql(
"SELECT * FROM nodes FOR SYSTEM_TIME AS OF TIMESTAMP '1705315200000000' ORDER BY name DESC LIMIT 10"
).expect("Should parse temporal query with ORDER BY and LIMIT");
assert!(query.temporal_context.is_some());
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::Sort { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Limit(10))));
}
#[test]
fn test_parse_system_time_between_basic() {
let query = parse_sql(
"SELECT * FROM nodes FOR SYSTEM_TIME BETWEEN TIMESTAMP '1000000' AND TIMESTAMP '2000000'"
).expect("Should parse FOR SYSTEM_TIME BETWEEN");
assert!(
query.temporal_context.is_some(),
"Query should have temporal context"
);
let ctx = query.temporal_context.as_ref().unwrap();
assert!(
ctx.transaction_time_between.is_some(),
"Should have between temporal context"
);
let range = ctx.transaction_time_between.as_ref().unwrap();
assert_eq!(range.start().wallclock(), 1000000);
assert_eq!(range.end().wallclock(), 2000000);
}
#[test]
fn test_parse_system_time_between_with_filter() {
let query = parse_sql(
"SELECT * FROM nodes FOR SYSTEM_TIME BETWEEN TIMESTAMP '1000000' AND TIMESTAMP '2000000' WHERE label = 'Person'"
).expect("Should parse temporal range query with WHERE");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
assert!(ctx.transaction_time_between.is_some());
let has_filter = query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_)));
assert!(has_filter);
}
#[test]
fn test_parse_valid_time_as_of_basic() {
let query =
parse_sql("SELECT * FROM nodes FOR VALID_TIME AS OF TIMESTAMP '1705315200000000'")
.expect("Should parse FOR VALID_TIME AS OF");
assert!(
query.temporal_context.is_some(),
"Query should have temporal context"
);
let ctx = query.temporal_context.as_ref().unwrap();
assert!(
ctx.as_of_tuple().is_some(),
"Should have as_of temporal context"
);
let (valid_time, _tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(valid_time.wallclock(), 1705315200000000);
}
#[test]
fn test_parse_valid_time_as_of_with_where() {
let query = parse_sql(
"SELECT * FROM Person FOR VALID_TIME AS OF TIMESTAMP '1705315200000000' WHERE status = 'active'"
).expect("Should parse valid time query with WHERE");
assert!(query.temporal_context.is_some());
let has_filter = query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_)));
assert!(has_filter);
}
#[test]
fn test_parse_valid_time_between_basic() {
let query = parse_sql(
"SELECT * FROM nodes FOR VALID_TIME BETWEEN TIMESTAMP '1000000' AND TIMESTAMP '2000000'"
).expect("Should parse FOR VALID_TIME BETWEEN");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
assert!(ctx.valid_time_between.is_some());
let range = ctx.valid_time_between.as_ref().unwrap();
assert_eq!(range.start().wallclock(), 1000000);
assert_eq!(range.end().wallclock(), 2000000);
}
#[test]
fn test_parse_bitemporal_system_and_valid_time() {
let query = parse_sql(
"SELECT * FROM nodes FOR SYSTEM_TIME AS OF TIMESTAMP '2000000' FOR VALID_TIME AS OF TIMESTAMP '1500000'"
).expect("Should parse bi-temporal query");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
assert!(ctx.as_of_tuple().is_some());
let (valid_time, tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(valid_time.wallclock(), 1500000);
assert_eq!(tx_time.wallclock(), 2000000);
}
#[test]
fn test_parse_bitemporal_reverse_order() {
let query = parse_sql(
"SELECT * FROM nodes FOR VALID_TIME AS OF TIMESTAMP '1500000' FOR SYSTEM_TIME AS OF TIMESTAMP '2000000'"
).expect("Should parse bi-temporal query in reverse order");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
assert!(ctx.as_of_tuple().is_some());
let (valid_time, tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(valid_time.wallclock(), 1500000);
assert_eq!(tx_time.wallclock(), 2000000);
}
#[test]
fn test_parse_bitemporal_with_complex_query() {
let query = parse_sql(
"SELECT name, age FROM Person FOR SYSTEM_TIME AS OF TIMESTAMP '2000000' FOR VALID_TIME AS OF TIMESTAMP '1500000' WHERE age > 21 ORDER BY name LIMIT 10"
).expect("Should parse complex bi-temporal query");
assert!(query.temporal_context.is_some());
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::Sort { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Limit(10))));
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Project(_))));
}
#[test]
fn test_temporal_clause_parse_timestamp() {
let ts = TemporalClause::parse_timestamp("1705315200000000");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705315200000000);
}
#[test]
fn test_temporal_clause_system_time_as_of() {
use crate::core::temporal::Timestamp;
let clause = TemporalClause::system_time_as_of(Timestamp::from(1000));
assert!(matches!(clause, TemporalClause::SystemTimeAsOf(_)));
}
#[test]
fn test_temporal_clause_system_time_between() {
use crate::core::temporal::Timestamp;
let clause =
TemporalClause::system_time_between(Timestamp::from(1000), Timestamp::from(2000));
assert!(clause.is_ok());
assert!(matches!(
clause.unwrap(),
TemporalClause::SystemTimeBetween(_)
));
}
#[test]
fn test_temporal_clause_invalid_range() {
use crate::core::temporal::Timestamp;
let clause =
TemporalClause::system_time_between(Timestamp::from(2000), Timestamp::from(1000));
assert!(clause.is_err());
}
#[test]
fn test_temporal_context_wired_through_full_pipeline() {
let query = parse_sql(
"SELECT * FROM Person FOR SYSTEM_TIME AS OF TIMESTAMP '1705315200000000' WHERE name = 'Alice'",
)
.expect("Should parse temporal query");
assert!(
query.temporal_context.is_some(),
"temporal_context must be set"
);
let ctx = query.temporal_context.as_ref().unwrap();
let (_valid_time, tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(tx_time.wallclock(), 1705315200000000);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::ScanNodes { label: Some(l) } if l == "person"
)));
}
#[test]
fn test_valid_time_wired_through_full_pipeline() {
let query =
parse_sql("SELECT * FROM nodes FOR VALID_TIME AS OF TIMESTAMP '1705315200000000'")
.expect("Should parse valid time query");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
let (valid_time, _tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(valid_time.wallclock(), 1705315200000000);
}
#[test]
fn test_bitemporal_wired_through_full_pipeline() {
let query = parse_sql(
"SELECT * FROM nodes FOR SYSTEM_TIME AS OF TIMESTAMP '2000000' FOR VALID_TIME AS OF TIMESTAMP '1500000'",
)
.expect("Should parse bi-temporal query");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
let (valid_time, tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(valid_time.wallclock(), 1500000);
assert_eq!(tx_time.wallclock(), 2000000);
}
#[test]
fn test_system_time_between_wired_through_full_pipeline() {
let query = parse_sql(
"SELECT * FROM nodes FOR SYSTEM_TIME BETWEEN TIMESTAMP '1000000' AND TIMESTAMP '2000000' WHERE age > 21",
)
.expect("Should parse temporal range + filter");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
assert!(ctx.transaction_time_between.is_some());
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
}
#[test]
fn test_no_temporal_clause_leaves_context_none() {
let query = parse_sql("SELECT * FROM Person WHERE name = 'Alice'").expect("parse");
assert!(
query.temporal_context.is_none(),
"Non-temporal query should have None context"
);
}
}
mod iso8601_parsing {
use super::*;
#[test]
fn test_parse_unix_microseconds_zero() {
let ts = TemporalClause::parse_timestamp("0");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 0);
}
#[test]
fn test_parse_unix_microseconds_negative() {
let ts = TemporalClause::parse_timestamp("-1000000");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), -1000000);
}
#[test]
fn test_parse_unix_microseconds_large() {
let ts = TemporalClause::parse_timestamp("9223372036854775000");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 9223372036854775000);
}
#[test]
fn test_parse_unix_microseconds_with_quotes() {
let ts = TemporalClause::parse_timestamp("'1705315200000000'");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705315200000000);
}
#[test]
fn test_parse_unix_microseconds_with_double_quotes() {
let ts = TemporalClause::parse_timestamp("\"1705315200000000\"");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705315200000000);
}
#[test]
fn test_parse_unix_microseconds_with_whitespace() {
let ts = TemporalClause::parse_timestamp(" 1705315200000000 ");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705315200000000);
}
#[test]
fn test_parse_iso8601_utc_basic() {
let ts = TemporalClause::parse_timestamp("2024-01-15T10:00:00Z");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800000000);
}
#[test]
fn test_parse_iso8601_utc_with_single_quotes() {
let ts = TemporalClause::parse_timestamp("'2024-01-15T10:00:00Z'");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800000000);
}
#[test]
fn test_parse_iso8601_utc_with_double_quotes() {
let ts = TemporalClause::parse_timestamp("\"2024-01-15T10:00:00Z\"");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800000000);
}
#[test]
fn test_parse_iso8601_utc_with_whitespace() {
let ts = TemporalClause::parse_timestamp(" 2024-01-15T10:00:00Z ");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800000000);
}
#[test]
fn test_parse_iso8601_utc_with_fractional_seconds() {
let ts = TemporalClause::parse_timestamp("2024-01-15T10:00:00.123456Z");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800123456);
}
#[test]
fn test_parse_iso8601_utc_midnight() {
let ts = TemporalClause::parse_timestamp("2024-01-15T00:00:00Z");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705276800000000);
}
#[test]
fn test_parse_iso8601_utc_epoch() {
let ts = TemporalClause::parse_timestamp("1970-01-01T00:00:00Z");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 0);
}
#[test]
fn test_parse_iso8601_with_zero_offset() {
let ts = TemporalClause::parse_timestamp("2024-01-15T10:00:00+00:00");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800000000);
}
#[test]
fn test_parse_iso8601_with_positive_offset() {
let ts = TemporalClause::parse_timestamp("2024-01-15T15:30:00+05:30");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800000000);
}
#[test]
fn test_parse_iso8601_with_negative_offset() {
let ts = TemporalClause::parse_timestamp("2024-01-15T02:00:00-08:00");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800000000);
}
#[test]
fn test_parse_sql_timestamp_format() {
let ts = TemporalClause::parse_timestamp("2024-01-15 10:00:00");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800000000);
}
#[test]
fn test_parse_sql_timestamp_midnight() {
let ts = TemporalClause::parse_timestamp("2024-01-15 00:00:00");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705276800000000);
}
#[test]
fn test_parse_sql_timestamp_with_fractional_seconds() {
let ts = TemporalClause::parse_timestamp("2024-01-15 10:00:00.123456");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800123456);
}
#[test]
fn test_parse_naive_datetime_with_t_separator() {
let ts = TemporalClause::parse_timestamp("2024-01-15T10:00:00");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800000000);
}
#[test]
fn test_parse_naive_datetime_with_t_and_fractional() {
let ts = TemporalClause::parse_timestamp("2024-01-15T10:00:00.123456");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705312800123456);
}
#[test]
fn test_parse_date_only() {
let ts = TemporalClause::parse_timestamp("2024-01-15");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705276800000000);
}
#[test]
fn test_parse_date_only_with_quotes() {
let ts = TemporalClause::parse_timestamp("'2024-01-15'");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1705276800000000);
}
#[test]
fn test_parse_empty_string() {
let ts = TemporalClause::parse_timestamp("");
assert!(ts.is_err());
}
#[test]
fn test_parse_invalid_date_feb_30() {
let ts = TemporalClause::parse_timestamp("2024-02-30T10:00:00Z");
assert!(ts.is_err());
}
#[test]
fn test_parse_invalid_leap_year() {
let ts = TemporalClause::parse_timestamp("2023-02-29T10:00:00Z");
assert!(ts.is_err());
}
#[test]
fn test_parse_valid_leap_year() {
let ts = TemporalClause::parse_timestamp("2024-02-29T00:00:00Z");
assert!(ts.is_ok());
assert_eq!(ts.unwrap().wallclock(), 1709164800000000);
}
#[test]
fn test_parse_far_future_date() {
let ts = TemporalClause::parse_timestamp("2100-12-31T23:59:59Z");
assert!(ts.is_ok());
}
#[test]
fn test_parse_invalid_format() {
let ts = TemporalClause::parse_timestamp("not a timestamp");
assert!(ts.is_err());
}
#[test]
fn test_parse_strict_rejects_trailing_characters_date() {
let ts = TemporalClause::parse_timestamp("2024-01-15-invalid");
assert!(ts.is_err(), "Should reject date with trailing characters");
}
#[test]
fn test_parse_strict_rejects_trailing_characters_datetime() {
let ts = TemporalClause::parse_timestamp("2024-01-15 10:00:00 invalid");
assert!(
ts.is_err(),
"Should reject datetime with trailing characters"
);
}
#[test]
fn test_parse_strict_rejects_partial_date() {
let ts = TemporalClause::parse_timestamp("2024-01");
assert!(ts.is_err(), "Should reject partial date");
}
#[test]
fn test_error_message_lists_formats() {
let result = TemporalClause::parse_timestamp("invalid");
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("ISO 8601") || err_msg.contains("microseconds"),
"Error message should list supported formats"
);
}
#[test]
fn test_sql_parse_with_iso8601_timestamp() {
let query =
parse_sql("SELECT * FROM nodes FOR SYSTEM_TIME AS OF TIMESTAMP '2024-01-15T10:00:00Z'")
.expect("Should parse SQL with ISO 8601 timestamp");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
assert!(ctx.as_of_tuple().is_some());
let (_valid_time, tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(tx_time.wallclock(), 1705312800000000);
}
#[test]
fn test_sql_parse_with_date_only() {
let query = parse_sql("SELECT * FROM nodes FOR VALID_TIME AS OF TIMESTAMP '2024-01-15'")
.expect("Should parse SQL with date-only timestamp");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
let (valid_time, _tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(valid_time.wallclock(), 1705276800000000);
}
#[test]
fn test_sql_parse_with_timezone_offset() {
let query = parse_sql(
"SELECT * FROM nodes FOR SYSTEM_TIME AS OF TIMESTAMP '2024-01-15T15:30:00+05:30'",
)
.expect("Should parse SQL with timezone offset");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
let (_valid_time, tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(tx_time.wallclock(), 1705312800000000);
}
#[test]
fn test_sql_parse_with_sql_format() {
let query =
parse_sql("SELECT * FROM nodes FOR SYSTEM_TIME AS OF TIMESTAMP '2024-01-15 10:00:00'")
.expect("Should parse SQL with SQL timestamp format");
assert!(query.temporal_context.is_some());
let ctx = query.temporal_context.as_ref().unwrap();
let (_valid_time, tx_time) = ctx.as_of_tuple().unwrap();
assert_eq!(tx_time.wallclock(), 1705312800000000);
}
}
mod phase3_graph {
use super::*;
use crate::query::ir::TraversalDepth;
#[test]
fn test_parse_match_outgoing_single_hop() {
let query = parse_sql(
"SELECT * FROM nodes AS source MATCH (source)-[:KNOWS]->(target) WHERE source.name = 'Alice'",
)
.unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::TraverseOut { label: Some(l), depth: TraversalDepth::Exact(1) } if l == "KNOWS"
)));
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
}
#[test]
fn test_parse_match_incoming_edge() {
let query =
parse_sql("SELECT * FROM nodes AS child MATCH (parent)<-[:PARENT_OF]-(child)").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::TraverseIn { label: Some(l), depth: TraversalDepth::Exact(1) } if l == "PARENT_OF"
)));
}
#[test]
fn test_parse_match_bidirectional() {
let query = parse_sql("SELECT * FROM nodes AS n MATCH (n)-[:RELATED]-(related)").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::TraverseBoth { label: Some(l), depth: TraversalDepth::Exact(1) } if l == "RELATED"
)));
}
#[test]
fn test_parse_match_with_where_order_limit() {
let query = parse_sql(
"SELECT * FROM nodes AS source MATCH (source)-[:KNOWS]->(target) WHERE source.name = 'Alice' ORDER BY target.name LIMIT 10",
)
.unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::TraverseOut { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::Sort { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Limit(10))));
}
#[test]
fn test_parse_match_no_where() {
let query = parse_sql("SELECT * FROM nodes AS a MATCH (a)-[:FOLLOWS]->(b)").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::TraverseOut { label: Some(l), .. } if l == "FOLLOWS"
)));
assert!(!query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
}
#[test]
fn test_parse_match_with_label_table() {
let query = parse_sql("SELECT * FROM Person AS p MATCH (p)-[:KNOWS]->(friend)").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::ScanNodes { label: Some(l) } if l == "person"
)));
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::TraverseOut { .. }))
);
}
#[test]
fn test_parse_match_variable_length_range() {
let query = parse_sql(
"SELECT * FROM nodes AS source MATCH (source)-[:KNOWS*1..3]->(target) WHERE source.name = 'Alice'",
)
.unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::TraverseOut { label: Some(l), depth: TraversalDepth::Range { min: 1, max: 3 } } if l == "KNOWS"
)));
}
#[test]
fn test_parse_match_exact_depth() {
let query =
parse_sql("SELECT * FROM nodes AS source MATCH (source)-[:KNOWS*2]->(target)").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::TraverseOut { label: Some(l), depth: TraversalDepth::Exact(2) } if l == "KNOWS"
)));
}
#[test]
fn test_parse_match_unbounded() {
let query =
parse_sql("SELECT * FROM nodes AS source MATCH (source)-[:KNOWS*]->(target)").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::TraverseOut { label: Some(l), depth: TraversalDepth::Variable } if l == "KNOWS"
)));
}
#[test]
fn test_parse_match_open_ended_min() {
let query =
parse_sql("SELECT * FROM nodes AS source MATCH (source)-[:KNOWS*2..]->(target)")
.unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::TraverseOut { label: Some(l), depth: TraversalDepth::Range { min: 2, max: 10 } } if l == "KNOWS"
)));
}
#[test]
fn test_parse_match_open_ended_max() {
let query =
parse_sql("SELECT * FROM nodes AS source MATCH (source)-[:KNOWS*..3]->(target)")
.unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::TraverseOut { label: Some(l), depth: TraversalDepth::Range { min: 1, max: 3 } } if l == "KNOWS"
)));
}
#[test]
fn test_parse_match_with_temporal() {
let query = parse_sql(
"SELECT * FROM nodes AS a FOR SYSTEM_TIME AS OF TIMESTAMP '2000000' MATCH (a)-[:KNOWS]->(b) WHERE a.name = 'Alice'",
)
.unwrap();
assert!(query.temporal_context.is_some());
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::TraverseOut { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
}
#[test]
fn test_parse_match_simple_traversal() {
let query =
parse_sql("SELECT * FROM nodes AS source MATCH (source)-[:KNOWS]->(target)").unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::TraverseOut { .. }))
);
}
#[test]
fn test_parse_match_variable_length() {
let query =
parse_sql("SELECT * FROM nodes AS source MATCH (source)-[:KNOWS*1..3]->(target)")
.unwrap();
let traverse = query
.ops
.iter()
.find(|op| matches!(op, QueryOp::TraverseOut { .. }));
if let Some(QueryOp::TraverseOut { depth, .. }) = traverse {
assert!(matches!(depth, TraversalDepth::Range { min: 1, max: 3 }));
} else {
panic!("Expected TraverseOut op");
}
}
#[test]
fn test_parse_match_incoming() {
let query =
parse_sql("SELECT * FROM nodes AS source MATCH (source)<-[:FOLLOWS]-(target)").unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::TraverseIn { .. }))
);
}
#[test]
fn test_parse_select_from_edges() {
let query = parse_sql("SELECT * FROM edges").unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::ScanEdges { edge_type: None }))
);
}
#[test]
fn test_parse_select_from_edges_with_filter() {
let query = parse_sql("SELECT * FROM edges WHERE type = 'KNOWS'").unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::ScanEdges { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
}
#[test]
fn test_parse_select_from_edges_with_limit() {
let query = parse_sql("SELECT * FROM edges LIMIT 10").unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::ScanEdges { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Limit(10))));
}
#[test]
fn test_parse_select_from_edges_with_order_by() {
let query = parse_sql("SELECT * FROM edges ORDER BY timestamp DESC").unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::ScanEdges { .. }))
);
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::Sort {
key: SortKey::Timestamp,
descending: true
}
)));
}
#[test]
fn test_join_gives_helpful_error() {
let result = parse_sql("SELECT p.name FROM Person p JOIN Company c ON p.company_id = c.id");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("MATCH"),
"Error should suggest MATCH alternative: {}",
err
);
}
#[test]
fn test_parse_multiple_match_clauses() {
let query = parse_sql(
"SELECT * FROM nodes AS a MATCH (a)-[:KNOWS]->(b) MATCH (b)-[:WORKS_AT]->(c) WHERE a.name = 'Alice'",
)
.unwrap();
let traverse_count = query
.ops
.iter()
.filter(|op| {
matches!(
op,
QueryOp::TraverseOut { .. }
| QueryOp::TraverseIn { .. }
| QueryOp::TraverseBoth { .. }
)
})
.count();
assert_eq!(
traverse_count, 2,
"Expected exactly 2 traversal ops, got {}",
traverse_count
);
}
#[test]
fn test_parse_match_no_edge_type() {
let query = parse_sql("SELECT * FROM nodes AS a MATCH (a)-[]->(b)").unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::TraverseOut { label: None, .. }))
);
}
}
mod integration {
use super::*;
use crate::query::planner::{QueryPlanner, Statistics};
use crate::storage::CurrentStorage;
use std::sync::Arc;
#[test]
fn test_sql_to_planner_integration() {
let query = parse_sql("SELECT * FROM nodes LIMIT 10").unwrap();
let storage = Arc::new(CurrentStorage::new());
let stats = Arc::new(Statistics::default());
let planner = QueryPlanner::new(stats, storage);
let plan = planner.plan(query);
assert!(plan.is_ok());
}
#[test]
fn test_sql_with_filter_to_planner() {
let query = parse_sql("SELECT * FROM nodes WHERE age > 21 LIMIT 100").unwrap();
let storage = Arc::new(CurrentStorage::new());
let stats = Arc::new(Statistics::default());
let planner = QueryPlanner::new(stats, storage);
let plan = planner.plan(query);
assert!(plan.is_ok());
}
#[test]
fn test_sql_with_order_to_planner() {
let query = parse_sql("SELECT * FROM nodes ORDER BY name ASC LIMIT 50").unwrap();
let storage = Arc::new(CurrentStorage::new());
let stats = Arc::new(Statistics::default());
let planner = QueryPlanner::new(stats, storage);
let plan = planner.plan(query);
assert!(plan.is_ok());
}
}
mod phase4_vector {
use super::*;
use crate::sql::converter::SqlParameterValue;
use std::collections::HashMap;
use std::sync::Arc;
#[test]
fn test_parse_vector_knn_with_literal() {
let query = parse_sql(
"SELECT * FROM Documents ORDER BY embedding <=> '[0.1, 0.2, 0.3]'::vector LIMIT 10",
)
.unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::VectorSearch { k: 10, .. }))
);
}
#[test]
fn test_parse_vector_knn_with_parameter() {
let embedding: Arc<[f32]> = vec![0.1, 0.2, 0.3].into();
let mut params = HashMap::new();
params.insert(
"query_embedding".to_string(),
SqlParameterValue::Embedding(embedding),
);
let query = parse_sql_with_params(
"SELECT * FROM Documents ORDER BY embedding <=> $query_embedding LIMIT 10",
params,
)
.unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::VectorSearch { .. }))
);
}
#[test]
fn test_parse_vector_knn_property_key() {
let query = parse_sql(
"SELECT * FROM Documents ORDER BY embedding <=> '[0.1, 0.2]'::vector LIMIT 5",
)
.unwrap();
if let Some(QueryOp::VectorSearch {
k, property_key, ..
}) = query
.ops
.iter()
.find(|op| matches!(op, QueryOp::VectorSearch { .. }))
{
assert_eq!(*k, 5);
assert_eq!(property_key.as_deref(), Some("embedding"));
} else {
panic!("Expected VectorSearch op");
}
}
#[test]
fn test_parse_vector_knn_function() {
let embedding: Arc<[f32]> = vec![0.1, 0.2, 0.3].into();
let mut params = HashMap::new();
params.insert("query".to_string(), SqlParameterValue::Embedding(embedding));
let query = parse_sql_with_params(
"SELECT * FROM KNN('Documents', 'embedding', $query, 10)",
params,
)
.unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::VectorSearch { k: 10, .. }))
);
}
#[test]
fn test_parse_hybrid_graph_plus_vector() {
let embedding: Arc<[f32]> = vec![0.1, 0.2, 0.3].into();
let mut params = HashMap::new();
params.insert("query".to_string(), SqlParameterValue::Embedding(embedding));
let query = parse_sql_with_params(
"SELECT * FROM nodes AS doctor MATCH (doctor)-[:COMPANION]->(companion) WHERE doctor.name = 'David Tennant' ORDER BY companion.embedding <=> $query LIMIT 10",
params,
)
.unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::TraverseOut { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::RankBySimilarity { .. }))
);
}
#[test]
fn test_parse_full_hybrid_temporal_graph_vector() {
let embedding: Arc<[f32]> = vec![0.1, 0.2, 0.3].into();
let mut params = HashMap::new();
params.insert("rec".to_string(), SqlParameterValue::Embedding(embedding));
let query = parse_sql_with_params(
"SELECT * FROM nodes AS user FOR SYSTEM_TIME AS OF TIMESTAMP '2000000' MATCH (user)-[:VIEWED]->(item) WHERE item.price < 100 ORDER BY item.embedding <=> $rec LIMIT 10",
params,
)
.unwrap();
assert!(query.temporal_context.is_some());
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::TraverseOut { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Filter(_))));
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::RankBySimilarity { .. }))
);
assert!(query.ops.iter().any(|op| matches!(op, QueryOp::Limit(10))));
}
#[test]
fn test_parse_vector_no_limit_defaults() {
let query =
parse_sql("SELECT * FROM Documents ORDER BY embedding <=> '[0.1, 0.2]'::vector")
.unwrap();
if let Some(QueryOp::VectorSearch { k, .. }) = query
.ops
.iter()
.find(|op| matches!(op, QueryOp::VectorSearch { .. }))
{
assert_eq!(*k, 10, "Default k should be 10");
} else {
panic!("Expected VectorSearch op");
}
}
#[test]
fn test_parse_vector_unbound_parameter_error() {
let result =
parse_sql("SELECT * FROM Documents ORDER BY embedding <=> $nonexistent LIMIT 10");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("nonexistent") || err.contains("Unbound"),
"Error should mention parameter: {}",
err
);
}
#[test]
fn test_parse_vector_l2_distance_metric() {
let query = parse_sql(
"SELECT * FROM Documents ORDER BY embedding <-> '[0.1, 0.2]'::vector LIMIT 5",
)
.unwrap();
if let Some(QueryOp::VectorSearch { metric, .. }) = query
.ops
.iter()
.find(|op| matches!(op, QueryOp::VectorSearch { .. }))
{
assert_eq!(
*metric,
crate::index::vector::DistanceMetric::Euclidean,
"Should use Euclidean for <->"
);
} else {
panic!("Expected VectorSearch op");
}
}
#[test]
fn test_parse_vector_similar_to() {
let embedding: Arc<[f32]> = vec![0.1, 0.2, 0.3].into();
let mut params = HashMap::new();
params.insert(
"query_embedding".to_string(),
SqlParameterValue::Embedding(embedding),
);
let query = parse_sql_with_params(
"SELECT * FROM Documents WHERE SIMILAR_TO(embedding, $query_embedding, 0.8)",
params,
)
.unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::VectorSearch { .. }))
);
}
#[test]
fn test_parse_vector_knn_function_replaces_scan() {
let embedding: Arc<[f32]> = vec![0.1, 0.2, 0.3].into();
let mut params = HashMap::new();
params.insert("query".to_string(), SqlParameterValue::Embedding(embedding));
let query = parse_sql_with_params(
"SELECT * FROM KNN('Documents', 'embedding', $query, 10)",
params,
)
.unwrap();
assert!(matches!(&query.ops[0], QueryOp::VectorSearch { .. }));
assert!(
!query
.ops
.iter()
.any(|op| matches!(op, QueryOp::ScanNodes { .. }))
);
}
#[test]
fn test_similar_to_enforces_threshold() {
let embedding: Arc<[f32]> = vec![0.1, 0.2, 0.3].into();
let mut params = HashMap::new();
params.insert(
"query_embedding".to_string(),
SqlParameterValue::Embedding(embedding),
);
let query = parse_sql_with_params(
"SELECT * FROM Documents WHERE SIMILAR_TO(embedding, $query_embedding, 0.8)",
params,
)
.unwrap();
assert!(
query
.ops
.iter()
.any(|op| matches!(op, QueryOp::VectorSearch { .. }))
);
let has_score_filter = query.ops.iter().any(|op| {
matches!(
op,
QueryOp::Filter(Predicate::Gte {
key,
value: PredicateValue::Float(t),
}) if key == "score" && (*t - 0.8).abs() < f64::EPSILON
)
});
assert!(
has_score_filter,
"Expected a Filter(Gte score >= 0.8) op, got: {:?}",
query.ops
);
}
#[test]
fn test_knn_offset_accounts_for_skip() {
let query = parse_sql(
"SELECT * FROM Documents ORDER BY embedding <=> '[0.1, 0.2]'::vector LIMIT 10 OFFSET 5",
)
.unwrap();
if let Some(QueryOp::VectorSearch { k, .. }) = query
.ops
.iter()
.find(|op| matches!(op, QueryOp::VectorSearch { .. }))
{
assert_eq!(*k, 15, "k should be limit + offset = 15");
} else {
panic!("Expected VectorSearch op");
}
assert!(
query.ops.iter().any(|op| matches!(op, QueryOp::Skip(5))),
"Expected Skip(5), got: {:?}",
query.ops
);
assert!(
query.ops.iter().any(|op| matches!(op, QueryOp::Limit(10))),
"Expected Limit(10), got: {:?}",
query.ops
);
}
#[test]
fn test_knn_without_offset_no_extra_limit() {
let query = parse_sql(
"SELECT * FROM Documents ORDER BY embedding <=> '[0.1, 0.2]'::vector LIMIT 10",
)
.unwrap();
if let Some(QueryOp::VectorSearch { k, .. }) = query
.ops
.iter()
.find(|op| matches!(op, QueryOp::VectorSearch { .. }))
{
assert_eq!(*k, 10);
} else {
panic!("Expected VectorSearch op");
}
assert!(
!query.ops.iter().any(|op| matches!(op, QueryOp::Limit(_))),
"No Limit op expected without OFFSET, got: {:?}",
query.ops
);
}
#[test]
fn test_knn_function_preserves_label_filter() {
let embedding: Arc<[f32]> = vec![0.1, 0.2, 0.3].into();
let mut params = HashMap::new();
params.insert("query".to_string(), SqlParameterValue::Embedding(embedding));
let query = parse_sql_with_params(
"SELECT * FROM KNN('Documents', 'embedding', $query, 10)",
params,
)
.unwrap();
assert!(matches!(&query.ops[0], QueryOp::VectorSearch { .. }));
let has_label_filter = query
.ops
.iter()
.any(|op| matches!(op, QueryOp::FilterLabel(l) if l == "documents"));
assert!(
has_label_filter,
"Expected FilterLabel('documents'), got: {:?}",
query.ops
);
assert!(
!query
.ops
.iter()
.any(|op| matches!(op, QueryOp::ScanNodes { .. }))
);
}
}
mod errors {
use super::*;
#[test]
fn test_error_invalid_syntax() {
let result = parse_sql("SELEC * FORM nodes");
assert!(matches!(result, Err(SqlError::ParseError(_))));
}
#[test]
fn test_error_unsupported_statement() {
let result = parse_sql("INSERT INTO nodes VALUES (1, 2, 3)");
assert!(matches!(result, Err(SqlError::UnsupportedFeature(_))));
}
#[test]
fn test_error_multiple_statements() {
let result = parse_sql("SELECT * FROM nodes; SELECT * FROM edges");
assert!(matches!(result, Err(SqlError::UnsupportedFeature(_))));
}
}
mod polish {
use super::*;
#[test]
fn test_order_by_simple_identifier() {
let query = parse_sql("SELECT * FROM Person ORDER BY name").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::Sort {
key: SortKey::Property(p),
descending: false
} if p == "name"
)));
}
#[test]
fn test_order_by_compound_identifier() {
let query = parse_sql("SELECT * FROM Person ORDER BY person.age DESC").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::Sort {
key: SortKey::Property(p),
descending: true
} if p == "age"
)));
}
#[test]
fn test_order_by_score_special_key() {
let query = parse_sql("SELECT * FROM nodes ORDER BY score DESC").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::Sort {
key: SortKey::Score,
descending: true
}
)));
}
#[test]
fn test_order_by_timestamp_special_key() {
let query = parse_sql("SELECT * FROM nodes ORDER BY timestamp ASC").unwrap();
assert!(query.ops.iter().any(|op| matches!(
op,
QueryOp::Sort {
key: SortKey::Timestamp,
descending: false
}
)));
}
#[test]
fn test_order_by_complex_expression_gives_clear_error() {
let result = parse_sql("SELECT * FROM nodes ORDER BY price * quantity");
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("ORDER BY") || err.contains("Complex") || err.contains("not supported"),
"Error should be about ORDER BY expressions: {}",
err
);
}
#[test]
fn test_order_by_function_gives_clear_error() {
let result = parse_sql("SELECT * FROM nodes ORDER BY LOWER(name)");
assert!(result.is_err());
}
#[test]
fn test_multiple_order_by_clauses() {
let query = parse_sql("SELECT * FROM Person ORDER BY age DESC, name ASC").unwrap();
let sort_ops: Vec<_> = query
.ops
.iter()
.filter(|op| matches!(op, QueryOp::Sort { .. }))
.collect();
assert_eq!(sort_ops.len(), 2, "Should have two sort operations");
}
}