@mikro-orm/sql 7.2.0-dev.2 → 7.2.0-dev.20

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 (46) hide show
  1. package/AbstractSqlConnection.d.ts +30 -3
  2. package/AbstractSqlConnection.js +75 -15
  3. package/AbstractSqlDriver.d.ts +1 -8
  4. package/AbstractSqlDriver.js +124 -55
  5. package/AbstractSqlPlatform.d.ts +4 -2
  6. package/AbstractSqlPlatform.js +35 -1
  7. package/SqlEntityManager.d.ts +2 -2
  8. package/SqlEntityManager.js +5 -4
  9. package/dialects/mssql/MsSqlNativeQueryBuilder.js +1 -1
  10. package/dialects/mysql/BaseMySqlPlatform.d.ts +2 -0
  11. package/dialects/mysql/BaseMySqlPlatform.js +4 -0
  12. package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -0
  13. package/dialects/mysql/MySqlSchemaHelper.js +4 -1
  14. package/dialects/oracledb/OracleNativeQueryBuilder.js +1 -1
  15. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +3 -1
  16. package/dialects/postgresql/BasePostgreSqlPlatform.js +35 -2
  17. package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
  18. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +22 -1
  19. package/dialects/postgresql/PostgreSqlSchemaHelper.js +181 -4
  20. package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -0
  21. package/dialects/sqlite/BaseSqliteConnection.js +15 -5
  22. package/dialects/sqlite/SqlitePlatform.d.ts +2 -0
  23. package/dialects/sqlite/SqlitePlatform.js +4 -0
  24. package/dialects/sqlite/SqliteSchemaHelper.js +2 -2
  25. package/package.json +4 -4
  26. package/plugin/transformer.d.ts +7 -1
  27. package/plugin/transformer.js +60 -1
  28. package/query/CriteriaNodeFactory.js +4 -0
  29. package/query/NativeQueryBuilder.js +1 -1
  30. package/query/ObjectCriteriaNode.d.ts +1 -0
  31. package/query/ObjectCriteriaNode.js +30 -5
  32. package/query/QueryBuilder.d.ts +40 -7
  33. package/query/QueryBuilder.js +181 -37
  34. package/query/QueryBuilderHelper.d.ts +5 -0
  35. package/query/QueryBuilderHelper.js +39 -9
  36. package/schema/DatabaseSchema.d.ts +4 -0
  37. package/schema/DatabaseSchema.js +107 -1
  38. package/schema/DatabaseTable.d.ts +15 -1
  39. package/schema/DatabaseTable.js +113 -19
  40. package/schema/SchemaComparator.d.ts +3 -0
  41. package/schema/SchemaComparator.js +123 -10
  42. package/schema/SchemaHelper.d.ts +26 -1
  43. package/schema/SchemaHelper.js +67 -9
  44. package/schema/SqlSchemaGenerator.d.ts +4 -0
  45. package/schema/SqlSchemaGenerator.js +59 -22
  46. package/typings.d.ts +20 -2
@@ -1,7 +1,17 @@
1
1
  import { isRaw, Utils, } from '@mikro-orm/core';
2
- /** Flattens `;\n` boundaries so the schema-generator's statement splitter doesn't break the routine DDL apart. Other whitespace is preserved. */
2
+ /**
3
+ * Flattens `;\n` boundaries and drops blank lines so the schema-generator's statement splitter
4
+ * doesn't break the routine or trigger DDL apart — it treats both as statement/group separators.
5
+ * Blank lines go first, otherwise a `;` followed by one would keep its newline. Like
6
+ * `normalizeViewDefinition`, this is not string-literal aware, so a blank line inside a multi-line
7
+ * literal is dropped too. Other whitespace is preserved.
8
+ */
3
9
  export function stripStatementNewlines(body) {
4
- return body.replace(/;[\t ]*\r?\n/g, '; ');
10
+ return body
11
+ .split('\n')
12
+ .filter(line => line.trim() !== '')
13
+ .join('\n')
14
+ .replace(/;[\t ]*\r?\n/g, '; ');
5
15
  }
