Skip to content

Commit 8e15fc3

Browse files
committed
feat: shared constraint validation, partial ref hints, inline ref tests
1 parent 37547f2 commit 8e15fc3

5 files changed

Lines changed: 423 additions & 192 deletions

File tree

packages/dbml-parse/__tests__/examples/services/code_actions/code_actions.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,4 +131,78 @@ describe('[example] code actions - ref constraint quick fixes', () => {
131131
expect(opFix?.title).toContain('>');
132132
});
133133
});
134+
135+
describe('inline ref quick fixes', () => {
136+
test('inline ref with nullable column suggests op change and NOT NULL', () => {
137+
const source = `
138+
Table users { id int [pk] }
139+
Table posts {
140+
user_id int [null, ref: > users.id]
141+
}
142+
`;
143+
const fixes = getQuickFixes(source);
144+
expect(fixes.some((f) => f.title.includes('in the ref'))).toBe(true);
145+
expect(fixes.some((f) => f.title.includes('NOT NULL'))).toBe(true);
146+
});
147+
148+
test('inline ref op change suggests correct operator', () => {
149+
const source = `
150+
Table users { id int [pk] }
151+
Table posts {
152+
user_id int [null, ref: > users.id]
153+
}
154+
`;
155+
const fixes = getQuickFixes(source);
156+
const opFix = fixes.find((f) => f.title.includes('in the ref'));
157+
expect(opFix?.title).toContain('>?');
158+
});
159+
160+
test('inline ref with matching constraints produces no fixes', () => {
161+
const source = `
162+
Table users { id int [pk] }
163+
Table posts {
164+
user_id int [not null, ref: > users.id]
165+
}
166+
`;
167+
const fixes = getQuickFixes(source);
168+
expect(fixes).toHaveLength(0);
169+
});
170+
171+
test('table partial ref can fix non-partial column', () => {
172+
const source = `
173+
Table users {
174+
id int [pk]
175+
code int
176+
}
177+
TablePartial user_ref {
178+
user_code int [not null, ref: - users.code]
179+
}
180+
Table posts {
181+
id int [pk]
182+
~user_ref
183+
}
184+
`;
185+
const fixes = getQuickFixes(source);
186+
// users.code is not unique/pk, so UNIQUE fix should be offered
187+
expect(fixes.some((f) => f.title.includes('UNIQUE'))).toBe(true);
188+
// No fix for user_code since it's from a partial
189+
expect(fixes.some((f) => f.title.includes('user_code'))).toBe(false);
190+
});
191+
192+
test('table partial inline ref produces no column fixes for partial columns', () => {
193+
const source = `
194+
Table users { id int [pk] }
195+
TablePartial timestamps {
196+
created_by int [null, ref: > users.id]
197+
}
198+
Table posts {
199+
id int [pk]
200+
~timestamps
201+
}
202+
`;
203+
const fixes = getQuickFixes(source);
204+
// No column fixes for partial columns, no op token to change
205+
expect(fixes.filter((f) => f.title.includes('NOT NULL'))).toHaveLength(0);
206+
});
207+
});
134208
});

