@jarenjs/json 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/ARCHITECTURE.md +175 -0
  2. package/LICENSE +21 -0
  3. package/README.md +471 -0
  4. package/dist/types/basic.d.ts +32 -0
  5. package/dist/types/index.d.ts +4 -0
  6. package/dist/types/jslt/dispatch.d.ts +11 -0
  7. package/dist/types/jslt/errors.d.ts +18 -0
  8. package/dist/types/jslt/index.d.ts +53 -0
  9. package/dist/types/jslt/stylesheet.d.ts +8 -0
  10. package/dist/types/jtlt/desugar.d.ts +19 -0
  11. package/dist/types/jtlt/errors.d.ts +18 -0
  12. package/dist/types/jtlt/index.d.ts +57 -0
  13. package/dist/types/jtlt/template.d.ts +8 -0
  14. package/dist/types/jtlt/writer.d.ts +6 -0
  15. package/dist/types/path.d.ts +235 -0
  16. package/dist/types/pointer.d.ts +114 -0
  17. package/dist/types/query/compile.d.ts +21 -0
  18. package/dist/types/query/errors.d.ts +18 -0
  19. package/dist/types/query/index.d.ts +70 -0
  20. package/dist/types/query/normalize.d.ts +68 -0
  21. package/dist/types/query/operators.d.ts +424 -0
  22. package/dist/types/query/runtime.d.ts +93 -0
  23. package/dist/types/segments.d.ts +62 -0
  24. package/dist/types/xquery/index.d.ts +19 -0
  25. package/dist/types/xquery/parse.d.ts +20 -0
  26. package/docs/JSLT-FORMAT.md +861 -0
  27. package/docs/JSLT-PRELUDE.md +159 -0
  28. package/docs/JTLT-FORMAT.md +659 -0
  29. package/docs/QUERY-FORMAT.md +1221 -0
  30. package/docs/XQUERY-FRONTEND.md +321 -0
  31. package/package.json +81 -0
  32. package/schemas/jaren-jslt.draft-07.schema.json +776 -0
  33. package/schemas/jaren-jslt.schema.json +776 -0
  34. package/schemas/jaren-query.draft-07.schema.json +613 -0
  35. package/schemas/jaren-query.schema.json +375 -0
  36. package/src/basic.js +300 -0
  37. package/src/index.js +4 -0
  38. package/src/jslt/dispatch.js +934 -0
  39. package/src/jslt/errors.js +34 -0
  40. package/src/jslt/index.js +121 -0
  41. package/src/jslt/stylesheet.js +234 -0
  42. package/src/jtlt/desugar.js +231 -0
  43. package/src/jtlt/errors.js +34 -0
  44. package/src/jtlt/index.js +155 -0
  45. package/src/jtlt/template.js +130 -0
  46. package/src/jtlt/writer.js +110 -0
  47. package/src/path.js +977 -0
  48. package/src/pointer.js +453 -0
  49. package/src/query/compile.js +817 -0
  50. package/src/query/errors.js +33 -0
  51. package/src/query/index.js +150 -0
  52. package/src/query/normalize.js +1047 -0
  53. package/src/query/operators.js +1253 -0
  54. package/src/query/runtime.js +233 -0
  55. package/src/segments.js +627 -0
  56. package/src/xquery/index.js +35 -0
  57. package/src/xquery/parse.js +1647 -0
