@omgbase/oqx 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +23 -15
  2. package/dist/adapters/indexed.d.ts +11 -0
  3. package/dist/adapters/indexed.d.ts.map +1 -0
  4. package/dist/adapters/indexed.js +57 -0
  5. package/dist/adapters/indexed.js.map +1 -0
  6. package/dist/adapters/sqlite.d.ts +23 -0
  7. package/dist/adapters/sqlite.d.ts.map +1 -0
  8. package/dist/adapters/sqlite.js +113 -0
  9. package/dist/adapters/sqlite.js.map +1 -0
  10. package/dist/ast.d.ts +121 -0
  11. package/dist/ast.d.ts.map +1 -0
  12. package/dist/ast.js +7 -0
  13. package/dist/ast.js.map +1 -0
  14. package/dist/context.d.ts +38 -0
  15. package/dist/context.d.ts.map +1 -0
  16. package/dist/context.js +47 -0
  17. package/dist/context.js.map +1 -0
  18. package/dist/engine.d.ts +49 -0
  19. package/dist/engine.d.ts.map +1 -0
  20. package/dist/engine.js +437 -0
  21. package/dist/engine.js.map +1 -0
  22. package/dist/errors.d.ts +6 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +13 -0
  25. package/dist/errors.js.map +1 -0
  26. package/dist/index.d.ts +34 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +74 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/lexer.d.ts +17 -0
  31. package/dist/lexer.d.ts.map +1 -0
  32. package/dist/lexer.js +187 -0
  33. package/dist/lexer.js.map +1 -0
  34. package/dist/parser.d.ts +6 -0
  35. package/dist/parser.d.ts.map +1 -0
  36. package/dist/parser.js +584 -0
  37. package/dist/parser.js.map +1 -0
  38. package/dist/plan.d.ts +21 -0
  39. package/dist/plan.d.ts.map +1 -0
  40. package/dist/plan.js +56 -0
  41. package/dist/plan.js.map +1 -0
  42. package/dist/planner.d.ts +24 -0
  43. package/dist/planner.d.ts.map +1 -0
  44. package/dist/planner.js +27 -0
  45. package/dist/planner.js.map +1 -0
  46. package/dist/semantics.d.ts +22 -0
  47. package/dist/semantics.d.ts.map +1 -0
  48. package/dist/semantics.js +142 -0
  49. package/dist/semantics.js.map +1 -0
  50. package/package.json +16 -6
  51. package/src/engine.ts +78 -26
