# 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 |

```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 <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.
