metal-orm 1.1.8 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +769 -764
  2. package/dist/index.cjs +2352 -226
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +605 -40
  5. package/dist/index.d.ts +605 -40
  6. package/dist/index.js +2324 -226
  7. package/dist/index.js.map +1 -1
  8. package/package.json +22 -17
  9. package/src/bulk/bulk-context.ts +83 -0
  10. package/src/bulk/bulk-delete-executor.ts +89 -0
  11. package/src/bulk/bulk-executor.base.ts +73 -0
  12. package/src/bulk/bulk-insert-executor.ts +74 -0
  13. package/src/bulk/bulk-types.ts +70 -0
  14. package/src/bulk/bulk-update-executor.ts +192 -0
  15. package/src/bulk/bulk-upsert-executor.ts +95 -0
  16. package/src/bulk/bulk-utils.ts +91 -0
  17. package/src/bulk/index.ts +18 -0
  18. package/src/codegen/typescript.ts +30 -21
  19. package/src/core/ast/expression-builders.ts +107 -10
  20. package/src/core/ast/expression-nodes.ts +52 -22
  21. package/src/core/ast/expression-visitor.ts +23 -13
  22. package/src/core/dialect/abstract.ts +30 -17
  23. package/src/core/dialect/mysql/index.ts +20 -5
  24. package/src/core/execution/db-executor.ts +96 -64
  25. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
  26. package/src/core/execution/executors/mssql-executor.ts +66 -34
  27. package/src/core/execution/executors/mysql-executor.ts +98 -66
  28. package/src/core/execution/executors/postgres-executor.ts +33 -11
  29. package/src/core/execution/executors/sqlite-executor.ts +86 -30
  30. package/src/decorators/bootstrap.ts +482 -398
  31. package/src/decorators/column-decorator.ts +87 -96
  32. package/src/decorators/decorator-metadata.ts +100 -24
  33. package/src/decorators/entity.ts +27 -24
  34. package/src/decorators/relations.ts +231 -149
  35. package/src/decorators/transformers/transformer-decorators.ts +26 -29
  36. package/src/decorators/validators/country-validators-decorators.ts +9 -15
  37. package/src/dto/apply-filter.ts +568 -551
  38. package/src/index.ts +16 -9
  39. package/src/orm/entity-hydration.ts +116 -72
  40. package/src/orm/entity-metadata.ts +347 -301
  41. package/src/orm/entity-relations.ts +264 -207
  42. package/src/orm/entity.ts +199 -199
  43. package/src/orm/execute.ts +13 -13
  44. package/src/orm/lazy-batch/morph-many.ts +70 -0
  45. package/src/orm/lazy-batch/morph-one.ts +69 -0
  46. package/src/orm/lazy-batch/morph-to.ts +59 -0
  47. package/src/orm/lazy-batch.ts +4 -1
  48. package/src/orm/orm-session.ts +170 -104
  49. package/src/orm/pooled-executor-factory.ts +99 -58
  50. package/src/orm/query-logger.ts +49 -40
  51. package/src/orm/relation-change-processor.ts +198 -96
  52. package/src/orm/relations/belongs-to.ts +143 -143
  53. package/src/orm/relations/has-many.ts +204 -204
  54. package/src/orm/relations/has-one.ts +174 -174
  55. package/src/orm/relations/many-to-many.ts +288 -288
  56. package/src/orm/relations/morph-many.ts +156 -0
  57. package/src/orm/relations/morph-one.ts +151 -0
  58. package/src/orm/relations/morph-to.ts +162 -0
  59. package/src/orm/save-graph.ts +116 -1
  60. package/src/query-builder/expression-table-mapper.ts +5 -0
  61. package/src/query-builder/hydration-manager.ts +345 -345
  62. package/src/query-builder/hydration-planner.ts +178 -148
  63. package/src/query-builder/relation-conditions.ts +171 -151
  64. package/src/query-builder/relation-cte-builder.ts +5 -1
  65. package/src/query-builder/relation-filter-utils.ts +9 -6
  66. package/src/query-builder/relation-include-strategies.ts +44 -2
  67. package/src/query-builder/relation-join-strategies.ts +8 -1
  68. package/src/query-builder/relation-service.ts +250 -241
  69. package/src/query-builder/select/cursor-pagination.ts +323 -0
  70. package/src/query-builder/select/select-operations.ts +110 -105
  71. package/src/query-builder/select.ts +42 -1
  72. package/src/query-builder/update-include.ts +4 -0
  73. package/src/schema/relation.ts +296 -188
  74. package/src/schema/types.ts +138 -123
  75. package/src/tree/tree-decorator.ts +127 -137