6
16
  /**
7
17
  * Strips SQL line comments and blank lines from a view definition. Comments are dropped by the
@@ -157,13 +167,14 @@ export class SchemaHelper {
157
167
  tableName = this.quote(tableName);
158
168
  const keyName = this.quote(index.keyName);
159
169
  const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
160
- let sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
170
+ const using = this.getIndexAccessMethodClause(index);
171
+ let sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}${using}`;
161
172
  if (index.unique && index.constraint) {
162
173
  sql = `alter table ${tableName} add constraint ${keyName} unique`;
163
174
  }
164
175
  if (index.columnNames.some(column => column.includes('.'))) {
165
176
  // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
166
- sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
177
+ sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}${using}`;
167
178
  const columns = this.platform.getJsonIndexDefinition(index);
168
179
  return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${this.getIndexWhereClause(index)}${defer}`;
169
180
  }
@@ -182,6 +193,18 @@ export class SchemaHelper {
182
193
  getCreateIndexSuffix(_index) {
183
194
  return '';
184
195
  }
196
+ /**
197
+ * Normalized index access method (e.g. `gin` on PostgreSQL), empty string when the
198
+ * platform default applies. Used for both DDL emission and index diffing.
199
+ */
200
+ getIndexAccessMethod(_index) {
201
+ return '';
202
+ }
203
+ /** Emits the access method between the table name and the column list (e.g. ` using gin`). */
204
+ getIndexAccessMethodClause(index) {
205
+ const method = this.getIndexAccessMethod(index);
206
+ return method ? ` using ${method}` : '';
207
+ }
185
208
  /**
186
209
  * Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
187
210
  * return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
@@ -436,7 +459,8 @@ export class SchemaHelper {
436
459
  this.append(ret, this.alterTableColumn(column, diff.fromTable, changedProperties));
437
460
  }
438
461
  for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff => diff.changedProperties.has('comment'))) {
439
- if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
462
+ if (this.hasInlineColumnComment() &&
463
+ ['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
440
464
  continue; // will be handled via column update
441
465
  }
442
466
  ret.push(this.getChangeColumnCommentSQL(tableName, column, schemaName));
@@ -480,6 +504,7 @@ export class SchemaHelper {
480
504
  if ('changedComment' in diff) {
481
505
  ret.push(this.alterTableComment(diff.toTable, diff.changedComment));
482
506
  }
507
+ this.append(ret, this.getRlsAlterSQL(diff, safe));
483
508
  return ret;
484
509
  }
485
510
  /** Returns SQL to add columns to an existing table. */
@@ -489,7 +514,13 @@ export class SchemaHelper {
489
514
  return `add ${this.createTableColumn(column, table)}`;
490
515
  })
491
516
  .join(', ');
492
- return [`alter table ${table.getQuotedName()} ${adds}`];
517
+ const ret = [`alter table ${table.getQuotedName()} ${adds}`];
518
+ if (!this.hasInlineColumnComment()) {
519
+ for (const column of columns.filter(column => column.comment)) {
520
+ ret.push(this.getChangeColumnCommentSQL(table.name, column, table.schema));
521
+ }
522
+ }
523
+ return ret;
493
524
  }
494
525
  getDropColumnsSQL(tableName, columns, schemaName) {
495
526
  const name = this.quote(this.getTableName(tableName, schemaName));
@@ -504,6 +535,10 @@ export class SchemaHelper {
504
535
  const defaultName = this.platform.getDefaultPrimaryName(table.name, pkIndex.columnNames);
505
536
  return pkIndex?.keyName !== defaultName;
506
537
  }
538
+ /** Returns the `constraint <name> ` prefix for a primary key definition, empty when the server assigns the default name on its own. */
539
+ getPrimaryKeyConstraintPrefix(table, index) {
540
+ return this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(index.keyName)} ` : '';
541
+ }
507
542
  /* v8 ignore next */
508
543
  castColumn(name, type) {
509
544
  return '';
@@ -608,6 +643,22 @@ export class SchemaHelper {
608
643
  getChangeColumnCommentSQL(tableName, to, schemaName) {
609
644
  return '';
610
645
  }
646
+ /** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
647
+ hasInlineColumnComment() {
648
+ return false;
649
+ }
650
+ /** Row level security DDL for a freshly created table (enable/force + create policies). Postgres only. */
651
+ getRlsCreateSQL(table) {
652
+ return [];
653
+ }
654
+ /** Drops removed/changed row level security policies; emitted in the pre-alter phase, before any column drop or type alter a policy expression can block. Postgres only. */
655
+ getRlsDropSQL(diff, safe) {
656
+ return [];
657
+ }
658
+ /** Row level security DDL for a table difference (enable/disable/force transitions + policy creation). Postgres only. */
659
+ getRlsAlterSQL(diff, safe) {
660
+ return [];
661
+ }
611
662
  async getNamespaces(connection, ctx) {
612
663
  return [];
613
664
  }
@@ -745,7 +796,7 @@ export class SchemaHelper {
745
796
  const primaryKey = table.getPrimaryKey();
746
797
  const createPrimary = !table.getColumns().some(c => c.autoincrement && c.primary) || this.hasNonDefaultPrimaryKeyName(table);
747
798
  if (createPrimary && primaryKey) {
748
- const name = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(primaryKey.keyName)} ` : '';
799
+ const name = this.getPrimaryKeyConstraintPrefix(table, primaryKey);
749
800
  sql += `, ${name}primary key (${primaryKey.columnNames.map(c => this.quote(c)).join(', ')})`;
750
801
  }
