1pub const QUERY_SYNTAX: &str = r##"# query — OQX syntax reference
9
10The `query` tool takes ONE plain OQX string (+ optional `limit`/`cursor`, and
11`bare: true` to drop the automatic `_id`/`_path` envelope — needs a `select`).
12There is no {from, filter, order} envelope: every concern is a clause of the
13string. Two rules tell the two syntaxes apart: DOT navigation belongs to the
14host object model (`_doc.layer`, `_section._blocks`, `_dst._title`); WHITESPACE
15query directives belong to OQX — a nested query is `<receiver> collect|exists|
16count|first|single { <block> }` (a postfix directive + a `{ … }` block), never a
17method. Its distinctive power is the receiver-constrained nested query that
18correlates to the current row: `from docs where _nodes exists { where _kind ==
19"md:task" }` returns only the docs that themselves contain a matching node.
20
21 [select <items>] from <target> [where <pred>] [frontier <pred>] [follow <dest> [{ … }], …]
22 [flatten <expr> [{ … }] [as <name>]]… [group by <key>, …] [having <pred>]
23 [order by <expr> [asc|desc], …] [limit N] [offset N]
24 <target> count|exists|none|first|single { <block> } (scalar/one-row form: a bare target at the root scope)
25
26CLAUSE ORDER IS FIXED: select, from, where, frontier, follow, flatten, group by, having, order by, limit, offset —
27each at most once (`flatten` may repeat); an out-of-order clause is a parse error naming the order.
28Only `select` may drop its keyword, and only as the first clause
29(`_path, era from docs where …`). A top-level predicate needs `where`, because
30`where` follows `from` (`era > 1600 from docs` is the clause-order error); a
31block may LEAD with a predicate (see Sugar), but a predicate after a projection
32still needs the keyword (`{ _name where _kind == "md:task" }`, not
33`{ _name, _kind == "md:task" }`); `from docs count` is an error (write
34`docs count { … }`). `where` and later `select` items may use the same body's
35`select` aliases (`select _path, old: era < 1000 from docs where old`; an alias
36shadows a same-named field there). `and` / `or` / `not` are the connectives;
37the symbols `&&` / `||` / `!` are accepted synonyms (the words are canonical and
38what `print` writes).
39
40## Sugar (OQX 0.17)
41
42Every form here is shorthand for an explicit directive (the AST is the explicit form):
43 nodes { _kind == "md:task" } ≡ nodes collect { where _kind == "md:task" } — a receiver block with
44 no consumer is collect; a block whose LEADING expression is a predicate
45 (comparison, call, !/is/not, in, literal, (…), a consumer test — anything
46 but a bare name, a dotted path or a ^lift) is where-first. Names still
47 project: _nodes { _name } ≡ _nodes collect { _name }; filter a bare field with
48 _nodes { is checked }. Wherever a value stands (0.20), call arguments included:
49 size(_children { is checked }); x distinct { … } ≡ x collect distinct { … }. After a
50 follow destination too: follow _children { depth 2 } is that destination's block.
51 (a, b) a tuple (0.20): two or more expressions, concatenated one level flat — a value
52 (size((tags, aliases)), "x" in (tags, aliases)) and a receiver ((a, b) { … },
53 follow (before, after) { depth 2 }); (a) alone is grouping
54 refs(x)[0] ≡ refs(x) first { offset 0 } positional (integer literal or ${binding});
55 out of range ⇒ absent; refs(x)[0]._path navigates the row
56 ^docs[_path == ^company] ≡ ^docs first { where _path == ^company } first match or absent
57 ^docs[_path == ^company]! ≡ … single { … } required: exactly one, else filter_invalid
58 _title! required: _title, or an error naming the expression (and the row's $key — its _id)
59 — never a filter, never a coercion (0!, ""! are values); tightest: a!.b vs a.b!
60 is x / not x truthiness of x, and its negation (prefix; `not` ≡ `!`)
61 x is y / x is not y identity ($key: a store row's id, else structural) — compares rows, which == does not;
62 x is null = absent; for scalars is ≡ ==; comparison precedence, no chaining
63 a and b / a or b the connectives (≡ && / ||: same precedence, short-circuit, value: title or _title coalesces)
64 boss: ^docs[_path == ^manager], bossName: boss._title a select item may use the items to its LEFT
65 Reserved words (never a bare field name): from where select is not and or, true false null.
66 Contextual words (limit order offset follow collect count first distinct desc by in frontier depth group having
67 flatten as) parse as fields in expression position (select o: order from docs).
68
69Results are LEAN hits — { _id, _path } + whatever `select` projects (or `values`,
70`count`, `exists`, `none`). The envelope keys ARE the attributes: copy a hit's
71`_id` straight into `where _id == "…"` (no translation); `select _id, _path` is
72simply the envelope (no duplicate columns); an alias `_id:`/`_path:` on any OTHER
73expression is refused — `select _path: _doc._path from blocks` fails ("projection
74`_path` collides with the hit envelope — alias it (`p: _doc._path`)"); a row
75surfacing as a value (an empty `collect { }`) is the same `{ _id, _path }`. Every
76other tool spells a row the same way (`docs_read` → `{ _id, _path, rev, properties,
77_raw }`, `blocks_read` → `{ _id, _type, _text, _raw, _hash, … }`, `docs_list` → `{ _id,
78_path, _updated_at, blocks }`, a revision → `{ _id, _path, _hash }`): hydrate by `_id`
79via blocks_read/docs_read.
80
81## Paths
82
83Every path the surface returns is `/`-rooted — the form a reference is
84written in (`[x](/projects/oqx.md)`, `before: [/timeline/kickoff.md]`): a
85hit's `_path`, an edge's `_dst_path`, every tool's paths. Every path it
86accepts tolerates both forms (`/a.md` or `a.md`): tool arguments, `refs(x)`,
87`within(...)`, and a string LITERAL compared with `_path`/`_dst_path` (on the row,
88or through a row handle: `_doc._path`, `_dst._path`, `$it._path`) by `==`/`!=` or
89passed to their `.startsWith(...)` (it is rooted first, so `_path == "a.md"` and
90`_path == "/a.md"` both match). A property holding a reference compares directly —
91and is never rooted: `where ^_path in list(after)`, `where customer == ^_path`,
92`where path == "/x.md"` (YOUR authored `path:` key — the bare name — compared as
93written) — no `"/" + ...` glue.
94
95## Targets & field namespaces
96
97THE NAME RULE — one rule on every record kind: a BARE name is YOURS (an authored
98property: frontmatter + inline on a doc, an attrs key on a block or node), an
99UNDERSCORE-prefixed name is the ENGINE's (however derived: `_id`, `_path`,
100`_title`, `_updated_at`, `_doc`), and `$` belongs to OQX — $it, $key, $depth,
101$stop, $leaf, $frontier, $ordinal are values the LANGUAGE injects into a scope;
102omgbase adds NO $ names (a `$id` in a query is an unknown name, absent). Nothing
103bare is reserved: `title`, `tags`, `path`, `id`, `format`, `doc` are your keys
104when you wrote them. Collections stay bare (`from docs`, `^docs`, `docs count { … }`,
105`entries(^$it)`) — `from` names a collection, not a row property — and so do the
106row functions in call position (`text("x")`, `under(…)`, `refs(x)`): a call is
107never an entry read. There is NO self alias (the 4.x `doc`/`block`/`section`):
108`$it` is the self, and `_doc`/`_block`/`_dst` reach ANOTHER row. So `_path` is the
109engine's /-rooted location and `path` your `path:` key; `_tags` is your tags ∪ the
110body #hashtags and `tags` the authored list alone; `_title` the computed title (the
111first H1) and `title` your `title:`; `_data.<k>` ≡ `<k>` (the merged authored view —
112kept for `entries(_data)` and to reach an authored key that itself starts with
113`_`), `_frontmatter.<k>` / `_inline.<k>` one source each. Same value, same name
114everywhere: a hit's `_id` is docs_read's `_id` and the `_id` argument of blocks_read.
115
116 Engine names per kind (attributes | handles | relations):
117 docs _id _path _raw _hash _updated_at _created_at _title _tags _format | _data _frontmatter _inline | _blocks _nodes _out_links _in_links _revisions
118 blocks _id _path _doc_id _raw _hash _updated_at _created_at _type _text _attrs _depth _ordinal | _doc | _children _nodes _out_links _section
119 nodes _id _path _doc_id _block_id _kind _name _value _attrs | _doc _block | _blocks _subsections _children
120 links _id _path _doc_id _block_id _via_id _dst_id _dst_path _uri _field _dst_kind _predicate _provenance _anchor | _doc _dst _block | —
121 revisions _id _path _rev _seq _doc_id _commit _ts _origin _actor _reason _hash _is_current _archived | _doc | —
122 root _id (^_id, 0^_id) | — | docs blocks nodes links revisions
123 Every bare name is yours; `_attrs.<k>` on a block or node is the same read as
124 the bare attrs key `<k>`.
125 UNKNOWN NAMES: a bare name that no object of the kind in this repository
126 carries is a property read that yields absent — on docs/blocks/nodes the result
127 gains `hints` (one line per name, the nearest match drawn from your keys and the
128 engine names: "no document in this repository carries a property `title`; did
129 you mean `_title`?") and the rows still come back; in a plan/apply/execute
130 selector it is refused. On links and revisions (closed: no authored data reaches
131 them) a bare name is a filter_invalid error. A `_x` that is no engine name of
132 the kind is filter_invalid naming the kind's engine names. A property some
133 objects carry is known (partial presence is normal).
134
135docs:
136 - bare identifier = a document PROPERTY (frontmatter + inline `key:: value`,
137 unioned; nested via dots: meta.owner). YAML/JSON docs
138 expose the parsed object's keys the same way. Nothing is
139 reserved: `title`, `tags`, `path`, `id` are your keys.
140 - source handles = _data.<k> (the merged authored view — the same read as the
141 bare key) / _frontmatter.<k> / _inline.<k>; the whole bag
142 as a collection via entries(_data) / entries(_frontmatter)
143 / entries(_inline) (see entries()).
144 - attributes = _id, _path (/-rooted, see Paths), _updated_at (ISO-8601,
145 sorts chronologically), _created_at, _raw (the whole file,
146 frontmatter included), _hash (its sha256, 64 hex), _title
147 (the computed title: the first H1; `title` is the authored
148 key), _tags (authored tags ∪ body #hashtags, deduped,
149 authored first; `tags` is the authored list), _format (the
150 doc's format — "markdown"/"yaml"/"json"; present on EVERY
151 doc, so has(_format) is always true and `_format == "yaml"`
152 is the filter).
153 - relations = _nodes, _blocks, _out_links / _in_links (a doc's links as
154 rows; the documents at their other end are
155 _out_links collect distinct { from _dst } and
156 _in_links collect distinct { from _doc } — there is no
157 out / in relation), _revisions (the document's stored
158 revisions, oldest first — see revisions below).
159
160blocks:
161 - bare fields = every attrs key FLATTENED onto the row (bare `checked`;
162 `_attrs.<k>` is the same read; absent ⇒ false) — a block's
163 authored side.
164 - attributes = _id, _path (the owning doc's path), _doc_id (the owning
165 doc), _type, _text (normalized), _ordinal (0-based among
166 siblings), _depth (0 at the top level), _raw (the block's
167 raw source — a container's raw holds its children), _hash
168 (its sha256 — the value update/split expect as
169 expect.content_hash), _updated_at, _created_at, _attrs.
170 - reach-through = _doc.<key> / _doc._path / _doc._id / _doc._format = the
171 CONTAINING doc (_doc._id and _doc._path cost nothing — they
172 read the row's own _doc_id / joined path, no lookup).
173 - relations = _children, _nodes, _out_links, _section (no self alias:
174 the 4.x `block.children` is `_children`).
175 NOTE: inside a `follow`, $depth/$ordinal are ALWAYS the walk's metadata; a
176 block's own nesting is `_depth` / `_ordinal` everywhere.
177
178nodes:
179 - bare fields = the attrs keys flattened onto the row (checked, level,
180 first_ordinal; `_attrs.<k>` is the same read).
181 - attributes = _id, _path, _doc_id, _block_id (null for a block-less
182 node), _kind (md:task / md:link / md:section / yaml:…),
183 _name, _value, _attrs.
184 - reach-through = _doc.<key> / _doc._id / _doc._path, _block._type /
185 _block._text / _block._id / _block._path.
186 - relations = _blocks, _children, _subsections (a section node's; the
187 4.x `section.blocks` is `_blocks`).
188
189links: (the authored link graph as rows — one per open link: the relation a link
190 establishes; the md:link NODE is its occurrence in the text and _via_id
191 points at it — a frontmatter link has a row and no node)
192 - attributes = _id, _path (the SOURCE doc's path), _doc_id (the SOURCE
193 doc), _block_id (the source block; null for a frontmatter
194 edge), _via_id, _dst_id (the destination node id: a doc id,
195 an external node id or phantom:<path>), _dst_path (target
196 doc path, /-rooted; NULL when dangling/external), _uri
197 (external URL; NULL otherwise), _predicate
198 ("references"/"embeds"/freeform), _provenance
199 ("link"/"frontmatter"/"inline_field"/…), _dst_kind
200 ("document"/"external"/"collection"), _anchor, _field (the
201 frontmatter key / inline field it was authored in).
202 - reach-through = _doc.<key> (the source document), _dst.<key> (the
203 destination document; absent when dangling/external),
204 _block.<key> (the source block). Closed: links carry no
205 authored data, so a bare name is filter_invalid. There is
206 no from_commit: a query sees open edges only (history is
207 blocks_history/changes_since).
208 - text()/semantic() are N/A on links.
209
210revisions: (history as rows — one per stored revision of every document of the
211 repository, deleted documents included: what docs_history lists, queryable)
212 - attributes = _id (the revision id, r_…), _rev (= _id), _seq (1, 2, … per
213 document), _doc_id, _commit, _ts (the commit's time, sorts
214 chronologically), _origin ("observed"/"api"/"import"/
215 "projection"), _actor, _reason, _hash (the rendered file's
216 sha256 — docs_history's _hash), _is_current (true for the
217 document's current revision), _archived (true when the
218 bodies moved to an archive segment: the row stays,
219 docs_read { at } reads through the segment), _path (the
220 path the revision was committed under — a move re-paths
221 only the current revision, so earlier ones keep the old
222 path).
223 - reach-through = _doc.<key> reaches the document's CURRENT row; absent for
224 a deleted document, so where _doc == null is the revisions
225 of deleted documents. Closed: a bare name is filter_invalid.
226 - on docs = _revisions is a relation (oldest first): a document with
227 more than one version is docs { _revisions count { } > 1 }.
228 - text()/semantic() are N/A on revisions. The body as of a revision is
229 docs_read { _path, at: "<rev>" } (docs_read_at is the same call).
230
231## Scoping (ADR-015; the root row since surface 2.0): bare names are LOCAL
232
233 name the CURRENT row only — never falls through to an enclosing row
234 or the repository; an absent field is simply absent.
235 ^name one scope OUT (the enclosing row's fields/intrinsics/lifts);
236 ^^name two scopes out. `^_path` = the enclosing row's path.
237 ^docs ^nodes ^blocks ^links ^revisions
238 THE ROOT ROW (surface 2.0, oqx 0.18): the repository is the root
239 scope's row, one scope out from a top-level row, so ^docs there is
240 the whole documents scan (uncorrelated until you add a ^ predicate);
241 one caret per enclosing block (^^docs from depth two), or the
242 absolute 0^docs from ANY depth. ^_id / 0^_id = the repository id.
243 ^$it = the root object: ^$it.docs ≡ ^docs, entries(^$it) names the
244 five collections. At the root scope itself a bare target is the scan
245 (docs count { … }, from docs, entries(docs)).
246 ERRORS: a bare `docs` INSIDE a block reads a property of the current
247 row, which has none — refused ("did you mean ^docs"); so is a bare
248 `blocks`/`nodes`/`revisions` there (a doc's own relations are
249 `_blocks`/`_nodes`/`_revisions`); `^docs` AT THE TOP LEVEL reaches
250 past the root — refused (write the bare `docs`); so is `$repo`
251 (surface < 2.0; the message names the replacement).
252 $it the current item itself (a row, or each ELEMENT when the
253 receiver is a list property); ^$it = the enclosing row, ^$it._id
254 its id. $value is NOT an intrinsic (since oqx 0.15): it reads a
255 property literally named $value, absent when there is none.
256 $key the row's identity (0.20): a store row's id (doc/block/node/edge), an entries(x)
257 entry's key, a follow occurrence's walk identity; project $key: <expr> to rekey the
258 rows a block yields (a destination block's $key: is not a column).
259
260## Scalar semantics (@omgbase/oqx)
261
262 comparisons == != < <= > >= strict & typed (5 == "5" is false); either
263 side may be a field (field-vs-field OK).
264 absence null and a missing field are the same "absent" value; two absents
265 are EQUAL, so `absent != "x"` is TRUE and `absent == null` is TRUE.
266 Relational (< <= > >=) on an absent operand → FALSE. Bare `f` in
267 boolean position uses JS truthiness (absent/false/0/"" ⇒ false;
268 NOTE: "false", [] and {} are truthy). has(f) = explicit presence.
269 boolean and or not grouping ( … ) — && || ! are accepted synonyms
270 arithmetic + - * / % (+ concatenates if either side is a string)
271 membership x in y array → typed membership; string → substring;
272 object → key exists; range → coverage. Any list
273 property works directly: "pricing" in tags. list(f)
274 coerces scalar-or-list (absent → []) — use it when a
275 key is sometimes scalar, sometimes a list.
276 ranges lo..hi (inclusive) lo...hi (exclusive hi) ..hi lo..
277 era in 800..1680; works over ISO dates. range(prop) reads a
278 range-shaped STRING property as an interval: "2026-01-15" in range(window).
279 strings x.contains("s") x.startsWith("p") x.endsWith("s") x.matches("^re$")
280 (real regex) x.lower() x.upper() size(x). ALL CASE-SENSITIVE —
281 fold with .lower(): _title.lower().contains("aurora").
282 Infix regex: x ~ "re" is x.matches("re"); x ~* "re" is the
283 case-insensitive match; x !~ "re" and x !~* "re" negate them.
284 Unanchored unless the pattern anchors; an absent x is false
285 under ~ and TRUE under !~ (not SQL's NULL).
286 functions has(f) size(x) list(x) entries(x) range(s)
287 dates a date is an ISO string, a duration a NUMBER OF SECONDS; arithmetic
288 is METHODS, never + or - (+ concatenates strings).
289 date("today"|"now"|"yesterday"|"tomorrow"|"sow"|"eow"|"som"|"eom"|
290 "soy"|"eoy") — the host clock, UTC; date("2026-10-15") canonical ISO
291 (absent if unparseable). dur("1h30m") dur("2 days, 3 hours")
292 dur("P1DT2H") dur(90) → seconds (year = 365 d, month = 30 d).
293 d.plus(dur("30 days")) d.minus(86400) d.plus("1 month") (calendar:
294 Jan 31 → Feb 28) a.until(b) → signed seconds d.year() d.month()
295 d.day() d.weekday() (1 = Mon) d.hour() d.minute() d.second()
296 d.date() d.format("EEE dd MMM yyyy") n.dur_text() → "1h 30m"
297 n.dur_iso() → "PT1H30M". Total: a non-date is absent, so
298 `where date(due) < date("today").minus(dur("30 days"))` just
299 filters. A datetime is never < its own date: compare
300 _updated_at.date() == date("today"), or < date("tomorrow").
301 literals "str" 'str' 123 1.5 true false null
302 determinism no random; the only clock is date("today"/"now"/…), read once per
303 query. Lists/structs are not literals; ternary is not supported.
304 Errors surface as filter_invalid.
305 scalars vs lists a list-authored key never == a scalar (tags == "x" is false
306 when tags is a list) — use `"x" in tags` / `in list(tags)`.
307
308## Domain predicates (omgbase)
309
310 text("terms") FTS5 PRUNING predicate (docs match via their blocks, nodes
311 via the node index); composes anywhere, incl. nested scopes.
312 semantic("phrase") embedding cosine SCORE (docs/blocks): threshold it
313 (semantic("x") > 0.6), project it, or `order by semantic("x")
314 desc` for top-K. Needs a provider that has embedded the repository; else provider_unavailable.
315 blocks target only:
316 under(id_or_heading) block in the containment subtree / section range
317 under_heading("Launch") an ancestor section heading contains the text (ci)
318 under_kind(type[, name]) an ancestor block has that type (name: its text
319 contains / its key equals name)
320 within("path" | "d_..") containing doc = id, exact path, or path glob (*);
321 either path form
322 yaml_path("a.b") YAML docs: the block at that key path
323 json_pointer("#/a/b") JSON docs: the block at that pointer
324 has_edge("pred"[, target]) an open authored edge with that predicate leaves here
325 has_anchor() block carries an authored ^ref
326 parent_type() == "x" parent block's type (must be compared)
327 child_count() > 0 number of direct children (must be compared)
328 refs(x) the live documents a property's document references name:
329 x is a string, a list or absent; each "/a/b.md", "a/b.md"
330 or "d_…" element resolves to that doc's row; dangling and
331 non-string elements are dropped. Yields docs rows, so it is
332 a source, a receiver or a follow destination:
333 follow refs(before), refs(after) walks a timeline both ways.
334 The reverse direction needs no function: a document's
335 _path is already the reference form, so
336 ^docs { where ^_path in list(after) } is
337 "the documents whose `after` names me".
338 A HIT IS A STORE ROW: a top-level row that is not a doc/block/node/edge fails
339 (filter_invalid). `follow before` over a list of paths
340 reaches the STRINGS — write `follow refs(before)`.
341
342## Nested queries & consumers
343
344 <receiver> collect|exists|none|count|first|single { <block> }
345 receiver = a relation (_nodes, _blocks, _doc._out_links, _section._blocks, …), a
346 reach-through row (_dst, _doc: one row — _dst { _title }), a
347 list property (tags), entries(x), or a root collection (^docs; 0^docs at depth ≥ 2).
348 block = [select …] [from E] [where …] [frontier …] [follow …] [flatten …] [group by …] [having …] [order by …] [limit N] [offset N]
349 — the same fixed order as the top level (`from` optional; the
350 receiver supplies the rows). A leading name/alias list is the
351 projection (`select` dropped); a predicate always needs `where`.
352 A `from E` inside a block RE-PROJECTS the rows through a relation
353 (a typed flatMap): `docs collect { from _nodes where _kind == "md:task" }`
354 starts at docs, projects each through `_nodes`, and queries the
355 resulting nodes; a scalar relation (`from _doc`) contributes one
356 row; a missing or non-navigable relation is a loud error, never a
357 global scan.
358 exists ≥1 row _nodes exists { where _kind == "md:task" and not checked }
359 none 0 rows _nodes none { where _kind == "md:task" and not checked } ("every task done")
360 count a number _nodes count { where _kind == "md:task" } >= 2
361 collect rows (default) — in select for nested results; in where with a lift
362 first / single zero-or-one (single errors on >1) — lookups in select
363 distinct on any consumer: dedup by projected value — _nodes count distinct { select _kind }
364 limit/offset bound the block's rows AFTER where/order/distinct, BEFORE the
365 consumer reduces: _nodes exists { offset 1 } = at least two;
366 first { … offset 1 } = the second.
367 lift `^name:` in a where-collect binds values outward:
368 select _path, open from docs where _nodes collect { ^open: _value where _kind == "md:task" and not checked }
369 joins ^<t> exists { where slug == ^ref } semi-join; not … exists anti-join;
370 ^nodes single { where _kind == "person" and _attrs.id == ^owner_id } lookup;
371 x in ^keys membership over a lifted set.
372
373## select — projection
374
375 select a, alias: expr, nested: _nodes collect { … }
376 bare key → your property (scalar-authored → scalar; list → array); _name →
377 the engine's field (_id, _path, _title, …); _data.<k> is the bare key's read;
378 a call/arithmetic needs an alias (n: size(tags)) unless in values mode;
379 an output key of _id/_path other than the attribute itself needs an alias.
380 _raw whole file bytes (docs) — docs_read is cheaper for one doc.
381 distinct select distinct _type (dedup hits by projected value)
382 values select <ONE item> values → bare values instead of records:
383 select distinct type values from docs → values: ["hub","lab-note",…]
384 select _path values from docs → values: ["/index.md", …]
385 tags: tags collect { $it values } → a plain array
386 h: _nodes first { _name values where _kind == "md:section" order by first_ordinal }
387 entries(x) a record as a collection ($key / $it per entry), key order:
388 select fm: entries(_data) collect { k: $key, v: $it }
389 where entries(_frontmatter) exists { where $key == "era" and $it > 1600 }
390 entries(_inline), entries(_attrs), a nested map, or a list (index keys)
391 $semantic_score is not a field — project semantic("…") under an alias instead.
392
393## flatten (OQX 0.25)
394
395 flatten <expr> [{ … }] [as <name>] after follow, before group by; may repeat
396 Splits one value into rows. Every hit is still the PARENT row — its _id/_path
397 repeat per element, as Dataview's TABLE … FLATTEN — with <name> bound beside
398 its fields at the same depth: an array → one row per element (an empty list
399 yields none), a missing field → one row with the name absent (never dropped),
400 an object or a scalar → one row with the value. A plain navigation names
401 itself (flatten tags rebinds tags to each element); anything else needs as.
402 The block is the ordinary receiver block over the elements (where, a
403 projection, order by, limit, distinct), applied first. Later clauses read the
404 name — a later flatten (flatten jobs as j flatten j.tags as t), group by,
405 having, select, order by; where runs before it. A flattened query pages by
406 position (cursor null); as _id / as _path is refused (the envelope reads them).
407 select _path, tags from docs where type == "process" flatten tags order by _path, tags
408 select _path, t: b._text from docs flatten _blocks { where _type == "task" and not checked } as b
409 select tags, n: count() from docs flatten tags group by tags order by count() desc
410
411## order by, limit/offset, pagination
412
413 order by era desc, _path asc absent values sort LAST (asc); ties → (_path, _id)
414 limit N / offset N define the result SET (integer literals);
415 the tool's limit/cursor then page WITHIN it
416 cursor opaque keyset on (_path, _id); a custom order by
417 disables it (you get the top page + truncated)
418
419## Graph traversal (follow) and links
420
421 The citation graph is the LINK ROWS walked through a destination block:
422 _dst is a link's destination document (absent for an external or dangling
423 link — it then contributes no successor), _doc its source.
424 from docs where <seed> follow _out_links collect { from _dst } docs the seed links TO
425 from docs where <seed> follow _in_links collect { from _doc } backlinks
426 follow _out_links collect { from _dst depth 3 } bound (1..8, default 8) — a clause of the block
427 follow _out_links collect { from _dst where layer == "canon" } keep only matching successors
428 follow _in_links collect { from _doc where ^_path in list(before) } ^ = the row being expanded
429 (^^ = the walk's enclosing scope)
430 follow _out_links collect { from _dst where _predicate == "depends_on" } one predicate only
431 follow _out_links collect { from _dst }, _in_links collect { from _doc }
432 several destinations: their union
433 (one step dedups by identity)
434 follow ^docs { where _out_links exists { where _dst_path == ^^_path } }
435 a destination block over the root's
436 documents, re-read per frontier row
437 (^ = that row) = the backlinks walk
438 follow _out_links collect { from _dst frontier type == "practitioner" } cut, keeping the frontier row
439 from docs where type == "process" frontier !verified follow _out_links collect { from _dst }
440 the BODY's frontier applies to the seeds:
441 an unverified seed is kept, not expanded
442 follow distinct _out_links collect { from _dst } / … { $key: group from _dst } identity control ($key)
443 follow refs(before), ^docs { where ^_path in list(after) } forward over `before`, backward
444 over every doc whose `after` names me
445 Per-occurrence metadata: $depth (seed = 1), $stop (interior|leaf|frontier|
446 depth|cycle), $leaf/$frontier, $ordinal (a deterministic 1..N rank over the
447 walk, ordered by depth then path — `order by $ordinal` for walk order,
448 `where $ordinal <= N` for a deterministic budget cut), $key — usable in
449 select/order by and the top-level post-walk where (a where conjunct that
450 reads one filters the walk's RESULT; the other conjuncts remain the seed
451 predicate), NOT metadata of the candidates a destination block's where reads,
452 nor valid inside a consumer directive. Cyclic graphs are safe: a revisited
453 node is admitted once as a `$stop == "cycle"` occurrence and never
454 re-expanded. The default keeps one occurrence per distinct walk path;
455 `follow distinct` dedups reached rows by identity.
456 Destination blocks (OQX 0.20): a brace after ANY destination opens that
457 destination's block — _children { depth 2 }, refs(x) { depth 2 }, _out_links
458 collect { from _dst depth 2 }, ^docs { where ^_path in list(after) } — with the
459 0.17 body rules (where-first, projections) and two clauses of its own after
460 `where`: frontier <pred> (occurrences reached through this destination that
461 match are frontiers; ^ there is the walk's enclosing scope) and depth <n>
462 (this destination expands a frontier row only while $depth < n). The body
463 that carries the follow may write frontier <pred> too, after its where and
464 before follow: it applies to the SEEDS (read like the body's where) — a
465 matching seed stays in the result with $stop == "frontier" and is not
466 expanded; each destination's frontier is its own. There is no options block;
467 the old `by` is $key. A bare follow ^docs (no brace, no consumer) is an
468 error naming both block spellings.
469 Type-preserving relations: a block's _children; a section node's _children and
470 _subsections; the citation graph is _out_links collect { from _dst } / _in_links collect { from _doc }.
471 from links where _dst_path == "/notes/x.md" inbound links to a doc, as rows
472 select d: _dst._title, k: _dst_kind from links the destination document's title
473 from docs where _out_links exists { where _dst.layer == "draft" } links into drafts
474 select b: _in_links collect distinct { from _doc } from docs backlinks as { _id, _path }
475 select _doc_id, _dst_path from links where _predicate == "depends_on"
476 from docs where not _in_links exists { } orphans (nothing links in)
477 (The `graph` tool is a convenience wrapper that compiles to a follow query.)
478
479## semantic grain
480
481from docs ranks whole DOCUMENTS (one hit per file); from blocks ranks PASSAGES.
482text()/where prune the candidate set; only order by reweights.
483
484## Examples
485
486 from docs where layer == "working"
487 from docs where _path.startsWith("/guides/") and _updated_at >= "2026-08-01"
488 select layer, tags from docs where "pricing" in list(tags)
489 from docs where _inline.owner == "alice"
490 from docs where _title.lower().contains("q3 plan")
491 from docs where _format == "yaml"
492 from docs where type == "practitioner" order by era desc limit 2
493 from docs where _nodes none { where _kind == "md:task" and not checked }
494 from docs where tags exists { where $it == "tria-prima" }
495 select fm: entries(_data) collect { k: $key, v: $it } from docs where _path == "/x.md"
496 select owner_id, owner: ^nodes single { where _kind == "person" and _attrs.id == ^owner_id } from docs
497 from blocks where _type == "task" and not checked and under_heading("Launch") and _doc.layer == "working"
498 from blocks where _type == "paragraph" and has_edge("references", "d_92aaaaa")
499 select _path, $depth from docs where _path == "/timeline/review.md" follow refs(before), refs(after)
500 select _path, next: ^docs collect { _path where ^_path in list(before) } from docs where type == "milestone"
501 select _path, n: size(^docs), repo: ^_id from docs limit 1
502 select path, title from docs where has(path)
503 docs { type == "substance" and not verified }
504 select subject, process: ^docs[slug == ^subject]!._path from docs where type == "lab-note"
505 select _path, back: _nodes first { select p: 0^docs collect { _path where ^^_path in list(after) } values } from docs where type == "milestone"
506 select _path, prior: refs(before) collect { _path, when } from docs where type == "milestone"
507 from blocks order by semantic("identity preservation across edits") desc limit 10
508 select _uri from links where _dst_kind == "external"
509 docs count { where layer == "canon" }
510 select _seq, _origin, _ts, _hash from revisions where _path == "/processes/calcination.md" order by _seq desc
511 from docs where _revisions count { where _origin == "api" } > 0
512 select p: _path, d: _doc._path from revisions where _doc == null
513 select layer, n: count(), titles: collect(_title), open: rows count { where _nodes exists { where _kind == "md:task" and not checked } } from docs group by layer having count() > 1 order by count() desc
514 select _path, tag: tags from docs where "process" in tags flatten tags order by _path, tags
515
516Common mistake: `$path`/`$id` — omgbase has no $ names; the engine's location and
517identity are `_path`/`_id`. Another: the 4.x bare `path`/`id`/`title`/`tags` — a
518bare `path` is now YOUR `path:` key (absent when you wrote none, with a hint
519pointing at `_path`); the engine's are `_path`/`_id`/`_title`/`_tags`; `select
520data.path` is gone — write `path`. Another: `where tags == "x"` on a list-valued
521key is false — write `"x" in tags`. Another: `select _path: _doc._path` — alias it
522(`p: _doc._path`).
523"##;
524
525#[cfg(test)]
526mod tests {
527 use super::QUERY_SYNTAX;
539
540 const TARGETS: [&str; 4] = ["docs", "blocks", "nodes", "links"];
541 const CONSUMERS: [&str; 7] = [
542 "collect", "exists", "none", "count", "first", "single", "distinct",
543 ];
544
545 fn has_top_level_from(q: &str) -> bool {
546 let chars: Vec<char> = q.chars().collect();
547 let (mut depth, mut quote): (i32, Option<char>) = (0, None);
548 for (i, &c) in chars.iter().enumerate() {
549 if let Some(open) = quote {
550 if c == open {
551 quote = None;
552 }
553 continue;
554 }
555 match c {
556 '"' | '\'' => quote = Some(c),
557 '{' => depth += 1,
558 '}' => depth -= 1,
559 _ if depth == 0 && chars[i..].starts_with(&[' ', 'f', 'r', 'o', 'm', ' ']) => {
560 return true;
561 }
562 _ => {}
563 }
564 }
565 false
566 }
567
568 fn has_placeholder(q: &str) -> bool {
570 let chars: Vec<char> = q.chars().collect();
571 for (i, &c) in chars.iter().enumerate() {
572 if c != '<' || !chars.get(i + 1).is_some_and(|n| n.is_ascii_alphabetic()) {
573 continue;
574 }
575 let mut j = i + 1;
576 while j < chars.len()
577 && (chars[j].is_ascii_alphabetic() || matches!(chars[j], '_' | '|' | ' ' | '…'))
578 {
579 j += 1;
580 }
581 if chars.get(j) == Some(&'>') {
582 return true;
583 }
584 }
585 false
586 }
587
588 fn is_prose(q: &str) -> bool {
589 q.contains(". ")
590 || q.contains("; ")
591 || q.ends_with('.')
592 || q.ends_with(':')
593 || q.ends_with(';')
594 }
595
596 fn root_consumer(q: &str) -> bool {
597 let mut it = q.splitn(3, ' ');
598 let (Some(target), Some(next)) = (it.next(), it.next()) else {
599 return false;
600 };
601 TARGETS.contains(&target) && (CONSUMERS.contains(&next) || next.starts_with('{'))
602 }
603
604 fn extract(text: &str) -> (Vec<(usize, String, String)>, Vec<String>) {
606 let (mut queries, mut fragments) = (Vec::new(), Vec::new());
607 let mut section = String::new();
608 for (i, raw) in text.lines().enumerate() {
609 if let Some(h) = raw.strip_prefix("## ") {
610 section = h.trim().to_owned();
611 continue;
612 }
613 if !raw.starts_with(" ") || raw.trim().is_empty() {
614 continue;
615 }
616 let trimmed = raw.trim();
617 if section == "Examples" {
618 queries.push((i + 1, section.clone(), trimmed.to_owned()));
619 continue;
620 }
621 let query = trimmed.split(" ").next().unwrap_or("").trim();
622 if has_placeholder(query) || query.contains('…') || is_prose(query) {
623 continue;
624 }
625 if !(query.starts_with("select ") || query.starts_with("from ") || root_consumer(query))
626 {
627 continue;
628 }
629 if query.starts_with("select ") && !has_top_level_from(query) {
630 fragments.push(query.to_owned());
631 continue;
632 }
633 queries.push((i + 1, section.clone(), query.to_owned()));
634 }
635 (queries, fragments)
636 }
637
638 #[test]
639 fn extraction_is_substantial() {
640 let (queries, fragments) = extract(QUERY_SYNTAX);
641 let in_examples = queries.iter().filter(|q| q.1 == "Examples").count();
642 assert!(in_examples >= 20, "{in_examples} examples");
643 assert!(
644 queries.len() - in_examples >= 10,
645 "{} outside Examples",
646 queries.len() - in_examples
647 );
648 for f in &fragments {
649 assert!(
650 !TARGETS.iter().any(|t| f.contains(&format!(" from {t}"))),
651 "a fragment must not carry a top-level from: {f}"
652 );
653 }
654 }
655
656 #[test]
657 fn every_example_parses() {
658 let (queries, _) = extract(QUERY_SYNTAX);
659 let failures: Vec<String> = queries
660 .iter()
661 .filter_map(|(line, section, q)| {
662 oqx::parse_string(q)
663 .err()
664 .map(|e| format!("line {line} ({section}): {q}\n {e}"))
665 })
666 .collect();
667 assert!(
668 failures.is_empty(),
669 "query_syntax examples that do not parse:\n{}",
670 failures.join("\n")
671 );
672 }
673
674 #[test]
675 fn error_examples_live_in_prose() {
676 let (queries, _) = extract(QUERY_SYNTAX);
677 for bad in [
678 "era > 1600 from docs",
679 "from docs count",
680 "from docs where \"pricing\" in list(tags) select layer, tags",
681 ] {
682 assert!(oqx::parse_string(bad).is_err());
683 assert!(
684 !queries.iter().any(|q| q.2 == bad),
685 "{bad} is an error example and must stay in prose"
686 );
687 }
688 }
689
690 #[test]
691 fn clause_order_sentence_matches_the_parser() {
692 let e = oqx::parse_string("from docs where x select y")
693 .unwrap_err()
694 .to_string();
695 let (_, rest) = e
696 .split_once("OQX clause order is ")
697 .expect("the parser names the order");
698 let order = rest
699 .split(" (")
700 .next()
701 .unwrap()
702 .trim_end_matches(['.', ' ']);
703 assert!(
704 QUERY_SYNTAX.contains(&format!("CLAUSE ORDER IS FIXED: {order}")),
705 "the text's order sentence drifted from the parser's ({order})"
706 );
707 }
708
709 #[test]
712 fn twin_is_byte_identical_to_the_reference() {
713 let path = concat!(
714 env!("CARGO_MANIFEST_DIR"),
715 "/../../packages/core/src/mcp/reference.ts"
716 );
717 let Ok(ts) = std::fs::read_to_string(path) else {
718 return;
719 };
720 let open = "export const QUERY_SYNTAX = `";
721 let start = ts.find(open).expect("the TS export") + open.len();
722 let end = ts.rfind("`;").expect("the TS literal's end");
723 let mut text = String::new();
724 let mut chars = ts[start..end].chars();
725 while let Some(c) = chars.next() {
726 if c == '\\' {
727 if let Some(n) = chars.next() {
728 text.push(n);
729 }
730 } else {
731 text.push(c);
732 }
733 }
734 assert!(
735 text == QUERY_SYNTAX,
736 "crates/omgbase-surface/src/reference.rs drifted from packages/core/src/mcp/reference.ts"
737 );
738 }
739}