@mikro-orm/sql 7.3.0-dev.11 → 7.3.0-dev.13
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/dialects/postgresql/PostgreSqlSchemaHelper.js +3 -1
- package/package.json +3 -3
- package/query/QueryBuilderHelper.js +18 -9
- package/schema/DatabaseTable.js +7 -2
- package/schema/SchemaComparator.d.ts +1 -1
- package/schema/SchemaComparator.js +10 -3
- package/schema/SqlSchemaGenerator.d.ts +7 -4
- package/schema/SqlSchemaGenerator.js +21 -10
- package/schema/partitioning.js +6 -6
|
@@ -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'],
|
|
@@ -527,7 +529,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
527
529
|
// SchemaHelper.createCheck).
|
|
528
530
|
const m = /^check \(\((.*)\)\)$/is.exec(check.expression);
|
|
529
531
|
const single = m ? null : /^check \((.*)\)$/is.exec(check.expression);
|
|
530
|
-
const def = m ? m[1].replace(
|
|
532
|
+
const def = m ? m[1].replace(CHECK_CAST_RE, '$1') : single ? single[1] : check.expression;
|
|
531
533
|
ret[key].push({
|
|
532
534
|
name: check.name,
|
|
533
535
|
columnName: check.column_name,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.3.0-dev.
|
|
3
|
+
"version": "7.3.0-dev.13",
|
|
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.
|
|
53
|
+
"@mikro-orm/core": "^7.2.3"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.3.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.3.0-dev.13"
|
|
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
|
|
372
|
+
if (/[im]/.test(re.flags)) {
|
|
363
373
|
return false;
|
|
364
374
|
}
|
|
365
|
-
//
|
|
366
|
-
return
|
|
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(
|
|
371
|
-
.replace(
|
|
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
|
-
|
|
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
|
}
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -977,17 +977,22 @@ export class DatabaseTable {
|
|
|
977
977
|
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\`.`);
|
|
978
978
|
}
|
|
979
979
|
const where = this.processIndexWhere(index.where, meta);
|
|
980
|
+
const expression = this.processIndexExpression(name, index.expression, meta);
|
|
980
981
|
this.#indexes.push({
|
|
981
982
|
keyName: name,
|
|
982
983
|
columnNames: properties,
|
|
983
984
|
composite: properties.length > 1,
|
|
984
985
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them.
|
|
985
986
|
// Partial indexes (`where`) must use CREATE [UNIQUE] INDEX form — constraints can't carry predicates.
|
|
986
|
-
|
|
987
|
+
// Raw `create [unique] index` expressions are plain indexes, not constraints.
|
|
988
|
+
constraint: type !== 'index' &&
|
|
989
|
+
!properties.some((d) => d.includes('.')) &&
|
|
990
|
+
!where &&
|
|
991
|
+
!/^\s*create\s/i.test(expression ?? ''),
|
|
987
992
|
primary: type === 'primary',
|
|
988
993
|
unique: type !== 'index',
|
|
989
994
|
type: index.type,
|
|
990
|
-
expression
|
|
995
|
+
expression,
|
|
991
996
|
where,
|
|
992
997
|
options: index.options,
|
|
993
998
|
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
|
-
|
|
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
|
}
|
|
@@ -909,6 +913,7 @@ export class SchemaComparator {
|
|
|
909
913
|
?.replace(/(?<![\w'])_\w+'(.*?)'/g, '$1')
|
|
910
914
|
.replace(/!=/g, '<>')
|
|
911
915
|
// `\b` keeps this from firing inside identifiers like `min(...)`
|
|
916
|
+
.replace(/\bnot\s+in\s*\((.*?)\)/gi, '<> all (array[$1])')
|
|
912
917
|
.replace(/\bin\s*\((.*?)\)/gi, '= any (array[$1])')
|
|
913
918
|
// MySQL normalizes count(*) to count(0)
|
|
914
919
|
.replace(/\bcount\s*\(\s*0\s*\)/gi, 'count(*)')
|
|
@@ -924,6 +929,8 @@ export class SchemaComparator {
|
|
|
924
929
|
// MySQL adds table/alias prefixes to columns (e.g., a.name or table_name.column vs just column)
|
|
925
930
|
// Strip these prefixes - match word.word patterns and keep only the last part
|
|
926
931
|
.replace(/\b\w+\.(\w+)/g, '$1')
|
|
932
|
+
// PostgreSQL rewrites `x [not] between a and b` to `x >= a and x <= b` (`x < a or x > b`), cast targets are skipped
|
|
933
|
+
.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}`)
|
|
927
934
|
// Normalize JOIN syntax: inner join -> join (equivalent in SQL)
|
|
928
935
|
.replace(/\binner\s+join\b/gi, 'join')
|
|
929
936
|
// 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
|
-
*
|
|
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
|
|
@@ -56,8 +59,8 @@ export declare class SqlSchemaGenerator extends AbstractSchemaGenerator<Abstract
|
|
|
56
59
|
}): Promise<void>;
|
|
57
60
|
/** Splits the SQL on the separator, keeping the separators that fall inside a string literal. */
|
|
58
61
|
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;
|
|
62
|
+
/** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. `previous` is the line before it in the current batch. */
|
|
63
|
+
protected startsBatch(_statement: string, _previous: string): boolean;
|
|
61
64
|
dropTableIfExists(name: string, schema?: string): Promise<void>;
|
|
62
65
|
private wrapSchema;
|
|
63
66
|
private append;
|
|
@@ -335,14 +335,16 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
335
335
|
// Tables whose partitioning was added/changed can't be altered in place — they are rebuilt as a unit
|
|
336
336
|
// (the helper handles the data-preserving swap), so keep them out of the regular alter passes.
|
|
337
337
|
const changedTables = Object.values(schemaDiff.changedTables);
|
|
338
|
+
const dropsTables = !!options.dropTables && !options.safe;
|
|
338
339
|
const partitioningRebuilds = changedTables
|
|
339
340
|
.filter(changedTable => changedTable.changedPartitioning)
|
|
340
|
-
.map(diff => ({ diff, inboundForeignKeys: this.getInboundForeignKeys(schemaDiff
|
|
341
|
+
.map(diff => ({ diff, inboundForeignKeys: this.getInboundForeignKeys(schemaDiff, diff.toTable, dropsTables) }));
|
|
341
342
|
const alteredTables = changedTables.filter(changedTable => !changedTable.changedPartitioning);
|
|
342
|
-
|
|
343
|
+
// before the rebuild, so inbound FKs the referencing table drops or changes are gone by then
|
|
343
344
|
for (const changedTable of alteredTables) {
|
|
344
345
|
this.append(ret, this.preAlterTable(changedTable, options.safe), true);
|
|
345
346
|
}
|
|
347
|
+
this.append(ret, this.helper.getPartitioningRebuildSQL(partitioningRebuilds, options.safe), true);
|
|
346
348
|
for (const changedTable of alteredTables) {
|
|
347
349
|
this.append(ret, this.helper.alterTable(changedTable, options.safe), true);
|
|
348
350
|
}
|
|
@@ -402,18 +404,24 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
402
404
|
return this.wrapSchema(ret, options);
|
|
403
405
|
}
|
|
404
406
|
/**
|
|
405
|
-
*
|
|
407
|
+
* Collects foreign keys from other tables that reference `table` (self-references are excluded — those travel with the rebuild).
|
|
408
|
+
* 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.
|
|
406
409
|
*/
|
|
407
|
-
|
|
408
|
-
getInboundForeignKeys(fromSchema, table) {
|
|
410
|
+
getInboundForeignKeys(schemaDiff, table, dropsTables) {
|
|
409
411
|
const unqualified = (name) => name.split('.').pop();
|
|
410
412
|
const targetName = unqualified(table.name);
|
|
411
413
|
const ret = [];
|
|
412
|
-
for (const other of fromSchema.getTables()) {
|
|
413
|
-
if (other.name === table.name && other.schema === table.schema)
|
|
414
|
+
for (const other of schemaDiff.fromSchema.getTables()) {
|
|
415
|
+
if ((other.name === table.name && other.schema === table.schema) ||
|
|
416
|
+
(dropsTables && other.getShortestName() in schemaDiff.removedTables)) {
|
|
414
417
|
continue;
|
|
415
418
|
}
|
|
419
|
+
const otherDiff = schemaDiff.changedTables[other.getShortestName(false)];
|
|
416
420
|
for (const foreignKey of Object.values(other.getForeignKeys())) {
|
|
421
|
+
const name = foreignKey.constraintName;
|
|
422
|
+
if (otherDiff && (name in otherDiff.removedForeignKeys || name in otherDiff.changedForeignKeys)) {
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
417
425
|
if (unqualified(foreignKey.referencedTableName) === targetName) {
|
|
418
426
|
ret.push({ table: other, foreignKey });
|
|
419
427
|
}
|
|
@@ -421,6 +429,9 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
421
429
|
}
|
|
422
430
|
return ret;
|
|
423
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* We need to drop foreign keys first for all tables to allow dropping PK constraints.
|
|
434
|
+
*/
|
|
424
435
|
preAlterTable(diff, safe) {
|
|
425
436
|
const ret = [];
|
|
426
437
|
// removed/changed policies must be dropped before any column type alter, including the pre-alter
|
|
@@ -468,7 +479,7 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
468
479
|
continue;
|
|
469
480
|
}
|
|
470
481
|
// same boundary an empty line creates, for statements that have to start their own batch
|
|
471
|
-
if (groups[i]?.length > 0 && this.startsBatch(stmt)) {
|
|
482
|
+
if (groups[i]?.length > 0 && this.startsBatch(stmt, groups[i].at(-1))) {
|
|
472
483
|
i++;
|
|
473
484
|
}
|
|
474
485
|
groups[i] ??= [];
|
|
@@ -512,8 +523,8 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
512
523
|
}
|
|
513
524
|
return parts;
|
|
514
525
|
}
|
|
515
|
-
/** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. */
|
|
516
|
-
startsBatch(_statement) {
|
|
526
|
+
/** Whether the statement has to be the first one in a query batch, e.g. `create trigger` on MSSQL. `previous` is the line before it in the current batch. */
|
|
527
|
+
startsBatch(_statement, _previous) {
|
|
517
528
|
return false;
|
|
518
529
|
}
|
|
519
530
|
async dropTableIfExists(name, schema) {
|
package/schema/partitioning.js
CHANGED
|
@@ -270,18 +270,18 @@ export const diffPartitioning = (from, to, defaultSchema) => {
|
|
|
270
270
|
if (!from) {
|
|
271
271
|
return true;
|
|
272
272
|
}
|
|
273
|
-
|
|
274
|
-
|
|
273
|
+
const toDefinition = normalizeQuotedIdentifiers(normalizePartitionDefinition(to.definition));
|
|
274
|
+
if (normalizeQuotedIdentifiers(normalizePartitionDefinition(from.definition)) !== toDefinition) {
|
|
275
275
|
return true;
|
|
276
276
|
}
|
|
277
|
-
|
|
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)
|
|
283
|
-
|
|
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);
|