Skip to main content

dr_strange_parser/
ast.rs

1//! The parser-level AST. Deliberately *not* core's `LogicalPlan`: a Cypher
2//! pattern binds several variables, but core's linear-pipeline row model only
3//! ever sees a single "current node" (arch/03 §2). So property/label access
4//! here keeps its variable qualifier (`p.year`, `q:Paper`); the compiler
5//! ([`crate::compile`]) is what decides *where* in the pipeline each predicate
6//! belongs and drops the qualifier when it lands on that variable's slot.
7
8use dr_strange_core::AggFunc;
9use dr_strange_core::Metric;
10use dr_strange_core::PropValue;
11use dr_strange_core::compute::expr::{ArithOp, CmpOp, LogicOp, StrOp};
12use dr_strange_core::types::Dir;
13
14/// A whole parsed query: a source (a `MATCH` pattern or one of the retrieval
15/// seeds), zero or more `BEAM` hops, then `[WHERE …] RETURN … [ORDER BY …]
16/// [SKIP n] [LIMIT n] [AS OF …]`.
17#[derive(Debug, Clone, PartialEq)]
18pub struct Query {
19    pub source: QuerySource,
20    pub beams: Vec<BeamClause>,
21    pub where_clause: Option<PExpr>,
22    pub ret: Return,
23    pub order_by: Vec<OrderKey>,
24    pub skip: Option<u64>,
25    pub limit: Option<u64>,
26    /// `AS OF <seq|"timestamp">` — read a past snapshot. Not part of the
27    /// compiled plan: it addresses the *plane handle*, so it rides beside it.
28    pub as_of: Option<AsOfSpec>,
29}
30
31/// The point `AS OF` pins reads to. A bare integer is a commit sequence; a
32/// quoted RFC-3339 instant and `TIME <ms>` are both wall-clock addresses.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum AsOfSpec {
35    Seq(u64),
36    /// Unix-epoch milliseconds.
37    Time(i64),
38}
39
40/// `BEAM (result[:Label]) <OUT|IN|BOTH> [:TYPE] [ON prop] NEAR <query>
41/// [METRIC m] WIDTH w DEPTH d` — similarity-guided beam traversal from the
42/// current frontier (compiles to `Step::ExpandBeam`), binding `result` as the
43/// new current node.
44#[derive(Debug, Clone, PartialEq)]
45pub struct BeamClause {
46    pub node: NodePat,
47    pub dir: Dir,
48    pub edge_type: Option<String>,
49    /// `None` ⇒ the conventional embedding property (see [`crate::compile`]).
50    pub property: Option<String>,
51    pub query: VecArg,
52    pub metric: Metric,
53    pub width: u32,
54    pub depth: u32,
55}
56
57/// Where the query's rows originate. Every form binds exactly one node pattern
58/// (`first`) and may continue with a relationship tail (`rest`) — so a typed
59/// hop can follow a retrieval seed just as it follows a `MATCH` node, and the
60/// rest of the query (WHERE/RETURN/ORDER BY/…) is shared by all of them.
61#[derive(Debug, Clone, PartialEq)]
62pub struct QuerySource {
63    pub kind: SourceKind,
64    pub first: NodePat,
65    pub rest: Vec<(RelPat, NodePat)>,
66}
67
68/// What seeds a query's rows.
69#[derive(Debug, Clone, PartialEq)]
70pub enum SourceKind {
71    /// `MATCH (a:Label)…` — a scan of the first node's label.
72    Match,
73    /// `SEARCH (v:Label) [ON prop] NEAR "text"|[..] [METRIC m] [TOPK k]` — the
74    /// indexed vector seed (`Source::VectorTopK`).
75    Search {
76        /// `None` ⇒ the conventional embedding property (see [`crate::compile`]).
77        property: Option<String>,
78        query: VecArg,
79        metric: Metric,
80        k: u64,
81    },
82    /// `SEARCH (v:Label) ON prop MATCHING "text" [TOPK k]` — the BM25 keyword
83    /// seed (`Source::KeywordTopK`). `ON` is required here: keyword properties
84    /// follow no convention, so there is nothing sound to default to.
85    Keyword {
86        property: Option<String>,
87        query: String,
88        k: u64,
89    },
90    /// `HYBRID (v:Label) …` — fused retrieval (`Source::Hybrid`).
91    Hybrid(HybridClause),
92    /// `CALL name(args) ON (v:Label)` — a graph algorithm (`Source::Algo`).
93    Call(CallClause),
94}
95
96/// `HYBRID (v:Label) [VECTOR ON p NEAR q [METRIC m] [WEIGHT w]]
97/// [KEYWORD ON p MATCHING "text" [WEIGHT w]]
98/// [GRAPH HOPS h DECAY d [SEEDS n] [WEIGHT w]] [CANDIDATES n] [TOPK k]` —
99/// every channel optional, but at least one required.
100#[derive(Debug, Clone, PartialEq)]
101pub struct HybridClause {
102    pub vector: Option<HybridVector>,
103    pub keyword: Option<HybridKeyword>,
104    pub graph: Option<HybridGraph>,
105    pub candidates: Option<u64>,
106    pub k: Option<u64>,
107}
108
109#[derive(Debug, Clone, PartialEq)]
110pub struct HybridVector {
111    /// `None` ⇒ the conventional embedding property (see [`crate::compile`]).
112    pub property: Option<String>,
113    pub query: VecArg,
114    pub metric: Metric,
115    pub weight: Option<f32>,
116}
117
118#[derive(Debug, Clone, PartialEq)]
119pub struct HybridKeyword {
120    /// Required in practice — the compiler rejects `None` with a clear error,
121    /// since keyword properties follow no convention to default to.
122    pub property: Option<String>,
123    pub query: String,
124    pub weight: Option<f32>,
125}
126
127#[derive(Debug, Clone, PartialEq)]
128pub struct HybridGraph {
129    pub hops: u32,
130    /// `None` ⇒ the same default the RPC/MCP/CLI surfaces use (see
131    /// [`crate::compile`]).
132    pub decay: Option<f32>,
133    pub seeds: Option<u64>,
134    pub weight: Option<f32>,
135}
136
137/// `CALL name(arg: value, …)` — an algorithm invocation. The compiler checks
138/// the name and its arguments; the grammar accepts any named-argument list so
139/// a typo reports as an unknown algorithm/argument rather than a parse error.
140#[derive(Debug, Clone, PartialEq)]
141pub struct CallClause {
142    pub name: String,
143    pub args: Vec<(String, Val)>,
144}
145
146/// A query-vector argument: either a literal vector (programmatic) or text to
147/// embed server-side (the ergonomic default — no 1024-float client payload).
148#[derive(Debug, Clone, PartialEq)]
149pub enum VecArg {
150    Vector(Vec<f32>),
151    Text(String),
152}
153
154/// A single linear path: one node, then zero or more `(relationship, node)`
155/// hops. Branching patterns are not supported in this cut.
156#[derive(Debug, Clone, PartialEq)]
157pub struct Pattern {
158    pub first: NodePat,
159    pub rest: Vec<(RelPat, NodePat)>,
160}
161
162#[derive(Debug, Clone, PartialEq)]
163pub struct NodePat {
164    pub var: Option<String>,
165    pub label: Option<String>,
166}
167
168#[derive(Debug, Clone, PartialEq)]
169pub struct RelPat {
170    pub dir: Dir,
171    pub ty: Option<String>,
172    /// `Some` for a variable-length relationship (`*`, `*n`, `*n..m`, `*..m`).
173    pub var_len: Option<VarLen>,
174}
175
176/// Variable-length bounds. `max = None` is an unbounded `*` / `*n..`, which the
177/// compiler rejects (core's `ExpandVar` needs a concrete upper bound).
178#[derive(Debug, Clone, Copy, PartialEq)]
179pub struct VarLen {
180    pub min: u32,
181    pub max: Option<u32>,
182}
183
184/// `RETURN [DISTINCT] <item> [, <item>]*`.
185///
186/// An item naming a node (`n`, `*`) returns node rows; anything else returns
187/// a table. The two cannot be mixed: a node is not a value (see
188/// [`crate::compile`]).
189#[derive(Debug, Clone, PartialEq)]
190pub struct Return {
191    pub distinct: bool,
192    pub items: Vec<ReturnItem>,
193}
194
195#[derive(Debug, Clone, PartialEq)]
196pub enum ReturnItem {
197    /// `RETURN *` — the matching rows themselves.
198    Star,
199    /// `RETURN n` — likewise, named by their variable.
200    Var(String),
201    /// `RETURN n.year [AS y]` — a projected column.
202    Value { expr: PExpr, name: String },
203    /// `RETURN count(*) [AS n]` — a column folded over each group.
204    Agg {
205        func: AggFunc,
206        /// `None` for `count(*)`, which reads no value.
207        arg: Option<PExpr>,
208        distinct: bool,
209        name: String,
210    },
211}
212
213/// `ORDER BY <key> [ASC|DESC]`. A projecting query orders by column, which
214/// `text` is what names.
215#[derive(Debug, Clone, PartialEq)]
216pub struct OrderKey {
217    pub target: SortTarget,
218    /// The key's source text, trimmed: the name an unaliased `RETURN` item
219    /// takes, so `ORDER BY count(*)` finds its column.
220    pub text: String,
221    pub descending: bool,
222}
223
224/// What an `ORDER BY` key names.
225#[derive(Debug, Clone, PartialEq)]
226pub enum SortTarget {
227    /// An expression — `ORDER BY n.year DESC`.
228    Expr(PExpr),
229    /// A bare name: a `RETURN` alias, which only a projecting query has. A
230    /// bare variable is not an expression in this language.
231    Name(String),
232    /// An aggregate — `ORDER BY count(*) DESC` — which names the projected
233    /// column that folds the same way, however that column was spelled.
234    Agg {
235        func: AggFunc,
236        arg: Option<PExpr>,
237        distinct: bool,
238    },
239}
240
241/// A parsed expression. Mirrors core's `Expr` but with variable-qualified
242/// property/label access so the compiler can attribute each term to a pattern
243/// variable. Reuses core's operator enums so compilation is a plain 1:1 map.
244#[derive(Debug, Clone, PartialEq)]
245pub enum PExpr {
246    Lit(PropValue),
247    /// A `$name` parameter placeholder used as a value — resolved from the
248    /// caller's params at parse time.
249    Param(String),
250    Prop {
251        var: String,
252        key: String,
253    },
254    HasLabel {
255        var: String,
256        label: String,
257    },
258    /// `key(v)` — the node's external key. An equality (or `IN`) on the
259    /// source variable compiles to a `SeekKeys` seek rather than a filter.
260    ExternalKey {
261        var: String,
262    },
263    /// `x IN [a, b, …]` — sugar for a chain of equalities.
264    In {
265        lhs: Box<PExpr>,
266        list: Vec<PExpr>,
267    },
268    /// `x IN <expr>` where the right side is not a literal list — membership
269    /// in a value only known per row (a `List` property, or a `Map`'s keys).
270    /// Kept apart from [`PExpr::In`] because that one is sugar the compiler
271    /// expands into equalities (and, on `key(n)`, into a seek); this one
272    /// cannot be, since the haystack isn't known until the row is.
273    InValue {
274        lhs: Box<PExpr>,
275        haystack: Box<PExpr>,
276    },
277    /// `a CONTAINS b`, `a STARTS WITH b`, `a ENDS WITH b`.
278    StringMatch {
279        op: StrOp,
280        lhs: Box<PExpr>,
281        rhs: Box<PExpr>,
282    },
283    IsNull(Box<PExpr>),
284    Not(Box<PExpr>),
285    Neg(Box<PExpr>),
286    Compare {
287        op: CmpOp,
288        lhs: Box<PExpr>,
289        rhs: Box<PExpr>,
290    },
291    Logic {
292        op: LogicOp,
293        lhs: Box<PExpr>,
294        rhs: Box<PExpr>,
295    },
296    Arith {
297        op: ArithOp,
298        lhs: Box<PExpr>,
299        rhs: Box<PExpr>,
300    },
301    // ---- scoring terms (vector search) ----
302    /// The row's similarity score channel — `score()`.
303    Score,
304    /// Hops taken to reach the current node — `hops()`.
305    Hops,
306    /// `similarity(v.prop, <query>, metric)` — higher is closer.
307    Similarity {
308        var: String,
309        property: String,
310        query: VecArg,
311        metric: Metric,
312    },
313    /// `distance(v.prop, <query>, metric)` — lower is closer.
314    Distance {
315        var: String,
316        property: String,
317        query: VecArg,
318        metric: Metric,
319    },
320}
321
322// ---- write statements -----------------------------------------------------
323
324/// A parsed top-level statement: a read (compiles to a `LogicalPlan`) or a
325/// write (executed against a `WriteTxn`).
326#[derive(Debug, Clone, PartialEq)]
327pub enum StmtAst {
328    // Boxed: a read `Query` is far larger than a `WriteAst`.
329    Read(Box<Query>),
330    Write(WriteAst),
331}
332
333/// A write statement: either a standalone `CREATE`, or a `MATCH … [WHERE …]`
334/// that binds the terminal node variable, followed by mutation ops that operate
335/// on it (`SET`/`REMOVE`/`DELETE`).
336#[derive(Debug, Clone, PartialEq)]
337pub struct WriteAst {
338    pub match_clause: Option<MatchClause>,
339    pub ops: Vec<WriteOp>,
340}
341
342/// The read half of a `MATCH … [WHERE …] SET/REMOVE/DELETE` — a pattern to find
343/// nodes to mutate. Compiled to a read `LogicalPlan` whose terminal node is the
344/// bound variable.
345#[derive(Debug, Clone, PartialEq)]
346pub struct MatchClause {
347    pub pattern: Pattern,
348    pub where_clause: Option<PExpr>,
349}
350
351#[derive(Debug, Clone, PartialEq)]
352pub enum WriteOp {
353    /// `CREATE (n:L {..}), (a)-[:T {..}]->(b), …`
354    Create(Vec<CreatePath>),
355    /// `MERGE (n:L {key:"k", ..}) [ON CREATE SET …] [ON MATCH SET …]`
356    Merge(MergeClause),
357    /// `SET n.p = v, n:Label, n += {..}`
358    Set(Vec<SetItem>),
359    /// `REMOVE n.p, n:Label`
360    Remove(Vec<RemoveItem>),
361    /// `[DETACH] DELETE n, m, …`
362    Delete { detach: bool, vars: Vec<String> },
363}
364
365/// `MERGE (a {key}) [ON CREATE SET …] [ON MATCH SET …]` — upsert one node by
366/// its external key — or `MERGE (a {key})-[:T]->(b {key})` — upsert each keyed
367/// node and ensure the edge (element-wise, idempotent). `path` reuses the CREATE
368/// path shape; `on_create`/`on_match` apply only to a single-node MERGE.
369#[derive(Debug, Clone, PartialEq)]
370pub struct MergeClause {
371    pub path: CreatePath,
372    pub on_create: Vec<SetItem>,
373    pub on_match: Vec<SetItem>,
374}
375
376/// A property value: a literal, or a `$name` parameter placeholder resolved
377/// from the caller's params at parse time.
378#[derive(Debug, Clone, PartialEq)]
379pub enum Val {
380    Lit(PropValue),
381    Param(String),
382}
383
384#[derive(Debug, Clone, PartialEq)]
385pub enum SetItem {
386    /// `n.key = value`
387    Prop {
388        var: String,
389        key: String,
390        value: Val,
391    },
392    /// `n:Label` — add a label.
393    Label { var: String, label: String },
394    /// `n += { .. }` — merge properties.
395    Merge {
396        var: String,
397        props: Vec<(String, Val)>,
398    },
399}
400
401#[derive(Debug, Clone, PartialEq)]
402pub enum RemoveItem {
403    /// `n.key` — remove a property.
404    Prop { var: String, key: String },
405    /// `n:Label` — remove a label.
406    Label { var: String, label: String },
407}
408
409/// One CREATE path: a node, then directed `[:TYPE {..}]` hops to more nodes.
410#[derive(Debug, Clone, PartialEq)]
411pub struct CreatePath {
412    pub first: CreateNode,
413    pub rest: Vec<(CreateRel, CreateNode)>,
414}
415
416/// A node in a CREATE: optional variable (to reference within the statement),
417/// optional label, optional external `key` (from an inline `key: "…"`), and the
418/// remaining inline properties.
419#[derive(Debug, Clone, PartialEq)]
420pub struct CreateNode {
421    pub var: Option<String>,
422    pub label: Option<String>,
423    /// The external key: a string literal, or a `$param` the write resolves
424    /// (and requires to be a string) before it runs.
425    pub key: Option<Val>,
426    pub props: Vec<(String, Val)>,
427}
428
429/// A relationship in a CREATE: a required type, a direction (`->`/`<-`; never
430/// undirected — dr-strange edges are directed), and inline properties.
431#[derive(Debug, Clone, PartialEq)]
432pub struct CreateRel {
433    pub dir: Dir,
434    pub ty: String,
435    pub props: Vec<(String, Val)>,
436}