@mikro-orm/sql 7.2.3-dev.9 → 7.2.3

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.
@@ -5,6 +5,8 @@ import { normalizePartitionBound, normalizePartitionDefinition } from '../../sch
5
5
  const POSTGIS_VIEWS = ['geography_columns', 'geometry_columns'];
6
6
  /** Dollar-quote delimiter, e.g. `$$` or `$body$`, captured so `split` keeps it. */
7
7
  const DOLLAR_QUOTE_TAG = /(\$(?:[A-Za-z_]\w*)?\$)/;
8
+ /** pg-added `(<operand>)::type` cast in a check body, including multi word types like `double precision`. */
9
+ const CHECK_CAST_RE = /\(([^()]*)\)::\w+(?: precision| varying| with(?:out)? time zone)?(?:\[\])?/g;
8
10
  export class PostgreSqlSchemaHelper extends SchemaHelper {
9
11
  static DEFAULT_VALUES = {
10
12
  'now()': ['now()', 'current_timestamp'],
@@ -513,7 +515,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
513
515
  // SchemaHelper.createCheck).
514
516
  const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
515
517
  const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
516
- const def = m ? m[1].replace(/\(([^()]*)\)::\w+(?:\[\])?/g, '$1') : single ? single[1] : check.expression;
518
+ const def = m ? m[1].replace(CHECK_CAST_RE, '$1') : single ? single[1] : check.expression;
517
519
  ret[key].push({
518
520
  name: check.name,
519
521
  columnName: check.column_name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.2.3-dev.9",
3
+ "version": "7.2.3",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -50,10 +50,10 @@
50
50
  "kysely": "0.29.6"
51
51
  },
52
52
  "devDependencies": {
53
- "@mikro-orm/core": "^7.2.2"
53
+ "@mikro-orm/core": "^7.2.3"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.2.3-dev.9"
56
+ "@mikro-orm/core": "7.2.3"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"
@@ -2,6 +2,16 @@ import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, JsonType, inspect,
2
2
  import { EMBEDDABLE_ARRAY_OPS, JoinType, QueryType } from './enums.js';
3
3
  /** Captures the column name that follows the alias placeholder. */
4
4
  const QUALIFYING_ALIAS_RE = new RegExp(ALIAS_REPLACEMENT_RE + '(?=["\'`\\]]*\\.["\'`\\[]?([\\w$]+))', 'g');
5
+ /** Maps regexp tokens to LIKE pattern tokens, escaping LIKE wildcards with `!`. */
6
+ const LIKE_REPLACEMENTS = {
7
+ '\\.': '.',
8
+ '\\/': '/',
9
+ '.*': '%',
10
+ '.': '_',
11
+ '!': '!!',
12
+ '%': '!%',
13
+ _: '!_',
14
+ };
5
15
  /**
6
16
  * @internal
7
17
  */
@@ -359,19 +369,17 @@ export class QueryBuilderHelper {
359
369
  if (!(re instanceof RegExp)) {
360
370
  return false;
361
371
  }
362
- if (re.flags.includes('i')) {
372
+ if (/[im]/.test(re.flags)) {
363
373
  return false;
364
374
  }
365
- // when including the opening bracket/paren we consider it complex
366
- return !/[{[(]/.exec(re.source);
375
+ // LIKE can only express edge anchors, `.`, `.*` and escaped `.` or `/`, anything else needs the regexp operator
376
+ return /^\^?(?:[^\\^$.*+?|{}[\]()]|\\[./]|\.\*?)*\$?$/.test(re.source);
367
377
  }
368
378
  getRegExpParam(re) {
369
379
  const value = re.source
370
- .replace(/\.\*/g, '%') // .* -> %
371
- .replace(/\./g, '_') // . -> _
372
- .replace(/\\_/g, '.') // \. -> .
373
- .replace(/^\^/g, '') // remove ^ from start
374
- .replace(/\$$/g, ''); // remove $ from end
380
+ .replace(/^\^/, '') // remove ^ from start
381
+ .replace(/\$$/, '') // remove $ from end
382
+ .replace(/\\[./]|\.\*|[.!%_]/g, m => LIKE_REPLACEMENTS[m]);
375
383
  if (re.source.startsWith('^') && re.source.endsWith('$')) {
376
384
  return value;
377
385
  }
@@ -445,7 +453,8 @@ export class QueryBuilderHelper {
445
453
  const parts = [];
446
454
  const params = [];
447
455
  if (this.isSimpleRegExp(cond[key])) {
448
- parts.push(`${this.#platform.quoteIdentifier(this.mapper(key, type))} like ?`);
456
+ const escape = /[!%_]/.test(cond[key].source) ? ` escape '!'` : '';
457
+ parts.push(`${this.#platform.quoteIdentifier(this.mapper(key, type))} like ?${escape}`);
449
458
  params.push(this.getRegExpParam(cond[key]));
450
459
  return { sql: parts.join(' and '), params };
451
460
  }
@@ -975,17 +975,22 @@ export class DatabaseTable {
975
975
  throw new Error(`Index '${name}' on entity '${meta.className}': cannot combine \`expression\` with \`where\` — inline the WHERE clause into the \`expression\` escape hatch, or drop \`expression\` and use structured \`properties\` + \`where\`.`);
976
976
  }
977
977
  const where = this.processIndexWhere(index.where, meta);
978
+ const expression = this.processIndexExpression(name, index.expression, meta);
978
979
  this.#indexes.push({
979
980
  keyName: name,
980
981
  columnNames: properties,
981
982
  composite: properties.length > 1,
982
983
  // JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them.
983
984
  // Partial indexes (`where`) must use CREATE [UNIQUE] INDEX form — constraints can't carry predicates.
984
- constraint: type !== 'index' && !properties.some((d) => d.includes('.')) && !where,
985
+ // Raw `create [unique] index` expressions are plain indexes, not constraints.
986
+ constraint: type !== 'index' &&
987
+ !properties.some((d) => d.includes('.')) &&
988
+ !where &&
989
+ !/^\s*create\s/i.test(expression ?? ''),
985
990
  primary: type === 'primary',
986
991
  unique: type !== 'index',
987
992
  type: index.type,
988
- expression: this.processIndexExpression(name, index.expression, meta),
993
+ expression,
989
994
  where,
990
995
  options: index.options,
991
996
  deferMode: index.deferMode,
@@ -30,7 +30,7 @@ export declare class SchemaComparator {
30
30
  * Returns the difference between the tables fromTable and toTable.
31
31
  * If there are no differences this method returns the boolean false.
32
32
  */
33
- diffTable(fromTable: DatabaseTable, toTable: DatabaseTable, inverseTableDiff?: TableDifference): TableDifference | false;
33
+ diffTable(fromTable: DatabaseTable, toTable: DatabaseTable, inverseTableDiff?: TableDifference, inverse?: boolean): TableDifference | false;
34
34
  /**
35
35
  * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop
36
36
  * however ambiguities between different possibilities should not lead to renaming at all.
@@ -77,7 +77,7 @@ export class SchemaComparator {
77
77
  diff.newTables[tableName] = toSchema.getTable(tableName);
78
78
  }
79
79
  else {
80
- const tableDifferences = this.diffTable(fromSchema.getTable(tableName), toSchema.getTable(tableName), inverseDiff?.changedTables[tableName]);
80
+ const tableDifferences = this.diffTable(fromSchema.getTable(tableName), toSchema.getTable(tableName), inverseDiff?.changedTables[tableName], !!inverseDiff);
81
81
  if (tableDifferences !== false) {
82
82
  diff.changedTables[tableName] = tableDifferences;
83
83
  }
@@ -319,7 +319,7 @@ export class SchemaComparator {
319
319
  * Returns the difference between the tables fromTable and toTable.
320
320
  * If there are no differences this method returns the boolean false.
321
321
  */
322
- diffTable(fromTable, toTable, inverseTableDiff) {
322
+ diffTable(fromTable, toTable, inverseTableDiff, inverse = false) {
323
323
  let changes = 0;
324
324
  const tableDifferences = {
325
325
  name: fromTable.getShortestName(),
@@ -353,7 +353,9 @@ export class SchemaComparator {
353
353
  });
354
354
  changes++;
355
355
  }
356
- if (diffPartitioning(fromTable.getPartitioning(), toTable.getPartitioning(), this.#platform.getDefaultSchemaName())) {
356
+ // partitioning is diffed one way (declared vs. existing), so the down diff has to compare it like the up one did
357
+ const [existingTable, declaredTable] = inverse ? [toTable, fromTable] : [fromTable, toTable];
358
+ if (diffPartitioning(existingTable.getPartitioning(), declaredTable.getPartitioning(), this.#platform.getDefaultSchemaName())) {
357
359
  tableDifferences.changedPartitioning = {
358
360
  from: fromTable.getPartitioning(),
359
361
  to: toTable.getPartitioning(),
@@ -477,8 +479,10 @@ export class SchemaComparator {
477
479
  if (!this.diffExpression(check.expression, toTableCheck.expression)) {
478
480
  continue;
479
481
  }
482
+ // only auto-generated enum checks, a user check tagged with an enum column must be diffed by expression
480
483
  if (fromColumn?.enumItems &&
481
484
  toColumn?.enumItems &&
485
+ check.name === this.#platform.getIndexName(fromTable.name, [fromColumn.name], 'check') &&
482
486
  !this.diffEnumItems(fromColumn.enumItems, toColumn.enumItems)) {
483
487
  continue;
484
488
  }
@@ -907,6 +911,7 @@ export class SchemaComparator {
907
911
  ?.replace(/(?<![\w'])_\w+'(.*?)'/g, '$1')
908
912
  .replace(/!=/g, '<>')
909
913
  // `\b` keeps this from firing inside identifiers like `min(...)`
914
+ .replace(/\bnot\s+in\s*\((.*?)\)/gi, '<> all (array[$1])')
910
915
  .replace(/\bin\s*\((.*?)\)/gi, '= any (array[$1])')
911
916
  // MySQL normalizes count(*) to count(0)
912
917
  .replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
@@ -922,6 +927,8 @@ export class SchemaComparator {
922
927
  // MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
923
928
  // Strip these prefixes - match word.word patterns and keep only the last part
924
929
  .replace(/\b\w+\.(\w+)/g, '$1')
930
+ // PostgreSQL rewrites `x [not] between a and b` to `x >= a and x <= b` (`x < a or x > b`), cast targets are skipped
931
+ .replace(/(?<!::\s*)\b(?!not\b)(\w+)\s+(not\s+)?between\s+(\S+)\s+and\s+(\S+)/gi, (_, x, not, a, b) => not ? `${x} < ${a} or ${x} > ${b}` : `${x} >= ${a} and ${x} <= ${b}`)
925
932
  // Normalize JOIN syntax: inner join -> join (equivalent in SQL)
926
933
  .replace(/\binner\s+join\b/gi, 'join')
927
934
  // PostgreSQL names an unaliased bare function call after the function itself,
@@ -38,10 +38,13 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
38
38
  schema?: string;
39
39
  }): string;
40
40
  /**
41
- * We need to drop foreign keys first for all tables to allow dropping PK constraints.
41
+ * Collects foreign keys from other tables that reference `table` (self-references are excluded — those travel with the rebuild).
42
+ * Foreign keys the referencing table drops or changes in the same diff are left to its own alter, and those of dropped tables are skipped.
42
43
  */
43
- /** Collects foreign keys from other tables that reference `table` (self-references are excluded — those travel with the rebuild). */
44
44
  private getInboundForeignKeys;
45
+ /**
46
+ * We need to drop foreign keys first for all tables to allow dropping PK constraints.
47
+ */
45
48
  private preAlterTable;
46
49
  /**
47
50
  * creates new database and connects to it
@@ -327,14 +327,16 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
327
327
  // Tables whose partitioning was added/changed can't be altered in place — they are rebuilt as a unit
328
328
  // (the helper handles the data-preserving swap), so keep them out of the regular alter passes.
329
329
  const changedTables = Object.values(schemaDiff.changedTables);
330
+ const dropsTables = !!options.dropTables && !options.safe;
330
331
  const partitioningRebuilds = changedTables
331
332
  .filter(changedTable => changedTable.changedPartitioning)
332
- .map(diff => ({ diff, inboundForeignKeys: this.getInboundForeignKeys(schemaDiff.fromSchema, diff.toTable) }));
333
+ .map(diff => ({ diff, inboundForeignKeys: this.getInboundForeignKeys(schemaDiff, diff.toTable, dropsTables) }));
333
334
  const alteredTables = changedTables.filter(changedTable => !changedTable.changedPartitioning);
334
- this.append(ret, this.helper.getPartitioningRebuildSQL(partitioningRebuilds, options.safe), true);
335
+ // before the rebuild, so inbound FKs the referencing table drops or changes are gone by then
335
336
  for (const changedTable of alteredTables) {
336
337
  this.append(ret, this.preAlterTable(changedTable, options.safe), true);
337
338
  }
339
+ this.append(ret, this.helper.getPartitioningRebuildSQL(partitioningRebuilds, options.safe), true);
338
340
  for (const changedTable of alteredTables) {
339
341
  this.append(ret, this.helper.alterTable(changedTable, options.safe), true);
340
342
  }
@@ -394,18 +396,24 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
394
396
  return this.wrapSchema(ret, options);
395
397
  }
396
398
  /**
397
- * We need to drop foreign keys first for all tables to allow dropping PK constraints.
399
+ * Collects foreign keys from other tables that reference `table` (self-references are excluded — those travel with the rebuild).
400
+ * Foreign keys the referencing table drops or changes in the same diff are left to its own alter, and those of dropped tables are skipped.
398
401
  */
399
- /** Collects foreign keys from other tables that reference `table` (self-references are excluded — those travel with the rebuild). */
400
- getInboundForeignKeys(fromSchema, table) {
402
+ getInboundForeignKeys(schemaDiff, table, dropsTables) {
401
403
  const unqualified = (name) => name.split('.').pop();
402
404
  const targetName = unqualified(table.name);
403
405
  const ret = [];
404
- for (const other of fromSchema.getTables()) {
405
- if (other.name === table.name && other.schema === table.schema) {
406
+ for (const other of schemaDiff.fromSchema.getTables()) {
407
+ if ((other.name === table.name && other.schema === table.schema) ||
408
+ (dropsTables && other.getShortestName() in schemaDiff.removedTables)) {
406
409
  continue;
407
410
  }
411
+ const otherDiff = schemaDiff.changedTables[other.getShortestName(false)];
408
412
  for (const foreignKey of Object.values(other.getForeignKeys())) {
413
+ const name = foreignKey.constraintName;
414
+ if (otherDiff && (name in otherDiff.removedForeignKeys || name in otherDiff.changedForeignKeys)) {
415
+ continue;
416
+ }
409
417
  if (unqualified(foreignKey.referencedTableName) === targetName) {
410
418
  ret.push({ table: other, foreignKey });
411
419
  }
@@ -413,6 +421,9 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
413
421
  }
414
422
  return ret;
415
423
  }
424
+ /**
425
+ * We need to drop foreign keys first for all tables to allow dropping PK constraints.
426
+ */
416
427
  preAlterTable(diff, safe) {
417
428
  const ret = [];
418
429
  // removed/changed policies must be dropped before any column type alter, including the pre-alter
@@ -270,18 +270,18 @@ export const diffPartitioning = (from, to, defaultSchema) => {
270
270
  if (!from) {
271
271
  return true;
272
272
  }
273
- if (normalizeQuotedIdentifiers(normalizePartitionDefinition(from.definition)) !==
274
- normalizeQuotedIdentifiers(normalizePartitionDefinition(to.definition))) {
273
+ const toDefinition = normalizeQuotedIdentifiers(normalizePartitionDefinition(to.definition));
274
+ if (normalizeQuotedIdentifiers(normalizePartitionDefinition(from.definition)) !== toDefinition) {
275
275
  return true;
276
276
  }
277
- if (from.partitions.length !== to.partitions.length) {
277
+ // list/range partitions created outside of metadata (e.g. at runtime) are left unmanaged, hash partitions are not
278
+ if (toDefinition.startsWith('hash') && from.partitions.length !== to.partitions.length) {
278
279
  return true;
279
280
  }
280
281
  const normalizeSchema = (schema) => (schema && schema !== defaultSchema ? schema : '');
281
282
  const serializePartition = (partition) => `${normalizeSchema(partition.schema)}.${partition.name}:${normalizeQuotedIdentifiers(normalizePartitionBound(partition.bound))}`;
282
- const fromPartitions = from.partitions.map(serializePartition).sort();
283
- const toPartitions = to.partitions.map(serializePartition).sort();
284
- return fromPartitions.some((partition, index) => partition !== toPartitions[index]);
283
+ const fromPartitions = new Set(from.partitions.map(serializePartition));
284
+ return to.partitions.some(partition => !fromPartitions.has(serializePartition(partition)));
285
285
  };
286
286
  const SUPPORTED_PARTITION_TYPES = ['hash', 'list', 'range'];
287
287
  const isSupportedPartitionType = (value) => SUPPORTED_PARTITION_TYPES.includes(value);