Skip to content

Commit

Permalink
fix(core): fix mapping of inserted PKs with custom field names from b…
Browse files Browse the repository at this point in the history
…atch insert

Closes #2977
  • Loading branch information
B4nan committed Mar 31, 2022
1 parent 6e5166b commit 080d8e0
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 21 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/drivers/DatabaseDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export abstract class DatabaseDriver<C extends Connection> implements IDatabaseD

protected getPrimaryKeyFields(entityName: string): string[] {
const meta = this.metadata.find(entityName);
return meta ? meta.primaryKeys : [this.config.getNamingStrategy().referenceColumnName()];
return meta ? Utils.flatten(meta.getPrimaryProps().map(pk => pk.fieldNames)) : [this.config.getNamingStrategy().referenceColumnName()];
}

protected getPivotInverseProperty(prop: EntityProperty): EntityProperty {
Expand Down
40 changes: 20 additions & 20 deletions packages/knex/src/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
options.convertCustomTypes ??= true;
const meta = this.metadata.find<T>(entityName)!;
const collections = this.extractManyToMany(entityName, data);
const pks = this.getPrimaryKeyFields(entityName);
const pks = meta?.primaryKeys ?? [this.config.getNamingStrategy().referenceColumnName()];
const qb = this.createQueryBuilder<T>(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
const res = await this.rethrow(qb.insert(data as unknown as RequiredEntityData<T>).execute('run', false));
res.row = res.row || {};
Expand Down Expand Up @@ -230,17 +230,18 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
data.forEach(row => Object.keys(row).forEach(k => set.add(k)));
const props = [...set].map(name => meta.properties[name] ?? { name, fieldNames: [name] }) as EntityProperty<T>[];
const fields = Utils.flatten(props.map(prop => prop.fieldNames));
let res: QueryResult<T>;
const params: unknown[] = [];

let sql = `insert into ${this.getTableName(meta, options)} `;
sql += fields.length > 0 ? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')' : `(${this.platform.quoteIdentifier(pks[0])})`;

if (fields.length === 0) {
const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes).withSchema(this.getSchemaName(meta, options));
res = await this.rethrow(qb.insert(data as unknown as RequiredEntityData<T>[]).execute('run', false));
if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
sql += ' values ';
} else {
let sql = `insert into ${this.getTableName(meta, options)} `;
/* istanbul ignore next */
sql += fields.length > 0 ? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')' : 'default';
sql += ` values `;
const params: any[] = [];
sql += ' ' + data.map(() => `select null as ${this.platform.quoteIdentifier(pks[0])}`).join(' union all ');
}

if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
sql += data.map(row => {
const keys: string[] = [];
props.forEach(prop => {
Expand All @@ -256,20 +257,19 @@ export abstract class AbstractSqlDriver<C extends AbstractSqlConnection = Abstra
}
});

return '(' + keys.join(', ') + ')';
return '(' + (keys.join(', ') || 'default') + ')';
}).join(', ');
}

if (this.platform.usesReturningStatement()) {
/* istanbul ignore next */
const returningProps = meta!.props.filter(prop => prop.primary || prop.defaultRaw);
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
/* istanbul ignore next */
sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
}

res = await this.execute(sql, params, 'run', options.ctx);
if (this.platform.usesReturningStatement()) {
/* istanbul ignore next */
const returningProps = meta!.props.filter(prop => prop.primary || prop.defaultRaw);
const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
/* istanbul ignore next */
sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
}

const res = await this.execute<QueryResult<T>>(sql, params, 'run', options.ctx);
let pk: any[];

/* istanbul ignore next */
Expand Down
1 change: 1 addition & 0 deletions packages/knex/src/query/QueryBuilderHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ export class QueryBuilderHelper {
});

if (!Utils.hasObjectKeys(data) && meta && multi) {
/* istanbul ignore next */
data[meta.primaryKeys[0]] = this.platform.usesDefaultKeyword() ? this.knex.raw('default') : undefined;
}

Expand Down
102 changes: 102 additions & 0 deletions tests/features/batch-insert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { MikroORM, Entity, PrimaryKey, ManyToOne } from '@mikro-orm/core';

@Entity()
export class Author {

@PrimaryKey({ name: 'author_id' })
id!: number;

}

@Entity()
export class Book {

@PrimaryKey({ name: 'book_id' })
id!: number;

@ManyToOne(() => Author)
author!: Author;

}

test('batch insert and mapping of PKs with custom field name [sqlite]', async () => {
const orm = await MikroORM.init({
entities: [Author, Book],
dbName: ':memory:',
type: 'sqlite',
});
await orm.getSchemaGenerator().refreshDatabase();
const authors = [new Author(), new Author(), new Author()];
const books = [new Book(), new Book(), new Book()];
books.forEach((b, idx) => b.author = authors[idx]);
await orm.em.persist(books).flush();
expect(authors.map(a => a.id)).toEqual([1, 2, 3]);
expect(books.map(b => b.id)).toEqual([1, 2, 3]);
await orm.close();
});

test('batch insert and mapping of PKs with custom field name [better-sqlite]', async () => {
const orm = await MikroORM.init({
entities: [Author, Book],
dbName: ':memory:',
type: 'better-sqlite',
});
await orm.getSchemaGenerator().refreshDatabase();
const authors = [new Author(), new Author(), new Author()];
const books = [new Book(), new Book(), new Book()];
books.forEach((b, idx) => b.author = authors[idx]);
await orm.em.persist(books).flush();
expect(authors.map(a => a.id)).toEqual([1, 2, 3]);
expect(books.map(b => b.id)).toEqual([1, 2, 3]);
await orm.close();
});

test('batch insert and mapping of PKs with custom field name [postgres]', async () => {
const orm = await MikroORM.init({
entities: [Author, Book],
dbName: 'mikro_orm_test_2977',
type: 'postgresql',
});
await orm.getSchemaGenerator().refreshDatabase();
const authors = [new Author(), new Author(), new Author()];
const books = [new Book(), new Book(), new Book()];
books.forEach((b, idx) => b.author = authors[idx]);
await orm.em.persist(books).flush();
expect(authors.map(a => a.id)).toEqual([1, 2, 3]);
expect(books.map(b => b.id)).toEqual([1, 2, 3]);
await orm.close();
});

test('batch insert and mapping of PKs with custom field name [mysql]', async () => {
const orm = await MikroORM.init({
entities: [Author, Book],
dbName: 'mikro_orm_test_2977',
type: 'mysql',
port: 3308,
});
await orm.getSchemaGenerator().refreshDatabase();
const authors = [new Author(), new Author(), new Author()];
const books = [new Book(), new Book(), new Book()];
books.forEach((b, idx) => b.author = authors[idx]);
await orm.em.persist(books).flush();
expect(authors.map(a => a.id)).toEqual([1, 2, 3]);
expect(books.map(b => b.id)).toEqual([1, 2, 3]);
await orm.close();
});

test('batch insert and mapping of PKs with custom field name [mariadb]', async () => {
const orm = await MikroORM.init({
entities: [Author, Book],
dbName: 'mikro_orm_test_2977',
type: 'mariadb',
port: 3309,
});
await orm.getSchemaGenerator().refreshDatabase();
const authors = [new Author(), new Author(), new Author()];
const books = [new Book(), new Book(), new Book()];
books.forEach((b, idx) => b.author = authors[idx]);
await orm.em.persist(books).flush();
expect(authors.map(a => a.id)).toEqual([1, 2, 3]);
expect(books.map(b => b.id)).toEqual([1, 2, 3]);
await orm.close();
});

0 comments on commit 080d8e0

Please sign in to comment.