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.
- package/README.md +149 -2
- package/dist/builder/json-optics.d.mts +106 -0
- package/dist/builder/json-optics.mjs +110 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +8 -0
- package/dist/normalize/expression.d.mts +26 -0
- package/dist/normalize/expression.mjs +330 -0
- package/dist/normalize/index.d.mts +4 -0
- package/dist/normalize/index.mjs +3 -0
- package/dist/normalize/query.d.mts +14 -0
- package/dist/normalize/query.mjs +126 -0
- package/dist/normalize/types.d.mts +38 -0
- package/dist/normalize/types.mjs +7 -0
- package/dist/optimize/index.d.mts +3 -0
- package/dist/optimize/index.mjs +2 -0
- package/dist/optimize/optimizer.d.mts +28 -0
- package/dist/optimize/optimizer.mjs +37 -0
- package/dist/optimize/rules.d.mts +31 -0
- package/dist/optimize/rules.mjs +161 -0
- package/dist/optimize/types.d.mts +29 -0
- package/dist/optimize/types.mjs +1 -0
- package/dist/sumak.d.mts +21 -3
- package/dist/sumak.mjs +26 -2
- package/package.json +1 -1
|
@@ -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,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,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[];
|