@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.
@@ -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>>;
@@ -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.1",
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.1"
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');
@@ -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
@@ -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 || !data || !usesReturningStatement) {
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
- qb.insert(this.#state.data);
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) {