Command line
The command line generates src/db/schema.ts from your migrations, and writes new migrations and Models. Run it from the folder with your Wrangler config:
npx d1-record --helpIt needs Node.js 22.13 or later, for Node’s built-in SQLite. Generators shows it step by step.
d1-record schema
Generate the schema file from your migrations:
npx d1-record schemaIt applies your migrations, in order, to an in-memory SQLite database, and writes each table’s columns to src/db/schema.ts.
| Option | |
|---|---|
--check | Write nothing: fail (exit 1) if the schema file is out of date. For CI. |
--watch | Regenerate whenever a migration changes. |
--out <file> | Write the schema here instead. With several databases, a folder for one file each. |
--migrations <dir> | Read migrations from here instead of the Wrangler config’s folder (one database only). |
--models <dir> | Where application-record.ts goes (src/models/ by default). |
--config <file> | Use this Wrangler config instead of finding wrangler.jsonc, wrangler.json, or wrangler.toml. |
What it reads: each entry of the Wrangler config’s d1_databases, with its binding, migrations_dir (default migrations/), migrations_pattern (for migrations in folders), and migrations_table (left out of the schema). Migrations apply in file-name order, as Wrangler applies them.
What it writes:
- The schema file: each table’s attributes, as a document, with a header naming the migrations and a fingerprint of the schema.
--checkcompares fingerprints, so formatting the file doesn’t make it stale. - The base class,
application-record.ts, when it’s missing:ApplicationRecord = modelFor(tables, Base), with the binding set onBase. It’s never written over, so it’s yours to edit. - With several databases: one schema file per binding (
src/db/DB.ts,src/db/LOGS.ts), and one base class each, named after it (DB→DbRecord,LOGS→LogsRecord).
Compared with Rails
The schema file plays the part of Rails’ schema.rb, and each base class is named as Rails names a second database’s.
What it refuses, writing nothing:
- A file it didn’t write (one without its “Generated by” header), such as a Drizzle schema.
- A file beside a folder of the same name (
src/db/schema.tsnext tosrc/db/schema/), which would take over that folder’s imports. - Two bindings whose names would give the same base class (
DBanddb). - A migration SQLite rejects: the error names the file, and the last good schema is kept.
Coming from Drizzle or Prisma?
Their schemas often live at src/db/schema.ts or src/db/schema/. Write d1-record’s somewhere else, with --out src/db/d1-record.ts, and, if application-record.ts already exists, point its import { tables } at it (a new one is written importing the right file).
d1-record console
Open your database with your Models in a console, or d1-record c for short (Console shows a session). It opens the local one unless you pass --remote:
npx d1-record console| Option | |
|---|---|
--database <name> | The D1 binding to open, when the config has several. |
--models <path> | Where the Models are: a folder (src/models) or one file (src/models.ts). |
--config <file> | Use this Wrangler config instead of finding wrangler.jsonc, wrangler.json, or .toml. |
--no-history | Don’t keep what you type in .wrangler/d1-record-history. |
--verbose | Print each statement a line runs. .verbose turns it on and off in the console. |
-e, --eval <code> | Run this code, print its result (alone, on stdout), and exit; a failure exits 1. |
--remote | Open your remote database instead, after you type its name. |
--yes | With --remote, don’t ask first (also how to open it without a terminal). |
--history | Keep what you type in a remote session, which isn’t kept unless you ask. |
It reads TypeScript Models and uses your project’s own wrangler and @forestfuture/d1-record. It refuses, printing why, when there’s no Wrangler config, with several databases and no --database, with no Models, with wrangler or the library not installed, or with --remote when the binding has no database_id, or when you don’t confirm it (or aren’t on a terminal and didn’t pass --yes). --yes only goes with --remote, --history can’t be combined with --no-history, and -e takes one piece of code, once.
Mapping columns to attributes
Each column becomes an attribute by the first of these that applies:
An override in the Model:
ApplicationRecord("products", { available: "boolean" }).The column’s declared type:
Declared as Attribute INTEGER,INT,BIGINT, … (anything withINT)integerTEXT,VARCHAR(…),CHAR(…),CLOBstringREAL,FLOAT,DOUBLErealBOOLEANbooleanDATETIME,TIMESTAMP,DATEdatetimeTEXTwithCHECK (json_valid(column))jsonJSONjsonNUMERIC,DECIMALAn error: store money as integer cents ( price_cents INTEGER)BLOBAn error: not supported yet anything else stringThe column’s name, only for a column declared exactly
TEXTorINTEGER:…_atTEXTis adatetime, andis_…orhas_…INTEGERis aboolean(with a note, since it’s a guess).
Also:
- Names:
price_centsbecomespriceCents. A column whose name doesn’t come back the same from camelCase getscolumn: "…". NOT NULLbecomesnull: false. A primary key is never null.- Primary keys: the key column is the Model’s
primaryKey. ATEXTkey getsgenerateId: true, so the Model’sgenerateIdmakes it; anINTEGER PRIMARY KEYis left to D1. A table with no key, or a key of several columns, is an error. - Defaults: literal defaults (numbers, strings,
TRUE/FALSE, JSON) are copied, so a new record shows them before it’s saved. Expressions such asCURRENT_TIMESTAMPare left to D1. - Foreign keys take the referenced column’s type. They don’t create associations, which are behavior you declare.
- Left out: SQLite’s own tables, Cloudflare’s (
_cf_…), the migrations table, and virtual tables.
Notes it prints when the schema is written:
- an
INTEGERread as abooleanfrom its name; - for migrations written by Drizzle or Prisma (recognized by their config files or the lines they write): a note naming the tool, then one for each column worth checking, such as an
INTEGERnamed…_atthat may hold a Unix timestamp, aTEXTnamed like JSON without ajson_validcheck, or, for Prisma, eachdatetime; - migrations in folders with no
migrations_patternto find them.
d1-record g model and g migration
npx d1-record g model <Name> [columns] [options]
npx d1-record g migration <Name> [columns] [options]generate is the same as g.
| Option | |
|---|---|
--id integer|text | integer: an INTEGER PRIMARY KEY that D1 generates. text (the default): a TEXT key generated in code. |
--no-timestamps | Leave out created_at and updated_at. |
--models <path> | Write Models in this folder (importing application-record.ts from it), or append them to one file (src/models.ts). |
--database <binding> | The D1 database to use, when the Wrangler config has several. |
--out <file> | The schema file to regenerate, as for d1-record schema (for a Drizzle project’s src/db/d1-record.ts, say). |
--config <file> | Use this Wrangler config. |
Column syntax
name[:type][!][=value][:unique|:index]
| Type | SQL |
|---|---|
string (the default) | TEXT |
integer | INTEGER |
real | REAL |
boolean | BOOLEAN |
datetime | DATETIME |
json | TEXT CHECK (json_valid(column)) |
references | <name>_id, typed like the referenced table’s key, REFERENCES <plural> (id), and indexed |
!makes the columnNOT NULL. Areferencescolumn isNOT NULLin a new table, and can beNULLwhen added to an existing one.=valueis a literal default, forstring,integer,real, andbooleancolumns (=draft,=0,=1.5,=true).:uniqueadds a unique index, and:indexan index, named likeindex_products_on_sku.- Names can be written
priceCentsorprice_cents; columns are always snake_case. Names SQLite reserves (order,group) are quoted.
Beyond Rails
=value is d1-record’s own addition: Rails’ generators can’t give a column a default.
Migration names
| Name | Writes |
|---|---|
Create<Table> | CREATE TABLE, as g model does |
Add<Anything>To<Table> | ADD COLUMN for each column, then their indexes |
Remove<Anything>From<Table> | DROP INDEX IF EXISTS for each column’s generated index, then DROP COLUMN |
| anything else | An empty migration, to write yourself |
A name can be written AddSkuToProducts or add_sku_to_products, and the table in it is made plural (CreateWidget creates widgets). A Model’s name is made singular, with a note (g model line_items writes LineItem). Names are letters, digits, and _, starting with a letter.
File names are numbered as Wrangler numbers them: the highest number in the migrations folder plus one, in four digits (0008_add_sku_to_products.sql).
What’s checked first
Nothing is written unless all of these hold:
- the new migration applies after the existing ones (in memory, as
d1-record schemaapplies them); - the Model’s file doesn’t exist yet (or, with one models file, doesn’t have the class yet);
- the config’s
migrations_pattern, if it has one, would match the new file, so Wrangler would apply it; - the schema file can be written (see What it refuses);
- it isn’t a required column added without a default.
Then the generator writes the files, regenerates the schema, and prints the next step: npx wrangler d1 migrations apply <database_name> --local.
Exit codes
Every command exits with 0 on success, and 1 when anything was refused or failed, or, with --check, when the schema is out of date. Messages go to standard output, and say what to do next.