bug: DBML Ref delete/update actions ignored — all FKs emitted as ON DELETE NO ACTION #2

Closed
opened 2026-04-04 14:06:47 +00:00 by sgcommand · 1 comment
Member

Summary

When converting a multi-file DBML schema to PostgreSQL DDL, relspecgo ignores the [delete: ...] and [update: ...] action modifiers on Ref: declarations. Every generated foreign key constraint is emitted as ON DELETE NO ACTION ON UPDATE NO ACTION regardless of what is specified in the source DBML.

Environment

  • Tool: relspec (relspecgo)
  • Input format: dbml
  • Output format: pgsql
  • Workflow: multi-file DBML schema merged and converted to PostgreSQL DDL
  • Repo where this was observed: https://git.warky.dev/wdevs/amcs (PR #20)

Reproduction

Input — cross-file Ref: declarations (bottom of schema/files.dbml)

Table stored_files {
  id bigserial [pk]
  guid uuid [unique, not null, default: `gen_random_uuid()`]
  thought_id uuid [ref: > thoughts.guid]
  project_id uuid [ref: > projects.guid]
  name text [not null]
  ...
}

// Cross-file refs (for relspecgo merge)
Ref: stored_files.thought_id > thoughts.guid [delete: set null]
Ref: stored_files.project_id > projects.guid [delete: set null]

Input — inline column [ref: ...] with delete action (same-file, schema/core.dbml)

Table thought_links {
  from_id bigint [not null, ref: > thoughts.id]
  to_id   bigint [not null, ref: > thoughts.id]
  relation text [not null]
  created_at timestamptz [default: `now()`]

  indexes {
    (from_id, to_id, relation) [pk]
    from_id
    to_id
  }
}

Original migration had ON DELETE CASCADE for thought_links.from_id and thought_links.to_id.

Top-level Ref: declarations with explicit delete actions (from schema/skills.dbml):

// Cross-file refs (for relspecgo merge)
Ref: project_skills.project_id > projects.guid [delete: cascade]
Ref: project_guardrails.project_id > projects.guid [delete: cascade]

And from schema/meta.dbml:

// Cross-file refs (for relspecgo merge)
Ref: chat_histories.project_id > projects.guid [delete: set null]

Generated output (from migrations/020_generated_schema.sql)

Every single FK in the output is:

ADD CONSTRAINT fk_activities_family_member_id
  FOREIGN KEY (family_member_id)
  ON DELETE NO ACTION
  ON UPDATE NO ACTION;

ADD CONSTRAINT fk_stored_files_thought_id
  FOREIGN KEY (thought_id)
  ON DELETE NO ACTION    -- expected: ON DELETE SET NULL
  ON UPDATE NO ACTION;

ADD CONSTRAINT fk_project_skills_project_id
  FOREIGN KEY (project_id)
  ON DELETE NO ACTION    -- expected: ON DELETE CASCADE
  ON UPDATE NO ACTION;

No FK in the entire 4400-line generated file has anything other than NO ACTION, regardless of whether the Ref specifies cascade, set null, or restrict.

Expected Behaviour

The pgsql writer should honour the [delete: ...] / [update: ...] action modifiers from DBML Ref: declarations and emit the appropriate ON DELETE / ON UPDATE clauses:

DBML action Expected SQL
[delete: cascade] ON DELETE CASCADE
[delete: set null] ON DELETE SET NULL
[delete: restrict] ON DELETE RESTRICT
[delete: no action] ON DELETE NO ACTION
[update: cascade] ON UPDATE CASCADE

Actual Behaviour

All FKs are emitted as ON DELETE NO ACTION ON UPDATE NO ACTION unconditionally.

Areas to Investigate

  1. DBML reader (pkg/readers/dbml/) — is the [delete: ...] modifier being parsed and stored on the Relation struct, or silently dropped?
  2. Internal relation model — does the canonical Relation/ForeignKey struct have a field for delete/update actions? If not, it needs one.
  3. pgsql writer (pkg/writers/pgsql/) — even if the action is parsed and stored, is the writer reading it and emitting the correct ON DELETE clause, or always defaulting to NO ACTION?
  4. Inline column ref vs top-level Ref — both formats appear to be affected. Worth checking whether the two code paths (inline [ref: > ...] on a column vs a top-level Ref: block) both carry delete actions through to the writer, or only one of them.
  5. Cross-file merge path — when multiple DBML files are merged before conversion, do Ref declarations in one file that reference tables in another file survive the merge with their action modifiers intact?

Workaround

None currently. The generated SQL must be manually edited after generation to add correct ON DELETE / ON UPDATE clauses, which defeats the purpose of schema-as-code.

