@mikro-orm/sql 7.2.0-dev.6 → 7.2.0-dev.7
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/SqlEntityManager.js +1 -2
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +4 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +27 -2
- package/package.json +3 -3
- package/query/QueryBuilder.d.ts +3 -4
- package/schema/DatabaseTable.js +6 -2
- package/schema/SchemaComparator.d.ts +1 -0
- package/schema/SchemaComparator.js +17 -4
- package/schema/SchemaHelper.d.ts +7 -0
- package/schema/SchemaHelper.js +15 -2
- package/schema/SqlSchemaGenerator.js +17 -16
package/SqlEntityManager.js
CHANGED
|
@@ -75,8 +75,7 @@ export class SqlEntityManager extends EntityManager {
|
|
|
75
75
|
*/
|
|
76
76
|
async countBy(entityName, groupBy, options = {}) {
|
|
77
77
|
const em = this.getContext(false);
|
|
78
|
-
options =
|
|
79
|
-
em.prepareOptions(options);
|
|
78
|
+
options = em.prepareOptions(options);
|
|
80
79
|
const meta = em.getMetadata().find(entityName);
|
|
81
80
|
const fields = Utils.asArray(groupBy);
|
|
82
81
|
const { where: rawWhere, ...countOptions } = options;
|
|
@@ -69,6 +69,8 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
69
69
|
createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
70
70
|
/** Generates SQL to drop a PostgreSQL trigger and its associated function. */
|
|
71
71
|
dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
|
|
72
|
+
/** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
|
|
73
|
+
private flattenDollarQuotedBodies;
|
|
72
74
|
createRoutine(routine: SqlRoutineDef): string;
|
|
73
75
|
dropRoutine(routine: SqlRoutineDef): string;
|
|
74
76
|
getAllRoutines(connection: AbstractSqlConnection, schemas?: string[]): Promise<SqlRoutineDef[]>;
|
|
@@ -131,6 +133,8 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
131
133
|
* Build the column list for a PostgreSQL index.
|
|
132
134
|
*/
|
|
133
135
|
protected getIndexColumns(index: IndexDef): string;
|
|
136
|
+
/** Non-default index access methods (gin, gist, brin, hash, ...), normalized to lower case. */
|
|
137
|
+
getIndexAccessMethod(index: IndexDef): string;
|
|
134
138
|
/**
|
|
135
139
|
* PostgreSQL-specific index options like fill factor.
|
|
136
140
|
*/
|
|
@@ -3,6 +3,8 @@ import { SchemaHelper, stripStatementNewlines } from '../../schema/SchemaHelper.
|
|
|
3
3
|
import { normalizePartitionBound, normalizePartitionDefinition } from '../../schema/partitioning.js';
|
|
4
4
|
/** PostGIS system views that should be automatically ignored */
|
|
5
5
|
const POSTGIS_VIEWS = ['geography_columns', 'geometry_columns'];
|
|
6
|
+
/** Dollar-quote delimiter, e.g. `$$` or `$body$`, captured so `split` keeps it. */
|
|
7
|
+
const DOLLAR_QUOTE_TAG = /(\$(?:[A-Za-z_]\w*)?\$)/;
|
|
6
8
|
export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
7
9
|
static DEFAULT_VALUES = {
|
|
8
10
|
'now()': ['now()', 'current_timestamp'],
|
|
@@ -505,7 +507,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
505
507
|
/** Generates SQL to create a PostgreSQL trigger and its associated function. */
|
|
506
508
|
createTrigger(table, trigger) {
|
|
507
509
|
if (trigger.expression) {
|
|
508
|
-
return trigger.expression;
|
|
510
|
+
return this.flattenDollarQuotedBodies(trigger.expression);
|
|
509
511
|
}
|
|
510
512
|
const timing = trigger.timing.toUpperCase();
|
|
511
513
|
const events = trigger.events.map(e => e.toUpperCase()).join(' OR ');
|
|
@@ -523,9 +525,24 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
523
525
|
const fnName = this.getSchemaQualifiedTriggerFnName(table, trigger);
|
|
524
526
|
return `drop trigger if exists ${triggerName} on ${table.getQuotedName()};\ndrop function if exists ${fnName}()`;
|
|
525
527
|
}
|
|
528
|
+
/** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
|
|
529
|
+
flattenDollarQuotedBodies(ddl) {
|
|
530
|
+
let openTag = '';
|
|
531
|
+
return ddl
|
|
532
|
+
.split(DOLLAR_QUOTE_TAG)
|
|
533
|
+
.map((part, i) => {
|
|
534
|
+
// the capture group puts the delimiters on the odd indexes
|
|
535
|
+
if (i % 2 === 1) {
|
|
536
|
+
openTag = openTag === part ? '' : openTag || part;
|
|
537
|
+
return part;
|
|
538
|
+
}
|
|
539
|
+
return openTag ? stripStatementNewlines(part) : part;
|
|
540
|
+
})
|
|
541
|
+
.join('');
|
|
542
|
+
}
|
|
526
543
|
createRoutine(routine) {
|
|
527
544
|
if (routine.expression) {
|
|
528
|
-
return routine.expression;
|
|
545
|
+
return this.flattenDollarQuotedBodies(routine.expression);
|
|
529
546
|
}
|
|
530
547
|
const qualifiedName = this.qualifiedRoutineName(routine);
|
|
531
548
|
const params = this.formatRoutineParams(routine);
|
|
@@ -1118,6 +1135,14 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
1118
1135
|
})
|
|
1119
1136
|
.join(', ');
|
|
1120
1137
|
}
|
|
1138
|
+
/** Non-default index access methods (gin, gist, brin, hash, ...), normalized to lower case. */
|
|
1139
|
+
getIndexAccessMethod(index) {
|
|
1140
|
+
// `fulltext` is a cross-dialect alias handled via `getFullTextIndexExpression`, not a pg access method
|
|
1141
|
+
if (typeof index.type !== 'string' || ['', 'btree', 'fulltext'].includes(index.type.toLowerCase())) {
|
|
1142
|
+
return '';
|
|
1143
|
+
}
|
|
1144
|
+
return index.type.toLowerCase();
|
|
1145
|
+
}
|
|
1121
1146
|
/**
|
|
1122
1147
|
* PostgreSQL-specific index options like fill factor.
|
|
1123
1148
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.2.0-dev.
|
|
3
|
+
"version": "7.2.0-dev.7",
|
|
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",
|
|
@@ -47,13 +47,13 @@
|
|
|
47
47
|
"copy": "node ../../scripts/copy.mjs"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"kysely": "0.29.
|
|
50
|
+
"kysely": "0.29.5"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@mikro-orm/core": "^7.1.11"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.2.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.2.0-dev.7"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -152,11 +152,10 @@ type ContextFilterKeys<Context> = {
|
|
|
152
152
|
type RawFilterKeys<RawAliases extends string> = {
|
|
153
153
|
[K in RawAliases]?: AliasedFilterValue;
|
|
154
154
|
};
|
|
155
|
-
type NestedFilterCondition<Entity, RootAlias extends string, Context, RawAliases extends string> = ObjectQuery<Entity> & (IsNever<RootAlias> extends true ? {} : string extends RootAlias ? {} : RootAliasFilterKeys<RootAlias, Entity>) & ([Context] extends [never] ? {} : ContextFilterKeys<Context>) & (IsNever<RawAliases> extends true ? {} : string extends RawAliases ? {} : RawFilterKeys<RawAliases>);
|
|
156
155
|
type GroupOperators<RootAlias extends string, Context, Entity, RawAliases extends string> = {
|
|
157
|
-
$and?:
|
|
158
|
-
$or?:
|
|
159
|
-
$not?:
|
|
156
|
+
$and?: QBFilterQuery<Entity, RootAlias, Context, RawAliases>[];
|
|
157
|
+
$or?: QBFilterQuery<Entity, RootAlias, Context, RawAliases>[];
|
|
158
|
+
$not?: QBFilterQuery<Entity, RootAlias, Context, RawAliases>;
|
|
160
159
|
};
|
|
161
160
|
export type AliasedFilterCondition<RootAlias extends string, Context, Entity, RawAliases extends string = never> = (IsNever<RootAlias> extends true ? {} : string extends RootAlias ? {} : RootAliasFilterKeys<RootAlias, Entity>) & ([Context] extends [never] ? {} : ContextFilterKeys<Context>) & (IsNever<RawAliases> extends true ? {} : string extends RawAliases ? {} : RawFilterKeys<RawAliases>) & GroupOperators<RootAlias, Context, Entity, RawAliases>;
|
|
162
161
|
export type QBFilterQuery<Entity, RootAlias extends string = never, Context = never, RawAliases extends string = never> = FilterObject<Entity> & AliasedFilterCondition<RootAlias, Context, Entity, RawAliases>;
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -1030,8 +1030,12 @@ export class DatabaseTable {
|
|
|
1030
1030
|
// mysql stores decimal defaults padded to scale (`0` → `0.00`); collapse to canonical numeric form
|
|
1031
1031
|
// so the metadata-side (`0`) and introspection-side (`0.00`) snapshots agree
|
|
1032
1032
|
let defaultValue = c.default ?? null;
|
|
1033
|
-
if (defaultValue != null && c.mappedType instanceof DecimalType
|
|
1034
|
-
|
|
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
|
+
}
|
|
1035
1039
|
}
|
|
1036
1040
|
const normalized = {
|
|
1037
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 ?? '')) {
|
|
@@ -989,6 +993,10 @@ export class SchemaComparator {
|
|
|
989
993
|
const val = defaultValue.replace(/^(_\w+\\)?'(.*?)\\?'$/, '$2').replace(/^\(?'(.*?)'\)?$/, '$1');
|
|
990
994
|
return parseJsonSafe(val);
|
|
991
995
|
}
|
|
996
|
+
parseDecimalDefault(defaultValue) {
|
|
997
|
+
const value = +('' + defaultValue).replace(/^'(.+)'$/, '$1');
|
|
998
|
+
return Number.isFinite(value) ? value : null;
|
|
999
|
+
}
|
|
992
1000
|
hasSameDefaultValue(from, to) {
|
|
993
1001
|
if (from.default == null ||
|
|
994
1002
|
from.default.toString().toLowerCase() === 'null' ||
|
|
@@ -1014,10 +1022,15 @@ export class SchemaComparator {
|
|
|
1014
1022
|
const defaultValueTo = to.default.toLowerCase().replace('current_timestamp', 'now').replace(/\(\)$/, '');
|
|
1015
1023
|
return defaultValueFrom === defaultValueTo;
|
|
1016
1024
|
}
|
|
1017
|
-
// mysql
|
|
1018
|
-
//
|
|
1019
|
-
if (to.mappedType instanceof DecimalType
|
|
1020
|
-
|
|
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
|
+
}
|
|
1021
1034
|
}
|
|
1022
1035
|
if (from.default && to.default) {
|
|
1023
1036
|
return from.default.toString().toLowerCase() === to.default.toString().toLowerCase();
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -70,6 +70,13 @@ export declare abstract class SchemaHelper {
|
|
|
70
70
|
* Hook for adding driver-specific index options (e.g., fill factor for PostgreSQL).
|
|
71
71
|
*/
|
|
72
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;
|
|
73
80
|
/**
|
|
74
81
|
* Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
|
|
75
82
|
* return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -167,13 +167,14 @@ export class SchemaHelper {
|
|
|
167
167
|
tableName = this.quote(tableName);
|
|
168
168
|
const keyName = this.quote(index.keyName);
|
|
169
169
|
const defer = index.deferMode ? ` deferrable initially ${index.deferMode}` : '';
|
|
170
|
-
|
|
170
|
+
const using = this.getIndexAccessMethodClause(index);
|
|
171
|
+
let sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}${using}`;
|
|
171
172
|
if (index.unique && index.constraint) {
|
|
172
173
|
sql = `alter table ${tableName} add constraint ${keyName} unique`;
|
|
173
174
|
}
|
|
174
175
|
if (index.columnNames.some(column => column.includes('.'))) {
|
|
175
176
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
176
|
-
sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}`;
|
|
177
|
+
sql = `create ${index.unique ? 'unique ' : ''}index ${keyName} on ${tableName}${using}`;
|
|
177
178
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
178
179
|
return `${sql} (${columns.join(', ')})${this.getCreateIndexSuffix(index)}${this.getIndexWhereClause(index)}${defer}`;
|
|
179
180
|
}
|
|
@@ -192,6 +193,18 @@ export class SchemaHelper {
|
|
|
192
193
|
getCreateIndexSuffix(_index) {
|
|
193
194
|
return '';
|
|
194
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
|
+
}
|
|
195
208
|
/**
|
|
196
209
|
* Default emits ` where <predicate>` for partial indexes. Only Oracle overrides this to
|
|
197
210
|
* return `''` (it emulates partials via CASE-WHEN columns). MySQL sidesteps the whole path
|
|
@@ -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));
|