@mikro-orm/sql 7.1.0-dev.21 → 7.1.0-dev.23

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.
@@ -50,8 +50,19 @@ export declare abstract class AbstractSqlPlatform extends Platform {
50
50
  * @internal
51
51
  */
52
52
  quoteCollation(collation: string): string;
53
- /** @internal */
54
- protected validateCollationName(collation: string): void;
53
+ /**
54
+ * PG ICU locale names include hyphens (`en-US-x-icu`) and libc locales include dots (`en_US.utf8`),
55
+ * so word-chars alone would reject valid real-world collations.
56
+ * @internal
57
+ */
58
+ validateCollationName(collation: string): void;
59
+ /**
60
+ * Whether collation names compare case-insensitively in this dialect. MySQL/MariaDB, MSSQL, and
61
+ * SQLite use case-insensitive collation identifiers; PostgreSQL stores them as case-sensitive
62
+ * names in `pg_collation` (e.g. `en-US-x-icu` is distinct from `EN-US-X-ICU`).
63
+ * @internal
64
+ */
65
+ caseInsensitiveCollationNames(): boolean;
55
66
  /** @internal */
56
67
  validateJsonPropertyName(name: string): void;
57
68
  /**
@@ -123,12 +123,25 @@ export class AbstractSqlPlatform extends Platform {
123
123
  this.validateCollationName(collation);
124
124
  return this.quoteIdentifier(collation);
125
125
  }
126
- /** @internal */
126
+ /**
127
+ * PG ICU locale names include hyphens (`en-US-x-icu`) and libc locales include dots (`en_US.utf8`),
128
+ * so word-chars alone would reject valid real-world collations.
129
+ * @internal
130
+ */
127
131
  validateCollationName(collation) {
128
- if (!/^[\w]+$/.test(collation)) {
129
- throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters.`);
132
+ if (!/^[\w\-.]+$/.test(collation)) {
133
+ throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters, hyphens, and dots.`);
130
134
  }
131
135
  }
136
+ /**
137
+ * Whether collation names compare case-insensitively in this dialect. MySQL/MariaDB, MSSQL, and
138
+ * SQLite use case-insensitive collation identifiers; PostgreSQL stores them as case-sensitive
139
+ * names in `pg_collation` (e.g. `en-US-x-icu` is distinct from `EN-US-X-ICU`).
140
+ * @internal
141
+ */
142
+ caseInsensitiveCollationNames() {
143
+ return true;
144
+ }
132
145
  /** @internal */
