sumak 0.0.9 → 0.0.10

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.
@@ -0,0 +1,330 @@
1
+ import { DEFAULT_NORMALIZE_OPTIONS } from "./types.mjs";
2
+
3
+ export function normalizeExpression(expr, opts) {
4
+ const o = {
5
+ ...DEFAULT_NORMALIZE_OPTIONS,
6
+ ...opts
7
+ };
8
+ let result = expr;
9
+ if (o.simplifyNegation) result = simplifyNegation(result);
10
+ if (o.foldConstants) result = foldConstants(result);
11
+ if (o.simplifyTautologies) result = simplifyTautologies(result);
12
+ if (o.flattenLogical) result = flattenLogical(result);
13
+ if (o.deduplicatePredicates) result = deduplicatePredicates(result);
14
+
15
+ if (o.simplifyTautologies) result = simplifyTautologies(result);
16
+ return result;
17
+ }
18
+
19
+
20
+ export function toCNF(expr) {
21
+ const conjuncts = flattenAnd(expr);
22
+ const clauses = conjuncts.map((c) => flattenOr(c));
23
+ return { clauses };
24
+ }
25
+
26
+ export function fromCNF(cnf) {
27
+ if (cnf.clauses.length === 0) return undefined;
28
+ const conjuncts = cnf.clauses.map((disjuncts) => {
29
+ if (disjuncts.length === 0) return undefined;
30
+ return disjuncts.reduce((acc, d) => ({
31
+ type: "binary_op",
32
+ op: "OR",
33
+ left: acc,
34
+ right: d
35
+ }));
36
+ });
37
+ const filtered = conjuncts.filter((c) => c !== undefined);
38
+ if (filtered.length === 0) return undefined;
39
+ return filtered.reduce((acc, c) => ({
40
+ type: "binary_op",
41
+ op: "AND",
42
+ left: acc,
43
+ right: c
44
+ }));
45
+ }
46
+
47
+ function flattenAnd(expr) {
48
+ if (expr.type === "binary_op" && expr.op === "AND") {
49
+ return [...flattenAnd(expr.left), ...flattenAnd(expr.right)];
50
+ }
51
+ return [expr];
52
+ }
53
+ function flattenOr(expr) {
54
+ if (expr.type === "binary_op" && expr.op === "OR") {
55
+ return [...flattenOr(expr.left), ...flattenOr(expr.right)];
56
+ }
57
+ return [expr];
58
+ }
59
+
60
+ function flattenLogical(expr) {
61
+ if (expr.type !== "binary_op") return recurse(expr, flattenLogical);
62
+ const e = expr;
63
+ if (e.op === "AND") {
64
+ const parts = flattenAnd(e).map(flattenLogical);
65
+ return parts.reduce((acc, p) => ({
66
+ type: "binary_op",
67
+ op: "AND",
68
+ left: acc,
69
+ right: p
70
+ }));
71
+ }
72
+ if (e.op === "OR") {
73
+ const parts = flattenOr(e).map(flattenLogical);
74
+ return parts.reduce((acc, p) => ({
75
+ type: "binary_op",
76
+ op: "OR",
77
+ left: acc,
78
+ right: p
79
+ }));
80
+ }
81
+ return recurse(expr, flattenLogical);
82
+ }
83
+
84
+
85
+ function deduplicatePredicates(expr) {
86
+ if (expr.type !== "binary_op" || expr.op !== "AND") return expr;
87
+ const parts = flattenAnd(expr);
88
+ const seen = new Set();
89
+ const unique = [];
90
+ for (const p of parts) {
91
+ const key = exprFingerprint(p);
92
+ if (!seen.has(key)) {
93
+ seen.add(key);
94
+ unique.push(p);
95
+ }
96
+ }
97
+ if (unique.length === 0) return {
98
+ type: "literal",
99
+ value: true
100
+ };
101
+ return unique.reduce((acc, p) => ({
102
+ type: "binary_op",
103
+ op: "AND",
104
+ left: acc,
105
+ right: p
106
+ }));
107
+ }
108
+
109
+ function simplifyTautologies(expr) {
110
+ if (expr.type !== "binary_op") return recurse(expr, simplifyTautologies);
111
+ const e = expr;
112
+ const left = simplifyTautologies(e.left);
113
+ const right = simplifyTautologies(e.right);
114
+ if (e.op === "AND") {
115
+
116
+ if (isTrue(left)) return right;
117
+ if (isTrue(right)) return left;
118
+
119
+ if (isFalse(left) || isFalse(right)) return {
120
+ type: "literal",
121
+ value: false
122
+ };
123
+ return {
124
+ ...e,
125
+ left,
126
+ right
127
+ };
128
+ }
129
+ if (e.op === "OR") {
130
+
131
+ if (isTrue(left) || isTrue(right)) return {
132
+ type: "literal",
133
+ value: true
134
+ };
135
+
136
+ if (isFalse(left)) return right;
137
+ if (isFalse(right)) return left;
138
+ return {
139
+ ...e,
140
+ left,
141
+ right
142
+ };
143
+ }
144
+ return {
145
+ ...e,
146
+ left,
147
+ right
148
+ };
149
+ }
150
+
151
+ function simplifyNegation(expr) {
152
+ if (expr.type === "unary_op") {
153
+ const u = expr;
154
+ if (u.op === "NOT") {
155
+ const inner = simplifyNegation(u.operand);
156
+
157
+ if (inner.type === "unary_op" && inner.op === "NOT") {
158
+ return inner.operand;
159
+ }
160
+
161
+ if (isTrue(inner)) return {
162
+ type: "literal",
163
+ value: false
164
+ };
165
+ if (isFalse(inner)) return {
166
+ type: "literal",
167
+ value: true
168
+ };
169
+
170
+ if (inner.type === "is_null") {
171
+ return {
172
+ ...inner,
173
+ negated: !inner.negated
174
+ };
175
+ }
176
+ return {
177
+ ...u,
178
+ operand: inner
179
+ };
180
+ }
181
+ }
182
+ return recurse(expr, simplifyNegation);
183
+ }
184
+
185
+ function foldConstants(expr) {
186
+ if (expr.type !== "binary_op") return recurse(expr, foldConstants);
187
+ const e = expr;
188
+ const left = foldConstants(e.left);
189
+ const right = foldConstants(e.right);
190
+
191
+ if (left.type === "literal" && right.type === "literal") {
192
+ const lv = left.value;
193
+ const rv = right.value;
194
+ if (typeof lv === "number" && typeof rv === "number") {
195
+ const folded = foldNumeric(e.op, lv, rv);
196
+ if (folded !== undefined) return {
197
+ type: "literal",
198
+ value: folded
199
+ };
200
+ }
201
+
202
+ if (typeof lv === "string" && typeof rv === "string" && e.op === "||") {
203
+ return {
204
+ type: "literal",
205
+ value: lv + rv
206
+ };
207
+ }
208
+ }
209
+
210
+ if (isComparisonOp(e.op) && left.type === "literal" && right.type !== "literal") {
211
+ return {
212
+ type: "binary_op",
213
+ op: flipComparison(e.op),
214
+ left: right,
215
+ right: left
216
+ };
217
+ }
218
+ return {
219
+ ...e,
220
+ left,
221
+ right
222
+ };
223
+ }
224
+ function foldNumeric(op, l, r) {
225
+ switch (op) {
226
+ case "+": return l + r;
227
+ case "-": return l - r;
228
+ case "*": return l * r;
229
+ case "/": return r !== 0 ? l / r : undefined;
230
+ case "%": return r !== 0 ? l % r : undefined;
231
+ default: return undefined;
232
+ }
233
+ }
234
+ function isComparisonOp(op) {
235
+ return op === "=" || op === "!=" || op === "<>" || op === "<" || op === ">" || op === "<=" || op === ">=";
236
+ }
237
+ function flipComparison(op) {
238
+ switch (op) {
239
+ case "<": return ">";
240
+ case ">": return "<";
241
+ case "<=": return ">=";
242
+ case ">=": return "<=";
243
+ default: return op;
244
+ }
245
+ }
246
+
247
+ function isTrue(expr) {
248
+ return expr.type === "literal" && expr.value === true;
249
+ }
250
+ function isFalse(expr) {
251
+ return expr.type === "literal" && expr.value === false;
252
+ }
253
+
254
+ function exprFingerprint(expr) {
255
+ switch (expr.type) {
256
+ case "column_ref": return `col:${expr.table ?? ""}:${expr.column}`;
257
+ case "literal": return `lit:${String(expr.value)}`;
258
+ case "param": return `param:${String(expr.value)}`;
259
+ case "binary_op": return `bin:${expr.op}:${exprFingerprint(expr.left)}:${exprFingerprint(expr.right)}`;
260
+ case "unary_op": return `un:${expr.op}:${exprFingerprint(expr.operand)}`;
261
+ case "is_null": return `isnull:${expr.negated}:${exprFingerprint(expr.expr)}`;
262
+ case "between": return `between:${expr.negated}:${exprFingerprint(expr.expr)}:${exprFingerprint(expr.low)}:${exprFingerprint(expr.high)}`;
263
+ case "in":
264
+ if (Array.isArray(expr.values)) {
265
+ return `in:${expr.negated}:${exprFingerprint(expr.expr)}:[${expr.values.map(exprFingerprint).join(",")}]`;
266
+ }
267
+ return `in:${expr.negated}:${exprFingerprint(expr.expr)}:subq`;
268
+ case "function_call": return `fn:${expr.name}:${expr.distinct ?? false}:[${expr.args.map(exprFingerprint).join(",")}]`;
269
+ case "cast": return `cast:${expr.dataType}:${exprFingerprint(expr.expr)}`;
270
+ case "exists": return `exists:${expr.negated}`;
271
+ case "star": return `star:${expr.table ?? ""}`;
272
+ case "raw": return `raw:${expr.sql}`;
273
+ case "subquery": return `subq:${expr.alias ?? ""}`;
274
+ default: return `unknown:${expr.type}`;
275
+ }
276
+ }
277
+
278
+ function recurse(expr, transform) {
279
+ switch (expr.type) {
280
+ case "binary_op": return {
281
+ ...expr,
282
+ left: transform(expr.left),
283
+ right: transform(expr.right)
284
+ };
285
+ case "unary_op": return {
286
+ ...expr,
287
+ operand: transform(expr.operand)
288
+ };
289
+ case "is_null": return {
290
+ ...expr,
291
+ expr: transform(expr.expr)
292
+ };
293
+ case "between": return {
294
+ ...expr,
295
+ expr: transform(expr.expr),
296
+ low: transform(expr.low),
297
+ high: transform(expr.high)
298
+ };
299
+ case "in":
300
+ if (Array.isArray(expr.values)) {
301
+ return {
302
+ ...expr,
303
+ expr: transform(expr.expr),
304
+ values: expr.values.map(transform)
305
+ };
306
+ }
307
+ return {
308
+ ...expr,
309
+ expr: transform(expr.expr)
310
+ };
311
+ case "cast": return {
312
+ ...expr,
313
+ expr: transform(expr.expr)
314
+ };
315
+ case "function_call": return {
316
+ ...expr,
317
+ args: expr.args.map(transform)
318
+ };
319
+ case "case": return {
320
+ ...expr,
321
+ operand: expr.operand ? transform(expr.operand) : undefined,
322
+ whens: expr.whens.map((w) => ({
323
+ condition: transform(w.condition),
324
+ result: transform(w.result)
325
+ })),
326
+ else_: expr.else_ ? transform(expr.else_) : undefined
327
+ };
328
+ default: return expr;
329
+ }
330
+ }
@@ -0,0 +1,4 @@
1
+ export { normalizeExpression, toCNF, fromCNF } from "./expression.mjs";
2
+ export { normalizeQuery } from "./query.mjs";
3
+ export type { CNF, NormalizeOptions } from "./types.mjs";
4
+ export { DEFAULT_NORMALIZE_OPTIONS } from "./types.mjs";
@@ -0,0 +1,3 @@
1
+ export { normalizeExpression, toCNF, fromCNF } from "./expression.mjs";
2
+ export { normalizeQuery } from "./query.mjs";
3
+ export { DEFAULT_NORMALIZE_OPTIONS } from "./types.mjs";
@@ -0,0 +1,14 @@
1
+ import type { ASTNode } from "../ast/nodes.mjs";
2
+ import type { NormalizeOptions } from "./types.mjs";
3
+ /**
4
+ * Normalize a full query AST node.
5
+ *
6
+ * Applies NbE normalization to all expression-bearing parts:
7
+ * - WHERE clauses (SELECT, UPDATE, DELETE)
8
+ * - HAVING clauses (SELECT)
9
+ * - JOIN ON conditions
10
+ * - ON CONFLICT WHERE (INSERT)
11
+ *
12
+ * Leaves non-expression parts (table refs, column lists, ORDER BY) unchanged.
13
+ */
14
+ export declare function normalizeQuery(node: ASTNode, opts?: NormalizeOptions): ASTNode;
@@ -0,0 +1,126 @@
1
+ import { normalizeExpression } from "./expression.mjs";
2
+ import { DEFAULT_NORMALIZE_OPTIONS } from "./types.mjs";
3
+
4
+ export function normalizeQuery(node, opts) {
5
+ const o = {
6
+ ...DEFAULT_NORMALIZE_OPTIONS,
7
+ ...opts
8
+ };
9
+ switch (node.type) {
10
+ case "select": return normalizeSelect(node, o);
11
+ case "update": return normalizeUpdate(node, o);
12
+ case "delete": return normalizeDelete(node, o);
13
+ case "insert": return normalizeInsert(node, o);
14
+ default: return node;
15
+ }
16
+ }
17
+ function normalizeSelect(node, opts) {
18
+ let result = { ...node };
19
+
20
+ if (result.where) {
21
+ const w = normalizeExpression(result.where, opts);
22
+ result = {
23
+ ...result,
24
+ where: w.type === "literal" && w.value === true ? undefined : w
25
+ };
26
+ }
27
+
28
+ if (result.having) {
29
+ const h = normalizeExpression(result.having, opts);
30
+ result = {
31
+ ...result,
32
+ having: h.type === "literal" && h.value === true ? undefined : h
33
+ };
34
+ }
35
+
36
+ if (result.joins.length > 0) {
37
+ result = {
38
+ ...result,
39
+ joins: result.joins.map((j) => {
40
+ if (!j.on) return j;
41
+ return {
42
+ ...j,
43
+ on: normalizeExpression(j.on, opts)
44
+ };
45
+ })
46
+ };
47
+ }
48
+
49
+ if (result.setOp) {
50
+ const normalizedSetQuery = normalizeSelect(result.setOp.query, opts);
51
+ result = {
52
+ ...result,
53
+ setOp: {
54
+ ...result.setOp,
55
+ query: normalizedSetQuery
56
+ }
57
+ };
58
+ }
59
+
60
+ if (result.ctes.length > 0) {
61
+ result = {
62
+ ...result,
63
+ ctes: result.ctes.map((cte) => ({
64
+ ...cte,
65
+ query: normalizeSelect(cte.query, opts)
66
+ }))
67
+ };
68
+ }
69
+ return result;
70
+ }
71
+ function normalizeUpdate(node, opts) {
72
+ let result = { ...node };
73
+ if (result.where) {
74
+ const w = normalizeExpression(result.where, opts);
75
+ result = {
76
+ ...result,
77
+ where: w.type === "literal" && w.value === true ? undefined : w
78
+ };
79
+ }
80
+ if (result.joins.length > 0) {
81
+ result = {
82
+ ...result,
83
+ joins: result.joins.map((j) => {
84
+ if (!j.on) return j;
85
+ return {
86
+ ...j,
87
+ on: normalizeExpression(j.on, opts)
88
+ };
89
+ })
90
+ };
91
+ }
92
+ return result;
93
+ }
94
+ function normalizeDelete(node, opts) {
95
+ let result = { ...node };
96
+ if (result.where) {
97
+ const w = normalizeExpression(result.where, opts);
98
+ result = {
99
+ ...result,
100
+ where: w.type === "literal" && w.value === true ? undefined : w
101
+ };
102
+ }
103
+ if (result.joins.length > 0) {
104
+ result = {
105
+ ...result,
106
+ joins: result.joins.map((j) => {
107
+ if (!j.on) return j;
108
+ return {
109
+ ...j,
110
+ on: normalizeExpression(j.on, opts)
111
+ };
112
+ })
113
+ };
114
+ }
115
+ return result;
116
+ }
117
+ function normalizeInsert(node, opts) {
118
+ if (!node.onConflict?.where) return node;
119
+ return {
120
+ ...node,
121
+ onConflict: {
122
+ ...node.onConflict,
123
+ where: normalizeExpression(node.onConflict.where, opts)
124
+ }
125
+ };
126
+ }
@@ -0,0 +1,38 @@
1
+ import type { ExpressionNode } from "../ast/nodes.mjs";
2
+ /**
3
+ * Semantic domain for NbE (Normalization by Evaluation).
4
+ *
5
+ * The normalizer evaluates AST nodes into a canonical semantic domain,
6
+ * then reifies them back into normal-form AST nodes.
7
+ *
8
+ * This separates "what the query means" from "how it was written".
9
+ */
10
+ /**
11
+ * A predicate in conjunctive normal form (CNF).
12
+ * Top-level is AND, each clause is a disjunction (OR) of atoms.
13
+ *
14
+ * Example: `(a = 1 AND b = 2 AND (c = 3 OR c = 4))`
15
+ * → `[[a=1], [b=2], [c=3, c=4]]`
16
+ *
17
+ * This representation makes deduplication and simplification trivial.
18
+ */
19
+ export interface CNF {
20
+ /** Each inner array is a disjunction (OR). Top-level is conjunction (AND). */
21
+ clauses: ExpressionNode[][];
22
+ }
23
+ /**
24
+ * Normalization options.
25
+ */
26
+ export interface NormalizeOptions {
27
+ /** Flatten nested AND/OR into CNF. Default: true */
28
+ flattenLogical?: boolean;
29
+ /** Remove duplicate predicates. Default: true */
30
+ deduplicatePredicates?: boolean;
31
+ /** Simplify tautologies (WHERE true) and contradictions (WHERE false). Default: true */
32
+ simplifyTautologies?: boolean;
33
+ /** Fold constant expressions (1 + 2 → 3). Default: true */
34
+ foldConstants?: boolean;
35
+ /** Simplify double negation (NOT NOT x → x). Default: true */
36
+ simplifyNegation?: boolean;
37
+ }
38
+ export declare const DEFAULT_NORMALIZE_OPTIONS: Required<NormalizeOptions>;
@@ -0,0 +1,7 @@
1
+ export const DEFAULT_NORMALIZE_OPTIONS = {
2
+ flattenLogical: true,
3
+ deduplicatePredicates: true,
4
+ simplifyTautologies: true,
5
+ foldConstants: true,
6
+ simplifyNegation: true
7
+ };
@@ -0,0 +1,3 @@
1
+ export { optimize, createRule } from "./optimizer.mjs";
2
+ export { predicatePushdown, subqueryFlattening, removeWhereTrue, BUILTIN_RULES } from "./rules.mjs";
3
+ export type { RewriteRule, OptimizeOptions } from "./types.mjs";
@@ -0,0 +1,2 @@
1
+ export { optimize, createRule } from "./optimizer.mjs";
2
+ export { predicatePushdown, subqueryFlattening, removeWhereTrue, BUILTIN_RULES } from "./rules.mjs";
@@ -0,0 +1,28 @@
1
+ import type { ASTNode } from "../ast/nodes.mjs";
2
+ import type { NormalizeOptions } from "../normalize/types.mjs";
3
+ import type { OptimizeOptions, RewriteRule } from "./types.mjs";
4
+ /**
5
+ * Full optimization pipeline: Normalize → Rewrite Rules (to fixpoint).
6
+ *
7
+ * The normalizer reduces expressions to canonical form (NbE).
8
+ * The optimizer applies rewrite rules bottom-up until no more changes occur.
9
+ *
10
+ * ```
11
+ * AST → normalize(NbE) → optimize(rules, fixpoint) → optimized AST
12
+ * ```
13
+ */
14
+ export declare function optimize(node: ASTNode, opts?: OptimizeOptions & {
15
+ normalize?: NormalizeOptions;
16
+ }): ASTNode;
17
+ /**
18
+ * Create a custom rewrite rule.
19
+ *
20
+ * ```ts
21
+ * const myRule = createRule({
22
+ * name: "my-optimization",
23
+ * match: (node) => node.type === "select" && hasPattern(node),
24
+ * apply: (node) => transformPattern(node),
25
+ * })
26
+ * ```
27
+ */
28
+ export declare function createRule(rule: RewriteRule): RewriteRule;
@@ -0,0 +1,37 @@
1
+ import { normalizeQuery } from "../normalize/query.mjs";
2
+ import { BUILTIN_RULES } from "./rules.mjs";
3
+ import { DEFAULT_OPTIMIZE_OPTIONS } from "./types.mjs";
4
+
5
+ export function optimize(node, opts) {
6
+
7
+ let result = normalizeQuery(node, opts?.normalize);
8
+
9
+ const maxIterations = opts?.maxIterations ?? DEFAULT_OPTIMIZE_OPTIONS.maxIterations;
10
+ const disableSet = new Set(opts?.disableRules ?? []);
11
+ const rules = (opts?.rules ?? BUILTIN_RULES).filter((r) => !disableSet.has(r.name));
12
+ for (let i = 0; i < maxIterations; i++) {
13
+ const next = applyRules(result, rules);
14
+ if (next === result) break;
15
+ result = next;
16
+ }
17
+ return result;
18
+ }
19
+
20
+ function applyRules(node, rules) {
21
+ let result = node;
22
+ let changed = false;
23
+ for (const rule of rules) {
24
+ if (rule.match(result)) {
25
+ const next = rule.apply(result);
26
+ if (next !== result) {
27
+ result = next;
28
+ changed = true;
29
+ }
30
+ }
31
+ }
32
+ return changed ? result : node;
33
+ }
34
+
35
+ export function createRule(rule) {
36
+ return rule;
37
+ }
@@ -0,0 +1,31 @@
1
+ import type { RewriteRule } from "./types.mjs";
2
+ /**
3
+ * Predicate pushdown: push WHERE conditions into JOIN ON when they
4
+ * reference only columns from one side of the join.
5
+ *
6
+ * Before: `SELECT ... FROM a JOIN b ON a.id = b.a_id WHERE b.active = true`
7
+ * After: `SELECT ... FROM a JOIN b ON a.id = b.a_id AND b.active = true`
8
+ *
9
+ * This helps the database optimizer by reducing the join's input set.
10
+ */
11
+ export declare const predicatePushdown: RewriteRule;
12
+ /**
13
+ * Remove redundant subquery wrapping when a subquery in FROM
14
+ * is a simple SELECT * with no additional clauses.
15
+ *
16
+ * Before: `SELECT * FROM (SELECT * FROM users) AS u`
17
+ * After: `SELECT * FROM users`
18
+ */
19
+ export declare const subqueryFlattening: RewriteRule;
20
+ /**
21
+ * Merge consecutive WHERE conditions that are just literal true.
22
+ * After normalization, this removes vestigial `WHERE true` from plugins.
23
+ */
24
+ export declare const removeWhereTrue: RewriteRule;
25
+ /**
26
+ * Convert `COUNT(*)` in SELECT with only one table and no GROUP BY
27
+ * to use the table name for clarity (optional cosmetic rule).
28
+ */
29
+ export declare const mergeConsecutiveLimits: RewriteRule;
30
+ /** All built-in optimization rules. */
31
+ export declare const BUILTIN_RULES: RewriteRule[];