@omgbase/core 0.28.0 → 0.29.2
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 +7 -5
- package/dist/src/core/read/document.d.ts +11 -10
- package/dist/src/core/read/document.js +12 -11
- package/dist/src/core/read/nodes.d.ts +10 -10
- package/dist/src/core/read/nodes.js +11 -11
- package/dist/src/core/read/reader.d.ts +37 -24
- package/dist/src/core/read/reader.js +44 -31
- package/dist/src/graph/history.d.ts +25 -22
- package/dist/src/graph/history.js +17 -17
- package/dist/src/graph/inbound-links.d.ts +7 -7
- package/dist/src/graph/inbound-links.js +7 -7
- package/dist/src/graph/link-health.d.ts +9 -9
- package/dist/src/graph/link-health.js +9 -9
- package/dist/src/graph/links.d.ts +3 -3
- package/dist/src/graph/links.js +4 -4
- package/dist/src/mcp/arguments.d.ts +10 -3
- package/dist/src/mcp/arguments.js +32 -9
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/graph.d.ts +22 -22
- package/dist/src/mcp/graph.js +54 -47
- package/dist/src/mcp/reference.d.ts +29 -1
- package/dist/src/mcp/reference.js +398 -255
- package/dist/src/mcp/server.d.ts +9 -0
- package/dist/src/mcp/server.js +170 -108
- package/dist/src/mutate/apply.d.ts +6 -6
- package/dist/src/mutate/apply.js +1 -1
- package/dist/src/mutate/canonical.d.ts +1 -1
- package/dist/src/mutate/canonical.js +2 -2
- package/dist/src/mutate/commit.d.ts +3 -3
- package/dist/src/mutate/commit.js +4 -4
- package/dist/src/mutate/docs.d.ts +1 -1
- package/dist/src/mutate/docs.js +8 -8
- package/dist/src/mutate/effects.d.ts +17 -17
- package/dist/src/mutate/effects.js +113 -113
- package/dist/src/mutate/macros.d.ts +5 -5
- package/dist/src/mutate/macros.js +1 -1
- package/dist/src/mutate/materialize.js +2 -2
- package/dist/src/mutate/ops.js +2 -2
- package/dist/src/mutate/opset.d.ts +7 -7
- package/dist/src/mutate/plan-update.js +2 -2
- package/dist/src/mutate/plan.d.ts +8 -8
- package/dist/src/mutate/plan.js +25 -25
- package/dist/src/mutate/select.d.ts +4 -4
- package/dist/src/mutate/select.js +57 -56
- package/dist/src/mutate/sugar.d.ts +10 -10
- package/dist/src/mutate/sugar.js +21 -21
- package/dist/src/oqx-js/context.d.ts +8 -4
- package/dist/src/oqx-js/context.js +206 -189
- package/dist/src/oqx-js/names.d.ts +28 -17
- package/dist/src/oqx-js/names.js +115 -78
- package/dist/src/oqx-js/planner.d.ts +4 -8
- package/dist/src/oqx-js/planner.js +6 -10
- package/dist/src/oqx-js/root-row.d.ts +6 -5
- package/dist/src/oqx-js/root-row.js +15 -16
- package/dist/src/oqx-js/run.d.ts +18 -17
- package/dist/src/oqx-js/run.js +57 -54
- package/dist/src/oqx-js/sql/translate.d.ts +5 -5
- package/dist/src/oqx-js/sql/translate.js +162 -161
- package/dist/src/oqx-js/store-index.d.ts +1 -1
- package/dist/src/oqx-js/store-index.js +28 -30
- package/dist/src/search/external.d.ts +10 -0
- package/dist/src/search/external.js +20 -2
- package/dist/src/search/resolve.d.ts +2 -2
- package/dist/src/search/resolve.js +1 -1
- package/dist/src/search/rrf.js +2 -2
- package/dist/src/search/text.d.ts +5 -5
- package/dist/src/search/text.js +2 -2
- package/dist/src/spec-versions.d.ts +7 -7
- package/dist/src/spec-versions.js +6 -6
- package/dist/src/surface-paths.d.ts +18 -17
- package/dist/src/surface-paths.js +23 -21
- package/dist/src/sync/admin.d.ts +2 -2
- package/dist/src/sync/admin.js +1 -1
- package/package.json +2 -2
|
@@ -28,8 +28,8 @@
|
|
|
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
30
|
// or WHOLE source handle is never read as a property (decline (b),
|
|
31
|
-
// `NON_PROPERTY_NAMES`; a key read THROUGH a handle — `
|
|
32
|
-
// `
|
|
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
|
|
33
33
|
// read is a TYPED push (the 1.2 patch): the stored type is tested in SQL
|
|
34
34
|
// before the value (`json_type(x) = 'true'`, `p.type = 'number' AND
|
|
35
35
|
// p.val_num >= ?`) and the whole test is wrapped `(…) IS 1` / `IS NOT 1`,
|
|
@@ -44,36 +44,36 @@ import { storagePath } from "../../core/paths.js";
|
|
|
44
44
|
import { TARGETS } from "../root-row.js";
|
|
45
45
|
// ---- names that are never property reads ------------------------------------
|
|
46
46
|
/**
|
|
47
|
-
* Per target, the
|
|
47
|
+
* Per target, the engine names (and `_doc.<k>` keys, via the docs set) that
|
|
48
48
|
* `context.ts` resolves to something other than a scalar field or property: the
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* reach-through handles, the relations, the property source handles and
|
|
50
|
+
* (blocks/nodes) the whole `_attrs` object. A comparison naming one of these is
|
|
51
|
+
* declined (spec/surface §1 decline (b)) — read as a property it would be
|
|
52
|
+
* `NULL`, where in memory the value is a row, an array or an object.
|
|
53
53
|
* `translate.test.ts` asserts this set against the store context.
|
|
54
54
|
*/
|
|
55
55
|
export const NON_PROPERTY_NAMES = {
|
|
56
|
-
docs: new Set(["
|
|
57
|
-
blocks: new Set(["
|
|
58
|
-
nodes: new Set(["
|
|
59
|
-
links: new Set(["
|
|
56
|
+
docs: new Set(["_nodes", "_blocks", "_out_links", "_in_links", "_revisions", "_data", "_frontmatter", "_inline"]),
|
|
57
|
+
blocks: new Set(["_doc", "_children", "_nodes", "_out_links", "_section", "_attrs"]),
|
|
58
|
+
nodes: new Set(["_doc", "_block", "_blocks", "_subsections", "_children", "_attrs"]),
|
|
59
|
+
links: new Set(["_doc", "_dst", "_block"]),
|
|
60
60
|
};
|
|
61
61
|
// ---- engine attributes ------------------------------------------------------
|
|
62
|
-
// The engine attributes (spec/surface §1.2,
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// scalar, per target, with its kind. The structural columns and
|
|
66
|
-
// translated in `translateOperand`. Anything not mapped
|
|
67
|
-
// a blob; `
|
|
68
|
-
// computed hashtags) returns null → residual, and the
|
|
69
|
-
// through to a property read
|
|
70
|
-
// `
|
|
71
|
-
// document's revisions (`current_rev` / `seq = 1`); a block's are
|
|
72
|
-
// before. `
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
62
|
+
// The engine attributes (spec/surface §1.2, 5.0: `_`-prefixed, snake_case —
|
|
63
|
+
// `_id`, `_path`, `_raw`, `_hash`, `_updated_at`, `_created_at` on docs and
|
|
64
|
+
// blocks, `_title`/`_tags` on docs, `_id`/`_path` on nodes and edges) → a
|
|
65
|
+
// param-free SQL scalar, per target, with its kind. The structural columns and
|
|
66
|
+
// foreign keys are translated in `translateOperand`. Anything not mapped
|
|
67
|
+
// (`_raw`, reconstructed or a blob; `_title`, computed; `_tags`, the union of
|
|
68
|
+
// the authored list and the computed hashtags) returns null → residual, and the
|
|
69
|
+
// caller must NOT fall through to a property read (an engine name is never the
|
|
70
|
+
// author's). `_updated_at` and `_created_at` (4.10) are correlated subqueries
|
|
71
|
+
// over the document's revisions (`current_rev` / `seq = 1`); a block's are
|
|
72
|
+
// residual, as before. `_path` (and the edge field `_dst_path`) is the REFERENCE
|
|
73
|
+
// form on the surface (spec/surface §1 "Paths", 2.0) — `'/' || <storage
|
|
74
|
+
// column>` in SQL — so every comparison, `startsWith`, `contains`, `lower()`…
|
|
75
|
+
// sees exactly the string the in-memory attribute yields; `pathColumn` keeps
|
|
76
|
+
// the bare indexed column at hand for the equality fast path.
|
|
77
77
|
/** The rooted path read over a storage path column. */
|
|
78
78
|
export function pathColumn(col) {
|
|
79
79
|
return { sql: `('/' || ${col})`, params: [], kind: "text", pathCol: col };
|
|
@@ -81,57 +81,57 @@ export function pathColumn(col) {
|
|
|
81
81
|
/** The identity/bytes/time attributes per kind — the names `attributeSql` owns
|
|
82
82
|
* (the structural columns are handled per target in `translateOperand`). */
|
|
83
83
|
export const ROW_ATTRIBUTES = {
|
|
84
|
-
docs: new Set(["
|
|
85
|
-
blocks: new Set(["
|
|
86
|
-
nodes: new Set(["
|
|
87
|
-
links: new Set(["
|
|
84
|
+
docs: new Set(["_id", "_path", "_raw", "_hash", "_updated_at", "_created_at", "_title", "_tags"]),
|
|
85
|
+
blocks: new Set(["_id", "_path", "_raw", "_hash", "_updated_at", "_created_at"]),
|
|
86
|
+
nodes: new Set(["_id", "_path"]),
|
|
87
|
+
links: new Set(["_id", "_path"]),
|
|
88
88
|
};
|
|
89
89
|
function attributeSql(name, ctx) {
|
|
90
90
|
const { self, doc, target } = ctx;
|
|
91
91
|
const text = (sql) => ({ sql, params: [], kind: "text" });
|
|
92
92
|
if (target === "docs")
|
|
93
93
|
switch (name) {
|
|
94
|
-
case "
|
|
95
|
-
case "
|
|
96
|
-
case "
|
|
97
|
-
case "
|
|
98
|
-
case "
|
|
94
|
+
case "_id": return text(`${self}.doc_id`);
|
|
95
|
+
case "_path": return pathColumn(`${doc}.path`);
|
|
96
|
+
case "_hash": return text(`lower(hex(${self}.file_hash))`);
|
|
97
|
+
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)`);
|
|
98
|
+
case "_created_at": return text(`(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.doc_id = ${self}.doc_id AND r.seq = 1)`);
|
|
99
99
|
}
|
|
100
100
|
else if (target === "blocks")
|
|
101
101
|
switch (name) {
|
|
102
|
-
case "
|
|
103
|
-
case "
|
|
104
|
-
case "
|
|
102
|
+
case "_id": return text(`${self}.block_id`);
|
|
103
|
+
case "_path": return pathColumn(`${doc}.path`);
|
|
104
|
+
case "_hash": return text(`lower(hex(${self}.raw_hash))`);
|
|
105
105
|
}
|
|
106
106
|
else if (target === "nodes")
|
|
107
107
|
switch (name) {
|
|
108
|
-
case "
|
|
109
|
-
case "
|
|
108
|
+
case "_id": return text(`${self}.node_id`);
|
|
109
|
+
case "_path": return pathColumn(`${doc}.path`);
|
|
110
110
|
}
|
|
111
111
|
else if (target === "links")
|
|
112
112
|
switch (name) {
|
|
113
|
-
case "
|
|
114
|
-
case "
|
|
113
|
+
case "_id": return text(`${self}.edge_id`);
|
|
114
|
+
case "_path": return pathColumn(`${doc}.path`);
|
|
115
115
|
}
|
|
116
116
|
return null;
|
|
117
117
|
}
|
|
118
|
-
/** The links target's plain fields → the store's columns (spec/surface §1.2
|
|
119
|
-
*
|
|
120
|
-
* and `
|
|
118
|
+
/** The links target's plain fields → the store's columns (spec/surface §1.2:
|
|
119
|
+
* a foreign key is named for what it is, `_`-prefixed since 5.0). `_dst_path`
|
|
120
|
+
* and `_uri` are correlated subqueries; `_dst_path` is the reference form. */
|
|
121
121
|
function edgeField(name, self) {
|
|
122
122
|
const text = (sql) => ({ sql, params: [], kind: "text" });
|
|
123
123
|
switch (name) {
|
|
124
|
-
case "
|
|
125
|
-
case "
|
|
126
|
-
case "
|
|
127
|
-
case "
|
|
128
|
-
case "
|
|
129
|
-
case "
|
|
130
|
-
case "
|
|
131
|
-
case "
|
|
132
|
-
case "
|
|
133
|
-
case "
|
|
134
|
-
case "
|
|
124
|
+
case "_doc_id": return text(`${self}.src_doc`);
|
|
125
|
+
case "_block_id": return text(`${self}.src_block`);
|
|
126
|
+
case "_via_id": return text(`${self}.via_node`);
|
|
127
|
+
case "_dst_id": return text(`${self}.dst_node`);
|
|
128
|
+
case "_dst_path": return text(`(SELECT '/' || dd.path FROM docs dd WHERE dd.doc_id = ${self}.dst_node)`);
|
|
129
|
+
case "_uri": return text(`(SELECT xn.uri FROM external_nodes xn WHERE xn.node_id = ${self}.dst_node)`);
|
|
130
|
+
case "_field": return text(`${self}.src_field`);
|
|
131
|
+
case "_predicate":
|
|
132
|
+
case "_provenance":
|
|
133
|
+
case "_dst_kind":
|
|
134
|
+
case "_anchor": return text(`${self}.${name.slice(1)}`);
|
|
135
135
|
default: return null;
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -140,14 +140,14 @@ const SEG = /^[A-Za-z_][A-Za-z0-9_]*$/; // injection-safe inlined identifier
|
|
|
140
140
|
// rule): the row for `key` on the document only when the key has exactly one
|
|
141
141
|
// row in scope and it is card='scalar' — a list-valued or nested key has none.
|
|
142
142
|
// `select` is the projected expression over the alias `p`. `source` narrows
|
|
143
|
-
// the scope to one authored source — the read `
|
|
143
|
+
// the scope to one authored source — the read `_frontmatter.<k>` / `_inline.<k>`
|
|
144
144
|
// performs (spec/surface §1.2; the in-memory `docProp(docId, key, source)`):
|
|
145
145
|
// both the row and the count are of that source's rows only. The merged
|
|
146
|
-
// `
|
|
146
|
+
// `_data.<k>` read is the bare read, so it passes no source. `docId` is the SQL
|
|
147
147
|
// expression naming the document: the scan's `d.doc_id`, or a link's
|
|
148
|
-
// `e.dst_node` for a `
|
|
148
|
+
// `e.dst_node` for a `_dst.<k>` read (the destination's rows by its id — a
|
|
149
149
|
// non-document or phantom destination has none, so the read is NULL exactly
|
|
150
|
-
// where `
|
|
150
|
+
// where `_dst` is absent in memory).
|
|
151
151
|
function propSubquery(docId, key, select, source) {
|
|
152
152
|
const src = source ? ` AND p.source = '${source}'` : "";
|
|
153
153
|
const src2 = source ? ` AND p2.source = '${source}'` : "";
|
|
@@ -173,12 +173,12 @@ function propScalarOf(docId, key, source) {
|
|
|
173
173
|
prop: source ? { docId, key, source } : { docId, key },
|
|
174
174
|
};
|
|
175
175
|
}
|
|
176
|
-
// A key read through a source handle (spec/surface §1.2
|
|
177
|
-
//
|
|
178
|
-
//
|
|
176
|
+
// A key read through a source handle (spec/surface §1.2): `_data.<k>` is the
|
|
177
|
+
// merged authored read — exactly the bare property `<k>`; `_frontmatter.<k>` /
|
|
178
|
+
// `_inline.<k>` the same read scoped to one source. Pushed since 3.0 (batch 3;
|
|
179
179
|
// before, every handle member was residual). A whole-handle reference
|
|
180
|
-
// (`
|
|
181
|
-
const SOURCE_HANDLES = {
|
|
180
|
+
// (`_frontmatter == null`) and `entries(<handle>)` still decline.
|
|
181
|
+
const SOURCE_HANDLES = { _data: undefined, _frontmatter: "frontmatter", _inline: "inline" };
|
|
182
182
|
function handleRead(docAlias, handle, key) {
|
|
183
183
|
return handleReadOf(`${docAlias}.doc_id`, handle, key);
|
|
184
184
|
}
|
|
@@ -207,8 +207,8 @@ function constOperand(v) {
|
|
|
207
207
|
default: return v === null ? { sql: "?", params: [null], kind: "null", value: null } : null;
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
|
-
// The dotted `
|
|
211
|
-
// not a plain identifier navigation.
|
|
210
|
+
// The dotted `_attrs.a.b` / `_doc.x` receiver chain as segments, or null if it
|
|
211
|
+
// is not a plain identifier navigation.
|
|
212
212
|
function memberSegments(e) {
|
|
213
213
|
if (e.kind === "ident")
|
|
214
214
|
return [e.name];
|
|
@@ -232,41 +232,45 @@ export function translateOperand(e, ctx) {
|
|
|
232
232
|
return constOperand(ctx.params[e.index]);
|
|
233
233
|
case "ident": {
|
|
234
234
|
// An OQX `$` name (`$it`, `$key`, the follow intrinsics) is the language's,
|
|
235
|
-
// never a column; omgbase adds no `$` names (spec/surface §1.2
|
|
235
|
+
// never a column; omgbase adds no `$` names (spec/surface §1.2).
|
|
236
236
|
if (e.name.startsWith("$"))
|
|
237
237
|
return null;
|
|
238
|
-
//
|
|
239
|
-
// (`
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
// scalar read (decline (b)); a bare target name is a relation or a loud
|
|
238
|
+
// An engine name (§1.2, 5.0: `_`-prefixed) is translated or declined
|
|
239
|
+
// (`_raw`, `_title`, `_tags`; a relation, a reach-through, a source handle,
|
|
240
|
+
// the whole `_attrs` object — decline (b)); never read as a property or
|
|
241
|
+
// an attr. A `_x` that is no engine name of the kind is refused before
|
|
242
|
+
// planning (names.ts), so it declines here. A bare target name is a loud
|
|
244
243
|
// error (root-row.ts), never a property.
|
|
245
|
-
if (
|
|
244
|
+
if (e.name.startsWith("_")) {
|
|
245
|
+
if (ROW_ATTRIBUTES[target].has(e.name))
|
|
246
|
+
return attributeSql(e.name, ctx);
|
|
247
|
+
if (NON_PROPERTY_NAMES[target].has(e.name))
|
|
248
|
+
return null;
|
|
249
|
+
if (target === "docs")
|
|
250
|
+
return e.name === "_format" ? text(`${self}.format`) : null; // a column
|
|
251
|
+
if (target === "blocks") {
|
|
252
|
+
if (e.name === "_doc_id" || e.name === "_type" || e.name === "_text")
|
|
253
|
+
return text(`${self}.${e.name.slice(1)}`);
|
|
254
|
+
if (e.name === "_ordinal" || e.name === "_depth")
|
|
255
|
+
return { sql: `${self}.${e.name.slice(1)}`, params: [], kind: "int" };
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
if (target === "nodes")
|
|
259
|
+
return ["_doc_id", "_block_id", "_kind", "_name", "_value"].includes(e.name) ? text(`${self}.${e.name.slice(1)}`) : null;
|
|
260
|
+
if (target === "links")
|
|
261
|
+
return edgeField(e.name, self);
|
|
246
262
|
return null;
|
|
247
|
-
// Bare field per target.
|
|
248
|
-
if (target === "docs") {
|
|
249
|
-
if (e.name === "format")
|
|
250
|
-
return text(`${self}.format`); // a column, not a property
|
|
251
|
-
return propScalar(doc, e.name);
|
|
252
|
-
}
|
|
253
|
-
if (target === "blocks") {
|
|
254
|
-
if (e.name === "doc_id" || e.name === "type" || e.name === "text")
|
|
255
|
-
return text(`${self}.${e.name}`);
|
|
256
|
-
if (e.name === "ordinal" || e.name === "depth")
|
|
257
|
-
return { sql: `${self}.${e.name}`, params: [], kind: "int" };
|
|
258
|
-
// Bare non-structural identifier flattens into attrs — same pushdown as
|
|
259
|
-
// the `attrs.<k>` member form (json_extract), so `checked` == `attrs.checked`.
|
|
260
|
-
return jsonExtract(`${self}.attrs`, [e.name]);
|
|
261
263
|
}
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
264
|
+
if (TARGETS.has(e.name))
|
|
265
|
+
return null;
|
|
266
|
+
// A bare name is the author's (§1.2): a document property, or on blocks
|
|
267
|
+
// and nodes an `attrs` key — the same pushdown as the `_attrs.<k>` member
|
|
268
|
+
// form (json_extract), so `checked` == `_attrs.checked`. A link row
|
|
269
|
+
// carries no authored data (names.ts refuses the read).
|
|
270
|
+
if (target === "docs")
|
|
271
|
+
return propScalar(doc, e.name);
|
|
272
|
+
if (target === "blocks" || target === "nodes")
|
|
266
273
|
return jsonExtract(`${self}.attrs`, [e.name]);
|
|
267
|
-
}
|
|
268
|
-
if (target === "links")
|
|
269
|
-
return edgeField(e.name, self);
|
|
270
274
|
return null;
|
|
271
275
|
}
|
|
272
276
|
case "member": {
|
|
@@ -274,56 +278,55 @@ export function translateOperand(e, ctx) {
|
|
|
274
278
|
if (!segs || segs.length < 2)
|
|
275
279
|
return null;
|
|
276
280
|
const [head, ...rest] = segs;
|
|
277
|
-
//
|
|
278
|
-
if (head === "
|
|
281
|
+
// _attrs.<path> → json_extract on the row's attrs (blocks/nodes).
|
|
282
|
+
if (head === "_attrs" && (target === "blocks" || target === "nodes")) {
|
|
279
283
|
return jsonExtract(`${self}.attrs`, rest);
|
|
280
284
|
}
|
|
281
|
-
// <handle>.<k> on docs — `
|
|
282
|
-
// — and `
|
|
283
|
-
// targets: a property read, scoped to the handle's source.
|
|
285
|
+
// <handle>.<k> on docs — `_data.layer`, `_frontmatter.layer`,
|
|
286
|
+
// `_inline.element` — and `_doc.<handle>.<k>` through the owning document
|
|
287
|
+
// on the other targets: a property read, scoped to the handle's source.
|
|
284
288
|
if (target === "docs" && head in SOURCE_HANDLES && rest.length === 1)
|
|
285
289
|
return handleRead(doc, head, rest[0]);
|
|
286
|
-
if (head === "
|
|
290
|
+
if (head === "_doc" && target !== "docs" && rest.length === 2 && rest[0] in SOURCE_HANDLES)
|
|
287
291
|
return handleRead(doc, rest[0], rest[1]);
|
|
288
|
-
//
|
|
289
|
-
//
|
|
290
|
-
// FOREIGN-KEY ELIMINATION (spec/surface §1, 3.0): `
|
|
291
|
-
// own FK column (`b.doc_id`, `n.doc_id`, `e.src_doc`) and
|
|
292
|
-
// `
|
|
293
|
-
// columns — no extra join, the same SQL as the
|
|
294
|
-
|
|
292
|
+
// _doc.<x> reach-through — the owning doc (alias `doc`, which is `d`) on
|
|
293
|
+
// blocks, nodes and links (a document has no `_doc` since 5.0: `$it` is
|
|
294
|
+
// the self). FOREIGN-KEY ELIMINATION (spec/surface §1, 3.0): `_doc._id` is
|
|
295
|
+
// the row's own FK column (`b.doc_id`, `n.doc_id`, `e.src_doc`) and
|
|
296
|
+
// `_doc._path` / `_doc._hash` / `_doc._updated_at` the scan's
|
|
297
|
+
// already-joined document columns — no extra join, the same SQL as the
|
|
298
|
+
// bare `_doc_id`.
|
|
299
|
+
if (head === "_doc" && target !== "docs") {
|
|
295
300
|
if (rest.length !== 1)
|
|
296
301
|
return null;
|
|
297
302
|
const k = rest[0];
|
|
298
|
-
if (k === "
|
|
303
|
+
if (k === "_id")
|
|
299
304
|
return text(target === "links" ? `${self}.src_doc` : `${self}.doc_id`);
|
|
300
|
-
if (k === "
|
|
305
|
+
if (k === "_path")
|
|
301
306
|
return pathColumn(`${doc}.path`);
|
|
302
|
-
if (k === "
|
|
307
|
+
if (k === "_hash")
|
|
303
308
|
return text(`lower(hex(${doc}.file_hash))`);
|
|
304
|
-
if (k === "
|
|
309
|
+
if (k === "_updated_at")
|
|
305
310
|
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)`);
|
|
306
|
-
if (k === "
|
|
311
|
+
if (k === "_created_at")
|
|
307
312
|
return text(`(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.doc_id = ${doc}.doc_id AND r.seq = 1)`);
|
|
308
|
-
if (k === "
|
|
313
|
+
if (k === "_format")
|
|
309
314
|
return text(`${doc}.format`);
|
|
310
|
-
if (k.startsWith("$"))
|
|
311
|
-
return null;
|
|
312
|
-
if (
|
|
313
|
-
return null; //
|
|
314
|
-
if (NON_PROPERTY_NAMES.docs.has(k) || TARGETS.has(k))
|
|
315
|
-
return null; // `doc.nodes`, `doc.data`, `doc.docs` … (decline (b))
|
|
315
|
+
if (k.startsWith("$") || k.startsWith("_"))
|
|
316
|
+
return null; // `_doc._raw`, `_doc._title`, `_doc._nodes`, an unknown engine name: not pushable (decline (b))
|
|
317
|
+
if (TARGETS.has(k))
|
|
318
|
+
return null; // a bare target name: the loud error, never a property
|
|
316
319
|
return propScalar(doc, k);
|
|
317
320
|
}
|
|
318
|
-
//
|
|
319
|
-
// destination DOCUMENT, keyed by the row's own `dst_node` — the lookup
|
|
320
|
-
// `
|
|
321
|
+
// _dst.<x> reach-through from a link (spec/surface §1.2, 3.0 follow-up):
|
|
322
|
+
// the destination DOCUMENT, keyed by the row's own `dst_node` — the lookup
|
|
323
|
+
// `_dst_path` already runs. A property read scopes `properties` by
|
|
321
324
|
// `e.dst_node` directly; the document's own columns are a primary-key
|
|
322
325
|
// probe `(SELECT … FROM docs dd WHERE dd.doc_id = e.dst_node)` — NULL
|
|
323
|
-
// wherever `
|
|
324
|
-
// phantom). `
|
|
325
|
-
// residual, as `
|
|
326
|
-
if (head === "
|
|
326
|
+
// wherever `_dst` is absent in memory (a block or external destination, a
|
|
327
|
+
// phantom). `_dst._raw` / `_dst._title` / `_dst._tags` and the relations
|
|
328
|
+
// stay residual, as `_doc.*`'s do.
|
|
329
|
+
if (head === "_dst" && target === "links") {
|
|
327
330
|
const dstId = `${self}.dst_node`;
|
|
328
331
|
if (rest.length === 2 && rest[0] in SOURCE_HANDLES)
|
|
329
332
|
return handleReadOf(dstId, rest[0], rest[1]);
|
|
@@ -331,58 +334,56 @@ export function translateOperand(e, ctx) {
|
|
|
331
334
|
return null;
|
|
332
335
|
const k = rest[0];
|
|
333
336
|
const probe = (col) => text(`(SELECT ${col} FROM docs dd WHERE dd.doc_id = ${dstId})`);
|
|
334
|
-
if (k === "
|
|
337
|
+
if (k === "_id")
|
|
335
338
|
return probe("dd.doc_id");
|
|
336
|
-
if (k === "
|
|
339
|
+
if (k === "_path")
|
|
337
340
|
return probe("'/' || dd.path");
|
|
338
|
-
if (k === "
|
|
341
|
+
if (k === "_hash")
|
|
339
342
|
return probe("lower(hex(dd.file_hash))");
|
|
340
|
-
if (k === "
|
|
343
|
+
if (k === "_updated_at")
|
|
341
344
|
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)");
|
|
342
|
-
if (k === "
|
|
345
|
+
if (k === "_created_at")
|
|
343
346
|
return probe("(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.doc_id = dd.doc_id AND r.seq = 1)");
|
|
344
|
-
if (k === "
|
|
347
|
+
if (k === "_format")
|
|
345
348
|
return probe("dd.format");
|
|
346
|
-
if (k.startsWith("$"))
|
|
347
|
-
return null;
|
|
348
|
-
if (ROW_ATTRIBUTES.docs.has(k) || NON_PROPERTY_NAMES.docs.has(k) || TARGETS.has(k))
|
|
349
|
+
if (k.startsWith("$") || k.startsWith("_") || TARGETS.has(k))
|
|
349
350
|
return null;
|
|
350
351
|
return propScalarOf(dstId, k);
|
|
351
352
|
}
|
|
352
|
-
//
|
|
353
|
-
// FK (`
|
|
354
|
-
// `
|
|
355
|
-
// a correlated read of the block row.
|
|
356
|
-
if (head === "
|
|
353
|
+
// _block.<x> reach-through from a node: the block's identity is the node's
|
|
354
|
+
// FK (`_block._id` → `n.block_id`, `_block._path` → the joined `d.path`,
|
|
355
|
+
// `_block._doc_id` → `n.doc_id` — FK elimination again); `_type`/`_text`
|
|
356
|
+
// are a correlated read of the block row.
|
|
357
|
+
if (head === "_block" && target === "nodes" && rest.length === 1) {
|
|
357
358
|
const k = rest[0];
|
|
358
|
-
if (k === "
|
|
359
|
+
if (k === "_id")
|
|
359
360
|
return text(`${self}.block_id`);
|
|
360
|
-
if (k === "
|
|
361
|
+
if (k === "_path")
|
|
361
362
|
return pathColumn(`${doc}.path`);
|
|
362
|
-
if (k === "
|
|
363
|
+
if (k === "_doc_id")
|
|
363
364
|
return text(`${self}.doc_id`);
|
|
364
|
-
if (k === "
|
|
365
|
-
return text(`(SELECT bb.${k} FROM blocks bb WHERE bb.block_id = ${self}.block_id)`);
|
|
365
|
+
if (k === "_type" || k === "_text")
|
|
366
|
+
return text(`(SELECT bb.${k.slice(1)} FROM blocks bb WHERE bb.block_id = ${self}.block_id)`);
|
|
366
367
|
return null;
|
|
367
368
|
}
|
|
368
|
-
//
|
|
369
|
-
// block, keyed by the row's own `src_block` — `
|
|
370
|
-
// (NULL for a frontmatter link, exactly where `
|
|
371
|
-
// everything else is a primary-key probe of the block row,
|
|
372
|
-
// same rows (`
|
|
373
|
-
// the probe, so it too is NULL when there is no block).
|
|
374
|
-
// `
|
|
375
|
-
if (head === "
|
|
369
|
+
// _block.<x> reach-through from a link (spec/surface §1.2, 4.11): the
|
|
370
|
+
// SOURCE block, keyed by the row's own `src_block` — `_block._id` is that
|
|
371
|
+
// FK column (NULL for a frontmatter link, exactly where `_block` is absent
|
|
372
|
+
// in memory); everything else is a primary-key probe of the block row,
|
|
373
|
+
// NULL for the same rows (`_block._path` reads the scan's joined source
|
|
374
|
+
// document inside the probe, so it too is NULL when there is no block).
|
|
375
|
+
// `_raw`, `_hash`, `_updated_at`, `_attrs` and the relations stay residual.
|
|
376
|
+
if (head === "_block" && target === "links" && rest.length === 1) {
|
|
376
377
|
const k = rest[0];
|
|
377
378
|
const probe = (col) => text(`(SELECT ${col} FROM blocks bb WHERE bb.block_id = ${self}.src_block)`);
|
|
378
|
-
if (k === "
|
|
379
|
+
if (k === "_id")
|
|
379
380
|
return text(`${self}.src_block`);
|
|
380
|
-
if (k === "
|
|
381
|
+
if (k === "_path")
|
|
381
382
|
return probe(`'/' || ${doc}.path`);
|
|
382
|
-
if (k === "
|
|
383
|
+
if (k === "_doc_id")
|
|
383
384
|
return probe("bb.doc_id");
|
|
384
|
-
if (k === "
|
|
385
|
-
return probe(`bb.${k}`);
|
|
385
|
+
if (k === "_type" || k === "_text")
|
|
386
|
+
return probe(`bb.${k.slice(1)}`);
|
|
386
387
|
return null;
|
|
387
388
|
}
|
|
388
389
|
return null;
|
|
@@ -537,7 +538,7 @@ function translateComparison(e, ctx) {
|
|
|
537
538
|
}
|
|
538
539
|
const FLIP = { "<": ">", "<=": ">=", ">": "<", ">=": "<=" };
|
|
539
540
|
/**
|
|
540
|
-
* The path equality fast path: `
|
|
541
|
+
* The path equality fast path: `_path == "/a.md"` (or `!=`, either side) against
|
|
541
542
|
* a rooted text constant is `d.path IS ?` with the constant's storage form, so
|
|
542
543
|
* the `(repo_id, path)` index serves it; `'/' || d.path IS ?` — what the general
|
|
543
544
|
* form would emit — is the same predicate without the index. Only a constant
|
|
@@ -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; `
|
|
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
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,11 +12,12 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Typed equality (OQX SEMANTICS §5) is preserved by probing per VALUE KIND:
|
|
14
14
|
//
|
|
15
|
-
// • a text column (`
|
|
16
|
-
// matches a string probe by `= ?` (TEXT = TEXT, exact); a
|
|
17
|
-
// object or any other non-string probe matches NO row (the
|
|
18
|
-
// strings or NULL and `5 == "5"` is false); an absent probe
|
|
19
|
-
// `undefined`: `
|
|
15
|
+
// • a text column (`_id`, `_path`, `_type`, `_kind`, `_name`, `_doc_id`,
|
|
16
|
+
// `_dst_id`, …) matches a string probe by `= ?` (TEXT = TEXT, exact); a
|
|
17
|
+
// number, boolean, object or any other non-string probe matches NO row (the
|
|
18
|
+
// column holds strings or NULL and `5 == "5"` is false); an absent probe
|
|
19
|
+
// (`null`, `undefined`: `_name == null`) is answered by the in-memory
|
|
20
|
+
// fallback below;
|
|
20
21
|
// • a document property (`customer`, `title`) matches a string probe only on
|
|
21
22
|
// a `type = 'string'` row (`val_text = ?`), a number probe only on a
|
|
22
23
|
// `type = 'number'` row (`val_num = ?`), a boolean only on a `type = 'bool'`
|
|
@@ -38,7 +39,6 @@
|
|
|
38
39
|
// contract but the engine never calls it for these indexes: it probes
|
|
39
40
|
// `lookupRows` first and so never reads the root whole.
|
|
40
41
|
import { semantics } from "@omgbase/oqx";
|
|
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 ------------------------------------------------
|
|
@@ -52,16 +52,16 @@ import { storagePath } from "../core/paths.js";
|
|
|
52
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: {
|
|
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
58
|
links: {
|
|
59
|
-
|
|
60
|
-
// `
|
|
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
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
|
|
@@ -75,17 +75,15 @@ function probeFor(target, path) {
|
|
|
75
75
|
return { kind: "column", sql: column };
|
|
76
76
|
if (target !== "docs")
|
|
77
77
|
return null;
|
|
78
|
-
// A docs property: any
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// `
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
if (key === "
|
|
78
|
+
// A docs property: any BARE name (spec/surface §1.2, 5.0: the author's data —
|
|
79
|
+
// `title`, `tags`, `layer`, …; an OQX `$` name is never a row read). Of the
|
|
80
|
+
// engine names only `_title` probes (the computed `$title` row, a scalar
|
|
81
|
+
// property): `_raw`, `_hash`, `_updated_at`, `_created_at`, `_tags` (authored
|
|
82
|
+
// ∪ hashtags), `_format` (an unindexed column), the handles and relations have
|
|
83
|
+
// no index and are never property probes.
|
|
84
|
+
if (key === "_title")
|
|
85
85
|
return { kind: "property", key: "$title", source: "computed" };
|
|
86
|
-
if (key.startsWith("$") || key
|
|
87
|
-
return null;
|
|
88
|
-
if (NON_PROPERTY_NAMES.docs.has(key) || ROW_ATTRIBUTES.docs.has(key))
|
|
86
|
+
if (key.startsWith("$") || key.startsWith("_"))
|
|
89
87
|
return null;
|
|
90
88
|
return { kind: "property", key, source: null };
|
|
91
89
|
}
|
|
@@ -110,10 +108,10 @@ class StoreIndex {
|
|
|
110
108
|
if (this.probe.kind === "column") {
|
|
111
109
|
if (typeof v !== "string")
|
|
112
110
|
return [];
|
|
113
|
-
// `
|
|
111
|
+
// `_path` / `_dst_path` are the reference form in memory (`/a.md`,
|
|
114
112
|
// spec/surface §1 "Paths"); the column holds the storage form. A rooted
|
|
115
113
|
// probe is de-rooted; a bare one can equal no rooted path — no row.
|
|
116
|
-
if (this.path === "
|
|
114
|
+
if (this.path === "_path" || this.path === "_dst_path")
|
|
117
115
|
return v.startsWith("/") ? this.column(storagePath(v)) : [];
|
|
118
116
|
return this.column(v);
|
|
119
117
|
}
|
|
@@ -137,13 +135,13 @@ class StoreIndex {
|
|
|
137
135
|
}
|
|
138
136
|
return out.sort((a, b) => a - b);
|
|
139
137
|
}
|
|
140
|
-
// A `
|
|
138
|
+
// A `_path` probe on a joined target drives from the document (`FROM_BY_DOC`),
|
|
141
139
|
// every other column probe from the target's own index. `?r` in a probe is
|
|
142
140
|
// the repo id, bound ahead of the value.
|
|
143
141
|
column(value) {
|
|
144
142
|
const t = this.target;
|
|
145
143
|
const probe = this.probe.sql;
|
|
146
|
-
const byDoc = t !== "docs" && this.path === "
|
|
144
|
+
const byDoc = t !== "docs" && this.path === "_path";
|
|
147
145
|
const g = byDoc ? guardsByDoc(t, this.deps.repoId) : guards(t, this.deps.repoId);
|
|
148
146
|
const from = byDoc ? FROM_BY_DOC[t] : FROM[t];
|
|
149
147
|
const params = probe.includes("?r") ? [this.deps.repoId, value] : [value];
|
|
@@ -182,7 +180,7 @@ class StoreIndex {
|
|
|
182
180
|
}
|
|
183
181
|
/** A store-backed `RowIndex` for the root scan of `target` on `path`, or
|
|
184
182
|
* `undefined` when SQLite has no index for the path (per-target table above;
|
|
185
|
-
* multi-segment paths; `
|
|
183
|
+
* multi-segment paths; `_format`, edge `_predicate`/`_field`, `_tags`, …). */
|
|
186
184
|
export function storeIndexFor(target, path, deps) {
|
|
187
185
|
const probe = probeFor(target, path);
|
|
188
186
|
return probe ? new StoreIndex(target, path[0], probe, deps) : undefined;
|
|
@@ -190,6 +188,6 @@ export function storeIndexFor(target, path, deps) {
|
|
|
190
188
|
/** The paths `storeIndexFor` answers from SQLite, per target (for tests and docs). */
|
|
191
189
|
export function indexablePaths(target) {
|
|
192
190
|
const cols = Object.keys(COLUMN_PROBES[target]);
|
|
193
|
-
return target === "docs" ? [...cols, "
|
|
191
|
+
return target === "docs" ? [...cols, "_title", "<property key>"] : cols;
|
|
194
192
|
}
|
|
195
193
|
//# sourceMappingURL=store-index.js.map
|