package/dist/plan.js ADDED
@@ -0,0 +1,56 @@
1
+ // Pushdown analysis helpers shared by planner adapters (tier 3). The unit of
2
+ // pushdown here is the top-level `where` conjunction: an adapter classifies each
3
+ // AND-conjunct as pushable (translatable into its native query) or residual
4
+ // (must be evaluated in-memory afterward). Whatever it pushes reduces the rows it
5
+ // produces; the residual Query re-runs over those rows in the in-memory engine,
6
+ // which preserves correctness even for partial pushdown.
7
+ //
8
+ // Deliberately conservative: only positive scalar leaves are pushable. Consumer
9
+ // ops (exists/count/collect), negation, and disjunction stay residual — an
10
+ // adapter that wants to push those can special-case them itself.
11
+ /** The synthetic root name the residual query scans — the rows a plan produced. */
12
+ export const ROWS_ROOT = "__oqx_rows__";
13
+ /** Split a top-level where into pushable scalar conjuncts and a residual tree. */
14
+ export function partitionPushable(where, canPush) {
15
+ if (!where)
16
+ return { pushed: [], residual: null };
17
+ const parts = where.kind === "and" ? where.parts : [where];
18
+ const pushed = [];
19
+ const rest = [];
20
+ for (const p of parts) {
21
+ if (p.kind === "scalar" && canPush(p.expr))
22
+ pushed.push(p.expr);
23
+ else
24
+ rest.push(p);
25
+ }
26
+ const residual = rest.length === 0 ? null : rest.length === 1 ? rest[0] : { kind: "and", parts: rest };
27
+ return { pushed, residual };
28
+ }
29
+ /** Rebuild a query to run in-memory over a plan's produced rows: scan the rows
30
+ * root, drop pushed top-level `from`/predicates, keep projection/order/consumer. */
31
+ export function residualQuery(query, residualWhere) {
32
+ return { ...query, source: { kind: "ident", name: ROWS_ROOT }, from: [], where: residualWhere };
33
+ }
34
+ /** A literal or binding — a value known without a row context. */
35
+ export function isConst(e) {
36
+ return e.kind === "lit" || e.kind === "binding";
37
+ }
38
+ /** Evaluate a constant expression against the query bindings. */
39
+ export function constValue(e, params) {
40
+ if (e.kind === "lit")
41
+ return e.value;
42
+ if (e.kind === "binding")
43
+ return params[e.index];
44
+ throw new Error("constValue: not a constant expression");
45
+ }
46
+ /** Recognize `field == const` / `const == field` (field is a bare row column). */
47
+ export function asEquality(e) {
48
+ if (e.kind !== "binary" || e.op !== "==")
49
+ return null;
50
+ if (e.left.kind === "ident" && isConst(e.right))
51
+ return { field: e.left.name, value: e.right };
52
+ if (e.right.kind === "ident" && isConst(e.left))
53
+ return { field: e.right.name, value: e.left };
54
+ return null;
55
+ }
56
+ //# sourceMappingURL=plan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan.js","sourceRoot":"","sources":["../src/plan.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { Query } from "./ast.js";
2
+ import type { Engine, OqxResult } from "./engine.js";
3
+ import type { DataContext } from "./context.js";
4
+ export interface Plan {
5
+ /** The rows the backend produced (already reduced by whatever it pushed). */
6
+ rows(): Iterable<unknown>;
7
+ /** The query to finish in-memory over `rows()`; its source is the rows root. */
8
+ residual: Query;
9
+ /** Optional context for evaluating the residual (to navigate relations of the
10
+ * produced rows). Defaults to plain-object access over `rows()`. */
11
+ context?: DataContext;
12
+ }
13
+ export interface QueryPlanner {
14
+ /** Plan a query, or return null to decline it entirely (full in-memory fallback). */
15
+ plan(query: Query, params: readonly unknown[]): Plan | null;
16
+ }
17
+ /** Runs a planner, finishing the residual on the in-memory engine. */
18
+ export declare class PlannedEngine implements Engine {
19
+ private planner;
20
+ private fallback;
21
+ constructor(planner: QueryPlanner, fallback?: DataContext);
22
+ run(query: Query, bindings?: readonly unknown[]): OqxResult;
23
+ }
24
+ //# sourceMappingURL=planner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner.d.ts","sourceRoot":"","sources":["../src/planner.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAErD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAIhD,MAAM,WAAW,IAAI;IACnB,6EAA6E;IAC7E,IAAI,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,EAAE,KAAK,CAAC;IAChB;wEACoE;IACpE,OAAO,CAAC,EAAE,WAAW,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,qFAAqF;IACrF,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;CAC7D;AAED,sEAAsE;AACtE,qBAAa,aAAc,YAAW,MAAM;IAC1C,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,QAAQ,CAAc;gBAElB,OAAO,EAAE,YAAY,EAAE,QAAQ,GAAE,WAAkC;IAK/E,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,GAAE,SAAS,OAAO,EAAO,GAAG,SAAS;CAOhE"}
@@ -0,0 +1,27 @@
1
+ // The tier-3 seam: a QueryPlanner delegates a query (or part of one) to an
2
+ // underlying store or query system, returning the rows it produced plus a
3
+ // RESIDUAL query for whatever it could not push down. `PlannedEngine` wires this
4
+ // to the in-memory engine: it hands the query to the planner, then finishes the
5
+ // residual over the produced rows — so a planner may be as partial as it likes
6
+ // and correctness is always preserved by the in-memory fallback.
7
+ import { InMemoryEngine } from "./engine.js";
8
+ import { DefaultContext } from "./context.js";
9
+ import { ROWS_ROOT } from "./plan.js";
10
+ /** Runs a planner, finishing the residual on the in-memory engine. */
11
+ export class PlannedEngine {
12
+ planner;
13
+ fallback;
14
+ constructor(planner, fallback = new DefaultContext()) {
15
+ this.planner = planner;
16
+ this.fallback = fallback;
17
+ }
18
+ run(query, bindings = []) {
19
+ const plan = this.planner.plan(query, bindings);
20
+ if (!plan)
21
+ return new InMemoryEngine(this.fallback).run(query, bindings);
22
+ const produced = Array.from(plan.rows());
23
+ const ctx = plan.context ?? new DefaultContext({ [ROWS_ROOT]: produced });
24
+ return new InMemoryEngine(ctx).run(plan.residual, bindings);
25
+ }
26
+ }
27
+ //# sourceMappingURL=planner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"planner.js","sourceRoot":"","sources":["../src/planner.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,0EAA0E;AAC1E,iFAAiF;AACjF,gFAAgF;AAChF,+EAA+E;AAC/E,iEAAiE;AAIjE,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAiBtC,sEAAsE;AACtE,MAAM,OAAO,aAAa;IAChB,OAAO,CAAe;IACtB,QAAQ,CAAc;IAE9B,YAAY,OAAqB,EAAE,WAAwB,IAAI,cAAc,EAAE;QAC7E,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAC3B,CAAC;IAED,GAAG,CAAC,KAAY,EAAE,WAA+B,EAAE;QACjD,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QAChD,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACzE,MAAM,QAAQ,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACzC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,IAAI,IAAI,cAAc,CAAC,EAAE,CAAC,SAAS,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC1E,OAAO,IAAI,cAAc,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAC9D,CAAC;CACF"}
@@ -0,0 +1,22 @@
1
+ /** Equality with absence-normalization (undefined ≡ null) and strict typing. */
2
+ export declare function equals(a: unknown, b: unknown): boolean;
3
+ /** The six relational operators. Absent operands (for ordering ops) yield false. */
4
+ export declare function relate(op: string, a: unknown, b: unknown): boolean;
5
+ /** Arithmetic. `+` concatenates when either side is a string. */
6
+ export declare function arith(op: string, a: unknown, b: unknown): unknown;
7
+ export declare function membership(needle: unknown, haystack: unknown): boolean;
8
+ export declare function truthy(v: unknown): boolean;
9
+ export declare function toNumber(v: unknown): number;
10
+ export declare function sizeOf(v: unknown): number;
11
+ export declare function toList(v: unknown): unknown[];
12
+ /** Normalize a host value into a queryable collection: arrays pass through,
13
+ * non-string iterables are materialized, absence is empty, any other single
14
+ * value becomes a one-element collection (a to-one relation). */
15
+ export declare function coerceCollection(v: unknown): unknown[];
16
+ /** Total order for `order by`, with absent values sorting last. */
17
+ export declare function compareForSort(a: unknown, b: unknown): number;
18
+ /** Free functions callable as `name(args)`. */
19
+ export declare const BUILTIN_FUNCTIONS: Record<string, (args: unknown[]) => unknown>;
20
+ /** Methods callable as `recv.name(args)`. */
21
+ export declare const BUILTIN_METHODS: Record<string, (recv: unknown, args: unknown[]) => unknown>;
22
+ //# sourceMappingURL=semantics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantics.d.ts","sourceRoot":"","sources":["../src/semantics.ts"],"names":[],"mappings":"AAmBA,gFAAgF;AAChF,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAItD;AAED,oFAAoF;AACpF,wBAAgB,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAWlE;AAED,iEAAiE;AACjE,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAYjE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAMtE;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAE1C;AAED,wBAAgB,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAE3C;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAKzC;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE,CAO5C;AAED;;iEAEiE;AACjE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,EAAE,CAOtD;AAED,mEAAmE;AACnE,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,MAAM,CAQ7D;AAED,+CAA+C;AAC/C,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAI1E,CAAC;AAEF,6CAA6C;AAC7C,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAYvF,CAAC"}
@@ -0,0 +1,142 @@
1
+ // Normative scalar semantics — the single source of truth for how OQX values
2
+ // compare, combine, and coerce. Both the in-memory engine AND any pushdown
3
+ // adapter (SQL, remote, …) MUST produce results consistent with these rules; the
4
+ // conformance suite (test/conformance.test.ts) is what verifies a backend obeys
5
+ // them. When a backend cannot reproduce a rule in its native query language, it
6
+ // must leave that fragment as an in-memory RESIDUAL rather than approximate it.
7
+ //
8
+ // The rules (deliberately CEL-flavored, since OQX descends from omgbase's CEL
9
+ // layer):
10
+ // • Absence: `undefined` and `null` are the same absent value.
11
+ // • Equality (== / !=): typed and strict — no cross-type coercion, so
12
+ // `5 == "5"` is false. Two absent values are equal.
13
+ // • Ordering (< <= > >=): a comparison with an absent operand is false (never
14
+ // throws, never orders). Numbers and strings order naturally; mixed types do
15
+ // not order (false).
16
+ // • Truthiness: JavaScript truthiness of the value.
17
+ // • `in`: membership in an array (by ==), substring in a string, or key in an
18
+ // object.
19
+ /** Equality with absence-normalization (undefined ≡ null) and strict typing. */
20
+ export function equals(a, b) {
21
+ const x = a === undefined ? null : a;
22
+ const y = b === undefined ? null : b;
23
+ return x === y;
24
+ }
25
+ /** The six relational operators. Absent operands (for ordering ops) yield false. */
26
+ export function relate(op, a, b) {
27
+ if (op === "==")
28
+ return equals(a, b);
29
+ if (op === "!=")
30
+ return !equals(a, b);
31
+ if (a == null || b == null)
32
+ return false;
33
+ switch (op) {
34
+ case "<": return a < b;
35
+ case "<=": return a <= b;
36
+ case ">": return a > b;
37
+ case ">=": return a >= b;
38
+ default: throw new Error(`not a relational operator: ${op}`);
39
+ }
40
+ }
41
+ /** Arithmetic. `+` concatenates when either side is a string. */
42
+ export function arith(op, a, b) {
43
+ if (op === "+" && (typeof a === "string" || typeof b === "string"))
44
+ return String(a) + String(b);
45
+ const x = toNumber(a);
46
+ const y = toNumber(b);
47
+ switch (op) {
48
+ case "+": return x + y;
49
+ case "-": return x - y;
50
+ case "*": return x * y;
51
+ case "/": return x / y;
52
+ case "%": return x % y;
53
+ default: throw new Error(`not an arithmetic operator: ${op}`);
54
+ }
55
+ }
56
+ export function membership(needle, haystack) {
57
+ if (haystack == null)
58
+ return false;
59
+ if (Array.isArray(haystack))
60
+ return haystack.some((x) => equals(x, needle));
61
+ if (typeof haystack === "string")
62
+ return haystack.includes(String(needle));
63
+ if (typeof haystack === "object")
64
+ return String(needle) in haystack;
65
+ return false;
66
+ }
67
+ export function truthy(v) {
68
+ return Boolean(v);
69
+ }
70
+ export function toNumber(v) {
71
+ return typeof v === "number" ? v : Number(v);
72
+ }
73
+ export function sizeOf(v) {
74
+ if (v == null)
75
+ return 0;
76
+ if (typeof v === "string" || Array.isArray(v))
77
+ return v.length;
78
+ if (typeof v === "object")
79
+ return Object.keys(v).length;
80
+ return 0;
81
+ }
82
+ export function toList(v) {
83
+ if (v == null)
84
+ return [];
85
+ if (Array.isArray(v))
86
+ return v;
87
+ if (typeof v === "object" && typeof v[Symbol.iterator] === "function") {
88
+ return Array.from(v);
89
+ }
90
+ return [v];
91
+ }
92
+ /** Normalize a host value into a queryable collection: arrays pass through,
93
+ * non-string iterables are materialized, absence is empty, any other single
94
+ * value becomes a one-element collection (a to-one relation). */
95
+ export function coerceCollection(v) {
96
+ if (v == null)
97
+ return [];
98
+ if (Array.isArray(v))
99
+ return v;
100
+ if (typeof v === "object" && typeof v[Symbol.iterator] === "function") {
101
+ return Array.from(v);
102
+ }
103
+ return [v];
104
+ }
105
+ /** Total order for `order by`, with absent values sorting last. */
106
+ export function compareForSort(a, b) {
107
+ const an = a == null, bn = b == null;
108
+ if (an && bn)
109
+ return 0;
110
+ if (an)
111
+ return 1;
112
+ if (bn)
113
+ return -1;
114
+ if (a < b)
115
+ return -1;
116
+ if (a > b)
117
+ return 1;
118
+ return 0;
119
+ }
120
+ /** Free functions callable as `name(args)`. */
121
+ export const BUILTIN_FUNCTIONS = {
122
+ list: (args) => toList(args[0]),
123
+ size: (args) => sizeOf(args[0]),
124
+ has: (args) => args[0] != null,
125
+ };
126
+ /** Methods callable as `recv.name(args)`. */
127
+ export const BUILTIN_METHODS = {
128
+ contains: (recv, args) => {
129
+ if (typeof recv === "string")
130
+ return recv.includes(String(args[0]));
131
+ if (Array.isArray(recv))
132
+ return recv.some((x) => equals(x, args[0]));
133
+ return false;
134
+ },
135
+ startsWith: (recv, args) => typeof recv === "string" && recv.startsWith(String(args[0])),
136
+ endsWith: (recv, args) => typeof recv === "string" && recv.endsWith(String(args[0])),
137
+ matches: (recv, args) => recv != null && new RegExp(String(args[0])).test(String(recv)),
138
+ size: (recv) => sizeOf(recv),
139
+ lower: (recv) => String(recv).toLowerCase(),
140
+ upper: (recv) => String(recv).toUpperCase(),
141
+ };
142
+ //# sourceMappingURL=semantics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"semantics.js","sourceRoot":"","sources":["../src/semantics.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,28 +1,38 @@
1
1
  {
2
2
  "name": "@omgbase/oqx",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Generic Object Query eXpression engine for JavaScript",
5
5
  "license": "ISC",
6
6
  "author": "",
7
7
  "type": "module",
8
- "main": "./src/index.ts",
9
- "types": "./src/index.ts",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
10
  "publishConfig": {
11
11
  "access": "public"
12
12
  },
13
13
  "exports": {
14
- ".": "./src/index.ts",
15
- "./sqlite": "./src/adapters/sqlite.ts"
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ },
18
+ "./sqlite": {
19
+ "types": "./dist/adapters/sqlite.d.ts",
20
+ "import": "./dist/adapters/sqlite.js"
21
+ }
16
22
  },
