Skip to content

Commit

Permalink
fix(core): support overlapping composite FKs with different nullability
Browse files Browse the repository at this point in the history
Closes #4478
  • Loading branch information
B4nan committed Sep 29, 2023
1 parent 1d2ed46 commit 208fbaa
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 8 deletions.
12 changes: 5 additions & 7 deletions packages/knex/src/AbstractSqlDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,16 +403,14 @@ export abstract class AbstractSqlDriver<Connection extends AbstractSqlConnection
const keys: string[] = [];
props.forEach(prop => {
if (prop.fieldNames.length > 1) {
const param = [...row[prop.name] as unknown[] ?? prop.fieldNames.map(() => null)];
const key = (row[prop.name] as unknown[] ?? prop.fieldNames).map(() => '?');
const param = Utils.flatten([...row[prop.name] ?? prop.fieldNames.map(() => null)]);
const key = param.map(() => '?');
prop.fieldNames.forEach((field, idx) => {
if (duplicates.includes(field)) {
param.splice(idx, 1);
key.splice(idx, 1);
if (!duplicates.includes(field)) {
params.push(param[idx]);
keys.push(key[idx]);
}
});
params.push(...param);
keys.push(...key);
} else if (prop.customType && 'convertToDatabaseValueSQL' in prop.customType && !this.platform.isRaw(row[prop.name])) {
keys.push(prop.customType.convertToDatabaseValueSQL!('?', this.platform));
addParams(prop, row);
Expand Down
2 changes: 1 addition & 1 deletion packages/knex/src/schema/DatabaseTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export class DatabaseTable {
unsigned: prop.unsigned && this.platform.isNumericColumn(mappedType),
autoincrement: prop.autoincrement ?? primary,
primary,
nullable: !!prop.nullable,
nullable: this.columns[field]?.nullable ?? !!prop.nullable,
length: prop.length,
precision: prop.precision,
scale: prop.scale,
Expand Down
101 changes: 101 additions & 0 deletions tests/features/composite-keys/GH4478.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Entity, PrimaryKey, ManyToOne, SimpleLogger, PrimaryKeyProp, wrap } from '@mikro-orm/core';
import { MikroORM } from '@mikro-orm/sqlite';
import { mockLogger } from '../../helpers';

@Entity()
class School {

@PrimaryKey()
schoolCode!: string;

}

@Entity()
class Class {

[PrimaryKeyProp]?: ['school', 'academicYear', 'classCode'];

@ManyToOne(() => School, { name: 'school_code', primary: true })
school!: School;

@PrimaryKey()
academicYear!: string;

@PrimaryKey()
classCode!: string;

}

@Entity()
class StudentAllocation {

[PrimaryKeyProp]?: ['studentId', 'academicYear'];

@PrimaryKey()
studentId!: string;

@PrimaryKey()
academicYear!: string;

@ManyToOne(() => School, { name: 'school_code' })
school!: School;

@ManyToOne(() => Class, { fieldNames: ['school_code', 'academic_year', 'class_code'], nullable: true })
class?: Class;

}

let orm: MikroORM;

beforeAll(async () => {
orm = await MikroORM.init({
entities: [StudentAllocation],
dbName: `:memory:`,
loggerFactory: options => new SimpleLogger(options),
});
await orm.schema.createSchema();
});

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

test(`GH issue 4478`, async () => {
expect(await orm.schema.getCreateSchemaSQL()).toMatchSnapshot();

const school = orm.em.create(School, { schoolCode: 'abc' });
orm.em.create(StudentAllocation, { studentId: '1', academicYear: '2023', school });
orm.em.create(StudentAllocation, {
studentId: '2',
academicYear: '2023',
school,
class: { school, classCode: 'cls', academicYear: '2023' },
});

const mock = mockLogger(orm);
await orm.em.flush();
expect(mock.mock.calls).toMatchSnapshot();
orm.em.clear();

const sa = await orm.em.find(StudentAllocation, { academicYear: '2023' }, { populate: true });
expect(wrap(sa[0]).toObject()).toEqual({
academicYear: '2023',
school: {
schoolCode: 'abc',
},
studentId: '1',
});

expect(wrap(sa[1]).toObject()).toEqual({
academicYear: '2023',
class: {
academicYear: '2023',
classCode: 'cls',
school: {
schoolCode: 'abc',
},
},
school: {
schoolCode: 'abc',
},
studentId: '2',
});
});
37 changes: 37 additions & 0 deletions tests/features/composite-keys/__snapshots__/GH4478.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`GH issue 4478 1`] = `
"pragma foreign_keys = off;
create table \`school\` (\`school_code\` text not null, primary key (\`school_code\`));
create table \`class\` (\`school_code\` text not null, \`academic_year\` text not null, \`class_code\` text not null, constraint \`class_school_code_foreign\` foreign key(\`school_code\`) references \`school\`(\`school_code\`) on update cascade, primary key (\`school_code\`, \`academic_year\`, \`class_code\`));
create index \`class_school_code_index\` on \`class\` (\`school_code\`);
create table \`student_allocation\` (\`student_id\` text not null, \`academic_year\` text not null, \`school_code\` text not null, \`class_code\` text null, constraint \`student_allocation_school_code_foreign\` foreign key(\`school_code\`) references \`school\`(\`school_code\`) on update cascade, constraint \`student_allocation_school_code_academic_year_class_code_foreign\` foreign key(\`school_code\`, \`academic_year\`, \`class_code\`) references \`class\`(\`school_code\`, \`academic_year\`, \`class_code\`) on delete set null on update cascade, primary key (\`student_id\`, \`academic_year\`));
create index \`student_allocation_school_code_index\` on \`student_allocation\` (\`school_code\`);
create index \`student_allocation_school_code_academic_year_class_code_index\` on \`student_allocation\` (\`school_code\`, \`academic_year\`, \`class_code\`);
pragma foreign_keys = on;
"
`;

exports[`GH issue 4478 2`] = `
[
[
"[query] begin",
],
[
"[query] insert into \`school\` (\`school_code\`) values ('abc') returning \`school_code\`",
],
[
"[query] insert into \`class\` (\`school_code\`, \`academic_year\`, \`class_code\`) values ('abc', '2023', 'cls') returning \`school_code\`, \`academic_year\`, \`class_code\`",
],
[
"[query] insert into \`student_allocation\` (\`student_id\`, \`academic_year\`, \`school_code\`, \`class_code\`) values ('1', '2023', 'abc', NULL), ('2', '2023', 'abc', 'cls') returning \`student_id\`, \`academic_year\`",
],
[
"[query] commit",
],
]
`;

0 comments on commit 208fbaa

Please sign in to comment.