metal-orm 1.1.9 → 1.1.11

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 (77) hide show
  1. package/README.md +769 -764
  2. package/dist/index.cjs +2255 -284
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +559 -39
  5. package/dist/index.d.ts +559 -39
  6. package/dist/index.js +2227 -284
  7. package/dist/index.js.map +1 -1
  8. package/package.json +17 -12
  9. package/scripts/generate-entities/render.mjs +21 -12
  10. package/scripts/generate-entities/schema.mjs +87 -73
  11. package/scripts/generate-entities/tree-detection.mjs +67 -61
  12. package/src/bulk/bulk-context.ts +83 -0
  13. package/src/bulk/bulk-delete-executor.ts +87 -0
  14. package/src/bulk/bulk-executor.base.ts +73 -0
  15. package/src/bulk/bulk-insert-executor.ts +74 -0
  16. package/src/bulk/bulk-types.ts +70 -0
  17. package/src/bulk/bulk-update-executor.ts +192 -0
  18. package/src/bulk/bulk-upsert-executor.ts +93 -0
  19. package/src/bulk/bulk-utils.ts +91 -0
  20. package/src/bulk/index.ts +18 -0
  21. package/src/codegen/typescript.ts +30 -21
  22. package/src/core/ast/expression-builders.ts +107 -10
  23. package/src/core/ast/expression-nodes.ts +52 -22
  24. package/src/core/ast/expression-visitor.ts +23 -13
  25. package/src/core/ddl/introspect/mysql.ts +113 -36
  26. package/src/core/dialect/abstract.ts +30 -17
  27. package/src/core/dialect/mysql/index.ts +20 -5
  28. package/src/core/execution/db-executor.ts +96 -64
  29. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
  30. package/src/core/execution/executors/mssql-executor.ts +66 -34
  31. package/src/core/execution/executors/mysql-executor.ts +98 -66
  32. package/src/core/execution/executors/postgres-executor.ts +33 -11
  33. package/src/core/execution/executors/sqlite-executor.ts +86 -30
  34. package/src/decorators/bootstrap.ts +482 -398
  35. package/src/decorators/column-decorator.ts +87 -96
  36. package/src/decorators/decorator-metadata.ts +100 -24
  37. package/src/decorators/entity.ts +27 -24
  38. package/src/decorators/relations.ts +231 -149
  39. package/src/decorators/transformers/transformer-decorators.ts +26 -29
  40. package/src/decorators/validators/country-validators-decorators.ts +9 -15
  41. package/src/dto/apply-filter.ts +568 -551
  42. package/src/index.ts +16 -9
  43. package/src/orm/entity-hydration.ts +116 -72
  44. package/src/orm/entity-metadata.ts +347 -301
  45. package/src/orm/entity-relations.ts +264 -207
  46. package/src/orm/entity.ts +199 -199
  47. package/src/orm/execute.ts +13 -13
  48. package/src/orm/lazy-batch/morph-many.ts +70 -0
  49. package/src/orm/lazy-batch/morph-one.ts +69 -0
  50. package/src/orm/lazy-batch/morph-to.ts +59 -0
  51. package/src/orm/lazy-batch.ts +4 -1
  52. package/src/orm/orm-session.ts +170 -104
  53. package/src/orm/pooled-executor-factory.ts +99 -58
  54. package/src/orm/query-logger.ts +49 -40
  55. package/src/orm/relation-change-processor.ts +198 -96
  56. package/src/orm/relations/belongs-to.ts +143 -143
  57. package/src/orm/relations/has-many.ts +204 -204
  58. package/src/orm/relations/has-one.ts +174 -174
  59. package/src/orm/relations/many-to-many.ts +288 -288
  60. package/src/orm/relations/morph-many.ts +156 -0
  61. package/src/orm/relations/morph-one.ts +151 -0
  62. package/src/orm/relations/morph-to.ts +162 -0
  63. package/src/orm/save-graph.ts +116 -1
  64. package/src/query-builder/expression-table-mapper.ts +5 -0
  65. package/src/query-builder/hydration-manager.ts +345 -345
  66. package/src/query-builder/hydration-planner.ts +178 -148
  67. package/src/query-builder/relation-conditions.ts +171 -151
  68. package/src/query-builder/relation-cte-builder.ts +5 -1
  69. package/src/query-builder/relation-filter-utils.ts +9 -6
  70. package/src/query-builder/relation-include-strategies.ts +44 -2
  71. package/src/query-builder/relation-join-strategies.ts +8 -1
  72. package/src/query-builder/relation-service.ts +250 -241
  73. package/src/query-builder/select/select-operations.ts +110 -105
  74. package/src/query-builder/update-include.ts +4 -0
  75. package/src/schema/relation.ts +296 -188
  76. package/src/schema/types.ts +138 -123
  77. package/src/tree/tree-decorator.ts +127 -137
