@omgbase/core 0.1.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/LICENSE +21 -0
- package/dist/src/cli/eval-matcher.d.ts +13 -0
- package/dist/src/cli/eval-matcher.js +36 -0
- package/dist/src/core/attach.d.ts +5 -0
- package/dist/src/core/attach.js +38 -0
- package/dist/src/core/cursor.d.ts +11 -0
- package/dist/src/core/cursor.js +35 -0
- package/dist/src/core/hash.d.ts +23 -0
- package/dist/src/core/hash.js +82 -0
- package/dist/src/core/ids.d.ts +6 -0
- package/dist/src/core/ids.js +26 -0
- package/dist/src/core/index.d.ts +1 -0
- package/dist/src/core/index.js +2 -0
- package/dist/src/core/ingest.d.ts +43 -0
- package/dist/src/core/ingest.js +287 -0
- package/dist/src/core/order-key.d.ts +8 -0
- package/dist/src/core/order-key.js +72 -0
- package/dist/src/core/parse/parse.d.ts +3 -0
- package/dist/src/core/parse/parse.js +104 -0
- package/dist/src/core/parse/render.d.ts +3 -0
- package/dist/src/core/parse/render.js +20 -0
- package/dist/src/core/parse/tree.d.ts +8 -0
- package/dist/src/core/parse/tree.js +43 -0
- package/dist/src/core/parse/types.d.ts +52 -0
- package/dist/src/core/parse/types.js +9 -0
- package/dist/src/core/read/document.d.ts +107 -0
- package/dist/src/core/read/document.js +191 -0
- package/dist/src/core/read/nodes.d.ts +47 -0
- package/dist/src/core/read/nodes.js +102 -0
- package/dist/src/core/read/outline.d.ts +12 -0
- package/dist/src/core/read/outline.js +76 -0
- package/dist/src/core/read/reader.d.ts +125 -0
- package/dist/src/core/read/reader.js +206 -0
- package/dist/src/core/read/refs.d.ts +15 -0
- package/dist/src/core/read/refs.js +51 -0
- package/dist/src/core/store/edges.d.ts +26 -0
- package/dist/src/core/store/edges.js +101 -0
- package/dist/src/core/store/fts.d.ts +10 -0
- package/dist/src/core/store/fts.js +22 -0
- package/dist/src/core/store/gc.d.ts +18 -0
- package/dist/src/core/store/gc.js +69 -0
- package/dist/src/core/store/nodes.d.ts +5 -0
- package/dist/src/core/store/nodes.js +57 -0
- package/dist/src/core/store/properties.d.ts +38 -0
- package/dist/src/core/store/properties.js +177 -0
- package/dist/src/core/store/rebuild.d.ts +3 -0
- package/dist/src/core/store/rebuild.js +29 -0
- package/dist/src/core/store/schema.d.ts +9 -0
- package/dist/src/core/store/schema.js +386 -0
- package/dist/src/core/store/sections.d.ts +2 -0
- package/dist/src/core/store/sections.js +29 -0
- package/dist/src/core/store/store.d.ts +24 -0
- package/dist/src/core/store/store.js +164 -0
- package/dist/src/core/store/writers.d.ts +52 -0
- package/dist/src/core/store/writers.js +89 -0
- package/dist/src/core/vec.d.ts +10 -0
- package/dist/src/core/vec.js +33 -0
- package/dist/src/format/adapter.d.ts +97 -0
- package/dist/src/format/adapter.js +14 -0
- package/dist/src/format/index.d.ts +6 -0
- package/dist/src/format/index.js +15 -0
- package/dist/src/format/json.d.ts +3 -0
- package/dist/src/format/json.js +324 -0
- package/dist/src/format/markdown.d.ts +5 -0
- package/dist/src/format/markdown.js +217 -0
- package/dist/src/format/registry.d.ts +6 -0
- package/dist/src/format/registry.js +24 -0
- package/dist/src/format/yaml.d.ts +5 -0
- package/dist/src/format/yaml.js +413 -0
- package/dist/src/graph/extract.d.ts +38 -0
- package/dist/src/graph/extract.js +242 -0
- package/dist/src/graph/history.d.ts +103 -0
- package/dist/src/graph/history.js +210 -0
- package/dist/src/graph/inbound-links.d.ts +33 -0
- package/dist/src/graph/inbound-links.js +130 -0
- package/dist/src/graph/link-destinations.d.ts +22 -0
- package/dist/src/graph/link-destinations.js +84 -0
- package/dist/src/graph/link-health.d.ts +79 -0
- package/dist/src/graph/link-health.js +146 -0
- package/dist/src/graph/links.d.ts +23 -0
- package/dist/src/graph/links.js +42 -0
- package/dist/src/index.d.ts +60 -0
- package/dist/src/index.js +70 -0
- package/dist/src/mcp/errors.d.ts +17 -0
- package/dist/src/mcp/errors.js +18 -0
- package/dist/src/mcp/graph.d.ts +63 -0
- package/dist/src/mcp/graph.js +212 -0
- package/dist/src/mcp/reference.d.ts +1 -0
- package/dist/src/mcp/reference.js +227 -0
- package/dist/src/mcp/server.d.ts +30 -0
- package/dist/src/mcp/server.js +1076 -0
- package/dist/src/mcp/stdio.d.ts +13 -0
- package/dist/src/mcp/stdio.js +24 -0
- package/dist/src/migrate/mrplex.d.ts +27 -0
- package/dist/src/migrate/mrplex.js +29 -0
- package/dist/src/mutate/apply.d.ts +83 -0
- package/dist/src/mutate/apply.js +302 -0
- package/dist/src/mutate/doc-store.d.ts +49 -0
- package/dist/src/mutate/doc-store.js +71 -0
- package/dist/src/mutate/docs.d.ts +60 -0
- package/dist/src/mutate/docs.js +232 -0
- package/dist/src/mutate/known-ids.d.ts +12 -0
- package/dist/src/mutate/known-ids.js +154 -0
- package/dist/src/mutate/load.d.ts +3 -0
- package/dist/src/mutate/load.js +66 -0
- package/dist/src/mutate/lower.d.ts +32 -0
- package/dist/src/mutate/lower.js +232 -0
- package/dist/src/mutate/macros.d.ts +91 -0
- package/dist/src/mutate/macros.js +230 -0
- package/dist/src/mutate/ops.d.ts +40 -0
- package/dist/src/mutate/ops.js +513 -0
- package/dist/src/mutate/opset.d.ts +92 -0
- package/dist/src/mutate/opset.js +79 -0
- package/dist/src/mutate/plan-update.d.ts +51 -0
- package/dist/src/mutate/plan-update.js +187 -0
- package/dist/src/mutate/tree.d.ts +46 -0
- package/dist/src/mutate/tree.js +167 -0
- package/dist/src/oqx/run.d.ts +1 -0
- package/dist/src/oqx/run.js +6 -0
- package/dist/src/oqx-js/context.d.ts +23 -0
- package/dist/src/oqx-js/context.js +564 -0
- package/dist/src/oqx-js/planner.d.ts +10 -0
- package/dist/src/oqx-js/planner.js +91 -0
- package/dist/src/oqx-js/run.d.ts +41 -0
- package/dist/src/oqx-js/run.js +332 -0
- package/dist/src/oqx-js/sql/translate.d.ts +27 -0
- package/dist/src/oqx-js/sql/translate.js +290 -0
- package/dist/src/reconcile/crossdoc.d.ts +34 -0
- package/dist/src/reconcile/crossdoc.js +76 -0
- package/dist/src/reconcile/eval/generator.d.ts +21 -0
- package/dist/src/reconcile/eval/generator.js +129 -0
- package/dist/src/reconcile/eval/metrics.d.ts +32 -0
- package/dist/src/reconcile/eval/metrics.js +74 -0
- package/dist/src/reconcile/flatten.d.ts +12 -0
- package/dist/src/reconcile/flatten.js +37 -0
- package/dist/src/reconcile/phase5.d.ts +8 -0
- package/dist/src/reconcile/phase5.js +167 -0
- package/dist/src/reconcile/phase6a.d.ts +2 -0
- package/dist/src/reconcile/phase6a.js +159 -0
- package/dist/src/reconcile/phases.d.ts +28 -0
- package/dist/src/reconcile/phases.js +0 -0
- package/dist/src/reconcile/reconcile.d.ts +21 -0
- package/dist/src/reconcile/reconcile.js +115 -0
- package/dist/src/reconcile/similarity.d.ts +7 -0
- package/dist/src/reconcile/similarity.js +41 -0
- package/dist/src/reconcile/types.d.ts +46 -0
- package/dist/src/reconcile/types.js +19 -0
- package/dist/src/search/cel/ast.d.ts +74 -0
- package/dist/src/search/cel/ast.js +3 -0
- package/dist/src/search/cel/compile.d.ts +60 -0
- package/dist/src/search/cel/compile.js +836 -0
- package/dist/src/search/cel/lexer.d.ts +9 -0
- package/dist/src/search/cel/lexer.js +139 -0
- package/dist/src/search/cel/parser.d.ts +7 -0
- package/dist/src/search/cel/parser.js +219 -0
- package/dist/src/search/drain.d.ts +39 -0
- package/dist/src/search/drain.js +97 -0
- package/dist/src/search/embeddings.d.ts +131 -0
- package/dist/src/search/embeddings.js +263 -0
- package/dist/src/search/external.d.ts +13 -0
- package/dist/src/search/external.js +177 -0
- package/dist/src/search/fts-query.d.ts +1 -0
- package/dist/src/search/fts-query.js +64 -0
- package/dist/src/search/provider.d.ts +17 -0
- package/dist/src/search/provider.js +13 -0
- package/dist/src/search/resolve.d.ts +18 -0
- package/dist/src/search/resolve.js +23 -0
- package/dist/src/search/rrf.d.ts +36 -0
- package/dist/src/search/rrf.js +84 -0
- package/dist/src/search/tasks.d.ts +12 -0
- package/dist/src/search/tasks.js +131 -0
- package/dist/src/search/text.d.ts +16 -0
- package/dist/src/search/text.js +21 -0
- package/dist/src/search/vector.d.ts +18 -0
- package/dist/src/search/vector.js +49 -0
- package/dist/src/sync/admin.d.ts +59 -0
- package/dist/src/sync/admin.js +53 -0
- package/dist/src/sync/attach.d.ts +13 -0
- package/dist/src/sync/attach.js +29 -0
- package/dist/src/sync/checkpoint.d.ts +21 -0
- package/dist/src/sync/checkpoint.js +52 -0
- package/dist/src/sync/driver.d.ts +25 -0
- package/dist/src/sync/driver.js +80 -0
- package/dist/src/sync/external-source.d.ts +14 -0
- package/dist/src/sync/external-source.js +137 -0
- package/dist/src/sync/freshness.d.ts +42 -0
- package/dist/src/sync/freshness.js +152 -0
- package/dist/src/sync/fs-util.d.ts +12 -0
- package/dist/src/sync/fs-util.js +55 -0
- package/dist/src/sync/git-heuristics.d.ts +6 -0
- package/dist/src/sync/git-heuristics.js +16 -0
- package/dist/src/sync/observe.d.ts +63 -0
- package/dist/src/sync/observe.js +84 -0
- package/dist/src/sync/plugin.d.ts +54 -0
- package/dist/src/sync/plugin.js +12 -0
- package/dist/src/sync/reconciling-ingest.d.ts +12 -0
- package/dist/src/sync/reconciling-ingest.js +209 -0
- package/dist/src/sync/recovery.d.ts +6 -0
- package/dist/src/sync/recovery.js +34 -0
- package/dist/src/sync/settings.d.ts +18 -0
- package/dist/src/sync/settings.js +58 -0
- package/dist/src/sync/sources.d.ts +37 -0
- package/dist/src/sync/sources.js +75 -0
- package/dist/src/sync/tombstone.d.ts +9 -0
- package/dist/src/sync/tombstone.js +55 -0
- package/dist/src/sync/watch-lease.d.ts +14 -0
- package/dist/src/sync/watch-lease.js +91 -0
- package/dist/src/sync/watcher.d.ts +19 -0
- package/dist/src/sync/watcher.js +34 -0
- package/dist/src/sync/workspace.d.ts +34 -0
- package/dist/src/sync/workspace.js +120 -0
- package/dist/src/sync/writer-lock.d.ts +16 -0
- package/dist/src/sync/writer-lock.js +121 -0
- package/package.json +80 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
// A store-backed DataContext (oqx-js tier 2): binds `@omgbase/oqx`'s query
|
|
2
|
+
// semantics to omgbase's SQLite store, so the InMemoryEngine reproduces the whole
|
|
3
|
+
// OQX surface (targets, intrinsics, reach-through, structural relations, the edge
|
|
4
|
+
// graph, follow, correlation, lifts) without a bespoke compiler. Rows are the
|
|
5
|
+
// store's raw column objects, tagged with their target; `get` routes field /
|
|
6
|
+
// intrinsic / relation resolution per target, lazily querying the store for
|
|
7
|
+
// relations. Domain functions (text/semantic/under_*/…) are row-scoped, so they
|
|
8
|
+
// arrive via `callMethod` on a synthetic `$self` receiver (see run.ts's AST
|
|
9
|
+
// rewrite) rather than as free functions (which see no row).
|
|
10
|
+
//
|
|
11
|
+
// This is the correctness tier: N+1 queries, materialized collections — fine for
|
|
12
|
+
// the corpus and the differential baseline. The tier-3 SQLite planner (planner.ts)
|
|
13
|
+
// pushes the hot predicates down; both must agree (conformance suite).
|
|
14
|
+
import { semantics } from "@omgbase/oqx";
|
|
15
|
+
import { detectRange } from "../core/store/properties.js";
|
|
16
|
+
import { docsRead } from "../core/read/document.js";
|
|
17
|
+
import { FilterInvalid } from "../search/cel/parser.js";
|
|
18
|
+
import { sanitizeFtsQuery } from "../search/fts-query.js";
|
|
19
|
+
import { cosineBytes } from "../core/vec.js";
|
|
20
|
+
const TARGET = Symbol("oqx.target");
|
|
21
|
+
const REPO_ROOT = Symbol("oqx.repoRoot");
|
|
22
|
+
const PROP_SOURCE = Symbol("oqx.propSource");
|
|
23
|
+
function tag(row, t) {
|
|
24
|
+
if (!row)
|
|
25
|
+
return undefined;
|
|
26
|
+
row[TARGET] = t;
|
|
27
|
+
return row;
|
|
28
|
+
}
|
|
29
|
+
function tagAll(rows, t) {
|
|
30
|
+
for (const r of rows)
|
|
31
|
+
r[TARGET] = t;
|
|
32
|
+
return rows;
|
|
33
|
+
}
|
|
34
|
+
// docs intrinsics whose BARE (non-$) form is almost always a typo (10 §2); a bare
|
|
35
|
+
// read of one is a loud error, matching the CEL guard.
|
|
36
|
+
// (The root-scan receiver is the `$repo` intrinsic — `$repo.docs`/`$repo.nodes`/… —
|
|
37
|
+
// so a bare `repo` is just an ordinary frontmatter key.)
|
|
38
|
+
const RESERVED_DOC_BASENAMES = new Set(["id", "path", "updated_at", "content_hash", "body"]);
|
|
39
|
+
export function makeStoreContext(store, repoId, opts = {}) {
|
|
40
|
+
const db = store.db;
|
|
41
|
+
const sv = opts.semanticVectors;
|
|
42
|
+
const all = (sql, ...params) => db.prepare(sql).all(...params);
|
|
43
|
+
const one = (sql, ...params) => db.prepare(sql).get(...params);
|
|
44
|
+
const scalar = (sql, ...params) => {
|
|
45
|
+
const r = db.prepare(sql).get(...params);
|
|
46
|
+
if (!r)
|
|
47
|
+
return undefined;
|
|
48
|
+
return Object.values(r)[0];
|
|
49
|
+
};
|
|
50
|
+
// ---- root collections (ordered for a stable (path, id) default) -----------
|
|
51
|
+
const docsRoot = () => tagAll(all(`SELECT * FROM docs WHERE repo_id = ? AND deleted_commit IS NULL ORDER BY path, doc_id`, repoId), "docs");
|
|
52
|
+
const blocksRoot = () => tagAll(all(`SELECT b.*, d.path AS __path FROM blocks b JOIN docs d ON d.doc_id = b.doc_id
|
|
53
|
+
WHERE b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL
|
|
54
|
+
ORDER BY d.path, b.block_id`, repoId), "blocks");
|
|
55
|
+
const nodesRoot = () => tagAll(all(`SELECT n.*, d.path AS __path FROM nodes n JOIN docs d ON d.doc_id = n.doc_id
|
|
56
|
+
WHERE n.repo_id = ? AND d.deleted_commit IS NULL ORDER BY d.path, n.node_id`, repoId), "nodes");
|
|
57
|
+
const edgesRoot = () => tagAll(all(`SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
|
|
58
|
+
WHERE e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL
|
|
59
|
+
ORDER BY d.path, e.edge_id`, repoId), "edges");
|
|
60
|
+
const rootFns = { docs: docsRoot, blocks: blocksRoot, nodes: nodesRoot, edges: edgesRoot };
|
|
61
|
+
// The repository handle behind the `$repo` intrinsic: `$repo.<target>` is the
|
|
62
|
+
// explicit root scan (rootFns), `$repo.$id` the repository id.
|
|
63
|
+
const repoRoot = { [REPO_ROOT]: true };
|
|
64
|
+
// ---- attrs / property decoding --------------------------------------------
|
|
65
|
+
const parseJson = (v) => {
|
|
66
|
+
if (typeof v !== "string")
|
|
67
|
+
return v ?? undefined;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(v);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return v;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
// A property decodes to a plain scalar. A range-valued string (a frontmatter
|
|
76
|
+
// `1..5` / `2026-01-01..2026-01-31`) stays a STRING here — range behavior is
|
|
77
|
+
// opt-in via the `range(prop)` function (see callFunction), so a value that
|
|
78
|
+
// merely looks rangey is never silently reinterpreted, and projection/equality
|
|
79
|
+
// see the authored text. The store still caches the parsed bounds in val_json
|
|
80
|
+
// (see properties.ts detectRange) as an index substrate; it does not change
|
|
81
|
+
// this decode.
|
|
82
|
+
const decodeProp = (r) => {
|
|
83
|
+
switch (r.type) {
|
|
84
|
+
case "number": return r.val_num;
|
|
85
|
+
case "bool": return r.val_bool ? true : false;
|
|
86
|
+
case "null": return null;
|
|
87
|
+
case "json": return parseJson(r.val_json);
|
|
88
|
+
default: return r.val_text;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
// A docs property key resolves to a scalar iff it has exactly one row in scope
|
|
92
|
+
// AND that row is card='scalar'; otherwise to the array of values (list-authored,
|
|
93
|
+
// repeated, or frontmatter+inline collision) — reproducing the CEL scalar-vs-list
|
|
94
|
+
// rule so `==` sees a scalar only when authored scalar, and `list()` sees all.
|
|
95
|
+
const docProp = (docId, key, source) => {
|
|
96
|
+
const rows = source
|
|
97
|
+
? all(`SELECT * FROM properties WHERE doc_id = ? AND key = ? AND source = ? AND deleted_commit IS NULL ORDER BY ord`, docId, key, source)
|
|
98
|
+
: all(`SELECT * FROM properties WHERE doc_id = ? AND key = ? AND deleted_commit IS NULL ORDER BY ord`, docId, key);
|
|
99
|
+
if (rows.length === 0) {
|
|
100
|
+
// No exact key: omgbase flattens nested YAML/JSON maps to dotted keys
|
|
101
|
+
// (`logging.level`), so a bare `logging` should reconstruct the nested
|
|
102
|
+
// object for `.level` member navigation.
|
|
103
|
+
return docPropObject(docId, key, source);
|
|
104
|
+
}
|
|
105
|
+
if (rows.length === 1 && rows[0].card === "scalar")
|
|
106
|
+
return decodeProp(rows[0]);
|
|
107
|
+
return rows.map(decodeProp);
|
|
108
|
+
};
|
|
109
|
+
// All of a doc's properties from one source (`frontmatter` / `inline`) as OQX
|
|
110
|
+
// entries, for `entries(frontmatter)`: one entry per TOP-LEVEL key, valued by
|
|
111
|
+
// the same scalar-vs-list rule as a bare read (`docProp`), with flattened dotted
|
|
112
|
+
// keys (`logging.level`) folded back into one nested entry. Keys come in the
|
|
113
|
+
// properties table's deterministic order — by key — because authored key
|
|
114
|
+
// position is not indexed (`ord` is the position WITHIN a list-valued key).
|
|
115
|
+
const docPropEntries = (docId, source) => {
|
|
116
|
+
const keys = all(`SELECT DISTINCT key FROM properties WHERE doc_id = ? AND source = ? AND deleted_commit IS NULL ORDER BY key`, docId, source).map((r) => String(r.key));
|
|
117
|
+
const tops = [];
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
for (const k of keys) {
|
|
120
|
+
const top = k.split(".")[0];
|
|
121
|
+
if (!seen.has(top)) {
|
|
122
|
+
seen.add(top);
|
|
123
|
+
tops.push(top);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return tops.map((t) => semantics.makeEntry(t, docProp(docId, t, source)));
|
|
127
|
+
};
|
|
128
|
+
// Reconstruct a nested object from flattened dotted keys under `prefix.`
|
|
129
|
+
// (undefined when there are none). Leaves are decoded property values.
|
|
130
|
+
const docPropObject = (docId, prefix, source) => {
|
|
131
|
+
const rows = source
|
|
132
|
+
? all(`SELECT * FROM properties WHERE doc_id = ? AND key LIKE ? AND source = ? AND deleted_commit IS NULL ORDER BY ord`, docId, `${prefix}.%`, source)
|
|
133
|
+
: all(`SELECT * FROM properties WHERE doc_id = ? AND key LIKE ? AND deleted_commit IS NULL ORDER BY ord`, docId, `${prefix}.%`);
|
|
134
|
+
if (rows.length === 0)
|
|
135
|
+
return undefined;
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const r of rows) {
|
|
138
|
+
const rest = String(r.key).slice(prefix.length + 1).split(".");
|
|
139
|
+
let cur = out;
|
|
140
|
+
for (let i = 0; i < rest.length - 1; i++) {
|
|
141
|
+
const seg = rest[i];
|
|
142
|
+
if (typeof cur[seg] !== "object" || cur[seg] == null)
|
|
143
|
+
cur[seg] = {};
|
|
144
|
+
cur = cur[seg];
|
|
145
|
+
}
|
|
146
|
+
cur[rest[rest.length - 1]] = decodeProp(r);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
};
|
|
150
|
+
// ---- top-ordinal of a block's top-level ancestor (for section ranges) -----
|
|
151
|
+
const topOrdinal = (block) => {
|
|
152
|
+
if (block.parent_block == null)
|
|
153
|
+
return block.ordinal;
|
|
154
|
+
// ancestor_path is '/b_x/b_y/…'; first segment is the top-level ancestor.
|
|
155
|
+
const ap = String(block.ancestor_path);
|
|
156
|
+
const first = ap.split("/").filter(Boolean)[0];
|
|
157
|
+
if (!first)
|
|
158
|
+
return block.ordinal;
|
|
159
|
+
const r = one(`SELECT ordinal FROM blocks WHERE doc_id = ? AND block_id = ?`, block.doc_id, first);
|
|
160
|
+
return r?.ordinal ?? block.ordinal;
|
|
161
|
+
};
|
|
162
|
+
// ---- relations (return row arrays) ----------------------------------------
|
|
163
|
+
const rel = {
|
|
164
|
+
docs: {
|
|
165
|
+
nodes: (r) => tagAll(all(`SELECT n.*, ? AS __path FROM nodes n WHERE n.doc_id = ?`, r.path, r.doc_id), "nodes"),
|
|
166
|
+
blocks: (r) => tagAll(all(`SELECT b.*, ? AS __path FROM blocks b WHERE b.doc_id = ? AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id`, r.path, r.doc_id), "blocks"),
|
|
167
|
+
out: (r) => tagAll(all(`SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.dst_node = d2.doc_id
|
|
168
|
+
WHERE e.src_doc = ? AND e.to_commit IS NULL AND d2.repo_id = ? AND d2.deleted_commit IS NULL ORDER BY d2.path, d2.doc_id`, r.doc_id, repoId), "docs"),
|
|
169
|
+
in: (r) => tagAll(all(`SELECT DISTINCT d2.* FROM docs d2 JOIN edges e ON e.src_doc = d2.doc_id
|
|
170
|
+
WHERE e.dst_node = ? AND e.to_commit IS NULL AND d2.repo_id = ? AND d2.deleted_commit IS NULL ORDER BY d2.path, d2.doc_id`, r.doc_id, repoId), "docs"),
|
|
171
|
+
out_edges: (r) => tagAll(all(`SELECT e.*, ? AS __path FROM edges e WHERE e.src_doc = ? AND e.to_commit IS NULL ORDER BY e.predicate, e.edge_id`, r.path, r.doc_id), "edges"),
|
|
172
|
+
in_edges: (r) => tagAll(all(`SELECT e.*, d.path AS __path FROM edges e JOIN docs d ON d.doc_id = e.src_doc
|
|
173
|
+
WHERE e.dst_node = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL ORDER BY e.predicate, e.edge_id`, r.doc_id), "edges"),
|
|
174
|
+
},
|
|
175
|
+
blocks: {
|
|
176
|
+
children: (r) => tagAll(all(`SELECT b.*, ? AS __path FROM blocks b WHERE b.parent_block = ? AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id`, r.__path, r.block_id), "blocks"),
|
|
177
|
+
nodes: (r) => tagAll(all(`SELECT n.*, ? AS __path FROM nodes n WHERE n.block_id = ?`, r.__path, r.block_id), "nodes"),
|
|
178
|
+
out_edges: (r) => tagAll(all(`SELECT e.*, ? AS __path FROM edges e WHERE e.src_block = ? AND e.to_commit IS NULL ORDER BY e.predicate, e.edge_id`, r.__path, r.block_id), "edges"),
|
|
179
|
+
// enclosing section node(s): md:section whose range contains this block's top ordinal.
|
|
180
|
+
section: (r) => {
|
|
181
|
+
const top = topOrdinal(r);
|
|
182
|
+
return tagAll(all(`SELECT n.*, ? AS __path FROM nodes n WHERE n.doc_id = ? AND n.kind = 'md:section'
|
|
183
|
+
AND json_extract(n.attrs,'$.first_ordinal') <= ? AND json_extract(n.attrs,'$.last_ordinal') >= ?
|
|
184
|
+
ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id`, r.__path, r.doc_id, top, top), "nodes");
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
nodes: {
|
|
188
|
+
// section.* — the node interpreted as a section (range attrs).
|
|
189
|
+
blocks: (r) => {
|
|
190
|
+
const f = jattr(r, "first_ordinal"), l = jattr(r, "last_ordinal");
|
|
191
|
+
if (f == null || l == null)
|
|
192
|
+
return [];
|
|
193
|
+
// blocks whose top-level ordinal falls in the section range.
|
|
194
|
+
const rows = all(`SELECT b.*, ? AS __path FROM blocks b WHERE b.doc_id = ? AND b.deleted_commit IS NULL ORDER BY b.ordinal, b.block_id`, r.__path, r.doc_id);
|
|
195
|
+
const kept = rows.filter((b) => { const t = topOrdinal(b); return t >= f && t <= l; });
|
|
196
|
+
return tagAll(kept, "blocks");
|
|
197
|
+
},
|
|
198
|
+
subsections: (r) => {
|
|
199
|
+
const f = jattr(r, "first_ordinal"), l = jattr(r, "last_ordinal"), lvl = jattr(r, "level");
|
|
200
|
+
if (f == null)
|
|
201
|
+
return [];
|
|
202
|
+
return tagAll(all(`SELECT n.*, ? AS __path FROM nodes n WHERE n.doc_id = ? AND n.kind = 'md:section'
|
|
203
|
+
AND json_extract(n.attrs,'$.first_ordinal') >= ? AND json_extract(n.attrs,'$.last_ordinal') <= ?
|
|
204
|
+
AND json_extract(n.attrs,'$.level') > ? ORDER BY json_extract(n.attrs,'$.first_ordinal'), n.node_id`, r.__path, r.doc_id, f, l, lvl), "nodes");
|
|
205
|
+
},
|
|
206
|
+
children: (r) => {
|
|
207
|
+
const f = jattr(r, "first_ordinal"), l = jattr(r, "last_ordinal"), lvl = jattr(r, "level");
|
|
208
|
+
if (f == null)
|
|
209
|
+
return [];
|
|
210
|
+
// immediate child sections: contained, deeper, with no intervening section.
|
|
211
|
+
return tagAll(all(`SELECT i.*, ? AS __path FROM nodes i WHERE i.doc_id = ? AND i.kind = 'md:section'
|
|
212
|
+
AND json_extract(i.attrs,'$.level') > ?
|
|
213
|
+
AND json_extract(i.attrs,'$.first_ordinal') >= ? AND json_extract(i.attrs,'$.last_ordinal') <= ?
|
|
214
|
+
AND NOT EXISTS (SELECT 1 FROM nodes m WHERE m.doc_id = i.doc_id AND m.kind = 'md:section'
|
|
215
|
+
AND json_extract(m.attrs,'$.level') > ? AND json_extract(m.attrs,'$.level') < json_extract(i.attrs,'$.level')
|
|
216
|
+
AND json_extract(m.attrs,'$.first_ordinal') <= json_extract(i.attrs,'$.first_ordinal')
|
|
217
|
+
AND json_extract(m.attrs,'$.last_ordinal') >= json_extract(i.attrs,'$.last_ordinal'))
|
|
218
|
+
ORDER BY json_extract(i.attrs,'$.first_ordinal'), i.node_id`, r.__path, r.doc_id, lvl, f, l, lvl), "nodes");
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
edges: {},
|
|
222
|
+
};
|
|
223
|
+
const jattr = (row, k) => {
|
|
224
|
+
const a = row.attrs;
|
|
225
|
+
const o = typeof a === "string" ? parseJson(a) : a;
|
|
226
|
+
return o && typeof o === "object" ? o[k] : undefined;
|
|
227
|
+
};
|
|
228
|
+
// owning-entity reach-through (single row).
|
|
229
|
+
const owningDoc = (row) => tag(one(`SELECT * FROM docs WHERE doc_id = ?`, row.doc_id ?? row.src_doc), "docs");
|
|
230
|
+
const owningBlock = (row) => row.block_id
|
|
231
|
+
? 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")
|
|
232
|
+
: undefined;
|
|
233
|
+
// ---- intrinsics ($-fields) ------------------------------------------------
|
|
234
|
+
const intrinsic = (row, t, name) => {
|
|
235
|
+
// `$self` = the current row: run.ts rewrites row-scoped domain functions
|
|
236
|
+
// (text/semantic/under_*/…) to `$self.fn(…)` so they arrive via callMethod
|
|
237
|
+
// with the row as receiver (a free function sees no row).
|
|
238
|
+
if (name === "$self")
|
|
239
|
+
return row;
|
|
240
|
+
if (t === "docs")
|
|
241
|
+
switch (name) {
|
|
242
|
+
case "$id": return row.doc_id;
|
|
243
|
+
case "$path": return row.path;
|
|
244
|
+
case "$content_hash": return row.file_hash == null ? null : hex(row.file_hash);
|
|
245
|
+
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;
|
|
246
|
+
case "$body": return docsRead(store, String(row.doc_id))?.content ?? null;
|
|
247
|
+
case "$title": return docProp(String(row.doc_id), "$title", "computed") ?? null;
|
|
248
|
+
case "$tags": return docProp(String(row.doc_id), "$tags", "computed") ?? null;
|
|
249
|
+
}
|
|
250
|
+
if (t === "blocks")
|
|
251
|
+
switch (name) {
|
|
252
|
+
case "$id": return row.block_id;
|
|
253
|
+
case "$doc": return row.doc_id;
|
|
254
|
+
case "$path": return row.__path;
|
|
255
|
+
case "$ordinal": return row.ordinal;
|
|
256
|
+
case "$depth": return row.depth;
|
|
257
|
+
case "$body": return row.text;
|
|
258
|
+
case "$content_hash": return row.raw_hash == null ? null : hex(row.raw_hash);
|
|
259
|
+
case "$updated_at": return scalar(`SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ?`, row.block_id) ?? null;
|
|
260
|
+
}
|
|
261
|
+
if (t === "nodes")
|
|
262
|
+
switch (name) {
|
|
263
|
+
case "$id":
|
|
264
|
+
case "$node_id": return row.node_id;
|
|
265
|
+
case "$doc_id": return row.doc_id;
|
|
266
|
+
case "$block_id": return row.block_id;
|
|
267
|
+
case "$path": return row.__path;
|
|
268
|
+
}
|
|
269
|
+
if (t === "edges")
|
|
270
|
+
switch (name) {
|
|
271
|
+
case "$id": return row.edge_id;
|
|
272
|
+
case "$src": return row.src_doc;
|
|
273
|
+
case "$dst": return row.dst_node;
|
|
274
|
+
case "$src_block": return row.src_block;
|
|
275
|
+
case "$via": return row.via_node;
|
|
276
|
+
case "$from_commit": return row.from_commit;
|
|
277
|
+
case "$path": return row.__path;
|
|
278
|
+
case "$dst_path": return scalar(`SELECT path FROM docs WHERE doc_id = ?`, row.dst_node) ?? null;
|
|
279
|
+
case "$dst_uri": return scalar(`SELECT uri FROM external_nodes WHERE node_id = ?`, row.dst_node) ?? null;
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
};
|
|
283
|
+
// ---- get ------------------------------------------------------------------
|
|
284
|
+
const getFrom = (row, key) => {
|
|
285
|
+
if (row == null)
|
|
286
|
+
return undefined;
|
|
287
|
+
// `$repo` is an intrinsic of EVERY scope — the root and any row, store-backed
|
|
288
|
+
// or plain. Bare names resolve against the current row only (oqx ≥ 0.7: no
|
|
289
|
+
// scope climbing), so a correlated subquery at any depth reaches the
|
|
290
|
+
// repository root through this local intrinsic, never by falling through
|
|
291
|
+
// to an enclosing scope.
|
|
292
|
+
if (key === "$repo")
|
|
293
|
+
return repoRoot;
|
|
294
|
+
if (row[REPO_ROOT]) {
|
|
295
|
+
if (key === "$id")
|
|
296
|
+
return repoId;
|
|
297
|
+
return rootFns[key] ? rootFns[key]() : undefined;
|
|
298
|
+
}
|
|
299
|
+
if (row[PROP_SOURCE]) {
|
|
300
|
+
const p = row;
|
|
301
|
+
return docProp(p.docId, key, p.source);
|
|
302
|
+
}
|
|
303
|
+
const t = row[TARGET];
|
|
304
|
+
if (!t) {
|
|
305
|
+
// a plain value (parsed attrs/json object, or a lifted array element): plain access.
|
|
306
|
+
return row[key];
|
|
307
|
+
}
|
|
308
|
+
const r = row;
|
|
309
|
+
if (key.startsWith("$"))
|
|
310
|
+
return intrinsic(r, t, key);
|
|
311
|
+
// self-alias namespaces: `doc.x` (docs), `block.x` (blocks), `section.x` (nodes).
|
|
312
|
+
if (t === "docs" && key === "doc")
|
|
313
|
+
return r;
|
|
314
|
+
if (t === "blocks" && key === "block")
|
|
315
|
+
return r;
|
|
316
|
+
if (t === "nodes" && key === "section")
|
|
317
|
+
return r;
|
|
318
|
+
// reach-through to owning entities.
|
|
319
|
+
if (t !== "docs" && key === "doc")
|
|
320
|
+
return owningDoc(r);
|
|
321
|
+
if (t === "nodes" && key === "block")
|
|
322
|
+
return owningBlock(r);
|
|
323
|
+
// relations.
|
|
324
|
+
const rels = rel[t];
|
|
325
|
+
if (rels[key])
|
|
326
|
+
return rels[key](r);
|
|
327
|
+
// per-target scalar fields.
|
|
328
|
+
if (t === "docs") {
|
|
329
|
+
if (key === "format")
|
|
330
|
+
return r.format;
|
|
331
|
+
if (key === "frontmatter" || key === "inline")
|
|
332
|
+
return { [PROP_SOURCE]: true, docId: String(r.doc_id), source: key };
|
|
333
|
+
if (RESERVED_DOC_BASENAMES.has(key)) {
|
|
334
|
+
throw new FilterInvalid(`bare '${key}' reads a frontmatter key; did you mean the intrinsic $${key}? (use frontmatter.${key} to force the property)`, "10 §2");
|
|
335
|
+
}
|
|
336
|
+
return docProp(String(r.doc_id), key);
|
|
337
|
+
}
|
|
338
|
+
if (t === "blocks") {
|
|
339
|
+
if (key === "type")
|
|
340
|
+
return r.type;
|
|
341
|
+
if (key === "text")
|
|
342
|
+
return r.text;
|
|
343
|
+
if (key === "attrs")
|
|
344
|
+
return parseJson(r.attrs);
|
|
345
|
+
// A bare non-structural identifier flattens into attrs: `checked` reads
|
|
346
|
+
// `attrs.checked`. Structural fields above win on collision; an absent key
|
|
347
|
+
// is undefined (silently false in a predicate), matching a doc's missing
|
|
348
|
+
// frontmatter key. The `attrs.<k>` form still works unchanged.
|
|
349
|
+
return jattr(r, key);
|
|
350
|
+
}
|
|
351
|
+
if (t === "nodes") {
|
|
352
|
+
if (key === "kind")
|
|
353
|
+
return r.kind;
|
|
354
|
+
if (key === "name")
|
|
355
|
+
return r.name;
|
|
356
|
+
if (key === "value")
|
|
357
|
+
return r.value;
|
|
358
|
+
if (key === "attrs")
|
|
359
|
+
return parseJson(r.attrs);
|
|
360
|
+
// Bare non-structural identifier flattens into attrs (see blocks above):
|
|
361
|
+
// `checked` reads `attrs.checked` on an `md:task`, `level` on an `md:section`.
|
|
362
|
+
return jattr(r, key);
|
|
363
|
+
}
|
|
364
|
+
// edges
|
|
365
|
+
if (key === "predicate" || key === "provenance" || key === "dst_kind" || key === "anchor" || key === "src_field")
|
|
366
|
+
return r[key];
|
|
367
|
+
return undefined;
|
|
368
|
+
};
|
|
369
|
+
// ---- domain functions (row-scoped, via callMethod on $self) ---------------
|
|
370
|
+
const method = (name, recv, args) => {
|
|
371
|
+
const r = recv;
|
|
372
|
+
const t = r?.[TARGET];
|
|
373
|
+
switch (name) {
|
|
374
|
+
case "text": return { handled: true, value: textMatch(t, r, String(args[0] ?? "")) };
|
|
375
|
+
case "semantic": return { handled: true, value: semanticScore(t, r, String(args[0] ?? "")) };
|
|
376
|
+
case "has_anchor": {
|
|
377
|
+
requireTarget(t, "blocks", "has_anchor");
|
|
378
|
+
return { handled: true, value: !!one(`SELECT 1 FROM edges WHERE src_block = ? AND anchor IS NOT NULL LIMIT 1`, r.block_id) };
|
|
379
|
+
}
|
|
380
|
+
case "child_count": {
|
|
381
|
+
requireTarget(t, "blocks", "child_count");
|
|
382
|
+
return { handled: true, value: scalar(`SELECT COUNT(*) FROM blocks WHERE parent_block = ? AND deleted_commit IS NULL`, r.block_id) };
|
|
383
|
+
}
|
|
384
|
+
case "parent_type": {
|
|
385
|
+
requireTarget(t, "blocks", "parent_type");
|
|
386
|
+
return { handled: true, value: scalar(`SELECT type FROM blocks WHERE block_id = ?`, r.parent_block) ?? null };
|
|
387
|
+
}
|
|
388
|
+
case "has_edge": {
|
|
389
|
+
const pred = String(args[0]);
|
|
390
|
+
const srcCol = t === "blocks" ? "src_block = ?" : "src_doc = ?";
|
|
391
|
+
const srcVal = t === "blocks" ? r.block_id : r.doc_id;
|
|
392
|
+
if (args.length >= 2)
|
|
393
|
+
return { handled: true, value: !!one(`SELECT 1 FROM edges WHERE ${srcCol} AND predicate = ? AND to_commit IS NULL AND dst_node = ? LIMIT 1`, srcVal, pred, args[1]) };
|
|
394
|
+
return { handled: true, value: !!one(`SELECT 1 FROM edges WHERE ${srcCol} AND predicate = ? AND to_commit IS NULL LIMIT 1`, srcVal, pred) };
|
|
395
|
+
}
|
|
396
|
+
case "under": {
|
|
397
|
+
requireTarget(t, "blocks", "under");
|
|
398
|
+
return { handled: true, value: underSubtree(r, String(args[0])) };
|
|
399
|
+
}
|
|
400
|
+
case "under_heading": {
|
|
401
|
+
requireTarget(t, "blocks", "under_heading");
|
|
402
|
+
return { handled: true, value: underHeading(r, String(args[0])) };
|
|
403
|
+
}
|
|
404
|
+
case "within": {
|
|
405
|
+
requireTarget(t, "blocks", "within");
|
|
406
|
+
return { handled: true, value: within(r, String(args[0])) };
|
|
407
|
+
}
|
|
408
|
+
case "under_kind": {
|
|
409
|
+
requireTarget(t, "blocks", "under_kind");
|
|
410
|
+
return { handled: true, value: underKind(r, String(args[0]), args[1]) };
|
|
411
|
+
}
|
|
412
|
+
case "yaml_path": {
|
|
413
|
+
requireTarget(t, "blocks", "yaml_path");
|
|
414
|
+
return { handled: true, value: keyPath(r, String(args[0]), "yaml") };
|
|
415
|
+
}
|
|
416
|
+
case "json_pointer": {
|
|
417
|
+
requireTarget(t, "blocks", "json_pointer");
|
|
418
|
+
return { handled: true, value: keyPath(r, String(args[0]), "json") };
|
|
419
|
+
}
|
|
420
|
+
default: return { handled: false };
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
const textMatch = (t, r, terms) => {
|
|
424
|
+
if (t === "edges")
|
|
425
|
+
throw new FilterInvalid("text(...) is not available on the edges target", "10 §5");
|
|
426
|
+
const match = sanitizeFtsQuery(terms);
|
|
427
|
+
if (match === "")
|
|
428
|
+
return false;
|
|
429
|
+
if (t === "docs")
|
|
430
|
+
return !!one(`SELECT 1 FROM blocks_fts JOIN blocks b ON b.rowid = blocks_fts.rowid WHERE b.doc_id = ? AND blocks_fts MATCH ? LIMIT 1`, r.doc_id, match);
|
|
431
|
+
if (t === "nodes")
|
|
432
|
+
return !!one(`SELECT 1 FROM nodes_fts WHERE rowid = (SELECT rowid FROM nodes WHERE node_id = ?) AND nodes_fts MATCH ? `, r.node_id, match);
|
|
433
|
+
// blocks
|
|
434
|
+
return !!one(`SELECT 1 FROM blocks_fts WHERE rowid = (SELECT rowid FROM blocks WHERE block_id = ?) AND blocks_fts MATCH ?`, r.block_id, match);
|
|
435
|
+
};
|
|
436
|
+
const semanticScore = (t, r, phrase) => {
|
|
437
|
+
if (t === "nodes" || t === "edges")
|
|
438
|
+
throw new FilterInvalid("semantic(...) is available on the docs and blocks targets", "OQX semantic");
|
|
439
|
+
const resolved = sv?.get(phrase);
|
|
440
|
+
if (!resolved)
|
|
441
|
+
throw new FilterInvalid(`semantic(${JSON.stringify(phrase)}) needs an embedding provider; none is configured for this query`, "OQX semantic");
|
|
442
|
+
const row = t === "docs"
|
|
443
|
+
? one(`SELECT vec FROM doc_embeddings WHERE doc_id = ? AND model = ?`, r.doc_id, resolved.model)
|
|
444
|
+
: one(`SELECT vec FROM embeddings WHERE content_hash = ? AND model = ? LIMIT 1`, r.raw_hash, resolved.model);
|
|
445
|
+
if (!row)
|
|
446
|
+
return null;
|
|
447
|
+
return cosineBytes(row.vec, resolved.vec);
|
|
448
|
+
};
|
|
449
|
+
const underSubtree = (r, target) => {
|
|
450
|
+
const ap = String(r.ancestor_path ?? "");
|
|
451
|
+
return ap.includes(`/${target}/`) || r.block_id === target;
|
|
452
|
+
};
|
|
453
|
+
const underHeading = (r, text) => {
|
|
454
|
+
const top = topOrdinal(r);
|
|
455
|
+
return !!one(`SELECT 1 FROM sections s JOIN blocks hb ON hb.block_id = s.heading_block
|
|
456
|
+
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);
|
|
457
|
+
};
|
|
458
|
+
const within = (r, target) => {
|
|
459
|
+
if (target.startsWith("d_"))
|
|
460
|
+
return r.doc_id === target;
|
|
461
|
+
if (target.includes("*")) {
|
|
462
|
+
const like = target.replace(/[%_]/g, "\\$&").replace(/\*/g, "%");
|
|
463
|
+
return !!one(`SELECT 1 WHERE ? LIKE ? ESCAPE '\\'`, r.__path, like);
|
|
464
|
+
}
|
|
465
|
+
return r.__path === target;
|
|
466
|
+
};
|
|
467
|
+
const underKind = (r, kind, name) => {
|
|
468
|
+
const ap = String(r.ancestor_path ?? "").split("/").filter(Boolean);
|
|
469
|
+
if (ap.length === 0)
|
|
470
|
+
return false;
|
|
471
|
+
const placeholders = ap.map(() => "?").join(",");
|
|
472
|
+
if (name != null) {
|
|
473
|
+
return !!one(`SELECT 1 FROM blocks WHERE block_id IN (${placeholders}) AND type = ? AND (lower(text) LIKE '%' || lower(?) || '%' OR json_extract(attrs,'$.key') = ?) LIMIT 1`, ...ap, kind, name, name);
|
|
474
|
+
}
|
|
475
|
+
return !!one(`SELECT 1 FROM blocks WHERE block_id IN (${placeholders}) AND type = ? LIMIT 1`, ...ap, kind);
|
|
476
|
+
};
|
|
477
|
+
const keyPath = (r, path, kind) => {
|
|
478
|
+
const key = kind === "json" ? path.replace(/^#?\/?/, "").split("/").join(".") : path;
|
|
479
|
+
const leaf = key.split(".").pop();
|
|
480
|
+
if (String(r.type ?? "").startsWith(`${kind}:`) === false)
|
|
481
|
+
return false;
|
|
482
|
+
return jattr(r, "key") === leaf || jattr(r, "key") === key;
|
|
483
|
+
};
|
|
484
|
+
return {
|
|
485
|
+
root(name) {
|
|
486
|
+
if (opts.rowsRoot && name === opts.rowsRoot.name)
|
|
487
|
+
return opts.rowsRoot.rows;
|
|
488
|
+
if (name === "$repo")
|
|
489
|
+
return repoRoot;
|
|
490
|
+
return rootFns[name] ? rootFns[name]() : undefined;
|
|
491
|
+
},
|
|
492
|
+
get: getFrom,
|
|
493
|
+
toRows(value) {
|
|
494
|
+
if (value == null)
|
|
495
|
+
return [];
|
|
496
|
+
if (Array.isArray(value))
|
|
497
|
+
return value;
|
|
498
|
+
if (typeof value === "object" && value[TARGET])
|
|
499
|
+
return [value];
|
|
500
|
+
if (typeof value === "object" && Symbol.iterator in value)
|
|
501
|
+
return value;
|
|
502
|
+
return [value];
|
|
503
|
+
},
|
|
504
|
+
identity(row) {
|
|
505
|
+
const t = row?.[TARGET];
|
|
506
|
+
if (t === "docs")
|
|
507
|
+
return row.doc_id;
|
|
508
|
+
if (t === "blocks")
|
|
509
|
+
return row.block_id;
|
|
510
|
+
if (t === "nodes")
|
|
511
|
+
return row.node_id;
|
|
512
|
+
if (t === "edges")
|
|
513
|
+
return row.edge_id;
|
|
514
|
+
return row;
|
|
515
|
+
},
|
|
516
|
+
// Free functions: oqx-js builtins (list/size/has) plus `range(s)`, which
|
|
517
|
+
// coerces a string value to a range so `<point> in range(prop)` tests
|
|
518
|
+
// coverage. range() is the explicit opt-in over the store's plain string;
|
|
519
|
+
// it delegates here (rather than only to the oqx-js builtin) so a future
|
|
520
|
+
// pushdown can serve the property's autopromoted/cached bounds — see
|
|
521
|
+
// detectRange + val_json in store/properties.ts.
|
|
522
|
+
callFunction(name, args) {
|
|
523
|
+
// `entries(frontmatter)` / `entries(inline)`: the source handle is lazy (a
|
|
524
|
+
// PropSourceRef, not a plain object), so materialize its property bag as
|
|
525
|
+
// entries here; any other argument falls through to the oqx builtin.
|
|
526
|
+
if (name === "entries" && args[0]?.[PROP_SOURCE]) {
|
|
527
|
+
const p = args[0];
|
|
528
|
+
return { handled: true, value: docPropEntries(p.docId, p.source) };
|
|
529
|
+
}
|
|
530
|
+
if (name === "range") {
|
|
531
|
+
const s = args[0];
|
|
532
|
+
if (semantics.isRange(s))
|
|
533
|
+
return { handled: true, value: s };
|
|
534
|
+
const rv = typeof s === "string" ? detectRange(s) : null;
|
|
535
|
+
return { handled: true, value: rv ? semantics.makeRange(rv.lo, rv.hi, rv.exclusiveEnd) : null };
|
|
536
|
+
}
|
|
537
|
+
const fn = semantics.BUILTIN_FUNCTIONS[name];
|
|
538
|
+
return fn ? { handled: true, value: fn(args) } : { handled: false };
|
|
539
|
+
},
|
|
540
|
+
// Domain methods first; fall back to oqx-js builtin methods
|
|
541
|
+
// (contains/startsWith/endsWith/matches/size/lower/upper).
|
|
542
|
+
callMethod(name, recv, args) {
|
|
543
|
+
const r = method(name, recv, args);
|
|
544
|
+
if (r.handled)
|
|
545
|
+
return r;
|
|
546
|
+
const fn = semantics.BUILTIN_METHODS[name];
|
|
547
|
+
return fn ? { handled: true, value: fn(recv, args) } : { handled: false };
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function requireTarget(t, want, fn) {
|
|
552
|
+
if (t !== want)
|
|
553
|
+
throw new FilterInvalid(`${fn}() is only available on the ${want} target`, "10 §5");
|
|
554
|
+
}
|
|
555
|
+
function hex(b) {
|
|
556
|
+
return Buffer.isBuffer(b) ? b.toString("hex") : String(b);
|
|
557
|
+
}
|
|
558
|
+
/** Tag SQL rows with their target so the store context resolves them (used by the
|
|
559
|
+
* tier-3 planner to hand produced rows back for residual evaluation). */
|
|
560
|
+
export function tagRows(rows, target) {
|
|
561
|
+
return tagAll(rows, target);
|
|
562
|
+
}
|
|
563
|
+
export { TARGET };
|
|
564
|
+
//# sourceMappingURL=context.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Query, QueryPlanner, Plan } from "@omgbase/oqx";
|
|
2
|
+
import type { Store } from "../core/store/store.js";
|
|
3
|
+
import { type StoreContextOptions } from "./context.js";
|
|
4
|
+
export declare class SQLiteQueryPlanner implements QueryPlanner {
|
|
5
|
+
private store;
|
|
6
|
+
private repoId;
|
|
7
|
+
private ctxOpts;
|
|
8
|
+
constructor(store: Store, repoId: string, ctxOpts?: StoreContextOptions);
|
|
9
|
+
plan(query: Query, params: readonly unknown[]): Plan | null;
|
|
10
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Tier-3 pushdown planner (ADR-013). Reduces the row set a query scans by
|
|
2
|
+
// translating its pushable top-level `where` conjuncts into ONE SQL statement
|
|
3
|
+
// against the store, then handing the produced rows (+ the untranslatable
|
|
4
|
+
// residual) back to the in-memory engine to finish. Correctness is guaranteed by
|
|
5
|
+
// the residual fallback — anything `translate.ts` declines stays in-memory — and
|
|
6
|
+
// verified by the differential conformance suite (planned == in-memory).
|
|
7
|
+
//
|
|
8
|
+
// This is deliberately conservative: only simple top-level target scans (no
|
|
9
|
+
// `follow`, no source re-projection) with at least one pushable scalar conjunct
|
|
10
|
+
// are planned; everything else declines to a full in-memory run.
|
|
11
|
+
import { ROWS_ROOT, partitionPushable, residualQuery } from "@omgbase/oqx";
|
|
12
|
+
import { makeStoreContext, tagRows } from "./context.js";
|
|
13
|
+
import { translatePredicate } from "./sql/translate.js";
|
|
14
|
+
const ALIAS = {
|
|
15
|
+
docs: { self: "d", doc: "d" },
|
|
16
|
+
blocks: { self: "b", doc: "d" },
|
|
17
|
+
nodes: { self: "n", doc: "d" },
|
|
18
|
+
edges: { self: "e", doc: "d" },
|
|
19
|
+
};
|
|
20
|
+
const FROM = {
|
|
21
|
+
docs: "docs d",
|
|
22
|
+
blocks: "blocks b JOIN docs d ON d.doc_id = b.doc_id",
|
|
23
|
+
nodes: "nodes n JOIN docs d ON d.doc_id = n.doc_id",
|
|
24
|
+
edges: "edges e JOIN docs d ON d.doc_id = e.src_doc",
|
|
25
|
+
};
|
|
26
|
+
// Row columns + the owning-doc path as __path (matches the context roots so
|
|
27
|
+
// produced rows are indistinguishable from a full scan's).
|
|
28
|
+
const COLS = {
|
|
29
|
+
docs: "d.*",
|
|
30
|
+
blocks: "b.*, d.path AS __path",
|
|
31
|
+
nodes: "n.*, d.path AS __path",
|
|
32
|
+
edges: "e.*, d.path AS __path",
|
|
33
|
+
};
|
|
34
|
+
const ORDER = {
|
|
35
|
+
docs: "d.path, d.doc_id",
|
|
36
|
+
blocks: "d.path, b.block_id",
|
|
37
|
+
nodes: "d.path, n.node_id",
|
|
38
|
+
edges: "d.path, e.edge_id",
|
|
39
|
+
};
|
|
40
|
+
function guards(target, repoId) {
|
|
41
|
+
switch (target) {
|
|
42
|
+
case "docs": return { sql: "d.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
|
|
43
|
+
case "blocks": return { sql: "b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
|
|
44
|
+
case "nodes": return { sql: "n.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
|
|
45
|
+
case "edges": return { sql: "e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// The root collection a query scans, if it is a bare `docs|blocks|nodes|edges`
|
|
49
|
+
// or `$repo.<target>` source (else null — not a pushable shape).
|
|
50
|
+
function rootTarget(source) {
|
|
51
|
+
const TARGETS = new Set(["docs", "blocks", "nodes", "edges"]);
|
|
52
|
+
if (source.kind === "ident" && TARGETS.has(source.name))
|
|
53
|
+
return source.name;
|
|
54
|
+
if (source.kind === "member" && source.recv.kind === "ident" && source.recv.name === "$repo" && TARGETS.has(source.name)) {
|
|
55
|
+
return source.name;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
export class SQLiteQueryPlanner {
|
|
60
|
+
store;
|
|
61
|
+
repoId;
|
|
62
|
+
ctxOpts;
|
|
63
|
+
constructor(store, repoId, ctxOpts = {}) {
|
|
64
|
+
this.store = store;
|
|
65
|
+
this.repoId = repoId;
|
|
66
|
+
this.ctxOpts = ctxOpts;
|
|
67
|
+
}
|
|
68
|
+
plan(query, params) {
|
|
69
|
+
// Only simple top-level scans: no follow, no `from E` re-projection.
|
|
70
|
+
if (query.follow || query.from.length > 0)
|
|
71
|
+
return null;
|
|
72
|
+
const target = rootTarget(query.source);
|
|
73
|
+
if (!target)
|
|
74
|
+
return null;
|
|
75
|
+
const ctx = { target, self: ALIAS[target].self, doc: ALIAS[target].doc, params };
|
|
76
|
+
const { pushed, residual } = partitionPushable(query.where, (e) => translatePredicate(e, ctx) !== null);
|
|
77
|
+
if (pushed.length === 0)
|
|
78
|
+
return null; // nothing to push — let the engine do it all
|
|
79
|
+
const frags = pushed.map((e) => translatePredicate(e, ctx));
|
|
80
|
+
const g = guards(target, this.repoId);
|
|
81
|
+
const whereSql = [g.sql, ...frags.map((f) => `(${f.sql})`)].join(" AND ");
|
|
82
|
+
const sqlParams = [...g.params, ...frags.flatMap((f) => f.params)];
|
|
83
|
+
const sql = `SELECT ${COLS[target]} FROM ${FROM[target]} WHERE ${whereSql} ORDER BY ${ORDER[target]}`;
|
|
84
|
+
const rows = tagRows(this.store.db.prepare(sql).all(...sqlParams), target);
|
|
85
|
+
// The residual scans ROWS_ROOT; give it a store context that resolves those
|
|
86
|
+
// produced rows (relations/intrinsics/domain fns still hit the store).
|
|
87
|
+
const context = makeStoreContext(this.store, this.repoId, { ...this.ctxOpts, rowsRoot: { name: ROWS_ROOT, rows } });
|
|
88
|
+
return { rows: () => rows, residual: residualQuery(query, residual), context };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=planner.js.map
|