@mikro-orm/sql 7.3.0-dev.1 → 7.3.0-dev.11
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/AbstractSqlDriver.js +16 -8
- package/SqlEntityManager.js +2 -1
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +2 -0
- package/dialects/mysql/MySqlSchemaHelper.js +13 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +2 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +72 -10
- package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
- package/dialects/sqlite/SqlitePlatform.js +3 -0
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +1 -0
- package/dialects/sqlite/SqliteSchemaHelper.js +20 -4
- package/package.json +3 -3
- package/plugin/transformer.d.ts +2 -1
- package/plugin/transformer.js +39 -13
- package/query/CriteriaNode.d.ts +2 -0
- package/query/CriteriaNode.js +8 -2
- package/query/CriteriaNodeFactory.js +4 -3
- package/query/ObjectCriteriaNode.d.ts +2 -0
- package/query/ObjectCriteriaNode.js +60 -10
- package/query/QueryBuilder.js +4 -2
- package/schema/DatabaseTable.js +2 -0
- package/schema/SchemaComparator.js +3 -1
- package/schema/SchemaHelper.d.ts +7 -1
- package/schema/SchemaHelper.js +42 -4
- package/schema/SqlSchemaGenerator.js +8 -0
- package/typings.d.ts +5 -1
package/AbstractSqlDriver.js
CHANGED
|
@@ -850,9 +850,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
850
850
|
props.forEach(prop => {
|
|
851
851
|
// For STI with conflicting fieldNames, use discriminator to determine which field gets value
|
|
852
852
|
if (prop.stiFieldNames && prop.stiFieldNameMap && meta.discriminatorColumn) {
|
|
853
|
-
|
|
853
|
+
// the map misses subtypes whose props were not initialized yet during discovery (root column owner, grandchildren)
|
|
854
|
+
const discriminator = row[meta.discriminatorColumn];
|
|
855
|
+
const childClass = meta.discriminatorMap?.[discriminator];
|
|
856
|
+
const activeField = prop.stiFieldNameMap[discriminator] ??
|
|
857
|
+
(childClass && this.metadata.find(childClass)?.properties[prop.name]?.fieldNames[0]);
|
|
854
858
|
for (const field of prop.stiFieldNames) {
|
|
855
|
-
params.push(field === activeField ? row[prop.name] : null);
|
|
859
|
+
params.push(field === activeField ? (row[prop.name] ?? null) : null);
|
|
856
860
|
keys.push('?');
|
|
857
861
|
}
|
|
858
862
|
return;
|
|
@@ -1809,7 +1813,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1809
1813
|
if (!this.platform.shouldHaveColumn(prop, populate, exclude)) {
|
|
1810
1814
|
return false;
|
|
1811
1815
|
}
|
|
1812
|
-
if (!fields ||
|
|
1816
|
+
if (!fields ||
|
|
1817
|
+
fields.includes('*') ||
|
|
1818
|
+
prop.primary ||
|
|
1819
|
+
meta.root.discriminatorColumn === prop.name ||
|
|
1820
|
+
meta.root.targetKeys?.includes(prop.name)) {
|
|
1813
1821
|
return true;
|
|
1814
1822
|
}
|
|
1815
1823
|
return fields.some(f => f === prop.name || f.toString().startsWith(prop.name + '.'));
|
|
@@ -1865,11 +1873,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
1865
1873
|
const tptMeta = this.metadata.get(targetMeta.class);
|
|
1866
1874
|
this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
|
|
1867
1875
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
}
|
|
1872
|
-
else {
|
|
1876
|
+
// target filters apply per target, both for filter :ref hints and populated relations
|
|
1877
|
+
qb.scheduleFilterCheck(targetPath);
|
|
1878
|
+
if (!ref) {
|
|
1873
1879
|
// Select fields from each target table
|
|
1874
1880
|
fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
|
|
1875
1881
|
...options,
|
|
@@ -2469,6 +2475,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
2469
2475
|
}
|
|
2470
2476
|
if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
|
|
2471
2477
|
ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
|
|
2478
|
+
// `targetKey` values identify the entity like its PK, references resolve it by them
|
|
2479
|
+
ret.push(...(meta.root.targetKeys ?? []).filter(key => meta.properties[key] && !options.fields.includes(key)));
|
|
2472
2480
|
}
|
|
2473
2481
|
if (meta.root.inheritanceType === 'sti' &&
|
|
2474
2482
|
!options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
|
package/SqlEntityManager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EntityManager, raw, Utils, } from '@mikro-orm/core';
|
|
1
|
+
import { EntityManager, QueryFlag, raw, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { MikroKyselyPlugin } from './plugin/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* @inheritDoc
|
|
@@ -87,6 +87,7 @@ export class SqlEntityManager extends EntityManager {
|
|
|
87
87
|
// match `em.count()` semantics: an active transaction always wins over the requested connection type
|
|
88
88
|
const connectionType = em.getTransactionContext() ? 'write' : options.connectionType;
|
|
89
89
|
const qb = em.createQueryBuilder(meta.class, undefined, connectionType);
|
|
90
|
+
qb.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
|
|
90
91
|
qb
|
|
91
92
|
.select([...fields, raw('count(*) as cnt')])
|
|
92
93
|
.where(where)
|
|
@@ -37,6 +37,7 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
|
|
|
37
37
|
getDefaultMappedType(type: string): Type<unknown>;
|
|
38
38
|
isNumericColumn(mappedType: Type<unknown>): boolean;
|
|
39
39
|
supportsUnsigned(): boolean;
|
|
40
|
+
supportsAutoincrementIncrement(): boolean;
|
|
40
41
|
/** MySQL/MariaDB identifier limit. */
|
|
41
42
|
getMaxIdentifierLength(): number;
|
|
42
43
|
getIndexName(tableName: string, columns: string[], type: 'index' | 'unique' | 'foreign' | 'primary' | 'sequence' | 'check'): string;
|
|
@@ -20,6 +20,8 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
|
|
|
20
20
|
tableExists(connection: AbstractSqlConnection, tableName: string, schemaName: string | undefined, ctx?: Transaction): Promise<boolean>;
|
|
21
21
|
disableForeignKeysSQL(): string;
|
|
22
22
|
enableForeignKeysSQL(): string;
|
|
23
|
+
getResetSequenceSQL(tableName: string, schemaName: string | undefined, columnName: string, startWith: number): string;
|
|
24
|
+
getAddColumnsSQL(table: DatabaseTable, columns: Column[]): string[];
|
|
23
25
|
finalizeTable(table: DatabaseTable, charset: string, collate?: string): string;
|
|
24
26
|
getListTablesSQL(): string;
|
|
25
27
|
getListViewsSQL(): string;
|
|
@@ -42,12 +42,25 @@ export class MySqlSchemaHelper extends SchemaHelper {
|
|
|
42
42
|
enableForeignKeysSQL() {
|
|
43
43
|
return 'set foreign_key_checks = 1;';
|
|
44
44
|
}
|
|
45
|
+
getResetSequenceSQL(tableName, schemaName, columnName, startWith) {
|
|
46
|
+
return `alter table ${this.quote(this.getTableName(tableName, schemaName))} auto_increment = ${startWith}`;
|
|
47
|
+
}
|
|
48
|
+
getAddColumnsSQL(table, columns) {
|
|
49
|
+
const [sql, ...rest] = super.getAddColumnsSQL(table, columns);
|
|
50
|
+
const startWith = columns.find(column => column.autoincrement)?.sequence?.startWith;
|
|
51
|
+
// the table option numbers the existing rows of the added column from the declared start too
|
|
52
|
+
return [startWith != null ? `${sql}, auto_increment = ${startWith}` : sql, ...rest];
|
|
53
|
+
}
|
|
45
54
|
finalizeTable(table, charset, collate) {
|
|
46
55
|
let sql = ` default character set ${charset}`;
|
|
47
56
|
if (collate) {
|
|
48
57
|
sql += ` collate ${collate}`;
|
|
49
58
|
}
|
|
50
59
|
sql += ' engine = InnoDB';
|
|
60
|
+
const startWith = table.getColumns().find(column => column.autoincrement)?.sequence?.startWith;
|
|
61
|
+
if (startWith != null) {
|
|
62
|
+
sql += ` auto_increment = ${startWith}`;
|
|
63
|
+
}
|
|
51
64
|
if (table.comment) {
|
|
52
65
|
sql += ` comment = ${this.platform.quoteValue(table.comment)}`;
|
|
53
66
|
}
|
|
@@ -29,6 +29,7 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
29
29
|
loadMaterializedViews(schema: DatabaseSchema, connection: AbstractSqlConnection, schemaName?: string, ctx?: Transaction): Promise<void>;
|
|
30
30
|
createMaterializedView(name: string, schema: string | undefined, definition: string, withData?: boolean): string;
|
|
31
31
|
createTable(table: DatabaseTable, alter?: boolean): string[];
|
|
32
|
+
getResetSequenceSQL(tableName: string, schemaName: string | undefined, columnName: string, startWith: number): string;
|
|
32
33
|
dropMaterializedViewIfExists(name: string, schema?: string): string;
|
|
33
34
|
refreshMaterializedView(name: string, schema?: string, concurrently?: boolean): string;
|
|
34
35
|
getNamespaces(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string[]>;
|
|
@@ -134,6 +135,7 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
134
135
|
castColumn(name: string, type: string): string;
|
|
135
136
|
dropForeignKey(tableName: string, constraintName: string): string;
|
|
136
137
|
getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[];
|
|
138
|
+
getAddColumnsSQL(table: DatabaseTable, columns: Column[]): string[];
|
|
137
139
|
private getAlterColumnAutoincrement;
|
|
138
140
|
getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
|
|
139
141
|
alterTableComment(table: DatabaseTable, comment?: string): string;
|
|
@@ -106,10 +106,23 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
106
106
|
}
|
|
107
107
|
createTable(table, alter) {
|
|
108
108
|
const partitioning = table.getPartitioning();
|
|
109
|
+
const [createTable, ...rest] = super.createTable(table, alter);
|
|
110
|
+
// `serial` columns can't declare sequence options inline, so alter the implicit sequence right after creating the table
|
|
111
|
+
const sequences = table
|
|
112
|
+
.getColumns()
|
|
113
|
+
.filter(column => column.autoincrement && !column.generated && this.getSequenceOptionsSQL(column))
|
|
114
|
+
.map(column => {
|
|
115
|
+
const sequence = `pg_get_serial_sequence(${this.platform.quoteValue(table.getQuotedName())}, ${this.platform.quoteValue(column.name)})`;
|
|
116
|
+
const { startWith = -1, incrementBy = 1 } = column.sequence;
|
|
117
|
+
// `alter sequence` keeps the ascending bounds of the serial, so a descending one gets the bounds and start of a fresh sequence
|
|
118
|
+
const options = incrementBy < 0
|
|
119
|
+
? `no minvalue maxvalue ${Math.max(startWith, -1)} start with ${startWith} increment by ${incrementBy}`
|
|
120
|
+
: this.getSequenceOptionsSQL(column);
|
|
121
|
+
return `do $$ begin execute format('alter sequence %s ${options} restart', ${sequence}); end $$`;
|
|
122
|
+
});
|
|
109
123
|
if (!partitioning) {
|
|
110
|
-
return
|
|
124
|
+
return [createTable, ...sequences, ...rest];
|
|
111
125
|
}
|
|
112
|
-
const [createTable, ...rest] = super.createTable(table, alter);
|
|
113
126
|
const partitions = partitioning.partitions.map(partition => {
|
|
114
127
|
const partitionName = this.quote(this.getTableName(partition.name, partition.schema ?? table.schema));
|
|
115
128
|
return `create table ${partitionName} partition of ${table.getQuotedName()} ${partition.bound}`;
|
|
@@ -119,7 +132,12 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
119
132
|
// regex replacement tokens like `$$`, `$&`, or `$1` inside user-supplied expressions (e.g., a
|
|
120
133
|
// callback that returns a dollar-quoted literal) are not interpreted as back-references.
|
|
121
134
|
const spliced = `${createTable.slice(0, -1)} partition by ${partitioning.definition};`;
|
|
122
|
-
return [spliced, ...rest, ...partitions];
|
|
135
|
+
return [spliced, ...sequences, ...rest, ...partitions];
|
|
136
|
+
}
|
|
137
|
+
getResetSequenceSQL(tableName, schemaName, columnName, startWith) {
|
|
138
|
+
const table = this.platform.quoteValue(this.quote(this.getTableName(tableName, schemaName)));
|
|
139
|
+
// `truncate … restart identity` goes back to the start the sequence was created with, which the metadata may not match
|
|
140
|
+
return `select setval(pg_get_serial_sequence(${table}, ${this.platform.quoteValue(columnName)}), ${startWith}, false)`;
|
|
123
141
|
}
|
|
124
142
|
dropMaterializedViewIfExists(name, schema) {
|
|
125
143
|
return `drop materialized view if exists ${this.quote(this.getTableName(name, schema))} cascade`;
|
|
@@ -389,11 +407,14 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
389
407
|
identity_generation,
|
|
390
408
|
generation_expression,
|
|
391
409
|
pg_catalog.col_description(pgc.oid, cols.ordinal_position::int) column_comment,
|
|
392
|
-
coll.collname as collation_name
|
|
410
|
+
coll.collname as collation_name,
|
|
411
|
+
seq.seqstart as sequence_start,
|
|
412
|
+
seq.seqincrement as sequence_increment
|
|
393
413
|
from information_schema.columns cols
|
|
394
414
|
join pg_class pgc on cols.table_name = pgc.relname
|
|
395
415
|
join pg_attribute pga on pgc.oid = pga.attrelid and cols.column_name = pga.attname
|
|
396
416
|
left join pg_collation coll on pga.attcollation = coll.oid and coll.collname <> 'default'
|
|
417
|
+
left join pg_sequence seq on seq.seqrelid = pg_get_serial_sequence(quote_ident(cols.table_schema) || '.' || quote_ident(cols.table_name), cols.column_name)::regclass
|
|
397
418
|
where (${[...tablesBySchemas.entries()].map(([schema, tables]) => `(table_schema = ${this.platform.quoteValue(schema)} and table_name in (${tables.map(t => this.platform.quoteValue(t.table_name)).join(',')}))`).join(' or ')})
|
|
398
419
|
order by ordinal_position`;
|
|
399
420
|
const allColumns = await connection.execute(sql, [], 'all', ctx);
|
|
@@ -444,6 +465,7 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
444
465
|
default: str(this.normalizeDefaultValue(col.column_default, col.length)),
|
|
445
466
|
unsigned: increments,
|
|
446
467
|
autoincrement: increments,
|
|
468
|
+
sequence: increments ? this.getIntrospectedSequence(col.sequence_start, col.sequence_increment) : undefined,
|
|
447
469
|
generated: col.is_identity === 'YES'
|
|
448
470
|
? col.identity_generation === 'BY DEFAULT'
|
|
449
471
|
? 'by default as identity'
|
|
@@ -1032,11 +1054,15 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
1032
1054
|
columnType = this.quote(columnType);
|
|
1033
1055
|
}
|
|
1034
1056
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1057
|
+
const sequenceOptions = this.getSequenceOptionsSQL(column);
|
|
1058
|
+
const generated = sequenceOptions && /^(by default as )?identity$/.test(column.generated ?? '')
|
|
1059
|
+
? `${column.generated} (${sequenceOptions})`
|
|
1060
|
+
: column.generated;
|
|
1061
|
+
if (generated?.startsWith('by default as identity')) {
|
|
1062
|
+
columnType += ` generated ${generated}`;
|
|
1037
1063
|
}
|
|
1038
|
-
else if (
|
|
1039
|
-
columnType += ` generated always as ${
|
|
1064
|
+
else if (generated) {
|
|
1065
|
+
columnType += ` generated always as ${generated}`;
|
|
1040
1066
|
}
|
|
1041
1067
|
col.push(columnType);
|
|
1042
1068
|
Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
|
|
@@ -1193,14 +1219,50 @@ export class PostgreSqlSchemaHelper extends SchemaHelper {
|
|
|
1193
1219
|
}
|
|
1194
1220
|
return ret;
|
|
1195
1221
|
}
|
|
1222
|
+
getAddColumnsSQL(table, columns) {
|
|
1223
|
+
const compositePK = table.getPrimaryKey()?.composite;
|
|
1224
|
+
const before = [];
|
|
1225
|
+
const after = [];
|
|
1226
|
+
const adds = columns.map(column => {
|
|
1227
|
+
const sequenceOptions = this.getSequenceOptionsSQL(column);
|
|
1228
|
+
if (!sequenceOptions || !column.autoincrement || column.generated) {
|
|
1229
|
+
return column;
|
|
1230
|
+
}
|
|
1231
|
+
// a `serial` column numbers the existing rows right away, so expand it to a sequence created with the options upfront
|
|
1232
|
+
const seqName = this.quote(this.getTableName(this.platform.getIndexName(table.name, [column.name], 'sequence'), table.schema));
|
|
1233
|
+
before.push(`create sequence ${seqName} ${sequenceOptions}`);
|
|
1234
|
+
after.push(`alter sequence ${seqName} owned by ${table.getQuotedName()}.${this.quote(column.name)}`);
|
|
1235
|
+
// the expanded column has no inline `primary key`, which `alterTable` expects from an added autoincrement PK
|
|
1236
|
+
if (column.primary && !compositePK && !this.hasNonDefaultPrimaryKeyName(table)) {
|
|
1237
|
+
after.push(`alter table ${table.getQuotedName()} add primary key (${this.quote(column.name)})`);
|
|
1238
|
+
}
|
|
1239
|
+
return {
|
|
1240
|
+
...column,
|
|
1241
|
+
type: column.mappedType.getColumnType({ autoincrement: false }, this.platform),
|
|
1242
|
+
autoincrement: false,
|
|
1243
|
+
nullable: false,
|
|
1244
|
+
default: `nextval('${seqName}')`,
|
|
1245
|
+
};
|
|
1246
|
+
});
|
|
1247
|
+
return [...before, ...super.getAddColumnsSQL(table, adds), ...after];
|
|
1248
|
+
}
|
|
1196
1249
|
getAlterColumnAutoincrement(tableName, column, schemaName) {
|
|
1197
1250
|
const ret = [];
|
|
1198
1251
|
/* v8 ignore next */
|
|
1199
1252
|
const name = (schemaName && schemaName !== this.platform.getDefaultSchemaName() ? schemaName + '.' : '') + tableName;
|
|
1200
1253
|
if (column.autoincrement) {
|
|
1201
1254
|
const seqName = this.platform.getIndexName(tableName, [column.name], 'sequence');
|
|
1202
|
-
|
|
1203
|
-
|
|
1255
|
+
const sequenceOptions = this.getSequenceOptionsSQL(column);
|
|
1256
|
+
const { incrementBy = 1, startWith = incrementBy < 0 ? -1 : 1 } = column.sequence ?? {};
|
|
1257
|
+
const lastValue = `(select ${incrementBy < 0 ? 'min' : 'max'}(${this.quote(column.name)}) from ${this.quote(name)})`;
|
|
1258
|
+
ret.push(`create sequence if not exists ${this.quote(seqName)}${sequenceOptions ? ` ${sequenceOptions}` : ''}`);
|
|
1259
|
+
if (sequenceOptions) {
|
|
1260
|
+
// continue after the existing values, but never before the declared start
|
|
1261
|
+
ret.push(`select setval('${seqName}', ${incrementBy < 0 ? 'least' : 'greatest'}(${lastValue} + ${incrementBy}, ${startWith}), false)`);
|
|
1262
|
+
}
|
|
1263
|
+
else {
|
|
1264
|
+
ret.push(`select setval('${seqName}', ${lastValue})`);
|
|
1265
|
+
}
|
|
1204
1266
|
ret.push(`alter table ${this.quote(name)} alter column ${this.quote(column.name)} set default nextval('${seqName}')`);
|
|
1205
1267
|
}
|
|
1206
1268
|
return ret;
|
|
@@ -14,6 +14,7 @@ export declare class SqlitePlatform extends AbstractSqlPlatform {
|
|
|
14
14
|
usesReturningStatement(): boolean;
|
|
15
15
|
usesEnumCheckConstraints(): boolean;
|
|
16
16
|
supportsComments(): boolean;
|
|
17
|
+
supportsAutoincrementIncrement(): boolean;
|
|
17
18
|
getCurrentTimestampSQL(length: number): string;
|
|
18
19
|
getDateTimeTypeDeclarationSQL(column: {
|
|
19
20
|
length: number;
|
|
@@ -21,6 +21,7 @@ export declare class SqliteSchemaHelper extends SchemaHelper {
|
|
|
21
21
|
getDropDatabaseSQL(name: string): string;
|
|
22
22
|
loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[], ctx?: Transaction): Promise<void>;
|
|
23
23
|
createTable(table: DatabaseTable, alter?: boolean): string[];
|
|
24
|
+
getResetSequenceSQL(tableName: string, schemaName: string | undefined, columnName: string, startWith: number): string;
|
|
24
25
|
createTableColumn(column: Column, table: DatabaseTable, _changedProperties?: Set<string>): string | undefined;
|
|
25
26
|
getAddColumnsSQL(table: DatabaseTable, columns: Column[], diff?: TableDifference): string[];
|
|
26
27
|
dropForeignKey(tableName: string, constraintName: string): string;
|
|
@@ -144,6 +144,13 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
144
144
|
}
|
|
145
145
|
const ret = [];
|
|
146
146
|
this.append(ret, sql);
|
|
147
|
+
const startWith = columns.find(column => column.autoincrement)?.sequence?.startWith;
|
|
148
|
+
if (startWith != null) {
|
|
149
|
+
// a table rebuild renames the counter row of the temp table over, so only seed it when missing
|
|
150
|
+
const sequenceTable = this.quote(this.getTableName('sqlite_sequence', table.schema));
|
|
151
|
+
const name = this.platform.quoteValue(table.name);
|
|
152
|
+
this.append(ret, `insert into ${sequenceTable} (name, seq) select ${name}, ${startWith - 1} where not exists (select 1 from ${sequenceTable} where name = ${name})`);
|
|
153
|
+
}
|
|
147
154
|
for (const index of table.getIndexes()) {
|
|
148
155
|
this.append(ret, this.createIndex(index, table));
|
|
149
156
|
}
|
|
@@ -152,6 +159,12 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
152
159
|
}
|
|
153
160
|
return ret;
|
|
154
161
|
}
|
|
162
|
+
getResetSequenceSQL(tableName, schemaName, columnName, startWith) {
|
|
163
|
+
const sequenceTable = this.quote(this.getTableName('sqlite_sequence', schemaName));
|
|
164
|
+
const name = this.platform.quoteValue(tableName);
|
|
165
|
+
// the counter row does not exist before the first insert when the table was created without `sequence`
|
|
166
|
+
return `delete from ${sequenceTable} where name = ${name};\ninsert into ${sequenceTable} (name, seq) values (${name}, ${startWith - 1})`;
|
|
167
|
+
}
|
|
155
168
|
createTableColumn(column, table, _changedProperties) {
|
|
156
169
|
const col = [this.quote(column.name)];
|
|
157
170
|
const checks = table.getChecks();
|
|
@@ -499,7 +512,9 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
499
512
|
if (Utils.hasObjectKeys(diff.removedChecks) ||
|
|
500
513
|
Utils.hasObjectKeys(diff.changedChecks) ||
|
|
501
514
|
Utils.hasObjectKeys(diff.changedForeignKeys) ||
|
|
502
|
-
Utils.hasObjectKeys(diff.changedColumns)
|
|
515
|
+
Utils.hasObjectKeys(diff.changedColumns) ||
|
|
516
|
+
// sqlite can only add or change a primary key by rebuilding the table
|
|
517
|
+
[...Object.values(diff.addedIndexes), ...Object.values(diff.changedIndexes)].some(index => index.primary)) {
|
|
503
518
|
return this.getAlterTempTableSQL(diff);
|
|
504
519
|
}
|
|
505
520
|
for (const index of Object.values(diff.removedIndexes)) {
|
|
@@ -644,10 +659,11 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
644
659
|
first.replace(`create table ${quotedName}`, `create table ${quotedTempName}`),
|
|
645
660
|
];
|
|
646
661
|
const columns = [];
|
|
662
|
+
const renamedFrom = Object.fromEntries(Object.entries(changedTable.renamedColumns).map(([oldName, column]) => [column.name, oldName]));
|
|
647
663
|
for (const column of changedTable.toTable.getColumns()) {
|
|
648
|
-
const
|
|
649
|
-
if (
|
|
650
|
-
columns.push(this.quote(
|
|
664
|
+
const fromName = renamedFrom[column.name] ?? column.name;
|
|
665
|
+
if (changedTable.fromTable.getColumn(fromName)) {
|
|
666
|
+
columns.push(this.quote(fromName));
|
|
651
667
|
}
|
|
652
668
|
else {
|
|
653
669
|
columns.push(`null as ${this.quote(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.11",
|
|
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.2"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.3.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.3.0-dev.11"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/plugin/transformer.d.ts
CHANGED
|
@@ -92,6 +92,7 @@ export declare class MikroTransformer extends OperationNodeTransformer {
|
|
|
92
92
|
* This is called for SELECT queries when columnNamingStrategy is 'property'
|
|
93
93
|
*/
|
|
94
94
|
transformResult(rows: Record<string, any>[] | undefined, entityMap: Map<string, EntityMetadata>): Record<string, any>[] | undefined;
|
|
95
|
+
private getResultMaps;
|
|
95
96
|
buildGlobalFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, EntityProperty>;
|
|
96
97
|
buildGlobalRelationFieldMap(entityMap: Map<string, EntityMetadata>): Record<string, string>;
|
|
97
98
|
/**
|
|
@@ -108,6 +109,6 @@ export declare class MikroTransformer extends OperationNodeTransformer {
|
|
|
108
109
|
/**
|
|
109
110
|
* Transform a single row by mapping column names to property names
|
|
110
111
|
*/
|
|
111
|
-
transformRow(row: Record<string, any>,
|
|
112
|
+
transformRow(row: Record<string, any>, fieldEntries: [string, EntityProperty][], relationEntries: [string, string][]): Record<string, any>;
|
|
112
113
|
prepareOutputValue(prop: EntityProperty | undefined, value: unknown): unknown;
|
|
113
114
|
}
|
package/plugin/transformer.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { ReferenceKind, isRaw, } from '@mikro-orm/core';
|
|
2
2
|
import { AliasNode, ColumnNode, ColumnUpdateNode, IdentifierNode, OperationNodeTransformer, PrimitiveValueListNode, RawNode, ReferenceNode, SchemableIdentifierNode, SelectAllNode, SelectionNode, TableNode, ValueListNode, ValueNode, ValuesNode, } from 'kysely';
|
|
3
|
+
/** Past this many query shapes the cache of one metadata storage starts over, so generated aliases cannot grow it. */
|
|
4
|
+
const RESULT_MAPS_LIMIT = 1000;
|
|
5
|
+
/**
|
|
6
|
+
* Result maps per query shape (the aliases and entities of its entity map). They are derived from metadata alone, so
|
|
7
|
+
* they are built once and shared by every transformer, as `getKysely()` creates a new one per call.
|
|
8
|
+
*/
|
|
9
|
+
const resultMapsCache = new WeakMap();
|
|
3
10
|
const EXPANDABLE_KINDS = new Set([
|
|
4
11
|
ReferenceKind.SCALAR,
|
|
5
12
|
ReferenceKind.EMBEDDED,
|
|
@@ -884,12 +891,7 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
884
891
|
if (byEntity) {
|
|
885
892
|
return byEntity;
|
|
886
893
|
}
|
|
887
|
-
|
|
888
|
-
const byTable = allMetadata.find(m => m.tableName === name);
|
|
889
|
-
if (byTable) {
|
|
890
|
-
return byTable;
|
|
891
|
-
}
|
|
892
|
-
return undefined;
|
|
894
|
+
return this.#metadata.getByTableName(name);
|
|
893
895
|
}
|
|
894
896
|
/**
|
|
895
897
|
* Transform result rows by mapping database column names to property names
|
|
@@ -906,11 +908,35 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
906
908
|
if (entityMap.size === 0) {
|
|
907
909
|
return rows;
|
|
908
910
|
}
|
|
909
|
-
//
|
|
910
|
-
const
|
|
911
|
-
const relationFieldMap = this.buildGlobalRelationFieldMap(entityMap);
|
|
911
|
+
// The global mapping from database field names to property objects, built once per query shape
|
|
912
|
+
const { fields, relations } = this.getResultMaps(entityMap);
|
|
912
913
|
// Transform each row
|
|
913
|
-
return rows.map(row => this.transformRow(row,
|
|
914
|
+
return rows.map(row => this.transformRow(row, fields, relations));
|
|
915
|
+
}
|
|
916
|
+
getResultMaps(entityMap) {
|
|
917
|
+
let byShape = resultMapsCache.get(this.#metadata);
|
|
918
|
+
if (!byShape) {
|
|
919
|
+
byShape = new Map();
|
|
920
|
+
resultMapsCache.set(this.#metadata, byShape);
|
|
921
|
+
}
|
|
922
|
+
let shape = '';
|
|
923
|
+
for (const [alias, meta] of entityMap) {
|
|
924
|
+
shape += `${alias}\0${meta._id}\0`;
|
|
925
|
+
}
|
|
926
|
+
const cached = byShape.get(shape);
|
|
927
|
+
if (cached && Array.from(entityMap.values()).every((meta, i) => meta.props === cached.props[i])) {
|
|
928
|
+
return cached;
|
|
929
|
+
}
|
|
930
|
+
if (byShape.size >= RESULT_MAPS_LIMIT) {
|
|
931
|
+
byShape.clear();
|
|
932
|
+
}
|
|
933
|
+
const maps = {
|
|
934
|
+
props: Array.from(entityMap.values(), meta => meta.props),
|
|
935
|
+
fields: Object.entries(this.buildGlobalFieldMap(entityMap)),
|
|
936
|
+
relations: Object.entries(this.buildGlobalRelationFieldMap(entityMap)),
|
|
937
|
+
};
|
|
938
|
+
byShape.set(shape, maps);
|
|
939
|
+
return maps;
|
|
914
940
|
}
|
|
915
941
|
buildGlobalFieldMap(entityMap) {
|
|
916
942
|
const map = {};
|
|
@@ -986,10 +1012,10 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
986
1012
|
/**
|
|
987
1013
|
* Transform a single row by mapping column names to property names
|
|
988
1014
|
*/
|
|
989
|
-
transformRow(row,
|
|
1015
|
+
transformRow(row, fieldEntries, relationEntries) {
|
|
990
1016
|
const transformed = { ...row };
|
|
991
1017
|
// First pass: map regular fields from fieldName to propertyName and convert values
|
|
992
|
-
for (const [fieldName, prop] of
|
|
1018
|
+
for (const [fieldName, prop] of fieldEntries) {
|
|
993
1019
|
if (!(fieldName in transformed)) {
|
|
994
1020
|
continue;
|
|
995
1021
|
}
|
|
@@ -1011,7 +1037,7 @@ export class MikroTransformer extends OperationNodeTransformer {
|
|
|
1011
1037
|
// Second pass: handle relation fields
|
|
1012
1038
|
// Only run if columnNamingStrategy is 'property', as we don't want to rename FKs otherwise
|
|
1013
1039
|
if (this.#options.columnNamingStrategy === 'property') {
|
|
1014
|
-
for (const [fieldName, relationPropertyName] of
|
|
1040
|
+
for (const [fieldName, relationPropertyName] of relationEntries) {
|
|
1015
1041
|
if (fieldName in transformed && !(relationPropertyName in transformed)) {
|
|
1016
1042
|
// Move the foreign key value to the relation property name
|
|
1017
1043
|
transformed[relationPropertyName] = transformed[fieldName];
|
package/query/CriteriaNode.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare class CriteriaNode<T extends object> implements ICriteriaNode<T>
|
|
|
17
17
|
index?: number;
|
|
18
18
|
constructor(metadata: MetadataStorage, entityName: EntityName<T>, parent?: ICriteriaNode<T> | undefined, key?: EntityKey<T> | RawQueryFragmentSymbol | undefined, validate?: boolean, strict?: boolean);
|
|
19
19
|
process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any;
|
|
20
|
+
/** Whether this node holds the condition for a single target of a polymorphic relation (e.g. `imageable[Article]`). */
|
|
21
|
+
isPolymorphicBranch(): boolean;
|
|
20
22
|
unwrap(): any;
|
|
21
23
|
shouldInline(payload: any): boolean;
|
|
22
24
|
willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
|
package/query/CriteriaNode.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RawQueryFragment, ReferenceKind, Utils, inspect, } from '@mikro-orm/core';
|
|
1
|
+
import { QueryHelper, RawQueryFragment, ReferenceKind, Utils, inspect, } from '@mikro-orm/core';
|
|
2
2
|
/**
|
|
3
3
|
* Helper for working with deeply nested where/orderBy/having criteria. Uses composite pattern to build tree from the payload.
|
|
4
4
|
* Auto-joins relations and converts payload from { books: { publisher: { name: '...' } } } to { 'publisher_alias.name': '...' }
|
|
@@ -28,7 +28,9 @@ export class CriteriaNode {
|
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
30
|
for (const k of pks) {
|
|
31
|
-
|
|
31
|
+
const [name, target] = QueryHelper.splitPolymorphicKey(k);
|
|
32
|
+
this.prop = meta.props.find(prop => (prop.name === name && (!target || QueryHelper.findTargetMetas(prop, k).length > 0)) ||
|
|
33
|
+
(prop.fieldNames?.length === 1 && prop.fieldNames[0] === k && prop.persist !== false));
|
|
32
34
|
const isProp = this.prop || meta.props.find(prop => (prop.fieldNames || []).includes(k));
|
|
33
35
|
// do not validate if the key is prefixed or type casted (e.g. `k::text`)
|
|
34
36
|
if (validate && !isProp && !k.includes('.') && !k.includes('::') && !Utils.isOperator(k)) {
|
|
@@ -40,6 +42,10 @@ export class CriteriaNode {
|
|
|
40
42
|
process(qb, options) {
|
|
41
43
|
return this.payload;
|
|
42
44
|
}
|
|
45
|
+
/** Whether this node holds the condition for a single target of a polymorphic relation (e.g. `imageable[Article]`). */
|
|
46
|
+
isPolymorphicBranch() {
|
|
47
|
+
return !!this.prop?.polymorphic && typeof this.key === 'string' && this.key.endsWith(']');
|
|
48
|
+
}
|
|
43
49
|
unwrap() {
|
|
44
50
|
return this.payload;
|
|
45
51
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GroupOperator, isRaw, JsonType, RawQueryFragment, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
|
|
1
|
+
import { GroupOperator, isRaw, JsonType, QueryHelper, RawQueryFragment, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
|
|
2
2
|
import { ObjectCriteriaNode } from './ObjectCriteriaNode.js';
|
|
3
3
|
import { ArrayCriteriaNode } from './ArrayCriteriaNode.js';
|
|
4
4
|
import { ScalarCriteriaNode } from './ScalarCriteriaNode.js';
|
|
@@ -50,8 +50,9 @@ export class CriteriaNodeFactory {
|
|
|
50
50
|
}
|
|
51
51
|
static createObjectItemNode(metadata, entityName, node, payload, key, meta, validate = true) {
|
|
52
52
|
const rawField = RawQueryFragment.isKnownFragmentSymbol(key);
|
|
53
|
-
const prop = rawField ? null : meta?.properties[key];
|
|
54
|
-
const
|
|
53
|
+
const prop = rawField ? null : meta?.properties[QueryHelper.splitPolymorphicKey(key)[0]];
|
|
54
|
+
const target = rawField ? undefined : QueryHelper.findTargetMeta(prop, key);
|
|
55
|
+
const childEntity = prop && prop.kind !== ReferenceKind.SCALAR && target ? target.class : entityName;
|
|
55
56
|
const isNotEmbedded = rawField || prop?.kind !== ReferenceKind.EMBEDDED;
|
|
56
57
|
const val = payload[key];
|
|
57
58
|
if (isNotEmbedded && prop?.customType instanceof JsonType) {
|
|
@@ -5,6 +5,8 @@ import type { ICriteriaNodeProcessOptions, IQueryBuilder } from '../typings.js';
|
|
|
5
5
|
*/
|
|
6
6
|
export declare class ObjectCriteriaNode<T extends object> extends CriteriaNode<T> {
|
|
7
7
|
process(qb: IQueryBuilder<T>, options?: ICriteriaNodeProcessOptions): any;
|
|
8
|
+
/** Orders by the first non-null value of the property across the joined polymorphic targets. */
|
|
9
|
+
private processPolymorphicOrderBy;
|
|
8
10
|
isStrict(): boolean;
|
|
9
11
|
unwrap(): any;
|
|
10
12
|
willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ALIAS_REPLACEMENT, GroupOperator, QueryFlag, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
|
|
1
|
+
import { ALIAS_REPLACEMENT, GroupOperator, QueryFlag, QueryHelper, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
|
|
2
2
|
import { CriteriaNode } from './CriteriaNode.js';
|
|
3
3
|
import { JoinType, QueryType } from './enums.js';
|
|
4
4
|
const COLLECTION_OPERATORS = ['$some', '$none', '$every', '$size', '$all'];
|
|
@@ -7,6 +7,10 @@ const COLLECTION_OPERATORS = ['$some', '$none', '$every', '$size', '$all'];
|
|
|
7
7
|
*/
|
|
8
8
|
export class ObjectCriteriaNode extends CriteriaNode {
|
|
9
9
|
process(qb, options) {
|
|
10
|
+
const targets = this.isPolymorphicBranch() ? QueryHelper.findTargetMetas(this.prop, this.key) : [];
|
|
11
|
+
if (options?.type === 'orderBy' && targets.length > 1) {
|
|
12
|
+
return this.processPolymorphicOrderBy(qb, targets, options);
|
|
13
|
+
}
|
|
10
14
|
const matchPopulateJoins = options?.matchPopulateJoins ||
|
|
11
15
|
(this.prop && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind));
|
|
12
16
|
const nestedAlias = qb.getAliasForJoinPath(this.getPath(options), { ...options, matchPopulateJoins });
|
|
@@ -99,7 +103,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
99
103
|
// with an exclusive condition on the join columns:
|
|
100
104
|
// - if the owning column is null, the row is missing, we don't apply the filter
|
|
101
105
|
// - if the target column is not null, the row is matched, we apply the filter
|
|
102
|
-
if (toOneProperty && this.prop.nullable && this.isStrict()) {
|
|
106
|
+
if (toOneProperty && this.prop.nullable && this.isStrict() && !this.isPolymorphicBranch()) {
|
|
103
107
|
const key = this.prop.owner ? this.prop.name : this.prop.referencedPKs;
|
|
104
108
|
qb.andWhere({
|
|
105
109
|
$or: [
|
|
@@ -109,7 +113,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
109
113
|
});
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
|
-
|
|
116
|
+
const ret = keys.reduce((o, field) => {
|
|
113
117
|
const childNode = this.payload[field];
|
|
114
118
|
const payload = childNode.process(qb, { ...options, alias: this.prop ? alias : ownerAlias });
|
|
115
119
|
const operator = Utils.isOperator(field);
|
|
@@ -149,6 +153,31 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
149
153
|
}
|
|
150
154
|
return o;
|
|
151
155
|
}, {});
|
|
156
|
+
// the target is left joined, so its condition must only match rows that point to it
|
|
157
|
+
if (this.isPolymorphicBranch() && alias && options?.type !== 'orderBy') {
|
|
158
|
+
const pk = this.metadata.find(this.entityName).primaryKeys[0];
|
|
159
|
+
this.inlineCondition(`${alias}.${pk}`, ret, { $ne: null });
|
|
160
|
+
}
|
|
161
|
+
return ret;
|
|
162
|
+
}
|
|
163
|
+
/** Orders by the first non-null value of the property across the joined polymorphic targets. */
|
|
164
|
+
processPolymorphicOrderBy(qb, targets, options) {
|
|
165
|
+
const ownerAlias = options.alias || qb.alias;
|
|
166
|
+
const aliases = targets.map(target => {
|
|
167
|
+
const path = `${this.parent.getPath()}.${this.prop.name}[${target.className}]`;
|
|
168
|
+
const alias = qb.getAliasForJoinPath(path, { ...options, matchPopulateJoins: true });
|
|
169
|
+
if (alias) {
|
|
170
|
+
return alias;
|
|
171
|
+
}
|
|
172
|
+
const newAlias = qb.getNextAlias(target.class);
|
|
173
|
+
qb.addPolymorphicJoin(this.prop, target, ownerAlias, newAlias, JoinType.leftJoin, path);
|
|
174
|
+
return newAlias;
|
|
175
|
+
});
|
|
176
|
+
return Utils.getObjectQueryKeys(this.payload).reduce((o, field) => {
|
|
177
|
+
const columns = targets.map((target, i) => `${aliases[i]}.${target.properties[field].fieldNames[0]}`);
|
|
178
|
+
o[raw(`coalesce(${columns.map(() => '??').join(', ')})`, columns)] = this.payload[field].payload;
|
|
179
|
+
return o;
|
|
180
|
+
}, {});
|
|
152
181
|
}
|
|
153
182
|
isStrict() {
|
|
154
183
|
return (this.strict ||
|
|
@@ -186,8 +215,9 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
186
215
|
const operator = Utils.isObject(payload) &&
|
|
187
216
|
Utils.getObjectQueryKeys(payload).every(k => {
|
|
188
217
|
if (k === '$not' && Utils.isPlainObject(payload[k])) {
|
|
189
|
-
// $not wrapping non-operator conditions (entity props) should be inlined
|
|
190
|
-
|
|
218
|
+
// $not wrapping non-operator conditions (entity props) or an empty one should be inlined
|
|
219
|
+
const inner = Utils.getObjectQueryKeys(payload[k]);
|
|
220
|
+
return inner.length > 0 && inner.every(ik => Utils.isOperator(ik, false));
|
|
191
221
|
}
|
|
192
222
|
return Utils.isOperator(k, false);
|
|
193
223
|
});
|
|
@@ -211,16 +241,19 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
211
241
|
this.inlineCondition(key, obj, value);
|
|
212
242
|
}
|
|
213
243
|
inlineChildPayload(o, payload, field, alias, childAlias) {
|
|
214
|
-
const prop = this.
|
|
244
|
+
const prop = this.payload[field].prop;
|
|
215
245
|
for (const k of Utils.getObjectQueryKeys(payload)) {
|
|
216
246
|
if (RawQueryFragment.isKnownFragmentSymbol(k)) {
|
|
217
247
|
o[k] = payload[k];
|
|
218
248
|
}
|
|
219
249
|
else if (k === '$not' &&
|
|
220
250
|
Utils.isPlainObject(payload[k]) &&
|
|
221
|
-
Utils.
|
|
222
|
-
|
|
223
|
-
|
|
251
|
+
(!Utils.hasObjectKeys(payload[k]) ||
|
|
252
|
+
Utils.getObjectQueryKeys(payload[k]).some(ik => !Utils.isOperator(ik, false)))) {
|
|
253
|
+
// $not wraps entity conditions (from auto-join) or an empty one, inline at current level with the same key mapping
|
|
254
|
+
const inner = {};
|
|
255
|
+
this.inlineChildPayload(inner, payload[k], field, alias, childAlias);
|
|
256
|
+
this.inlineCondition(k, o, inner);
|
|
224
257
|
}
|
|
225
258
|
else if (Utils.isOperator(k, false)) {
|
|
226
259
|
const tmp = payload[k];
|
|
@@ -273,6 +306,10 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
273
306
|
if (keys.some(k => this.isCollectionOperator(k))) {
|
|
274
307
|
return true;
|
|
275
308
|
}
|
|
309
|
+
// the target's discriminator is checked in the join condition, so it is needed even for PK conditions
|
|
310
|
+
if (this.isPolymorphicBranch()) {
|
|
311
|
+
return !nestedAlias;
|
|
312
|
+
}
|
|
276
313
|
const meta = this.metadata.find(this.entityName);
|
|
277
314
|
const embeddable = this.prop.kind === ReferenceKind.EMBEDDED;
|
|
278
315
|
const knownKey = [ReferenceKind.SCALAR, ReferenceKind.MANY_TO_ONE, ReferenceKind.EMBEDDED].includes(this.prop.kind) ||
|
|
@@ -291,6 +328,13 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
291
328
|
});
|
|
292
329
|
const primaryKeys = knownKey &&
|
|
293
330
|
keys.every(key => {
|
|
331
|
+
if (key === '$not') {
|
|
332
|
+
// e.g. `{ author: { $not: { id: 1 } } }` compares the FK column, relation PKs still need the join
|
|
333
|
+
const childPayload = this.payload[key].payload;
|
|
334
|
+
return (Utils.isPlainObject(childPayload) &&
|
|
335
|
+
Utils.getObjectQueryKeys(childPayload).every(k => meta.primaryKeys.includes(k) &&
|
|
336
|
+
meta.properties[k].kind === ReferenceKind.SCALAR));
|
|
337
|
+
}
|
|
294
338
|
if (typeof key !== 'string' || !meta.primaryKeys.includes(key)) {
|
|
295
339
|
return false;
|
|
296
340
|
}
|
|
@@ -317,10 +361,16 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
317
361
|
if (this.prop.kind === ReferenceKind.MANY_TO_MANY && (scalar || operator)) {
|
|
318
362
|
qb.join(field, nestedAlias, undefined, JoinType.pivotJoin, path);
|
|
319
363
|
}
|
|
364
|
+
else if (this.isPolymorphicBranch()) {
|
|
365
|
+
const targetMeta = this.metadata.find(this.entityName);
|
|
366
|
+
qb.addPolymorphicJoin(this.prop, targetMeta, alias, nestedAlias, JoinType.leftJoin, path);
|
|
367
|
+
}
|
|
320
368
|
else {
|
|
321
369
|
const prev = qb.state.fields?.slice();
|
|
322
370
|
const toOneProperty = [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind);
|
|
323
|
-
|
|
371
|
+
// ordering must not drop rows whose polymorphic relation points to another target
|
|
372
|
+
const orderByPolymorphic = this.prop.polymorphic && options?.type === 'orderBy';
|
|
373
|
+
const joinType = toOneProperty && !this.prop.nullable && !orderByPolymorphic ? JoinType.innerJoin : JoinType.leftJoin;
|
|
324
374
|
qb[method](field, nestedAlias, undefined, joinType, path);
|
|
325
375
|
if (!qb.hasFlag(QueryFlag.INFER_POPULATE)) {
|
|
326
376
|
qb.state.fields = prev;
|
package/query/QueryBuilder.js
CHANGED
|
@@ -1032,9 +1032,11 @@ export class QueryBuilder {
|
|
|
1032
1032
|
* @internal
|
|
1033
1033
|
*/
|
|
1034
1034
|
addPolymorphicJoin(prop, targetMeta, ownerAlias, alias, type, path, schema) {
|
|
1035
|
-
// Override referencedColumnNames to use the specific target's PK columns
|
|
1035
|
+
// Override referencedColumnNames to use the specific target's PK (or `targetKey`) columns
|
|
1036
1036
|
// (polymorphic targets may have different PK column names, e.g. org_id vs user_id)
|
|
1037
|
-
const referencedColumnNames =
|
|
1037
|
+
const referencedColumnNames = prop.targetKey
|
|
1038
|
+
? targetMeta.properties[prop.targetKey].fieldNames
|
|
1039
|
+
: targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
|
|
1038
1040
|
const targetProp = { ...prop, targetMeta, referencedColumnNames };
|
|
1039
1041
|
const aliasedName = `${ownerAlias}.${prop.name}[${targetMeta.className}]#${alias}`;
|
|
1040
1042
|
this.#state.joins[aliasedName] = this.helper.joinManyToOneReference(targetProp, ownerAlias, alias, type, {}, schema);
|
package/schema/DatabaseTable.js
CHANGED
|
@@ -155,6 +155,7 @@ export class DatabaseTable {
|
|
|
155
155
|
comment: prop.comment,
|
|
156
156
|
collation: prop.collation,
|
|
157
157
|
extra: prop.extra,
|
|
158
|
+
sequence: prop.sequence,
|
|
158
159
|
ignoreSchemaChanges: prop.ignoreSchemaChanges,
|
|
159
160
|
};
|
|
160
161
|
this.#columns[field].unsigned ??= this.#columns[field].autoincrement;
|
|
@@ -796,6 +797,7 @@ export class DatabaseTable {
|
|
|
796
797
|
nullable: column.nullable,
|
|
797
798
|
primary: column.primary && persist,
|
|
798
799
|
autoincrement: column.autoincrement,
|
|
800
|
+
sequence: column.sequence,
|
|
799
801
|
fieldName: column.name,
|
|
800
802
|
unsigned: column.unsigned,
|
|
801
803
|
length: column.length,
|
|
@@ -708,7 +708,9 @@ export class SchemaComparator {
|
|
|
708
708
|
log(`'nullable' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
|
709
709
|
changedProperties.add('nullable');
|
|
710
710
|
}
|
|
711
|
-
|
|
711
|
+
// identity options (start/increment) are applied on create only, same as the `sequence` option
|
|
712
|
+
const withoutIdentityOptions = (generated) => generated?.replace(/^((?:by default as )?identity)\s*\(.*\)$/is, '$1');
|
|
713
|
+
if (this.diffExpression(withoutIdentityOptions(fromColumn.generated), withoutIdentityOptions(toColumn.generated))) {
|
|
712
714
|
log(`'generated' changed for column ${fromTable.name}.${fromColumn.name}`, { fromColumn, toColumn });
|
|
713
715
|
changedProperties.add('generated');
|
|
714
716
|
}
|
package/schema/SchemaHelper.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Connection, type Dictionary, type Options, type Transaction, type RawQueryFragment } from '@mikro-orm/core';
|
|
1
|
+
import { type Connection, type Dictionary, type Options, type Transaction, type RawQueryFragment, type SequenceOptions } from '@mikro-orm/core';
|
|
2
2
|
import type { AbstractSqlConnection } from '../AbstractSqlConnection.js';
|
|
3
3
|
import type { AbstractSqlPlatform } from '../AbstractSqlPlatform.js';
|
|
4
4
|
import type { CheckDef, Column, ForeignKey, IndexDef, Table, TableDifference, SqlTriggerDef, SqlRoutineDef } from '../typings.js';
|
|
@@ -45,6 +45,12 @@ export declare abstract class SchemaHelper {
|
|
|
45
45
|
resolveMigrationSchema(schema: string | undefined): string | undefined;
|
|
46
46
|
finalizeTable(table: DatabaseTable, charset: string, collate?: string): string;
|
|
47
47
|
appendComments(table: DatabaseTable): string[];
|
|
48
|
+
/** Returns SQL that moves the autoincrement counter back to `sequence.startWith` after `clear()` truncated the table. */
|
|
49
|
+
getResetSequenceSQL(tableName: string, schemaName: string | undefined, columnName: string, startWith: number): string;
|
|
50
|
+
/** Maps the introspected start and increment of an autoincrement column to its `sequence` option, omitting defaults. */
|
|
51
|
+
protected getIntrospectedSequence(start: unknown, increment: unknown): SequenceOptions | undefined;
|
|
52
|
+
/** Renders the `start with … increment by …` clause for the `sequence` option of an autoincrement column. */
|
|
53
|
+
protected getSequenceOptionsSQL(column: Column): string;
|
|
48
54
|
supportsSchemaConstraints(): boolean;
|
|
49
55
|
getPrimaryKeys(connection: AbstractSqlConnection, indexes: IndexDef[] | undefined, tableName: string, schemaName?: string): Promise<string[]>;
|
|
50
56
|
inferLengthFromColumnType(type: string): number | undefined;
|
package/schema/SchemaHelper.js
CHANGED
|
@@ -91,6 +91,38 @@ export class SchemaHelper {
|
|
|
91
91
|
appendComments(table) {
|
|
92
92
|
return [];
|
|
93
93
|
}
|
|
94
|
+
/** Returns SQL that moves the autoincrement counter back to `sequence.startWith` after `clear()` truncated the table. */
|
|
95
|
+
/* v8 ignore next */
|
|
96
|
+
getResetSequenceSQL(tableName, schemaName, columnName, startWith) {
|
|
97
|
+
return '';
|
|
98
|
+
}
|
|
99
|
+
/** Maps the introspected start and increment of an autoincrement column to its `sequence` option, omitting defaults. */
|
|
100
|
+
getIntrospectedSequence(start, increment) {
|
|
101
|
+
if (start == null) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const incrementBy = Number(increment ?? 1);
|
|
105
|
+
const startWith = Number(start);
|
|
106
|
+
const sequence = {};
|
|
107
|
+
if (startWith !== (incrementBy < 0 ? -1 : 1)) {
|
|
108
|
+
sequence.startWith = startWith;
|
|
109
|
+
}
|
|
110
|
+
if (incrementBy !== 1) {
|
|
111
|
+
sequence.incrementBy = incrementBy;
|
|
112
|
+
}
|
|
113
|
+
return Utils.hasObjectKeys(sequence) ? sequence : undefined;
|
|
114
|
+
}
|
|
115
|
+
/** Renders the `start with … increment by …` clause for the `sequence` option of an autoincrement column. */
|
|
116
|
+
getSequenceOptionsSQL(column) {
|
|
117
|
+
const { startWith, incrementBy } = column.sequence ?? {};
|
|
118
|
+
const descending = incrementBy != null && incrementBy < 0;
|
|
119
|
+
// a sequence is bounded by 1 (ascending) or -1 (descending) by default, a start beyond that has to move the bound
|
|
120
|
+
const bound = startWith != null &&
|
|
121
|
+
(descending ? startWith > -1 && `maxvalue ${startWith}` : startWith < 1 && `minvalue ${startWith}`);
|
|
122
|
+
return [startWith != null && `start with ${startWith}`, incrementBy != null && `increment by ${incrementBy}`, bound]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join(' ');
|
|
125
|
+
}
|
|
94
126
|
supportsSchemaConstraints() {
|
|
95
127
|
return true;
|
|
96
128
|
}
|
|
@@ -474,10 +506,16 @@ export class SchemaHelper {
|
|
|
474
506
|
for (const foreignKey of Object.values(diff.changedForeignKeys)) {
|
|
475
507
|
ret.push(this.createForeignKey(diff.toTable, foreignKey));
|
|
476
508
|
}
|
|
477
|
-
for (const index of Object.values(diff.addedIndexes)) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
509
|
+
for (const index of [...Object.values(diff.addedIndexes), ...Object.values(diff.changedIndexes)]) {
|
|
510
|
+
const addedColumn = diff.addedColumns[index.columnNames[0]];
|
|
511
|
+
// an added autoincrement PK column already declares the primary key inline
|
|
512
|
+
if (index.primary &&
|
|
513
|
+
!index.composite &&
|
|
514
|
+
addedColumn?.autoincrement &&
|
|
515
|
+
addedColumn.primary &&
|
|
516
|
+
!this.hasNonDefaultPrimaryKeyName(diff.toTable)) {
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
481
519
|
ret.push(this.createIndex(index, diff.toTable, true));
|
|
482
520
|
}
|
|
483
521
|
for (const [oldIndexName, index] of Object.entries(diff.renamedIndexes)) {
|
|
@@ -141,6 +141,14 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
|
|
|
141
141
|
.withSchema(schema)
|
|
142
142
|
.truncate()
|
|
143
143
|
.execute();
|
|
144
|
+
// the counters of a TPT child's inherited columns (PK included) belong to the parent table
|
|
145
|
+
const props = meta.tptParent ? (meta.ownProps ?? []) : meta.props;
|
|
146
|
+
for (const prop of props.filter(prop => prop.autoincrement && prop.sequence?.startWith != null)) {
|
|
147
|
+
const sql = this.helper.getResetSequenceSQL(meta.tableName, this.getSchemaName(meta, { schema }), prop.fieldNames[0], prop.sequence.startWith);
|
|
148
|
+
if (sql) {
|
|
149
|
+
await this.execute(sql, { ctx: this.em?.getTransactionContext() });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
144
152
|
}
|
|
145
153
|
catch (e) {
|
|
146
154
|
if (this.platform.getExceptionConverter().convertException(e) instanceof TableNotFoundException) {
|
package/typings.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Generated, Kysely } from 'kysely';
|
|
2
|
-
import type { CheckCallback, DeferMode, Dictionary, EntityName, EntityProperty, EntitySchemaWithMeta, ExtractDefineEntityProperties, FilterQuery, GroupOperator, IndexColumnOptions, InferEntityName, Opt, Primary, PrimaryProperty, QueryFlag, QueryOrderMap, RawQueryFragment, RoutineIgnoreField, Scalar, Type } from '@mikro-orm/core';
|
|
2
|
+
import type { CheckCallback, DeferMode, Dictionary, EntityName, EntityMetadata, EntityProperty, EntitySchemaWithMeta, ExtractDefineEntityProperties, FilterQuery, GroupOperator, IndexColumnOptions, InferEntityName, Opt, Primary, PrimaryProperty, QueryFlag, QueryOrderMap, RawQueryFragment, RoutineIgnoreField, Scalar, SequenceOptions, Type } from '@mikro-orm/core';
|
|
3
3
|
import type { JoinType, QueryType } from './query/enums.js';
|
|
4
4
|
import type { DatabaseSchema } from './schema/DatabaseSchema.js';
|
|
5
5
|
import type { DatabaseTable } from './schema/DatabaseTable.js';
|
|
@@ -37,6 +37,8 @@ export interface Column {
|
|
|
37
37
|
mappedType: Type<unknown>;
|
|
38
38
|
unsigned?: boolean;
|
|
39
39
|
autoincrement?: boolean;
|
|
40
|
+
/** applied only when the column is created, never diffed */
|
|
41
|
+
sequence?: SequenceOptions;
|
|
40
42
|
nullable?: boolean;
|
|
41
43
|
length?: number;
|
|
42
44
|
precision?: number;
|
|
@@ -295,6 +297,8 @@ export interface IQueryBuilder<T> {
|
|
|
295
297
|
scheduleFilterCheck(path: string): void;
|
|
296
298
|
/** @internal */
|
|
297
299
|
ensureTPTJoins(): void;
|
|
300
|
+
/** @internal */
|
|
301
|
+
addPolymorphicJoin(prop: EntityProperty, targetMeta: EntityMetadata, ownerAlias: string, alias: string, type: JoinType, path: string, schema?: string): void;
|
|
298
302
|
withSchema(schema: string): this;
|
|
299
303
|
}
|
|
300
304
|
export interface ICriteriaNodeProcessOptions {
|