@omgbase/core 0.7.1 → 0.10.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/cursor.d.ts +13 -1
- package/dist/src/core/cursor.js +21 -2
- package/dist/src/core/paths.d.ts +10 -0
- package/dist/src/core/paths.js +34 -0
- package/dist/src/core/read/document.d.ts +1 -0
- package/dist/src/core/read/document.js +3 -2
- package/dist/src/core/read/reader.d.ts +6 -3
- package/dist/src/core/read/reader.js +31 -16
- package/dist/src/graph/history.js +16 -10
- package/dist/src/graph/link-health.d.ts +2 -1
- package/dist/src/graph/link-health.js +13 -7
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -1
- package/dist/src/mcp/errors.js +1 -1
- package/dist/src/mcp/graph.js +50 -15
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +102 -33
- package/dist/src/mcp/server.js +32 -28
- package/dist/src/oqx-js/context.d.ts +2 -1
- package/dist/src/oqx-js/context.js +181 -49
- package/dist/src/oqx-js/planner.d.ts +8 -1
- package/dist/src/oqx-js/planner.js +22 -42
- package/dist/src/oqx-js/root-row.d.ts +21 -0
- package/dist/src/oqx-js/root-row.js +55 -0
- package/dist/src/oqx-js/run.d.ts +3 -1
- package/dist/src/oqx-js/run.js +128 -164
- package/dist/src/oqx-js/sql/scan.d.ts +27 -0
- package/dist/src/oqx-js/sql/scan.js +62 -0
- package/dist/src/oqx-js/sql/translate.d.ts +7 -0
- package/dist/src/oqx-js/sql/translate.js +42 -11
- package/dist/src/oqx-js/store-index.d.ts +20 -0
- package/dist/src/oqx-js/store-index.js +193 -0
- package/dist/src/spec-versions.d.ts +3 -3
- package/dist/src/spec-versions.js +2 -2
- package/dist/src/surface-paths.d.ts +33 -0
- package/dist/src/surface-paths.js +75 -0
- package/package.json +2 -2
|
@@ -12,15 +12,19 @@
|
|
|
12
12
|
// the corpus and the differential baseline. The tier-3 SQLite planner (planner.ts)
|
|
13
13
|
// pushes the hot predicates down; both must agree (conformance suite).
|
|
14
14
|
import { semantics } from "@omgbase/oqx";
|
|
15
|
+
import { storeIndexFor } from "./store-index.js";
|
|
16
|
+
import { TARGETS, bareTargetMessage } from "./root-row.js";
|
|
17
|
+
import { COLS, FROM, ORDER, guards } from "./sql/scan.js";
|
|
15
18
|
import { detectRange } from "../core/store/properties.js";
|
|
16
19
|
import { docsRead } from "../core/read/document.js";
|
|
20
|
+
import { referencePath, storagePath } from "../core/paths.js";
|
|
17
21
|
import { FilterInvalid } from "../search/cel/parser.js";
|
|
18
22
|
import { sanitizeFtsQuery } from "../search/fts-query.js";
|
|
19
23
|
import { cosineBytes } from "../core/vec.js";
|
|
20
24
|
import { ctxHashHex } from "../search/embeddings.js";
|
|
21
25
|
import { makeBlockContextResolver } from "../search/tasks.js";
|
|
22
26
|
const TARGET = Symbol("oqx.target");
|
|
23
|
-
const
|
|
27
|
+
const ROOT_ROW = Symbol("oqx.rootRow");
|
|
24
28
|
const PROP_SOURCE = Symbol("oqx.propSource");
|
|
25
29
|
function tag(row, t) {
|
|
26
30
|
if (!row)
|
|
@@ -34,9 +38,9 @@ function tagAll(rows, t) {
|
|
|
34
38
|
return rows;
|
|
35
39
|
}
|
|
36
40
|
// docs intrinsics whose BARE (non-$) form is almost always a typo (10 §2); a bare
|
|
37
|
-
// read of one is a loud error, matching the CEL guard.
|
|
38
|
-
//
|
|
39
|
-
//
|
|
41
|
+
// read of one is a loud error, matching the CEL guard. (A bare target name —
|
|
42
|
+
// `docs`, `edges`, … — is a loud error of its own, root-row.ts; `repo` is just an
|
|
43
|
+
// ordinary frontmatter key.)
|
|
40
44
|
const RESERVED_DOC_BASENAMES = new Set(["id", "path", "updated_at", "content_hash", "body"]);
|
|
41
45
|
export function makeStoreContext(store, repoId, opts = {}) {
|
|
42
46
|
const db = store.db;
|
|
@@ -49,20 +53,73 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
49
53
|
return undefined;
|
|
50
54
|
return Object.values(r)[0];
|
|
51
55
|
};
|
|
52
|
-
// ---- root collections (
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
56
|
+
// ---- root collections (the shared scan shape of sql/scan.ts: live rows of
|
|
57
|
+
// this repo, ordered for a stable (path, id) default) -----------------------
|
|
58
|
+
const scan = (t) => {
|
|
59
|
+
const g = guards(t, repoId);
|
|
60
|
+
return tagAll(all(`SELECT ${COLS[t]} FROM ${FROM[t]} WHERE ${g.sql} ORDER BY ${ORDER[t]}`, ...g.params), t);
|
|
61
|
+
};
|
|
62
|
+
// A root scan is handed out LAZILY: an array (a `Proxy` over one — `Array.isArray`,
|
|
63
|
+
// iteration, indexing, `length`, rendering all see a plain array of tagged
|
|
64
|
+
// rows) that runs its SELECT on first use. The engine's optimizer asks
|
|
65
|
+
// `indexFor` about a nested block's receiver BEFORE reading it, so a
|
|
66
|
+
// correlated probe on `^docs` / `^edges` / … is answered from the
|
|
67
|
+
// store's indexes (store-index.ts) and the scan never runs; anything else
|
|
68
|
+
// (a top-level `from docs`, a `count { }`, a non-indexable path) reads it
|
|
69
|
+
// whole exactly as before. One handle per target per context: a context lives
|
|
70
|
+
// for one run, during which the store does not change, so `^docs` read
|
|
71
|
+
// from every outer row is one SELECT, not one per row — and the engine, which
|
|
72
|
+
// keys collections by identity, sees the same collection each time.
|
|
73
|
+
// `rootScans` recognizes the handles for `indexFor`. Because the handle IS an
|
|
74
|
+
// array, this context needs no `DataContext.materialize` (the Rust port's
|
|
75
|
+
// marker value does); `spec/surface` `lazy-root-*` pins what the language
|
|
76
|
+
// sees either way.
|
|
77
|
+
const rootScans = new WeakMap();
|
|
78
|
+
const handles = new Map();
|
|
79
|
+
const lazyRoot = (t) => {
|
|
80
|
+
let h = handles.get(t);
|
|
81
|
+
if (!h)
|
|
82
|
+
handles.set(t, (h = makeLazyRoot(t)));
|
|
83
|
+
return h;
|
|
84
|
+
};
|
|
85
|
+
const makeLazyRoot = (t) => {
|
|
86
|
+
const backing = [];
|
|
87
|
+
let filled = false;
|
|
88
|
+
const fill = () => {
|
|
89
|
+
if (!filled) {
|
|
90
|
+
filled = true;
|
|
91
|
+
for (const r of scan(t))
|
|
92
|
+
backing.push(r);
|
|
93
|
+
}
|
|
94
|
+
return backing;
|
|
95
|
+
};
|
|
96
|
+
const proxy = new Proxy(backing, {
|
|
97
|
+
get(target, prop, receiver) { fill(); return Reflect.get(target, prop, receiver); },
|
|
98
|
+
has(target, prop) { fill(); return Reflect.has(target, prop); },
|
|
99
|
+
ownKeys(target) { fill(); return Reflect.ownKeys(target); },
|
|
100
|
+
getOwnPropertyDescriptor(target, prop) { fill(); return Reflect.getOwnPropertyDescriptor(target, prop); },
|
|
101
|
+
set(target, prop, value, receiver) { fill(); return Reflect.set(target, prop, value, receiver); },
|
|
102
|
+
defineProperty(target, prop, desc) { fill(); return Reflect.defineProperty(target, prop, desc); },
|
|
103
|
+
deleteProperty(target, prop) { fill(); return Reflect.deleteProperty(target, prop); },
|
|
104
|
+
});
|
|
105
|
+
rootScans.set(proxy, { target: t, rows: fill });
|
|
106
|
+
return proxy;
|
|
107
|
+
};
|
|
108
|
+
const rootFns = {
|
|
109
|
+
docs: () => lazyRoot("docs"), blocks: () => lazyRoot("blocks"), nodes: () => lazyRoot("nodes"), edges: () => lazyRoot("edges"),
|
|
110
|
+
};
|
|
111
|
+
// The root row (spec/surface §1.1, 2.0; spec/oqx 0.18 `rootObject`): the
|
|
112
|
+
// repository, reached as `^$it` from a top-level row (`0^$it` from any depth).
|
|
113
|
+
// Its own enumerable keys are the four collections — lazy getters, so
|
|
114
|
+
// `entries(^$it)` names them without running a scan and `^$it.docs` is the
|
|
115
|
+
// same handle as `^docs` — and `get` answers `$id` with the repository id (an
|
|
116
|
+
// intrinsic, not an entry). Tagged so `get` recognizes it.
|
|
117
|
+
const rootRow = Object.defineProperties({ [ROOT_ROW]: true }, {
|
|
118
|
+
docs: { enumerable: true, get: () => lazyRoot("docs") },
|
|
119
|
+
blocks: { enumerable: true, get: () => lazyRoot("blocks") },
|
|
120
|
+
nodes: { enumerable: true, get: () => lazyRoot("nodes") },
|
|
121
|
+
edges: { enumerable: true, get: () => lazyRoot("edges") },
|
|
122
|
+
});
|
|
66
123
|
// ---- attrs / property decoding --------------------------------------------
|
|
67
124
|
const parseJson = (v) => {
|
|
68
125
|
if (typeof v !== "string")
|
|
@@ -270,6 +327,10 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
270
327
|
? tag(one(`SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id WHERE b.block_id = ?`, row.block_id), "blocks")
|
|
271
328
|
: undefined;
|
|
272
329
|
// ---- intrinsics ($-fields) ------------------------------------------------
|
|
330
|
+
// `$path` / `$dst_path` are the REFERENCE form (`/a/b.md`, spec/surface §1
|
|
331
|
+
// "Paths", 2.0): the rows carry the storage form (`path`, the `__path` join
|
|
332
|
+
// column) and the intrinsic roots it on read, so nothing below the binding
|
|
333
|
+
// changes. run.ts roots the string literals a query compares them with.
|
|
273
334
|
const intrinsic = (row, t, name) => {
|
|
274
335
|
// `$self` = the current row: run.ts rewrites row-scoped domain functions
|
|
275
336
|
// (text/semantic/under_*/…) to `$self.fn(…)` so they arrive via callMethod
|
|
@@ -279,7 +340,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
279
340
|
if (t === "docs")
|
|
280
341
|
switch (name) {
|
|
281
342
|
case "$id": return row.doc_id;
|
|
282
|
-
case "$path": return row.path;
|
|
343
|
+
case "$path": return referencePath(String(row.path));
|
|
283
344
|
case "$content_hash": return row.file_hash == null ? null : hex(row.file_hash);
|
|
284
345
|
case "$updated_at": return scalar(`SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ?`, row.current_rev) ?? null;
|
|
285
346
|
case "$body": return docsRead(store, String(row.doc_id))?.content ?? null;
|
|
@@ -290,7 +351,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
290
351
|
switch (name) {
|
|
291
352
|
case "$id": return row.block_id;
|
|
292
353
|
case "$doc": return row.doc_id;
|
|
293
|
-
case "$path": return row.__path;
|
|
354
|
+
case "$path": return referencePath(String(row.__path));
|
|
294
355
|
case "$ordinal": return row.ordinal;
|
|
295
356
|
case "$depth": return row.depth;
|
|
296
357
|
case "$body": return row.text;
|
|
@@ -303,7 +364,7 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
303
364
|
case "$node_id": return row.node_id;
|
|
304
365
|
case "$doc_id": return row.doc_id;
|
|
305
366
|
case "$block_id": return row.block_id;
|
|
306
|
-
case "$path": return row.__path;
|
|
367
|
+
case "$path": return referencePath(String(row.__path));
|
|
307
368
|
}
|
|
308
369
|
if (t === "edges")
|
|
309
370
|
switch (name) {
|
|
@@ -313,8 +374,11 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
313
374
|
case "$src_block": return row.src_block;
|
|
314
375
|
case "$via": return row.via_node;
|
|
315
376
|
case "$from_commit": return row.from_commit;
|
|
316
|
-
case "$path": return row.__path;
|
|
317
|
-
case "$dst_path":
|
|
377
|
+
case "$path": return referencePath(String(row.__path));
|
|
378
|
+
case "$dst_path": {
|
|
379
|
+
const p = scalar(`SELECT path FROM docs WHERE doc_id = ?`, row.dst_node);
|
|
380
|
+
return p == null ? null : referencePath(String(p));
|
|
381
|
+
}
|
|
318
382
|
case "$dst_uri": return scalar(`SELECT uri FROM external_nodes WHERE node_id = ?`, row.dst_node) ?? null;
|
|
319
383
|
}
|
|
320
384
|
return undefined;
|
|
@@ -323,14 +387,9 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
323
387
|
const getFrom = (row, key) => {
|
|
324
388
|
if (row == null)
|
|
325
389
|
return undefined;
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
// repository root through this local intrinsic, never by falling through
|
|
330
|
-
// to an enclosing scope.
|
|
331
|
-
if (key === "$repo")
|
|
332
|
-
return repoRoot;
|
|
333
|
-
if (row[REPO_ROOT]) {
|
|
390
|
+
// The root row: `^$it.$id` is the repository id, `^$it.<target>` the lazy
|
|
391
|
+
// root scan (the same handle `^<target>` / `root(name)` hands out).
|
|
392
|
+
if (row[ROOT_ROW]) {
|
|
334
393
|
if (key === "$id")
|
|
335
394
|
return repoId;
|
|
336
395
|
return rootFns[key] ? rootFns[key]() : undefined;
|
|
@@ -363,6 +422,13 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
363
422
|
const rels = rel[t];
|
|
364
423
|
if (rels[key])
|
|
365
424
|
return rels[key](r);
|
|
425
|
+
// A bare target name that is not one of this row's relations reads a
|
|
426
|
+
// property the row has none of (spec/surface §1.1, 2.0): loud, naming the
|
|
427
|
+
// root-row spelling. `docs`/`edges` never get here (run.ts refuses them
|
|
428
|
+
// statically); `blocks` on a block or edge row and `nodes` on a node or
|
|
429
|
+
// edge row do.
|
|
430
|
+
if (TARGETS.has(key))
|
|
431
|
+
throw new FilterInvalid(bareTargetMessage(key, null), "OQX");
|
|
366
432
|
// per-target scalar fields.
|
|
367
433
|
if (t === "docs") {
|
|
368
434
|
if (key === "format")
|
|
@@ -505,14 +571,17 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
505
571
|
return !!one(`SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
|
|
506
572
|
WHERE s.doc_id = ? AND lower(hb.text) LIKE '%' || lower(?) || '%' AND s.first_ordinal <= ? AND s.last_ordinal >= ? LIMIT 1`, r.doc_id, text, top, top);
|
|
507
573
|
};
|
|
574
|
+
// `within(t)`: a doc id, else a path in either form (`/texts/*`, `texts/*`,
|
|
575
|
+
// `texts/x.md`) matched against the storage path.
|
|
508
576
|
const within = (r, target) => {
|
|
509
577
|
if (target.startsWith("d_"))
|
|
510
578
|
return r.doc_id === target;
|
|
511
|
-
|
|
512
|
-
|
|
579
|
+
const path = storagePath(target);
|
|
580
|
+
if (path.includes("*")) {
|
|
581
|
+
const like = path.replace(/[%_]/g, "\\$&").replace(/\*/g, "%");
|
|
513
582
|
return !!one(`SELECT 1 WHERE ? LIKE ? ESCAPE '\\'`, r.__path, like);
|
|
514
583
|
}
|
|
515
|
-
return r.__path ===
|
|
584
|
+
return r.__path === path;
|
|
516
585
|
};
|
|
517
586
|
const underKind = (r, kind, name) => {
|
|
518
587
|
const ap = String(r.ancestor_path ?? "").split("/").filter(Boolean);
|
|
@@ -531,14 +600,67 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
531
600
|
return false;
|
|
532
601
|
return jattr(r, "key") === leaf || jattr(r, "key") === key;
|
|
533
602
|
};
|
|
603
|
+
// ---- refs(x): document references held in a property (spec/surface §1.3, 1.5)
|
|
604
|
+
// `x` is a string, a list, or absent; every string element that names a live
|
|
605
|
+
// document of this repo — a doc id (`d_…`), a repo-root-absolute path
|
|
606
|
+
// (`/a/b.md`) or a bare repo-relative path (`a/b.md`) — resolves to that
|
|
607
|
+
// document's row; anything else (a dangling reference, a non-string element)
|
|
608
|
+
// is dropped. Order preserved, duplicates kept. Each element is one indexed
|
|
609
|
+
// lookup under the root scan's guards and columns (`sql/scan.ts`), so the rows
|
|
610
|
+
// are indistinguishable from scanned ones and the docs root is never read.
|
|
611
|
+
const refDoc = (ref) => {
|
|
612
|
+
const g = guards("docs", repoId);
|
|
613
|
+
const path = storagePath(ref);
|
|
614
|
+
const byPath = one(`SELECT ${COLS.docs} FROM ${FROM.docs} WHERE ${g.sql} AND d.path = ?`, ...g.params, path);
|
|
615
|
+
if (byPath)
|
|
616
|
+
return tag(byPath, "docs");
|
|
617
|
+
if (!ref.startsWith("d_"))
|
|
618
|
+
return undefined;
|
|
619
|
+
return tag(one(`SELECT ${COLS.docs} FROM ${FROM.docs} WHERE ${g.sql} AND d.doc_id = ?`, ...g.params, ref), "docs");
|
|
620
|
+
};
|
|
621
|
+
const refs = (x) => {
|
|
622
|
+
const items = x == null ? [] : Array.isArray(x) ? x : [x];
|
|
623
|
+
const out = [];
|
|
624
|
+
for (const item of items) {
|
|
625
|
+
if (typeof item !== "string")
|
|
626
|
+
continue;
|
|
627
|
+
const row = refDoc(item);
|
|
628
|
+
if (row)
|
|
629
|
+
out.push(row);
|
|
630
|
+
}
|
|
631
|
+
return out;
|
|
632
|
+
};
|
|
633
|
+
// Store indexes by (target, path) — one object, one set of prepared statements,
|
|
634
|
+
// for the whole run (`null`: the path is not indexable).
|
|
635
|
+
const indexes = new Map();
|
|
636
|
+
const identity = (row) => {
|
|
637
|
+
const t = row?.[TARGET];
|
|
638
|
+
if (t === "docs")
|
|
639
|
+
return row.doc_id;
|
|
640
|
+
if (t === "blocks")
|
|
641
|
+
return row.block_id;
|
|
642
|
+
if (t === "nodes")
|
|
643
|
+
return row.node_id;
|
|
644
|
+
if (t === "edges")
|
|
645
|
+
return row.edge_id;
|
|
646
|
+
return row;
|
|
647
|
+
};
|
|
534
648
|
return {
|
|
649
|
+
// The root scope's names (spec/oqx §2 step 5 at the root): the four lazy
|
|
650
|
+
// scans and `$id`, the repository id — so `^docs` / `0^docs` from any row is
|
|
651
|
+
// the scan and `^$id` / `0^$id` the id.
|
|
535
652
|
root(name) {
|
|
536
653
|
if (opts.rowsRoot && name === opts.rowsRoot.name)
|
|
537
654
|
return opts.rowsRoot.rows;
|
|
538
|
-
if (name === "$
|
|
539
|
-
return
|
|
655
|
+
if (name === "$id")
|
|
656
|
+
return repoId;
|
|
540
657
|
return rootFns[name] ? rootFns[name]() : undefined;
|
|
541
658
|
},
|
|
659
|
+
// The root scope's row (oqx 0.18): the repository, so `^$it` from a
|
|
660
|
+
// top-level row is the root row and `^$it.docs` the docs scan.
|
|
661
|
+
rootObject() {
|
|
662
|
+
return rootRow;
|
|
663
|
+
},
|
|
542
664
|
get: getFrom,
|
|
543
665
|
toRows(value) {
|
|
544
666
|
if (value == null)
|
|
@@ -551,17 +673,24 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
551
673
|
return value;
|
|
552
674
|
return [value];
|
|
553
675
|
},
|
|
554
|
-
identity
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
if (
|
|
561
|
-
return
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
676
|
+
identity,
|
|
677
|
+
// A store-backed equality index for a ROOT SCAN handle on a column or
|
|
678
|
+
// property SQLite indexes (store-index.ts); `undefined` for any other
|
|
679
|
+
// collection (a relation's rows, a lifted array) or path, so the engine
|
|
680
|
+
// builds its own over the materialized rows.
|
|
681
|
+
indexFor(collection, path) {
|
|
682
|
+
if (collection === null || typeof collection !== "object")
|
|
683
|
+
return undefined;
|
|
684
|
+
const root = rootScans.get(collection);
|
|
685
|
+
if (!root)
|
|
686
|
+
return undefined;
|
|
687
|
+
const key = `${root.target}\u0000${JSON.stringify(path)}`;
|
|
688
|
+
let index = indexes.get(key);
|
|
689
|
+
if (index === undefined) {
|
|
690
|
+
index = storeIndexFor(root.target, path, { db, repoId, tag: (rows, t) => tagAll(rows, t), get: getFrom, rows: root.rows, identity }) ?? null;
|
|
691
|
+
indexes.set(key, index);
|
|
692
|
+
}
|
|
693
|
+
return index ?? undefined;
|
|
565
694
|
},
|
|
566
695
|
// Free functions: oqx-js builtins (list/size/has) plus `range(s)`, which
|
|
567
696
|
// coerces a string value to a range so `<point> in range(prop)` tests
|
|
@@ -577,6 +706,8 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
577
706
|
const p = args[0];
|
|
578
707
|
return { handled: true, value: docPropEntries(p.docId, p.source) };
|
|
579
708
|
}
|
|
709
|
+
if (name === "refs")
|
|
710
|
+
return { handled: true, value: refs(args[0]) };
|
|
580
711
|
if (name === "range") {
|
|
581
712
|
const s = args[0];
|
|
582
713
|
if (semantics.isRange(s))
|
|
@@ -613,7 +744,8 @@ export function tagRows(rows, target) {
|
|
|
613
744
|
/**
|
|
614
745
|
* A store row surfacing as a VALUE in a result (spec/surface §1.4, rows as
|
|
615
746
|
* values, 1.2) is rendered as `{ id, path }` — its id column and its document's
|
|
616
|
-
* path, the same two keys every hit
|
|
747
|
+
* path in the reference form (`/a.md`, §1 "Paths"), the same two keys every hit
|
|
748
|
+
* carries — never the store row itself.
|
|
617
749
|
* Returns undefined for anything that is not a tagged row (run.ts leaves those
|
|
618
750
|
* alone).
|
|
619
751
|
*/
|
|
@@ -624,7 +756,7 @@ export function rowRef(value) {
|
|
|
624
756
|
const r = value;
|
|
625
757
|
const id = t === "docs" ? r.doc_id : t === "blocks" ? r.block_id : t === "nodes" ? r.node_id : r.edge_id;
|
|
626
758
|
const path = t === "docs" ? r.path : r.__path;
|
|
627
|
-
return { id: String(id), path:
|
|
759
|
+
return { id: String(id), path: path == null ? "" : referencePath(String(path)) };
|
|
628
760
|
}
|
|
629
761
|
export { TARGET };
|
|
630
762
|
//# sourceMappingURL=context.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import type { Query, Where, QueryPlanner, Plan } from "@omgbase/oqx";
|
|
1
|
+
import type { Query, Expr, Where, QueryPlanner, Plan } from "@omgbase/oqx";
|
|
2
2
|
import type { Store } from "../core/store/store.js";
|
|
3
3
|
import { type StoreContextOptions } from "./context.js";
|
|
4
4
|
import { type Target } from "./sql/translate.js";
|
|
5
|
+
export declare function rootTarget(source: Expr): Target | null;
|
|
5
6
|
/**
|
|
6
7
|
* True when evaluating `where` in memory could raise an OQX error the pushed
|
|
7
8
|
* conjuncts might hide by emptying the scan. The walk is generic over the whole
|
|
@@ -14,6 +15,12 @@ import { type Target } from "./sql/translate.js";
|
|
|
14
15
|
* • a bare reserved docs basename (`id path updated_at content_hash body`): as
|
|
15
16
|
* an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
|
|
16
17
|
* on any target at any depth (the reach-through row is a doc)
|
|
18
|
+
* • a bare target name (`docs blocks nodes edges`, root-row.ts) read as a
|
|
19
|
+
* property: as an `ident` at the ROOT scope when it is not a relation of
|
|
20
|
+
* the target's rows (`blocks` on blocks, `nodes` on nodes, any on edges),
|
|
21
|
+
* as an `ident` at any depth below (the rows there are unknown), or as
|
|
22
|
+
* `<head>.<name>` unless the head is a reach-through whose row has that
|
|
23
|
+
* relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
|
|
17
24
|
*
|
|
18
25
|
* A residual made only of comparisons, logical operators, `in`/ranges, `!`,
|
|
19
26
|
* literals, bindings and plain reads (including exists/none/count/collect/first
|
|
@@ -17,49 +17,15 @@
|
|
|
17
17
|
import { ROWS_ROOT, partitionPushable, residualQuery } from "@omgbase/oqx";
|
|
18
18
|
import { makeStoreContext, tagRows } from "./context.js";
|
|
19
19
|
import { translatePredicate, RESERVED_DOC_BASENAMES } from "./sql/translate.js";
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
blocks: { self: "b", doc: "d" },
|
|
23
|
-
nodes: { self: "n", doc: "d" },
|
|
24
|
-
edges: { self: "e", doc: "d" },
|
|
25
|
-
};
|
|
26
|
-
const FROM = {
|
|
27
|
-
docs: "docs d",
|
|
28
|
-
blocks: "blocks b JOIN docs d ON d.doc_id = b.doc_id",
|
|
29
|
-
nodes: "nodes n JOIN docs d ON d.doc_id = n.doc_id",
|
|
30
|
-
edges: "edges e JOIN docs d ON d.doc_id = e.src_doc",
|
|
31
|
-
};
|
|
32
|
-
// Row columns + the owning-doc path as __path (matches the context roots so
|
|
33
|
-
// produced rows are indistinguishable from a full scan's).
|
|
34
|
-
const COLS = {
|
|
35
|
-
docs: "d.*",
|
|
36
|
-
blocks: "b.*, d.path AS __path",
|
|
37
|
-
nodes: "n.*, d.path AS __path",
|
|
38
|
-
edges: "e.*, d.path AS __path",
|
|
39
|
-
};
|
|
40
|
-
const ORDER = {
|
|
41
|
-
docs: "d.path, d.doc_id",
|
|
42
|
-
blocks: "d.path, b.block_id",
|
|
43
|
-
nodes: "d.path, n.node_id",
|
|
44
|
-
edges: "d.path, e.edge_id",
|
|
45
|
-
};
|
|
46
|
-
function guards(target, repoId) {
|
|
47
|
-
switch (target) {
|
|
48
|
-
case "docs": return { sql: "d.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
|
|
49
|
-
case "blocks": return { sql: "b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
|
|
50
|
-
case "nodes": return { sql: "n.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
|
|
51
|
-
case "edges": return { sql: "e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
|
|
52
|
-
}
|
|
53
|
-
}
|
|
20
|
+
import { ALIAS, COLS, FROM, ORDER, guards } from "./sql/scan.js";
|
|
21
|
+
import { REACH_THROUGH, TARGETS, isTargetRelationOf } from "./root-row.js";
|
|
54
22
|
// The root collection a query scans, if it is a bare `docs|blocks|nodes|edges`
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
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). run.ts also asks: a bare
|
|
25
|
+
// root scan's rows are store rows by construction.
|
|
26
|
+
export function rootTarget(source) {
|
|
58
27
|
if (source.kind === "ident" && TARGETS.has(source.name))
|
|
59
28
|
return source.name;
|
|
60
|
-
if (source.kind === "member" && source.recv.kind === "ident" && source.recv.name === "$repo" && TARGETS.has(source.name)) {
|
|
61
|
-
return source.name;
|
|
62
|
-
}
|
|
63
29
|
return null;
|
|
64
30
|
}
|
|
65
31
|
// ---- the residual walk (decline (c)) -----------------------------------------
|
|
@@ -75,6 +41,12 @@ function rootTarget(source) {
|
|
|
75
41
|
* • a bare reserved docs basename (`id path updated_at content_hash body`): as
|
|
76
42
|
* an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
|
|
77
43
|
* on any target at any depth (the reach-through row is a doc)
|
|
44
|
+
* • a bare target name (`docs blocks nodes edges`, root-row.ts) read as a
|
|
45
|
+
* property: as an `ident` at the ROOT scope when it is not a relation of
|
|
46
|
+
* the target's rows (`blocks` on blocks, `nodes` on nodes, any on edges),
|
|
47
|
+
* as an `ident` at any depth below (the rows there are unknown), or as
|
|
48
|
+
* `<head>.<name>` unless the head is a reach-through whose row has that
|
|
49
|
+
* relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
|
|
78
50
|
*
|
|
79
51
|
* A residual made only of comparisons, logical operators, `in`/ranges, `!`,
|
|
80
52
|
* literals, bindings and plain reads (including exists/none/count/collect/first
|
|
@@ -133,13 +105,21 @@ function exprMayRaise(e, target, root) {
|
|
|
133
105
|
switch (e.kind) {
|
|
134
106
|
case "call": return true;
|
|
135
107
|
case "outer": return true;
|
|
136
|
-
case "ident":
|
|
108
|
+
case "ident":
|
|
109
|
+
if (TARGETS.has(e.name))
|
|
110
|
+
return !root || !isTargetRelationOf(target, e.name);
|
|
111
|
+
return root && target === "docs" && RESERVED_DOC_BASENAMES.has(e.name);
|
|
137
112
|
case "member":
|
|
138
113
|
if (e.recv.kind === "ident" && e.recv.name === "doc" && RESERVED_DOC_BASENAMES.has(e.name))
|
|
139
114
|
return true;
|
|
115
|
+
if (TARGETS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
|
|
116
|
+
return true;
|
|
140
117
|
return exprMayRaise(e.recv, target, root);
|
|
141
|
-
case "index": return exprMayRaise(e.recv, target, root) || exprMayRaise(e.index, target, root);
|
|
142
118
|
case "unary": return exprMayRaise(e.expr, target, root);
|
|
119
|
+
// `x!` raises when absent; a value-position directive may `single`-fail or
|
|
120
|
+
// read anything its block reads.
|
|
121
|
+
case "required": return true;
|
|
122
|
+
case "op": return opMayRaise(e, target, root);
|
|
143
123
|
case "binary":
|
|
144
124
|
case "logical":
|
|
145
125
|
case "in":
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Target = "docs" | "blocks" | "nodes" | "edges";
|
|
2
|
+
export declare const TARGETS: ReadonlySet<string>;
|
|
3
|
+
/** The target names that are a relation of each target's rows (§1.2). */
|
|
4
|
+
export declare const TARGET_RELATIONS: Readonly<Record<Target, ReadonlySet<string>>>;
|
|
5
|
+
/** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`). */
|
|
6
|
+
export declare const REACH_THROUGH: Readonly<Record<string, Target>>;
|
|
7
|
+
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`";
|
|
8
|
+
/**
|
|
9
|
+
* The bare-target error. `depth` is the scope depth the name was read at when
|
|
10
|
+
* known statically (the carets in the hint count it); `null` when raised at read
|
|
11
|
+
* time, where the hint spells out the rule instead.
|
|
12
|
+
*/
|
|
13
|
+
export declare function bareTargetMessage(name: string, depth: number | null): string;
|
|
14
|
+
/**
|
|
15
|
+
* The past-the-root error: `^docs` at the root scope (more carets than there
|
|
16
|
+
* are enclosing scopes) would read absent and count 0 — refused, naming the
|
|
17
|
+
* bare spelling. `levels` is the caret count written.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pastRootMessage(name: string, levels: number): string;
|
|
20
|
+
/** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
|
|
21
|
+
export declare function isTargetRelationOf(target: Target | undefined, name: string): boolean;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The root row (spec/surface §1.1, 2.0; spec/oqx 0.18). The repository is OQX's
|
|
2
|
+
// root row: from a top-level row `^docs` reaches the documents (one caret per
|
|
3
|
+
// enclosing block, or the absolute `0^docs`), `^$id` the repository id, and the
|
|
4
|
+
// root object (`^$it`) exposes the four collections. Two spellings are refused
|
|
5
|
+
// as `filter_invalid`, with the messages below, in both engines:
|
|
6
|
+
//
|
|
7
|
+
// • `$repo` (removed in 2.0) anywhere — bare, as a member head (`$repo.docs`),
|
|
8
|
+
// `^$repo`, `0^$repo` — statically, before any row is read, so the error is
|
|
9
|
+
// deterministic whether or not a row would have reached it (run.ts
|
|
10
|
+
// `checkRootSpellings`);
|
|
11
|
+
// • a bare target name at scope depth ≥ 1 (inside a block, or in a top-level
|
|
12
|
+
// row's clauses), where a bare name reads a property of the current row,
|
|
13
|
+
// which has none: `docs` and `edges` are no row's relation, so they are
|
|
14
|
+
// refused statically by the same walk; `blocks` and `nodes` ARE relations
|
|
15
|
+
// of some rows (`docs.blocks`, `docs.nodes`, `blocks.nodes`,
|
|
16
|
+
// `section.blocks`), so the context raises the same error at read time on a
|
|
17
|
+
// row that lacks the relation (context.ts). A frontmatter key named after a
|
|
18
|
+
// target is unreachable by bare name (use `frontmatter.<k>`).
|
|
19
|
+
export const TARGETS = new Set(["docs", "blocks", "nodes", "edges"]);
|
|
20
|
+
/** The target names that are a relation of each target's rows (§1.2). */
|
|
21
|
+
export const TARGET_RELATIONS = {
|
|
22
|
+
docs: new Set(["nodes", "blocks"]),
|
|
23
|
+
blocks: new Set(["nodes"]),
|
|
24
|
+
nodes: new Set(["blocks"]),
|
|
25
|
+
edges: new Set(),
|
|
26
|
+
};
|
|
27
|
+
/** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`). */
|
|
28
|
+
export const REACH_THROUGH = { doc: "docs", block: "blocks", section: "nodes" };
|
|
29
|
+
const NOUN = { docs: "documents", blocks: "blocks", nodes: "nodes", edges: "edges" };
|
|
30
|
+
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
|
+
/**
|
|
32
|
+
* The bare-target error. `depth` is the scope depth the name was read at when
|
|
33
|
+
* known statically (the carets in the hint count it); `null` when raised at read
|
|
34
|
+
* time, where the hint spells out the rule instead.
|
|
35
|
+
*/
|
|
36
|
+
export function bareTargetMessage(name, depth) {
|
|
37
|
+
const t = name;
|
|
38
|
+
const carets = "^".repeat(depth ?? 1);
|
|
39
|
+
const how = depth === null ? `; one caret per enclosing block, or the absolute \`0^${name}\`` : "";
|
|
40
|
+
return `\`${name}\` inside a block reads a property of the current row, which has none — did you mean \`${carets}${name}\` (the repository's ${NOUN[t]}${how})?`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The past-the-root error: `^docs` at the root scope (more carets than there
|
|
44
|
+
* are enclosing scopes) would read absent and count 0 — refused, naming the
|
|
45
|
+
* bare spelling. `levels` is the caret count written.
|
|
46
|
+
*/
|
|
47
|
+
export function pastRootMessage(name, levels) {
|
|
48
|
+
const t = name;
|
|
49
|
+
return `\`${"^".repeat(levels)}${name}\` reaches past the root — there is no enclosing row at this depth; at the top level the repository's ${NOUN[t]} are the bare \`${name}\` (\`${name} count { … }\`, \`from ${name}\`, \`entries(${name})\`)`;
|
|
50
|
+
}
|
|
51
|
+
/** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
|
|
52
|
+
export function isTargetRelationOf(target, name) {
|
|
53
|
+
return target !== undefined && TARGET_RELATIONS[target].has(name);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=root-row.js.map
|
package/dist/src/oqx-js/run.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Query } from "@omgbase/oqx";
|
|
1
|
+
import type { Query, Expr } from "@omgbase/oqx";
|
|
2
2
|
import type { Store } from "../core/store/store.js";
|
|
3
3
|
import type { SemanticVec } from "../search/cel/compile.js";
|
|
4
4
|
export type OqxConsumer = "collect" | "count" | "exists" | "none" | "first" | "single";
|
|
@@ -34,6 +34,8 @@ export interface OqxOptions {
|
|
|
34
34
|
* the differential conformance suite to prove planned == in-memory. */
|
|
35
35
|
plan?: boolean;
|
|
36
36
|
}
|
|
37
|
+
export declare function rootPathLiterals(e: Expr): Expr;
|
|
38
|
+
export declare function checkRootSpellings(q: Query): void;
|
|
37
39
|
export declare function collectSemanticPhrases(source: string): string[];
|
|
38
40
|
export declare function oqxRun(store: Store, repoId: string, source: string, opts?: OqxOptions): OqxResult;
|
|
39
41
|
export declare function oqxRunAsync(store: Store, repoId: string, source: string, opts?: OqxOptions, embedQuery?: EmbedQuery): Promise<OqxResult>;
|