@mikro-orm/sql 7.1.0-dev.21 → 7.1.0-dev.22
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.
- package/AbstractSqlPlatform.d.ts +13 -2
- package/AbstractSqlPlatform.js +16 -3
- package/dialects/mysql/MySqlSchemaHelper.js +25 -17
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +1 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +3 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +7 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +19 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +13 -0
- package/package.json +2 -2
- package/schema/DatabaseTable.d.ts +7 -0
- package/schema/DatabaseTable.js +11 -0
- package/schema/SchemaComparator.d.ts +9 -0
- package/schema/SchemaComparator.js +18 -0
- package/schema/SchemaHelper.d.ts +2 -0
- package/schema/SchemaHelper.js +10 -3
- package/typings.d.ts +2 -1
package/AbstractSqlPlatform.d.ts
CHANGED
|
@@ -50,8 +50,19 @@ export declare abstract class AbstractSqlPlatform extends Platform {
|
|
|
50
50
|
* @internal
|
|
51
51
|
*/
|
|
52
52
|
quoteCollation(collation: string): string;
|
|
53
|
-
/**
|
|
54
|
-
|
|
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
|
/**
|
package/AbstractSqlPlatform.js
CHANGED
|
@@ -123,12 +123,25 @@ export class AbstractSqlPlatform extends Platform {
|
|
|
123
123
|
this.validateCollationName(collation);
|
|
124
124
|
return this.quoteIdentifier(collation);
|
|
125
125
|
}
|
|
126
|
-
/**
|
|
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
|
-
|
|
229
|
-
|
|
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
|
}
|
|
@@ -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.
|
|
3
|
+
"version": "7.1.0-dev.22",
|
|
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.0.12"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.1.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.1.0-dev.22"
|
|
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[];
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -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.
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -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[];
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -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
|
-
|
|
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[];
|