Skip to content

Commit

Permalink
fix(core): reload default values after flush in mysql/sqlite
Browse files Browse the repository at this point in the history
Closes #2581
  • Loading branch information
B4nan committed Dec 30, 2021
1 parent fcdb9b0 commit d57a6a9
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 12 deletions.
40 changes: 28 additions & 12 deletions packages/core/src/unit-of-work/ChangeSetPersister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { AnyEntity, Dictionary, EntityData, EntityMetadata, EntityProperty,
import type { EntityFactory } from '../entity';
import { EntityIdentifier } from '../entity';
import type { ChangeSet } from './ChangeSet';
import { ChangeSetType } from './ChangeSet';
import type { QueryResult } from '../connections';
import type { Configuration } from '../utils';
import { Utils } from '../utils';
Expand Down Expand Up @@ -307,31 +308,46 @@ export class ChangeSetPersister {
}
}

/**
* This method also handles reloading of database default values for inserts, so we use
* a single query in case of both versioning and default values is used.
*/
private async reloadVersionValues<T extends AnyEntity<T>>(meta: EntityMetadata<T>, changeSets: ChangeSet<T>[], options?: DriverMethodOptions) {
if (!meta.versionProperty) {
const reloadProps = meta.versionProperty ? [meta.properties[meta.versionProperty]] : [];

if (changeSets[0].type === ChangeSetType.CREATE) {
// do not reload things that already had a runtime value
reloadProps.push(...meta.props.filter(prop => prop.defaultRaw && prop.defaultRaw.toLowerCase() !== 'null' && changeSets[0].entity[prop.name] == null));
}

if (reloadProps.length === 0) {
return;
}

const pk = Utils.getPrimaryKeyHash(meta.primaryKeys);
const pks = changeSets.map(cs => cs.getPrimaryKey());
options = this.propagateSchemaFromMetadata(meta, options, {
fields: [meta.versionProperty],
fields: reloadProps.map(prop => prop.fieldNames[0]),
});
const data = await this.driver.find<T>(meta.name!, { [pk]: { $in: pks } } as FilterQuery<T>, options);
const map = new Map<string, Date>();
data.forEach(e => map.set(Utils.getCompositeKeyHash(e, meta), e[meta.versionProperty] as Date));
const map = new Map<string, Dictionary>();
data.forEach(item => map.set(Utils.getCompositeKeyHash(item, meta), item));

for (const changeSet of changeSets) {
const version = map.get(changeSet.entity.__helper!.getSerializedPrimaryKey());
const data = map.get(changeSet.entity.__helper!.getSerializedPrimaryKey());

// needed for sqlite
if (meta.properties[meta.versionProperty].type.toLowerCase() === 'date') {
changeSet.entity[meta.versionProperty] = new Date(version as unknown as string) as unknown as T[keyof T & string];
} else {
changeSet.entity[meta.versionProperty] = version as unknown as T[keyof T & string];
}
for (const prop of reloadProps) {
const value = data![prop.name];

// needed for sqlite
if (prop.type.toLowerCase() === 'date') {
changeSet.entity[prop.name] = new Date(value) as unknown as T[keyof T & string];
} else {
changeSet.entity[prop.name] = value;
}

changeSet.payload![meta.versionProperty] = version;
changeSet.payload![prop.name] = value;
}
}
}

Expand Down
60 changes: 60 additions & 0 deletions tests/features/default-values/default-values.mysql.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Entity, PrimaryKey, Property, MikroORM } from '@mikro-orm/core';
import { mockLogger } from '../../helpers';

@Entity()
class A {

@PrimaryKey()
id!: number;

@Property({ default: 50 })
foo1!: number;

@Property({ default: 50 })
foo2: number = 50;

@Property()
foo3: number = 50;

@Property({ version: true })
version!: number;

}

describe('default values in mysql', () => {

let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init({
entities: [A],
dbName: `mikro_orm_test_default_values`,
type: 'mysql',
port: 3307,
});
await orm.getSchemaGenerator().ensureDatabase();
await orm.getSchemaGenerator().dropSchema();
await orm.getSchemaGenerator().createSchema();
});

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

test(`database defaults will be available after flush`, async () => {
const mock = mockLogger(orm, ['query']);

const a = new A();
expect(a.foo1).toBeUndefined();
expect(a.foo2).toBe(50);
expect(a.foo3).toBe(50);
expect(a.version).toBeUndefined();
await orm.em.persistAndFlush(a);

// mysql needs to reload via separate select query (inside tx, so 4 in total)
expect(mock).toBeCalledTimes(4);
expect(a.foo1).toBe(50);
expect(a.foo2).toBe(50);
expect(a.foo3).toBe(50);
expect(a.version).toBe(1);
});

});
59 changes: 59 additions & 0 deletions tests/features/default-values/default-values.postgre.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Entity, PrimaryKey, Property, MikroORM } from '@mikro-orm/core';
import { mockLogger } from '../../helpers';

@Entity()
class A {

@PrimaryKey()
id!: number;

@Property({ default: 50 })
foo1!: number;

@Property({ default: 50 })
foo2: number = 50;

@Property()
foo3: number = 50;

@Property({ version: true })
version!: number;

}

describe('default values in postgres', () => {

let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init({
entities: [A],
dbName: `mikro_orm_test_default_values`,
type: 'postgresql',
});
await orm.getSchemaGenerator().ensureDatabase();
await orm.getSchemaGenerator().dropSchema();
await orm.getSchemaGenerator().createSchema();
});

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

test(`database defaults will be available after flush`, async () => {
const mock = mockLogger(orm, ['query']);

const a = new A();
expect(a.foo1).toBeUndefined();
expect(a.foo2).toBe(50);
expect(a.foo3).toBe(50);
expect(a.version).toBeUndefined();
await orm.em.persistAndFlush(a);

// postgres uses returning clause, so just a single insert query (inside tx, so 3 in total)
expect(mock).toBeCalledTimes(3);
expect(a.foo1).toBe(50);
expect(a.foo2).toBe(50);
expect(a.foo3).toBe(50);
expect(a.version).toBe(1);
});

});
57 changes: 57 additions & 0 deletions tests/features/default-values/default-values.sqlite.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Entity, PrimaryKey, Property, MikroORM } from '@mikro-orm/core';
import { mockLogger } from '../../helpers';

@Entity()
class A {

@PrimaryKey()
id!: number;

@Property({ default: 50 })
foo1!: number;

@Property({ default: 50 })
foo2: number = 50;

@Property()
foo3: number = 50;

@Property({ version: true })
version!: number;

}

describe('default values in sqlite', () => {

let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init({
entities: [A],
dbName: `:memory:`,
type: 'sqlite',
});
await orm.getSchemaGenerator().createSchema();
});

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

test(`database defaults will be available after flush`, async () => {
const mock = mockLogger(orm, ['query']);

const a = new A();
expect(a.foo1).toBeUndefined();
expect(a.foo2).toBe(50);
expect(a.foo3).toBe(50);
expect(a.version).toBeUndefined();
await orm.em.persistAndFlush(a);

// sqlite needs to reload via separate select query (inside tx, so 4 in total)
expect(mock).toBeCalledTimes(4);
expect(a.foo1).toBe(50);
expect(a.foo2).toBe(50);
expect(a.foo3).toBe(50);
expect(a.version).toBe(1);
});

});

0 comments on commit d57a6a9

Please sign in to comment.