From 50bfc9ae19c56bca03cc2bf78ceae59187b19504 Mon Sep 17 00:00:00 2001 From: Rick Lubbers Date: Tue, 21 Oct 2025 16:06:54 +0200 Subject: [PATCH] Give warning when there are no unique constraints on one-to-one columns --- src/errors/SchemaValidationError.ts | 5 ++++ src/tools/validateSchema.ts | 10 ++++++++ tests/pg-backend/validateSchema.test.ts | 34 +++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/errors/SchemaValidationError.ts b/src/errors/SchemaValidationError.ts index 80efded..8c1207e 100644 --- a/src/errors/SchemaValidationError.ts +++ b/src/errors/SchemaValidationError.ts @@ -40,6 +40,11 @@ const ormSvCodeMap = { message: 'Column type for one-to-one relation (owned side) must be bigint', explanation: 'The column type for a one-to-one relation (owned side) must be defined as a bigint. Resolve by changing the column type to bigint.', }, + // 3103 Missing index on owned side does not make sense + 'ORM-SV-3104': { + message: 'No unique constraint or unique index was defined for one-to-one relation', + explanation: 'Without the database forcing uniqueness, the one-to-one property might be violated and turn into a many-to-one. Putting a unique constraint or unique index on the column is an effective way to prevent this problem.', + }, // One-to-one inverse relations 'ORM-SV-3110': { diff --git a/src/tools/validateSchema.ts b/src/tools/validateSchema.ts index d679833..40a0be5 100644 --- a/src/tools/validateSchema.ts +++ b/src/tools/validateSchema.ts @@ -70,6 +70,16 @@ export async function validateSchema(entityDefinitions: Record { + return idx.tablename == tableName && + idx.indexdef.split('USING')[1].includes('(' + columnName + ')') && + idx.indexdef.includes('CREATE UNIQUE INDEX'); + }); + if (!hasUniqueIndex) { + warnings.push(new SchemaValidationError('ORM-SV-3104', { entity: entityName, field: fieldName, table: tableName, column: columnName })); + } } // One-to-one inverse relations diff --git a/tests/pg-backend/validateSchema.test.ts b/tests/pg-backend/validateSchema.test.ts index 026ca98..a4f1c13 100644 --- a/tests/pg-backend/validateSchema.test.ts +++ b/tests/pg-backend/validateSchema.test.ts @@ -201,5 +201,39 @@ describe('Postgres: validateSchema', () => { expect(result.valid).toBe(true); expect(result.warnings.map(w => w.code)).not.toContain('ORM-SV-3113'); }); + + it('give a warning when one-to-one does not have unique constraint or unique index', async () => { + const result = await runValidationAgainstSchema(` + create table "user" (id bigserial primary key, full_name character varying not null, username character varying not null); + create table "todo_item" (id bigserial primary key, created_at timestamptz not null, description character varying not null, author_id bigint not null references "user"); + alter table "user" add column favorite_todo_id bigint references "todo_item"; + create index on "user" (favorite_todo_id); + `, `drop table if exists "user", "todo_item" cascade;`, entitySchema); + expect(result.valid).toBe(true); + expect(result.warnings.map(w => w.code)).toContain('ORM-SV-3104'); + }); + + it('not give a warning when one-to-one does have a unique constraint', async () => { + const result = await runValidationAgainstSchema(` + create table "user" (id bigserial primary key, full_name character varying not null, username character varying not null); + create table "todo_item" (id bigserial primary key, created_at timestamptz not null, description character varying not null, author_id bigint not null references "user"); + alter table "user" add column favorite_todo_id bigint references "todo_item"; + create index on "user" (favorite_todo_id); + alter table "user" add constraint uniq_user_favorite_todo unique (favorite_todo_id); + `, `drop table if exists "user", "todo_item" cascade;`, entitySchema); + expect(result.valid).toBe(true); + expect(result.warnings.map(w => w.code)).not.toContain('ORM-SV-3104'); + }); + + it('not give a warning when one-to-one does have a unique index', async () => { + const result = await runValidationAgainstSchema(` + create table "user" (id bigserial primary key, full_name character varying not null, username character varying not null); + create table "todo_item" (id bigserial primary key, created_at timestamptz not null, description character varying not null, author_id bigint not null references "user"); + alter table "user" add column favorite_todo_id bigint references "todo_item"; + create unique index on "user" (favorite_todo_id); + `, `drop table if exists "user", "todo_item" cascade;`, entitySchema); + expect(result.valid).toBe(true); + expect(result.warnings.map(w => w.code)).not.toContain('ORM-SV-3104'); + }); });