1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
//! dr-strange's query language: an openCypher subset that parses into core's
//! `LogicalPlan` (arch/00 §5 — "a Cypher/GQL-subset parser lands in v2 on top
//! of the same logical plan layer"). The `nom` grammar and the plan compiler
//! live here so the parser-combinator dependency never reaches the lean core;
//! the crate depends on core only for the plan types it targets.
//!
//! ```text
//! MATCH (p:Paper)-[:CITES]->(q:Paper)
//! WHERE p.year >= 2020
//! RETURN q
//! ORDER BY q.year DESC
//! LIMIT 10
//! ```
//!
//! # Supported
//! Every source below binds one node pattern and may continue with a
//! relationship tail, so a typed hop chains off a retrieval seed exactly as it
//! does off a `MATCH` node (ROADMAP §7).
//! - `MATCH` one linear path: `(a:Label)`, `-[:TYPE]->` / `<-[:TYPE]-` / `-[:T]-`
//! (in/out/both), bare `-->`/`--`, and bounded variable-length `-[:T*1..3]->`.
//! - **`SEARCH (v:Label) [ON prop] NEAR "text"|[..] [METRIC m] [TOPK k]`** — an
//! indexed vector seed (`Source::VectorTopK`). `"text"` is embedded server-side
//! via [`parse_with_embedder`]; `[..]` is a literal escape hatch. `ON` may be
//! omitted: every `NEAR` defaults to the `embedding` property, which is what
//! the digest pipeline writes.
//! - **`SEARCH (v:Label) ON prop MATCHING "text" [TOPK k]`** — a BM25 keyword
//! seed (`Source::KeywordTopK`) over a declared keyword index. Same verb as
//! the vector seed, different operator: `NEAR` compares meaning, `MATCHING`
//! compares words.
//! - **`HYBRID (v:Label) [VECTOR [ON p] NEAR q [METRIC m] [WEIGHT w]]
//! [KEYWORD ON p MATCHING "text" [WEIGHT w]]
//! [GRAPH HOPS h [DECAY d] [SEEDS n] [WEIGHT w]] [CANDIDATES n] [TOPK k]`** —
//! fused retrieval (`Source::Hybrid`); channels in any order, at least one of
//! VECTOR/KEYWORD.
//! - **`CALL <pagerank|components|shortest_path|louvain>(arg: v, …) ON (v[:Label])`**
//! — a graph algorithm as a source (`Source::Algo`). `ON` both scopes the
//! algorithm and binds the variable; the per-node result rides `score()`.
//! - **`BEAM (result[:Label]) <OUT|IN|BOTH> [:TYPE] ON prop NEAR "text"|[..]
//! [METRIC m] WIDTH w DEPTH d`** — similarity-guided beam traversal
//! (`Step::ExpandBeam`) from the current frontier; chains after any source.
//! - `WHERE` over `=,<>,!=,<,<=,>,>=`, `AND`/`OR`/`NOT`, `+ - * /`, `IS [NOT] NULL`,
//! `x IN [a, b]`, property access `a.key`, the label predicate `a:Label`, the
//! external key `key(a)`, and the scoring terms `score()`, `hops()`,
//! `similarity(a.prop, "text"|[..][, metric])`, `distance(...)` (usable in
//! `ORDER BY` too — a brute-force rank).
//! - `RETURN [DISTINCT] <var|*>` — node records — or a **projection**:
//! `RETURN a.name, count(*) AS n, …`, each item a column named by its alias
//! or its own text, over any variable the pattern bound (an earlier one
//! compiles to `Expr::At`). Folds: `count`/`sum`/`avg`/`min`/`max`/`collect`,
//! each optionally `DISTINCT`, grouped by every column that isn't one.
//! - `ORDER BY expr [ASC|DESC], …`, `SKIP n`, `LIMIT n` — steps over node rows;
//! on the projection tail otherwise, where `ORDER BY` names a returned column
//! (by alias, by the expression it returned, or by the aggregate it folds:
//! `ORDER BY count(*)`).
//! - **`AS OF <seq|"RFC-3339"|TIME <ms>>`** — last clause; reads a past
//! snapshot (native backend). Not a plan node: it rides on [`ReadQuery`] for
//! the surface to apply with `PlaneHandle::as_of`.
//!
//! `key(n) = "…"` (or `key(n) IN […]`) on a scanned source compiles to a
//! `SeekKeys` seek rather than a scan-and-filter — an index lookup, so an LLM
//! can anchor on an entity it knows by key.
//!
//! # Writes (via [`parse_statement`], applied with [`WriteStatement::apply`])
//! - `CREATE (n:L {k: v, …}), (a)-[:T {…}]->(b), …` — a string `key:` sets the
//! external key; edges are directed (`->`/`<-`).
//! - `MERGE (n:L {key: "…", …}) [ON CREATE SET …] [ON MATCH SET …]` — upsert a
//! node by its external key; a path `MERGE (a {key})-[:T]->(b {key})` upserts
//! each keyed node and ensures the edge (idempotent, element-wise — also
//! against what earlier clauses of the same statement created).
//! - `MATCH pattern [WHERE …] SET n.p = v, n:Label, n += {…}` /
//! `REMOVE n.p, n:Label` / `[DETACH] DELETE n` — find-then-mutate on the
//! pattern's terminal variable, once per distinct matched node however many
//! paths reach it (plain `DELETE` refuses a connected node).
//! - `MATCH pattern [WHERE …] CREATE (n)-[:T]->(x {…})` /
//! `MERGE (n)-[:T]->(x {key})` — once per matched row, with the terminal
//! variable pre-bound so `(n)` anchors to the matched node.
//!
//! # Parameters
//! `$name` placeholders in value positions (WHERE/ORDER literals, SET/CREATE/
//! MERGE props, and a node's `key:`) are resolved from a caller-supplied
//! [`Params`] map via [`parse_statement_full`] — the SDK-safe way to pass
//! values (no string interpolation).
//!
//! # Null
//! Core's evaluator is two-valued: a missing property makes a predicate
//! false, so left alone `n.p <> 1`, `NOT n.p = 1` and `NOT n.p IN [..]` would
//! be *true* for a node with no `p`. The compiler guards those — `<>`, `NOT`,
//! and `=` with no constant side — with `IS NOT NULL` on every property (or
//! channel, or literal `null`) they read, so for a predicate over one
//! property the rows match openCypher: a missing value satisfies neither the
//! test nor its negation, `n.p = null` keeps nothing, and `IS [NOT] NULL` is
//! how absence is asked about. Where this is stricter than openCypher: under
//! `NOT`, a compound predicate whose false branch would have absorbed a null
//! (`NOT (n.a = 1 AND n.b = 2)` keeps a node with `b = 3` and no `a` there,
//! not here); and `x IN [1, null]` is null here even when `x = 1`. Ordered
//! comparisons, `IN` and the string predicates are already false on a
//! missing value and stay unguarded.
//!
//! # Lexical rules
//! Keywords and function names are case-insensitive. Identifiers are Unicode
//! words, or anything between backticks. Strings take either quote with the
//! escapes `\'` `\"` `\\` `\n` `\t` `\r` `\b` `\f` `\uXXXX`; numbers are
//! ints or floats (`1.5`, `1e9`). Expressions nest at most
//! `parse::MAX_NESTING` levels, an operator of an `AND`/`OR`/arithmetic chain
//! counting one; deeper is a syntax error, not a stack overflow.
//!
//! # Not yet (each a clear error naming the rewrite, never a silent miscompile)
//! - cross-variable predicates (`p.year < q.year`);
//! - returning the *rows* of a non-terminal variable (`RETURN p` after a hop);
//! its values project (`RETURN p.name`);
//! - `WITH` pipelining: a projection is a tail, so nothing follows it;
//! - unbounded variable-length (`*`, `*n..`);
//! - a second `MATCH`, `OPTIONAL MATCH`, `UNION`, `UNWIND`, and a pattern with
//! several paths (`(a)-->(b), (a)-->(c)`): a query holds one linear path —
//! run one query per pattern or branch;
//! - relationship variables (`-[r:T]->`): an edge can't be bound, returned or
//! filtered on — drop the variable;
//! - inline property predicates in a `MATCH` node (`(n {name: "x"})`): write
//! them in `WHERE`;
//! - list and map literals as values (`RETURN [1, 2]`, `n.x = {a: 1}`); a
//! list is only sugar after `IN` (and a vector after `NEAR`), a map only a
//! `CREATE`/`SET` property map;
//! - the `%` and `^` operators.
//!
//! Errors point at the fault: a mistyped clause is reported from the reading
//! of the statement that got furthest, not from the first token.
//!
//! # Completing a prefix
//! [`complete`] answers the other question an editor asks: not "is this a
//! statement" but "what could this become". It reads a half-typed query
//! against a plane's [`Vocab`] — its labels, edge types and properties, with
//! the counts — so the guess after `MATCH ` is the label that plane holds most
//! of, and the guess after a node is the edge that most often leaves it. It is
//! a second, smaller reading of this same grammar; see [`complete`] for why a
//! `nom` parser cannot do it.
use AHashMap;
use ;
pub use AsOfSpec;
pub use ;
pub use grammar_hint;
pub use ;
/// Values for `$name` placeholders in a query, supplied by the caller and
/// resolved at parse time (so no string interpolation — the SDK-safe way to
/// pass values). Keyed by the bare name (no `$`).
pub type Params = ;
/// Resolve a `$name` placeholder against the caller's params.
pub
/// A parsed statement: a read (a runnable plan) or a write (applied with
/// [`WriteStatement::apply`]). Surfaces branch on this to run a query.
/// A compiled read: the plan to run, and the snapshot to run it against.
///
/// `AS OF` is not a plan node — it addresses the *plane handle* — so it rides
/// alongside. A surface applies it with `PlaneHandle::as_of` before running
/// the plan; `None` reads the latest commit.
/// Resolves the text in a `SEARCH … NEAR "text"` (or `similarity(p, "text")`)
/// into a query vector. The pure parser owns no embedding machinery — a
/// surface (web/MCP/CLI) supplies this, backed by the server's configured
/// provider (API key from the environment, never the client). Keeping it a
/// one-method trait keeps `dr-strange-parser` dependency-free.
/// Why a query didn't turn into a plan.
/// Parse a *read* query into a runnable [`ReadQuery`]. A write statement
/// (`CREATE`, …) is an error here — use [`parse_statement`]. A text
/// `SEARCH … NEAR "…"` also errors (no embedder); use [`parse_with_embedder`]
/// or a literal vector.
/// Like [`parse`], but resolves text `NEAR "…"` terms into query vectors via
/// `embedder` — the semantic-search entry point the surfaces use for reads.
/// Parse a statement — either a read query or a write (`CREATE`, …). This is the
/// entry point a surface uses when it wants to run *either*.
/// Like [`parse_statement`], but resolves a text `SEARCH … NEAR "…"` via `embedder`.
/// The general entry point: parse a statement, resolving text `SEARCH` via an
/// optional `embedder` and `$name` placeholders via `params`. The surfaces that
/// accept parameters (CLI/MCP/plane.cypher) use this.
/// Turn the grammar's failure into the error a caller sees. A recognised but
/// unsupported shape (`parse::unsupported`) is a `Compile` error carrying its
/// rewrite hint; anything else is a `Syntax` error located by `describe`.