@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.
- package/README.md +23 -15
- package/dist/adapters/indexed.d.ts +11 -0
- package/dist/adapters/indexed.d.ts.map +1 -0
- package/dist/adapters/indexed.js +57 -0
- package/dist/adapters/indexed.js.map +1 -0
- package/dist/adapters/sqlite.d.ts +23 -0
- package/dist/adapters/sqlite.d.ts.map +1 -0
- package/dist/adapters/sqlite.js +113 -0
- package/dist/adapters/sqlite.js.map +1 -0
- package/dist/ast.d.ts +121 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +7 -0
- package/dist/ast.js.map +1 -0
- package/dist/context.d.ts +38 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +47 -0
- package/dist/context.js.map +1 -0
- package/dist/engine.d.ts +49 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +437 -0
- package/dist/engine.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +13 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +74 -0
- package/dist/index.js.map +1 -0
- package/dist/lexer.d.ts +17 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +187 -0
- package/dist/lexer.js.map +1 -0
- package/dist/parser.d.ts +6 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +584 -0
- package/dist/parser.js.map +1 -0
- package/dist/plan.d.ts +21 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +56 -0
- package/dist/plan.js.map +1 -0
- package/dist/planner.d.ts +24 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +27 -0
- package/dist/planner.js.map +1 -0
- package/dist/semantics.d.ts +22 -0
- package/dist/semantics.d.ts.map +1 -0
- package/dist/semantics.js +142 -0
- package/dist/semantics.js.map +1 -0
- package/package.json +16 -6
- 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
|
package/dist/plan.js.map
ADDED
|
@@ -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"}
|
package/dist/planner.js
ADDED
|
@@ -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.
|
|
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": "./
|
|
9
|
-
"types": "./
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
|
-
".":
|
|
15
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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;
|