Files
relspecgo/pkg/writers/sqlite
HeinandClaude Sonnet 5 ce3b615b0a feat(dbml): @postgres/@sqlite dialect directives (#19)
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
2026-09-08 16:17:37 +02:00
..

SQLite Writer

SQLite DDL (Data Definition Language) writer for RelSpec. Converts database schemas to SQLite-compatible SQL statements.

Features

  • Schema Flattening - SQLite doesn't support PostgreSQL-style schemas. Non-default schema names are flattened into table name prefixes (e.g., auth.sessionsauth_sessions); the default schema (public/main) is left as bare table names (e.g., public.usersusers)
  • Type Mapping - Converts PostgreSQL data types to SQLite type affinities (TEXT, INTEGER, REAL, NUMERIC, BLOB)
  • Auto-Increment Detection - Automatically converts SERIAL types and auto-increment columns to INTEGER PRIMARY KEY AUTOINCREMENT
  • Function Translation - Converts PostgreSQL functions to SQLite equivalents (e.g., now()CURRENT_TIMESTAMP)
  • Boolean Handling - Maps boolean values to INTEGER (true=1, false=0)
  • Constraint Generation - Creates indexes, unique constraints, and inline FOREIGN KEY clauses in CREATE TABLE
  • Identifier Quoting - Properly quotes identifiers using double quotes
  • Direct Execution - Can execute the generated DDL directly against a .db file instead of writing a .sql script (see below)

Usage

Convert PostgreSQL to SQLite

relspec convert --from pgsql --from-conn "postgres://user:pass@localhost/mydb" \
                --to sqlite --to-path schema.sql

Convert DBML to SQLite

relspec convert --from dbml --from-path schema.dbml \
                --to sqlite --to-path schema.sql

Multi-Schema Databases

SQLite doesn't support schemas, so multi-schema databases are automatically flattened. The default schema (public/main) keeps bare table names; other schemas are prefixed to avoid collisions:

# Input has auth.users and public.posts
# Output will have auth_users and posts
relspec convert --from json --from-path multi_schema.json \
                --to sqlite --to-path flattened.sql

Direct Execution Against a Database File

relspec merge can execute the generated DDL directly against a SQLite file instead of writing a .sql script, by passing the file path as --output-conn:

relspec merge --source dbml --source-path schema.dbml \
              --output sqlite --output-conn ./app.db

Passing --output-conn opens ./app.db and applies the schema directly; passing --output-path instead (or omitting --output-conn) writes a .sql script as before.

Type Mapping

PostgreSQL Type SQLite Affinity Examples
TEXT TEXT varchar, text, char, citext, uuid, timestamp, json
INTEGER INTEGER int, integer, smallint, bigint, serial, boolean
REAL REAL real, float, double precision
NUMERIC NUMERIC numeric, decimal
BLOB BLOB bytea, blob

Auto-Increment Handling

Columns are converted to INTEGER PRIMARY KEY AUTOINCREMENT when they meet these criteria:

  • Marked as primary key
  • Integer type
  • Have AutoIncrement flag set, OR
  • Type contains "serial", OR
  • Default value contains "nextval"

Example:

-- Input (PostgreSQL)
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100)
);

-- Output (SQLite)
CREATE TABLE "users" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "name" TEXT
);

Default Value Translation

PostgreSQL SQLite Notes
now(), CURRENT_TIMESTAMP CURRENT_TIMESTAMP Timestamp functions
CURRENT_DATE CURRENT_DATE Date function
CURRENT_TIME CURRENT_TIME Time function
true, false 1, 0 Boolean values
gen_random_uuid() (removed) SQLite has no built-in UUID
nextval(...) (removed) Handled by AUTOINCREMENT

Foreign Keys

SQLite has no ALTER TABLE ADD CONSTRAINT, so foreign keys are generated as inline FOREIGN KEY clauses inside CREATE TABLE, exactly as SQLite requires:

CREATE TABLE "posts" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "user_id" INTEGER NOT NULL,
    FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
);

PRAGMA foreign_keys = ON; is emitted at the top of the output (and executed first in direct-execution mode) so these constraints are actually enforced.

