@omgbase/core 0.10.0 → 0.11.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/README.md +10 -10
- package/dist/src/core/read/document.d.ts +18 -5
- package/dist/src/core/read/document.js +21 -7
- package/dist/src/core/read/nodes.d.ts +7 -7
- package/dist/src/core/read/nodes.js +5 -3
- package/dist/src/core/read/reader.d.ts +11 -7
- package/dist/src/core/read/reader.js +8 -10
- package/dist/src/format/adapter.d.ts +1 -1
- package/dist/src/graph/history.d.ts +20 -14
- package/dist/src/graph/history.js +12 -12
- package/dist/src/graph/inbound-links.d.ts +4 -4
- package/dist/src/graph/inbound-links.js +4 -4
- package/dist/src/graph/link-health.d.ts +8 -8
- package/dist/src/graph/link-health.js +6 -6
- package/dist/src/graph/links.d.ts +3 -3
- package/dist/src/graph/links.js +4 -4
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/mcp/arguments.d.ts +27 -0
- package/dist/src/mcp/arguments.js +147 -0
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/graph.d.ts +16 -9
- package/dist/src/mcp/graph.js +58 -35
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +215 -121
- package/dist/src/mcp/server.d.ts +9 -1
- package/dist/src/mcp/server.js +359 -268
- package/dist/src/mutate/apply.d.ts +11 -4
- package/dist/src/mutate/apply.js +3 -2
- package/dist/src/mutate/docs.js +7 -7
- package/dist/src/mutate/macros.d.ts +4 -1
- package/dist/src/mutate/macros.js +1 -1
- package/dist/src/mutate/ops.js +2 -2
- package/dist/src/mutate/opset.d.ts +8 -5
- package/dist/src/mutate/plan-update.js +9 -8
- package/dist/src/oqx-js/context.d.ts +17 -4
- package/dist/src/oqx-js/context.js +268 -131
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +14 -15
- package/dist/src/oqx-js/root-row.d.ts +10 -3
- package/dist/src/oqx-js/root-row.js +18 -8
- package/dist/src/oqx-js/run.d.ts +6 -0
- package/dist/src/oqx-js/run.js +71 -24
- package/dist/src/oqx-js/sql/scan.js +7 -7
- package/dist/src/oqx-js/sql/translate.d.ts +12 -9
- package/dist/src/oqx-js/sql/translate.js +183 -75
- package/dist/src/oqx-js/store-index.d.ts +1 -1
- package/dist/src/oqx-js/store-index.js +28 -26
- package/dist/src/search/cel/ast.d.ts +1 -1
- package/dist/src/search/cel/compile.d.ts +1 -1
- package/dist/src/search/cel/compile.js +43 -70
- package/dist/src/search/cel/parser.js +1 -1
- package/dist/src/search/resolve.d.ts +2 -0
- package/dist/src/search/rrf.js +2 -2
- package/dist/src/search/text.d.ts +2 -2
- package/dist/src/search/text.js +1 -1
- package/dist/src/spec-versions.d.ts +6 -6
- package/dist/src/spec-versions.js +5 -5
- package/dist/src/surface-paths.d.ts +41 -8
- package/dist/src/surface-paths.js +34 -18
- package/dist/src/sync/admin.d.ts +2 -1
- package/dist/src/sync/admin.js +1 -1
- package/package.json +2 -2
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
//
|
|
14
14
|
// • oqx-js string ops are CASE-SENSITIVE, but SQLite `LIKE` is case-insensitive
|
|
15
15
|
// for ASCII. So `startsWith`/`contains`/`endsWith` translate to
|
|
16
|
-
// `substr`/`instr`, NEVER `LIKE`.
|
|
16
|
+
// `substr`/`instr`, NEVER `LIKE`. `path.lower().startsWith("lab/")` becomes
|
|
17
17
|
// `substr(lower(d.path), 1, length(?)) = ?` — the lowercasing is explicit and
|
|
18
18
|
// the match is byte-exact, matching the in-memory path.
|
|
19
19
|
// • oqx-js `==`/`!=` are absence-NORMALIZED (two absent values are equal;
|
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
// `card='scalar'` row (reads NULL). So a comparison is only pushed when the
|
|
28
28
|
// KINDS of its two operands are provably compared the same way — see
|
|
29
29
|
// `comparable` (the spec/surface §1 decline (a)) — and a name that is a relation, reach-through
|
|
30
|
-
// or source handle is never read as a property (decline (b),
|
|
31
|
-
// `NON_PROPERTY_NAMES
|
|
30
|
+
// or WHOLE source handle is never read as a property (decline (b),
|
|
31
|
+
// `NON_PROPERTY_NAMES`; a key read THROUGH a handle — `data.k`,
|
|
32
|
+
// `frontmatter.k` — is a property read, `handleRead`). A bool or num literal against a JSON or property
|
|
32
33
|
// read is a TYPED push (the 1.2 patch): the stored type is tested in SQL
|
|
33
34
|
// before the value (`json_type(x) = 'true'`, `p.type = 'number' AND
|
|
34
35
|
// p.val_num >= ?`) and the whole test is wrapped `(…) IS 1` / `IS NOT 1`,
|
|
@@ -52,94 +53,137 @@ import { TARGETS } from "../root-row.js";
|
|
|
52
53
|
* `translate.test.ts` asserts this set against the store context.
|
|
53
54
|
*/
|
|
54
55
|
export const NON_PROPERTY_NAMES = {
|
|
55
|
-
docs: new Set(["doc", "nodes", "blocks", "
|
|
56
|
-
blocks: new Set(["block", "doc", "children", "nodes", "
|
|
56
|
+
docs: new Set(["doc", "nodes", "blocks", "out_links", "in_links", "data", "frontmatter", "inline"]),
|
|
57
|
+
blocks: new Set(["block", "doc", "children", "nodes", "out_links", "section", "attrs"]),
|
|
57
58
|
nodes: new Set(["section", "doc", "block", "blocks", "subsections", "children", "attrs"]),
|
|
58
|
-
|
|
59
|
+
links: new Set(["doc", "dst"]),
|
|
59
60
|
};
|
|
60
|
-
// ----
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
61
|
+
// ---- engine attributes ------------------------------------------------------
|
|
62
|
+
// The engine attributes (spec/surface §1.2, 3.0: bare, snake_case, winning on
|
|
63
|
+
// collision — `id`, `path`, `raw`, `hash`, `updated_at` on docs and blocks,
|
|
64
|
+
// `title`/`tags` on docs, `id`/`path` on nodes and edges) → a param-free SQL
|
|
65
|
+
// scalar, per target, with its kind. The structural columns and foreign keys are
|
|
66
|
+
// translated in `translateOperand`. Anything not mapped (`raw`, reconstructed or
|
|
67
|
+
// a blob; `title`, computed; `tags`, the union of the authored list and the
|
|
68
|
+
// computed hashtags) returns null → residual, and the caller must NOT fall
|
|
69
|
+
// through to a property read of the same name (the attribute wins in memory).
|
|
70
|
+
// `updated_at` is a correlated subquery. `path` (and the edge field `dst_path`)
|
|
71
|
+
// is the REFERENCE form on the surface (spec/surface §1 "Paths", 2.0) — `'/' ||
|
|
72
|
+
// <storage column>` in SQL — so every comparison, `startsWith`, `contains`,
|
|
73
|
+
// `lower()`… sees exactly the string the in-memory attribute yields;
|
|
74
|
+
// `pathColumn` keeps the bare indexed column at hand for the equality fast path.
|
|
69
75
|
/** The rooted path read over a storage path column. */
|
|
70
76
|
export function pathColumn(col) {
|
|
71
77
|
return { sql: `('/' || ${col})`, params: [], kind: "text", pathCol: col };
|
|
72
78
|
}
|
|
73
|
-
|
|
79
|
+
/** The identity/bytes/time attributes per kind — the names `attributeSql` owns
|
|
80
|
+
* (the structural columns are handled per target in `translateOperand`). */
|
|
81
|
+
export const ROW_ATTRIBUTES = {
|
|
82
|
+
docs: new Set(["id", "path", "raw", "hash", "updated_at", "title", "tags"]),
|
|
83
|
+
blocks: new Set(["id", "path", "raw", "hash", "updated_at"]),
|
|
84
|
+
nodes: new Set(["id", "path"]),
|
|
85
|
+
links: new Set(["id", "path"]),
|
|
86
|
+
};
|
|
87
|
+
function attributeSql(name, ctx) {
|
|
74
88
|
const { self, doc, target } = ctx;
|
|
75
89
|
const text = (sql) => ({ sql, params: [], kind: "text" });
|
|
76
|
-
const int = (sql) => ({ sql, params: [], kind: "int" });
|
|
77
90
|
if (target === "docs")
|
|
78
91
|
switch (name) {
|
|
79
|
-
case "
|
|
80
|
-
case "
|
|
81
|
-
case "
|
|
82
|
-
case "
|
|
92
|
+
case "id": return text(`${self}.doc_id`);
|
|
93
|
+
case "path": return pathColumn(`${doc}.path`);
|
|
94
|
+
case "hash": return text(`lower(hex(${self}.file_hash))`);
|
|
95
|
+
case "updated_at": return text(`(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ${self}.current_rev)`);
|
|
83
96
|
}
|
|
84
97
|
else if (target === "blocks")
|
|
85
98
|
switch (name) {
|
|
86
|
-
case "
|
|
87
|
-
case "
|
|
88
|
-
case "
|
|
89
|
-
case "$ordinal": return int(`${self}.ordinal`);
|
|
90
|
-
case "$depth": return int(`${self}.depth`);
|
|
91
|
-
case "$body": return text(`${self}.text`);
|
|
92
|
-
case "$content_hash": return text(`lower(hex(${self}.raw_hash))`);
|
|
99
|
+
case "id": return text(`${self}.block_id`);
|
|
100
|
+
case "path": return pathColumn(`${doc}.path`);
|
|
101
|
+
case "hash": return text(`lower(hex(${self}.raw_hash))`);
|
|
93
102
|
}
|
|
94
103
|
else if (target === "nodes")
|
|
95
104
|
switch (name) {
|
|
96
|
-
case "
|
|
97
|
-
case "
|
|
98
|
-
case "$doc_id": return text(`${self}.doc_id`);
|
|
99
|
-
case "$block_id": return text(`${self}.block_id`);
|
|
100
|
-
case "$path": return pathColumn(`${doc}.path`);
|
|
105
|
+
case "id": return text(`${self}.node_id`);
|
|
106
|
+
case "path": return pathColumn(`${doc}.path`);
|
|
101
107
|
}
|
|
102
|
-
else if (target === "
|
|
108
|
+
else if (target === "links")
|
|
103
109
|
switch (name) {
|
|
104
|
-
case "
|
|
105
|
-
case "
|
|
106
|
-
case "$dst": return text(`${self}.dst_node`);
|
|
107
|
-
case "$src_block": return text(`${self}.src_block`);
|
|
108
|
-
case "$via": return text(`${self}.via_node`);
|
|
109
|
-
case "$from_commit": return text(`${self}.from_commit`);
|
|
110
|
-
case "$path": return pathColumn(`${doc}.path`);
|
|
111
|
-
case "$dst_path": return text(`(SELECT '/' || dd.path FROM docs dd WHERE dd.doc_id = ${self}.dst_node)`);
|
|
112
|
-
case "$dst_uri": return text(`(SELECT xn.uri FROM external_nodes xn WHERE xn.node_id = ${self}.dst_node)`);
|
|
110
|
+
case "id": return text(`${self}.edge_id`);
|
|
111
|
+
case "path": return pathColumn(`${doc}.path`);
|
|
113
112
|
}
|
|
114
113
|
return null;
|
|
115
114
|
}
|
|
116
|
-
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
115
|
+
/** The links target's plain fields → the store's columns (spec/surface §1.2,
|
|
116
|
+
* 3.0: a foreign key is named for what it is, never `$`-prefixed). `dst_path`
|
|
117
|
+
* and `uri` are correlated subqueries; `dst_path` is the reference form. */
|
|
118
|
+
function edgeField(name, self) {
|
|
119
|
+
const text = (sql) => ({ sql, params: [], kind: "text" });
|
|
120
|
+
switch (name) {
|
|
121
|
+
case "doc_id": return text(`${self}.src_doc`);
|
|
122
|
+
case "block_id": return text(`${self}.src_block`);
|
|
123
|
+
case "via_id": return text(`${self}.via_node`);
|
|
124
|
+
case "dst_id": return text(`${self}.dst_node`);
|
|
125
|
+
case "dst_path": return text(`(SELECT '/' || dd.path FROM docs dd WHERE dd.doc_id = ${self}.dst_node)`);
|
|
126
|
+
case "uri": return text(`(SELECT xn.uri FROM external_nodes xn WHERE xn.node_id = ${self}.dst_node)`);
|
|
127
|
+
case "field": return text(`${self}.src_field`);
|
|
128
|
+
case "predicate":
|
|
129
|
+
case "provenance":
|
|
130
|
+
case "dst_kind":
|
|
131
|
+
case "anchor": return text(`${self}.${name}`);
|
|
132
|
+
default: return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
120
135
|
const SEG = /^[A-Za-z_][A-Za-z0-9_]*$/; // injection-safe inlined identifier
|
|
121
136
|
// The single-scalar-row scope of a document property (the CEL scalar-in-scope
|
|
122
137
|
// rule): the row for `key` on the document only when the key has exactly one
|
|
123
138
|
// row in scope and it is card='scalar' — a list-valued or nested key has none.
|
|
124
|
-
// `select` is the projected expression over the alias `p`.
|
|
125
|
-
|
|
139
|
+
// `select` is the projected expression over the alias `p`. `source` narrows
|
|
140
|
+
// the scope to one authored source — the read `frontmatter.<k>` / `inline.<k>`
|
|
141
|
+
// performs (spec/surface §1.2; the in-memory `docProp(docId, key, source)`):
|
|
142
|
+
// both the row and the count are of that source's rows only. The merged
|
|
143
|
+
// `data.<k>` read is the bare read, so it passes no source. `docId` is the SQL
|
|
144
|
+
// expression naming the document: the scan's `d.doc_id`, or a link's
|
|
145
|
+
// `e.dst_node` for a `dst.<k>` read (the destination's rows by its id — a
|
|
146
|
+
// non-document or phantom destination has none, so the read is NULL exactly
|
|
147
|
+
// where `dst` is absent in memory).
|
|
148
|
+
function propSubquery(docId, key, select, source) {
|
|
149
|
+
const src = source ? ` AND p.source = '${source}'` : "";
|
|
150
|
+
const src2 = source ? ` AND p2.source = '${source}'` : "";
|
|
126
151
|
return `(SELECT ${select} FROM properties p
|
|
127
|
-
WHERE p.doc_id = ${
|
|
128
|
-
AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = ${
|
|
152
|
+
WHERE p.doc_id = ${docId} AND p.key = '${key}' AND p.card = 'scalar' AND p.deleted_commit IS NULL${src}
|
|
153
|
+
AND (SELECT COUNT(*) FROM properties p2 WHERE p2.doc_id = ${docId} AND p2.key = '${key}' AND p2.deleted_commit IS NULL${src2}) = 1
|
|
129
154
|
LIMIT 1)`;
|
|
130
155
|
}
|
|
131
156
|
// A single-valued document property: the scalar value of its one row in scope,
|
|
132
157
|
// else NULL — matching the store context's `docProp` for a scalar read.
|
|
133
|
-
|
|
158
|
+
// `docAlias` is the document's table alias; `propScalarOf` takes the id
|
|
159
|
+
// expression itself.
|
|
160
|
+
function propScalar(docAlias, key, source) {
|
|
161
|
+
return propScalarOf(`${docAlias}.doc_id`, key, source);
|
|
162
|
+
}
|
|
163
|
+
function propScalarOf(docId, key, source) {
|
|
134
164
|
if (!SEG.test(key))
|
|
135
165
|
return null;
|
|
136
166
|
return {
|
|
137
|
-
sql: propSubquery(
|
|
167
|
+
sql: propSubquery(docId, key, "COALESCE(p.val_text, p.val_num, p.val_bool)", source),
|
|
138
168
|
params: [],
|
|
139
169
|
kind: "prop",
|
|
140
|
-
prop: {
|
|
170
|
+
prop: source ? { docId, key, source } : { docId, key },
|
|
141
171
|
};
|
|
142
172
|
}
|
|
173
|
+
// A key read through a source handle (spec/surface §1.2, 3.0): `data.<k>` is
|
|
174
|
+
// the merged authored read — exactly the bare property `<k>`; `frontmatter.<k>`
|
|
175
|
+
// / `inline.<k>` the same read scoped to one source. Pushed since 3.0 (batch 3;
|
|
176
|
+
// before, every handle member was residual). A whole-handle reference
|
|
177
|
+
// (`frontmatter == null`) and `entries(<handle>)` still decline.
|
|
178
|
+
const SOURCE_HANDLES = { data: undefined, frontmatter: "frontmatter", inline: "inline" };
|
|
179
|
+
function handleRead(docAlias, handle, key) {
|
|
180
|
+
return handleReadOf(`${docAlias}.doc_id`, handle, key);
|
|
181
|
+
}
|
|
182
|
+
function handleReadOf(docId, handle, key) {
|
|
183
|
+
if (!(handle in SOURCE_HANDLES))
|
|
184
|
+
return null;
|
|
185
|
+
return propScalarOf(docId, key, SOURCE_HANDLES[handle]);
|
|
186
|
+
}
|
|
143
187
|
// json_extract path from validated segments, or null if any segment is unsafe.
|
|
144
188
|
function jsonExtract(col, segs) {
|
|
145
189
|
if (segs.some((s) => !SEG.test(s)))
|
|
@@ -184,8 +228,14 @@ export function translateOperand(e, ctx) {
|
|
|
184
228
|
case "binding":
|
|
185
229
|
return constOperand(ctx.params[e.index]);
|
|
186
230
|
case "ident": {
|
|
231
|
+
// An OQX `$` name (`$it`, `$key`, the follow intrinsics) is the language's,
|
|
232
|
+
// never a column; omgbase adds no `$` names (spec/surface §1.2, 3.0).
|
|
187
233
|
if (e.name.startsWith("$"))
|
|
188
|
-
return
|
|
234
|
+
return null;
|
|
235
|
+
// The row's own attributes win on collision (§1.2): translated or declined
|
|
236
|
+
// (`raw`, `title`, `tags`), never read as a property or an attr.
|
|
237
|
+
if (ROW_ATTRIBUTES[target].has(e.name))
|
|
238
|
+
return attributeSql(e.name, ctx);
|
|
189
239
|
// A relation, reach-through, source handle or the whole attrs object is not a
|
|
190
240
|
// scalar read (decline (b)); a bare target name is a relation or a loud
|
|
191
241
|
// error (root-row.ts), never a property.
|
|
@@ -195,28 +245,25 @@ export function translateOperand(e, ctx) {
|
|
|
195
245
|
if (target === "docs") {
|
|
196
246
|
if (e.name === "format")
|
|
197
247
|
return text(`${self}.format`); // a column, not a property
|
|
198
|
-
if (RESERVED_DOC_BASENAMES.has(e.name))
|
|
199
|
-
return null; // residual raises the guard
|
|
200
248
|
return propScalar(doc, e.name);
|
|
201
249
|
}
|
|
202
250
|
if (target === "blocks") {
|
|
203
|
-
if (e.name === "type" || e.name === "text")
|
|
251
|
+
if (e.name === "doc_id" || e.name === "type" || e.name === "text")
|
|
204
252
|
return text(`${self}.${e.name}`);
|
|
253
|
+
if (e.name === "ordinal" || e.name === "depth")
|
|
254
|
+
return { sql: `${self}.${e.name}`, params: [], kind: "int" };
|
|
205
255
|
// Bare non-structural identifier flattens into attrs — same pushdown as
|
|
206
256
|
// the `attrs.<k>` member form (json_extract), so `checked` == `attrs.checked`.
|
|
207
257
|
return jsonExtract(`${self}.attrs`, [e.name]);
|
|
208
258
|
}
|
|
209
259
|
if (target === "nodes") {
|
|
210
|
-
if (
|
|
260
|
+
if (["doc_id", "block_id", "kind", "name", "value"].includes(e.name))
|
|
211
261
|
return text(`${self}.${e.name}`);
|
|
212
262
|
// Bare non-structural identifier flattens into attrs (see blocks above).
|
|
213
263
|
return jsonExtract(`${self}.attrs`, [e.name]);
|
|
214
264
|
}
|
|
215
|
-
if (target === "
|
|
216
|
-
|
|
217
|
-
return text(`${self}.${e.name}`);
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
265
|
+
if (target === "links")
|
|
266
|
+
return edgeField(e.name, self);
|
|
220
267
|
return null;
|
|
221
268
|
}
|
|
222
269
|
case "member": {
|
|
@@ -228,27 +275,88 @@ export function translateOperand(e, ctx) {
|
|
|
228
275
|
if (head === "attrs" && (target === "blocks" || target === "nodes")) {
|
|
229
276
|
return jsonExtract(`${self}.attrs`, rest);
|
|
230
277
|
}
|
|
278
|
+
// <handle>.<k> on docs — `data.layer`, `frontmatter.layer`, `inline.element`
|
|
279
|
+
// — and `doc.<handle>.<k>` through the owning document on the other
|
|
280
|
+
// targets: a property read, scoped to the handle's source.
|
|
281
|
+
if (target === "docs" && head in SOURCE_HANDLES && rest.length === 1)
|
|
282
|
+
return handleRead(doc, head, rest[0]);
|
|
283
|
+
if (head === "doc" && rest.length === 2 && rest[0] in SOURCE_HANDLES)
|
|
284
|
+
return handleRead(doc, rest[0], rest[1]);
|
|
231
285
|
// doc.<x> reach-through — the owning doc (alias `doc`, which is `d`). On the
|
|
232
286
|
// docs target `doc` is the row itself; either way it resolves against `doc`.
|
|
287
|
+
// FOREIGN-KEY ELIMINATION (spec/surface §1, 3.0): `doc.id` is the row's
|
|
288
|
+
// own FK column (`b.doc_id`, `n.doc_id`, `e.src_doc`) and `doc.path` /
|
|
289
|
+
// `doc.hash` / `doc.updated_at` the scan's already-joined document
|
|
290
|
+
// columns — no extra join, the same SQL as the bare `doc_id`.
|
|
233
291
|
if (head === "doc") {
|
|
234
292
|
if (rest.length !== 1)
|
|
235
293
|
return null;
|
|
236
294
|
const k = rest[0];
|
|
237
|
-
if (k === "
|
|
295
|
+
if (k === "id")
|
|
296
|
+
return text(target === "links" ? `${self}.src_doc` : `${self}.doc_id`);
|
|
297
|
+
if (k === "path")
|
|
238
298
|
return pathColumn(`${doc}.path`);
|
|
299
|
+
if (k === "hash")
|
|
300
|
+
return text(`lower(hex(${doc}.file_hash))`);
|
|
301
|
+
if (k === "updated_at")
|
|
302
|
+
return text(`(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ${doc}.current_rev)`);
|
|
239
303
|
if (k === "format")
|
|
240
304
|
return text(`${doc}.format`);
|
|
241
305
|
if (k.startsWith("$"))
|
|
242
306
|
return null;
|
|
243
|
-
if (
|
|
244
|
-
return null;
|
|
307
|
+
if (ROW_ATTRIBUTES.docs.has(k))
|
|
308
|
+
return null; // `doc.raw`, `doc.title`, `doc.tags`: the attribute, not pushable
|
|
245
309
|
if (NON_PROPERTY_NAMES.docs.has(k) || TARGETS.has(k))
|
|
246
|
-
return null; // `doc.nodes`, `doc.
|
|
310
|
+
return null; // `doc.nodes`, `doc.data`, `doc.docs` … (decline (b))
|
|
247
311
|
return propScalar(doc, k);
|
|
248
312
|
}
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
313
|
+
// dst.<x> reach-through from a link (spec/surface §1.2, 3.0 follow-up): the
|
|
314
|
+
// destination DOCUMENT, keyed by the row's own `dst_node` — the lookup
|
|
315
|
+
// `dst_path` already runs. A property read scopes `properties` by
|
|
316
|
+
// `e.dst_node` directly; the document's own columns are a primary-key
|
|
317
|
+
// probe `(SELECT … FROM docs dd WHERE dd.doc_id = e.dst_node)` — NULL
|
|
318
|
+
// wherever `dst` is absent in memory (a block or external destination, a
|
|
319
|
+
// phantom). `dst.raw` / `dst.title` / `dst.tags` and the relations stay
|
|
320
|
+
// residual, as `doc.*`'s do.
|
|
321
|
+
if (head === "dst" && target === "links") {
|
|
322
|
+
const dstId = `${self}.dst_node`;
|
|
323
|
+
if (rest.length === 2 && rest[0] in SOURCE_HANDLES)
|
|
324
|
+
return handleReadOf(dstId, rest[0], rest[1]);
|
|
325
|
+
if (rest.length !== 1)
|
|
326
|
+
return null;
|
|
327
|
+
const k = rest[0];
|
|
328
|
+
const probe = (col) => text(`(SELECT ${col} FROM docs dd WHERE dd.doc_id = ${dstId})`);
|
|
329
|
+
if (k === "id")
|
|
330
|
+
return probe("dd.doc_id");
|
|
331
|
+
if (k === "path")
|
|
332
|
+
return probe("'/' || dd.path");
|
|
333
|
+
if (k === "hash")
|
|
334
|
+
return probe("lower(hex(dd.file_hash))");
|
|
335
|
+
if (k === "updated_at")
|
|
336
|
+
return probe("(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = dd.current_rev)");
|
|
337
|
+
if (k === "format")
|
|
338
|
+
return probe("dd.format");
|
|
339
|
+
if (k.startsWith("$"))
|
|
340
|
+
return null;
|
|
341
|
+
if (ROW_ATTRIBUTES.docs.has(k) || NON_PROPERTY_NAMES.docs.has(k) || TARGETS.has(k))
|
|
342
|
+
return null;
|
|
343
|
+
return propScalarOf(dstId, k);
|
|
344
|
+
}
|
|
345
|
+
// block.<x> reach-through from a node: the block's identity is the node's
|
|
346
|
+
// FK (`block.id` → `n.block_id`, `block.path` → the joined `d.path`,
|
|
347
|
+
// `block.doc_id` → `n.doc_id` — FK elimination again); `type`/`text` are
|
|
348
|
+
// a correlated read of the block row.
|
|
349
|
+
if (head === "block" && target === "nodes" && rest.length === 1) {
|
|
350
|
+
const k = rest[0];
|
|
351
|
+
if (k === "id")
|
|
352
|
+
return text(`${self}.block_id`);
|
|
353
|
+
if (k === "path")
|
|
354
|
+
return pathColumn(`${doc}.path`);
|
|
355
|
+
if (k === "doc_id")
|
|
356
|
+
return text(`${self}.doc_id`);
|
|
357
|
+
if (k === "type" || k === "text")
|
|
358
|
+
return text(`(SELECT bb.${k} FROM blocks bb WHERE bb.block_id = ${self}.block_id)`);
|
|
359
|
+
return null;
|
|
252
360
|
}
|
|
253
361
|
return null;
|
|
254
362
|
}
|
|
@@ -305,7 +413,7 @@ const IS_OP = {
|
|
|
305
413
|
* ordered, exactly as in memory.
|
|
306
414
|
* Every other pair declines: SQLite sees JSON `true` and `1`, `val_bool` and
|
|
307
415
|
* `val_num` alike (`$ordinal == checked`, `true == 1`), orders every
|
|
308
|
-
* integer before every text (`$ordinal < "3"`, `level < "x"`,
|
|
416
|
+
* integer before every text (`$ordinal < "3"`, `level < "x"`, `path > 5`),
|
|
309
417
|
* and two JSON or property reads carry no type at plan time
|
|
310
418
|
* (`attrs.a == attrs.b`, `era == stages`).
|
|
311
419
|
*
|
|
@@ -402,7 +510,7 @@ function translateComparison(e, ctx) {
|
|
|
402
510
|
}
|
|
403
511
|
const FLIP = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" };
|
|
404
512
|
/**
|
|
405
|
-
* The path equality fast path:
|
|
513
|
+
* The path equality fast path: `path == "/a.md"` (or `!=`, either side) against
|
|
406
514
|
* a rooted text constant is `d.path IS ?` with the constant's storage form, so
|
|
407
515
|
* the `(repo_id, path)` index serves it; `'/' || d.path IS ?` — what the general
|
|
408
516
|
* form would emit — is the same predicate without the index. Only a constant
|
|
@@ -455,9 +563,9 @@ function typedComparison(op, read, constant) {
|
|
|
455
563
|
};
|
|
456
564
|
}
|
|
457
565
|
if (read.prop) {
|
|
458
|
-
const {
|
|
566
|
+
const { docId, key, source } = read.prop;
|
|
459
567
|
const test = constant.kind === "bool" ? `p.type = 'bool' AND p.val_bool = ?` : `p.type = 'number' AND p.val_num ${cmp} ?`;
|
|
460
|
-
return { sql: `(${propSubquery(
|
|
568
|
+
return { sql: `(${propSubquery(docId, key, test, source)} ${wrap})`, params: [...constant.params] };
|
|
461
569
|
}
|
|
462
570
|
return null;
|
|
463
571
|
}
|
|
@@ -14,7 +14,7 @@ export interface StoreIndexDeps {
|
|
|
14
14
|
}
|
|
15
15
|
/** A store-backed `RowIndex` for the root scan of `target` on `path`, or
|
|
16
16
|
* `undefined` when SQLite has no index for the path (per-target table above;
|
|
17
|
-
* multi-segment paths; `format`, edge `predicate`/`
|
|
17
|
+
* multi-segment paths; `format`, edge `predicate`/`field`, `tags`, …). */
|
|
18
18
|
export declare function storeIndexFor(target: Target, path: readonly string[], deps: StoreIndexDeps): RowIndex | undefined;
|
|
19
19
|
/** The paths `storeIndexFor` answers from SQLite, per target (for tests and docs). */
|
|
20
20
|
export declare function indexablePaths(target: Target): readonly string[];
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Store-backed equality indexes for the OQX engine (`DataContext.indexFor`):
|
|
2
|
-
// when a nested block's receiver is a ROOT SCAN (`^docs`, `^
|
|
2
|
+
// when a nested block's receiver is a ROOT SCAN (`^docs`, `^links`,
|
|
3
3
|
// a bare `docs` at the root scope) and its correlated or constant equality is on
|
|
4
|
-
// a column or property SQLite can look up — `where
|
|
5
|
-
// `where customer ==
|
|
4
|
+
// a column or property SQLite can look up — `where path == ^^path`,
|
|
5
|
+
// `where customer == ^path`, `where dst_id == ^id`, `where type == "order"` —
|
|
6
6
|
// the probe runs ONE prepared statement against the store's indexes instead of
|
|
7
7
|
// materializing every row of the target and hashing it. The rows come back
|
|
8
8
|
// exactly as the root scan constructs them (same columns, same `__path`, same
|
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Typed equality (OQX SEMANTICS §5) is preserved by probing per VALUE KIND:
|
|
14
14
|
//
|
|
15
|
-
// • a text column (`
|
|
15
|
+
// • a text column (`id`, `path`, `type`, `kind`, `name`, `doc_id`, `dst_id`, …)
|
|
16
16
|
// matches a string probe by `= ?` (TEXT = TEXT, exact); a number, boolean,
|
|
17
17
|
// object or any other non-string probe matches NO row (the column holds
|
|
18
18
|
// strings or NULL and `5 == "5"` is false); an absent probe (`null`,
|
|
19
19
|
// `undefined`: `name == null`) is answered by the in-memory fallback below;
|
|
20
|
-
// • a document property (`customer`,
|
|
20
|
+
// • a document property (`customer`, `title`) matches a string probe only on
|
|
21
21
|
// a `type = 'string'` row (`val_text = ?`), a number probe only on a
|
|
22
22
|
// `type = 'number'` row (`val_num = ?`), a boolean only on a `type = 'bool'`
|
|
23
23
|
// row (`val_bool = ?`), each under the scalar-in-scope rule the in-memory
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// contract but the engine never calls it for these indexes: it probes
|
|
39
39
|
// `lookupRows` first and so never reads the root whole.
|
|
40
40
|
import { semantics } from "@omgbase/oqx";
|
|
41
|
-
import { NON_PROPERTY_NAMES,
|
|
41
|
+
import { NON_PROPERTY_NAMES, ROW_ATTRIBUTES } from "./sql/translate.js";
|
|
42
42
|
import { COLS, FROM, FROM_BY_DOC, ORDER, guards, guardsByDoc } from "./sql/scan.js";
|
|
43
43
|
import { storagePath } from "../core/paths.js";
|
|
44
44
|
// ---- which paths SQLite answers ------------------------------------------------
|
|
@@ -49,19 +49,19 @@ import { storagePath } from "../core/paths.js";
|
|
|
49
49
|
* `block_id` (PK), `idx_blocks_doc (doc_id, …)`, `idx_blocks_type (repo_id,
|
|
50
50
|
* type)`; nodes `node_id` (PK), `idx_nodes_doc`, `idx_nodes_kind`,
|
|
51
51
|
* `idx_nodes_name`; edges `edge_id` (PK), `idx_edges_src (src_doc, …)`,
|
|
52
|
-
* `idx_edges_dst (dst_node, …)`;
|
|
52
|
+
* `idx_edges_dst (dst_node, …)`; `path` on a joined target reaches the docs
|
|
53
53
|
* `(repo_id, path)` key and then the target's `doc_id`/`src_doc` index. */
|
|
54
54
|
const COLUMN_PROBES = {
|
|
55
|
-
docs: {
|
|
56
|
-
blocks: {
|
|
57
|
-
nodes: {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
55
|
+
docs: { id: "d.doc_id = ?", path: "d.path = ?" },
|
|
56
|
+
blocks: { id: "b.block_id = ?", doc_id: "b.doc_id = ?", type: "b.type = ?", path: "d.path = ?" },
|
|
57
|
+
nodes: { id: "n.node_id = ?", doc_id: "n.doc_id = ?", kind: "n.kind = ?", name: "n.name = ?", path: "d.path = ?" },
|
|
58
|
+
links: {
|
|
59
|
+
id: "e.edge_id = ?", doc_id: "e.src_doc = ?", dst_id: "e.dst_node = ?", path: "d.path = ?",
|
|
60
|
+
// `dst_path` is the destination document's path (deleted or not — the
|
|
61
|
+
// field reads `docs` by id without a liveness guard). A destination is
|
|
62
62
|
// resolved within the edge's repo (spec/graph §3), where `(repo_id, path)`
|
|
63
63
|
// is UNIQUE, so the probe is that one document; `?r` is the repo id.
|
|
64
|
-
|
|
64
|
+
dst_path: "e.dst_node IN (SELECT doc_id FROM docs WHERE repo_id = ?r AND path = ?)",
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
67
|
/** How a one-segment path on `target` is probed, or null when SQLite has no
|
|
@@ -76,14 +76,16 @@ function probeFor(target, path) {
|
|
|
76
76
|
if (target !== "docs")
|
|
77
77
|
return null;
|
|
78
78
|
// A docs property: any bare name the context reads through `docProp` — not a
|
|
79
|
-
// relation / handle (`
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
79
|
+
// relation / handle (`out_links`, `data`, `frontmatter`, …), not the `format` column
|
|
80
|
+
// (not indexed), not an OQX `$` name, and of the engine attributes only
|
|
81
|
+
// `title` (the computed `$title` row, a scalar property the author's `title:`
|
|
82
|
+
// never reaches — `data.title` does): `raw`, `hash`, `updated_at` and `tags`
|
|
83
|
+
// (authored ∪ hashtags) have no index and are never property probes.
|
|
84
|
+
if (key === "title")
|
|
83
85
|
return { kind: "property", key: "$title", source: "computed" };
|
|
84
86
|
if (key.startsWith("$") || key === "format")
|
|
85
87
|
return null;
|
|
86
|
-
if (NON_PROPERTY_NAMES.docs.has(key) ||
|
|
88
|
+
if (NON_PROPERTY_NAMES.docs.has(key) || ROW_ATTRIBUTES.docs.has(key))
|
|
87
89
|
return null;
|
|
88
90
|
return { kind: "property", key, source: null };
|
|
89
91
|
}
|
|
@@ -108,10 +110,10 @@ class StoreIndex {
|
|
|
108
110
|
if (this.probe.kind === "column") {
|
|
109
111
|
if (typeof v !== "string")
|
|
110
112
|
return [];
|
|
111
|
-
//
|
|
113
|
+
// `path` / `dst_path` are the reference form in memory (`/a.md`,
|
|
112
114
|
// spec/surface §1 "Paths"); the column holds the storage form. A rooted
|
|
113
115
|
// probe is de-rooted; a bare one can equal no rooted path — no row.
|
|
114
|
-
if (this.path === "
|
|
116
|
+
if (this.path === "path" || this.path === "dst_path")
|
|
115
117
|
return v.startsWith("/") ? this.column(storagePath(v)) : [];
|
|
116
118
|
return this.column(v);
|
|
117
119
|
}
|
|
@@ -135,13 +137,13 @@ class StoreIndex {
|
|
|
135
137
|
}
|
|
136
138
|
return out.sort((a, b) => a - b);
|
|
137
139
|
}
|
|
138
|
-
// A
|
|
140
|
+
// A `path` probe on a joined target drives from the document (`FROM_BY_DOC`),
|
|
139
141
|
// every other column probe from the target's own index. `?r` in a probe is
|
|
140
142
|
// the repo id, bound ahead of the value.
|
|
141
143
|
column(value) {
|
|
142
144
|
const t = this.target;
|
|
143
145
|
const probe = this.probe.sql;
|
|
144
|
-
const byDoc = t !== "docs" && this.path === "
|
|
146
|
+
const byDoc = t !== "docs" && this.path === "path";
|
|
145
147
|
const g = byDoc ? guardsByDoc(t, this.deps.repoId) : guards(t, this.deps.repoId);
|
|
146
148
|
const from = byDoc ? FROM_BY_DOC[t] : FROM[t];
|
|
147
149
|
const params = probe.includes("?r") ? [this.deps.repoId, value] : [value];
|
|
@@ -180,7 +182,7 @@ class StoreIndex {
|
|
|
180
182
|
}
|
|
181
183
|
/** A store-backed `RowIndex` for the root scan of `target` on `path`, or
|
|
182
184
|
* `undefined` when SQLite has no index for the path (per-target table above;
|
|
183
|
-
* multi-segment paths; `format`, edge `predicate`/`
|
|
185
|
+
* multi-segment paths; `format`, edge `predicate`/`field`, `tags`, …). */
|
|
184
186
|
export function storeIndexFor(target, path, deps) {
|
|
185
187
|
const probe = probeFor(target, path);
|
|
186
188
|
return probe ? new StoreIndex(target, path[0], probe, deps) : undefined;
|
|
@@ -188,6 +190,6 @@ export function storeIndexFor(target, path, deps) {
|
|
|
188
190
|
/** The paths `storeIndexFor` answers from SQLite, per target (for tests and docs). */
|
|
189
191
|
export function indexablePaths(target) {
|
|
190
192
|
const cols = Object.keys(COLUMN_PROBES[target]);
|
|
191
|
-
return target === "docs" ? [...cols, "
|
|
193
|
+
return target === "docs" ? [...cols, "title", "<property key>"] : cols;
|
|
192
194
|
}
|
|
193
195
|
//# sourceMappingURL=store-index.js.map
|
|
@@ -61,7 +61,7 @@ export interface Quantifier {
|
|
|
61
61
|
varName: string;
|
|
62
62
|
predicate: Node;
|
|
63
63
|
}
|
|
64
|
-
/** dotted field path; `$` set when intrinsic (
|
|
64
|
+
/** dotted field path; `$` set when intrinsic (id, path, doc.path handled by segments) */
|
|
65
65
|
export interface FieldRef {
|
|
66
66
|
kind: "field";
|
|
67
67
|
segments: string[];
|