From 346a812d949ca3a85c0dc65fbc00cb60a2b0e9fc Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Mon, 11 May 2026 15:57:21 +0700 Subject: [PATCH 1/5] feat(ref): add inactive setting validation --- .../dbml-parse/src/core/local_modules/ref/validate.ts | 11 +++++++++++ packages/dbml-parse/src/core/types/keywords.ts | 1 + 2 files changed, 12 insertions(+) diff --git a/packages/dbml-parse/src/core/local_modules/ref/validate.ts b/packages/dbml-parse/src/core/local_modules/ref/validate.ts index 3074949f7..0576e2055 100644 --- a/packages/dbml-parse/src/core/local_modules/ref/validate.ts +++ b/packages/dbml-parse/src/core/local_modules/ref/validate.ts @@ -251,6 +251,17 @@ export function validateFieldSettings (settings: ListExpressionNode): Report 1) { + errors.push(...attrs.map((attr) => new CompileError(CompileErrorCode.DUPLICATE_REF_SETTING, '\'inactive\' can only appear once', attr))); + } + attrs.forEach((attr) => { + if (attr.value) { + errors.push(new CompileError(CompileErrorCode.INVALID_REF_SETTING_VALUE, '\'inactive\' cannot have a value', attr!)); + } + }); + clean[name] = attrs; + break; default: attrs.forEach((attr) => errors.push(new CompileError(CompileErrorCode.UNKNOWN_REF_SETTING, `Unknown ref setting '${name}'`, attr))); } diff --git a/packages/dbml-parse/src/core/types/keywords.ts b/packages/dbml-parse/src/core/types/keywords.ts index f9da0252e..b9207a0d4 100644 --- a/packages/dbml-parse/src/core/types/keywords.ts +++ b/packages/dbml-parse/src/core/types/keywords.ts @@ -35,4 +35,5 @@ export enum SettingName { Update = 'update', Delete = 'delete', + Inactive = 'inactive', } From a13f18b192159eeb5dc95f9f105ddbf11f9c1516 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Mon, 11 May 2026 16:01:25 +0700 Subject: [PATCH 2/5] test(ref): add inactive setting validation tests --- .../validator/ref_inactive_setting.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/dbml-parse/__tests__/examples/validator/ref_inactive_setting.test.ts diff --git a/packages/dbml-parse/__tests__/examples/validator/ref_inactive_setting.test.ts b/packages/dbml-parse/__tests__/examples/validator/ref_inactive_setting.test.ts new file mode 100644 index 000000000..0e003f984 --- /dev/null +++ b/packages/dbml-parse/__tests__/examples/validator/ref_inactive_setting.test.ts @@ -0,0 +1,48 @@ +import { + describe, expect, test, +} from 'vitest'; +import { + CompileErrorCode, +} from '@/core/types/errors'; +import { + analyze, +} from '@tests/utils'; + +describe('[example] ref inactive setting', () => { + test('should accept ref with inactive setting', () => { + const source = ` + Table users { id int } + Table posts { user_id int } + Ref: posts.user_id > users.id [inactive] + `; + const errors = analyze(source).getErrors(); + + expect(errors).toHaveLength(0); + }); + + test('should reject duplicate inactive setting', () => { + const source = ` + Table users { id int } + Table posts { user_id int } + Ref: posts.user_id > users.id [inactive, inactive] + `; + const errors = analyze(source).getErrors(); + + expect(errors).toHaveLength(2); + expect(errors[0].code).toBe(CompileErrorCode.DUPLICATE_REF_SETTING); + expect(errors[0].diagnostic).toBe("'inactive' can only appear once"); + }); + + test('should reject inactive setting with a value', () => { + const source = ` + Table users { id int } + Table posts { user_id int } + Ref: posts.user_id > users.id [inactive: true] + `; + const errors = analyze(source).getErrors(); + + expect(errors).toHaveLength(1); + expect(errors[0].code).toBe(CompileErrorCode.INVALID_REF_SETTING_VALUE); + expect(errors[0].diagnostic).toBe("'inactive' cannot have a value"); + }); +}); From c6e1de80d5699c3d65c27f482379bf1ab99c7c8f Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Mon, 11 May 2026 16:24:00 +0700 Subject: [PATCH 3/5] feat(ref): propagate inactive setting to schema and autocompletion --- .../examples/interpreter/ref_inactive.test.ts | 44 +++++++++++++++++++ .../services/suggestions/general.test.ts | 4 +- .../src/core/global_modules/ref/interpret.ts | 2 + .../dbml-parse/src/core/types/schemaJson.ts | 1 + .../src/core/types/symbol/metadata.ts | 12 +++-- .../src/services/suggestions/provider.ts | 29 +++++++----- 6 files changed, 77 insertions(+), 15 deletions(-) create mode 100644 packages/dbml-parse/__tests__/examples/interpreter/ref_inactive.test.ts diff --git a/packages/dbml-parse/__tests__/examples/interpreter/ref_inactive.test.ts b/packages/dbml-parse/__tests__/examples/interpreter/ref_inactive.test.ts new file mode 100644 index 000000000..46465b821 --- /dev/null +++ b/packages/dbml-parse/__tests__/examples/interpreter/ref_inactive.test.ts @@ -0,0 +1,44 @@ +import { + describe, expect, test, +} from 'vitest'; +import { + interpret, +} from '@tests/utils'; + +describe('[example] ref inactive setting', () => { + test('should set inactive to true when inactive setting present', () => { + const source = ` + Table users { id int } + Table posts { user_id int } + Ref: posts.user_id > users.id [inactive] + `; + const db = interpret(source).getValue()!; + + expect(db.refs).toHaveLength(1); + expect(db.refs[0].inactive).toBe(true); + }); + + test('should not set inactive when setting absent', () => { + const source = ` + Table users { id int } + Table posts { user_id int } + Ref: posts.user_id > users.id + `; + const db = interpret(source).getValue()!; + + expect(db.refs).toHaveLength(1); + expect(db.refs[0].inactive).toBeUndefined(); + }); + + test('should not set inactive when other settings present', () => { + const source = ` + Table users { id int } + Table posts { user_id int } + Ref: posts.user_id > users.id [delete: cascade] + `; + const db = interpret(source).getValue()!; + + expect(db.refs).toHaveLength(1); + expect(db.refs[0].inactive).toBeUndefined(); + }); +}); diff --git a/packages/dbml-parse/__tests__/examples/services/suggestions/general.test.ts b/packages/dbml-parse/__tests__/examples/services/suggestions/general.test.ts index f5630efc9..c4cac5d6e 100644 --- a/packages/dbml-parse/__tests__/examples/services/suggestions/general.test.ts +++ b/packages/dbml-parse/__tests__/examples/services/suggestions/general.test.ts @@ -1047,19 +1047,19 @@ describe('[example] CompletionItemProvider', () => { // Test labels const labels = result.suggestions.map((s) => s.label); expect(labels).toEqual([ + 'inactive', 'update', 'delete', 'color', - ]); // Test insertTexts const insertTexts = result.suggestions.map((s) => s.insertText); expect(insertTexts).toEqual([ + 'inactive', 'update: ', 'delete: ', 'color: ', - ]); }); diff --git a/packages/dbml-parse/src/core/global_modules/ref/interpret.ts b/packages/dbml-parse/src/core/global_modules/ref/interpret.ts index 5da080327..50d2080d8 100644 --- a/packages/dbml-parse/src/core/global_modules/ref/interpret.ts +++ b/packages/dbml-parse/src/core/global_modules/ref/interpret.ts @@ -159,6 +159,8 @@ export class RefInterpreter { : extractVariableFromExpression(updateSetting) as string; this.ref.color = settingMap.color?.length ? extractColor(settingMap.color?.at(0)?.value as any) : undefined; + + this.ref.inactive = settingMap.inactive?.length ? true : undefined; } return []; diff --git a/packages/dbml-parse/src/core/types/schemaJson.ts b/packages/dbml-parse/src/core/types/schemaJson.ts index 3a0295a62..cf898ac04 100644 --- a/packages/dbml-parse/src/core/types/schemaJson.ts +++ b/packages/dbml-parse/src/core/types/schemaJson.ts @@ -176,6 +176,7 @@ export interface Ref { color?: string; onDelete?: string; onUpdate?: string; + inactive?: boolean; token: TokenPosition; } diff --git a/packages/dbml-parse/src/core/types/symbol/metadata.ts b/packages/dbml-parse/src/core/types/symbol/metadata.ts index a45717bb4..393d97d8a 100644 --- a/packages/dbml-parse/src/core/types/symbol/metadata.ts +++ b/packages/dbml-parse/src/core/types/symbol/metadata.ts @@ -23,15 +23,13 @@ import { } from '../module'; import { ElementKind, + SettingName, } from '../keywords'; import { getBody, destructureComplexVariableTuple, destructureCallExpression, } from '@/core/utils/expression'; -import { - TablePartial, -} from '../schemaJson'; export enum MetadataKind { Ref = 'ref', @@ -170,6 +168,14 @@ export class RefMetadata extends NodeMetadata { return undefined; } + active (compiler: Compiler): boolean { + if (!(this.declaration instanceof ElementDeclarationNode)) return true; + const field = getBody(this.declaration)[0]; + if (!field) return true; + const s = compiler.nodeSettings(field).getFiltered(UNHANDLED); + return !s?.[SettingName.Inactive]?.length; + } + leftToken (): SyntaxNode { // Standalone ref if (this.declaration instanceof ElementDeclarationNode) { diff --git a/packages/dbml-parse/src/services/suggestions/provider.ts b/packages/dbml-parse/src/services/suggestions/provider.ts index 22150030c..a4b0b8e1c 100644 --- a/packages/dbml-parse/src/services/suggestions/provider.ts +++ b/packages/dbml-parse/src/services/suggestions/provider.ts @@ -586,16 +586,25 @@ function suggestAttributeName (compiler: Compiler, filepath: Filepath, offset: n case ScopeKind.REF: return { suggestions: [ - SettingName.Update, - SettingName.Delete, - SettingName.Color, - ].map((name) => ({ - label: name, - insertText: `${name}: `, - kind: CompletionItemKind.Property, - insertTextRules: CompletionItemInsertTextRule.KeepWhitespace, - range: undefined as any, - })), + { + label: SettingName.Inactive, + insertText: SettingName.Inactive, + kind: CompletionItemKind.Property, + insertTextRules: CompletionItemInsertTextRule.KeepWhitespace, + range: undefined as any, + }, + ...[ + SettingName.Update, + SettingName.Delete, + SettingName.Color, + ].map((name) => ({ + label: name, + insertText: `${name}: `, + kind: CompletionItemKind.Property, + insertTextRules: CompletionItemInsertTextRule.KeepWhitespace, + range: undefined as any, + })), + ], }; case ScopeKind.CHECKS: return { From aa44aafb75c17ef0f3711d8443eb073f6dc571e6 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Mon, 11 May 2026 16:36:27 +0700 Subject: [PATCH 4/5] feat(ref): add inactive to dbml-core model and DbmlExporter --- .../examples/exporter/exporter.spec.ts | 32 ++++++++++++++ .../model_exporter/model_exporter.spec.ts | 42 +++++++++++++++++++ packages/dbml-core/src/export/DbmlExporter.ts | 3 ++ packages/dbml-core/src/model_structure/ref.js | 5 ++- .../dbml-core/types/model_structure/ref.d.ts | 3 ++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/packages/dbml-core/__tests__/examples/exporter/exporter.spec.ts b/packages/dbml-core/__tests__/examples/exporter/exporter.spec.ts index a0e476d0f..f2b5d5094 100644 --- a/packages/dbml-core/__tests__/examples/exporter/exporter.spec.ts +++ b/packages/dbml-core/__tests__/examples/exporter/exporter.spec.ts @@ -59,6 +59,38 @@ const EXPECTED_DBML_WITHOUT_RECORDS = "name" varchar }`; +const DBML_WITH_INACTIVE_REF = ` +Table users { + id integer [pk] +} +Table posts { + user_id integer +} +Ref: posts.user_id > users.id [inactive] +`.trim(); + +const DBML_WITHOUT_INACTIVE_REF = ` +Table users { + id integer [pk] +} +Table posts { + user_id integer +} +Ref: posts.user_id > users.id +`.trim(); + +describe('@dbml/core - ref inactive setting', () => { + test('exports inactive ref with inactive flag', () => { + const res = exporter.export(DBML_WITH_INACTIVE_REF, 'dbml'); + expect(res).toContain('[inactive]'); + }); + + test('does not export inactive flag when setting absent', () => { + const res = exporter.export(DBML_WITHOUT_INACTIVE_REF, 'dbml'); + expect(res).not.toContain('inactive'); + }); +}); + describe('@dbml/core - exporter flags', () => { describe('includeRecords', () => { test('includes records by default', () => { diff --git a/packages/dbml-core/__tests__/examples/model_exporter/model_exporter.spec.ts b/packages/dbml-core/__tests__/examples/model_exporter/model_exporter.spec.ts index c01a900f3..41e21b8c4 100644 --- a/packages/dbml-core/__tests__/examples/model_exporter/model_exporter.spec.ts +++ b/packages/dbml-core/__tests__/examples/model_exporter/model_exporter.spec.ts @@ -208,3 +208,45 @@ describe('@dbml/core - JsonExporter isNormalized option', () => { expect(parsed.records[0].columns).toEqual(['id', 'name']); }); }); + +const DBML_WITH_INACTIVE_REF = ` +Table users { + id integer [pk] +} +Table posts { + user_id integer +} +Ref: posts.user_id > users.id [inactive] +`.trim(); + +describe('@dbml/core - ref inactive', () => { + test('model stores inactive as true', () => { + const database = (new Parser()).parse(DBML_WITH_INACTIVE_REF, 'dbmlv2'); + const ref = database.schemas[0].refs[0]; + expect(ref.inactive).toBe(true); + }); + + test('normalized model stores inactive as true', () => { + const database = (new Parser()).parse(DBML_WITH_INACTIVE_REF, 'dbmlv2'); + const normalized = database.normalize(); + const ref = Object.values(normalized.refs)[0]; + expect(ref.inactive).toBe(true); + }); + + test('DbmlExporter exports inactive flag', () => { + const database = (new Parser()).parse(DBML_WITH_INACTIVE_REF, 'dbmlv2'); + const res = DbmlExporter.export(database.normalize(), { includeRecords: false }); + expect(res).toContain('[inactive]'); + }); + + test('inactive is undefined when setting absent', () => { + const source = ` +Table users { id integer [pk] } +Table posts { user_id integer } +Ref: posts.user_id > users.id +`.trim(); + const database = (new Parser()).parse(source, 'dbmlv2'); + const ref = database.schemas[0].refs[0]; + expect(ref.inactive).toBeUndefined(); + }); +}); diff --git a/packages/dbml-core/src/export/DbmlExporter.ts b/packages/dbml-core/src/export/DbmlExporter.ts index 8c4753fa0..37fbd4cc4 100644 --- a/packages/dbml-core/src/export/DbmlExporter.ts +++ b/packages/dbml-core/src/export/DbmlExporter.ts @@ -316,6 +316,9 @@ class DbmlExporter { if (ref.onDelete) { refActions.push(`delete: ${ref.onDelete.toLowerCase()}`); } + if (ref.inactive) { + refActions.push('inactive'); + } if (refActions.length > 0) { line += ` [${refActions.join(', ')}]`; } diff --git a/packages/dbml-core/src/model_structure/ref.js b/packages/dbml-core/src/model_structure/ref.js index b35ed1dcb..8044d2a26 100644 --- a/packages/dbml-core/src/model_structure/ref.js +++ b/packages/dbml-core/src/model_structure/ref.js @@ -17,7 +17,7 @@ class Ref extends Element { * @param {import('../../types/model_structure/ref').RawRef} param0 */ constructor ({ - name, color, endpoints, onDelete, onUpdate, token, schema = {}, injectedPartial = null, + name, color, endpoints, onDelete, onUpdate, inactive, token, schema = {}, injectedPartial = null, } = {}) { super(token); /** @type {string} */ @@ -28,6 +28,8 @@ class Ref extends Element { this.onDelete = onDelete; /** @type {any} */ this.onUpdate = onUpdate; + /** @type {boolean} */ + this.inactive = inactive; /** @type {import('../../types/model_structure/endpoint').default[]} */ this.endpoints = []; /** @type {import('../../types/model_structure/schema').default} */ @@ -89,6 +91,7 @@ class Ref extends Element { color: this.color, onDelete: this.onDelete, onUpdate: this.onUpdate, + inactive: this.inactive, injectedPartialId: this.injectedPartial?.id, }; } diff --git a/packages/dbml-core/types/model_structure/ref.d.ts b/packages/dbml-core/types/model_structure/ref.d.ts index f443018dc..7437e6e23 100644 --- a/packages/dbml-core/types/model_structure/ref.d.ts +++ b/packages/dbml-core/types/model_structure/ref.d.ts @@ -10,6 +10,7 @@ export interface RawRef { endpoints: Endpoint[]; onDelete: any; onUpdate: any; + inactive?: boolean; token: Token; schema: Schema; } @@ -19,6 +20,7 @@ declare class Ref extends Element { endpoints: Endpoint[]; onDelete: any; onUpdate: any; + inactive?: boolean; schema: Schema; dbState: DbState; id: number; @@ -68,6 +70,7 @@ export interface NormalizedRef { color?: string; onUpdate?: string; onDelete?: string; + inactive?: boolean; schemaId: number; endpointIds: number[]; injectedPartialId?: number; From 7de7658152271359c125b39122e6504cf1a5f812 Mon Sep 17 00:00:00 2001 From: Huy-DNA Date: Mon, 11 May 2026 16:39:12 +0700 Subject: [PATCH 5/5] docs: add inactive ref to enrichment & visualization --- .../docs/syntax/enrichment-visualization.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/dbml-homepage/docs/syntax/enrichment-visualization.md b/dbml-homepage/docs/syntax/enrichment-visualization.md index 4f2540393..f6ead6cb9 100644 --- a/dbml-homepage/docs/syntax/enrichment-visualization.md +++ b/dbml-homepage/docs/syntax/enrichment-visualization.md @@ -18,6 +18,7 @@ This part covers features specific to diagram & wiki tools like [dbdiagram.io](h - [TableGroup Settings](#tablegroup-settings) - [DiagramView](#diagramview) - [Colors](#colors) +- [Inactive Ref](#inactive-ref) ## Note Definition @@ -255,3 +256,22 @@ TableGroup e_commerce [color: #3498DB] { countries } ``` + +## Inactive Ref + +Use `inactive` on a relationship to mark it as inactive. Inactive refs are displayed as a dotted line in the diagram, allowing you to document relationships that exist logically but are not actively enforced. + +```text +// short form +Ref: posts.user_id > users.id [inactive] + +// with other settings +Ref: posts.user_id > users.id [delete: cascade, inactive] + +// long form +Ref { + posts.user_id > users.id [inactive] +} +``` + +The `inactive` setting takes no value.