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
85 lines
2.8 KiB
Go
85 lines
2.8 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.warky.dev/wdevs/relspecgo/pkg/models"
|
|
)
|
|
|
|
// directiveNamespace is the dialect namespace this writer consumes. Directives
|
|
// for other namespaces (e.g. "postgres") are ignored and never emitted as SQL.
|
|
const directiveNamespace = "sqlite"
|
|
|
|
// sqliteHandledDirectives maps a directive location to the set of sqlite keys
|
|
// this writer knows how to translate. In strict mode an unknown key for this
|
|
// namespace at a supported location is a hard error.
|
|
var sqliteHandledDirectives = map[string]map[string]bool{
|
|
models.DirectiveLocationTable: {"without": true, "strict": true},
|
|
models.DirectiveLocationColumn: {"collate": true},
|
|
}
|
|
|
|
// checkDirectives validates sqlite directives across a schema when strict mode
|
|
// is enabled. With strict mode off it is a no-op.
|
|
func (w *Writer) checkDirectives(schema *models.Schema) error {
|
|
if w.options == nil || !w.options.StrictDirectives {
|
|
return nil
|
|
}
|
|
for _, table := range schema.Tables {
|
|
if err := checkObjectDirectives(table.Metadata, models.DirectiveLocationTable, table.Name); err != nil {
|
|
return err
|
|
}
|
|
for _, col := range table.Columns {
|
|
if err := checkObjectDirectives(col.Metadata, models.DirectiveLocationColumn, table.Name+"."+col.Name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, idx := range table.Indexes {
|
|
if err := checkObjectDirectives(idx.Metadata, models.DirectiveLocationIndex, idx.Name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkObjectDirectives(meta map[string]any, location, owner string) error {
|
|
for _, d := range models.DirectivesForNamespace(meta, directiveNamespace) {
|
|
if !sqliteHandledDirectives[location][d.Key] {
|
|
return fmt.Errorf("sqlite: %s: unsupported @sqlite directive %q at %s level (strict mode)", owner, d.Key, location)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sqliteTableOptions returns the trailing table-option clause for a CREATE TABLE
|
|
// statement, e.g. "WITHOUT ROWID, STRICT". WITHOUT ROWID is emitted before
|
|
// STRICT, matching SQLite's own grammar ordering.
|
|
func sqliteTableOptions(table *models.Table) string {
|
|
var opts []string
|
|
if models.HasDirective(table.Metadata, directiveNamespace, "without") {
|
|
opts = append(opts, "WITHOUT ROWID")
|
|
}
|
|
if models.HasDirective(table.Metadata, directiveNamespace, "strict") {
|
|
opts = append(opts, "STRICT")
|
|
}
|
|
return strings.Join(opts, ", ")
|
|
}
|
|
|
|
// sqliteColumnCollate returns a " COLLATE <name>" clause for a column carrying an
|
|
// @sqlite(col): collate <name> directive, or "".
|
|
func sqliteColumnCollate(col *models.Column) string {
|
|
for _, d := range models.DirectivesForNamespace(col.Metadata, directiveNamespace) {
|
|
if d.Key != "collate" {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(d.Args), "collate"))
|
|
name = strings.TrimSpace(name)
|
|
if name == "" {
|
|
return ""
|
|
}
|
|
return " COLLATE " + name
|
|
}
|
|
return ""
|
|
}
|