-
Naming is identical to postgres-dba. snake_case, lowercase,
unquoted. Tables plural (orders), columns singular (shipped_at),
primary key literally id, FKs <table_singular>_id, booleans
is_/has_, timestamps past-tense _at. In Drizzle the TypeScript
property may be camelCase, but the column name argument is always
the snake_case name — createdAt: integer('created_at', …). The
database never sees a quoted mixed-case identifier. See
references/naming.md.
-
bun:sqlite + drizzle-orm/bun-sqlite, one Database per process.
That is the dev driver. Every connection runs the pragma block (Rule 3)
at open. SQLite has one writer at a time; a single shared Database
instance plus serialized writes is the model — do not invent a pool of
writers. See references/drizzle.md.
-
Pragmas are mandatory and per-connection. Every connection sets, in
order: journal_mode = WAL, foreign_keys = ON, busy_timeout = 5000,
synchronous = NORMAL. foreign_keys is OFF by default in SQLite —
forgetting it silently disables every ON DELETE you wrote. This is the
single most common SQLite data-integrity bug. See references/drizzle.md.
-
Tables are STRICT, types come from the Postgres-portable set only.
Declare STRICT (SQLite ≥ 3.37) so a column actually rejects wrong
types instead of silently coercing. Use only the type vocabulary in
references/types.md: INTEGER, TEXT, REAL, BLOB mapped to a
concrete Postgres target. No VARCHAR(n), no DATETIME, no BOOLEAN,
no NUMERIC — those are affinity theater in SQLite and lie about the
Postgres column you will eventually create.
-
FKs are NOT NULL by default with explicit ON DELETE. Same rule
as postgres-dba Rule 4. A nullable FK carries a
// nullable-fk: <reason> comment or it is a defect. Many-to-many is a
compound primary key, no surrogate id on a pure junction. See
references/portability.md.
-
Enums are text + a CHECK (col IN (...)), mirroring the Postgres
enum. SQLite has no enum type. Use Drizzle text({ enum: [...] })
and an explicit check constraint so the values match the
CREATE TYPE you will write in Postgres. Booleans are
integer({ mode: 'boolean' }) + CHECK (col IN (0,1)). See
references/portability.md.
-
Timestamps are one chosen representation, app-set, always UTC.
SQLite has no timestamptz. Default: integer epoch-milliseconds via
Drizzle { mode: 'timestamp_ms' }, set by the app, never
CURRENT_TIMESTAMP (which emits a non-ISO, tz-ambiguous string). This
ports to Postgres timestamptz mechanically. See references/types.md.
-
JSON is text({ mode: 'json' }) with a relational spine, mirroring
the JSONB rule. Keys/FKs/hot fields are real columns; open-ended data
is a JSON document, hot fields lifted out via a GENERATED ... STORED
column and indexed. This is postgres-dba Rule 8 with jsonb → text
JSON. See references/portability.md.
-
Set-based / multi-row business logic lives in an explicit
transaction in a named module — not scattered across callers. This
is the one place SQLite cannot match postgres-dba Rule 7 (no
plpgsql). Quarantine that logic in one repository/service function
wrapped in a db.transaction(...) so the Postgres cutover has exactly
one place to consider promoting to a function. See
references/portability.md.