SQLite can't ALTER TABLE ADD CONSTRAINT, so foreign keys are now written as inline FOREIGN KEY clauses in CREATE TABLE instead of commented-out ALTER statements. The default schema (public/main) now produces bare table names instead of a "public_" prefix; other schemas are still prefixed to avoid collisions. Also adds direct-to-file execution: the sqlite writer can now apply generated DDL straight to a .db file via Metadata["connection_string"], wired into `relspec merge --output-conn`.
7.7 KiB
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.sessions→auth_sessions); the default schema (public/main) is left as bare table names (e.g.,public.users→users) - 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 KEYclauses inCREATE TABLE - Identifier Quoting - Properly quotes identifiers using double quotes
- Direct Execution - Can execute the generated DDL directly against a
.dbfile instead of writing a.sqlscript (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
AutoIncrementflag 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 INDEXstatements - Check Constraints: Generated as comments (should be added to CREATE TABLE manually)
- Indexes: Generated without PostgreSQL-specific features (no GIN, GiST, operator classes)
Output Structure
Generated SQL follows this order:
- Header comments
PRAGMA foreign_keys = ON;- CREATE TABLE statements (sorted by schema, then table), with primary keys and foreign keys defined inline
- CREATE INDEX statements
- CREATE UNIQUE INDEX statements (for unique constraints)
- 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 anyCREATE TABLE - Setting
Metadata["connection_string"]to a.dbfile path (or passing--output-conntorelspec merge) executes the DDL directly against that file instead of writing a.sqlscript - For complex schemas, review and test the generated SQL before use in production