@mikro-orm/sql 7.1.3-dev.5 → 7.1.3-dev.6

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.
@@ -97,6 +97,20 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
97
97
  private getEnumDefinitions;
98
98
  protected getCollateSQL(collation: string): string;
99
99
  createTableColumn(column: Column, table: DatabaseTable): string | undefined;
100
+ /**
101
+ * Adding partitioning to an existing table (or changing an existing definition) can't be done in place in
102
+ * PostgreSQL, so we rebuild it: park the original table in a temp schema (its indexes/constraints/sequences
103
+ * move with it, freeing the names), create the new partitioned table, copy the data across, and restore the
104
+ * foreign keys that pointed at it. In safe mode the original table is kept in the temp schema for manual
105
+ * verification; otherwise the temp schema is dropped at the end.
106
+ */
107
+ getPartitioningRebuildSQL(rebuilds: {
108
+ diff: TableDifference;
109
+ inboundForeignKeys: {
110
+ table: DatabaseTable;
111
+ foreignKey: ForeignKey;
112
+ }[];
113
+ }[], safe: boolean): string[];
100
114
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
101
115
  castColumn(name: string, type: string): string;
102
116
  dropForeignKey(tableName: string, constraintName: string): string;
@@ -723,6 +723,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
723
723
  join pg_namespace nsp2 on nsp2.oid = cls2.relnamespace
724
724
  where (${[...tablesBySchemas.entries()].map(([schema, tables]) => `(cls1.relname in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}) and nsp1.nspname = ${this.platform.quoteValue(schema)})`).join(' or ')})
725
725
  and confrelid > 0
