@osqd/jql 0.1.1

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 (115) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/LICENSE +102 -0
  3. package/README.md +84 -0
  4. package/bin/jql.mjs +5 -0
  5. package/conformance/cases.json +290 -0
  6. package/dist/array.d.ts +11 -0
  7. package/dist/async.d.ts +44 -0
  8. package/dist/canonical.d.ts +18 -0
  9. package/dist/cjs/array.d.ts +11 -0
  10. package/dist/cjs/async.d.ts +44 -0
  11. package/dist/cjs/canonical.d.ts +18 -0
  12. package/dist/cjs/cli.d.ts +15 -0
  13. package/dist/cjs/collections.d.ts +34 -0
  14. package/dist/cjs/core.d.ts +117 -0
  15. package/dist/cjs/errors.d.ts +17 -0
  16. package/dist/cjs/explain.d.ts +30 -0
  17. package/dist/cjs/global.d.ts +90 -0
  18. package/dist/cjs/group.d.ts +47 -0
  19. package/dist/cjs/index.d.ts +25 -0
  20. package/dist/cjs/internal/closest.d.ts +9 -0
  21. package/dist/cjs/internal/duration.d.ts +15 -0
  22. package/dist/cjs/internal/equal.d.ts +34 -0
  23. package/dist/cjs/internal/glob.d.ts +30 -0
  24. package/dist/cjs/internal/order.d.ts +24 -0
  25. package/dist/cjs/internal/path.d.ts +109 -0
  26. package/dist/cjs/internal/record.d.ts +18 -0
  27. package/dist/cjs/internal/values.d.ts +41 -0
  28. package/dist/cjs/limits.d.ts +64 -0
  29. package/dist/cjs/operators.d.ts +81 -0
  30. package/dist/cjs/package.json +3 -0
  31. package/dist/cjs/plan.d.ts +79 -0
  32. package/dist/cjs/search.d.ts +42 -0
  33. package/dist/cjs/targets/mongo.d.ts +55 -0
  34. package/dist/cjs/text/index.d.ts +12 -0
  35. package/dist/cjs/text/parse.d.ts +91 -0
  36. package/dist/cjs/text/suggest.d.ts +16 -0
  37. package/dist/cjs/text/write.d.ts +34 -0
  38. package/dist/cjs/types.d.ts +236 -0
  39. package/dist/cjs/vocabulary.d.ts +106 -0
  40. package/dist/cli.d.ts +15 -0
  41. package/dist/cli.js +2729 -0
  42. package/dist/cli.js.map +1 -0
  43. package/dist/collections.d.ts +34 -0
  44. package/dist/core.d.ts +117 -0
  45. package/dist/errors.d.ts +17 -0
  46. package/dist/explain.d.ts +30 -0
  47. package/dist/global.cjs +1953 -0
  48. package/dist/global.cjs.map +1 -0
  49. package/dist/global.d.ts +90 -0
  50. package/dist/global.js +1950 -0
  51. package/dist/global.js.map +1 -0
  52. package/dist/group.d.ts +47 -0
  53. package/dist/index.cjs +2529 -0
  54. package/dist/index.cjs.map +1 -0
  55. package/dist/index.d.ts +25 -0
  56. package/dist/index.js +2495 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/internal/closest.d.ts +9 -0
  59. package/dist/internal/duration.d.ts +15 -0
  60. package/dist/internal/equal.d.ts +34 -0
  61. package/dist/internal/glob.d.ts +30 -0
  62. package/dist/internal/order.d.ts +24 -0
  63. package/dist/internal/path.d.ts +109 -0
  64. package/dist/internal/record.d.ts +18 -0
  65. package/dist/internal/values.d.ts +41 -0
  66. package/dist/limits.d.ts +64 -0
  67. package/dist/mongo.cjs +357 -0
  68. package/dist/mongo.cjs.map +1 -0
  69. package/dist/mongo.js +354 -0
  70. package/dist/mongo.js.map +1 -0
  71. package/dist/operators.d.ts +81 -0
  72. package/dist/plan.d.ts +79 -0
  73. package/dist/search.d.ts +42 -0
  74. package/dist/targets/mongo.d.ts +55 -0
  75. package/dist/text/index.d.ts +12 -0
  76. package/dist/text/parse.d.ts +91 -0
  77. package/dist/text/suggest.d.ts +16 -0
  78. package/dist/text/write.d.ts +34 -0
  79. package/dist/text.cjs +674 -0
  80. package/dist/text.cjs.map +1 -0
  81. package/dist/text.js +667 -0
  82. package/dist/text.js.map +1 -0
  83. package/dist/types.d.ts +236 -0
  84. package/dist/vocabulary.d.ts +106 -0
  85. package/docs/course/01-first-query.md +217 -0
  86. package/docs/course/02-operators.md +285 -0
  87. package/docs/course/03-arrays-and-paths.md +239 -0
  88. package/docs/course/04-combining.md +221 -0
  89. package/docs/course/05-dates.md +214 -0
  90. package/docs/course/06-typed-queries.md +240 -0
  91. package/docs/course/07-requests.md +261 -0
  92. package/docs/course/08-grouping.md +210 -0
  93. package/docs/course/09-explaining.md +171 -0
  94. package/docs/course/10-vocabulary.md +276 -0
  95. package/docs/course/11-the-search-box.md +349 -0
  96. package/docs/course/12-untrusted.md +257 -0
  97. package/docs/course/13-saved-filters.md +199 -0
  98. package/docs/course/14-streams-and-cli.md +276 -0
  99. package/docs/course/15-pushdown.md +240 -0
  100. package/docs/course/16-extending.md +199 -0
  101. package/docs/course/index.md +185 -0
  102. package/docs/design/decisions.md +198 -0
  103. package/docs/design/performance.md +102 -0
  104. package/docs/guides/adopting.md +81 -0
  105. package/docs/guides/pushdown.md +147 -0
  106. package/docs/guides/typescript.md +115 -0
  107. package/docs/guides/untrusted-input.md +86 -0
  108. package/docs/index.md +102 -0
  109. package/docs/reference/api.md +266 -0
  110. package/docs/reference/cli.md +103 -0
  111. package/docs/reference/index.md +12 -0
  112. package/docs/reference/specification.md +549 -0
  113. package/docs/reference/text-syntax.md +152 -0
  114. package/docs/start/quick-start.md +84 -0
  115. package/package.json +136 -0
