@mikro-orm/sql 7.1.0-dev.20 → 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 +2 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +21 -3
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +7 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +19 -1
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +2 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +30 -17
- package/package.json +2 -2
- package/schema/DatabaseSchema.js +8 -4
- package/schema/DatabaseTable.d.ts +7 -0
- package/schema/DatabaseTable.js +133 -29
- package/schema/SchemaComparator.d.ts +9 -0
- package/schema/SchemaComparator.js +18 -0
- package/schema/SchemaHelper.d.ts +2 -0
- package/schema/SchemaHelper.js +11 -4
- 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 : '';
|
|
@@ -47,6 +47,7 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
47
47
|
precision?: number;
|
|
48
48
|
scale?: number;
|
|
49
49
|
autoincrement?: boolean;
|
|
50
|
+
columnTypes?: string[];
|
|
50
51
|
}): string;
|
|
51
52
|
getMappedType(type: string): Type<unknown>;
|
|
52
53
|
getRegExpOperator(val?: unknown, flags?: string): string;
|
|
@@ -111,4 +112,5 @@ export declare class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
111
112
|
}[]): string;
|
|
112
113
|
getJsonArrayElementPropertySQL(alias: string, property: string, type: string): string;
|
|
113
114
|
getDefaultClientUrl(): string;
|
|
115
|
+
caseInsensitiveCollationNames(): boolean;
|
|
114
116
|
}
|
|
@@ -93,18 +93,27 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
93
93
|
}
|
|
94
94
|
normalizeColumnType(type, options) {
|
|
95
95
|
const simpleType = this.extractSimpleType(type);
|
|
96
|
-
if (['int', 'int4', 'integer'].includes(simpleType)) {
|
|
96
|
+
if (['int', 'int4', 'integer', 'serial'].includes(simpleType)) {
|
|
97
97
|
return this.getIntegerTypeDeclarationSQL({});
|
|
98
98
|
}
|
|
99
|
-
if (['bigint', 'int8'].includes(simpleType)) {
|
|
99
|
+
if (['bigint', 'int8', 'bigserial'].includes(simpleType)) {
|
|
100
100
|
return this.getBigIntTypeDeclarationSQL({});
|
|
101
101
|
}
|
|
102
|
-
if (['smallint', 'int2'].includes(simpleType)) {
|
|
102
|
+
if (['smallint', 'int2', 'smallserial'].includes(simpleType)) {
|
|
103
103
|
return this.getSmallIntTypeDeclarationSQL({});
|
|
104
104
|
}
|
|
105
105
|
if (['boolean', 'bool'].includes(simpleType)) {
|
|
106
106
|
return this.getBooleanTypeDeclarationSQL();
|
|
107
107
|
}
|
|
108
|
+
if (['double', 'double precision', 'float8'].includes(simpleType)) {
|
|
109
|
+
return this.getDoubleDeclarationSQL();
|
|
110
|
+
}
|
|
111
|
+
if (['real', 'float4'].includes(simpleType)) {
|
|
112
|
+
return this.getFloatDeclarationSQL();
|
|
113
|
+
}
|
|
114
|
+
if (['timestamptz', 'timestamp with time zone'].includes(simpleType)) {
|
|
115
|
+
return this.getDateTimeTypeDeclarationSQL(options);
|
|
116
|
+
}
|
|
108
117
|
if (['varchar', 'character varying'].includes(simpleType)) {
|
|
109
118
|
return this.getVarcharTypeDeclarationSQL(options);
|
|
110
119
|
}
|
|
@@ -117,6 +126,12 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
117
126
|
if (['interval'].includes(simpleType)) {
|
|
118
127
|
return this.getIntervalTypeDeclarationSQL(options);
|
|
119
128
|
}
|
|
129
|
+
// TimeType.getColumnType drops the timezone qualifier, so detect tz aliases from the original column type.
|
|
130
|
+
const originalType = options.columnTypes?.[0]?.toLowerCase() ?? type;
|
|
131
|
+
if (/^timetz\b/.test(originalType) || /^time\s+with\s+time\s+zone\b/.test(originalType)) {
|
|
132
|
+
const length = options.length ?? this.getDefaultDateTimeLength();
|
|
133
|
+
return `timetz(${length})`;
|
|
134
|
+
}
|
|
120
135
|
return super.normalizeColumnType(type, options);
|
|
121
136
|
}
|
|
122
137
|
getMappedType(type) {
|
|
@@ -365,4 +380,7 @@ export class BasePostgreSqlPlatform extends AbstractSqlPlatform {
|
|
|
365
380
|
getDefaultClientUrl() {
|
|
366
381
|
return 'postgresql://postgres@127.0.0.1:5432';
|
|
367
382
|
}
|
|
383
|
+
caseInsensitiveCollationNames() {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
368
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
|
}
|
|
@@ -15,6 +15,7 @@ export declare class SqlitePlatform extends AbstractSqlPlatform {
|
|
|
15
15
|
getDateTimeTypeDeclarationSQL(column: {
|
|
16
16
|
length: number;
|
|
17
17
|
}): string;
|
|
18
|
+
getDefaultVersionLength(): number;
|
|
18
19
|
getBeginTransactionSQL(options?: {
|
|
19
20
|
isolationLevel?: IsolationLevel;
|
|
20
21
|
readOnly?: boolean;
|
|
@@ -24,6 +24,10 @@ export class SqlitePlatform extends AbstractSqlPlatform {
|
|
|
24
24
|
getDateTimeTypeDeclarationSQL(column) {
|
|
25
25
|
return 'datetime';
|
|
26
26
|
}
|
|
27
|
+
// sqlite's datetime DDL drops precision and the current-ts expression hardcodes ms scaling
|
|
28
|
+
getDefaultVersionLength() {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
27
31
|
getBeginTransactionSQL(options) {
|
|
28
32
|
return ['begin'];
|
|
29
33
|
}
|
|
@@ -45,7 +45,8 @@ export declare class SqliteSchemaHelper extends SchemaHelper {
|
|
|
45
45
|
* We need to add them back so they match what we generate in DDL.
|
|
46
46
|
*/
|
|
47
47
|
private wrapExpressionDefault;
|
|
48
|
-
|
|
48
|
+
/** Extract enum values from `IN (…)` CHECKs only — a `!= 'x'` check would otherwise be misread as a one-item enum. */
|
|
49
|
+
private extractEnumValuesFromChecks;
|
|
49
50
|
getPrimaryKeys(connection: AbstractSqlConnection, indexes: IndexDef[], tableName: string, schemaName?: string, ctx?: Transaction): Promise<string[]>;
|
|
50
51
|
private getIndexes;
|
|
51
52
|
private getChecks;
|
|
@@ -101,7 +101,7 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
101
101
|
const checks = await this.getChecks(connection, table.name, table.schema, ctx);
|
|
102
102
|
const pks = await this.getPrimaryKeys(connection, indexes, table.name, table.schema, ctx);
|
|
103
103
|
const fks = await this.getForeignKeys(connection, table.name, table.schema, ctx);
|
|
104
|
-
const enums =
|
|
104
|
+
const enums = this.extractEnumValuesFromChecks(checks);
|
|
105
105
|
const triggers = await this.getTableTriggers(connection, table.name);
|
|
106
106
|
table.init(cols, indexes, checks, pks, fks, enums);
|
|
107
107
|
table.setTriggers(triggers);
|
|
@@ -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
|
}
|
|
@@ -327,23 +340,23 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
327
340
|
// everything else is an expression that had its outer parens stripped
|
|
328
341
|
return `(${value})`;
|
|
329
342
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// check constraints are defined as (note that last closing paren is missing):
|
|
337
|
-
// `type` text check (`type` in ('local', 'global')
|
|
338
|
-
const match = /[`["']([^`\]"']+)[`\]"'] text check \(.* \((.*)\)/i.exec(item);
|
|
339
|
-
/* v8 ignore next */
|
|
340
|
-
if (match) {
|
|
341
|
-
o[match[1]] = match[2]
|
|
342
|
-
.split(/,(?=\s*'(?:[^']|'')*'(?:\s*\)|$))/)
|
|
343
|
-
.map((item) => /^\(?'((?:[^']|'')*)'/.exec(item.trim())[1].replace(/''/g, "'"));
|
|
343
|
+
/** Extract enum values from `IN (…)` CHECKs only — a `!= 'x'` check would otherwise be misread as a one-item enum. */
|
|
344
|
+
extractEnumValuesFromChecks(checks) {
|
|
345
|
+
const result = {};
|
|
346
|
+
for (const check of checks) {
|
|
347
|
+
if (!check.columnName || typeof check.expression !== 'string') {
|
|
348
|
+
continue;
|
|
344
349
|
}
|
|
345
|
-
|
|
346
|
-
|
|
350
|
+
const inClause = /\bin\s*\(([^)]*)\)/i.exec(check.expression);
|
|
351
|
+
if (!inClause) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const items = [...inClause[1].matchAll(/'((?:[^']|'')*)'/g)].map(m => m[1].replace(/''/g, "'"));
|
|
355
|
+
if (items.length > 0) {
|
|
356
|
+
result[check.columnName] = items;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
347
360
|
}
|
|
348
361
|
async getPrimaryKeys(connection, indexes, tableName, schemaName, ctx) {
|
|
349
362
|
const prefix = this.getSchemaPrefix(schemaName);
|
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"
|
package/schema/DatabaseSchema.js
CHANGED
|
@@ -341,12 +341,16 @@ export class DatabaseSchema {
|
|
|
341
341
|
(prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner));
|
|
342
342
|
}
|
|
343
343
|
toJSON() {
|
|
344
|
+
// locale-independent comparison so the snapshot is stable across machines
|
|
345
|
+
const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
346
|
+
const tableKey = (t) => `${t.schema ?? ''}.${t.name}`;
|
|
347
|
+
const byTable = (a, b) => byString(tableKey(a), tableKey(b));
|
|
344
348
|
return {
|
|
345
349
|
name: this.name,
|
|
346
|
-
namespaces: [...this.#namespaces],
|
|
347
|
-
tables: this.#tables,
|
|
348
|
-
views: this.#views,
|
|
349
|
-
nativeEnums: this.#nativeEnums,
|
|
350
|
+
namespaces: [...this.#namespaces].sort(),
|
|
351
|
+
tables: [...this.#tables].sort(byTable),
|
|
352
|
+
views: [...this.#views].sort(byTable),
|
|
353
|
+
nativeEnums: Object.fromEntries(Object.entries(this.#nativeEnums).sort(([a], [b]) => byString(a, b))),
|
|
350
354
|
};
|
|
351
355
|
}
|
|
352
356
|
prune(schema, wildcardSchemaTables) {
|
|
@@ -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;
|
|
@@ -68,6 +75,7 @@ export class DatabaseTable {
|
|
|
68
75
|
this.#indexes = indexes;
|
|
69
76
|
this.#checks = checks;
|
|
70
77
|
this.#foreignKeys = fks;
|
|
78
|
+
const helper = this.#platform.getSchemaHelper();
|
|
71
79
|
this.#columns = cols.reduce((o, v) => {
|
|
72
80
|
const index = indexes.filter(i => i.columnNames[0] === v.name);
|
|
73
81
|
v.primary = v.primary || pks.includes(v.name);
|
|
@@ -76,6 +84,11 @@ export class DatabaseTable {
|
|
|
76
84
|
v.mappedType = this.#platform.getMappedType(type);
|
|
77
85
|
v.default = v.default?.toString().startsWith('nextval(') ? null : v.default;
|
|
78
86
|
v.enumItems ??= enums[v.name] || [];
|
|
87
|
+
// recover length from the declared type so introspection matches `addColumnFromProperty`;
|
|
88
|
+
// scoped to types with `getDefaultLength` to skip mysql's `tinyint(1)` boolean width
|
|
89
|
+
if (v.length == null && v.type && helper && typeof v.mappedType.getDefaultLength !== 'undefined') {
|
|
90
|
+
v.length = helper.inferLengthFromColumnType(v.type);
|
|
91
|
+
}
|
|
79
92
|
o[v.name] = v;
|
|
80
93
|
return o;
|
|
81
94
|
}, {});
|
|
@@ -85,7 +98,9 @@ export class DatabaseTable {
|
|
|
85
98
|
}
|
|
86
99
|
addColumnFromProperty(prop, meta, config) {
|
|
87
100
|
prop.fieldNames?.forEach((field, idx) => {
|
|
88
|
-
|
|
101
|
+
// numeric enums fall through to the underlying numeric type — no platform emits a CHECK we could parse back
|
|
102
|
+
const isStringEnum = !!prop.nativeEnumName || !!prop.items?.every(item => typeof item === 'string');
|
|
103
|
+
const type = prop.enum && isStringEnum ? 'enum' : prop.columnTypes[idx];
|
|
89
104
|
const mappedType = this.#platform.getMappedType(type);
|
|
90
105
|
if (mappedType instanceof DecimalType) {
|
|
91
106
|
const match = /\w+\((\d+), ?(\d+)\)/.exec(prop.columnTypes[idx]);
|
|
@@ -122,6 +137,7 @@ export class DatabaseTable {
|
|
|
122
137
|
default: prop.defaultRaw,
|
|
123
138
|
enumItems: prop.nativeEnumName || prop.items?.every(i => typeof i === 'string') ? prop.items : undefined,
|
|
124
139
|
comment: prop.comment,
|
|
140
|
+
collation: prop.collation,
|
|
125
141
|
extra: prop.extra,
|
|
126
142
|
ignoreSchemaChanges: prop.ignoreSchemaChanges,
|
|
127
143
|
};
|
|
@@ -657,6 +673,7 @@ export class DatabaseTable {
|
|
|
657
673
|
columnOptions.scale = column.scale;
|
|
658
674
|
columnOptions.extra = column.extra;
|
|
659
675
|
columnOptions.comment = column.comment;
|
|
676
|
+
columnOptions.collation = column.collation;
|
|
660
677
|
columnOptions.enum = !!column.enumItems?.length;
|
|
661
678
|
columnOptions.items = column.enumItems;
|
|
662
679
|
}
|
|
@@ -723,6 +740,7 @@ export class DatabaseTable {
|
|
|
723
740
|
scale: column.scale,
|
|
724
741
|
extra: column.extra,
|
|
725
742
|
comment: column.comment,
|
|
743
|
+
collation: column.collation,
|
|
726
744
|
index: index ? index.keyName : undefined,
|
|
727
745
|
unique: unique ? unique.keyName : undefined,
|
|
728
746
|
enum: !!column.enumItems?.length,
|
|
@@ -933,52 +951,138 @@ export class DatabaseTable {
|
|
|
933
951
|
}
|
|
934
952
|
toJSON() {
|
|
935
953
|
const columns = this.#columns;
|
|
936
|
-
|
|
954
|
+
// locale-independent comparison so the snapshot is stable across machines
|
|
955
|
+
const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
956
|
+
const sortedColumnKeys = Utils.keys(columns).sort(byString);
|
|
957
|
+
// mirror `DatabaseTable.init()`: derive `primary`/`unique` from index membership
|
|
958
|
+
// so metadata (which keeps `primary: false` on composite PK columns) and introspection agree
|
|
959
|
+
const primaryColumns = new Set();
|
|
960
|
+
const uniqueColumns = new Set();
|
|
961
|
+
for (const idx of this.#indexes) {
|
|
962
|
+
if (idx.primary) {
|
|
963
|
+
idx.columnNames.forEach(c => primaryColumns.add(c));
|
|
964
|
+
}
|
|
965
|
+
if (idx.unique && !idx.primary && idx.columnNames.length > 0) {
|
|
966
|
+
uniqueColumns.add(idx.columnNames[0]);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
// integer/float widths live in the type name (`int2`/`int4`/`float8`) — drop redundant precision/scale
|
|
970
|
+
const isFixedPrecisionFamily = (mappedType) => mappedType instanceof t.integer ||
|
|
971
|
+
mappedType instanceof t.smallint ||
|
|
972
|
+
mappedType instanceof t.tinyint ||
|
|
973
|
+
mappedType instanceof t.mediumint ||
|
|
974
|
+
mappedType instanceof t.bigint ||
|
|
975
|
+
mappedType instanceof t.float ||
|
|
976
|
+
mappedType instanceof t.double;
|
|
977
|
+
const supportsUnsigned = this.#platform.supportsUnsigned();
|
|
978
|
+
const columnsMapped = sortedColumnKeys.reduce((o, col) => {
|
|
937
979
|
const c = columns[col];
|
|
980
|
+
// omit `autoincrement` from options so `serial` (metadata) and `int4` (introspection) collapse the same
|
|
981
|
+
const rawType = c.type?.toLowerCase();
|
|
982
|
+
const normOptions = { length: c.length, precision: c.precision, scale: c.scale };
|
|
983
|
+
const type = this.#platform.normalizeColumnType(c.type ?? '', normOptions)?.toLowerCase() || rawType;
|
|
984
|
+
const fixedPrecision = isFixedPrecisionFamily(c.mappedType);
|
|
938
985
|
const normalized = {
|
|
939
986
|
name: c.name,
|
|
940
|
-
type
|
|
941
|
-
unsigned: !!c.unsigned,
|
|
987
|
+
type,
|
|
988
|
+
unsigned: supportsUnsigned && !!c.unsigned,
|
|
942
989
|
autoincrement: !!c.autoincrement,
|
|
943
|
-
primary: !!c.primary,
|
|
990
|
+
primary: primaryColumns.has(c.name) || !!c.primary,
|
|
944
991
|
nullable: !!c.nullable,
|
|
945
|
-
unique: !!c.unique,
|
|
946
|
-
length: c.length
|
|
947
|
-
precision: c.precision ?? null,
|
|
948
|
-
scale: c.scale ?? null,
|
|
992
|
+
unique: uniqueColumns.has(c.name) || !!c.unique,
|
|
993
|
+
length: c.length || null,
|
|
994
|
+
precision: fixedPrecision ? null : (c.precision ?? null),
|
|
995
|
+
scale: fixedPrecision ? null : (c.scale ?? null),
|
|
949
996
|
default: c.default ?? null,
|
|
950
997
|
comment: c.comment ?? null,
|
|
998
|
+
collation: c.collation ?? null,
|
|
951
999
|
enumItems: c.enumItems ?? [],
|
|
952
1000
|
mappedType: Utils.keys(t).find(k => t[k] === c.mappedType.constructor),
|
|
953
1001
|
};
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
normalized.ignoreSchemaChanges = c.ignoreSchemaChanges;
|
|
965
|
-
}
|
|
966
|
-
if (c.defaultConstraint) {
|
|
967
|
-
normalized.defaultConstraint = c.defaultConstraint;
|
|
1002
|
+
for (const field of [
|
|
1003
|
+
'generated',
|
|
1004
|
+
'nativeEnumName',
|
|
1005
|
+
'extra',
|
|
1006
|
+
'ignoreSchemaChanges',
|
|
1007
|
+
'defaultConstraint',
|
|
1008
|
+
]) {
|
|
1009
|
+
if (c[field]) {
|
|
1010
|
+
normalized[field] = c[field];
|
|
1011
|
+
}
|
|
968
1012
|
}
|
|
969
1013
|
o[col] = normalized;
|
|
970
1014
|
return o;
|
|
971
1015
|
}, {});
|
|
1016
|
+
const normalizeIndex = (idx) => {
|
|
1017
|
+
const out = {
|
|
1018
|
+
columnNames: idx.columnNames,
|
|
1019
|
+
composite: !!idx.composite,
|
|
1020
|
+
// PK indexes are always backed by a constraint — force it so postgres introspection matches
|
|
1021
|
+
constraint: !!idx.constraint || !!idx.primary,
|
|
1022
|
+
keyName: idx.keyName,
|
|
1023
|
+
primary: !!idx.primary,
|
|
1024
|
+
unique: !!idx.unique,
|
|
1025
|
+
};
|
|
1026
|
+
const optional = [
|
|
1027
|
+
'expression',
|
|
1028
|
+
'type',
|
|
1029
|
+
'deferMode',
|
|
1030
|
+
'columns',
|
|
1031
|
+
'include',
|
|
1032
|
+
'fillFactor',
|
|
1033
|
+
'invisible',
|
|
1034
|
+
'disabled',
|
|
1035
|
+
'clustered',
|
|
1036
|
+
];
|
|
1037
|
+
for (const field of optional) {
|
|
1038
|
+
if (idx[field] != null && idx[field] !== false) {
|
|
1039
|
+
out[field] = idx[field];
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return out;
|
|
1043
|
+
};
|
|
1044
|
+
const normalizeFk = (fk) => {
|
|
1045
|
+
const isNoAction = (rule) => !rule || rule.toLowerCase() === 'no action';
|
|
1046
|
+
// JSON.stringify drops undefined properties — let them through instead of guarding
|
|
1047
|
+
return {
|
|
1048
|
+
columnNames: fk.columnNames,
|
|
1049
|
+
constraintName: fk.constraintName,
|
|
1050
|
+
localTableName: fk.localTableName,
|
|
1051
|
+
referencedColumnNames: fk.referencedColumnNames,
|
|
1052
|
+
referencedTableName: fk.referencedTableName,
|
|
1053
|
+
updateRule: isNoAction(fk.updateRule) ? undefined : fk.updateRule,
|
|
1054
|
+
deleteRule: isNoAction(fk.deleteRule) ? undefined : fk.deleteRule,
|
|
1055
|
+
deferMode: fk.deferMode,
|
|
1056
|
+
};
|
|
1057
|
+
};
|
|
1058
|
+
const normalizeCheck = (check) => {
|
|
1059
|
+
const out = { name: check.name };
|
|
1060
|
+
if (typeof check.expression === 'string') {
|
|
1061
|
+
out.expression = check.expression;
|
|
1062
|
+
}
|
|
1063
|
+
for (const field of ['definition', 'columnName']) {
|
|
1064
|
+
if (check[field]) {
|
|
1065
|
+
out[field] = check[field];
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return out;
|
|
1069
|
+
};
|
|
1070
|
+
const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
|
|
1071
|
+
const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
|
|
1072
|
+
const sortedTriggers = [...this.#triggers].sort((a, b) => byString(a.name, b.name));
|
|
1073
|
+
const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
|
|
1074
|
+
.sort(([a], [b]) => byString(a, b))
|
|
1075
|
+
.map(([k, v]) => [k, normalizeFk(v)]));
|
|
972
1076
|
return {
|
|
973
1077
|
name: this.name,
|
|
974
1078
|
schema: this.schema,
|
|
975
1079
|
columns: columnsMapped,
|
|
976
|
-
indexes:
|
|
977
|
-
checks:
|
|
978
|
-
triggers:
|
|
979
|
-
foreignKeys:
|
|
980
|
-
|
|
981
|
-
comment: this.comment,
|
|
1080
|
+
indexes: sortedIndexes,
|
|
1081
|
+
checks: sortedChecks,
|
|
1082
|
+
triggers: sortedTriggers,
|
|
1083
|
+
foreignKeys: sortedForeignKeys,
|
|
1084
|
+
// emit `comment` even when unset so introspection (which always reads it) matches metadata
|
|
1085
|
+
comment: this.comment ?? null,
|
|
982
1086
|
};
|
|
983
1087
|
}
|
|
984
1088
|
}
|
|
@@ -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
|
@@ -42,7 +42,7 @@ export class SchemaHelper {
|
|
|
42
42
|
}
|
|
43
43
|
inferLengthFromColumnType(type) {
|
|
44
44
|
const match = /^\w+\s*(?:\(\s*(\d+)\s*\)|$)/.exec(type);
|
|
45
|
-
if (
|
|
45
|
+
if (match?.[1] == null) {
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
return +match[1];
|
|
@@ -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[];
|