Skip to content

Commit

Permalink
fix(core): fix returning statement hydration after em.upsert
Browse files Browse the repository at this point in the history
Closes #4945
  • Loading branch information
B4nan committed Nov 26, 2023
1 parent d80f0aa commit a7e9a82
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 14 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/EntityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,7 +703,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where) ? Object.keys(where) : meta!.primaryKeys) as (keyof Entity)[];
const returning = getOnConflictReturningFields(meta, data, uniqueFields, options) as string[];

if (options.onConflictAction === 'ignore' || !helper(entity).hasPrimaryKey() || (returning.length > 0 && !this.getPlatform().usesReturningStatement())) {
if (options.onConflictAction === 'ignore' || !helper(entity).hasPrimaryKey() || (returning.length > 0 && !(this.getPlatform().usesReturningStatement() && ret.row))) {
const where = {} as FilterQuery<Entity>;
uniqueFields.forEach(prop => where[prop as string] = data![prop as string]);
const data2 = await this.driver.findOne(meta.className, where, {
Expand Down Expand Up @@ -895,7 +895,7 @@ export class EntityManager<D extends IDatabaseDriver = IDatabaseDriver> {
// skip if we got the PKs via returning statement (`rows`)
const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(allWhere![0]) ? Object.keys(allWhere![0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta!.primaryKeys) as (keyof Entity)[];
const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options) as string[];
const reloadFields = returning.length > 0 && !this.getPlatform().usesReturningStatement();
const reloadFields = returning.length > 0 && !(this.getPlatform().usesReturningStatement() && res.rows?.length);

if (options.onConflictAction === 'ignore' || (!res.rows?.length && loadPK.size > 0) || reloadFields) {
const unique = meta.hydrateProps.filter(p => !p.lazy).map(p => p.name);
Expand Down
16 changes: 4 additions & 12 deletions packages/core/src/unit-of-work/ChangeSetPersister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ReferenceType } from '../enums';
export class ChangeSetPersister {

private readonly platform = this.driver.getPlatform();
private readonly comparator = this.config.getComparator(this.metadata);

constructor(private readonly driver: IDatabaseDriver,
private readonly metadata: MetadataStorage,
Expand Down Expand Up @@ -380,18 +381,9 @@ export class ChangeSetPersister {
*/
mapReturnedValues<T extends object>(entity: T, payload: EntityDictionary<T>, row: Dictionary | undefined, meta: EntityMetadata<T>, override = false): void {
if (this.platform.usesReturningStatement() && row && Utils.hasObjectKeys(row)) {
const data = meta.props.reduce((ret, prop) => {
if (prop.fieldNames && row[prop.fieldNames[0]] != null && (override || entity[prop.name] == null)) {
ret[prop.name] = row[prop.fieldNames[0]];
}

return ret;
}, {} as Dictionary);

if (Utils.hasObjectKeys(data)) {
this.hydrator.hydrate(entity, meta, data as EntityData<T>, this.factory, 'full', false, true);
Object.assign(payload, data); // merge to the changeset payload, so it gets saved to the entity snapshot
}
const mapped = this.comparator.mapResult<T>(meta.className, row as EntityDictionary<T>);
this.hydrator.hydrate(entity, meta, mapped!, this.factory, 'full', false, true);
Object.assign(payload, mapped); // merge to the changeset payload, so it gets saved to the entity snapshot
}
}

Expand Down
2 changes: 2 additions & 0 deletions tests/features/upsert/GH4242.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ test('4242 2/4', async () => {
expect(loadedDs4).toEqual([{
id: expect.any(String),
updatedAt: expect.any(Date),
optional: null,
tenantWorkflowId: 1,
}]);
await orm.em.flush();
Expand Down Expand Up @@ -180,6 +181,7 @@ test('4242 4/4', async () => {
expect(loadedDs4).toEqual({
id: expect.any(String),
updatedAt: expect.any(Date),
optional: null,
tenantWorkflowId: 1,
});
await orm.em.flush();
Expand Down
100 changes: 100 additions & 0 deletions tests/issues/GH4945.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Entity, IDatabaseDriver, MikroORM, PrimaryKey, Property, ManyToOne, Ref, ref, PrimaryKeyType } from '@mikro-orm/core';


@Entity()
class EntityA {

@PrimaryKey()
id!: string;

@PrimaryKey({ name: 'env_id' })
envID!: string;

@Property({ defaultRaw: `CURRENT_TIMESTAMP` })
createdAt?: Date;

[PrimaryKeyType]?: [string, string];

}

@Entity()
class EntityB {

@PrimaryKey()
id!: string;

@PrimaryKey({ name: 'env_id', nullable: false })
envID!: string;

@Property({ type: 'text' })
name!: string;

@ManyToOne(() => EntityA, {
name: 'entity_a_id',
default: null,
onDelete: 'set default',
nullable: true,
fieldNames: ['entity_a_id', 'env_id'],
})
entityA: Ref<EntityA> | null = null;

[PrimaryKeyType]?: [string, string];

}

const options = {
'sqlite': { dbName: ':memory:' },
'better-sqlite': { dbName: ':memory:' },
'postgresql': { dbName: 'mikro_orm_upsert' },
};

describe.each(Object.keys(options))('GH #4945 [%s]', type => {
let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init<IDatabaseDriver>({
entities: [EntityA, EntityB],
type,
...options[type],
});
await orm.schema.refreshDatabase();
});

beforeEach(async () => {
await orm.schema.clearDatabase();
});

afterAll(() => orm.close());

test('GH #4945 em.upsert()', async () => {
const a = await orm.em.upsert(EntityA, { id: 'entity-a-1', envID: 'env-1' });
const b = await orm.em.upsert(EntityB, { id: 'entity-b-1', envID: 'env-1', name: 'entity-b-1', entityA: ref(EntityA, ['entity-a-1', 'env-1']) });
expect(a).toMatchObject({ createdAt: expect.any(Date) });
orm.em.clear();
const entityA = await orm.em.upsert(EntityA, { id: 'entity-a-1', envID: 'env-1' });
const entityB = await orm.em.upsert(EntityB, { id: 'entity-b-1', envID: 'env-1', name: 'entity-b-1', entityA: ref(EntityA, ['entity-a-1', 'env-1']) });
expect(entityA).toMatchObject({ createdAt: expect.any(Date) });

await orm.em.flush();

expect(entityA).toBeInstanceOf(EntityA);
expect(entityB).toBeInstanceOf(EntityB);
});

test('GH #4945 em.upsertMany()', async () => {
const a = await orm.em.upsertMany(EntityA, [{ id: 'entity-a-1', envID: 'env-1' }, { id: 'entity-a-2', envID: 'env-1' }]);
const b = await orm.em.upsertMany(EntityB, [{ id: 'entity-b-1', envID: 'env-1', name: 'entity-b-1', entityA: ref(EntityA, ['entity-a-1', 'env-1']) }, { id: 'entity-b-2', envID: 'env-1', name: 'entity-b-2', entityA: ref(EntityA, ['entity-a-2', 'env-1']) }]);
expect(a[0]).toMatchObject({ createdAt: expect.any(Date) });
orm.em.clear();

const entitiesA = await orm.em.upsertMany(EntityA, [{ id: 'entity-a-1', envID: 'env-1' }, { id: 'entity-a-2', envID: 'env-1' }]);
const entitiesB = await orm.em.upsertMany(EntityB, [{ id: 'entity-b-1', envID: 'env-1', name: 'entity-b-1', entityA: ref(EntityA, ['entity-a-1', 'env-1']) }, { id: 'entity-b-2', envID: 'env-1', name: 'entity-b-2', entityA: ref(EntityA, ['entity-a-2', 'env-1']) }]);
expect(entitiesA[0]).toMatchObject({ createdAt: expect.any(Date) });

await orm.em.flush();

expect(entitiesA).toHaveLength(2);
expect(entitiesB).toHaveLength(2);
});

});

0 comments on commit a7e9a82

Please sign in to comment.