use qql_core::ast::{OrderDirection, PrefetchSource, QueryExpr, QueryStmt, Stmt};
use qql_core::fmt::format_stmt;
use qql_core::parser::Parser;
fn parse(source: &str) -> Stmt {
Parser::parse(source).unwrap_or_else(|e| panic!("parse {source}: {e}"))
}
fn strip_spans(stmt: &mut Stmt) {
if let Stmt::Query(q) = stmt {
strip_query_spans(q);
}
}
fn strip_query_spans(q: &mut QueryStmt) {
q.collection_span = None;
if let Some(group) = q.group.as_mut() {
group.field_span = None;
}
for cte in &mut q.ctes {
strip_query_spans(&mut cte.query);
}
let prefetch = match &mut q.expression {
QueryExpr::Nearest { prefetch, .. }
| QueryExpr::Recommend { prefetch, .. }
| QueryExpr::Context { prefetch, .. }
| QueryExpr::Discover { prefetch, .. }
| QueryExpr::Fusion { prefetch, .. }
| QueryExpr::Formula { prefetch, .. }
| QueryExpr::RelevanceFeedback { prefetch, .. }
| QueryExpr::Rerank { prefetch, .. }
| QueryExpr::CrossRerank { prefetch, .. } => Some(prefetch),
QueryExpr::Points { .. }
| QueryExpr::OrderBy { .. }
| QueryExpr::SampleRandom
| QueryExpr::Hybrid { .. } => None,
};
if let Some(prefetches) = prefetch {
for stage in prefetches {
if let PrefetchSource::Query(nested) = &mut stage.source {
strip_query_spans(nested);
}
}
}
}
fn assert_round_trip(source: &str, expected: &str) {
let stmt = parse(source);
assert_eq!(format_stmt(&stmt), expected, "canonical form of {source}");
let mut reparsed = parse(expected);
let mut stmt = stmt;
strip_spans(&mut reparsed);
strip_spans(&mut stmt);
assert_eq!(reparsed, stmt, "AST mismatch for {source}");
}
#[test]
fn scroll_order_by_parses_and_round_trips() {
assert_round_trip(
"SCROLL FROM docs ORDER BY created_at DESC LIMIT 10;",
"SCROLL FROM docs ORDER BY created_at DESC LIMIT 10",
);
assert_round_trip(
"scroll from docs order by created_at limit 10;",
"SCROLL FROM docs ORDER BY created_at ASC LIMIT 10",
);
}
#[test]
fn scroll_order_by_start_from_values() {
assert_round_trip(
"SCROLL FROM docs ORDER BY score DESC START FROM 100 LIMIT 10;",
"SCROLL FROM docs ORDER BY score DESC START FROM 100 LIMIT 10",
);
assert_round_trip(
"SCROLL FROM docs ORDER BY score ASC START FROM 4.5 LIMIT 10;",
"SCROLL FROM docs ORDER BY score ASC START FROM 4.5 LIMIT 10",
);
assert_round_trip(
"SCROLL FROM docs ORDER BY created_at DESC START FROM '2024-01-01T00:00:00Z' LIMIT 10;",
"SCROLL FROM docs ORDER BY created_at DESC START FROM '2024-01-01T00:00:00Z' LIMIT 10",
);
}
#[test]
fn scroll_order_by_combines_with_other_clauses() {
let stmt = parse(
"SCROLL FROM docs WHERE status = 'active' AFTER 7 ORDER BY created_at DESC SHARD 'acme' WITH PAYLOAD INCLUDE (title) WITH VECTOR (dense) LIMIT 10;",
);
let Stmt::Scroll(scroll) = &stmt else {
panic!("expected SCROLL, got {stmt:?}");
};
assert_eq!(scroll.collection, "docs");
assert_eq!(scroll.limit, 10);
assert!(scroll.filter.is_some());
assert!(scroll.after.is_some());
let order = scroll.order_by.as_ref().expect("order_by");
assert_eq!(order.field, "created_at");
assert_eq!(order.direction, OrderDirection::Desc);
assert!(scroll.shard_key.is_some());
assert!(scroll.with_payload.is_some());
assert!(scroll.with_vector.is_some());
assert_eq!(
format_stmt(&stmt),
"SCROLL FROM docs WHERE status = 'active' AFTER 7 ORDER BY created_at DESC SHARD 'acme' WITH PAYLOAD INCLUDE (title) WITH VECTOR (dense) LIMIT 10"
);
}
#[test]
fn scroll_order_by_rejects_bad_start() {
assert!(Parser::parse("SCROLL FROM docs ORDER BY x START 5 LIMIT 10;").is_err());
assert!(Parser::parse("SCROLL FROM docs ORDER BY x START FROM true LIMIT 10;").is_err());
assert!(Parser::parse("SCROLL FROM docs ORDER BY x START FROM null LIMIT 10;").is_err());
assert!(Parser::parse("SCROLL FROM docs ORDER BY x START FROM [1] LIMIT 10;").is_err());
assert!(Parser::parse("SCROLL FROM docs ORDER BY x START FROM {a: 1} LIMIT 10;").is_err());
}
#[test]
fn scroll_payload_selectors() {
assert_round_trip(
"SCROLL FROM docs WITH PAYLOAD false LIMIT 10;",
"SCROLL FROM docs WITH PAYLOAD false LIMIT 10",
);
assert_round_trip(
"SCROLL FROM docs WITH PAYLOAD INCLUDE (title, url) LIMIT 10;",
"SCROLL FROM docs WITH PAYLOAD INCLUDE (title, url) LIMIT 10",
);
assert_round_trip(
"SCROLL FROM docs WITH PAYLOAD EXCLUDE (secret) LIMIT 10;",
"SCROLL FROM docs WITH PAYLOAD EXCLUDE (secret) LIMIT 10",
);
let Stmt::Scroll(scroll) = parse("SCROLL FROM docs LIMIT 10;") else {
panic!("expected SCROLL");
};
assert!(scroll.with_payload.is_none());
}
#[test]
fn query_order_by_start_from() {
assert_round_trip(
"QUERY ORDER BY created_at DESC START FROM '2024-01-01T00:00:00Z' FROM docs LIMIT 20;",
"QUERY ORDER BY created_at DESC START FROM '2024-01-01T00:00:00Z'\nFROM docs\nLIMIT 20",
);
let Stmt::Query(query) = parse("QUERY ORDER BY score ASC START FROM 100 FROM docs LIMIT 5;")
else {
panic!("expected QUERY");
};
let QueryExpr::OrderBy {
field,
direction,
start_from,
} = &query.expression
else {
panic!("expected ORDER BY, got {:?}", query.expression);
};
assert_eq!(field, "score");
assert_eq!(*direction, OrderDirection::Asc);
assert_eq!(*start_from, Some(qql_core::ast::Value::Int(100)));
let Stmt::Query(plain) = parse("QUERY ORDER BY rank ASC FROM docs LIMIT 5;") else {
panic!("expected QUERY");
};
assert!(matches!(
plain.expression,
QueryExpr::OrderBy {
start_from: None,
..
}
));
}
#[test]
fn group_lookup_full_selectors() {
assert_round_trip(
"QUERY TEXT 'news' MODEL 'e5' FROM docs GROUP BY topic SIZE 5 LOOKUP FROM topics WITH PAYLOAD INCLUDE (title) WITH VECTOR (dense) LIMIT 20;",
"QUERY TEXT 'news' MODEL 'e5'\nFROM docs\nGROUP BY topic SIZE 5 LOOKUP FROM topics WITH PAYLOAD INCLUDE (title) WITH VECTOR (dense)\nLIMIT 20",
);
let Stmt::Query(query) = parse(
"QUERY TEXT 'x' FROM docs GROUP BY topic LOOKUP FROM t WITH PAYLOAD false WITH VECTOR false LIMIT 5;",
) else {
panic!("expected QUERY");
};
let group = query.group.as_ref().expect("group");
let lookup = group.lookup.as_ref().expect("lookup");
assert_eq!(lookup.collection, "t");
assert_eq!(lookup.payload, Some(qql_core::ast::PayloadSelector::None));
assert_eq!(lookup.vectors, Some(qql_core::ast::VectorSelector::None));
let Stmt::Query(bare) = parse("QUERY TEXT 'x' FROM docs GROUP BY topic LOOKUP FROM t LIMIT 5;")
else {
panic!("expected QUERY");
};
let lookup = bare
.group
.as_ref()
.expect("group")
.lookup
.as_ref()
.expect("lookup");
assert!(lookup.payload.is_none());
assert!(lookup.vectors.is_none());
}
#[test]
fn prefetch_lookup_shard_key() {
assert_round_trip(
"WITH a AS (QUERY TEXT 'x' MODEL 'e5' FROM docs USING dense LIMIT 50) QUERY FUSION RRF FROM docs PREFETCH (a LOOKUP FROM docs2 VECTOR dense SHARD 'acme') LIMIT 10;",
"WITH\n a AS (QUERY TEXT 'x' MODEL 'e5' FROM docs USING dense LIMIT 50)\nQUERY FUSION RRF\nFROM docs\nPREFETCH (a LOOKUP FROM docs2 VECTOR dense SHARD 'acme')\nLIMIT 10",
);
let Stmt::Query(query) = parse(
"QUERY TEXT 'x' FROM docs USING dense PREFETCH (QUERY TEXT 'y' FROM docs USING dense LIMIT 5 LOOKUP FROM c SHARD 101) LIMIT 10;",
) else {
panic!("expected QUERY");
};
let QueryExpr::Nearest { prefetch, .. } = &query.expression else {
panic!("expected nearest, got {:?}", query.expression);
};
let lookup = prefetch[0].lookup.as_ref().expect("lookup");
assert_eq!(lookup.collection, "c");
assert_eq!(lookup.shard_key, Some(qql_core::ast::ShardKey::Number(101)));
}