@mikro-orm/sql 7.0.0-rc.1 → 7.0.0-rc.3

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 (47) hide show
  1. package/AbstractSqlConnection.js +2 -1
  2. package/AbstractSqlDriver.d.ts +18 -12
  3. package/AbstractSqlDriver.js +187 -38
  4. package/AbstractSqlPlatform.d.ts +1 -0
  5. package/AbstractSqlPlatform.js +5 -3
  6. package/PivotCollectionPersister.js +2 -2
  7. package/SqlEntityManager.d.ts +5 -4
  8. package/SqlEntityManager.js +5 -5
  9. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +2 -0
  10. package/dialects/mssql/MsSqlNativeQueryBuilder.js +8 -4
  11. package/dialects/mysql/BaseMySqlPlatform.js +1 -2
  12. package/dialects/mysql/MySqlSchemaHelper.js +21 -10
  13. package/dialects/postgresql/BasePostgreSqlPlatform.js +38 -30
  14. package/dialects/postgresql/PostgreSqlSchemaHelper.js +63 -47
  15. package/dialects/sqlite/BaseSqliteConnection.d.ts +4 -1
  16. package/dialects/sqlite/BaseSqliteConnection.js +4 -0
  17. package/dialects/sqlite/NodeSqliteDialect.d.ts +21 -0
  18. package/dialects/sqlite/NodeSqliteDialect.js +43 -0
  19. package/dialects/sqlite/SqliteDriver.d.ts +12 -0
  20. package/dialects/sqlite/SqliteDriver.js +14 -0
  21. package/dialects/sqlite/{BaseSqlitePlatform.d.ts → SqlitePlatform.d.ts} +5 -2
  22. package/dialects/sqlite/{BaseSqlitePlatform.js → SqlitePlatform.js} +30 -4
  23. package/dialects/sqlite/SqliteSchemaHelper.d.ts +5 -0
  24. package/dialects/sqlite/SqliteSchemaHelper.js +31 -10
  25. package/dialects/sqlite/index.d.ts +3 -1
  26. package/dialects/sqlite/index.js +3 -1
  27. package/package.json +30 -30
  28. package/plugin/transformer.js +17 -16
  29. package/query/CriteriaNode.js +28 -10
  30. package/query/CriteriaNodeFactory.js +5 -1
  31. package/query/NativeQueryBuilder.d.ts +25 -0
  32. package/query/NativeQueryBuilder.js +61 -1
  33. package/query/ObjectCriteriaNode.js +71 -27
  34. package/query/QueryBuilder.d.ts +177 -48
  35. package/query/QueryBuilder.js +233 -54
  36. package/query/QueryBuilderHelper.d.ts +4 -3
  37. package/query/QueryBuilderHelper.js +47 -17
  38. package/query/ScalarCriteriaNode.js +14 -7
  39. package/query/raw.js +1 -1
  40. package/schema/DatabaseSchema.js +21 -15
  41. package/schema/DatabaseTable.js +114 -54
  42. package/schema/SchemaComparator.js +56 -32
  43. package/schema/SchemaHelper.js +28 -8
  44. package/schema/SqlSchemaGenerator.d.ts +2 -1
  45. package/schema/SqlSchemaGenerator.js +15 -8
  46. package/tsconfig.build.tsbuildinfo +1 -1
  47. package/typings.d.ts +15 -4
@@ -0,0 +1,14 @@
1
+ import { AbstractSqlDriver } from '../../AbstractSqlDriver.js';
2
+ import { BaseSqliteConnection } from './BaseSqliteConnection.js';
3
+ import { SqlitePlatform } from './SqlitePlatform.js';
4
+ /**
5
+ * Generic SQLite driver that uses `driverOptions` for the Kysely dialect.
6
+ * Use this with any SQLite library by passing a Kysely dialect via `driverOptions`.
7
+ *
8
+ * For the default better-sqlite3 experience, use `@mikro-orm/sqlite` instead.
9
+ */
10
+ export class SqliteDriver extends AbstractSqlDriver {
11
+ constructor(config) {
12
+ super(config, new SqlitePlatform(), BaseSqliteConnection, ['kysely']);
13
+ }
14
+ }
@@ -3,7 +3,7 @@ import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js';
3
3
  import { SqliteNativeQueryBuilder } from './SqliteNativeQueryBuilder.js';
