@omgbase/core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/dist/src/core/read/document.d.ts +18 -5
- package/dist/src/core/read/document.js +21 -7
- package/dist/src/core/read/nodes.d.ts +7 -7
- package/dist/src/core/read/nodes.js +5 -3
- package/dist/src/core/read/reader.d.ts +11 -7
- package/dist/src/core/read/reader.js +8 -10
- package/dist/src/format/adapter.d.ts +1 -1
- package/dist/src/graph/history.d.ts +20 -14
- package/dist/src/graph/history.js +12 -12
- package/dist/src/graph/inbound-links.d.ts +4 -4
- package/dist/src/graph/inbound-links.js +4 -4
- package/dist/src/graph/link-health.d.ts +8 -8
- package/dist/src/graph/link-health.js +6 -6
- package/dist/src/graph/links.d.ts +3 -3
- package/dist/src/graph/links.js +4 -4
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/mcp/arguments.d.ts +27 -0
- package/dist/src/mcp/arguments.js +147 -0
- package/dist/src/mcp/errors.d.ts +1 -1
- package/dist/src/mcp/graph.d.ts +16 -9
- package/dist/src/mcp/graph.js +58 -35
- package/dist/src/mcp/reference.d.ts +1 -1
- package/dist/src/mcp/reference.js +215 -121
- package/dist/src/mcp/server.d.ts +9 -1
- package/dist/src/mcp/server.js +359 -268
- package/dist/src/mutate/apply.d.ts +11 -4
- package/dist/src/mutate/apply.js +3 -2
- package/dist/src/mutate/docs.js +7 -7
- package/dist/src/mutate/macros.d.ts +4 -1
- package/dist/src/mutate/macros.js +1 -1
- package/dist/src/mutate/ops.js +2 -2
- package/dist/src/mutate/opset.d.ts +8 -5
- package/dist/src/mutate/plan-update.js +9 -8
- package/dist/src/oqx-js/context.d.ts +17 -4
- package/dist/src/oqx-js/context.js +268 -131
- package/dist/src/oqx-js/planner.d.ts +3 -3
- package/dist/src/oqx-js/planner.js +14 -15
- package/dist/src/oqx-js/root-row.d.ts +10 -3
- package/dist/src/oqx-js/root-row.js +18 -8
- package/dist/src/oqx-js/run.d.ts +6 -0
- package/dist/src/oqx-js/run.js +71 -24
- package/dist/src/oqx-js/sql/scan.js +7 -7
- package/dist/src/oqx-js/sql/translate.d.ts +12 -9
- package/dist/src/oqx-js/sql/translate.js +183 -75
- package/dist/src/oqx-js/store-index.d.ts +1 -1
- package/dist/src/oqx-js/store-index.js +28 -26
- package/dist/src/search/cel/ast.d.ts +1 -1
- package/dist/src/search/cel/compile.d.ts +1 -1
- package/dist/src/search/cel/compile.js +43 -70
- package/dist/src/search/cel/parser.js +1 -1
- package/dist/src/search/resolve.d.ts +2 -0
- package/dist/src/search/rrf.js +2 -2
- package/dist/src/search/text.d.ts +2 -2
- package/dist/src/search/text.js +1 -1
- package/dist/src/spec-versions.d.ts +6 -6
- package/dist/src/spec-versions.js +5 -5
- package/dist/src/surface-paths.d.ts +41 -8
- package/dist/src/surface-paths.js +34 -18
- package/dist/src/sync/admin.d.ts +2 -1
- package/dist/src/sync/admin.js +1 -1
- package/package.json +2 -2
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
// are planned; everything else declines to a full in-memory run. One more
|
|
11
11
|
// decline keeps the planned path invisible (spec/surface §1 decline (c)): a
|
|
12
12
|
// pushed conjunct that empties the scan would also hide an OQX eval error a
|
|
13
|
-
// residual conjunct raises in memory (`path == "x" &&
|
|
13
|
+
// residual conjunct raises in memory (`path == "x" && path == "nope.md"` was
|
|
14
14
|
// `[]` planned, `filter_invalid` in memory), so when any residual conjunct could
|
|
15
15
|
// raise — a function or method call, a `single` block, a `^`-escaped name, or a
|
|
16
16
|
// bare reserved docs basename — the WHOLE query runs unplanned.
|
|
17
17
|
import { ROWS_ROOT, partitionPushable, residualQuery } from "@omgbase/oqx";
|
|
18
18
|
import { makeStoreContext, tagRows } from "./context.js";
|
|
19
|
-
import { translatePredicate
|
|
19
|
+
import { translatePredicate } from "./sql/translate.js";
|
|
20
20
|
import { ALIAS, COLS, FROM, ORDER, guards } from "./sql/scan.js";
|
|
21
21
|
import { REACH_THROUGH, TARGETS, isTargetRelationOf } from "./root-row.js";
|
|
22
|
-
// The root collection a query scans, if it is a bare `docs|blocks|nodes|
|
|
22
|
+
// The root collection a query scans, if it is a bare `docs|blocks|nodes|links`
|
|
23
23
|
// source at the root scope (else null — not a pushable shape; a nested `^docs`
|
|
24
24
|
// is a block's receiver, never the query's source). run.ts also asks: a bare
|
|
25
25
|
// root scan's rows are store rows by construction.
|
|
@@ -38,12 +38,12 @@ export function rootTarget(source) {
|
|
|
38
38
|
* • any `call` node (a function or method: unknown name, bad regex, wrong target…)
|
|
39
39
|
* • any nested block whose consumer is `single` (raises on 0 or >1 rows)
|
|
40
40
|
* • any `^`-escaped name: an `outer` reference or a `^name:` lift
|
|
41
|
-
* • a bare reserved docs basename (`id path updated_at
|
|
41
|
+
* • a bare reserved docs basename (`id path updated_at hash raw`): as
|
|
42
42
|
* an `ident` at the ROOT scope when the target is `docs`, or as `doc.<name>`
|
|
43
43
|
* on any target at any depth (the reach-through row is a doc)
|
|
44
|
-
* • a bare target name (`docs blocks nodes
|
|
44
|
+
* • a bare target name (`docs blocks nodes links`, root-row.ts) read as a
|
|
45
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
|
|
46
|
+
* the target's rows (`blocks` on blocks, `nodes` on nodes, any on links),
|
|
47
47
|
* as an `ident` at any depth below (the rows there are unknown), or as
|
|
48
48
|
* `<head>.<name>` unless the head is a reach-through whose row has that
|
|
49
49
|
* relation (`doc.nodes`, `doc.blocks`, `block.nodes`, `section.blocks`)
|
|
@@ -86,6 +86,8 @@ function subMayRaise(s, target) {
|
|
|
86
86
|
}
|
|
87
87
|
if (s.orderBy?.some((o) => exprMayRaise(o.expr, target, false)))
|
|
88
88
|
return true;
|
|
89
|
+
if (s.frontier && exprMayRaise(s.frontier, target, false))
|
|
90
|
+
return true;
|
|
89
91
|
if (s.follow && followMayRaise(s.follow, target))
|
|
90
92
|
return true;
|
|
91
93
|
if (s.limit && exprMayRaise(s.limit, target, false))
|
|
@@ -95,23 +97,18 @@ function subMayRaise(s, target) {
|
|
|
95
97
|
return false;
|
|
96
98
|
}
|
|
97
99
|
// A destination is a relation Expr read in the frontier row's scope, or a
|
|
98
|
-
// destination block (an OpNode) whose receiver
|
|
100
|
+
// destination block (an OpNode) whose receiver, body, `frontier` and `depth`
|
|
101
|
+
// are its own (oqx 0.20).
|
|
99
102
|
function followMayRaise(f, target) {
|
|
100
|
-
|
|
101
|
-
return true;
|
|
102
|
-
return [f.where, f.frontier, f.by].some((e) => e !== null && exprMayRaise(e, target, false));
|
|
103
|
+
return f.destinations.some((d) => (d.kind === "op" ? opMayRaise(d, target, false) : exprMayRaise(d, target, false)));
|
|
103
104
|
}
|
|
104
105
|
function exprMayRaise(e, target, root) {
|
|
105
106
|
switch (e.kind) {
|
|
106
107
|
case "call": return true;
|
|
107
108
|
case "outer": return true;
|
|
108
109
|
case "ident":
|
|
109
|
-
|
|
110
|
-
return !root || !isTargetRelationOf(target, e.name);
|
|
111
|
-
return root && target === "docs" && RESERVED_DOC_BASENAMES.has(e.name);
|
|
110
|
+
return TARGETS.has(e.name) && (!root || !isTargetRelationOf(target, e.name));
|
|
112
111
|
case "member":
|
|
113
|
-
if (e.recv.kind === "ident" && e.recv.name === "doc" && RESERVED_DOC_BASENAMES.has(e.name))
|
|
114
|
-
return true;
|
|
115
112
|
if (TARGETS.has(e.name) && !(e.recv.kind === "ident" && isTargetRelationOf(REACH_THROUGH[e.recv.name], e.name)))
|
|
116
113
|
return true;
|
|
117
114
|
return exprMayRaise(e.recv, target, root);
|
|
@@ -126,6 +123,8 @@ function exprMayRaise(e, target, root) {
|
|
|
126
123
|
return exprMayRaise(e.left, target, root) || exprMayRaise(e.right, target, root);
|
|
127
124
|
case "range":
|
|
128
125
|
return (e.lo !== null && exprMayRaise(e.lo, target, root)) || (e.hi !== null && exprMayRaise(e.hi, target, root));
|
|
126
|
+
// A tuple (oqx 0.20) is never pushed down; it raises iff an element does.
|
|
127
|
+
case "tuple": return e.items.some((x) => exprMayRaise(x, target, root));
|
|
129
128
|
case "lit":
|
|
130
129
|
case "binding": return false;
|
|
131
130
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export type Target = "docs" | "blocks" | "nodes" | "
|
|
1
|
+
export type Target = "docs" | "blocks" | "nodes" | "links";
|
|
2
2
|
export declare const TARGETS: ReadonlySet<string>;
|
|
3
3
|
/** The target names that are a relation of each target's rows (§1.2). */
|
|
4
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`). */
|
|
5
|
+
/** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`, and a link's `dst.x`). */
|
|
6
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
|
|
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
8
|
/**
|
|
9
9
|
* The bare-target error. `depth` is the scope depth the name was read at when
|
|
10
10
|
* known statically (the carets in the hint count it); `null` when raised at read
|
|
@@ -17,5 +17,12 @@ export declare function bareTargetMessage(name: string, depth: number | null): s
|
|
|
17
17
|
* bare spelling. `levels` is the caret count written.
|
|
18
18
|
*/
|
|
19
19
|
export declare function pastRootMessage(name: string, levels: number): string;
|
|
20
|
+
/**
|
|
21
|
+
* The unknown-target error (3.0 name pass): a top-level `from <name>` whose name
|
|
22
|
+
* is not one of the four collections would read an absent root and return an
|
|
23
|
+
* empty result — refused instead, naming the collections; `edges`, the links
|
|
24
|
+
* target's name before 3.0, gets its new name spelled out.
|
|
25
|
+
*/
|
|
26
|
+
export declare function unknownTargetMessage(name: string): string;
|
|
20
27
|
/** 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
28
|
export declare function isTargetRelationOf(target: Target | undefined, name: string): boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The root row (spec/surface §1.1, 2.0; spec/oqx 0.18). The repository is OQX's
|
|
2
2
|
// root row: from a top-level row `^docs` reaches the documents (one caret per
|
|
3
|
-
// enclosing block, or the absolute `0^docs`),
|
|
3
|
+
// enclosing block, or the absolute `0^docs`), `^id` the repository id, and the
|
|
4
4
|
// root object (`^$it`) exposes the four collections. Two spellings are refused
|
|
5
5
|
// as `filter_invalid`, with the messages below, in both engines:
|
|
6
6
|
//
|
|
@@ -10,24 +10,24 @@
|
|
|
10
10
|
// `checkRootSpellings`);
|
|
11
11
|
// • a bare target name at scope depth ≥ 1 (inside a block, or in a top-level
|
|
12
12
|
// row's clauses), where a bare name reads a property of the current row,
|
|
13
|
-
// which has none: `docs` and `
|
|
13
|
+
// which has none: `docs` and `links` are no row's relation, so they are
|
|
14
14
|
// refused statically by the same walk; `blocks` and `nodes` ARE relations
|
|
15
15
|
// of some rows (`docs.blocks`, `docs.nodes`, `blocks.nodes`,
|
|
16
16
|
// `section.blocks`), so the context raises the same error at read time on a
|
|
17
17
|
// row that lacks the relation (context.ts). A frontmatter key named after a
|
|
18
18
|
// target is unreachable by bare name (use `frontmatter.<k>`).
|
|
19
|
-
export const TARGETS = new Set(["docs", "blocks", "nodes", "
|
|
19
|
+
export const TARGETS = new Set(["docs", "blocks", "nodes", "links"]);
|
|
20
20
|
/** The target names that are a relation of each target's rows (§1.2). */
|
|
21
21
|
export const TARGET_RELATIONS = {
|
|
22
22
|
docs: new Set(["nodes", "blocks"]),
|
|
23
23
|
blocks: new Set(["nodes"]),
|
|
24
24
|
nodes: new Set(["blocks"]),
|
|
25
|
-
|
|
25
|
+
links: new Set(),
|
|
26
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",
|
|
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
|
|
27
|
+
/** The reach-through heads and the target of the row they reach (`doc.x`, `block.x`, `section.x`, and a link's `dst.x`). */
|
|
28
|
+
export const REACH_THROUGH = { doc: "docs", block: "blocks", section: "nodes", dst: "docs" };
|
|
29
|
+
const NOUN = { docs: "documents", blocks: "blocks", nodes: "nodes", links: "links" };
|
|
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
31
|
/**
|
|
32
32
|
* The bare-target error. `depth` is the scope depth the name was read at when
|
|
33
33
|
* known statically (the carets in the hint count it); `null` when raised at read
|
|
@@ -48,6 +48,16 @@ export function pastRootMessage(name, levels) {
|
|
|
48
48
|
const t = name;
|
|
49
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
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* The unknown-target error (3.0 name pass): a top-level `from <name>` whose name
|
|
53
|
+
* is not one of the four collections would read an absent root and return an
|
|
54
|
+
* empty result — refused instead, naming the collections; `edges`, the links
|
|
55
|
+
* target's name before 3.0, gets its new name spelled out.
|
|
56
|
+
*/
|
|
57
|
+
export function unknownTargetMessage(name) {
|
|
58
|
+
const was = name === "edges" ? " (`edges` was renamed `links` in surface 3.0)" : "";
|
|
59
|
+
return `\`${name}\` is not a target — the repository's collections are \`docs\`, \`blocks\`, \`nodes\`, \`links\`${was}`;
|
|
60
|
+
}
|
|
51
61
|
/** 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
62
|
export function isTargetRelationOf(target, name) {
|
|
53
63
|
return target !== undefined && TARGET_RELATIONS[target].has(name);
|
package/dist/src/oqx-js/run.d.ts
CHANGED
|
@@ -6,6 +6,12 @@ export type EmbedQuery = (text: string) => Promise<{
|
|
|
6
6
|
model: string;
|
|
7
7
|
vec: Float32Array;
|
|
8
8
|
}>;
|
|
9
|
+
/** A query hit (spec/surface §1.4, 3.0): the row's identity under the SAME
|
|
10
|
+
* bare names the expression language reads — `id`, `path` — plus whatever
|
|
11
|
+
* `select` projected. A key copied from a hit is valid in the next `where` with
|
|
12
|
+
* no translation (`where id == "<hit.id>"`); a `select id, path` is simply the
|
|
13
|
+
* envelope, and any other projection whose output key would be `id` or `path`
|
|
14
|
+
* must be aliased (`envelopeCollision`). */
|
|
9
15
|
export interface OqxHit {
|
|
10
16
|
id: string;
|
|
11
17
|
path: string;
|
package/dist/src/oqx-js/run.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// omgbase's OQX entry point, served by `@omgbase/oqx`: parse the source, run it
|
|
2
2
|
// through the in-memory engine over a store-backed DataContext (context.ts), and
|
|
3
|
-
// shape the engine's result into omgbase's OqxResult (lean {id, path, …} hits
|
|
4
|
-
//
|
|
3
|
+
// shape the engine's result into omgbase's OqxResult (lean {id, path, …} hits
|
|
4
|
+
// — the envelope keys ARE the attributes, spec/surface §1.4 3.0 — keyset
|
|
5
|
+
// pagination, count/exists scalars). Same signature + shape as the former
|
|
5
6
|
// in-tree compiler, so every caller and the corpus are unchanged.
|
|
6
7
|
import { parse, PlannedEngine, InMemoryEngine, OqxError, resolveAliases, transform, visit, build, semantics } from "@omgbase/oqx";
|
|
7
8
|
import { makeStoreContext, rowRef } from "./context.js";
|
|
8
9
|
import { SQLiteQueryPlanner, rootTarget } from "./planner.js";
|
|
9
|
-
import { REPO_REMOVED_MESSAGE, TARGETS, bareTargetMessage, pastRootMessage } from "./root-row.js";
|
|
10
|
+
import { REPO_REMOVED_MESSAGE, TARGETS, bareTargetMessage, pastRootMessage, unknownTargetMessage } from "./root-row.js";
|
|
10
11
|
import { FilterInvalid } from "../search/cel/parser.js";
|
|
11
12
|
import { float32ToBlob } from "../core/vec.js";
|
|
12
13
|
import { encodeCursor as encodeKeyset, decodePathCursor } from "../core/cursor.js";
|
|
@@ -24,15 +25,25 @@ const ROW_FNS = new Set([
|
|
|
24
25
|
"yaml_path", "json_pointer", "has_edge", "has_anchor", "child_count", "parent_type",
|
|
25
26
|
]);
|
|
26
27
|
// Paths on the surface are the reference form (spec/surface §1 "Paths", 2.0):
|
|
27
|
-
//
|
|
28
|
-
// with one of them —
|
|
29
|
-
//
|
|
30
|
-
// property or binding compared with a path is never touched
|
|
31
|
-
// author's value)
|
|
32
|
-
//
|
|
33
|
-
|
|
28
|
+
// `path` and the edge field `dst_path` read `/a/b.md`. A string literal a query
|
|
29
|
+
// compares with one of them — `path == "a.md"`, `"a.md" != dst_path`,
|
|
30
|
+
// `path.startsWith("lab/")`, `doc.path == "a.md"` — is rooted first, so both
|
|
31
|
+
// spellings match; a property or binding compared with a path is never touched
|
|
32
|
+
// (it is the author's value), and neither is an authored `path:` key read
|
|
33
|
+
// through a source handle (`data.path`, `frontmatter.path`, `inline.path`): a
|
|
34
|
+
// member read is a path read only through a ROW handle (`doc`, `block`, `dst`,
|
|
35
|
+
// `$it`).
|
|
36
|
+
// The planner sees the rewritten tree too, so the pushed SQL and the in-memory
|
|
37
|
+
// engine agree by construction.
|
|
38
|
+
const PATH_ATTRIBUTES = new Set(["path", "dst_path"]);
|
|
39
|
+
const ROW_HANDLES = new Set(["doc", "block", "dst", "$it"]);
|
|
34
40
|
function isPathRead(e) {
|
|
35
|
-
|
|
41
|
+
if (e.kind === "ident" || e.kind === "outer")
|
|
42
|
+
return PATH_ATTRIBUTES.has(e.name);
|
|
43
|
+
if (e.kind === "member" && PATH_ATTRIBUTES.has(e.name)) {
|
|
44
|
+
return (e.recv.kind === "ident" || e.recv.kind === "outer") && ROW_HANDLES.has(e.recv.name);
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
36
47
|
}
|
|
37
48
|
function rootLiteral(e) {
|
|
38
49
|
return e.kind === "lit" && typeof e.value === "string" ? { ...e, value: referencePath(e.value) } : e;
|
|
@@ -50,7 +61,7 @@ export function rootPathLiterals(e) {
|
|
|
50
61
|
return e;
|
|
51
62
|
}
|
|
52
63
|
// The root row (root-row.ts, spec/surface §1.1, 2.0): `$repo` anywhere, and a
|
|
53
|
-
// bare `docs`/`
|
|
64
|
+
// bare `docs`/`links` at scope depth ≥ 1 (a property read of a row that has
|
|
54
65
|
// none), are refused before evaluation — one `visit` over the parsed tree, so
|
|
55
66
|
// the error does not depend on which rows the data happens to hold and the
|
|
56
67
|
// planned and in-memory paths agree trivially. `blocks`/`nodes` are relations of
|
|
@@ -59,13 +70,18 @@ export function rootPathLiterals(e) {
|
|
|
59
70
|
// top level) would read absent and count 0: refused too, naming the bare
|
|
60
71
|
// spelling.
|
|
61
72
|
export function checkRootSpellings(q) {
|
|
73
|
+
// A top-level `from <name>` that is no collection (`from edges`, the links
|
|
74
|
+
// target's name before 3.0) would scan an absent root and come back empty:
|
|
75
|
+
// refused, naming the four collections (root-row.ts).
|
|
76
|
+
if (q.source.kind === "ident" && !TARGETS.has(q.source.name))
|
|
77
|
+
throw new FilterInvalid(unknownTargetMessage(q.source.name), "OQX");
|
|
62
78
|
visit(q, {
|
|
63
79
|
enter(node, ctx) {
|
|
64
80
|
if (node.kind !== "ident" && node.kind !== "outer")
|
|
65
81
|
return;
|
|
66
82
|
if (node.name === "$repo")
|
|
67
83
|
throw new FilterInvalid(REPO_REMOVED_MESSAGE, "OQX");
|
|
68
|
-
if (node.kind === "ident" && ctx.depth >= 1 && (node.name === "docs" || node.name === "
|
|
84
|
+
if (node.kind === "ident" && ctx.depth >= 1 && (node.name === "docs" || node.name === "links")) {
|
|
69
85
|
throw new FilterInvalid(bareTargetMessage(node.name, ctx.depth), "OQX");
|
|
70
86
|
}
|
|
71
87
|
if (node.kind === "outer" && TARGETS.has(node.name) && node.levels > ctx.depth) {
|
|
@@ -110,13 +126,34 @@ export function collectSemanticPhrases(source) {
|
|
|
110
126
|
});
|
|
111
127
|
return [...phrases];
|
|
112
128
|
}
|
|
113
|
-
const ID_ITEM = build.field("__oqx_id", build.ident("
|
|
114
|
-
const PATH_ITEM = build.field("__oqx_path", build.ident("
|
|
129
|
+
const ID_ITEM = build.field("__oqx_id", build.ident("id"));
|
|
130
|
+
const PATH_ITEM = build.field("__oqx_path", build.ident("path"));
|
|
131
|
+
// Result projections may not overwrite the envelope (spec/surface §1.4, 3.0):
|
|
132
|
+
// a projection whose output key is `id` or `path` is allowed only when it IS
|
|
133
|
+
// the envelope's own selection — the bare attribute of the same name, which
|
|
134
|
+
// collapses onto the envelope key (`select id, path` is simply the envelope) —
|
|
135
|
+
// and is otherwise a loud error asking for an alias: `select data.path` must be
|
|
136
|
+
// `select p: data.path`, `select path: doc.layer` likewise. Checked over the
|
|
137
|
+
// resolved items (an alias is just a name here), before the engine runs, so the
|
|
138
|
+
// error never depends on the rows. A `values` projection returns no hits, so
|
|
139
|
+
// the envelope — and the rule — do not apply.
|
|
140
|
+
const ENVELOPE_KEYS = new Set(["id", "path"]);
|
|
141
|
+
function envelopeCollision(items) {
|
|
142
|
+
for (const item of items) {
|
|
143
|
+
if (!ENVELOPE_KEYS.has(item.name))
|
|
144
|
+
continue;
|
|
145
|
+
if (item.kind === "field" && item.expr.kind === "ident" && item.expr.name === item.name)
|
|
146
|
+
continue;
|
|
147
|
+
const hint = item.name === "path" ? "p: data.path" : "i: data.id";
|
|
148
|
+
return new FilterInvalid(`projection \`${item.name}\` collides with the hit envelope — alias it (\`${hint}\`)`, "OQX");
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
115
152
|
// A hit is a store row (spec/surface §1.4, 1.5). The engine's top-level rows
|
|
116
153
|
// are store rows by construction when the source is a bare root scan and
|
|
117
154
|
// nothing re-projects them; a `follow` destination (`follow before` over a
|
|
118
155
|
// frontmatter list of paths), a `from E` re-projection or any other source can
|
|
119
|
-
// reach a scalar, which the injected
|
|
156
|
+
// reach a scalar, which the injected `id`/`path` reads would render as the
|
|
120
157
|
// junk hit `{ id: "undefined", path: "" }`. For those queries the row itself is
|
|
121
158
|
// projected too (`$it`), and `toHit` fails the query when it is not a row. Only
|
|
122
159
|
// then: a row clone per hit is free here but not in the Rust port, and a root
|
|
@@ -155,9 +192,11 @@ const VALUE_KEY = "__oqx_value";
|
|
|
155
192
|
// spec/oqx §12), a `values` projection of `$it`/`$self`, a field bound to a
|
|
156
193
|
// row — and a store row is not a wire shape (column names, `attrs` as a JSON
|
|
157
194
|
// string, the `__path` join column, Buffers). Walk the projected value and
|
|
158
|
-
// render every tagged row as `{ id, path }
|
|
159
|
-
//
|
|
160
|
-
//
|
|
195
|
+
// render every tagged row as `{ id, path }` — the hit envelope's own keys
|
|
196
|
+
// (3.0), one notation for "a document reference" everywhere a row shape
|
|
197
|
+
// appears in a result; everything else passes through untouched (plain records
|
|
198
|
+
// and arrays are descended, scalars kept). Hits are records the engine built
|
|
199
|
+
// from the injected id/path items, never rows.
|
|
161
200
|
function renderValue(v) {
|
|
162
201
|
if (v === null || typeof v !== "object")
|
|
163
202
|
return v;
|
|
@@ -179,11 +218,13 @@ function toHit(row) {
|
|
|
179
218
|
const { __oqx_id, __oqx_path, __oqx_self, ...rest } = o;
|
|
180
219
|
if (SELF_KEY in o && !rowRef(__oqx_self))
|
|
181
220
|
throw notAStoreRow(__oqx_self);
|
|
221
|
+
// The envelope keys lead; a user projection of `id`/`path` collapses onto
|
|
222
|
+
// them in place (the same value, read by the same attribute).
|
|
182
223
|
return { id: String(__oqx_id), path: String(__oqx_path ?? ""), ...renderValue(rest) };
|
|
183
224
|
}
|
|
184
225
|
// Top-level `select distinct`: dedup hits by their USER projection (every field
|
|
185
|
-
// except the
|
|
186
|
-
// real
|
|
226
|
+
// except the envelope's `id`/`path`), keeping the first (so the retained hit
|
|
227
|
+
// carries a real identity). Order-preserving.
|
|
187
228
|
function dedupHitsByProjection(hits) {
|
|
188
229
|
const seen = new Set();
|
|
189
230
|
const out = [];
|
|
@@ -248,6 +289,11 @@ function oqxRunInner(store, repoId, source, opts) {
|
|
|
248
289
|
// the final page below and returned as `values` with `hits` empty.
|
|
249
290
|
const topValues = parsed.values;
|
|
250
291
|
const userSelect = topValues ? [{ ...parsed.select[0], name: VALUE_KEY }] : parsed.select;
|
|
292
|
+
if (!topValues) {
|
|
293
|
+
const collision = envelopeCollision(userSelect);
|
|
294
|
+
if (collision)
|
|
295
|
+
throw collision;
|
|
296
|
+
}
|
|
251
297
|
// On the collect path a top-level `limit`/`offset` is ALSO taken out of the
|
|
252
298
|
// engine query and applied here, after the runner's own distinct — the engine
|
|
253
299
|
// would otherwise bound the raw rows before dedup (`select distinct type
|
|
@@ -302,9 +348,10 @@ export async function oqxRunAsync(store, repoId, source, opts = {}, embedQuery)
|
|
|
302
348
|
}
|
|
303
349
|
return oqxRun(store, repoId, source, { ...opts, semanticVectors });
|
|
304
350
|
}
|
|
305
|
-
// The collect page's keyset is (path, id) — the hit's path
|
|
306
|
-
// form (`/a.md`)
|
|
307
|
-
// two-part tuple
|
|
351
|
+
// The collect page's keyset is (path, id) — the hit's `path`, so the reference
|
|
352
|
+
// form (`/a.md`), and its `id`: the kernel's shared cursor encoding
|
|
353
|
+
// (core/cursor.ts) with a two-part tuple of VALUES (no key names ride in it, so
|
|
354
|
+
// a 2.x cursor stays valid on 3.0). A malformed cursor is CursorInvalid, which the MCP layer maps
|
|
308
355
|
// to filter_invalid; so is a cursor whose path is not rooted — one issued by a
|
|
309
356
|
// 1.x surface, whose keyset would otherwise sort before every rooted hit and
|
|
310
357
|
// silently replay the first page (spec/surface §1.4, 2.0).
|
|
@@ -9,13 +9,13 @@ export const ALIAS = {
|
|
|
9
9
|
docs: { self: "d", doc: "d" },
|
|
10
10
|
blocks: { self: "b", doc: "d" },
|
|
11
11
|
nodes: { self: "n", doc: "d" },
|
|
12
|
-
|
|
12
|
+
links: { self: "e", doc: "d" },
|
|
13
13
|
};
|
|
14
14
|
export const FROM = {
|
|
15
15
|
docs: "docs d",
|
|
16
16
|
blocks: "blocks b JOIN docs d ON d.doc_id = b.doc_id",
|
|
17
17
|
nodes: "nodes n JOIN docs d ON d.doc_id = n.doc_id",
|
|
18
|
-
|
|
18
|
+
links: "edges e JOIN docs d ON d.doc_id = e.src_doc",
|
|
19
19
|
};
|
|
20
20
|
/** Row columns + the owning-doc path as `__path` (matches the context roots so
|
|
21
21
|
* produced rows are indistinguishable from a full scan's). */
|
|
@@ -23,7 +23,7 @@ export const COLS = {
|
|
|
23
23
|
docs: "d.*",
|
|
24
24
|
blocks: "b.*, d.path AS __path",
|
|
25
25
|
nodes: "n.*, d.path AS __path",
|
|
26
|
-
|
|
26
|
+
links: "e.*, d.path AS __path",
|
|
27
27
|
};
|
|
28
28
|
/** The joined targets driven FROM the document: the same rows and columns as
|
|
29
29
|
* `FROM`, with the loop order fixed by `CROSS JOIN` so a predicate on the
|
|
@@ -32,13 +32,13 @@ export const COLS = {
|
|
|
32
32
|
export const FROM_BY_DOC = {
|
|
33
33
|
blocks: "docs d CROSS JOIN blocks b ON b.doc_id = d.doc_id",
|
|
34
34
|
nodes: "docs d CROSS JOIN nodes n ON n.doc_id = d.doc_id",
|
|
35
|
-
|
|
35
|
+
links: "docs d CROSS JOIN edges e ON e.src_doc = d.doc_id",
|
|
36
36
|
};
|
|
37
37
|
export const ORDER = {
|
|
38
38
|
docs: "d.path, d.doc_id",
|
|
39
39
|
blocks: "d.path, b.block_id",
|
|
40
40
|
nodes: "d.path, n.node_id",
|
|
41
|
-
|
|
41
|
+
links: "d.path, e.edge_id",
|
|
42
42
|
};
|
|
43
43
|
/** The live-row and repository guards of a target scan. */
|
|
44
44
|
export function guards(target, repoId) {
|
|
@@ -46,7 +46,7 @@ export function guards(target, repoId) {
|
|
|
46
46
|
case "docs": return { sql: "d.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
|
|
47
47
|
case "blocks": return { sql: "b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
|
|
48
48
|
case "nodes": return { sql: "n.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId] };
|
|
49
|
-
case "
|
|
49
|
+
case "links": return { sql: "e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId] };
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
/** `guards` for a `FROM_BY_DOC` statement: the same tests, with the document's
|
|
@@ -56,7 +56,7 @@ export function guardsByDoc(target, repoId) {
|
|
|
56
56
|
switch (target) {
|
|
57
57
|
case "blocks": return { sql: "d.repo_id = ? AND +b.repo_id = ? AND b.deleted_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId, repoId] };
|
|
58
58
|
case "nodes": return { sql: "d.repo_id = ? AND +n.repo_id = ? AND d.deleted_commit IS NULL", params: [repoId, repoId] };
|
|
59
|
-
case "
|
|
59
|
+
case "links": return { sql: "d.repo_id = ? AND +e.repo_id = ? AND e.to_commit IS NULL AND d.deleted_commit IS NULL", params: [repoId, repoId] };
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
//# sourceMappingURL=scan.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Expr } from "@omgbase/oqx";
|
|
2
2
|
/** SQL row domain, matching the store's tables. */
|
|
3
|
-
export type Target = "docs" | "blocks" | "nodes" | "
|
|
3
|
+
export type Target = "docs" | "blocks" | "nodes" | "links";
|
|
4
4
|
/** The SQL aliases the compiler assigned to the current scope's row (`self`) and
|
|
5
5
|
* its owning document (`doc`). On the `docs` target the row IS the document, so
|
|
6
6
|
* both are the same alias. */
|
|
@@ -24,7 +24,7 @@ export interface Frag {
|
|
|
24
24
|
* decline (a)):
|
|
25
25
|
*
|
|
26
26
|
* text a string literal/binding, a text column or intrinsic, `.lower()`/`.upper()`
|
|
27
|
-
* int an integer-typed
|
|
27
|
+
* int an integer-typed column (blocks `ordinal`, `depth`)
|
|
28
28
|
* num a number literal/binding
|
|
29
29
|
* bool a boolean literal/binding
|
|
30
30
|
* null a null (or undefined) literal/binding
|
|
@@ -43,12 +43,16 @@ export interface Operand extends Frag {
|
|
|
43
43
|
col: string;
|
|
44
44
|
path: string;
|
|
45
45
|
};
|
|
46
|
+
/** A property read: `docId` is the SQL expression naming the document whose
|
|
47
|
+
* rows are read (`d.doc_id` for the row's own document, `e.dst_node` for a
|
|
48
|
+
* link's destination). */
|
|
46
49
|
prop?: {
|
|
47
|
-
|
|
50
|
+
docId: string;
|
|
48
51
|
key: string;
|
|
52
|
+
source?: string;
|
|
49
53
|
};
|
|
50
54
|
value?: unknown;
|
|
51
|
-
/** The storage-form path column behind a
|
|
55
|
+
/** The storage-form path column behind a `path` / `doc.path` read
|
|
52
56
|
* (`d.path`): the operand's SQL is the rooted `'/' || <col>`, and an
|
|
53
57
|
* equality against a rooted text constant is re-shaped onto the bare,
|
|
54
58
|
* indexed column (`pathEquality`). */
|
|
@@ -66,10 +70,9 @@ export interface Operand extends Frag {
|
|
|
66
70
|
export declare const NON_PROPERTY_NAMES: Readonly<Record<Target, ReadonlySet<string>>>;
|
|
67
71
|
/** The rooted path read over a storage path column. */
|
|
68
72
|
export declare function pathColumn(col: string): Operand;
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
export declare const RESERVED_DOC_BASENAMES: ReadonlySet<string>;
|
|
73
|
+
/** The identity/bytes/time attributes per kind — the names `attributeSql` owns
|
|
74
|
+
* (the structural columns are handled per target in `translateOperand`). */
|
|
75
|
+
export declare const ROW_ATTRIBUTES: Readonly<Record<Target, ReadonlySet<string>>>;
|
|
73
76
|
/** Translate an expression used as a VALUE (comparison operand, method receiver,
|
|
74
77
|
* function argument) to a SQL scalar with its operand kind. Returns null if not
|
|
75
78
|
* faithfully translatable. */
|
|
@@ -100,7 +103,7 @@ export declare function translateValue(e: Expr, ctx: TranslateCtx): Frag | null;
|
|
|
100
103
|
* ordered, exactly as in memory.
|
|
101
104
|
* Every other pair declines: SQLite sees JSON `true` and `1`, `val_bool` and
|
|
102
105
|
* `val_num` alike (`$ordinal == checked`, `true == 1`), orders every
|
|
103
|
-
* integer before every text (`$ordinal < "3"`, `level < "x"`,
|
|
106
|
+
* integer before every text (`$ordinal < "3"`, `level < "x"`, `path > 5`),
|
|
104
107
|
* and two JSON or property reads carry no type at plan time
|
|
105
108
|
* (`attrs.a == attrs.b`, `era == stages`).
|
|
106
109
|
*
|