@omgbase/core 0.24.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/core/edit-script.d.ts +15 -0
- package/dist/src/core/edit-script.js +72 -0
- package/dist/src/core/ingest.js +42 -12
- package/dist/src/core/read/document.d.ts +7 -0
- package/dist/src/core/read/document.js +38 -17
- package/dist/src/core/store/archive.d.ts +28 -0
- package/dist/src/core/store/archive.js +106 -17
- package/dist/src/core/store/delta.d.ts +131 -0
- package/dist/src/core/store/delta.js +470 -0
- package/dist/src/core/store/gc.d.ts +10 -0
- package/dist/src/core/store/gc.js +23 -7
- package/dist/src/core/store/retention.d.ts +1 -1
- package/dist/src/core/store/retention.js +3 -17
- package/dist/src/core/store/schema.d.ts +1 -1
- package/dist/src/core/store/schema.js +2 -1
- package/dist/src/core/store/store.js +7 -0
- package/dist/src/core/store/writers.d.ts +42 -6
- package/dist/src/core/store/writers.js +67 -25
- package/dist/src/format/json.js +5 -0
- package/dist/src/format/yaml.js +15 -3
- package/dist/src/graph/history.d.ts +1 -15
- package/dist/src/graph/history.js +14 -74
- package/dist/src/mcp/arguments.d.ts +11 -0
- package/dist/src/mcp/arguments.js +47 -4
- package/dist/src/mcp/errors.js +1 -1
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +1 -1
- package/dist/src/mcp/server.js +4 -2
- package/dist/src/mutate/effects.js +87 -13
- package/dist/src/mutate/frontmatter-parts.d.ts +71 -0
- package/dist/src/mutate/frontmatter-parts.js +237 -0
- package/dist/src/mutate/load.js +9 -9
- package/dist/src/mutate/select.js +9 -3
- package/dist/src/oqx-js/context.js +14 -0
- package/dist/src/oqx-js/names.js +1 -1
- package/dist/src/oqx-js/run.d.ts +8 -0
- package/dist/src/oqx-js/run.js +87 -1
- package/dist/src/oqx-js/sql/translate.js +21 -1
- package/dist/src/search/cel/parser.d.ts +3 -1
- package/dist/src/search/cel/parser.js +4 -1
- package/dist/src/spec-versions.d.ts +4 -4
- package/dist/src/spec-versions.js +4 -4
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// Frontmatter entries (spec/mutate §11.3 "Frontmatter entries", 3.6). The
|
|
2
|
+
// effect planner's `set`/`unset frontmatter.<key>` edits ONE entry of a
|
|
3
|
+
// document's frontmatter and leaves every other byte alone. To do that the
|
|
4
|
+
// `frontmatter` block's raw (fences included) is cut into an ordered list of
|
|
5
|
+
// PARTS — the opening fence line, one part per top-level YAML entry (with the
|
|
6
|
+
// lines that continue it and the comment lines that lead it), the trailing
|
|
7
|
+
// comment/blank lines, the closing fence — by a LEXICAL, line-based rule, not
|
|
8
|
+
// a YAML parse: it is total (every frontmatter the store holds, valid YAML or
|
|
9
|
+
// not, splits), byte-exact (the parts concatenate back to the raw, always) and
|
|
10
|
+
// identical in both engines; what a line *means* is the properties layer's
|
|
11
|
+
// business. Where the rule and YAML disagree (a pathological document) the
|
|
12
|
+
// edit may touch a wider or narrower run of lines than a YAML parser would,
|
|
13
|
+
// but never loses a byte of the other entries.
|
|
14
|
+
//
|
|
15
|
+
// Storage is untouched: a revision's frontmatter stays one blob (spec/store
|
|
16
|
+
// 15.0). The parts are the planner's view of the bytes, computed from
|
|
17
|
+
// `frontmatter_raw` when a frontmatter key is set or unset.
|
|
18
|
+
/** Split text into lines, each keeping its own line ending (`\n` or `\r\n`); the last line has none. `""` → `[""]`. */
|
|
19
|
+
export function splitLines(text) {
|
|
20
|
+
const out = [];
|
|
21
|
+
let start = 0;
|
|
22
|
+
for (let i = 0; i < text.length; i++) {
|
|
23
|
+
if (text.charCodeAt(i) === 10) {
|
|
24
|
+
out.push(text.slice(start, i + 1));
|
|
25
|
+
start = i + 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
out.push(text.slice(start));
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/** A line without its line ending. */
|
|
32
|
+
function content(line) {
|
|
33
|
+
return line.endsWith("\r\n") ? line.slice(0, -2) : line.endsWith("\n") ? line.slice(0, -1) : line;
|
|
34
|
+
}
|
|
35
|
+
/** The line ending of `line` (`"\r\n"`, `"\n"`, or `""` when it has none). */
|
|
36
|
+
export function lineEndingOf(line) {
|
|
37
|
+
return line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
|
|
38
|
+
}
|
|
39
|
+
/** A blank line (empty, or spaces and tabs only). */
|
|
40
|
+
export function isBlankLine(line) {
|
|
41
|
+
return /^[ \t]*$/.test(content(line));
|
|
42
|
+
}
|
|
43
|
+
/** A comment line — `#` in column 0. (An indented `#` is content: a block scalar's line, for one.) */
|
|
44
|
+
export function isCommentLine(line) {
|
|
45
|
+
return content(line).startsWith("#");
|
|
46
|
+
}
|
|
47
|
+
/** A LEAD line: blank or comment — the lines that travel with the entry that follows them. */
|
|
48
|
+
function isLeadLine(line) {
|
|
49
|
+
return isBlankLine(line) || isCommentLine(line);
|
|
50
|
+
}
|
|
51
|
+
// Characters that cannot begin a plain YAML key (flow indicators, anchors,
|
|
52
|
+
// aliases, tags, block scalar indicators, directives, reserved).
|
|
53
|
+
const NOT_A_PLAIN_KEY_START = new Set(["[", "]", "{", "}", ",", "&", "*", "!", "|", ">", "%", "@", "`"]);
|
|
54
|
+
/**
|
|
55
|
+
* The entry boundary: the key of a column-0 line that opens a top-level
|
|
56
|
+
* mapping entry, or `null` when the line does not. A line opens an entry when
|
|
57
|
+
* it is not blank, not a comment, not a sequence item (`-` followed by a
|
|
58
|
+
* space, a tab or the end of the line), not an explicit key or value
|
|
59
|
+
* indicator (`?` / `:` the same way), not a document marker (`---` / `...`
|
|
60
|
+
* followed by whitespace or the end of the line), and reads as `<key>:`
|
|
61
|
+
* followed by a space, a tab or the end of the line — the key either
|
|
62
|
+
* - quoted: a `"…"` string (`\` escapes the next character) or a `'…'` string
|
|
63
|
+
* (`''` is a literal quote), closed on the same line, then optional spaces/
|
|
64
|
+
* tabs, then the colon; the key is the content with `\\` → `\`, `\"` → `"`
|
|
65
|
+
* (double) or `''` → `'` (single) undone and every other escape kept as
|
|
66
|
+
* written; or
|
|
67
|
+
* - plain: the text before the FIRST `:` that is followed by a space, a tab or
|
|
68
|
+
* the end of the line, trailing spaces/tabs trimmed — non-empty, not
|
|
69
|
+
* starting with a flow indicator, an anchor/alias/tag/block-scalar/
|
|
70
|
+
* directive/reserved character (`[ ] { } , & * ! | > % @ \``) and not
|
|
71
|
+
* containing ` #` or `\t#` (a comment would start there).
|
|
72
|
+
* Anything else — an indented line, a flow collection's continuation, a block
|
|
73
|
+
* scalar's content — is a continuation of the entry above it.
|
|
74
|
+
*/
|
|
75
|
+
export function entryKeyOf(line) {
|
|
76
|
+
const c = content(line);
|
|
77
|
+
if (c.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
const c0 = c[0];
|
|
80
|
+
if (c0 === " " || c0 === "\t" || c0 === "#")
|
|
81
|
+
return null;
|
|
82
|
+
if (c0 === '"' || c0 === "'")
|
|
83
|
+
return quotedKey(c, c0);
|
|
84
|
+
const c1 = c.length > 1 ? c[1] : "";
|
|
85
|
+
const wsOrEnd = (ch) => ch === "" || ch === " " || ch === "\t";
|
|
86
|
+
if ((c0 === "-" || c0 === "?" || c0 === ":") && wsOrEnd(c1))
|
|
87
|
+
return null;
|
|
88
|
+
if ((c.startsWith("---") || c.startsWith("...")) && wsOrEnd(c.length > 3 ? c[3] : ""))
|
|
89
|
+
return null;
|
|
90
|
+
if (NOT_A_PLAIN_KEY_START.has(c0))
|
|
91
|
+
return null;
|
|
92
|
+
const m = /^(.*?):(?:[ \t]|$)/.exec(c);
|
|
93
|
+
if (!m)
|
|
94
|
+
return null;
|
|
95
|
+
const key = m[1].replace(/[ \t]+$/, "");
|
|
96
|
+
if (key.length === 0 || / #|\t#/.test(key))
|
|
97
|
+
return null;
|
|
98
|
+
return key;
|
|
99
|
+
}
|
|
100
|
+
function quotedKey(c, quote) {
|
|
101
|
+
let i = 1;
|
|
102
|
+
let close = -1;
|
|
103
|
+
if (quote === '"') {
|
|
104
|
+
while (i < c.length) {
|
|
105
|
+
const ch = c[i];
|
|
106
|
+
if (ch === "\\")
|
|
107
|
+
i += 2;
|
|
108
|
+
else if (ch === '"') {
|
|
109
|
+
close = i;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
else
|
|
113
|
+
i++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
while (i < c.length) {
|
|
118
|
+
if (c[i] === "'") {
|
|
119
|
+
if (c[i + 1] === "'")
|
|
120
|
+
i += 2;
|
|
121
|
+
else {
|
|
122
|
+
close = i;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else
|
|
127
|
+
i++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (close < 0)
|
|
131
|
+
return null;
|
|
132
|
+
if (!/^[ \t]*:(?:[ \t]|$)/.test(c.slice(close + 1)))
|
|
133
|
+
return null;
|
|
134
|
+
const inner = c.slice(1, close);
|
|
135
|
+
return quote === '"' ? inner.replace(/\\(["\\])/g, "$1") : inner.replace(/''/g, "'");
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The frontmatter block's raw (fences included, no trailing line ending) → its
|
|
139
|
+
* parts, in order. The parts always concatenate back to `raw` byte for byte:
|
|
140
|
+
* every line of the raw lands in exactly one part.
|
|
141
|
+
*
|
|
142
|
+
* - part 0 is the opening fence line with its line ending, key `null`;
|
|
143
|
+
* - the last part is the closing fence line (no line ending), key `null`;
|
|
144
|
+
* - between them, the body lines: every line before the first entry line is
|
|
145
|
+
* one part with key `null` (comments, blank lines, or — when no line opens
|
|
146
|
+
* an entry: a bare scalar, a sequence — the whole body); then one part per
|
|
147
|
+
* entry, from the first of the lead lines (blank / column-0 comment)
|
|
148
|
+
* immediately preceding its key line (never reaching back past the
|
|
149
|
+
* previous entry's key line) through the line before the next entry's
|
|
150
|
+
* lead, the last entry ending before the trailing lead run; the trailing
|
|
151
|
+
* lead run (blank / comment lines after the last entry's content) is one
|
|
152
|
+
* part with key `null`.
|
|
153
|
+
*
|
|
154
|
+
* A raw with fewer than two lines (not a frontmatter block) is one part.
|
|
155
|
+
*/
|
|
156
|
+
export function splitFrontmatterRaw(raw) {
|
|
157
|
+
const lines = splitLines(raw);
|
|
158
|
+
if (lines.length < 2)
|
|
159
|
+
return [{ key: null, bytes: raw }];
|
|
160
|
+
const open = lines[0];
|
|
161
|
+
const close = lines[lines.length - 1];
|
|
162
|
+
const body = lines.slice(1, -1);
|
|
163
|
+
const parts = [{ key: null, bytes: open }];
|
|
164
|
+
const entryAt = body.map(entryKeyOf);
|
|
165
|
+
const entryIdx = [];
|
|
166
|
+
entryAt.forEach((k, i) => {
|
|
167
|
+
if (k !== null)
|
|
168
|
+
entryIdx.push(i);
|
|
169
|
+
});
|
|
170
|
+
if (entryIdx.length === 0) {
|
|
171
|
+
if (body.length > 0)
|
|
172
|
+
parts.push({ key: null, bytes: body.join("") });
|
|
173
|
+
parts.push({ key: null, bytes: close });
|
|
174
|
+
return parts;
|
|
175
|
+
}
|
|
176
|
+
// Where each entry's part starts: its lead run, bounded by the previous
|
|
177
|
+
// entry's key line; the first entry starts at its key line (the lines
|
|
178
|
+
// before it are the leading null part).
|
|
179
|
+
const starts = entryIdx.map((i, n) => {
|
|
180
|
+
if (n === 0)
|
|
181
|
+
return i;
|
|
182
|
+
const floor = entryIdx[n - 1] + 1;
|
|
183
|
+
let j = i;
|
|
184
|
+
while (j - 1 >= floor && isLeadLine(body[j - 1]))
|
|
185
|
+
j--;
|
|
186
|
+
return j;
|
|
187
|
+
});
|
|
188
|
+
// The trailing null part: the lead run after the last entry's key line.
|
|
189
|
+
let tail = body.length;
|
|
190
|
+
const lastKey = entryIdx[entryIdx.length - 1];
|
|
191
|
+
while (tail - 1 > lastKey && isLeadLine(body[tail - 1]))
|
|
192
|
+
tail--;
|
|
193
|
+
if (starts[0] > 0)
|
|
194
|
+
parts.push({ key: null, bytes: body.slice(0, starts[0]).join("") });
|
|
195
|
+
entryIdx.forEach((i, n) => {
|
|
196
|
+
const end = n + 1 < starts.length ? starts[n + 1] : tail;
|
|
197
|
+
parts.push({ key: entryAt[i], bytes: body.slice(starts[n], end).join("") });
|
|
198
|
+
});
|
|
199
|
+
if (tail < body.length)
|
|
200
|
+
parts.push({ key: null, bytes: body.slice(tail).join("") });
|
|
201
|
+
parts.push({ key: null, bytes: close });
|
|
202
|
+
return parts;
|
|
203
|
+
}
|
|
204
|
+
/** The inverse of {@link splitFrontmatterRaw}: the parts' bytes concatenated. */
|
|
205
|
+
export function joinFrontmatterParts(parts) {
|
|
206
|
+
return parts.map((p) => p.bytes).join("");
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The frontmatter block's raw and its trailing trivia, out of a document's
|
|
210
|
+
* `frontmatter_raw` (`raw + docs.frontmatter_trivia`, spec/mutate §2): the
|
|
211
|
+
* closing fence is the first line after the opening one that is `---`
|
|
212
|
+
* followed only by spaces/tabs (the format layer's rule); the raw ends with
|
|
213
|
+
* that line's content, the trivia is everything after. `null` when no closing
|
|
214
|
+
* fence is found (not a frontmatter the format layer would have produced).
|
|
215
|
+
*/
|
|
216
|
+
export function splitFrontmatterTrivia(frontmatterRaw) {
|
|
217
|
+
const lines = splitLines(frontmatterRaw);
|
|
218
|
+
for (let i = 1; i < lines.length; i++) {
|
|
219
|
+
const line = lines[i];
|
|
220
|
+
if (/^---[ \t]*$/.test(content(line))) {
|
|
221
|
+
const upto = lines.slice(0, i).join("") + content(line);
|
|
222
|
+
return { raw: upto, trivia: frontmatterRaw.slice(upto.length) };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
/** The lines of a part that LEAD its key line (blank / column-0 comment lines), as one string; `""` for a part without a key. */
|
|
228
|
+
export function leadOf(part) {
|
|
229
|
+
if (part.key === null)
|
|
230
|
+
return "";
|
|
231
|
+
const lines = splitLines(part.bytes);
|
|
232
|
+
let n = 0;
|
|
233
|
+
while (n < lines.length - 1 && isLeadLine(lines[n]))
|
|
234
|
+
n++;
|
|
235
|
+
return lines.slice(0, n).join("");
|
|
236
|
+
}
|
|
237
|
+
//# sourceMappingURL=frontmatter-parts.js.map
|
package/dist/src/mutate/load.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentFrontmatterRaw } from "../core/read/document.js";
|
|
1
2
|
export function loadMutDoc(db, docId) {
|
|
2
3
|
const doc = db.prepare("SELECT doc_id, path, format, leading_trivia, frontmatter_trivia, current_rev FROM docs WHERE doc_id = ? AND deleted_commit IS NULL").get(docId);
|
|
3
4
|
if (!doc)
|
|
@@ -44,15 +45,14 @@ export function loadMutDoc(db, docId) {
|
|
|
44
45
|
}
|
|
45
46
|
let frontmatterRaw = null;
|
|
46
47
|
if (doc.current_rev) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
48
|
+
// The current revision's frontmatter, resolved when stored as a delta
|
|
49
|
+
// (spec/store §6.1, 16.0). Append the exact stored separator
|
|
50
|
+
// (docs.frontmatter_trivia) so the write path round-trips byte-for-byte.
|
|
51
|
+
// Ingest always captures it when a frontmatter block exists, so the ?? ""
|
|
52
|
+
// is just a type guard.
|
|
53
|
+
const fm = currentFrontmatterRaw(db, doc.current_rev);
|
|
54
|
+
if (fm !== null)
|
|
55
|
+
frontmatterRaw = fm + (doc.frontmatter_trivia ?? "");
|
|
56
56
|
}
|
|
57
57
|
return {
|
|
58
58
|
docId: doc.doc_id,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OqxError, parse } from "@omgbase/oqx";
|
|
2
|
-
import { checkRootSpellings, oqxRunQuery, reservedAlias } from "../oqx-js/run.js";
|
|
2
|
+
import { TUPLE_HINT, bracketListAsTuple, checkRootSpellings, oqxRunQuery, reservedAlias } from "../oqx-js/run.js";
|
|
3
3
|
import { rootTarget } from "../oqx-js/planner.js";
|
|
4
4
|
import { FilterInvalid } from "../search/cel/parser.js";
|
|
5
5
|
import { EFFECT_OPS, EXPRESSION_LEADER, bareTarget, effectInvalid, findDocumentTarget, isNewSymbol, isTargetId, looksLikePath, validateEffect, validateTarget, } from "./effects.js";
|
|
@@ -545,8 +545,14 @@ function admitSelector(select, request) {
|
|
|
545
545
|
checkRootSpellings(raw);
|
|
546
546
|
}
|
|
547
547
|
catch (e) {
|
|
548
|
-
if (e instanceof OqxError)
|
|
549
|
-
|
|
548
|
+
if (e instanceof OqxError) {
|
|
549
|
+
// (spec/surface 4.11) `id in ["a", "b"]` — the bracket list rewritten to a
|
|
550
|
+
// tuple is the `fix` (the same request over the corrected selector).
|
|
551
|
+
const rewritten = bracketListAsTuple(select, e.message);
|
|
552
|
+
if (rewritten === null)
|
|
553
|
+
throw selectionInvalid(`select: ${e.message}`);
|
|
554
|
+
throw selectionInvalid(`select: ${e.message}${TUPLE_HINT}`, request === undefined ? {} : { fix: fixRequest({ ...request, select: rewritten }) });
|
|
555
|
+
}
|
|
550
556
|
if (e instanceof FilterInvalid)
|
|
551
557
|
throw selectionInvalid(`select: ${e.message}`);
|
|
552
558
|
throw e;
|
|
@@ -451,6 +451,18 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
451
451
|
return tag(load(), "blocks");
|
|
452
452
|
return lazyOwner({ block_id: row.block_id, doc_id: row.doc_id, __path: row.__path }, "blocks", load);
|
|
453
453
|
};
|
|
454
|
+
// `block` on a link row (spec/surface §1.2, 4.11): the SOURCE block the link
|
|
455
|
+
// was written in — the blocks row `block_id` (the edge's `src_block`) names,
|
|
456
|
+
// in the source document (`__path` is already the block's document path) —
|
|
457
|
+
// mirroring `doc`; absent when the link has none (a frontmatter link).
|
|
458
|
+
const linkBlock = (row) => {
|
|
459
|
+
if (!row.src_block)
|
|
460
|
+
return undefined;
|
|
461
|
+
const load = () => 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.src_block);
|
|
462
|
+
if (typeof row.__path !== "string")
|
|
463
|
+
return tag(load(), "blocks");
|
|
464
|
+
return lazyOwner({ block_id: row.src_block, doc_id: row.src_doc, __path: row.__path }, "blocks", load);
|
|
465
|
+
};
|
|
454
466
|
// ---- engine attributes -----------------------------------------------------
|
|
455
467
|
// Bare, snake_case, winning on collision (see the name rule above; spec/surface
|
|
456
468
|
// §1.2, 3.0). `path` (and the edge field `dst_path`) is the REFERENCE form
|
|
@@ -628,6 +640,8 @@ export function makeStoreContext(store, repoId, opts = {}) {
|
|
|
628
640
|
return owningBlock(r) ?? ABSENT_SINGLE;
|
|
629
641
|
if (t === "links" && key === "dst")
|
|
630
642
|
return destinationDoc(r) ?? ABSENT_SINGLE;
|
|
643
|
+
if (t === "links" && key === "block")
|
|
644
|
+
return linkBlock(r) ?? ABSENT_SINGLE;
|
|
631
645
|
// relations.
|
|
632
646
|
const rels = rel[t];
|
|
633
647
|
if (rels[key])
|
package/dist/src/oqx-js/names.js
CHANGED
|
@@ -41,7 +41,7 @@ export const HANDLES = {
|
|
|
41
41
|
docs: { doc: "docs" },
|
|
42
42
|
blocks: { block: "blocks", doc: "docs" },
|
|
43
43
|
nodes: { section: "nodes", doc: "docs", block: "blocks" },
|
|
44
|
-
links: { doc: "docs", dst: "docs" },
|
|
44
|
+
links: { doc: "docs", dst: "docs", block: "blocks" },
|
|
45
45
|
revisions: { doc: "docs" },
|
|
46
46
|
};
|
|
47
47
|
/** The kinds whose row shape is closed: no authored data reaches a row by bare name. */
|
package/dist/src/oqx-js/run.d.ts
CHANGED
|
@@ -69,6 +69,11 @@ export interface OqxOptions {
|
|
|
69
69
|
* the default of the AST entry (`oqxRunQuery`), whose caller is the write
|
|
70
70
|
* selector compiler, where a name no row carries would select every row. */
|
|
71
71
|
unknownNames?: UnknownNames;
|
|
72
|
+
/** The `query` call as the caller received it (spec/surface 4.11): the
|
|
73
|
+
* arguments a corrected call is spelled from when a parse error can be fixed
|
|
74
|
+
* (`data.fix`, the bracket-list rule below). Absent, the message still gains
|
|
75
|
+
* its hint and no `fix` is composed. */
|
|
76
|
+
request?: Record<string, unknown>;
|
|
72
77
|
}
|
|
73
78
|
export declare function rootPathLiterals(e: Expr): Expr;
|
|
74
79
|
export declare function checkRootSpellings(q: Query): void;
|
|
@@ -78,6 +83,9 @@ export declare function reservedAlias(items: readonly SelectItem[]): FilterInval
|
|
|
78
83
|
export declare const BARE_NEEDS_SELECT_MESSAGE = "`bare` needs a `select` \u2014 without a projection a hit is only its envelope";
|
|
79
84
|
export declare const BARE_WITH_VALUES_MESSAGE = "`bare` is redundant with `values` \u2014 `values` is already the bare-values form; drop one";
|
|
80
85
|
export declare function oqxRun(store: Store, repoId: string, source: string, opts?: OqxOptions): OqxResult;
|
|
86
|
+
export declare const TUPLE_HINT = "; a list is written as a tuple: (\u2026)";
|
|
87
|
+
/** The source with the bracket list at the error's offset rewritten to a tuple, or null when the rule does not apply. */
|
|
88
|
+
export declare function bracketListAsTuple(source: string, message: string): string | null;
|
|
81
89
|
/**
|
|
82
90
|
* The AST entry (4.1): run an already parsed query — the source path above
|
|
83
91
|
* after `parse`, and the selector compiler (`mutate/select.ts`), which appends
|
package/dist/src/oqx-js/run.js
CHANGED
|
@@ -14,6 +14,7 @@ import { FilterInvalid } from "../search/cel/parser.js";
|
|
|
14
14
|
import { float32ToBlob } from "../core/vec.js";
|
|
15
15
|
import { encodeCursor as encodeKeyset, decodePathCursor } from "../core/cursor.js";
|
|
16
16
|
import { referencePath } from "../core/paths.js";
|
|
17
|
+
import { canonicalJson } from "../mutate/canonical.js";
|
|
17
18
|
// Row-scoped domain functions: authored as free calls (`text("x")`) that
|
|
18
19
|
// implicitly reference the current row. `@omgbase/oqx` free functions receive no
|
|
19
20
|
// row, so we rewrite them to `$self.fn(…)` (a method whose receiver is the row)
|
|
@@ -312,10 +313,95 @@ export function oqxRun(store, repoId, source, opts = {}) {
|
|
|
312
313
|
catch (e) {
|
|
313
314
|
// Normalize the library's errors to omgbase's contract (filter_invalid).
|
|
314
315
|
if (e instanceof OqxError)
|
|
315
|
-
throw
|
|
316
|
+
throw parseInvalid(source, e, opts.request);
|
|
316
317
|
throw e;
|
|
317
318
|
}
|
|
318
319
|
}
|
|
320
|
+
// ---- the bracket-list fix (spec/surface §4 "Error data", 4.11) ----------------------
|
|
321
|
+
//
|
|
322
|
+
// OQX has no list literal; a tuple `("a", "b")` is the collection (`spec/oqx`
|
|
323
|
+
// SEMANTICS §3b), so `id in ["a", "b"]` — the SQL habit — is the parse error
|
|
324
|
+
// `unexpected '[' — expected a value (at offset N)`. When the source at N opens a
|
|
325
|
+
// bracketed comma list of LITERALS closed by `]` (two or more members, each a
|
|
326
|
+
// string, number, `true`, `false` or `null`; nothing nested), the message gains
|
|
327
|
+
// `; a list is written as a tuple: (…)` and, when the request is known, the
|
|
328
|
+
// error carries `fix`: the same request with that `[…]` rewritten to `(…)`.
|
|
329
|
+
// Anything else at N (an identifier member, a nested bracket, one member, an
|
|
330
|
+
// unclosed list) is the parser's error as it stands.
|
|
331
|
+
const BRACKET_ERROR = /^unexpected '\[' — expected a value \(at offset (\d+)\)$/;
|
|
332
|
+
export const TUPLE_HINT = "; a list is written as a tuple: (…)";
|
|
333
|
+
/** The source with the bracket list at the error's offset rewritten to a tuple, or null when the rule does not apply. */
|
|
334
|
+
export function bracketListAsTuple(source, message) {
|
|
335
|
+
const m = BRACKET_ERROR.exec(message);
|
|
336
|
+
if (!m)
|
|
337
|
+
return null;
|
|
338
|
+
const cps = Array.from(source);
|
|
339
|
+
const open = Number(m[1]);
|
|
340
|
+
if (cps[open] !== "[")
|
|
341
|
+
return null;
|
|
342
|
+
let i = open + 1;
|
|
343
|
+
let members = 0;
|
|
344
|
+
for (;;) {
|
|
345
|
+
i = skipSpaces(cps, i);
|
|
346
|
+
const end = literalEnd(cps, i);
|
|
347
|
+
if (end === null)
|
|
348
|
+
return null;
|
|
349
|
+
members++;
|
|
350
|
+
i = skipSpaces(cps, end);
|
|
351
|
+
if (cps[i] === ",") {
|
|
352
|
+
i++;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (cps[i] === "]")
|
|
356
|
+
break;
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
if (members < 2)
|
|
360
|
+
return null;
|
|
361
|
+
cps[open] = "(";
|
|
362
|
+
cps[i] = ")";
|
|
363
|
+
return cps.join("");
|
|
364
|
+
}
|
|
365
|
+
function skipSpaces(cps, i) {
|
|
366
|
+
while (i < cps.length && /\s/.test(cps[i]))
|
|
367
|
+
i++;
|
|
368
|
+
return i;
|
|
369
|
+
}
|
|
370
|
+
/** The index just past one OQX literal starting at `i` (a string in either quote with `\<c>` escapes, an unsigned number, `true`/`false`/`null`), or null. */
|
|
371
|
+
function literalEnd(cps, i) {
|
|
372
|
+
const c = cps[i];
|
|
373
|
+
if (c === undefined)
|
|
374
|
+
return null;
|
|
375
|
+
if (c === '"' || c === "'") {
|
|
376
|
+
let j = i + 1;
|
|
377
|
+
while (j < cps.length) {
|
|
378
|
+
if (cps[j] === "\\")
|
|
379
|
+
j += 2;
|
|
380
|
+
else if (cps[j] === c)
|
|
381
|
+
return j + 1;
|
|
382
|
+
else
|
|
383
|
+
j++;
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
if (/[0-9]/.test(c)) {
|
|
388
|
+
const m = /^[0-9]+(\.[0-9]+)?([eE][+-]?[0-9]+)?/.exec(cps.slice(i).join(""));
|
|
389
|
+
return m ? i + Array.from(m[0]).length : null;
|
|
390
|
+
}
|
|
391
|
+
for (const word of ["true", "false", "null"]) {
|
|
392
|
+
if (cps.slice(i, i + word.length).join("") === word && !/[A-Za-z0-9_$]/.test(cps[i + word.length] ?? ""))
|
|
393
|
+
return i + word.length;
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
/** The surface's `filter_invalid` for a parse error: the library's message, plus the tuple hint and `fix` when the bracket rule applies. */
|
|
398
|
+
function parseInvalid(source, e, request) {
|
|
399
|
+
const rewritten = bracketListAsTuple(source, e.message);
|
|
400
|
+
if (rewritten === null)
|
|
401
|
+
return new FilterInvalid(e.message, "OQX");
|
|
402
|
+
const fix = request !== undefined ? canonicalJson({ ...request, query: rewritten }) : undefined;
|
|
403
|
+
return new FilterInvalid(e.message + TUPLE_HINT, "OQX", fix);
|
|
404
|
+
}
|
|
319
405
|
function oqxRunInner(store, repoId, source, opts) {
|
|
320
406
|
const raw = parse(source);
|
|
321
407
|
const reserved = reservedAlias(raw.select);
|
|
@@ -56,7 +56,7 @@ export const NON_PROPERTY_NAMES = {
|
|
|
56
56
|
docs: new Set(["doc", "nodes", "blocks", "out_links", "in_links", "revisions", "data", "frontmatter", "inline"]),
|
|
57
57
|
blocks: new Set(["block", "doc", "children", "nodes", "out_links", "section", "attrs"]),
|
|
58
58
|
nodes: new Set(["section", "doc", "block", "blocks", "subsections", "children", "attrs"]),
|
|
59
|
-
links: new Set(["doc", "dst"]),
|
|
59
|
+
links: new Set(["doc", "dst", "block"]),
|
|
60
60
|
};
|
|
61
61
|
// ---- engine attributes ------------------------------------------------------
|
|
62
62
|
// The engine attributes (spec/surface §1.2, 3.0: bare, snake_case, winning on
|
|
@@ -365,6 +365,26 @@ export function translateOperand(e, ctx) {
|
|
|
365
365
|
return text(`(SELECT bb.${k} FROM blocks bb WHERE bb.block_id = ${self}.block_id)`);
|
|
366
366
|
return null;
|
|
367
367
|
}
|
|
368
|
+
// block.<x> reach-through from a link (spec/surface §1.2, 4.11): the SOURCE
|
|
369
|
+
// block, keyed by the row's own `src_block` — `block.id` is that FK column
|
|
370
|
+
// (NULL for a frontmatter link, exactly where `block` is absent in memory);
|
|
371
|
+
// everything else is a primary-key probe of the block row, NULL for the
|
|
372
|
+
// same rows (`block.path` reads the scan's joined source document inside
|
|
373
|
+
// the probe, so it too is NULL when there is no block). `raw`, `hash`,
|
|
374
|
+
// `updated_at`, `attrs` and the relations stay residual.
|
|
375
|
+
if (head === "block" && target === "links" && rest.length === 1) {
|
|
376
|
+
const k = rest[0];
|
|
377
|
+
const probe = (col) => text(`(SELECT ${col} FROM blocks bb WHERE bb.block_id = ${self}.src_block)`);
|
|
378
|
+
if (k === "id")
|
|
379
|
+
return text(`${self}.src_block`);
|
|
380
|
+
if (k === "path")
|
|
381
|
+
return probe(`'/' || ${doc}.path`);
|
|
382
|
+
if (k === "doc_id")
|
|
383
|
+
return probe("bb.doc_id");
|
|
384
|
+
if (k === "type" || k === "text")
|
|
385
|
+
return probe(`bb.${k}`);
|
|
386
|
+
return null;
|
|
387
|
+
}
|
|
368
388
|
return null;
|
|
369
389
|
}
|
|
370
390
|
case "call": {
|
|
@@ -2,6 +2,8 @@ import type { Node } from "./ast.js";
|
|
|
2
2
|
export declare class FilterInvalid extends Error {
|
|
3
3
|
reason: string;
|
|
4
4
|
hint: string;
|
|
5
|
-
|
|
5
|
+
/** (spec/surface 4.11) a literal corrected call, canonical JSON, when the surface can spell one (`data.fix`). */
|
|
6
|
+
fix: string | undefined;
|
|
7
|
+
constructor(reason: string, hint?: string, fix?: string);
|
|
6
8
|
}
|
|
7
9
|
export declare function parseFilter(src: string): Node;
|
|
@@ -4,10 +4,13 @@ import { lex, LexError } from "./lexer.js";
|
|
|
4
4
|
export class FilterInvalid extends Error {
|
|
5
5
|
reason;
|
|
6
6
|
hint;
|
|
7
|
-
|
|
7
|
+
/** (spec/surface 4.11) a literal corrected call, canonical JSON, when the surface can spell one (`data.fix`). */
|
|
8
|
+
fix;
|
|
9
|
+
constructor(reason, hint = "see query-language.md §3", fix) {
|
|
8
10
|
super(reason);
|
|
9
11
|
this.reason = reason;
|
|
10
12
|
this.hint = hint;
|
|
13
|
+
this.fix = fix;
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
const RELOPS = new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
@@ -2,13 +2,13 @@ export declare const SPEC_VERSIONS: {
|
|
|
2
2
|
readonly oqx: "0.25";
|
|
3
3
|
readonly format: "0.2";
|
|
4
4
|
readonly reconcile: "2.3";
|
|
5
|
-
readonly store: "
|
|
6
|
-
readonly properties: "1.
|
|
5
|
+
readonly store: "16.0";
|
|
6
|
+
readonly properties: "1.2";
|
|
7
7
|
readonly graph: "1.1";
|
|
8
8
|
readonly search: "2.0";
|
|
9
|
-
readonly mutate: "3.
|
|
9
|
+
readonly mutate: "3.6";
|
|
10
10
|
readonly sync: "2.2";
|
|
11
|
-
readonly surface: "4.
|
|
11
|
+
readonly surface: "4.11";
|
|
12
12
|
readonly cli: "4.6";
|
|
13
13
|
};
|
|
14
14
|
export type SpecName = keyof typeof SPEC_VERSIONS;
|
|
@@ -13,13 +13,13 @@ export const SPEC_VERSIONS = {
|
|
|
13
13
|
oqx: LANGUAGE_VERSION,
|
|
14
14
|
format: "0.2",
|
|
15
15
|
reconcile: "2.3",
|
|
16
|
-
store: "
|
|
17
|
-
properties: "1.
|
|
16
|
+
store: "16.0",
|
|
17
|
+
properties: "1.2",
|
|
18
18
|
graph: "1.1",
|
|
19
19
|
search: "2.0",
|
|
20
|
-
mutate: "3.
|
|
20
|
+
mutate: "3.6",
|
|
21
21
|
sync: "2.2",
|
|
22
|
-
surface: "4.
|
|
22
|
+
surface: "4.11",
|
|
23
23
|
cli: "4.6",
|
|
24
24
|
};
|
|
25
25
|
//# sourceMappingURL=spec-versions.js.map
|