@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.
Files changed (46) hide show
  1. package/AbstractSqlConnection.d.ts +30 -3
  2. package/AbstractSqlConnection.js +75 -15
  3. package/AbstractSqlDriver.d.ts +1 -8
  4. package/AbstractSqlDriver.js +124 -55
  5. package/AbstractSqlPlatform.d.ts +4 -2
  6. package/AbstractSqlPlatform.js +35 -1
  7. package/SqlEntityManager.d.ts +2 -2
  8. package/SqlEntityManager.js +5 -4
  9. package/dialects/mssql/MsSqlNativeQueryBuilder.js +1 -1
  10. package/dialects/mysql/BaseMySqlPlatform.d.ts +2 -0
  11. package/dialects/mysql/BaseMySqlPlatform.js +4 -0
  12. package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -0
  13. package/dialects/mysql/MySqlSchemaHelper.js +4 -1
  14. package/dialects/oracledb/OracleNativeQueryBuilder.js +1 -1
  15. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +3 -1
  16. package/dialects/postgresql/BasePostgreSqlPlatform.js +35 -2
  17. package/dialects/postgresql/PostgreSqlExceptionConverter.js +8 -1
  18. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +22 -1
  19. package/dialects/postgresql/PostgreSqlSchemaHelper.js +181 -4
  20. package/dialects/sqlite/BaseSqliteConnection.d.ts +3 -0
  21. package/dialects/sqlite/BaseSqliteConnection.js +15 -5
  22. package/dialects/sqlite/SqlitePlatform.d.ts +2 -0
  23. package/dialects/sqlite/SqlitePlatform.js +4 -0
  24. package/dialects/sqlite/SqliteSchemaHelper.js +2 -2
  25. package/package.json +4 -4
  26. package/plugin/transformer.d.ts +7 -1
  27. package/plugin/transformer.js +60 -1
  28. package/query/CriteriaNodeFactory.js +4 -0
  29. package/query/NativeQueryBuilder.js +1 -1
  30. package/query/ObjectCriteriaNode.d.ts +1 -0
  31. package/query/ObjectCriteriaNode.js +30 -5
  32. package/query/QueryBuilder.d.ts +40 -7
  33. package/query/QueryBuilder.js +181 -37
  34. package/query/QueryBuilderHelper.d.ts +5 -0
  35. package/query/QueryBuilderHelper.js +39 -9
  36. package/schema/DatabaseSchema.d.ts +4 -0
  37. package/schema/DatabaseSchema.js +107 -1
  38. package/schema/DatabaseTable.d.ts +15 -1
  39. package/schema/DatabaseTable.js +113 -19
  40. package/schema/SchemaComparator.d.ts +3 -0
  41. package/schema/SchemaComparator.js +123 -10
  42. package/schema/SchemaHelper.d.ts +26 -1
  43. package/schema/SchemaHelper.js +67 -9
  44. package/schema/SqlSchemaGenerator.d.ts +4 -0
  45. package/schema/SqlSchemaGenerator.js +59 -22
  46. package/typings.d.ts +20 -2
@@ -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 hasAdvancedOptions = index.columns?.length ||
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.columnNames.length === fkColumnsLength &&
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 Set();
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
- propBaseNames.add(columnName);
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
- propBaseNames.add(columnName);
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
- propBaseNames.add(propName);
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
- return Array.from(propBaseNames).map(baseName => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)));
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 && !idx.composite && !idx.unique && !idx.primary);
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 && !idx.composite && idx.unique && !idx.primary);
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 && Number.isFinite(+defaultValue)) {
993
- defaultValue = this.#platform.formatDecimal(defaultValue, c.scale).toString();
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
- return {
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
- .replace(/in\s*\((.*?)\)/gi, '= any (array[$1])')
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
- .replace(/[()\n[\]*]|::\w+| +/g, '')
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 skip diffing when the metadata side uses it.
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 metadata side has expression — the raw DDL cannot be compared to
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 stores decimal defaults padded to scale (`0` → `0.00`); compare numerically so the
1008
- // entity-side raw literal and the introspected padded form don't churn the no-op migration
1009
- if (to.mappedType instanceof DecimalType && Number.isFinite(+from.default) && Number.isFinite(+to.default)) {
1010
- return (this.#platform.formatDecimal(from.default, to.scale) === this.#platform.formatDecimal(to.default, to.scale));
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();
@@ -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
- /** Flattens `;\n` boundaries so the schema-generator's statement splitter doesn't break the routine DDL apart. Other whitespace is preserved. */
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;