Add parseable `@<namespace>[(<target>)]: <args>` directives embedded in DBML.
They are stored losslessly on each object's Metadata, round-trip unchanged
through the DBML writer, and are translated to SQL only by the writer for the
matching dialect.
- models: Directive type + catalog; Metadata map added to Column and Index
- dbml reader: parse and attach directives at database/table/column/index
level; line-numbered errors; repeatable by default with singleton duplicate
detection. Fixes a preexisting bug where an `indexes {}` closing brace ended
the table early, dropping trailing Note: and directive lines.
- dbml writer: re-emit directives at their location; idempotent output
- pgsql writer: PARTITION BY / INHERITS / WITH / TABLESPACE (table),
STORAGE / COMPRESSION / identity (column), WITH / TABLESPACE (index)
- sqlite writer: WITHOUT ROWID / STRICT (table), COLLATE (column)
- --strict-directives flag on ReaderOptions and WriterOptions
- docs/DBML_DIRECTIVES.md + reader/writer READMEs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ss2MY5J11cRGwEz86ZXk7d
197 lines
4.3 KiB
Markdown
197 lines
4.3 KiB
Markdown
# DBML Writer
|
|
|
|
Generates Database Markup Language (DBML) files from database schema information.
|
|
|
|
## Overview
|
|
|
|
The DBML Writer converts RelSpec's internal database model representation into DBML syntax, suitable for use with dbdiagram.io and other DBML-compatible tools.
|
|
|
|
## Features
|
|
|
|
- Generates DBML syntax
|
|
- Creates table definitions with columns
|
|
- Defines relationships
|
|
- Includes indexes
|
|
- Adds notes and documentation
|
|
- Supports enums
|
|
|
|
## Usage
|
|
|
|
### Basic Example
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/writers"
|
|
"git.warky.dev/wdevs/relspecgo/pkg/writers/dbml"
|
|
)
|
|
|
|
func main() {
|
|
options := &writers.WriterOptions{
|
|
OutputPath: "schema.dbml",
|
|
}
|
|
|
|
writer := dbml.NewWriter(options)
|
|
err := writer.WriteDatabase(db)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### CLI Examples
|
|
|
|
```bash
|
|
# Generate DBML from PostgreSQL database
|
|
relspec --input pgsql \
|
|
--conn "postgres://localhost/mydb" \
|
|
--output dbml \
|
|
--out-file schema.dbml
|
|
|
|
# Convert GORM models to DBML
|
|
relspec --input gorm --in-file models.go --output dbml --out-file database.dbml
|
|
|
|
# Convert JSON to DBML for visualization
|
|
relspec --input json --in-file schema.json --output dbml --out-file diagram.dbml
|
|
```
|
|
|
|
## Generated DBML Example
|
|
|
|
```dbml
|
|
Project MyDatabase {
|
|
database_type: 'PostgreSQL'
|
|
}
|
|
|
|
Table users {
|
|
id bigserial [pk, increment]
|
|
username varchar(50) [not null, unique]
|
|
email varchar(100) [not null]
|
|
bio text [null]
|
|
created_at timestamp [not null, default: `now()`]
|
|
|
|
Note: 'Users table'
|
|
|
|
indexes {
|
|
email [name: 'idx_users_email']
|
|
}
|
|
}
|
|
|
|
Table posts {
|
|
id bigserial [pk, increment]
|
|
user_id bigint [not null]
|
|
title varchar(200) [not null]
|
|
content text [null]
|
|
created_at timestamp [default: `now()`]
|
|
|
|
indexes {
|
|
user_id [name: 'idx_posts_user_id']
|
|
(user_id, created_at) [name: 'idx_posts_user_created']
|
|
}
|
|
}
|
|
|
|
Ref: posts.user_id > users.id [delete: cascade, update: no action]
|
|
```
|
|
|
|
## DBML Features
|
|
|
|
### Table Definitions
|
|
```dbml
|
|
Table table_name {
|
|
column_name type [attributes]
|
|
}
|
|
```
|
|
|
|
### Column Attributes
|
|
- `pk` - Primary key
|
|
- `increment` - Auto-increment
|
|
- `not null` - NOT NULL constraint
|
|
- `null` - Nullable (explicit)
|
|
- `unique` - Unique constraint
|
|
- `default: value` - Default value
|
|
- `note: 'text'` - Column note
|
|
|
|
### Relationships
|
|
```dbml
|
|
Ref: table1.column > table2.column
|
|
Ref: table1.column < table2.column
|
|
Ref: table1.column - table2.column
|
|
```
|
|
|
|
Relationship types:
|
|
- `>` - Many-to-one
|
|
- `<` - One-to-many
|
|
- `-` - One-to-one
|
|
|
|
Relationship actions:
|
|
```dbml
|
|
Ref: posts.user_id > users.id [delete: cascade, update: restrict]
|
|
```
|
|
|
|
### Indexes
|
|
```dbml
|
|
indexes {
|
|
column_name
|
|
(column1, column2) [name: 'idx_name', unique]
|
|
}
|
|
```
|
|
|
|
### Dialect directives
|
|
|
|
Dialect directives stored on a model object's `Metadata` (namespace `postgres`,
|
|
`sqlite`, …) are re-emitted verbatim, one line per directive, at the location
|
|
they belong to:
|
|
|
|
```dbml
|
|
@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)
|
|
}
|
|
}
|
|
```
|
|
|
|
| Emitted at | From |
|
|
|------------|------|
|
|
| Before the first table | `Database.Metadata` |
|
|
| After a column line, as `@ns(col): …` | `Column.Metadata` |
|
|
| After an index line, inside `indexes { }` | `Index.Metadata` |
|
|
| After the `indexes` block, before `Note:` | `Table.Metadata` |
|
|
|
|
Output is deterministic (ordered by namespace, then source line, then args), so a
|
|
`DBML → model → DBML` round-trip is idempotent. See
|
|
[`docs/DBML_DIRECTIVES.md`](../../../docs/DBML_DIRECTIVES.md) for the grammar and
|
|
the list of directives the PostgreSQL and SQLite writers translate to SQL.
|
|
|
|
## Type Mapping
|
|
|
|
| SQL Type | DBML Type |
|
|
|----------|-----------|
|
|
| bigint | bigint |
|
|
| integer | int |
|
|
| varchar(n) | varchar(n) |
|
|
| text | text |
|
|
| boolean | boolean |
|
|
| timestamp | timestamp |
|
|
| date | date |
|
|
| json | json |
|
|
| uuid | uuid |
|
|
|
|
## Notes
|
|
|
|
- DBML is designed for database visualization
|
|
- Can be imported into dbdiagram.io
|
|
- Human-readable format
|
|
- Schema names can be included in table names
|
|
- Comments and notes are preserved
|
|
- Ideal for documentation and sharing designs
|