133
146
  validateJsonPropertyName(name) {
134
147
  if (!AbstractSqlPlatform.#JSON_PROPERTY_NAME_RE.test(name)) {
@@ -35,7 +35,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
35
35
  return sql;
36
36
  }
37
37
  getListTablesSQL() {
38
- return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema()`;
38
+ return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment, table_collation as table_collation from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema()`;
39
39
  }
40
40
  getListViewsSQL() {
41
41
  return `select table_name as view_name, nullif(table_schema, schema()) as schema_name, view_definition from information_schema.views where table_schema = schema()`;
@@ -72,6 +72,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
72
72
  for (const t of tables) {
73
73
  const key = this.getTableKey(t);
74
74
  const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
75
+ table.collation = t.table_collation ?? undefined;
75
76
  const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
76
77
  table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums[key]);
77
78
  if (triggers[key]) {
@@ -211,22 +212,25 @@ export class MySqlSchemaHelper extends SchemaHelper {
211
212
  return sql;
212
213
  }
213
214
  async getAllColumns(connection, tables, ctx) {
214
- const sql = `select table_name as table_name,
215
- nullif(table_schema, schema()) as schema_name,
216
- column_name as column_name,
217
- column_default as column_default,
218
- nullif(column_comment, '') as column_comment,
219
- is_nullable as is_nullable,
220
- data_type as data_type,
221
- column_type as column_type,
222
- column_key as column_key,
223
- extra as extra,
224
- generation_expression as generation_expression,
225
- numeric_precision as numeric_precision,
226
- numeric_scale as numeric_scale,
227
- ifnull(datetime_precision, character_maximum_length) length
228
- from information_schema.columns where table_schema = database() and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name))})
229
- order by ordinal_position`;
215
+ const sql = `select c.table_name as table_name,
216
+ nullif(c.table_schema, schema()) as schema_name,
217
+ c.column_name as column_name,
218
+ c.column_default as column_default,
219
+ nullif(c.column_comment, '') as column_comment,
220
+ c.is_nullable as is_nullable,
221
+ c.data_type as data_type,
222
+ c.column_type as column_type,
223
+ c.column_key as column_key,
224
+ c.extra as extra,
225
+ c.generation_expression as generation_expression,
226
+ c.numeric_precision as numeric_precision,
227
+ c.numeric_scale as numeric_scale,
228
+ ifnull(c.datetime_precision, c.character_maximum_length) length,
229
+ nullif(c.collation_name, t.table_collation) as collation_name
230
+ from information_schema.columns c
231
+ join information_schema.tables t on t.table_schema = c.table_schema and t.table_name = c.table_name
232
+ where c.table_schema = database() and c.table_name in (${tables.map(t => this.platform.quoteValue(t.table_name))})
233
+ order by c.ordinal_position`;
230
234
  const allColumns = await connection.execute(sql, [], 'all', ctx);
231
235
  const str = (val) => (val != null ? '' + val : val);
232
236
  const extra = (val) => val.replace(/auto_increment|default_generated|(stored|virtual) generated/i, '').trim() || undefined;
@@ -257,6 +261,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
257
261
  precision: col.numeric_precision,
258
262
  scale: col.numeric_scale,
259
263
  comment: col.column_comment,
264
+ collation: col.collation_name ?? undefined,
260
265
  extra: extra(col.extra),
261
266
  generated,
262
267
  });
@@ -417,10 +422,13 @@ export class MySqlSchemaHelper extends SchemaHelper {
417
422
  const col = this.createTableColumn(column, table, changedProperties);
418
423
  return [`alter table ${table.getQuotedName()} modify ${col}`];
419
424
  }
425
+ // MySQL MODIFY/CHANGE resets omitted column attributes to the table default, so collation must
426
+ // be re-emitted on rename and comment-only paths to preserve a non-default column collation.
420
427
  getColumnDeclarationSQL(col) {
421
428
  let ret = col.type;
422
429
  ret += col.unsigned ? ' unsigned' : '';
423
430
  ret += col.autoincrement ? ' auto_increment' : '';
431
+ ret += col.collation ? ` ${this.getCollateSQL(col.collation)}` : '';
424
432
  ret += ' ';
425
433
  ret += col.nullable ? 'null' : 'not null';
426
434
  ret += col.default ? ' default ' + col.default : '';
@@ -112,4 +112,5 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
112
112
  }[]): string;
113
113
  getJsonArrayElementPropertySQL(alias: string, property: string, type: string): string;
114
114
  getDefaultClientUrl(): string;
115
+ caseInsensitiveCollationNames(): boolean;
115
116
  }
@@ -380,4 +380,7 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
380
380
  getDefaultClientUrl() {
381
381
  return 'postgresql://postgres@127.0.0.1:5432';
382
382
  }
383
+ caseInsensitiveCollationNames() {
384
+ return false;
385
+ }
383
386
  }
@@ -67,6 +67,12 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
67
67
  /** Generates SQL to drop a PostgreSQL trigger and its associated function. */
68
68
  dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
69
69
  private getSchemaQualifiedTriggerFnName;
