metal-orm 1.0.3 → 1.0.5
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/docs/hydration.md +10 -1
- package/package.json +1 -1
- package/src/ast/expression.ts +632 -614
- package/src/ast/query.ts +110 -49
- package/src/builder/delete-query-state.ts +42 -0
- package/src/builder/delete.ts +57 -0
- package/src/builder/hydration-manager.ts +3 -2
- package/src/builder/hydration-planner.ts +89 -33
- package/src/builder/insert-query-state.ts +62 -0
- package/src/builder/insert.ts +59 -0
- package/src/builder/operations/relation-manager.ts +1 -23
- package/src/builder/relation-conditions.ts +45 -1
- package/src/builder/relation-service.ts +81 -18
- package/src/builder/relation-types.ts +15 -0
- package/src/builder/relation-utils.ts +12 -0
- package/src/builder/select.ts +2 -1
- package/src/builder/update-query-state.ts +59 -0
- package/src/builder/update.ts +61 -0
- package/src/dialect/abstract.ts +107 -47
- package/src/dialect/mssql/index.ts +31 -6
- package/src/dialect/mysql/index.ts +31 -6
- package/src/dialect/postgres/index.ts +45 -6
- package/src/dialect/sqlite/index.ts +45 -6
- package/src/index.ts +6 -3
- package/src/playground/features/playground/data/scenarios/hydration.ts +23 -11
- package/src/playground/features/playground/data/schema.ts +10 -6
- package/src/runtime/hydration.ts +17 -5
- package/src/schema/relation.ts +59 -18
- package/tests/belongs-to-many.test.ts +57 -0
- package/tests/dml.test.ts +206 -0
package/src/ast/expression.ts
CHANGED
|
@@ -1,620 +1,638 @@
|
|
|
1
|
-
import { ColumnDef } from '../schema/column';
|
|
2
|
-
import type { SelectQueryNode, OrderByNode } from './query';
|
|
3
|
-
import { OrderDirection, SqlOperator } from '../constants/sql';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* AST node representing a literal value
|
|
7
|
-
*/
|
|
8
|
-
export interface LiteralNode {
|
|
9
|
-
type: 'Literal';
|
|
10
|
-
/** The literal value (string, number, boolean, or null) */
|
|
11
|
-
value: string | number | boolean | null;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* AST node representing a column reference
|
|
16
|
-
*/
|
|
17
|
-
export interface ColumnNode {
|
|
18
|
-
type: 'Column';
|
|
19
|
-
/** Table name the column belongs to */
|
|
20
|
-
table: string;
|
|
21
|
-
/** Column name */
|
|
22
|
-
name: string;
|
|
23
|
-
/** Optional alias for the column */
|
|
24
|
-
alias?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* AST node representing a function call
|
|
29
|
-
*/
|
|
30
|
-
export interface FunctionNode {
|
|
31
|
-
type: 'Function';
|
|
32
|
-
/** Function name (e.g., COUNT, SUM) */
|
|
33
|
-
name: string;
|
|
34
|
-
/** Function arguments */
|
|
35
|
-
args: (ColumnNode | LiteralNode | JsonPathNode)[];
|
|
36
|
-
/** Optional alias for the function result */
|
|
37
|
-
alias?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* AST node representing a JSON path expression
|
|
42
|
-
*/
|
|
43
|
-
export interface JsonPathNode {
|
|
44
|
-
type: 'JsonPath';
|
|
45
|
-
/** Source column */
|
|
46
|
-
column: ColumnNode;
|
|
47
|
-
/** JSON path expression */
|
|
48
|
-
path: string;
|
|
49
|
-
/** Optional alias for the result */
|
|
50
|
-
alias?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* AST node representing a scalar subquery
|
|
55
|
-
*/
|
|
56
|
-
export interface ScalarSubqueryNode {
|
|
57
|
-
type: 'ScalarSubquery';
|
|
58
|
-
/** Subquery to execute */
|
|
59
|
-
query: SelectQueryNode;
|
|
60
|
-
/** Optional alias for the subquery result */
|
|
61
|
-
alias?: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* AST node representing a CASE expression
|
|
66
|
-
*/
|
|
67
|
-
export interface CaseExpressionNode {
|
|
68
|
-
type: 'CaseExpression';
|
|
69
|
-
/** WHEN-THEN conditions */
|
|
70
|
-
conditions: { when: ExpressionNode; then: OperandNode }[];
|
|
71
|
-
/** Optional ELSE clause */
|
|
72
|
-
else?: OperandNode;
|
|
73
|
-
/** Optional alias for the result */
|
|
74
|
-
alias?: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* AST node representing a window function
|
|
79
|
-
*/
|
|
80
|
-
export interface WindowFunctionNode {
|
|
81
|
-
type: 'WindowFunction';
|
|
82
|
-
/** Window function name (e.g., ROW_NUMBER, RANK) */
|
|
83
|
-
name: string;
|
|
84
|
-
/** Function arguments */
|
|
85
|
-
args: (ColumnNode | LiteralNode | JsonPathNode)[];
|
|
86
|
-
/** Optional PARTITION BY clause */
|
|
87
|
-
partitionBy?: ColumnNode[];
|
|
88
|
-
/** Optional ORDER BY clause */
|
|
89
|
-
orderBy?: OrderByNode[];
|
|
90
|
-
/** Optional alias for the result */
|
|
91
|
-
alias?: string;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Union type representing any operand that can be used in expressions
|
|
96
|
-
*/
|
|
1
|
+
import { ColumnDef } from '../schema/column';
|
|
2
|
+
import type { SelectQueryNode, OrderByNode } from './query';
|
|
3
|
+
import { OrderDirection, SqlOperator } from '../constants/sql';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* AST node representing a literal value
|
|
7
|
+
*/
|
|
8
|
+
export interface LiteralNode {
|
|
9
|
+
type: 'Literal';
|
|
10
|
+
/** The literal value (string, number, boolean, or null) */
|
|
11
|
+
value: string | number | boolean | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* AST node representing a column reference
|
|
16
|
+
*/
|
|
17
|
+
export interface ColumnNode {
|
|
18
|
+
type: 'Column';
|
|
19
|
+
/** Table name the column belongs to */
|
|
20
|
+
table: string;
|
|
21
|
+
/** Column name */
|
|
22
|
+
name: string;
|
|
23
|
+
/** Optional alias for the column */
|
|
24
|
+
alias?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* AST node representing a function call
|
|
29
|
+
*/
|
|
30
|
+
export interface FunctionNode {
|
|
31
|
+
type: 'Function';
|
|
32
|
+
/** Function name (e.g., COUNT, SUM) */
|
|
33
|
+
name: string;
|
|
34
|
+
/** Function arguments */
|
|
35
|
+
args: (ColumnNode | LiteralNode | JsonPathNode)[];
|
|
36
|
+
/** Optional alias for the function result */
|
|
37
|
+
alias?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* AST node representing a JSON path expression
|
|
42
|
+
*/
|
|
43
|
+
export interface JsonPathNode {
|
|
44
|
+
type: 'JsonPath';
|
|
45
|
+
/** Source column */
|
|
46
|
+
column: ColumnNode;
|
|
47
|
+
/** JSON path expression */
|
|
48
|
+
path: string;
|
|
49
|
+
/** Optional alias for the result */
|
|
50
|
+
alias?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* AST node representing a scalar subquery
|
|
55
|
+
*/
|
|
56
|
+
export interface ScalarSubqueryNode {
|
|
57
|
+
type: 'ScalarSubquery';
|
|
58
|
+
/** Subquery to execute */
|
|
59
|
+
query: SelectQueryNode;
|
|
60
|
+
/** Optional alias for the subquery result */
|
|
61
|
+
alias?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* AST node representing a CASE expression
|
|
66
|
+
*/
|
|
67
|
+
export interface CaseExpressionNode {
|
|
68
|
+
type: 'CaseExpression';
|
|
69
|
+
/** WHEN-THEN conditions */
|
|
70
|
+
conditions: { when: ExpressionNode; then: OperandNode }[];
|
|
71
|
+
/** Optional ELSE clause */
|
|
72
|
+
else?: OperandNode;
|
|
73
|
+
/** Optional alias for the result */
|
|
74
|
+
alias?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* AST node representing a window function
|
|
79
|
+
*/
|
|
80
|
+
export interface WindowFunctionNode {
|
|
81
|
+
type: 'WindowFunction';
|
|
82
|
+
/** Window function name (e.g., ROW_NUMBER, RANK) */
|
|
83
|
+
name: string;
|
|
84
|
+
/** Function arguments */
|
|
85
|
+
args: (ColumnNode | LiteralNode | JsonPathNode)[];
|
|
86
|
+
/** Optional PARTITION BY clause */
|
|
87
|
+
partitionBy?: ColumnNode[];
|
|
88
|
+
/** Optional ORDER BY clause */
|
|
89
|
+
orderBy?: OrderByNode[];
|
|
90
|
+
/** Optional alias for the result */
|
|
91
|
+
alias?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Union type representing any operand that can be used in expressions
|
|
96
|
+
*/
|
|
97
97
|
export type OperandNode = ColumnNode | LiteralNode | FunctionNode | JsonPathNode | ScalarSubqueryNode | CaseExpressionNode | WindowFunctionNode;
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
type: 'BinaryExpression';
|
|
104
|
-
/** Left operand */
|
|
105
|
-
left: OperandNode;
|
|
106
|
-
/** Comparison operator */
|
|
107
|
-
operator: SqlOperator;
|
|
108
|
-
/** Right operand */
|
|
109
|
-
right: OperandNode;
|
|
110
|
-
/** Optional escape character for LIKE expressions */
|
|
111
|
-
escape?: LiteralNode;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* AST node representing a logical expression (AND/OR)
|
|
116
|
-
*/
|
|
117
|
-
export interface LogicalExpressionNode {
|
|
118
|
-
type: 'LogicalExpression';
|
|
119
|
-
/** Logical operator (AND or OR) */
|
|
120
|
-
operator: 'AND' | 'OR';
|
|
121
|
-
/** Operands to combine */
|
|
122
|
-
operands: ExpressionNode[];
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* AST node representing a null check expression
|
|
127
|
-
*/
|
|
128
|
-
export interface NullExpressionNode {
|
|
129
|
-
type: 'NullExpression';
|
|
130
|
-
/** Operand to check for null */
|
|
131
|
-
left: OperandNode;
|
|
132
|
-
/** Null check operator */
|
|
133
|
-
operator: 'IS NULL' | 'IS NOT NULL';
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* AST node representing an IN/NOT IN expression
|
|
138
|
-
*/
|
|
139
|
-
export interface InExpressionNode {
|
|
140
|
-
type: 'InExpression';
|
|
141
|
-
/** Left operand to check */
|
|
142
|
-
left: OperandNode;
|
|
143
|
-
/** IN/NOT IN operator */
|
|
144
|
-
operator: 'IN' | 'NOT IN';
|
|
145
|
-
/** Values to check against */
|
|
146
|
-
right: OperandNode[];
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* AST node representing an EXISTS/NOT EXISTS expression
|
|
151
|
-
*/
|
|
152
|
-
export interface ExistsExpressionNode {
|
|
153
|
-
type: 'ExistsExpression';
|
|
154
|
-
/** EXISTS/NOT EXISTS operator */
|
|
155
|
-
operator: SqlOperator;
|
|
156
|
-
/** Subquery to check */
|
|
157
|
-
subquery: SelectQueryNode;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* AST node representing a BETWEEN/NOT BETWEEN expression
|
|
162
|
-
*/
|
|
163
|
-
export interface BetweenExpressionNode {
|
|
164
|
-
type: 'BetweenExpression';
|
|
165
|
-
/** Operand to check */
|
|
166
|
-
left: OperandNode;
|
|
167
|
-
/** BETWEEN/NOT BETWEEN operator */
|
|
168
|
-
operator: 'BETWEEN' | 'NOT BETWEEN';
|
|
169
|
-
/** Lower bound */
|
|
170
|
-
lower: OperandNode;
|
|
171
|
-
/** Upper bound */
|
|
172
|
-
upper: OperandNode;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Union type representing any supported expression node
|
|
100
|
+
* Converts a primitive or existing operand into an operand node
|
|
101
|
+
* @param value - Value or operand to normalize
|
|
102
|
+
* @returns OperandNode representing the value
|
|
177
103
|
*/
|
|
178
|
-
export
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
'Column',
|
|
188
|
-
'Literal',
|
|
189
|
-
'Function',
|
|
190
|
-
'JsonPath',
|
|
191
|
-
'ScalarSubquery',
|
|
192
|
-
'CaseExpression',
|
|
193
|
-
'WindowFunction'
|
|
194
|
-
]);
|
|
195
|
-
|
|
196
|
-
const isOperandNode = (node: any): node is OperandNode => {
|
|
197
|
-
return node && operandTypes.has(node.type);
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
export const isFunctionNode = (node: any): node is FunctionNode => node?.type === 'Function';
|
|
201
|
-
export const isCaseExpressionNode = (node: any): node is CaseExpressionNode => node?.type === 'CaseExpression';
|
|
202
|
-
export const isWindowFunctionNode = (node: any): node is WindowFunctionNode => node?.type === 'WindowFunction';
|
|
203
|
-
export const isExpressionSelectionNode = (
|
|
204
|
-
node: ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode
|
|
205
|
-
): node is FunctionNode | CaseExpressionNode | WindowFunctionNode =>
|
|
206
|
-
isFunctionNode(node) || isCaseExpressionNode(node) || isWindowFunctionNode(node);
|
|
207
|
-
|
|
208
|
-
// Helper to convert Schema definition to AST Node
|
|
209
|
-
const toNode = (col: ColumnDef | OperandNode): OperandNode => {
|
|
210
|
-
if (isOperandNode(col)) return col as OperandNode;
|
|
211
|
-
const def = col as ColumnDef;
|
|
212
|
-
return { type: 'Column', table: def.table || 'unknown', name: def.name };
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const toLiteralNode = (value: string | number | boolean | null): LiteralNode => ({
|
|
216
|
-
type: 'Literal',
|
|
217
|
-
value
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
const toOperand = (val: OperandNode | ColumnDef | string | number | boolean | null): OperandNode => {
|
|
221
|
-
if (val === null) return { type: 'Literal', value: null };
|
|
222
|
-
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
|
|
223
|
-
return { type: 'Literal', value: val };
|
|
104
|
+
export const valueToOperand = (value: unknown): OperandNode => {
|
|
105
|
+
if (
|
|
106
|
+
value === null ||
|
|
107
|
+
value === undefined ||
|
|
108
|
+
typeof value === 'string' ||
|
|
109
|
+
typeof value === 'number' ||
|
|
110
|
+
typeof value === 'boolean'
|
|
111
|
+
) {
|
|
112
|
+
return { type: 'Literal', value: value === undefined ? null : value } as LiteralNode;
|
|
224
113
|
}
|
|
225
|
-
return
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// Factories
|
|
229
|
-
const createBinaryExpression = (
|
|
230
|
-
operator: SqlOperator,
|
|
231
|
-
left: OperandNode | ColumnDef,
|
|
232
|
-
right: OperandNode | ColumnDef | string | number | boolean | null,
|
|
233
|
-
escape?: string
|
|
234
|
-
): BinaryExpressionNode => {
|
|
235
|
-
const node: BinaryExpressionNode = {
|
|
236
|
-
type: 'BinaryExpression',
|
|
237
|
-
left: toNode(left),
|
|
238
|
-
operator,
|
|
239
|
-
right: toOperand(right)
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
if (escape !== undefined) {
|
|
243
|
-
node.escape = toLiteralNode(escape);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return node;
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Creates an equality expression (left = right)
|
|
251
|
-
* @param left - Left operand
|
|
252
|
-
* @param right - Right operand
|
|
253
|
-
* @returns Binary expression node with equality operator
|
|
254
|
-
*/
|
|
255
|
-
export const eq = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
256
|
-
createBinaryExpression('=', left, right);
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Creates a greater-than expression (left > right)
|
|
260
|
-
* @param left - Left operand
|
|
261
|
-
* @param right - Right operand
|
|
262
|
-
* @returns Binary expression node with greater-than operator
|
|
263
|
-
*/
|
|
264
|
-
export const gt = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
265
|
-
createBinaryExpression('>', left, right);
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Creates a less-than expression (left < right)
|
|
269
|
-
* @param left - Left operand
|
|
270
|
-
* @param right - Right operand
|
|
271
|
-
* @returns Binary expression node with less-than operator
|
|
272
|
-
*/
|
|
273
|
-
export const lt = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
274
|
-
createBinaryExpression('<', left, right);
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Creates a LIKE pattern matching expression
|
|
278
|
-
* @param left - Left operand
|
|
279
|
-
* @param pattern - Pattern to match
|
|
280
|
-
* @param escape - Optional escape character
|
|
281
|
-
* @returns Binary expression node with LIKE operator
|
|
282
|
-
*/
|
|
283
|
-
export const like = (left: OperandNode | ColumnDef, pattern: string, escape?: string): BinaryExpressionNode =>
|
|
284
|
-
createBinaryExpression('LIKE', left, pattern, escape);
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Creates a NOT LIKE pattern matching expression
|
|
288
|
-
* @param left - Left operand
|
|
289
|
-
* @param pattern - Pattern to match
|
|
290
|
-
* @param escape - Optional escape character
|
|
291
|
-
* @returns Binary expression node with NOT LIKE operator
|
|
292
|
-
*/
|
|
293
|
-
export const notLike = (left: OperandNode | ColumnDef, pattern: string, escape?: string): BinaryExpressionNode =>
|
|
294
|
-
createBinaryExpression('NOT LIKE', left, pattern, escape);
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Creates a logical AND expression
|
|
298
|
-
* @param operands - Expressions to combine with AND
|
|
299
|
-
* @returns Logical expression node with AND operator
|
|
300
|
-
*/
|
|
301
|
-
export const and = (...operands: ExpressionNode[]): LogicalExpressionNode => ({
|
|
302
|
-
type: 'LogicalExpression',
|
|
303
|
-
operator: 'AND',
|
|
304
|
-
operands
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Creates a logical OR expression
|
|
309
|
-
* @param operands - Expressions to combine with OR
|
|
310
|
-
* @returns Logical expression node with OR operator
|
|
311
|
-
*/
|
|
312
|
-
export const or = (...operands: ExpressionNode[]): LogicalExpressionNode => ({
|
|
313
|
-
type: 'LogicalExpression',
|
|
314
|
-
operator: 'OR',
|
|
315
|
-
operands
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Creates an IS NULL expression
|
|
320
|
-
* @param left - Operand to check for null
|
|
321
|
-
* @returns Null expression node with IS NULL operator
|
|
322
|
-
*/
|
|
323
|
-
export const isNull = (left: OperandNode | ColumnDef): NullExpressionNode => ({
|
|
324
|
-
type: 'NullExpression',
|
|
325
|
-
left: toNode(left),
|
|
326
|
-
operator: 'IS NULL'
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Creates an IS NOT NULL expression
|
|
331
|
-
* @param left - Operand to check for non-null
|
|
332
|
-
* @returns Null expression node with IS NOT NULL operator
|
|
333
|
-
*/
|
|
334
|
-
export const isNotNull = (left: OperandNode | ColumnDef): NullExpressionNode => ({
|
|
335
|
-
type: 'NullExpression',
|
|
336
|
-
left: toNode(left),
|
|
337
|
-
operator: 'IS NOT NULL'
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
const createInExpression = (
|
|
341
|
-
operator: 'IN' | 'NOT IN',
|
|
342
|
-
left: OperandNode | ColumnDef,
|
|
343
|
-
values: (string | number | LiteralNode)[]
|
|
344
|
-
): InExpressionNode => ({
|
|
345
|
-
type: 'InExpression',
|
|
346
|
-
left: toNode(left),
|
|
347
|
-
operator,
|
|
348
|
-
right: values.map(v => toOperand(v))
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* Creates an IN expression (value IN list)
|
|
353
|
-
* @param left - Operand to check
|
|
354
|
-
* @param values - Values to check against
|
|
355
|
-
* @returns IN expression node
|
|
356
|
-
*/
|
|
357
|
-
export const inList = (left: OperandNode | ColumnDef, values: (string | number | LiteralNode)[]): InExpressionNode =>
|
|
358
|
-
createInExpression('IN', left, values);
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Creates a NOT IN expression (value NOT IN list)
|
|
362
|
-
* @param left - Operand to check
|
|
363
|
-
* @param values - Values to check against
|
|
364
|
-
* @returns NOT IN expression node
|
|
365
|
-
*/
|
|
366
|
-
export const notInList = (left: OperandNode | ColumnDef, values: (string | number | LiteralNode)[]): InExpressionNode =>
|
|
367
|
-
createInExpression('NOT IN', left, values);
|
|
368
|
-
|
|
369
|
-
const createBetweenExpression = (
|
|
370
|
-
operator: 'BETWEEN' | 'NOT BETWEEN',
|
|
371
|
-
left: OperandNode | ColumnDef,
|
|
372
|
-
lower: OperandNode | ColumnDef | string | number,
|
|
373
|
-
upper: OperandNode | ColumnDef | string | number
|
|
374
|
-
): BetweenExpressionNode => ({
|
|
375
|
-
type: 'BetweenExpression',
|
|
376
|
-
left: toNode(left),
|
|
377
|
-
operator,
|
|
378
|
-
lower: toOperand(lower),
|
|
379
|
-
upper: toOperand(upper)
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Creates a BETWEEN expression (value BETWEEN lower AND upper)
|
|
384
|
-
* @param left - Operand to check
|
|
385
|
-
* @param lower - Lower bound
|
|
386
|
-
* @param upper - Upper bound
|
|
387
|
-
* @returns BETWEEN expression node
|
|
388
|
-
*/
|
|
389
|
-
export const between = (
|
|
390
|
-
left: OperandNode | ColumnDef,
|
|
391
|
-
lower: OperandNode | ColumnDef | string | number,
|
|
392
|
-
upper: OperandNode | ColumnDef | string | number
|
|
393
|
-
): BetweenExpressionNode => createBetweenExpression('BETWEEN', left, lower, upper);
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Creates a NOT BETWEEN expression (value NOT BETWEEN lower AND upper)
|
|
397
|
-
* @param left - Operand to check
|
|
398
|
-
* @param lower - Lower bound
|
|
399
|
-
* @param upper - Upper bound
|
|
400
|
-
* @returns NOT BETWEEN expression node
|
|
401
|
-
*/
|
|
402
|
-
export const notBetween = (
|
|
403
|
-
left: OperandNode | ColumnDef,
|
|
404
|
-
lower: OperandNode | ColumnDef | string | number,
|
|
405
|
-
upper: OperandNode | ColumnDef | string | number
|
|
406
|
-
): BetweenExpressionNode => createBetweenExpression('NOT BETWEEN', left, lower, upper);
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Creates a JSON path expression
|
|
410
|
-
* @param col - Source column
|
|
411
|
-
* @param path - JSON path expression
|
|
412
|
-
* @returns JSON path node
|
|
413
|
-
*/
|
|
414
|
-
export const jsonPath = (col: ColumnDef | ColumnNode, path: string): JsonPathNode => ({
|
|
415
|
-
type: 'JsonPath',
|
|
416
|
-
column: toNode(col) as ColumnNode,
|
|
417
|
-
path
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Creates a COUNT function expression
|
|
422
|
-
* @param col - Column to count
|
|
423
|
-
* @returns Function node with COUNT
|
|
424
|
-
*/
|
|
425
|
-
export const count = (col: ColumnDef | ColumnNode): FunctionNode => ({
|
|
426
|
-
type: 'Function',
|
|
427
|
-
name: 'COUNT',
|
|
428
|
-
args: [toNode(col) as ColumnNode]
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Creates a SUM function expression
|
|
433
|
-
* @param col - Column to sum
|
|
434
|
-
* @returns Function node with SUM
|
|
435
|
-
*/
|
|
436
|
-
export const sum = (col: ColumnDef | ColumnNode): FunctionNode => ({
|
|
437
|
-
type: 'Function',
|
|
438
|
-
name: 'SUM',
|
|
439
|
-
args: [toNode(col) as ColumnNode]
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Creates an AVG function expression
|
|
444
|
-
* @param col - Column to average
|
|
445
|
-
* @returns Function node with AVG
|
|
446
|
-
*/
|
|
447
|
-
export const avg = (col: ColumnDef | ColumnNode): FunctionNode => ({
|
|
448
|
-
type: 'Function',
|
|
449
|
-
name: 'AVG',
|
|
450
|
-
args: [toNode(col) as ColumnNode]
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Creates an EXISTS expression
|
|
455
|
-
* @param subquery - Subquery to check for existence
|
|
456
|
-
* @returns EXISTS expression node
|
|
457
|
-
*/
|
|
458
|
-
export const exists = (subquery: SelectQueryNode): ExistsExpressionNode => ({
|
|
459
|
-
type: 'ExistsExpression',
|
|
460
|
-
operator: 'EXISTS',
|
|
461
|
-
subquery
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Creates a NOT EXISTS expression
|
|
466
|
-
* @param subquery - Subquery to check for non-existence
|
|
467
|
-
* @returns NOT EXISTS expression node
|
|
468
|
-
*/
|
|
469
|
-
export const notExists = (subquery: SelectQueryNode): ExistsExpressionNode => ({
|
|
470
|
-
type: 'ExistsExpression',
|
|
471
|
-
operator: 'NOT EXISTS',
|
|
472
|
-
subquery
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Creates a CASE expression
|
|
477
|
-
* @param conditions - Array of WHEN-THEN conditions
|
|
478
|
-
* @param elseValue - Optional ELSE value
|
|
479
|
-
* @returns CASE expression node
|
|
480
|
-
*/
|
|
481
|
-
export const caseWhen = (
|
|
482
|
-
conditions: { when: ExpressionNode; then: OperandNode | ColumnDef | string | number | boolean | null }[],
|
|
483
|
-
elseValue?: OperandNode | ColumnDef | string | number | boolean | null
|
|
484
|
-
): CaseExpressionNode => ({
|
|
485
|
-
type: 'CaseExpression',
|
|
486
|
-
conditions: conditions.map(c => ({
|
|
487
|
-
when: c.when,
|
|
488
|
-
then: toOperand(c.then)
|
|
489
|
-
})),
|
|
490
|
-
else: elseValue !== undefined ? toOperand(elseValue) : undefined
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
// Window function factories
|
|
494
|
-
const buildWindowFunction = (
|
|
495
|
-
name: string,
|
|
496
|
-
args: (ColumnNode | LiteralNode | JsonPathNode)[] = [],
|
|
497
|
-
partitionBy?: ColumnNode[],
|
|
498
|
-
orderBy?: OrderByNode[]
|
|
499
|
-
): WindowFunctionNode => {
|
|
500
|
-
const node: WindowFunctionNode = {
|
|
501
|
-
type: 'WindowFunction',
|
|
502
|
-
name,
|
|
503
|
-
args
|
|
504
|
-
};
|
|
505
|
-
|
|
506
|
-
if (partitionBy && partitionBy.length) {
|
|
507
|
-
node.partitionBy = partitionBy;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
if (orderBy && orderBy.length) {
|
|
511
|
-
node.orderBy = orderBy;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
return node;
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Creates a ROW_NUMBER window function
|
|
519
|
-
* @returns Window function node for ROW_NUMBER
|
|
520
|
-
*/
|
|
521
|
-
export const rowNumber = (): WindowFunctionNode => buildWindowFunction('ROW_NUMBER');
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Creates a RANK window function
|
|
525
|
-
* @returns Window function node for RANK
|
|
526
|
-
*/
|
|
527
|
-
export const rank = (): WindowFunctionNode => buildWindowFunction('RANK');
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* Creates a DENSE_RANK window function
|
|
531
|
-
* @returns Window function node for DENSE_RANK
|
|
532
|
-
*/
|
|
533
|
-
export const denseRank = (): WindowFunctionNode => buildWindowFunction('DENSE_RANK');
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Creates an NTILE window function
|
|
537
|
-
* @param n - Number of buckets
|
|
538
|
-
* @returns Window function node for NTILE
|
|
539
|
-
*/
|
|
540
|
-
export const ntile = (n: number): WindowFunctionNode => buildWindowFunction('NTILE', [{ type: 'Literal', value: n }]);
|
|
541
|
-
|
|
542
|
-
const columnOperand = (col: ColumnDef | ColumnNode): ColumnNode => toNode(col) as ColumnNode;
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Creates a LAG window function
|
|
546
|
-
* @param col - Column to lag
|
|
547
|
-
* @param offset - Offset (defaults to 1)
|
|
548
|
-
* @param defaultValue - Default value if no row exists
|
|
549
|
-
* @returns Window function node for LAG
|
|
550
|
-
*/
|
|
551
|
-
export const lag = (col: ColumnDef | ColumnNode, offset: number = 1, defaultValue?: any): WindowFunctionNode => {
|
|
552
|
-
const args: (ColumnNode | LiteralNode | JsonPathNode)[] = [columnOperand(col), { type: 'Literal', value: offset }];
|
|
553
|
-
if (defaultValue !== undefined) {
|
|
554
|
-
args.push({ type: 'Literal', value: defaultValue });
|
|
555
|
-
}
|
|
556
|
-
return buildWindowFunction('LAG', args);
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Creates a LEAD window function
|
|
561
|
-
* @param col - Column to lead
|
|
562
|
-
* @param offset - Offset (defaults to 1)
|
|
563
|
-
* @param defaultValue - Default value if no row exists
|
|
564
|
-
* @returns Window function node for LEAD
|
|
565
|
-
*/
|
|
566
|
-
export const lead = (col: ColumnDef | ColumnNode, offset: number = 1, defaultValue?: any): WindowFunctionNode => {
|
|
567
|
-
const args: (ColumnNode | LiteralNode | JsonPathNode)[] = [columnOperand(col), { type: 'Literal', value: offset }];
|
|
568
|
-
if (defaultValue !== undefined) {
|
|
569
|
-
args.push({ type: 'Literal', value: defaultValue });
|
|
570
|
-
}
|
|
571
|
-
return buildWindowFunction('LEAD', args);
|
|
572
|
-
};
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Creates a FIRST_VALUE window function
|
|
576
|
-
* @param col - Column to get first value from
|
|
577
|
-
* @returns Window function node for FIRST_VALUE
|
|
578
|
-
*/
|
|
579
|
-
export const firstValue = (col: ColumnDef | ColumnNode): WindowFunctionNode => buildWindowFunction('FIRST_VALUE', [columnOperand(col)]);
|
|
580
|
-
|
|
581
|
-
/**
|
|
582
|
-
* Creates a LAST_VALUE window function
|
|
583
|
-
* @param col - Column to get last value from
|
|
584
|
-
* @returns Window function node for LAST_VALUE
|
|
585
|
-
*/
|
|
586
|
-
export const lastValue = (col: ColumnDef | ColumnNode): WindowFunctionNode => buildWindowFunction('LAST_VALUE', [columnOperand(col)]);
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Creates a custom window function
|
|
590
|
-
* @param name - Window function name
|
|
591
|
-
* @param args - Function arguments
|
|
592
|
-
* @param partitionBy - Optional PARTITION BY columns
|
|
593
|
-
* @param orderBy - Optional ORDER BY clauses
|
|
594
|
-
* @returns Window function node
|
|
595
|
-
*/
|
|
596
|
-
export const windowFunction = (
|
|
597
|
-
name: string,
|
|
598
|
-
args: (ColumnDef | ColumnNode | LiteralNode | JsonPathNode)[] = [],
|
|
599
|
-
partitionBy?: (ColumnDef | ColumnNode)[],
|
|
600
|
-
orderBy?: { column: ColumnDef | ColumnNode, direction: OrderDirection }[]
|
|
601
|
-
): WindowFunctionNode => {
|
|
602
|
-
const nodeArgs = args.map(arg => {
|
|
603
|
-
if ((arg as LiteralNode).value !== undefined) {
|
|
604
|
-
return arg as LiteralNode;
|
|
605
|
-
}
|
|
606
|
-
if ((arg as JsonPathNode).path) {
|
|
607
|
-
return arg as JsonPathNode;
|
|
608
|
-
}
|
|
609
|
-
return columnOperand(arg as ColumnDef | ColumnNode);
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
const partitionNodes = partitionBy?.map(col => columnOperand(col)) ?? undefined;
|
|
613
|
-
const orderNodes: OrderByNode[] | undefined = orderBy?.map(o => ({
|
|
614
|
-
type: 'OrderBy',
|
|
615
|
-
column: columnOperand(o.column),
|
|
616
|
-
direction: o.direction
|
|
617
|
-
}));
|
|
618
|
-
|
|
619
|
-
return buildWindowFunction(name, nodeArgs, partitionNodes, orderNodes);
|
|
114
|
+
return value as OperandNode;
|
|
620
115
|
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* AST node representing a binary expression (e.g., column = value)
|
|
119
|
+
*/
|
|
120
|
+
export interface BinaryExpressionNode {
|
|
121
|
+
type: 'BinaryExpression';
|
|
122
|
+
/** Left operand */
|
|
123
|
+
left: OperandNode;
|
|
124
|
+
/** Comparison operator */
|
|
125
|
+
operator: SqlOperator;
|
|
126
|
+
/** Right operand */
|
|
127
|
+
right: OperandNode;
|
|
128
|
+
/** Optional escape character for LIKE expressions */
|
|
129
|
+
escape?: LiteralNode;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* AST node representing a logical expression (AND/OR)
|
|
134
|
+
*/
|
|
135
|
+
export interface LogicalExpressionNode {
|
|
136
|
+
type: 'LogicalExpression';
|
|
137
|
+
/** Logical operator (AND or OR) */
|
|
138
|
+
operator: 'AND' | 'OR';
|
|
139
|
+
/** Operands to combine */
|
|
140
|
+
operands: ExpressionNode[];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* AST node representing a null check expression
|
|
145
|
+
*/
|
|
146
|
+
export interface NullExpressionNode {
|
|
147
|
+
type: 'NullExpression';
|
|
148
|
+
/** Operand to check for null */
|
|
149
|
+
left: OperandNode;
|
|
150
|
+
/** Null check operator */
|
|
151
|
+
operator: 'IS NULL' | 'IS NOT NULL';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* AST node representing an IN/NOT IN expression
|
|
156
|
+
*/
|
|
157
|
+
export interface InExpressionNode {
|
|
158
|
+
type: 'InExpression';
|
|
159
|
+
/** Left operand to check */
|
|
160
|
+
left: OperandNode;
|
|
161
|
+
/** IN/NOT IN operator */
|
|
162
|
+
operator: 'IN' | 'NOT IN';
|
|
163
|
+
/** Values to check against */
|
|
164
|
+
right: OperandNode[];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* AST node representing an EXISTS/NOT EXISTS expression
|
|
169
|
+
*/
|
|
170
|
+
export interface ExistsExpressionNode {
|
|
171
|
+
type: 'ExistsExpression';
|
|
172
|
+
/** EXISTS/NOT EXISTS operator */
|
|
173
|
+
operator: SqlOperator;
|
|
174
|
+
/** Subquery to check */
|
|
175
|
+
subquery: SelectQueryNode;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* AST node representing a BETWEEN/NOT BETWEEN expression
|
|
180
|
+
*/
|
|
181
|
+
export interface BetweenExpressionNode {
|
|
182
|
+
type: 'BetweenExpression';
|
|
183
|
+
/** Operand to check */
|
|
184
|
+
left: OperandNode;
|
|
185
|
+
/** BETWEEN/NOT BETWEEN operator */
|
|
186
|
+
operator: 'BETWEEN' | 'NOT BETWEEN';
|
|
187
|
+
/** Lower bound */
|
|
188
|
+
lower: OperandNode;
|
|
189
|
+
/** Upper bound */
|
|
190
|
+
upper: OperandNode;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Union type representing any supported expression node
|
|
195
|
+
*/
|
|
196
|
+
export type ExpressionNode =
|
|
197
|
+
| BinaryExpressionNode
|
|
198
|
+
| LogicalExpressionNode
|
|
199
|
+
| NullExpressionNode
|
|
200
|
+
| InExpressionNode
|
|
201
|
+
| ExistsExpressionNode
|
|
202
|
+
| BetweenExpressionNode;
|
|
203
|
+
|
|
204
|
+
const operandTypes = new Set<OperandNode['type']>([
|
|
205
|
+
'Column',
|
|
206
|
+
'Literal',
|
|
207
|
+
'Function',
|
|
208
|
+
'JsonPath',
|
|
209
|
+
'ScalarSubquery',
|
|
210
|
+
'CaseExpression',
|
|
211
|
+
'WindowFunction'
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
const isOperandNode = (node: any): node is OperandNode => {
|
|
215
|
+
return node && operandTypes.has(node.type);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const isFunctionNode = (node: any): node is FunctionNode => node?.type === 'Function';
|
|
219
|
+
export const isCaseExpressionNode = (node: any): node is CaseExpressionNode => node?.type === 'CaseExpression';
|
|
220
|
+
export const isWindowFunctionNode = (node: any): node is WindowFunctionNode => node?.type === 'WindowFunction';
|
|
221
|
+
export const isExpressionSelectionNode = (
|
|
222
|
+
node: ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode
|
|
223
|
+
): node is FunctionNode | CaseExpressionNode | WindowFunctionNode =>
|
|
224
|
+
isFunctionNode(node) || isCaseExpressionNode(node) || isWindowFunctionNode(node);
|
|
225
|
+
|
|
226
|
+
// Helper to convert Schema definition to AST Node
|
|
227
|
+
const toNode = (col: ColumnDef | OperandNode): OperandNode => {
|
|
228
|
+
if (isOperandNode(col)) return col as OperandNode;
|
|
229
|
+
const def = col as ColumnDef;
|
|
230
|
+
return { type: 'Column', table: def.table || 'unknown', name: def.name };
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const toLiteralNode = (value: string | number | boolean | null): LiteralNode => ({
|
|
234
|
+
type: 'Literal',
|
|
235
|
+
value
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const toOperand = (val: OperandNode | ColumnDef | string | number | boolean | null): OperandNode => {
|
|
239
|
+
if (val === null) return { type: 'Literal', value: null };
|
|
240
|
+
if (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') {
|
|
241
|
+
return { type: 'Literal', value: val };
|
|
242
|
+
}
|
|
243
|
+
return toNode(val as OperandNode | ColumnDef);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// Factories
|
|
247
|
+
const createBinaryExpression = (
|
|
248
|
+
operator: SqlOperator,
|
|
249
|
+
left: OperandNode | ColumnDef,
|
|
250
|
+
right: OperandNode | ColumnDef | string | number | boolean | null,
|
|
251
|
+
escape?: string
|
|
252
|
+
): BinaryExpressionNode => {
|
|
253
|
+
const node: BinaryExpressionNode = {
|
|
254
|
+
type: 'BinaryExpression',
|
|
255
|
+
left: toNode(left),
|
|
256
|
+
operator,
|
|
257
|
+
right: toOperand(right)
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if (escape !== undefined) {
|
|
261
|
+
node.escape = toLiteralNode(escape);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return node;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Creates an equality expression (left = right)
|
|
269
|
+
* @param left - Left operand
|
|
270
|
+
* @param right - Right operand
|
|
271
|
+
* @returns Binary expression node with equality operator
|
|
272
|
+
*/
|
|
273
|
+
export const eq = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
274
|
+
createBinaryExpression('=', left, right);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Creates a greater-than expression (left > right)
|
|
278
|
+
* @param left - Left operand
|
|
279
|
+
* @param right - Right operand
|
|
280
|
+
* @returns Binary expression node with greater-than operator
|
|
281
|
+
*/
|
|
282
|
+
export const gt = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
283
|
+
createBinaryExpression('>', left, right);
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Creates a less-than expression (left < right)
|
|
287
|
+
* @param left - Left operand
|
|
288
|
+
* @param right - Right operand
|
|
289
|
+
* @returns Binary expression node with less-than operator
|
|
290
|
+
*/
|
|
291
|
+
export const lt = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
292
|
+
createBinaryExpression('<', left, right);
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Creates a LIKE pattern matching expression
|
|
296
|
+
* @param left - Left operand
|
|
297
|
+
* @param pattern - Pattern to match
|
|
298
|
+
* @param escape - Optional escape character
|
|
299
|
+
* @returns Binary expression node with LIKE operator
|
|
300
|
+
*/
|
|
301
|
+
export const like = (left: OperandNode | ColumnDef, pattern: string, escape?: string): BinaryExpressionNode =>
|
|
302
|
+
createBinaryExpression('LIKE', left, pattern, escape);
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Creates a NOT LIKE pattern matching expression
|
|
306
|
+
* @param left - Left operand
|
|
307
|
+
* @param pattern - Pattern to match
|
|
308
|
+
* @param escape - Optional escape character
|
|
309
|
+
* @returns Binary expression node with NOT LIKE operator
|
|
310
|
+
*/
|
|
311
|
+
export const notLike = (left: OperandNode | ColumnDef, pattern: string, escape?: string): BinaryExpressionNode =>
|
|
312
|
+
createBinaryExpression('NOT LIKE', left, pattern, escape);
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Creates a logical AND expression
|
|
316
|
+
* @param operands - Expressions to combine with AND
|
|
317
|
+
* @returns Logical expression node with AND operator
|
|
318
|
+
*/
|
|
319
|
+
export const and = (...operands: ExpressionNode[]): LogicalExpressionNode => ({
|
|
320
|
+
type: 'LogicalExpression',
|
|
321
|
+
operator: 'AND',
|
|
322
|
+
operands
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Creates a logical OR expression
|
|
327
|
+
* @param operands - Expressions to combine with OR
|
|
328
|
+
* @returns Logical expression node with OR operator
|
|
329
|
+
*/
|
|
330
|
+
export const or = (...operands: ExpressionNode[]): LogicalExpressionNode => ({
|
|
331
|
+
type: 'LogicalExpression',
|
|
332
|
+
operator: 'OR',
|
|
333
|
+
operands
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Creates an IS NULL expression
|
|
338
|
+
* @param left - Operand to check for null
|
|
339
|
+
* @returns Null expression node with IS NULL operator
|
|
340
|
+
*/
|
|
341
|
+
export const isNull = (left: OperandNode | ColumnDef): NullExpressionNode => ({
|
|
342
|
+
type: 'NullExpression',
|
|
343
|
+
left: toNode(left),
|
|
344
|
+
operator: 'IS NULL'
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Creates an IS NOT NULL expression
|
|
349
|
+
* @param left - Operand to check for non-null
|
|
350
|
+
* @returns Null expression node with IS NOT NULL operator
|
|
351
|
+
*/
|
|
352
|
+
export const isNotNull = (left: OperandNode | ColumnDef): NullExpressionNode => ({
|
|
353
|
+
type: 'NullExpression',
|
|
354
|
+
left: toNode(left),
|
|
355
|
+
operator: 'IS NOT NULL'
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const createInExpression = (
|
|
359
|
+
operator: 'IN' | 'NOT IN',
|
|
360
|
+
left: OperandNode | ColumnDef,
|
|
361
|
+
values: (string | number | LiteralNode)[]
|
|
362
|
+
): InExpressionNode => ({
|
|
363
|
+
type: 'InExpression',
|
|
364
|
+
left: toNode(left),
|
|
365
|
+
operator,
|
|
366
|
+
right: values.map(v => toOperand(v))
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Creates an IN expression (value IN list)
|
|
371
|
+
* @param left - Operand to check
|
|
372
|
+
* @param values - Values to check against
|
|
373
|
+
* @returns IN expression node
|
|
374
|
+
*/
|
|
375
|
+
export const inList = (left: OperandNode | ColumnDef, values: (string | number | LiteralNode)[]): InExpressionNode =>
|
|
376
|
+
createInExpression('IN', left, values);
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Creates a NOT IN expression (value NOT IN list)
|
|
380
|
+
* @param left - Operand to check
|
|
381
|
+
* @param values - Values to check against
|
|
382
|
+
* @returns NOT IN expression node
|
|
383
|
+
*/
|
|
384
|
+
export const notInList = (left: OperandNode | ColumnDef, values: (string | number | LiteralNode)[]): InExpressionNode =>
|
|
385
|
+
createInExpression('NOT IN', left, values);
|
|
386
|
+
|
|
387
|
+
const createBetweenExpression = (
|
|
388
|
+
operator: 'BETWEEN' | 'NOT BETWEEN',
|
|
389
|
+
left: OperandNode | ColumnDef,
|
|
390
|
+
lower: OperandNode | ColumnDef | string | number,
|
|
391
|
+
upper: OperandNode | ColumnDef | string | number
|
|
392
|
+
): BetweenExpressionNode => ({
|
|
393
|
+
type: 'BetweenExpression',
|
|
394
|
+
left: toNode(left),
|
|
395
|
+
operator,
|
|
396
|
+
lower: toOperand(lower),
|
|
397
|
+
upper: toOperand(upper)
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Creates a BETWEEN expression (value BETWEEN lower AND upper)
|
|
402
|
+
* @param left - Operand to check
|
|
403
|
+
* @param lower - Lower bound
|
|
404
|
+
* @param upper - Upper bound
|
|
405
|
+
* @returns BETWEEN expression node
|
|
406
|
+
*/
|
|
407
|
+
export const between = (
|
|
408
|
+
left: OperandNode | ColumnDef,
|
|
409
|
+
lower: OperandNode | ColumnDef | string | number,
|
|
410
|
+
upper: OperandNode | ColumnDef | string | number
|
|
411
|
+
): BetweenExpressionNode => createBetweenExpression('BETWEEN', left, lower, upper);
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Creates a NOT BETWEEN expression (value NOT BETWEEN lower AND upper)
|
|
415
|
+
* @param left - Operand to check
|
|
416
|
+
* @param lower - Lower bound
|
|
417
|
+
* @param upper - Upper bound
|
|
418
|
+
* @returns NOT BETWEEN expression node
|
|
419
|
+
*/
|
|
420
|
+
export const notBetween = (
|
|
421
|
+
left: OperandNode | ColumnDef,
|
|
422
|
+
lower: OperandNode | ColumnDef | string | number,
|
|
423
|
+
upper: OperandNode | ColumnDef | string | number
|
|
424
|
+
): BetweenExpressionNode => createBetweenExpression('NOT BETWEEN', left, lower, upper);
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Creates a JSON path expression
|
|
428
|
+
* @param col - Source column
|
|
429
|
+
* @param path - JSON path expression
|
|
430
|
+
* @returns JSON path node
|
|
431
|
+
*/
|
|
432
|
+
export const jsonPath = (col: ColumnDef | ColumnNode, path: string): JsonPathNode => ({
|
|
433
|
+
type: 'JsonPath',
|
|
434
|
+
column: toNode(col) as ColumnNode,
|
|
435
|
+
path
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Creates a COUNT function expression
|
|
440
|
+
* @param col - Column to count
|
|
441
|
+
* @returns Function node with COUNT
|
|
442
|
+
*/
|
|
443
|
+
export const count = (col: ColumnDef | ColumnNode): FunctionNode => ({
|
|
444
|
+
type: 'Function',
|
|
445
|
+
name: 'COUNT',
|
|
446
|
+
args: [toNode(col) as ColumnNode]
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Creates a SUM function expression
|
|
451
|
+
* @param col - Column to sum
|
|
452
|
+
* @returns Function node with SUM
|
|
453
|
+
*/
|
|
454
|
+
export const sum = (col: ColumnDef | ColumnNode): FunctionNode => ({
|
|
455
|
+
type: 'Function',
|
|
456
|
+
name: 'SUM',
|
|
457
|
+
args: [toNode(col) as ColumnNode]
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Creates an AVG function expression
|
|
462
|
+
* @param col - Column to average
|
|
463
|
+
* @returns Function node with AVG
|
|
464
|
+
*/
|
|
465
|
+
export const avg = (col: ColumnDef | ColumnNode): FunctionNode => ({
|
|
466
|
+
type: 'Function',
|
|
467
|
+
name: 'AVG',
|
|
468
|
+
args: [toNode(col) as ColumnNode]
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Creates an EXISTS expression
|
|
473
|
+
* @param subquery - Subquery to check for existence
|
|
474
|
+
* @returns EXISTS expression node
|
|
475
|
+
*/
|
|
476
|
+
export const exists = (subquery: SelectQueryNode): ExistsExpressionNode => ({
|
|
477
|
+
type: 'ExistsExpression',
|
|
478
|
+
operator: 'EXISTS',
|
|
479
|
+
subquery
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Creates a NOT EXISTS expression
|
|
484
|
+
* @param subquery - Subquery to check for non-existence
|
|
485
|
+
* @returns NOT EXISTS expression node
|
|
486
|
+
*/
|
|
487
|
+
export const notExists = (subquery: SelectQueryNode): ExistsExpressionNode => ({
|
|
488
|
+
type: 'ExistsExpression',
|
|
489
|
+
operator: 'NOT EXISTS',
|
|
490
|
+
subquery
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Creates a CASE expression
|
|
495
|
+
* @param conditions - Array of WHEN-THEN conditions
|
|
496
|
+
* @param elseValue - Optional ELSE value
|
|
497
|
+
* @returns CASE expression node
|
|
498
|
+
*/
|
|
499
|
+
export const caseWhen = (
|
|
500
|
+
conditions: { when: ExpressionNode; then: OperandNode | ColumnDef | string | number | boolean | null }[],
|
|
501
|
+
elseValue?: OperandNode | ColumnDef | string | number | boolean | null
|
|
502
|
+
): CaseExpressionNode => ({
|
|
503
|
+
type: 'CaseExpression',
|
|
504
|
+
conditions: conditions.map(c => ({
|
|
505
|
+
when: c.when,
|
|
506
|
+
then: toOperand(c.then)
|
|
507
|
+
})),
|
|
508
|
+
else: elseValue !== undefined ? toOperand(elseValue) : undefined
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Window function factories
|
|
512
|
+
const buildWindowFunction = (
|
|
513
|
+
name: string,
|
|
514
|
+
args: (ColumnNode | LiteralNode | JsonPathNode)[] = [],
|
|
515
|
+
partitionBy?: ColumnNode[],
|
|
516
|
+
orderBy?: OrderByNode[]
|
|
517
|
+
): WindowFunctionNode => {
|
|
518
|
+
const node: WindowFunctionNode = {
|
|
519
|
+
type: 'WindowFunction',
|
|
520
|
+
name,
|
|
521
|
+
args
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
if (partitionBy && partitionBy.length) {
|
|
525
|
+
node.partitionBy = partitionBy;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (orderBy && orderBy.length) {
|
|
529
|
+
node.orderBy = orderBy;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return node;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Creates a ROW_NUMBER window function
|
|
537
|
+
* @returns Window function node for ROW_NUMBER
|
|
538
|
+
*/
|
|
539
|
+
export const rowNumber = (): WindowFunctionNode => buildWindowFunction('ROW_NUMBER');
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Creates a RANK window function
|
|
543
|
+
* @returns Window function node for RANK
|
|
544
|
+
*/
|
|
545
|
+
export const rank = (): WindowFunctionNode => buildWindowFunction('RANK');
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Creates a DENSE_RANK window function
|
|
549
|
+
* @returns Window function node for DENSE_RANK
|
|
550
|
+
*/
|
|
551
|
+
export const denseRank = (): WindowFunctionNode => buildWindowFunction('DENSE_RANK');
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Creates an NTILE window function
|
|
555
|
+
* @param n - Number of buckets
|
|
556
|
+
* @returns Window function node for NTILE
|
|
557
|
+
*/
|
|
558
|
+
export const ntile = (n: number): WindowFunctionNode => buildWindowFunction('NTILE', [{ type: 'Literal', value: n }]);
|
|
559
|
+
|
|
560
|
+
const columnOperand = (col: ColumnDef | ColumnNode): ColumnNode => toNode(col) as ColumnNode;
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Creates a LAG window function
|
|
564
|
+
* @param col - Column to lag
|
|
565
|
+
* @param offset - Offset (defaults to 1)
|
|
566
|
+
* @param defaultValue - Default value if no row exists
|
|
567
|
+
* @returns Window function node for LAG
|
|
568
|
+
*/
|
|
569
|
+
export const lag = (col: ColumnDef | ColumnNode, offset: number = 1, defaultValue?: any): WindowFunctionNode => {
|
|
570
|
+
const args: (ColumnNode | LiteralNode | JsonPathNode)[] = [columnOperand(col), { type: 'Literal', value: offset }];
|
|
571
|
+
if (defaultValue !== undefined) {
|
|
572
|
+
args.push({ type: 'Literal', value: defaultValue });
|
|
573
|
+
}
|
|
574
|
+
return buildWindowFunction('LAG', args);
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Creates a LEAD window function
|
|
579
|
+
* @param col - Column to lead
|
|
580
|
+
* @param offset - Offset (defaults to 1)
|
|
581
|
+
* @param defaultValue - Default value if no row exists
|
|
582
|
+
* @returns Window function node for LEAD
|
|
583
|
+
*/
|
|
584
|
+
export const lead = (col: ColumnDef | ColumnNode, offset: number = 1, defaultValue?: any): WindowFunctionNode => {
|
|
585
|
+
const args: (ColumnNode | LiteralNode | JsonPathNode)[] = [columnOperand(col), { type: 'Literal', value: offset }];
|
|
586
|
+
if (defaultValue !== undefined) {
|
|
587
|
+
args.push({ type: 'Literal', value: defaultValue });
|
|
588
|
+
}
|
|
589
|
+
return buildWindowFunction('LEAD', args);
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Creates a FIRST_VALUE window function
|
|
594
|
+
* @param col - Column to get first value from
|
|
595
|
+
* @returns Window function node for FIRST_VALUE
|
|
596
|
+
*/
|
|
597
|
+
export const firstValue = (col: ColumnDef | ColumnNode): WindowFunctionNode => buildWindowFunction('FIRST_VALUE', [columnOperand(col)]);
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Creates a LAST_VALUE window function
|
|
601
|
+
* @param col - Column to get last value from
|
|
602
|
+
* @returns Window function node for LAST_VALUE
|
|
603
|
+
*/
|
|
604
|
+
export const lastValue = (col: ColumnDef | ColumnNode): WindowFunctionNode => buildWindowFunction('LAST_VALUE', [columnOperand(col)]);
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Creates a custom window function
|
|
608
|
+
* @param name - Window function name
|
|
609
|
+
* @param args - Function arguments
|
|
610
|
+
* @param partitionBy - Optional PARTITION BY columns
|
|
611
|
+
* @param orderBy - Optional ORDER BY clauses
|
|
612
|
+
* @returns Window function node
|
|
613
|
+
*/
|
|
614
|
+
export const windowFunction = (
|
|
615
|
+
name: string,
|
|
616
|
+
args: (ColumnDef | ColumnNode | LiteralNode | JsonPathNode)[] = [],
|
|
617
|
+
partitionBy?: (ColumnDef | ColumnNode)[],
|
|
618
|
+
orderBy?: { column: ColumnDef | ColumnNode, direction: OrderDirection }[]
|
|
619
|
+
): WindowFunctionNode => {
|
|
620
|
+
const nodeArgs = args.map(arg => {
|
|
621
|
+
if ((arg as LiteralNode).value !== undefined) {
|
|
622
|
+
return arg as LiteralNode;
|
|
623
|
+
}
|
|
624
|
+
if ((arg as JsonPathNode).path) {
|
|
625
|
+
return arg as JsonPathNode;
|
|
626
|
+
}
|
|
627
|
+
return columnOperand(arg as ColumnDef | ColumnNode);
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const partitionNodes = partitionBy?.map(col => columnOperand(col)) ?? undefined;
|
|
631
|
+
const orderNodes: OrderByNode[] | undefined = orderBy?.map(o => ({
|
|
632
|
+
type: 'OrderBy',
|
|
633
|
+
column: columnOperand(o.column),
|
|
634
|
+
direction: o.direction
|
|
635
|
+
}));
|
|
636
|
+
|
|
637
|
+
return buildWindowFunction(name, nodeArgs, partitionNodes, orderNodes);
|
|
638
|
+
};
|