Skip to content

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:

sh
npx d1-record --help

It 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:

sh
npx d1-record schema

It applies your migrations, in order, to an in-memory SQLite database, and writes each table’s columns to src/db/schema.ts.

Option
--checkWrite nothing: fail (exit 1) if the schema file is out of date. For CI.
--watchRegenerate 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. --check compares 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 on Base. 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.ts next to src/db/schema/), which would take over that folder’s imports.
  • Two bindings whose names would give the same base class (DB and db).
  • 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:

sh
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-historyDon’t keep what you type in .wrangler/d1-record-history.
--verbosePrint 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.
--remoteOpen your remote database instead, after you type its name.
--yesWith --remote, don’t ask first (also how to open it without a terminal).
--historyKeep 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:

  1. An override in the Model: ApplicationRecord("products", { available: "boolean" }).

  2. The column’s declared type:

    Declared asAttribute
    INTEGER, INT, BIGINT, … (anything with INT)integer
    TEXT, VARCHAR(…), CHAR(…), CLOBstring
    REAL, FLOAT, DOUBLEreal
    BOOLEANboolean
    DATETIME, TIMESTAMP, DATEdatetime
    TEXT with CHECK (json_valid(column))json
    JSONjson
    NUMERIC, DECIMALAn error: store money as integer cents (price_cents INTEGER)
    BLOBAn error: not supported yet
    anything elsestring
  3. The column’s name, only for a column declared exactly TEXT or INTEGER: …_at TEXT is a datetime, and is_… or has_… INTEGER is a boolean (with a note, since it’s a guess).

Also:

  • Names: price_cents becomes priceCents. A column whose name doesn’t come back the same from camelCase gets column: "…".
  • NOT NULL becomes null: false. A primary key is never null.
  • Primary keys: the key column is the Model’s primaryKey. A TEXT key gets generateId: true, so the Model’s generateId makes it; an INTEGER PRIMARY KEY is 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 as CURRENT_TIMESTAMP are 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 INTEGER read as a boolean from 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 INTEGER named …_at that may hold a Unix timestamp, a TEXT named like JSON without a json_valid check, or, for Prisma, each datetime;
  • migrations in folders with no migrations_pattern to find them.

d1-record g model and g migration ​

sh
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|textinteger: an INTEGER PRIMARY KEY that D1 generates. text (the default): a TEXT key generated in code.
--no-timestampsLeave 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]

TypeSQL
string (the default)TEXT
integerINTEGER
realREAL
booleanBOOLEAN
datetimeDATETIME
jsonTEXT CHECK (json_valid(column))
references<name>_id, typed like the referenced table’s key, REFERENCES <plural> (id), and indexed
  • ! makes the column NOT NULL. A references column is NOT NULL in a new table, and can be NULL when added to an existing one.
  • =value is a literal default, for string, integer, real, and boolean columns (=draft, =0, =1.5, =true).
  • :unique adds a unique index, and :index an index, named like index_products_on_sku.
  • Names can be written priceCents or price_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 ​

NameWrites
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 elseAn 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 schema applies 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.