@mikro-orm/sql 7.1.0-dev.1 → 7.1.0-dev.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AbstractSqlDriver.d.ts +6 -0
- package/AbstractSqlDriver.js +103 -0
- package/dialects/mssql/MsSqlNativeQueryBuilder.js +4 -0
- package/dialects/mysql/MySqlNativeQueryBuilder.js +11 -0
- package/package.json +2 -2
- package/query/NativeQueryBuilder.d.ts +6 -0
- package/query/NativeQueryBuilder.js +16 -1
- package/query/QueryBuilder.d.ts +41 -0
- package/query/QueryBuilder.js +122 -5
package/AbstractSqlDriver.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
|
|
|
52
52
|
private mapJoinedProp;
|
|
53
53
|
count<T extends object>(entityName: EntityName<T>, where: any, options?: CountOptions<T>): Promise<number>;
|
|
54
54
|
nativeInsert<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
|
|
55
|
+
nativeClone<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, overrides?: EntityData<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
|
|
56
|
+
private nativeCloneSimple;
|
|
57
|
+
private nativeCloneTPT;
|
|
58
|
+
private mapCloneOverrides;
|
|
59
|
+
private buildCloneFields;
|
|
60
|
+
private getCloneableProps;
|
|
55
61
|
nativeInsertMany<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
|
|
56
62
|
nativeUpdate<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T> & UpsertOptions<T>): Promise<QueryResult<T>>;
|
|
57
63
|
nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T> & UpsertManyOptions<T>, transform?: (sql: string, params: any[]) => string): Promise<QueryResult<T>>;
|
package/AbstractSqlDriver.js
CHANGED
|
@@ -574,6 +574,109 @@ export class AbstractSqlDriver extends DatabaseDriver {
|
|
|
574
574
|
await this.processManyToMany(meta, pk, collections, false, options);
|
|
575
575
|
return res;
|
|
576
576
|
}
|
|
577
|
+
async nativeClone(entityName, where, overrides, options = {}) {
|
|
578
|
+
options.convertCustomTypes ??= true;
|
|
579
|
+
const meta = this.metadata.get(entityName);
|
|
580
|
+
if (meta.inheritanceType === 'tpt' || meta.tptParent) {
|
|
581
|
+
return this.nativeCloneTPT(meta, where, overrides, options);
|
|
582
|
+
}
|
|
583
|
+
return this.nativeCloneSimple(meta, where, overrides, options);
|
|
584
|
+
}
|
|
585
|
+
async nativeCloneSimple(meta, where, overrides, options = {}) {
|
|
586
|
+
const props = this.getCloneableProps(meta);
|
|
587
|
+
const mappedOverrides = this.mapCloneOverrides(overrides, meta, options);
|
|
588
|
+
const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, meta);
|
|
589
|
+
const selectQb = this.createQueryBuilder(meta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
590
|
+
selectQb.select(selectFields).where(where);
|
|
591
|
+
const insertQb = this.createQueryBuilder(meta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
|
|
592
|
+
return this.rethrow(insertQb
|
|
593
|
+
.insertFrom(selectQb, { columns: insertColumns })
|
|
594
|
+
.execute('run', false));
|
|
595
|
+
}
|
|
596
|
+
async nativeCloneTPT(leafMeta, where, overrides, options = {}) {
|
|
597
|
+
const hierarchy = [];
|
|
598
|
+
let current = leafMeta;
|
|
599
|
+
while (current) {
|
|
600
|
+
hierarchy.unshift(current);
|
|
601
|
+
current = current.tptParent;
|
|
602
|
+
}
|
|
603
|
+
const rootMeta = hierarchy[0];
|
|
604
|
+
let newPk;
|
|
605
|
+
let rootResult;
|
|
606
|
+
for (const tableMeta of hierarchy) {
|
|
607
|
+
const props = this.getCloneableProps(tableMeta, true);
|
|
608
|
+
const mappedOverrides = this.mapCloneOverrides(overrides, tableMeta, options);
|
|
609
|
+
const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, tableMeta);
|
|
610
|
+
// For child tables, prepend the new PK value
|
|
611
|
+
if (tableMeta !== rootMeta && newPk != null) {
|
|
612
|
+
for (const pkName of tableMeta.primaryKeys) {
|
|
613
|
+
const prop = tableMeta.properties[pkName];
|
|
614
|
+
for (const fieldName of prop.fieldNames) {
|
|
615
|
+
insertColumns.unshift(fieldName);
|
|
616
|
+
selectFields.unshift(raw('? as ??', [newPk, fieldName]));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const sourceWhere = tableMeta === rootMeta ? where : (Utils.extractPK(where, tableMeta) ?? where);
|
|
621
|
+
const selectQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
|
|
622
|
+
selectQb.select(selectFields).where(sourceWhere);
|
|
623
|
+
const insertQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
|
|
624
|
+
const res = await this.rethrow(insertQb
|
|
625
|
+
.insertFrom(selectQb, { columns: insertColumns })
|
|
626
|
+
.execute('run', false));
|
|
627
|
+
if (tableMeta === rootMeta) {
|
|
628
|
+
rootResult = res;
|
|
629
|
+
newPk = res.insertId ?? res.row?.[rootMeta.primaryKeys[0]];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return rootResult;
|
|
633
|
+
}
|
|
634
|
+
mapCloneOverrides(overrides, meta, options) {
|
|
635
|
+
if (!overrides) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
return super.mapDataToFieldNames(overrides, true, meta.properties, options.convertCustomTypes);
|
|
639
|
+
}
|
|
640
|
+
buildCloneFields(props, mappedOverrides, meta) {
|
|
641
|
+
const selectFields = [];
|
|
642
|
+
const insertColumns = [];
|
|
643
|
+
for (const prop of props) {
|
|
644
|
+
for (const fieldName of prop.fieldNames) {
|
|
645
|
+
insertColumns.push(fieldName);
|
|
646
|
+
if (mappedOverrides && fieldName in mappedOverrides) {
|
|
647
|
+
selectFields.push(raw('? as ??', [mappedOverrides[fieldName], fieldName]));
|
|
648
|
+
}
|
|
649
|
+
else if (meta.versionProperty === prop.name) {
|
|
650
|
+
const initial = prop.runtimeType === 'Date' ? new Date() : 1;
|
|
651
|
+
selectFields.push(raw('? as ??', [initial, fieldName]));
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
selectFields.push(fieldName);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return { selectFields, insertColumns };
|
|
659
|
+
}
|
|
660
|
+
getCloneableProps(meta, ownProps) {
|
|
661
|
+
return (ownProps ? (meta.ownProps ?? meta.props) : meta.props).filter(prop => {
|
|
662
|
+
if (prop.persist === false) {
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
if (prop.primary) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
if (!prop.fieldNames?.length) {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
|
|
672
|
+
return false;
|
|
673
|
+
}
|
|
674
|
+
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
return true;
|
|
678
|
+
});
|
|
679
|
+
}
|
|
577
680
|
async nativeInsertMany(entityName, data, options = {}, transform) {
|
|
578
681
|
options.processCollections ??= true;
|
|
579
682
|
options.convertCustomTypes ??= true;
|
|
@@ -56,6 +56,10 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
|
|
|
56
56
|
return this.combineParts();
|
|
57
57
|
}
|
|
58
58
|
compileInsert() {
|
|
59
|
+
if (this.options.insertSubQuery) {
|
|
60
|
+
super.compileInsert();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
59
63
|
if (!this.options.data) {
|
|
60
64
|
throw new Error('No data provided');
|
|
61
65
|
}
|
|
@@ -3,6 +3,17 @@ import { NativeQueryBuilder } from '../../query/NativeQueryBuilder.js';
|
|
|
3
3
|
/** @internal */
|
|
4
4
|
export class MySqlNativeQueryBuilder extends NativeQueryBuilder {
|
|
5
5
|
compileInsert() {
|
|
6
|
+
if (this.options.insertSubQuery) {
|
|
7
|
+
super.compileInsert();
|
|
8
|
+
// Inject 'ignore' after 'insert' for MySQL's INSERT IGNORE ... SELECT syntax
|
|
9
|
+
if (this.options.onConflict?.ignore) {
|
|
10
|
+
const insertIdx = this.parts.indexOf('insert');
|
|
11
|
+
if (insertIdx >= 0) {
|
|
12
|
+
this.parts.splice(insertIdx + 1, 0, 'ignore');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
6
17
|
if (!this.options.data) {
|
|
7
18
|
throw new Error('No data provided');
|
|
8
19
|
}
|
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.3",
|
|
4
4
|
"description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"data-mapper",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@mikro-orm/core": "^7.0.11"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@mikro-orm/core": "7.1.0-dev.
|
|
56
|
+
"@mikro-orm/core": "7.1.0-dev.3"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">= 22.17.0"
|
|
@@ -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');
|
package/query/QueryBuilder.d.ts
CHANGED
|
@@ -158,6 +158,8 @@ export interface QBState<Entity extends object> {
|
|
|
158
158
|
schema?: string;
|
|
159
159
|
cond: Dictionary;
|
|
160
160
|
data?: Dictionary;
|
|
161
|
+
insertSubQuery?: QueryBuilder<any>;
|
|
162
|
+
insertColumns?: string[];
|
|
161
163
|
orderBy: QueryOrderMap<Entity>[];
|
|
162
164
|
groupBy: InternalField<Entity>[];
|
|
163
165
|
having: Dictionary;
|
|
@@ -324,6 +326,35 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
324
326
|
* ```
|
|
325
327
|
*/
|
|
326
328
|
insert(data: RequiredEntityData<Entity> | RequiredEntityData<Entity>[]): InsertQueryBuilder<Entity, RootAlias, Context>;
|
|
329
|
+
/**
|
|
330
|
+
* Creates an INSERT ... SELECT query that copies rows from the source query.
|
|
331
|
+
*
|
|
332
|
+
* Column resolution (3 tiers):
|
|
333
|
+
* 1. No explicit select on source, no explicit columns → all cloneable columns derived from entity metadata
|
|
334
|
+
* 2. Explicit select on source, no explicit columns → columns derived from selected field names
|
|
335
|
+
* 3. Explicit `columns` option → user-provided column list
|
|
336
|
+
*
|
|
337
|
+
* @example
|
|
338
|
+
* ```ts
|
|
339
|
+
* // Clone all fields (columns auto-derived from metadata)
|
|
340
|
+
* const source = em.createQueryBuilder(User).where({ id: 1 });
|
|
341
|
+
* await em.createQueryBuilder(User).insertFrom(source).execute();
|
|
342
|
+
*
|
|
343
|
+
* // Clone with overrides via raw() aliases
|
|
344
|
+
* const source = em.createQueryBuilder(User)
|
|
345
|
+
* .select(['name', raw("'new@email.com'").as('email')])
|
|
346
|
+
* .where({ id: 1 });
|
|
347
|
+
* await em.createQueryBuilder(User).insertFrom(source).execute();
|
|
348
|
+
*
|
|
349
|
+
* // Explicit columns for full control
|
|
350
|
+
* await em.createQueryBuilder(User)
|
|
351
|
+
* .insertFrom(source, { columns: ['name', 'email'] })
|
|
352
|
+
* .execute();
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
insertFrom(subQuery: QueryBuilder<any>, options?: {
|
|
356
|
+
columns?: Field<Entity, RootAlias, Context>[];
|
|
357
|
+
}): InsertQueryBuilder<Entity, RootAlias, Context>;
|
|
327
358
|
/**
|
|
328
359
|
* Creates an UPDATE query with the given data.
|
|
329
360
|
* Use `where()` to specify which rows to update.
|
|
@@ -877,6 +908,16 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
|
|
|
877
908
|
protected resolveNestedPath(field: string): string | string[];
|
|
878
909
|
protected init(type: QueryType, data?: any, cond?: any): this;
|
|
879
910
|
private getQueryBase;
|
|
911
|
+
/**
|
|
912
|
+
* Resolves the INSERT column list for `insertFrom()`.
|
|
913
|
+
*
|
|
914
|
+
* Tier 1: Explicit `insertColumns` from `options.columns` → map property names to field names
|
|
915
|
+
* Tier 2: Source QB has explicit select fields → derive from those
|
|
916
|
+
* Tier 3: Derive from target entity metadata (all cloneable columns), auto-populate source select
|
|
917
|
+
*/
|
|
918
|
+
private resolveInsertFromColumns;
|
|
919
|
+
/** Returns properties that are safe to clone (persistable, non-PK, non-generated). */
|
|
920
|
+
private getCloneableProps;
|
|
880
921
|
private applyDiscriminatorCondition;
|
|
881
922
|
/**
|
|
882
923
|
* Ensures TPT joins are applied. Can be called early before finalize() to populate
|
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.
|
|
@@ -789,12 +824,15 @@ export class QueryBuilder {
|
|
|
789
824
|
if (this.#state.lockMode) {
|
|
790
825
|
this.helper.getLockSQL(qb, this.#state.lockMode, this.#state.lockTables, this.#state.joins);
|
|
791
826
|
}
|
|
792
|
-
this.processReturningStatement(qb, this.mainAlias.meta, this.#state.data, this.#state.returning);
|
|
827
|
+
this.processReturningStatement(qb, this.mainAlias.meta, this.#state.insertSubQuery ? undefined : this.#state.data, this.#state.returning);
|
|
793
828
|
return (this.#query.qb = qb);
|
|
794
829
|
}
|
|
795
830
|
processReturningStatement(qb, meta, data, returning) {
|
|
796
831
|
const usesReturningStatement = this.platform.usesReturningStatement() || this.platform.usesOutputStatement();
|
|
797
|
-
if (!meta || !
|
|
832
|
+
if (!meta || !usesReturningStatement) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (!data && !this.#state.insertSubQuery) {
|
|
798
836
|
return;
|
|
799
837
|
}
|
|
800
838
|
// always respect explicit returning hint
|
|
@@ -805,13 +843,13 @@ export class QueryBuilder {
|
|
|
805
843
|
if (this.type === QueryType.INSERT) {
|
|
806
844
|
const returningProps = meta.hydrateProps
|
|
807
845
|
.filter(prop => prop.returning || (prop.persist !== false && ((prop.primary && prop.autoincrement) || prop.defaultRaw)))
|
|
808
|
-
.filter(prop => !(prop.name in data));
|
|
846
|
+
.filter(prop => !data || !(prop.name in data));
|
|
809
847
|
if (returningProps.length > 0) {
|
|
810
848
|
qb.returning(Utils.flatten(returningProps.map(prop => prop.fieldNames)));
|
|
811
849
|
}
|
|
812
850
|
return;
|
|
813
851
|
}
|
|
814
|
-
if (this.type === QueryType.UPDATE) {
|
|
852
|
+
if (this.type === QueryType.UPDATE && data) {
|
|
815
853
|
const returningProps = meta.hydrateProps.filter(prop => prop.fieldNames && isRaw(data[prop.fieldNames[0]]));
|
|
816
854
|
if (returningProps.length > 0) {
|
|
817
855
|
qb.returning(returningProps.flatMap((prop) => {
|
|
@@ -1603,7 +1641,14 @@ export class QueryBuilder {
|
|
|
1603
1641
|
break;
|
|
1604
1642
|
}
|
|
1605
1643
|
case QueryType.INSERT:
|
|
1606
|
-
|
|
1644
|
+
if (this.#state.insertSubQuery) {
|
|
1645
|
+
const columns = this.resolveInsertFromColumns();
|
|
1646
|
+
const compiled = this.#state.insertSubQuery.toQuery();
|
|
1647
|
+
qb.insertSelect(columns, raw(compiled.sql, compiled.params));
|
|
1648
|
+
}
|
|
1649
|
+
else {
|
|
1650
|
+
qb.insert(this.#state.data);
|
|
1651
|
+
}
|
|
1607
1652
|
break;
|
|
1608
1653
|
case QueryType.UPDATE:
|
|
1609
1654
|
qb.update(this.#state.data);
|
|
@@ -1619,6 +1664,78 @@ export class QueryBuilder {
|
|
|
1619
1664
|
}
|
|
1620
1665
|
return qb;
|
|
1621
1666
|
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Resolves the INSERT column list for `insertFrom()`.
|
|
1669
|
+
*
|
|
1670
|
+
* Tier 1: Explicit `insertColumns` from `options.columns` → map property names to field names
|
|
1671
|
+
* Tier 2: Source QB has explicit select fields → derive from those
|
|
1672
|
+
* Tier 3: Derive from target entity metadata (all cloneable columns), auto-populate source select
|
|
1673
|
+
*/
|
|
1674
|
+
resolveInsertFromColumns() {
|
|
1675
|
+
const meta = this.mainAlias.meta;
|
|
1676
|
+
const subQuery = this.#state.insertSubQuery;
|
|
1677
|
+
// Tier 1: explicit columns
|
|
1678
|
+
if (this.#state.insertColumns?.length) {
|
|
1679
|
+
return this.#state.insertColumns.flatMap(col => {
|
|
1680
|
+
const prop = meta?.properties[col];
|
|
1681
|
+
return prop?.fieldNames ?? [col];
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
// Tier 2: source QB has explicit select fields
|
|
1685
|
+
const sourceFields = subQuery.state.fields;
|
|
1686
|
+
if (sourceFields && subQuery.state.type === QueryType.SELECT) {
|
|
1687
|
+
return sourceFields
|
|
1688
|
+
.filter((field) => typeof field === 'string' || isRaw(field))
|
|
1689
|
+
.flatMap((field) => {
|
|
1690
|
+
if (typeof field === 'string') {
|
|
1691
|
+
// Strip alias prefix like 'a0.'
|
|
1692
|
+
const bare = field.replace(/^\w+\./, '');
|
|
1693
|
+
const prop = meta?.properties[bare];
|
|
1694
|
+
return prop?.fieldNames ?? [bare];
|
|
1695
|
+
}
|
|
1696
|
+
// RawQueryFragment with alias: raw('...').as('name')
|
|
1697
|
+
const alias = String(field.params[field.params.length - 1]);
|
|
1698
|
+
const prop = meta?.properties[alias];
|
|
1699
|
+
return prop?.fieldNames ?? [alias];
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
// Tier 3: derive from metadata — all cloneable columns
|
|
1703
|
+
const cloneableProps = this.getCloneableProps(meta);
|
|
1704
|
+
const selectFields = [];
|
|
1705
|
+
const columns = [];
|
|
1706
|
+
for (const prop of cloneableProps) {
|
|
1707
|
+
for (const fieldName of prop.fieldNames) {
|
|
1708
|
+
columns.push(fieldName);
|
|
1709
|
+
selectFields.push(fieldName);
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
// Auto-populate source select with matching fields
|
|
1713
|
+
if (!sourceFields) {
|
|
1714
|
+
subQuery.select(selectFields);
|
|
1715
|
+
}
|
|
1716
|
+
return columns;
|
|
1717
|
+
}
|
|
1718
|
+
/** Returns properties that are safe to clone (persistable, non-PK, non-generated). */
|
|
1719
|
+
getCloneableProps(meta) {
|
|
1720
|
+
return meta.props.filter(prop => {
|
|
1721
|
+
if (prop.persist === false) {
|
|
1722
|
+
return false;
|
|
1723
|
+
}
|
|
1724
|
+
if (prop.primary) {
|
|
1725
|
+
return false;
|
|
1726
|
+
}
|
|
1727
|
+
if (!prop.fieldNames?.length) {
|
|
1728
|
+
return false;
|
|
1729
|
+
}
|
|
1730
|
+
if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
return true;
|
|
1737
|
+
});
|
|
1738
|
+
}
|
|
1622
1739
|
applyDiscriminatorCondition() {
|
|
1623
1740
|
const meta = this.mainAlias.meta;
|
|
1624
1741
|
if (meta.root.inheritanceType !== 'sti' || !meta.discriminatorValue) {
|