70
+ /**
71
+ * Resolves the real name of the implicit 'default' collation (the DB's `datcollate`),
72
+ * so the comparator can treat `@Property({ collation: '<datcollate>' })` as equivalent
73
+ * to a column that introspects as using the default.
74
+ */
75
+ getDatabaseCollation(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string | undefined>;
70
76
  getAllTriggers(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>): Promise<Dictionary<SqlTriggerDef[]>>;
71
77
  private getTriggersSQL;
72
78
  getAllForeignKeys(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<Dictionary<ForeignKey>>>;
@@ -79,6 +85,7 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
79
85
  getDropNativeEnumSQL(name: string, schema?: string): string;
80
86
  getAlterNativeEnumSQL(name: string, schema?: string, value?: string, items?: string[], oldItems?: string[]): string;
81
87
  private getEnumDefinitions;
88
+ protected getCollateSQL(collation: string): string;
82
89
  createTableColumn(column: Column, table: DatabaseTable): string | undefined;
83
90
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
84
91
  castColumn(name: string, type: string): string;
@@ -149,9 +149,11 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
149
149
  const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
150
150
  const partitionings = await this.getPartitions(connection, tablesBySchema, ctx);
151
151
  const triggers = await this.getAllTriggers(connection, tablesBySchema);
152
+ const dbCollation = await this.getDatabaseCollation(connection, ctx);
152
153
  for (const t of tables) {
153
154
  const key = this.getTableKey(t);
154
155
  const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
156
+ table.collation = dbCollation;
155
157
  const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
156
158
  const enums = this.getEnumDefinitions(checks[key] ?? []);
157
159
  if (columns[key]) {
@@ -366,10 +368,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
366
368
  is_identity,
367
369
  identity_generation,
368
370
  generation_expression,
369
- pg_catalog.col_description(pgc.oid, cols.ordinal_position::int) column_comment
371
+ pg_catalog.col_description(pgc.oid, cols.ordinal_position::int) column_comment,
372
+ coll.collname as collation_name
370
373
  from information_schema.columns cols
371
374
  join pg_class pgc on cols.table_name = pgc.relname
372
375
  join pg_attribute pga on pgc.oid = pga.attrelid and cols.column_name = pga.attname
376
+ left join pg_collation coll on pga.attcollation = coll.oid and coll.collname <> 'default'
373
377
  where (${[...tablesBySchemas.entries()].map(([schema, tables]) => `(table_schema = ${this.platform.quoteValue(schema)} and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}))`).join(' or ')})
374
378
  order by ordinal_position`;
375
379
  const allColumns = await connection.execute(sql, [], 'all', ctx);
@@ -419,6 +423,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
419
423
  ? col.generation_expression + ' stored'
420
424
  : undefined,
421
425
  comment: col.column_comment,
426
+ collation: col.collation_name ?? undefined,
422
427
  };
423
428
  let enumKey = column.type;
424
429
  let enumEntry = nativeEnums?.[enumKey];
@@ -504,6 +509,15 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
504
509
  }
505
510
  return this.platform.quoteIdentifier(rawName);
506
511
  }
512
+ /**
513
+ * Resolves the real name of the implicit 'default' collation (the DB's `datcollate`),
514
+ * so the comparator can treat `@Property({ collation: '<datcollate>' })` as equivalent
515
+ * to a column that introspects as using the default.
516
+ */
517
+ async getDatabaseCollation(connection, ctx) {
518
+ const [row] = await connection.execute(`select datcollate as collation from pg_database where datname = current_database()`, [], 'all', ctx);
519
+ return row?.collation;
520
+ }
507
521
  async getAllTriggers(connection, tablesBySchemas) {
508
522
  const sql = this.getTriggersSQL(tablesBySchemas);
509
523
  const allTriggers = await connection.execute(sql);
@@ -694,6 +708,9 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
694
708
  return o;
695
709
  }, {});
696
710
  }
711
+ getCollateSQL(collation) {
712
+ return `collate ${this.platform.quoteCollation(collation)}`;
713
+ }
697
714
  createTableColumn(column, table) {
698
715
  const pk = table.getPrimaryKey();
699
716
  const compositePK = pk?.composite;
@@ -726,6 +743,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
726
743
  columnType += ` generated always as ${column.generated}`;
727
744
  }
728
745
  col.push(columnType);
746
+ Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
729
747
  Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
730
748
  Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable);
731
749
  }
@@ -165,6 +165,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
165
165
  col.push(`check (${checks[check].expression})`);
166
166
  checks.splice(check, 1);
167
167
  }
168
+ Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
168
169
  Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
169
170
  Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated);
170
171
  Utils.runIfNotEmpty(() => col.push('primary key'), column.primary);
@@ -291,6 +292,17 @@ export class SqliteSchemaHelper extends SchemaHelper {
291
292
  generated = `${match[2]} ${storage}`;
292
293
  }
293
294
  }
295
+ // Strip string literals first (their contents could contain unbalanced parens), then
296
+ // repeatedly strip the innermost balanced `(...)` until none remain — a single pass would
297
+ // only remove the innermost level, leaving `collate` tokens inside nested CHECK/default
298
+ // expressions exposed to the column-collation regex.
299
+ let cleanDef = (columnDefinitions[col.name]?.definition ?? '').replace(/'[^']*'/g, '').replace(/"[^"]*"/g, '');
300
+ let prev;
301
+ do {
302
+ prev = cleanDef;
303
+ cleanDef = cleanDef.replace(/\([^()]*\)/g, '');
304
+ } while (cleanDef !== prev);
305
+ const collationMatch = /\bcollate\s+([`"']?)([\w\-.]+)\1/i.exec(cleanDef);
294
306
  return {
295
307
  name: col.name,
296
308
  type: col.type,
@@ -301,6 +313,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
301
313
  unsigned: false,
302
314
  autoincrement: !composite && col.pk && this.platform.isNumericColumn(mappedType) && hasAutoincrement,
303
315
  generated,
316
+ collation: collationMatch ? collationMatch[2] : undefined,
304
317
  };
305
318
  });
306
319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.0-dev.21",
3
+ "version": "7.1.0-dev.23",
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",
@@ -50,10 +50,10 @@
50
50
  "kysely": "0.28.16"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.0.12"
53
+ "@mikro-orm/core": "^7.0.13"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.0-dev.21"
56
+ "@mikro-orm/core": "7.1.0-dev.23"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -16,6 +16,13 @@ export declare class DatabaseTable {
16
16
  }>;
17
17
  comment?: string;
18
18
  partitioning?: TablePartitioning;
19
+ /**
20
+ * Effective collation the column defaults to when no explicit `COLLATE` is set on a column.
21
+ * For MySQL/MariaDB this is the table collation; for PostgreSQL and MSSQL this is the database default;
22
+ * SQLite has no configurable default. Used by `SchemaComparator.diffCollation` to avoid flapping
23
+ * when a property explicitly names the default collation.
24
+ */
25
+ collation?: string;
19
26
  constructor(platform: AbstractSqlPlatform, name: string, schema?: string | undefined);
20
27
  getQuotedName(): string;
21
28
  getColumns(): Column[];
@@ -15,6 +15,13 @@ export class DatabaseTable {
15
15
  nativeEnums = {}; // for postgres
16
16
  comment;
17
17
  partitioning;
18
+ /**
19
+ * Effective collation the column defaults to when no explicit `COLLATE` is set on a column.
20
+ * For MySQL/MariaDB this is the table collation; for PostgreSQL and MSSQL this is the database default;
21
+ * SQLite has no configurable default. Used by `SchemaComparator.diffCollation` to avoid flapping
22
+ * when a property explicitly names the default collation.
23
+ */
24
+ collation;
18
25
  constructor(platform, name, schema) {
19
26
  this.name = name;
20
27
  this.schema = schema;
@@ -130,6 +137,7 @@ export class DatabaseTable {
130
137
  default: prop.defaultRaw,
131
138
  enumItems: prop.nativeEnumName || prop.items?.every(i => typeof i === 'string') ? prop.items : undefined,
132
139
  comment: prop.comment,
140
+ collation: prop.collation,
133
141
  extra: prop.extra,
134
142
  ignoreSchemaChanges: prop.ignoreSchemaChanges,
135
143
  };
@@ -665,6 +673,7 @@ export class DatabaseTable {
665
673
  columnOptions.scale = column.scale;
666
674
  columnOptions.extra = column.extra;
667
675
  columnOptions.comment = column.comment;
676
+ columnOptions.collation = column.collation;
668
677
  columnOptions.enum = !!column.enumItems?.length;
669
678
  columnOptions.items = column.enumItems;
670
679
  }
@@ -731,6 +740,7 @@ export class DatabaseTable {
731
740
  scale: column.scale,
732
741
  extra: column.extra,
733
742
  comment: column.comment,
743
+ collation: column.collation,
734
744
  index: index ? index.keyName : undefined,
735
745
  unique: unique ? unique.keyName : undefined,
736
746
  enum: !!column.enumItems?.length,
@@ -985,6 +995,7 @@ export class DatabaseTable {
985
995
  scale: fixedPrecision ? null : (c.scale ?? null),
986
996
  default: c.default ?? null,
987
997
  comment: c.comment ?? null,
998
+ collation: c.collation ?? null,
988
999
  enumItems: c.enumItems ?? [],
989
1000
  mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
990
1001
  };
@@ -39,6 +39,15 @@ export declare class SchemaComparator {
39
39
  diffColumn(fromColumn: Column, toColumn: Column, fromTable: DatabaseTable, logging?: boolean): Set<string>;
40
40
  diffEnumItems(items1?: string[], items2?: string[]): boolean;
41
41
  diffComment(comment1?: string, comment2?: string): boolean;
42
+ /**
43
+ * `from` is the introspected DB state, `to` is the target metadata. A column-level `COLLATE`
44
+ * clause naming the table/database default is just verbose syntax for inheriting that default,
45
+ * so both sides are normalized — anything matching `tableDefault` collapses to `undefined` and
46
+ * compares equal to "no explicit collation". Comparison is case-insensitive on dialects that
47
+ * treat collation identifiers as case-insensitive (MySQL/MSSQL/SQLite); PostgreSQL's
48
+ * `pg_collation.collname` is case-sensitive and is compared verbatim.
49
+ */
50
+ diffCollation(fromCollation?: string, toCollation?: string, tableDefault?: string): boolean;
42
51
  /**
43
52
  * Finds the difference between the indexes index1 and index2.
44
53
  * Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
@@ -571,6 +571,11 @@ export class SchemaComparator {
571
571
  log(`'comment' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
572
572
  changedProperties.add('comment');
573
573
  }
574
+ if (!(fromColumn.ignoreSchemaChanges?.includes('collation') || toColumn.ignoreSchemaChanges?.includes('collation')) &&
575
+ this.diffCollation(fromColumn.collation, toColumn.collation, fromTable.collation)) {
576
+ log(`'collation' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
577
+ changedProperties.add('collation');
578
+ }
574
579
  const isNonNativeEnumArray = !(fromColumn.nativeEnumName || toColumn.nativeEnumName) &&
575
580
  (fromColumn.mappedType instanceof ArrayType || toColumn.mappedType instanceof ArrayType);
576
581
  if (!isNonNativeEnumArray && this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)) {
@@ -592,6 +597,19 @@ export class SchemaComparator {
592
597
  // eslint-disable-next-line eqeqeq
593
598
  return comment1 != comment2 && !(comment1 == null && comment2 === '') && !(comment2 == null && comment1 === '');
594
599
  }
600
+ /**
601
+ * `from` is the introspected DB state, `to` is the target metadata. A column-level `COLLATE`
602
+ * clause naming the table/database default is just verbose syntax for inheriting that default,
603
+ * so both sides are normalized — anything matching `tableDefault` collapses to `undefined` and
604
+ * compares equal to "no explicit collation". Comparison is case-insensitive on dialects that
605
+ * treat collation identifiers as case-insensitive (MySQL/MSSQL/SQLite); PostgreSQL's
606
+ * `pg_collation.collname` is case-sensitive and is compared verbatim.
607
+ */
608
+ diffCollation(fromCollation, toCollation, tableDefault) {
609
+ const fold = this.#platform.caseInsensitiveCollationNames() ? (s) => s.toLowerCase() : (s) => s;
610
+ const norm = (c) => c && tableDefault && fold(c) === fold(tableDefault) ? undefined : c == null ? undefined : fold(c);
611
+ return norm(fromCollation) !== norm(toCollation);
612
+ }
595
613
  /**
596
614
  * Finds the difference between the indexes index1 and index2.
597
615
  * Compares index1 with index2 and returns index2 if there are any differences or false in case there are no differences.
@@ -97,6 +97,8 @@ export declare abstract class SchemaHelper {
97
97
  hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean;
98
98
  castColumn(name: string, type: string): string;
99
99
  alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
100
+ /** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
101
+ protected getCollateSQL(collation: string): string;
100
102
  createTableColumn(column: Column, table: DatabaseTable, changedProperties?: Set<string>): string | undefined;
101
103
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
102
104
  getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[];
@@ -381,7 +381,7 @@ export class SchemaHelper {
381
381
  this.append(ret, this.alterTableColumn(column, diff.fromTable, changedProperties));
382
382
  }
383
383
  for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff => diff.changedProperties.has('comment'))) {
384
- if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems'].some(t => changedProperties.has(t))) {
384
+ if (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
385
385
  continue; // will be handled via column update
386
386
  }
387
387
  ret.push(this.getChangeColumnCommentSQL(tableName, column, schemaName));
@@ -458,7 +458,7 @@ export class SchemaHelper {
458
458
  if (changedProperties.has('default') && column.default == null) {
459
459
  sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} drop default`);
460
460
  }
461
- if (changedProperties.has('type')) {
461
+ if (changedProperties.has('type') || changedProperties.has('collation')) {
462
462
  let type = column.type + (column.generated ? ` generated always as ${column.generated}` : '');
463
463
  if (column.nativeEnumName) {
464
464
  const parts = type.split('.');
@@ -470,7 +470,8 @@ export class SchemaHelper {
470
470
  }
471
471
  type = this.quote(type);
472
472
  }
473
- sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} type ${type + this.castColumn(column.name, type)}`);
473
+ const collateClause = column.collation ? ` ${this.getCollateSQL(column.collation)}` : '';
474
+ sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} type ${type + collateClause + this.castColumn(column.name, type)}`);
474
475
  }
475
476
  if (changedProperties.has('default') && column.default != null) {
476
477
  sql.push(`alter table ${table.getQuotedName()} alter column ${this.quote(column.name)} set default ${column.default}`);
@@ -481,6 +482,11 @@ export class SchemaHelper {
481
482
  }
482
483
  return sql;
483
484
  }
485
+ /** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
486
+ getCollateSQL(collation) {
487
+ this.platform.validateCollationName(collation);
488
+ return `collate ${collation}`;
489
+ }
484
490
  createTableColumn(column, table, changedProperties) {
485
491
  const compositePK = table.getPrimaryKey()?.composite;
486
492
  const primaryKey = !changedProperties && !this.hasNonDefaultPrimaryKeyName(table);
@@ -488,6 +494,7 @@ export class SchemaHelper {
488
494
  const useDefault = column.default != null && column.default !== 'null' && !column.autoincrement;
489
495
  const col = [this.quote(column.name), columnType];
490
496
  Utils.runIfNotEmpty(() => col.push('unsigned'), column.unsigned && this.platform.supportsUnsigned());
497
+ Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
491
498
  Utils.runIfNotEmpty(() => col.push('null'), column.nullable);
492
499
  Utils.runIfNotEmpty(() => col.push('not null'), !column.nullable && !column.generated);
493
500
  Utils.runIfNotEmpty(() => col.push('auto_increment'), column.autoincrement);
package/typings.d.ts CHANGED
@@ -44,6 +44,7 @@ export interface Column {
44
44
  default?: string | null;
45
45
  defaultConstraint?: string;
46
46
  comment?: string;
47
+ collation?: string;
47
48
  generated?: string;
48
49
  nativeEnumName?: string;
49
50
  enumItems?: string[];
@@ -51,7 +52,7 @@ export interface Column {
51
52
  unique?: boolean;
52
53
  /** mysql only */
53
54
  extra?: string;
54
- ignoreSchemaChanges?: ('type' | 'extra' | 'default')[];
55
+ ignoreSchemaChanges?: ('type' | 'extra' | 'default' | 'collation')[];
55
56
  }
56
57
  export interface ForeignKey {
57
58
  columnNames: string[];