4
4
  import { SqliteSchemaHelper } from './SqliteSchemaHelper.js';
5
5
  import { SqliteExceptionConverter } from './SqliteExceptionConverter.js';
6
- export declare abstract class BaseSqlitePlatform extends AbstractSqlPlatform {
6
+ export declare class SqlitePlatform extends AbstractSqlPlatform {
7
7
  protected readonly schemaHelper: SqliteSchemaHelper;
8
8
  protected readonly exceptionConverter: SqliteExceptionConverter;
9
9
  /** @internal */
@@ -71,6 +71,9 @@ export declare abstract class BaseSqlitePlatform extends AbstractSqlPlatform {
71
71
  supportsSchemas(): boolean;
72
72
  getDefaultSchemaName(): string | undefined;
73
73
  getFullTextWhereClause(): string;
74
- quoteVersionValue(value: Date | number, prop: EntityProperty): Date | string | number;
74
+ escape(value: any): string;
75
+ convertVersionValue(value: Date | number, prop: EntityProperty): number | {
76
+ $in: (string | number)[];
77
+ };
75
78
  quoteValue(value: any): string;
76
79
  }
@@ -2,7 +2,7 @@ import { AbstractSqlPlatform } from '../../AbstractSqlPlatform.js';
2
2
  import { SqliteNativeQueryBuilder } from './SqliteNativeQueryBuilder.js';
3
3
  import { SqliteSchemaHelper } from './SqliteSchemaHelper.js';
4
4
  import { SqliteExceptionConverter } from './SqliteExceptionConverter.js';
5
- export class BaseSqlitePlatform extends AbstractSqlPlatform {
5
+ export class SqlitePlatform extends AbstractSqlPlatform {
6
6
  schemaHelper = new SqliteSchemaHelper(this);
7
7
  exceptionConverter = new SqliteExceptionConverter();
8
8
  /** @internal */
@@ -19,7 +19,7 @@ export class BaseSqlitePlatform extends AbstractSqlPlatform {
19
19
  return true;
20
20
  }
21
21
  getCurrentTimestampSQL(length) {
22
- return super.getCurrentTimestampSQL(0);
22
+ return `(strftime('%s', 'now') * 1000)`;
23
23
  }
24
24
  getDateTimeTypeDeclarationSQL(column) {
25
25
  return 'datetime';
@@ -101,9 +101,35 @@ export class BaseSqlitePlatform extends AbstractSqlPlatform {
101
101
  getFullTextWhereClause() {
102
102
  return `:column: match :query`;
103
103
  }
104
- quoteVersionValue(value, prop) {
104
+ escape(value) {
105
+ if (value == null) {
106
+ return 'null';
107
+ }
108
+ if (typeof value === 'boolean') {
109
+ return value ? 'true' : 'false';
110
+ }
111
+ if (typeof value === 'number' || typeof value === 'bigint') {
112
+ return '' + value;
113
+ }
114
+ if (value instanceof Date) {
115
+ return '' + +value;
116
+ }
117
+ if (Array.isArray(value)) {
118
+ return value.map(v => this.escape(v)).join(', ');
119
+ }
120
+ if (Buffer.isBuffer(value)) {
121
+ return `X'${value.toString('hex')}'`;
122
+ }
123
+ return `'${String(value).replace(/'/g, "''")}'`;
124
+ }
125
+ convertVersionValue(value, prop) {
105
126
  if (prop.runtimeType === 'Date') {
106
- return this.escape(value).replace(/^'|\.\d{3}'$/g, '');
127
+ const ts = +value;
128
+ const str = new Date(ts)
129
+ .toISOString()
130
+ .replace('T', ' ')
131
+ .replace(/\.\d{3}Z$/, '');
132
+ return { $in: [ts, str] };
107
133
  }
108
134
  return value;
109
135
  }
@@ -39,6 +39,11 @@ export declare class SqliteSchemaHelper extends SchemaHelper {
39
39
  */
40
40
  private extractViewDefinition;
41
41
  private getColumns;
42
+ /**
43
+ * SQLite strips outer parentheses from expression defaults (`DEFAULT (expr)` → `expr` in pragma).
44
+ * We need to add them back so they match what we generate in DDL.
45
+ */
46
+ private wrapExpressionDefault;
42
47
  private getEnumDefinitions;
43
48
  getPrimaryKeys(connection: AbstractSqlConnection, indexes: IndexDef[], tableName: string, schemaName?: string): Promise<string[]>;
44
49
  private getIndexes;
@@ -31,8 +31,8 @@ export class SqliteSchemaHelper extends SchemaHelper {
31
31
  return '';
32
32
  }
33
33
  getListTablesSQL() {
34
- return `select name as table_name from sqlite_master where type = 'table' and name != 'sqlite_sequence' and name != 'geometry_columns' and name != 'spatial_ref_sys' `
35
- + `union all select name as table_name from sqlite_temp_master where type = 'table' order by name`;
34
+ return (`select name as table_name from sqlite_master where type = 'table' and name != 'sqlite_sequence' and name != 'geometry_columns' and name != 'spatial_ref_sys' ` +
35
+ `union all select name as table_name from sqlite_temp_master where type = 'table' order by name`);
36
36
  }
37
37
  async getAllTables(connection, schemas) {
38
38
  const databases = await this.getDatabaseList(connection);
@@ -183,9 +183,11 @@ export class SqliteSchemaHelper extends SchemaHelper {
183
183
  getDropColumnsSQL(tableName, columns, schemaName) {
184
184
  /* v8 ignore next */
185
185
  const name = this.quote((schemaName && schemaName !== this.platform.getDefaultSchemaName() ? schemaName + '.' : '') + tableName);
186
- return columns.map(column => {
186
+ return columns
187
+ .map(column => {
187
188
  return `alter table ${name} drop column ${this.quote(column.name)}`;
188
- }).join(';\n');
189
+ })
190
+ .join(';\n');
189
191
  }
190
192
  getCreateIndexSQL(tableName, index) {
191
193
  /* v8 ignore next */
@@ -286,7 +288,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
286
288
  return {
287
289
  name: col.name,
288
290
  type: col.type,
289
- default: col.dflt_value,
291
+ default: this.wrapExpressionDefault(col.dflt_value),
290
292
  nullable: !col.notnull,
291
293
  primary: !!col.pk,
292
294
  mappedType,
@@ -296,6 +298,25 @@ export class SqliteSchemaHelper extends SchemaHelper {
296
298
  };
297
299
  });
298
300
  }
301
+ /**
302
+ * SQLite strips outer parentheses from expression defaults (`DEFAULT (expr)` → `expr` in pragma).
303
+ * We need to add them back so they match what we generate in DDL.
304
+ */
305
+ wrapExpressionDefault(value) {
306
+ if (value == null) {
307
+ return null;
308
+ }
309
+ // simple values that are returned as-is from pragma (no wrapping needed)
310
+ if (/^-?\d/.test(value) || /^[xX]'/.test(value) || value[0] === "'" || value[0] === '"' || value[0] === '(') {
311
+ return value;
312
+ }
313
+ const lower = value.toLowerCase();
314
+ if (['null', 'true', 'false', 'current_timestamp', 'current_date', 'current_time'].includes(lower)) {
315
+ return value;
316
+ }
317
+ // everything else is an expression that had its outer parens stripped
318
+ return `(${value})`;
319
+ }
299
320
  async getEnumDefinitions(connection, tableName, schemaName) {
300
321
  const prefix = this.getSchemaPrefix(schemaName);
301
322
  const sql = `select sql from ${prefix}sqlite_master where type = ? and name = ?`;
@@ -434,10 +455,10 @@ export class SqliteSchemaHelper extends SchemaHelper {
434
455
  alterTable(diff, safe) {
435
456
  const ret = [];
436
457
  const [schemaName, tableName] = this.splitTableName(diff.name);
437
- if (Utils.hasObjectKeys(diff.removedChecks)
438
- || Utils.hasObjectKeys(diff.changedChecks)
439
- || Utils.hasObjectKeys(diff.changedForeignKeys)
440
- || Utils.hasObjectKeys(diff.changedColumns)) {
458
+ if (Utils.hasObjectKeys(diff.removedChecks) ||
459
+ Utils.hasObjectKeys(diff.changedChecks) ||
460
+ Utils.hasObjectKeys(diff.changedForeignKeys) ||
461
+ Utils.hasObjectKeys(diff.changedColumns)) {
441
462
  return this.getAlterTempTableSQL(diff);
442
463
  }
443
464
  for (const index of Object.values(diff.removedIndexes)) {
@@ -477,7 +498,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
477
498
  return ret;
478
499
  }
479
500
  getAlterTempTableSQL(changedTable) {
480
- const tempName = `${(changedTable.toTable.name)}__temp_alter`;
501
+ const tempName = `${changedTable.toTable.name}__temp_alter`;
481
502
  const quotedName = this.quote(changedTable.toTable.name);
482
503
  const quotedTempName = this.quote(tempName);
483
504
  const [first, ...rest] = this.createTable(changedTable.toTable);
@@ -1,4 +1,6 @@
1
1
  export * from './BaseSqliteConnection.js';
2
- export * from './BaseSqlitePlatform.js';
2
+ export * from './NodeSqliteDialect.js';
3
+ export * from './SqliteDriver.js';
4
+ export * from './SqlitePlatform.js';
3
5
  export * from './SqliteSchemaHelper.js';
4
6
  export * from './SqliteNativeQueryBuilder.js';
@@ -1,4 +1,6 @@
1
1
  export * from './BaseSqliteConnection.js';
2
- export * from './BaseSqlitePlatform.js';
2
+ export * from './NodeSqliteDialect.js';
3
+ export * from './SqliteDriver.js';
4
+ export * from './SqlitePlatform.js';
3
5
  export * from './SqliteSchemaHelper.js';
4
6
  export * from './SqliteNativeQueryBuilder.js';
package/package.json CHANGED
@@ -1,44 +1,44 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.0.0-rc.1",
3
+ "version": "7.0.0-rc.3",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
- "type": "module",
6
- "exports": {
7
- "./package.json": "./package.json",
8
- ".": "./index.js"
9
- },
10
- "repository": {
11
- "type": "git",
12
- "url": "git+ssh://git@github.com/mikro-orm/mikro-orm.git"
13
- },
14
5
  "keywords": [
15
- "orm",
6
+ "data-mapper",
7
+ "ddd",
8
+ "entity",
9
+ "identity-map",
10
+ "javascript",
11
+ "js",
12
+ "mariadb",
13
+ "mikro-orm",
16
14
  "mongo",
17
15
  "mongodb",
18
16
  "mysql",
19
- "mariadb",
17
+ "orm",
20
18
  "postgresql",
21
19
  "sqlite",
22
20
  "sqlite3",
23
21
  "ts",
24
22
  "typescript",
25
- "js",
26
- "javascript",
27
- "entity",
28
- "ddd",
29
- "mikro-orm",
30
- "unit-of-work",
31
- "data-mapper",
32
- "identity-map"
23
+ "unit-of-work"
33
24
  ],
34
- "author": "Martin Adámek",
35
- "license": "MIT",
25
+ "homepage": "https://mikro-orm.io",
36
26
  "bugs": {
37
27
  "url": "https://github.com/mikro-orm/mikro-orm/issues"
38
28
  },
39
- "homepage": "https://mikro-orm.io",
40
- "engines": {
41
- "node": ">= 22.17.0"
29
+ "license": "MIT",
30
+ "author": "Martin Adámek",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+ssh://git@github.com/mikro-orm/mikro-orm.git"
34
+ },
35
+ "type": "module",
36
+ "exports": {
37
+ "./package.json": "./package.json",
38
+ ".": "./index.js"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "yarn compile && yarn copy",
@@ -46,16 +46,16 @@
46
46
  "compile": "yarn run -T tsc -p tsconfig.build.json",
47
47
  "copy": "node ../../scripts/copy.mjs"
48
48
  },
49
- "publishConfig": {
50
- "access": "public"
51
- },
52
49
  "dependencies": {
53
50
  "kysely": "0.28.11"
54
51
  },
55
52
  "devDependencies": {
56
- "@mikro-orm/core": "^6.6.4"
53
+ "@mikro-orm/core": "^6.6.8"
57
54
  },
58
55
  "peerDependencies": {
59
- "@mikro-orm/core": "7.0.0-rc.1"
56
+ "@mikro-orm/core": "7.0.0-rc.3"
57
+ },
58
+ "engines": {
59
+ "node": ">= 22.17.0"
60
60
  }
61
61
  }
@@ -79,12 +79,8 @@ export class MikroTransformer extends OperationNodeTransformer {
79
79
  }
80
80
  }
81
81
  }
82
- const nodeWithHooks = this.options.processOnCreateHooks && entityMeta
83
- ? this.processOnCreateHooks(node, entityMeta)
84
- : node;
85
- const nodeWithConvertedValues = this.options.convertValues && entityMeta
86
- ? this.processInsertValues(nodeWithHooks, entityMeta)
87
- : nodeWithHooks;
82
+ const nodeWithHooks = this.options.processOnCreateHooks && entityMeta ? this.processOnCreateHooks(node, entityMeta) : node;
83
+ const nodeWithConvertedValues = this.options.convertValues && entityMeta ? this.processInsertValues(nodeWithHooks, entityMeta) : nodeWithHooks;
88
84
  // Handle ON CONFLICT clause
89
85
  let finalNode = nodeWithConvertedValues;
90
86
  if (node.onConflict?.updates && entityMeta) {
@@ -150,12 +146,8 @@ export class MikroTransformer extends OperationNodeTransformer {
150
146
  this.processJoinNode(join, currentContext);
151
147
  }
152
148
  }
153
- const nodeWithHooks = this.options.processOnUpdateHooks && entityMeta
154
- ? this.processOnUpdateHooks(node, entityMeta)
155
- : node;
156
- const nodeWithConvertedValues = this.options.convertValues && entityMeta
157
- ? this.processUpdateValues(nodeWithHooks, entityMeta)
158
- : nodeWithHooks;
149
+ const nodeWithHooks = this.options.processOnUpdateHooks && entityMeta ? this.processOnUpdateHooks(node, entityMeta) : node;
150
+ const nodeWithConvertedValues = this.options.convertValues && entityMeta ? this.processUpdateValues(nodeWithHooks, entityMeta) : nodeWithHooks;
159
151
  return super.transformUpdateQuery(nodeWithConvertedValues, queryId);
160
152
  }
161
153
  finally {
@@ -217,7 +209,9 @@ export class MikroTransformer extends OperationNodeTransformer {
217
209
  }
218
210
  // Transform column names when columnNamingStrategy is 'property'
219
211
  // Support ColumnNode, ColumnUpdateNode, and ReferenceNode (for JOIN conditions)
220
- if (this.options.columnNamingStrategy === 'property' && parent && (ColumnNode.is(parent) || ColumnUpdateNode.is(parent) || ReferenceNode.is(parent))) {
212
+ if (this.options.columnNamingStrategy === 'property' &&
213
+ parent &&
214
+ (ColumnNode.is(parent) || ColumnUpdateNode.is(parent) || ReferenceNode.is(parent))) {
221
215
  const ownerMeta = this.findOwnerEntityInContext();
222
216
  if (ownerMeta) {
223
217
  const prop = ownerMeta.properties[node.name];
@@ -480,7 +474,11 @@ export class MikroTransformer extends OperationNodeTransformer {
480
474
  }
481
475
  }
482
476
  if (prop.customType && !isRaw(value)) {
483
- return prop.customType.convertToDatabaseValue(value, this.platform, { fromQuery: true, key: prop.name, mode: 'query-data' });
477
+ return prop.customType.convertToDatabaseValue(value, this.platform, {
478
+ fromQuery: true,
479
+ key: prop.name,
480
+ mode: 'query-data',
481
+ });
484
482
  }
485
483
  if (value instanceof Date) {
486
484
  return this.platform.processDateProperty(value);
@@ -723,7 +721,9 @@ export class MikroTransformer extends OperationNodeTransformer {
723
721
  */
724
722
  transformResult(rows, entityMap) {
725
723
  // Only transform if columnNamingStrategy is 'property' or convertValues is true, and we have data
726
- if ((this.options.columnNamingStrategy !== 'property' && !this.options.convertValues) || !rows || rows.length === 0) {
724
+ if ((this.options.columnNamingStrategy !== 'property' && !this.options.convertValues) ||
725
+ !rows ||
726
+ rows.length === 0) {
727
727
  return rows;
728
728
  }
729
729
  // If no entities found (e.g. raw query without known tables), return rows as is
@@ -870,7 +870,8 @@ export class MikroTransformer extends OperationNodeTransformer {
870
870
  }
871
871
  // For non-local timezone, check if value already has timezone info
872
872
  // Number (timestamp) doesn't need timezone handling, string needs check
873
- if (typeof value === 'number' || (typeof value === 'string' && (value.includes('+') || value.lastIndexOf('-') > 10 || value.endsWith('Z')))) {
873
+ if (typeof value === 'number' ||
874
+ (typeof value === 'string' && (value.includes('+') || value.lastIndexOf('-') > 10 || value.endsWith('Z')))) {
874
875
  return this.platform.parseDate(value);
875
876
  }
876
877
  // Append timezone if not present (only for string values)
@@ -53,22 +53,34 @@ export class CriteriaNode {
53
53
  const type = this.prop ? this.prop.kind : null;
54
54
  const composite = this.prop?.joinColumns ? this.prop.joinColumns.length > 1 : false;
55
55
  const rawField = RawQueryFragment.isKnownFragmentSymbol(this.key);
56
- const scalar = payload === null || Utils.isPrimaryKey(payload) || payload instanceof RegExp || payload instanceof Date || rawField;
56
+ const scalar = payload === null ||
57
+ Utils.isPrimaryKey(payload) ||
58
+ payload instanceof RegExp ||
59
+ payload instanceof Date ||
60
+ rawField;
57
61
  const operator = Utils.isPlainObject(payload) && Utils.getObjectQueryKeys(payload).every(k => Utils.isOperator(k, false));
58
62
  if (composite) {
59
63
  return true;
60
64
  }
61
65
  switch (type) {
62
- case ReferenceKind.MANY_TO_ONE: return false;
63
- case ReferenceKind.ONE_TO_ONE: return !this.prop.owner;
64
- case ReferenceKind.ONE_TO_MANY: return scalar || operator;
65
- case ReferenceKind.MANY_TO_MANY: return scalar || operator;
66
- default: return false;
66
+ case ReferenceKind.MANY_TO_ONE:
67
+ return false;
68
+ case ReferenceKind.ONE_TO_ONE:
69
+ return !this.prop.owner;
70
+ case ReferenceKind.ONE_TO_MANY:
71
+ return scalar || operator;
72
+ case ReferenceKind.MANY_TO_MANY:
73
+ return scalar || operator;
74
+ default:
75
+ return false;
67
76
  }
68
77
  }
69
78
  renameFieldToPK(qb, ownerAlias) {
70
79
  const joinAlias = qb.getAliasForJoinPath(this.getPath(), { matchPopulateJoins: true });
71
- if (!joinAlias && this.parent && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind) && this.prop.owner) {
80
+ if (!joinAlias &&
81
+ this.parent &&
82
+ [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind) &&
83
+ this.prop.owner) {
72
84
  const alias = qb.getAliasForJoinPath(this.parent.getPath()) ?? ownerAlias ?? qb.alias;
73
85
  return Utils.getPrimaryKeyHash(this.prop.joinColumns.map(col => `${alias}.${col}`));
74
86
  }
@@ -84,7 +96,9 @@ export class CriteriaNode {
84
96
  const parentPath = opts?.parentPath ?? this.parent?.getPath({ addIndex: addParentIndex }) ?? Utils.className(this.entityName);
85
97
  const index = opts?.addIndex && this.index != null ? `[${this.index}]` : '';
86
98
  // ignore group operators to allow easier mapping (e.g. for orderBy)
87
- const key = this.key && !RawQueryFragment.isKnownFragmentSymbol(this.key) && !['$and', '$or', '$not'].includes(this.key) ? '.' + this.key : '';
99
+ const key = this.key && !RawQueryFragment.isKnownFragmentSymbol(this.key) && !['$and', '$or', '$not'].includes(this.key)
100
+ ? '.' + this.key
101
+ : '';
88
102
  const ret = parentPath + index + key;
89
103
  if (this.isPivotJoin()) {
90
104
  // distinguish pivot table join from target entity join
@@ -97,7 +111,11 @@ export class CriteriaNode {
97
111
  return false;
98
112
  }
99
113
  const rawField = RawQueryFragment.isKnownFragmentSymbol(this.key);
100
- const scalar = this.payload === null || Utils.isPrimaryKey(this.payload) || this.payload instanceof RegExp || this.payload instanceof Date || rawField;
114
+ const scalar = this.payload === null ||
115
+ Utils.isPrimaryKey(this.payload) ||
116
+ this.payload instanceof RegExp ||
117
+ this.payload instanceof Date ||
118
+ rawField;
101
119
  const operator = Utils.isObject(this.payload) && Utils.getObjectQueryKeys(this.payload).every(k => Utils.isOperator(k, false));
102
120
  return this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator);
103
121
  }
@@ -116,7 +134,7 @@ export class CriteriaNode {
116
134
  const o = {};
117
135
  ['entityName', 'key', 'index', 'payload']
118
136
  .filter(k => this[k] !== undefined)
119
- .forEach(k => o[k] = this[k]);
137
+ .forEach(k => (o[k] = this[k]));
120
138
  return `${this.constructor.name} ${inspect(o)}`;
121
139
  }
122
140
  }
@@ -8,7 +8,11 @@ import { ScalarCriteriaNode } from './ScalarCriteriaNode.js';
8
8
  export class CriteriaNodeFactory {
9
9
  static createNode(metadata, entityName, payload, parent, key, validate = true) {
10
10
  const rawField = RawQueryFragment.isKnownFragmentSymbol(key);
11
- const scalar = Utils.isPrimaryKey(payload) || isRaw(payload) || payload instanceof RegExp || payload instanceof Date || rawField;
11
+ const scalar = Utils.isPrimaryKey(payload) ||
12
+ isRaw(payload) ||
13
+ payload instanceof RegExp ||
14
+ payload instanceof Date ||
15
+ rawField;
12
16
  if (Array.isArray(payload) && !scalar) {
13
17
  return this.createArrayNode(metadata, entityName, payload, parent, key, validate);
14
18
  }
@@ -1,6 +1,17 @@
1
1
  import { type Dictionary, LockMode, type QueryFlag, RawQueryFragment, type Subquery } from '@mikro-orm/core';
2
2
  import { QueryType } from './enums.js';
3
3
  import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
4
+ export interface CteOptions {
5
+ columns?: string[];
6
+ /** PostgreSQL: MATERIALIZED / NOT MATERIALIZED */
7
+ materialized?: boolean;
8
+ }
9
+ interface CteClause extends CteOptions {
10
+ name: string;
11
+ sql: string;
12
+ params: unknown[];
13
+ recursive?: boolean;
14
+ }
4
15
  interface Options {
5
16
  tableName?: string | RawQueryFragment;
6
17
  indexHint?: string;
@@ -32,6 +43,7 @@ interface Options {
32
43
  hintComment?: string[];
33
44
  flags?: Set<QueryFlag>;
34
45
  wrap?: [prefix: string, suffix: string];
46
+ ctes?: CteClause[];
35
47
  }
36
48
  interface TableOptions {
37
49
  schema?: string;
@@ -65,6 +77,17 @@ export declare class NativeQueryBuilder implements Subquery {
65
77
  groupBy(groupBy: (string | RawQueryFragment)[]): this;
66
78
  join(sql: string, params: unknown[]): this;
67
79
  orderBy(orderBy: string): this;
80
+ /**
81
+ * The sub-query is compiled eagerly at call time — later mutations to the
82
+ * sub-query builder will not be reflected in this CTE.
83
+ */
84
+ with(name: string, query: NativeQueryBuilder | RawQueryFragment, options?: CteOptions): this;
85
+ /**
86
+ * Adds a recursive CTE (`WITH RECURSIVE` on PostgreSQL/MySQL/SQLite, plain `WITH` on MSSQL).
87
+ * The sub-query is compiled eagerly — later mutations will not be reflected.
88
+ */
89
+ withRecursive(name: string, query: NativeQueryBuilder | RawQueryFragment, options?: CteOptions): this;
90
+ private addCte;
68
91
  toString(): string;
69
92
  compile(): {
70
93
  sql: string;
@@ -103,6 +126,8 @@ export declare class NativeQueryBuilder implements Subquery {
103
126
  protected compileDelete(): void;
104
127
  protected compileTruncate(): void;
105
128
  protected addHintComment(): void;
129
+ protected compileCtes(): void;
130
+ protected getCteKeyword(hasRecursive: boolean): string;
106
131
  protected getTableName(): string;
107
132
  protected quote(id: string | RawQueryFragment | NativeQueryBuilder): string;
108
133
  }
@@ -1,4 +1,4 @@
1
- import { LockMode, raw, RawQueryFragment, Utils } from '@mikro-orm/core';
1
+ import { LockMode, raw, RawQueryFragment, Utils, } from '@mikro-orm/core';
2
2
  import { QueryType } from './enums.js';
3
3
  /** @internal */
4
4
  export class NativeQueryBuilder {
@@ -59,6 +59,36 @@ export class NativeQueryBuilder {
59
59
  this.options.orderBy = orderBy;
60
60
  return this;
61
61
  }
62
+ /**
63
+ * The sub-query is compiled eagerly at call time — later mutations to the
64
+ * sub-query builder will not be reflected in this CTE.
65
+ */
66
+ with(name, query, options) {
67
+ return this.addCte(name, query, options);
68
+ }
69
+ /**
70
+ * Adds a recursive CTE (`WITH RECURSIVE` on PostgreSQL/MySQL/SQLite, plain `WITH` on MSSQL).
71
+ * The sub-query is compiled eagerly — later mutations will not be reflected.
72
+ */
73
+ withRecursive(name, query, options) {
74
+ return this.addCte(name, query, options, true);
75
+ }
76
+ addCte(name, query, options, recursive) {
77
+ this.options.ctes ??= [];
78
+ if (this.options.ctes.some(cte => cte.name === name)) {
79
+ throw new Error(`CTE with name '${name}' already exists`);
80
+ }
81
+ const { sql, params } = query instanceof NativeQueryBuilder ? query.compile() : { sql: query.sql, params: [...query.params] };
82
+ this.options.ctes.push({
83
+ name,
84
+ sql,
85
+ params,
86
+ recursive,
87
+ columns: options?.columns,
88
+ materialized: options?.materialized,
89
+ });
90
+ return this;
91
+ }
62
92
  toString() {
63
93
  const { sql, params } = this.compile();
64
94
  return this.platform.formatQuery(sql, params);
@@ -72,6 +102,7 @@ export class NativeQueryBuilder {
72
102
  if (this.options.comment) {
73
103
  this.parts.push(...this.options.comment.map(comment => `/* ${comment} */`));
74
104
  }
105
+ this.compileCtes();
75
106
  switch (this.type) {
76
107
  case QueryType.SELECT:
77
108
  case QueryType.COUNT:
@@ -387,6 +418,35 @@ export class NativeQueryBuilder {
387
418
  this.parts.push(`/*+ ${this.options.hintComment.join(' ')} */`);
388
419
  }
389
420
  }
421
+ compileCtes() {
422
+ const ctes = this.options.ctes;
423
+ if (!ctes || ctes.length === 0) {
424
+ return;
425
+ }
426
+ const hasRecursive = ctes.some(cte => cte.recursive);
427
+ const keyword = this.getCteKeyword(hasRecursive);
428
+ const cteParts = [];
429
+ for (const cte of ctes) {
430
+ let part = this.quote(cte.name);
431
+ if (cte.columns?.length) {
432
+ part += ` (${cte.columns.map(c => this.quote(c)).join(', ')})`;
433
+ }
434
+ part += ' as';
435
+ if (cte.materialized === true) {
436
+ part += ' materialized';
437
+ }
438
+ else if (cte.materialized === false) {
439
+ part += ' not materialized';
440
+ }
441
+ part += ` (${cte.sql})`;
442
+ this.params.push(...cte.params);
443
+ cteParts.push(part);
444
+ }
445
+ this.parts.push(`${keyword} ${cteParts.join(', ')}`);
446
+ }
447
+ getCteKeyword(hasRecursive) {
448
+ return hasRecursive ? 'with recursive' : 'with';
449
+ }
390
450
  getTableName() {
391
451
  if (!this.options.tableName) {
392
452
  throw new Error('No table name provided');