751
802
  sql += ')';
@@ -763,6 +814,8 @@ export class SchemaHelper {
763
814
  for (const trigger of table.getTriggers()) {
764
815
  this.append(ret, this.createTrigger(table, trigger));
765
816
  }
817
+ // RLS policies can reference other tables, so they are deferred until every table exists (see the
818
+ // callers of getRlsCreateSQL in SqlSchemaGenerator) rather than emitted inline here
766
819
  }
767
820
  return ret;
768
821
  }
@@ -828,7 +881,7 @@ export class SchemaHelper {
828
881
  const columns = index.columnNames.map(c => this.quote(c)).join(', ');
829
882
  const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
830
883
  if (index.primary) {
831
- const keyName = this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${index.keyName} ` : '';
884
+ const keyName = this.getPrimaryKeyConstraintPrefix(table, index);
832
885
  return `alter table ${table.getQuotedName()} add ${keyName}primary key (${columns})${defer}`;
833
886
  }
834
887
  if (index.type === 'fulltext') {
@@ -861,7 +914,7 @@ export class SchemaHelper {
861
914
  const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
862
915
  const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
863
916
  const when = trigger.when ? ` when (${trigger.when})` : '';
864
- return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`;
917
+ return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${this.normalizeTriggerBody(trigger.body)} end`;
865
918
  }
866
919
  /**
867
920
  * Generates SQL to drop a database trigger from a table.
@@ -885,6 +938,11 @@ export class SchemaHelper {
885
938
  async getAllRoutines(_connection, _schemas = []) {
886
939
  return [];
887
940
  }
941
+ /** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
942
+ normalizeTriggerBody(body) {
943
+ const trimmed = stripStatementNewlines(body).trim();
944
+ return /;\s*$/.test(trimmed) ? trimmed : `${trimmed};`;
945
+ }
888
946
  /** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */
889
947
  wrapRoutineBody(body) {
890
948
  const trimmed = stripStatementNewlines(body).trim();
@@ -54,6 +54,10 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
54
54
  wrap?: boolean;
55
55
  ctx?: Transaction;
56
56
  }): Promise<void>;
57
+ /** Splits the SQL on the separator, keeping the separators that fall inside a string literal. */
58
+ private splitOutsideLiterals;
59
+ /** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. */
60
+ protected startsBatch(_statement: string): boolean;
57
61
  dropTableIfExists(name: string, schema?: string): Promise<void>;
58
62
  private wrapSchema;
59
63
  private append;
@@ -94,6 +94,10 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
94
94
  this.append(ret, fks, true);
95
95
  }
96
96
  }
97
+ // RLS policies are deferred until every table exists, so a policy expression can reference another table
98
+ for (const table of toSchema.getTables()) {
99
+ this.append(ret, this.helper.getRlsCreateSQL(table));
100
+ }
97
101
  const sortedViews = this.sortViewsByDependencies(toSchema.getViews());
98
102
  for (const view of sortedViews) {
99
103
  this.appendViewCreation(ret, view);
@@ -308,22 +312,6 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
308
312
  for (const newTable of Object.values(schemaDiff.newTables)) {
309
313
  this.append(ret, this.helper.createTable(newTable, true), true);
310
314
  }
311
- if (this.helper.supportsSchemaConstraints()) {
312
- for (const newTable of Object.values(schemaDiff.newTables)) {
313
- const sql = [];
314
- if (this.options.createForeignKeyConstraints) {
315
- const fks = Object.values(newTable.getForeignKeys()).map(fk => this.helper.createForeignKey(newTable, fk));
316
- this.append(sql, fks);
317
- }
318
- for (const check of newTable.getChecks()) {
319
- this.append(sql, this.helper.createCheck(newTable, check));
320
- }
321
- for (const trigger of newTable.getTriggers()) {
322
- this.append(sql, this.helper.createTrigger(newTable, trigger));
323
- }
324
- this.append(ret, sql, true);
325
- }
326
- }
327
315
  if (options.dropTables && !options.safe) {
328
316
  for (const table of Object.values(schemaDiff.removedTables)) {
329
317
  // Drop triggers before the table so driver-specific cleanup runs (e.g. PostgreSQL function removal)
@@ -353,6 +341,24 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
353
341
  for (const changedTable of alteredTables) {
354
342
  this.append(ret, this.helper.getPostAlterTable(changedTable, options.safe), true);
355
343
  }
344
+ // after the alters, so a new table's FK can reference a unique constraint an existing table gains in the same diff
345
+ if (this.helper.supportsSchemaConstraints()) {
346
+ for (const newTable of Object.values(schemaDiff.newTables)) {
347
+ const sql = [];
348
+ if (this.options.createForeignKeyConstraints) {
349
+ const fks = Object.values(newTable.getForeignKeys()).map(fk => this.helper.createForeignKey(newTable, fk));
350
+ this.append(sql, fks);
351
+ }
352
+ for (const check of newTable.getChecks()) {
353
+ this.append(sql, this.helper.createCheck(newTable, check));
354
+ }
355
+ for (const trigger of newTable.getTriggers()) {
356
+ this.append(sql, this.helper.createTrigger(newTable, trigger));
357
+ }
358
+ this.append(sql, this.helper.getRlsCreateSQL(newTable));
359
+ this.append(ret, sql, true);
360
+ }
361
+ }
356
362
  if (!options.safe && this.platform.supportsNativeEnums()) {
357
363
  for (const removedNativeEnum of schemaDiff.removedNativeEnums) {
358
364
  this.append(ret, this.helper.getDropNativeEnumSQL(removedNativeEnum.name, removedNativeEnum.schema));
@@ -409,6 +415,9 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
409
415
  }
410
416
  preAlterTable(diff, safe) {
411
417
  const ret = [];
418
+ // removed/changed policies must be dropped before any column type alter, including the pre-alter
419
+ // uuid-to-text cast on postgres — a policy expression blocks type changes on the columns it references
420
+ this.append(ret, this.helper.getRlsDropSQL(diff, safe));
412
421
  this.append(ret, this.helper.getPreAlterTable(diff, safe));
413
422
  for (const foreignKey of Object.values(diff.removedForeignKeys)) {
414
423
  ret.push(this.helper.dropForeignKey(diff.toTable.getShortestName(), foreignKey.constraintName));
@@ -439,18 +448,23 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
439
448
  }
440
449
  async execute(sql, options = {}) {
441
450
  options.wrap ??= false;
442
- const lines = this.wrapSchema(sql, options).split('\n');
451
+ const lines = this.splitOutsideLiterals(this.wrapSchema(sql, options), '\n');
443
452
  const groups = [];
444
453
  let i = 0;
445
454
  for (const line of lines) {
446
- if (line.trim() === '') {
455
+ const stmt = line.trim();
456
+ if (stmt === '') {
447
457
  if (groups[i]?.length > 0) {
448
458
  i++;
449
459
  }
450
460
  continue;
451
461
  }
462
+ // same boundary an empty line creates, for statements that have to start their own batch
463
+ if (groups[i]?.length > 0 && this.startsBatch(stmt)) {
464
+ i++;
465
+ }
452
466
  groups[i] ??= [];
453
- groups[i].push(line.trim());
467
+ groups[i].push(stmt);
454
468
  }
455
469
  if (groups.length === 0) {
456
470
  return;
@@ -463,14 +477,37 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
463
477
  return;
464
478
  }
465
479
  const statements = groups.flatMap(group => {
466
- return group
467
- .join('\n')
468
- .split(';\n')
480
+ return this.splitOutsideLiterals(group.join('\n'), ';\n')
469
481
  .map(s => s.trim())
470
482
  .filter(s => s);
471
483
  });
472
484
  await Utils.runSerial(statements, stmt => this.driver.execute(stmt));
473
485
  }
486
+ /** Splits the SQL on the separator, keeping the separators that fall inside a string literal. */
487
+ splitOutsideLiterals(sql, separator) {
488
+ const [idOpen, idClose] = this.platform.quoteIdentifier('');
489
+ // mysql escapes quotes as `\'`, the other dialects double them, which pairs up on its own
490
+ const esc = this.platform.quoteValue(`'`).includes(`\\'`) ? '\\\\.|' : '';
491
+ // complete literals, quoted identifiers and `--` comments, so that an apostrophe inside an
492
+ // identifier or a comment is not mistaken for one opening a literal
493
+ const tokens = new RegExp(`'(?:${esc}[^'])*'|\\${idOpen}[^\\${idClose}]*\\${idClose}|--[^\n]*`, 'g');
494
+ const parts = [];
495
+ for (const chunk of sql.split(separator)) {
496
+ const prev = parts.at(-1);
497
+ // whatever quote is left once the complete tokens are gone opened a literal we are still inside of
498
+ if (prev?.replace(tokens, '').includes(`'`)) {
499
+ parts[parts.length - 1] = prev + separator + chunk;
500
+ }
501
+ else {
502
+ parts.push(chunk);
503
+ }
504
+ }
505
+ return parts;
506
+ }
507
+ /** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. */
508
+ startsBatch(_statement) {
509
+ return false;
510
+ }
474
511
  async dropTableIfExists(name, schema) {
475
512
  const sql = this.helper.dropTableIfExists(name, schema);
476
513
  return this.execute(sql);
package/typings.d.ts CHANGED
@@ -114,6 +114,15 @@ export interface CheckDef<T = unknown> {
114
114
  definition?: string;
115
115
  columnName?: string;
116
116
  }
117
+ /** Resolved row level security policy for schema operations (all callbacks resolved to strings). */
118
+ export interface SqlPolicyDef {
119
+ name: string;
120
+ command: 'select' | 'insert' | 'update' | 'delete' | 'all';
121
+ type: 'permissive' | 'restrictive';
122
+ roles: string[];
123
+ using?: string;
124
+ check?: string;
125
+ }
117
126
  /** Resolved trigger definition for schema operations (all callbacks resolved to strings). */
118
127
  export interface SqlTriggerDef {
119
128
  name: string;
@@ -191,6 +200,13 @@ export interface TableDifference {
191
200
  addedTriggers: Dictionary<SqlTriggerDef>;
192
201
  changedTriggers: Dictionary<SqlTriggerDef>;
193
202
  removedTriggers: Dictionary<SqlTriggerDef>;
203
+ addedPolicies: Dictionary<SqlPolicyDef>;
204
+ /** Changed policies surface as `removedPolicies` + `addedPolicies` pairs (drop + recreate). */
205
+ removedPolicies: Dictionary<SqlPolicyDef>;
206
+ /** New RLS enable state, present only when it changed. */
207
+ changedRlsEnabled?: boolean;
208
+ /** New RLS force state, present only when it changed. */
209
+ changedRlsForced?: boolean;
194
210
  addedForeignKeys: Dictionary<ForeignKey>;
195
211
  changedForeignKeys: Dictionary<ForeignKey>;
196
212
  removedForeignKeys: Dictionary<ForeignKey>;
@@ -277,6 +293,8 @@ export interface IQueryBuilder<T> {
277
293
  with(name: string, query: AnyQueryBuilder | NativeQueryBuilder | RawQueryFragment, options?: CteOptions): this;
278
294
  withRecursive(name: string, query: AnyQueryBuilder | NativeQueryBuilder | RawQueryFragment, options?: CteOptions): this;
279
295
  scheduleFilterCheck(path: string): void;
296
+ /** @internal */
297
+ ensureTPTJoins(): void;
280
298
  withSchema(schema: string): this;
281
299
  }
282
300
  export interface ICriteriaNodeProcessOptions {
@@ -368,9 +386,9 @@ type MaybeGenerated<TValue, TOptions, TProcessOnCreate extends boolean> = TOptio
368
386
  } ? TValue | null : TOptions extends {
369
387
  autoincrement: true;
370
388
  } ? Generated<TValue> : TOptions extends {
371
- default: true;
389
+ default: unknown;
372
390
  } ? Generated<TValue> : TOptions extends {
373
- defaultRaw: true;
391
+ defaultRaw: unknown;
374
392
  } ? Generated<TValue> : TProcessOnCreate extends false ? TValue : TOptions extends {
375
393
  onCreate: Function;
376
394
  } ? Generated<TValue> : TValue;