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.
@@ -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
- * AST node representing a binary expression (e.g., column = value)
101
- */
102
- export interface BinaryExpressionNode {
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 type ExpressionNode =
179
- | BinaryExpressionNode
180
- | LogicalExpressionNode
181
- | NullExpressionNode
182
- | InExpressionNode
183
- | ExistsExpressionNode
184
- | BetweenExpressionNode;
185
-
186
- const operandTypes = new Set<OperandNode['type']>([
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 toNode(val as OperandNode | ColumnDef);
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
+ };