@mikro-orm/sql 7.1.0-dev.2 → 7.1.0-dev.20
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/AbstractSqlConnection.d.ts +1 -1
- package/AbstractSqlConnection.js +2 -2
- package/AbstractSqlDriver.d.ts +25 -1
- package/AbstractSqlDriver.js +315 -15
- package/PivotCollectionPersister.js +13 -2
- package/SqlEntityManager.d.ts +5 -1
- package/SqlEntityManager.js +36 -1
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +4 -0
- package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
- package/dialects/mysql/BaseMySqlPlatform.js +3 -0
- package/dialects/mysql/MySqlNativeQueryBuilder.js +11 -0
- package/dialects/mysql/MySqlSchemaHelper.d.ts +9 -3
- package/dialects/mysql/MySqlSchemaHelper.js +102 -4
- package/dialects/oracledb/OracleDialect.d.ts +1 -1
- package/dialects/oracledb/OracleDialect.js +2 -1
- package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +1 -0
- package/dialects/postgresql/BasePostgreSqlPlatform.js +3 -0
- package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +21 -1
- package/dialects/postgresql/PostgreSqlSchemaHelper.js +200 -4
- package/dialects/sqlite/SqliteSchemaHelper.d.ts +6 -1
- package/dialects/sqlite/SqliteSchemaHelper.js +114 -2
- package/package.json +3 -3
- package/query/CriteriaNode.d.ts +1 -1
- package/query/CriteriaNode.js +2 -2
- package/query/NativeQueryBuilder.d.ts +6 -0
- package/query/NativeQueryBuilder.js +16 -1
- package/query/ObjectCriteriaNode.js +1 -1
- package/query/QueryBuilder.d.ts +77 -0
- package/query/QueryBuilder.js +170 -6
- package/schema/DatabaseSchema.js +18 -0
- package/schema/DatabaseTable.d.ts +13 -1
- package/schema/DatabaseTable.js +50 -3
- package/schema/SchemaComparator.d.ts +1 -0
- package/schema/SchemaComparator.js +86 -1
- package/schema/SchemaHelper.d.ts +51 -1
- package/schema/SchemaHelper.js +191 -2
- package/schema/SqlSchemaGenerator.js +7 -0
- package/schema/partitioning.d.ts +13 -0
- package/schema/partitioning.js +326 -0
- package/typings.d.ts +32 -1
|
@@ -15,6 +15,7 @@ const SPATIALITE_VIEWS = [
|
|
|
15
15
|
'ElementaryGeometries',
|
|
16
16
|
];
|
|
17
17
|
export class SqliteSchemaHelper extends SchemaHelper {
|
|
18
|
+
static PARTIAL_WHERE_RE = /\swhere\s+(.+?)\s*$/is;
|
|
18
19
|
disableForeignKeysSQL() {
|
|
19
20
|
return 'pragma foreign_keys = off;';
|
|
20
21
|
}
|
|
@@ -101,7 +102,9 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
101
102
|
const pks = await this.getPrimaryKeys(connection, indexes, table.name, table.schema, ctx);
|
|
102
103
|
const fks = await this.getForeignKeys(connection, table.name, table.schema, ctx);
|
|
103
104
|
const enums = await this.getEnumDefinitions(connection, table.name, table.schema, ctx);
|
|
105
|
+
const triggers = await this.getTableTriggers(connection, table.name);
|
|
104
106
|
table.init(cols, indexes, checks, pks, fks, enums);
|
|
107
|
+
table.setTriggers(triggers);
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
createTable(table, alter) {
|
|
@@ -140,6 +143,9 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
140
143
|
for (const index of table.getIndexes()) {
|
|
141
144
|
this.append(ret, this.createIndex(index, table));
|
|
142
145
|
}
|
|
146
|
+
for (const trigger of table.getTriggers()) {
|
|
147
|
+
this.append(ret, this.createTrigger(table, trigger));
|
|
148
|
+
}
|
|
143
149
|
return ret;
|
|
144
150
|
}
|
|
145
151
|
createTableColumn(column, table, _changedProperties) {
|
|
@@ -211,10 +217,10 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
211
217
|
if (index.columnNames.some(column => column.includes('.'))) {
|
|
212
218
|
// JSON columns can have unique index but not unique constraint, and we need to distinguish those, so we can properly drop them
|
|
213
219
|
const columns = this.platform.getJsonIndexDefinition(index);
|
|
214
|
-
return `${sqlPrefix} (${columns.join(', ')})`;
|
|
220
|
+
return `${sqlPrefix} (${columns.join(', ')})${this.getIndexWhereClause(index)}`;
|
|
215
221
|
}
|
|
216
222
|
// Use getIndexColumns to support advanced options like sort order and collation
|
|
217
|
-
return `${sqlPrefix} (${this.getIndexColumns(index)})`;
|
|
223
|
+
return `${sqlPrefix} (${this.getIndexColumns(index)})${this.getIndexWhereClause(index)}`;
|
|
218
224
|
}
|
|
219
225
|
parseTableDefinition(sql, cols) {
|
|
220
226
|
const columns = {};
|
|
@@ -350,6 +356,16 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
350
356
|
const sql = `pragma ${prefix}table_info(\`${tableName}\`)`;
|
|
351
357
|
const cols = await connection.execute(sql, [], 'all', ctx);
|
|
352
358
|
const indexes = await connection.execute(`pragma ${prefix}index_list(\`${tableName}\`)`, [], 'all', ctx);
|
|
359
|
+
// sqlite_master.sql holds the original CREATE INDEX statement — the only place a partial
|
|
360
|
+
// index's WHERE predicate is preserved (PRAGMA index_* don't expose it).
|
|
361
|
+
const indexSqls = await connection.execute(`select name, sql from ${prefix}sqlite_master where type = 'index' and tbl_name = ?`, [tableName], 'all', ctx);
|
|
362
|
+
const wherePredicates = new Map();
|
|
363
|
+
for (const row of indexSqls) {
|
|
364
|
+
const match = row.sql && SqliteSchemaHelper.PARTIAL_WHERE_RE.exec(row.sql);
|
|
365
|
+
if (match) {
|
|
366
|
+
wherePredicates.set(row.name, match[1].trim());
|
|
367
|
+
}
|
|
368
|
+
}
|
|
353
369
|
const ret = [];
|
|
354
370
|
for (const col of cols.filter(c => c.pk)) {
|
|
355
371
|
ret.push({
|
|
@@ -362,12 +378,14 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
362
378
|
}
|
|
363
379
|
for (const index of indexes.filter(index => !this.isImplicitIndex(index.name))) {
|
|
364
380
|
const res = await connection.execute(`pragma ${prefix}index_info(\`${index.name}\`)`, [], 'all', ctx);
|
|
381
|
+
const where = wherePredicates.get(index.name);
|
|
365
382
|
ret.push(...res.map(row => ({
|
|
366
383
|
columnNames: [row.name],
|
|
367
384
|
keyName: index.name,
|
|
368
385
|
unique: !!index.unique,
|
|
369
386
|
constraint: !!index.unique,
|
|
370
387
|
primary: false,
|
|
388
|
+
...(where ? { where } : {}),
|
|
371
389
|
})));
|
|
372
390
|
}
|
|
373
391
|
return this.mapIndexes(ret);
|
|
@@ -501,8 +519,102 @@ export class SqliteSchemaHelper extends SchemaHelper {
|
|
|
501
519
|
this.append(ret, this.getRenameIndexSQL(diff.name, index, oldIndexName));
|
|
502
520
|
}
|
|
503
521
|
}
|
|
522
|
+
for (const trigger of Object.values(diff.removedTriggers)) {
|
|
523
|
+
this.append(ret, this.dropTrigger(diff.toTable, trigger));
|
|
524
|
+
}
|
|
525
|
+
for (const trigger of Object.values(diff.changedTriggers)) {
|
|
526
|
+
this.append(ret, this.dropTrigger(diff.toTable, trigger));
|
|
527
|
+
this.append(ret, this.createTrigger(diff.toTable, trigger));
|
|
528
|
+
}
|
|
529
|
+
for (const trigger of Object.values(diff.addedTriggers)) {
|
|
530
|
+
this.append(ret, this.createTrigger(diff.toTable, trigger));
|
|
531
|
+
}
|
|
504
532
|
return ret;
|
|
505
533
|
}
|
|
534
|
+
/** Generates SQL to create SQLite triggers. SQLite requires one trigger per event. */
|
|
535
|
+
createTrigger(table, trigger) {
|
|
536
|
+
if (trigger.expression) {
|
|
537
|
+
return trigger.expression;
|
|
538
|
+
}
|
|
539
|
+
const timing = trigger.timing.toUpperCase();
|
|
540
|
+
const forEach = trigger.forEach === 'statement' ? 'STATEMENT' : 'ROW';
|
|
541
|
+
const ret = [];
|
|
542
|
+
for (const event of trigger.events) {
|
|
543
|
+
const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
|
|
544
|
+
const when = trigger.when ? `\n when ${trigger.when}` : '';
|
|
545
|
+
ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ${forEach}${when} begin ${trigger.body}; end`);
|
|
546
|
+
}
|
|
547
|
+
return ret.join(';\n');
|
|
548
|
+
}
|
|
549
|
+
async getTableTriggers(connection, tableName) {
|
|
550
|
+
const rows = await connection.execute(`select name, sql from sqlite_master where type = 'trigger' and tbl_name = ?`, [tableName]);
|
|
551
|
+
// First pass: parse all triggers and collect names to detect multi-event groups
|
|
552
|
+
const parsedRows = [];
|
|
553
|
+
for (const row of rows) {
|
|
554
|
+
/* v8 ignore next 3 */
|
|
555
|
+
if (!row.sql) {
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
const parsed = this.parseTriggerDDL(row.sql, row.name);
|
|
559
|
+
if (parsed) {
|
|
560
|
+
parsedRows.push({ name: row.name, parsed });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const allNames = parsedRows.map(r => r.name);
|
|
564
|
+
const triggers = [];
|
|
565
|
+
const triggerMap = new Map();
|
|
566
|
+
for (const { name, parsed } of parsedRows) {
|
|
567
|
+
// Only strip event suffix when another trigger with the same base exists
|
|
568
|
+
const eventLower = parsed.events[0];
|
|
569
|
+
const candidateBase = name.endsWith(`_${eventLower}`) ? name.slice(0, -eventLower.length - 1) : null;
|
|
570
|
+
const baseName = candidateBase && allNames.some(n => n !== name && n.startsWith(`${candidateBase}_`)) ? candidateBase : name;
|
|
571
|
+
if (triggerMap.has(baseName)) {
|
|
572
|
+
const existing = triggerMap.get(baseName);
|
|
573
|
+
if (!existing.events.includes(parsed.events[0])) {
|
|
574
|
+
existing.events.push(parsed.events[0]);
|
|
575
|
+
}
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
const trigger = { ...parsed, name: baseName };
|
|
579
|
+
triggers.push(trigger);
|
|
580
|
+
triggerMap.set(baseName, trigger);
|
|
581
|
+
}
|
|
582
|
+
return triggers;
|
|
583
|
+
}
|
|
584
|
+
parseTriggerDDL(sql, name) {
|
|
585
|
+
// Split at the last top-level BEGIN to separate header from body,
|
|
586
|
+
// so that a WHEN clause containing the word "begin" in a string literal doesn't confuse parsing.
|
|
587
|
+
const beginIdx = sql.search(/\bbegin\b(?=[^]*$)/i);
|
|
588
|
+
/* v8 ignore next 3 */
|
|
589
|
+
if (beginIdx === -1) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const header = sql.slice(0, beginIdx);
|
|
593
|
+
const bodyPart = sql.slice(beginIdx);
|
|
594
|
+
const headerMatch = /create\s+trigger\s+["`]?\w+["`]?\s+(before|after|instead\s+of)\s+(insert|update|delete)\s+on\s+["`]?\w+["`]?\s*(?:for\s+each\s+(row|statement))?\s*(?:when\s+([\s\S]*?))?\s*$/i.exec(header);
|
|
595
|
+
/* v8 ignore next 3 */
|
|
596
|
+
if (!headerMatch) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
const bodyMatch = /^begin\s+([\s\S]*?)\s*end/i.exec(bodyPart);
|
|
600
|
+
/* v8 ignore next 3 */
|
|
601
|
+
if (!bodyMatch) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
const timing = headerMatch[1].toLowerCase();
|
|
605
|
+
const event = headerMatch[2].toLowerCase();
|
|
606
|
+
const forEach = (headerMatch[3]?.toLowerCase() ?? 'row');
|
|
607
|
+
const when = headerMatch[4]?.trim() || undefined;
|
|
608
|
+
const body = bodyMatch[1].trim().replace(/;\s*$/, '');
|
|
609
|
+
return {
|
|
610
|
+
name,
|
|
611
|
+
timing,
|
|
612
|
+
events: [event],
|
|
613
|
+
forEach,
|
|
614
|
+
body,
|
|
615
|
+
when,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
506
618
|
getAlterTempTableSQL(changedTable) {
|
|
507
619
|
const tempName = `${changedTable.toTable.name}__temp_alter`;
|
|
508
620
|
const quotedName = this.quote(changedTable.toTable.name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikro-orm/sql",
|
|
3
|
-
"version": "7.1.0-dev.
|
|
3
|
+
"version": "7.1.0-dev.20",
|
|
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.28.16"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"@mikro-orm/core": "^7.0.
|
|
53
|
+
"@mikro-orm/core": "^7.0.12"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.1.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.1.0-dev.20"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
package/query/CriteriaNode.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export declare class CriteriaNode<T extends object> implements ICriteriaNode<T>
|
|
|
21
21
|
shouldInline(payload: any): boolean;
|
|
22
22
|
willAutoJoin(qb: IQueryBuilder<T>, alias?: string, options?: ICriteriaNodeProcessOptions): boolean;
|
|
23
23
|
shouldRename(payload: any): boolean;
|
|
24
|
-
renameFieldToPK<T>(qb: IQueryBuilder<T>, ownerAlias?: string): string;
|
|
24
|
+
renameFieldToPK<T>(qb: IQueryBuilder<T>, ownerAlias?: string, options?: ICriteriaNodeProcessOptions): string;
|
|
25
25
|
getPath(opts?: {
|
|
26
26
|
addIndex?: boolean;
|
|
27
27
|
parentPath?: string;
|
package/query/CriteriaNode.js
CHANGED
|
@@ -83,8 +83,8 @@ export class CriteriaNode {
|
|
|
83
83
|
return false;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
renameFieldToPK(qb, ownerAlias) {
|
|
87
|
-
const joinAlias = qb.getAliasForJoinPath(this.getPath(), { matchPopulateJoins: true });
|
|
86
|
+
renameFieldToPK(qb, ownerAlias, options) {
|
|
87
|
+
const joinAlias = qb.getAliasForJoinPath(this.getPath(), { ...options, matchPopulateJoins: true });
|
|
88
88
|
if (!joinAlias &&
|
|
89
89
|
this.parent &&
|
|
90
90
|
[ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(this.prop.kind) &&
|
|
@@ -37,6 +37,11 @@ interface Options {
|
|
|
37
37
|
limit?: number;
|
|
38
38
|
offset?: number;
|
|
39
39
|
data?: Dictionary;
|
|
40
|
+
insertSubQuery?: {
|
|
41
|
+
sql: string;
|
|
42
|
+
params: unknown[];
|
|
43
|
+
columns: string[];
|
|
44
|
+
};
|
|
40
45
|
onConflict?: OnConflictClause;
|
|
41
46
|
lockMode?: LockMode;
|
|
42
47
|
lockTables?: string[];
|
|
@@ -105,6 +110,7 @@ export declare class NativeQueryBuilder implements Subquery {
|
|
|
105
110
|
limit(limit: number): this;
|
|
106
111
|
offset(offset: number): this;
|
|
107
112
|
insert(data: Dictionary): this;
|
|
113
|
+
insertSelect(columns: string[], subQuery: NativeQueryBuilder | RawQueryFragment): this;
|
|
108
114
|
update(data: Dictionary): this;
|
|
109
115
|
delete(): this;
|
|
110
116
|
truncate(): this;
|
|
@@ -215,6 +215,12 @@ export class NativeQueryBuilder {
|
|
|
215
215
|
this.options.data = data;
|
|
216
216
|
return this;
|
|
217
217
|
}
|
|
218
|
+
insertSelect(columns, subQuery) {
|
|
219
|
+
this.type = QueryType.INSERT;
|
|
220
|
+
const { sql, params } = subQuery instanceof NativeQueryBuilder ? subQuery.compile() : { sql: subQuery.sql, params: [...subQuery.params] };
|
|
221
|
+
this.options.insertSubQuery = { sql, params, columns: columns.map(c => this.quote(c)) };
|
|
222
|
+
return this;
|
|
223
|
+
}
|
|
218
224
|
update(data) {
|
|
219
225
|
this.type = QueryType.UPDATE;
|
|
220
226
|
this.options.data ??= {};
|
|
@@ -352,12 +358,21 @@ export class NativeQueryBuilder {
|
|
|
352
358
|
return fields;
|
|
353
359
|
}
|
|
354
360
|
compileInsert() {
|
|
355
|
-
if (!this.options.data) {
|
|
361
|
+
if (!this.options.data && !this.options.insertSubQuery) {
|
|
356
362
|
throw new Error('No data provided');
|
|
357
363
|
}
|
|
358
364
|
this.parts.push('insert');
|
|
359
365
|
this.addHintComment();
|
|
360
366
|
this.parts.push(`into ${this.getTableName()}`);
|
|
367
|
+
if (this.options.insertSubQuery) {
|
|
368
|
+
if (this.options.insertSubQuery.columns.length) {
|
|
369
|
+
this.parts.push(`(${this.options.insertSubQuery.columns.join(', ')})`);
|
|
370
|
+
}
|
|
371
|
+
this.addOutputClause('inserted');
|
|
372
|
+
this.parts.push(this.options.insertSubQuery.sql);
|
|
373
|
+
this.params.push(...this.options.insertSubQuery.params);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
361
376
|
if (Object.keys(this.options.data).length === 0) {
|
|
362
377
|
this.addOutputClause('inserted');
|
|
363
378
|
this.parts.push('default values');
|
|
@@ -105,7 +105,7 @@ export class ObjectCriteriaNode extends CriteriaNode {
|
|
|
105
105
|
this.inlineChildPayload(o, payload, field, a, childAlias);
|
|
106
106
|
}
|
|
107
107
|
else if (childNode.shouldRename(payload)) {
|
|
108
|
-
this.inlineCondition(childNode.renameFieldToPK(qb, alias), o, payload);
|
|
108
|
+
this.inlineCondition(childNode.renameFieldToPK(qb, alias, options), o, payload);
|
|
109
109
|
}
|
|
110
110
|
else if (isRawField) {
|
|
111
111
|
const rawField = RawQueryFragment.getKnownFragment(field);
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -11,6 +11,20 @@ export interface ExecuteOptions {
|
|
|
11
11
|
mergeResults?: boolean;
|
|
12
12
|
}
|
|
13
13
|
export interface QBStreamOptions {
|
|
14
|
+
/**
|
|
15
|
+
* How many rows to fetch in one round-trip.
|
|
16
|
+
* Lower values will result in more queries and network bandwidth, but less memory usage.
|
|
17
|
+
* Higher values will result in fewer queries and network bandwidth, but higher memory usage.
|
|
18
|
+
* Note that the results are iterated one row at a time regardless of this value.
|
|
19
|
+
*
|
|
20
|
+
* Honored on PostgreSQL (cursor-based fetch), MSSQL (tedious stream chunk size)
|
|
21
|
+
* and Oracle (mapped to `fetchArraySize`). Ignored on MySQL, MariaDB, SQLite and
|
|
22
|
+
* libSQL, where the underlying driver already streams row-by-row with no batching
|
|
23
|
+
* knob.
|
|
24
|
+
*
|
|
25
|
+
* @default 100 on dialects that honor it.
|
|
26
|
+
*/
|
|
27
|
+
chunkSize?: number;
|
|
14
28
|
/**
|
|
15
29
|
* Results are mapped to entities, if you set `mapResults: false` you will get POJOs instead.
|
|
16
30
|
*
|
|
@@ -158,6 +172,8 @@ export interface QBState<Entity extends object> {
|
|
|
158
172
|
schema?: string;
|
|
159
173
|
cond: Dictionary;
|
|
160
174
|
data?: Dictionary;
|
|
175
|
+
insertSubQuery?: QueryBuilder<any>;
|
|
176
|
+
insertColumns?: string[];
|
|
161
177
|
orderBy: QueryOrderMap<Entity>[];
|
|
162
178
|
groupBy: InternalField<Entity>[];
|
|
163
179
|
having: Dictionary;
|
|
@@ -191,6 +207,11 @@ export interface QBState<Entity extends object> {
|
|
|
191
207
|
})[];
|
|
192
208
|
tptJoinsApplied: boolean;
|
|
193
209
|
autoJoinedPaths: string[];
|
|
210
|
+
partitionLimit?: {
|
|
211
|
+
partitionBy: string;
|
|
212
|
+
limit: number;
|
|
213
|
+
offset?: number;
|
|
214
|
+
};
|
|
194
215
|
}
|
|
195
216
|
/**
|
|
196
217
|
* SQL query builder with fluent interface.
|
|
@@ -324,6 +345,35 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
324
345
|
* ```
|
|
325
346
|
*/
|
|
326
347
|
insert(data: RequiredEntityData<Entity> | RequiredEntityData<Entity>[]): InsertQueryBuilder<Entity, RootAlias, Context>;
|
|
348
|
+
/**
|
|
349
|
+
* Creates an INSERT ... SELECT query that copies rows from the source query.
|
|
350
|
+
*
|
|
351
|
+
* Column resolution (3 tiers):
|
|
352
|
+
* 1. No explicit select on source, no explicit columns → all cloneable columns derived from entity metadata
|
|
353
|
+
* 2. Explicit select on source, no explicit columns → columns derived from selected field names
|
|
354
|
+
* 3. Explicit `columns` option → user-provided column list
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* ```ts
|
|
358
|
+
* // Clone all fields (columns auto-derived from metadata)
|
|
359
|
+
* const source = em.createQueryBuilder(User).where({ id: 1 });
|
|
360
|
+
* await em.createQueryBuilder(User).insertFrom(source).execute();
|
|
361
|
+
*
|
|
362
|
+
* // Clone with overrides via raw() aliases
|
|
363
|
+
* const source = em.createQueryBuilder(User)
|
|
364
|
+
* .select(['name', raw("'new@email.com'").as('email')])
|
|
365
|
+
* .where({ id: 1 });
|
|
366
|
+
* await em.createQueryBuilder(User).insertFrom(source).execute();
|
|
367
|
+
*
|
|
368
|
+
* // Explicit columns for full control
|
|
369
|
+
* await em.createQueryBuilder(User)
|
|
370
|
+
* .insertFrom(source, { columns: ['name', 'email'] })
|
|
371
|
+
* .execute();
|
|
372
|
+
* ```
|
|
373
|
+
*/
|
|
374
|
+
insertFrom(subQuery: QueryBuilder<any>, options?: {
|
|
375
|
+
columns?: Field<Entity, RootAlias, Context>[];
|
|
376
|
+
}): InsertQueryBuilder<Entity, RootAlias, Context>;
|
|
327
377
|
/**
|
|
328
378
|
* Creates an UPDATE query with the given data.
|
|
329
379
|
* Use `where()` to specify which rows to update.
|
|
@@ -644,6 +694,12 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
644
694
|
setFlag(flag: QueryFlag): this;
|
|
645
695
|
unsetFlag(flag: QueryFlag): this;
|
|
646
696
|
hasFlag(flag: QueryFlag): boolean;
|
|
697
|
+
/** @internal */
|
|
698
|
+
setPartitionLimit(opts: {
|
|
699
|
+
partitionBy: string;
|
|
700
|
+
limit: number;
|
|
701
|
+
offset?: number;
|
|
702
|
+
}): this;
|
|
647
703
|
cache(config?: boolean | number | [string, number]): this;
|
|
648
704
|
/**
|
|
649
705
|
* Adds index hint to the FROM clause.
|
|
@@ -877,6 +933,16 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
877
933
|
protected resolveNestedPath(field: string): string | string[];
|
|
878
934
|
protected init(type: QueryType, data?: any, cond?: any): this;
|
|
879
935
|
private getQueryBase;
|
|
936
|
+
/**
|
|
937
|
+
* Resolves the INSERT column list for `insertFrom()`.
|
|
938
|
+
*
|
|
939
|
+
* Tier 1: Explicit `insertColumns` from `options.columns` → map property names to field names
|
|
940
|
+
* Tier 2: Source QB has explicit select fields → derive from those
|
|
941
|
+
* Tier 3: Derive from target entity metadata (all cloneable columns), auto-populate source select
|
|
942
|
+
*/
|
|
943
|
+
private resolveInsertFromColumns;
|
|
944
|
+
/** Returns properties that are safe to clone (persistable, non-PK, non-generated). */
|
|
945
|
+
private getCloneableProps;
|
|
880
946
|
private applyDiscriminatorCondition;
|
|
881
947
|
/**
|
|
882
948
|
* Ensures TPT joins are applied. Can be called early before finalize() to populate
|
|
@@ -911,6 +977,17 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
911
977
|
private processNestedJoins;
|
|
912
978
|
private hasToManyJoins;
|
|
913
979
|
protected wrapPaginateSubQuery(meta: EntityMetadata): void;
|
|
980
|
+
/**
|
|
981
|
+
* Wraps the inner query (which has ROW_NUMBER in SELECT) with an outer query
|
|
982
|
+
* that filters by the __rn column to apply per-parent limiting.
|
|
983
|
+
*/
|
|
984
|
+
protected wrapPartitionLimitSubQuery(innerQb: NativeQueryBuilder): NativeQueryBuilder;
|
|
985
|
+
/**
|
|
986
|
+
* Adds ROW_NUMBER() OVER (PARTITION BY ...) to the SELECT list and prepares
|
|
987
|
+
* the query state for per-parent limiting. The actual wrapping into a subquery
|
|
988
|
+
* with __rn filtering happens in getNativeQuery().
|
|
989
|
+
*/
|
|
990
|
+
protected preparePartitionLimit(): void;
|
|
914
991
|
/**
|
|
915
992
|
* Computes the set of populate paths from the _populate hints.
|
|
916
993
|
*/
|
package/query/QueryBuilder.js
CHANGED
|
@@ -159,6 +159,41 @@ export class QueryBuilder {
|
|
|
159
159
|
insert(data) {
|
|
160
160
|
return this.init(QueryType.INSERT, data);
|
|
161
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Creates an INSERT ... SELECT query that copies rows from the source query.
|
|
164
|
+
*
|
|
165
|
+
* Column resolution (3 tiers):
|
|
166
|
+
* 1. No explicit select on source, no explicit columns → all cloneable columns derived from entity metadata
|
|
167
|
+
* 2. Explicit select on source, no explicit columns → columns derived from selected field names
|
|
168
|
+
* 3. Explicit `columns` option → user-provided column list
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* // Clone all fields (columns auto-derived from metadata)
|
|
173
|
+
* const source = em.createQueryBuilder(User).where({ id: 1 });
|
|
174
|
+
* await em.createQueryBuilder(User).insertFrom(source).execute();
|
|
175
|
+
*
|
|
176
|
+
* // Clone with overrides via raw() aliases
|
|
177
|
+
* const source = em.createQueryBuilder(User)
|
|
178
|
+
* .select(['name', raw("'new@email.com'").as('email')])
|
|
179
|
+
* .where({ id: 1 });
|
|
180
|
+
* await em.createQueryBuilder(User).insertFrom(source).execute();
|
|
181
|
+
*
|
|
182
|
+
* // Explicit columns for full control
|
|
183
|
+
* await em.createQueryBuilder(User)
|
|
184
|
+
* .insertFrom(source, { columns: ['name', 'email'] })
|
|
185
|
+
* .execute();
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
insertFrom(subQuery, options) {
|
|
189
|
+
this.ensureNotFinalized();
|
|
190
|
+
this.#state.type = QueryType.INSERT;
|
|
191
|
+
this.#state.insertSubQuery = subQuery;
|
|
192
|
+
if (options?.columns) {
|
|
193
|
+
this.#state.insertColumns = Utils.asArray(options.columns);
|
|
194
|
+
}
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
162
197
|
/**
|
|
163
198
|
* Creates an UPDATE query with the given data.
|
|
164
199
|
* Use `where()` to specify which rows to update.
|
|
@@ -686,6 +721,12 @@ export class QueryBuilder {
|
|
|
686
721
|
hasFlag(flag) {
|
|
687
722
|
return this.#state.flags.has(flag);
|
|
688
723
|
}
|
|
724
|
+
/** @internal */
|
|
725
|
+
setPartitionLimit(opts) {
|
|
726
|
+
this.ensureNotFinalized();
|
|
727
|
+
this.#state.partitionLimit = opts;
|
|
728
|
+
return this;
|
|
729
|
+
}
|
|
689
730
|
cache(config = true) {
|
|
690
731
|
this.ensureNotFinalized();
|
|
691
732
|
this.#state.cache = config;
|
|
@@ -789,12 +830,18 @@ export class QueryBuilder {
|
|
|
789
830
|
if (this.#state.lockMode) {
|
|
790
831
|
this.helper.getLockSQL(qb, this.#state.lockMode, this.#state.lockTables, this.#state.joins);
|
|
791
832
|
}
|
|
792
|
-
this.processReturningStatement(qb, this.mainAlias.meta, this.#state.data, this.#state.returning);
|
|
833
|
+
this.processReturningStatement(qb, this.mainAlias.meta, this.#state.insertSubQuery ? undefined : this.#state.data, this.#state.returning);
|
|
834
|
+
if (this.#state.partitionLimit) {
|
|
835
|
+
return (this.#query.qb = this.wrapPartitionLimitSubQuery(qb));
|
|
836
|
+
}
|
|
793
837
|
return (this.#query.qb = qb);
|
|
794
838
|
}
|
|
795
839
|
processReturningStatement(qb, meta, data, returning) {
|
|
796
840
|
const usesReturningStatement = this.platform.usesReturningStatement() || this.platform.usesOutputStatement();
|
|
797
|
-
if (!meta || !
|
|
841
|
+
if (!meta || !usesReturningStatement) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (!data && !this.#state.insertSubQuery) {
|
|
798
845
|
return;
|
|
799
846
|
}
|
|
800
847
|
// always respect explicit returning hint
|
|
@@ -805,13 +852,13 @@ export class QueryBuilder {
|
|
|
805
852
|
if (this.type === QueryType.INSERT) {
|
|
806
853
|
const returningProps = meta.hydrateProps
|
|
807
854
|
.filter(prop => prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw)))
|
|
808
|
-
.filter(prop => !(prop.name in data));
|
|
855
|
+
.filter(prop => !data || !(prop.name in data));
|
|
809
856
|
if (returningProps.length > 0) {
|
|
810
857
|
qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames)));
|
|
811
858
|
}
|
|
812
859
|
return;
|
|
813
860
|
}
|
|
814
|
-
if (this.type === QueryType.UPDATE) {
|
|
861
|
+
if (this.type === QueryType.UPDATE && data) {
|
|
815
862
|
const returningProps = meta.hydrateProps.filter(prop => prop.fieldNames && isRaw(data[prop.fieldNames[0]]));
|
|
816
863
|
if (returningProps.length > 0) {
|
|
817
864
|
qb.returning(returningProps.flatMap((prop) => {
|
|
@@ -1017,9 +1064,10 @@ export class QueryBuilder {
|
|
|
1017
1064
|
options ??= {};
|
|
1018
1065
|
options.mergeResults ??= true;
|
|
1019
1066
|
options.mapResults ??= true;
|
|
1067
|
+
const chunkSize = options.chunkSize ?? 100;
|
|
1020
1068
|
const query = this.toQuery();
|
|
1021
1069
|
const loggerContext = { id: this.em?.id, ...this.loggerContext };
|
|
1022
|
-
const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext);
|
|
1070
|
+
const res = this.getConnection().stream(query.sql, query.params, this.context, loggerContext, chunkSize);
|
|
1023
1071
|
const meta = this.mainAlias.meta;
|
|
1024
1072
|
if (options.rawResults || !meta) {
|
|
1025
1073
|
yield* res;
|
|
@@ -1603,7 +1651,14 @@ export class QueryBuilder {
|
|
|
1603
1651
|
break;
|
|
1604
1652
|
}
|
|
1605
1653
|
case QueryType.INSERT:
|
|
1606
|
-
|
|
1654
|
+
if (this.#state.insertSubQuery) {
|
|
1655
|
+
const columns = this.resolveInsertFromColumns();
|
|
1656
|
+
const compiled = this.#state.insertSubQuery.toQuery();
|
|
1657
|
+
qb.insertSelect(columns, raw(compiled.sql, compiled.params));
|
|
1658
|
+
}
|
|
1659
|
+
else {
|
|
1660
|
+
qb.insert(this.#state.data);
|
|
1661
|
+
}
|
|
1607
1662
|
break;
|
|
1608
1663
|
case QueryType.UPDATE:
|
|
1609
1664
|
qb.update(this.#state.data);
|
|
@@ -1619,6 +1674,78 @@ export class QueryBuilder {
|
|
|
1619
1674
|
}
|
|
1620
1675
|
return qb;
|
|
1621
1676
|
}
|
|
1677
|
+
/**
|
|
1678
|
+
* Resolves the INSERT column list for `insertFrom()`.
|
|
1679
|
+
*
|
|
1680
|
+
* Tier 1: Explicit `insertColumns` from `options.columns` → map property names to field names
|
|
1681
|
+
* Tier 2: Source QB has explicit select fields → derive from those
|
|
1682
|
+
* Tier 3: Derive from target entity metadata (all cloneable columns), auto-populate source select
|
|
1683
|
+
*/
|
|
1684
|
+
resolveInsertFromColumns() {
|
|
1685
|
+
const meta = this.mainAlias.meta;
|
|
1686
|
+
const subQuery = this.#state.insertSubQuery;
|
|
1687
|
+
// Tier 1: explicit columns
|
|
1688
|
+
if (this.#state.insertColumns?.length) {
|
|
1689
|
+
return this.#state.insertColumns.flatMap(col => {
|
|
1690
|
+
const prop = meta?.properties[col];
|
|
1691
|
+
return prop?.fieldNames ?? [col];
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
// Tier 2: source QB has explicit select fields
|
|
1695
|
+
const sourceFields = subQuery.state.fields;
|
|
1696
|
+
if (sourceFields && subQuery.state.type === QueryType.SELECT) {
|
|
1697
|
+
return sourceFields
|
|
1698
|
+
.filter((field) => typeof field === 'string' || isRaw(field))
|
|
1699
|
+
.flatMap((field) => {
|
|
1700
|
+
if (typeof field === 'string') {
|
|
1701
|
+
// Strip alias prefix like 'a0.'
|
|
1702
|
+
const bare = field.replace(/^\w+\./, '');
|
|
1703
|
+
const prop = meta?.properties[bare];
|
|
1704
|
+
return prop?.fieldNames ?? [bare];
|
|
1705
|
+
}
|
|
1706
|
+
// RawQueryFragment with alias: raw('...').as('name')
|
|
1707
|
+
const alias = String(field.params[field.params.length - 1]);
|
|
1708
|
+
const prop = meta?.properties[alias];
|
|
1709
|
+
return prop?.fieldNames ?? [alias];
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
// Tier 3: derive from metadata — all cloneable columns
|
|
1713
|
+
const cloneableProps = this.getCloneableProps(meta);
|
|
1714
|
+
const selectFields = [];
|
|
1715
|
+
const columns = [];
|
|
1716
|
+
for (const prop of cloneableProps) {
|
|
1717
|
+
for (const fieldName of prop.fieldNames) {
|
|
1718
|
+
columns.push(fieldName);
|
|
1719
|
+
selectFields.push(fieldName);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
// Auto-populate source select with matching fields
|
|
1723
|
+
if (!sourceFields) {
|
|
1724
|
+
subQuery.select(selectFields);
|
|
1725
|
+
}
|
|
1726
|
+
return columns;
|
|
1727
|
+
}
|
|
1728
|
+
/** Returns properties that are safe to clone (persistable, non-PK, non-generated). */
|
|
1729
|
+
getCloneableProps(meta) {
|
|
1730
|
+
return meta.props.filter(prop => {
|
|
1731
|
+
if (prop.persist === false) {
|
|
1732
|
+
return false;
|
|
1733
|
+
}
|
|
1734
|
+
if (prop.primary) {
|
|
1735
|
+
return false;
|
|
1736
|
+
}
|
|
1737
|
+
if (!prop.fieldNames?.length) {
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
1744
|
+
return false;
|
|
1745
|
+
}
|
|
1746
|
+
return true;
|
|
1747
|
+
});
|
|
1748
|
+
}
|
|
1622
1749
|
applyDiscriminatorCondition() {
|
|
1623
1750
|
const meta = this.mainAlias.meta;
|
|
1624
1751
|
if (meta.root.inheritanceType !== 'sti' || !meta.discriminatorValue) {
|
|
@@ -1782,6 +1909,9 @@ export class QueryBuilder {
|
|
|
1782
1909
|
(this.#state.limit > 0 || this.#state.offset > 0)) {
|
|
1783
1910
|
this.wrapPaginateSubQuery(meta);
|
|
1784
1911
|
}
|
|
1912
|
+
if (this.#state.partitionLimit) {
|
|
1913
|
+
this.preparePartitionLimit();
|
|
1914
|
+
}
|
|
1785
1915
|
if (meta &&
|
|
1786
1916
|
(this.#state.flags.has(QueryFlag.UPDATE_SUB_QUERY) || this.#state.flags.has(QueryFlag.DELETE_SUB_QUERY))) {
|
|
1787
1917
|
this.wrapModifySubQuery(meta);
|
|
@@ -2001,6 +2131,40 @@ export class QueryBuilder {
|
|
|
2001
2131
|
[Utils.getPrimaryKeyHash(meta.primaryKeys)]: { $in: raw(sql, params) },
|
|
2002
2132
|
});
|
|
2003
2133
|
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Wraps the inner query (which has ROW_NUMBER in SELECT) with an outer query
|
|
2136
|
+
* that filters by the __rn column to apply per-parent limiting.
|
|
2137
|
+
*/
|
|
2138
|
+
wrapPartitionLimitSubQuery(innerQb) {
|
|
2139
|
+
const { limit, offset = 0 } = this.#state.partitionLimit;
|
|
2140
|
+
const rnCol = this.platform.quoteIdentifier('__rn');
|
|
2141
|
+
innerQb.as(this.mainAlias.aliasName);
|
|
2142
|
+
const outerQb = this.platform.createNativeQueryBuilder();
|
|
2143
|
+
outerQb.select('*').from(innerQb);
|
|
2144
|
+
outerQb.where(`${rnCol} > ? and ${rnCol} <= ?`, [offset, offset + limit]);
|
|
2145
|
+
outerQb.orderBy(rnCol);
|
|
2146
|
+
return outerQb;
|
|
2147
|
+
}
|
|
2148
|
+
/**
|
|
2149
|
+
* Adds ROW_NUMBER() OVER (PARTITION BY ...) to the SELECT list and prepares
|
|
2150
|
+
* the query state for per-parent limiting. The actual wrapping into a subquery
|
|
2151
|
+
* with __rn filtering happens in getNativeQuery().
|
|
2152
|
+
*/
|
|
2153
|
+
preparePartitionLimit() {
|
|
2154
|
+
const { partitionBy } = this.#state.partitionLimit;
|
|
2155
|
+
// `partitionBy` is always a declared property name, so mapper returns a string here.
|
|
2156
|
+
const partitionCol = this.helper.mapper(partitionBy, this.type, undefined, null);
|
|
2157
|
+
const quotedPartition = partitionCol
|
|
2158
|
+
.split('.')
|
|
2159
|
+
.map(e => this.platform.quoteIdentifier(e))
|
|
2160
|
+
.join('.');
|
|
2161
|
+
const queryOrder = this.helper.getQueryOrder(this.type, this.#state.orderBy, this.#state.populateMap, this.#state.collation);
|
|
2162
|
+
const orderBySql = queryOrder.length > 0 ? Utils.unique(queryOrder).join(', ') : quotedPartition;
|
|
2163
|
+
const rnAlias = this.platform.quoteIdentifier('__rn');
|
|
2164
|
+
this.#state.fields.push(raw(`row_number() over (partition by ${quotedPartition} order by ${orderBySql}) as ${rnAlias}`));
|
|
2165
|
+
// Moved into the OVER clause; outer query re-applies via wrapPartitionLimitSubQuery
|
|
2166
|
+
this.#state.orderBy = [];
|
|
2167
|
+
}
|
|
2004
2168
|
/**
|
|
2005
2169
|
* Computes the set of populate paths from the _populate hints.
|
|
2006
2170
|
*/
|