@omgbase/core 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/core/cursor.d.ts +13 -1
- package/dist/src/core/cursor.js +21 -2
- package/dist/src/core/paths.d.ts +10 -0
- package/dist/src/core/paths.js +34 -0
- package/dist/src/core/read/document.d.ts +1 -0
- package/dist/src/core/read/document.js +3 -2
- package/dist/src/core/read/reader.d.ts +6 -3
- package/dist/src/core/read/reader.js +31 -16
- package/dist/src/graph/history.js +16 -10
- package/dist/src/graph/link-health.d.ts +2 -1
- package/dist/src/graph/link-health.js +13 -7
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +3 -1
- package/dist/src/mcp/errors.js +1 -1
- package/dist/src/mcp/graph.js +50 -15
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +93 -26
- package/dist/src/mcp/server.js +32 -28
- package/dist/src/oqx-js/context.d.ts +2 -1
- package/dist/src/oqx-js/context.js +93 -29
- package/dist/src/oqx-js/planner.d.ts +8 -1
- package/dist/src/oqx-js/planner.js +21 -8
- package/dist/src/oqx-js/root-row.d.ts +21 -0
- package/dist/src/oqx-js/root-row.js +55 -0
- package/dist/src/oqx-js/run.d.ts +3 -1
- package/dist/src/oqx-js/run.js +127 -163
- package/dist/src/oqx-js/sql/translate.d.ts +7 -0
- package/dist/src/oqx-js/sql/translate.js +42 -11
- package/dist/src/oqx-js/store-index.js +12 -3
- package/dist/src/spec-versions.d.ts +3 -3
- package/dist/src/spec-versions.js +2 -2
- package/dist/src/surface-paths.d.ts +33 -0
- package/dist/src/surface-paths.js +75 -0
- package/package.json +2 -2
|
@@ -18,15 +18,14 @@ import { ROWS_ROOT, partitionPushable, residualQuery } from "@omgbase/oqx";
|
|
|
18
18
|
import { makeStoreContext, tagRows } from "./context.js";
|
|
19
19
|
import { translatePredicate, RESERVED_DOC_BASENAMES } from "./sql/translate.js";
|
|
20
20
|
import { ALIAS, COLS, FROM, ORDER, guards } from "./sql/scan.js";
|
|
21
|
+
import { REACH_THROUGH, TARGETS, isTargetRelationOf } from "./root-row.js";
|
|
21
22
|
// The root collection a query scans, if it is a bare `docs|blocks|nodes|edges`
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// source at the root scope (else null — not a pushable shape; a nested `^docs`
|
|
24
|
+
// is a block's receiver, never the query's source). run.ts also asks: a bare
|
|
25
|
+
// root scan's rows are store rows by construction.
|
|
26
|
+
export function rootTarget(source) {
|
|
25
27
|
if (source.kind === "ident" && TARGETS.has(source.name))
|
|
26
28
|
return source.name;
|
|
27
|
-
if (source.kind === "member" && source.recv.kind === "ident" && source.recv.name === "$repo" && TARGETS.has(source.name)) {
|
|
28
|
-
return source.name;
|
|
29
|
-
}
|
|
30
29
|
return null;
|
|
31
30
|
}
|
|
32
31
|
// ---- the residual walk (decline (c)) -----------------------------------------
|
|
@@ -42,6 +41,12 @@ function rootTarget(source) {
|
|
|
42
41
|
* • a bare reserved docs basename (`id path updated_at content_hash body`): as
|
|
43
42
|
* an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
|
|
44
43
|
* on any target at any depth (the reach-through row is a doc)
|
|
44
|
+
* • a bare target name (`docs blocks nodes edges`, root-row.ts) read as a
|
|
45
|
+
* property: as an `ident` at the ROOT scope when it is not a relation of
|
|
46
|
+
* the target's rows (`blocks` on blocks, `nodes` on nodes, any on edges),
|
|
47
|
+
* as an `ident` at any depth below (the rows there are unknown), or as
|
|
48
|
+
* `<head>.<name>` unless the head is a reach-through whose row has that
|
|
49
|
+
* relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
|
|
45
50
|
*
|
|
46
51
|
* A residual made only of comparisons, logical operators, `in`/ranges, `!`,
|
|
47
52
|
* literals, bindings and plain reads (including exists/none/count/collect/first
|
|
@@ -100,13 +105,21 @@ function exprMayRaise(e, target, root) {
|
|
|
100
105
|
switch (e.kind) {
|
|
101
106
|
case "call": return true;
|
|
102
107
|
case "outer": return true;
|
|
103
|
-
case "ident":
|
|
108
|
+
case "ident":
|
|
109
|
+
if (TARGETS.has(e.name))
|
|
110
|
+
return !root || !isTargetRelationOf(target, e.name);
|
|
111
|
+
return root && target === "docs" && RESERVED_DOC_BASENAMES.has(e.name);
|
|
104
112
|
case "member":
|
|
105
113
|
if (e.recv.kind === "ident" && e.recv.name === "doc" && RESERVED_DOC_BASENAMES.has(e.name))
|
|
106
114
|
return true;
|
|
115
|
+
if (TARGETS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
|
|
116
|
+
return true;
|
|
107
117
|
return exprMayRaise(e.recv, target, root);
|
|
108
|
-
case "index": return exprMayRaise(e.recv, target, root) || exprMayRaise(e.index, target, root);
|
|
109
118
|
case "unary": return exprMayRaise(e.expr, target, root);
|
|
119
|
+
// `x!` raises when absent; a value-position directive may `single`-fail or
|
|
120
|
+
// read anything its block reads.
|
|
121
|
+
case "required": return true;
|
|
122
|
+
case "op": return opMayRaise(e, target, root);
|
|
110
123
|
case "binary":
|
|
111
124
|
case "logical":
|
|
112
125
|
case "in":
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type Target = "docs" | "blocks" | "nodes" | "edges";
|
|
2
|
+
export declare const TARGETS: ReadonlySet<string>;
|
|
3
|
+
/** The target names that are a relation of each target's rows (§1.2). */
|
|
4
|
+
export declare const TARGET_RELATIONS: Readonly<Record<Target, ReadonlySet<string>>>;
|
|
5
|
+
/** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`). */
|
|
6
|
+
export declare const REACH_THROUGH: Readonly<Record<string, Target>>;
|
|
7
|
+
export declare const REPO_REMOVED_MESSAGE = "`$repo` was removed in surface 2.0 \u2014 reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^$id` / `0^$id`";
|
|
8
|
+
/**
|
|
9
|
+
* The bare-target error. `depth` is the scope depth the name was read at when
|
|
10
|
+
* known statically (the carets in the hint count it); `null` when raised at read
|
|
11
|
+
* time, where the hint spells out the rule instead.
|
|
12
|
+
*/
|
|
13
|
+
export declare function bareTargetMessage(name: string, depth: number | null): string;
|
|
14
|
+
/**
|
|
15
|
+
* The past-the-root error: `^docs` at the root scope (more carets than there
|
|
16
|
+
* are enclosing scopes) would read absent and count 0 — refused, naming the
|
|
17
|
+
* bare spelling. `levels` is the caret count written.
|
|
18
|
+
*/
|
|
19
|
+
export declare function pastRootMessage(name: string, levels: number): string;
|
|
20
|
+
/** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
|
|
21
|
+
export declare function isTargetRelationOf(target: Target | undefined, name: string): boolean;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The root row (spec/surface §1.1, 2.0; spec/oqx 0.18). The repository is OQX's
|
|
2
|
+
// root row: from a top-level row `^docs` reaches the documents (one caret per
|
|
3
|
+
// enclosing block, or the absolute `0^docs`), `^$id` the repository id, and the
|
|
4
|
+
// root object (`^$it`) exposes the four collections. Two spellings are refused
|
|
5
|
+
// as `filter_invalid`, with the messages below, in both engines:
|
|
6
|
+
//
|
|
7
|
+
// • `$repo` (removed in 2.0) anywhere — bare, as a member head (`$repo.docs`),
|
|
8
|
+
// `^$repo`, `0^$repo` — statically, before any row is read, so the error is
|
|
9
|
+
// deterministic whether or not a row would have reached it (run.ts
|
|
10
|
+
// `checkRootSpellings`);
|
|
11
|
+
// • a bare target name at scope depth ≥ 1 (inside a block, or in a top-level
|
|
12
|
+
// row's clauses), where a bare name reads a property of the current row,
|
|
13
|
+
// which has none: `docs` and `edges` are no row's relation, so they are
|
|
14
|
+
// refused statically by the same walk; `blocks` and `nodes` ARE relations
|
|
15
|
+
// of some rows (`docs.blocks`, `docs.nodes`, `blocks.nodes`,
|
|
16
|
+
// `section.blocks`), so the context raises the same error at read time on a
|
|
17
|
+
// row that lacks the relation (context.ts). A frontmatter key named after a
|
|
18
|
+
// target is unreachable by bare name (use `frontmatter.<k>`).
|
|
19
|
+
export const TARGETS = new Set(["docs", "blocks", "nodes", "edges"]);
|
|
20
|
+
/** The target names that are a relation of each target's rows (§1.2). */
|
|
21
|
+
export const TARGET_RELATIONS = {
|
|
22
|
+
docs: new Set(["nodes", "blocks"]),
|
|
23
|
+
blocks: new Set(["nodes"]),
|
|
24
|
+
nodes: new Set(["blocks"]),
|
|
25
|
+
edges: new Set(),
|
|
26
|
+
};
|
|
27
|
+
/** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`). */
|
|
28
|
+
export const REACH_THROUGH = { doc: "docs", block: "blocks", section: "nodes" };
|
|
29
|
+
const NOUN = { docs: "documents", blocks: "blocks", nodes: "nodes", edges: "edges" };
|
|
30
|
+
export const REPO_REMOVED_MESSAGE = "`$repo` was removed in surface 2.0 — reach the repository's collections through the root row: `^docs` from a top-level row (one caret per enclosing block, or the absolute `0^docs`), and the repository id as `^$id` / `0^$id`";
|
|
31
|
+
/**
|
|
32
|
+
* The bare-target error. `depth` is the scope depth the name was read at when
|
|
33
|
+
* known statically (the carets in the hint count it); `null` when raised at read
|
|
34
|
+
* time, where the hint spells out the rule instead.
|
|
35
|
+
*/
|
|
36
|
+
export function bareTargetMessage(name, depth) {
|
|
37
|
+
const t = name;
|
|
38
|
+
const carets = "^".repeat(depth ?? 1);
|
|
39
|
+
const how = depth === null ? `; one caret per enclosing block, or the absolute \`0^${name}\`` : "";
|
|
40
|
+
return `\`${name}\` inside a block reads a property of the current row, which has none — did you mean \`${carets}${name}\` (the repository's ${NOUN[t]}${how})?`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The past-the-root error: `^docs` at the root scope (more carets than there
|
|
44
|
+
* are enclosing scopes) would read absent and count 0 — refused, naming the
|
|
45
|
+
* bare spelling. `levels` is the caret count written.
|
|
46
|
+
*/
|
|
47
|
+
export function pastRootMessage(name, levels) {
|
|
48
|
+
const t = name;
|
|
49
|
+
return `\`${"^".repeat(levels)}${name}\` reaches past the root — there is no enclosing row at this depth; at the top level the repository's ${NOUN[t]} are the bare \`${name}\` (\`${name} count { … }\`, \`from ${name}\`, \`entries(${name})\`)`;
|
|
50
|
+
}
|
|
51
|
+
/** Whether `name`, read as `<head>.<name>` or bare on a row of `target`, is one of that row's relations named after a target. */
|
|
52
|
+
export function isTargetRelationOf(target, name) {
|
|
53
|
+
return target !== undefined && TARGET_RELATIONS[target].has(name);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=root-row.js.map
|
package/dist/src/oqx-js/run.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Query } from "@omgbase/oqx";
|
|
1
|
+
import type { Query, Expr } from "@omgbase/oqx";
|
|
2
2
|
import type { Store } from "../core/store/store.js";
|
|
3
3
|
import type { SemanticVec } from "../search/cel/compile.js";
|
|
4
4
|
export type OqxConsumer = "collect" | "count" | "exists" | "none" | "first" | "single";
|
|
@@ -34,6 +34,8 @@ export interface OqxOptions {
|
|
|
34
34
|
* the differential conformance suite to prove planned == in-memory. */
|
|
35
35
|
plan?: boolean;
|
|
36
36
|
}
|
|
37
|
+
export declare function rootPathLiterals(e: Expr): Expr;
|
|
38
|
+
export declare function checkRootSpellings(q: Query): void;
|
|
37
39
|
export declare function collectSemanticPhrases(source: string): string[];
|
|
38
40
|
export declare function oqxRun(store: Store, repoId: string, source: string, opts?: OqxOptions): OqxResult;
|
|
39
41
|
export declare function oqxRunAsync(store: Store, repoId: string, source: string, opts?: OqxOptions, embedQuery?: EmbedQuery): Promise<OqxResult>;
|
package/dist/src/oqx-js/run.js
CHANGED
|
@@ -3,92 +3,79 @@
|
|
|
3
3
|
// shape the engine's result into omgbase's OqxResult (lean {id, path, …} hits,
|
|
4
4
|
// keyset pagination, count/exists scalars). Same signature + shape as the former
|
|
5
5
|
// in-tree compiler, so every caller and the corpus are unchanged.
|
|
6
|
-
import { parse, PlannedEngine, InMemoryEngine, OqxError } from "@omgbase/oqx";
|
|
6
|
+
import { parse, PlannedEngine, InMemoryEngine, OqxError, resolveAliases, transform, visit, build, semantics } from "@omgbase/oqx";
|
|
7
7
|
import { makeStoreContext, rowRef } from "./context.js";
|
|
8
|
-
import { SQLiteQueryPlanner } from "./planner.js";
|
|
8
|
+
import { SQLiteQueryPlanner, rootTarget } from "./planner.js";
|
|
9
|
+
import { REPO_REMOVED_MESSAGE, TARGETS, bareTargetMessage, pastRootMessage } from "./root-row.js";
|
|
9
10
|
import { FilterInvalid } from "../search/cel/parser.js";
|
|
10
11
|
import { float32ToBlob } from "../core/vec.js";
|
|
11
|
-
import { encodeCursor as encodeKeyset,
|
|
12
|
+
import { encodeCursor as encodeKeyset, decodePathCursor } from "../core/cursor.js";
|
|
13
|
+
import { referencePath } from "../core/paths.js";
|
|
12
14
|
// Row-scoped domain functions: authored as free calls (`text("x")`) that
|
|
13
15
|
// implicitly reference the current row. `@omgbase/oqx` free functions receive no
|
|
14
|
-
// row, so we rewrite them to `$self.fn(…)` (a method whose receiver is the row)
|
|
16
|
+
// row, so we rewrite them to `$self.fn(…)` (a method whose receiver is the row)
|
|
17
|
+
// — one `transform` over the AST (spec/oqx/AST.md §5), every block included.
|
|
18
|
+
// `refs(x)` is NOT here: it reads no row (its argument is the value), so it stays
|
|
19
|
+
// a free function served by the context's `callFunction`. The planner treats
|
|
20
|
+
// every call alike (translate declines it; `exprMayRaise` flags it), `has_edge`
|
|
21
|
+
// and `refs` included.
|
|
15
22
|
const ROW_FNS = new Set([
|
|
16
23
|
"text", "semantic", "under", "under_heading", "within", "under_kind",
|
|
17
24
|
"yaml_path", "json_pointer", "has_edge", "has_anchor", "child_count", "parent_type",
|
|
18
25
|
]);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
case "range":
|
|
30
|
-
return { ...e, lo: e.lo ? rewriteExpr(e.lo) : null, hi: e.hi ? rewriteExpr(e.hi) : null };
|
|
31
|
-
case "call": {
|
|
32
|
-
const recv = e.recv ? rewriteExpr(e.recv) : null;
|
|
33
|
-
const args = e.args.map(rewriteExpr);
|
|
34
|
-
if (recv === null && ROW_FNS.has(e.name))
|
|
35
|
-
return { kind: "call", recv: selfRef, name: e.name, args };
|
|
36
|
-
return { kind: "call", recv, name: e.name, args };
|
|
37
|
-
}
|
|
38
|
-
default: return e; // lit, ident, outer, binding
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function rewriteWhere(w) {
|
|
42
|
-
switch (w.kind) {
|
|
43
|
-
case "and": return { kind: "and", parts: w.parts.map(rewriteWhere) };
|
|
44
|
-
case "or": return { kind: "or", parts: w.parts.map(rewriteWhere) };
|
|
45
|
-
case "not": return { kind: "not", expr: rewriteWhere(w.expr) };
|
|
46
|
-
case "scalar": return { kind: "scalar", expr: rewriteExpr(w.expr) };
|
|
47
|
-
case "op": return rewriteOp(w);
|
|
48
|
-
}
|
|
26
|
+
// Paths on the surface are the reference form (spec/surface §1 "Paths", 2.0):
|
|
27
|
+
// `$path` and `$dst_path` read `/a/b.md`. A string literal a query compares
|
|
28
|
+
// with one of them — `$path == "a.md"`, `"a.md" != $dst_path`,
|
|
29
|
+
// `$path.startsWith("lab/")` — is rooted first, so both spellings match; a
|
|
30
|
+
// property or binding compared with a path is never touched (it is the
|
|
31
|
+
// author's value). The planner sees the rewritten tree too, so the pushed SQL
|
|
32
|
+
// and the in-memory engine agree by construction.
|
|
33
|
+
const PATH_INTRINSICS = new Set(["$path", "$dst_path"]);
|
|
34
|
+
function isPathRead(e) {
|
|
35
|
+
return (e.kind === "ident" || e.kind === "outer" || e.kind === "member") && PATH_INTRINSICS.has(e.name);
|
|
49
36
|
}
|
|
50
|
-
function
|
|
51
|
-
return { ...
|
|
37
|
+
function rootLiteral(e) {
|
|
38
|
+
return e.kind === "lit" && typeof e.value === "string" ? { ...e, value: referencePath(e.value) } : e;
|
|
52
39
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return items.map((it) => it.kind === "field" ? { ...it, expr: rewriteExpr(it.expr) } : { ...it, op: rewriteOp(it.op) });
|
|
40
|
+
export function rootPathLiterals(e) {
|
|
41
|
+
if (e.kind === "binary" && (e.op === "==" || e.op === "!=")) {
|
|
42
|
+
if (isPathRead(e.left))
|
|
43
|
+
return { ...e, right: rootLiteral(e.right) };
|
|
44
|
+
if (isPathRead(e.right))
|
|
45
|
+
return { ...e, left: rootLiteral(e.left) };
|
|
46
|
+
}
|
|
47
|
+
if (e.kind === "call" && e.recv !== null && e.name === "startsWith" && e.args.length === 1 && isPathRead(e.recv)) {
|
|
48
|
+
return { ...e, args: [rootLiteral(e.args[0])] };
|
|
49
|
+
}
|
|
50
|
+
return e;
|
|
65
51
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
52
|
+
// The root row (root-row.ts, spec/surface §1.1, 2.0): `$repo` anywhere, and a
|
|
53
|
+
// bare `docs`/`edges` at scope depth ≥ 1 (a property read of a row that has
|
|
54
|
+
// none), are refused before evaluation — one `visit` over the parsed tree, so
|
|
55
|
+
// the error does not depend on which rows the data happens to hold and the
|
|
56
|
+
// planned and in-memory paths agree trivially. `blocks`/`nodes` are relations of
|
|
57
|
+
// some rows and are judged by the context at read time (same message). A
|
|
58
|
+
// `^<target>` with more carets than enclosing scopes (`^docs count { }` at the
|
|
59
|
+
// top level) would read absent and count 0: refused too, naming the bare
|
|
60
|
+
// spelling.
|
|
61
|
+
export function checkRootSpellings(q) {
|
|
62
|
+
visit(q, {
|
|
63
|
+
enter(node, ctx) {
|
|
64
|
+
if (node.kind !== "ident" && node.kind !== "outer")
|
|
65
|
+
return;
|
|
66
|
+
if (node.name === "$repo")
|
|
67
|
+
throw new FilterInvalid(REPO_REMOVED_MESSAGE, "OQX");
|
|
68
|
+
if (node.kind === "ident" && ctx.depth >= 1 && (node.name === "docs" || node.name === "edges")) {
|
|
69
|
+
throw new FilterInvalid(bareTargetMessage(node.name, ctx.depth), "OQX");
|
|
70
|
+
}
|
|
71
|
+
if (node.kind === "outer" && TARGETS.has(node.name) && node.levels > ctx.depth) {
|
|
72
|
+
throw new FilterInvalid(pastRootMessage(node.name, node.levels), "OQX");
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
77
76
|
}
|
|
78
77
|
function rewriteQuery(q) {
|
|
79
|
-
return {
|
|
80
|
-
source: rewriteExpr(q.source),
|
|
81
|
-
from: q.from.map(rewriteExpr),
|
|
82
|
-
where: q.where ? rewriteWhere(q.where) : null,
|
|
83
|
-
select: rewriteSelect(q.select),
|
|
84
|
-
orderBy: q.orderBy ? q.orderBy.map((o) => ({ ...o, expr: rewriteExpr(o.expr) })) : null,
|
|
85
|
-
consumer: q.consumer,
|
|
86
|
-
follow: q.follow ? rewriteFollow(q.follow) : null,
|
|
87
|
-
...(q.distinct ? { distinct: true } : {}),
|
|
88
|
-
...(q.values ? { values: true } : {}),
|
|
89
|
-
...(q.limit ? { limit: rewriteExpr(q.limit) } : {}),
|
|
90
|
-
...(q.offset ? { offset: rewriteExpr(q.offset) } : {}),
|
|
91
|
-
};
|
|
78
|
+
return transform(q, (e) => (e.kind === "call" && e.recv === null && ROW_FNS.has(e.name) ? { ...e, recv: build.ident("$self") } : rootPathLiterals(e)));
|
|
92
79
|
}
|
|
93
80
|
// A top-level `limit`/`offset` on the collect path is applied by the runner (see
|
|
94
81
|
// oqxRunInner), so it must be a plain number literal here — there is no row to
|
|
@@ -101,76 +88,9 @@ function constBound(e, word) {
|
|
|
101
88
|
return e.value;
|
|
102
89
|
throw new FilterInvalid(`top-level ${word} must be a non-negative integer literal`, "OQX");
|
|
103
90
|
}
|
|
104
|
-
// Distinct phrases referenced by `semantic("…")` (free calls in the raw parse)
|
|
91
|
+
// Distinct phrases referenced by `semantic("…")` (free calls in the raw parse),
|
|
92
|
+
// found by one `visit` over the whole tree.
|
|
105
93
|
export function collectSemanticPhrases(source) {
|
|
106
|
-
const phrases = new Set();
|
|
107
|
-
const visitExpr = (e) => {
|
|
108
|
-
switch (e.kind) {
|
|
109
|
-
case "call":
|
|
110
|
-
if (e.recv === null && e.name === "semantic" && e.args[0]?.kind === "lit" && typeof e.args[0].value === "string") {
|
|
111
|
-
phrases.add(e.args[0].value);
|
|
112
|
-
}
|
|
113
|
-
if (e.recv)
|
|
114
|
-
visitExpr(e.recv);
|
|
115
|
-
e.args.forEach(visitExpr);
|
|
116
|
-
return;
|
|
117
|
-
case "member":
|
|
118
|
-
visitExpr(e.recv);
|
|
119
|
-
return;
|
|
120
|
-
case "index":
|
|
121
|
-
visitExpr(e.recv);
|
|
122
|
-
visitExpr(e.index);
|
|
123
|
-
return;
|
|
124
|
-
case "unary":
|
|
125
|
-
visitExpr(e.expr);
|
|
126
|
-
return;
|
|
127
|
-
case "binary":
|
|
128
|
-
case "logical":
|
|
129
|
-
case "in":
|
|
130
|
-
visitExpr(e.left);
|
|
131
|
-
visitExpr(e.right);
|
|
132
|
-
return;
|
|
133
|
-
case "range":
|
|
134
|
-
if (e.lo)
|
|
135
|
-
visitExpr(e.lo);
|
|
136
|
-
if (e.hi)
|
|
137
|
-
visitExpr(e.hi);
|
|
138
|
-
return;
|
|
139
|
-
default: return;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
const visitWhere = (w) => {
|
|
143
|
-
switch (w.kind) {
|
|
144
|
-
case "and":
|
|
145
|
-
case "or":
|
|
146
|
-
w.parts.forEach(visitWhere);
|
|
147
|
-
return;
|
|
148
|
-
case "not":
|
|
149
|
-
visitWhere(w.expr);
|
|
150
|
-
return;
|
|
151
|
-
case "scalar":
|
|
152
|
-
visitExpr(w.expr);
|
|
153
|
-
return;
|
|
154
|
-
case "op":
|
|
155
|
-
visitOp(w);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
const visitOp = (op) => { visitExpr(op.receiver); visitSub(op.sub); };
|
|
160
|
-
const visitFollow = (f) => {
|
|
161
|
-
f.destinations.forEach((d) => (d.kind === "op" ? visitOp(d) : visitExpr(d)));
|
|
162
|
-
[f.where, f.frontier, f.by].forEach((x) => x && visitExpr(x));
|
|
163
|
-
};
|
|
164
|
-
const visitSub = (s) => {
|
|
165
|
-
s.from.forEach(visitExpr);
|
|
166
|
-
if (s.where)
|
|
167
|
-
visitWhere(s.where);
|
|
168
|
-
s.select.forEach((it) => it.kind === "field" ? visitExpr(it.expr) : visitOp(it.op));
|
|
169
|
-
if (s.orderBy)
|
|
170
|
-
s.orderBy.forEach((o) => visitExpr(o.expr));
|
|
171
|
-
if (s.follow)
|
|
172
|
-
visitFollow(s.follow);
|
|
173
|
-
};
|
|
174
94
|
let q;
|
|
175
95
|
try {
|
|
176
96
|
q = parse(source);
|
|
@@ -178,19 +98,53 @@ export function collectSemanticPhrases(source) {
|
|
|
178
98
|
catch {
|
|
179
99
|
return [];
|
|
180
100
|
}
|
|
181
|
-
|
|
182
|
-
q
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
101
|
+
const phrases = new Set();
|
|
102
|
+
visit(q, {
|
|
103
|
+
enter(node) {
|
|
104
|
+
if (node.kind === "call" && node.recv === null && node.name === "semantic") {
|
|
105
|
+
const arg = node.args[0];
|
|
106
|
+
if (arg?.kind === "lit" && typeof arg.value === "string")
|
|
107
|
+
phrases.add(arg.value);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
});
|
|
190
111
|
return [...phrases];
|
|
191
112
|
}
|
|
192
|
-
const ID_ITEM =
|
|
193
|
-
const PATH_ITEM =
|
|
113
|
+
const ID_ITEM = build.field("__oqx_id", build.ident("$id"));
|
|
114
|
+
const PATH_ITEM = build.field("__oqx_path", build.ident("$path"));
|
|
115
|
+
// A hit is a store row (spec/surface §1.4, 1.5). The engine's top-level rows
|
|
116
|
+
// are store rows by construction when the source is a bare root scan and
|
|
117
|
+
// nothing re-projects them; a `follow` destination (`follow before` over a
|
|
118
|
+
// frontmatter list of paths), a `from E` re-projection or any other source can
|
|
119
|
+
// reach a scalar, which the injected `$id`/`$path` reads would render as the
|
|
120
|
+
// junk hit `{ id: "undefined", path: "" }`. For those queries the row itself is
|
|
121
|
+
// projected too (`$it`), and `toHit` fails the query when it is not a row. Only
|
|
122
|
+
// then: a row clone per hit is free here but not in the Rust port, and a root
|
|
123
|
+
// scan cannot need it. A `values` projection returns no hits, so it is exempt
|
|
124
|
+
// (`from docs from tags select $it values` is a flat list of tags).
|
|
125
|
+
const SELF_KEY = "__oqx_self";
|
|
126
|
+
const SELF_ITEM = build.field(SELF_KEY, build.ident("$it"));
|
|
127
|
+
function mayReachNonRows(q) {
|
|
128
|
+
return q.follow !== null || q.from.length > 0 || rootTarget(q.source) === null;
|
|
129
|
+
}
|
|
130
|
+
function describeValue(v) {
|
|
131
|
+
if (v === null || v === undefined)
|
|
132
|
+
return "an absent value";
|
|
133
|
+
if (typeof v === "string")
|
|
134
|
+
return `a string (${JSON.stringify(v)})`;
|
|
135
|
+
if (typeof v === "number")
|
|
136
|
+
return `a number (${String(v)})`;
|
|
137
|
+
if (typeof v === "boolean")
|
|
138
|
+
return `a boolean (${String(v)})`;
|
|
139
|
+
if (Array.isArray(v))
|
|
140
|
+
return "an array";
|
|
141
|
+
if (semantics.isRange(v))
|
|
142
|
+
return "a range";
|
|
143
|
+
return "an object";
|
|
144
|
+
}
|
|
145
|
+
function notAStoreRow(v) {
|
|
146
|
+
return new FilterInvalid(`a hit must be a document, block, node or edge row — the query reached ${describeValue(v)}; to follow document references held in a property use refs(<field>)`, "OQX");
|
|
147
|
+
}
|
|
194
148
|
// The reserved key a top-level `values` projection's single item is renamed to,
|
|
195
149
|
// so it rides through id/path injection, keyset paging, and distinct as an
|
|
196
150
|
// ordinary record field and is peeled off at the end.
|
|
@@ -222,7 +176,9 @@ function renderValue(v) {
|
|
|
222
176
|
}
|
|
223
177
|
function toHit(row) {
|
|
224
178
|
const o = row;
|
|
225
|
-
const { __oqx_id, __oqx_path, ...rest } = o;
|
|
179
|
+
const { __oqx_id, __oqx_path, __oqx_self, ...rest } = o;
|
|
180
|
+
if (SELF_KEY in o && !rowRef(__oqx_self))
|
|
181
|
+
throw notAStoreRow(__oqx_self);
|
|
226
182
|
return { id: String(__oqx_id), path: String(__oqx_path ?? ""), ...renderValue(rest) };
|
|
227
183
|
}
|
|
228
184
|
// Top-level `select distinct`: dedup hits by their USER projection (every field
|
|
@@ -255,7 +211,11 @@ export function oqxRun(store, repoId, source, opts = {}) {
|
|
|
255
211
|
}
|
|
256
212
|
}
|
|
257
213
|
function oqxRunInner(store, repoId, source, opts) {
|
|
258
|
-
|
|
214
|
+
// The query's `select` aliases are resolved HERE, once, before the runner
|
|
215
|
+
// renames/injects items (an engine evaluates the query it is given).
|
|
216
|
+
const raw = parse(source);
|
|
217
|
+
checkRootSpellings(raw);
|
|
218
|
+
const parsed = rewriteQuery(resolveAliases(raw));
|
|
259
219
|
const consumer = parsed.consumer;
|
|
260
220
|
const ctxOpts = opts.semanticVectors ? { semanticVectors: opts.semanticVectors } : {};
|
|
261
221
|
const ctx = makeStoreContext(store, repoId, ctxOpts);
|
|
@@ -281,21 +241,22 @@ function oqxRunInner(store, repoId, source, opts) {
|
|
|
281
241
|
// id/path are unique per row and would defeat the engine's projection dedup, so
|
|
282
242
|
// we run without engine-distinct and dedup hits by their USER projection below,
|
|
283
243
|
// keeping the first row's id/path.
|
|
284
|
-
const topDistinct =
|
|
244
|
+
const topDistinct = parsed.distinct;
|
|
285
245
|
// A top-level `values` projection runs as a RECORD projection whose single
|
|
286
246
|
// item is renamed to VALUE_KEY (the engine's own values mode is switched off),
|
|
287
247
|
// so pagination and distinct work unchanged; the bare values are peeled off
|
|
288
248
|
// the final page below and returned as `values` with `hits` empty.
|
|
289
|
-
const topValues =
|
|
249
|
+
const topValues = parsed.values;
|
|
290
250
|
const userSelect = topValues ? [{ ...parsed.select[0], name: VALUE_KEY }] : parsed.select;
|
|
291
251
|
// On the collect path a top-level `limit`/`offset` is ALSO taken out of the
|
|
292
252
|
// engine query and applied here, after the runner's own distinct — the engine
|
|
293
253
|
// would otherwise bound the raw rows before dedup (`select distinct type
|
|
294
254
|
// limit 3` must be three distinct types). first/single keep theirs: the
|
|
295
255
|
// engine's offset-aware cap is exactly right for them.
|
|
296
|
-
const { limit: topLimit, offset: topOffset
|
|
297
|
-
const base = consumer === "collect" ?
|
|
298
|
-
const
|
|
256
|
+
const { limit: topLimit, offset: topOffset } = parsed;
|
|
257
|
+
const base = consumer === "collect" ? { ...parsed, limit: null, offset: null } : parsed;
|
|
258
|
+
const guard = !topValues && mayReachNonRows(parsed) ? [SELF_ITEM] : [];
|
|
259
|
+
const q = { ...base, distinct: false, values: false, select: [ID_ITEM, PATH_ITEM, ...guard, ...userSelect] };
|
|
299
260
|
const res = engine.run(q, []);
|
|
300
261
|
if (consumer === "first" || consumer === "single") {
|
|
301
262
|
const row = res.consumer === "first" || res.consumer === "single" ? res.row : null;
|
|
@@ -341,14 +302,17 @@ export async function oqxRunAsync(store, repoId, source, opts = {}, embedQuery)
|
|
|
341
302
|
}
|
|
342
303
|
return oqxRun(store, repoId, source, { ...opts, semanticVectors });
|
|
343
304
|
}
|
|
344
|
-
// The collect page's keyset is (path, id)
|
|
345
|
-
// (
|
|
346
|
-
// which the MCP layer maps
|
|
305
|
+
// The collect page's keyset is (path, id) — the hit's path, so the reference
|
|
306
|
+
// form (`/a.md`): the kernel's shared cursor encoding (core/cursor.ts) with a
|
|
307
|
+
// two-part tuple. A malformed cursor is CursorInvalid, which the MCP layer maps
|
|
308
|
+
// to filter_invalid; so is a cursor whose path is not rooted — one issued by a
|
|
309
|
+
// 1.x surface, whose keyset would otherwise sort before every rooted hit and
|
|
310
|
+
// silently replay the first page (spec/surface §1.4, 2.0).
|
|
347
311
|
function encodeCursor(path, id) {
|
|
348
312
|
return encodeKeyset([path, id]);
|
|
349
313
|
}
|
|
350
314
|
function decodeCursor(cursor) {
|
|
351
|
-
const [path, id] =
|
|
315
|
+
const [path, id] = decodePathCursor(cursor, "query", 2);
|
|
352
316
|
return { path: path, id: id };
|
|
353
317
|
}
|
|
354
318
|
/** Parse an OQX source into the `@omgbase/oqx` AST (re-exported for callers). */
|
|
@@ -48,6 +48,11 @@ export interface Operand extends Frag {
|
|
|
48
48
|
key: string;
|
|
49
49
|
};
|
|
50
50
|
value?: unknown;
|
|
51
|
+
/** The storage-form path column behind a `$path` / `doc.$path` read
|
|
52
|
+
* (`d.path`): the operand's SQL is the rooted `'/' || <col>`, and an
|
|
53
|
+
* equality against a rooted text constant is re-shaped onto the bare,
|
|
54
|
+
* indexed column (`pathEquality`). */
|
|
55
|
+
pathCol?: string;
|
|
51
56
|
}
|
|
52
57
|
/**
|
|
53
58
|
* Per target, the bare identifiers (and `doc.<k>` keys, via the docs set) that
|
|
@@ -59,6 +64,8 @@ export interface Operand extends Frag {
|
|
|
59
64
|
* `translate.test.ts` asserts this set against the store context.
|
|
60
65
|
*/
|
|
61
66
|
export declare const NON_PROPERTY_NAMES: Readonly<Record<Target, ReadonlySet<string>>>;
|
|
67
|
+
/** The rooted path read over a storage path column. */
|
|
68
|
+
export declare function pathColumn(col: string): Operand;
|
|
62
69
|
/** docs intrinsics whose BARE (non-$) form is a loud error in-memory (10 §2) — not
|
|
63
70
|
* pushable, so the residual raises it (and the planner keeps the whole query
|
|
64
71
|
* unplanned when one is residual: spec/surface §1 decline (c)). */
|