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}