726
+ and con.conparentid = 0
726
727
  order by nsp1.nspname, cls1.relname, constraint_name, ord`;
727
728
  const allFks = await connection.execute(sql, [], 'all', ctx);
728
729
  const ret = {};
@@ -885,13 +886,70 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
885
886
  Utils.runIfNotEmpty(() => col.push(`default ${column.default}`), useDefault);
886
887
  return col.join(' ');
887
888
  }
888
- getPreAlterTable(tableDiff, safe) {
889
- if (tableDiff.changedPartitioning) {
890
- const from = tableDiff.changedPartitioning.from?.definition;
891
- const to = tableDiff.changedPartitioning.to?.definition;
892
- const action = !from ? 'Adding' : !to ? 'Removing' : 'Changing';
893
- throw new Error(`${action} partition definitions for existing PostgreSQL tables is not supported automatically (${tableDiff.name}: '${from ?? '<none>'}' -> '${to ?? '<none>'}'); create a manual migration instead`);
889
+ /**
890
+ * Adding partitioning to an existing table (or changing an existing definition) can't be done in place in
891
+ * PostgreSQL, so we rebuild it: park the original table in a temp schema (its indexes/constraints/sequences
892
+ * move with it, freeing the names), create the new partitioned table, copy the data across, and restore the
893
+ * foreign keys that pointed at it. In safe mode the original table is kept in the temp schema for manual
894
+ * verification; otherwise the temp schema is dropped at the end.
895
+ */
896
+ getPartitioningRebuildSQL(rebuilds, safe) {
897
+ if (rebuilds.length === 0) {
898
+ return [];
899
+ }
900
+ const tmpSchema = 'mikro_orm_partition_swap';
901
+ const ret = [];
902
+ ret.push(`-- WARNING: changing partitioning rebuilds the table by copying all rows into a new partitioned table under an exclusive lock.`);
903
+ ret.push(`-- Review for data volume, locking and downtime before running.`);
904
+ ret.push(`create schema if not exists ${this.quote(tmpSchema)}`);
905
+ for (const { diff, inboundForeignKeys } of rebuilds) {
906
+ const table = diff.toTable;
907
+ const parked = `${this.quote(tmpSchema)}.${this.quote(table.name)}`;
908
+ // drop foreign keys that reference this table so it can be parked and replaced
909
+ for (const { table: localTable, foreignKey } of inboundForeignKeys) {
910
+ ret.push(`alter table ${localTable.getQuotedName()} drop constraint ${this.quote(foreignKey.constraintName)}`);
911
+ }
912
+ // move the existing table out of the way; its indexes/constraints/sequences travel with it
913
+ ret.push(`alter table ${table.getQuotedName()} set schema ${this.quote(tmpSchema)}`);
914
+ // child partitions are separate tables that don't move with the parent — park them too so their
915
+ // names are free for the new partitions
916
+ for (const partition of diff.fromTable.getPartitioning()?.partitions ?? []) {
917
+ const partitionName = this.quote(this.getTableName(partition.name, partition.schema ?? table.schema));
918
+ ret.push(`alter table ${partitionName} set schema ${this.quote(tmpSchema)}`);
919
+ }
920
+ // create the new partitioned table (+ partitions, indexes, checks)
921
+ this.append(ret, this.createTable(table, true));
922
+ // recreate the table's own foreign keys and triggers (createTable emits them separately)
923
+ if (this.options.createForeignKeyConstraints) {
924
+ for (const foreignKey of Object.values(table.getForeignKeys())) {
925
+ this.append(ret, this.createForeignKey(table, foreignKey));
926
+ }
927
+ }
928
+ for (const trigger of table.getTriggers()) {
929
+ this.append(ret, this.createTrigger(table, trigger));
930
+ }
931
+ // copy data for the columns that exist on both sides
932
+ const fromColumns = new Set(diff.fromTable.getColumns().map(col => col.name));
933
+ const columns = table
934
+ .getColumns()
935
+ .filter(col => fromColumns.has(col.name))
936
+ .map(col => this.quote(col.name))
937
+ .join(', ');
938
+ ret.push(`insert into ${table.getQuotedName()} (${columns}) select ${columns} from ${parked}`);
939
+ // restore the inbound foreign keys against the new table
940
+ for (const { table: localTable, foreignKey } of inboundForeignKeys) {
941
+ this.append(ret, this.createForeignKey(localTable, foreignKey));
942
+ }
894
943
  }
944
+ if (safe) {
945
+ ret.push(`-- safe mode: original tables kept in schema "${tmpSchema}"; drop that schema manually once the data is verified`);
946
+ }
947
+ else {
948
+ ret.push(`drop schema if exists ${this.quote(tmpSchema)} cascade`);
949
+ }
950
+ return ret;
951
+ }
952
+ getPreAlterTable(tableDiff, safe) {
895
953
  const ret = [];
896
954
  const parts = tableDiff.name.split('.');
897
955
  const tableName = parts.pop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.3-dev.5",
3
+ "version": "7.1.3-dev.6",
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
5
  "keywords": [
6
6
  "data-mapper",
@@ -53,7 +53,7 @@
53
53
  "@mikro-orm/core": "^7.1.2"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.3-dev.5"
56
+ "@mikro-orm/core": "7.1.3-dev.6"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -114,6 +114,17 @@ export declare abstract class SchemaHelper {
114
114
  createTableColumn(column: Column, table: DatabaseTable, changedProperties?: Set<string>): string | undefined;
115
115
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
116
116
  getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[];
117
+ /**
118
+ * Generates a data-preserving rebuild for tables whose partitioning was added or changed. Empty by default;
119
+ * only PostgreSQL introspects partitioning, so this is never invoked for other platforms.
120
+ */
121
+ getPartitioningRebuildSQL(rebuilds: {
122
+ diff: TableDifference;
123
+ inboundForeignKeys: {
124
+ table: DatabaseTable;
125
+ foreignKey: ForeignKey;
126
+ }[];
127
+ }[], safe: boolean): string[];
117
128
  getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
118
129
  getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;
119
130
  protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>;
@@ -580,6 +580,13 @@ export class SchemaHelper {
580
580
  getPostAlterTable(tableDiff, safe) {
581
581
  return [];
582
582
  }
583
+ /**
584
+ * Generates a data-preserving rebuild for tables whose partitioning was added or changed. Empty by default;
585
+ * only PostgreSQL introspects partitioning, so this is never invoked for other platforms.
586
+ */
587
+ getPartitioningRebuildSQL(rebuilds, safe) {
588
+ return [];
589
+ }
583
590
  getChangeColumnCommentSQL(tableName, to, schemaName) {
584
591
  return '';
585
592
  }
@@ -40,6 +40,8 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
40
40
  /**
41
41
  * We need to drop foreign keys first for all tables to allow dropping PK constraints.
42
42
  */
43
+ /** Collects foreign keys from other tables that reference `table` (self-references are excluded — those travel with the rebuild). */
44
+ private getInboundForeignKeys;
43
45
  private preAlterTable;
44
46
  /**
45
47
  * creates new database and connects to it
@@ -336,13 +336,21 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
336
336
  ret.push('');
337
337
  }
338
338
  }
339
- for (const changedTable of Object.values(schemaDiff.changedTables)) {
339
+ // Tables whose partitioning was added/changed can't be altered in place — they are rebuilt as a unit
340
+ // (the helper handles the data-preserving swap), so keep them out of the regular alter passes.
341
+ const changedTables = Object.values(schemaDiff.changedTables);
342
+ const partitioningRebuilds = changedTables
343
+ .filter(changedTable => changedTable.changedPartitioning)
344
+ .map(diff => ({ diff, inboundForeignKeys: this.getInboundForeignKeys(schemaDiff.fromSchema, diff.toTable) }));
345
+ const alteredTables = changedTables.filter(changedTable => !changedTable.changedPartitioning);
346
+ this.append(ret, this.helper.getPartitioningRebuildSQL(partitioningRebuilds, options.safe), true);
347
+ for (const changedTable of alteredTables) {
340
348
  this.append(ret, this.preAlterTable(changedTable, options.safe), true);
341
349
  }
342
- for (const changedTable of Object.values(schemaDiff.changedTables)) {
350
+ for (const changedTable of alteredTables) {
343
351
  this.append(ret, this.helper.alterTable(changedTable, options.safe), true);
344
352
  }
345
- for (const changedTable of Object.values(schemaDiff.changedTables)) {
353
+ for (const changedTable of alteredTables) {
346
354
  this.append(ret, this.helper.getPostAlterTable(changedTable, options.safe), true);
347
355
  }
348
356
  if (!options.safe && this.platform.supportsNativeEnums()) {
@@ -382,6 +390,23 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
382
390
  /**
383
391
  * We need to drop foreign keys first for all tables to allow dropping PK constraints.
384
392
  */
393
+ /** Collects foreign keys from other tables that reference `table` (self-references are excluded — those travel with the rebuild). */
394
+ getInboundForeignKeys(fromSchema, table) {
395
+ const unqualified = (name) => name.split('.').pop();
396
+ const targetName = unqualified(table.name);
397
+ const ret = [];
398
+ for (const other of fromSchema.getTables()) {
399
+ if (other.name === table.name && other.schema === table.schema) {
400
+ continue;
401
+ }
402
+ for (const foreignKey of Object.values(other.getForeignKeys())) {
403
+ if (unqualified(foreignKey.referencedTableName) === targetName) {
404
+ ret.push({ table: other, foreignKey });
405
+ }
406
+ }
407
+ }
408
+ return ret;
409
+ }
385
410
  preAlterTable(diff, safe) {
386
411
  const ret = [];
387
412
  this.append(ret, this.helper.getPreAlterTable(diff, safe));
@@ -261,10 +261,13 @@ export const getTablePartitioning = (meta, tableSchema, quoteIdentifier = id =>
261
261
  };
262
262
  /** @internal */
263
263
  export const diffPartitioning = (from, to, defaultSchema) => {
264
- if (!from && !to) {
264
+ // Metadata does not declare `partitionBy`, so partitioning is left unmanaged — never drop or alter an
265
+ // existing table's partitioning just because the entity omits it (in-place removal isn't supported anyway).
266
+ if (!to) {
265
267
  return false;
266
268
  }
267
- if (!from || !to) {
269
+ // Metadata declares partitioning the table does not have yet (adding it in place isn't supported).
270
+ if (!from) {
268
271
  return true;
269
272
  }
270
273
  if (normalizeQuotedIdentifiers(normalizePartitionDefinition(from.definition)) !==