@omgbase/core 0.19.0 → 0.23.0
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.
- package/dist/src/core/ids.d.ts +1 -1
- package/dist/src/core/ingest.d.ts +2 -0
- package/dist/src/core/ingest.js +13 -0
- package/dist/src/core/read/document.d.ts +13 -9
- package/dist/src/core/read/document.js +18 -8
- package/dist/src/core/store/archive.d.ts +196 -0
- package/dist/src/core/store/archive.js +606 -0
- package/dist/src/core/store/gc.d.ts +37 -4
- package/dist/src/core/store/gc.js +62 -28
- package/dist/src/core/store/rebuild.d.ts +2 -0
- package/dist/src/core/store/rebuild.js +8 -2
- package/dist/src/core/store/retention.d.ts +138 -0
- package/dist/src/core/store/retention.js +409 -0
- package/dist/src/core/store/schema.d.ts +3 -2
- package/dist/src/core/store/schema.js +30 -4
- package/dist/src/core/store/size.d.ts +24 -2
- package/dist/src/core/store/size.js +33 -3
- package/dist/src/core/store/store.d.ts +1 -0
- package/dist/src/core/store/store.js +53 -1
- package/dist/src/graph/history.d.ts +2 -0
- package/dist/src/graph/history.js +8 -4
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -0
- package/dist/src/mcp/arguments.d.ts +10 -2
- package/dist/src/mcp/arguments.js +37 -3
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/errors.js +4 -0
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +58 -8
- package/dist/src/mcp/server.js +51 -24
- package/dist/src/mutate/effects.d.ts +23 -5
- package/dist/src/mutate/effects.js +111 -22
- package/dist/src/mutate/known-ids.d.ts +19 -0
- package/dist/src/mutate/known-ids.js +47 -7
- package/dist/src/mutate/ops.d.ts +1 -0
- package/dist/src/mutate/ops.js +20 -1
- package/dist/src/mutate/select.d.ts +0 -11
- package/dist/src/mutate/select.js +33 -9
- package/dist/src/oqx-js/context.d.ts +9 -2
- package/dist/src/oqx-js/context.js +92 -30
- package/dist/src/oqx-js/dates.d.ts +105 -0
- package/dist/src/oqx-js/dates.js +427 -0
- package/dist/src/oqx-js/names.d.ts +40 -0
- package/dist/src/oqx-js/names.js +358 -0
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +30 -11
- package/dist/src/oqx-js/root-row.d.ts +15 -4
- package/dist/src/oqx-js/root-row.js +18 -10
- package/dist/src/oqx-js/run.d.ts +19 -1
- package/dist/src/oqx-js/run.js +97 -13
- package/dist/src/oqx-js/sql/translate.js +1 -1
- package/dist/src/spec-versions.d.ts +5 -5
- package/dist/src/spec-versions.js +4 -4
- package/dist/src/sync/observe.js +9 -3
- package/package.json +2 -2
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// Unknown names (spec/surface §1.5, 4.6). A query that projects, filters or
|
|
2
|
+
// orders by a name no row of the scope's kind can carry — `select path, target
|
|
3
|
+
// from links` (links have `dst_path`), `where preview != ""` on blocks — used
|
|
4
|
+
// to run silently: an absent column in every row, a predicate true or false of
|
|
5
|
+
// every row. This pass judges every name the query reads (`@omgbase/oqx`'s
|
|
6
|
+
// `references`, AST.md §5b) against the VOCABULARY of the row kind it is read
|
|
7
|
+
// from, statically — before any row is read, so the verdict never depends on
|
|
8
|
+
// the query's own filter — and either records a hint or refuses the query.
|
|
9
|
+
//
|
|
10
|
+
// The three tiers of a name at a scope whose kind is known:
|
|
11
|
+
// 1. an engine attribute, handle or relation of the kind — known;
|
|
12
|
+
// 2. an authored property at least one live object of the kind in this
|
|
13
|
+
// repository carries (docs: a frontmatter or inline key — `stage` on three
|
|
14
|
+
// processes is enough; blocks and nodes: a key of a live row's `attrs`,
|
|
15
|
+
// the bare-name read of those kinds) — known, however partial;
|
|
16
|
+
// 3. a name no object of the kind carries — the complaint.
|
|
17
|
+
// Severity: `links` has a closed row shape (no authored data reaches a link
|
|
18
|
+
// row), so a tier-3 name there is `filter_invalid`; docs, blocks and nodes are
|
|
19
|
+
// open — on a read the result carries one hint per offending name and the rows
|
|
20
|
+
// still come back; a write selector (the AST entry, `unknownNames: "refuse"`)
|
|
21
|
+
// is refused, since a name no row carries would select every row. A repository
|
|
22
|
+
// with no live object of the kind has no vocabulary: the check is skipped for
|
|
23
|
+
// that kind. The vocabulary is read once per (repository, kind) and cached on
|
|
24
|
+
// the repository's head commit (`MAX(commits.seq)`), the one thing every
|
|
25
|
+
// property or attrs change moves.
|
|
26
|
+
import { references } from "@omgbase/oqx";
|
|
27
|
+
import { ATTRIBUTES, SOURCE_HANDLES } from "./context.js";
|
|
28
|
+
import { ROOTS } from "./root-row.js";
|
|
29
|
+
import { FROM, guards } from "./sql/scan.js";
|
|
30
|
+
import { FilterInvalid } from "../search/cel/parser.js";
|
|
31
|
+
/** The relations of each kind and the kind of the rows they yield (spec/surface §1.2). */
|
|
32
|
+
export const RELATIONS = {
|
|
33
|
+
docs: { blocks: "blocks", nodes: "nodes", out_links: "links", in_links: "links", revisions: "revisions" },
|
|
34
|
+
blocks: { children: "blocks", nodes: "nodes", out_links: "links", section: "nodes" },
|
|
35
|
+
nodes: { blocks: "blocks", subsections: "nodes", children: "nodes" },
|
|
36
|
+
links: {},
|
|
37
|
+
revisions: {},
|
|
38
|
+
};
|
|
39
|
+
/** The self and reach-through handles of each kind and the kind of the row they reach (§1.2). */
|
|
40
|
+
export const HANDLES = {
|
|
41
|
+
docs: { doc: "docs" },
|
|
42
|
+
blocks: { block: "blocks", doc: "docs" },
|
|
43
|
+
nodes: { section: "nodes", doc: "docs", block: "blocks" },
|
|
44
|
+
links: { doc: "docs", dst: "docs" },
|
|
45
|
+
revisions: { doc: "docs" },
|
|
46
|
+
};
|
|
47
|
+
/** The kinds whose row shape is closed: no authored data reaches a row by bare name. */
|
|
48
|
+
const CLOSED = new Set(["links", "revisions"]);
|
|
49
|
+
const NOUN = { docs: "document", blocks: "block", nodes: "node", links: "link", revisions: "revision" };
|
|
50
|
+
const CACHE = new WeakMap();
|
|
51
|
+
function headSeq(store, repoId) {
|
|
52
|
+
const row = store.db.prepare("SELECT COALESCE(MAX(seq), 0) AS seq FROM commits WHERE repo_id = ?").get(repoId);
|
|
53
|
+
return row?.seq ?? 0;
|
|
54
|
+
}
|
|
55
|
+
function repoCache(store, repoId) {
|
|
56
|
+
let repos = CACHE.get(store);
|
|
57
|
+
if (!repos)
|
|
58
|
+
CACHE.set(store, (repos = new Map()));
|
|
59
|
+
const head = headSeq(store, repoId);
|
|
60
|
+
let c = repos.get(repoId);
|
|
61
|
+
if (!c || c.head !== head)
|
|
62
|
+
repos.set(repoId, (c = { head, vocab: {} }));
|
|
63
|
+
return c;
|
|
64
|
+
}
|
|
65
|
+
/** The authored vocabulary of a kind: docs — every live frontmatter or inline
|
|
66
|
+
* key (the merged read a bare name performs); blocks and nodes — every key a
|
|
67
|
+
* live row's `attrs` holds (the bare-name read of those kinds). Computed keys
|
|
68
|
+
* (`$title`, `$tags`) are the intrinsics `title`/`tags`, not authored keys. */
|
|
69
|
+
function authoredKeys(store, repoId, target) {
|
|
70
|
+
let sql;
|
|
71
|
+
switch (target) {
|
|
72
|
+
case "docs":
|
|
73
|
+
sql = "SELECT DISTINCT key FROM properties WHERE repo_id = ? AND deleted_commit IS NULL AND source IN ('frontmatter', 'inline') ORDER BY key";
|
|
74
|
+
return store.db.prepare(sql).all(repoId).map((r) => r.key);
|
|
75
|
+
case "blocks":
|
|
76
|
+
case "nodes": {
|
|
77
|
+
const g = guards(target, repoId);
|
|
78
|
+
const alias = target === "blocks" ? "b" : "n";
|
|
79
|
+
sql = `SELECT DISTINCT j.key AS key FROM ${FROM[target]}, json_each(${alias}.attrs) j WHERE ${g.sql} ORDER BY key`;
|
|
80
|
+
return store.db.prepare(sql).all(...g.params).map((r) => r.key);
|
|
81
|
+
}
|
|
82
|
+
case "links":
|
|
83
|
+
case "revisions": return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function present(store, repoId, target) {
|
|
87
|
+
// A closed kind has no authored vocabulary to be absent; the question never reaches it.
|
|
88
|
+
if (target === "revisions")
|
|
89
|
+
return true;
|
|
90
|
+
const g = guards(target, repoId);
|
|
91
|
+
return store.db.prepare(`SELECT 1 FROM ${FROM[target]} WHERE ${g.sql} LIMIT 1`).get(...g.params) !== undefined;
|
|
92
|
+
}
|
|
93
|
+
/** The vocabulary of a kind in a repository (cached on the head commit; the SQL runs only when a name is not intrinsic). */
|
|
94
|
+
export function vocabulary(store, repoId, target) {
|
|
95
|
+
const v = vocabularyOf(store, repoId, target);
|
|
96
|
+
return { present: v.present, keys: v.list };
|
|
97
|
+
}
|
|
98
|
+
function vocabularyOf(store, repoId, target) {
|
|
99
|
+
const c = repoCache(store, repoId);
|
|
100
|
+
let v = c.vocab[target];
|
|
101
|
+
if (v)
|
|
102
|
+
return v;
|
|
103
|
+
const list = authoredKeys(store, repoId, target);
|
|
104
|
+
const keys = new Set();
|
|
105
|
+
for (const k of list) {
|
|
106
|
+
const segs = k.split(".");
|
|
107
|
+
for (let i = 1; i <= segs.length; i++)
|
|
108
|
+
keys.add(segs.slice(0, i).join("."));
|
|
109
|
+
}
|
|
110
|
+
v = { present: list.length > 0 || present(store, repoId, target), keys, list };
|
|
111
|
+
c.vocab[target] = v;
|
|
112
|
+
return v;
|
|
113
|
+
}
|
|
114
|
+
/** The kind of the rows an expression yields when read on rows of `on` (`"root"`: the root scope), or null when unknown. */
|
|
115
|
+
function kindOfExpr(e, on, readDepth, byDepth) {
|
|
116
|
+
switch (e.kind) {
|
|
117
|
+
case "ident": return kindOfPath(on, [e.name]);
|
|
118
|
+
case "outer": {
|
|
119
|
+
const k = readDepth - e.levels;
|
|
120
|
+
return kindOfPath(k <= 0 ? "root" : byDepth.get(k) ?? null, [e.name]);
|
|
121
|
+
}
|
|
122
|
+
case "member": {
|
|
123
|
+
const path = dotted(e);
|
|
124
|
+
if (path === null)
|
|
125
|
+
return null;
|
|
126
|
+
const head = path.head;
|
|
127
|
+
const base = head.kind === "ident" ? on : (readDepth - head.levels <= 0 ? "root" : byDepth.get(readDepth - head.levels) ?? null);
|
|
128
|
+
return kindOfPath(base, [head.name, ...path.rest]);
|
|
129
|
+
}
|
|
130
|
+
// `refs(x)` resolves document references to live document rows (§1.3).
|
|
131
|
+
case "call": return e.recv === null && e.name === "refs" ? "docs" : null;
|
|
132
|
+
// A value-position block with no projection passes its receiver's rows through (then its own `from`).
|
|
133
|
+
case "op": {
|
|
134
|
+
if (e.sub.select.some((it) => it.name !== "$key") || e.sub.values)
|
|
135
|
+
return null;
|
|
136
|
+
let k = kindOfExpr(e.receiver, on, readDepth, byDepth);
|
|
137
|
+
for (const f of e.sub.from)
|
|
138
|
+
k = k === null ? null : kindOfExpr(f, k, readDepth + 1, byDepth);
|
|
139
|
+
return k;
|
|
140
|
+
}
|
|
141
|
+
default: return null; // a literal, a binding, a tuple (mixed), a template, an operator
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** A dotted navigation headed by a name. */
|
|
145
|
+
function dotted(e) {
|
|
146
|
+
if (e.kind === "ident" || e.kind === "outer")
|
|
147
|
+
return { head: e, rest: [] };
|
|
148
|
+
if (e.kind !== "member")
|
|
149
|
+
return null;
|
|
150
|
+
const inner = dotted(e.recv);
|
|
151
|
+
return inner === null ? null : { head: inner.head, rest: [...inner.rest, e.name] };
|
|
152
|
+
}
|
|
153
|
+
/** Follow a dotted path from a row kind through handles and relations (`doc.blocks` on a link → blocks). */
|
|
154
|
+
function kindOfPath(on, path) {
|
|
155
|
+
let cur = on;
|
|
156
|
+
for (let i = 0; i < path.length; i++) {
|
|
157
|
+
const seg = path[i];
|
|
158
|
+
if (cur === null)
|
|
159
|
+
return null;
|
|
160
|
+
if (cur === "root") {
|
|
161
|
+
if (seg === "$it")
|
|
162
|
+
continue;
|
|
163
|
+
if (!ROOTS.has(seg))
|
|
164
|
+
return null;
|
|
165
|
+
cur = seg;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// At a group scope `rows` yields the grouped rows (their kind); a key is a
|
|
169
|
+
// value the language built, not a row of any kind.
|
|
170
|
+
if (typeof cur === "object") {
|
|
171
|
+
if (seg !== "rows" || i !== 0)
|
|
172
|
+
return null;
|
|
173
|
+
cur = cur.group;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (seg === "$it" && i === 0)
|
|
177
|
+
continue;
|
|
178
|
+
const next = HANDLES[cur][seg] ?? RELATIONS[cur][seg];
|
|
179
|
+
if (next === undefined)
|
|
180
|
+
return null;
|
|
181
|
+
cur = next;
|
|
182
|
+
}
|
|
183
|
+
return cur === "root" || typeof cur === "object" ? null : cur;
|
|
184
|
+
}
|
|
185
|
+
/** The kind of the frontier rows of a walk: the seeds' kind when every destination yields rows of that same kind, else unknown. */
|
|
186
|
+
function kindOfFrontier(seed, f, readDepth, byDepth) {
|
|
187
|
+
if (seed === null)
|
|
188
|
+
return null;
|
|
189
|
+
// A destination block is a value-position `op`: its receiver on the frontier row, then its own `from`.
|
|
190
|
+
for (const d of f.destinations)
|
|
191
|
+
if (kindOfExpr(d, seed, readDepth, byDepth) !== seed)
|
|
192
|
+
return null;
|
|
193
|
+
return seed;
|
|
194
|
+
}
|
|
195
|
+
/** The kind of the rows a scope holds, from its row chain (AST.md §5b); null when the chain reaches a value no kind describes. */
|
|
196
|
+
export function kindOfScope(steps) {
|
|
197
|
+
if (steps.length === 0)
|
|
198
|
+
return null; // the root scope: root names are judged elsewhere
|
|
199
|
+
const byDepth = new Map([[0, "root"]]);
|
|
200
|
+
let cur = "root";
|
|
201
|
+
let readDepth = 0;
|
|
202
|
+
for (const step of steps) {
|
|
203
|
+
const e = step.expr;
|
|
204
|
+
if (e.kind === "follow")
|
|
205
|
+
cur = kindOfFrontier(cur === "root" || typeof cur === "object" ? null : cur, e, readDepth, byDepth);
|
|
206
|
+
// A `group by` step (oqx 0.24): the rows at this depth become groups of them.
|
|
207
|
+
else if (e.kind === "groupBy")
|
|
208
|
+
cur = { group: cur === "root" || typeof cur === "object" ? null : cur };
|
|
209
|
+
else
|
|
210
|
+
cur = kindOfExpr(e, cur, readDepth, byDepth);
|
|
211
|
+
byDepth.set(step.depth, cur);
|
|
212
|
+
readDepth = step.depth;
|
|
213
|
+
}
|
|
214
|
+
return cur === "root" || typeof cur === "object" ? null : cur;
|
|
215
|
+
}
|
|
216
|
+
/** Judge a dotted name read on rows of `target`; null = known. */
|
|
217
|
+
function judge(store, repoId, target, path) {
|
|
218
|
+
const [head, ...rest] = path;
|
|
219
|
+
// An engine attribute wins on collision; what follows it (`attrs.checked`, `tags.size`) is the value's own navigation.
|
|
220
|
+
if (ATTRIBUTES[target].has(head))
|
|
221
|
+
return null;
|
|
222
|
+
// The source handles reach the authored data under them.
|
|
223
|
+
if (target === "docs" && SOURCE_HANDLES.has(head))
|
|
224
|
+
return rest.length === 0 ? null : judgeAuthored(store, repoId, "docs", rest, head);
|
|
225
|
+
const via = HANDLES[target][head] ?? RELATIONS[target][head];
|
|
226
|
+
if (via !== undefined)
|
|
227
|
+
return rest.length === 0 ? null : judge(store, repoId, via, rest);
|
|
228
|
+
// A bare target name on a row is the context's own loud error (root-row.ts); not doubled here.
|
|
229
|
+
if (ROOTS.has(head))
|
|
230
|
+
return null;
|
|
231
|
+
return judgeAuthored(store, repoId, target, path);
|
|
232
|
+
}
|
|
233
|
+
function judgeAuthored(store, repoId, target, path, handle) {
|
|
234
|
+
const name = path.join(".");
|
|
235
|
+
if (CLOSED.has(target))
|
|
236
|
+
return { target, name, closed: true };
|
|
237
|
+
const v = vocabularyOf(store, repoId, target);
|
|
238
|
+
if (!v.present)
|
|
239
|
+
return null; // no object of the kind: no vocabulary to judge by
|
|
240
|
+
if (v.keys.has(name))
|
|
241
|
+
return null;
|
|
242
|
+
return { target, name, closed: false, ...(handle !== undefined ? { handle } : {}) };
|
|
243
|
+
}
|
|
244
|
+
// ---- messages --------------------------------------------------------------
|
|
245
|
+
/** The names a kind knows, for the nearest match: attributes, handles, relations, the authored keys. */
|
|
246
|
+
function candidates(store, repoId, target) {
|
|
247
|
+
const out = [...ATTRIBUTES[target], ...Object.keys(HANDLES[target]), ...(target === "docs" ? [...SOURCE_HANDLES] : []), ...Object.keys(RELATIONS[target])];
|
|
248
|
+
if (!CLOSED.has(target))
|
|
249
|
+
out.push(...vocabularyOf(store, repoId, target).list);
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
/** Optimal string alignment distance (Damerau–Levenshtein with adjacent transpositions, each edit 1). */
|
|
253
|
+
export function editDistance(a, b) {
|
|
254
|
+
const s = Array.from(a), t = Array.from(b);
|
|
255
|
+
const m = s.length, n = t.length;
|
|
256
|
+
const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
257
|
+
for (let i = 0; i <= m; i++)
|
|
258
|
+
d[i][0] = i;
|
|
259
|
+
for (let j = 0; j <= n; j++)
|
|
260
|
+
d[0][j] = j;
|
|
261
|
+
for (let i = 1; i <= m; i++) {
|
|
262
|
+
for (let j = 1; j <= n; j++) {
|
|
263
|
+
const cost = s[i - 1] === t[j - 1] ? 0 : 1;
|
|
264
|
+
let v = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
265
|
+
if (i > 1 && j > 1 && s[i - 1] === t[j - 2] && s[i - 2] === t[j - 1])
|
|
266
|
+
v = Math.min(v, d[i - 2][j - 2] + 1);
|
|
267
|
+
d[i][j] = v;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return d[m][n];
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* The nearest match of `name` among `candidates` (§1.5): case-insensitive; a
|
|
274
|
+
* candidate qualifies when its distance is ≤ 2, or when one of the two is a
|
|
275
|
+
* substring of the other and the shorter has at least three characters; the
|
|
276
|
+
* lowest distance wins, ties broken by the shorter candidate, then
|
|
277
|
+
* alphabetically. Null when nothing qualifies.
|
|
278
|
+
*/
|
|
279
|
+
export function nearest(name, candidates) {
|
|
280
|
+
const needle = name.toLowerCase();
|
|
281
|
+
// A name under three characters is within distance 2 of nearly anything, so
|
|
282
|
+
// only a substring match (which needs three characters anyway) could qualify.
|
|
283
|
+
if (needle.length < 3)
|
|
284
|
+
return null;
|
|
285
|
+
let best = null;
|
|
286
|
+
for (const c of new Set(candidates)) {
|
|
287
|
+
const hay = c.toLowerCase();
|
|
288
|
+
if (hay === needle)
|
|
289
|
+
continue;
|
|
290
|
+
const d = editDistance(needle, hay);
|
|
291
|
+
const shorter = needle.length <= hay.length ? needle : hay;
|
|
292
|
+
const longer = shorter === needle ? hay : needle;
|
|
293
|
+
const sub = shorter.length >= 3 && longer.includes(shorter);
|
|
294
|
+
if (!(d <= 2 || sub))
|
|
295
|
+
continue;
|
|
296
|
+
// One typo (distance 1) wins first; then a substring match outranks a
|
|
297
|
+
// distance-2 match (`via` → `via_id`, not `id`; `dst_pth` → `dst_path`, not
|
|
298
|
+
// `dst`); within a class the lowest distance, then the shorter candidate,
|
|
299
|
+
// then alphabetical.
|
|
300
|
+
const rank = (x) => (x.d === 1 ? 0 : x.sub ? 1 : 2);
|
|
301
|
+
const better = best === null ||
|
|
302
|
+
rank({ d, sub }) < rank(best) ||
|
|
303
|
+
(rank({ d, sub }) === rank(best) && (d < best.d || (d === best.d && (c.length < best.c.length || (c.length === best.c.length && c < best.c)))));
|
|
304
|
+
if (better)
|
|
305
|
+
best = { c, d, sub };
|
|
306
|
+
}
|
|
307
|
+
return best === null ? null : best.c;
|
|
308
|
+
}
|
|
309
|
+
function suggestion(name, candidates) {
|
|
310
|
+
const c = nearest(name, candidates);
|
|
311
|
+
return c === null ? "" : `; did you mean \`${c}\`?`;
|
|
312
|
+
}
|
|
313
|
+
/** The error of a closed kind: `` `target` is not a field of links (fields: id, path, …); did you mean `…`? ``. */
|
|
314
|
+
export function fieldErrorMessage(store, repoId, target, name) {
|
|
315
|
+
return `\`${name}\` is not a field of ${target} (fields: ${[...ATTRIBUTES[target]].join(", ")})${suggestion(name, candidates(store, repoId, target))}`;
|
|
316
|
+
}
|
|
317
|
+
/** The hint of an open kind: `` `preview` is not a field of blocks and no block in this repository carries a property by that name; did you mean `…`? ``.
|
|
318
|
+
* Under a source handle the name is an authored lookup by construction, so the
|
|
319
|
+
* hint says only that: `` no document in this repository carries a property `path` (`data.path`); did you mean `…`? ``,
|
|
320
|
+
* the nearest match drawn from the authored keys alone. */
|
|
321
|
+
export function propertyHintMessage(store, repoId, target, name, handle) {
|
|
322
|
+
if (handle !== undefined) {
|
|
323
|
+
return `no ${NOUN[target]} in this repository carries a property \`${name}\` (\`${handle}.${name}\`)${suggestion(name, vocabularyOf(store, repoId, target).list)}`;
|
|
324
|
+
}
|
|
325
|
+
return `\`${name}\` is not a field of ${target} and no ${NOUN[target]} in this repository carries a property by that name${suggestion(name, candidates(store, repoId, target))}`;
|
|
326
|
+
}
|
|
327
|
+
// ---- the pass --------------------------------------------------------------
|
|
328
|
+
/**
|
|
329
|
+
* Judge every name the query reads. Returns the hints (one per offending name
|
|
330
|
+
* of an open kind, first-reference order, deduplicated) under `"hint"`; throws
|
|
331
|
+
* `FilterInvalid` for a tier-3 name on a closed kind in either mode, and for
|
|
332
|
+
* one on an open kind under `"refuse"`.
|
|
333
|
+
*/
|
|
334
|
+
export function checkNames(store, repoId, raw, mode) {
|
|
335
|
+
const hints = [];
|
|
336
|
+
const seen = new Set();
|
|
337
|
+
for (const ref of references(raw)) {
|
|
338
|
+
if (ref.scope.steps.length === 0)
|
|
339
|
+
continue; // a root name
|
|
340
|
+
const kind = kindOfScope(ref.scope.steps);
|
|
341
|
+
if (kind === null)
|
|
342
|
+
continue;
|
|
343
|
+
const verdict = judge(store, repoId, kind, ref.name.split("."));
|
|
344
|
+
if (verdict === null)
|
|
345
|
+
continue;
|
|
346
|
+
if (verdict.closed)
|
|
347
|
+
throw new FilterInvalid(fieldErrorMessage(store, repoId, verdict.target, verdict.name), "OQX");
|
|
348
|
+
const message = propertyHintMessage(store, repoId, verdict.target, verdict.name, verdict.handle);
|
|
349
|
+
if (mode === "refuse")
|
|
350
|
+
throw new FilterInvalid(message, "OQX");
|
|
351
|
+
if (!seen.has(message)) {
|
|
352
|
+
seen.add(message);
|
|
353
|
+
hints.push(message);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return hints;
|
|
357
|
+
}
|
|
358
|
+
//# sourceMappingURL=names.js.map
|
|
@@ -15,9 +15,9 @@ export declare function rootTarget(source: Expr): Target | null;
|
|
|
15
15
|
* • a bare reserved docs basename (`id path updated_at hash raw`): as
|
|
16
16
|
* an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
|
|
17
17
|
* on any target at any depth (the reach-through row is a doc)
|
|
18
|
-
* • a bare
|
|
19
|
-
* property: as an `ident` at the ROOT scope when it is not a relation
|
|
20
|
-
* the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
|
|
18
|
+
* • a bare root name (`docs blocks nodes links revisions`, root-row.ts) read
|
|
19
|
+
* as a property: as an `ident` at the ROOT scope when it is not a relation
|
|
20
|
+
* of the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
|
|
21
21
|
* as an `ident` at any depth below (the rows there are unknown), or as
|
|
22
22
|
* `<head>.<name>` unless the head is a reach-through whose row has that
|
|
23
23
|
* relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
|
|
@@ -14,15 +14,16 @@
|
|
|
14
14
|
// `[]` planned, `filter_invalid` in memory), so when any residual conjunct could
|
|
15
15
|
// raise — a function or method call, a `single` block, a `^`-escaped name, or a
|
|
16
16
|
// bare reserved docs basename — the WHOLE query runs unplanned.
|
|
17
|
-
import { ROWS_ROOT, partitionPushable, residualQuery } from "@omgbase/oqx";
|
|
17
|
+
import { ROWS_ROOT, isRegexOp, partitionPushable, residualQuery } from "@omgbase/oqx";
|
|
18
18
|
import { makeStoreContext, tagRows } from "./context.js";
|
|
19
19
|
import { translatePredicate } from "./sql/translate.js";
|
|
20
20
|
import { ALIAS, COLS, FROM, ORDER, guards } from "./sql/scan.js";
|
|
21
|
-
import { REACH_THROUGH, TARGETS, isTargetRelationOf } from "./root-row.js";
|
|
21
|
+
import { REACH_THROUGH, ROOTS, TARGETS, isTargetRelationOf } from "./root-row.js";
|
|
22
22
|
// The root collection a query scans, if it is a bare `docs|blocks|nodes|links`
|
|
23
23
|
// source at the root scope (else null — not a pushable shape; a nested `^docs`
|
|
24
|
-
// is a block's receiver, never the query's source).
|
|
25
|
-
//
|
|
24
|
+
// is a block's receiver, never the query's source). `revisions` (4.7) is a root
|
|
25
|
+
// the planner never plans — it is not a `Target` here — so a revisions scan
|
|
26
|
+
// always runs in memory.
|
|
26
27
|
export function rootTarget(source) {
|
|
27
28
|
if (source.kind === "ident" && TARGETS.has(source.name))
|
|
28
29
|
return source.name;
|
|
@@ -41,9 +42,9 @@ export function rootTarget(source) {
|
|
|
41
42
|
* • a bare reserved docs basename (`id path updated_at hash raw`): as
|
|
42
43
|
* an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
|
|
43
44
|
* on any target at any depth (the reach-through row is a doc)
|
|
44
|
-
* • a bare
|
|
45
|
-
* property: as an `ident` at the ROOT scope when it is not a relation
|
|
46
|
-
* the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
|
|
45
|
+
* • a bare root name (`docs blocks nodes links revisions`, root-row.ts) read
|
|
46
|
+
* as a property: as an `ident` at the ROOT scope when it is not a relation
|
|
47
|
+
* of the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
|
|
47
48
|
* as an `ident` at any depth below (the rows there are unknown), or as
|
|
48
49
|
* `<head>.<name>` unless the head is a reach-through whose row has that
|
|
49
50
|
* relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
|
|
@@ -90,6 +91,12 @@ function subMayRaise(s, target) {
|
|
|
90
91
|
return true;
|
|
91
92
|
if (s.follow && followMayRaise(s.follow, target))
|
|
92
93
|
return true;
|
|
94
|
+
// A grouped block (oqx 0.24) evaluates its keys, `having` and its aggregates
|
|
95
|
+
// (`sum` over a string raises) per group.
|
|
96
|
+
if (s.groupBy?.keys.some((k) => exprMayRaise(k.expr, target, false)))
|
|
97
|
+
return true;
|
|
98
|
+
if (s.having && whereMayRaise(s.having, target, false))
|
|
99
|
+
return true;
|
|
93
100
|
if (s.limit && exprMayRaise(s.limit, target, false))
|
|
94
101
|
return true;
|
|
95
102
|
if (s.offset && exprMayRaise(s.offset, target, false))
|
|
@@ -107,9 +114,9 @@ function exprMayRaise(e, target, root) {
|
|
|
107
114
|
case "call": return true;
|
|
108
115
|
case "outer": return true;
|
|
109
116
|
case "ident":
|
|
110
|
-
return
|
|
117
|
+
return ROOTS.has(e.name) && (!root || !isTargetRelationOf(target, e.name));
|
|
111
118
|
case "member":
|
|
112
|
-
if (
|
|
119
|
+
if (ROOTS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
|
|
113
120
|
return true;
|
|
114
121
|
return exprMayRaise(e.recv, target, root);
|
|
115
122
|
case "unary": return exprMayRaise(e.expr, target, root);
|
|
@@ -117,7 +124,10 @@ function exprMayRaise(e, target, root) {
|
|
|
117
124
|
// read anything its block reads.
|
|
118
125
|
case "required": return true;
|
|
119
126
|
case "op": return opMayRaise(e, target, root);
|
|
127
|
+
// A regex operator (oqx 0.22) is a `matches` call: an invalid pattern
|
|
128
|
+
// raises, exactly as the call would.
|
|
120
129
|
case "binary":
|
|
130
|
+
return isRegexOp(e.op) || exprMayRaise(e.left, target, root) || exprMayRaise(e.right, target, root);
|
|
121
131
|
case "logical":
|
|
122
132
|
case "in":
|
|
123
133
|
return exprMayRaise(e.left, target, root) || exprMayRaise(e.right, target, root);
|
|
@@ -129,6 +139,9 @@ function exprMayRaise(e, target, root) {
|
|
|
129
139
|
// a record is an eval error only the data decides, so with any interpolation
|
|
130
140
|
// it may raise.
|
|
131
141
|
case "template": return e.parts.some((p) => typeof p !== "string");
|
|
142
|
+
// An aggregate (oqx 0.24) raises on a non-number under `sum`/`avg`, which
|
|
143
|
+
// only the data decides; a grouped query is never planned anyway (below).
|
|
144
|
+
case "aggregate": return true;
|
|
132
145
|
case "lit":
|
|
133
146
|
case "binding": return false;
|
|
134
147
|
}
|
|
@@ -144,8 +157,14 @@ export class SQLiteQueryPlanner {
|
|
|
144
157
|
this.ctxOpts = ctxOpts;
|
|
145
158
|
}
|
|
146
159
|
plan(query, params) {
|
|
147
|
-
// Only simple top-level scans: no follow, no `from E` re-projection
|
|
148
|
-
|
|
160
|
+
// Only simple top-level scans: no follow, no `from E` re-projection, no
|
|
161
|
+
// `group by` (oqx 0.24). A grouped query is declined whole: its `where`
|
|
162
|
+
// conjuncts could be pushed (grouping happens after them, so the residual
|
|
163
|
+
// would group the produced rows correctly), but a pushed conjunct that
|
|
164
|
+
// empties the scan would hide an eval error an aggregate or `having` raises
|
|
165
|
+
// in memory (`sum` over a string) — decline (c) extended to the post-group
|
|
166
|
+
// clauses. SQL `GROUP BY` pushdown is a later round.
|
|
167
|
+
if (query.follow || query.from.length > 0 || query.groupBy)
|
|
149
168
|
return null;
|
|
150
169
|
const target = rootTarget(query.source);
|
|
151
170
|
if (!target)
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
export type Target = "docs" | "blocks" | "nodes" | "links";
|
|
2
|
+
/** Every root collection (spec/surface §1.1): the four row kinds the
|
|
3
|
+
* unknown-names pass judges (`TARGETS`, names.ts) plus `revisions` (4.7), the
|
|
4
|
+
* history target — one row per revision of the repository's documents. */
|
|
5
|
+
export type Root = Target | "revisions";
|
|
6
|
+
/** The four row kinds whose names the unknown-names pass judges (names.ts keys
|
|
7
|
+
* its vocabulary tables by them); `revisions` joins when that pass learns the
|
|
8
|
+
* kind. Every OTHER root-collection test uses `ROOTS`. */
|
|
2
9
|
export declare const TARGETS: ReadonlySet<string>;
|
|
3
|
-
/** The
|
|
4
|
-
export declare const
|
|
10
|
+
/** The root collections, in the root object's entry order (§1.1). */
|
|
11
|
+
export declare const ROOT_NAMES: readonly Root[];
|
|
12
|
+
export declare const ROOTS: ReadonlySet<string>;
|
|
13
|
+
/** The root names that are a relation of each kind's rows (§1.2): `revisions`
|
|
14
|
+
* on a document is its revision list (4.7). */
|
|
15
|
+
export declare const TARGET_RELATIONS: Readonly<Record<Root, ReadonlySet<string>>>;
|
|
5
16
|
/** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`, and a link's `dst.x`). */
|
|
6
17
|
export declare const REACH_THROUGH: Readonly<Record<string, Target>>;
|
|
7
18
|
export declare const REPO_REMOVED_MESSAGE = "`$repo` was removed in surface 2.0 \u2014 reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^id` / `0^id`";
|
|
@@ -19,10 +30,10 @@ export declare function bareTargetMessage(name: string, depth: number | null): s
|
|
|
19
30
|
export declare function pastRootMessage(name: string, levels: number): string;
|
|
20
31
|
/**
|
|
21
32
|
* The unknown-target error (3.0 name pass): a top-level `from <name>` whose name
|
|
22
|
-
* is not one of the
|
|
33
|
+
* is not one of the five collections would read an absent root and return an
|
|
23
34
|
* empty result — refused instead, naming the collections; `edges`, the links
|
|
24
35
|
* target's name before 3.0, gets its new name spelled out.
|
|
25
36
|
*/
|
|
26
37
|
export declare function unknownTargetMessage(name: string): string;
|
|
27
38
|
/** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
|
|
28
|
-
export declare function isTargetRelationOf(target:
|
|
39
|
+
export declare function isTargetRelationOf(target: Root | undefined, name: string): boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The root row (spec/surface §1.1, 2.0; spec/oqx 0.18). The repository is OQX's
|
|
2
2
|
// root row: from a top-level row `^docs` reaches the documents (one caret per
|
|
3
3
|
// enclosing block, or the absolute `0^docs`), `^id` the repository id, and the
|
|
4
|
-
// root object (`^$it`) exposes the
|
|
4
|
+
// root object (`^$it`) exposes the five collections (`revisions` since 4.7). Two spellings are refused
|
|
5
5
|
// as `filter_invalid`, with the messages below, in both engines:
|
|
6
6
|
//
|
|
7
7
|
// • `$repo` (removed in 2.0) anywhere — bare, as a member head (`$repo.docs`),
|
|
@@ -11,22 +11,30 @@
|
|
|
11
11
|
// • a bare target name at scope depth ≥ 1 (inside a block, or in a top-level
|
|
12
12
|
// row's clauses), where a bare name reads a property of the current row,
|
|
13
13
|
// which has none: `docs` and `links` are no row's relation, so they are
|
|
14
|
-
// refused statically by the same walk; `blocks` and `
|
|
15
|
-
// of some rows (`docs.blocks`, `docs.nodes`, `blocks.nodes`,
|
|
16
|
-
// `section.blocks`), so the context raises the same error
|
|
17
|
-
// row that lacks the relation (context.ts). A frontmatter key named after a
|
|
14
|
+
// refused statically by the same walk; `blocks`, `nodes` and `revisions`
|
|
15
|
+
// ARE relations of some rows (`docs.blocks`, `docs.nodes`, `blocks.nodes`,
|
|
16
|
+
// `section.blocks`, `docs.revisions`), so the context raises the same error
|
|
17
|
+
// at read time on a row that lacks the relation (context.ts). A frontmatter key named after a
|
|
18
18
|
// target is unreachable by bare name (use `frontmatter.<k>`).
|
|
19
|
+
/** The four row kinds whose names the unknown-names pass judges (names.ts keys
|
|
20
|
+
* its vocabulary tables by them); `revisions` joins when that pass learns the
|
|
21
|
+
* kind. Every OTHER root-collection test uses `ROOTS`. */
|
|
19
22
|
export const TARGETS = new Set(["docs", "blocks", "nodes", "links"]);
|
|
20
|
-
/** The
|
|
23
|
+
/** The root collections, in the root object's entry order (§1.1). */
|
|
24
|
+
export const ROOT_NAMES = ["docs", "blocks", "nodes", "links", "revisions"];
|
|
25
|
+
export const ROOTS = new Set(ROOT_NAMES);
|
|
26
|
+
/** The root names that are a relation of each kind's rows (§1.2): `revisions`
|
|
27
|
+
* on a document is its revision list (4.7). */
|
|
21
28
|
export const TARGET_RELATIONS = {
|
|
22
|
-
docs: new Set(["nodes", "blocks"]),
|
|
29
|
+
docs: new Set(["nodes", "blocks", "revisions"]),
|
|
23
30
|
blocks: new Set(["nodes"]),
|
|
24
31
|
nodes: new Set(["blocks"]),
|
|
25
32
|
links: new Set(),
|
|
33
|
+
revisions: new Set(),
|
|
26
34
|
};
|
|
27
35
|
/** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`, and a link's `dst.x`). */
|
|
28
36
|
export const REACH_THROUGH = { doc: "docs", block: "blocks", section: "nodes", dst: "docs" };
|
|
29
|
-
const NOUN = { docs: "documents", blocks: "blocks", nodes: "nodes", links: "links" };
|
|
37
|
+
const NOUN = { docs: "documents", blocks: "blocks", nodes: "nodes", links: "links", revisions: "revisions" };
|
|
30
38
|
export const REPO_REMOVED_MESSAGE = "`$repo` was removed in surface 2.0 — reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^id` / `0^id`";
|
|
31
39
|
/**
|
|
32
40
|
* The bare-target error. `depth` is the scope depth the name was read at when
|
|
@@ -50,13 +58,13 @@ export function pastRootMessage(name, levels) {
|
|
|
50
58
|
}
|
|
51
59
|
/**
|
|
52
60
|
* The unknown-target error (3.0 name pass): a top-level `from <name>` whose name
|
|
53
|
-
* is not one of the
|
|
61
|
+
* is not one of the five collections would read an absent root and return an
|
|
54
62
|
* empty result — refused instead, naming the collections; `edges`, the links
|
|
55
63
|
* target's name before 3.0, gets its new name spelled out.
|
|
56
64
|
*/
|
|
57
65
|
export function unknownTargetMessage(name) {
|
|
58
66
|
const was = name === "edges" ? " (`edges` was renamed `links` in surface 3.0)" : "";
|
|
59
|
-
return `\`${name}\` is not a target — the repository's collections are
|
|
67
|
+
return `\`${name}\` is not a target — the repository's collections are ${ROOT_NAMES.map((r) => `\`${r}\``).join(", ")}${was}`;
|
|
60
68
|
}
|
|
61
69
|
/** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
|
|
62
70
|
export function isTargetRelationOf(target, name) {
|
package/dist/src/oqx-js/run.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Query, Expr, SelectItem } from "@omgbase/oqx";
|
|
2
|
+
import { type UnknownNames } from "./names.js";
|
|
2
3
|
import type { Store } from "../core/store/store.js";
|
|
3
4
|
import { FilterInvalid } from "../search/cel/parser.js";
|
|
4
5
|
import type { SemanticVec } from "../search/cel/compile.js";
|
|
@@ -36,6 +37,12 @@ export interface OqxResult {
|
|
|
36
37
|
* empty). Paged/deduped exactly like hits — `truncated`/`cursor` apply. For
|
|
37
38
|
* first/single it holds zero or one value. */
|
|
38
39
|
values?: unknown[];
|
|
40
|
+
/** Unknown names (spec/surface §1.5, 4.6): one line per name the query reads
|
|
41
|
+
* that is neither a field of its row kind nor a property any object of that
|
|
42
|
+
* kind in the repository carries — the rows are still returned. First-
|
|
43
|
+
* reference order, deduplicated; absent when there is none (never `[]`).
|
|
44
|
+
* Last member of the result, after everything above. */
|
|
45
|
+
hints?: string[];
|
|
39
46
|
}
|
|
40
47
|
export interface OqxOptions {
|
|
41
48
|
limit?: number;
|
|
@@ -55,9 +62,17 @@ export interface OqxOptions {
|
|
|
55
62
|
/** Force the pure in-memory engine (skip tier-3 pushdown). Internal — used by
|
|
56
63
|
* the differential conformance suite to prove planned == in-memory. */
|
|
57
64
|
plan?: boolean;
|
|
65
|
+
/** What a tier-3 name on an open kind does (spec/surface §1.5, 4.6; a tier-3
|
|
66
|
+
* name on `links` is `filter_invalid` either way): `"hint"` records it in
|
|
67
|
+
* `hints` and runs the query — the default of the source entry (`oqxRun`,
|
|
68
|
+
* every read); `"refuse"` throws `filter_invalid` before any row is read —
|
|
69
|
+
* the default of the AST entry (`oqxRunQuery`), whose caller is the write
|
|
70
|
+
* selector compiler, where a name no row carries would select every row. */
|
|
71
|
+
unknownNames?: UnknownNames;
|
|
58
72
|
}
|
|
59
73
|
export declare function rootPathLiterals(e: Expr): Expr;
|
|
60
74
|
export declare function checkRootSpellings(q: Query): void;
|
|
75
|
+
export declare function foldDates(e: Expr): Expr;
|
|
61
76
|
export declare function collectSemanticPhrases(source: string): string[];
|
|
62
77
|
export declare function reservedAlias(items: readonly SelectItem[]): FilterInvalid | null;
|
|
63
78
|
export declare const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` \u2014 without a projection a hit is only its envelope";
|
|
@@ -68,7 +83,10 @@ export declare function oqxRun(store: Store, repoId: string, source: string, opt
|
|
|
68
83
|
* after `parse`, and the selector compiler (`mutate/select.ts`), which appends
|
|
69
84
|
* its slot items to the RAW tree so a user alias a template reads is inlined
|
|
70
85
|
* by the alias resolution here. Errors are the library's own (`OqxError`);
|
|
71
|
-
* `oqxRun` normalizes them, a caller of this entry does the same.
|
|
86
|
+
* `oqxRun` normalizes them, a caller of this entry does the same. Unknown
|
|
87
|
+
* names (4.6) default to `"refuse"` here: this entry's caller is the write
|
|
88
|
+
* selector, where a name no row carries would select every row; the source
|
|
89
|
+
* entry passes `"hint"`.
|
|
72
90
|
*/
|
|
73
91
|
export declare function oqxRunQuery(store: Store, repoId: string, raw: Query, opts?: OqxOptions): OqxResult;
|
|
74
92
|
export declare function oqxRunAsync(store: Store, repoId: string, source: string, opts?: OqxOptions, embedQuery?: EmbedQuery): Promise<OqxResult>;
|