@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.
@@ -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 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;
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.columnNames.length === fkColumnsLength &&
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 Set();
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
- propBaseNames.add(columnName);
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
- propBaseNames.add(columnName);
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
- propBaseNames.add(propName);
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
- return Array.from(propBaseNames).map(baseName => this.getPropertyName(namingStrategy, baseName, fksOnColumnProps.get(baseName)));
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 && !idx.composite && !idx.unique && !idx.primary);
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 && !idx.composite && idx.unique && !idx.primary);
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 && Number.isFinite(+defaultValue)) {
993
- defaultValue = this.#platform.formatDecimal(defaultValue, c.scale).toString();
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
- .replace(/in\s*\((.*?)\)/gi, '= any (array[$1])')
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
- .replace(/[()\n[\]*]|::\w+| +/g, '')
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 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));
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();
@@ -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,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;
@@ -1,7 +1,17 @@
1
1
  import { isRaw, Utils, } from '@mikro-orm/core';
2
- /** Flattens `;\n` boundaries so the schema-generator's statement splitter doesn't break the routine DDL apart. Other whitespace is preserved. */
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.replace(/;[\t ]*\r?\n/g, '; ');
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
- let sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
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 (['type', 'nullable', 'autoincrement', 'unsigned', 'default', 'enumItems', 'collation'].some(t => changedProperties.has(t))) {
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
- return [`alter table ${table.getQuotedName()} ${adds}`];
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.hasNonDefaultPrimaryKeyName(table) ? `constraint ${this.quote(primaryKey.keyName)} ` : '';
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.hasNonDefaultPrimaryKeyName(table) ? `constraint ${index.keyName} ` : '';
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}; end`;
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).split('\n');
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
- if (line.trim() === '') {
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(line.trim());
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: true;
371
+ default: unknown;
372
372
  } ? Generated<TValue> : TOptions extends {
373
- defaultRaw: true;
373
+ defaultRaw: unknown;
374
374
  } ? Generated<TValue> : TProcessOnCreate extends false ? TValue : TOptions extends {
375
375
  onCreate: Function;
376
376
  } ? Generated<TValue> : TValue;