mrplex 0.0.1
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/README.md +284 -0
- package/dist/cli/config.d.ts +28 -0
- package/dist/cli/config.js +48 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/exit-codes.d.ts +13 -0
- package/dist/cli/exit-codes.js +39 -0
- package/dist/cli/exit-codes.js.map +1 -0
- package/dist/cli/format.d.ts +22 -0
- package/dist/cli/format.js +148 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/main.d.ts +16 -0
- package/dist/cli/main.js +1518 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/client/kernel-client.d.ts +97 -0
- package/dist/client/kernel-client.js +14 -0
- package/dist/client/kernel-client.js.map +1 -0
- package/dist/client/local.d.ts +26 -0
- package/dist/client/local.js +103 -0
- package/dist/client/local.js.map +1 -0
- package/dist/client/remote-mcp.d.ts +35 -0
- package/dist/client/remote-mcp.js +174 -0
- package/dist/client/remote-mcp.js.map +1 -0
- package/dist/embed/backfill.d.ts +24 -0
- package/dist/embed/backfill.js +33 -0
- package/dist/embed/backfill.js.map +1 -0
- package/dist/embed/chunker.d.ts +31 -0
- package/dist/embed/chunker.js +121 -0
- package/dist/embed/chunker.js.map +1 -0
- package/dist/embed/cmd-hook.d.ts +21 -0
- package/dist/embed/cmd-hook.js +100 -0
- package/dist/embed/cmd-hook.js.map +1 -0
- package/dist/embed/config.d.ts +61 -0
- package/dist/embed/config.js +109 -0
- package/dist/embed/config.js.map +1 -0
- package/dist/embed/hook.d.ts +38 -0
- package/dist/embed/hook.js +56 -0
- package/dist/embed/hook.js.map +1 -0
- package/dist/embed/http-hook.d.ts +13 -0
- package/dist/embed/http-hook.js +39 -0
- package/dist/embed/http-hook.js.map +1 -0
- package/dist/embed/worker.d.ts +62 -0
- package/dist/embed/worker.js +228 -0
- package/dist/embed/worker.js.map +1 -0
- package/dist/fixtures/hwe/characters/albion-ever.md +12 -0
- package/dist/fixtures/hwe/characters/cent.md +11 -0
- package/dist/fixtures/hwe/concepts/cent-demiurge-resonance.md +10 -0
- package/dist/fixtures/hwe/concepts/phase-looper.md +10 -0
- package/dist/fixtures/hwe/entities/demiurge.md +11 -0
- package/dist/fixtures/hwe/entities/monad.md +11 -0
- package/dist/fixtures/hwe/moc/globocorp-network.md +19 -0
- package/dist/fixtures/hwe/organizations/globocorp.md +11 -0
- package/dist/fixtures/hwe/readme.md +24 -0
- package/dist/fixtures/starship/crew/aria-okonkwo.md +14 -0
- package/dist/fixtures/starship/crew/bexley-orr.md +14 -0
- package/dist/fixtures/starship/crew/dax-thorne.md +14 -0
- package/dist/fixtures/starship/crew/isolde-marsh.md +16 -0
- package/dist/fixtures/starship/crew/kestrel-vance.md +15 -0
- package/dist/fixtures/starship/crew/quill-vasquez.md +14 -0
- package/dist/fixtures/starship/crew/soren-halloway.md +16 -0
- package/dist/fixtures/starship/encounters/the-drift-choir.md +14 -0
- package/dist/fixtures/starship/encounters/the-hollow-signal.md +16 -0
- package/dist/fixtures/starship/encounters/the-wandering-buoy.md +14 -0
- package/dist/fixtures/starship/equipment/coolant-loop-b.md +13 -0
- package/dist/fixtures/starship/equipment/emergency-ration-bars.md +12 -0
- package/dist/fixtures/starship/equipment/long-range-sensors.md +13 -0
- package/dist/fixtures/starship/equipment/plasma-manifold-3.md +15 -0
- package/dist/fixtures/starship/equipment/shuttle-corvid.md +13 -0
- package/dist/fixtures/starship/logs/marsh-4419-1.md +13 -0
- package/dist/fixtures/starship/logs/okonkwo-4415-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4413-1.md +13 -0
- package/dist/fixtures/starship/logs/orr-4420-1.md +12 -0
- package/dist/fixtures/starship/logs/thorne-4413-2.md +13 -0
- package/dist/fixtures/starship/logs/vance-4419-2.md +13 -0
- package/dist/fixtures/starship/misc/the-galley-ficus.md +11 -0
- package/dist/fixtures/starship/missions/the-cinder-run.md +17 -0
- package/dist/fixtures/starship/missions/the-drift-choir.md +17 -0
- package/dist/fixtures/starship/missions/the-hollow-signal.md +17 -0
- package/dist/fixtures/starship/missions/the-silent-beacon.md +17 -0
- package/dist/fixtures/starship/moc/crew.md +19 -0
- package/dist/fixtures/starship/moc/encounters.md +14 -0
- package/dist/fixtures/starship/moc/missions.md +15 -0
- package/dist/fixtures/starship/readme.md +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/auth/glob.d.ts +46 -0
- package/dist/kernel/auth/glob.js +112 -0
- package/dist/kernel/auth/glob.js.map +1 -0
- package/dist/kernel/auth/scope.d.ts +53 -0
- package/dist/kernel/auth/scope.js +83 -0
- package/dist/kernel/auth/scope.js.map +1 -0
- package/dist/kernel/casefold.d.ts +37 -0
- package/dist/kernel/casefold.js +44 -0
- package/dist/kernel/casefold.js.map +1 -0
- package/dist/kernel/constants.d.ts +21 -0
- package/dist/kernel/constants.js +22 -0
- package/dist/kernel/constants.js.map +1 -0
- package/dist/kernel/context.d.ts +70 -0
- package/dist/kernel/context.js +114 -0
- package/dist/kernel/context.js.map +1 -0
- package/dist/kernel/deletion.d.ts +50 -0
- package/dist/kernel/deletion.js +83 -0
- package/dist/kernel/deletion.js.map +1 -0
- package/dist/kernel/diff.d.ts +36 -0
- package/dist/kernel/diff.js +61 -0
- package/dist/kernel/diff.js.map +1 -0
- package/dist/kernel/errors.d.ts +37 -0
- package/dist/kernel/errors.js +55 -0
- package/dist/kernel/errors.js.map +1 -0
- package/dist/kernel/frontmatter-input.d.ts +32 -0
- package/dist/kernel/frontmatter-input.js +85 -0
- package/dist/kernel/frontmatter-input.js.map +1 -0
- package/dist/kernel/graph.d.ts +34 -0
- package/dist/kernel/graph.js +559 -0
- package/dist/kernel/graph.js.map +1 -0
- package/dist/kernel/history.d.ts +74 -0
- package/dist/kernel/history.js +143 -0
- package/dist/kernel/history.js.map +1 -0
- package/dist/kernel/kernel.d.ts +162 -0
- package/dist/kernel/kernel.js +675 -0
- package/dist/kernel/kernel.js.map +1 -0
- package/dist/kernel/path-config.d.ts +101 -0
- package/dist/kernel/path-config.js +171 -0
- package/dist/kernel/path-config.js.map +1 -0
- package/dist/kernel/query/ast.d.ts +24 -0
- package/dist/kernel/query/ast.js +52 -0
- package/dist/kernel/query/ast.js.map +1 -0
- package/dist/kernel/query/cel-parse.d.ts +37 -0
- package/dist/kernel/query/cel-parse.js +88 -0
- package/dist/kernel/query/cel-parse.js.map +1 -0
- package/dist/kernel/query/degrees.d.ts +31 -0
- package/dist/kernel/query/degrees.js +135 -0
- package/dist/kernel/query/degrees.js.map +1 -0
- package/dist/kernel/query/graph-ast.d.ts +51 -0
- package/dist/kernel/query/graph-ast.js +139 -0
- package/dist/kernel/query/graph-ast.js.map +1 -0
- package/dist/kernel/query/query.d.ts +56 -0
- package/dist/kernel/query/query.js +276 -0
- package/dist/kernel/query/query.js.map +1 -0
- package/dist/kernel/validation.d.ts +64 -0
- package/dist/kernel/validation.js +183 -0
- package/dist/kernel/validation.js.map +1 -0
- package/dist/kernel/version-id.d.ts +10 -0
- package/dist/kernel/version-id.js +21 -0
- package/dist/kernel/version-id.js.map +1 -0
- package/dist/kernel/wire.d.ts +148 -0
- package/dist/kernel/wire.js +6 -0
- package/dist/kernel/wire.js.map +1 -0
- package/dist/links/backfill.d.ts +28 -0
- package/dist/links/backfill.js +33 -0
- package/dist/links/backfill.js.map +1 -0
- package/dist/links/extract.d.ts +29 -0
- package/dist/links/extract.js +233 -0
- package/dist/links/extract.js.map +1 -0
- package/dist/links/link-config.d.ts +70 -0
- package/dist/links/link-config.js +107 -0
- package/dist/links/link-config.js.map +1 -0
- package/dist/links/maintain.d.ts +37 -0
- package/dist/links/maintain.js +91 -0
- package/dist/links/maintain.js.map +1 -0
- package/dist/links/repair.d.ts +46 -0
- package/dist/links/repair.js +96 -0
- package/dist/links/repair.js.map +1 -0
- package/dist/links/resolve.d.ts +41 -0
- package/dist/links/resolve.js +121 -0
- package/dist/links/resolve.js.map +1 -0
- package/dist/links/stale.d.ts +32 -0
- package/dist/links/stale.js +73 -0
- package/dist/links/stale.js.map +1 -0
- package/dist/markdown/content-hash.d.ts +34 -0
- package/dist/markdown/content-hash.js +46 -0
- package/dist/markdown/content-hash.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +69 -0
- package/dist/markdown/frontmatter.js +147 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/hash-backfill.d.ts +22 -0
- package/dist/markdown/hash-backfill.js +43 -0
- package/dist/markdown/hash-backfill.js.map +1 -0
- package/dist/mcp/query-syntax.d.ts +20 -0
- package/dist/mcp/query-syntax.js +194 -0
- package/dist/mcp/query-syntax.js.map +1 -0
- package/dist/mcp/render.d.ts +31 -0
- package/dist/mcp/render.js +110 -0
- package/dist/mcp/render.js.map +1 -0
- package/dist/mcp/server.d.ts +60 -0
- package/dist/mcp/server.js +170 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +93 -0
- package/dist/mcp/tools.js +1218 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/rest/conditional.d.ts +37 -0
- package/dist/rest/conditional.js +58 -0
- package/dist/rest/conditional.js.map +1 -0
- package/dist/rest/negotiate.d.ts +33 -0
- package/dist/rest/negotiate.js +54 -0
- package/dist/rest/negotiate.js.map +1 -0
- package/dist/rest/routes.d.ts +37 -0
- package/dist/rest/routes.js +744 -0
- package/dist/rest/routes.js.map +1 -0
- package/dist/seed/fixture-seed.d.ts +35 -0
- package/dist/seed/fixture-seed.js +75 -0
- package/dist/seed/fixture-seed.js.map +1 -0
- package/dist/seed/starship-config.d.ts +3 -0
- package/dist/seed/starship-config.js +5 -0
- package/dist/seed/starship-config.js.map +1 -0
- package/dist/server/headers.d.ts +36 -0
- package/dist/server/headers.js +30 -0
- package/dist/server/headers.js.map +1 -0
- package/dist/server/http-error.d.ts +53 -0
- package/dist/server/http-error.js +104 -0
- package/dist/server/http-error.js.map +1 -0
- package/dist/server/serve.d.ts +56 -0
- package/dist/server/serve.js +135 -0
- package/dist/server/serve.js.map +1 -0
- package/dist/shell/audit.d.ts +32 -0
- package/dist/shell/audit.js +28 -0
- package/dist/shell/audit.js.map +1 -0
- package/dist/shell/guard.d.ts +50 -0
- package/dist/shell/guard.js +176 -0
- package/dist/shell/guard.js.map +1 -0
- package/dist/shell/keys.d.ts +36 -0
- package/dist/shell/keys.js +64 -0
- package/dist/shell/keys.js.map +1 -0
- package/dist/shell/login.d.ts +56 -0
- package/dist/shell/login.js +130 -0
- package/dist/shell/login.js.map +1 -0
- package/dist/shell/oidc.d.ts +62 -0
- package/dist/shell/oidc.js +102 -0
- package/dist/shell/oidc.js.map +1 -0
- package/dist/shell/policy.d.ts +100 -0
- package/dist/shell/policy.js +232 -0
- package/dist/shell/policy.js.map +1 -0
- package/dist/shell/proxy-policy.d.ts +34 -0
- package/dist/shell/proxy-policy.js +120 -0
- package/dist/shell/proxy-policy.js.map +1 -0
- package/dist/shell/proxy.d.ts +47 -0
- package/dist/shell/proxy.js +253 -0
- package/dist/shell/proxy.js.map +1 -0
- package/dist/shell/serve.d.ts +86 -0
- package/dist/shell/serve.js +0 -0
- package/dist/shell/serve.js.map +1 -0
- package/dist/shell/stdio.d.ts +55 -0
- package/dist/shell/stdio.js +63 -0
- package/dist/shell/stdio.js.map +1 -0
- package/dist/storage/registry.d.ts +20 -0
- package/dist/storage/registry.js +38 -0
- package/dist/storage/registry.js.map +1 -0
- package/dist/storage/search-plan.d.ts +93 -0
- package/dist/storage/search-plan.js +15 -0
- package/dist/storage/search-plan.js.map +1 -0
- package/dist/storage/types.d.ts +448 -0
- package/dist/storage/types.js +14 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/storage/versions-since.d.ts +72 -0
- package/dist/storage/versions-since.js +92 -0
- package/dist/storage/versions-since.js.map +1 -0
- package/dist/storage-postgres/adapter.d.ts +17 -0
- package/dist/storage-postgres/adapter.js +782 -0
- package/dist/storage-postgres/adapter.js.map +1 -0
- package/dist/storage-postgres/compile-postgres.d.ts +22 -0
- package/dist/storage-postgres/compile-postgres.js +830 -0
- package/dist/storage-postgres/compile-postgres.js.map +1 -0
- package/dist/storage-postgres/errors.d.ts +34 -0
- package/dist/storage-postgres/errors.js +50 -0
- package/dist/storage-postgres/errors.js.map +1 -0
- package/dist/storage-postgres/migrations/0001_init.sql +113 -0
- package/dist/storage-postgres/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-postgres/migrations/index.d.ts +22 -0
- package/dist/storage-postgres/migrations/index.js +70 -0
- package/dist/storage-postgres/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/adapter.d.ts +2 -0
- package/dist/storage-sqlite/adapter.js +688 -0
- package/dist/storage-sqlite/adapter.js.map +1 -0
- package/dist/storage-sqlite/compile-filter.d.ts +41 -0
- package/dist/storage-sqlite/compile-filter.js +881 -0
- package/dist/storage-sqlite/compile-filter.js.map +1 -0
- package/dist/storage-sqlite/compile-sqlite.d.ts +19 -0
- package/dist/storage-sqlite/compile-sqlite.js +176 -0
- package/dist/storage-sqlite/compile-sqlite.js.map +1 -0
- package/dist/storage-sqlite/migrations/0001_init.sql +123 -0
- package/dist/storage-sqlite/migrations/0002_content_hash.sql +7 -0
- package/dist/storage-sqlite/migrations/index.d.ts +6 -0
- package/dist/storage-sqlite/migrations/index.js +41 -0
- package/dist/storage-sqlite/migrations/index.js.map +1 -0
- package/dist/storage-sqlite/vec.d.ts +40 -0
- package/dist/storage-sqlite/vec.js +62 -0
- package/dist/storage-sqlite/vec.js.map +1 -0
- package/dist/sync/cursor.d.ts +36 -0
- package/dist/sync/cursor.js +66 -0
- package/dist/sync/cursor.js.map +1 -0
- package/dist/sync/daemon.d.ts +40 -0
- package/dist/sync/daemon.js +202 -0
- package/dist/sync/daemon.js.map +1 -0
- package/dist/sync/feed.d.ts +38 -0
- package/dist/sync/feed.js +125 -0
- package/dist/sync/feed.js.map +1 -0
- package/dist/sync/fs-store.d.ts +8 -0
- package/dist/sync/fs-store.js +76 -0
- package/dist/sync/fs-store.js.map +1 -0
- package/dist/sync/intrinsics.d.ts +52 -0
- package/dist/sync/intrinsics.js +76 -0
- package/dist/sync/intrinsics.js.map +1 -0
- package/dist/sync/paths.d.ts +38 -0
- package/dist/sync/paths.js +56 -0
- package/dist/sync/paths.js.map +1 -0
- package/dist/sync/push.d.ts +48 -0
- package/dist/sync/push.js +328 -0
- package/dist/sync/push.js.map +1 -0
- package/dist/sync/reconcile.d.ts +54 -0
- package/dist/sync/reconcile.js +252 -0
- package/dist/sync/reconcile.js.map +1 -0
- package/dist/sync/run.d.ts +27 -0
- package/dist/sync/run.js +46 -0
- package/dist/sync/run.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +5 -0
- package/dist/version.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CEL AST → SQLite SQL compiler — design §5.
|
|
3
|
+
*
|
|
4
|
+
* Every user value ends up as a `?` placeholder in the emitted SQL; no
|
|
5
|
+
* string interpolation of literals. The kernel wraps the returned
|
|
6
|
+
* fragment in the WHERE clause of a versions query.
|
|
7
|
+
*
|
|
8
|
+
* Design-mandated ergonomics:
|
|
9
|
+
* • Bare identifiers refer to frontmatter keys, compiled to
|
|
10
|
+
* `json_extract(versions.frontmatter, '$."name"')`.
|
|
11
|
+
* • `$path`, `$updated_at` — kernel-owned intrinsics, compiled to
|
|
12
|
+
* `versions.path` / `versions.created_at` (§5.1). `$updated_at` names
|
|
13
|
+
* the intrinsic from the caller's mental model — queries always see
|
|
14
|
+
* current versions, so the version's created_at IS the doc's last
|
|
15
|
+
* update time.
|
|
16
|
+
* • `list(x)` — a compile-time hint that the enclosing expression should
|
|
17
|
+
* compile against scalar OR list frontmatter shapes (§5.2).
|
|
18
|
+
* • Missing key → false predicate (m2-plan §5 decision).
|
|
19
|
+
*
|
|
20
|
+
* Compiled SQL fragments are pure boolean predicates (no SELECT, no FROM);
|
|
21
|
+
* the kernel wraps them in the main query.
|
|
22
|
+
*/
|
|
23
|
+
import { globToRegexSource } from "../kernel/auth/glob.js";
|
|
24
|
+
import { KernelError } from "../kernel/errors.js";
|
|
25
|
+
import { unwrapConstant, unwrapListHint } from "../kernel/query/ast.js";
|
|
26
|
+
import { INTRINSIC_PREFIX } from "../kernel/query/cel-parse.js";
|
|
27
|
+
import { asGraphCollection, asGraphMembership, assertNotReservedGraphName, } from "../kernel/query/graph-ast.js";
|
|
28
|
+
import { BODY_FIELD } from "../links/extract.js";
|
|
29
|
+
// Per-compile state for the current pass: the context plus a graph-subquery
|
|
30
|
+
// alias counter (reset each compileFilter so aliases are stable per query,
|
|
31
|
+
// matching the Postgres compiler's per-Builder counter). Compilation is
|
|
32
|
+
// fully synchronous, so a module-level cell is safe and avoids threading
|
|
33
|
+
// state through every compileExpr signature.
|
|
34
|
+
let CTX = {};
|
|
35
|
+
let GRAPH_ALIAS_SEQ = 0;
|
|
36
|
+
export function compileFilter(expr, ctx = {}) {
|
|
37
|
+
CTX = ctx;
|
|
38
|
+
GRAPH_ALIAS_SEQ = 0;
|
|
39
|
+
try {
|
|
40
|
+
return compileExpr(expr);
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
CTX = {};
|
|
44
|
+
GRAPH_ALIAS_SEQ = 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Alias for a graph subquery's joined `versions` row; unique per compile. */
|
|
48
|
+
function nextGraphAlias() {
|
|
49
|
+
return `lv${GRAPH_ALIAS_SEQ++}`;
|
|
50
|
+
}
|
|
51
|
+
// -----------------------------------------------------------------------------
|
|
52
|
+
// Intrinsic + frontmatter symbol resolution
|
|
53
|
+
// -----------------------------------------------------------------------------
|
|
54
|
+
const INTRINSIC_COLUMNS = {
|
|
55
|
+
path: "versions.path",
|
|
56
|
+
// `$updated_at` reads as "the timestamp of the current version" — which is
|
|
57
|
+
// what filters see (`next_id IS NULL` is implicit). The underlying column
|
|
58
|
+
// is `versions.created_at` (when this version row was written), but from
|
|
59
|
+
// a query-writer's mental model that's the doc's last update time.
|
|
60
|
+
updated_at: "versions.created_at",
|
|
61
|
+
// The document body — a text column, addressable via $body for the
|
|
62
|
+
// consistency of the $-prefixed convention. Design §5.1's example
|
|
63
|
+
// `contains(body, "pricing")` is updated to `$body`.
|
|
64
|
+
body: "versions.body",
|
|
65
|
+
// Canonical content fingerprint (sync/history plan §2.5). Reads the
|
|
66
|
+
// persisted column; pre-backfill rows carry null and simply don't match
|
|
67
|
+
// a hash filter until `mrplex hash backfill` populates them (§2.6).
|
|
68
|
+
content_hash: "versions.content_hash",
|
|
69
|
+
};
|
|
70
|
+
function compileIntrinsic(mangledName) {
|
|
71
|
+
const name = mangledName.slice(INTRINSIC_PREFIX.length);
|
|
72
|
+
const column = INTRINSIC_COLUMNS[name];
|
|
73
|
+
if (!column) {
|
|
74
|
+
// Derive the "expected" list from INTRINSIC_COLUMNS so the message
|
|
75
|
+
// can't drift from the code.
|
|
76
|
+
const expected = Object.keys(INTRINSIC_COLUMNS)
|
|
77
|
+
.map((k) => `$${k}`)
|
|
78
|
+
.join(", ");
|
|
79
|
+
throw new KernelError("filter_invalid", {
|
|
80
|
+
reason: `unknown intrinsic $${name} (expected ${expected})`,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return column;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Escape a JSON-path key for the `'$."name"'` form. SQLite's json1 doesn't
|
|
87
|
+
* allow `"` inside a double-quoted key in the path — but our frontmatter
|
|
88
|
+
* keys come through YAML validation and don't include double quotes in
|
|
89
|
+
* practice. We defensively escape by doubling the `"` (SQLite's convention
|
|
90
|
+
* for identifier quoting) and refusing keys with newlines / `$` prefixes.
|
|
91
|
+
*/
|
|
92
|
+
function frontmatterExtract(name) {
|
|
93
|
+
if (name.includes("\n") || name.includes("\r")) {
|
|
94
|
+
throw new KernelError("filter_invalid", {
|
|
95
|
+
reason: `frontmatter key contains newline: ${JSON.stringify(name)}`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const escaped = name.replace(/"/g, '""');
|
|
99
|
+
return `json_extract(versions.frontmatter, '$."${escaped}"')`;
|
|
100
|
+
}
|
|
101
|
+
// -----------------------------------------------------------------------------
|
|
102
|
+
// Expression compiler — dispatches on exprKind.case
|
|
103
|
+
// -----------------------------------------------------------------------------
|
|
104
|
+
function compileExpr(expr) {
|
|
105
|
+
const kind = expr.exprKind.case;
|
|
106
|
+
switch (kind) {
|
|
107
|
+
case "constExpr":
|
|
108
|
+
return compileConst(expr);
|
|
109
|
+
case "identExpr":
|
|
110
|
+
return compileIdent(expr);
|
|
111
|
+
case "selectExpr":
|
|
112
|
+
return compileSelect(expr);
|
|
113
|
+
case "callExpr":
|
|
114
|
+
return compileCall(expr);
|
|
115
|
+
case "comprehensionExpr":
|
|
116
|
+
return compileComprehension(expr);
|
|
117
|
+
case "listExpr":
|
|
118
|
+
case "structExpr":
|
|
119
|
+
throw new KernelError("filter_invalid", {
|
|
120
|
+
reason: `${kind ?? "unknown"} expressions are not supported in filters`,
|
|
121
|
+
});
|
|
122
|
+
default:
|
|
123
|
+
throw new KernelError("filter_invalid", {
|
|
124
|
+
reason: `unsupported expression kind: ${String(kind)}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function compileConst(expr) {
|
|
129
|
+
const value = unwrapConstant(expr);
|
|
130
|
+
if (value === undefined) {
|
|
131
|
+
throw new KernelError("filter_invalid", { reason: "unsupported constant kind" });
|
|
132
|
+
}
|
|
133
|
+
if (value === null)
|
|
134
|
+
return { sql: "NULL", params: [] };
|
|
135
|
+
if (typeof value === "boolean")
|
|
136
|
+
return { sql: value ? "1" : "0", params: [] };
|
|
137
|
+
return { sql: "?", params: [value] };
|
|
138
|
+
}
|
|
139
|
+
function compileIdent(expr) {
|
|
140
|
+
if (expr.exprKind.case !== "identExpr")
|
|
141
|
+
throw new Error("compileIdent: wrong kind");
|
|
142
|
+
const name = expr.exprKind.value.name;
|
|
143
|
+
if (name.startsWith(INTRINSIC_PREFIX)) {
|
|
144
|
+
return { sql: compileIntrinsic(name), params: [] };
|
|
145
|
+
}
|
|
146
|
+
// Bare identifiers are frontmatter keys. Missing key → predicate false
|
|
147
|
+
// (m2-plan §5 decision). The compileCall layer wraps comparisons to make
|
|
148
|
+
// that shape work; the raw ident extract is what feeds them.
|
|
149
|
+
return { sql: frontmatterExtract(name), params: [] };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Selects like `x.y` — we support nested frontmatter access when the LHS is
|
|
153
|
+
* a bare identifier or another select.
|
|
154
|
+
*/
|
|
155
|
+
function compileSelect(expr) {
|
|
156
|
+
if (expr.exprKind.case !== "selectExpr")
|
|
157
|
+
throw new Error("compileSelect: wrong kind");
|
|
158
|
+
const sel = expr.exprKind.value;
|
|
159
|
+
const path = collectSelectPath(expr);
|
|
160
|
+
if (!path) {
|
|
161
|
+
throw new KernelError("filter_invalid", {
|
|
162
|
+
reason: `unsupported select expression: field '${sel.field}'`,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
// The first segment is the root — either an intrinsic or frontmatter.
|
|
166
|
+
const [root, ...rest] = path;
|
|
167
|
+
const rootExpr = root;
|
|
168
|
+
if (rootExpr.startsWith(INTRINSIC_PREFIX)) {
|
|
169
|
+
if (rest.length > 0) {
|
|
170
|
+
throw new KernelError("filter_invalid", {
|
|
171
|
+
reason: `intrinsics do not have subfields ($${rootExpr.slice(INTRINSIC_PREFIX.length)})`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return { sql: compileIntrinsic(rootExpr), params: [] };
|
|
175
|
+
}
|
|
176
|
+
const parts = [rootExpr, ...rest].map((p) => `"${p.replace(/"/g, '""')}"`);
|
|
177
|
+
return {
|
|
178
|
+
sql: `json_extract(versions.frontmatter, '$.${parts.join(".")}')`,
|
|
179
|
+
params: [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function collectSelectPath(expr) {
|
|
183
|
+
if (expr.exprKind.case === "identExpr")
|
|
184
|
+
return [expr.exprKind.value.name];
|
|
185
|
+
if (expr.exprKind.case !== "selectExpr")
|
|
186
|
+
return undefined;
|
|
187
|
+
const inner = expr.exprKind.value.operand;
|
|
188
|
+
if (!inner)
|
|
189
|
+
return undefined;
|
|
190
|
+
const innerPath = collectSelectPath(inner);
|
|
191
|
+
if (!innerPath)
|
|
192
|
+
return undefined;
|
|
193
|
+
return [...innerPath, expr.exprKind.value.field];
|
|
194
|
+
}
|
|
195
|
+
// -----------------------------------------------------------------------------
|
|
196
|
+
// Call dispatch
|
|
197
|
+
// -----------------------------------------------------------------------------
|
|
198
|
+
function compileCall(expr) {
|
|
199
|
+
if (expr.exprKind.case !== "callExpr")
|
|
200
|
+
throw new Error("compileCall: wrong kind");
|
|
201
|
+
const call = expr.exprKind.value;
|
|
202
|
+
const fn = call.function;
|
|
203
|
+
const args = call.args;
|
|
204
|
+
const target = call.target;
|
|
205
|
+
// Boolean/comparison operators
|
|
206
|
+
const binOp = BIN_OPS[fn];
|
|
207
|
+
if (binOp && args.length === 2) {
|
|
208
|
+
return compileBinary(binOp, args[0], args[1]);
|
|
209
|
+
}
|
|
210
|
+
if (fn === "!_" && args.length === 1)
|
|
211
|
+
return compileNot(args[0]);
|
|
212
|
+
if (fn === "-_" && args.length === 1) {
|
|
213
|
+
const inner = compileExpr(args[0]);
|
|
214
|
+
return { sql: `-(${inner.sql})`, params: inner.params };
|
|
215
|
+
}
|
|
216
|
+
if (fn === "@in" && args.length === 2) {
|
|
217
|
+
return compileIn(args[0], args[1]);
|
|
218
|
+
}
|
|
219
|
+
// Free-standing calls
|
|
220
|
+
if (target === undefined) {
|
|
221
|
+
if (fn === "size" && args.length === 1)
|
|
222
|
+
return compileSize(args[0]);
|
|
223
|
+
if (fn === "contains" && args.length === 2) {
|
|
224
|
+
return compileContains(args[0], args[1]);
|
|
225
|
+
}
|
|
226
|
+
if (fn === "startsWith" && args.length === 2) {
|
|
227
|
+
return compileStartsWith(args[0], args[1]);
|
|
228
|
+
}
|
|
229
|
+
if (fn === "endsWith" && args.length === 2) {
|
|
230
|
+
return compileEndsWith(args[0], args[1]);
|
|
231
|
+
}
|
|
232
|
+
if (fn === "matches" && args.length === 2) {
|
|
233
|
+
return compileMatches(args[0], args[1]);
|
|
234
|
+
}
|
|
235
|
+
if (fn === "list" && args.length === 1) {
|
|
236
|
+
// Bare `list(x)` outside a `@in` / comprehension context is a silent
|
|
237
|
+
// semantic loss — `list(tags) == "foo"` would compile as
|
|
238
|
+
// `tags == "foo"`, which fails on list-valued frontmatter. Since
|
|
239
|
+
// list() is a compile-time hint (§5.2) that only makes sense in
|
|
240
|
+
// hint-consuming contexts, reject it here so users get a clear
|
|
241
|
+
// signal that they probably meant `"foo" in list(tags)`.
|
|
242
|
+
throw new KernelError("filter_invalid", {
|
|
243
|
+
reason: "list() is a hint that must sit inside `in` or a comprehension (.all/.exists), not stand alone",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// Method-style calls: `x.startsWith(y)`, etc.
|
|
248
|
+
if (target) {
|
|
249
|
+
// `$backlinks_static().size()` parses as method-form size (target =
|
|
250
|
+
// the collection call, zero args) — dispatch to the collection COUNT.
|
|
251
|
+
if (fn === "size" && args.length === 0) {
|
|
252
|
+
const collection = asGraphCollection(target);
|
|
253
|
+
if (collection)
|
|
254
|
+
return compileGraphCollectionSize(collection);
|
|
255
|
+
}
|
|
256
|
+
if (fn === "startsWith" && args.length === 1) {
|
|
257
|
+
return compileStartsWith(target, args[0]);
|
|
258
|
+
}
|
|
259
|
+
if (fn === "endsWith" && args.length === 1) {
|
|
260
|
+
return compileEndsWith(target, args[0]);
|
|
261
|
+
}
|
|
262
|
+
if (fn === "contains" && args.length === 1) {
|
|
263
|
+
return compileContains(target, args[0]);
|
|
264
|
+
}
|
|
265
|
+
if (fn === "matches" && args.length === 1) {
|
|
266
|
+
return compileMatches(target, args[0]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Graph membership predicates: $in_static / $has_static (§11.2).
|
|
270
|
+
const membership = asGraphMembership(expr);
|
|
271
|
+
if (membership)
|
|
272
|
+
return compileGraphMembership(membership);
|
|
273
|
+
// A bare collection call ($backlinks_static()) reaching a boolean context
|
|
274
|
+
// is meaningless — collections only compose via .size()/.exists()/.all().
|
|
275
|
+
const collection = asGraphCollection(expr);
|
|
276
|
+
if (collection) {
|
|
277
|
+
throw new KernelError("filter_invalid", {
|
|
278
|
+
reason: `$${collection.direction}() is a collection; use .size(), .exists(), or .all()`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
// Reserved `_dyn` graph names (need Phase 2 embedded queries).
|
|
282
|
+
assertNotReservedGraphName(fn);
|
|
283
|
+
throw new KernelError("filter_invalid", {
|
|
284
|
+
reason: `unsupported function: ${target ? "<target>." : ""}${fn}(...) with ${args.length} arg(s)`,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
const BIN_OPS = {
|
|
288
|
+
"_==_": "=",
|
|
289
|
+
"_!=_": "<>",
|
|
290
|
+
"_<_": "<",
|
|
291
|
+
"_<=_": "<=",
|
|
292
|
+
"_>_": ">",
|
|
293
|
+
"_>=_": ">=",
|
|
294
|
+
"_&&_": "AND",
|
|
295
|
+
"_||_": "OR",
|
|
296
|
+
};
|
|
297
|
+
// -----------------------------------------------------------------------------
|
|
298
|
+
// Graph predicates — $in_static / $has_static / $backlinks_static() /
|
|
299
|
+
// $links_static() over the `links` index (design §11.2, WS4).
|
|
300
|
+
//
|
|
301
|
+
// Direction fixes which end of the edge the OUTER `versions` row sits on:
|
|
302
|
+
// in / backlinks → outer doc is the TARGET; the OTHER end is the source.
|
|
303
|
+
// has / links → outer doc is the SOURCE; the OTHER end is the target.
|
|
304
|
+
//
|
|
305
|
+
// Scope (§5 decision 5): the other endpoint's version row carries the
|
|
306
|
+
// caller's read scope, so an edge to/from an unreadable doc is invisible.
|
|
307
|
+
// For `has`/`links` a resolved edge joins the target's version; a *dangling*
|
|
308
|
+
// `has` edge has no target version to scope, but the source (the outer,
|
|
309
|
+
// already-scoped row) is what the caller reads, and the target is just a
|
|
310
|
+
// written path — so dangling edges count for `has_static` (matches
|
|
311
|
+
// "$has_static(\"horses.md\") — dangling target ok").
|
|
312
|
+
// -----------------------------------------------------------------------------
|
|
313
|
+
/**
|
|
314
|
+
* The full visibility predicate for a joined endpoint `alias`: read scope
|
|
315
|
+
* AND sigil-exclusion, so the visible graph equals the readable graph
|
|
316
|
+
* (§11.2). Errors if the compile context didn't supply the builders (a
|
|
317
|
+
* graph predicate must never leak an unfiltered endpoint). Sigil-exclusion
|
|
318
|
+
* has no fragment when the plan excludes nothing (`include_hidden/system`),
|
|
319
|
+
* in which case only scope contributes.
|
|
320
|
+
*/
|
|
321
|
+
function graphVisibility(alias) {
|
|
322
|
+
if (!CTX.graphScope) {
|
|
323
|
+
throw new KernelError("filter_invalid", {
|
|
324
|
+
reason: "graph predicates are not available in this query context",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const scope = CTX.graphScope(alias);
|
|
328
|
+
const sigils = CTX.graphSigils ? CTX.graphSigils(alias) : { sql: "", params: [] };
|
|
329
|
+
if (!sigils.sql)
|
|
330
|
+
return scope;
|
|
331
|
+
return { sql: `(${scope.sql} AND ${sigils.sql})`, params: [...scope.params, ...sigils.params] };
|
|
332
|
+
}
|
|
333
|
+
/** Anchored regexp over a path glob (gitignore-style, §8.2 machinery). */
|
|
334
|
+
function globRegex(glob) {
|
|
335
|
+
return `^${globToRegexSource(glob)}$`;
|
|
336
|
+
}
|
|
337
|
+
/** `AND l.field = ?` fragment (or '$body' sentinel) when a field is given. */
|
|
338
|
+
function fieldClause(field) {
|
|
339
|
+
if (field === undefined)
|
|
340
|
+
return { sql: "", params: [] };
|
|
341
|
+
const f = field === "$body" ? BODY_FIELD : field;
|
|
342
|
+
return { sql: " AND l.field = ?", params: [f] };
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The correlated EXISTS body shared by membership ($in/$has) and the
|
|
346
|
+
* collection `.size()`/comprehension forms. Returns the FROM+WHERE of a
|
|
347
|
+
* subquery correlated to the outer `versions` row, minus the leading
|
|
348
|
+
* SELECT (callers wrap it).
|
|
349
|
+
*/
|
|
350
|
+
function graphSubqueryCore(direction, opts) {
|
|
351
|
+
const params = [];
|
|
352
|
+
if (direction === "in" || direction === "backlinks") {
|
|
353
|
+
// Outer = target; join the source version. Resolved edges only
|
|
354
|
+
// (target_id must equal the outer doc), source must be live + readable.
|
|
355
|
+
const alias = opts.predicateAlias ?? nextGraphAlias();
|
|
356
|
+
const parts = [
|
|
357
|
+
"FROM links l",
|
|
358
|
+
`JOIN versions ${alias} ON l.source_id = ${alias}.document_id AND ${alias}.next_id IS NULL`,
|
|
359
|
+
"WHERE l.target_id = versions.document_id",
|
|
360
|
+
];
|
|
361
|
+
if (opts.glob !== undefined) {
|
|
362
|
+
parts.push(`AND regexp(?, ${alias}.path)`);
|
|
363
|
+
params.push(globRegex(opts.glob));
|
|
364
|
+
}
|
|
365
|
+
const fc = fieldClause(opts.field);
|
|
366
|
+
if (fc.sql) {
|
|
367
|
+
parts.push(fc.sql.replace(/^ AND /, "AND "));
|
|
368
|
+
params.push(...fc.params);
|
|
369
|
+
}
|
|
370
|
+
const vis = graphVisibility(alias);
|
|
371
|
+
parts.push(`AND ${vis.sql}`);
|
|
372
|
+
params.push(...vis.params);
|
|
373
|
+
return { sql: parts.join(" "), params };
|
|
374
|
+
}
|
|
375
|
+
// direction === "has" || "links": outer = source; the other end is target.
|
|
376
|
+
// Match the written folded target (target_norm) so dangling edges count.
|
|
377
|
+
// When the target resolves, also require it to be readable; a dangling
|
|
378
|
+
// target has no version row to scope (the target path isn't a live doc).
|
|
379
|
+
const alias = opts.predicateAlias ?? nextGraphAlias();
|
|
380
|
+
const parts = [
|
|
381
|
+
`FROM links l LEFT JOIN versions ${alias} ON l.target_id = ${alias}.document_id AND ${alias}.next_id IS NULL`,
|
|
382
|
+
"WHERE l.source_id = versions.document_id",
|
|
383
|
+
];
|
|
384
|
+
if (opts.glob !== undefined) {
|
|
385
|
+
// has/links match against the resolved/written target path. Prefer the
|
|
386
|
+
// resolved live path when bound; fall back to target_norm for danglers.
|
|
387
|
+
parts.push(`AND regexp(?, COALESCE(${alias}.path, l.target_norm))`);
|
|
388
|
+
params.push(globRegex(opts.glob));
|
|
389
|
+
}
|
|
390
|
+
const fc = fieldClause(opts.field);
|
|
391
|
+
if (fc.sql) {
|
|
392
|
+
parts.push(fc.sql.replace(/^ AND /, "AND "));
|
|
393
|
+
params.push(...fc.params);
|
|
394
|
+
}
|
|
395
|
+
// Visibility: a bound target must be readable AND not sigil-hidden (so an
|
|
396
|
+
// inbound edge to a deleted/hidden doc doesn't surface the source); a
|
|
397
|
+
// dangling edge (no target row) is visible because the source is the
|
|
398
|
+
// caller's own already-filtered row.
|
|
399
|
+
const vis = graphVisibility(alias);
|
|
400
|
+
parts.push(`AND (l.target_id IS NULL OR ${vis.sql})`);
|
|
401
|
+
params.push(...vis.params);
|
|
402
|
+
return { sql: parts.join(" "), params };
|
|
403
|
+
}
|
|
404
|
+
function compileGraphMembership(m) {
|
|
405
|
+
const core = graphSubqueryCore(m.direction, { glob: m.glob, field: m.field });
|
|
406
|
+
return { sql: `EXISTS (SELECT 1 ${core.sql})`, params: core.params };
|
|
407
|
+
}
|
|
408
|
+
/** COUNT(*) scalar for size($backlinks_static()) / size($links_static()). */
|
|
409
|
+
function compileGraphCollectionSize(c) {
|
|
410
|
+
const core = graphSubqueryCore(c.direction, {});
|
|
411
|
+
return { sql: `(SELECT COUNT(*) ${core.sql})`, params: core.params };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* $backlinks_static().exists(d, pred) / .all(d, pred). The iter var ranges
|
|
415
|
+
* over the OTHER document's live version (aliased); the predicate compiles
|
|
416
|
+
* against that alias. `.exists` → EXISTS(core AND pred); `.all` →
|
|
417
|
+
* NOT EXISTS(core AND NOT pred) — vacuously true on an empty collection,
|
|
418
|
+
* matching CEL's `.all` semantics.
|
|
419
|
+
*/
|
|
420
|
+
function compileGraphComprehension(comp, collection) {
|
|
421
|
+
const shape = classifyComprehension(comp);
|
|
422
|
+
if (shape === "unsupported") {
|
|
423
|
+
throw new KernelError("filter_invalid", {
|
|
424
|
+
reason: "unsupported comprehension over a link collection (use .exists or .all)",
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
const alias = nextGraphAlias();
|
|
428
|
+
const core = graphSubqueryCore(collection.direction, { predicateAlias: alias });
|
|
429
|
+
const userPred = extractUserPredicate(comp.loopStep);
|
|
430
|
+
const pred = compileAgainstAlias(userPred, comp.iterVar, alias);
|
|
431
|
+
if (shape === "exists") {
|
|
432
|
+
return {
|
|
433
|
+
sql: `EXISTS (SELECT 1 ${core.sql} AND (${pred.sql}))`,
|
|
434
|
+
params: [...core.params, ...pred.params],
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
// "all": no member violates the predicate (vacuously true when empty).
|
|
438
|
+
return {
|
|
439
|
+
sql: `NOT EXISTS (SELECT 1 ${core.sql} AND NOT (${pred.sql}))`,
|
|
440
|
+
params: [...core.params, ...pred.params],
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Compile a comprehension predicate where the iter var denotes the OTHER
|
|
445
|
+
* document's version row (aliased). `d` bare is meaningless; `d.field`
|
|
446
|
+
* reads that doc's frontmatter, and `d.$path`/`d.$updated_at`/`d.$body`
|
|
447
|
+
* read its intrinsics. Everything else compiles normally (referring to the
|
|
448
|
+
* OUTER `versions` row) — so a predicate can mix both documents.
|
|
449
|
+
*/
|
|
450
|
+
function compileAgainstAlias(expr, iterVar, alias) {
|
|
451
|
+
const kind = expr.exprKind.case;
|
|
452
|
+
if (kind === "identExpr") {
|
|
453
|
+
if (expr.exprKind.value.name === iterVar) {
|
|
454
|
+
// Bare iter var in a boolean/scalar position isn't supported — you
|
|
455
|
+
// must access a field (d.status) or intrinsic (d.$path).
|
|
456
|
+
throw new KernelError("filter_invalid", {
|
|
457
|
+
reason: `bare '${iterVar}' is not usable; access a field like ${iterVar}.status or ${iterVar}.$path`,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
return compileExpr(expr);
|
|
461
|
+
}
|
|
462
|
+
if (kind === "selectExpr") {
|
|
463
|
+
const path = collectSelectPath(expr);
|
|
464
|
+
if (path && path[0] === iterVar) {
|
|
465
|
+
return aliasFieldAccess(alias, path.slice(1));
|
|
466
|
+
}
|
|
467
|
+
return compileExpr(expr);
|
|
468
|
+
}
|
|
469
|
+
if (kind === "callExpr") {
|
|
470
|
+
const call = expr.exprKind.value;
|
|
471
|
+
const target = call.target
|
|
472
|
+
? compileAgainstAlias(call.target, iterVar, alias)
|
|
473
|
+
: undefined;
|
|
474
|
+
const args = call.args.map((a) => compileAgainstAlias(a, iterVar, alias));
|
|
475
|
+
return recompileCallWithSubstitutedArgs(call.function, target, args);
|
|
476
|
+
}
|
|
477
|
+
// Constants and anything else: compile normally.
|
|
478
|
+
return compileExpr(expr);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Field access on the aliased other-document row. An empty tail is the bare
|
|
482
|
+
* iter var (rejected above). A `$`-prefixed head is an intrinsic
|
|
483
|
+
* ($path/$updated_at/$body); otherwise it's a frontmatter path.
|
|
484
|
+
*/
|
|
485
|
+
function aliasFieldAccess(alias, tail) {
|
|
486
|
+
const head = tail[0];
|
|
487
|
+
if (head === undefined) {
|
|
488
|
+
throw new KernelError("filter_invalid", { reason: "empty field access on link iter var" });
|
|
489
|
+
}
|
|
490
|
+
if (head.startsWith(INTRINSIC_PREFIX)) {
|
|
491
|
+
if (tail.length > 1) {
|
|
492
|
+
throw new KernelError("filter_invalid", { reason: "intrinsics have no subfields" });
|
|
493
|
+
}
|
|
494
|
+
const name = head.slice(INTRINSIC_PREFIX.length);
|
|
495
|
+
const col = INTRINSIC_COLUMNS[name];
|
|
496
|
+
if (!col) {
|
|
497
|
+
throw new KernelError("filter_invalid", { reason: `unknown intrinsic $${name}` });
|
|
498
|
+
}
|
|
499
|
+
// INTRINSIC_COLUMNS maps to `versions.<col>`; retarget to the alias.
|
|
500
|
+
return { sql: col.replace(/^versions\./, `${alias}.`), params: [] };
|
|
501
|
+
}
|
|
502
|
+
const parts = tail.map((p) => `"${p.replace(/"/g, '""')}"`);
|
|
503
|
+
return { sql: `json_extract(${alias}.frontmatter, '$.${parts.join(".")}')`, params: [] };
|
|
504
|
+
}
|
|
505
|
+
function compileBinary(op, a, b) {
|
|
506
|
+
const lhs = compileExpr(a);
|
|
507
|
+
const rhs = compileExpr(b);
|
|
508
|
+
return {
|
|
509
|
+
sql: `(${lhs.sql} ${op} ${rhs.sql})`,
|
|
510
|
+
params: [...lhs.params, ...rhs.params],
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function compileNot(inner) {
|
|
514
|
+
const c = compileExpr(inner);
|
|
515
|
+
return { sql: `NOT (${c.sql})`, params: c.params };
|
|
516
|
+
}
|
|
517
|
+
// -----------------------------------------------------------------------------
|
|
518
|
+
// String methods
|
|
519
|
+
// -----------------------------------------------------------------------------
|
|
520
|
+
function compileContains(haystack, needle) {
|
|
521
|
+
const h = compileExpr(haystack);
|
|
522
|
+
const n = compileExpr(needle);
|
|
523
|
+
return {
|
|
524
|
+
sql: `(instr(${h.sql}, ${n.sql}) > 0)`,
|
|
525
|
+
params: [...h.params, ...n.params],
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
function compileStartsWith(target, prefix) {
|
|
529
|
+
// Literal prefix → use LIKE with the literal escaped. Dynamic prefix →
|
|
530
|
+
// use substr-based check (LIKE with runtime pattern is possible but
|
|
531
|
+
// needs LIKE-escaping which we'd rather avoid).
|
|
532
|
+
const constPrefix = unwrapConstant(prefix);
|
|
533
|
+
const t = compileExpr(target);
|
|
534
|
+
if (typeof constPrefix === "string") {
|
|
535
|
+
const escaped = escapeLikePattern(constPrefix);
|
|
536
|
+
return {
|
|
537
|
+
sql: `(${t.sql} LIKE ? ESCAPE '\\')`,
|
|
538
|
+
params: [...t.params, `${escaped}%`],
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const p = compileExpr(prefix);
|
|
542
|
+
// SQL contains p.sql twice: length(p) and = p. Duplicate p.params 2x.
|
|
543
|
+
return {
|
|
544
|
+
sql: `(substr(${t.sql}, 1, length(${p.sql})) = ${p.sql})`,
|
|
545
|
+
params: [...t.params, ...p.params, ...p.params],
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function compileEndsWith(target, suffix) {
|
|
549
|
+
const constSuffix = unwrapConstant(suffix);
|
|
550
|
+
const t = compileExpr(target);
|
|
551
|
+
if (typeof constSuffix === "string") {
|
|
552
|
+
const escaped = escapeLikePattern(constSuffix);
|
|
553
|
+
return {
|
|
554
|
+
sql: `(${t.sql} LIKE ? ESCAPE '\\')`,
|
|
555
|
+
params: [...t.params, `%${escaped}`],
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const s = compileExpr(suffix);
|
|
559
|
+
return {
|
|
560
|
+
sql: `(substr(${t.sql}, length(${t.sql}) - length(${s.sql}) + 1) = ${s.sql})`,
|
|
561
|
+
params: [...t.params, ...t.params, ...s.params, ...s.params],
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function compileMatches(target, pattern) {
|
|
565
|
+
// `regexp` is registered as a user function by the adapter (m2-plan §5).
|
|
566
|
+
const t = compileExpr(target);
|
|
567
|
+
const p = compileExpr(pattern);
|
|
568
|
+
return { sql: `(regexp(${p.sql}, ${t.sql}))`, params: [...p.params, ...t.params] };
|
|
569
|
+
}
|
|
570
|
+
function escapeLikePattern(literal) {
|
|
571
|
+
return literal.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
572
|
+
}
|
|
573
|
+
// -----------------------------------------------------------------------------
|
|
574
|
+
// Membership + size (list() polymorphism, §5.2)
|
|
575
|
+
// -----------------------------------------------------------------------------
|
|
576
|
+
function compileIn(needle, container) {
|
|
577
|
+
const listHint = unwrapListHint(container);
|
|
578
|
+
const n = compileExpr(needle);
|
|
579
|
+
if (listHint) {
|
|
580
|
+
// Polymorphic scalar-or-list frontmatter membership.
|
|
581
|
+
const field = compileExpr(listHint);
|
|
582
|
+
// needle params appear THREE times: once for scalar branch, twice
|
|
583
|
+
// for the list-branch inner comparison? No — just twice: scalar
|
|
584
|
+
// comparison + json_each subquery.
|
|
585
|
+
return {
|
|
586
|
+
sql: `(
|
|
587
|
+
${field.sql} = ${n.sql}
|
|
588
|
+
OR EXISTS (
|
|
589
|
+
SELECT 1 FROM json_each(${field.sql}) WHERE value = ${n.sql}
|
|
590
|
+
)
|
|
591
|
+
)`,
|
|
592
|
+
// field.sql appears twice; n.sql appears twice → duplicate both.
|
|
593
|
+
params: [...field.params, ...n.params, ...field.params, ...n.params],
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
// No list() hint → plain @in against a materialized list. Not supported
|
|
597
|
+
// in M2 (there's no scalar-list variable to iterate against yet).
|
|
598
|
+
throw new KernelError("filter_invalid", {
|
|
599
|
+
reason: '@in without list(field): use `"x" in list(field)` for polymorphic membership',
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
function compileSize(inner) {
|
|
603
|
+
// size($backlinks_static()) / size($links_static()) → COUNT subquery.
|
|
604
|
+
const collection = asGraphCollection(inner);
|
|
605
|
+
if (collection)
|
|
606
|
+
return compileGraphCollectionSize(collection);
|
|
607
|
+
// size(list(field)) — polymorphic: 1 if scalar, N if list, 0 if null.
|
|
608
|
+
// We use the two-arg form of json_type — json_type(root, path) — so it
|
|
609
|
+
// looks at the JSON representation BEFORE unwrapping (a bare scalar
|
|
610
|
+
// like "one" would otherwise fail json_type as invalid JSON).
|
|
611
|
+
const listHint = unwrapListHint(inner);
|
|
612
|
+
if (listHint) {
|
|
613
|
+
const jsonType = jsonTypeSql(listHint);
|
|
614
|
+
if (jsonType) {
|
|
615
|
+
const field = compileExpr(listHint);
|
|
616
|
+
return {
|
|
617
|
+
sql: `(CASE
|
|
618
|
+
WHEN ${jsonType.sql} IS NULL THEN 0
|
|
619
|
+
WHEN ${jsonType.sql} = 'array' THEN json_array_length(${field.sql})
|
|
620
|
+
ELSE 1
|
|
621
|
+
END)`,
|
|
622
|
+
params: [...jsonType.params, ...jsonType.params, ...field.params],
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
// size(string) → length(str). size(list_literal) not supported.
|
|
627
|
+
const c = compileExpr(inner);
|
|
628
|
+
return { sql: `length(${c.sql})`, params: c.params };
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Build a `json_type(root, path)` SQL fragment for a frontmatter access
|
|
632
|
+
* expression, so we can classify scalar vs list without json_extract
|
|
633
|
+
* unwrapping the value. Returns undefined for anything other than a
|
|
634
|
+
* top-level frontmatter identifier or a select over frontmatter.
|
|
635
|
+
*/
|
|
636
|
+
function jsonTypeSql(expr) {
|
|
637
|
+
if (expr.exprKind.case === "identExpr") {
|
|
638
|
+
const name = expr.exprKind.value.name;
|
|
639
|
+
if (name.startsWith(INTRINSIC_PREFIX))
|
|
640
|
+
return undefined;
|
|
641
|
+
const escaped = name.replace(/"/g, '""');
|
|
642
|
+
return {
|
|
643
|
+
sql: `json_type(versions.frontmatter, '$."${escaped}"')`,
|
|
644
|
+
params: [],
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
if (expr.exprKind.case === "selectExpr") {
|
|
648
|
+
const path = collectSelectPath(expr);
|
|
649
|
+
if (!path)
|
|
650
|
+
return undefined;
|
|
651
|
+
const [root, ...rest] = path;
|
|
652
|
+
if (!root || root.startsWith(INTRINSIC_PREFIX))
|
|
653
|
+
return undefined;
|
|
654
|
+
const parts = [root, ...rest].map((p) => `"${p.replace(/"/g, '""')}"`);
|
|
655
|
+
return {
|
|
656
|
+
sql: `json_type(versions.frontmatter, '$.${parts.join(".")}')`,
|
|
657
|
+
params: [],
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
return undefined;
|
|
661
|
+
}
|
|
662
|
+
// -----------------------------------------------------------------------------
|
|
663
|
+
// Comprehensions — `list(field).all(v, pred)` and `.exists(v, pred)`
|
|
664
|
+
// -----------------------------------------------------------------------------
|
|
665
|
+
function compileComprehension(expr) {
|
|
666
|
+
if (expr.exprKind.case !== "comprehensionExpr") {
|
|
667
|
+
throw new Error("compileComprehension: wrong kind");
|
|
668
|
+
}
|
|
669
|
+
const comp = expr.exprKind.value;
|
|
670
|
+
// CEL comprehensions are macro-expanded by the parser. The parser emits
|
|
671
|
+
// one of a few canonical shapes:
|
|
672
|
+
//
|
|
673
|
+
// .all(v, pred) → accuInit=true, loopStep = accu && pred,
|
|
674
|
+
// loopCondition = accu (short-circuit)
|
|
675
|
+
// .exists(v, pred) → accuInit=false, loopStep = accu || pred
|
|
676
|
+
// .filter(v, pred) → accumulator is a list (not supported here)
|
|
677
|
+
// .map(...) → likewise not supported
|
|
678
|
+
// Graph collection comprehension: $backlinks_static().exists(d, pred) /
|
|
679
|
+
// $links_static().all(d, pred). The iter var ranges over the OTHER
|
|
680
|
+
// document's version row (§11.2), so the predicate compiles against a
|
|
681
|
+
// joined `versions` alias rather than json_each rows.
|
|
682
|
+
const collection = asGraphCollection(comp.iterRange);
|
|
683
|
+
if (collection)
|
|
684
|
+
return compileGraphComprehension(comp, collection);
|
|
685
|
+
// We detect the two supported shapes structurally. iterRange must be a
|
|
686
|
+
// list() hint (§5.2).
|
|
687
|
+
const listHint = unwrapListHint(comp.iterRange);
|
|
688
|
+
if (!listHint) {
|
|
689
|
+
throw new KernelError("filter_invalid", {
|
|
690
|
+
reason: "comprehensions must iterate over list(field)",
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
const shape = classifyComprehension(comp);
|
|
694
|
+
if (shape === "unsupported") {
|
|
695
|
+
throw new KernelError("filter_invalid", {
|
|
696
|
+
reason: "unsupported comprehension shape (use .all or .exists)",
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
const field = compileExpr(listHint);
|
|
700
|
+
// The predicate uses `comp.iterVar` (the loop var) as an ident referring
|
|
701
|
+
// to the current element. We compile predicates against a "row.value"
|
|
702
|
+
// sourced from json_each; substitute the iter var with a placeholder
|
|
703
|
+
// that maps to `_row.value`.
|
|
704
|
+
const predicate = compilePredicateAgainstIter(comp.loopStep, comp.iterVar, shape);
|
|
705
|
+
// json_type(root, path) is used to classify scalar vs array BEFORE
|
|
706
|
+
// json_extract unwraps the value (which would otherwise leave us with
|
|
707
|
+
// a bare scalar that json_type(...) alone can't process).
|
|
708
|
+
const jt = jsonTypeSql(listHint);
|
|
709
|
+
const jtSql = jt?.sql ?? "NULL";
|
|
710
|
+
const jtParams = jt?.params ?? [];
|
|
711
|
+
if (shape === "exists") {
|
|
712
|
+
// exists over list(field): try each list element; on scalar frontmatter
|
|
713
|
+
// treat the scalar as a one-element list.
|
|
714
|
+
const scalarPred = compilePredicateAgainstIter(comp.loopStep, comp.iterVar, shape, {
|
|
715
|
+
source: "scalar",
|
|
716
|
+
fieldSql: field.sql,
|
|
717
|
+
fieldParams: field.params,
|
|
718
|
+
});
|
|
719
|
+
return {
|
|
720
|
+
sql: `(
|
|
721
|
+
(${jtSql} IS NOT NULL AND ${jtSql} != 'array' AND (${scalarPred.sql}))
|
|
722
|
+
OR (${jtSql} = 'array' AND EXISTS (SELECT 1 FROM json_each(${field.sql}) WHERE ${predicate.sql}))
|
|
723
|
+
)`,
|
|
724
|
+
params: [
|
|
725
|
+
...jtParams,
|
|
726
|
+
...jtParams,
|
|
727
|
+
...scalarPred.params,
|
|
728
|
+
...jtParams,
|
|
729
|
+
...field.params,
|
|
730
|
+
...predicate.params,
|
|
731
|
+
],
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
// "all"
|
|
735
|
+
const scalarPredAll = compilePredicateAgainstIter(comp.loopStep, comp.iterVar, shape, {
|
|
736
|
+
source: "scalar",
|
|
737
|
+
fieldSql: field.sql,
|
|
738
|
+
fieldParams: field.params,
|
|
739
|
+
});
|
|
740
|
+
return {
|
|
741
|
+
sql: `(
|
|
742
|
+
CASE
|
|
743
|
+
WHEN ${jtSql} IS NULL THEN 1
|
|
744
|
+
WHEN ${jtSql} != 'array' THEN (${scalarPredAll.sql})
|
|
745
|
+
ELSE NOT EXISTS (SELECT 1 FROM json_each(${field.sql}) WHERE NOT (${predicate.sql}))
|
|
746
|
+
END
|
|
747
|
+
)`,
|
|
748
|
+
params: [
|
|
749
|
+
...jtParams,
|
|
750
|
+
...jtParams,
|
|
751
|
+
...scalarPredAll.params,
|
|
752
|
+
...field.params,
|
|
753
|
+
...predicate.params,
|
|
754
|
+
],
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function classifyComprehension(comp) {
|
|
758
|
+
const init = unwrapConstant(comp.accuInit);
|
|
759
|
+
if (init === true)
|
|
760
|
+
return "all";
|
|
761
|
+
if (init === false)
|
|
762
|
+
return "exists";
|
|
763
|
+
return "unsupported";
|
|
764
|
+
}
|
|
765
|
+
/**
|
|
766
|
+
* Compile a comprehension predicate, substituting occurrences of the
|
|
767
|
+
* loop-variable identifier with either `_row.value` (list branch) or the
|
|
768
|
+
* field's own SQL (scalar branch). The predicate walker is a recursive
|
|
769
|
+
* compileExpr with a scoped override for `identExpr` matches on `iterVar`.
|
|
770
|
+
*/
|
|
771
|
+
function compilePredicateAgainstIter(predicateBase, iterVar, _shape, scalarCtx) {
|
|
772
|
+
// The loopStep for CEL's .all/.exists is a boolean combinator around the
|
|
773
|
+
// user's predicate. For .all: loopStep = _&&_(accu, pred). For .exists:
|
|
774
|
+
// loopStep = _||_(accu, pred). We extract the raw predicate.
|
|
775
|
+
const userPred = extractUserPredicate(predicateBase);
|
|
776
|
+
return compileWithIterSubstitution(userPred, iterVar, scalarCtx);
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* loopStep in CEL's canonical .all/.exists macros is `__result__ && pred` or
|
|
780
|
+
* `__result__ || pred`. Peel the accumulator off — we compile only the
|
|
781
|
+
* user's predicate; the accumulator semantics are handled by our
|
|
782
|
+
* EXISTS / NOT-EXISTS wrapping.
|
|
783
|
+
*/
|
|
784
|
+
function extractUserPredicate(loopStep) {
|
|
785
|
+
if (loopStep.exprKind.case === "callExpr") {
|
|
786
|
+
const call = loopStep.exprKind.value;
|
|
787
|
+
if ((call.function === "_&&_" || call.function === "_||_") && call.args.length === 2) {
|
|
788
|
+
const [first, second] = call.args;
|
|
789
|
+
// Heuristic: the accumulator is an identExpr against @result / __result__.
|
|
790
|
+
if (first?.exprKind.case === "identExpr" &&
|
|
791
|
+
(first.exprKind.value.name === "@result" || first.exprKind.value.name === "__result__")) {
|
|
792
|
+
return second;
|
|
793
|
+
}
|
|
794
|
+
if (second?.exprKind.case === "identExpr" &&
|
|
795
|
+
(second.exprKind.value.name === "@result" || second.exprKind.value.name === "__result__")) {
|
|
796
|
+
return first;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return loopStep;
|
|
801
|
+
}
|
|
802
|
+
function compileWithIterSubstitution(expr, iterVar, scalarCtx) {
|
|
803
|
+
const kind = expr.exprKind.case;
|
|
804
|
+
if (kind === "identExpr" && expr.exprKind.value.name === iterVar) {
|
|
805
|
+
if (scalarCtx) {
|
|
806
|
+
return { sql: scalarCtx.fieldSql, params: [...scalarCtx.fieldParams] };
|
|
807
|
+
}
|
|
808
|
+
// Inside a FROM json_each(...) subquery, the current element is
|
|
809
|
+
// exposed as the unqualified column `value`.
|
|
810
|
+
return { sql: "value", params: [] };
|
|
811
|
+
}
|
|
812
|
+
if (kind === "callExpr") {
|
|
813
|
+
const call = expr.exprKind.value;
|
|
814
|
+
const target = call.target;
|
|
815
|
+
const args = call.args.map((a) => compileWithIterSubstitution(a, iterVar, scalarCtx));
|
|
816
|
+
const targetCompiled = target
|
|
817
|
+
? compileWithIterSubstitution(target, iterVar, scalarCtx)
|
|
818
|
+
: undefined;
|
|
819
|
+
return recompileCallWithSubstitutedArgs(call.function, targetCompiled, args);
|
|
820
|
+
}
|
|
821
|
+
if (kind === "selectExpr") {
|
|
822
|
+
// Guard against silent-wrong-results: if the select's root IS the iter
|
|
823
|
+
// var (e.g. `list(authors).all(a, a.name == "alice")`), the fall-
|
|
824
|
+
// through to compileExpr would compile `a.name` as if it were a
|
|
825
|
+
// frontmatter path — semantically wrong and impossible to notice.
|
|
826
|
+
// Reject explicitly until we grow proper member-access support.
|
|
827
|
+
const root = collectSelectPath(expr)?.[0];
|
|
828
|
+
if (root === iterVar) {
|
|
829
|
+
throw new KernelError("filter_invalid", {
|
|
830
|
+
reason: `member access on comprehension iter-var '${iterVar}' is not yet supported`,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return compileExpr(expr);
|
|
834
|
+
}
|
|
835
|
+
return compileExpr(expr);
|
|
836
|
+
}
|
|
837
|
+
function recompileCallWithSubstitutedArgs(fn, target, args) {
|
|
838
|
+
// Small dispatch for the operators/functions we actually see inside a
|
|
839
|
+
// comprehension predicate: comparison ops + string methods.
|
|
840
|
+
const binOp = BIN_OPS[fn];
|
|
841
|
+
if (binOp && args.length === 2 && !target) {
|
|
842
|
+
const [a, b] = args;
|
|
843
|
+
return { sql: `(${a.sql} ${binOp} ${b.sql})`, params: [...a.params, ...b.params] };
|
|
844
|
+
}
|
|
845
|
+
if (fn === "!_" && args.length === 1 && !target) {
|
|
846
|
+
const [a] = args;
|
|
847
|
+
return { sql: `NOT (${a.sql})`, params: a.params };
|
|
848
|
+
}
|
|
849
|
+
if (fn === "startsWith") {
|
|
850
|
+
const t = target ?? args[0];
|
|
851
|
+
const p = target ? args[0] : args[1];
|
|
852
|
+
if (!t || !p)
|
|
853
|
+
throw new KernelError("filter_invalid", { reason: "startsWith needs 2 args" });
|
|
854
|
+
// SQL contains p.sql twice (length(p), = p). Duplicate p.params 2x.
|
|
855
|
+
return {
|
|
856
|
+
sql: `(substr(${t.sql}, 1, length(${p.sql})) = ${p.sql})`,
|
|
857
|
+
params: [...t.params, ...p.params, ...p.params],
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
if (fn === "endsWith") {
|
|
861
|
+
const t = target ?? args[0];
|
|
862
|
+
const p = target ? args[0] : args[1];
|
|
863
|
+
if (!t || !p)
|
|
864
|
+
throw new KernelError("filter_invalid", { reason: "endsWith needs 2 args" });
|
|
865
|
+
return {
|
|
866
|
+
sql: `(substr(${t.sql}, length(${t.sql}) - length(${p.sql}) + 1) = ${p.sql})`,
|
|
867
|
+
params: [...t.params, ...t.params, ...p.params, ...p.params],
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
if (fn === "contains") {
|
|
871
|
+
const t = target ?? args[0];
|
|
872
|
+
const n = target ? args[0] : args[1];
|
|
873
|
+
if (!t || !n)
|
|
874
|
+
throw new KernelError("filter_invalid", { reason: "contains needs 2 args" });
|
|
875
|
+
return { sql: `(instr(${t.sql}, ${n.sql}) > 0)`, params: [...t.params, ...n.params] };
|
|
876
|
+
}
|
|
877
|
+
throw new KernelError("filter_invalid", {
|
|
878
|
+
reason: `unsupported call inside comprehension predicate: ${fn}`,
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
//# sourceMappingURL=compile-filter.js.map
|