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
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 → DBMLround-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:
postgrespartition,tablespace,inherits,storage,compression,identity;sqlitewithout,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 anindexesblock- 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 always → GENERATED ALWAYS AS IDENTITY; identity default / identity by default → GENERATED 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.