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,830 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SearchPlan → Postgres SQL compiler (m5-plan WS5).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the SQLite compiler (src/storage-sqlite/compile-sqlite.ts)
|
|
5
|
+
* with dialect swaps:
|
|
6
|
+
* - `$n` placeholders instead of `?`; params are appended in the
|
|
7
|
+
* order they land in the SQL text.
|
|
8
|
+
* - jsonb access: `frontmatter->'k' = '"v"'::jsonb OR frontmatter->'k'
|
|
9
|
+
* @> '["v"]'::jsonb` covers scalar+list under one GIN.
|
|
10
|
+
* - `->>` + casts for typed compare (mirrors json_extract semantics).
|
|
11
|
+
* - `~` for CEL matches() and scope-glob regexes; POSIX ARE.
|
|
12
|
+
* - `position()` / `LIKE ESCAPE` for contains/startsWith/endsWith.
|
|
13
|
+
* - Native booleans (never `1`/`0` predicates).
|
|
14
|
+
* - FTS: `websearch_to_tsquery` (never throws on user input) +
|
|
15
|
+
* `ts_rank`; ordering by rank when text is present.
|
|
16
|
+
*/
|
|
17
|
+
import { globToRegexSource } from "../kernel/auth/glob.js";
|
|
18
|
+
import { KernelError } from "../kernel/errors.js";
|
|
19
|
+
import { unwrapConstant, unwrapListHint } from "../kernel/query/ast.js";
|
|
20
|
+
import { INTRINSIC_PREFIX } from "../kernel/query/cel-parse.js";
|
|
21
|
+
import { asGraphCollection, asGraphMembership, assertNotReservedGraphName, } from "../kernel/query/graph-ast.js";
|
|
22
|
+
import { BODY_FIELD } from "../links/extract.js";
|
|
23
|
+
// A running builder that keeps params + `$n` counter aligned as we
|
|
24
|
+
// weave nested fragments together. Also carries the plan's scope + sigils
|
|
25
|
+
// so graph subqueries can apply the caller's full visibility filter to the
|
|
26
|
+
// OTHER endpoint (§11.2), plus an alias sequence so nested graph subqueries
|
|
27
|
+
// don't collide.
|
|
28
|
+
class Builder {
|
|
29
|
+
params = [];
|
|
30
|
+
scope = { kind: "allow_all" };
|
|
31
|
+
sigils = [];
|
|
32
|
+
aliasSeq = 0;
|
|
33
|
+
push(v) {
|
|
34
|
+
this.params.push(v);
|
|
35
|
+
return `$${this.params.length}`;
|
|
36
|
+
}
|
|
37
|
+
nextAlias() {
|
|
38
|
+
return `lv${this.aliasSeq++}`;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// -----------------------------------------------------------------------------
|
|
42
|
+
// Public entry
|
|
43
|
+
// -----------------------------------------------------------------------------
|
|
44
|
+
export function compileSearchPlan(plan) {
|
|
45
|
+
const b = new Builder();
|
|
46
|
+
// Graph subqueries apply the caller's full visibility filter (scope +
|
|
47
|
+
// sigil-exclusion) to the other endpoint (§11.2).
|
|
48
|
+
b.scope = plan.scope;
|
|
49
|
+
b.sigils = plan.sigils;
|
|
50
|
+
const clauses = ["versions.next_id IS NULL"];
|
|
51
|
+
// repo_id filter.
|
|
52
|
+
clauses.push(`versions.repo_id = ANY(${b.push(plan.repo_ids)}::bigint[])`);
|
|
53
|
+
if (plan.filter_ast) {
|
|
54
|
+
clauses.push(`(${compileExpr(plan.filter_ast, b)})`);
|
|
55
|
+
}
|
|
56
|
+
const sigilSql = compileSigilExclusion(plan.sigils, b, "versions");
|
|
57
|
+
if (sigilSql.length > 0)
|
|
58
|
+
clauses.push(sigilSql);
|
|
59
|
+
if (plan.scope.kind === "groups") {
|
|
60
|
+
const scopeSql = compileScopeGroups(plan.scope.groups, b, "versions");
|
|
61
|
+
if (scopeSql.length > 0)
|
|
62
|
+
clauses.push(`(${scopeSql})`);
|
|
63
|
+
else
|
|
64
|
+
clauses.push("false");
|
|
65
|
+
}
|
|
66
|
+
// allow_all: no clause.
|
|
67
|
+
if (plan.candidate_ids && plan.candidate_ids.length > 0) {
|
|
68
|
+
clauses.push(`versions.id = ANY(${b.push(plan.candidate_ids)}::bigint[])`);
|
|
69
|
+
}
|
|
70
|
+
// Candidate DOCUMENT-id whitelist (graph read surface). Restricts to live
|
|
71
|
+
// versions of these documents so a BFS round's visibility check is one pass.
|
|
72
|
+
if (plan.candidate_document_ids && plan.candidate_document_ids.length > 0) {
|
|
73
|
+
clauses.push(`versions.document_id = ANY(${b.push(plan.candidate_document_ids)}::bigint[])`);
|
|
74
|
+
}
|
|
75
|
+
const cols = `versions.id, versions.document_id, versions.repo_id,
|
|
76
|
+
versions.prev_id, versions.next_id, versions.path,
|
|
77
|
+
versions.frontmatter_raw, versions.frontmatter,
|
|
78
|
+
versions.body, versions.author, versions.created_at,
|
|
79
|
+
versions.content_hash`;
|
|
80
|
+
let sql;
|
|
81
|
+
if (plan.text !== undefined) {
|
|
82
|
+
// websearch_to_tsquery never throws on user input. Order by ts_rank
|
|
83
|
+
// then id for stability.
|
|
84
|
+
const textPh = b.push(plan.text);
|
|
85
|
+
clauses.push(`versions.fts_tsv @@ websearch_to_tsquery('english', ${textPh})`);
|
|
86
|
+
sql = `SELECT ${cols}
|
|
87
|
+
FROM versions
|
|
88
|
+
WHERE ${clauses.join(" AND ")}
|
|
89
|
+
ORDER BY ts_rank(versions.fts_tsv, websearch_to_tsquery('english', ${textPh})) DESC,
|
|
90
|
+
versions.id DESC
|
|
91
|
+
LIMIT ${b.push(plan.limit)}`;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
sql = `SELECT ${cols}
|
|
95
|
+
FROM versions
|
|
96
|
+
WHERE ${clauses.join(" AND ")}
|
|
97
|
+
ORDER BY versions.created_at DESC, versions.id DESC
|
|
98
|
+
LIMIT ${b.push(plan.limit)}`;
|
|
99
|
+
}
|
|
100
|
+
return { sql, params: b.params };
|
|
101
|
+
}
|
|
102
|
+
// -----------------------------------------------------------------------------
|
|
103
|
+
// Sigil exclusion + scope groups
|
|
104
|
+
// -----------------------------------------------------------------------------
|
|
105
|
+
function compileSigilExclusion(groups, b, alias) {
|
|
106
|
+
const parts = [];
|
|
107
|
+
for (const g of groups) {
|
|
108
|
+
if (g.sigils.length === 0)
|
|
109
|
+
continue;
|
|
110
|
+
if (g.repo_ids.length === 0)
|
|
111
|
+
continue;
|
|
112
|
+
const repoPh = b.push(g.repo_ids);
|
|
113
|
+
const notInAny = `${alias}.repo_id <> ALL(${repoPh}::bigint[])`;
|
|
114
|
+
const sigilChecks = [];
|
|
115
|
+
for (const sigil of g.sigils) {
|
|
116
|
+
const escaped = sigil.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
117
|
+
sigilChecks.push(`${alias}.path NOT LIKE ${b.push(`${escaped}%`)}`);
|
|
118
|
+
sigilChecks.push(`${alias}.path NOT LIKE ${b.push(`%/${escaped}%`)}`);
|
|
119
|
+
}
|
|
120
|
+
parts.push(`(${notInAny} OR (${sigilChecks.join(" AND ")}))`);
|
|
121
|
+
}
|
|
122
|
+
return parts.join(" AND ");
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Full visibility predicate for a joined endpoint `alias` — read scope AND
|
|
126
|
+
* sigil-exclusion (§11.2 "visible graph = readable graph"). Returns "TRUE"
|
|
127
|
+
* when nothing constrains it.
|
|
128
|
+
*/
|
|
129
|
+
function visibilityForAlias(b, alias) {
|
|
130
|
+
const scope = scopeFragmentForAlias(b, alias);
|
|
131
|
+
const sigils = compileSigilExclusion(b.sigils, b, alias);
|
|
132
|
+
if (!sigils)
|
|
133
|
+
return scope;
|
|
134
|
+
return `(${scope} AND ${sigils})`;
|
|
135
|
+
}
|
|
136
|
+
function compileScopeGroups(groups, b, alias) {
|
|
137
|
+
const parts = [];
|
|
138
|
+
for (const g of groups) {
|
|
139
|
+
if (g.globs.length === 0)
|
|
140
|
+
continue;
|
|
141
|
+
if (g.repos === "*") {
|
|
142
|
+
const globExpr = compileScopeGlobs(g.globs, b, alias);
|
|
143
|
+
parts.push(`(${globExpr})`);
|
|
144
|
+
}
|
|
145
|
+
else if (g.repos.length > 0) {
|
|
146
|
+
// Push the repo-array param BEFORE the globs so `$n` numbering matches
|
|
147
|
+
// the emitted text order (`repo_id = ANY($k) AND (<globs>)`). Getting
|
|
148
|
+
// this backwards silently swaps params — the bug the graph-scope
|
|
149
|
+
// parity test caught. (The `repos === "*"` branch has no repo param,
|
|
150
|
+
// so it was never exposed there.)
|
|
151
|
+
const ph = b.push(g.repos);
|
|
152
|
+
const globExpr = compileScopeGlobs(g.globs, b, alias);
|
|
153
|
+
parts.push(`(${alias}.repo_id = ANY(${ph}::bigint[]) AND (${globExpr}))`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return parts.join(" OR ");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Scope predicate for an aliased `versions` row inside a graph subquery
|
|
160
|
+
* (§11.2 — the visible graph equals the readable graph). Mirrors the
|
|
161
|
+
* SQLite compiler's scopeFragmentForAlias.
|
|
162
|
+
*/
|
|
163
|
+
function scopeFragmentForAlias(b, alias) {
|
|
164
|
+
if (b.scope.kind === "allow_all")
|
|
165
|
+
return "TRUE";
|
|
166
|
+
if (b.scope.kind === "deny_all")
|
|
167
|
+
return "FALSE";
|
|
168
|
+
const sql = compileScopeGroups(b.scope.groups, b, alias);
|
|
169
|
+
return sql.length > 0 ? `(${sql})` : "FALSE";
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Gitignore-style last-match-wins list, encoded as a CASE nesting where
|
|
173
|
+
* later globs wrap earlier ones (so the outermost WHEN — the last glob
|
|
174
|
+
* — wins if it matches).
|
|
175
|
+
*/
|
|
176
|
+
function compileScopeGlobs(globs, b, alias) {
|
|
177
|
+
// We need `$n`s to appear in outermost-first order in the emitted SQL.
|
|
178
|
+
// The compileWithReverseOrder pattern: build the fragment text first
|
|
179
|
+
// with named placeholders, then emit params in the correct order.
|
|
180
|
+
//
|
|
181
|
+
// Simpler: build a stack of {regex, verdict}, walk it outward and
|
|
182
|
+
// push params in that order as we build the string.
|
|
183
|
+
// Last-match-wins: the LAST glob is the OUTERMOST CASE (if it matches,
|
|
184
|
+
// its verdict wins; otherwise fall through to earlier globs). Build the
|
|
185
|
+
// nesting outermost-first via prefix/suffix so `$n` params land in text
|
|
186
|
+
// order (last glob's param first) — matching the SQLite compiler's shape.
|
|
187
|
+
let prefix = "";
|
|
188
|
+
let suffix = "";
|
|
189
|
+
for (let i = globs.length - 1; i >= 0; i--) {
|
|
190
|
+
const g = globs[i];
|
|
191
|
+
const negated = g.startsWith("!");
|
|
192
|
+
const raw = negated ? g.slice(1) : g;
|
|
193
|
+
const ph = b.push(`^${globToRegexSource(raw)}$`);
|
|
194
|
+
prefix += `(CASE WHEN ${alias}.path ~ ${ph} THEN ${negated ? "FALSE" : "TRUE"} ELSE `;
|
|
195
|
+
suffix = ` END)${suffix}`;
|
|
196
|
+
}
|
|
197
|
+
return `${prefix}FALSE${suffix}`;
|
|
198
|
+
}
|
|
199
|
+
// -----------------------------------------------------------------------------
|
|
200
|
+
// CEL AST → Postgres SQL
|
|
201
|
+
// -----------------------------------------------------------------------------
|
|
202
|
+
const INTRINSIC_COLUMNS = {
|
|
203
|
+
path: "versions.path",
|
|
204
|
+
// See compile-filter.ts for the $updated_at naming rationale.
|
|
205
|
+
updated_at: "versions.created_at",
|
|
206
|
+
body: "versions.body",
|
|
207
|
+
// Canonical content fingerprint (sync/history plan §2.5).
|
|
208
|
+
content_hash: "versions.content_hash",
|
|
209
|
+
};
|
|
210
|
+
function compileIntrinsic(mangledName) {
|
|
211
|
+
const name = mangledName.slice(INTRINSIC_PREFIX.length);
|
|
212
|
+
const column = INTRINSIC_COLUMNS[name];
|
|
213
|
+
if (!column) {
|
|
214
|
+
const expected = Object.keys(INTRINSIC_COLUMNS)
|
|
215
|
+
.map((k) => `$${k}`)
|
|
216
|
+
.join(", ");
|
|
217
|
+
throw new KernelError("filter_invalid", {
|
|
218
|
+
reason: `unknown intrinsic $${name} (expected ${expected})`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return column;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* jsonb path access. Returns the SQL for `frontmatter -> 'k' -> ...`.
|
|
225
|
+
* The `->>` variant (text) is used inside typed comparisons.
|
|
226
|
+
*/
|
|
227
|
+
function frontmatterPath(parts) {
|
|
228
|
+
// Parts come from the CEL parser (identExpr.value.name /
|
|
229
|
+
// selectExpr.value.field) — the CEL grammar forbids newlines and
|
|
230
|
+
// other control chars in identifiers, so single-quote escaping is
|
|
231
|
+
// the only string-safety concern here.
|
|
232
|
+
if (parts.length === 0)
|
|
233
|
+
throw new Error("frontmatterPath: empty parts");
|
|
234
|
+
let expr = `versions.frontmatter -> '${parts[0].replace(/'/g, "''")}'`;
|
|
235
|
+
for (let i = 1; i < parts.length; i++) {
|
|
236
|
+
const p = parts[i];
|
|
237
|
+
expr = `${expr} -> '${p.replace(/'/g, "''")}'`;
|
|
238
|
+
}
|
|
239
|
+
return expr;
|
|
240
|
+
}
|
|
241
|
+
function frontmatterText(parts) {
|
|
242
|
+
// `->>` returns text. Convert the last hop to `->>` for that.
|
|
243
|
+
if (parts.length === 0)
|
|
244
|
+
throw new Error("frontmatterText: empty parts");
|
|
245
|
+
if (parts.length === 1) {
|
|
246
|
+
const p = parts[0];
|
|
247
|
+
return `versions.frontmatter ->> '${p.replace(/'/g, "''")}'`;
|
|
248
|
+
}
|
|
249
|
+
const prefix = frontmatterPath(parts.slice(0, -1));
|
|
250
|
+
const last = parts[parts.length - 1];
|
|
251
|
+
return `${prefix} ->> '${last.replace(/'/g, "''")}'`;
|
|
252
|
+
}
|
|
253
|
+
function collectSelectPath(expr) {
|
|
254
|
+
if (expr.exprKind.case === "identExpr")
|
|
255
|
+
return [expr.exprKind.value.name];
|
|
256
|
+
if (expr.exprKind.case !== "selectExpr")
|
|
257
|
+
return undefined;
|
|
258
|
+
const inner = expr.exprKind.value.operand;
|
|
259
|
+
if (!inner)
|
|
260
|
+
return undefined;
|
|
261
|
+
const innerPath = collectSelectPath(inner);
|
|
262
|
+
if (!innerPath)
|
|
263
|
+
return undefined;
|
|
264
|
+
return [...innerPath, expr.exprKind.value.field];
|
|
265
|
+
}
|
|
266
|
+
function compileExpr(expr, b) {
|
|
267
|
+
const kind = expr.exprKind.case;
|
|
268
|
+
switch (kind) {
|
|
269
|
+
case "constExpr":
|
|
270
|
+
return compileConst(expr, b);
|
|
271
|
+
case "identExpr":
|
|
272
|
+
return compileIdent(expr);
|
|
273
|
+
case "selectExpr":
|
|
274
|
+
return compileSelect(expr);
|
|
275
|
+
case "callExpr":
|
|
276
|
+
return compileCall(expr, b);
|
|
277
|
+
case "comprehensionExpr":
|
|
278
|
+
return compileComprehension(expr, b);
|
|
279
|
+
case "listExpr":
|
|
280
|
+
case "structExpr":
|
|
281
|
+
throw new KernelError("filter_invalid", {
|
|
282
|
+
reason: `${kind ?? "unknown"} expressions are not supported in filters`,
|
|
283
|
+
});
|
|
284
|
+
default:
|
|
285
|
+
throw new KernelError("filter_invalid", {
|
|
286
|
+
reason: `unsupported expression kind: ${String(kind)}`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function compileConst(expr, b) {
|
|
291
|
+
const value = unwrapConstant(expr);
|
|
292
|
+
if (value === undefined) {
|
|
293
|
+
throw new KernelError("filter_invalid", { reason: "unsupported constant kind" });
|
|
294
|
+
}
|
|
295
|
+
if (value === null)
|
|
296
|
+
return "NULL";
|
|
297
|
+
if (typeof value === "boolean")
|
|
298
|
+
return value ? "TRUE" : "FALSE";
|
|
299
|
+
return b.push(value);
|
|
300
|
+
}
|
|
301
|
+
function compileIdent(expr) {
|
|
302
|
+
if (expr.exprKind.case !== "identExpr")
|
|
303
|
+
throw new Error("compileIdent: wrong kind");
|
|
304
|
+
const name = expr.exprKind.value.name;
|
|
305
|
+
if (name.startsWith(INTRINSIC_PREFIX))
|
|
306
|
+
return compileIntrinsic(name);
|
|
307
|
+
// Bare frontmatter key → text access. Comparisons wrap with casts.
|
|
308
|
+
return frontmatterText([name]);
|
|
309
|
+
}
|
|
310
|
+
function compileSelect(expr) {
|
|
311
|
+
if (expr.exprKind.case !== "selectExpr")
|
|
312
|
+
throw new Error("compileSelect: wrong kind");
|
|
313
|
+
const sel = expr.exprKind.value;
|
|
314
|
+
const path = collectSelectPath(expr);
|
|
315
|
+
if (!path) {
|
|
316
|
+
throw new KernelError("filter_invalid", {
|
|
317
|
+
reason: `unsupported select expression: field '${sel.field}'`,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const [root, ...rest] = path;
|
|
321
|
+
const rootExpr = root;
|
|
322
|
+
if (rootExpr.startsWith(INTRINSIC_PREFIX)) {
|
|
323
|
+
if (rest.length > 0) {
|
|
324
|
+
throw new KernelError("filter_invalid", {
|
|
325
|
+
reason: `intrinsics do not have subfields ($${rootExpr.slice(INTRINSIC_PREFIX.length)})`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return compileIntrinsic(rootExpr);
|
|
329
|
+
}
|
|
330
|
+
return frontmatterText([rootExpr, ...rest]);
|
|
331
|
+
}
|
|
332
|
+
const BIN_OPS = {
|
|
333
|
+
"_==_": "=",
|
|
334
|
+
"_!=_": "<>",
|
|
335
|
+
"_<_": "<",
|
|
336
|
+
"_<=_": "<=",
|
|
337
|
+
"_>_": ">",
|
|
338
|
+
"_>=_": ">=",
|
|
339
|
+
"_&&_": "AND",
|
|
340
|
+
"_||_": "OR",
|
|
341
|
+
};
|
|
342
|
+
function compileCall(expr, b) {
|
|
343
|
+
if (expr.exprKind.case !== "callExpr")
|
|
344
|
+
throw new Error("compileCall: wrong kind");
|
|
345
|
+
const call = expr.exprKind.value;
|
|
346
|
+
const fn = call.function;
|
|
347
|
+
const args = call.args;
|
|
348
|
+
const target = call.target;
|
|
349
|
+
const binOp = BIN_OPS[fn];
|
|
350
|
+
if (binOp && args.length === 2) {
|
|
351
|
+
return compileBinary(binOp, args[0], args[1], b);
|
|
352
|
+
}
|
|
353
|
+
if (fn === "!_" && args.length === 1) {
|
|
354
|
+
return `NOT (${compileExpr(args[0], b)})`;
|
|
355
|
+
}
|
|
356
|
+
if (fn === "-_" && args.length === 1) {
|
|
357
|
+
return `-(${compileExpr(args[0], b)})`;
|
|
358
|
+
}
|
|
359
|
+
if (fn === "@in" && args.length === 2) {
|
|
360
|
+
return compileIn(args[0], args[1], b);
|
|
361
|
+
}
|
|
362
|
+
if (target === undefined) {
|
|
363
|
+
if (fn === "size" && args.length === 1)
|
|
364
|
+
return compileSize(args[0], b);
|
|
365
|
+
if (fn === "contains" && args.length === 2)
|
|
366
|
+
return compileContains(args[0], args[1], b);
|
|
367
|
+
if (fn === "startsWith" && args.length === 2)
|
|
368
|
+
return compileStartsWith(args[0], args[1], b);
|
|
369
|
+
if (fn === "endsWith" && args.length === 2)
|
|
370
|
+
return compileEndsWith(args[0], args[1], b);
|
|
371
|
+
if (fn === "matches" && args.length === 2)
|
|
372
|
+
return compileMatches(args[0], args[1], b);
|
|
373
|
+
if (fn === "list" && args.length === 1) {
|
|
374
|
+
throw new KernelError("filter_invalid", {
|
|
375
|
+
reason: "list() is a hint that must sit inside `in` or a comprehension (.all/.exists), not stand alone",
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (target) {
|
|
380
|
+
// `$backlinks_static().size()` — method-form size on a collection.
|
|
381
|
+
if (fn === "size" && args.length === 0) {
|
|
382
|
+
const collection = asGraphCollection(target);
|
|
383
|
+
if (collection)
|
|
384
|
+
return compileGraphCollectionSize(collection, b);
|
|
385
|
+
}
|
|
386
|
+
if (fn === "startsWith" && args.length === 1)
|
|
387
|
+
return compileStartsWith(target, args[0], b);
|
|
388
|
+
if (fn === "endsWith" && args.length === 1)
|
|
389
|
+
return compileEndsWith(target, args[0], b);
|
|
390
|
+
if (fn === "contains" && args.length === 1)
|
|
391
|
+
return compileContains(target, args[0], b);
|
|
392
|
+
if (fn === "matches" && args.length === 1)
|
|
393
|
+
return compileMatches(target, args[0], b);
|
|
394
|
+
}
|
|
395
|
+
// Graph membership predicates: $in_static / $has_static (§11.2).
|
|
396
|
+
const membership = asGraphMembership(expr);
|
|
397
|
+
if (membership)
|
|
398
|
+
return compileGraphMembership(membership, b);
|
|
399
|
+
const collection = asGraphCollection(expr);
|
|
400
|
+
if (collection) {
|
|
401
|
+
throw new KernelError("filter_invalid", {
|
|
402
|
+
reason: `$${collection.direction}() is a collection; use .size(), .exists(), or .all()`,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
assertNotReservedGraphName(fn);
|
|
406
|
+
throw new KernelError("filter_invalid", {
|
|
407
|
+
reason: `unsupported function: ${target ? "<target>." : ""}${fn}(...) with ${args.length} arg(s)`,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
// -----------------------------------------------------------------------------
|
|
411
|
+
// Graph predicates (design §11.2, WS4) — Postgres mirror of the SQLite
|
|
412
|
+
// emitters in compile-filter.ts. See there for the directional model.
|
|
413
|
+
// -----------------------------------------------------------------------------
|
|
414
|
+
function globRegex(glob) {
|
|
415
|
+
return `^${globToRegexSource(glob)}$`;
|
|
416
|
+
}
|
|
417
|
+
function graphSubqueryCore(direction, b, opts) {
|
|
418
|
+
const alias = opts.alias ?? b.nextAlias();
|
|
419
|
+
if (direction === "in" || direction === "backlinks") {
|
|
420
|
+
const parts = [
|
|
421
|
+
"FROM links l",
|
|
422
|
+
`JOIN versions ${alias} ON l.source_id = ${alias}.document_id AND ${alias}.next_id IS NULL`,
|
|
423
|
+
"WHERE l.target_id = versions.document_id",
|
|
424
|
+
];
|
|
425
|
+
if (opts.glob !== undefined)
|
|
426
|
+
parts.push(`AND ${alias}.path ~ ${b.push(globRegex(opts.glob))}`);
|
|
427
|
+
if (opts.field !== undefined) {
|
|
428
|
+
parts.push(`AND l.field = ${b.push(opts.field === "$body" ? BODY_FIELD : opts.field)}`);
|
|
429
|
+
}
|
|
430
|
+
parts.push(`AND ${visibilityForAlias(b, alias)}`);
|
|
431
|
+
return { from: parts.join(" "), alias };
|
|
432
|
+
}
|
|
433
|
+
// has / links: outer = source; other end is the (maybe-dangling) target.
|
|
434
|
+
const parts = [
|
|
435
|
+
`FROM links l LEFT JOIN versions ${alias} ON l.target_id = ${alias}.document_id AND ${alias}.next_id IS NULL`,
|
|
436
|
+
"WHERE l.source_id = versions.document_id",
|
|
437
|
+
];
|
|
438
|
+
if (opts.glob !== undefined) {
|
|
439
|
+
parts.push(`AND COALESCE(${alias}.path, l.target_norm) ~ ${b.push(globRegex(opts.glob))}`);
|
|
440
|
+
}
|
|
441
|
+
if (opts.field !== undefined) {
|
|
442
|
+
parts.push(`AND l.field = ${b.push(opts.field === "$body" ? BODY_FIELD : opts.field)}`);
|
|
443
|
+
}
|
|
444
|
+
parts.push(`AND (l.target_id IS NULL OR ${visibilityForAlias(b, alias)})`);
|
|
445
|
+
return { from: parts.join(" "), alias };
|
|
446
|
+
}
|
|
447
|
+
function compileGraphMembership(m, b) {
|
|
448
|
+
const core = graphSubqueryCore(m.direction, b, { glob: m.glob, field: m.field });
|
|
449
|
+
return `EXISTS (SELECT 1 ${core.from})`;
|
|
450
|
+
}
|
|
451
|
+
function compileGraphCollectionSize(c, b) {
|
|
452
|
+
const core = graphSubqueryCore(c.direction, b, {});
|
|
453
|
+
return `(SELECT COUNT(*) ${core.from})`;
|
|
454
|
+
}
|
|
455
|
+
function compileBinary(op, a, b_, b) {
|
|
456
|
+
// For comparison ops on frontmatter text vs a typed literal, we need
|
|
457
|
+
// a cast. Detect the RHS literal shape and cast the LHS accordingly.
|
|
458
|
+
const isBool = op === "AND" || op === "OR";
|
|
459
|
+
if (isBool) {
|
|
460
|
+
return `(${compileExpr(a, b)} ${op} ${compileExpr(b_, b)})`;
|
|
461
|
+
}
|
|
462
|
+
// For comparison, if the LHS is a bare frontmatter ident/select and
|
|
463
|
+
// the RHS is a constant, cast the text extract to the const's type.
|
|
464
|
+
const lhsFm = fmPathIfPure(a);
|
|
465
|
+
const rhsConst = unwrapConstant(b_);
|
|
466
|
+
if (lhsFm !== undefined && rhsConst !== undefined) {
|
|
467
|
+
const rhsSql = compileConst(b_, b);
|
|
468
|
+
return typedCompare(lhsFm, op, rhsConst, rhsSql);
|
|
469
|
+
}
|
|
470
|
+
// Otherwise, compare the two expressions verbatim. Both sides text.
|
|
471
|
+
return `(${compileExpr(a, b)} ${op} ${compileExpr(b_, b)})`;
|
|
472
|
+
}
|
|
473
|
+
function fmPathIfPure(expr) {
|
|
474
|
+
if (expr.exprKind.case === "identExpr") {
|
|
475
|
+
const name = expr.exprKind.value.name;
|
|
476
|
+
if (name.startsWith(INTRINSIC_PREFIX))
|
|
477
|
+
return undefined;
|
|
478
|
+
return [name];
|
|
479
|
+
}
|
|
480
|
+
if (expr.exprKind.case === "selectExpr") {
|
|
481
|
+
const path = collectSelectPath(expr);
|
|
482
|
+
if (!path)
|
|
483
|
+
return undefined;
|
|
484
|
+
if (path[0].startsWith(INTRINSIC_PREFIX))
|
|
485
|
+
return undefined;
|
|
486
|
+
return path;
|
|
487
|
+
}
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
function typedCompare(fmPath, op, rhsConst, rhsSql) {
|
|
491
|
+
const textExpr = frontmatterText(fmPath);
|
|
492
|
+
if (typeof rhsConst === "number") {
|
|
493
|
+
// json_extract in SQLite auto-typecasts number frontmatter to number.
|
|
494
|
+
// For PG we cast the ->>text back with `::numeric`, but a missing key
|
|
495
|
+
// yields NULL — NULL compares are FALSE (matches SQLite semantics of
|
|
496
|
+
// missing key → predicate false).
|
|
497
|
+
return `(${textExpr})::numeric ${op} ${rhsSql}::numeric`;
|
|
498
|
+
}
|
|
499
|
+
if (typeof rhsConst === "bigint") {
|
|
500
|
+
return `(${textExpr})::numeric ${op} ${rhsSql}::numeric`;
|
|
501
|
+
}
|
|
502
|
+
if (typeof rhsConst === "boolean") {
|
|
503
|
+
return `(${textExpr})::boolean ${op} ${rhsConst ? "TRUE" : "FALSE"}`;
|
|
504
|
+
}
|
|
505
|
+
// Strings and everything else — direct text compare.
|
|
506
|
+
return `(${textExpr}) ${op} ${rhsSql}`;
|
|
507
|
+
}
|
|
508
|
+
function compileContains(haystack, needle, b) {
|
|
509
|
+
const h = compileExpr(haystack, b);
|
|
510
|
+
const n = compileExpr(needle, b);
|
|
511
|
+
return `(position(${n} in ${h}) > 0)`;
|
|
512
|
+
}
|
|
513
|
+
function compileStartsWith(target, prefix, b) {
|
|
514
|
+
const constPrefix = unwrapConstant(prefix);
|
|
515
|
+
const t = compileExpr(target, b);
|
|
516
|
+
if (typeof constPrefix === "string") {
|
|
517
|
+
const escaped = constPrefix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
518
|
+
return `(${t} LIKE ${b.push(`${escaped}%`)})`;
|
|
519
|
+
}
|
|
520
|
+
const p = compileExpr(prefix, b);
|
|
521
|
+
return `(substring(${t} from 1 for length(${p})) = ${p})`;
|
|
522
|
+
}
|
|
523
|
+
function compileEndsWith(target, suffix, b) {
|
|
524
|
+
const constSuffix = unwrapConstant(suffix);
|
|
525
|
+
const t = compileExpr(target, b);
|
|
526
|
+
if (typeof constSuffix === "string") {
|
|
527
|
+
const escaped = constSuffix.replace(/[\\%_]/g, (ch) => `\\${ch}`);
|
|
528
|
+
return `(${t} LIKE ${b.push(`%${escaped}`)})`;
|
|
529
|
+
}
|
|
530
|
+
const s = compileExpr(suffix, b);
|
|
531
|
+
return `(substring(${t} from length(${t}) - length(${s}) + 1) = ${s})`;
|
|
532
|
+
}
|
|
533
|
+
function compileMatches(target, pattern, b) {
|
|
534
|
+
// POSIX ARE via `~`. Bad pattern → SQLSTATE 2201B, adapter maps to
|
|
535
|
+
// filter_invalid; here it's just a query error.
|
|
536
|
+
const t = compileExpr(target, b);
|
|
537
|
+
const p = compileExpr(pattern, b);
|
|
538
|
+
return `(${t} ~ ${p})`;
|
|
539
|
+
}
|
|
540
|
+
// -----------------------------------------------------------------------------
|
|
541
|
+
// list() polymorphism + membership
|
|
542
|
+
// -----------------------------------------------------------------------------
|
|
543
|
+
function compileIn(needle, container, b) {
|
|
544
|
+
const listHint = unwrapListHint(container);
|
|
545
|
+
if (!listHint) {
|
|
546
|
+
throw new KernelError("filter_invalid", {
|
|
547
|
+
reason: '@in without list(field): use `"x" in list(field)` for polymorphic membership',
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
const fmPath = fmPathIfPure(listHint);
|
|
551
|
+
if (!fmPath) {
|
|
552
|
+
throw new KernelError("filter_invalid", {
|
|
553
|
+
reason: "list() must wrap a frontmatter key",
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
const jsonbPath = frontmatterPath(fmPath);
|
|
557
|
+
const needleConst = unwrapConstant(needle);
|
|
558
|
+
if (needleConst !== undefined) {
|
|
559
|
+
// Build a jsonb literal for the value to compare/contain.
|
|
560
|
+
const jsonbLit = jsonbLiteral(needleConst);
|
|
561
|
+
if (jsonbLit === null) {
|
|
562
|
+
// Non-primitive needle — fall through to expression path below.
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
// Scalar match: `path = '"v"'::jsonb`
|
|
566
|
+
// List match: `path @> '["v"]'::jsonb`
|
|
567
|
+
const scalarLit = b.push(jsonbLit.scalar);
|
|
568
|
+
const arrayLit = b.push(jsonbLit.array);
|
|
569
|
+
return `(${jsonbPath} = ${scalarLit}::jsonb OR ${jsonbPath} @> ${arrayLit}::jsonb)`;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// Dynamic needle — fall back to typed text compare + jsonb_array_elements_text.
|
|
573
|
+
const n = compileExpr(needle, b);
|
|
574
|
+
const textPath = frontmatterText(fmPath);
|
|
575
|
+
return `(${textPath} = ${n} OR EXISTS (SELECT 1 FROM jsonb_array_elements_text(${jsonbPath}) v WHERE v = ${n}))`;
|
|
576
|
+
}
|
|
577
|
+
function jsonbLiteral(v) {
|
|
578
|
+
if (typeof v === "string") {
|
|
579
|
+
const j = JSON.stringify(v);
|
|
580
|
+
return { scalar: j, array: `[${j}]` };
|
|
581
|
+
}
|
|
582
|
+
if (typeof v === "number" || typeof v === "bigint") {
|
|
583
|
+
const s = String(v);
|
|
584
|
+
return { scalar: s, array: `[${s}]` };
|
|
585
|
+
}
|
|
586
|
+
if (typeof v === "boolean") {
|
|
587
|
+
return { scalar: v ? "true" : "false", array: v ? "[true]" : "[false]" };
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
function compileSize(inner, b) {
|
|
592
|
+
const collection = asGraphCollection(inner);
|
|
593
|
+
if (collection)
|
|
594
|
+
return compileGraphCollectionSize(collection, b);
|
|
595
|
+
const listHint = unwrapListHint(inner);
|
|
596
|
+
if (listHint) {
|
|
597
|
+
const fmPath = fmPathIfPure(listHint);
|
|
598
|
+
if (fmPath) {
|
|
599
|
+
const path = frontmatterPath(fmPath);
|
|
600
|
+
// jsonb_typeof: 'array' → jsonb_array_length; scalar → 1; null → 0.
|
|
601
|
+
return `(CASE
|
|
602
|
+
WHEN ${path} IS NULL THEN 0
|
|
603
|
+
WHEN jsonb_typeof(${path}) = 'array' THEN jsonb_array_length(${path})
|
|
604
|
+
ELSE 1
|
|
605
|
+
END)`;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Fallback: string length.
|
|
609
|
+
return `length(${compileExpr(inner, b)})`;
|
|
610
|
+
}
|
|
611
|
+
// -----------------------------------------------------------------------------
|
|
612
|
+
// Comprehensions
|
|
613
|
+
// -----------------------------------------------------------------------------
|
|
614
|
+
function compileComprehension(expr, b) {
|
|
615
|
+
if (expr.exprKind.case !== "comprehensionExpr") {
|
|
616
|
+
throw new Error("compileComprehension: wrong kind");
|
|
617
|
+
}
|
|
618
|
+
const comp = expr.exprKind.value;
|
|
619
|
+
// Graph collection comprehension: $backlinks_static().exists(d, pred) etc.
|
|
620
|
+
const collection = asGraphCollection(comp.iterRange);
|
|
621
|
+
if (collection)
|
|
622
|
+
return compileGraphComprehension(comp, collection, b);
|
|
623
|
+
const listHint = unwrapListHint(comp.iterRange);
|
|
624
|
+
if (!listHint) {
|
|
625
|
+
throw new KernelError("filter_invalid", {
|
|
626
|
+
reason: "comprehensions must iterate over list(field)",
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
const fmPath = fmPathIfPure(listHint);
|
|
630
|
+
if (!fmPath) {
|
|
631
|
+
throw new KernelError("filter_invalid", {
|
|
632
|
+
reason: "list() must wrap a frontmatter key",
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const shape = classifyComprehension(comp);
|
|
636
|
+
if (shape === "unsupported") {
|
|
637
|
+
throw new KernelError("filter_invalid", {
|
|
638
|
+
reason: "unsupported comprehension shape (use .all or .exists)",
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
const jsonbPath = frontmatterPath(fmPath);
|
|
642
|
+
const userPred = extractUserPredicate(comp.loopStep);
|
|
643
|
+
const arrayPred = compileWithIterSubstitution(userPred, comp.iterVar, "elem.value::text", b);
|
|
644
|
+
const scalarPred = compileWithIterSubstitution(userPred, comp.iterVar, frontmatterText(fmPath), b);
|
|
645
|
+
if (shape === "exists") {
|
|
646
|
+
return `(
|
|
647
|
+
(${jsonbPath} IS NOT NULL AND jsonb_typeof(${jsonbPath}) <> 'array' AND (${scalarPred}))
|
|
648
|
+
OR (jsonb_typeof(${jsonbPath}) = 'array' AND EXISTS (
|
|
649
|
+
SELECT 1 FROM jsonb_array_elements_text(${jsonbPath}) AS elem(value) WHERE ${arrayPred}
|
|
650
|
+
))
|
|
651
|
+
)`;
|
|
652
|
+
}
|
|
653
|
+
return `(
|
|
654
|
+
CASE
|
|
655
|
+
WHEN ${jsonbPath} IS NULL THEN TRUE
|
|
656
|
+
WHEN jsonb_typeof(${jsonbPath}) <> 'array' THEN (${scalarPred})
|
|
657
|
+
ELSE NOT EXISTS (
|
|
658
|
+
SELECT 1 FROM jsonb_array_elements_text(${jsonbPath}) AS elem(value) WHERE NOT (${arrayPred})
|
|
659
|
+
)
|
|
660
|
+
END
|
|
661
|
+
)`;
|
|
662
|
+
}
|
|
663
|
+
function classifyComprehension(comp) {
|
|
664
|
+
const init = unwrapConstant(comp.accuInit);
|
|
665
|
+
if (init === true)
|
|
666
|
+
return "all";
|
|
667
|
+
if (init === false)
|
|
668
|
+
return "exists";
|
|
669
|
+
return "unsupported";
|
|
670
|
+
}
|
|
671
|
+
function extractUserPredicate(loopStep) {
|
|
672
|
+
if (loopStep.exprKind.case === "callExpr") {
|
|
673
|
+
const call = loopStep.exprKind.value;
|
|
674
|
+
if ((call.function === "_&&_" || call.function === "_||_") && call.args.length === 2) {
|
|
675
|
+
const [first, second] = call.args;
|
|
676
|
+
if (first?.exprKind.case === "identExpr" &&
|
|
677
|
+
(first.exprKind.value.name === "@result" || first.exprKind.value.name === "__result__")) {
|
|
678
|
+
return second;
|
|
679
|
+
}
|
|
680
|
+
if (second?.exprKind.case === "identExpr" &&
|
|
681
|
+
(second.exprKind.value.name === "@result" || second.exprKind.value.name === "__result__")) {
|
|
682
|
+
return first;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return loopStep;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Recursively compile a predicate expression, substituting occurrences
|
|
690
|
+
* of `iterVar` with the given SQL expression. Kept minimal — supports
|
|
691
|
+
* comparisons, boolean combinators, and the string methods we care
|
|
692
|
+
* about.
|
|
693
|
+
*/
|
|
694
|
+
function compileWithIterSubstitution(expr, iterVar, iterSql, b) {
|
|
695
|
+
const kind = expr.exprKind.case;
|
|
696
|
+
if (kind === "identExpr" && expr.exprKind.value.name === iterVar) {
|
|
697
|
+
return iterSql;
|
|
698
|
+
}
|
|
699
|
+
if (kind === "callExpr") {
|
|
700
|
+
const call = expr.exprKind.value;
|
|
701
|
+
const target = call.target;
|
|
702
|
+
const compiledArgs = call.args.map((a) => compileWithIterSubstitution(a, iterVar, iterSql, b));
|
|
703
|
+
const compiledTarget = target
|
|
704
|
+
? compileWithIterSubstitution(target, iterVar, iterSql, b)
|
|
705
|
+
: undefined;
|
|
706
|
+
return combineCallWithSubstitutedArgs(call.function, compiledTarget, compiledArgs, b);
|
|
707
|
+
}
|
|
708
|
+
if (kind === "selectExpr") {
|
|
709
|
+
const root = collectSelectPath(expr)?.[0];
|
|
710
|
+
if (root === iterVar) {
|
|
711
|
+
throw new KernelError("filter_invalid", {
|
|
712
|
+
reason: `member access on comprehension iter-var '${iterVar}' is not yet supported`,
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
return compileExpr(expr, b);
|
|
716
|
+
}
|
|
717
|
+
return compileExpr(expr, b);
|
|
718
|
+
}
|
|
719
|
+
function combineCallWithSubstitutedArgs(fn, target, args, b) {
|
|
720
|
+
const binOp = BIN_OPS[fn];
|
|
721
|
+
if (binOp && args.length === 2 && !target) {
|
|
722
|
+
return `(${args[0]} ${binOp} ${args[1]})`;
|
|
723
|
+
}
|
|
724
|
+
if (fn === "!_" && args.length === 1 && !target) {
|
|
725
|
+
return `NOT (${args[0]})`;
|
|
726
|
+
}
|
|
727
|
+
if (fn === "startsWith") {
|
|
728
|
+
const t = target ?? args[0];
|
|
729
|
+
const p = target ? args[0] : args[1];
|
|
730
|
+
if (t === undefined || p === undefined)
|
|
731
|
+
throw new KernelError("filter_invalid", { reason: "startsWith needs 2 args" });
|
|
732
|
+
return `(substring(${t} from 1 for length(${p})) = ${p})`;
|
|
733
|
+
}
|
|
734
|
+
if (fn === "endsWith") {
|
|
735
|
+
const t = target ?? args[0];
|
|
736
|
+
const p = target ? args[0] : args[1];
|
|
737
|
+
if (t === undefined || p === undefined)
|
|
738
|
+
throw new KernelError("filter_invalid", { reason: "endsWith needs 2 args" });
|
|
739
|
+
return `(substring(${t} from length(${t}) - length(${p}) + 1) = ${p})`;
|
|
740
|
+
}
|
|
741
|
+
if (fn === "contains") {
|
|
742
|
+
const t = target ?? args[0];
|
|
743
|
+
const n = target ? args[0] : args[1];
|
|
744
|
+
if (t === undefined || n === undefined)
|
|
745
|
+
throw new KernelError("filter_invalid", { reason: "contains needs 2 args" });
|
|
746
|
+
return `(position(${n} in ${t}) > 0)`;
|
|
747
|
+
}
|
|
748
|
+
// Silence unused
|
|
749
|
+
void b;
|
|
750
|
+
throw new KernelError("filter_invalid", {
|
|
751
|
+
reason: `unsupported call inside comprehension predicate: ${fn}`,
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
// -----------------------------------------------------------------------------
|
|
755
|
+
// Graph collection comprehensions (design §11.2, WS4).
|
|
756
|
+
// -----------------------------------------------------------------------------
|
|
757
|
+
function compileGraphComprehension(comp, collection, b) {
|
|
758
|
+
const shape = classifyComprehension(comp);
|
|
759
|
+
if (shape === "unsupported") {
|
|
760
|
+
throw new KernelError("filter_invalid", {
|
|
761
|
+
reason: "unsupported comprehension over a link collection (use .exists or .all)",
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
const alias = b.nextAlias();
|
|
765
|
+
const core = graphSubqueryCore(collection.direction, b, { alias });
|
|
766
|
+
const userPred = extractUserPredicate(comp.loopStep);
|
|
767
|
+
const pred = compileAgainstAlias(userPred, comp.iterVar, alias, b);
|
|
768
|
+
if (shape === "exists") {
|
|
769
|
+
return `EXISTS (SELECT 1 ${core.from} AND (${pred}))`;
|
|
770
|
+
}
|
|
771
|
+
return `NOT EXISTS (SELECT 1 ${core.from} AND NOT (${pred}))`;
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* Compile a predicate where the iter var denotes the OTHER document's row
|
|
775
|
+
* (aliased). `d.field` → that doc's frontmatter; `d.$path` etc. → its
|
|
776
|
+
* intrinsics; everything else compiles against the outer `versions` row.
|
|
777
|
+
*/
|
|
778
|
+
function compileAgainstAlias(expr, iterVar, alias, b) {
|
|
779
|
+
const kind = expr.exprKind.case;
|
|
780
|
+
if (kind === "identExpr") {
|
|
781
|
+
if (expr.exprKind.value.name === iterVar) {
|
|
782
|
+
throw new KernelError("filter_invalid", {
|
|
783
|
+
reason: `bare '${iterVar}' is not usable; access a field like ${iterVar}.status or ${iterVar}.$path`,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return compileExpr(expr, b);
|
|
787
|
+
}
|
|
788
|
+
if (kind === "selectExpr") {
|
|
789
|
+
const path = collectSelectPath(expr);
|
|
790
|
+
if (path && path[0] === iterVar) {
|
|
791
|
+
return aliasFieldAccess(alias, path.slice(1));
|
|
792
|
+
}
|
|
793
|
+
return compileExpr(expr, b);
|
|
794
|
+
}
|
|
795
|
+
if (kind === "callExpr") {
|
|
796
|
+
const call = expr.exprKind.value;
|
|
797
|
+
const target = call.target
|
|
798
|
+
? compileAgainstAlias(call.target, iterVar, alias, b)
|
|
799
|
+
: undefined;
|
|
800
|
+
const args = call.args.map((a) => compileAgainstAlias(a, iterVar, alias, b));
|
|
801
|
+
return combineCallWithSubstitutedArgs(call.function, target, args, b);
|
|
802
|
+
}
|
|
803
|
+
return compileExpr(expr, b);
|
|
804
|
+
}
|
|
805
|
+
/** Field/intrinsic access on the aliased other-document row (text form). */
|
|
806
|
+
function aliasFieldAccess(alias, tail) {
|
|
807
|
+
const head = tail[0];
|
|
808
|
+
if (head === undefined) {
|
|
809
|
+
throw new KernelError("filter_invalid", { reason: "empty field access on link iter var" });
|
|
810
|
+
}
|
|
811
|
+
if (head.startsWith(INTRINSIC_PREFIX)) {
|
|
812
|
+
if (tail.length > 1) {
|
|
813
|
+
throw new KernelError("filter_invalid", { reason: "intrinsics have no subfields" });
|
|
814
|
+
}
|
|
815
|
+
const name = head.slice(INTRINSIC_PREFIX.length);
|
|
816
|
+
const col = INTRINSIC_COLUMNS[name];
|
|
817
|
+
if (!col)
|
|
818
|
+
throw new KernelError("filter_invalid", { reason: `unknown intrinsic $${name}` });
|
|
819
|
+
return col.replace(/^versions\./, `${alias}.`);
|
|
820
|
+
}
|
|
821
|
+
// jsonb text access on the aliased frontmatter.
|
|
822
|
+
const escaped = tail.map((p) => p.replace(/'/g, "''"));
|
|
823
|
+
if (escaped.length === 1)
|
|
824
|
+
return `${alias}.frontmatter ->> '${escaped[0]}'`;
|
|
825
|
+
let e = `${alias}.frontmatter -> '${escaped[0]}'`;
|
|
826
|
+
for (let i = 1; i < escaped.length - 1; i++)
|
|
827
|
+
e = `${e} -> '${escaped[i]}'`;
|
|
828
|
+
return `${e} ->> '${escaped[escaped.length - 1]}'`;
|
|
829
|
+
}
|
|
830
|
+
//# sourceMappingURL=compile-postgres.js.map
|