Constraints

  • Primary Keys: Inline for auto-increment columns, separate constraint for composite keys
  • Unique Constraints: Converted to CREATE UNIQUE INDEX statements
  • Check Constraints: Generated as comments (should be added to CREATE TABLE manually)
  • Indexes: Generated without PostgreSQL-specific features (no GIN, GiST, operator classes)

DBML dialect directives

@sqlite: directives carried on a model object's Metadata (typically from a DBML source file) are translated to SQL:

Directive Location Emitted
@sqlite: without rowid table WITHOUT ROWID table option
@sqlite: strict table STRICT table option (after WITHOUT ROWID)
@sqlite(col): collate … column COLLATE … in the column definition

Directives for other dialects (@postgres: …) are ignored. With WriterOptions.StrictDirectives (CLI --strict-directives) an untranslatable @sqlite: key is an error. Full reference: docs/DBML_DIRECTIVES.md.

Output Structure

Generated SQL follows this order:

  1. Header comments
  2. PRAGMA foreign_keys = ON;
  3. CREATE TABLE statements (sorted by schema, then table), with primary keys and foreign keys defined inline
  4. CREATE INDEX statements
  5. CREATE UNIQUE INDEX statements (for unique constraints)
  6. Check constraint comments

Example

Input (multi-schema PostgreSQL):

CREATE SCHEMA auth;
CREATE TABLE auth.users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    created_at TIMESTAMP DEFAULT now()
);

CREATE SCHEMA public;
CREATE TABLE public.posts (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES auth.users(id),
    title VARCHAR(200) NOT NULL,
    published BOOLEAN DEFAULT false
);

Output (SQLite with flattened schemas):

-- SQLite Database Schema
-- Database: mydb
-- Generated by RelSpec
-- Note: SQLite has no schema concept; non-default schema names are flattened into table name prefixes (e.g., auth.sessions -> auth_sessions)

-- Enable foreign key constraints
PRAGMA foreign_keys = ON;

-- Schema: auth (flattened into table names)

CREATE TABLE "auth_users" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "username" TEXT NOT NULL,
    "created_at" TEXT DEFAULT CURRENT_TIMESTAMP
);

CREATE UNIQUE INDEX "auth_users_users_username_key" ON "auth_users" ("username");

CREATE TABLE "posts" (
    "id" INTEGER PRIMARY KEY AUTOINCREMENT,
    "user_id" INTEGER NOT NULL,
    "title" TEXT NOT NULL,
    "published" INTEGER DEFAULT 0,
    FOREIGN KEY ("user_id") REFERENCES "auth_users" ("id")
);

Note that public.posts becomes bare posts (the default schema isn't prefixed), while auth.users becomes auth_users (a non-default schema is), and the foreign key to auth_users is defined inline rather than as a separate statement.

Programmatic Usage

import (
    "git.warky.dev/wdevs/relspecgo/pkg/models"
    "git.warky.dev/wdevs/relspecgo/pkg/writers"
    "git.warky.dev/wdevs/relspecgo/pkg/writers/sqlite"
)

func main() {
    // Create writer (automatically enables schema flattening)
    writer := sqlite.NewWriter(&writers.WriterOptions{
        OutputPath: "schema.sql",
    })

    // Write database schema
    db := &models.Database{
        Name: "mydb",
        Schemas: []*models.Schema{
            // ... your schema data
        },
    }

    err := writer.WriteDatabase(db)
    if err != nil {
        panic(err)
    }
}

Notes

  • Schema flattening is always enabled for SQLite output (cannot be disabled); the default schema (public/main) produces bare table names, other schemas are prefixed
  • Constraint and index names are prefixed with the flattened table name to avoid collisions
  • Generated SQL is compatible with SQLite 3.x
  • Foreign key constraints require PRAGMA foreign_keys = ON; to be enforced, which is emitted (and, in direct-execution mode, run) before any CREATE TABLE
  • Setting Metadata["connection_string"] to a .db file path (or passing --output-conn to relspec merge) executes the DDL directly against that file instead of writing a .sql script
  • For complex schemas, review and test the generated SQL before use in production