Files
relspecgo/pkg/readers/dbml/README.md
T
warkanum b4f18f088b feat(pgsql): unique constraints on apply, matching unique index names, FK deferral
- migration writer now emits unique constraints (direct apply and live diff
  previously dropped them); idempotent via the existing DO block
- unique constraint adopts a same-named model unique index with
  ADD CONSTRAINT ... UNIQUE USING INDEX instead of building a second index
- dbml reader names unnamed unique indexes ukey_<table>_<cols> to match the
  unique constraint naming
- foreign key deferral: Constraint.Deferrable/InitiallyDeferred are *bool
  (nil = unspecified, never diffed); writers emit DEFERRABLE, DEFERRABLE
  INITIALLY DEFERRED and NOT DEFERRABLE; live reader reads condeferrable and
  condeferred; dbml reader/writer support deferrable and initially deferred
- tests and README/naming docs updated
2026-10-08 00:07:49 +02:00

5.4 KiB

DBML Reader

Reads Database Markup Language (DBML) files and extracts database schema information.

Overview

The DBML Reader parses .dbml files that define database schemas using the DBML syntax (used by dbdiagram.io) and converts them into RelSpec's internal database model representation.

Features

  • Parses DBML syntax
  • Extracts tables, columns, and relationships
  • Supports DBML-specific features:
    • Table groups and notes
    • Enum definitions
    • Indexes
    • Foreign key relationships

Usage

Basic Example

package main

import (
    "fmt"
    "git.warky.dev/wdevs/relspecgo/pkg/readers"
    "git.warky.dev/wdevs/relspecgo/pkg/readers/dbml"
)

func main() {
    options := &readers.ReaderOptions{
        FilePath: "/path/to/schema.dbml",
    }

    reader := dbml.NewReader(options)
    db, err := reader.ReadDatabase()
    if err != nil {
        panic(err)
    }

    fmt.Printf("Found %d schemas\n", len(db.Schemas))
}

CLI Example

# Read DBML file and convert to JSON
relspec --input dbml --in-file schema.dbml --output json --out-file schema.json

# Convert DBML to GORM models
relspec --input dbml --in-file database.dbml --output gorm --out-file models.go

Example DBML File

Table users {
  id bigserial [pk, increment]
  username varchar(50) [not null, unique]
  email varchar(100) [not null]
  created_at timestamp [not null, default: `now()`]

  Note: 'Users table'
}

Table posts {
  id bigserial [pk]
  user_id bigint [not null, ref: > users.id]
  title varchar(200) [not null]
  content text

  indexes {
    user_id
    (user_id, created_at) [name: 'idx_user_posts']
  }
}

Ref: posts.user_id > users.id [delete: cascade]

DBML Features Supported

  • Table definitions with columns
  • Primary keys (pk)
  • Not null constraints (not null)
  • Unique constraints (unique) → constraint ukey_<table>_<col>
  • Default values (default)
  • Inline references (ref)
  • Standalone Ref blocks
  • FK deferral: deferrable, initially deferred (see below)
  • Commented cross-file refs (// Ref: — see below)
  • Indexes and composite indexes
  • Table notes and column notes
  • Enums
  • Dialect directives (@postgres: / @sqlite: — see below)

Naming

Source Generated name
Column [unique] constraint ukey_<table>_<col>
Unnamed unique index index ukey_<table>_<cols> (same name as the constraint on the same columns)
Unnamed index idx_<table>_<cols>

Explicit name: is never changed. Leading _ in the table name is stripped.

FK deferral

col int [ref: > t.id, deferrable, initially deferred]
Ref: a.x > b.id [delete: cascade, deferrable]
Setting SQL
deferrable DEFERRABLE
initially deferred DEFERRABLE INITIALLY DEFERRED
neither unspecified (existing FK left as is)

Commented cross-file refs

// Ref: / // ref: lines (ignored by dbdiagram) become FKs + relationships once both ends are loaded.

Rule Behaviour
When Single file / directory: end of read. --from-list, merge, jobs: after all inputs are combined
Match schema.table.column on both sides, case-insensitive
Operators >, <, - (parsed like Ref:)
Duplicate of an FK on the same columns Skipped silently
Target not loaded Kept pending; skipped with a warning on the final pass
Column type mismatch Warning, FK still created (serial≈integer, bigserial≈bigint)
Pending state Database.Metadata["dbml.commented_refs"] ([]string)

API: dbml.ResolveCommentedRefs(db, final bool) []string returns warnings.

Dialect directives

Lines of the form @<namespace>[(<column>)]: <args> embed database-specific features that plain DBML cannot express (partitioning, WITHOUT ROWID, tablespaces, index storage parameters, …). They are stored losslessly on the relevant object's Metadata and round-trip unchanged through the DBML writer; the PostgreSQL and SQLite writers translate the ones they understand to SQL.

@postgres: search_path myapp

Table myapp.events {
  id bigint [pk]
  created_at timestamp [not null]
  @postgres(id): identity always
  @postgres: partition by RANGE (created_at)
  @sqlite: without rowid

  indexes {
    (created_at) [name: 'idx_events_created']
    @postgres: with (fillfactor=90)
  }
}
Position Attaches to
Before the first Table { database
Table body, no (target) that table
Table body, (col) target column col (error if unknown)
Inside indexes { } the most recently listed index entry

args is preserved verbatim; the key (lowercased first token) drives duplicate detection. Repeated directives are kept in order; catalog "singleton" keys error on a second occurrence at the same location. All errors are line-numbered.

ReaderOptions.StrictDirectives (CLI --strict-directives) turns an unknown namespace or key into an error instead of preserving it silently.

See docs/DBML_DIRECTIVES.md for the full grammar and the supported-directive matrix.

Notes

  • Column notes GENERATED ALWAYS AS (expr) STORED and GENERATED ALWAYS|BY DEFAULT AS IDENTITY set Generated/GenerationExpression and Identity/IdentityGeneration; they are not kept as comments
  • DBML is designed for database documentation and diagramming
  • Schema name defaults to public
  • Relationship cardinality is preserved