use crate::ast::{
AggregateCall, ArchetypePredicate, ClassExprOperand, ColumnExpr, CompareOperand,
ContainsConstraint, ContainsExpr, FunctionCall, IdentifiedExpr, IdentifiedPath, LikeOperand,
Limit, MatchesOperand, NodeNameConstraint, NodePredicate, ObjectPath, OrderByExpr, PathPart,
PathPredicate, PathPredicateOperand, Primitive, SelectClause, SelectExpr, SelectQuery,
SortOrder, StandardPredicate, StatFunc, Terminal, TerminologyFunction, Top, TopDirection,
ValueListItem, VersionPredicate, WhereExpr,
};
use crate::lexer::Token;
use chumsky::prelude::*;
#[expect(
unused_qualifications,
reason = "the local alias shadows the name being qualified — dropping the path makes the definition self-referential"
)]
type Err<'a> = chumsky::extra::Err<Simple<'a, Token>>;
pub fn parse(tokens: &[Token]) -> Result<SelectQuery, Vec<Simple<'_, Token>>> {
query().parse(tokens).into_result()
}
pub fn parse_str(src: &str) -> Result<SelectQuery, String> {
let tokens = crate::lexer::lex(src).map_err(|e| e.to_string())?;
parse(&tokens).map_err(|errs| {
errs.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("; ")
})
}
fn ident<'a>() -> impl Parser<'a, &'a [Token], String, Err<'a>> + Clone {
select! { Token::Identifier(s) => s }
}
fn unquote(s: &str) -> String {
let inner = s
.len()
.checked_sub(1)
.and_then(|end| s.get(1..end))
.unwrap_or(s);
unescape(inner)
}
fn unescape(inner: &str) -> String {
if !inner.contains('\\') {
return inner.to_string();
}
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars().peekable();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
let Some(&next) = chars.peek() else {
out.push('\\');
break;
};
match next {
'\'' | '"' | '?' | '\\' => {
out.push(next);
chars.next();
}
'a' => push_escaped(&mut out, &mut chars, '\u{07}'),
'b' => push_escaped(&mut out, &mut chars, '\u{08}'),
'f' => push_escaped(&mut out, &mut chars, '\u{0C}'),
'n' => push_escaped(&mut out, &mut chars, '\n'),
'r' => push_escaped(&mut out, &mut chars, '\r'),
't' => push_escaped(&mut out, &mut chars, '\t'),
'v' => push_escaped(&mut out, &mut chars, '\u{0B}'),
'u' => {
chars.next(); let hex: String = chars.clone().take(4).collect();
if hex.len() == 4
&& let Ok(cp) = u32::from_str_radix(&hex, 16)
&& let Some(ch) = char::from_u32(cp)
{
for _ in 0..4 {
chars.next();
}
out.push(ch);
} else {
out.push('\\');
out.push('u');
}
}
'0'..='7' => {
chars.next(); let mut digits = String::new();
digits.push(next);
while digits.len() < 3
&& let Some(&d) = chars.peek()
&& ('0'..='7').contains(&d)
{
digits.push(d);
chars.next();
}
if let Ok(cp) = u32::from_str_radix(&digits, 8)
&& let Some(ch) = char::from_u32(cp)
{
out.push(ch);
} else {
out.push('\\');
out.push_str(&digits);
}
}
_ => out.push('\\'),
}
}
out
}
fn push_escaped(out: &mut String, chars: &mut std::iter::Peekable<std::str::Chars<'_>>, ch: char) {
chars.next();
out.push(ch);
}
#[derive(Clone, Copy)]
enum NumKind {
Int,
Real,
SciInt,
}
fn parse_number(kind: NumKind, s: &str) -> Option<Primitive> {
match kind {
NumKind::Int => s.parse::<i64>().ok().map(Primitive::Integer),
NumKind::Real => {
let r = s.parse::<f64>().ok()?;
r.is_finite().then_some(Primitive::Real(r))
}
NumKind::SciInt => {
let r = s.parse::<f64>().ok()?;
if !r.is_finite() {
return None;
}
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
reason = "the guard on this very line proves r is integral and inside the i64 range; the i64::MIN/MAX casts are the bound check itself"
)]
if r.fract() == 0.0 && r >= i64::MIN as f64 && r <= i64::MAX as f64 {
Some(Primitive::Integer(r as i64))
} else {
Some(Primitive::Real(r))
}
}
}
}
fn primitive<'a>() -> impl Parser<'a, &'a [Token], Primitive, Err<'a>> + Clone {
let unsigned = select! {
Token::Integer(s) => (NumKind::Int, s),
Token::Real(s) => (NumKind::Real, s),
Token::SciInteger(s) => (NumKind::SciInt, s),
Token::SciReal(s) => (NumKind::Real, s),
}
.try_map(|(kind, s), span| parse_number(kind, &s).ok_or_else(|| Simple::new(None, span)));
let signed = just(Token::Minus)
.repeated()
.foldr(unsigned, |_minus, p| match p {
Primitive::Integer(n) => Primitive::Integer(n.wrapping_neg()),
Primitive::Real(r) => Primitive::Real(-r),
other => other,
});
let other = select! {
Token::String(s) => Primitive::String(unquote(&s)),
Token::True => Primitive::Boolean(true),
Token::False => Primitive::Boolean(false),
Token::Null => Primitive::Null,
};
signed.or(other)
}
fn parameter<'a>() -> impl Parser<'a, &'a [Token], String, Err<'a>> + Clone {
select! { Token::Parameter(s) => s }
}
#[expect(
clippy::type_complexity,
reason = "the tuple of three mutually-recursive chumsky parsers is the return type; naming it would need an unnameable opaque-type alias per element"
)]
fn path_parsers<'a>() -> (
impl Parser<'a, &'a [Token], IdentifiedPath, Err<'a>> + Clone,
impl Parser<'a, &'a [Token], PathPredicate, Err<'a>> + Clone,
impl Parser<'a, &'a [Token], StandardPredicate, Err<'a>> + Clone,
) {
let mut identified = Recursive::declare();
let mut object = Recursive::declare();
let mut predicate = Recursive::declare();
let code = select! { Token::IdCode(s) => s, Token::AtCode(s) => s };
let predicate_operand = primitive()
.map(PathPredicateOperand::Primitive)
.or(parameter().map(PathPredicateOperand::Parameter))
.or(code.map(PathPredicateOperand::Code))
.or(object.clone().map(PathPredicateOperand::Path));
let comparison = select! { Token::Comparison(op) => op };
let standard = object
.clone()
.then(comparison)
.then(predicate_operand)
.map(|((path, op), operand)| StandardPredicate { path, op, operand });
let archetype = select! { Token::ArchetypeHrid(s) => ArchetypePredicate::Hrid(s) }
.or(parameter().map(ArchetypePredicate::Parameter));
let name_constraint = select! {
Token::String(s) => NodeNameConstraint::String(unquote(&s)),
Token::TermCode(s) => NodeNameConstraint::TermCode(s),
Token::IdCode(s) => NodeNameConstraint::Code(s),
Token::AtCode(s) => NodeNameConstraint::Code(s),
}
.or(parameter().map(NodeNameConstraint::Parameter));
let node_code = code
.then(
just(Token::Comma)
.ignore_then(name_constraint.clone())
.or_not(),
)
.map(|(code, name)| NodePredicate::Code { code, name });
let node_archetype = select! { Token::ArchetypeHrid(s) => s }
.then(just(Token::Comma).ignore_then(name_constraint).or_not())
.map(|(hrid, name)| NodePredicate::Archetype { hrid, name });
let node_matches_regex = object
.clone()
.then_ignore(just(Token::Matches))
.then(select! { Token::ContainedRegex(s) => s })
.map(|(path, regex)| NodePredicate::MatchesRegex { path, regex });
let node_atom = node_code
.or(node_archetype)
.or(parameter().map(NodePredicate::Parameter))
.or(node_matches_regex)
.or(standard
.clone()
.map(|s| NodePredicate::Standard(Box::new(s))));
let node_and = node_atom.clone().foldl(
just(Token::And).ignore_then(node_atom.clone()).repeated(),
|l, r| NodePredicate::And(Box::new(l), Box::new(r)),
);
let node = node_and
.clone()
.foldl(just(Token::Or).ignore_then(node_and).repeated(), |l, r| {
NodePredicate::Or(Box::new(l), Box::new(r))
});
predicate.define(
archetype
.clone()
.map(PathPredicate::Archetype)
.or(node.map(|n| match n {
NodePredicate::Standard(s) => PathPredicate::Standard(s),
other => PathPredicate::Node(Box::new(other)),
}))
.delimited_by(just(Token::LeftBracket), just(Token::RightBracket)),
);
let path_part = ident()
.then(predicate.clone().or_not())
.map(|(name, predicate)| PathPart { name, predicate });
object.define(
path_part
.separated_by(just(Token::Slash))
.at_least(1)
.collect::<Vec<_>>()
.map(|parts| ObjectPath { parts }),
);
identified.define(
ident()
.then(predicate.clone().or_not())
.then(just(Token::Slash).ignore_then(object.clone()).or_not())
.map(|((root, predicate), path)| IdentifiedPath {
root,
predicate,
path,
}),
);
(identified, predicate, standard)
}
fn terminology_fn<'a>() -> impl Parser<'a, &'a [Token], TerminologyFunction, Err<'a>> + Clone {
let string = select! { Token::String(s) => unquote(&s) };
just(Token::Terminology)
.ignore_then(
string
.then_ignore(just(Token::Comma))
.then(string)
.then_ignore(just(Token::Comma))
.then(string)
.delimited_by(just(Token::LeftParen), just(Token::RightParen)),
)
.map(|((operation, arg2), arg3)| TerminologyFunction {
operation,
arg2,
arg3,
})
}
fn function_parsers<'a>(
identified: impl Parser<'a, &'a [Token], IdentifiedPath, Err<'a>> + Clone + 'a,
terminal: impl Parser<'a, &'a [Token], Terminal, Err<'a>> + Clone + 'a,
) -> (
impl Parser<'a, &'a [Token], FunctionCall, Err<'a>> + Clone,
impl Parser<'a, &'a [Token], AggregateCall, Err<'a>> + Clone,
) {
let named = ident()
.or(just(Token::Contains).to("contains".to_owned()))
.then(
terminal
.separated_by(just(Token::Comma))
.collect::<Vec<_>>()
.delimited_by(just(Token::LeftParen), just(Token::RightParen)),
)
.map(|(name, args)| FunctionCall::Named { name, args });
let function = terminology_fn().map(FunctionCall::Terminology).or(named);
let count = just(Token::Count).ignore_then(
just(Token::Distinct)
.or_not()
.then(identified.clone())
.map(|(d, p)| AggregateCall::Count {
distinct: d.is_some(),
path: Some(p),
})
.or(just(Token::Asterisk).map(|_| AggregateCall::Count {
distinct: false,
path: None,
}))
.delimited_by(just(Token::LeftParen), just(Token::RightParen)),
);
let stat_name = select! {
Token::Min => StatFunc::Min,
Token::Max => StatFunc::Max,
Token::Sum => StatFunc::Sum,
Token::Avg => StatFunc::Avg,
};
let stat = stat_name
.then(
identified
.clone()
.delimited_by(just(Token::LeftParen), just(Token::RightParen)),
)
.map(|(func, path)| AggregateCall::Stat { func, path });
(function, count.or(stat))
}
#[expect(
clippy::too_many_lines,
reason = "one combinator builder for the whole AqlParser.g4 grammar; the sub-parsers are mutually referential, so splitting it would only move the wiring"
)]
fn query<'a>() -> impl Parser<'a, &'a [Token], SelectQuery, Err<'a>> {
let (identified, predicate, standard) = path_parsers();
let mut terminal = Recursive::declare();
let (function, aggregate) = function_parsers(identified.clone(), terminal.clone());
terminal.define(
primitive()
.map(Terminal::Primitive)
.or(parameter().map(Terminal::Parameter))
.or(function.clone().map(Terminal::Function))
.or(identified.clone().map(Terminal::Path)),
);
let column = aggregate
.clone()
.map(ColumnExpr::Aggregate)
.or(function.clone().map(ColumnExpr::Function))
.or(primitive().map(ColumnExpr::Primitive))
.or(identified.clone().map(ColumnExpr::Path));
let select_expr = column
.then(just(Token::As).ignore_then(ident()).or_not())
.map(|(column, alias)| SelectExpr { column, alias });
let top_count = select! { Token::Integer(s) => s }
.try_map(|s: String, span| s.parse::<i64>().ok().ok_or_else(|| Simple::new(None, span)));
let top = just(Token::Top)
.ignore_then(top_count)
.then(
select! {
Token::Forward => TopDirection::Forward,
Token::Backward => TopDirection::Backward,
}
.or_not(),
)
.map(|(count, direction)| Top { count, direction });
let select_clause = just(Token::Select)
.ignore_then(just(Token::Distinct).or_not())
.then(top.or_not())
.then(
select_expr
.separated_by(just(Token::Comma))
.at_least(1)
.collect::<Vec<_>>(),
)
.map(|((distinct, top), columns)| SelectClause {
distinct: distinct.is_some(),
top,
columns,
});
let version_predicate = select! {
Token::LatestVersion => VersionPredicate::Latest,
Token::AllVersions => VersionPredicate::All,
}
.or(standard.map(|s| VersionPredicate::Standard(Box::new(s))));
let class_operand = just(Token::Version)
.ignore_then(ident().or_not())
.then(
version_predicate
.delimited_by(just(Token::LeftBracket), just(Token::RightBracket))
.or_not(),
)
.map(|(variable, predicate)| ClassExprOperand::Version {
variable,
predicate,
})
.or(ident().then(ident().or_not()).then(predicate.or_not()).map(
|((rm_type, variable), predicate)| ClassExprOperand::Class {
rm_type,
variable,
predicate,
},
));
let contains = recursive(|contains| {
let atom = class_operand
.then(
just(Token::Not)
.or_not()
.then_ignore(just(Token::Contains))
.then(contains.clone())
.map(|(neg, expr)| {
Box::new(ContainsConstraint {
negated: neg.is_some(),
expr,
})
})
.or_not(),
)
.map(|(operand, contains)| ContainsExpr::Contained { operand, contains })
.or(contains
.clone()
.delimited_by(just(Token::LeftParen), just(Token::RightParen)));
let and = atom.clone().foldl(
just(Token::And).ignore_then(atom.clone()).repeated(),
|l, r| ContainsExpr::And(Box::new(l), Box::new(r)),
);
and.clone()
.foldl(just(Token::Or).ignore_then(and).repeated(), |l, r| {
ContainsExpr::Or(Box::new(l), Box::new(r))
})
});
let like_operand = select! { Token::String(s) => LikeOperand::String(unquote(&s)) }
.or(parameter().map(LikeOperand::Parameter));
let value_item = primitive()
.map(ValueListItem::Primitive)
.or(parameter().map(ValueListItem::Parameter))
.or(terminology_fn().map(ValueListItem::Terminology));
let matches_operand = terminology_fn()
.map(MatchesOperand::Terminology)
.or(select! { Token::Uri(u) => MatchesOperand::Uri(u) }
.delimited_by(just(Token::LeftCurly), just(Token::RightCurly)))
.or(value_item
.separated_by(just(Token::Comma))
.at_least(1)
.collect::<Vec<_>>()
.delimited_by(just(Token::LeftCurly), just(Token::RightCurly))
.map(MatchesOperand::ValueList));
let comparison = select! { Token::Comparison(op) => op };
let identified_expr = just(Token::Exists)
.ignore_then(identified.clone())
.map(IdentifiedExpr::Exists)
.or(function
.clone()
.then(comparison)
.then(terminal.clone())
.map(|((f, op), rhs)| IdentifiedExpr::Compare {
lhs: CompareOperand::Function(f),
op,
rhs,
}))
.or(identified
.clone()
.then(just(Token::Like).ignore_then(like_operand))
.map(|(path, operand)| IdentifiedExpr::Like { path, operand }))
.or(identified
.clone()
.then(just(Token::Matches).ignore_then(matches_operand))
.map(|(path, operand)| IdentifiedExpr::Matches { path, operand }))
.or(identified
.clone()
.then(comparison)
.then(terminal.clone())
.map(|((path, op), rhs)| IdentifiedExpr::Compare {
lhs: CompareOperand::Path(path),
op,
rhs,
}));
let where_expr = recursive(|where_expr| {
let atom = identified_expr.map(WhereExpr::Identified).or(where_expr
.clone()
.delimited_by(just(Token::LeftParen), just(Token::RightParen)));
let unary = just(Token::Not)
.repeated()
.foldr(atom, |_not, e| WhereExpr::Not(Box::new(e)));
let and = unary
.clone()
.foldl(just(Token::And).ignore_then(unary).repeated(), |l, r| {
WhereExpr::And(Box::new(l), Box::new(r))
});
and.clone()
.foldl(just(Token::Or).ignore_then(and).repeated(), |l, r| {
WhereExpr::Or(Box::new(l), Box::new(r))
})
});
let order_by_expr = identified
.clone()
.then(
select! {
Token::Descending => SortOrder::Descending,
Token::Desc => SortOrder::Descending,
Token::Ascending => SortOrder::Ascending,
Token::Asc => SortOrder::Ascending,
}
.or_not(),
)
.map(|(path, order)| OrderByExpr { path, order });
let int = select! { Token::Integer(s) => s }
.try_map(|s: String, span| s.parse::<i64>().ok().ok_or_else(|| Simple::new(None, span)));
let limit = just(Token::Limit)
.ignore_then(int)
.then(just(Token::Offset).ignore_then(int).or_not())
.map(|(limit, offset)| Limit { limit, offset });
select_clause
.then(just(Token::From).ignore_then(contains))
.then(just(Token::Where).ignore_then(where_expr).or_not())
.then(
just(Token::Order)
.ignore_then(just(Token::By))
.ignore_then(
order_by_expr
.separated_by(just(Token::Comma))
.at_least(1)
.collect::<Vec<_>>(),
)
.or_not(),
)
.then(limit.or_not())
.then_ignore(just(Token::DoubleDash).or_not())
.map(
|((((select, from), where_), order_by), limit)| SelectQuery {
select,
from,
where_,
order_by: order_by.unwrap_or_default(),
limit,
},
)
.then_ignore(end())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minimal_select_from() {
let q = parse_str("SELECT c FROM COMPOSITION c").expect("parse");
assert_eq!(q.select.columns.len(), 1);
assert!(matches!(
&q.from,
ContainsExpr::Contained {
operand: ClassExprOperand::Class { rm_type, variable, .. },
..
} if rm_type == "COMPOSITION" && variable.as_deref() == Some("c")
));
}
#[test]
fn select_path_where_compare() {
let q = parse_str("SELECT e/ehr_id/value FROM EHR e WHERE e/ehr_id/value = $id")
.expect("parse");
assert!(matches!(q.select.columns[0].column, ColumnExpr::Path(_)));
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Compare { .. }))
));
}
#[test]
fn contains_with_archetype_predicate() {
let q = parse_str(
"SELECT o FROM EHR e CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v1]",
)
.expect("parse");
match &q.from {
ContainsExpr::Contained {
contains: Some(c), ..
} => {
assert!(!c.negated);
assert!(matches!(c.expr, ContainsExpr::Contained { .. }));
}
other => panic!("expected contains, got {other:?}"),
}
}
#[test]
fn distinct_alias_orderby_limit() {
let q = parse_str(
"SELECT DISTINCT c/name/value AS n FROM COMPOSITION c ORDER BY c/name/value DESC LIMIT 10 OFFSET 5",
)
.expect("parse");
assert!(q.select.distinct);
assert_eq!(q.select.columns[0].alias.as_deref(), Some("n"));
assert_eq!(q.order_by.len(), 1);
assert_eq!(q.order_by[0].order, Some(SortOrder::Descending));
assert_eq!(
q.limit,
Some(Limit {
limit: 10,
offset: Some(5)
})
);
}
#[test]
fn timewindow_is_rejected() {
for q in [
"SELECT e/ehr_id/value FROM EHR e TIMEWINDOW PT12H/2019-10-24",
"SELECT c FROM COMPOSITION c TIMEWINDOW PT12H/2019-10-24",
"SELECT c FROM COMPOSITION c TIMEWINDOW PT12H",
] {
assert!(parse_str(q).is_err(), "TIMEWINDOW must not parse: {q}");
}
}
#[test]
fn aggregate_count() {
let q = parse_str("SELECT COUNT(*) FROM COMPOSITION c").expect("parse");
assert!(matches!(
q.select.columns[0].column,
ColumnExpr::Aggregate(AggregateCall::Count { path: None, .. })
));
}
#[test]
fn where_boolean_precedence() {
let q = parse_str("SELECT c FROM COMPOSITION c WHERE c/x = 1 AND c/y = 2 OR c/z = 3")
.expect("parse");
assert!(matches!(q.where_, Some(WhereExpr::Or(_, _))));
}
#[test]
fn not_binds_tighter_than_and() {
let q = parse_str("SELECT c FROM COMPOSITION c WHERE NOT EXISTS c/x AND EXISTS c/y")
.expect("parse");
match q.where_ {
Some(WhereExpr::And(l, _)) => assert!(matches!(*l, WhereExpr::Not(_))),
other => panic!("expected And(Not(..), ..), got {other:?}"),
}
}
#[test]
fn node_predicate_boolean_tree() {
let q = parse_str("SELECT o FROM OBSERVATION o[at0001 AND value/magnitude > 5]")
.expect("parse");
match &q.from {
ContainsExpr::Contained {
operand:
ClassExprOperand::Class {
predicate: Some(PathPredicate::Node(n)),
..
},
..
} => assert!(matches!(**n, NodePredicate::And(_, _))),
other => panic!("expected node AND predicate, got {other:?}"),
}
}
#[test]
fn contained_regex_predicate() {
let q = parse_str("SELECT o FROM OBSERVATION o[name/value MATCHES {/blood.*/}]")
.expect("parse");
assert!(matches!(
&q.from,
ContainsExpr::Contained {
operand: ClassExprOperand::Class {
predicate: Some(PathPredicate::Node(_)),
..
},
..
}
));
}
#[test]
fn trailing_tokens_are_rejected() {
assert!(parse_str("SELECT c FROM COMPOSITION c EXTRA").is_err());
}
#[test]
fn version_latest_and_all_predicates() {
for (src, want) in [
(
"SELECT c FROM VERSION v[LATEST_VERSION]",
VersionPredicate::Latest,
),
(
"SELECT c FROM VERSION v[ALL_VERSIONS]",
VersionPredicate::All,
),
] {
let q = parse_str(src).expect("parse");
match q.from {
ContainsExpr::Contained {
operand:
ClassExprOperand::Version {
predicate: Some(p), ..
},
..
} => assert_eq!(p, want),
other => panic!("expected version operand, got {other:?}"),
}
}
}
#[test]
fn version_standard_predicate_parses() {
let q = parse_str("SELECT c FROM VERSION v[commit_audit/time_committed > '2020-01-01']")
.expect("parse");
match q.from {
ContainsExpr::Contained {
operand:
ClassExprOperand::Version {
variable,
predicate: Some(VersionPredicate::Standard(s)),
},
..
} => {
assert_eq!(variable.as_deref(), Some("v"));
assert_eq!(s.op, crate::lexer::CompOp::Gt);
}
other => panic!("expected VERSION standard predicate, got {other:?}"),
}
}
#[test]
fn version_standard_predicate_with_parameter() {
let q = parse_str("SELECT c FROM VERSION v[uid/value = $vid]").expect("parse");
assert!(matches!(
q.from,
ContainsExpr::Contained {
operand: ClassExprOperand::Version {
predicate: Some(VersionPredicate::Standard(_)),
..
},
..
}
));
}
#[test]
fn integer_overflow_is_a_parse_error() {
assert!(
parse_str("SELECT c FROM COMPOSITION c WHERE c/x = 99999999999999999999999").is_err()
);
}
#[test]
fn in_range_integer_still_parses() {
let q = parse_str("SELECT c FROM COMPOSITION c WHERE c/x = 9223372036854775807")
.expect("parse i64::MAX");
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Compare {
rhs: Terminal::Primitive(Primitive::Integer(9_223_372_036_854_775_807)),
..
}))
));
}
#[test]
fn limit_and_top_overflow_are_parse_errors() {
assert!(parse_str("SELECT c FROM COMPOSITION c LIMIT 99999999999999999999999").is_err());
assert!(parse_str("SELECT TOP 99999999999999999999999 c FROM COMPOSITION c").is_err());
}
#[test]
fn real_overflow_is_a_parse_error() {
assert!(parse_str("SELECT c FROM COMPOSITION c WHERE c/x = 1.0e999").is_err());
}
#[test]
fn double_unary_minus_parses() {
let q = parse_str("SELECT c FROM COMPOSITION c WHERE c/x = - - 5").expect("parse");
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Compare {
rhs: Terminal::Primitive(Primitive::Integer(5)),
..
}))
));
}
#[test]
fn single_negative_numeric_parses() {
let q = parse_str("SELECT c FROM COMPOSITION c WHERE c/x = -5").expect("parse");
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Compare {
rhs: Terminal::Primitive(Primitive::Integer(-5)),
..
}))
));
}
#[test]
fn string_escapes_are_decoded() {
let q = parse_str(r"SELECT c FROM COMPOSITION c WHERE c/x = 'a\nb\t\\c'").expect("parse");
match q.where_ {
Some(WhereExpr::Identified(IdentifiedExpr::Compare {
rhs: Terminal::Primitive(Primitive::String(s)),
..
})) => assert_eq!(s, "a\nb\t\\c"),
other => panic!("expected decoded string, got {other:?}"),
}
}
#[test]
fn string_unicode_and_quote_escapes_are_decoded() {
assert_eq!(unquote(r"'A\''"), "A'");
assert_eq!(unquote(r#""a\"b""#), "a\"b");
assert_eq!(unquote(r"'\u0041\u00e9'"), "Aé");
assert_eq!(unquote(r"'\101'"), "A");
assert_eq!(unquote(r"'x\z'"), r"x\z");
assert_eq!(unquote(r"'\u12'"), r"\u12");
}
#[test]
fn bare_standard_predicate_classifies_as_standard() {
let q = parse_str("SELECT e FROM EHR e[ehr_id/value = '123']").expect("parse");
match q.from {
ContainsExpr::Contained {
operand:
ClassExprOperand::Class {
predicate: Some(PathPredicate::Standard(s)),
..
},
..
} => assert_eq!(s.op, crate::lexer::CompOp::Eq),
other => panic!("expected PathPredicate::Standard, got {other:?}"),
}
}
#[test]
fn boolean_predicate_stays_node() {
let q = parse_str("SELECT o FROM OBSERVATION o[at0001 AND value/magnitude > 5]")
.expect("parse");
assert!(matches!(
q.from,
ContainsExpr::Contained {
operand: ClassExprOperand::Class {
predicate: Some(PathPredicate::Node(_)),
..
},
..
}
));
}
#[test]
fn scientific_integer_stays_integer() {
let q = parse_str("SELECT c FROM COMPOSITION c WHERE c/x = 1e3").expect("parse");
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Compare {
rhs: Terminal::Primitive(Primitive::Integer(1000)),
..
}))
));
}
#[test]
fn scientific_real_is_real() {
let q = parse_str("SELECT c FROM COMPOSITION c WHERE c/x = 1.5e2").expect("parse");
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Compare {
rhs: Terminal::Primitive(Primitive::Real(_)),
..
}))
));
}
#[test]
fn like_operand_parses() {
let q = parse_str("SELECT c FROM COMPOSITION c WHERE c/name/value LIKE 'blood%'")
.expect("parse");
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Like { .. }))
));
let q =
parse_str("SELECT c FROM COMPOSITION c WHERE c/name/value LIKE $pat").expect("parse");
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Like {
operand: LikeOperand::Parameter(_),
..
}))
));
}
#[test]
fn matches_uri_and_terminology_operands() {
let q = parse_str(
"SELECT o FROM OBSERVATION o WHERE o/value MATCHES {http://openehr.org/vs/x}",
)
.expect("parse");
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Matches {
operand: MatchesOperand::Uri(_),
..
}))
));
let q = parse_str(
"SELECT o FROM OBSERVATION o WHERE o/value/defining_code \
MATCHES terminology('expand', 'snomed', 'x')",
)
.expect("parse");
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Matches {
operand: MatchesOperand::Terminology(_),
..
}))
));
}
#[test]
fn aggregate_stats_parse() {
for (src, want) in [
(
"SELECT MIN(o/value/magnitude) FROM OBSERVATION o",
StatFunc::Min,
),
(
"SELECT MAX(o/value/magnitude) FROM OBSERVATION o",
StatFunc::Max,
),
(
"SELECT SUM(o/value/magnitude) FROM OBSERVATION o",
StatFunc::Sum,
),
(
"SELECT AVG(o/value/magnitude) FROM OBSERVATION o",
StatFunc::Avg,
),
] {
let q = parse_str(src).expect("parse");
match &q.select.columns[0].column {
ColumnExpr::Aggregate(AggregateCall::Stat { func, .. }) => assert_eq!(*func, want),
other => panic!("expected stat aggregate, got {other:?}"),
}
}
}
#[test]
fn top_forward_backward_parses() {
let q = parse_str("SELECT TOP 5 BACKWARD c FROM COMPOSITION c").expect("parse");
let top = q.select.top.expect("top");
assert_eq!(top.count, 5);
assert_eq!(top.direction, Some(TopDirection::Backward));
}
#[test]
fn parameter_predicates_parse() {
assert!(parse_str("SELECT o FROM OBSERVATION o[$archetypeId]").is_ok());
let q = parse_str("SELECT o FROM OBSERVATION o[at0002, $name]").expect("parse");
assert!(matches!(
q.from,
ContainsExpr::Contained {
operand: ClassExprOperand::Class {
predicate: Some(PathPredicate::Node(_)),
..
},
..
}
));
}
#[test]
fn hyphenated_term_code_in_predicate_parses() {
let q =
parse_str("SELECT o FROM OBSERVATION o[at0001, SNOMED-CT::1234|x|]").expect("parse");
assert!(matches!(
q.from,
ContainsExpr::Contained {
operand: ClassExprOperand::Class {
predicate: Some(PathPredicate::Node(_)),
..
},
..
}
));
}
#[test]
fn line_comment_in_query_parses() {
let q = parse_str("SELECT c -- pick the composition\nFROM COMPOSITION c").expect("parse");
assert_eq!(q.select.columns.len(), 1);
}
#[test]
fn nested_contains_and_matches_valueset() {
let q = parse_str(
"SELECT a/value FROM EHR e CONTAINS COMPOSITION c CONTAINS OBSERVATION o \
WHERE o/value/defining_code MATCHES {'at0001', 'at0002'}",
)
.expect("parse");
assert!(matches!(
&q.from,
ContainsExpr::Contained {
contains: Some(_),
..
}
));
assert!(matches!(
q.where_,
Some(WhereExpr::Identified(IdentifiedExpr::Matches { .. }))
));
}
}