@@ -0,0 +1,1221 @@
1
+ # The Jaren JSON Query Format
2
+
3
+ **Version 0.1 — Specification**
4
+
5
+ Module: `@jarenjs/json/query`. This document is the language contract; the
6
+ package [README](../README.md) carries the guided tour and the engine's
7
+ internals are described in [ARCHITECTURE](../ARCHITECTURE.md).
8
+
9
+ > **Naming note (non-normative).** "Jaren JSON Query format" is a working name
10
+ > and is used consistently throughout this document. The obvious short name
11
+ > "JSON Query" collides with the independent jsonquery.org project; final
12
+ > naming/branding is an open question and is deliberately not settled here.
13
+
14
+ ---
15
+
16
+ ## 1. Introduction
17
+
18
+ ### 1.1 What this language is
19
+
20
+ The Jaren JSON Query format is a declarative query-and-transformation language
21
+ for JSON documents. Its semantics are those of **XQuery 3.1** — sequences,
22
+ FLWOR expressions, effective boolean value, existential comparisons — but its
23
+ surface syntax is **JSON itself**: a query document is a JSON value, the way an
24
+ XSLT stylesheet is an XML document. Navigation leaves are **RFC 9535 JSONPath**
25
+ strings, and the degenerate query is a bare JSONPath string.
26
+
27
+ Semantics follow XQuery 3.1 except where a numbered deviation (D1–D7, §11)
28
+ says otherwise.
29
+
30
+ A non-normative XQuery *text* front-end (`parseXQuery(text)` producing a query
31
+ document) is a planned compatibility layer; only the JSON format is specified
32
+ here. The JSON query document is the canonical language.
33
+
34
+ ### 1.2 Conformance and normative language
35
+
36
+ The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
37
+ **SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY**, and **OPTIONAL** in this
38
+ document are to be interpreted as described in RFC 2119.
39
+
40
+ Two conformance roles exist:
41
+
42
+ - A **producer** emits query documents. A producer MUST emit documents that
43
+ are structurally valid per §12's JSON Schema and semantically valid per this
44
+ specification.
45
+ - A **consumer** (compiler + evaluator) MUST accept every valid query document
46
+ and MUST reject invalid documents with the compile errors of §10; it MUST
47
+ raise the runtime errors of §10 under the conditions specified there.
48
+
49
+ ### 1.3 Terminology
50
+
51
+ - **Query document** — the top-level JSON value handed to the compiler (§4).
52
+ - **Expression** — any JSON value in expression position, interpreted per the
53
+ encoding rules of §3.
54
+ - **Phrase** — an object whose keys are all `$`-prefixed and which matches one
55
+ of the closed vocabulary shapes of §§4–8 (operator phrases, the FLWOR
56
+ phrase, quantifier phrases, the version envelope).
57
+ - **Path** — a string expression that is an RFC 9535 JSONPath query, absolute
58
+ or variable-rooted (§5).
59
+
60
+ ---
61
+
62
+ ## 2. Data model
63
+
64
+ ### 2.1 Items and sequences
65
+
66
+ 1. An **item** is any JSON value: `null`, a boolean, a number, a string, an
67
+ array, or an object. There is no node identity beyond JSON value structure;
68
+ arrays and objects are items like any other.
69
+ 2. Numbers are IEEE 754 double-precision floats (**D1**). There is no separate
70
+ integer, decimal, or float type. Integer-valued positions and lengths are
71
+ doubles with integral values.
72
+ 3. Every expression evaluates to a **sequence** of zero or more items.
73
+ Sequences are **flat** and **ordered**: a sequence never contains another
74
+ sequence. Combining sequences concatenates them.
75
+ 4. The **empty sequence** `()` contains no items. The empty sequence is *not*
76
+ the same thing as `null`: `null` is an item, and a sequence containing
77
+ `null` has length 1.
78
+ 5. A sequence of exactly one item is identified with that item ("singleton ≡
79
+ item"). A literal `42` and a one-item sequence containing `42` are
80
+ indistinguishable.
81
+
82
+ ### 2.2 Effective boolean value (EBV)
83
+
84
+ Several constructs (`$where`, `$if`, `$and`, `$or`, `$not`, `$satisfies`)
85
+ reduce a sequence to a boolean, its **effective boolean value**. The EBV of a
86
+ sequence MUST be computed as follows, in order:
87
+
88
+ | Sequence | EBV |
89
+ |---|---|
90
+ | empty sequence | `false` |
91
+ | singleton `false` / `true` | the boolean itself |
92
+ | singleton number `n` | `false` if `n` is `0`, `-0`, or `NaN`; otherwise `true` |
93
+ | singleton string `s` | `false` if `s` is empty; otherwise `true` |
94
+ | singleton `null` | `false` |
95
+ | singleton array or object | `true` (**D3** — XQuery raises FORG0006 here) |
96
+ | any sequence of two or more items | runtime error `JQ2003` |
97
+
98
+ Note that D3 makes the EBV total over all singletons; only multi-item
99
+ sequences are erroneous.
100
+
101
+ ---
102
+
103
+ ## 3. Encoding rules
104
+
105
+ Three rules assign a meaning to every JSON value in expression position. They
106
+ are exhaustive: every JSON value is either an expression or a compile error.
107
+
108
+ ### 3.1 Rule 1 — objects
109
+
110
+ Partition an object's keys by whether they start with `$` (U+0024):
111
+
112
+ 1. **All keys `$`-prefixed → operator phrase.** The object MUST match one
113
+ shape from the closed phrase vocabulary (§§4, 6, 7, 8). An unknown `$`-key
114
+ is compile error `JQ0002`; a known key with the wrong value shape, arity,
115
+ or key combination is compile error `JQ0003`.
116
+ 2. **No key `$`-prefixed → map constructor.** Each key is a literal member
117
+ name; each value is evaluated as an expression. The result is a single
118
+ object item. Member value expressions follow the content rules of §3.4.
119
+
120
+ ```json
121
+ { "title": "$b.title", "inStock": true, "tags": ["new", "sale"] }
122
+ ```
123
+
124
+ constructs an object with members `title` (value of the path `$b.title`),
125
+ `inStock` (literal `true`), and `tags` (a constructed array, Rule 3).
126
+ 3. **Mixed → compile error `JQ0001`.** An object with both `$`-prefixed and
127
+ plain keys is never valid.
128
+
129
+ ```json
130
+ { "title": "$b.title", "$where": true }
131
+ ```
132
+
133
+ is a compile error (`JQ0001`).
134
+
135
+ To construct an object whose member names start with `$` or are computed at
136
+ runtime, use the escape hatches of §3.5 (`$const`, `$map`).
137
+
138
+ An empty object `{}` has no `$`-prefixed key and is therefore a map
139
+ constructor: it constructs the empty object.
140
+
141
+ ### 3.2 Rule 2 — strings
142
+
143
+ A string starting with `$` (U+0024) is a **query expression**; any other
144
+ string is a **literal string**. The forms, checked in order:
145
+
146
+ 1. **Absolute path.** `$` alone, or a string starting with `$.`, `$[`, or
147
+ `$..`, MUST be a complete RFC 9535 JSONPath query. Its root `$` is the
148
+ input document. (RFC 9535 queries begin with `$` by grammar, so the sigil
149
+ costs nothing.) Examples: `"$"`, `"$.store.book[*]"`,
150
+ `"$..price"`, `"$['odd key']"`.
151
+ 2. **Variable-rooted path.** `$name`, where *name* matches
152
+ `[A-Za-z_][A-Za-z0-9_]*`, optionally followed by RFC 9535 *segments*:
153
+ `"$b"`, `"$b.price"`, `"$b['odd key'][0]"`, `"$b[?@.x > 1]"`. The grammar
154
+ is RFC 9535 with the root identifier `$` replaced by the variable
155
+ reference. The path is evaluated with the variable's value as its root.
156
+ 3. **Literal string escape.** A string starting with `$$` denotes the literal
157
+ string obtained by dropping exactly one leading `$`: `"$$price"` is the
158
+ string `"$price"`, `"$$$x"` is the string `"$$x"`. The escape applies only
159
+ to the two leading characters; no other unescaping occurs.
160
+ 4. **Literal string.** Any string not starting with `$` is itself:
161
+ `"hello"` is the string `"hello"`.
162
+
163
+ A string starting with `$` that matches none of forms 1–3 (e.g. `"$9foo"`,
164
+ `"$ x"`), or that matches a head form but is not a grammatically valid path
165
+ (e.g. `"$.store["`), is compile error `JQ0004`.
166
+
167
+ Inside the bracketed filter expressions embedded in a path (either form),
168
+ `@` and `$` keep their RFC 9535 meanings — the current node and the *input
169
+ document* root respectively. **Query variables are not visible inside path
170
+ filters**; a filter that needs another variable is a cross-variable predicate
171
+ and belongs in `$where` (§6.4). See §5.2 for the two filter dialects.
172
+
173
+ ### 3.3 Rule 3 — scalars are literals
174
+
175
+ Numbers, booleans, and `null` denote themselves: `42`, `true`, `null` are
176
+ literal items (singleton sequences).
177
+
178
+ ### 3.4 Rule 3, continued — arrays are array constructors
179
+
180
+ An array in expression position is an **array constructor**. Each element is
181
+ evaluated in order, and the resulting sequences are concatenated per XQuery
182
+ sequence flattening into the members of one new array item: an element
183
+ evaluating to a sequence of N items contributes N members; an element
184
+ evaluating to the empty sequence contributes none.
185
+
186
+ ```json
187
+ [1, "$.store.book[*].price", { "$seq": [2, 3] }]
188
+ ```
189
+
190
+ Against a store with four book prices, this constructs a single 7-element
191
+ array: `1`, the four prices, `2`, `3`.
192
+
193
+ The same flattening applies to the member value expressions of a map
194
+ constructor and to every other expression position; it is a property of the
195
+ sequence data model, not of the array constructor alone. To embed an array
196
+ *as a value* without evaluation, use `$const` (§3.5.1); to bind one without
197
+ iteration, use `$let` (§6.3).
198
+
199
+ ### 3.5 Escape hatches
200
+
201
+ #### 3.5.1 `$const` — quote
202
+
203
+ ```json
204
+ { "$const": v }
205
+ ```
206
+
207
+ evaluates to `v` verbatim, as a single item. Nothing inside `v` is evaluated:
208
+ strings starting with `$` stay as written, objects with `$`-keys are plain
209
+ data, arrays are not flattened. `v` MAY be any JSON value.
210
+
211
+ ```json
212
+ { "template": { "$const": { "$for": "kept verbatim", "price": null } }, "label": "$$price" }
213
+ ```
214
+
215
+ constructs `{ "template": { "$for": "kept verbatim", "price": null }, "label": "$price" }`.
216
+
217
+ #### 3.5.2 `$map` — general map constructor
218
+
219
+ ```json
220
+ { "$map": [[keyExpr, valueExpr], ...] }
221
+ ```
222
+
223
+ constructs one object from a list of key/value expression pairs. Use it for
224
+ computed member names and for names that start with `$`. For each pair, in
225
+ order:
226
+
227
+ - *keyExpr* MUST evaluate to a single string; any other result (empty
228
+ sequence, non-string, multi-item sequence) is runtime error `JQ2004`.
229
+ - *valueExpr* is evaluated as an ordinary expression (its result flows per
230
+ §3.4).
231
+ - Later pairs win on duplicate keys.
232
+
233
+ Each pair MUST be an array of exactly two expressions (`JQ0003` otherwise).
234
+ An empty pair list constructs the empty object.
235
+
236
+ ```json
237
+ { "$let": { "prefix": "col_", "book": "$.store.book[0]" },
238
+ "$return": { "$map": [
239
+ [{ "$concat": ["$prefix", "title"] }, "$book.title"],
240
+ [{ "$concat": ["$prefix", "price"] }, "$book.price"]
241
+ ] } }
242
+ ```
243
+
244
+ constructs `{ "col_title": ..., "col_price": ... }`.
245
+
246
+ ---
247
+
248
+ ## 4. Query documents
249
+
250
+ ### 4.1 Top level
251
+
252
+ A query document is either:
253
+
254
+ 1. a **bare expression** — any expression per §3. In particular a bare
255
+ JSONPath string is a complete query (the degenerate case), and so is a
256
+ bare scalar; or
257
+ 2. the **version envelope** phrase:
258
+
259
+ ```json
260
+ { "$query": "0.1", "$expr": <expression> }
261
+ ```
262
+
263
+ Both keys are REQUIRED and no other key is permitted (`JQ0003`).
264
+
265
+ ### 4.2 Versioning
266
+
267
+ The version identifier for this specification is the string `"0.1"`. A
268
+ missing envelope implies the consumer's current version. A `$query` value
269
+ that is not a version string the consumer implements — including non-string
270
+ values and unknown future versions — is compile error `JQ0006`.
271
+
272
+ The envelope is only recognized at the top level of a query document; an
273
+ object with `$query`/`$expr` keys in any other expression position is an
274
+ unknown operator phrase (`JQ0002`/`JQ0003`).
275
+
276
+ ---
277
+
278
+ ## 5. Paths
279
+
280
+ ### 5.1 Path semantics
281
+
282
+ A path (absolute or variable-rooted, §3.2) evaluates to the sequence of the
283
+ nodes' values selected by the RFC 9535 query, **in document order** as defined
284
+ by RFC 9535 (children of an object in member order, of an array in index
285
+ order; descendant traversal per RFC 9535 §2.5.2). That node sequence then
286
+ flows through the query as an ordinary XQuery sequence.
287
+
288
+ A variable-rooted path evaluates its segments against each item of the
289
+ variable's bound sequence, in order, concatenating results. For the common
290
+ case of a `$for` variable the binding is a single item.
291
+
292
+ **Singular queries** (RFC 9535 singular query: name and index selectors only)
293
+ are statically known to evaluate to zero or one item; consumers MAY exploit
294
+ this, and later work uses it for static cardinality checks.
295
+
296
+ An absolute path in any expression position reads from the one input
297
+ document; there is no context-item drift — `$` is always the document root.
298
+
299
+ ### 5.2 The two filter dialects
300
+
301
+ Two boolean mini-languages coexist in this format, and both keep the exact
302
+ semantics of their own standard:
303
+
304
+ 1. **Inside a path's `[?...]` filter**, RFC 9535 semantics apply unchanged —
305
+ including the special absent-value `Nothing`, where `Nothing == Nothing`
306
+ is **true** for two empty singular-query results.
307
+ 2. **At query level** (`$where`, `$eq`, ...), XQuery semantics apply —
308
+ comparisons are existential over sequences, so a comparison in which
309
+ either side is the empty sequence is **false** (§8.4).
310
+
311
+ Worked example. Input:
312
+
313
+ ```json
314
+ { "a": [ { "n": 1 } ] }
315
+ ```
316
+
317
+ The item in `a` has neither member `b` nor member `c`.
318
+
319
+ - Path dialect: `"$.a[?@.b == @.c]"` **selects the item** — both sides are
320
+ empty nodelists of singular queries, i.e. `Nothing == Nothing`, which
321
+ RFC 9535 defines as true.
322
+ - Query dialect:
323
+
324
+ ```json
325
+ { "$for": { "x": "$.a[*]" },
326
+ "$where": { "$eq": ["$x.b", "$x.c"] },
327
+ "$return": "$x" }
328
+ ```
329
+
330
+ returns the **empty sequence** — `$x.b` and `$x.c` are both empty, an
331
+ existential comparison over empty sequences finds no witnessing pair, so
332
+ `$eq` is false.
333
+
334
+ Neither dialect is wrong; they answer different questions. Producers SHOULD
335
+ choose the dialect deliberately when absent members are possible.
336
+
337
+ ---
338
+
339
+ ## 6. The FLWOR phrase
340
+
341
+ ### 6.1 Shape and clause order
342
+
343
+ A FLWOR phrase is an operator phrase containing `$for` and/or `$let`, plus
344
+ optional clauses, plus the REQUIRED `$return`:
345
+
346
+ | Key | Value | Presence |
347
+ |---|---|---|
348
+ | `$for` | binding object (§6.2) | at least one of `$for`, `$let` |
349
+ | `$let` | binding object (§6.3) | at least one of `$for`, `$let` |
350
+ | `$as` | schema assertion object (§6.8) | OPTIONAL |
351
+ | `$where` | expression | OPTIONAL |
352
+ | `$groupby` | binding object (§6.5) | OPTIONAL |
353
+ | `$orderby` | key spec or array of key specs (§6.6) | OPTIONAL |
354
+ | `$count` | variable name string (§6.7) | OPTIONAL |
355
+ | `$return` | expression | REQUIRED |
356
+
357
+ The clauses apply in **fixed semantic order regardless of their order in the
358
+ JSON document** (**D7**):
359
+
360
+ ```
361
+ $for → $let → $as → $where → $groupby → $orderby → $count → $return
362
+ ```
363
+
364
+ JSON key order is not interoperable — several ecosystems (e.g. Go maps)
365
+ neither preserve nor guarantee it — so this format MUST NOT give key order
366
+ semantic weight anywhere, and does not.
367
+
368
+ Exotic clause interleavings (a `$let` between two `$for`s, a `$where` before
369
+ a `$for`, multiple `$where`s) are expressed by **nesting** FLWOR phrases,
370
+ which is standard XQuery practice anyway:
371
+
372
+ ```json
373
+ { "$for": { "a": "$.store.book[*]" },
374
+ "$return": { "$let": { "p": "$a.price" },
375
+ "$where": { "$gt": ["$p", 10] },
376
+ "$return": "$a.title" } }
377
+ ```
378
+
379
+ The FLWOR phrase evaluates to the concatenation of the `$return` results over
380
+ the surviving tuple stream, in tuple order.
381
+
382
+ ### 6.2 `$for` — iteration bindings
383
+
384
+ ```json
385
+ "$for": { name: source, ... }
386
+ ```
387
+
388
+ Each *name* MUST match `[A-Za-z_][A-Za-z0-9_]*` (`JQ0003` otherwise). Each
389
+ *source* is either an expression or the extended binding form below. A `$for`
390
+ binding evaluates its source and iterates the resulting item sequence,
391
+ binding *name* to one item per tuple.
392
+
393
+ **Auto-iteration of arrays (D4).** When an item produced by the source is an
394
+ array, `$for` unpacks it into its members — one level only — and iterates
395
+ those. This is the ergonomic default for the JSON data model, where `[*]`
396
+ already produced values, not nodes. To bind an array *as a value*, use `$let`
397
+ or `{"$const": [...]}`. Objects are NOT auto-iterated; iterate an object's
398
+ values with a `[*]` path segment (a future `$entries` operator is the planned
399
+ counterpart for member pairs).
400
+
401
+ **Multiple bindings** nest left-to-right in document key order, exactly like
402
+ consecutive XQuery `for` clauses, and **may be correlated**: a later source
403
+ may reference variables bound earlier in the same `$for` object.
404
+
405
+ > **Interop caveat.** Correlation is the one place where the *relative order*
406
+ > of keys inside a single binding object matters. Producers on
407
+ > key-order-hostile stacks SHOULD emit one binding per phrase, nesting
408
+ > phrases, instead of relying on multi-key binding objects.
409
+
410
+ **Extended binding form** — a source written as
411
+
412
+ ```json
413
+ { "$in": expr, "$at": "posName" }
414
+ ```
415
+
416
+ iterates `expr` like a plain source and additionally binds *posName* to the
417
+ **0-based** (D6) position of the current item within the iterated sequence.
418
+ Both keys are REQUIRED in this form (a positionless binding is simply the
419
+ plain form); *posName* MUST be a valid variable name.
420
+
421
+ ```json
422
+ { "$for": { "b": { "$in": "$.store.book[*]", "$at": "i" } },
423
+ "$count": "n",
424
+ "$return": { "index": "$i", "row": "$n", "title": "$b.title" } }
425
+ ```
426
+
427
+ ### 6.3 `$let` — sequence bindings
428
+
429
+ ```json
430
+ "$let": { name: expr, ... }
431
+ ```
432
+
433
+ Binds each *name* to the **full sequence** its expression evaluates to — no
434
+ iteration, no array unpacking. Name rules, document-key-order evaluation, and
435
+ correlation rules are identical to `$for` (later `$let` sources see earlier
436
+ `$let` variables of the same object). The extended `$in`/`$at` form is not
437
+ available in `$let` (`JQ0003`).
438
+
439
+ Binding the same variable name twice within one FLWOR phrase — in one binding
440
+ object (where JSON parsers permit duplicate keys), across the phrase's `$for`
441
+ and `$let` objects, as an `$at` name, or in `$groupby` (as a new key name) —
442
+ is compile error `JQ0007`. Rebinding a name from an *enclosing* phrase is
443
+ ordinary shadowing and is allowed.
444
+
445
+ ### 6.4 `$where` — tuple filter
446
+
447
+ ```json
448
+ "$where": expr
449
+ ```
450
+
451
+ Evaluates *expr* once per tuple and keeps the tuples whose **EBV** (§2.2) is
452
+ true. Cross-variable predicates (joins) belong here, not in path filters
453
+ (§3.2, §5.2).
454
+
455
+ ### 6.5 `$groupby` — grouping
456
+
457
+ ```json
458
+ "$groupby": { name: keyExpr, ... }
459
+ ```
460
+
461
+ XQuery 3.1 group-by semantics:
462
+
463
+ - Each *keyExpr* is evaluated per tuple; each *name* becomes a
464
+ **grouping-key variable**, bound in every subsequent clause to that group's
465
+ key value (a singleton per group).
466
+ - Tuples with equal key combinations form one group. Key equality is **deep
467
+ structural JSON equality** (`equalsJson`-grade, the same relation as `$eq`
468
+ item equality, §8.4), with numbers compared mathematically.
469
+ - Every other variable bound in the phrase is **rebound to the sequence** of
470
+ its values across the group's tuples, in tuple order.
471
+ - The tuple stream after `$groupby` has one tuple per group, in order of
472
+ **first appearance** of each group's key combination in the incoming
473
+ tuple stream.
474
+
475
+ *name* MUST be a valid variable name, distinct from every other variable
476
+ bound in the phrase (`JQ0007`).
477
+
478
+ ```json
479
+ { "$for": { "b": "$.store.book[*]" },
480
+ "$groupby": { "genre": "$b.category" },
481
+ "$return": { "genre": "$genre",
482
+ "count": { "$count": "$b" },
483
+ "avg": { "$avg": "$b.price" } } }
484
+ ```
485
+
486
+ Here, inside `$return`, `$genre` is one key value and `$b` is the sequence of
487
+ that genre's books.
488
+
489
+ ### 6.6 `$orderby` — ordering
490
+
491
+ ```json
492
+ "$orderby": keySpec
493
+ "$orderby": [keySpec, ...]
494
+ ```
495
+
496
+ A *keySpec* is either an expression (shorthand for ascending, empty-least) or
497
+ the explicit form
498
+
499
+ ```json
500
+ { "$key": expr, "$dir": "asc" | "desc", "$empty": "least" | "greatest" }
501
+ ```
502
+
503
+ with `$key` REQUIRED and `$dir` (default `"asc"`) and `$empty` (default
504
+ `"least"`) OPTIONAL.
505
+
506
+ - An array value of `$orderby` is **always a list of key specs**, ordered
507
+ major to minor, and MUST NOT be empty. Consequently a *single* key spec is
508
+ never written as a bare array: an array-constructor sort key (a runtime
509
+ error under the key-type rules anyway) MUST be wrapped as
510
+ `{"$key": [...]}`. The schema enforces this reading.
511
+ - The sort is **stable**.
512
+ - Each key expression is evaluated per tuple. A key value MUST be the empty
513
+ sequence or a single number or string; within one comparison pair the two
514
+ key values MUST be both numbers or both strings — numbers compare
515
+ mathematically, strings by **Unicode scalar values** (code point order).
516
+ Comparing any other combination (number with string, or a key that is a
517
+ boolean, `null`, array, object, or multi-item sequence) is runtime error
518
+ `JQ2005`.
519
+ - Empty key sequences sort per `$empty`: `"least"` (default) places them
520
+ first ascending / last descending; `"greatest"` the reverse.
521
+
522
+ ```json
523
+ { "$for": { "b": "$.store.book[*]" },
524
+ "$orderby": [ { "$key": "$b.price", "$dir": "desc", "$empty": "greatest" }, "$b.title" ],
525
+ "$return": "$b.title" }
526
+ ```
527
+
528
+ ### 6.7 `$count` — tuple numbering
529
+
530
+ ```json
531
+ "$count": "name"
532
+ ```
533
+
534
+ Binds *name* (a valid variable name string) to the **0-based** (D6) index of
535
+ the tuple in the stream *after* `$where`, `$groupby`, and `$orderby` have
536
+ applied. See §6.2 for an example.
537
+
538
+ Collision note: as a FLWOR clause, `$count`'s value is a **name string**; as
539
+ an operator (§8.7), `$count` is a single-key object whose value is an
540
+ expression. The two are structurally unambiguous — the clause occurs only
541
+ among other FLWOR keys with `$return` present; the operator phrase is exactly
542
+ one key.
543
+
544
+ ### 6.8 `$as` — schema assertions on bindings
545
+
546
+ ```json
547
+ "$as": { name: schema, ... }
548
+ ```
549
+
550
+ Numbered after `$count` for historical reasons; in the fixed clause order of
551
+ §6.1, `$as` occupies the slot **between `$let` and `$where`**.
552
+
553
+ Each member names a variable bound by **this phrase's** `$for` (including
554
+ `$at` position names) or `$let`, and pairs it with a **JSON Schema literal**
555
+ (§8.11): a JSON value taken **verbatim** — never interpreted as a query
556
+ expression, so JSON Schema's `$`-prefixed keywords (`$ref`, `$defs`, ...) do
557
+ not collide with Rule 1. A name not bound by the phrase's `$for`/`$let` is
558
+ compile error `JQ0005`. Each schema is compiled once, at query compile time,
559
+ by the consumer's type-test compiler (`JQ0008` when none is installed,
560
+ `JQ0009` when it rejects the schema — see §8.11).
561
+
562
+ Per tuple, after the `$for`/`$let` bindings are established and before
563
+ `$where` applies:
564
+
565
+ - a `$for` or `$at` variable is validated as its one bound **item**;
566
+ - a `$let` variable is validated **per item of its bound sequence** (the
567
+ empty sequence passes vacuously — the schema sees items, never the
568
+ sequence itself, §8.11).
569
+
570
+ The first failing item is runtime error `JQ2008`, naming the variable. `$as`
571
+ never drops tuples — it asserts; use `$valid` in `$where` (§8.11) to filter
572
+ instead.
573
+
574
+ ```json
575
+ { "$for": { "b": "$.store.book[*]" },
576
+ "$as": { "b": { "type": "object", "required": ["title", "price"] } },
577
+ "$return": "$b.title" }
578
+ ```
579
+
580
+ ---
581
+
582
+ ## 7. Quantifier phrases
583
+
584
+ ```json
585
+ { "$some": { name: expr, ... }, "$satisfies": expr }
586
+ { "$every": { name: expr, ... }, "$satisfies": expr }
587
+ ```
588
+
589
+ XQuery `some/every ... satisfies`. The binding object follows `$for` rules
590
+ (names, document-key-order nesting, correlation, D4 array unpacking) except
591
+ that the extended `$in`/`$at` form is not available (`JQ0003`). Both keys are
592
+ REQUIRED.
593
+
594
+ For each binding tuple, the EBV (§2.2) of `$satisfies` is computed:
595
+
596
+ - `$some` is `true` iff at least one tuple satisfies; `false` over an empty
597
+ tuple stream.
598
+ - `$every` is `true` iff every tuple satisfies; `true` over an empty tuple
599
+ stream.
600
+
601
+ Evaluation MUST short-circuit ("early exit"): `$some` MAY stop at the first
602
+ satisfying tuple, `$every` at the first failing one; a runtime error in a
603
+ tuple after the deciding tuple is then not raised.
604
+
605
+ ```json
606
+ { "$every": { "b": "$.store.book[*]" }, "$satisfies": { "$exists": "$b.title" } }
607
+ ```
608
+
609
+ ```json
610
+ { "$let": { "books": "$.store.book[*]" },
611
+ "$return": { "$some": { "b": "$books" },
612
+ "$satisfies": { "$gt": ["$b.price", 20] } } }
613
+ ```
614
+
615
+ ---
616
+
617
+ ## 8. Operators
618
+
619
+ ### 8.1 Vocabulary and conventions
620
+
621
+ Every operator is a **single-`$`-key object** (the phrases of §§4, 6, 7 and
622
+ the two-key quantifier shapes are the only exceptions). The vocabulary is
623
+ **closed**: an object whose single `$`-key is not listed in this section is
624
+ compile error `JQ0002`. Wrong argument shapes and arities are `JQ0003`.
625
+
626
+ Signature notation: `expr` is any expression; `[a, b]` is a JSON array of
627
+ exactly two expressions; `[e, ...]` is variadic; `?` marks an optional
628
+ trailing argument. "Cardinality" describes the *runtime* sequence each
629
+ argument position accepts; argument positions are structurally always
630
+ expressions (per-position runtime rules are enforced by the evaluator with
631
+ `JQ2xxx` errors, not by the schema).
632
+
633
+ Unless a definition below says otherwise, an operator's result is exactly
634
+ one item. Operators whose semantics come from XQuery 3.1 Functions and
635
+ Operators keep that reference in their definition; F&O behavior is adapted
636
+ to the JSON data model per the deviations of §11 (notably D1 numbers and
637
+ D6 0-based positions).
638
+
639
+ The JSON-Schema-as-type-system layer defines `$valid` and `$assert` (§8.11)
640
+ and the FLWOR clause `$as` (§6.8); no keys remain reserved in this version.
641
+
642
+ ### 8.2 Sequences
643
+
644
+ | Operator | Signature | Result |
645
+ |---|---|---|
646
+ | `$seq` | `{"$seq": [e, ...]}` | XQuery comma: evaluate each element, concatenate all results into one flat sequence. `{"$seq": []}` is the empty sequence. |
647
+ | `$exists` | `{"$exists": e}` | `true` iff `e`'s result is non-empty (XQuery `fn:exists`). |
648
+ | `$empty` | `{"$empty": e}` | `true` iff `e`'s result is empty (XQuery `fn:empty`). |
649
+
650
+ ```json
651
+ { "$if": [ { "$exists": "$.store.bicycle" },
652
+ { "$seq": ["$.store.bicycle.color", "$.store.bicycle.price"] } ] }
653
+ ```
654
+
655
+ ### 8.3 Conditional
656
+
657
+ | Operator | Signature | Result |
658
+ |---|---|---|
659
+ | `$if` | `{"$if": [cond, then, else?]}` | EBV of *cond*; on `true` evaluates *then*, on `false` evaluates *else*. A missing *else* means the empty sequence. Only the taken branch is evaluated. |
660
+
661
+ Arity 2–3 (`JQ0003` otherwise). Example above.
662
+
663
+ ### 8.4 Comparisons — `$eq $ne $lt $le $gt $ge`
664
+
665
+ Signature: `{"$eq": [left, right]}` etc., arity exactly 2.
666
+
667
+ These are **general comparisons** in the XQuery sense (the `=` family):
668
+ existentially lifted over sequences. `{"$op": [L, R]}` is `true` iff **some**
669
+ item `l` of `L`'s result and **some** item `r` of `R`'s result satisfy the
670
+ item comparison; otherwise `false`. Consequences:
671
+
672
+ - Over empty sequences the result is `false` — there is no witnessing pair.
673
+ Contrast this with `Nothing == Nothing` inside path filters, which is true;
674
+ see §5.2, "The two filter dialects".
675
+ - `$ne` is existential too: `{"$ne": [L, R]}` is true iff some pair differs —
676
+ it is NOT the negation of `$eq` on multi-item sequences. Use
677
+ `{"$not": {"$eq": [L, R]}}` for the negation.
678
+
679
+ **Item comparison rules:**
680
+
681
+ - `$eq` / `$ne` use **deep structural JSON equality** (**D2**): two items are
682
+ equal iff they are structurally identical JSON values; numbers compare
683
+ mathematically (`1` equals `1.0`; `NaN` equals nothing, `-0` equals `0`);
684
+ arrays memberwise in order; objects by key set and per-key values,
685
+ key order irrelevant.
686
+ - `$lt $le $gt $ge` compare numbers with numbers (mathematically) and strings
687
+ with strings (Unicode scalar value order). For **any other item pair**
688
+ (number vs string, booleans, `null`, arrays, objects) the item comparison
689
+ is simply **false** for that pair — not an error — and contributes no
690
+ witness.
691
+
692
+ ```json
693
+ { "$and": [
694
+ { "$eq": ["$.store.bicycle.color", "red"] },
695
+ { "$or": [ { "$lt": ["$.store.bicycle.price", 400] }, { "$not": false } ] },
696
+ { "$ne": [1, 2] }, { "$le": [1, 1] }, { "$gt": [2, 1] }, { "$ge": [2, 2] }
697
+ ] }
698
+ ```
699
+
700
+ ### 8.5 Arithmetic — `$add $sub $mul $div $idiv $mod`, `$neg`
701
+
702
+ Signatures: `{"$add": [a, b]}` etc., arity exactly 2; `{"$neg": a}` unary
703
+ (the value is the operand expression itself, not a one-element array).
704
+
705
+ Operand rules (each operand, after evaluation):
706
+
707
+ - empty sequence → the operator's result is the **empty sequence** (XQuery);
708
+ - a singleton number → used as is;
709
+ - a singleton non-number, or a multi-item sequence → runtime type error
710
+ `JQ2001` (XPTY0004-shaped).
711
+
712
+ All arithmetic is IEEE double arithmetic (**D1**):
713
+
714
+ - `$div` by zero follows IEEE 754: `Infinity`, `-Infinity`, or `NaN` — the
715
+ XQuery *double* division semantics, not the decimal FOAR0001 error.
716
+ - `$idiv` is **truncating division** (quotient rounded toward zero to an
717
+ integral double); `$idiv` or `$mod` with a zero divisor is runtime error
718
+ `JQ2002`.
719
+ - `$mod` takes the sign of the dividend (XQuery `mod` semantics).
720
+ - `$neg` is unary minus.
721
+
722
+ ```json
723
+ { "$seq": [ { "$add": [1, 2] }, { "$sub": [3, 1] }, { "$mul": [2, 4] },
724
+ { "$div": [1, 0] }, { "$idiv": [7, 2] }, { "$mod": [7, 2] },
725
+ { "$neg": "$.store.bicycle.price" } ] }
726
+ ```
727
+
728
+ ### 8.6 Logic — `$and $or $not`
729
+
730
+ | Operator | Signature | Result |
731
+ |---|---|---|
732
+ | `$and` | `{"$and": [e, ...]}` (≥ 1) | `true` iff every operand's EBV is true. |
733
+ | `$or` | `{"$or": [e, ...]}` (≥ 1) | `true` iff some operand's EBV is true. |
734
+ | `$not` | `{"$not": e}` | negated EBV of `e` (XQuery `fn:not`). |
735
+
736
+ Operands are reduced by EBV (§2.2) left to right with **short-circuit**
737
+ evaluation: `$and` stops at the first false, `$or` at the first true;
738
+ operands after the deciding one are not evaluated and cannot raise errors.
739
+ Example in §8.4.
740
+
741
+ ### 8.7 Strings
742
+
743
+ **String parameters.** Every argument of the operators in this section
744
+ (except where a definition says otherwise) MUST evaluate to the empty
745
+ sequence — read as `""`, the F&O `xs:string?` convention — or to a single
746
+ string; a singleton of any other type, or a sequence of two or more items,
747
+ is runtime error `JQ2001`. `$concat` and `$string-join` additionally cast
748
+ their *item* operands per the `$string` table (§8.10). All results are
749
+ single items.
750
+
751
+ | Operator | Signature | Definition |
752
+ |---|---|---|
753
+ | `$concat` | `{"$concat": [e, ...]}` (≥ 0) | Variadic concatenation. Each operand is cast to string per `$string` (§8.10); an empty-sequence operand contributes `''`. `{"$concat": []}` is `""`. |
754
+ | `$string-join` | `[seq, sep?]` (1–2) | Concatenates the items of *seq*, each cast to string per `$string`, separated by *sep* (a string parameter, default `""`). An empty *seq* yields `""` (F&O `fn:string-join`). |
755
+ | `$substring` | `[str, start, len?]` (2–3) | The code points of *str* at 0-based (**D6** — deviation from F&O's 1-based positions) positions `p` with `round(start) ≤ p` and, when *len* is given, `p < round(start) + round(len)` (F&O `fn:substring` bounds; `round` rounds half toward +∞). *start*/*len* MUST each be a single number (`JQ2001`); a `NaN` bound selects nothing. |
756
+ | `$contains` | `[str, sub]` | `true` iff *str* contains *sub* (`fn:contains`; every string contains `""`). |
757
+ | `$starts-with` | `[str, prefix]` | `true` iff *str* starts with *prefix* (`fn:starts-with`). |
758
+ | `$ends-with` | `[str, suffix]` | `true` iff *str* ends with *suffix* (`fn:ends-with`). |
759
+ | `$upper` | `{"$upper": e}` | Unicode default uppercase (`fn:upper-case`). |
760
+ | `$lower` | `{"$lower": e}` | Unicode default lowercase (`fn:lower-case`). |
761
+ | `$string-length` | `{"$string-length": e}` | Length in Unicode scalar values (code points), `fn:string-length`. Empty → `0`. |
762
+ | `$normalize-space` | `{"$normalize-space": e}` | Strips leading and trailing whitespace (space, tab, CR, LF) and collapses every internal whitespace run to one space (`fn:normalize-space`). |
763
+ | `$match` | `[input, pattern]` | `true` iff *pattern* matches **all** of *input* (anchored, the RFC 9535 `match()` behavior). |
764
+ | `$search` | `[input, pattern]` | `true` iff *pattern* matches a substring of *input* (RFC 9535 `search()`, XQuery `fn:matches`). |
765
+ | `$replace` | `[input, pattern, replacement]` (exactly 3) | Replaces every non-overlapping match of *pattern* in *input* with *replacement* (`fn:replace`). *replacement* is inserted **literally** — there are no capture-group references (I-Regexp guarantees no capture semantics). |
766
+
767
+ Regular expression operators use **I-Regexp (RFC 9485)** syntax — the same
768
+ interoperable regex dialect RFC 9535 uses — not XSD regular expressions
769
+ (**D5**). I-Regexp has no flags argument, hence `$replace`'s fixed arity 3.
770
+ A *pattern* that is not a syntactically valid I-Regexp makes `$match` and
771
+ `$search` evaluate to `false` (the RFC 9535 rule for nonconforming
772
+ patterns), but is runtime error `JQ2001` in `$replace` (the F&O
773
+ `err:FORX0002` condition — a replacement cannot silently do nothing). A
774
+ `$replace` *pattern* that matches the zero-length string (e.g. `"a*"`) is
775
+ also `JQ2001` (F&O `err:FORX0003`). Pattern *type* errors remain `JQ2001`
776
+ in all three operators per the string-parameter rule above.
777
+
778
+ ```json
779
+ { "$seq": [ { "$concat": ["a", "b"] },
780
+ { "$string-join": ["$.store.book[*].title", ", "] },
781
+ { "$substring": ["hello", 1, 3] },
782
+ { "$contains": ["hello", "ell"] },
783
+ { "$starts-with": ["hello", "he"] },
784
+ { "$ends-with": ["hello", "lo"] },
785
+ { "$upper": "abc" }, { "$lower": "ABC" },
786
+ { "$string-length": "abc" }, { "$normalize-space": " a b " },
787
+ { "$match": ["abc", "a.c"] }, { "$search": ["abc", "b"] },
788
+ { "$replace": ["abc", "b", "x"] } ] }
789
+ ```
790
+
791
+ ### 8.8 Aggregates — `$count $sum $avg $min $max`
792
+
793
+ Each is unary: `{"$count": e}` etc. Aggregates consume their operand
794
+ sequence **as-is**: an item that is an array counts as *one* item — D4
795
+ unpacking is a `$for`/quantifier binding rule, not a sequence rule. (So
796
+ `{"$count": {"$const": [1, 2, 3]}}` is `1`, while iterating the same value
797
+ with `$for` yields three tuples.) See §6.7 for the `$count`
798
+ clause/operator collision note.
799
+
800
+ | Operator | Empty sequence | Definition |
801
+ |---|---|---|
802
+ | `$count` | `0` | The number of items in the operand's result (`fn:count`). |
803
+ | `$sum` | `0` | The sum of the items (`fn:sum`). Every item MUST be a number (`JQ2001`). |
804
+ | `$avg` | empty | The arithmetic mean of the items (`fn:avg`). Every item MUST be a number (`JQ2001`). |
805
+ | `$min` | empty | The least item (`fn:min`). Items MUST be all numbers or all strings (`JQ2001` otherwise, including mixed); numbers compare mathematically, strings by Unicode scalar values. A `NaN` item makes the result `NaN` (F&O). |
806
+ | `$max` | empty | The greatest item (`fn:max`), same rules as `$min`. |
807
+
808
+ ### 8.9 Sequence operators — `$distinct $reverse $sort $head $tail $subsequence $index-of $range $get`
809
+
810
+ | Operator | Signature | Definition |
811
+ |---|---|---|
812
+ | `$distinct` | `{"$distinct": e}` | The distinct items of the operand, in first-occurrence order (`fn:distinct-values` adapted to D2 deep equality). Equality is the **grouping key relation** of §6.5: deep structural equality with `NaN` equal to itself and `-0` equal to `0` — `$distinct` and `$groupby` always agree. |
813
+ | `$reverse` | `{"$reverse": e}` | The operand's items in reverse order (`fn:reverse`). |
814
+ | `$sort` | `{"$sort": e}` | The operand's items sorted ascending by value (`fn:sort`, natural order only — key-based sorting is `$orderby`'s job). Items MUST be all numbers or all strings; anything else, or a mix, is runtime error `JQ2005` (the `$orderby` key-type rules). `NaN` orders per §6.6: equal to itself, less than every other number. The sort is stable. |
815
+ | `$head` | `{"$head": e}` | The first item, or empty (`fn:head`). |
816
+ | `$tail` | `{"$tail": e}` | Every item but the first; empty for operands of one or zero items (`fn:tail`). |
817
+ | `$subsequence` | `[seq, start, len?]` (2–3) | The items of *seq* at the 0-based (D6) positions selected by the `$substring` bound rules (§8.7, F&O `fn:subsequence`): `round(start) ≤ p`, and `p < round(start) + round(len)` when *len* is given. *start*/*len* MUST each be a single number (`JQ2001`). |
818
+ | `$index-of` | `[seq, item]` | The 0-based (D6) positions in *seq* of the items deep-equal to *item*, as a sequence, in order (`fn:index-of`). Equality is the `$eq` item relation (D2) — `NaN` matches nothing. *item* MUST be exactly one item (`JQ2001`). |
819
+ | `$range` | `[start, end]` | The integers from *start* to *end* **inclusive** (the XQuery `to` operator). Either operand empty → empty; *start* > *end* → empty. A non-integral or non-number operand is `JQ2001`. A result of more than 2³² items is runtime error `JQ2007` (resource guard). |
820
+ | `$get` | `[target, key]` | Dynamic lookup, the runtime counterpart of a path leaf: an object *target* with a string *key* yields the member value or empty; an array *target* with an integer *key* yields the element at that 0-based (D6) index — a negative index counts from the end, like the RFC 9535 index selector — or empty. **Every other combination** (wrong type pairing, non-integral index, empty or multi-item operands) is simply the empty sequence, never an error. |
821
+
822
+ Combined example (also exercises the aggregates of §8.8):
823
+
824
+ ```json
825
+ { "$seq": [ { "$count": "$.store.book[*]" }, { "$sum": "$.store.book[*].price" },
826
+ { "$avg": "$.store.book[*].price" }, { "$min": "$.store.book[*].price" },
827
+ { "$max": "$.store.book[*].price" }, { "$distinct": "$.store.book[*].category" },
828
+ { "$reverse": "$.store.book[*].title" }, { "$sort": "$.store.book[*].price" },
829
+ { "$head": "$.store.book[*]" }, { "$tail": "$.store.book[*]" },
830
+ { "$subsequence": ["$.store.book[*]", 1, 2] },
831
+ { "$index-of": ["$.store.book[*].category", "fiction"] },
832
+ { "$range": [1, 5] }, { "$get": ["$.store.bicycle", "color"] } ] }
833
+ ```
834
+
835
+ ### 8.10 Types and casts
836
+
837
+ **Type predicates.** Unary `{"$is-string": e}`, `$is-number`,
838
+ `$is-boolean`, `$is-null`, `$is-array`, `$is-object` — `true` iff `e`'s
839
+ result is a **singleton** of that type. The empty sequence and sequences
840
+ of two or more items yield `false`, never an error — these are cheap
841
+ tests, not assertions.
842
+
843
+ **Casts.** Unary `{"$string": e}`, `{"$number": e}`, `{"$boolean": e}`.
844
+ `$string` and `$number` propagate the empty sequence (empty → empty); a
845
+ multi-item operand, or a singleton the cast table rejects, is runtime
846
+ error `JQ2001`.
847
+
848
+ | Cast | Definition |
849
+ |---|---|
850
+ | `$string` | string → itself; number → its shortest JavaScript serialization (`String(n)`; `NaN`/`Infinity` serialize by name); `true`/`false` → `"true"`/`"false"`; `null` → `"null"`; array or object → `JQ2001`; empty → empty. (`$concat`/`$string-join` cast items by this table, with an explicit `''` for empty operands — §8.7.) |
851
+ | `$number` | number → itself; string → the number it spells **iff** it is a syntactically valid JSON number (RFC 8259 grammar — no leading `+`, no bare or trailing `.`, no whitespace, no `Infinity`/`NaN`), else `JQ2001`; `true`/`false` → `1`/`0`; `null`, array, object → `JQ2001`; empty → empty. |
852
+ | `$boolean` | The **EBV** (§2.2) as an operator: empty → `false`, singletons per the EBV table (D3 included), a sequence of two or more items → `JQ2003`. |
853
+
854
+ | Operator | Signature | Definition |
855
+ |---|---|---|
856
+ | `$coalesce` | `{"$coalesce": [e, ...]}` (≥ 1) | The result of the first operand whose result is non-empty, else empty. Evaluation is lazy: operands after the deciding one are **not evaluated** and cannot raise errors. |
857
+ | `$default` | `[e, fallback]` | `e`'s result if non-empty, else *fallback*'s — sugar for `$coalesce` of exactly two. |
858
+
859
+ ```json
860
+ { "$seq": [ { "$is-string": "abc" }, { "$is-number": 1 }, { "$is-boolean": true },
861
+ { "$is-null": null }, { "$is-array": { "$const": [1] } },
862
+ { "$is-object": { "$const": {} } },
863
+ { "$string": 12 }, { "$number": "12" }, { "$boolean": 1 },
864
+ { "$coalesce": ["$.missing", "fallback"] },
865
+ { "$default": ["$.missing", 0] } ] }
866
+ ```
867
+
868
+ ### 8.11 Schema operators — `$valid $assert`
869
+
870
+ **JSON Schema is this language's type system.** Where XQuery bolted XML
871
+ Schema onto its type lattice, this format embeds JSON Schema documents
872
+ directly inside query documents, as type tests and assertions. The same
873
+ vocabulary that validates the data validates it *inside* queries.
874
+
875
+ **Schema literals.** The second argument of `$valid`/`$assert` — like each
876
+ member value of the `$as` clause (§6.8) — is a **JSON Schema literal**: a
877
+ JSON value taken **verbatim**. It is never normalized or evaluated as a
878
+ query expression; JSON Schema's `$`-prefixed keywords (`$ref`, `$defs`, ...)
879
+ do not collide with Rule 1, `"$name"` strings inside it stay literal strings,
880
+ and its arrays are not array constructors. Consumers MUST deep-copy and
881
+ freeze the literal (like `$const`) and MUST compile it exactly once, at
882
+ query compile time. The format's own JSON Schema (§12) admits any JSON value
883
+ in schema-literal position and does **not** meta-validate it; validity of
884
+ the embedded schema is the type-test compiler's judgment (`JQ0009`).
885
+
886
+ **Per-item validation.** Both operators (and `$as`) validate the **items**
887
+ of a sequence, one at a time — the schema sees each item, never the sequence
888
+ itself. A sequence of three numbers validates against
889
+ `{"type": "number"}`, not against an array schema.
890
+
891
+ | Operator | Signature | Definition |
892
+ |---|---|---|
893
+ | `$valid` | `[expr, schema]` (exactly 2) | `true` iff **every** item of *expr*'s result satisfies *schema*; `true` over the empty sequence (vacuously, like `$every`). Never an error — the cheap test. |
894
+ | `$assert` | `[expr, schema]` (exactly 2) | Identity on success: returns *expr*'s result unchanged when every item satisfies *schema*. The first failing item is runtime error `JQ2008` (at the operator's `docPath`). |
895
+
896
+ ```json
897
+ { "$for": { "b": "$.store.book[*]" },
898
+ "$where": { "$valid": ["$b", { "type": "object", "required": ["isbn"] }] },
899
+ "$return": { "$assert": ["$b.price", { "type": "number", "minimum": 0 }] } }
900
+ ```
901
+
902
+ **The type-test compiler hook (non-normative implementation note).** The
903
+ reference engine (`@jarenjs/json`) has **no dependency** on any JSON Schema
904
+ validator. It defines an extension point instead:
905
+
906
+ ```
907
+ compileJsonQuery(doc, { compileTypeTest: (schemaJson, docPath) => (value => boolean) })
908
+ ```
909
+
910
+ The hook is invoked once per schema literal at **query compile time**, with
911
+ the frozen literal and its RFC 6901 pointer; it returns the hot-path item
912
+ predicate the compiled query closes over. `@jarenjs/validate/query` exports
913
+ `createTypeTestCompiler(validatorOrFactory?)`, which compiles literals with
914
+ a `JarenValidator` (boolean mode, errors off) — supplying an instance with
915
+ registered schemas lets `$ref`s in query schema literals resolve against
916
+ them. The dependency direction is validate → json; any conforming validator
917
+ can implement the hook. A query using `$valid`/`$assert`/`$as` compiled
918
+ **without** a hook is compile error `JQ0008`; a hook that rejects a schema
919
+ literal (throws) is compile error `JQ0009` at the operator's `docPath`.
920
+
921
+ The same package closes the loop in the other direction: its `$query`
922
+ schema keyword embeds a query document inside a JSON Schema and asserts the
923
+ query's EBV (§2.2) against each validated instance — schemas inside queries
924
+ here, queries inside schemas there. Compiled queries expose `query.ebv`
925
+ beside `first`/`exists` for exactly this. See the `@jarenjs/validate`
926
+ README's "`$query` — cross-field assertions" section.
927
+
928
+ ---
929
+
930
+ ## 9. Variables, scoping, and external parameters
931
+
932
+ 1. Variables are introduced by `$for`, `$let`, `$at`, `$count`, `$groupby`
933
+ key names, and quantifier bindings. A variable is in scope in every
934
+ *later* clause of its phrase (per the semantic order of §6.1), in later
935
+ bindings of the same binding object (§6.2), and in all nested expressions
936
+ there — but **not** inside embedded path filters (§3.2).
937
+ 2. Inner bindings shadow outer bindings of the same name. Duplicate bindings
938
+ within one phrase are `JQ0007` (§6.3).
939
+ 3. **Externals.** A variable referenced but not bound by any enclosing phrase
940
+ is an **external parameter**. The set of external names is collected at
941
+ compile time and exposed by the compiled query; callers bind externals at
942
+ call time. Evaluating a reference to an external that the caller did not
943
+ bind is runtime error `JQ2006`. (There is no declaration syntax; use is
944
+ the declaration. Consumers MAY offer static "all externals bound" checks.)
945
+
946
+ ```json
947
+ { "$query": "0.1",
948
+ "$expr": { "$for": { "b": "$.store.book[*]" },
949
+ "$where": { "$ge": ["$b.price", "$minPrice"] },
950
+ "$return": "$b.title" } }
951
+ ```
952
+
953
+ `$minPrice` is free — an external the caller binds at call time.
954
+
955
+ ---
956
+
957
+ ## 10. Errors
958
+
959
+ ### 10.1 Error objects
960
+
961
+ Consumers MUST raise compile-time errors as `JsonQueryCompileError` and
962
+ runtime errors as `JsonQueryRuntimeError`. Every error carries:
963
+
964
+ - `code` — a stable identifier from the registry below;
965
+ - `docPath` — an **RFC 6901 JSON Pointer into the query document** locating
966
+ the offending construct (e.g. `/$expr/$where/$eq/1`);
967
+ - `message` — human-readable, non-normative.
968
+
969
+ ### 10.2 Compile errors (`JQ0xxx`)
970
+
971
+ | Code | Condition | XQuery analogue (non-normative) |
972
+ |---|---|---|
973
+ | `JQ0001` | Object mixes `$`-prefixed and plain keys (§3.1) | XPST0003 |
974
+ | `JQ0002` | Unknown operator / `$`-key outside the vocabulary | XPST0017 |
975
+ | `JQ0003` | Known phrase with bad arity, value shape, or key combination | XPST0003 |
976
+ | `JQ0004` | String starting `$` is not a valid path or escape (§3.2) | XPST0003 |
977
+ | `JQ0005` | Variable reference that is neither bound nor collectible as an external (reserved for closed-world compilation modes; see §9); also an `$as` member naming a variable not bound by its phrase's `$for`/`$let` (§6.8) | XPST0008 |
978
+ | `JQ0006` | Version envelope with unknown or non-string `$query` (§4.2) | XQST0031 |
979
+ | `JQ0007` | Duplicate variable binding within one phrase (§6.3) | XQST0089 |
980
+ | `JQ0008` | Schema operator (`$valid`/`$assert`/`$as`) in a query compiled without a type-test compiler (§8.11) | XQST0009 |
981
+ | `JQ0009` | Schema literal rejected by the type-test compiler (invalid embedded schema, §8.11) | XQST0059 |
982
+
983
+ ### 10.3 Runtime errors (`JQ2xxx`)
984
+
985
+ | Code | Condition | XQuery analogue (non-normative) |
986
+ |---|---|---|
987
+ | `JQ2001` | Runtime type error (non-number arithmetic operand, bad cast, ...) | XPTY0004 |
988
+ | `JQ2002` | `$idiv`/`$mod` by zero (§8.5) | FOAR0001 |
989
+ | `JQ2003` | EBV of a multi-item sequence (§2.2) | FORG0006 |
990
+ | `JQ2004` | `$map` key expression not a single string (§3.5.2) | XPTY0004 |
991
+ | `JQ2005` | Incomparable `$orderby`/`$sort` keys (§6.6, §8.9) | XPTY0004 |
992
+ | `JQ2006` | Reference to an unbound external parameter (§9) | XPDY0002 |
993
+ | `JQ2007` | Resource guard: an operator result exceeding an implementation limit (`$range` over 2³² items, §8.9) | XPDY0130 |
994
+ | `JQ2008` | Schema assertion failure: an item rejected by `$assert`'s schema, or a bound variable rejected by its `$as` schema (§6.8, §8.11) | XPTY0004 |
995
+
996
+ ---
997
+
998
+ ## 11. Deviations from XQuery 3.1
999
+
1000
+ Normative registry. Everything not listed here follows XQuery 3.1.
1001
+
1002
+ | # | Deviation | Rationale |
1003
+ |---|---|---|
1004
+ | **D1** | All numbers are IEEE 754 doubles; no integer/decimal/float tower. `$div` by 0 is IEEE (`±Infinity`/`NaN`); `$idiv`/`$mod` by 0 error. | JSON has one number type; JavaScript engines have one. A numeric tower would be a fiction the data model cannot honor. |
1005
+ | **D2** | `$eq`/`$ne` item equality is deep structural JSON equality (arrays/objects compare by structure), numbers mathematically. Existential lifting over sequences is retained. | XQuery's `eq` on maps/arrays is an error; for JSON, structural equality is the useful, obvious relation (and `$groupby`/`$distinct` need it anyway). |
1006
+ | **D3** | EBV of a singleton array or object is `true`. | XQuery raises FORG0006 for function items; for JSON data, "the object is there" is the intuitive truthiness. Keeps EBV total over singletons. |
1007
+ | **D4** | `$for` unpacks an item that is an array into its members (one level). Objects are not unpacked. | RFC 9535 paths return arrays as values; without unpacking, `$for` over `"$b.tags"` would iterate one array item. Escape: `$let` / `$const`. |
1008
+ | **D5** | Regex operators use I-Regexp (RFC 9485), not XSD regular expressions. No flags argument. | Matches RFC 9535's regex dialect (one regex language across path filters and operators) and is interoperable by design. |
1009
+ | **D6** | Positional values are 0-based: `$at`, `$count` (clause), `$substring`/`$subsequence` starts, `$index-of` results, `$get` indexes. | JSON and RFC 9535 array indexes are 0-based; a 1-based island inside them invites off-by-one errors. |
1010
+ | **D7** | FLWOR clause keys apply in the fixed semantic order of §6.1 regardless of document key order; clause interleavings are expressed by nesting. One `$where`/`$groupby`/`$orderby`/`$count` per phrase. | JSON key order is not interoperable across ecosystems; semantics must not depend on it. Nesting expresses every interleaving. See also the correlation interop caveat, §6.2. |
1011
+
1012
+ ---
1013
+
1014
+ ## 12. The JSON Schema for query documents
1015
+
1016
+ ### 12.1 Artifacts and draft policy
1017
+
1018
+ The complete structural grammar of this format is published as JSON Schema,
1019
+ in two artifacts:
1020
+
1021
+ - `packages/json/schemas/jaren-query.schema.json` — **canonical**, draft
1022
+ 2020-12, `$id` `https://jarenjs.dev/schemas/jaren-query/0.1`.
1023
+ - `packages/json/schemas/jaren-query.draft-07.schema.json` — the draft-07
1024
+ twin, `$id` `https://jarenjs.dev/schemas/jaren-query/0.1/draft-07`.
1025
+
1026
+ Nothing in this format requires post-draft-07 keywords, so the canonical
1027
+ schema is authored in a **draft-neutral keyword subset** and the twin is a
1028
+ **mechanical derivation**: swap `$schema`, rename `$defs` →
1029
+ `definitions`, rewrite `#/$defs/` ref targets, suffix the `$id` with
1030
+ `/draft-07`. The subset rules (normative for schema maintenance):
1031
+
1032
+ - no keywords beside `$ref` in any schema object that has `$ref` (draft-07
1033
+ ignores `$ref` siblings; wrap in `allOf` where composition is needed);
1034
+ - no `unevaluatedProperties` / `unevaluatedItems`;
1035
+ - no `$dynamicRef` / `$dynamicAnchor`;
1036
+ - no tuple validation (2020-12 `prefixItems` and draft-07 array-form `items`
1037
+ are mutually incompatible) — operator argument arrays validate with uniform
1038
+ `items` plus `minItems`/`maxItems`. Nothing is lost: every argument
1039
+ position is an expression anyway, and per-position facts ("this key must
1040
+ evaluate to a string") are runtime rules owned by the compiler (`JQ2xxx`),
1041
+ not structure;
1042
+ - allowed keywords: `type`, `oneOf`/`anyOf`/`allOf`, `properties`,
1043
+ `required`, `additionalProperties`, `patternProperties`, `propertyNames`,
1044
+ `pattern`, `enum`/`const`, boolean schemas, `minProperties`/
1045
+ `maxProperties`, `minItems`/`maxItems`, uniform `items`, `format`,
1046
+ recursive `$ref` — all draft-06+ with identical semantics in both drafts.
1047
+
1048
+ ### 12.2 What the schema does and does not enforce
1049
+
1050
+ The schema is **structural** validation; the compiler remains authoritative.
1051
+ The schema enforces: the object partition rule (a map constructor admits no
1052
+ `$`-prefixed key, an operator phrase admits only its own keys — a mixed
1053
+ object matches neither and fails), the closed operator vocabulary and all
1054
+ arities, binding-object shapes and variable-name lexemes, the envelope, and
1055
+ the string forms of §3.2 (query-string head patterns; absolute paths
1056
+ additionally carry `format: "json-path"` — the RFC 9535 format implemented by
1057
+ `@jarenjs/formats`; note `format` is annotation-only by default from draft
1058
+ 2020-12 unless the validator enables assertion).
1059
+
1060
+ The schema cannot express, and therefore leaves to the compiler (stated in
1061
+ `description`s in the artifacts): fixed clause ordering (semantic, not
1062
+ structural — every key order is valid JSON), variable scoping and duplicate
1063
+ detection (`JQ0005`/`JQ0007`), `$as` name binding (`JQ0005`), full grammar
1064
+ of variable-rooted path segments (only the head is pattern-checked), and all
1065
+ runtime typing rules. Schema-literal positions (§8.11) validate as `true` —
1066
+ draft-neutral by definition; embedded JSON Schemas are deliberately **not**
1067
+ meta-validated by these artifacts (the type-test compiler is authoritative,
1068
+ `JQ0009`). Where the schema and this text disagree, this text wins and the
1069
+ schema has a bug.
1070
+
1071
+ ---
1072
+
1073
+ ## Appendix A. Worked examples (normative fixtures)
1074
+
1075
+ Each example is committed verbatim as a fixture under
1076
+ `test/json/fixtures/query-format/valid/` and MUST validate against both
1077
+ schema artifacts. Examples run against the RFC 9535 bookstore document
1078
+ (plus a `ratings` array where shown).
1079
+
1080
+ ### A.1 Degenerate query — a bare JSONPath string
1081
+
1082
+ ```json
1083
+ "$.store.book[?@.price < 10].title"
1084
+ ```
1085
+
1086
+ The complete query document. Result: the titles of the cheap books, in
1087
+ document order.
1088
+
1089
+ ### A.2 Bookstore FLWOR
1090
+
1091
+ ```json
1092
+ { "$for": { "b": "$.store.book[*]" },
1093
+ "$where": { "$lt": ["$b.price", 10] },
1094
+ "$orderby": "$b.price",
1095
+ "$return": { "title": "$b.title", "price": "$b.price" } }
1096
+ ```
1097
+
1098
+ One object per cheap book, cheapest first.
1099
+
1100
+ ### A.3 Join
1101
+
1102
+ ```json
1103
+ { "$for": { "b": "$.store.book[*]", "r": "$.ratings[*]" },
1104
+ "$where": { "$eq": ["$b.isbn", "$r.isbn"] },
1105
+ "$orderby": "$b.price",
1106
+ "$return": { "title": "$b.title", "stars": "$r.stars" } }
1107
+ ```
1108
+
1109
+ Nested iteration over books × ratings; the `$where` equijoin keeps matching
1110
+ pairs (books without `isbn` produce the empty sequence on the left — the
1111
+ existential `$eq` is false, so they drop out; contrast §5.2).
1112
+
1113
+ ### A.4 Group + aggregate
1114
+
1115
+ ```json
1116
+ { "$for": { "b": "$.store.book[*]" },
1117
+ "$groupby": { "genre": "$b.category" },
1118
+ "$return": { "genre": "$genre",
1119
+ "count": { "$count": "$b" },
1120
+ "avg": { "$avg": "$b.price" } } }
1121
+ ```
1122
+
1123
+ Books per genre: after grouping, `$b` is the group's book sequence (§6.5).
1124
+
1125
+ ### A.5 `$let` + reshaping with computed keys
1126
+
1127
+ ```json
1128
+ { "$let": { "prefix": "col_", "book": "$.store.book[0]" },
1129
+ "$return": { "$map": [
1130
+ [{ "$concat": ["$prefix", "title"] }, "$book.title"],
1131
+ [{ "$concat": ["$prefix", "price"] }, "$book.price"]
1132
+ ] } }
1133
+ ```
1134
+
1135
+ ### A.6 `$const` and `$$` escapes in one document
1136
+
1137
+ ```json
1138
+ { "template": { "$const": { "$for": "kept verbatim", "price": null } },
1139
+ "label": "$$price" }
1140
+ ```
1141
+
1142
+ A map constructor whose `template` member is quoted data (the inner `$for`
1143
+ is never interpreted) and whose `label` member is the literal string
1144
+ `"$price"`.
1145
+
1146
+ ### A.7 Quantifier over a variable-rooted path
1147
+
1148
+ ```json
1149
+ { "$let": { "books": "$.store.book[*]" },
1150
+ "$return": { "$some": { "b": "$books" },
1151
+ "$satisfies": { "$gt": ["$b.price", 20] } } }
1152
+ ```
1153
+
1154
+ ### A.8 External parameter
1155
+
1156
+ ```json
1157
+ { "$query": "0.1",
1158
+ "$expr": { "$for": { "b": "$.store.book[*]" },
1159
+ "$where": { "$ge": ["$b.price", "$minPrice"] },
1160
+ "$return": "$b.title" } }
1161
+ ```
1162
+
1163
+ `$minPrice` is an external (§9), bound by the caller; also demonstrates the
1164
+ version envelope.
1165
+
1166
+ ### A.9 Schema type tests — `$as` and `$valid`
1167
+
1168
+ ```json
1169
+ { "$for": { "b": "$.store.book[?@.isbn]", "r": "$.ratings[*]" },
1170
+ "$as": { "b": { "type": "object", "required": ["isbn", "price"] } },
1171
+ "$where": { "$and": [
1172
+ { "$eq": ["$b.isbn", "$r.isbn"] },
1173
+ { "$valid": ["$r.stars", { "type": "number", "minimum": 0, "maximum": 5 }] }
1174
+ ] },
1175
+ "$orderby": "$b.price",
1176
+ "$return": { "title": "$b.title", "stars": "$r.stars" } }
1177
+ ```
1178
+
1179
+ The join of A.3 with JSON Schema as the type system (§6.8, §8.11): the path
1180
+ filter pre-selects books that have an `isbn`, the `$as` clause *asserts*
1181
+ that every joined `b` is an object carrying `isbn` and `price` (a violation
1182
+ would be `JQ2008`, not a dropped tuple), and the `$valid` conjunct *filters*
1183
+ rating pairs to plausible star values. Note the schema literals are verbatim
1184
+ JSON Schema — their keywords are not query operators.
1185
+
1186
+ ---
1187
+
1188
+ ## Appendix B. LLM structured output (non-normative)
1189
+
1190
+ The entire language is one JSON Schema. That is not an implementation detail;
1191
+ it is a headline feature.
1192
+
1193
+ Constrained decoding — the "structured output" mode of every major LLM
1194
+ serving stack — takes a JSON Schema and makes it impossible for the model to
1195
+ emit a token that leads outside the schema. Point that machinery at
1196
+ `jaren-query.schema.json` and a model **cannot produce a structurally invalid
1197
+ query**: no unknown operators, no three-argument `$eq`, no mixed
1198
+ `$`/plain-key objects, no malformed version envelope. The closed vocabulary,
1199
+ exact arities, and anchored patterns in the schema were designed with this
1200
+ consumer in mind — every constraint the schema can express is one hallucination
1201
+ class removed at generation time, before any code runs.
1202
+
1203
+ The residue is small and cheap to check: path-string grammar beyond the
1204
+ anchored head patterns (fully validated where the `json-path` format is
1205
+ asserted, since absolute path leaves carry `format: "json-path"`), variable
1206
+ scoping, and the runtime typing rules — exactly the `JQ0xxx`/`JQ2xxx` errors
1207
+ of §10, which arrive with a `docPath` pointer that can be fed straight back
1208
+ to the model for repair. Compare the usual alternative — asking a model to
1209
+ emit a bespoke query DSL as free text and parsing it hopefully — and the
1210
+ trade is: grammar errors eliminated by construction, semantic errors reduced
1211
+ to a machine-checkable, machine-repairable residue.
1212
+
1213
+ Because query documents are plain JSON, they also travel well through the
1214
+ rest of an LLM toolchain: function-call arguments, retrieval filters, and
1215
+ audit logs all speak JSON already, and a generated query can be validated,
1216
+ diffed, stored, and replayed without ever touching a parser.
1217
+
1218
+ The same constrained-decoding model applies to complete recursive
1219
+ stylesheets; see
1220
+ [JSLT-FORMAT Appendix B](./JSLT-FORMAT.md#appendix-b-llm-structured-output-non-normative)
1221
+ and its mechanically query-derived schema twins.