@omgbase/core 0.1.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/LICENSE +21 -0
- package/dist/src/cli/eval-matcher.d.ts +13 -0
- package/dist/src/cli/eval-matcher.js +36 -0
- package/dist/src/core/attach.d.ts +5 -0
- package/dist/src/core/attach.js +38 -0
- package/dist/src/core/cursor.d.ts +11 -0
- package/dist/src/core/cursor.js +35 -0
- package/dist/src/core/hash.d.ts +23 -0
- package/dist/src/core/hash.js +82 -0
- package/dist/src/core/ids.d.ts +6 -0
- package/dist/src/core/ids.js +26 -0
- package/dist/src/core/index.d.ts +1 -0
- package/dist/src/core/index.js +2 -0
- package/dist/src/core/ingest.d.ts +43 -0
- package/dist/src/core/ingest.js +287 -0
- package/dist/src/core/order-key.d.ts +8 -0
- package/dist/src/core/order-key.js +72 -0
- package/dist/src/core/parse/parse.d.ts +3 -0
- package/dist/src/core/parse/parse.js +104 -0
- package/dist/src/core/parse/render.d.ts +3 -0
- package/dist/src/core/parse/render.js +20 -0
- package/dist/src/core/parse/tree.d.ts +8 -0
- package/dist/src/core/parse/tree.js +43 -0
- package/dist/src/core/parse/types.d.ts +52 -0
- package/dist/src/core/parse/types.js +9 -0
- package/dist/src/core/read/document.d.ts +107 -0
- package/dist/src/core/read/document.js +191 -0
- package/dist/src/core/read/nodes.d.ts +47 -0
- package/dist/src/core/read/nodes.js +102 -0
- package/dist/src/core/read/outline.d.ts +12 -0
- package/dist/src/core/read/outline.js +76 -0
- package/dist/src/core/read/reader.d.ts +125 -0
- package/dist/src/core/read/reader.js +206 -0
- package/dist/src/core/read/refs.d.ts +15 -0
- package/dist/src/core/read/refs.js +51 -0
- package/dist/src/core/store/edges.d.ts +26 -0
- package/dist/src/core/store/edges.js +101 -0
- package/dist/src/core/store/fts.d.ts +10 -0
- package/dist/src/core/store/fts.js +22 -0
- package/dist/src/core/store/gc.d.ts +18 -0
- package/dist/src/core/store/gc.js +69 -0
- package/dist/src/core/store/nodes.d.ts +5 -0
- package/dist/src/core/store/nodes.js +57 -0
- package/dist/src/core/store/properties.d.ts +38 -0
- package/dist/src/core/store/properties.js +177 -0
- package/dist/src/core/store/rebuild.d.ts +3 -0
- package/dist/src/core/store/rebuild.js +29 -0
- package/dist/src/core/store/schema.d.ts +9 -0
- package/dist/src/core/store/schema.js +386 -0
- package/dist/src/core/store/sections.d.ts +2 -0
- package/dist/src/core/store/sections.js +29 -0
- package/dist/src/core/store/store.d.ts +24 -0
- package/dist/src/core/store/store.js +164 -0
- package/dist/src/core/store/writers.d.ts +52 -0
- package/dist/src/core/store/writers.js +89 -0
- package/dist/src/core/vec.d.ts +10 -0
- package/dist/src/core/vec.js +33 -0
- package/dist/src/format/adapter.d.ts +97 -0
- package/dist/src/format/adapter.js +14 -0
- package/dist/src/format/index.d.ts +6 -0
- package/dist/src/format/index.js +15 -0
- package/dist/src/format/json.d.ts +3 -0
- package/dist/src/format/json.js +324 -0
- package/dist/src/format/markdown.d.ts +5 -0
- package/dist/src/format/markdown.js +217 -0
- package/dist/src/format/registry.d.ts +6 -0
- package/dist/src/format/registry.js +24 -0
- package/dist/src/format/yaml.d.ts +5 -0
- package/dist/src/format/yaml.js +413 -0
- package/dist/src/graph/extract.d.ts +38 -0
- package/dist/src/graph/extract.js +242 -0
- package/dist/src/graph/history.d.ts +103 -0
- package/dist/src/graph/history.js +210 -0
- package/dist/src/graph/inbound-links.d.ts +33 -0
- package/dist/src/graph/inbound-links.js +130 -0
- package/dist/src/graph/link-destinations.d.ts +22 -0
- package/dist/src/graph/link-destinations.js +84 -0
- package/dist/src/graph/link-health.d.ts +79 -0
- package/dist/src/graph/link-health.js +146 -0
- package/dist/src/graph/links.d.ts +23 -0
- package/dist/src/graph/links.js +42 -0
- package/dist/src/index.d.ts +60 -0
- package/dist/src/index.js +70 -0
- package/dist/src/mcp/errors.d.ts +17 -0
- package/dist/src/mcp/errors.js +18 -0
- package/dist/src/mcp/graph.d.ts +63 -0
- package/dist/src/mcp/graph.js +212 -0
- package/dist/src/mcp/reference.d.ts +1 -0
- package/dist/src/mcp/reference.js +227 -0
- package/dist/src/mcp/server.d.ts +30 -0
- package/dist/src/mcp/server.js +1076 -0
- package/dist/src/mcp/stdio.d.ts +13 -0
- package/dist/src/mcp/stdio.js +24 -0
- package/dist/src/migrate/mrplex.d.ts +27 -0
- package/dist/src/migrate/mrplex.js +29 -0
- package/dist/src/mutate/apply.d.ts +83 -0
- package/dist/src/mutate/apply.js +302 -0
- package/dist/src/mutate/doc-store.d.ts +49 -0
- package/dist/src/mutate/doc-store.js +71 -0
- package/dist/src/mutate/docs.d.ts +60 -0
- package/dist/src/mutate/docs.js +232 -0
- package/dist/src/mutate/known-ids.d.ts +12 -0
- package/dist/src/mutate/known-ids.js +154 -0
- package/dist/src/mutate/load.d.ts +3 -0
- package/dist/src/mutate/load.js +66 -0
- package/dist/src/mutate/lower.d.ts +32 -0
- package/dist/src/mutate/lower.js +232 -0
- package/dist/src/mutate/macros.d.ts +91 -0
- package/dist/src/mutate/macros.js +230 -0
- package/dist/src/mutate/ops.d.ts +40 -0
- package/dist/src/mutate/ops.js +513 -0
- package/dist/src/mutate/opset.d.ts +92 -0
- package/dist/src/mutate/opset.js +79 -0
- package/dist/src/mutate/plan-update.d.ts +51 -0
- package/dist/src/mutate/plan-update.js +187 -0
- package/dist/src/mutate/tree.d.ts +46 -0
- package/dist/src/mutate/tree.js +167 -0
- package/dist/src/oqx/run.d.ts +1 -0
- package/dist/src/oqx/run.js +6 -0
- package/dist/src/oqx-js/context.d.ts +23 -0
- package/dist/src/oqx-js/context.js +564 -0
- package/dist/src/oqx-js/planner.d.ts +10 -0
- package/dist/src/oqx-js/planner.js +91 -0
- package/dist/src/oqx-js/run.d.ts +41 -0
- package/dist/src/oqx-js/run.js +332 -0
- package/dist/src/oqx-js/sql/translate.d.ts +27 -0
- package/dist/src/oqx-js/sql/translate.js +290 -0
- package/dist/src/reconcile/crossdoc.d.ts +34 -0
- package/dist/src/reconcile/crossdoc.js +76 -0
- package/dist/src/reconcile/eval/generator.d.ts +21 -0
- package/dist/src/reconcile/eval/generator.js +129 -0
- package/dist/src/reconcile/eval/metrics.d.ts +32 -0
- package/dist/src/reconcile/eval/metrics.js +74 -0
- package/dist/src/reconcile/flatten.d.ts +12 -0
- package/dist/src/reconcile/flatten.js +37 -0
- package/dist/src/reconcile/phase5.d.ts +8 -0
- package/dist/src/reconcile/phase5.js +167 -0
- package/dist/src/reconcile/phase6a.d.ts +2 -0
- package/dist/src/reconcile/phase6a.js +159 -0
- package/dist/src/reconcile/phases.d.ts +28 -0
- package/dist/src/reconcile/phases.js +0 -0
- package/dist/src/reconcile/reconcile.d.ts +21 -0
- package/dist/src/reconcile/reconcile.js +115 -0
- package/dist/src/reconcile/similarity.d.ts +7 -0
- package/dist/src/reconcile/similarity.js +41 -0
- package/dist/src/reconcile/types.d.ts +46 -0
- package/dist/src/reconcile/types.js +19 -0
- package/dist/src/search/cel/ast.d.ts +74 -0
- package/dist/src/search/cel/ast.js +3 -0
- package/dist/src/search/cel/compile.d.ts +60 -0
- package/dist/src/search/cel/compile.js +836 -0
- package/dist/src/search/cel/lexer.d.ts +9 -0
- package/dist/src/search/cel/lexer.js +139 -0
- package/dist/src/search/cel/parser.d.ts +7 -0
- package/dist/src/search/cel/parser.js +219 -0
- package/dist/src/search/drain.d.ts +39 -0
- package/dist/src/search/drain.js +97 -0
- package/dist/src/search/embeddings.d.ts +131 -0
- package/dist/src/search/embeddings.js +263 -0
- package/dist/src/search/external.d.ts +13 -0
- package/dist/src/search/external.js +177 -0
- package/dist/src/search/fts-query.d.ts +1 -0
- package/dist/src/search/fts-query.js +64 -0
- package/dist/src/search/provider.d.ts +17 -0
- package/dist/src/search/provider.js +13 -0
- package/dist/src/search/resolve.d.ts +18 -0
- package/dist/src/search/resolve.js +23 -0
- package/dist/src/search/rrf.d.ts +36 -0
- package/dist/src/search/rrf.js +84 -0
- package/dist/src/search/tasks.d.ts +12 -0
- package/dist/src/search/tasks.js +131 -0
- package/dist/src/search/text.d.ts +16 -0
- package/dist/src/search/text.js +21 -0
- package/dist/src/search/vector.d.ts +18 -0
- package/dist/src/search/vector.js +49 -0
- package/dist/src/sync/admin.d.ts +59 -0
- package/dist/src/sync/admin.js +53 -0
- package/dist/src/sync/attach.d.ts +13 -0
- package/dist/src/sync/attach.js +29 -0
- package/dist/src/sync/checkpoint.d.ts +21 -0
- package/dist/src/sync/checkpoint.js +52 -0
- package/dist/src/sync/driver.d.ts +25 -0
- package/dist/src/sync/driver.js +80 -0
- package/dist/src/sync/external-source.d.ts +14 -0
- package/dist/src/sync/external-source.js +137 -0
- package/dist/src/sync/freshness.d.ts +42 -0
- package/dist/src/sync/freshness.js +152 -0
- package/dist/src/sync/fs-util.d.ts +12 -0
- package/dist/src/sync/fs-util.js +55 -0
- package/dist/src/sync/git-heuristics.d.ts +6 -0
- package/dist/src/sync/git-heuristics.js +16 -0
- package/dist/src/sync/observe.d.ts +63 -0
- package/dist/src/sync/observe.js +84 -0
- package/dist/src/sync/plugin.d.ts +54 -0
- package/dist/src/sync/plugin.js +12 -0
- package/dist/src/sync/reconciling-ingest.d.ts +12 -0
- package/dist/src/sync/reconciling-ingest.js +209 -0
- package/dist/src/sync/recovery.d.ts +6 -0
- package/dist/src/sync/recovery.js +34 -0
- package/dist/src/sync/settings.d.ts +18 -0
- package/dist/src/sync/settings.js +58 -0
- package/dist/src/sync/sources.d.ts +37 -0
- package/dist/src/sync/sources.js +75 -0
- package/dist/src/sync/tombstone.d.ts +9 -0
- package/dist/src/sync/tombstone.js +55 -0
- package/dist/src/sync/watch-lease.d.ts +14 -0
- package/dist/src/sync/watch-lease.js +91 -0
- package/dist/src/sync/watcher.d.ts +19 -0
- package/dist/src/sync/watcher.js +34 -0
- package/dist/src/sync/workspace.d.ts +34 -0
- package/dist/src/sync/workspace.js +120 -0
- package/dist/src/sync/writer-lock.d.ts +16 -0
- package/dist/src/sync/writer-lock.js +121 -0
- package/package.json +80 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import { FilterInvalid } from "./parser.js";
|
|
2
|
+
import { sanitizeFtsQuery } from "../fts-query.js";
|
|
3
|
+
function resolveOuter(ctx, name) {
|
|
4
|
+
const b = ctx.outer?.(name);
|
|
5
|
+
if (!b) {
|
|
6
|
+
throw new FilterInvalid(`no binding ^${name} one scope outward — a ^ reference reads a name bound in the immediately enclosing query scope`, "OQX ^");
|
|
7
|
+
}
|
|
8
|
+
return b;
|
|
9
|
+
}
|
|
10
|
+
export function defaultCtx(target) {
|
|
11
|
+
if (target === "docs")
|
|
12
|
+
return { self: "d", doc: "d" };
|
|
13
|
+
if (target === "blocks")
|
|
14
|
+
return { self: "b", doc: "d" };
|
|
15
|
+
if (target === "edges")
|
|
16
|
+
return { self: "e", doc: "d" };
|
|
17
|
+
return { self: "n", doc: "d" };
|
|
18
|
+
}
|
|
19
|
+
// Build a JSON1 path literal from parsed identifier segments. Segments come
|
|
20
|
+
// from the lexer's identifier rule ([A-Za-z_][A-Za-z0-9_]*), so they are safe
|
|
21
|
+
// to inline — no user free-text reaches this. Inlining (rather than binding)
|
|
22
|
+
// keeps field expressions parameter-free, so repeating an expression in SQL
|
|
23
|
+
// never desynchronizes the bound-param list.
|
|
24
|
+
function jsonPath(segs) {
|
|
25
|
+
for (const s of segs) {
|
|
26
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) {
|
|
27
|
+
throw new FilterInvalid(`invalid field segment '${s}'`, "10 §2");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return `'$.${segs.join(".")}'`;
|
|
31
|
+
}
|
|
32
|
+
// ---- properties routing (properties-table) -------------------------------
|
|
33
|
+
// Documents-target fields (and doc.<k> reach-through from blocks/nodes) resolve
|
|
34
|
+
// against the indexed `properties` table instead of json_extract over a JSON
|
|
35
|
+
// blob. A field access is one of: a scalar-value expression (comparisons), or
|
|
36
|
+
// an existence/count predicate (membership, size, has, bool-context). All
|
|
37
|
+
// correlate on `<doc>.doc_id` (the correlated docs row, present on every target).
|
|
38
|
+
const PROP_SOURCES = new Set(["frontmatter", "inline", "computed"]);
|
|
39
|
+
// Computed properties surfaced as document intrinsics (12 §4). Engine-derived,
|
|
40
|
+
// stored as source='computed' rows keyed by their $ name. Filterable like any
|
|
41
|
+
// property (scalar ==, list(), membership) but never shadow authored keys.
|
|
42
|
+
const COMPUTED_INTRINSICS = new Set(["$title", "$tags"]);
|
|
43
|
+
// Base names of the document intrinsics ($id, $path, ...) MINUS their `$`. On
|
|
44
|
+
// the docs open namespace (and doc.<k> reach-through), a BARE first segment
|
|
45
|
+
// equal to one of these is almost always a typo for the intrinsic: it would
|
|
46
|
+
// otherwise resolve to an absent frontmatter key and silently match nothing
|
|
47
|
+
// (a misleading empty). We reject it with a hint instead. `title`/`tags` are
|
|
48
|
+
// deliberately EXCLUDED: their $-forms are `computed` intrinsics that do not
|
|
49
|
+
// shadow authored frontmatter, so bare `title`/`tags` remain legitimate key
|
|
50
|
+
// access (see COMPUTED_INTRINSICS and QUERY_SYNTAX). A source-scoped form
|
|
51
|
+
// (frontmatter.path / inline.path) is an explicit request for that property
|
|
52
|
+
// and is likewise not caught — only the bare first-segment case is the trap.
|
|
53
|
+
const RESERVED_INTRINSIC_BASENAMES = new Set([
|
|
54
|
+
"id", "path", "repo", "updated_at", "content_hash", "body",
|
|
55
|
+
]);
|
|
56
|
+
// Guard the docs open namespace against reserved-intrinsic collisions. `segs`
|
|
57
|
+
// are the property key segments (after any doc./source scope has been peeled).
|
|
58
|
+
// Throws when the FIRST segment shadows an intrinsic base name.
|
|
59
|
+
function guardReservedCollision(segs) {
|
|
60
|
+
if (RESERVED_INTRINSIC_BASENAMES.has(segs[0])) {
|
|
61
|
+
const name = segs[0];
|
|
62
|
+
throw new FilterInvalid(`bare '${name}' reads a frontmatter key; did you mean the intrinsic $${name}? (use frontmatter.${name} to force the property)`, "10 §2");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Validate + join identifier segments into a dotted property key. Segments come
|
|
66
|
+
// from the lexer's identifier rule, so inlining is injection-safe (mirrors
|
|
67
|
+
// jsonPath); keeping keys param-free preserves the "fields carry no params"
|
|
68
|
+
// invariant the comparison/membership param ordering relies on.
|
|
69
|
+
function propKey(segs) {
|
|
70
|
+
for (const s of segs) {
|
|
71
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) {
|
|
72
|
+
throw new FilterInvalid(`invalid field segment '${s}'`, "10 §2");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return segs.join(".");
|
|
76
|
+
}
|
|
77
|
+
// Is this field a properties-routed document property? Returns {key, source?}
|
|
78
|
+
// or null (intrinsic, `format`, or a non-doc field). A leading
|
|
79
|
+
// frontmatter./inline./computed. segment (with something after it) is a
|
|
80
|
+
// source scope, not a key segment.
|
|
81
|
+
function propRef(field, target) {
|
|
82
|
+
let segs = field.segments;
|
|
83
|
+
if (target === "docs") {
|
|
84
|
+
// Computed intrinsic ($title, $tags): route to source='computed' rows.
|
|
85
|
+
if (field.intrinsic) {
|
|
86
|
+
if (segs.length === 1 && COMPUTED_INTRINSICS.has(segs[0]))
|
|
87
|
+
return { key: segs[0], source: "computed" };
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
if (segs[0] === "format")
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
if (field.intrinsic)
|
|
95
|
+
return null;
|
|
96
|
+
// blocks / nodes: only doc.<k> reach-through routes to properties.
|
|
97
|
+
if (segs[0] !== "doc")
|
|
98
|
+
return null;
|
|
99
|
+
segs = segs.slice(1);
|
|
100
|
+
// doc.$title reach-through to a computed intrinsic.
|
|
101
|
+
if (segs.length === 1 && COMPUTED_INTRINSICS.has(segs[0]))
|
|
102
|
+
return { key: segs[0], source: "computed" };
|
|
103
|
+
if (segs.length === 0 || segs[0].startsWith("$") || segs[0] === "format")
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (segs.length >= 2 && PROP_SOURCES.has(segs[0])) {
|
|
107
|
+
const source = segs[0];
|
|
108
|
+
return { key: propKey(segs.slice(1)), source };
|
|
109
|
+
}
|
|
110
|
+
// Bare (non-`$`, non-source-scoped) key: reject a first segment that shadows
|
|
111
|
+
// a reserved intrinsic base name (path/id/repo/...) so a typo for `$path`
|
|
112
|
+
// fails loud instead of silently resolving to an absent frontmatter key.
|
|
113
|
+
guardReservedCollision(segs);
|
|
114
|
+
return { key: propKey(segs) };
|
|
115
|
+
}
|
|
116
|
+
function srcClause(source, alias = "p") {
|
|
117
|
+
return source ? ` AND ${alias}.source = '${source}'` : "";
|
|
118
|
+
}
|
|
119
|
+
// Scalar value of a property for comparisons: the scalar-authored value, but
|
|
120
|
+
// ONLY when the key is single-valued in the queried scope. A key resolves to a
|
|
121
|
+
// scalar iff it has exactly one row in scope AND that row is card='scalar'.
|
|
122
|
+
// This makes:
|
|
123
|
+
// - a lone inline `element:: fire` (card='scalar', 1 row) comparable;
|
|
124
|
+
// - a YAML list `tags: [a]` (1 row, card='list') NOT scalar-equal;
|
|
125
|
+
// - a frontmatter+inline collision on a bare key (≥2 rows in the union scope)
|
|
126
|
+
// fall back to list semantics — `job == "farmer"` is false, you must use
|
|
127
|
+
// `"farmer" in list(job)`.
|
|
128
|
+
// The inner COUNT spans ALL cards in scope so multiplicity — not just authored
|
|
129
|
+
// shape — governs scalar eligibility. NULL (⇒ false) when the gate fails.
|
|
130
|
+
function propScalarExpr(ref, ctx) {
|
|
131
|
+
return `(SELECT COALESCE(p.val_text, p.val_num, p.val_bool) FROM properties p
|
|
132
|
+
WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}' AND p.card = 'scalar'${srcClause(ref.source)}
|
|
133
|
+
AND p.deleted_commit IS NULL
|
|
134
|
+
AND (SELECT COUNT(*) FROM properties p2
|
|
135
|
+
WHERE p2.doc_id = ${ctx.doc}.doc_id AND p2.key = '${ref.key}'${srcClause(ref.source, "p2")}
|
|
136
|
+
AND p2.deleted_commit IS NULL) = 1
|
|
137
|
+
LIMIT 1)`;
|
|
138
|
+
}
|
|
139
|
+
// EXISTS over ALL rows for the key (any card) whose value equals ? — the
|
|
140
|
+
// membership primitive. Spans scalar and list rows, so `"canon" in list(layer)`
|
|
141
|
+
// matches a scalar frontmatter value and `"a" in list(tags)` matches a list.
|
|
142
|
+
function propMemberExists(ref, ctx) {
|
|
143
|
+
return `EXISTS (SELECT 1 FROM properties p WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}'${srcClause(ref.source)}
|
|
144
|
+
AND p.deleted_commit IS NULL AND COALESCE(p.val_text, p.val_num, p.val_bool) = ?)`;
|
|
145
|
+
}
|
|
146
|
+
// COUNT of all rows for the key (size(list(k))): 0 when absent, 1 for a scalar,
|
|
147
|
+
// N for a list — matching json_array_length / scalar=1 / null=0 today.
|
|
148
|
+
function propCountExpr(ref, ctx) {
|
|
149
|
+
return `(SELECT COUNT(*) FROM properties p WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}'${srcClause(ref.source)}
|
|
150
|
+
AND p.deleted_commit IS NULL)`;
|
|
151
|
+
}
|
|
152
|
+
// Existence of the key in any card (has(k)).
|
|
153
|
+
function propHasExists(ref, ctx) {
|
|
154
|
+
return `EXISTS (SELECT 1 FROM properties p WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}'${srcClause(ref.source)}
|
|
155
|
+
AND p.deleted_commit IS NULL)`;
|
|
156
|
+
}
|
|
157
|
+
// Truthy in boolean position: present list row, or present scalar with a
|
|
158
|
+
// truthy value (not 0/''/false). Absent ⇒ false; `!k` negates.
|
|
159
|
+
function propTruthyExists(ref, ctx) {
|
|
160
|
+
return `EXISTS (SELECT 1 FROM properties p WHERE p.doc_id = ${ctx.doc}.doc_id AND p.key = '${ref.key}'${srcClause(ref.source)}
|
|
161
|
+
AND p.deleted_commit IS NULL
|
|
162
|
+
AND (p.card = 'list' OR COALESCE(p.val_text, p.val_num, p.val_bool) NOT IN (0, '', 'false')))`;
|
|
163
|
+
}
|
|
164
|
+
// Column/JSON accessor for a field on a given target. Returns a SQL scalar
|
|
165
|
+
// expression that is NULL when the field is absent. Never carries params.
|
|
166
|
+
function fieldSql(field, target, ctx) {
|
|
167
|
+
const segs = field.segments;
|
|
168
|
+
const head = segs[0];
|
|
169
|
+
const self = ctx.self;
|
|
170
|
+
const doc = ctx.doc;
|
|
171
|
+
// Recursion intrinsics ($depth/$stop/$leaf/$frontier) resolve to the walk's
|
|
172
|
+
// materialized columns when a follow query's post-walk scope is active. Routed
|
|
173
|
+
// before the per-target intrinsic switch so `$depth` means RECURSION depth here
|
|
174
|
+
// (shadowing block-nesting depth). Only a bare intrinsic (`$stop`, not
|
|
175
|
+
// `$stop.x`) is a recursion column. Param-free, so the field param invariant holds.
|
|
176
|
+
if (field.intrinsic && ctx.recur && segs.length === 1) {
|
|
177
|
+
switch (head) {
|
|
178
|
+
case "$depth": return { expr: ctx.recur.depth };
|
|
179
|
+
case "$stop": return { expr: ctx.recur.stop };
|
|
180
|
+
case "$leaf": return { expr: ctx.recur.leaf };
|
|
181
|
+
case "$frontier": return { expr: ctx.recur.frontier };
|
|
182
|
+
case "$ordinal": return { expr: ctx.recur.ordinal };
|
|
183
|
+
default: break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (field.intrinsic) {
|
|
187
|
+
if (target === "docs") {
|
|
188
|
+
switch (head) {
|
|
189
|
+
case "$id": return { expr: `${self}.doc_id` };
|
|
190
|
+
case "$path": return { expr: `${self}.path` };
|
|
191
|
+
case "$repo": return { expr: `${self}.repo_id` };
|
|
192
|
+
case "$updated_at": return { expr: `(SELECT c.ts FROM revisions r JOIN commits c ON c.commit_id = r.commit_id WHERE r.rev_id = ${self}.current_rev)` };
|
|
193
|
+
case "$content_hash": return { expr: `lower(hex(${self}.file_hash))` };
|
|
194
|
+
default: {
|
|
195
|
+
// Computed property intrinsic ($title, $tags) → properties row scalar.
|
|
196
|
+
const ref = propRef(field, target);
|
|
197
|
+
if (ref)
|
|
198
|
+
return { expr: propScalarExpr(ref, ctx) };
|
|
199
|
+
throw new FilterInvalid(`unknown intrinsic ${head} on docs`, "10 §2");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (target === "nodes") {
|
|
204
|
+
switch (head) {
|
|
205
|
+
case "$id": return { expr: `${self}.node_id` };
|
|
206
|
+
case "$node_id": return { expr: `${self}.node_id` };
|
|
207
|
+
case "$doc_id": return { expr: `${self}.doc_id` };
|
|
208
|
+
case "$block_id": return { expr: `${self}.block_id` };
|
|
209
|
+
case "$path": return { expr: `${doc}.path` };
|
|
210
|
+
default: throw new FilterInvalid(`unknown intrinsic ${head} on nodes`, "10 §2");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else if (target === "edges") {
|
|
214
|
+
switch (head) {
|
|
215
|
+
case "$id": return { expr: `${self}.edge_id` };
|
|
216
|
+
case "$src": return { expr: `${self}.src_doc` };
|
|
217
|
+
case "$dst": return { expr: `${self}.dst_node` };
|
|
218
|
+
case "$src_block": return { expr: `${self}.src_block` };
|
|
219
|
+
case "$via": return { expr: `${self}.via_node` };
|
|
220
|
+
case "$from_commit": return { expr: `${self}.from_commit` };
|
|
221
|
+
case "$path": return { expr: `${doc}.path` }; // source document's path
|
|
222
|
+
// Meaningful destination, resolved per dst_kind (cheap correlated lookups,
|
|
223
|
+
// NULL when not applicable): the target document's path, or the external
|
|
224
|
+
// node's URI. Lets `from edges` show real targets, not opaque node ids.
|
|
225
|
+
case "$dst_path": return { expr: `(SELECT dd.path FROM docs dd WHERE dd.doc_id = ${self}.dst_node)` };
|
|
226
|
+
case "$dst_uri": return { expr: `(SELECT xn.uri FROM external_nodes xn WHERE xn.node_id = ${self}.dst_node)` };
|
|
227
|
+
default: throw new FilterInvalid(`unknown intrinsic ${head} on edges`, "10 §2");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
switch (head) {
|
|
232
|
+
case "$id": return { expr: `${self}.block_id` };
|
|
233
|
+
case "$doc": return { expr: `${self}.doc_id` };
|
|
234
|
+
case "$path": return { expr: `${doc}.path` };
|
|
235
|
+
case "$ordinal": return { expr: `${self}.ordinal` };
|
|
236
|
+
case "$depth": return { expr: `${self}.depth` };
|
|
237
|
+
case "$content_hash": return { expr: `lower(hex(${self}.raw_hash))` };
|
|
238
|
+
case "$body": return { expr: `${self}.text` };
|
|
239
|
+
case "$updated_at": return { expr: `(SELECT MAX(c.ts) FROM block_changes bc JOIN commits c ON c.commit_id = bc.commit_id WHERE bc.block_id = ${self}.block_id)` };
|
|
240
|
+
default: throw new FilterInvalid(`unknown intrinsic ${head} on blocks`, "10 §2");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (target === "docs") {
|
|
245
|
+
if (head === "format")
|
|
246
|
+
return { expr: `${self}.format` };
|
|
247
|
+
const ref = propRef(field, target);
|
|
248
|
+
if (ref)
|
|
249
|
+
return { expr: propScalarExpr(ref, ctx) };
|
|
250
|
+
return { expr: `json_extract(${self}.metadata, ${jsonPath(segs)})` };
|
|
251
|
+
}
|
|
252
|
+
if (target === "nodes") {
|
|
253
|
+
if (head === "kind")
|
|
254
|
+
return { expr: `${self}.kind` };
|
|
255
|
+
if (head === "name")
|
|
256
|
+
return { expr: `${self}.name` };
|
|
257
|
+
if (head === "value")
|
|
258
|
+
return { expr: `${self}.value` };
|
|
259
|
+
if (head === "attrs") {
|
|
260
|
+
return { expr: `json_extract(${self}.attrs, ${jsonPath(segs.slice(1))})` };
|
|
261
|
+
}
|
|
262
|
+
if (head === "doc") {
|
|
263
|
+
const rest = segs.slice(1);
|
|
264
|
+
if (rest[0]?.startsWith("$")) {
|
|
265
|
+
return fieldSql({ kind: "field", segments: rest, intrinsic: true }, "docs", { self: doc, doc });
|
|
266
|
+
}
|
|
267
|
+
if (rest[0] === "format")
|
|
268
|
+
return { expr: `${doc}.format` };
|
|
269
|
+
const ref = propRef(field, target);
|
|
270
|
+
if (ref)
|
|
271
|
+
return { expr: propScalarExpr(ref, ctx) };
|
|
272
|
+
return { expr: `json_extract(${doc}.metadata, ${jsonPath(rest)})` };
|
|
273
|
+
}
|
|
274
|
+
if (head === "block") {
|
|
275
|
+
const rest = segs.slice(1);
|
|
276
|
+
if (rest[0] === "type")
|
|
277
|
+
return { expr: `(SELECT bb.type FROM blocks bb WHERE bb.block_id = ${self}.block_id)` };
|
|
278
|
+
if (rest[0] === "text")
|
|
279
|
+
return { expr: `(SELECT bb.text FROM blocks bb WHERE bb.block_id = ${self}.block_id)` };
|
|
280
|
+
throw new FilterInvalid(`unknown block field '${rest.join(".")}' on nodes`, "10 §2");
|
|
281
|
+
}
|
|
282
|
+
throw new FilterInvalid(`unknown field '${segs.join(".")}' on nodes`, "10 §2");
|
|
283
|
+
}
|
|
284
|
+
if (target === "edges") {
|
|
285
|
+
if (head === "predicate")
|
|
286
|
+
return { expr: `${self}.predicate` };
|
|
287
|
+
if (head === "provenance")
|
|
288
|
+
return { expr: `${self}.provenance` };
|
|
289
|
+
if (head === "dst_kind")
|
|
290
|
+
return { expr: `${self}.dst_kind` };
|
|
291
|
+
if (head === "anchor")
|
|
292
|
+
return { expr: `${self}.anchor` };
|
|
293
|
+
if (head === "src_field")
|
|
294
|
+
return { expr: `${self}.src_field` };
|
|
295
|
+
if (head === "doc") {
|
|
296
|
+
// reach-through to the SOURCE document (edges join docs on src_doc).
|
|
297
|
+
const rest = segs.slice(1);
|
|
298
|
+
if (rest[0]?.startsWith("$")) {
|
|
299
|
+
return fieldSql({ kind: "field", segments: rest, intrinsic: true }, "docs", { self: doc, doc });
|
|
300
|
+
}
|
|
301
|
+
if (rest[0] === "format")
|
|
302
|
+
return { expr: `${doc}.format` };
|
|
303
|
+
const ref = propRef(field, target);
|
|
304
|
+
if (ref)
|
|
305
|
+
return { expr: propScalarExpr(ref, ctx) };
|
|
306
|
+
return { expr: `json_extract(${doc}.metadata, ${jsonPath(rest)})` };
|
|
307
|
+
}
|
|
308
|
+
throw new FilterInvalid(`unknown field '${segs.join(".")}' on edges`, "10 §2");
|
|
309
|
+
}
|
|
310
|
+
// blocks target
|
|
311
|
+
if (head === "type")
|
|
312
|
+
return { expr: `${self}.type` };
|
|
313
|
+
if (head === "text")
|
|
314
|
+
return { expr: `${self}.text` };
|
|
315
|
+
if (head === "attrs") {
|
|
316
|
+
return { expr: `json_extract(${self}.attrs, ${jsonPath(segs.slice(1))})` };
|
|
317
|
+
}
|
|
318
|
+
if (head === "doc") {
|
|
319
|
+
const rest = segs.slice(1);
|
|
320
|
+
if (rest[0]?.startsWith("$")) {
|
|
321
|
+
return fieldSql({ kind: "field", segments: rest, intrinsic: true }, "docs", { self: doc, doc });
|
|
322
|
+
}
|
|
323
|
+
if (rest[0] === "format")
|
|
324
|
+
return { expr: `${doc}.format` };
|
|
325
|
+
const ref = propRef(field, target);
|
|
326
|
+
if (ref)
|
|
327
|
+
return { expr: propScalarExpr(ref, ctx) };
|
|
328
|
+
return { expr: `json_extract(${doc}.metadata, ${jsonPath(rest)})` };
|
|
329
|
+
}
|
|
330
|
+
throw new FilterInvalid(`unknown field '${segs.join(".")}' on blocks`, "10 §2");
|
|
331
|
+
}
|
|
332
|
+
function literalParam(lit) {
|
|
333
|
+
return lit.value;
|
|
334
|
+
}
|
|
335
|
+
// A scalar-valued operand in a comparison: a field, or a value-returning call
|
|
336
|
+
// (size(list(f)), child_count(), parent_type()). Returns a NULL-when-absent
|
|
337
|
+
// SQL scalar expression + its params.
|
|
338
|
+
function scalarSql(node, target, ctx) {
|
|
339
|
+
if (node.kind === "field")
|
|
340
|
+
return { expr: fieldSql(node, target, ctx).expr, params: [] };
|
|
341
|
+
if (node.kind === "outerref") {
|
|
342
|
+
const b = resolveOuter(ctx, node.name);
|
|
343
|
+
if (!b.scalar) {
|
|
344
|
+
throw new FilterInvalid(`^${node.name} is a collection binding; use it as \`<value> in ^${node.name}\`, not as a scalar`, "OQX ^");
|
|
345
|
+
}
|
|
346
|
+
return b.scalar();
|
|
347
|
+
}
|
|
348
|
+
if (node.kind === "call") {
|
|
349
|
+
switch (node.name) {
|
|
350
|
+
case "semantic":
|
|
351
|
+
return compileSemantic(argString(node.args, 0), target, ctx);
|
|
352
|
+
case "size": {
|
|
353
|
+
const inner = node.args[0];
|
|
354
|
+
if (inner && inner.kind === "call" && inner.name === "list") {
|
|
355
|
+
const lf = argField(inner.args, 0);
|
|
356
|
+
const ref = propRef(lf, target);
|
|
357
|
+
if (ref)
|
|
358
|
+
return { expr: propCountExpr(ref, ctx), params: [] };
|
|
359
|
+
const f = fieldSql(lf, target, ctx);
|
|
360
|
+
return {
|
|
361
|
+
expr: `(CASE WHEN ${f.expr} IS NULL THEN 0
|
|
362
|
+
WHEN json_valid(${f.expr}) AND json_type(${f.expr})='array'
|
|
363
|
+
THEN json_array_length(${f.expr})
|
|
364
|
+
ELSE 1 END)`,
|
|
365
|
+
params: [],
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
if (inner && inner.kind === "field") {
|
|
369
|
+
return { expr: `length(${fieldSql(inner, target, ctx).expr})`, params: [] };
|
|
370
|
+
}
|
|
371
|
+
throw new FilterInvalid("size() takes a field or list(field)");
|
|
372
|
+
}
|
|
373
|
+
case "child_count":
|
|
374
|
+
requireBlocks(target, "child_count");
|
|
375
|
+
return { expr: `(SELECT COUNT(*) FROM blocks cb WHERE cb.parent_block = ${ctx.self}.block_id AND cb.deleted_commit IS NULL)`, params: [] };
|
|
376
|
+
case "parent_type":
|
|
377
|
+
requireBlocks(target, "parent_type");
|
|
378
|
+
return { expr: `(SELECT pb.type FROM blocks pb WHERE pb.block_id = ${ctx.self}.parent_block)`, params: [] };
|
|
379
|
+
default:
|
|
380
|
+
throw new FilterInvalid(`function '${node.name}()' is not a comparable value`, "10 §5");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
throw new FilterInvalid("comparisons must be between a field/value and a literal", "10 §3.1");
|
|
384
|
+
}
|
|
385
|
+
function compileComparison(cmp, target, ctx) {
|
|
386
|
+
const rejectList = (n) => {
|
|
387
|
+
if (n.kind === "call" && n.name === "list") {
|
|
388
|
+
throw new FilterInvalid("list() is only valid inside `in`, size(), .all, .exists", "10 §4");
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
// Literal-on-one-side: the canonical form, kept byte-identical (a bound `?`).
|
|
392
|
+
if (cmp.right.kind === "literal" || cmp.left.kind === "literal") {
|
|
393
|
+
let scalar;
|
|
394
|
+
let litNode;
|
|
395
|
+
let op = cmp.op;
|
|
396
|
+
if (cmp.right.kind === "literal") {
|
|
397
|
+
scalar = cmp.left;
|
|
398
|
+
litNode = cmp.right;
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
scalar = cmp.right;
|
|
402
|
+
litNode = cmp.left;
|
|
403
|
+
op = flip(op);
|
|
404
|
+
}
|
|
405
|
+
rejectList(scalar);
|
|
406
|
+
const s = scalarSql(scalar, target, ctx);
|
|
407
|
+
// NULL-safe: absent operands yield NULL; `NULL <op> x` is NULL (falsy),
|
|
408
|
+
// exactly the absence=false rule — including `!=` (10 §3.3 note).
|
|
409
|
+
return {
|
|
410
|
+
sql: `(${s.expr} ${SQL_OP[op]} ?) IS 1`,
|
|
411
|
+
params: [...s.params, coerce(litNode)],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
// Neither side is a literal. This is only reachable via a `^name` correlation
|
|
415
|
+
// operand (`attrs.id == ^owner_id`) — the base `query` grammar still requires a
|
|
416
|
+
// literal (field-vs-field stays an error, unchanged behaviour).
|
|
417
|
+
if (cmp.left.kind !== "outerref" && cmp.right.kind !== "outerref") {
|
|
418
|
+
throw new FilterInvalid("one side of a comparison must be a literal", "10 §3.1");
|
|
419
|
+
}
|
|
420
|
+
rejectList(cmp.left);
|
|
421
|
+
rejectList(cmp.right);
|
|
422
|
+
const l = scalarSql(cmp.left, target, ctx);
|
|
423
|
+
const r = scalarSql(cmp.right, target, ctx);
|
|
424
|
+
return {
|
|
425
|
+
sql: `(${l.expr} ${SQL_OP[cmp.op]} ${r.expr}) IS 1`,
|
|
426
|
+
params: [...l.params, ...r.params],
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const SQL_OP = {
|
|
430
|
+
"==": "=", "!=": "<>", "<": "<", "<=": "<=", ">": ">", ">=": ">=",
|
|
431
|
+
};
|
|
432
|
+
function flip(op) {
|
|
433
|
+
switch (op) {
|
|
434
|
+
case "<": return ">";
|
|
435
|
+
case "<=": return ">=";
|
|
436
|
+
case ">": return "<";
|
|
437
|
+
case ">=": return "<=";
|
|
438
|
+
default: return op;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function coerce(lit) {
|
|
442
|
+
if (lit.type === "bool")
|
|
443
|
+
return lit.value ? 1 : 0;
|
|
444
|
+
return literalParam(lit);
|
|
445
|
+
}
|
|
446
|
+
// A bare field in boolean position coerces to false when absent/false.
|
|
447
|
+
function compileBoolField(field, target, ctx) {
|
|
448
|
+
const ref = propRef(field, target);
|
|
449
|
+
if (ref)
|
|
450
|
+
return { sql: propTruthyExists(ref, ctx), params: [] };
|
|
451
|
+
const f = fieldSql(field, target, ctx);
|
|
452
|
+
// truthy: not null, not 0, not '', not false
|
|
453
|
+
return {
|
|
454
|
+
sql: `(${f.expr} IS NOT NULL AND ${f.expr} NOT IN (0, '', 'false'))`,
|
|
455
|
+
params: [],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
// A value-expression fragment for OQX projection (`select name: <expr>`). Parses
|
|
459
|
+
// a single scalar CEL expression (a field or value-returning call) and returns a
|
|
460
|
+
// NULL-when-absent SQL scalar + params. Comparisons/booleans are not values and
|
|
461
|
+
// are rejected. The alias context is supplied by OQX per scope (defaults to the
|
|
462
|
+
// canonical per-target aliases).
|
|
463
|
+
export function scalarValue(node, target, ctx = defaultCtx(target)) {
|
|
464
|
+
return scalarSql(node, target, ctx);
|
|
465
|
+
}
|
|
466
|
+
export function compile(node, target, ctx = defaultCtx(target)) {
|
|
467
|
+
switch (node.kind) {
|
|
468
|
+
case "or": {
|
|
469
|
+
const l = compile(node.left, target, ctx);
|
|
470
|
+
const r = compile(node.right, target, ctx);
|
|
471
|
+
return { sql: `(${l.sql} OR ${r.sql})`, params: [...l.params, ...r.params] };
|
|
472
|
+
}
|
|
473
|
+
case "and": {
|
|
474
|
+
const l = compile(node.left, target, ctx);
|
|
475
|
+
const r = compile(node.right, target, ctx);
|
|
476
|
+
return { sql: `(${l.sql} AND ${r.sql})`, params: [...l.params, ...r.params] };
|
|
477
|
+
}
|
|
478
|
+
case "not": {
|
|
479
|
+
const inner = compile(node.operand, target, ctx);
|
|
480
|
+
return { sql: `(NOT (${inner.sql}))`, params: inner.params };
|
|
481
|
+
}
|
|
482
|
+
case "comparison":
|
|
483
|
+
return compileComparison(node, target, ctx);
|
|
484
|
+
case "membership":
|
|
485
|
+
return compileMembership(node.value, node.field, target, ctx);
|
|
486
|
+
case "outerMembership":
|
|
487
|
+
return compileOuterMembership(node.value, node.collection, target, ctx);
|
|
488
|
+
case "field":
|
|
489
|
+
return compileBoolField(node, target, ctx);
|
|
490
|
+
case "call":
|
|
491
|
+
return compileCall(node.name, node.args, target, ctx);
|
|
492
|
+
case "method":
|
|
493
|
+
return compileMethod(node, target, ctx);
|
|
494
|
+
default:
|
|
495
|
+
throw new FilterInvalid(`unsupported expression '${node.kind}' in this position`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function compileMembership(value, field, target, ctx) {
|
|
499
|
+
// Documents-target (and doc.<k>) properties: membership is an indexed EXISTS
|
|
500
|
+
// over ALL rows for the key (any card) — scalar or list value equals v.
|
|
501
|
+
const ref = propRef(field, target);
|
|
502
|
+
if (ref)
|
|
503
|
+
return { sql: propMemberExists(ref, ctx), params: [coerce(value)] };
|
|
504
|
+
// "v" in list(field): field may be scalar or JSON array. Match either the
|
|
505
|
+
// scalar equals v, or (when the value is a JSON array) the array contains v.
|
|
506
|
+
// Absent ⇒ NULL ⇒ [] ⇒ false. json_each over a non-array/NULL yields no rows,
|
|
507
|
+
// so a single EXISTS covers the array case; the scalar case is a direct `=`.
|
|
508
|
+
const f = fieldSql(field, target, ctx);
|
|
509
|
+
const isArray = `(json_valid(${f.expr}) AND json_type(${f.expr}) = 'array')`;
|
|
510
|
+
const sql = `(
|
|
511
|
+
(NOT ${isArray} AND ${f.expr} = ?)
|
|
512
|
+
OR (${isArray} AND EXISTS (SELECT 1 FROM json_each(${f.expr}) je WHERE je.value = ?))
|
|
513
|
+
)`;
|
|
514
|
+
// param order matches expr occurrences: isArray(1), f=?, isArray(1), value, isArray in EXISTS(1), value
|
|
515
|
+
return { sql, params: [coerce(value), coerce(value)] };
|
|
516
|
+
}
|
|
517
|
+
// `<value> in ^name`: does the one-scope-outward collection (a json array, e.g.
|
|
518
|
+
// a lift) contain the current scope's value? json_each yields no rows over a
|
|
519
|
+
// NULL/non-array, so absence ⇒ false. The collection expression is resolved from
|
|
520
|
+
// the enclosing scope (it references the parent row's aliases, valid here as a
|
|
521
|
+
// correlated subquery); its params precede the value's in statement order.
|
|
522
|
+
function compileOuterMembership(value, ref, target, ctx) {
|
|
523
|
+
const b = resolveOuter(ctx, ref.name);
|
|
524
|
+
if (!b.collection) {
|
|
525
|
+
throw new FilterInvalid(`^${ref.name} is a scalar binding; membership needs a collection (a lift or a parent collect)`, "OQX ^");
|
|
526
|
+
}
|
|
527
|
+
const coll = b.collection();
|
|
528
|
+
let vExpr;
|
|
529
|
+
let vParams;
|
|
530
|
+
if (value.kind === "literal") {
|
|
531
|
+
vExpr = "?";
|
|
532
|
+
vParams = [coerce(value)];
|
|
533
|
+
}
|
|
534
|
+
else {
|
|
535
|
+
const v = scalarSql(value, target, ctx);
|
|
536
|
+
vExpr = v.expr;
|
|
537
|
+
vParams = v.params;
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
sql: `EXISTS (SELECT 1 FROM json_each(${coll.expr}) je WHERE je.value = ${vExpr})`,
|
|
541
|
+
params: [...coll.params, ...vParams],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
function argField(args, i) {
|
|
545
|
+
const a = args[i];
|
|
546
|
+
if (!a || a.kind !== "field")
|
|
547
|
+
throw new FilterInvalid("expected a field argument");
|
|
548
|
+
return a;
|
|
549
|
+
}
|
|
550
|
+
function argString(args, i) {
|
|
551
|
+
const a = args[i];
|
|
552
|
+
if (!a || a.kind !== "literal" || a.type !== "string")
|
|
553
|
+
throw new FilterInvalid("expected a string argument");
|
|
554
|
+
return a.value;
|
|
555
|
+
}
|
|
556
|
+
function compileCall(name, args, target, ctx) {
|
|
557
|
+
switch (name) {
|
|
558
|
+
case "has": {
|
|
559
|
+
const hf = argField(args, 0);
|
|
560
|
+
const ref = propRef(hf, target);
|
|
561
|
+
if (ref)
|
|
562
|
+
return { sql: propHasExists(ref, ctx), params: [] };
|
|
563
|
+
const f = fieldSql(hf, target, ctx);
|
|
564
|
+
return { sql: `(${f.expr} IS NOT NULL)`, params: [] };
|
|
565
|
+
}
|
|
566
|
+
case "size":
|
|
567
|
+
throw new FilterInvalid("size(...) must be compared, e.g. size(list(tags)) > 2", "10 §4");
|
|
568
|
+
case "semantic":
|
|
569
|
+
// semantic(...) yields a cosine SCORE, not a boolean; it must be compared
|
|
570
|
+
// (threshold prune) or projected — e.g. semantic("aurora") > 0.6.
|
|
571
|
+
throw new FilterInvalid('semantic(...) returns a score; compare it, e.g. semantic("x") > 0.6', "OQX semantic");
|
|
572
|
+
// structural functions (blocks target) — 10 §5
|
|
573
|
+
case "under":
|
|
574
|
+
requireBlocks(target, "under");
|
|
575
|
+
return compileUnder(argString(args, 0), ctx);
|
|
576
|
+
case "under_heading":
|
|
577
|
+
requireBlocks(target, "under_heading");
|
|
578
|
+
return compileUnderHeading(argString(args, 0), ctx);
|
|
579
|
+
case "within":
|
|
580
|
+
requireBlocks(target, "within");
|
|
581
|
+
return compileWithin(argString(args, 0), ctx);
|
|
582
|
+
case "text":
|
|
583
|
+
return compileText(argString(args, 0), target, ctx);
|
|
584
|
+
case "has_edge":
|
|
585
|
+
return compileHasEdge(args, target, ctx);
|
|
586
|
+
case "has_anchor":
|
|
587
|
+
requireBlocks(target, "has_anchor");
|
|
588
|
+
return { sql: `EXISTS (SELECT 1 FROM edges e2 WHERE e2.src_block = ${ctx.self}.block_id AND e2.anchor IS NOT NULL)`, params: [] };
|
|
589
|
+
case "parent_type":
|
|
590
|
+
requireBlocks(target, "parent_type");
|
|
591
|
+
// parent_type() returns a value; only valid inside a comparison. Emitted
|
|
592
|
+
// as a scalar subquery via a synthetic field is complex; support the
|
|
593
|
+
// common form parent_type() == "x" by rewriting here is not possible
|
|
594
|
+
// without the comparison context, so expose as a scalar the caller wraps.
|
|
595
|
+
throw new FilterInvalid("parent_type() must be compared, e.g. parent_type() == \"blockquote\" (unsupported standalone)", "10 §5");
|
|
596
|
+
case "child_count":
|
|
597
|
+
requireBlocks(target, "child_count");
|
|
598
|
+
throw new FilterInvalid("child_count() must be compared, e.g. child_count() > 0", "10 §5");
|
|
599
|
+
case "under_kind":
|
|
600
|
+
requireBlocks(target, "under_kind");
|
|
601
|
+
return compileUnderKind(argString(args, 0), args.length >= 2 ? argString(args, 1) : undefined, ctx);
|
|
602
|
+
case "yaml_path":
|
|
603
|
+
requireBlocks(target, "yaml_path");
|
|
604
|
+
return compileYamlPath(argString(args, 0), ctx);
|
|
605
|
+
case "json_pointer":
|
|
606
|
+
requireBlocks(target, "json_pointer");
|
|
607
|
+
return compileJsonPointer(argString(args, 0), ctx);
|
|
608
|
+
case "list":
|
|
609
|
+
throw new FilterInvalid("list() is only valid inside `in`, size(), .all, .exists", "10 §4");
|
|
610
|
+
default:
|
|
611
|
+
throw new FilterInvalid(`unknown function '${name}'`, "10 §3.2");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function requireBlocks(target, fn) {
|
|
615
|
+
if (target !== "blocks")
|
|
616
|
+
throw new FilterInvalid(`${fn}() is only available on the blocks target`, "10 §5");
|
|
617
|
+
}
|
|
618
|
+
function compileUnder(target, ctx) {
|
|
619
|
+
// subtree membership via ancestor_path prefix, or section range when the
|
|
620
|
+
// target is a heading. v1: ancestor_path prefix on the target block id.
|
|
621
|
+
return {
|
|
622
|
+
sql: `(${ctx.self}.ancestor_path LIKE ? OR ${ctx.self}.block_id = ?)`,
|
|
623
|
+
params: [`%/${target}/%`, target],
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function compileUnderHeading(text, ctx) {
|
|
627
|
+
// Some ancestor section's heading text contains `text` (case-insensitive).
|
|
628
|
+
// Section ranges are in TOP-LEVEL ordinals, so we compare against the block's
|
|
629
|
+
// top-level ancestor's ordinal (the block itself if it is already top-level).
|
|
630
|
+
const top = topOrdinal(ctx.self);
|
|
631
|
+
return {
|
|
632
|
+
sql: `EXISTS (
|
|
633
|
+
SELECT 1 FROM sections s
|
|
634
|
+
JOIN blocks hb ON hb.block_id = s.heading_block
|
|
635
|
+
WHERE s.doc_id = ${ctx.self}.doc_id
|
|
636
|
+
AND lower(hb.text) LIKE '%' || lower(?) || '%'
|
|
637
|
+
AND ${top} >= s.first_ordinal AND ${top} <= s.last_ordinal
|
|
638
|
+
)`,
|
|
639
|
+
params: [text],
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
// Ordinal of a block's top-level ancestor. ancestor_path is '/b_x/b_y/…'; the
|
|
643
|
+
// first segment (when present) is the top-level ancestor. If the block is
|
|
644
|
+
// itself top-level (parent_block IS NULL), its own ordinal is the top ordinal.
|
|
645
|
+
// Parameterized by the block alias so it composes with OQX's per-scope aliases
|
|
646
|
+
// and cross-target relation correlations (e.g. section.blocks).
|
|
647
|
+
export function topOrdinal(self) {
|
|
648
|
+
return `(
|
|
649
|
+
CASE WHEN ${self}.parent_block IS NULL THEN ${self}.ordinal
|
|
650
|
+
ELSE (SELECT tb.ordinal FROM blocks tb
|
|
651
|
+
WHERE tb.doc_id = ${self}.doc_id AND tb.parent_block IS NULL
|
|
652
|
+
AND tb.block_id = REPLACE(
|
|
653
|
+
SUBSTR(${self}.ancestor_path, 2, INSTR(SUBSTR(${self}.ancestor_path, 2), '/') - 1), '/', ''))
|
|
654
|
+
END)`;
|
|
655
|
+
}
|
|
656
|
+
function compileWithin(target, ctx) {
|
|
657
|
+
// doc id, exact path, or glob path
|
|
658
|
+
if (target.startsWith("d_")) {
|
|
659
|
+
return { sql: `(${ctx.self}.doc_id = ?)`, params: [target] };
|
|
660
|
+
}
|
|
661
|
+
if (target.includes("*")) {
|
|
662
|
+
const like = target.replace(/[%_]/g, "\\$&").replace(/\*/g, "%");
|
|
663
|
+
return { sql: `(${ctx.doc}.path LIKE ? ESCAPE '\\')`, params: [like] };
|
|
664
|
+
}
|
|
665
|
+
return { sql: `(${ctx.doc}.path = ?)`, params: [target] };
|
|
666
|
+
}
|
|
667
|
+
// text("terms") — a full-text PRUNING predicate (FTS5, the same index the
|
|
668
|
+
// `query` tool's `text:` uses). It is a boolean, not a ranker: it says "this
|
|
669
|
+
// row's text matches", composing with every other predicate (and, notably,
|
|
670
|
+
// inside correlated subqueries / collects). bm25 relevance ordering is a
|
|
671
|
+
// ranking concern handled separately (order by). The literal is sanitized at
|
|
672
|
+
// compile time (fts-query.js); a query that reduces to no searchable token
|
|
673
|
+
// matches nothing (mirrors the `query` tool's empty-text short-circuit). Blocks
|
|
674
|
+
// match their own row; docs match when any of their blocks does; nodes use the
|
|
675
|
+
// node FTS index. Aliased by the scope so it works under OQX's per-scope aliases.
|
|
676
|
+
function compileText(terms, target, ctx) {
|
|
677
|
+
if (target === "edges")
|
|
678
|
+
throw new FilterInvalid("text(...) is not available on the edges target (edges carry no text index)", "10 §5");
|
|
679
|
+
const match = sanitizeFtsQuery(terms);
|
|
680
|
+
if (match === "")
|
|
681
|
+
return { sql: "(1 = 0)", params: [] };
|
|
682
|
+
if (target === "docs") {
|
|
683
|
+
return {
|
|
684
|
+
sql: `${ctx.self}.doc_id IN (SELECT b2.doc_id FROM blocks_fts JOIN blocks b2 ON b2.rowid = blocks_fts.rowid WHERE blocks_fts MATCH ?)`,
|
|
685
|
+
params: [match],
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const ftsTable = target === "nodes" ? "nodes_fts" : "blocks_fts";
|
|
689
|
+
return {
|
|
690
|
+
sql: `${ctx.self}.rowid IN (SELECT rowid FROM ${ftsTable} WHERE ${ftsTable} MATCH ?)`,
|
|
691
|
+
params: [match],
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
// semantic("phrase") — a scalar SCORE: cosine similarity of the current row's
|
|
695
|
+
// embedding to the (pre-computed) query vector, via the `cosine` UDF. A SCORE,
|
|
696
|
+
// not a ranker — usable as a threshold prune (`semantic("x") > 0.6`) or a
|
|
697
|
+
// projection (`select s: semantic("x")`); ORDER BY does the actual ranking.
|
|
698
|
+
// docs score their whole-document vector (doc_embeddings), blocks their own
|
|
699
|
+
// block vector (embeddings, keyed by raw_hash); nodes have no embeddings. A row
|
|
700
|
+
// with no stored vector scores NULL (cosine of a missing BLOB) ⇒ absent, so a
|
|
701
|
+
// threshold comparison excludes it (absence = false). The query vector + model
|
|
702
|
+
// come from ctx.semantic (the runner embedded the literal); absent ⇒ loud.
|
|
703
|
+
function compileSemantic(phrase, target, ctx) {
|
|
704
|
+
if (target === "nodes" || target === "edges") {
|
|
705
|
+
throw new FilterInvalid('semantic(...) is available on the docs and blocks targets (nodes/edges have no embeddings)', "OQX semantic");
|
|
706
|
+
}
|
|
707
|
+
const resolved = ctx.semantic?.(phrase);
|
|
708
|
+
if (!resolved) {
|
|
709
|
+
throw new FilterInvalid(`semantic(${JSON.stringify(phrase)}) needs an embedding provider; none is configured for this query`, "OQX semantic");
|
|
710
|
+
}
|
|
711
|
+
const expr = target === "docs"
|
|
712
|
+
? `(SELECT cosine(de.vec, ?) FROM doc_embeddings de WHERE de.doc_id = ${ctx.self}.doc_id AND de.model = ?)`
|
|
713
|
+
: `(SELECT cosine(e.vec, ?) FROM embeddings e WHERE e.content_hash = ${ctx.self}.raw_hash AND e.model = ? LIMIT 1)`;
|
|
714
|
+
return { expr, params: [resolved.vec, resolved.model] };
|
|
715
|
+
}
|
|
716
|
+
function compileHasEdge(args, target, ctx) {
|
|
717
|
+
const pred = argString(args, 0);
|
|
718
|
+
const srcCol = target === "blocks" ? `e2.src_block = ${ctx.self}.block_id` : `e2.src_doc = ${ctx.doc}.doc_id`;
|
|
719
|
+
if (args.length >= 2) {
|
|
720
|
+
const dst = argString(args, 1);
|
|
721
|
+
return {
|
|
722
|
+
sql: `EXISTS (SELECT 1 FROM edges e2 WHERE ${srcCol} AND e2.predicate = ? AND e2.to_commit IS NULL AND e2.dst_node = ?)`,
|
|
723
|
+
params: [pred, dst],
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
sql: `EXISTS (SELECT 1 FROM edges e2 WHERE ${srcCol} AND e2.predicate = ? AND e2.to_commit IS NULL)`,
|
|
728
|
+
params: [pred],
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
function compileMethod(node, target, ctx) {
|
|
732
|
+
const { receiver, name, args } = node;
|
|
733
|
+
if (receiver.kind !== "field") {
|
|
734
|
+
throw new FilterInvalid(`method .${name}() must be called on a field`, "10 §3.2");
|
|
735
|
+
}
|
|
736
|
+
const f = fieldSql(receiver, target, ctx);
|
|
737
|
+
switch (name) {
|
|
738
|
+
case "contains":
|
|
739
|
+
return { sql: `(${f.expr} LIKE '%' || ? || '%')`, params: [argString(args, 0)] };
|
|
740
|
+
case "startsWith":
|
|
741
|
+
return { sql: `(${f.expr} LIKE ? || '%')`, params: [argString(args, 0)] };
|
|
742
|
+
case "endsWith":
|
|
743
|
+
return { sql: `(${f.expr} LIKE '%' || ?)`, params: [argString(args, 0)] };
|
|
744
|
+
case "matches":
|
|
745
|
+
// RE2 not available in SQLite by default → mark for post-filter.
|
|
746
|
+
throw new FilterInvalid("matches() requires post-filter (not yet wired); use contains/startsWith for indexed queries", "10 §8");
|
|
747
|
+
default:
|
|
748
|
+
throw new FilterInvalid(`unknown method .${name}()`, "10 §3.2");
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// under_kind(kind, name?) — blocks that are children/descendants of a block
|
|
752
|
+
// with the given type and optional key/text match. Uses ancestor_path to find
|
|
753
|
+
// the ancestor block, then checks its type and attrs.
|
|
754
|
+
function compileUnderKind(kind, name, ctx) {
|
|
755
|
+
const self = ctx.self;
|
|
756
|
+
if (name) {
|
|
757
|
+
return {
|
|
758
|
+
sql: `EXISTS (
|
|
759
|
+
SELECT 1 FROM blocks ab
|
|
760
|
+
WHERE ab.doc_id = ${self}.doc_id
|
|
761
|
+
AND ${self}.ancestor_path LIKE '%/' || ab.block_id || '/%'
|
|
762
|
+
AND ab.type = ?
|
|
763
|
+
AND (ab.text LIKE '%' || ? || '%' OR json_extract(ab.attrs, '$.key') = ?)
|
|
764
|
+
AND ab.deleted_commit IS NULL
|
|
765
|
+
)`,
|
|
766
|
+
params: [kind, name, name],
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
sql: `EXISTS (
|
|
771
|
+
SELECT 1 FROM blocks ab
|
|
772
|
+
WHERE ab.doc_id = ${self}.doc_id
|
|
773
|
+
AND ${self}.ancestor_path LIKE '%/' || ab.block_id || '/%'
|
|
774
|
+
AND ab.type = ?
|
|
775
|
+
AND ab.deleted_commit IS NULL
|
|
776
|
+
)`,
|
|
777
|
+
params: [kind],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
// yaml_path("database.host") — blocks at a YAML key path. Builds nested
|
|
781
|
+
// EXISTS subqueries walking parent_block upward so each alias is in scope.
|
|
782
|
+
function compileYamlPath(path, ctx) {
|
|
783
|
+
const segments = path.split(".");
|
|
784
|
+
if (segments.length === 0)
|
|
785
|
+
throw new FilterInvalid("yaml_path() requires a non-empty key path");
|
|
786
|
+
if (segments.length === 1) {
|
|
787
|
+
return {
|
|
788
|
+
sql: `(${ctx.self}.type LIKE 'yaml:%' AND json_extract(${ctx.self}.attrs, '$.key') = ? AND ${ctx.self}.deleted_commit IS NULL)`,
|
|
789
|
+
params: [segments[0]],
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
return compileKeyPathChain(segments, "yaml", ctx);
|
|
793
|
+
}
|
|
794
|
+
// json_pointer("#/definitions/User") — blocks at a JSON Pointer path.
|
|
795
|
+
function compileJsonPointer(pointer, ctx) {
|
|
796
|
+
const normalized = pointer.replace(/^#?\/?/, "");
|
|
797
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
798
|
+
if (segments.length === 0)
|
|
799
|
+
throw new FilterInvalid("json_pointer() requires a non-empty pointer");
|
|
800
|
+
if (segments.length === 1) {
|
|
801
|
+
return {
|
|
802
|
+
sql: `(${ctx.self}.type LIKE 'json:%' AND json_extract(${ctx.self}.attrs, '$.key') = ? AND ${ctx.self}.deleted_commit IS NULL)`,
|
|
803
|
+
params: [segments[0]],
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return compileKeyPathChain(segments, "json", ctx);
|
|
807
|
+
}
|
|
808
|
+
// Build properly nested EXISTS for a multi-segment key path (yaml or json).
|
|
809
|
+
// segments = ["database", "host"]: the leaf block has key "host", its parent
|
|
810
|
+
// has key "database". Each ancestor check is a nested EXISTS so inner aliases
|
|
811
|
+
// can reference the enclosing scope.
|
|
812
|
+
function compileKeyPathChain(segments, prefix, ctx) {
|
|
813
|
+
const self = ctx.self;
|
|
814
|
+
const leaf = segments[segments.length - 1];
|
|
815
|
+
const ancestors = segments.slice(0, -1);
|
|
816
|
+
const params = [leaf];
|
|
817
|
+
// Build from outermost ancestor (root) to the leaf's immediate parent.
|
|
818
|
+
// The innermost (leaf) condition is on `self` itself; each ancestor wraps
|
|
819
|
+
// its child in a nested EXISTS.
|
|
820
|
+
let sql = `${self}.type LIKE '${prefix}:%' AND json_extract(${self}.attrs, '$.key') = ? AND ${self}.deleted_commit IS NULL`;
|
|
821
|
+
let innerRef = self;
|
|
822
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
823
|
+
const alias = `kp${i}`;
|
|
824
|
+
sql = `EXISTS (
|
|
825
|
+
SELECT 1 FROM blocks ${alias}
|
|
826
|
+
WHERE ${alias}.block_id = ${innerRef}.parent_block
|
|
827
|
+
AND ${alias}.type LIKE '${prefix}:%'
|
|
828
|
+
AND json_extract(${alias}.attrs, '$.key') = ?
|
|
829
|
+
AND ${alias}.deleted_commit IS NULL
|
|
830
|
+
) AND ${sql}`;
|
|
831
|
+
params.unshift(ancestors[i]);
|
|
832
|
+
innerRef = alias;
|
|
833
|
+
}
|
|
834
|
+
return { sql: `(${sql})`, params };
|
|
835
|
+
}
|
|
836
|
+
//# sourceMappingURL=compile.js.map
|