packages/dbml-parse/__tests__/snapshots/interpreter/output/tablepartial_causing_circular_ref.out.json

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,102 @@
258258
}
259259
},
260260
"infos": [
261+
{
262+
"code": "INVALID_REF_RELATIONSHIP",
263+
"diagnostic": "Column 'col1' is nullable but operator '>' requires it to be NOT NULL",
264+
"filepath": "/main.dbml",
265+
"level": "info",
266+
"node": {
267+
"context": {
268+
"id": "node@<function-application>@col1@[L1:C2, L1:C27]",
269+
"snippet": "col1 type ... > T.col1]"
270+
}
271+
}
272+
},
273+
{
274+
"code": "INVALID_REF_RELATIONSHIP",
275+
"diagnostic": "Column 'col1' is nullable but operator '>' requires it to be NOT NULL",
276+
"filepath": "/main.dbml",
277+
"level": "info",
278+
"node": {
279+
"context": {
280+
"id": "node@<function-application>@col1@[L1:C2, L1:C27]",
281+
"snippet": "col1 type ... > T.col1]"
282+
}
283+
}
284+
},
285+
{
286+
"code": "INVALID_REF_RELATIONSHIP",
287+
"diagnostic": "Column 'col1' is nullable but operator '>' requires it to be NOT NULL",
288+
"filepath": "/main.dbml",
289+
"level": "info",
290+
"node": {
291+
"context": {
292+
"id": "node@<function-application>@col1@[L1:C2, L1:C27]",
293+
"snippet": "col1 type ... > T.col1]"
294+
}
295+
}
296+
},
297+
{
298+
"code": "INVALID_REF_RELATIONSHIP",
299+
"diagnostic": "Column 'col1' should be unique or primary key for operator '>'",
300+
"filepath": "/main.dbml",
301+
"level": "info",
302+
"node": {
303+
"context": {
304+
"id": "node@<function-application>@col1@[L1:C2, L1:C27]",
305+
"snippet": "col1 type ... > T.col1]"
306+
}
307+
}
308+
},
309+
{
310+
"code": "INVALID_REF_RELATIONSHIP",
311+
"diagnostic": "Column 'col1' is nullable but operator '>' requires it to be NOT NULL",
312+
"filepath": "/main.dbml",
313+
"level": "info",
314+
"node": {
315+
"context": {
316+
"id": "node@<attribute>@@[L1:C13, L1:C26]",
317+
"snippet": "ref: > T.col1"
318+
}
319+
}
320+
},
321+
{
322+
"code": "INVALID_REF_RELATIONSHIP",
323+
"diagnostic": "Column 'col1' should be unique or primary key for operator '>'",
324+
"filepath": "/main.dbml",
325+
"level": "info",
326+
"node": {
327+
"context": {
328+
"id": "node@<attribute>@@[L1:C13, L1:C26]",
329+
"snippet": "ref: > T.col1"
330+
}
331+
}
332+
},
333+
{
334+
"code": "INVALID_REF_RELATIONSHIP",
335+
"diagnostic": "Column 'col3' is nullable but operator '>' requires it to be NOT NULL",
336+
"filepath": "/main.dbml",
337+
"level": "info",
338+
"node": {
339+
"context": {
340+
"id": "node@<function-application>@col3@[L2:C2, L2:C27]",
341+
"snippet": "col3 type ... > T.col2]"
342+
}
343+
}
344+
},
345+
{
346+
"code": "INVALID_REF_RELATIONSHIP",
347+
"diagnostic": "Column 'col3' is nullable but operator '>' requires it to be NOT NULL",
348+
"filepath": "/main.dbml",
349+
"level": "info",
350+
"node": {
351+
"context": {
352+
"id": "node@<function-application>@col3@[L2:C2, L2:C27]",
353+
"snippet": "col3 type ... > T.col2]"
354+
}
355+
}
356+
},
261357
{
262358
"code": "INVALID_REF_RELATIONSHIP",
263359
"diagnostic": "Column 'col3' is nullable but operator '>' requires it to be NOT NULL",
@@ -282,6 +378,42 @@
282378
}
283379
}
284380
},
381+
{
382+
"code": "INVALID_REF_RELATIONSHIP",
383+
"diagnostic": "Column 'col2' is nullable but operator '>' requires it to be NOT NULL",
384+
"filepath": "/main.dbml",
385+
"level": "info",
386+
"node": {
387+
"context": {
388+
"id": "node@<attribute>@@[L2:C13, L2:C26]",
389+
"snippet": "ref: > T.col2"
390+
}
391+
}
392+
},
393+
{
394+
"code": "INVALID_REF_RELATIONSHIP",
395+
"diagnostic": "Column 'col2' should be unique or primary key for operator '>'",
396+
"filepath": "/main.dbml",
397+
"level": "info",
398+
"node": {
399+
"context": {
400+
"id": "node@<attribute>@@[L2:C13, L2:C26]",
401+
"snippet": "ref: > T.col2"
402+
}
403+
}
404+
},
405+
{
406+
"code": "INVALID_REF_RELATIONSHIP",
407+
"diagnostic": "Column 'col2' is nullable but operator '>' requires it to be NOT NULL",
408+
"filepath": "/main.dbml",
409+
"level": "info",
410+
"node": {
411+
"context": {
412+
"id": "node@<function-application>@col2@[L7:C2, L7:C25]",
413+
"snippet": "col2 type ...f: > col3]"
414+
}
415+
}
416+
},
285417
{
286418
"code": "INVALID_REF_RELATIONSHIP",
287419
"diagnostic": "Column 'col2' is nullable but operator '>' requires it to be NOT NULL",
@@ -306,6 +438,18 @@
306438
}
307439
}
308440
},
441+
{
442+
"code": "INVALID_REF_RELATIONSHIP",
443+
"diagnostic": "Column 'col2' should be unique or primary key for operator '>'",
444+
"filepath": "/main.dbml",
445+
"level": "info",
446+
"node": {
447+
"context": {
448+
"id": "node@<function-application>@col2@[L7:C2, L7:C25]",
449+
"snippet": "col2 type ...f: > col3]"
450+
}
451+
}
452+
},
309453
{
310454
"code": "INVALID_REF_RELATIONSHIP",
311455
"diagnostic": "Column 'col3' is nullable but operator '>' requires it to be NOT NULL",

packages/dbml-parse/src/core/global_modules/program/interpret.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { validateForeignKeys, validatePrimaryKey, validateUnique } from '../reco
3030
import type { TableInfo } from '../records/utils/constraints/fk';
3131
import { getTokenPosition } from '@/core/utils/interpret';
3232
import { getMultiplicities } from '@/core/types/relation';
33+
import { validatePartialRef } from '../ref/constraint_fixes';
3334

3435
export default class ProgramInterpreter {
3536
private compiler: Compiler;
@@ -72,6 +73,7 @@ export default class ProgramInterpreter {
7273
this.interpretAllSymbols();
7374
this.interpretAllMetadata();
7475
this.interpretAllAliases();
76+
this.validatePartialRefs();
7577
this.warnings.push(...this.validateRecords());
7678
return new Report(this.db, this.errors, this.warnings, this.hints);
7779
}
@@ -322,6 +324,14 @@ export default class ProgramInterpreter {
322324
return warnings;
323325
}
324326

327+
private validatePartialRefs () {
328+
const partialMetas = this.compiler.symbolMetadata(this.programSymbol)
329+
.filter((m): m is PartialRefMetadata => m instanceof PartialRefMetadata);
330+
for (const meta of partialMetas) {
331+
this.hints.push(...validatePartialRef(this.compiler, meta));
332+
}
333+
}
334+
325335
private collectPartialRefs (fkTableMap: Map<InternedNodeSymbol, TableInfo>): Ref[] {
326336
const partialMetas = this.compiler.symbolMetadata(this.programSymbol)
327337
.filter((m): m is PartialRefMetadata => m instanceof PartialRefMetadata);

0 commit comments

Comments
 (0)