@mikro-orm/sql 7.2.0-dev.2 → 7.2.0-dev.20
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 +30 -3
- package/AbstractSqlConnection.js +75 -15
- package/AbstractSqlDriver.d.ts +1 -8
- package/AbstractSqlDriver.js +124 -55
- package/AbstractSqlPlatform.d.ts +4 -2
- package/AbstractSqlPlatform.js +35 -1
- package/SqlEntityManager.d.ts +2 -2
- package/SqlEntityManager.js +5 -4
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +1 -1
- package/dialects/mysql/BaseMySqlPlatform.d.ts +2 -0
- package/dialects/mysql/BaseMySqlPlatform.js +4 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -0
- package/dialects/mysql/MySqlSchemaHelper.js +4 -1
- package/dialects/oracledb/OracleNativeQueryBuilder.js +1 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +3 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.js +35 -2
- package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +22 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +181 -4
- package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -0
- package/dialects/sqlite/BaseSqliteConnection.js +15 -5
- package/dialects/sqlite/SqlitePlatform.d.ts +2 -0
- package/dialects/sqlite/SqlitePlatform.js +4 -0
- package/dialects/sqlite/SqliteSchemaHelper.js +2 -2
- package/package.json +4 -4
- package/plugin/transformer.d.ts +7 -1
- package/plugin/transformer.js +60 -1
- package/query/CriteriaNodeFactory.js +4 -0
- package/query/NativeQueryBuilder.js +1 -1
- package/query/ObjectCriteriaNode.d.ts +1 -0
- package/query/ObjectCriteriaNode.js +30 -5
- package/query/QueryBuilder.d.ts +40 -7
- package/query/QueryBuilder.js +181 -37
- package/query/QueryBuilderHelper.d.ts +5 -0
- package/query/QueryBuilderHelper.js +39 -9
- package/schema/DatabaseSchema.d.ts +4 -0
- package/schema/DatabaseSchema.js +107 -1
- package/schema/DatabaseTable.d.ts +15 -1
- package/schema/DatabaseTable.js +113 -19
- package/schema/SchemaComparator.d.ts +3 -0
- package/schema/SchemaComparator.js +123 -10
- package/schema/SchemaHelper.d.ts +26 -1
- package/schema/SchemaHelper.js +67 -9
- package/schema/SqlSchemaGenerator.d.ts +4 -0
- package/schema/SqlSchemaGenerator.js +59 -22
- package/typings.d.ts +20 -2
package/schema/DatabaseTable.js
CHANGED
|
@@ -10,10 +10,15 @@ export class DatabaseTable {
|
|
|
10
10
|
#indexes = [];
|
|
11
11
|
#checks = [];
|
|
12
12
|
#triggers = [];
|
|
13
|
+
#policies = [];
|
|
13
14
|
#foreignKeys = {};
|
|
14
15
|
#platform;
|
|
15
16
|
nativeEnums = {}; // for postgres
|
|
16
17
|
comment;
|
|
18
|
+
/** Whether row level security is enabled on the table (postgres only). */
|
|
19
|
+
rlsEnabled = false;
|
|
20
|
+
/** Whether row level security is also enforced for the table owner (postgres `force`). */
|
|
21
|
+
rlsForced = false;
|
|
17
22
|
partitioning;
|
|
18
23
|
/**
|
|
19
24
|
* Effective collation the column defaults to when no explicit `COLLATE` is set on a column.
|
|
@@ -55,6 +60,17 @@ export class DatabaseTable {
|
|
|
55
60
|
getTriggers() {
|
|
56
61
|
return this.#triggers;
|
|
57
62
|
}
|
|
63
|
+
getPolicies() {
|
|
64
|
+
return this.#policies;
|
|
65
|
+
}
|
|
66
|
+
/** `[]` and `['public']` both mean PUBLIC — a single predicate so metadata, introspection and codegen agree. */
|
|
67
|
+
static isDefaultPolicyRoles(roles) {
|
|
68
|
+
return roles.length === 0 || (roles.length === 1 && roles[0] === 'public');
|
|
69
|
+
}
|
|
70
|
+
/** @internal */
|
|
71
|
+
setPolicies(policies) {
|
|
72
|
+
this.#policies = policies;
|
|
73
|
+
}
|
|
58
74
|
/** @internal */
|
|
59
75
|
setIndexes(indexes) {
|
|
60
76
|
this.#indexes = indexes;
|
|
@@ -219,6 +235,8 @@ export class DatabaseTable {
|
|
|
219
235
|
skippedColumnNames.includes(index.columnNames[0]) || // Non-composite indexes for skipped columns are to be mapped as entity decorators.
|
|
220
236
|
index.deferMode ||
|
|
221
237
|
index.expression ||
|
|
238
|
+
index.where ||
|
|
239
|
+
this.hasAdvancedIndexOptions(index) ||
|
|
222
240
|
!(index.columnNames[0] in columnFks)) && // Trivial non-composite indexes for scalar props are to be mapped to the column.
|
|
223
241
|
// 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
242
|
!(index.columnNames.some(col => !col) && !index.expression));
|
|
@@ -254,14 +272,7 @@ export class DatabaseTable {
|
|
|
254
272
|
}
|
|
255
273
|
}
|
|
256
274
|
// 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;
|
|
275
|
+
const isTrivial = !index.deferMode && !index.expression && !index.where && !this.hasAdvancedIndexOptions(index);
|
|
265
276
|
if (isTrivial) {
|
|
266
277
|
// Index is for FK. Map to the FK prop and move on.
|
|
267
278
|
const fkForIndex = fkIndexes.get(index);
|
|
@@ -299,6 +310,19 @@ export class DatabaseTable {
|
|
|
299
310
|
}
|
|
300
311
|
schema.addIndex(ret);
|
|
301
312
|
}
|
|
313
|
+
for (const check of this.getChecks()) {
|
|
314
|
+
// skip checks that were consumed by enum conversion — the enum property recreates an
|
|
315
|
+
// equivalent check under the conventional name during discovery (only on platforms that
|
|
316
|
+
// emulate enums via check constraints; mysql/mariadb enums are native and recreate nothing)
|
|
317
|
+
const enumItems = check.columnName ? this.getColumn(check.columnName)?.enumItems : undefined;
|
|
318
|
+
if (this.#platform.usesEnumCheckConstraints() &&
|
|
319
|
+
enumItems?.length &&
|
|
320
|
+
(check.expression === this.#platform.getEnumCheckConstraintExpression(check.columnName, enumItems) ||
|
|
321
|
+
check.name === this.#platform.getIndexName(this.name, [check.columnName], 'check'))) {
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
schema.meta.checks.push({ name: check.name, expression: check.expression });
|
|
325
|
+
}
|
|
302
326
|
const addedStandaloneFkPropsBasedOnColumn = new Set();
|
|
303
327
|
const nonSkippedColumns = this.getColumns().filter(column => !skippedColumnNames.includes(column.name));
|
|
304
328
|
for (const column of nonSkippedColumns) {
|
|
@@ -507,7 +531,9 @@ export class DatabaseTable {
|
|
|
507
531
|
findFkIndex(currentFk) {
|
|
508
532
|
const fkColumnsLength = currentFk.columnNames.length;
|
|
509
533
|
const possibleIndexes = this.#indexes.filter(index => {
|
|
510
|
-
return (index.
|
|
534
|
+
return (!index.where &&
|
|
535
|
+
!this.hasAdvancedIndexOptions(index) &&
|
|
536
|
+
index.columnNames.length === fkColumnsLength &&
|
|
511
537
|
!currentFk.columnNames.some((columnName, i) => index.columnNames[i] !== columnName));
|
|
512
538
|
});
|
|
513
539
|
possibleIndexes.sort((a, b) => {
|
|
@@ -521,13 +547,31 @@ export class DatabaseTable {
|
|
|
521
547
|
});
|
|
522
548
|
return possibleIndexes.at(0);
|
|
523
549
|
}
|
|
550
|
+
/** Advanced options require an entity-level declaration, as the property-level `index`/`unique` cannot carry them. */
|
|
551
|
+
hasAdvancedIndexOptions(index) {
|
|
552
|
+
return !!(index.columns?.length ||
|
|
553
|
+
index.include?.length ||
|
|
554
|
+
index.fillFactor ||
|
|
555
|
+
index.type ||
|
|
556
|
+
index.invisible ||
|
|
557
|
+
index.disabled ||
|
|
558
|
+
index.clustered);
|
|
559
|
+
}
|
|
524
560
|
getIndexProperties(index, columnFks, fksOnColumnProps, fksOnStandaloneProps, namingStrategy) {
|
|
525
|
-
const propBaseNames = new
|
|
561
|
+
const propBaseNames = new Map();
|
|
526
562
|
const columnNames = index.columnNames;
|
|
527
563
|
const l = columnNames.length;
|
|
528
564
|
if (columnNames.some(col => !col)) {
|
|
529
565
|
return;
|
|
530
566
|
}
|
|
567
|
+
const addPropBaseName = (baseName, position) => {
|
|
568
|
+
const positions = propBaseNames.get(baseName);
|
|
569
|
+
if (positions) {
|
|
570
|
+
positions.last = position;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
propBaseNames.set(baseName, { first: position, last: position });
|
|
574
|
+
};
|
|
531
575
|
for (let i = 0; i < l; ++i) {
|
|
532
576
|
const columnName = columnNames[i];
|
|
533
577
|
// The column is not involved with FKs.
|
|
@@ -538,14 +582,14 @@ export class DatabaseTable {
|
|
|
538
582
|
}
|
|
539
583
|
// It has a prop named after it.
|
|
540
584
|
// Add it and move on.
|
|
541
|
-
|
|
585
|
+
addPropBaseName(columnName, i);
|
|
542
586
|
continue;
|
|
543
587
|
}
|
|
544
588
|
// If the prop named after the column has a FK and the FK's columns are a subset of this index,
|
|
545
589
|
// include this prop and move on.
|
|
546
590
|
const columnPropFk = fksOnColumnProps.get(columnName);
|
|
547
591
|
if (columnPropFk && !columnPropFk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
|
|
548
|
-
|
|
592
|
+
addPropBaseName(columnName, i);
|
|
549
593
|
continue;
|
|
550
594
|
}
|
|
551
595
|
// If there is at least one standalone FK featuring this column,
|
|
@@ -557,7 +601,7 @@ export class DatabaseTable {
|
|
|
557
601
|
continue;
|
|
558
602
|
}
|
|
559
603
|
if (!fk.columnNames.some(fkColumnName => !columnNames.includes(fkColumnName))) {
|
|
560
|
-
|
|
604
|
+
addPropBaseName(propName, i);
|
|
561
605
|
propAdded = true;
|
|
562
606
|
}
|
|
563
607
|
}
|
|
@@ -568,7 +612,10 @@ export class DatabaseTable {
|
|
|
568
612
|
// Break the whole prop creation.
|
|
569
613
|
return;
|
|
570
614
|
}
|
|
571
|
-
|
|
615
|
+
// Props sharing their first column would otherwise follow FK discovery order, so break ties on the last one.
|
|
616
|
+
return Array.from(propBaseNames)
|
|
617
|
+
.sort(([, a], [, b]) => a.first - b.first || a.last - b.last)
|
|
618
|
+
.map(([baseName]) => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)));
|
|
572
619
|
}
|
|
573
620
|
getSafeBaseNameForFkProp(namingStrategy, currentFk, fks, columnName) {
|
|
574
621
|
if (columnName &&
|
|
@@ -639,6 +686,12 @@ export class DatabaseTable {
|
|
|
639
686
|
hasTrigger(triggerName) {
|
|
640
687
|
return !!this.getTrigger(triggerName);
|
|
641
688
|
}
|
|
689
|
+
getPolicy(policyName) {
|
|
690
|
+
return this.#policies.find(p => p.name === policyName);
|
|
691
|
+
}
|
|
692
|
+
hasPolicy(policyName) {
|
|
693
|
+
return !!this.getPolicy(policyName);
|
|
694
|
+
}
|
|
642
695
|
getPrimaryKey() {
|
|
643
696
|
return this.#indexes.find(i => i.primary);
|
|
644
697
|
}
|
|
@@ -694,9 +747,19 @@ export class DatabaseTable {
|
|
|
694
747
|
const prop = this.getPropertyName(namingStrategy, column.name, fk);
|
|
695
748
|
const persist = !(column.name in columnFks && typeof fk === 'undefined');
|
|
696
749
|
const index = compositeFkIndexes[prop] ||
|
|
697
|
-
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
750
|
+
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
751
|
+
!idx.composite &&
|
|
752
|
+
!idx.unique &&
|
|
753
|
+
!idx.primary &&
|
|
754
|
+
!idx.where &&
|
|
755
|
+
!this.hasAdvancedIndexOptions(idx));
|
|
698
756
|
const unique = compositeFkUniques[prop] ||
|
|
699
|
-
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
757
|
+
this.#indexes.find(idx => idx.columnNames[0] === column.name &&
|
|
758
|
+
!idx.composite &&
|
|
759
|
+
idx.unique &&
|
|
760
|
+
!idx.primary &&
|
|
761
|
+
!idx.where &&
|
|
762
|
+
!this.hasAdvancedIndexOptions(idx));
|
|
700
763
|
const kind = this.getReferenceKind(fk, unique);
|
|
701
764
|
const runtimeType = this.getPropertyTypeForColumn(namingStrategy, column, fk);
|
|
702
765
|
const type = fk
|
|
@@ -949,6 +1012,9 @@ export class DatabaseTable {
|
|
|
949
1012
|
addTrigger(trigger) {
|
|
950
1013
|
this.#triggers.push(trigger);
|
|
951
1014
|
}
|
|
1015
|
+
addPolicy(policy) {
|
|
1016
|
+
this.#policies.push(policy);
|
|
1017
|
+
}
|
|
952
1018
|
toJSON() {
|
|
953
1019
|
const columns = this.#columns;
|
|
954
1020
|
// locale-independent comparison so the snapshot is stable across machines
|
|
@@ -989,8 +1055,12 @@ export class DatabaseTable {
|
|
|
989
1055
|
// mysql stores decimal defaults padded to scale (`0` → `0.00`); collapse to canonical numeric form
|
|
990
1056
|
// so the metadata-side (`0`) and introspection-side (`0.00`) snapshots agree
|
|
991
1057
|
let defaultValue = c.default ?? null;
|
|
992
|
-
if (defaultValue != null && c.mappedType instanceof DecimalType
|
|
993
|
-
|
|
1058
|
+
if (defaultValue != null && c.mappedType instanceof DecimalType) {
|
|
1059
|
+
// string defaults like `default: '0.00'` are quoted in metadata, so strip the quotes first
|
|
1060
|
+
const unquoted = defaultValue.replace(/^'(.*)'$/, '$1');
|
|
1061
|
+
if (Number.isFinite(+unquoted)) {
|
|
1062
|
+
defaultValue = this.#platform.formatDecimal(unquoted, c.scale).toString();
|
|
1063
|
+
}
|
|
994
1064
|
}
|
|
995
1065
|
const normalized = {
|
|
996
1066
|
name: c.name,
|
|
@@ -1078,13 +1148,26 @@ export class DatabaseTable {
|
|
|
1078
1148
|
}
|
|
1079
1149
|
return out;
|
|
1080
1150
|
};
|
|
1151
|
+
const normalizePolicy = (policy) => {
|
|
1152
|
+
const out = { name: policy.name, command: policy.command, type: policy.type };
|
|
1153
|
+
if (!DatabaseTable.isDefaultPolicyRoles(policy.roles)) {
|
|
1154
|
+
out.roles = [...policy.roles].sort(byString);
|
|
1155
|
+
}
|
|
1156
|
+
for (const field of ['using', 'check']) {
|
|
1157
|
+
if (policy[field]) {
|
|
1158
|
+
out[field] = policy[field];
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
return out;
|
|
1162
|
+
};
|
|
1081
1163
|
const sortedIndexes = [...this.#indexes].sort((a, b) => byString(a.keyName, b.keyName)).map(normalizeIndex);
|
|
1082
1164
|
const sortedChecks = [...this.#checks].sort((a, b) => byString(a.name, b.name)).map(normalizeCheck);
|
|
1083
1165
|
const sortedTriggers = [...this.#triggers].sort((a, b) => byString(a.name, b.name));
|
|
1166
|
+
const sortedPolicies = [...this.#policies].sort((a, b) => byString(a.name, b.name)).map(normalizePolicy);
|
|
1084
1167
|
const sortedForeignKeys = Object.fromEntries(Object.entries(this.#foreignKeys)
|
|
1085
1168
|
.sort(([a], [b]) => byString(a, b))
|
|
1086
1169
|
.map(([k, v]) => [k, normalizeFk(v)]));
|
|
1087
|
-
|
|
1170
|
+
const ret = {
|
|
1088
1171
|
name: this.name,
|
|
1089
1172
|
schema: this.schema,
|
|
1090
1173
|
columns: columnsMapped,
|
|
@@ -1097,5 +1180,16 @@ export class DatabaseTable {
|
|
|
1097
1180
|
// platforms that can't read comments back (sqlite), where keeping it would flip the snapshot
|
|
1098
1181
|
comment: supportsComments ? this.comment || null : null,
|
|
1099
1182
|
};
|
|
1183
|
+
// emit RLS state only when set, so snapshots of non-RLS tables stay byte-for-byte unchanged
|
|
1184
|
+
if (sortedPolicies.length > 0) {
|
|
1185
|
+
ret.policies = sortedPolicies;
|
|
1186
|
+
}
|
|
1187
|
+
if (this.rlsEnabled) {
|
|
1188
|
+
ret.rlsEnabled = true;
|
|
1189
|
+
}
|
|
1190
|
+
if (this.rlsForced) {
|
|
1191
|
+
ret.rlsForced = true;
|
|
1192
|
+
}
|
|
1193
|
+
return ret;
|
|
1100
1194
|
}
|
|
1101
1195
|
}
|
|
@@ -88,7 +88,10 @@ export declare class SchemaComparator {
|
|
|
88
88
|
*/
|
|
89
89
|
private diffViewExpression;
|
|
90
90
|
private diffTrigger;
|
|
91
|
+
private diffPolicies;
|
|
92
|
+
private diffPolicy;
|
|
91
93
|
parseJsonDefault(defaultValue?: string | null): Dictionary | string | null;
|
|
94
|
+
private parseDecimalDefault;
|
|
92
95
|
hasSameDefaultValue(from: Column, to: Column): boolean;
|
|
93
96
|
private mapColumnToProperty;
|
|
94
97
|
private log;
|
|
@@ -328,6 +328,7 @@ export class SchemaComparator {
|
|
|
328
328
|
addedIndexes: {},
|
|
329
329
|
addedChecks: {},
|
|
330
330
|
addedTriggers: {},
|
|
331
|
+
addedPolicies: {},
|
|
331
332
|
changedColumns: {},
|
|
332
333
|
changedForeignKeys: {},
|
|
333
334
|
changedIndexes: {},
|
|
@@ -338,6 +339,7 @@ export class SchemaComparator {
|
|
|
338
339
|
removedIndexes: {},
|
|
339
340
|
removedChecks: {},
|
|
340
341
|
removedTriggers: {},
|
|
342
|
+
removedPolicies: {},
|
|
341
343
|
renamedColumns: {},
|
|
342
344
|
renamedIndexes: {},
|
|
343
345
|
fromTable,
|
|
@@ -517,6 +519,9 @@ export class SchemaComparator {
|
|
|
517
519
|
}
|
|
518
520
|
}
|
|
519
521
|
}
|
|
522
|
+
if (this.#platform.supportsRowLevelSecurity()) {
|
|
523
|
+
changes += this.diffPolicies(fromTable, toTable, tableDifferences);
|
|
524
|
+
}
|
|
520
525
|
const fromForeignKeys = { ...fromTable.getForeignKeys() };
|
|
521
526
|
const toForeignKeys = { ...toTable.getForeignKeys() };
|
|
522
527
|
for (const fromConstraint of Object.values(fromForeignKeys)) {
|
|
@@ -820,6 +825,10 @@ export class SchemaComparator {
|
|
|
820
825
|
if (!!index1.clustered !== !!index2.clustered) {
|
|
821
826
|
return false;
|
|
822
827
|
}
|
|
828
|
+
// Compare the index access method (e.g. `using gin` on PostgreSQL); unset means the platform default
|
|
829
|
+
if (this.#helper.getIndexAccessMethod(index1) !== this.#helper.getIndexAccessMethod(index2)) {
|
|
830
|
+
return false;
|
|
831
|
+
}
|
|
823
832
|
// Compare WHERE predicate of partial indexes structurally (whitespace/quoting/casing
|
|
824
833
|
// are normalized via the same helper used for check constraints).
|
|
825
834
|
if (this.diffExpression(index1.where ?? '', index2.where ?? '')) {
|
|
@@ -897,9 +906,17 @@ export class SchemaComparator {
|
|
|
897
906
|
// lookbehind: only strip a real charset introducer, never an underscore inside a literal like 'a_b'
|
|
898
907
|
?.replace(/(?<![\w'])_\w+'(.*?)'/g, '$1')
|
|
899
908
|
.replace(/!=/g, '<>')
|
|
900
|
-
|
|
909
|
+
// `\b` keeps this from firing inside identifiers like `min(...)`
|
|
910
|
+
.replace(/\bin\s*\((.*?)\)/gi, '= any (array[$1])')
|
|
901
911
|
// MySQL normalizes count(*) to count(0)
|
|
902
912
|
.replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
|
|
913
|
+
// multi word type names in casts, the generic `::\w+` below only covers single word ones
|
|
914
|
+
// the precision is kept, so `timestamptz(3)` and `timestamp(3) with time zone` leave the same residue
|
|
915
|
+
.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')
|
|
916
|
+
// Protect dots inside string literals before the quote strip below, or the alias-prefix normalization
|
|
917
|
+
// would mangle literal contents — `current_setting('app.tenant')` and `current_setting('req.tenant')`
|
|
918
|
+
// must not both collapse to `current_settingtenant`
|
|
919
|
+
.replace(/'([^']*)'/g, (_, inner) => `'${inner.replaceAll('.', '\u0000')}'`)
|
|
903
920
|
// Remove quotes first so we can process identifiers
|
|
904
921
|
.replace(/['"`]/g, '')
|
|
905
922
|
// MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
|
|
@@ -907,12 +924,18 @@ export class SchemaComparator {
|
|
|
907
924
|
.replace(/\b\w+\.(\w+)/g, '$1')
|
|
908
925
|
// Normalize JOIN syntax: inner join -> join (equivalent in SQL)
|
|
909
926
|
.replace(/\binner\s+join\b/gi, 'join')
|
|
927
|
+
// PostgreSQL names an unaliased bare function call after the function itself,
|
|
928
|
+
// so `max(created_at)` comes back as `max(created_at) AS max`
|
|
929
|
+
// the lookahead skips table function column alias lists like `unnest(a) AS unnest(c)`, which are meaningful
|
|
930
|
+
.replace(/\b(\w+)\s*\(((?:[^()]|\([^()]*\))*)\)\s+as\s+\1\b(?!\s*\()/gi, '$1($2)')
|
|
910
931
|
// Remove redundant column aliases like `title AS title` -> `title`
|
|
911
932
|
.replace(/\b(\w+)\s+as\s+\1\b/gi, '$1')
|
|
912
933
|
// Remove AS keyword (optional in SQL, MySQL may add/remove it)
|
|
913
934
|
.replace(/\bas\b/gi, '')
|
|
914
935
|
// Remove remaining special chars, parentheses, type casts, asterisks, and normalize whitespace
|
|
915
|
-
|
|
936
|
+
// tabs and CRs included — the schema generator trims every line before executing the DDL,
|
|
937
|
+
// so indentation and CRLF endings can never come back from introspection
|
|
938
|
+
.replace(/[()\n\r\t[\]*]|::\w+| +/g, '')
|
|
916
939
|
.replace(/anyarray\[(.*)]/gi, '$1')
|
|
917
940
|
.toLowerCase()
|
|
918
941
|
// PostgreSQL adds default aliases to aggregate functions (e.g., count(*) AS count)
|
|
@@ -949,13 +972,13 @@ export class SchemaComparator {
|
|
|
949
972
|
}
|
|
950
973
|
diffTrigger(from, to) {
|
|
951
974
|
// Raw DDL expression cannot be meaningfully compared to introspected
|
|
952
|
-
// trigger metadata, so
|
|
953
|
-
if (to.expression) {
|
|
975
|
+
// trigger metadata, so it needs special handling when either side uses it.
|
|
976
|
+
if (from.expression || to.expression) {
|
|
954
977
|
// Both sides have expression — compare the raw DDL directly
|
|
955
|
-
if (from.expression) {
|
|
978
|
+
if (from.expression && to.expression) {
|
|
956
979
|
return this.diffExpression(from.expression, to.expression);
|
|
957
980
|
}
|
|
958
|
-
// Only
|
|
981
|
+
// Only one side has expression — the raw DDL cannot be compared to
|
|
959
982
|
// introspected metadata. Changes to the expression value won't be detected;
|
|
960
983
|
// drop and recreate the trigger manually to apply expression changes.
|
|
961
984
|
return false;
|
|
@@ -971,6 +994,87 @@ export class SchemaComparator {
|
|
|
971
994
|
}
|
|
972
995
|
return this.diffExpression(from.body, to.body);
|
|
973
996
|
}
|
|
997
|
+
diffPolicies(fromTable, toTable, diff) {
|
|
998
|
+
let changes = 0;
|
|
999
|
+
// `ignorePolicies` makes RLS create-only: declared policies are still added and RLS is still enabled/forced,
|
|
1000
|
+
// but existing policies are never diffed for drop/alter and RLS is never disabled or unforced — this protects
|
|
1001
|
+
// hand-written policies on databases that adopted RLS before the ORM managed it
|
|
1002
|
+
const ignorePolicies = this.#platform.getConfig().get('schemaGenerator').ignorePolicies;
|
|
1003
|
+
// postgres rejects `alter column ... type` on a column referenced by any policy, so a type change forces us to
|
|
1004
|
+
// drop every still-present policy around the alter (dropped before via `getRlsDropSQL`, recreated after via
|
|
1005
|
+
// `getRlsAlterSQL`) even when the policy itself is otherwise unchanged; a `generated`-only change is emitted
|
|
1006
|
+
// as a drop + re-add of the same column, which a policy's column dependency blocks the same way
|
|
1007
|
+
const hasColumnTypeChange = Object.values(diff.changedColumns).some(c => c.changedProperties.has('type')) ||
|
|
1008
|
+
Object.keys(diff.removedColumns).some(name => name in diff.addedColumns);
|
|
1009
|
+
for (const policy of toTable.getPolicies()) {
|
|
1010
|
+
if (!fromTable.hasPolicy(policy.name)) {
|
|
1011
|
+
diff.addedPolicies[policy.name] = policy;
|
|
1012
|
+
this.log(`policy ${policy.name} added to table ${diff.name}`, { policy });
|
|
1013
|
+
changes++;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (fromTable.rlsEnabled !== toTable.rlsEnabled && (!ignorePolicies || toTable.rlsEnabled)) {
|
|
1017
|
+
diff.changedRlsEnabled = toTable.rlsEnabled;
|
|
1018
|
+
changes++;
|
|
1019
|
+
}
|
|
1020
|
+
if (fromTable.rlsForced !== toTable.rlsForced && (!ignorePolicies || toTable.rlsForced)) {
|
|
1021
|
+
diff.changedRlsForced = toTable.rlsForced;
|
|
1022
|
+
changes++;
|
|
1023
|
+
}
|
|
1024
|
+
if (ignorePolicies) {
|
|
1025
|
+
// existing policies are unmanaged here, but a type change still needs them dropped and recreated for the
|
|
1026
|
+
// alter to succeed — recreate each verbatim from introspection so the hand-written definition is preserved
|
|
1027
|
+
if (hasColumnTypeChange) {
|
|
1028
|
+
for (const policy of fromTable.getPolicies()) {
|
|
1029
|
+
diff.removedPolicies[policy.name] = policy;
|
|
1030
|
+
diff.addedPolicies[policy.name] = policy;
|
|
1031
|
+
changes += 2;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return changes;
|
|
1035
|
+
}
|
|
1036
|
+
for (const policy of fromTable.getPolicies()) {
|
|
1037
|
+
const toPolicy = toTable.getPolicy(policy.name);
|
|
1038
|
+
if (!toPolicy) {
|
|
1039
|
+
diff.removedPolicies[policy.name] = policy;
|
|
1040
|
+
this.log(`policy ${policy.name} removed from table ${diff.name}`);
|
|
1041
|
+
changes++;
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
// changed policies are always dropped (before column drops, which the old expression can block via its
|
|
1045
|
+
// column dependencies) and recreated (after column adds) — postgres could alter some of the changes in
|
|
1046
|
+
// place, but not a policy's command or type, nor unset an expression
|
|
1047
|
+
if (this.diffPolicy(policy, toPolicy)) {
|
|
1048
|
+
diff.removedPolicies[policy.name] = policy;
|
|
1049
|
+
diff.addedPolicies[policy.name] = toPolicy;
|
|
1050
|
+
this.log(`policy ${policy.name} recreated in table ${diff.name}`, { from: policy, to: toPolicy });
|
|
1051
|
+
changes += 2;
|
|
1052
|
+
continue;
|
|
1053
|
+
}
|
|
1054
|
+
// an unchanged policy still blocks a type change on any column, so drop + recreate it around the alter
|
|
1055
|
+
if (hasColumnTypeChange) {
|
|
1056
|
+
diff.removedPolicies[policy.name] = policy;
|
|
1057
|
+
diff.addedPolicies[policy.name] = toPolicy;
|
|
1058
|
+
this.log(`policy ${policy.name} recreated around a column type change in table ${diff.name}`);
|
|
1059
|
+
changes += 2;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return changes;
|
|
1063
|
+
}
|
|
1064
|
+
diffPolicy(from, to) {
|
|
1065
|
+
// normalize so an omitted `roles` matches introspected `{public}`
|
|
1066
|
+
const normalizeRoles = (roles) => DatabaseTable.isDefaultPolicyRoles(roles) ? '' : [...roles].sort().join(',');
|
|
1067
|
+
if (from.command !== to.command || from.type !== to.type) {
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
if (normalizeRoles(from.roles) !== normalizeRoles(to.roles)) {
|
|
1071
|
+
return true;
|
|
1072
|
+
}
|
|
1073
|
+
if (this.diffExpression(from.using ?? '', to.using ?? '')) {
|
|
1074
|
+
return true;
|
|
1075
|
+
}
|
|
1076
|
+
return this.diffExpression(from.check ?? '', to.check ?? '');
|
|
1077
|
+
}
|
|
974
1078
|
parseJsonDefault(defaultValue) {
|
|
975
1079
|
/* v8 ignore next */
|
|
976
1080
|
if (!defaultValue) {
|
|
@@ -979,6 +1083,10 @@ export class SchemaComparator {
|
|
|
979
1083
|
const val = defaultValue.replace(/^(_\w+\\)?'(.*?)\\?'$/, '$2').replace(/^\(?'(.*?)'\)?$/, '$1');
|
|
980
1084
|
return parseJsonSafe(val);
|
|
981
1085
|
}
|
|
1086
|
+
parseDecimalDefault(defaultValue) {
|
|
1087
|
+
const value = +('' + defaultValue).replace(/^'(.+)'$/, '$1');
|
|
1088
|
+
return Number.isFinite(value) ? value : null;
|
|
1089
|
+
}
|
|
982
1090
|
hasSameDefaultValue(from, to) {
|
|
983
1091
|
if (from.default == null ||
|
|
984
1092
|
from.default.toString().toLowerCase() === 'null' ||
|
|
@@ -1004,10 +1112,15 @@ export class SchemaComparator {
|
|
|
1004
1112
|
const defaultValueTo = to.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
|
|
1005
1113
|
return defaultValueFrom === defaultValueTo;
|
|
1006
1114
|
}
|
|
1007
|
-
// mysql
|
|
1008
|
-
//
|
|
1009
|
-
if (to.mappedType instanceof DecimalType
|
|
1010
|
-
|
|
1115
|
+
// mysql pads decimal defaults to scale (`0` → `0.00`) and postgres reports them unquoted
|
|
1116
|
+
// while metadata keeps them quoted; compare numerically so neither churns a no-op migration
|
|
1117
|
+
if (to.mappedType instanceof DecimalType) {
|
|
1118
|
+
const defaultValueFrom = this.parseDecimalDefault(from.default);
|
|
1119
|
+
const defaultValueTo = this.parseDecimalDefault(to.default);
|
|
1120
|
+
if (defaultValueFrom != null && defaultValueTo != null) {
|
|
1121
|
+
return (this.#platform.formatDecimal(defaultValueFrom, to.scale) ===
|
|
1122
|
+
this.#platform.formatDecimal(defaultValueTo, to.scale));
|
|
1123
|
+
}
|
|
1011
1124
|
}
|
|
1012
1125
|
if (from.default && to.default) {
|
|
1013
1126
|
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,14 @@ 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;
|
|
157
|
+
/** Row level security DDL for a freshly created table (enable/force + create policies). Postgres only. */
|
|
158
|
+
getRlsCreateSQL(table: DatabaseTable): string[];
|
|
159
|
+
/** Drops removed/changed row level security policies; emitted in the pre-alter phase, before any column drop or type alter a policy expression can block. Postgres only. */
|
|
160
|
+
getRlsDropSQL(diff: TableDifference, safe?: boolean): string[];
|
|
161
|
+
/** Row level security DDL for a table difference (enable/disable/force transitions + policy creation). Postgres only. */
|
|
162
|
+
getRlsAlterSQL(diff: TableDifference, safe?: boolean): string[];
|
|
140
163
|
getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;
|
|
141
164
|
protected mapIndexes(indexes: IndexDef[]): Promise<IndexDef[]>;
|
|
142
165
|
mapForeignKeys(fks: any[], tableName: string, schemaName?: string): Dictionary;
|
|
@@ -176,6 +199,8 @@ export declare abstract class SchemaHelper {
|
|
|
176
199
|
createRoutine(_routine: SqlRoutineDef): string;
|
|
177
200
|
dropRoutine(_routine: SqlRoutineDef): string;
|
|
178
201
|
getAllRoutines(_connection: AbstractSqlConnection, _schemas?: string[]): Promise<SqlRoutineDef[]>;
|
|
202
|
+
/** Flattens internal `;\n` so the statement splitter doesn't tear the DDL, and ensures exactly one trailing `;` for the enclosing `begin ... end` block. */
|
|
203
|
+
protected normalizeTriggerBody(body: string): string;
|
|
179
204
|
/** 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
205
|
protected wrapRoutineBody(body: string): string;
|
|
181
206
|
protected stripRoutineBody(body: string): string;
|