@@ -0,0 +1,95 @@
1
+ import { InsertQueryBuilder } from '../query-builder/insert.js';
2
+ import { findPrimaryKey } from '../query-builder/hydration-planner.js';
3
+ import type { TableDef } from '../schema/table.js';
4
+ import type { OrmSession } from '../orm/orm-session.js';
5
+ import type { BulkUpsertOptions, ChunkOutcome, InsertRow } from './bulk-types.js';
6
+ import type { UpsertClause } from '../core/ast/query.js';
7
+ import type { ValueOperandInput } from '../core/ast/expression.js';
8
+ import type { ColumnNode } from '../core/ast/expression.js';
9
+ import { BulkBaseExecutor, type BulkExecutorOptions } from './bulk-executor.base.js';
10
+ import { resolveReturningColumns, flattenQueryResults, executeCompiled, createBulkExecutionContext } from './bulk-context.js';
11
+ import { splitIntoChunks, runWithConcurrency, runChunk, maybeTransaction, aggregateOutcomes, aggregateOutcomesWithTimings } from './bulk-utils.js';
12
+
13
+ interface UpsertExecutorOptions extends BulkExecutorOptions {
14
+ conflictColumns?: string[];
15
+ updateColumns?: string[];
16
+ returning?: boolean | import('../schema/column-types.js').ColumnDef[];
17
+ }
18
+
19
+ const DEFAULT_CHUNK_SIZE = 500;
20
+
21
+ export class BulkUpsertExecutor extends BulkBaseExecutor<UpsertExecutorOptions> {
22
+ private readonly conflictTargetNodes: ColumnNode[];
23
+ private readonly updateColumns: string[];
24
+
25
+ constructor(
26
+ session: OrmSession,
27
+ table: TableDef,
28
+ rows: InsertRow[],
29
+ options: BulkUpsertOptions = {}
30
+ ) {
31
+ super(session, table, rows, { ...options, chunkSize: options.chunkSize ?? DEFAULT_CHUNK_SIZE });
32
+
33
+ const pkName = findPrimaryKey(table);
34
+ const conflictTargetNames = options.conflictColumns ?? [pkName];
35
+ this.conflictTargetNodes = conflictTargetNames.map(name => ({
36
+ type: 'Column' as const,
37
+ table: table.name,
38
+ name,
39
+ }));
40
+
41
+ const conflictSet = new Set(conflictTargetNames);
42
+ this.updateColumns =
43
+ options.updateColumns ??
44
+ Object.keys(rows[0] ?? {}).filter(col => !conflictSet.has(col) && col in table.columns);
45
+ }
46
+
47
+ protected async executeChunk(chunk: InsertRow[], chunkIndex: number): Promise<ChunkOutcome> {
48
+ const returningColumns = resolveReturningColumns(this.ctx, this.table, this.options.returning);
49
+
50
+ const set: Record<string, ValueOperandInput> = {};
51
+ for (const col of this.updateColumns) {
52
+ set[col] = { type: 'ExcludedColumn', name: col } as any;
53
+ }
54
+
55
+ let builder: InsertQueryBuilder<unknown>;
56
+
57
+ if (this.updateColumns.length === 0) {
58
+ builder = new InsertQueryBuilder(this.table).values(chunk).onConflict(this.conflictTargetNodes as any).doNothing();
59
+ } else {
60
+ builder = new InsertQueryBuilder(this.table)
61
+ .values(chunk)
62
+ .onConflict(this.conflictTargetNodes as any)
63
+ .doUpdate(set);
64
+ }
65
+
66
+ const finalBuilder = builder as InsertQueryBuilder<unknown>;
67
+
68
+ if (returningColumns?.length) {
69
+ finalBuilder.returning(...returningColumns);
70
+ }
71
+
72
+ const compiled = finalBuilder.compile(this.ctx.dialect);
73
+ const resultSets = await executeCompiled(this.ctx, compiled);
74
+
75
+ return {
76
+ processedRows: chunk.length,
77
+ returning: returningColumns ? flattenQueryResults(resultSets) : [],
78
+ elapsedMs: 0,
79
+ };
80
+ }
81
+ }
82
+
83
+ export async function bulkUpsert<TTable extends TableDef>(
84
+ session: OrmSession,
85
+ table: TTable,
86
+ rows: InsertRow[],
87
+ options: BulkUpsertOptions = {}
88
+ ): Promise<import('./bulk-types.js').BulkResult> {
89
+ if (!rows.length) {
90
+ return { processedRows: 0, chunksExecuted: 0, returning: [] };
91
+ }
92
+
93
+ const executor = new BulkUpsertExecutor(session, table, rows, options);
94
+ return executor.execute();
95
+ }
@@ -0,0 +1,91 @@
1
+ import type { BulkConcurrency, ChunkOutcome, ChunkCompleteInfo, BulkResult } from './bulk-types.js';
2
+
3
+ export function splitIntoChunks<T>(items: T[], size: number): T[][] {
4
+ if (size < 1) throw new RangeError(`chunkSize must be >= 1, got ${size}`);
5
+ const chunks: T[][] = [];
6
+ for (let i = 0; i < items.length; i += size) {
7
+ chunks.push(items.slice(i, i + size));
8
+ }
9
+ return chunks;
10
+ }
11
+
12
+ type Task<T> = () => Promise<T>;
13
+
14
+ export async function runWithConcurrency<T>(
15
+ tasks: Task<T>[],
16
+ concurrency: BulkConcurrency
17
+ ): Promise<T[]> {
18
+ const limit = concurrency === 'sequential' ? 1 : Math.max(1, concurrency);
19
+
20
+ if (limit === 1) {
21
+ const results: T[] = [];
22
+ for (const task of tasks) {
23
+ results.push(await task());
24
+ }
25
+ return results;
26
+ }
27
+
28
+ const results: T[] = new Array(tasks.length);
29
+ let nextIndex = 0;
30
+
31
+ const worker = async (): Promise<void> => {
32
+ while (nextIndex < tasks.length) {
33
+ const i = nextIndex++;
34
+ results[i] = await tasks[i]();
35
+ }
36
+ };
37
+
38
+ const workers = Array.from({ length: Math.min(limit, tasks.length) }, worker);
39
+ await Promise.all(workers);
40
+ return results;
41
+ }
42
+
43
+ export async function runChunk<T>(
44
+ task: Task<T>,
45
+ chunkIndex: number,
46
+ totalChunks: number,
47
+ rowsInChunk: number,
48
+ timing: boolean,
49
+ onChunkComplete?: (info: ChunkCompleteInfo) => void | Promise<void>
50
+ ): Promise<T> {
51
+ const start = timing || onChunkComplete ? Date.now() : 0;
52
+ const result = await task();
53
+ const elapsedMs = start ? Date.now() - start : 0;
54
+
55
+ if (onChunkComplete) {
56
+ await onChunkComplete({ chunkIndex, totalChunks, rowsInChunk, elapsedMs });
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ export async function maybeTransaction<T>(
63
+ session: unknown,
64
+ transactional: boolean,
65
+ fn: () => Promise<T>
66
+ ): Promise<T> {
67
+ if (!transactional) return fn();
68
+ const ormSession = session as { transaction: (fn: () => Promise<T>) => Promise<T> };
69
+ return ormSession.transaction(fn);
70
+ }
71
+
72
+ export function aggregateOutcomes(outcomes: ChunkOutcome[]): BulkResult {
73
+ const result: BulkResult = {
74
+ processedRows: 0,
75
+ chunksExecuted: outcomes.length,
76
+ returning: [],
77
+ };
78
+
79
+ for (const o of outcomes) {
80
+ result.processedRows += o.processedRows;
81
+ result.returning.push(...o.returning);
82
+ }
83
+
84
+ return result;
85
+ }
86
+
87
+ export function aggregateOutcomesWithTimings(outcomes: ChunkOutcome[]): BulkResult {
88
+ const result = aggregateOutcomes(outcomes);
89
+ result.chunkTimings = outcomes.map(o => o.elapsedMs);
90
+ return result;
91
+ }
@@ -0,0 +1,18 @@
1
+ export { bulkInsert, BulkInsertExecutor } from './bulk-insert-executor.js';
2
+ export { bulkUpdate, bulkUpdateWhere, BulkUpdateExecutor } from './bulk-update-executor.js';
3
+ export { bulkDelete, bulkDeleteWhere, BulkDeleteExecutor } from './bulk-delete-executor.js';
4
+ export { bulkUpsert, BulkUpsertExecutor } from './bulk-upsert-executor.js';
5
+
6
+ export type {
7
+ BulkResult,
8
+ BulkBaseOptions,
9
+ BulkConcurrency,
10
+ BulkInsertOptions,
11
+ BulkUpsertOptions,
12
+ BulkUpdateOptions,
13
+ BulkDeleteOptions,
14
+ InsertRow,
15
+ UpdateRow,
16
+ ChunkCompleteInfo,
17
+ ChunkOutcome,
18
+ } from './bulk-types.js';
@@ -2,9 +2,10 @@ import { OrderingTerm, SelectQueryNode } from '../core/ast/query.js';
2
2
  import {
3
3
  ExpressionNode,
4
4
  OperandNode,
5
- BinaryExpressionNode,
6
- LogicalExpressionNode,
7
- InExpressionNode,
5
+ BinaryExpressionNode,
6
+ LogicalExpressionNode,
7
+ NotExpressionNode,
8
+ InExpressionNode,
8
9
  NullExpressionNode,
9
10
  JsonPathNode,
10
11
  ExistsExpressionNode,
@@ -216,13 +217,17 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
216
217
  return this.printBinaryExpression(binary);
217
218
  }
218
219
 
219
- public visitLogicalExpression(logical: LogicalExpressionNode): string {
220
- return this.printLogicalExpression(logical);
221
- }
222
-
223
- public visitNullExpression(nullExpr: NullExpressionNode): string {
224
- return this.printNullExpression(nullExpr);
225
- }
220
+ public visitLogicalExpression(logical: LogicalExpressionNode): string {
221
+ return this.printLogicalExpression(logical);
222
+ }
223
+
224
+ public visitNotExpression(notExpr: NotExpressionNode): string {
225
+ return this.printNotExpression(notExpr);
226
+ }
227
+
228
+ public visitNullExpression(nullExpr: NullExpressionNode): string {
229
+ return this.printNullExpression(nullExpr);
230
+ }
226
231
 
227
232
  public visitInExpression(inExpr: InExpressionNode): string {
228
233
  return this.printInExpression(inExpr);
@@ -301,17 +306,21 @@ export class TypeScriptGenerator implements ExpressionVisitor<string>, OperandVi
301
306
  * @param logical - Logical expression node
302
307
  * @returns TypeScript code representation
303
308
  */
304
- private printLogicalExpression(logical: LogicalExpressionNode): string {
305
- if (logical.operands.length === 0) return '';
306
- const parts = logical.operands.map(op => {
307
- const compiled = this.printExpression(op);
308
- return op.type === 'LogicalExpression' ? `(${compiled})` : compiled;
309
- });
310
- return `${this.mapOp(logical.operator)}(\n ${parts.join(',\n ')}\n )`;
311
- }
312
-
313
- private printArithmeticExpression(expr: ArithmeticExpressionNode): string {
314
- const left = this.printOperand(expr.left);
309
+ private printLogicalExpression(logical: LogicalExpressionNode): string {
310
+ if (logical.operands.length === 0) return '';
311
+ const parts = logical.operands.map(op => {
312
+ const compiled = this.printExpression(op);
313
+ return op.type === 'LogicalExpression' ? `(${compiled})` : compiled;
314
+ });
315
+ return `${this.mapOp(logical.operator)}(\n ${parts.join(',\n ')}\n )`;
316
+ }
317
+
318
+ private printNotExpression(notExpr: NotExpressionNode): string {
319
+ return `not(${this.printExpression(notExpr.operand)})`;
320
+ }
321
+
322
+ private printArithmeticExpression(expr: ArithmeticExpressionNode): string {
323
+ const left = this.printOperand(expr.left);
315
324
  const right = this.printOperand(expr.right);
316
325
  return `${left} ${expr.operator} ${right}`;
317
326
  }
@@ -9,10 +9,11 @@ import {
9
9
  CaseExpressionNode,
10
10
  CastExpressionNode,
11
11
  BinaryExpressionNode,
12
- ExpressionNode,
13
- LogicalExpressionNode,
14
- NullExpressionNode,
15
- InExpressionNode,
12
+ ExpressionNode,
13
+ LogicalExpressionNode,
14
+ NotExpressionNode,
15
+ NullExpressionNode,
16
+ InExpressionNode,
16
17
  ExistsExpressionNode,
17
18
  InExpressionRight,
18
19
  ScalarSubqueryNode,
@@ -21,7 +22,8 @@ import {
21
22
  AliasRefNode,
22
23
  ArithmeticExpressionNode,
23
24
  BitwiseExpressionNode,
24
- CollateExpressionNode
25
+ CollateExpressionNode,
26
+ IsDistinctExpressionNode
25
27
  } from './expression-nodes.js';
26
28
 
27
29
  export type LiteralValue = LiteralNode['value'];
@@ -341,11 +343,28 @@ export const and = (...operands: ExpressionNode[]): LogicalExpressionNode => ({
341
343
  * eq(users.role, 'moderator')
342
344
  * );
343
345
  */
344
- export const or = (...operands: ExpressionNode[]): LogicalExpressionNode => ({
345
- type: 'LogicalExpression',
346
- operator: 'OR',
347
- operands
348
- });
346
+ export const or = (...operands: ExpressionNode[]): LogicalExpressionNode => ({
347
+ type: 'LogicalExpression',
348
+ operator: 'OR',
349
+ operands
350
+ });
351
+
352
+ /**
353
+ * Creates a unary NOT expression (`NOT (expr)`).
354
+ *
355
+ * @param operand - Expression to negate.
356
+ * @returns A `NotExpressionNode`.
357
+ *
358
+ * @example
359
+ * not(or(
360
+ * eq(users.status, 'inactive'),
361
+ * eq(users.role, 'guest')
362
+ * ));
363
+ */
364
+ export const not = (operand: ExpressionNode): NotExpressionNode => ({
365
+ type: 'NotExpression',
366
+ operand
367
+ });
349
368
 
350
369
  /**
351
370
  * Creates an IS NULL check (`left IS NULL`).
@@ -735,3 +754,81 @@ export const collate = (
735
754
  expression: toOperand(expression),
736
755
  collation
737
756
  });
757
+
758
+ /**
759
+ * Creates an `IS DISTINCT FROM` expression.
760
+ *
761
+ * Unlike `neq(a, b)`, this comparison is **null-safe**:
762
+ * returns `true` when values are different **including** when one
763
+ * (or both) is `NULL`. Never returns `NULL`.
764
+ *
765
+ * | left | right | neq | isDistinctFrom |
766
+ * |-------|-------|-------|----------------|
767
+ * | 1 | 2 | true | true |
768
+ * | 1 | 1 | false | false |
769
+ * | NULL | 1 | NULL | **true** |
770
+ * | NULL | NULL | NULL | **false** |
771
+ *
772
+ * Compilation by dialect:
773
+ * - PostgreSQL / SQLite / MSSQL → `left IS DISTINCT FROM right`
774
+ * - MySQL → `NOT (left <=> right)`
775
+ *
776
+ * @param left - The left operand.
777
+ * @param right - The right operand.
778
+ * @returns An `IsDistinctExpressionNode`.
779
+ *
780
+ * @example
781
+ * ```ts
782
+ * // Find users whose email has changed compared to backup
783
+ * where(isDistinctFrom(users.columns.email, backup.columns.email))
784
+ * ```
785
+ */
786
+ export const isDistinctFrom = (
787
+ left: OperandNode | ColumnRef,
788
+ right: OperandNode | ColumnRef | LiteralValue
789
+ ): IsDistinctExpressionNode => ({
790
+ type: 'IsDistinctExpression',
791
+ left: toOperandNode(left),
792
+ operator: 'IS DISTINCT FROM',
793
+ right: toOperand(right),
794
+ });
795
+
796
+ /**
797
+ * Creates an `IS NOT DISTINCT FROM` expression.
798
+ *
799
+ * The inverse of `isDistinctFrom`. Equivalent to a **null-safe** equality:
800
+ * returns `true` when values are equal **including** when both
801
+ * are `NULL`. Never returns `NULL`.
802
+ *
803
+ * | left | right | eq | isNotDistinctFrom |
804
+ * |-------|-------|-------|------------------|
805
+ * | 1 | 1 | true | true |
806
+ * | 1 | 2 | false | false |
807
+ * | NULL | 1 | NULL | **false** |
808
+ * | NULL | NULL | NULL | **true** |
809
+ *
810
+ * Compilation by dialect:
811
+ * - PostgreSQL / SQLite / MSSQL → `left IS NOT DISTINCT FROM right`
812
+ * - MySQL → `left <=> right`
813
+ *
814
+ * @param left - The left operand.
815
+ * @param right - The right operand.
816
+ * @returns An `IsDistinctExpressionNode`.
817
+ *
818
+ * @example
819
+ * ```ts
820
+ * // Rows where deletedAt is NULL or equal to a specific date
821
+ * where(isNotDistinctFrom(orders.columns.deletedAt, null))
822
+ * // → WHERE "orders"."deletedAt" IS NOT DISTINCT FROM NULL
823
+ * // equivalent to: WHERE deletedAt IS NULL (but works with any value)
824
+ * ```
825
+ */
826
+ export const isNotDistinctFrom = (
827
+ left: OperandNode | ColumnRef,
828
+ right: OperandNode | ColumnRef | LiteralValue
829
+ ): IsDistinctExpressionNode => ({
830
+ type: 'IsDistinctExpression',
831
+ left: toOperandNode(left),
832
+ operator: 'IS NOT DISTINCT FROM',
833
+ right: toOperand(right),
834
+ });
@@ -233,19 +233,28 @@ export interface BitwiseExpressionNode {
233
233
  /**
234
234
  * AST node representing a logical expression (AND/OR)
235
235
  */
236
- export interface LogicalExpressionNode {
237
- type: 'LogicalExpression';
238
- /** Logical operator (AND or OR) */
239
- operator: 'AND' | 'OR';
240
- /** Operands to combine */
241
- operands: ExpressionNode[];
242
- }
243
-
244
- /**
245
- * AST node representing a null check expression
246
- */
247
- export interface NullExpressionNode {
248
- type: 'NullExpression';
236
+ export interface LogicalExpressionNode {
237
+ type: 'LogicalExpression';
238
+ /** Logical operator (AND or OR) */
239
+ operator: 'AND' | 'OR';
240
+ /** Operands to combine */
241
+ operands: ExpressionNode[];
242
+ }
243
+
244
+ /**
245
+ * AST node representing a unary NOT expression
246
+ */
247
+ export interface NotExpressionNode {
248
+ type: 'NotExpression';
249
+ /** Expression to negate */
250
+ operand: ExpressionNode;
251
+ }
252
+
253
+ /**
254
+ * AST node representing a null check expression
255
+ */
256
+ export interface NullExpressionNode {
257
+ type: 'NullExpression';
249
258
  /** Operand to check for null */
250
259
  left: OperandNode;
251
260
  /** Null check operator */
@@ -291,15 +300,36 @@ export interface BetweenExpressionNode {
291
300
  upper: OperandNode;
292
301
  }
293
302
 
303
+ /**
304
+ * AST node representing an IS DISTINCT FROM / IS NOT DISTINCT FROM expression
305
+ */
306
+ export interface IsDistinctExpressionNode {
307
+ type: 'IsDistinctExpression';
308
+
309
+ /** The operand on the left side. */
310
+ left: OperandNode;
311
+
312
+ /**
313
+ * `'IS DISTINCT FROM'` → true when values are different, even if one is NULL.
314
+ * `'IS NOT DISTINCT FROM'` → true when values are equal, treating NULL = NULL.
315
+ */
316
+ operator: 'IS DISTINCT FROM' | 'IS NOT DISTINCT FROM';
317
+
318
+ /** The operand on the right side. */
319
+ right: OperandNode;
320
+ }
321
+
294
322
  /**
295
323
  * Union type representing any supported expression node
296
324
  */
297
- export type ExpressionNode =
298
- | BinaryExpressionNode
299
- | LogicalExpressionNode
300
- | NullExpressionNode
301
- | InExpressionNode
302
- | ExistsExpressionNode
303
- | BetweenExpressionNode
304
- | ArithmeticExpressionNode
305
- | BitwiseExpressionNode;
325
+ export type ExpressionNode =
326
+ | BinaryExpressionNode
327
+ | LogicalExpressionNode
328
+ | NotExpressionNode
329
+ | NullExpressionNode
330
+ | InExpressionNode
331
+ | ExistsExpressionNode
332
+ | BetweenExpressionNode
333
+ | ArithmeticExpressionNode
334
+ | BitwiseExpressionNode
335
+ | IsDistinctExpressionNode;
@@ -1,7 +1,8 @@
1
1
  import {
2
- BinaryExpressionNode,
3
- LogicalExpressionNode,
4
- NullExpressionNode,
2
+ BinaryExpressionNode,
3
+ LogicalExpressionNode,
4
+ NotExpressionNode,
5
+ NullExpressionNode,
5
6
  InExpressionNode,
6
7
  ExistsExpressionNode,
7
8
  BetweenExpressionNode,
@@ -18,21 +19,24 @@ import {
18
19
  WindowFunctionNode,
19
20
  CollateExpressionNode,
20
21
  AliasRefNode,
21
- BitwiseExpressionNode
22
+ BitwiseExpressionNode,
23
+ IsDistinctExpressionNode
22
24
  } from './expression-nodes.js';
23
25
 
24
26
  /**
25
27
  * Visitor for expression nodes
26
28
  */
27
29
  export interface ExpressionVisitor<R> {
28
- visitBinaryExpression?(node: BinaryExpressionNode): R;
29
- visitLogicalExpression?(node: LogicalExpressionNode): R;
30
- visitNullExpression?(node: NullExpressionNode): R;
30
+ visitBinaryExpression?(node: BinaryExpressionNode): R;
31
+ visitLogicalExpression?(node: LogicalExpressionNode): R;
32
+ visitNotExpression?(node: NotExpressionNode): R;
33
+ visitNullExpression?(node: NullExpressionNode): R;
31
34
  visitInExpression?(node: InExpressionNode): R;
32
35
  visitExistsExpression?(node: ExistsExpressionNode): R;
33
36
  visitBetweenExpression?(node: BetweenExpressionNode): R;
34
37
  visitArithmeticExpression?(node: ArithmeticExpressionNode): R;
35
38
  visitBitwiseExpression?(node: BitwiseExpressionNode): R;
39
+ visitIsDistinctExpression?(node: IsDistinctExpressionNode): R;
36
40
  otherwise?(node: ExpressionNode): R;
37
41
  }
38
42
 
@@ -143,12 +147,15 @@ export const visitExpression = <R>(node: ExpressionNode, visitor: ExpressionVisi
143
147
  case 'BinaryExpression':
144
148
  if (visitor.visitBinaryExpression) return visitor.visitBinaryExpression(node);
145
149
  break;
146
- case 'LogicalExpression':
147
- if (visitor.visitLogicalExpression) return visitor.visitLogicalExpression(node);
148
- break;
149
- case 'NullExpression':
150
- if (visitor.visitNullExpression) return visitor.visitNullExpression(node);
151
- break;
150
+ case 'LogicalExpression':
151
+ if (visitor.visitLogicalExpression) return visitor.visitLogicalExpression(node);
152
+ break;
153
+ case 'NotExpression':
154
+ if (visitor.visitNotExpression) return visitor.visitNotExpression(node);
155
+ break;
156
+ case 'NullExpression':
157
+ if (visitor.visitNullExpression) return visitor.visitNullExpression(node);
158
+ break;
152
159
  case 'InExpression':
153
160
  if (visitor.visitInExpression) return visitor.visitInExpression(node);
154
161
  break;
@@ -164,6 +171,9 @@ export const visitExpression = <R>(node: ExpressionNode, visitor: ExpressionVisi
164
171
  case 'BitwiseExpression':
165
172
  if (visitor.visitBitwiseExpression) return visitor.visitBitwiseExpression(node);
166
173
  break;
174
+ case 'IsDistinctExpression':
175
+ if (visitor.visitIsDistinctExpression) return visitor.visitIsDistinctExpression(node);
176
+ break;
167
177
  default:
168
178
  break;
169
179
  }
@@ -8,10 +8,11 @@ import {
8
8
  OrderingTerm
9
9
  } from '../ast/query.js';
10
10
  import {
11
- ExpressionNode,
12
- BinaryExpressionNode,
13
- LogicalExpressionNode,
14
- NullExpressionNode,
11
+ ExpressionNode,
12
+ BinaryExpressionNode,
13
+ LogicalExpressionNode,
14
+ NotExpressionNode,
15
+ NullExpressionNode,
15
16
  InExpressionNode,
16
17
  ExistsExpressionNode,
17
18
  LiteralNode,
@@ -28,6 +29,7 @@ import {
28
29
  BitwiseExpressionNode,
29
30
  CollateExpressionNode,
30
31
  AliasRefNode,
32
+ IsDistinctExpressionNode,
31
33
  isOperandNode
32
34
  } from '../ast/expression.js';
33
35
  import { ProcedureCallNode } from '../ast/procedure.js';
@@ -416,19 +418,24 @@ export abstract class Dialect
416
418
  return base;
417
419
  });
418
420
 
419
- this.registerExpressionCompiler('LogicalExpression', (logical: LogicalExpressionNode, ctx) => {
420
- if (logical.operands.length === 0) return '';
421
- const parts = logical.operands.map(op => {
422
- const compiled = this.compileExpression(op, ctx);
423
- return op.type === 'LogicalExpression' ? `(${compiled})` : compiled;
424
- });
425
- return parts.join(` ${logical.operator} `);
426
- });
427
-
428
- this.registerExpressionCompiler('NullExpression', (nullExpr: NullExpressionNode, ctx) => {
429
- const left = this.compileOperand(nullExpr.left, ctx);
430
- return `${left} ${nullExpr.operator}`;
431
- });
421
+ this.registerExpressionCompiler('LogicalExpression', (logical: LogicalExpressionNode, ctx) => {
422
+ if (logical.operands.length === 0) return '';
423
+ const parts = logical.operands.map(op => {
424
+ const compiled = this.compileExpression(op, ctx);
425
+ return op.type === 'LogicalExpression' ? `(${compiled})` : compiled;
426
+ });
427
+ return parts.join(` ${logical.operator} `);
428
+ });
429
+
430
+ this.registerExpressionCompiler('NotExpression', (notExpr: NotExpressionNode, ctx) => {
431
+ const operand = this.compileExpression(notExpr.operand, ctx);
432
+ return `NOT (${operand})`;
433
+ });
434
+
435
+ this.registerExpressionCompiler('NullExpression', (nullExpr: NullExpressionNode, ctx) => {
436
+ const left = this.compileOperand(nullExpr.left, ctx);
437
+ return `${left} ${nullExpr.operator}`;
438
+ });
432
439
 
433
440
  this.registerExpressionCompiler('InExpression', (inExpr: InExpressionNode, ctx) => {
434
441
  const left = this.compileOperand(inExpr.left, ctx);
@@ -463,6 +470,12 @@ export abstract class Dialect
463
470
  const right = this.compileOperand(bitwise.right, ctx);
464
471
  return `${left} ${bitwise.operator} ${right}`;
465
472
  });
473
+
474
+ this.registerExpressionCompiler('IsDistinctExpression', (node: IsDistinctExpressionNode, ctx) => {
475
+ const left = this.compileOperand(node.left, ctx);
476
+ const right = this.compileOperand(node.right, ctx);
477
+ return `${left} ${node.operator} ${right}`;
478
+ });
466
479
  }
467
480
 
468
481
  private registerDefaultOperandCompilers(): void {
@@ -1,8 +1,8 @@
1
- import { CompilerContext, CompiledProcedureCall } from '../abstract.js';
2
- import { JsonPathNode } from '../../ast/expression.js';
3
- import { InsertQueryNode } from '../../ast/query.js';
4
- import { SqlDialectBase } from '../base/sql-dialect.js';
5
- import { MysqlFunctionStrategy } from './functions.js';
1
+ import { CompilerContext, CompiledProcedureCall } from '../abstract.js';
2
+ import { JsonPathNode, IsDistinctExpressionNode } from '../../ast/expression.js';
3
+ import { InsertQueryNode } from '../../ast/query.js';
4
+ import { SqlDialectBase } from '../base/sql-dialect.js';
5
+ import { MysqlFunctionStrategy } from './functions.js';
6
6
  import { ProcedureCallNode } from '../../ast/procedure.js';
7
7
 
8
8
  const sanitizeVariableSuffix = (value: string): string =>
@@ -18,6 +18,21 @@ export class MySqlDialect extends SqlDialectBase {
18
18
  */
19
19
  public constructor() {
20
20
  super(new MysqlFunctionStrategy());
21
+
22
+ this.registerExpressionCompiler(
23
+ 'IsDistinctExpression',
24
+ (node: IsDistinctExpressionNode, ctx: CompilerContext): string => {
25
+ const left = this.compileOperand(node.left, ctx);
26
+ const right = this.compileOperand(node.right, ctx);
27
+ const spaceship = `${left} <=> ${right}`;
28
+
29
+ if (node.operator === 'IS NOT DISTINCT FROM') {
30
+ return spaceship;
31
+ }
32
+
33
+ return `NOT (${spaceship})`;
34
+ }
35
+ );
21
36
  }
22
37
 
23
38
  /**