# 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: ``` @[()]: ``` | 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 | ```dbml @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 ` appended to `CREATE TABLE` | e.g. `@postgres: partition by RANGE (created_at)` | | `inherits` | table | `INHERITS ()` — args verbatim | | | `with` | table, index | `WITH ()` | On an index, wins over `WITH` derived from the index comment. `@postgres: with (fillfactor=90)` | | `tablespace` | table, index | `TABLESPACE ` | Emitted after `WITH`, before `WHERE` on indexes | | `storage` | column | `STORAGE ` in the column definition | e.g. `@postgres(blob): storage external` | | `compression` | column | `COMPRESSION ` | | | `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 ` 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.