Files
relspecgo/docs/DBML_DIRECTIVES.md
T
HeinandClaude Sonnet 5 ce3b615b0a feat(dbml): @postgres/@sqlite dialect directives (#19)
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
2026-09-08 16:17:37 +02:00

4.9 KiB

DBML Dialect Directives

DBML has no dialect-neutral way to express database-specific features such as PostgreSQL table partitioning or SQLite WITHOUT ROWID. RelSpec adds dialect directives — explicit, parseable lines embedded in a .dbml file that are:

  • stored losslessly in the intermediate model (under each object's Metadata),
  • preserved unchanged through a DBML → model → DBML round-trip,
  • translated to SQL only by the writer for the matching dialect (@postgres: clauses appear in PostgreSQL output, never in SQLite output, and vice-versa).

Grammar

A directive is a single line, matched on its trimmed content:

@<namespace>[(<target>)]: <args>
Part Rules
namespace ^[a-z][a-z0-9_]*$ — e.g. postgres, sqlite. Future dialects allowed.
(target) Optional. A column name only, valid only on a directive line inside a table body. Bare or single/double quoted.
args Everything after the first :, trimmed. Otherwise preserved verbatim. Must be non-empty.

The key of a directive is derived: the lowercased first whitespace-delimited token of args (partition by RANGE (created_at)partition). It drives duplicate detection and writer dispatch.

Location

Where the line appears determines which object it attaches to:

Position in the file Attaches to
Before the first Table { database (db.Metadata)
Table body, no (target) that table
Table body, (col) target column col of that table (error if col is unknown)
Inside an indexes { } block the most recently listed index entry in that block; (target) is not allowed
@postgres: search_path myapp          -- database

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

  indexes {
    (created_at) [name: 'idx_events_created']
    @postgres: with (fillfactor=90)   -- index "idx_events_created"
    @postgres: tablespace idx_space    -- index "idx_events_created"
  }
}

Duplicate policy

  • Repeatable by default — every directive with the same (namespace, key) at one location is kept, in source order.
  • Singletons raise a line-numbered error on a second occurrence at the same location. Current singletons: postgres partition, tablespace, inherits, storage, compression, identity; sqlite without, strict, collate.

Strict mode

CLI flag --strict-directives (also ReaderOptions.StrictDirectives / WriterOptions.StrictDirectives):

  • Reader: an unknown namespace or key is a hard error. Without strict mode it is stored and preserved silently, and round-trips unchanged.
  • PostgreSQL / SQLite writer: a directive for that writer's own dialect whose key it cannot translate is a hard error. Without strict mode, translatable keys are emitted and the rest are skipped. Directives for other dialects are always ignored, never emitted.

Errors

All are line-numbered (dbml: line N: …):

  • no colon, or empty args
  • namespace empty or not matching [a-z][a-z0-9_]*
  • (target) naming an unknown column, or used at the top level / in an indexes block
  • a directive in the catalog used at a location it is not valid for
  • duplicate singleton at the same location
  • (strict mode) unknown (namespace, key)

Supported directive matrix

@postgres

Key Locations SQL emitted Notes
partition table PARTITION BY <args> appended to CREATE TABLE e.g. @postgres: partition by RANGE (created_at)
inherits table INHERITS (<args>) — args verbatim
with table, index WITH (<params>) On an index, wins over WITH derived from the index comment. @postgres: with (fillfactor=90)
tablespace table, index TABLESPACE <name> Emitted after WITH, before WHERE on indexes
storage column STORAGE <mode> in the column definition e.g. @postgres(blob): storage external
compression column COMPRESSION <method>
identity column identity alwaysGENERATED ALWAYS AS IDENTITY; identity default / identity by defaultGENERATED BY DEFAULT AS IDENTITY

@sqlite

Key Locations SQL emitted Notes
without table WITHOUT ROWID table option @sqlite: without rowid
strict table STRICT table option WITHOUT ROWID is emitted before STRICT
collate column COLLATE <name> in the column definition e.g. @sqlite(name): collate NOCASE

Unknown namespaces and keys not in these tables are still preserved losslessly (and round-trip through the DBML writer) whenever strict mode is off.