@mikro-orm/sql 7.2.0-dev.0 → 7.2.0-dev.10
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/AbstractSqlConnection.d.ts +18 -2
- package/AbstractSqlConnection.js +43 -17
- package/AbstractSqlDriver.d.ts +2 -2
- package/AbstractSqlDriver.js +64 -21
- package/AbstractSqlPlatform.d.ts +7 -0
- package/AbstractSqlPlatform.js +23 -2
- package/SqlEntityManager.d.ts +2 -2
- package/SqlEntityManager.js +1 -2
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +1 -1
- package/dialects/mysql/BaseMySqlPlatform.js +1 -1
- package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -0
- package/dialects/mysql/MySqlSchemaHelper.js +7 -3
- package/dialects/oracledb/OracleNativeQueryBuilder.js +1 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +4 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +32 -5
- package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -0
- package/dialects/sqlite/BaseSqliteConnection.js +15 -5
- package/dialects/sqlite/SqliteSchemaHelper.js +2 -2
- package/package.json +4 -4
- package/query/NativeQueryBuilder.js +1 -1
- package/query/QueryBuilder.d.ts +37 -19
- package/query/QueryBuilder.js +47 -17
- package/query/QueryBuilderHelper.js +6 -1
- package/schema/DatabaseTable.d.ts +2 -0
- package/schema/DatabaseTable.js +63 -18
- package/schema/SchemaComparator.d.ts +1 -0
- package/schema/SchemaComparator.js +29 -6
- package/schema/SchemaHelper.d.ts +20 -1
- package/schema/SchemaHelper.js +52 -9
- package/schema/SqlSchemaGenerator.d.ts +4 -0
- package/schema/SqlSchemaGenerator.js +51 -22
- package/typings.d.ts +2 -2
package/schema/DatabaseTable.js
CHANGED
|
@@ -219,6 +219,8 @@ export class DatabaseTable {
|
|
|
219
219
|
skippedColumnNames.includes(index.columnNames[0]) || // Non-composite indexes for skipped columns are to be mapped as entity decorators.
|
|
220
220
|
index.deferMode ||
|
|
221
221
|
index.expression ||
|
|
222
|
+
index.where ||
|
|
223
|
+
this.hasAdvancedIndexOptions(index) ||
|
|
222
224
|
!(index.columnNames[0] in columnFks)) && // Trivial non-composite indexes for scalar props are to be mapped to the column.
|
|
223
225
|
// ignore indexes that don't have all column names (this can happen in sqlite where there is no way to infer this for expressions)
|
|
224
226
|
!(index.columnNames.some(col => !col) && !index.expression));
|
|
@@ -254,14 +256,7 @@ export class DatabaseTable {
|
|
|
254
256
|
}
|
|
255
257
|
}
|
|
256
258
|
// An index is trivial if it has no special options that require entity-level declaration
|
|
257
|
-
const
|
|
258
|
-
index.include?.length ||
|
|
259
|
-
index.fillFactor ||
|
|
260
|
-
index.type ||
|
|
261
|
-
index.invisible ||
|
|
262
|
-
index.disabled ||
|
|
263
|
-
index.clustered;
|
|
264
|
-
const isTrivial = !index.deferMode && !index.expression && !index.where && !hasAdvancedOptions;
|
|
259
|
+
const isTrivial = !index.deferMode && !index.expression && !index.where && !this.hasAdvancedIndexOptions(index);
|
|
265
260
|
if (isTrivial) {
|
|
266
261
|
// Index is for FK. Map to the FK prop and move on.
|
|
267
262
|
const fkForIndex = fkIndexes.get(index);
|
|
@@ -299,6 +294,19 @@ export class DatabaseTable {
|
|
|
299
294
|
}
|
|
300
295
|
schema.addIndex(ret);
|
|
301
296
|
}
|
|
297
|
+
for (const check of this.getChecks()) {
|
|
298
|
+
// skip checks that were consumed by enum conversion — the enum property recreates an
|
|
299
|
+
// equivalent check under the conventional name during discovery (only on platforms that
|
|
300
|
+
// emulate enums via check constraints; mysql/mariadb enums are native and recreate nothing)
|
|
301
|
+
const enumItems = check.columnName ? this.getColumn(check.columnName)?.enumItems : undefined;
|
|
302
|
+
if (this.#platform.usesEnumCheckConstraints() &&
|
|
303
|
+
enumItems?.length &&
|
|
304
|
+
(check.expression === this.#platform.getEnumCheckConstraintExpression(check.columnName, enumItems) ||
|
|
305
|
+
check.name === this.#platform.getIndexName(this.name, [check.columnName], 'check'))) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
schema.meta.checks.push({ name: check.name, expression: check.expression });
|
|
309
|
+
}
|
|
302
310
|
const addedStandaloneFkPropsBasedOnColumn = new Set();
|
|
303
311
|
const nonSkippedColumns = this.getColumns().filter(column => !skippedColumnNames.includes(column.name));
|
|
304
312
|
for (const column of nonSkippedColumns) {
|
|
@@ -507,7 +515,9 @@ export class DatabaseTable {
|
|
|
507
515
|
findFkIndex(currentFk) {
|
|
508
516
|
const fkColumnsLength = currentFk.columnNames.length;
|
|
509
517
|
const possibleIndexes = this.#indexes.filter(index => {
|
|
510
|
-
return (index.
|
|
518
|
+
return (!index.where &&
|
|
519
|
+
!this.hasAdvancedIndexOptions(index) &&
|
|
520
|
+
index.columnNames.length === fkColumnsLength &&
|
|
511
521
|
!currentFk.columnNames.some((columnName, i) => index.columnNames[i] !== columnName));
|
|
512
522
|
});
|
|
513
523
|
possibleIndexes.sort((a, b) => {
|
|
@@ -521,13 +531,31 @@ export class DatabaseTable {
|
|
|
521
531
|
});
|
|
522
532
|
return possibleIndexes.at(0);
|
|
523
533
|
}
|
|
534
|
+
/** Advanced options require an entity-level declaration, as the property-level `index`/`unique` cannot carry them. */
|
|
535
|
+
hasAdvancedIndexOptions(index) {
|
|
536
|
+
return !!(index.columns?.length ||
|
|
537
|
+
index.include?.length ||
|
|
538
|
+
index.fillFactor ||
|
|
539
|
+
index.type ||
|
|
540
|
+
index.invisible ||
|
|
541
|
+
index.disabled ||
|
|
542
|
+
index.clustered);
|
|
543
|
+
}
|
|
524
544
|
getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy) {
|
|
525
|
-
const propBaseNames = new
|
|
545
|
+
const propBaseNames = new Map();
|
|
526
546
|
const columnNames = index.columnNames;
|
|
527
547
|
const l = columnNames.length;
|
|
528
548
|
if (columnNames.some(col => !col)) {
|
|
529
549
|
return;
|
|
530
550
|
}
|
|
551
|
+
const addPropBaseName = (baseName, position) => {
|
|
552
|
+
const positions = propBaseNames.get(baseName);
|
|
553
|
+
if (positions) {
|
|
554
|
+
positions.last = position;
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
propBaseNames.set(baseName, { first: position, last: position });
|
|
558
|
+
};
|
|
531
559
|
for (let i = 0; i < l; ++i) {
|
|
532
560
|
const columnName = columnNames[i];
|
|
533
561
|
// The column is not involved with FKs.
|
|
@@ -538,14 +566,14 @@ export class DatabaseTable {
|
|
|
538
566
|
}
|
|
539
567
|
// It has a prop named after it.
|
|
540
568
|
// Add it and move on.
|
|
541
|
-
|
|
569
|
+
addPropBaseName(columnName, i);
|
|
542
570
|
continue;
|
|
543
571
|
}
|
|
544
572
|
// If the prop named after the column has a FK and the FK's columns are a subset of this index,
|
|
545
573
|
// include this prop and move on.
|
|
546
574
|
const columnPropFk = fksOnColumnProps.get(columnName);
|
|
547
575
|
if (columnPropFk && !columnPropFk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
|
|
548
|
-
|
|
576
|
+
addPropBaseName(columnName, i);
|
|
549
577
|
continue;
|
|
550
578
|
}
|
|
551
579
|
// If there is at least one standalone FK featuring this column,
|
|
@@ -557,7 +585,7 @@ export class DatabaseTable {
|
|
|
557
585
|
continue;
|
|
558
586
|
}
|
|
559
587
|
if (!fk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
|
|
560
|
-
|
|
588
|
+
addPropBaseName(propName, i);
|
|
561
589
|
propAdded = true;
|
|
562
590
|
}
|
|
563
591
|
}
|
|
@@ -568,7 +596,10 @@ export class DatabaseTable {
|
|
|
568
596
|
// Break the whole prop creation.
|
|
569
597
|
return;
|
|
570
598
|
}
|
|
571
|
-
|
|
599
|
+
// Props sharing their first column would otherwise follow FK discovery order, so break ties on the last one.
|
|
600
|
+
return Array.from(propBaseNames)
|
|
601
|
+
.sort(([, a], [, b]) => a.first - b.first || a.last - b.last)
|
|
602
|
+
.map(([baseName]) => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)));
|
|
572
603
|
}
|
|
573
604
|
getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName) {
|
|
574
605
|
if (columnName &&
|
|
@@ -694,9 +725,19 @@ export class DatabaseTable {
|
|
|
694
725
|
const prop = this.getPropertyName(namingStrategy, column.name, fk);
|
|
695
726
|
const persist = !(column.name in columnFks && typeof fk === 'undefined');
|
|
696
727
|
const index = compositeFkIndexes[prop] ||
|
|
697
|
-
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
728
|
+
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
729
|
+
!idx.composite &&
|
|
730
|
+
!idx.unique &&
|
|
731
|
+
!idx.primary &&
|
|
732
|
+
!idx.where &&
|
|
733
|
+
!this.hasAdvancedIndexOptions(idx));
|
|
698
734
|
const unique = compositeFkUniques[prop] ||
|
|
699
|
-
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
735
|
+
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
736
|
+
!idx.composite &&
|
|
737
|
+
idx.unique &&
|
|
738
|
+
!idx.primary &&
|
|
739
|
+
!idx.where &&
|
|
740
|
+
!this.hasAdvancedIndexOptions(idx));
|
|
700
741
|
const kind = this.getReferenceKind(fk, unique);
|
|
701
742
|
const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk);
|
|
702
743
|
const type = fk
|
|
@@ -989,8 +1030,12 @@ export class DatabaseTable {
|
|
|
989
1030
|
// mysql stores decimal defaults padded to scale (`0` → `0.00`); collapse to canonical numeric form
|
|
990
1031
|
// so the metadata-side (`0`) and introspection-side (`0.00`) snapshots agree
|
|
991
1032
|
let defaultValue = c.default ?? null;
|
|
992
|
-
if (defaultValue != null && c.mappedType instanceof DecimalType
|
|
993
|
-
|
|
1033
|
+
if (defaultValue != null && c.mappedType instanceof DecimalType) {
|
|
1034
|
+
// string defaults like `default: '0.00'` are quoted in metadata, so strip the quotes first
|
|
1035
|
+
const unquoted = defaultValue.replace(/^'(.*)'$/, '$1');
|
|
1036
|
+
if (Number.isFinite(+unquoted)) {
|
|
1037
|
+
defaultValue = this.#platform.formatDecimal(unquoted, c.scale).toString();
|
|
1038
|
+
}
|
|
994
1039
|
}
|
|
995
1040
|
const normalized = {
|
|
996
1041
|
name: c.name,
|
|
@@ -89,6 +89,7 @@ export declare class SchemaComparator {
|
|
|
89
89
|
private diffViewExpression;
|
|
90
90
|
private diffTrigger;
|
|
91
91
|
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
|
|
92
|
+
private parseDecimalDefault;
|
|
92
93
|
hasSameDefaultValue(from: Column, to: Column): boolean;
|
|
93
94
|
private mapColumnToProperty;
|
|
94
95
|
private log;
|
|
@@ -820,6 +820,10 @@ export class SchemaComparator {
|
|
|
820
820
|
if (!!index1.clustered !== !!index2.clustered) {
|
|
821
821
|
return false;
|
|
822
822
|
}
|
|
823
|
+
// Compare the index access method (e.g. `using gin` on PostgreSQL); unset means the platform default
|
|
824
|
+
if (this.#helper.getIndexAccessMethod(index1) !== this.#helper.getIndexAccessMethod(index2)) {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
823
827
|
// Compare WHERE predicate of partial indexes structurally (whitespace/quoting/casing
|
|
824
828
|
// are normalized via the same helper used for check constraints).
|
|
825
829
|
if (this.diffExpression(index1.where ?? '', index2.where ?? '')) {
|
|
@@ -897,9 +901,13 @@ export class SchemaComparator {
|
|
|
897
901
|
// lookbehind: only strip a real charset introducer, never an underscore inside a literal like 'a_b'
|
|
898
902
|
?.replace(/(?<![\w'])_\w+'(.*?)'/g, '$1')
|
|
899
903
|
.replace(/!=/g, '<>')
|
|
900
|
-
|
|
904
|
+
// `\b` keeps this from firing inside identifiers like `min(...)`
|
|
905
|
+
.replace(/\bin\s*\((.*?)\)/gi, '= any (array[$1])')
|
|
901
906
|
// MySQL normalizes count(*) to count(0)
|
|
902
907
|
.replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
|
|
908
|
+
// multi word type names in casts, the generic `::\w+` below only covers single word ones
|
|
909
|
+
// the precision is kept, so `timestamptz(3)` and `timestamp(3) with time zone` leave the same residue
|
|
910
|
+
.replace(/::\s*(?:character\s+varying|bit\s+varying|double\s+precision|(?:timestamp|time)\b(\s*\(\d+\))?(?:\s+with(?:out)?\s+time\s+zone)?)/gi, '$1')
|
|
903
911
|
// Remove quotes first so we can process identifiers
|
|
904
912
|
.replace(/['"`]/g, '')
|
|
905
913
|
// MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
|
|
@@ -907,12 +915,18 @@ export class SchemaComparator {
|
|
|
907
915
|
.replace(/\b\w+\.(\w+)/g, '$1')
|
|
908
916
|
// Normalize JOIN syntax: inner join -> join (equivalent in SQL)
|
|
909
917
|
.replace(/\binner\s+join\b/gi, 'join')
|
|
918
|
+
// PostgreSQL names an unaliased bare function call after the function itself,
|
|
919
|
+
// so `max(created_at)` comes back as `max(created_at) AS max`
|
|
920
|
+
// the lookahead skips table function column alias lists like `unnest(a) AS unnest(c)`, which are meaningful
|
|
921
|
+
.replace(/\b(\w+)\s*\(((?:[^()]|\([^()]*\))*)\)\s+as\s+\1\b(?!\s*\()/gi, '$1($2)')
|
|
910
922
|
// Remove redundant column aliases like `title AS title` -> `title`
|
|
911
923
|
.replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
|
|
912
924
|
// Remove AS keyword (optional in SQL, MySQL may add/remove it)
|
|
913
925
|
.replace(/\bas\b/gi, '')
|
|
914
926
|
// Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
|
|
915
|
-
|
|
927
|
+
// tabs and CRs included — the schema generator trims every line before executing the DDL,
|
|
928
|
+
// so indentation and CRLF endings can never come back from introspection
|
|
929
|
+
.replace(/[()\n\r\t[\]*]|::\w+| +/g, '')
|
|
916
930
|
.replace(/anyarray\[(.*)]/gi, '$1')
|
|
917
931
|
.toLowerCase()
|
|
918
932
|
// PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
|
|
@@ -979,6 +993,10 @@ export class SchemaComparator {
|
|
|
979
993
|
const val = defaultValue.replace(/^(_\w+\\)?'(.*?)\\?'$/, '$2').replace(/^\(?'(.*?)'\)?$/, '$1');
|
|
980
994
|
return parseJsonSafe(val);
|
|
981
995
|
}
|
|
996
|
+
parseDecimalDefault(defaultValue) {
|
|
997
|
+
const value = +('' + defaultValue).replace(/^'(.+)'$/, '$1');
|
|
998
|
+
return Number.isFinite(value) ? value : null;
|
|
999
|
+
}
|
|
982
1000
|
hasSameDefaultValue(from, to) {
|
|
983
1001
|
if (from.default == null ||
|
|
984
1002
|
from.default.toString().toLowerCase() === 'null' ||
|
|
@@ -1004,10 +1022,15 @@ export class SchemaComparator {
|
|
|
1004
1022
|
const defaultValueTo = to.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
|
|
1005
1023
|
return defaultValueFrom === defaultValueTo;
|
|
1006
1024
|
}
|
|
1007
|
-
// mysql
|
|
1008
|
-
//
|
|
1009
|
-
if (to.mappedType instanceof DecimalType
|
|
1010
|
-
|
|
1025
|
+
// mysql pads decimal defaults to scale (`0` → `0.00`) and postgres reports them unquoted
|
|
1026
|
+
// while metadata keeps them quoted; compare numerically so neither churns a no-op migration
|
|
1027
|
+
if (to.mappedType instanceof DecimalType) {
|
|
1028
|
+
const defaultValueFrom = this.parseDecimalDefault(from.default);
|
|
1029
|
+
const defaultValueTo = this.parseDecimalDefault(to.default);
|
|
1030
|
+
if (defaultValueFrom != null && defaultValueTo != null) {
|
|
1031
|
+
return (this.#platform.formatDecimal(defaultValueFrom, to.scale) ===
|
|
1032
|
+
this.#platform.formatDecimal(defaultValueTo, to.scale));
|
|
1033
|
+
}
|
|
1011
1034
|
}
|
|
1012
1035
|
if (from.default && to.default) {
|
|
1013
1036
|
return from.default.toString().toLowerCase() === to.default.toString().toLowerCase();
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -4,7 +4,13 @@ import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
|
4
4
|
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef, SqlRoutineDef } from '../typings.js';
|
|
5
5
|
import type { DatabaseSchema } from './DatabaseSchema.js';
|
|
6
6
|
import type { DatabaseTable } from './DatabaseTable.js';
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Flattens `;\n` boundaries and drops blank lines so the schema-generator's statement splitter
|
|
9
|
+
* doesn't break the routine or trigger DDL apart — it treats both as statement/group separators.
|
|
10
|
+
* Blank lines go first, otherwise a `;` followed by one would keep its newline. Like
|
|
11
|
+
* `normalizeViewDefinition`, this is not string-literal aware, so a blank line inside a multi-line
|
|
12
|
+
* literal is dropped too. Other whitespace is preserved.
|
|
13
|
+
*/
|
|
8
14
|
export declare function stripStatementNewlines(body: string): string;
|
|
9
15
|
/**
|
|
10
16
|
* Strips SQL line comments and blank lines from a view definition. Comments are dropped by the
|
|
@@ -64,6 +70,13 @@ export declare abstract class SchemaHelper {
|
|
|
64
70
|
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
|
|
65
71
|
*/
|
|
66
72
|
protected getCreateIndexSuffix(_index: IndexDef): string;
|
|
73
|
+
/**
|
|
74
|
+
* Normalized index access method (e.g. `gin` on PostgreSQL), empty string when the
|
|
75
|
+
* platform default applies. Used for both DDL emission and index diffing.
|
|
76
|
+
*/
|
|
77
|
+
getIndexAccessMethod(_index: IndexDef): string;
|
|
78
|
+
/** Emits the access method between the table name and the column list (e.g. ` using gin`). */
|
|
79
|
+
protected getIndexAccessMethodClause(index: IndexDef): string;
|
|
67
80
|
/**
|
|
68
81
|
* Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
|
|
69
82
|
* return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
|
|
@@ -118,6 +131,8 @@ export declare abstract class SchemaHelper {
|
|
|
118
131
|
getAddColumnsSQL(table: DatabaseTable, columns: Column[]): string[];
|
|
119
132
|
getDropColumnsSQL(tableName: string, columns: Column[], schemaName?: string): string;
|
|
120
133
|
hasNonDefaultPrimaryKeyName(table: DatabaseTable): boolean;
|
|
134
|
+
/** Returns the `constraint <name> ` prefix for a primary key definition, empty when the server assigns the default name on its own. */
|
|
135
|
+
protected getPrimaryKeyConstraintPrefix(table: DatabaseTable, index: IndexDef): string;
|
|
121
136
|
castColumn(name: string, type: string): string;
|
|
122
137
|
alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
|
|
123
138
|
/** Returns the bare `collate <name>` clause for column DDL. Overridden by PostgreSQL to quote the identifier. */
|
|
@@ -137,6 +152,8 @@ export declare abstract class SchemaHelper {
|
|
|
137
152
|
}[];
|
|
138
153
|
}[], safe: boolean): string[];
|
|
139
154
|
getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
|
|
155
|
+
/** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
|
|
156
|
+
protected hasInlineColumnComment(): boolean;
|
|
140
157
|
getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;
|
|
141
158
|
protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>;
|
|
142
159
|
mapForeignKeys(fks: any[], tableName: string, schemaName?: string): Dictionary;
|
|
@@ -176,6 +193,8 @@ export declare abstract class SchemaHelper {
|
|
|
176
193
|
createRoutine(_routine: SqlRoutineDef): string;
|
|
177
194
|
dropRoutine(_routine: SqlRoutineDef): string;
|
|
178
195
|
getAllRoutines(_connection: AbstractSqlConnection, _schemas?: string[]): Promise<SqlRoutineDef[]>;
|
|
196
|
+
/** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
|
|
197
|
+
protected normalizeTriggerBody(body: string): string;
|
|
179
198
|
/** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */
|
|
180
199
|
protected wrapRoutineBody(body: string): string;
|
|
181
200
|
protected stripRoutineBody(body: string): string;
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { isRaw, Utils, } from '@mikro-orm/core';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* Flattens `;\n` boundaries and drops blank lines so the schema-generator's statement splitter
|
|
4
|
+
* doesn't break the routine or trigger DDL apart — it treats both as statement/group separators.
|
|
5
|
+
* Blank lines go first, otherwise a `;` followed by one would keep its newline. Like
|
|
6
|
+
* `normalizeViewDefinition`, this is not string-literal aware, so a blank line inside a multi-line
|
|
7
|
+
* literal is dropped too. Other whitespace is preserved.
|
|
8
|
+
*/
|
|
3
9
|
export function stripStatementNewlines(body) {
|
|
4
|
-
return body
|
|
10
|
+
return body
|
|
11
|
+
.split('\n')
|
|
12
|
+
.filter(line => line.trim() !== '')
|
|
13
|
+
.join('\n')
|
|
14
|
+
.replace(/;[\t ]*\r?\n/g, '; ');
|
|
5
15
|
}
|
|
6
16
|
/**
|
|
7
17
|
* Strips SQL line comments and blank lines from a view definition. Comments are dropped by the
|
|
@@ -157,13 +167,14 @@ export class SchemaHelper {
|
|
|
157
167
|
tableName = this.quote(tableName);
|
|
158
168
|
const keyName = this.quote(index.keyName);
|
|
159
169
|
const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
|
|
160
|
-
|
|
170
|
+
const using = this.getIndexAccessMethodClause(index);
|
|
171
|
+
let sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}${using}`;
|
|
161
172
|
if (index.unique && index.constraint) {
|
|
162
173
|
sql = `alter table ${tableName} add constraint ${keyName} unique`;
|
|
163
174
|
}
|
|
164
175
|
if (index.columnNames.some(column => column.includes('.'))) {
|
|
165
176
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
166
|
-
sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
|
|
177
|
+
sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}${using}`;
|
|
167
178
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
168
179
|
return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${this.getIndexWhereClause(index)}${defer}`;
|
|
169
180
|
}
|
|
@@ -182,6 +193,18 @@ export class SchemaHelper {
|
|
|
182
193
|
getCreateIndexSuffix(_index) {
|
|
183
194
|
return '';
|
|
184
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Normalized index access method (e.g. `gin` on PostgreSQL), empty string when the
|
|
198
|
+
* platform default applies. Used for both DDL emission and index diffing.
|
|
199
|
+
*/
|
|
200
|
+
getIndexAccessMethod(_index) {
|
|
201
|
+
return '';
|
|
202
|
+
}
|
|
203
|
+
/** Emits the access method between the table name and the column list (e.g. ` using gin`). */
|
|
204
|
+
getIndexAccessMethodClause(index) {
|
|
205
|
+
const method = this.getIndexAccessMethod(index);
|
|
206
|
+
return method ? ` using ${method}` : '';
|
|
207
|
+
}
|
|
185
208
|
/**
|
|
186
209
|
* Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
|
|
187
210
|
* return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
|
|
@@ -436,7 +459,8 @@ export class SchemaHelper {
|
|
|
436
459
|
this.append(ret, this.alterTableColumn(column, diff.fromTable, changedProperties));
|
|
437
460
|
}
|
|
438
461
|
for (const { column, changedProperties } of Object.values(diff.changedColumns).filter(diff => diff.changedProperties.has('comment'))) {
|
|
439
|
-
if (
|
|
462
|
+
if (this.hasInlineColumnComment() &&
|
|
463
|
+
['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
|
|
440
464
|
continue; // will be handled via column update
|
|
441
465
|
}
|
|
442
466
|
ret.push(this.getChangeColumnCommentSQL(tableName, column, schemaName));
|
|
@@ -489,7 +513,13 @@ export class SchemaHelper {
|
|
|
489
513
|
return `add ${this.createTableColumn(column, table)}`;
|
|
490
514
|
})
|
|
491
515
|
.join(', ');
|
|
492
|
-
|
|
516
|
+
const ret = [`alter table ${table.getQuotedName()} ${adds}`];
|
|
517
|
+
if (!this.hasInlineColumnComment()) {
|
|
518
|
+
for (const column of columns.filter(column => column.comment)) {
|
|
519
|
+
ret.push(this.getChangeColumnCommentSQL(table.name, column, table.schema));
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return ret;
|
|
493
523
|
}
|
|
494
524
|
getDropColumnsSQL(tableName, columns, schemaName) {
|
|
495
525
|
const name = this.quote(this.getTableName(tableName, schemaName));
|
|
@@ -504,6 +534,10 @@ export class SchemaHelper {
|
|
|
504
534
|
const defaultName = this.platform.getDefaultPrimaryName(table.name, pkIndex.columnNames);
|
|
505
535
|
return pkIndex?.keyName !== defaultName;
|
|
506
536
|
}
|
|
537
|
+
/** Returns the `constraint <name> ` prefix for a primary key definition, empty when the server assigns the default name on its own. */
|
|
538
|
+
getPrimaryKeyConstraintPrefix(table, index) {
|
|
539
|
+
return this.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(index.keyName)} ` : '';
|
|
540
|
+
}
|
|
507
541
|
/* v8 ignore next */
|
|
508
542
|
castColumn(name, type) {
|
|
509
543
|
return '';
|
|
@@ -608,6 +642,10 @@ export class SchemaHelper {
|
|
|
608
642
|
getChangeColumnCommentSQL(tableName, to, schemaName) {
|
|
609
643
|
return '';
|
|
610
644
|
}
|
|
645
|
+
/** Whether the column comment is part of the column declaration, as opposed to a separate statement. */
|
|
646
|
+
hasInlineColumnComment() {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
611
649
|
async getNamespaces(connection, ctx) {
|
|
612
650
|
return [];
|
|
613
651
|
}
|
|
@@ -745,7 +783,7 @@ export class SchemaHelper {
|
|
|
745
783
|
const primaryKey = table.getPrimaryKey();
|
|
746
784
|
const createPrimary = !table.getColumns().some(c => c.autoincrement && c.primary) || this.hasNonDefaultPrimaryKeyName(table);
|
|
747
785
|
if (createPrimary && primaryKey) {
|
|
748
|
-
const name = this.
|
|
786
|
+
const name = this.getPrimaryKeyConstraintPrefix(table, primaryKey);
|
|
749
787
|
sql += `, ${name}primary key (${primaryKey.columnNames.map(c => this.quote(c)).join(', ')})`;
|
|
750
788
|
}
|
|
751
789
|
sql += ')';
|
|
@@ -828,7 +866,7 @@ export class SchemaHelper {
|
|
|
828
866
|
const columns = index.columnNames.map(c => this.quote(c)).join(', ');
|
|
829
867
|
const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
|
|
830
868
|
if (index.primary) {
|
|
831
|
-
const keyName = this.
|
|
869
|
+
const keyName = this.getPrimaryKeyConstraintPrefix(table, index);
|
|
832
870
|
return `alter table ${table.getQuotedName()} add ${keyName}primary key (${columns})${defer}`;
|
|
833
871
|
}
|
|
834
872
|
if (index.type === 'fulltext') {
|
|
@@ -861,7 +899,7 @@ export class SchemaHelper {
|
|
|
861
899
|
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
862
900
|
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
863
901
|
const when = trigger.when ? ` when (${trigger.when})` : '';
|
|
864
|
-
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}
|
|
902
|
+
return `create trigger ${this.quote(trigger.name)} ${timing} ${events} on ${table.getQuotedName()} for each ${forEach}${when} begin ${this.normalizeTriggerBody(trigger.body)} end`;
|
|
865
903
|
}
|
|
866
904
|
/**
|
|
867
905
|
* Generates SQL to drop a database trigger from a table.
|
|
@@ -885,6 +923,11 @@ export class SchemaHelper {
|
|
|
885
923
|
async getAllRoutines(_connection, _schemas = []) {
|
|
886
924
|
return [];
|
|
887
925
|
}
|
|
926
|
+
/** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
|
|
927
|
+
normalizeTriggerBody(body) {
|
|
928
|
+
const trimmed = stripStatementNewlines(body).trim();
|
|
929
|
+
return /;\s*$/.test(trimmed) ? trimmed : `${trimmed};`;
|
|
930
|
+
}
|
|
888
931
|
/** Wraps the body in `BEGIN ... END` if not already, and flattens internal `;\n` so the schema-generator's statement splitter doesn't tear the DDL. */
|
|
889
932
|
wrapRoutineBody(body) {
|
|
890
933
|
const trimmed = stripStatementNewlines(body).trim();
|
|
@@ -54,6 +54,10 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
|
|
|
54
54
|
wrap?: boolean;
|
|
55
55
|
ctx?: Transaction;
|
|
56
56
|
}): Promise<void>;
|
|
57
|
+
/** Splits the SQL on the separator, keeping the separators that fall inside a string literal. */
|
|
58
|
+
private splitOutsideLiterals;
|
|
59
|
+
/** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. */
|
|
60
|
+
protected startsBatch(_statement: string): boolean;
|
|
57
61
|
dropTableIfExists(name: string, schema?: string): Promise<void>;
|
|
58
62
|
private wrapSchema;
|
|
59
63
|
private append;
|
|
@@ -308,22 +308,6 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
308
308
|
for (const newTable of Object.values(schemaDiff.newTables)) {
|
|
309
309
|
this.append(ret, this.helper.createTable(newTable, true), true);
|
|
310
310
|
}
|
|
311
|
-
if (this.helper.supportsSchemaConstraints()) {
|
|
312
|
-
for (const newTable of Object.values(schemaDiff.newTables)) {
|
|
313
|
-
const sql = [];
|
|
314
|
-
if (this.options.createForeignKeyConstraints) {
|
|
315
|
-
const fks = Object.values(newTable.getForeignKeys()).map(fk => this.helper.createForeignKey(newTable, fk));
|
|
316
|
-
this.append(sql, fks);
|
|
317
|
-
}
|
|
318
|
-
for (const check of newTable.getChecks()) {
|
|
319
|
-
this.append(sql, this.helper.createCheck(newTable, check));
|
|
320
|
-
}
|
|
321
|
-
for (const trigger of newTable.getTriggers()) {
|
|
322
|
-
this.append(sql, this.helper.createTrigger(newTable, trigger));
|
|
323
|
-
}
|
|
324
|
-
this.append(ret, sql, true);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
311
|
if (options.dropTables && !options.safe) {
|
|
328
312
|
for (const table of Object.values(schemaDiff.removedTables)) {
|
|
329
313
|
// Drop triggers before the table so driver-specific cleanup runs (e.g. PostgreSQL function removal)
|
|
@@ -353,6 +337,23 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
353
337
|
for (const changedTable of alteredTables) {
|
|
354
338
|
this.append(ret, this.helper.getPostAlterTable(changedTable, options.safe), true);
|
|
355
339
|
}
|
|
340
|
+
// after the alters, so a new table's FK can reference a unique constraint an existing table gains in the same diff
|
|
341
|
+
if (this.helper.supportsSchemaConstraints()) {
|
|
342
|
+
for (const newTable of Object.values(schemaDiff.newTables)) {
|
|
343
|
+
const sql = [];
|
|
344
|
+
if (this.options.createForeignKeyConstraints) {
|
|
345
|
+
const fks = Object.values(newTable.getForeignKeys()).map(fk => this.helper.createForeignKey(newTable, fk));
|
|
346
|
+
this.append(sql, fks);
|
|
347
|
+
}
|
|
348
|
+
for (const check of newTable.getChecks()) {
|
|
349
|
+
this.append(sql, this.helper.createCheck(newTable, check));
|
|
350
|
+
}
|
|
351
|
+
for (const trigger of newTable.getTriggers()) {
|
|
352
|
+
this.append(sql, this.helper.createTrigger(newTable, trigger));
|
|
353
|
+
}
|
|
354
|
+
this.append(ret, sql, true);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
356
357
|
if (!options.safe && this.platform.supportsNativeEnums()) {
|
|
357
358
|
for (const removedNativeEnum of schemaDiff.removedNativeEnums) {
|
|
358
359
|
this.append(ret, this.helper.getDropNativeEnumSQL(removedNativeEnum.name, removedNativeEnum.schema));
|
|
@@ -439,18 +440,23 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
439
440
|
}
|
|
440
441
|
async execute(sql, options = {}) {
|
|
441
442
|
options.wrap ??= false;
|
|
442
|
-
const lines = this.wrapSchema(sql, options)
|
|
443
|
+
const lines = this.splitOutsideLiterals(this.wrapSchema(sql, options), '\n');
|
|
443
444
|
const groups = [];
|
|
444
445
|
let i = 0;
|
|
445
446
|
for (const line of lines) {
|
|
446
|
-
|
|
447
|
+
const stmt = line.trim();
|
|
448
|
+
if (stmt === '') {
|
|
447
449
|
if (groups[i]?.length > 0) {
|
|
448
450
|
i++;
|
|
449
451
|
}
|
|
450
452
|
continue;
|
|
451
453
|
}
|
|
454
|
+
// same boundary an empty line creates, for statements that have to start their own batch
|
|
455
|
+
if (groups[i]?.length > 0 && this.startsBatch(stmt)) {
|
|
456
|
+
i++;
|
|
457
|
+
}
|
|
452
458
|
groups[i] ??= [];
|
|
453
|
-
groups[i].push(
|
|
459
|
+
groups[i].push(stmt);
|
|
454
460
|
}
|
|
455
461
|
if (groups.length === 0) {
|
|
456
462
|
return;
|
|
@@ -463,14 +469,37 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
463
469
|
return;
|
|
464
470
|
}
|
|
465
471
|
const statements = groups.flatMap(group => {
|
|
466
|
-
return group
|
|
467
|
-
.join('\n')
|
|
468
|
-
.split(';\n')
|
|
472
|
+
return this.splitOutsideLiterals(group.join('\n'), ';\n')
|
|
469
473
|
.map(s => s.trim())
|
|
470
474
|
.filter(s => s);
|
|
471
475
|
});
|
|
472
476
|
await Utils.runSerial(statements, stmt => this.driver.execute(stmt));
|
|
473
477
|
}
|
|
478
|
+
/** Splits the SQL on the separator, keeping the separators that fall inside a string literal. */
|
|
479
|
+
splitOutsideLiterals(sql, separator) {
|
|
480
|
+
const [idOpen, idClose] = this.platform.quoteIdentifier('');
|
|
481
|
+
// mysql escapes quotes as `\'`, the other dialects double them, which pairs up on its own
|
|
482
|
+
const esc = this.platform.quoteValue(`'`).includes(`\\'`) ? '\\\\.|' : '';
|
|
483
|
+
// complete literals, quoted identifiers and `--` comments, so that an apostrophe inside an
|
|
484
|
+
// identifier or a comment is not mistaken for one opening a literal
|
|
485
|
+
const tokens = new RegExp(`'(?:${esc}[^'])*'|\\${idOpen}[^\\${idClose}]*\\${idClose}|--[^\n]*`, 'g');
|
|
486
|
+
const parts = [];
|
|
487
|
+
for (const chunk of sql.split(separator)) {
|
|
488
|
+
const prev = parts.at(-1);
|
|
489
|
+
// whatever quote is left once the complete tokens are gone opened a literal we are still inside of
|
|
490
|
+
if (prev?.replace(tokens, '').includes(`'`)) {
|
|
491
|
+
parts[parts.length - 1] = prev + separator + chunk;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
parts.push(chunk);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
return parts;
|
|
498
|
+
}
|
|
499
|
+
/** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. */
|
|
500
|
+
startsBatch(_statement) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
474
503
|
async dropTableIfExists(name, schema) {
|
|
475
504
|
const sql = this.helper.dropTableIfExists(name, schema);
|
|
476
505
|
return this.execute(sql);
|
package/typings.d.ts
CHANGED
|
@@ -368,9 +368,9 @@ type MaybeGenerated<TValue, TOptions, TProcessOnCreate extends boolean> = TOptio
|
|
|
368
368
|
} ? TValue | null : TOptions extends {
|
|
369
369
|
autoincrement: true;
|
|
370
370
|
} ? Generated<TValue> : TOptions extends {
|
|
371
|
-
default:
|
|
371
|
+
default: unknown;
|
|
372
372
|
} ? Generated<TValue> : TOptions extends {
|
|
373
|
-
defaultRaw:
|
|
373
|
+
defaultRaw: unknown;
|
|
374
374
|
} ? Generated<TValue> : TProcessOnCreate extends false ? TValue : TOptions extends {
|
|
375
375
|
onCreate: Function;
|
|
376
376
|
} ? Generated<TValue> : TValue;
|