@@ -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
  }
@@ -98,6 +98,31 @@ const databaseFunction: FunctionNode = {
98
98
  args: []
99
99
  };
100
100
 
101
+ const readMysqlField = (row: object, field: string): unknown => {
102
+ const record = row as Record<string, unknown>;
103
+
104
+ if (Object.prototype.hasOwnProperty.call(record, field)) {
105
+ return record[field];
106
+ }
107
+
108
+ const lower = field.toLowerCase();
109
+ if (lower !== field && Object.prototype.hasOwnProperty.call(record, lower)) {
110
+ return record[lower];
111
+ }
112
+
113
+ const upper = field.toUpperCase();
114
+ if (upper !== field && Object.prototype.hasOwnProperty.call(record, upper)) {
115
+ return record[upper];
116
+ }
117
+
118
+ return undefined;
119
+ };
120
+
121
+ const readMysqlStringField = (row: object, field: string): string | undefined => {
122
+ const value = readMysqlField(row, field);
123
+ return typeof value === 'string' ? value : undefined;
124
+ };
125
+
101
126
  /**
102
127
  * Schema introspector for MySQL.
103
128
  * Queries information_schema tables to extract schema metadata.
@@ -254,30 +279,58 @@ export const mysqlIntrospector: SchemaIntrospector = {
254
279
 
255
280
  const tableComments = new Map<string, string>();
256
281
  tableRows.forEach(r => {
257
- const key = `${r.table_schema}.${r.table_name}`;
258
- if (r.table_comment) {
259
- tableComments.set(key, r.table_comment);
282
+ const tableSchema = readMysqlStringField(r, 'table_schema');
283
+ const tableName = readMysqlStringField(r, 'table_name');
284
+ const tableComment = readMysqlStringField(r, 'table_comment');
285
+ if (!tableSchema || !tableName) return;
286
+ const key = `${tableSchema}.${tableName}`;
287
+ if (tableComment) {
288
+ tableComments.set(key, tableComment);
260
289
  }
261
290
  });
262
291
 
263
292
  const pkMap = new Map<string, string[]>();
264
293
  pkRows.forEach(r => {
265
- const key = `${r.table_schema}.${r.table_name}`;
294
+ const tableSchema = readMysqlStringField(r, 'table_schema');
295
+ const tableName = readMysqlStringField(r, 'table_name');
296
+ const columnName = readMysqlStringField(r, 'column_name');
297
+ if (!tableSchema || !tableName || !columnName) return;
298
+ const key = `${tableSchema}.${tableName}`;
266
299
  const list = pkMap.get(key) || [];
267
- list.push(r.column_name);
300
+ list.push(columnName);
268
301
  pkMap.set(key, list);
269
302
  });
270
303
 
271
304
  const fkMap = new Map<string, MysqlForeignKeyEntry[]>();
272
305
  fkRows.forEach(r => {
273
- const key = `${r.table_schema}.${r.table_name}.${r.column_name}`;
306
+ const tableSchema = readMysqlStringField(r, 'table_schema');
307
+ const tableName = readMysqlStringField(r, 'table_name');
308
+ const columnName = readMysqlStringField(r, 'column_name');
309
+ const constraintName = readMysqlStringField(r, 'constraint_name');
310
+ const referencedTableSchema = readMysqlStringField(r, 'referenced_table_schema');
311
+ const referencedTableName = readMysqlStringField(r, 'referenced_table_name');
312
+ const referencedColumnName = readMysqlStringField(r, 'referenced_column_name');
313
+ const deleteRule = readMysqlStringField(r, 'delete_rule');
314
+ const updateRule = readMysqlStringField(r, 'update_rule');
315
+ if (
316
+ !tableSchema ||
317
+ !tableName ||
318
+ !columnName ||
319
+ !constraintName ||
320
+ !referencedTableSchema ||
321
+ !referencedTableName ||
322
+ !referencedColumnName
323
+ ) {
324
+ return;
325
+ }
326
+ const key = `${tableSchema}.${tableName}.${columnName}`;
274
327
  const list = fkMap.get(key) || [];
275
328
  list.push({
276
- table: `${r.referenced_table_schema}.${r.referenced_table_name}`,
277
- column: r.referenced_column_name,
278
- onDelete: r.delete_rule,
279
- onUpdate: r.update_rule,
280
- name: r.constraint_name
329
+ table: `${referencedTableSchema}.${referencedTableName}`,
330
+ column: referencedColumnName,
331
+ onDelete: deleteRule,
332
+ onUpdate: updateRule,
333
+ name: constraintName
281
334
  });
282
335
  fkMap.set(key, list);
283
336
  });
@@ -285,12 +338,21 @@ export const mysqlIntrospector: SchemaIntrospector = {
285
338
  const tablesByKey = new Map<string, DatabaseTable>();
286
339
 
287
340
  columnRows.forEach(r => {
288
- const key = `${r.table_schema}.${r.table_name}`;
289
- if (!shouldIncludeTable(r.table_name, options)) return;
341
+ const tableSchema = readMysqlStringField(r, 'table_schema');
342
+ const tableName = readMysqlStringField(r, 'table_name');
343
+ const columnName = readMysqlStringField(r, 'column_name');
344
+ const columnType = readMysqlStringField(r, 'column_type') || readMysqlStringField(r, 'data_type');
345
+ const isNullable = readMysqlStringField(r, 'is_nullable');
346
+ const columnDefault = readMysqlField(r, 'column_default');
347
+ const extra = readMysqlStringField(r, 'extra');
348
+ const columnComment = readMysqlStringField(r, 'column_comment');
349
+ if (!tableSchema || !tableName || !columnName || !columnType || !isNullable) return;
350
+ const key = `${tableSchema}.${tableName}`;
351
+ if (!shouldIncludeTable(tableName, options)) return;
290
352
  if (!tablesByKey.has(key)) {
291
353
  tablesByKey.set(key, {
292
- name: r.table_name,
293
- schema: r.table_schema,
354
+ name: tableName,
355
+ schema: tableSchema,
294
356
  columns: [],
295
357
  primaryKey: pkMap.get(key) || [],
296
358
  indexes: [],
@@ -298,17 +360,16 @@ export const mysqlIntrospector: SchemaIntrospector = {
298
360
  });
299
361
  }
300
362
  const table = tablesByKey.get(key)!;
301
- const columnType = r.column_type || r.data_type;
302
- const comment = r.column_comment?.trim() ? r.column_comment : undefined;
363
+ const comment = columnComment?.trim() ? columnComment : undefined;
303
364
  const column: DatabaseColumn = {
304
- name: r.column_name,
365
+ name: columnName,
305
366
  type: columnType,
306
- notNull: r.is_nullable === 'NO',
307
- default: r.column_default ?? undefined,
308
- autoIncrement: typeof r.extra === 'string' && r.extra.includes('auto_increment'),
367
+ notNull: isNullable === 'NO',
368
+ default: columnDefault ?? undefined,
369
+ autoIncrement: typeof extra === 'string' && extra.includes('auto_increment'),
309
370
  comment
310
371
  };
311
- const fk = fkMap.get(`${key}.${r.column_name}`)?.[0];
372
+ const fk = fkMap.get(`${key}.${columnName}`)?.[0];
312
373
  if (fk) {
313
374
  column.references = {
314
375
  table: fk.table,
@@ -322,14 +383,20 @@ export const mysqlIntrospector: SchemaIntrospector = {
322
383
  });
323
384
 
324
385
  indexRows.forEach(r => {
325
- const key = `${r.table_schema}.${r.table_name}`;
386
+ const tableSchema = readMysqlStringField(r, 'table_schema');
387
+ const tableName = readMysqlStringField(r, 'table_name');
388
+ const indexName = readMysqlStringField(r, 'index_name');
389
+ const nonUnique = readMysqlField(r, 'non_unique');
390
+ const colsValue = readMysqlField(r, 'cols');
391
+ if (!tableSchema || !tableName || !indexName) return;
392
+ const key = `${tableSchema}.${tableName}`;
326
393
  const table = tablesByKey.get(key);
327
394
  if (!table) return;
328
- const cols = (typeof r.cols === 'string' ? r.cols.split(',') : []).map(c => ({ column: c.trim() }));
395
+ const cols = (typeof colsValue === 'string' ? colsValue.split(',') : []).map(c => ({ column: c.trim() }));
329
396
  const idx: DatabaseIndex = {
330
- name: r.index_name,
397
+ name: indexName,
331
398
  columns: cols,
332
- unique: r.non_unique === 0
399
+ unique: Number(nonUnique) === 0
333
400
  };
334
401
  table.indexes = table.indexes || [];
335
402
  table.indexes.push(idx);
@@ -409,26 +476,36 @@ export const mysqlIntrospector: SchemaIntrospector = {
409
476
  const viewsByKey = new Map<string, DatabaseView>();
410
477
 
411
478
  for (const r of viewRows) {
412
- if (!shouldIncludeView(r.table_name, options)) continue;
413
- const key = `${r.table_schema}.${r.table_name}`;
479
+ const tableSchema = readMysqlStringField(r, 'table_schema');
480
+ const tableName = readMysqlStringField(r, 'table_name');
481
+ const viewDefinition = readMysqlStringField(r, 'view_definition');
482
+ if (!tableSchema || !tableName) continue;
483
+ if (!shouldIncludeView(tableName, options)) continue;
484
+ const key = `${tableSchema}.${tableName}`;
414
485
  viewsByKey.set(key, {
415
- name: r.table_name,
416
- schema: r.table_schema,
486
+ name: tableName,
487
+ schema: tableSchema,
417
488
  columns: [],
418
- definition: r.view_definition || undefined
489
+ definition: viewDefinition || undefined
419
490
  });
420
491
  }
421
492
 
422
493
  for (const r of viewColumnRows) {
423
- const key = `${r.table_schema}.${r.table_name}`;
494
+ const tableSchema = readMysqlStringField(r, 'table_schema');
495
+ const tableName = readMysqlStringField(r, 'table_name');
496
+ const columnName = readMysqlStringField(r, 'column_name');
497
+ const columnType = readMysqlStringField(r, 'column_type') || readMysqlStringField(r, 'data_type');
498
+ const isNullable = readMysqlStringField(r, 'is_nullable');
499
+ const columnComment = readMysqlStringField(r, 'column_comment');
500
+ if (!tableSchema || !tableName || !columnName || !columnType || !isNullable) continue;
501
+ const key = `${tableSchema}.${tableName}`;
424
502
  const view = viewsByKey.get(key);
425
503
  if (!view) continue;
426
- const columnType = r.column_type || r.data_type;
427
504
  const column: DatabaseColumn = {
428
- name: r.column_name,
505
+ name: columnName,
429
506
  type: columnType,
430
- notNull: r.is_nullable === 'NO',
431
- comment: r.column_comment?.trim() || undefined
507
+ notNull: isNullable === 'NO',
508
+ comment: columnComment?.trim() || undefined
432
509
  };
433
510
  view.columns.push(column);
434
511
  }
@@ -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
  /**