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
10 KiB
PostgreSQL Writer
Generates PostgreSQL DDL (Data Definition Language) SQL scripts from database schema information.
Overview
The PostgreSQL Writer converts RelSpec's internal database model representation into PostgreSQL-compatible SQL DDL scripts, including CREATE TABLE statements, constraints, indexes, views, and sequences.
Features
- Generates complete PostgreSQL DDL
- Creates schemas, tables, columns
- Defines constraints (PK, FK, unique, check)
- Creates indexes
- Generates views and sequences
- Supports migration scripts
- Includes audit triggers (optional)
- Handles PostgreSQL-specific data types
Usage
Basic Example
package main
import (
"git.warky.dev/wdevs/relspecgo/pkg/models"
"git.warky.dev/wdevs/relspecgo/pkg/writers"
"git.warky.dev/wdevs/relspecgo/pkg/writers/pgsql"
)
func main() {
options := &writers.WriterOptions{
OutputPath: "schema.sql",
}
writer := pgsql.NewWriter(options)
err := writer.WriteDatabase(db)
if err != nil {
panic(err)
}
}
CLI Examples
# Generate PostgreSQL DDL from JSON schema
relspec --input json \
--in-file schema.json \
--output pgsql \
--out-file schema.sql
# Convert GORM models to PostgreSQL DDL
relspec --input gorm \
--in-file models.go \
--output pgsql \
--out-file create_tables.sql
# Export live database schema to SQL
relspec --input pgsql \
--conn "postgres://localhost/source_db" \
--output pgsql \
--out-file backup_schema.sql
Generated SQL Example
-- Schema: public
CREATE SCHEMA IF NOT EXISTS public;
-- Table: public.users
CREATE TABLE IF NOT EXISTS public.users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
bio TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- Constraints for public.users
ALTER TABLE public.users
ADD CONSTRAINT uq_users_username UNIQUE (username);
-- Indexes for public.users
CREATE INDEX idx_users_email ON public.users (email);
-- Table: public.posts
CREATE TABLE IF NOT EXISTS public.posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
title VARCHAR(200) NOT NULL,
content TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Foreign Keys for public.posts
ALTER TABLE public.posts
ADD CONSTRAINT fk_posts_user_id
FOREIGN KEY (user_id)
REFERENCES public.users (id)
ON DELETE CASCADE
ON UPDATE NO ACTION;
-- Indexes for public.posts
CREATE INDEX idx_posts_user_id ON public.posts (user_id);
Writer Options
Metadata Options
options := &writers.WriterOptions{
OutputPath: "schema.sql",
Metadata: map[string]interface{}{
"include_drop": true, // Include DROP statements
"include_audit": true, // Include audit triggers
"if_not_exists": true, // Use IF NOT EXISTS
"migration_mode": false, // Generate migration script
},
}
Features
Full DDL Generation
Generates complete database structure:
- CREATE SCHEMA statements
- CREATE TABLE with all columns and types
- PRIMARY KEY constraints
- FOREIGN KEY constraints with actions
- UNIQUE constraints
- CHECK constraints
- CREATE INDEX statements
- CREATE VIEW statements
- CREATE SEQUENCE statements
Migration Mode
When migration_mode is enabled, generates migration scripts with:
- Version tracking
- Up/down migrations
- Transactional DDL
- Rollback support
Audit Triggers
When include_audit is enabled, adds:
- Created/updated timestamp triggers
- Audit logging functionality
- Change tracking
PostgreSQL-Specific Features
- Serial types (SERIAL, BIGSERIAL)
- Advanced types (UUID, JSONB, ARRAY)
- Schema-qualified names
- Constraint actions (CASCADE, RESTRICT, SET NULL)
- Partial indexes
- Function-based indexes
- Concurrent index creation (
CREATE INDEX CONCURRENTLY) viaIndex.Concurrent - Check constraints with expressions
- Extension types and indexes: PostGIS, pgvector, citext, hstore, ltree (see below)
- DBML dialect directives (
@postgres:— see below)
DBML dialect directives
@postgres: directives carried on a model object's Metadata (typically from a
DBML source file) are translated to SQL:
| Directive | Location | Emitted |
|---|---|---|
@postgres: partition by … |
table | PARTITION BY … on CREATE TABLE |
@postgres: inherits … |
table | INHERITS (…) |
@postgres: with (…) |
table, index | WITH (…) (on an index, overrides the comment-derived WITH) |
@postgres: tablespace … |
table, index | TABLESPACE … |
@postgres(col): storage … |
column | STORAGE … |
@postgres(col): compression … |
column | COMPRESSION … |
@postgres(col): identity always / identity by default |
column | GENERATED ALWAYS/BY DEFAULT AS IDENTITY |
Directives for other dialects (@sqlite: …) are ignored. With
WriterOptions.StrictDirectives (CLI --strict-directives) an untranslatable
@postgres: key is an error. Full reference: docs/DBML_DIRECTIVES.md.
Data Types
Supports all PostgreSQL data types:
- Integer types: SMALLINT, INTEGER, BIGINT, SERIAL, BIGSERIAL
- Numeric types: NUMERIC, DECIMAL, REAL, DOUBLE PRECISION
- String types: VARCHAR, CHAR, TEXT
- Date/Time: DATE, TIME, TIMESTAMP, TIMESTAMPTZ, INTERVAL
- Boolean: BOOLEAN
- Binary: BYTEA
- JSON: JSON, JSONB
- UUID: UUID
- Network: INET, CIDR, MACADDR
- Special: ARRAY, HSTORE
Extension Types (PostGIS, pgvector)
Extension column types are preserved verbatim, including their type modifier:
| Type | Example column type | Extension |
|---|---|---|
| PostGIS | geometry(Point,4326), geography(Point), box2d, raster |
postgis, postgis_raster, postgis_topology |
| pgvector | vector(1536), halfvec(768), sparsevec(1000) |
vector |
| Other | citext, hstore, ltree |
citext, hstore, ltree |
CREATE EXTENSION IF NOT EXISTS <ext>; is emitted automatically for every extension the
schema needs. See Extensions.
Extension Indexes
Index.Type selects the access method: gist, spgist, brin (PostGIS), hnsw, ivfflat
(pgvector), vchordrq, vchordg (VectorChord), bm25 (pg_search).
Operator class and access-method parameters ride in Index.Comment:
opclass=vector_l2_ops; with (lists=100)
opclass=<name>— used only when compatible with the column type; otherwise ignored. Bare operator class names in the comment (e.g.gin_trgm_ops) are also recognized.with (k=v, …)— rendered asWITH (k = v, …). Only well-formedkey = valuepairs are kept, so comment prose never reaches the DDL. Values may be bare (lists=100), quoted (key_field='id'), or dollar-quoted (options=$$[build.internal]$$).
Defaults when no operator class is requested:
| Access method | Column type | Emitted operator class |
|---|---|---|
hnsw, ivfflat, vchordrq, vchordg |
vector / halfvec / sparsevec / bit |
vector_cosine_ops / halfvec_cosine_ops / sparsevec_cosine_ops / bit_hamming_ops |
gist, spgist, brin |
geometry, geography |
none (PostGIS default operator class) |
gin |
text / jsonb / array |
gin_trgm_ops / jsonb_ops / array_ops |
pgvector defines no default operator class, so a vector index always names one.
CREATE INDEX IF NOT EXISTS idx_documents_embedding
ON public.documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
CREATE INDEX IF NOT EXISTS idx_documents_location
ON public.documents USING gist (location);
Migrations only recreate an index when both sides specify a hint and they differ, so a model without hints does not churn against a live database.
Extensions
CREATE EXTENSION IF NOT EXISTS <ext>; is emitted per schema, deduplicated and ordered so
dependencies come first (postgis before postgis_topology/postgis_raster/pgrouting,
vector before vchord). Names needing quoting are quoted: CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
Detection
| Source | Example | Extension |
|---|---|---|
| Column type | vector(1536), geometry(Point,4326), citext, ltree |
vector, postgis, citext, ltree |
| Index access method | hnsw, ivfflat / vchordrq, vchordg / bm25 |
vector / vchord / pg_search |
| Operator class | gin_trgm_ops, gist_ltree_ops |
pg_trgm, ltree |
| GIN/GiST on a scalar type | USING gin (views) |
btree_gin / btree_gist |
Function in a default, CHECK, index WHERE, or view body |
uuid_generate_v4(), crypt(), ST_Area(), unaccent(), json_matches_schema() |
uuid-ossp, pgcrypto, postgis, unaccent, pg_jsonschema |
gen_random_uuid() is built in since PostgreSQL 13 and does not pull in pgcrypto.
Declaring extensions explicitly
Extensions that leave no trace in the schema go in schema.Metadata["extensions"], as a list
or a comma-separated string. Dependencies are pulled in automatically; unknown names are kept
as given. The PostgreSQL reader populates this from pg_extension for the schemas it reads.
metadata:
extensions: [pg_cron, timescaledb, pg_stat_statements]
Recognized extensions
| Category | Extensions |
|---|---|
| ai/search | vector, vchord |
| document | hstore, ltree |
| federation | postgres_fdw |
| geospatial | postgis, postgis_raster, postgis_topology, pgrouting |
| indexing | btree_gin, btree_gist |
| integration | http |
| integrity | amcheck |
| jobs / scheduling | pg_background, pg_cron |
| maintenance | pg_repack, pgstattuple |
| observability | pg_qualstats, pg_stat_statements |
| partitioning | pg_partman |
| procedural | plpython3u |
| search | pg_search, pg_textsearch |
| security | pgcrypto |
| text | citext, fuzzystrmatch, pg_trgm, unaccent |
| time-series | timescaledb |
| utility | uuid-ossp |
| validation | pg_jsonschema |
Notes
- Generated SQL is formatted and readable
- Comments are preserved from source schema
- Schema names are fully qualified
- Default values are properly quoted
- Constraint names follow PostgreSQL conventions
- Compatible with PostgreSQL 12+