Skip to main content

surrealguard_syntax/lower/
expr.rs

1//! Expression and idiom lowering.
2//!
3//! CST shapes this encodes (verified against the grammar, see
4//! `examples/dump_cst.rs` for the inspection tool):
5//!
6//! - `Number` wraps an `Int`/`Float`/`Decimal` child; a leading minus is part
7//!   of the `Number` text, not a prefix expression.
8//! - `String` has no children; `d'…'`/`u'…'`/`r'…'` prefixes select
9//!   datetime/uuid/regex literals, normalized here.
10//! - `None` covers both `NONE` and `null`, distinguished by text.
11//! - `Path` is `[start, subscript/lookup/filter...]` where `start` is an
12//!   `Ident` (row field) or a value node like `VariableName` (`$user.name`).
13//! - `Subscript` carries `.field`, `.{destructure}`, or `.method()`.
14//! - `Filter` carries `[index-expr]` or `[WHERE …]` — one node, two meanings.
15//! - `Lookup` is one graph step: direction token plus a bare edge `Ident` or
16//!   a `LookupSelection` (`(edge WHERE …)` with `GraphPredicate` targets).
17
18use tree_sitter::Node;
19
20use super::{node_range, partial};
21use crate::ast::{
22    BinaryOp, Block, Call, Closure, Expr, GraphDir, GraphStep, Idiom, IdiomPart, Literal, PrefixOp,
23    Spanned, TypeExpr,
24};
25
26/// Lowers an expression-position CST node.
27pub fn lower_expr(node: Node<'_>, text: &str) -> Spanned<Expr> {
28    Lowerer { text }.expr(node)
29}
30
31/// Lowers a type-position node (`Type`, `TypeName`, `ParameterizedType`,
32/// `UnionType`, `LiteralType`) — used by DEFINE FIELD/cast lowering and
33/// schema extraction.
34pub fn lower_type_expr(node: Node<'_>, text: &str) -> Spanned<TypeExpr> {
35    Lowerer { text }.type_expr(node)
36}
37
38/// Lowers a `Block` node — used by statement lowering for IF/FOR bodies and
39/// statement-position blocks.
40pub fn lower_block_node(node: Node<'_>, text: &str) -> Block {
41    Lowerer { text }.block(node)
42}
43
44/// Lowers a path-position node (`Path`, `Idiom`, or bare `Ident`) to an
45/// [`Idiom`] — used by statement lowering for clause paths.
46pub fn lower_idiom_node(node: Node<'_>, text: &str) -> Idiom {
47    let lowerer = Lowerer { text };
48    match node.kind() {
49        "Ident" => Idiom {
50            parts: vec![lowerer.spanned(node, IdiomPart::Field(text[node.byte_range()].into()))],
51        },
52        _ => lowerer.idiom(node),
53    }
54}
55
56struct Lowerer<'a> {
57    text: &'a str,
58}
59
60impl Lowerer<'_> {
61    fn node_text(&self, node: Node<'_>) -> &str {
62        &self.text[node.byte_range()]
63    }
64
65    fn spanned<T>(&self, node: Node<'_>, value: T) -> Spanned<T> {
66        Spanned::new(value, node_range(node))
67    }
68
69    fn expr(&self, node: Node<'_>) -> Spanned<Expr> {
70        if node.is_error() || node.is_missing() {
71            return self.spanned(node, Expr::Partial(partial(node)));
72        }
73
74        let expr = match node.kind() {
75            // Wrappers the grammar puts around single expressions.
76            "Predicate" | "Fields" => match single_named_child(node) {
77                Some(child) => return self.expr(child),
78                None => Expr::Partial(partial(node)),
79            },
80            "Number" => self.number_literal(node),
81            "String" => self.string_literal(node),
82            "Bool" => Expr::Literal(Literal::Bool(
83                self.node_text(node).eq_ignore_ascii_case("true"),
84            )),
85            "None" => {
86                if self.node_text(node).eq_ignore_ascii_case("null") {
87                    Expr::Literal(Literal::Null)
88                } else {
89                    Expr::Literal(Literal::None)
90                }
91            }
92            "Duration" => Expr::Literal(Literal::Duration(self.node_text(node).to_string())),
93            "Regex" => Expr::Literal(Literal::Regex(
94                self.node_text(node).trim_matches('/').to_string(),
95            )),
96            "VariableName" => Expr::Param(self.param_name(node)),
97            "RecordId" => self.record_id(node),
98            "Array" => Expr::Array(
99                named_children(node)
100                    .into_iter()
101                    .map(|child| self.expr(child))
102                    .collect(),
103            ),
104            "Object" => self.object(node),
105            "BinaryExpression" => self.binary(node),
106            "PrefixExpression" => self.prefix(node),
107            "FunctionCall" => Expr::Call(self.call(node)),
108            "TypeCast" => self.cast(node),
109            "SubQuery" => self.subquery(node),
110            // A bare responding statement in value position (`LET $x = SELECT
111            // …`, `RETURN CREATE …`, `RETURN IF c { a } ELSE { b }`) is a
112            // subquery without the parentheses: lower it to the same
113            // `Expr::Subquery` so its response shape types the surrounding
114            // expression (an IF-as-value unions its branch values).
115            "SelectStatement" | "CreateStatement" | "UpdateStatement" | "UpsertStatement"
116            | "DeleteStatement" | "InsertStatement" | "RelateStatement" | "IfElseStatement" => {
117                Expr::Subquery(Box::new(super::statement::lower_statement(node, self.text)))
118            }
119            "Block" => Expr::Block(self.block(node)),
120            "Closure" => self.closure(node),
121            "Path" | "Idiom" => Expr::Idiom(self.idiom(node)),
122            "Ident" => Expr::Idiom(Idiom {
123                parts: vec![self.spanned(node, IdiomPart::Field(self.node_text(node).into()))],
124            }),
125            _ => Expr::Partial(partial(node)),
126        };
127        self.spanned(node, expr)
128    }
129
130    fn number_literal(&self, node: Node<'_>) -> Expr {
131        // The Int/Float/Decimal child classifies; the Number node's own text
132        // carries the sign.
133        let text = self.node_text(node);
134        let literal = match named_children(node).first().map(tree_sitter::Node::kind) {
135            Some("Float") => text
136                .parse::<f64>()
137                .map_or(Literal::Float(0.0), Literal::Float),
138            Some("Decimal") => Literal::Decimal,
139            _ => text.parse::<i64>().map_or(Literal::Int(0), Literal::Int),
140        };
141        Expr::Literal(literal)
142    }
143
144    fn string_literal(&self, node: Node<'_>) -> Expr {
145        let text = self.node_text(node);
146        let bytes = text.as_bytes();
147        let prefixed = bytes.len() > 2 && matches!(bytes.get(1), Some(b'\'' | b'"'));
148        let inner = || text[1..].trim_matches(['\'', '"']).to_string();
149        let literal = match bytes.first().map(u8::to_ascii_lowercase) {
150            Some(b'd') if prefixed => Literal::Datetime(inner()),
151            Some(b'u') if prefixed => Literal::Uuid(inner()),
152            Some(b'r') if prefixed => Literal::Regex(inner()),
153            _ => {
154                let content = text
155                    .trim_start_matches(['d', 'u', 'r'])
156                    .trim_matches(['\'', '"']);
157                Literal::String(content.to_string())
158            }
159        };
160        Expr::Literal(literal)
161    }
162
163    fn record_id(&self, node: Node<'_>) -> Expr {
164        let table = named_children(node)
165            .into_iter()
166            .find(|c| c.kind() == "RecordTbIdent");
167        let id = named_children(node)
168            .into_iter()
169            .find(|c| !matches!(c.kind(), "RecordTbIdent" | "Colon"));
170        match (table, id) {
171            (Some(table), Some(id)) => Expr::RecordId {
172                table: self.spanned(table, self.node_text(table).to_string()),
173                id: node_range(id),
174                range: id.kind() == "RecordIdRange",
175            },
176            _ => Expr::Partial(partial(node)),
177        }
178    }
179
180    fn param_name(&self, node: Node<'_>) -> String {
181        self.node_text(node).trim_start_matches('$').to_string()
182    }
183
184    fn object(&self, node: Node<'_>) -> Expr {
185        let mut fields = Vec::new();
186        collect_object_properties(node, &mut |property| {
187            let Some(key_node) = first_descendant_of_kind(property, "ObjectKey") else {
188                return;
189            };
190            let key_leaf = single_named_child(key_node).unwrap_or(key_node);
191            let key = self.spanned(
192                key_leaf,
193                self.node_text(key_leaf)
194                    .trim_matches(['`', '"', '\''])
195                    .to_string(),
196            );
197            let value = named_children(property)
198                .into_iter()
199                .rfind(|child| child.kind() != "ObjectKey");
200            let value = match value {
201                Some(value_node) => self.expr(value_node),
202                None => self.spanned(property, Expr::Partial(partial(property))),
203            };
204            fields.push((key, value));
205        });
206        Expr::Object(fields)
207    }
208
209    fn binary(&self, node: Node<'_>) -> Expr {
210        let children = named_children(node);
211        let Some(op_index) = children.iter().position(|c| c.kind() == "Operator") else {
212            return Expr::Partial(partial(node));
213        };
214        let lhs = children[..op_index]
215            .iter()
216            .rev()
217            .find(|c| c.kind() != "Operator");
218        let rhs = children[op_index + 1..]
219            .iter()
220            .find(|c| c.kind() != "Operator");
221        let (Some(&lhs), Some(&rhs)) = (lhs, rhs) else {
222            return Expr::Partial(partial(node));
223        };
224        let op_node = children[op_index];
225
226        Expr::Binary {
227            lhs: Box::new(self.expr(lhs)),
228            op: self.spanned(op_node, binary_op(self.node_text(op_node))),
229            rhs: Box::new(self.expr(rhs)),
230        }
231    }
232
233    fn prefix(&self, node: Node<'_>) -> Expr {
234        let children = named_children(node);
235        let op_node = children.iter().find(|c| c.kind() == "Operator");
236        let operand = children.iter().find(|c| c.kind() != "Operator");
237        let (Some(&op_node), Some(&operand)) = (op_node, operand) else {
238            return Expr::Partial(partial(node));
239        };
240
241        Expr::Prefix {
242            op: self.spanned(op_node, prefix_op(self.node_text(op_node))),
243            expr: Box::new(self.expr(operand)),
244        }
245    }
246
247    fn call(&self, node: Node<'_>) -> Call {
248        let name = first_child_of_kind(node, "FunctionName");
249        let path = match name {
250            Some(name) => self.spanned(name, normalize_function_path(self.node_text(name))),
251            None => Spanned::new(String::new(), node_range(node)),
252        };
253        let args = first_child_of_kind(node, "ArgumentList")
254            .map(|list| {
255                named_children(list)
256                    .into_iter()
257                    .map(|arg| self.expr(arg))
258                    .collect()
259            })
260            .unwrap_or_default();
261        Call { path, args }
262    }
263
264    fn cast(&self, node: Node<'_>) -> Expr {
265        let children = named_children(node);
266        let ty = children
267            .iter()
268            .find(|c| matches!(c.kind(), "TypeName" | "Type"));
269        let value = children
270            .iter()
271            .find(|c| !matches!(c.kind(), "TypeName" | "Type"));
272        let (Some(&ty), Some(&value)) = (ty, value) else {
273            return Expr::Partial(partial(node));
274        };
275
276        Expr::Cast {
277            ty: self.type_expr(ty),
278            expr: Box::new(self.expr(value)),
279        }
280    }
281
282    /// Structural type lowering: names, parameterized types (`array<string>`,
283    /// with `option<T>` normalized to `Optional`), unions, and literal types.
284    fn type_expr(&self, node: Node<'_>) -> Spanned<TypeExpr> {
285        let ty = match node.kind() {
286            "TypeName" => TypeExpr::Name(self.spanned(node, self.node_text(node).to_string())),
287            "Type" => match single_named_child(node) {
288                Some(child) => return self.type_expr(child),
289                None => TypeExpr::Partial(partial(node)),
290            },
291            "ParameterizedType" => {
292                let children = named_children(node);
293                let Some((name_node, args)) = children.split_first() else {
294                    return self.spanned(node, TypeExpr::Partial(partial(node)));
295                };
296                let name = self.spanned(*name_node, self.node_text(*name_node).to_string());
297                let args: Vec<_> = args.iter().map(|arg| self.type_expr(*arg)).collect();
298                // `option<T>` is sugar for an optional type.
299                if name.node.eq_ignore_ascii_case("option") && args.len() == 1 {
300                    TypeExpr::Optional(Box::new(
301                        args.into_iter().next().expect("one option argument"),
302                    ))
303                } else {
304                    TypeExpr::Parameterized { name, args }
305                }
306            }
307            "UnionType" => {
308                let variants: Vec<_> = named_children(node)
309                    .into_iter()
310                    .filter(|child| child.kind() != "Pipe")
311                    .map(|child| self.type_expr(child))
312                    .collect();
313                TypeExpr::Union(variants)
314            }
315            // `{ name: string, ... }` — an object type. The grammar nests it
316            // under `LiteralType`, but the field TYPE clause can also hand it
317            // to us directly, so handle both entry points.
318            "ObjectType" => self.object_type(node),
319            "LiteralType" => match single_named_child(node) {
320                Some(value) if value.kind() == "ObjectType" => return self.type_expr(value),
321                Some(value) => match self.expr(value).node {
322                    Expr::Literal(literal) => TypeExpr::Literal(literal),
323                    _ => TypeExpr::Partial(partial(node)),
324                },
325                None => TypeExpr::Partial(partial(node)),
326            },
327            _ => TypeExpr::Partial(partial(node)),
328        };
329        self.spanned(node, ty)
330    }
331
332    /// Lowers an `ObjectType` node (`{ key: T, ... }`) to a structural
333    /// [`TypeExpr::Object`], recursing into each property's declared type.
334    fn object_type(&self, node: Node<'_>) -> TypeExpr {
335        let mut properties = Vec::new();
336        collect_object_type_properties(node, &mut |property| {
337            let Some(key_node) = first_descendant_of_kind(property, "ObjectKey") else {
338                return;
339            };
340            let key_leaf = single_named_child(key_node).unwrap_or(key_node);
341            let key = self.spanned(
342                key_leaf,
343                self.node_text(key_leaf)
344                    .trim_matches(['`', '"', '\''])
345                    .to_string(),
346            );
347            let value = named_children(property)
348                .into_iter()
349                .find(|child| !matches!(child.kind(), "ObjectKey" | "Colon"));
350            let value = match value {
351                Some(ty_node) => self.type_expr(ty_node),
352                None => self.spanned(property, TypeExpr::Partial(partial(property))),
353            };
354            properties.push((key, value));
355        });
356        TypeExpr::Object(properties)
357    }
358
359    /// `( … )` — either grouping parentheses around a value, or a genuine
360    /// subquery around a statement.
361    ///
362    /// Parentheses are a semantic no-op in SurrealQL: `(email + 1)` **is**
363    /// `email + 1`, so it must lower to the inner expression and be inferred,
364    /// checked and narrowed identically. Wrapping it in an `Expr::Subquery`
365    /// instead put an opaque node in front of every consumer that matches on
366    /// expression shape, silently disabling narrowing, field validation and
367    /// every expression-level diagnostic behind a pair of parentheses.
368    /// Grouping still holds: the CST already nests `(a + b) * c` as
369    /// `Binary(Binary(a,+,b), *, c)`, and the outer span (applied by
370    /// [`Self::expr`]) keeps covering the parentheses for diagnostics.
371    ///
372    /// Only a *statement* inside the parentheses (`(SELECT …)`, `({ … })`,
373    /// `(THROW …)`) is a real subquery value.
374    fn subquery(&self, node: Node<'_>) -> Expr {
375        let Some(inner) = subquery_content(node) else {
376            return Expr::Partial(partial(node));
377        };
378        if !is_statement_kind(inner.kind()) {
379            return self.expr(inner).node;
380        }
381        Expr::Subquery(Box::new(super::statement::lower_statement(
382            inner, self.text,
383        )))
384    }
385
386    fn block(&self, node: Node<'_>) -> Block {
387        let mut statements = Vec::new();
388        for child in named_children(node) {
389            if matches!(child.kind(), "BraceOpen" | "BraceClose") {
390                continue;
391            }
392            // Recover valid statements around a broken sibling: tree-sitter may
393            // nest the statement following a syntax error inside the broken
394            // one's subtree, so a plain per-child lowering would drop it.
395            super::statement::recover_statement(child, self.text, &mut statements);
396        }
397        Block { statements }
398    }
399
400    fn closure(&self, node: Node<'_>) -> Expr {
401        let mut params = Vec::new();
402        let mut return_ty = None;
403        let mut body = None;
404        let mut saw_arrow = false;
405
406        for child in named_children(node) {
407            match child.kind() {
408                "Pipe" => {}
409                "LookupRight" => saw_arrow = true,
410                "ParamDefinition" => {
411                    let mut name = None;
412                    let mut ty = None;
413                    for part in named_children(child) {
414                        match part.kind() {
415                            "VariableName" => {
416                                name = Some(self.spanned(
417                                    part,
418                                    self.node_text(part).trim_start_matches('$').to_string(),
419                                ));
420                            }
421                            "Type" | "TypeName" | "ParameterizedType" | "UnionType"
422                            | "LiteralType" => ty = Some(self.type_expr(part)),
423                            _ => {}
424                        }
425                    }
426                    if let Some(name) = name {
427                        params.push((name, ty));
428                    }
429                }
430                "Type" | "TypeName" | "ParameterizedType" | "UnionType" | "LiteralType"
431                    if saw_arrow =>
432                {
433                    return_ty = Some(self.type_expr(child));
434                }
435                _ if body.is_none() => body = Some(self.expr(child)),
436                _ => {}
437            }
438        }
439
440        match body {
441            Some(body) => Expr::Closure(Closure {
442                params,
443                return_ty,
444                body: Box::new(body),
445            }),
446            None => Expr::Partial(partial(node)),
447        }
448    }
449
450    fn idiom(&self, node: Node<'_>) -> Idiom {
451        let mut parts: Vec<Spanned<IdiomPart>> = Vec::new();
452
453        for child in named_children(node) {
454            if child.is_error() || child.is_missing() {
455                parts.push(self.spanned(child, IdiomPart::Partial(partial(child))));
456                continue;
457            }
458            match child.kind() {
459                // `Path` nests later fields inside `Subscript`s, but `Idiom`
460                // nodes (FETCH/SPLIT/GROUP paths) list bare `Ident`s
461                // sequentially — a field is a field at any position.
462                "Ident" => {
463                    parts.push(
464                        self.spanned(child, IdiomPart::Field(self.node_text(child).to_string())),
465                    );
466                }
467                "Subscript" => self.subscript_parts(child, &mut parts),
468                "Lookup" => parts.push(self.spanned(child, self.graph_part(child))),
469                "Filter" => parts.push(self.spanned(child, self.filter_part(child))),
470                // `Idiom` nodes carry `[*]` as a bare `Any` child rather than
471                // the `Filter`/`Subscript` wrapper a `Path` uses — this is the
472                // shape a `DEFINE FIELD items[*].price` path takes. It is the
473                // same element step either way.
474                "Any" => parts.push(self.spanned(child, IdiomPart::All)),
475                // Any leading value node (`$user.name`, `fn().field`, ...).
476                _ if parts.is_empty() => {
477                    parts.push(self.spanned(child, IdiomPart::Start(Box::new(self.expr(child)))));
478                }
479                _ => parts.push(self.spanned(child, IdiomPart::Partial(partial(child)))),
480            }
481        }
482
483        Idiom { parts }
484    }
485
486    fn subscript_parts(&self, node: Node<'_>, parts: &mut Vec<Spanned<IdiomPart>>) {
487        for child in named_children(node) {
488            let part = match child.kind() {
489                "Ident" => IdiomPart::Field(self.node_text(child).to_string()),
490                "Destructure" => IdiomPart::Destructure(self.destructure_fields(child)),
491                "Recurse" => {
492                    // `{1..3}` bounded; `{..}` / `{1..}` unbounded above.
493                    let text = self.node_text(child);
494                    let bounded = text.rsplit("..").next().is_some_and(|tail| {
495                        tail.trim_end_matches(['}', ' '])
496                            .chars()
497                            .any(|c| c.is_ascii_digit())
498                    });
499                    IdiomPart::Recurse { bounded }
500                }
501                "IdiomFunction" => self.method_part(child),
502                "Any" => IdiomPart::All,
503                _ if child.is_error() || child.is_missing() => IdiomPart::Partial(partial(child)),
504                _ => IdiomPart::Partial(partial(child)),
505            };
506            parts.push(self.spanned(child, part));
507        }
508    }
509
510    fn destructure_fields(&self, node: Node<'_>) -> Vec<Spanned<Idiom>> {
511        named_children(node)
512            .into_iter()
513            .filter(|child| !matches!(child.kind(), "BraceOpen" | "BraceClose"))
514            .map(|child| match child.kind() {
515                "Ident" => self.spanned(
516                    child,
517                    Idiom {
518                        parts: vec![self
519                            .spanned(child, IdiomPart::Field(self.node_text(child).to_string()))],
520                    },
521                ),
522                "Path" => self.spanned(child, self.idiom(child)),
523                _ => self.spanned(
524                    child,
525                    Idiom {
526                        parts: vec![self.spanned(child, IdiomPart::Partial(partial(child)))],
527                    },
528                ),
529            })
530            .collect()
531    }
532
533    fn method_part(&self, node: Node<'_>) -> IdiomPart {
534        let name = match first_child_of_kind(node, "FunctionName") {
535            Some(name) => self.spanned(name, self.node_text(name).to_string()),
536            None => return IdiomPart::Partial(partial(node)),
537        };
538        let args = first_child_of_kind(node, "ArgumentList")
539            .map(|list| {
540                named_children(list)
541                    .into_iter()
542                    .map(|arg| self.expr(arg))
543                    .collect()
544            })
545            .unwrap_or_default();
546        IdiomPart::Method { name, args }
547    }
548
549    fn graph_part(&self, node: Node<'_>) -> IdiomPart {
550        let mut dir = None;
551        let mut step = GraphStep {
552            targets: Vec::new(),
553            where_clause: None,
554            reference: false,
555        };
556
557        for child in named_children(node) {
558            match child.kind() {
559                "LookupRight" => dir = Some(self.spanned(child, GraphDir::Out)),
560                // `<-` is a graph-edge step; `<~` is a record-reference step.
561                // Both alias to `LookupLeft` in the grammar, so the `~` in the
562                // operator text is what distinguishes a reference traversal.
563                "LookupLeft" => {
564                    step.reference = self.node_text(child).contains('~');
565                    dir = Some(self.spanned(child, GraphDir::In));
566                }
567                "LookupBoth" => dir = Some(self.spanned(child, GraphDir::Both)),
568                "Ident" => step
569                    .targets
570                    .push(self.spanned(child, self.node_text(child).to_string())),
571                "LookupSelection" => self.lookup_selection(child, &mut step),
572                _ => {}
573            }
574        }
575
576        match dir {
577            Some(dir) => IdiomPart::Graph { dir, step },
578            None => IdiomPart::Partial(partial(node)),
579        }
580    }
581
582    fn lookup_selection(&self, node: Node<'_>, step: &mut GraphStep) {
583        for child in named_children(node) {
584            match child.kind() {
585                "GraphPredicate" => {
586                    // The predicate wraps the edge-table identifier.
587                    match single_named_child(child) {
588                        Some(ident) if ident.kind() == "Ident" => step
589                            .targets
590                            .push(self.spanned(ident, self.node_text(ident).to_string())),
591                        _ => {}
592                    }
593                }
594                "WhereClause" => {
595                    if let Some(expr_node) = where_clause_expr(child) {
596                        step.where_clause = Some(Box::new(self.expr(expr_node)));
597                    }
598                }
599                _ => {}
600            }
601        }
602    }
603
604    fn filter_part(&self, node: Node<'_>) -> IdiomPart {
605        let children = named_children(node);
606        match children.as_slice() {
607            [child] if child.kind() == "WhereClause" => match where_clause_expr(*child) {
608                Some(expr_node) => IdiomPart::Where(Box::new(self.expr(expr_node))),
609                None => IdiomPart::Partial(partial(node)),
610            },
611            [child] if child.kind() == "Any" => IdiomPart::All,
612            [child] if child.kind() == "Last" => IdiomPart::Last,
613            [child] if !child.is_error() => IdiomPart::Index(Box::new(self.expr(*child))),
614            _ => IdiomPart::Partial(partial(node)),
615        }
616    }
617}
618
619/// The expression of a `WHERE <expr>` clause (the last named non-keyword child).
620fn where_clause_expr(clause: Node<'_>) -> Option<Node<'_>> {
621    named_children(clause)
622        .into_iter()
623        .rfind(|child| child.kind() != "Keyword")
624}
625
626fn binary_op(text: &str) -> BinaryOp {
627    match text.to_ascii_uppercase().as_str() {
628        "+" => BinaryOp::Add,
629        "-" => BinaryOp::Sub,
630        "*" => BinaryOp::Mul,
631        "/" => BinaryOp::Div,
632        "=" | "==" => BinaryOp::Eq,
633        "!=" => BinaryOp::NotEq,
634        "<" => BinaryOp::Lt,
635        "<=" => BinaryOp::LtEq,
636        ">" => BinaryOp::Gt,
637        ">=" => BinaryOp::GtEq,
638        "AND" | "&&" => BinaryOp::And,
639        "OR" | "||" => BinaryOp::Or,
640        "??" => BinaryOp::NullCoalesce,
641        _ => BinaryOp::Other(text.to_string()),
642    }
643}
644
645fn prefix_op(text: &str) -> PrefixOp {
646    match text {
647        "!" => PrefixOp::Not,
648        "-" => PrefixOp::Neg,
649        "+" => PrefixOp::Pos,
650        _ => PrefixOp::Other(text.to_string()),
651    }
652}
653
654/// `type::is::record` → `type::is_record` (matches the function analyzers'
655/// canonical paths).
656fn normalize_function_path(path: &str) -> String {
657    path.trim().replace("::is::", "::is_")
658}
659
660/// The named children of `node`, minus comments.
661///
662/// `Comment`/`BlockComment` are grammar *extras*, so tree-sitter may insert
663/// one between any two tokens of any rule — including between an operand and
664/// its operator (`n\n-- why\n= 1` puts `Comment` between `Ident` and
665/// `Operator`). Every scan below picks operands positionally ("the child
666/// before the operator", "the last non-keyword child"), so a comment left in
667/// the list is silently selected as an operand and the real one is discarded.
668/// Dropping extras here makes all of them immune by construction; no lowering
669/// site ever wants a comment node.
670fn named_children<'tree>(node: Node<'tree>) -> Vec<Node<'tree>> {
671    let mut cursor = node.walk();
672    let children = node
673        .children(&mut cursor)
674        .filter(|child| child.is_named() && !is_comment(*child))
675        .collect();
676    children
677}
678
679/// Whether `node` is a comment extra. Not to be confused with `CommentClause`
680/// (`DEFINE … COMMENT "…"`), which is real syntax.
681fn is_comment(node: Node<'_>) -> bool {
682    matches!(node.kind(), "Comment" | "BlockComment")
683}
684
685fn single_named_child<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
686    let children = named_children(node);
687    match children.as_slice() {
688        [only] => Some(*only),
689        _ => None,
690    }
691}
692
693/// The single value/statement a `SubQuery`'s parentheses wrap, ignoring
694/// comments (`( /* why */ 1 )`). `None` for an empty or multi-child
695/// `SubQuery`, which has no inner expression to be transparent about.
696fn subquery_content<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
697    let mut children = named_children(node).into_iter();
698    let first = children.next()?;
699    children.next().is_none().then_some(first)
700}
701
702/// The expression a `SubQuery`'s parentheses merely *group* — `Some` for
703/// `(email + 1)` / `(user)`, `None` for `(SELECT …)` and for a malformed
704/// `SubQuery`. Statement-position callers (a SELECT `FROM` source) use this to
705/// see through grouping parentheses exactly as expression lowering does.
706pub(crate) fn paren_group_inner<'tree>(node: Node<'tree>) -> Option<Node<'tree>> {
707    let inner = subquery_content(node)?;
708    (!is_statement_kind(inner.kind())).then_some(inner)
709}
710
711/// Whether a CST node kind sits in statement position. Inside a `SubQuery`,
712/// these are the contents that make a genuine subquery value; everything else
713/// is a grouped expression.
714fn is_statement_kind(kind: &str) -> bool {
715    kind.ends_with("Statement") || kind == "Block"
716}
717
718fn first_child_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
719    named_children(node)
720        .into_iter()
721        .find(|child| child.kind() == kind)
722}
723
724fn first_descendant_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
725    if node.kind() == kind {
726        return Some(node);
727    }
728    for child in named_children(node) {
729        if let Some(found) = first_descendant_of_kind(child, kind) {
730            return Some(found);
731        }
732    }
733    None
734}
735
736fn collect_object_properties(node: Node<'_>, visit: &mut impl FnMut(Node<'_>)) {
737    for child in named_children(node) {
738        match child.kind() {
739            "ObjectProperty" => visit(child),
740            "ObjectContent" => collect_object_properties(child, visit),
741            _ => {}
742        }
743    }
744}
745
746fn collect_object_type_properties(node: Node<'_>, visit: &mut impl FnMut(Node<'_>)) {
747    for child in named_children(node) {
748        match child.kind() {
749            "ObjectTypeProperty" => visit(child),
750            "ObjectTypeContent" => collect_object_type_properties(child, visit),
751            _ => {}
752        }
753    }
754}
755
756#[cfg(test)]
757mod tests {
758    use super::*;
759    use crate::parse::{parse_source, ParsedSource};
760    use crate::source::SourceId;
761
762    fn parse(query: &str) -> ParsedSource {
763        parse_source(SourceId::new("lower:test"), query).expect("test query parses")
764    }
765
766    /// Finds the first named node of `kind` and lowers it.
767    fn lower_first(parsed: &ParsedSource, kind: &str) -> Spanned<Expr> {
768        let node = find_first(parsed.tree().root_node(), kind)
769            .unwrap_or_else(|| panic!("no {kind} node in {:?}", parsed.text()));
770        lower_expr(node, parsed.text())
771    }
772
773    fn find_first<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
774        if node.kind() == kind {
775            return Some(node);
776        }
777        let mut cursor = node.walk();
778        let found = node
779            .children(&mut cursor)
780            .find_map(|child| find_first(child, kind));
781        found
782    }
783
784    fn idiom_parts(expr: &Spanned<Expr>) -> &[Spanned<IdiomPart>] {
785        match &expr.node {
786            Expr::Idiom(idiom) => &idiom.parts,
787            other => panic!("expected idiom, got {other:?}"),
788        }
789    }
790
791    #[test]
792    fn lowers_every_literal_kind_with_prefix_normalization() {
793        let parsed = parse(
794            "RETURN [1, -2, 2.5, 1dec, 'hi', \"there\", d'2024-01-01T00:00:00Z', u'0189-aa', r'ab+', true, false, NONE, null, 1h];",
795        );
796
797        let array = lower_first(&parsed, "Array");
798        let Expr::Array(elements) = &array.node else {
799            panic!("expected array, got {:?}", array.node);
800        };
801        let literals: Vec<_> = elements
802            .iter()
803            .map(|e| match &e.node {
804                Expr::Literal(lit) => lit.clone(),
805                other => panic!("expected literal, got {other:?}"),
806            })
807            .collect();
808
809        assert_eq!(
810            literals,
811            vec![
812                Literal::Int(1),
813                Literal::Int(-2),
814                Literal::Float(2.5),
815                Literal::Decimal,
816                Literal::String("hi".into()),
817                Literal::String("there".into()),
818                Literal::Datetime("2024-01-01T00:00:00Z".into()),
819                Literal::Uuid("0189-aa".into()),
820                Literal::Regex("ab+".into()),
821                Literal::Bool(true),
822                Literal::Bool(false),
823                Literal::None,
824                Literal::Null,
825                Literal::Duration("1h".into()),
826            ]
827        );
828    }
829
830    #[test]
831    fn lowers_param_rooted_idiom_with_start_part() {
832        let parsed = parse("RETURN $user.name;");
833
834        let path = lower_first(&parsed, "Path");
835        let parts = idiom_parts(&path);
836
837        assert_eq!(parts.len(), 2);
838        let IdiomPart::Start(start) = &parts[0].node else {
839            panic!("expected Start, got {:?}", parts[0].node);
840        };
841        assert_eq!(start.node, Expr::Param("user".into()));
842        assert_eq!(parts[1].node, IdiomPart::Field("name".into()));
843    }
844
845    #[test]
846    fn lowers_graph_traversal_with_per_arrow_spans_and_destructure() {
847        let query = "SELECT ->likes->post.{title, id} FROM person;";
848        let parsed = parse(query);
849
850        let path = lower_first(&parsed, "Path");
851        let parts = idiom_parts(&path);
852        assert_eq!(parts.len(), 3);
853
854        let IdiomPart::Graph { dir, step } = &parts[0].node else {
855            panic!("expected graph part, got {:?}", parts[0].node);
856        };
857        assert_eq!(dir.node, GraphDir::Out);
858        // The direction span covers exactly the arrow token.
859        assert_eq!(
860            &query[dir.span.start() as usize..dir.span.end() as usize],
861            "->"
862        );
863        assert_eq!(step.targets.len(), 1);
864        assert_eq!(step.targets[0].node, "likes");
865        assert!(step.where_clause.is_none());
866
867        let IdiomPart::Destructure(fields) = &parts[2].node else {
868            panic!("expected destructure, got {:?}", parts[2].node);
869        };
870        let names: Vec<_> = fields
871            .iter()
872            .map(|idiom| match &idiom.node.parts[0].node {
873                IdiomPart::Field(name) => name.clone(),
874                other => panic!("expected field, got {other:?}"),
875            })
876            .collect();
877        assert_eq!(names, vec!["title", "id"]);
878    }
879
880    /// `.{}` is valid SurrealQL — `SELECT VALUE id.{} FROM ONLY user:ada`
881    /// returns `{}` on 3.0.5. The grammar used to require at least one selected
882    /// entry, so every such expression was a *parse* error, and a parse error
883    /// is fatal to the whole source rather than to one expression.
884    #[test]
885    fn lowers_an_empty_destructure_without_a_parse_error() {
886        let query = "SELECT id.{} FROM user;";
887        let parsed = parse(query);
888        assert!(
889            !parsed.has_error(),
890            "an empty destructure must parse: {:?}",
891            parsed.syntax_diagnostics()
892        );
893
894        let path = lower_first(&parsed, "Path");
895        let parts = idiom_parts(&path);
896
897        let IdiomPart::Destructure(fields) = &parts[1].node else {
898            panic!("expected destructure, got {:?}", parts[1].node);
899        };
900        assert!(fields.is_empty(), "nothing was selected");
901    }
902
903    #[test]
904    fn lowers_filtered_graph_step_with_inline_where() {
905        let parsed = parse("SELECT ->(likes WHERE since > $x)->post FROM person;");
906
907        let path = lower_first(&parsed, "Path");
908        let parts = idiom_parts(&path);
909
910        let IdiomPart::Graph { step, .. } = &parts[0].node else {
911            panic!("expected graph part, got {:?}", parts[0].node);
912        };
913        assert_eq!(step.targets[0].node, "likes");
914        let where_clause = step.where_clause.as_ref().expect("has inline WHERE");
915        assert!(matches!(where_clause.node, Expr::Binary { .. }));
916    }
917
918    #[test]
919    fn lowers_index_and_where_filters_distinctly() {
920        let parsed = parse("SELECT tags[0], tags[$i], tags[WHERE active] FROM person;");
921
922        let root = parsed.tree().root_node();
923        let mut paths = Vec::new();
924        collect_kind(root, "Path", &mut paths);
925        let lowered: Vec<_> = paths
926            .iter()
927            .map(|p| lower_expr(*p, parsed.text()))
928            .collect();
929
930        let by_index = idiom_parts(&lowered[0]);
931        let IdiomPart::Index(index) = &by_index[1].node else {
932            panic!("expected index, got {:?}", by_index[1].node);
933        };
934        assert_eq!(index.node, Expr::Literal(Literal::Int(0)));
935
936        let by_param = idiom_parts(&lowered[1]);
937        let IdiomPart::Index(index) = &by_param[1].node else {
938            panic!("expected index, got {:?}", by_param[1].node);
939        };
940        assert_eq!(index.node, Expr::Param("i".into()));
941
942        let by_where = idiom_parts(&lowered[2]);
943        assert!(matches!(by_where[1].node, IdiomPart::Where(_)));
944    }
945
946    fn collect_kind<'tree>(node: Node<'tree>, kind: &str, out: &mut Vec<Node<'tree>>) {
947        if node.kind() == kind {
948            out.push(node);
949            return;
950        }
951        let mut cursor = node.walk();
952        for child in node.children(&mut cursor) {
953            collect_kind(child, kind, out);
954        }
955    }
956
957    #[test]
958    fn lowers_method_call_idiom_part() {
959        let parsed = parse("RETURN foo.len();");
960
961        let path = lower_first(&parsed, "Path");
962        let parts = idiom_parts(&path);
963
964        let IdiomPart::Method { name, args } = &parts[1].node else {
965            panic!("expected method, got {:?}", parts[1].node);
966        };
967        assert_eq!(name.node, "len");
968        assert!(args.is_empty());
969    }
970
971    #[test]
972    fn lowers_calls_with_normalized_paths_and_spanned_args() {
973        let query = "RETURN type::is::record($id);";
974        let parsed = parse(query);
975
976        let call = lower_first(&parsed, "FunctionCall");
977        let Expr::Call(call) = &call.node else {
978            panic!("expected call, got {:?}", call.node);
979        };
980
981        assert_eq!(call.path.node, "type::is_record");
982        assert_eq!(call.args.len(), 1);
983        assert_eq!(call.args[0].node, Expr::Param("id".into()));
984        assert_eq!(
985            &query[call.args[0].span.start() as usize..call.args[0].span.end() as usize],
986            "$id"
987        );
988    }
989
990    #[test]
991    fn lowers_binary_and_prefix_operators() {
992        let parsed = parse("SELECT * FROM person WHERE age > 18 AND !banned;");
993
994        let outer = lower_first(&parsed, "BinaryExpression");
995        let Expr::Binary { lhs, op, rhs } = &outer.node else {
996            panic!("expected binary, got {:?}", outer.node);
997        };
998        assert_eq!(op.node, BinaryOp::And);
999
1000        let Expr::Binary { op: inner_op, .. } = &lhs.node else {
1001            panic!("expected nested binary, got {:?}", lhs.node);
1002        };
1003        assert_eq!(inner_op.node, BinaryOp::Gt);
1004
1005        let Expr::Prefix { op: prefix, .. } = &rhs.node else {
1006            panic!("expected prefix, got {:?}", rhs.node);
1007        };
1008        assert_eq!(prefix.node, PrefixOp::Not);
1009    }
1010
1011    #[test]
1012    fn lowers_object_literals_with_trimmed_keys() {
1013        let parsed = parse("RETURN { name: 'a', \"age\": 1 };");
1014
1015        let object = lower_first(&parsed, "Object");
1016        let Expr::Object(fields) = &object.node else {
1017            panic!("expected object, got {:?}", object.node);
1018        };
1019
1020        assert_eq!(fields.len(), 2);
1021        assert_eq!(fields[0].0.node, "name");
1022        assert_eq!(fields[0].1.node, Expr::Literal(Literal::String("a".into())));
1023        assert_eq!(fields[1].0.node, "age");
1024        assert_eq!(fields[1].1.node, Expr::Literal(Literal::Int(1)));
1025    }
1026
1027    #[test]
1028    fn lowers_object_and_record_union_field_types() {
1029        // Object field types: `{ street: string, zip: int }` — the grammar
1030        // nests the ObjectType under a LiteralType.
1031        let parsed = parse("DEFINE FIELD address ON person TYPE { street: string, zip: int };");
1032        let object_type =
1033            find_first(parsed.tree().root_node(), "ObjectType").expect("has an ObjectType node");
1034        let ty = lower_type_expr(object_type, parsed.text());
1035        let TypeExpr::Object(properties) = &ty.node else {
1036            panic!("expected object type, got {:?}", ty.node);
1037        };
1038        assert_eq!(properties.len(), 2);
1039        assert_eq!(properties[0].0.node, "street");
1040        assert!(matches!(&properties[0].1.node, TypeExpr::Name(n) if n.node == "string"));
1041        assert_eq!(properties[1].0.node, "zip");
1042        assert!(matches!(&properties[1].1.node, TypeExpr::Name(n) if n.node == "int"));
1043
1044        // Record unions: `record<team | user | organization>` nests the table
1045        // names as a single UnionType argument.
1046        let parsed =
1047            parse("DEFINE FIELD owner ON thing TYPE record<team | user | organization>;");
1048        let param = find_first(parsed.tree().root_node(), "ParameterizedType")
1049            .expect("has a ParameterizedType node");
1050        let ty = lower_type_expr(param, parsed.text());
1051        let TypeExpr::Parameterized { name, args } = &ty.node else {
1052            panic!("expected parameterized type, got {:?}", ty.node);
1053        };
1054        assert_eq!(name.node, "record");
1055        assert_eq!(args.len(), 1);
1056        let TypeExpr::Union(variants) = &args[0].node else {
1057            panic!("expected union argument, got {:?}", args[0].node);
1058        };
1059        let names: Vec<_> = variants
1060            .iter()
1061            .map(|v| match &v.node {
1062                TypeExpr::Name(n) => n.node.clone(),
1063                other => panic!("expected table name, got {other:?}"),
1064            })
1065            .collect();
1066        assert_eq!(names, vec!["team", "user", "organization"]);
1067    }
1068
1069    #[test]
1070    fn lowers_type_cast() {
1071        let parsed = parse("RETURN <int> '42';");
1072
1073        let cast = lower_first(&parsed, "TypeCast");
1074        let Expr::Cast { ty, expr } = &cast.node else {
1075            panic!("expected cast, got {:?}", cast.node);
1076        };
1077        let TypeExpr::Name(name) = &ty.node else {
1078            panic!("expected type name, got {:?}", ty.node);
1079        };
1080        assert_eq!(name.node, "int");
1081        assert_eq!(expr.node, Expr::Literal(Literal::String("42".into())));
1082    }
1083
1084    #[test]
1085    fn error_nodes_lower_to_explicit_partials() {
1086        // Broken input must produce Partial, never be silently skipped.
1087        let parsed = parse("SELECT name, FROM person;");
1088
1089        let error = find_first(parsed.tree().root_node(), "ERROR").expect("input has ERROR node");
1090        let lowered = lower_expr(error, parsed.text());
1091
1092        assert!(
1093            matches!(lowered.node, Expr::Partial(_)),
1094            "ERROR must lower to Partial, got {:?}",
1095            lowered.node
1096        );
1097    }
1098
1099    #[test]
1100    fn an_idiom_node_carries_its_wildcard_as_an_element_step() {
1101        // `Path` wraps `[*]` in a `Filter`, but an `Idiom` node (a DEFINE FIELD
1102        // path, a FETCH/SPLIT/GROUP path) lists a bare `Any` child. Both are the
1103        // same element step — dropping it collapses `items[*].price` onto
1104        // `items.price`, which is a different declaration entirely.
1105        let parsed = parse("DEFINE FIELD items[*].price ON t TYPE string;");
1106        let node = find_first(parsed.tree().root_node(), "Idiom").expect("an Idiom node");
1107        let idiom = lower_idiom_node(node, parsed.text());
1108
1109        assert!(matches!(idiom.parts[0].node, IdiomPart::Field(ref n) if n == "items"));
1110        assert!(matches!(idiom.parts[1].node, IdiomPart::All));
1111        assert!(matches!(idiom.parts[2].node, IdiomPart::Field(ref n) if n == "price"));
1112    }
1113
1114    #[test]
1115    fn wildcard_and_last_filters_lower_to_their_idiom_parts() {
1116        let parsed = parse("SELECT tags[*], tags[$] FROM person;");
1117
1118        let path = lower_first(&parsed, "Path");
1119        let parts = idiom_parts(&path);
1120        assert!(matches!(parts[0].node, IdiomPart::Field(_)));
1121        assert!(matches!(parts[1].node, IdiomPart::All));
1122
1123        let root = parsed.tree().root_node();
1124        let mut paths = Vec::new();
1125        collect_kind(root, "Path", &mut paths);
1126        let last_path = lower_expr(paths[1], parsed.text());
1127        let parts = idiom_parts(&last_path);
1128        assert!(matches!(parts[1].node, IdiomPart::Last));
1129    }
1130
1131    #[test]
1132    fn closures_lower_to_explicit_non_silent_variants() {
1133        // Closures are deliberately unmodeled: if the grammar parses one it
1134        // must lower to `Expr::Closure(..)`; if the grammar ERRORs on it,
1135        // `Expr::Partial(..)` is the honest answer. Anything else would mean
1136        // the closure was silently misread as a value.
1137        let parsed = parse("RETURN array::map([1], |$v| $v);");
1138
1139        let call = lower_first(&parsed, "FunctionCall");
1140        let Expr::Call(call) = &call.node else {
1141            panic!("expected call, got {:?}", call.node);
1142        };
1143        let closure_arg = call.args.get(1).expect("closure argument present");
1144        assert!(
1145            matches!(closure_arg.node, Expr::Closure(_) | Expr::Partial(_)),
1146            "closure argument must be explicitly unmodeled, got {:?}",
1147            closure_arg.node
1148        );
1149    }
1150
1151    #[test]
1152    fn grouping_parentheses_lower_to_the_inner_expression() {
1153        // Parentheses are a semantic no-op: `(a + b)` IS `a + b`. Lowering it
1154        // to an opaque `Expr::Subquery` put a wall in front of every consumer
1155        // that matches on expression shape, silently turning off narrowing,
1156        // field validation and every expression-level diagnostic.
1157        let parsed = parse("RETURN (1 + 2);");
1158        let lowered = lower_first(&parsed, "SubQuery");
1159        assert!(
1160            matches!(lowered.node, Expr::Binary { .. }),
1161            "a grouped expression must lower to itself, got {:?}",
1162            lowered.node
1163        );
1164        // The span still covers the parentheses, so diagnostics point at the
1165        // expression exactly as written.
1166        let span = lowered.span.start() as usize..lowered.span.end() as usize;
1167        assert_eq!(&parsed.text()[span], "(1 + 2)");
1168
1169        // Nested parentheses collapse all the way down.
1170        let parsed = parse("RETURN (((1 + 2)));");
1171        let lowered = lower_first(&parsed, "SubQuery");
1172        assert!(matches!(lowered.node, Expr::Binary { .. }));
1173
1174        // Grouping is still structural: `(a + b) * c` keeps its shape.
1175        let parsed = parse("RETURN (1 + 2) * 3;");
1176        let lowered = lower_first(&parsed, "BinaryExpression");
1177        let Expr::Binary { lhs, op, .. } = &lowered.node else {
1178            panic!("expected a binary, got {:?}", lowered.node);
1179        };
1180        assert!(matches!(op.node, BinaryOp::Mul));
1181        let Expr::Binary { op: inner_op, .. } = &lhs.node else {
1182            panic!("expected the grouped binary on the left, got {:?}", lhs.node);
1183        };
1184        assert!(matches!(inner_op.node, BinaryOp::Add));
1185    }
1186
1187    #[test]
1188    fn parenthesized_statements_stay_subqueries() {
1189        // A *statement* in parentheses is a genuine subquery value, and must
1190        // keep its `Expr::Subquery` wrapper — only grouping is transparent.
1191        let parsed = parse("RETURN (SELECT * FROM person);");
1192        let lowered = lower_first(&parsed, "SubQuery");
1193        let Expr::Subquery(inner) = &lowered.node else {
1194            panic!("expected a subquery, got {:?}", lowered.node);
1195        };
1196        assert!(matches!(inner.node, crate::ast::Statement::Select(_)));
1197
1198        let parsed = parse("RETURN ({ RETURN 1; });");
1199        let lowered = lower_first(&parsed, "SubQuery");
1200        assert!(matches!(lowered.node, Expr::Subquery(_)));
1201    }
1202
1203    #[test]
1204    fn a_comment_inside_parentheses_does_not_recurse_forever() {
1205        // `( /* why */ 1 )` gives the `SubQuery` two named children. Falling
1206        // back to the `SubQuery` node itself made lowering re-enter through
1207        // the bare-expression statement arm and overflow the stack.
1208        let parsed = parse("RETURN (/* why */ 1);");
1209        let lowered = lower_first(&parsed, "SubQuery");
1210        assert!(
1211            matches!(lowered.node, Expr::Literal(Literal::Int(1))),
1212            "a commented group is still its inner expression, got {:?}",
1213            lowered.node
1214        );
1215    }
1216
1217    #[test]
1218    fn a_comment_between_operands_does_not_swallow_an_operand() {
1219        // Comments are grammar extras, so tree-sitter puts a `Comment` node
1220        // *between* an operand and its operator. Selecting operands by
1221        // position then picked the comment and discarded the real operand —
1222        // dropping an arbitrarily large side of the expression, and with it
1223        // every check that would have run on it.
1224        for query in [
1225            // before the operator
1226            "SELECT * FROM t WHERE a = 1\n  -- why\n  AND b = 2;",
1227            // after the operator
1228            "SELECT * FROM t WHERE a = 1 AND\n  -- why\n  b = 2;",
1229            // on both sides, both comment syntaxes
1230            "SELECT * FROM t WHERE a = 1 /* one */ AND -- two\n b = 2;",
1231            // inside grouping parentheses
1232            "SELECT * FROM t WHERE (a = 1 -- why\n) AND b = 2;",
1233            // a multi-line WHERE with a comment on every line
1234            "SELECT * FROM t\nWHERE -- head\n  a = 1 -- first\n  AND -- mid\n  b = 2 -- tail\n;",
1235        ] {
1236            let parsed = parse(query);
1237            let lowered = lower_first(&parsed, "BinaryExpression");
1238            let Expr::Binary { lhs, op, rhs } = &lowered.node else {
1239                panic!("expected a binary for {query:?}, got {:?}", lowered.node);
1240            };
1241            assert!(matches!(op.node, BinaryOp::And), "operator of {query:?}");
1242            assert!(
1243                matches!(lhs.node, Expr::Binary { .. }),
1244                "left operand of {query:?} was discarded: {:?}",
1245                lhs.node
1246            );
1247            assert!(
1248                matches!(rhs.node, Expr::Binary { .. }),
1249                "right operand of {query:?} was discarded: {:?}",
1250                rhs.node
1251            );
1252        }
1253
1254        // The minimal shape: a comment between a bare field and its operator.
1255        let parsed = parse("SELECT * FROM t WHERE n\n  -- why\n  = \"x\";");
1256        let lowered = lower_first(&parsed, "BinaryExpression");
1257        let Expr::Binary { lhs, .. } = &lowered.node else {
1258            panic!("expected a binary, got {:?}", lowered.node);
1259        };
1260        assert!(
1261            matches!(&lhs.node, Expr::Idiom(idiom) if idiom.parts.len() == 1),
1262            "left operand should still be the field `n`, got {:?}",
1263            lhs.node
1264        );
1265    }
1266
1267    #[test]
1268    fn a_comment_after_a_prefix_operator_is_not_the_operand() {
1269        let parsed = parse("SELECT * FROM t WHERE ! -- why\n active;");
1270        let lowered = lower_first(&parsed, "PrefixExpression");
1271        let Expr::Prefix { op, expr } = &lowered.node else {
1272            panic!("expected a prefix, got {:?}", lowered.node);
1273        };
1274        assert!(matches!(op.node, PrefixOp::Not));
1275        assert!(
1276            matches!(expr.node, Expr::Idiom(_)),
1277            "operand should be `active`, got {:?}",
1278            expr.node
1279        );
1280    }
1281
1282    #[test]
1283    fn comments_do_not_displace_operands_elsewhere() {
1284        // Every other positional operand scan has the same exposure, so they
1285        // are covered by the same filter: a cast's value, an array's
1286        // elements, and a record id's id part.
1287        let parsed = parse("SELECT * FROM t WHERE <int> /* why */ a;");
1288        let lowered = lower_first(&parsed, "TypeCast");
1289        let Expr::Cast { expr, .. } = &lowered.node else {
1290            panic!("expected a cast, got {:?}", lowered.node);
1291        };
1292        assert!(
1293            matches!(expr.node, Expr::Idiom(_)),
1294            "cast value should be `a`, got {:?}",
1295            expr.node
1296        );
1297
1298        let parsed = parse("RETURN [1, /* why */ 2];");
1299        let lowered = lower_first(&parsed, "Array");
1300        let Expr::Array(items) = &lowered.node else {
1301            panic!("expected an array, got {:?}", lowered.node);
1302        };
1303        assert_eq!(items.len(), 2, "a comment is not an element: {items:?}");
1304
1305        let parsed = parse("RETURN { k /* why */ : 1 };");
1306        let lowered = lower_first(&parsed, "RecordId");
1307        let Expr::RecordId { table, id, .. } = &lowered.node else {
1308            panic!("expected a record id, got {:?}", lowered.node);
1309        };
1310        assert_eq!(table.node, "k");
1311        assert_eq!(&parsed.text()[id.start() as usize..id.end() as usize], "1");
1312    }
1313}