Skip to content

Commit baf9cc2

Browse files
alban bertoliniclaude
andcommitted
test(datasource-sql): reproduce missing FK relations with multi-schema
When two PostgreSQL schemas have tables with the same names and FK columns with the same names, PostgreSQL generates identical auto-constraint names. Sequelize's FK introspection query joins constraint_column_usage on constraint_name without schema qualifier, producing a cross-schema join. The extra rows are misdetected as composite FKs and filtered out, losing the relations entirely. Ref: https://community.forestadmin.com/t/missing-related-data/8385 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 590359b commit baf9cc2

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

packages/datasource-sql/test/introspection/introspector.integration.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,4 +441,126 @@ ALTER TABLE "TableA" ADD CONSTRAINT "A_fkey" FOREIGN KEY ("type") REFERENCES "Ta
441441
);
442442
});
443443
});
444+
445+
/**
446+
* Bug reproduction: FK relations disappear when another schema has same table names.
447+
* @see https://community.forestadmin.com/t/missing-related-data/8385
448+
*
449+
* Root cause: Sequelize's FK query joins constraint_column_usage on constraint_name
450+
* WITHOUT schema qualifier. When two schemas have identical table/column names,
451+
* PostgreSQL generates identical auto-constraint names in both schemas. The cross-schema
452+
* join produces extra rows, which the composite FK detection misinterprets as composite
453+
* foreign keys and filters out.
454+
*/
455+
describe('relations with same table names across schemas', () => {
456+
const db = 'database_introspector_multi_schema_fk';
457+
458+
describe.each(POSTGRESQL_DETAILS)('on $name', connectionDetails => {
459+
let sequelize: Sequelize;
460+
let sequelizeSchema1: Sequelize;
461+
462+
beforeEach(async () => {
463+
sequelize = await setupEmptyDatabase(connectionDetails, db);
464+
465+
await sequelize
466+
.getQueryInterface()
467+
.dropSchema('schema1')
468+
.catch(() => {});
469+
await sequelize
470+
.getQueryInterface()
471+
.dropSchema('schema2')
472+
.catch(() => {});
473+
474+
await sequelize.getQueryInterface().createSchema('schema1');
475+
await sequelize.getQueryInterface().createSchema('schema2');
476+
477+
sequelizeSchema1 = new Sequelize(connectionDetails.url(db), {
478+
logging: false,
479+
schema: 'schema1',
480+
});
481+
});
482+
483+
afterEach(async () => {
484+
await sequelizeSchema1?.close();
485+
await sequelize?.close();
486+
});
487+
488+
it('should not misdetect composite FK when another schema has same constraint names', async () => {
489+
// The Sequelize FK query joins information_schema.constraint_column_usage (ccu)
490+
// on constraint_name WITHOUT schema qualifier. When two schemas have tables with
491+
// the same name and FK columns with the same name, PostgreSQL generates identical
492+
// auto-constraint names (e.g. "xxx_model_code_fkey") in both schemas.
493+
// The cross-schema join produces extra rows that the code misdetects as composite FKs,
494+
// filtering them out and losing the relation.
495+
await sequelize.query(`
496+
CREATE TABLE schema1.main_table (
497+
id SERIAL PRIMARY KEY,
498+
unique_code TEXT NOT NULL UNIQUE
499+
);
500+
501+
CREATE TABLE schema1.xxx_model (
502+
id SERIAL PRIMARY KEY,
503+
code TEXT NOT NULL REFERENCES schema1.main_table(unique_code)
504+
);
505+
506+
CREATE TABLE schema1.yyy_model (
507+
id SERIAL PRIMARY KEY,
508+
code TEXT NOT NULL REFERENCES schema1.main_table(unique_code)
509+
);
510+
511+
CREATE TABLE schema1.a_b_c (
512+
id SERIAL PRIMARY KEY,
513+
code TEXT NOT NULL REFERENCES schema1.main_table(unique_code)
514+
);
515+
516+
-- Schema2: same table names AND same FK column names → same auto-constraint names
517+
CREATE TABLE schema2.main_table (
518+
id SERIAL PRIMARY KEY,
519+
unique_code TEXT NOT NULL UNIQUE
520+
);
521+
522+
CREATE TABLE schema2.xxx_model (
523+
id SERIAL PRIMARY KEY,
524+
code TEXT NOT NULL REFERENCES schema2.main_table(unique_code)
525+
);
526+
527+
CREATE TABLE schema2.yyy_model (
528+
id SERIAL PRIMARY KEY,
529+
code TEXT NOT NULL REFERENCES schema2.main_table(unique_code)
530+
);
531+
532+
CREATE TABLE schema2.a_b_c (
533+
id SERIAL PRIMARY KEY,
534+
code TEXT NOT NULL REFERENCES schema2.main_table(unique_code)
535+
);
536+
`);
537+
538+
const logger = jest.fn();
539+
const { tables } = await Introspector.introspect(sequelizeSchema1, logger);
540+
541+
expect(tables).toHaveLength(4);
542+
543+
const xxxModel = tables.find(t => t.name === 'xxx_model');
544+
const yyyModel = tables.find(t => t.name === 'yyy_model');
545+
const abcModel = tables.find(t => t.name === 'a_b_c');
546+
547+
// All 3 models should preserve their FK constraint to main_table.unique_code
548+
expect(xxxModel?.columns.find(c => c.name === 'code')?.constraints).toEqual([
549+
{ table: 'main_table', column: 'unique_code' },
550+
]);
551+
expect(yyyModel?.columns.find(c => c.name === 'code')?.constraints).toEqual([
552+
{ table: 'main_table', column: 'unique_code' },
553+
]);
554+
expect(abcModel?.columns.find(c => c.name === 'code')?.constraints).toEqual([
555+
{ table: 'main_table', column: 'unique_code' },
556+
]);
557+
558+
// Should NOT log composite relation warnings for these single-column FKs
559+
expect(logger).not.toHaveBeenCalledWith(
560+
'Warn',
561+
expect.stringContaining('Composite relations are not supported'),
562+
);
563+
});
564+
});
565+
});
444566
});

0 commit comments

Comments
 (0)