@@ -0,0 +1,199 @@
1
+ # Lesson 13 — Saved filters
2
+
3
+ **Goal:** give every question one shape and one short name, so a list of saved filters does
4
+ not quietly hold the same filter three times.
5
+
6
+ ← [Course](index.md) · Previous: [Queries from outside](12-untrusted.md) · Next: [Logs and streams](14-streams-and-cli.md)
7
+
8
+ ---
9
+
10
+ ## The same question, four spellings
11
+
12
+ Once people can save a filter, they save the same one repeatedly. One was typed into the
13
+ search box, one was written by hand, one was built by a form, one came back from an API with
14
+ its keys in a different order.
15
+
16
+ They are one question and four rows in a table — and a cache keyed on the query text misses on
17
+ every one of them.
18
+
19
+ Two functions fix that:
20
+
21
+ - **`canonical(query)`** rewrites a query into one fixed shape;
22
+ - **`fingerprint(query)`** names that shape in sixteen characters.
23
+
24
+ ## One filter, one name
25
+
26
+ ```js
27
+ import { fingerprint } from "@osqd/jql";
28
+
29
+ const written = [
30
+ { status: "open", total: { $gte: 100 } },
31
+ { total: { $gte: 100 }, status: "open" },
32
+ { status: { $eq: "open" }, total: { $gte: 100 } },
33
+ { $and: [{ status: "open" }, { total: { $gte: 100 } }], $comment: "chasing" },
34
+ ];
35
+
36
+ for (const query of written) console.log(fingerprint(query), JSON.stringify(query));
37
+
38
+ console.log("distinct filters here:", new Set(written.map(fingerprint)).size);
39
+ ```
40
+
41
+ ```
42
+ 03646d4ac8b6def3 {"status":"open","total":{"$gte":100}}
43
+ 03646d4ac8b6def3 {"total":{"$gte":100},"status":"open"}
44
+ 03646d4ac8b6def3 {"status":{"$eq":"open"},"total":{"$gte":100}}
45
+ 03646d4ac8b6def3 {"$and":[{"status":"open"},{"total":{"$gte":100}}],"$comment":"chasing"}
46
+ distinct filters here: 1
47
+ ```
48
+
49
+ Four spellings, one name. Note the fourth in particular: a `$and` of two clauses, and a
50
+ comment, and it still lands on the same fingerprint as the other three.
51
+
52
+ ## The shape behind the name
53
+
54
+ ```js
55
+ import { canonical } from "@osqd/jql";
56
+
57
+ const typed = { $and: [{ status: "open" }, { total: { $gte: 100 } }], $comment: "chasing" };
58
+
59
+ console.log(JSON.stringify(canonical(typed), null, 2));
60
+ ```
61
+
62
+ ```
63
+ {
64
+ "status": {
65
+ "$eq": "open"
66
+ },
67
+ "total": {
68
+ "$gte": 100
69
+ }
70
+ }
71
+ ```
72
+
73
+ What comes back **is still a query** — you can run it, store it, or put it in a URL — and it
74
+ matches exactly what the original matched. The test suite holds it to that on thousands of
75
+ generated queries by running both and comparing answers, rather than by comparing shapes.
76
+
77
+ | Written | Becomes |
78
+ | --- | --- |
79
+ | `{ a: 1 }` | `{ a: { $eq: 1 } }` — a bare value is a condition |
80
+ | keys in any order | keys sorted |
81
+ | `$comment: "…"` | dropped: a note decides nothing |
82
+ | `{ $and: [x, y] }` | `{ …x, …y }` — the box writes the first, a person writes the second |
83
+ | `$and` inside `$and` | one `$and` |
84
+ | `{ $in: [3, 1, 3] }` | `{ $in: [1, 3] }` — a list that is a set is sorted and deduplicated |
85
+ | `$options: "ui"` | `$options: "iu"` |
86
+ | the branches of an `$or` | sorted, so branch order stops mattering |
87
+
88
+ ## Different questions, different names
89
+
90
+ ```js
91
+ import { fingerprint } from "@osqd/jql";
92
+
93
+ const questions = [
94
+ { status: "open" },
95
+ { status: "paid" },
96
+ { status: { $ne: "open" } },
97
+ { $or: [{ status: "open" }, { status: "paid" }] },
98
+ { $or: [{ status: "paid" }, { status: "open" }] },
99
+ ];
100
+
101
+ for (const query of questions) console.log(fingerprint(query), JSON.stringify(query));
102
+ ```
103
+
104
+ ```
105
+ d4d13aede48bfdad {"status":"open"}
106
+ 0c24bc335f3c3269 {"status":"paid"}
107
+ a5ba405e8cd835c8 {"status":{"$ne":"open"}}
108
+ bc66ac7eefa26bc0 {"$or":[{"status":"open"},{"status":"paid"}]}
109
+ bc66ac7eefa26bc0 {"$or":[{"status":"paid"},{"status":"open"}]}
110
+ ```
111
+
112
+ The last two are the same question with its branches the other way round, so they share a
113
+ name.
114
+
115
+ ## What it deliberately will not do
116
+
117
+ ```js
118
+ import { fingerprint } from "@osqd/jql";
119
+
120
+ console.log(fingerprint({ total: { $gt: 3 } }), "{ total: { $gt: 3 } }");
121
+ console.log(fingerprint({ total: { $gte: 4 } }), "{ total: { $gte: 4 } }");
122
+ ```
123
+
124
+ ```
125
+ e7a73371277f4960 { total: { $gt: 3 } }
126
+ a27bb5f54416fc83 { total: { $gte: 4 } }
127
+ ```
128
+
129
+ Those two agree on every integer, and disagree on 3.5. They get different names, and that is
130
+ the honest limit of the whole idea:
131
+
132
+ > **Equal fingerprints mean the same query. Different fingerprints mean only that the queries
133
+ > are written differently.**
134
+
135
+ One direction is a cheap, decidable property. The other — deciding whether two arbitrary
136
+ queries select the same rows — is not, and a tool that claimed to do it would be wrong in ways
137
+ you would find out about much later.
138
+
139
+ ## About the name itself
140
+
141
+ A fingerprint is a 64-bit checksum in hexadecimal. It is **not** a cryptographic hash: two
142
+ different queries can collide, rarely, and never in a way that matters for a cache you are
143
+ allowed to miss.
144
+
145
+ It **is** stable across processes and across versions of this library, so it is safe as a
146
+ stored key. A set of fingerprints is written into the test suite precisely so that changing
147
+ what a query is called takes a deliberate act rather than happening as a side effect.
148
+
149
+ ## Where this earns its place
150
+
151
+ - **A saved-filter list.** Fingerprint before inserting, and say "you already have this one,
152
+ saved as *Chasing large orders*" instead of adding a fifth copy.
153
+ - **A cache key.** `` `orders:${fingerprint(query)}` ``.
154
+ - **A log line.** Sixteen characters that say which filter ran, without printing a query that
155
+ may be long and may contain a customer's email address.
156
+ - **Telling two dashboards apart** when both claim to show "open orders" and one of them is
157
+ wrong.
158
+
159
+ ## Exercise
160
+
161
+ Harbour's filter list stores `{ name, query }`. Write `save(list, name, query)` so that saving
162
+ a duplicate gives back the existing entry instead of adding a row.
163
+
164
+ <details>
165
+ <summary>Answer</summary>
166
+
167
+ ```js
168
+ import { canonical, fingerprint } from "@osqd/jql";
169
+
170
+ const save = (list, name, query) => {
171
+ const key = fingerprint(query);
172
+ const already = list.find((entry) => entry.key === key);
173
+ if (already !== undefined) return { saved: false, as: already.name };
174
+ list.push({ key, name, query: canonical(query) });
175
+ return { saved: true, as: name };
176
+ };
177
+
178
+ const list = [];
179
+
180
+ console.log(save(list, "Chasing large orders", { status: "open", total: { $gte: 100 } }));
181
+ console.log(save(list, "Big open ones", { total: { $gte: 100 }, status: { $eq: "open" } }));
182
+ console.log("rows in the list:", list.length);
183
+ ```
184
+
185
+ ```
186
+ { saved: true, as: 'Chasing large orders' }
187
+ { saved: false, as: 'Chasing large orders' }
188
+ rows in the list: 1
189
+ ```
190
+
191
+ Store `canonical(query)` rather than whatever was typed, so the row you read back tomorrow has
192
+ one shape instead of whichever spelling happened to arrive first. Keep the fingerprint beside
193
+ it, so the lookup is an index rather than a scan.
194
+ </details>
195
+
196
+ ## Related
197
+
198
+ - [Library API](../reference/api.md) — `canonical` and `fingerprint`
199
+ - [Specification §13](../reference/specification.md#13-canonical-form-optional) — the rules, so another implementation agrees
@@ -0,0 +1,276 @@
1
+ # Lesson 14 — Logs and streams
2
+
3
+ **Goal:** run a query over something that arrives over time, and over a file, from a shell.
4
+
5
+ ← [Course](index.md) · Previous: [Saved filters](13-saved-filters.md) · Next: [Pushing into a store](15-pushdown.md)
6
+
7
+ ---
8
+
9
+ ## Data that arrives over time
10
+
11
+ Harbour exports its order book nightly as JSON lines. That is the shape most real data arrives
12
+ in — a log, a `kubectl` dump, a store's export — and by the time it matters it is too big to
13
+ hold in memory.
14
+
15
+ The asynchronous helpers are the same engine with an `await` in the loop. The query is
16
+ compiled once, before the first item, and **they stop reading as soon as the answer cannot
17
+ change** — which is what makes a source that never ends answerable at all.
18
+
19
+ First, write the export:
20
+
21
+ ```bash
22
+ node -e "import('./orders.mjs').then(({orders}) => require('node:fs').writeFileSync('orders.jsonl', orders.map(o => JSON.stringify(o)).join('\n') + '\n'))"
23
+ ```
24
+
25
+ Then save this as `harbour/exported.mjs`, so the examples can read it back a line at a time:
26
+
27
+ ```js
28
+ import { createReadStream } from "node:fs";
29
+ import { createInterface } from "node:readline";
30
+
31
+ /** Every line of the nightly export, parsed, as it arrives. */
32
+ export async function* exported() {
33
+ const reader = createInterface({
34
+ input: createReadStream("orders.jsonl", "utf8"),
35
+ crlfDelay: Number.POSITIVE_INFINITY,
36
+ });
37
+ for await (const line of reader) if (line.trim() !== "") yield JSON.parse(line);
38
+ }
39
+ ```
40
+
41
+ ## Asking a source the usual questions
42
+
43
+ ```js
44
+ import { countAsync, findAsync, someAsync } from "@osqd/jql";
45
+ import { exported } from "./exported.mjs";
46
+
47
+ console.log("the first open one:", (await findAsync(exported(), { status: "open" })).id);
48
+ console.log("how many paid :", await countAsync(exported(), { status: "paid" }));
49
+ console.log("anything over £200:", await someAsync(exported(), { total: { $gt: 200 } }));
50
+ ```
51
+
52
+ ```
53
+ the first open one: o-1002
54
+ how many paid : 4
55
+ anything over £200: true
56
+ ```
57
+
58
+ Same names as lesson 1, same queries, `await` in front. `findAsync` and `someAsync` stop at
59
+ the first match — the rest of the file is never read.
60
+
61
+ ## A page from a stream
62
+
63
+ ```js
64
+ import { searchAsync } from "@osqd/jql";
65
+ import { exported } from "./exported.mjs";
66
+
67
+ const page = await searchAsync(exported(), {
68
+ where: { status: { $ne: "cancelled" } },
69
+ sort: { total: -1 },
70
+ limit: 2,
71
+ fields: ["id", "total"],
72
+ });
73
+
74
+ console.log(page);
75
+ ```
76
+
77
+ ```
78
+ [ { id: 'o-1005', total: 170 }, { id: 'o-1004', total: 36 } ]
79
+ ```
80
+
81
+ ## A stream you can walk away from
82
+
83
+ ```js
84
+ import { filterStream } from "@osqd/jql";
85
+ import { exported } from "./exported.mjs";
86
+
87
+ for await (const order of filterStream(exported(), { "customer.country": "GB" })) {
88
+ console.log(order.id, order.customer.name);
89
+ if (order.id === "o-1003") break;
90
+ }
91
+
92
+ console.log("stopped early, and the file stopped being read");
93
+ ```
94
+
95
+ ```
96
+ o-1001 Ada Lovelace
97
+ o-1003 Alan Turing
98
+ stopped early, and the file stopped being read
99
+ ```
100
+
101
+ `filterStream` yields matches as it finds them. Stop consuming and the source stops being
102
+ pulled — which is the whole point of the next example.
103
+
104
+ ## A source that never ends
105
+
106
+ ```js
107
+ import { filterStream } from "@osqd/jql";
108
+
109
+ /** A feed with no end, like a socket or a tail. */
110
+ async function* forever() {
111
+ for (let i = 0; ; i++) yield { id: i, status: i % 3 === 0 ? "open" : "paid" };
112
+ }
113
+
114
+ const pulled = [];
115
+ for await (const item of filterStream(forever(), { status: "open" })) {
116
+ pulled.push(item.id);
117
+ if (pulled.length === 3) break;
118
+ }
119
+
120
+ console.log(pulled);
121
+ ```
122
+
123
+ ```
124
+ [ 0, 3, 6 ]
125
+ ```
126
+
127
+ That program terminates. Nothing pulls from `forever()` once the loop breaks, so an endless
128
+ feed is an ordinary source as long as the question has a stopping point.
129
+
130
+ ## What stops early, and what cannot
131
+
132
+ | | |
133
+ | --- | --- |
134
+ | `findAsync`, `someAsync` | stop at the first match |
135
+ | `filterStream` | yields as it goes; stop consuming and the source stops |
136
+ | `searchAsync` **without** a sort | stops once the page is full |
137
+ | `searchAsync` **with** a sort | reads everything — it has to — but holds only `skip + limit` items while it does |
138
+
139
+ That last row is the one that surprises people, and it is worth being explicit: an order over
140
+ items you have not seen yet is not an order. A sorted page needs the whole source. What it
141
+ does *not* need is the whole source in memory, and the bounded heap from lesson 7 keeps only
142
+ the page you asked for.
143
+
144
+ ## The same thing from a shell
145
+
146
+ The library ships the command that does all this over a file, which is often quicker than
147
+ writing the script:
148
+
149
+ ```bash
150
+ jql '{"status":"open"}' orders.jsonl | head -1
151
+ ```
152
+
153
+ ```
154
+ {"id":"o-1002","placed":"2026-09-03T14:02:00Z","status":"open","channel":"web","customer":{"name":"Grace Hopper","country":"US","email":"grace@example.com"},"lines":[{"sku":"pad-a5","title":"A5 pad","quantity":10,"price":3}],"total":30,"paid":0,"tags":[]}
155
+ ```
156
+
157
+ ```bash
158
+ jql --count '{"customer.country":"GB"}' orders.jsonl
159
+ ```
160
+
161
+ ```
162
+ 3
163
+ ```
164
+
165
+ ```bash
166
+ jql --text 'status:paid total:>20' --sort total:desc --fields id,total orders.jsonl
167
+ ```
168
+
169
+ ```
170
+ {"id":"o-1006","total":33}
171
+ {"id":"o-1001","total":22.5}
172
+ ```
173
+
174
+ ```bash
175
+ jql --group status '{}' orders.jsonl
176
+ ```
177
+
178
+ ```
179
+ {"key":"paid","count":4}
180
+ {"key":"open","count":2}
181
+ {"key":"cancelled","count":1}
182
+ {"key":"refunded","count":1}
183
+ ```
184
+
185
+ ```bash
186
+ cat orders.jsonl | jql --text 'customer.country:gb' --fields id,customer.name -
187
+ ```
188
+
189
+ ```
190
+ {"id":"o-1001","customer":{"name":"Ada Lovelace"}}
191
+ {"id":"o-1003","customer":{"name":"Alan Turing"}}
192
+ {"id":"o-1005","customer":{"name":"Tim Berners-Lee"}}
193
+ ```
194
+
195
+ Two rules shape the command, and both are worth copying into tools of your own.
196
+
197
+ ### stdout is the artifact
198
+
199
+ Matching lines and nothing else, so `jql … > kept.jsonl` produces a file worth having. Counts,
200
+ warnings and explanations go to stderr, which is why `--explain` combines with anything:
201
+
202
+ ```bash
203
+ jql --explain --count '{"status":"paid"}' orders.jsonl
204
+ ```
205
+
206
+ ```
207
+ 4
208
+ ```
209
+
210
+ — on stdout, with the explanation beside it on stderr.
211
+
212
+ A matching line is written back **exactly as it arrived**, byte for byte, so filtering a file
213
+ cannot change the data in it. That is not fussiness: re-serialising turns a thirty-digit
214
+ identifier into `1.2345678901234568e+29`, and `1e400` into `null`, silently. Ask for the
215
+ result to be reshaped — `--fields`, `--omit`, `--group`, `--pretty` — and you get JSON,
216
+ because then you have asked for something other than the line.
217
+
218
+ ### A value is validated, never coerced
219
+
220
+ `--limit all` is an error rather than a limit that quietly stops limiting, and a query that is
221
+ not JQL is refused with the reason and exit code 2:
222
+
223
+ ```bash
224
+ jql '{"total":{"$gtt":1}}' orders.jsonl; echo "exit $?"
225
+ ```
226
+
227
+ ```
228
+ jql: at total.$gtt: "$gtt" is not an operator; did you mean "$gt"?
229
+ exit 2
230
+ ```
231
+
232
+ Note what `--text` did in the examples above: `customer.country:gb` uses the **storage path**,
233
+ because there is no vocabulary on the command line to translate a short name. Everything in
234
+ lesson 10 still applies in your own code; the command is the generic tool.
235
+
236
+ ## Exercise
237
+
238
+ Find the three largest unpaid orders in the export, printing only the id and the total, from
239
+ the shell alone.
240
+
241
+ <details>
242
+ <summary>Answer</summary>
243
+
244
+ ```bash
245
+ jql '{"paid":0}' --sort total:desc --limit 3 --fields id,total orders.jsonl
246
+ ```
247
+
248
+ ```
249
+ {"id":"o-1007","total":280}
250
+ {"id":"o-1005","total":170}
251
+ {"id":"o-1004","total":36}
252
+ ```
253
+
254
+ Now try the spelling you probably reached for first:
255
+
256
+ ```bash
257
+ jql --text 'paid:0' --sort total:desc --limit 3 --fields id,total orders.jsonl
258
+ ```
259
+
260
+ ```
261
+ ```
262
+
263
+ Nothing at all. **There is no vocabulary on the command line**, so every field is `text` — and
264
+ `paid:0` becomes `{ paid: { $contains: "0", $options: "i" } }`, which asks whether a *string*
265
+ contains a zero. `paid` holds a number, and `$contains` does not match numbers.
266
+
267
+ That is the argument for lesson 10 in a single line. A `kind` is what turns `paid:0` into a
268
+ comparison; without one the text syntax can only guess from the shape of what was typed, and
269
+ it guesses conservatively rather than coercing. On the command line, prefer JSON for anything
270
+ that is not prose.
271
+ </details>
272
+
273
+ ## Related
274
+
275
+ - [The command](../reference/cli.md) — every option, the exit codes, the two rules
276
+ - [Library API](../reference/api.md) — the `*Async` helpers