17
23
  "files": [
24
+ "dist",
18
25
  "src"
19
26
  ],
20
27
  "engines": {
21
28
  "node": ">=22.6.0"
22
29
  },
23
30
  "scripts": {
31
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node scripts/fix-dts-extensions.mjs",
32
+ "clean": "rm -rf dist",
24
33
  "test": "node --test test/*.test.ts",
25
- "typecheck": "tsc --noEmit"
34
+ "typecheck": "tsc --noEmit",
35
+ "prepublishOnly": "npm run build"
26
36
  },
27
37
  "devDependencies": {
28
38
  "@types/node": "^22.20.2",
package/src/engine.ts CHANGED
@@ -109,35 +109,76 @@ export class InMemoryEngine implements Engine {
109
109
  return this.shape(query.consumer, scopes, query.select);
110
110
  }
111
111
 
112
+ // A bounded, per-path recursive walk. Each occurrence carries recursion
113
+ // metadata: `$depth` (seed = 1), a categorical `$stop`, and a deterministic
114
+ // `$ordinal`. Semantics:
115
+ // • per-path — a node reached by N distinct paths yields N occurrences
116
+ // (unless `distinct`, which keeps the minimal (depth, path) per identity);
117
+ // • cycles are safe — revisiting a key already on the current path admits ONE
118
+ // occurrence with `$stop == "cycle"` and does not expand it (no runaway);
119
+ // • `$stop` ∈ interior | leaf | frontier | depth | cycle, with precedence
120
+ // cycle > frontier > depth > leaf > interior; only `interior` rows expand;
121
+ // • `$leaf` = (stop == leaf); `$frontier` = (stop ∈ {frontier, depth}) — the
122
+ // "there is unfollowed graph beyond me" signal;
123
+ // • identity for cycle detection + `distinct` is `by <expr>` when given, else
124
+ // `ctx.identity(row)`.
112
125
  private followWalk(seedRows: unknown[], follow: Follow, parent: Scope): Occurrence[] {
113
126
  const cap = follow.depth ?? HARD_DEPTH_CAP;
114
- const idOf = (row: unknown): unknown =>
115
- follow.by ? this.evalExpr(follow.by, { row, parent, bindings: parent.bindings }) : this.ctx.identity(row);
116
- const occ: Occurrence[] = [];
117
- const seen = new Set<unknown>();
118
- let level: { row: unknown; depth: number }[] = seedRows.map((r) => ({ row: r, depth: 1 }));
119
-
120
- while (level.length > 0) {
121
- const next: { row: unknown; depth: number }[] = [];
122
- for (const cur of level) {
123
- const key = idOf(cur.row);
124
- if (seen.has(key)) continue;
125
- seen.add(key);
126
- const s: Scope = { row: cur.row, parent, bindings: parent.bindings };
127
- const raw = this.rowsOf(this.evalExpr(follow.receiver, s));
128
- const succ = follow.where
129
- ? raw.filter((x) => truthy(this.evalExpr(follow.where!, { row: x, parent, bindings: parent.bindings })))
130
- : raw;
131
- const atCap = cur.depth >= cap;
132
- const frontierHit = follow.frontier ? truthy(this.evalExpr(follow.frontier, s)) : false;
133
- const isLeaf = succ.length === 0;
134
- const stop = frontierHit ? "frontier" : atCap ? "depth" : isLeaf ? "leaf" : "continue";
135
- occ.push({ row: cur.row, meta: { $depth: cur.depth, $stop: stop, $leaf: isLeaf, $frontier: frontierHit } });
136
- if (!atCap && !frontierHit && !isLeaf) for (const x of succ) next.push({ row: x, depth: cur.depth + 1 });
127
+ const scopeFor = (row: unknown): Scope => ({ row, parent, bindings: parent.bindings });
128
+ const keyOf = (row: unknown): string =>
129
+ String(follow.by ? this.evalExpr(follow.by, scopeFor(row)) : this.ctx.identity(row));
130
+ const succOf = (row: unknown): unknown[] => {
131
+ const raw = this.rowsOf(this.evalExpr(follow.receiver, scopeFor(row)));
132
+ return follow.where ? raw.filter((x) => truthy(this.evalExpr(follow.where!, scopeFor(x)))) : raw;
133
+ };
134
+ const frontierHit = (row: unknown): boolean =>
135
+ follow.frontier ? truthy(this.evalExpr(follow.frontier, scopeFor(row))) : false;
136
+
137
+ interface Walked { row: unknown; depth: number; path: string; key: string; stop: string; }
138
+ const walked: Walked[] = [];
139
+
140
+ const visit = (row: unknown, depth: number, ancestors: string[]): void => {
141
+ const key = keyOf(row);
142
+ const path = `/${[...ancestors, key].join("/")}/`;
143
+ let stop: string;
144
+ if (ancestors.includes(key)) stop = "cycle";
145
+ else if (frontierHit(row)) stop = "frontier";
146
+ else if (depth >= cap) stop = "depth";
147
+ else {
148
+ const succ = succOf(row);
149
+ if (succ.length === 0) stop = "leaf";
150
+ else {
151
+ walked.push({ row, depth, path, key, stop: "interior" });
152
+ for (const s of succ) visit(s, depth + 1, [...ancestors, key]);
153
+ return;
154
+ }
137
155
  }
138
- level = next;
156
+ walked.push({ row, depth, path, key, stop });
157
+ };
158
+ for (const r of seedRows) visit(r, 1, []);
159
+
160
+ let rows = walked;
161
+ if (follow.distinct) {
162
+ // keep the minimal (depth, path) occurrence per identity key.
163
+ const best = new Map<string, Walked>();
164
+ for (const w of rows) {
165
+ const prev = best.get(w.key);
166
+ if (!prev || w.depth < prev.depth || (w.depth === prev.depth && w.path < prev.path)) best.set(w.key, w);
167
+ }
168
+ rows = [...best.values()];
139
169
  }
140
- return occ;
170
+ // $ordinal: a deterministic 1..N rank over (depth, path).
171
+ rows = rows.slice().sort((a, b) => a.depth - b.depth || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
172
+ return rows.map((w, i) => ({
173
+ row: w.row,
174
+ meta: {
175
+ $depth: w.depth,
176
+ $stop: w.stop,
177
+ $leaf: w.stop === "leaf",
178
+ $frontier: w.stop === "frontier" || w.stop === "depth",
179
+ $ordinal: i + 1,
180
+ },
181
+ }));
141
182
  }
142
183
 
143
184
  // ---- consumer shaping -----------------------------------------------------
@@ -253,7 +294,18 @@ export class InMemoryEngine implements Engine {
253
294
  if (!orderBy || orderBy.length === 0) return;
254
295
  scopes.sort((a, b) => {
255
296
  for (const spec of orderBy) {
256
- const c = compareForSort(this.evalExpr(spec.expr, a), this.evalExpr(spec.expr, b));
297
+ const av = this.evalExpr(spec.expr, a);
298
+ const bv = this.evalExpr(spec.expr, b);
299
+ // Absent (null/undefined) sorts LAST regardless of direction: `desc`
300
+ // reverses the ordering of PRESENT values only, and must not hoist rows
301
+ // that lack the sort key to the top. (Negating the direction over
302
+ // `compareForSort`'s absent-handling result would flip absent-last to
303
+ // absent-first under `desc` — the bug this guards against.)
304
+ const an = av == null, bn = bv == null;
305
+ if (an && bn) continue;
306
+ if (an) return 1;
307
+ if (bn) return -1;
308
+ const c = compareForSort(av, bv);
257
309
  if (c !== 0) return spec.desc ? -c : c;
258
310
  }
259
311
  return 0;