## Summary When converting a multi-file DBML schema to PostgreSQL DDL, relspecgo ignores the `[delete: ...]` and `[update: ...]` action modifiers on `Ref:` declarations. Every generated foreign key constraint is emitted as `ON DELETE NO ACTION ON UPDATE NO ACTION` regardless of what is specified in the source DBML. ## Environment - Tool: `relspec` (relspecgo) - Input format: `dbml` - Output format: `pgsql` - Workflow: multi-file DBML schema merged and converted to PostgreSQL DDL - Repo where this was observed: https://git.warky.dev/wdevs/amcs (PR #20) ## Reproduction ### Input — cross-file `Ref:` declarations (bottom of `schema/files.dbml`) ```dbml Table stored_files { id bigserial [pk] guid uuid [unique, not null, default: `gen_random_uuid()`] thought_id uuid [ref: > thoughts.guid] project_id uuid [ref: > projects.guid] name text [not null] ... } // Cross-file refs (for relspecgo merge) Ref: stored_files.thought_id > thoughts.guid [delete: set null] Ref: stored_files.project_id > projects.guid [delete: set null] ``` ### Input — inline column `[ref: ...]` with delete action (same-file, `schema/core.dbml`) ```dbml Table thought_links { from_id bigint [not null, ref: > thoughts.id] to_id bigint [not null, ref: > thoughts.id] relation text [not null] created_at timestamptz [default: `now()`] indexes { (from_id, to_id, relation) [pk] from_id to_id } } ``` Original migration had `ON DELETE CASCADE` for `thought_links.from_id` and `thought_links.to_id`. Top-level `Ref:` declarations with explicit delete actions (from `schema/skills.dbml`): ```dbml // Cross-file refs (for relspecgo merge) Ref: project_skills.project_id > projects.guid [delete: cascade] Ref: project_guardrails.project_id > projects.guid [delete: cascade] ``` And from `schema/meta.dbml`: ```dbml // Cross-file refs (for relspecgo merge) Ref: chat_histories.project_id > projects.guid [delete: set null] ``` ### Generated output (from `migrations/020_generated_schema.sql`) Every single FK in the output is: ```sql ADD CONSTRAINT fk_activities_family_member_id FOREIGN KEY (family_member_id) ON DELETE NO ACTION ON UPDATE NO ACTION; ADD CONSTRAINT fk_stored_files_thought_id FOREIGN KEY (thought_id) ON DELETE NO ACTION -- expected: ON DELETE SET NULL ON UPDATE NO ACTION; ADD CONSTRAINT fk_project_skills_project_id FOREIGN KEY (project_id) ON DELETE NO ACTION -- expected: ON DELETE CASCADE ON UPDATE NO ACTION; ``` No FK in the entire 4400-line generated file has anything other than `NO ACTION`, regardless of whether the Ref specifies `cascade`, `set null`, or `restrict`. ## Expected Behaviour The pgsql writer should honour the `[delete: ...]` / `[update: ...]` action modifiers from DBML `Ref:` declarations and emit the appropriate `ON DELETE` / `ON UPDATE` clauses: | DBML action | Expected SQL | |---|---| | `[delete: cascade]` | `ON DELETE CASCADE` | | `[delete: set null]` | `ON DELETE SET NULL` | | `[delete: restrict]` | `ON DELETE RESTRICT` | | `[delete: no action]` | `ON DELETE NO ACTION` | | `[update: cascade]` | `ON UPDATE CASCADE` | ## Actual Behaviour All FKs are emitted as `ON DELETE NO ACTION ON UPDATE NO ACTION` unconditionally. ## Areas to Investigate 1. **DBML reader** (`pkg/readers/dbml/`) — is the `[delete: ...]` modifier being parsed and stored on the `Relation` struct, or silently dropped? 2. **Internal relation model** — does the canonical `Relation`/`ForeignKey` struct have a field for delete/update actions? If not, it needs one. 3. **pgsql writer** (`pkg/writers/pgsql/`) — even if the action is parsed and stored, is the writer reading it and emitting the correct `ON DELETE` clause, or always defaulting to `NO ACTION`? 4. **Inline column ref vs top-level Ref** — both formats appear to be affected. Worth checking whether the two code paths (inline `[ref: > ...]` on a column vs a top-level `Ref:` block) both carry delete actions through to the writer, or only one of them. 5. **Cross-file merge path** — when multiple DBML files are merged before conversion, do Ref declarations in one file that reference tables in another file survive the merge with their action modifiers intact? ## Workaround None currently. The generated SQL must be manually edited after generation to add correct `ON DELETE` / `ON UPDATE` clauses, which defeats the purpose of schema-as-code. ## Related - Discovered while wiring relspecgo into https://git.warky.dev/wdevs/amcs (issue #19, PR #20) - The DBML spec explicitly supports action modifiers on Refs: https://dbml.dbdiagram.io/docs/#relationship-actions
Owner

Summary: The DBML parser only recognized ondelete: / onupdate: prefixes, but the standard DBML spec uses delete: and update:. Fixed by adding delete: / update: as the primary recognized prefixes (while retaining ondelete: / onupdate:
for backward compatibility). Updated the test data (complex.dbml) to use standard DBML syntax so the existing tests now verify the correct behavior.

Summary: The DBML parser only recognized ondelete: / onupdate: prefixes, but the standard DBML spec uses delete: and update:. Fixed by adding delete: / update: as the primary recognized prefixes (while retaining ondelete: / onupdate: for backward compatibility). Updated the test data (complex.dbml) to use standard DBML syntax so the existing tests now verify the correct behavior.
Sign in to join this conversation.
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wdevs/relspecgo#2