Skip to content

Generators ​

Generators write a table’s migration and its Model from a short description, then regenerate src/db/schema.ts. The Command line reference lists every option.

Adding a table ​

Name the Model, and list its columns, with g model:

sh
npx d1-record g model Product 'name:string!' 'priceCents:integer!=0' user:references
Wrote migrations/0003_create_products.sql
Wrote src/models/product.ts
Wrote src/db/schema.ts (3 tables) from migrations/
Next: npx wrangler d1 migrations apply my-app-db --local

Then apply the migration to your local database:

sh
npx wrangler d1 migrations apply my-app-db --local

The table is products, with an id generated in code, created_at, and updated_at.

  • --id integer gives it a key D1 generates instead.
  • --no-timestamps leaves out the timestamps.

Compared with Rails

Keys are UUIDs, not auto-incrementing integers. With --id integer, a record and the records built through it no longer save atomically. See Autosaving.

Use --remote instead of --local when you deploy.

Writing columns ​

Each column is name:type, with optional marks:

ColumnMeans
titleA string, the default type
email:string!:uniqueRequired (NOT NULL), with a unique index
count:integer!=0Required, with a default of 0
settings:jsonJSON, checked by D1 (CHECK (json_valid(settings)))
user:referencesuser_id, pointing at users, indexed; the Model gets user = belongsTo(User)

The column syntax lists every type.

TIP

Quote columns that contain !, so bash and zsh leave it alone.

Beyond Rails

Rails’ generators have no syntax for a column’s default. d1-record adds =value (=0, =true, =draft), because SQLite can’t add a required (NOT NULL) column to an existing table without one.

Changing a table ​

Write a migration with g migration. It reads what to do from the migration’s name:

sh
npx d1-record g migration AddSlugToPosts slug:string:unique   # ADD COLUMN, with a unique index
npx d1-record g migration RemoveSlugFromPosts slug             # DROP COLUMN
npx d1-record g migration BackfillSlugs                         # an empty migration to write yourself

The name can be written AddSlugToPosts or add_slug_to_posts. Apply each migration afterwards, as above.

SQLite can’t add a required column without a default, so the generator refuses one. Give it a default, slug:string!=draft, or leave out the !.

A default and a unique index don’t mix on a table with rows

g migration AddSlugToPosts 'slug:string!=draft:unique' is accepted, but applying it to a table with more than one row fails: every row gets draft, which the unique index refuses. Add the column without the index, fill it in, then add the index in a later migration.

Referencing the same table ​

g model Category parent:references points parent_id at a parents table and writes parent = belongsTo(Parent), since it can’t know the parent is another category. Edit both files before applying the migration:

  1. In the migration, point the column at categories, and make it optional, since top-level categories have no parent:

    sql
    parent_id TEXT REFERENCES categories (id),
  2. In the Model, use Category itself, and add the other direction if you want it.

ts
import { belongsTo, hasMany } from "@forestfuture/d1-record";

export class Category extends ApplicationRecord("categories") {
  parent = belongsTo(Category); // foreign key: parentId, from the association's name
  children = hasMany(Category, { foreignKey: "parentId" }); // not the default, categoryId
}

Compared with Rails

Rails writes belongs_to :parent, class_name: "Category", optional: true. Here the association takes the class itself, so there’s no class_name, and whether a parent is required comes from the column: parentId can be null because the migration allows it.

Keeping Models in another folder ​

By default, Models and application-record.ts go in src/models/. To keep them elsewhere, pass --models to every command:

sh
npx d1-record schema --models src/app/models
npx d1-record g model Product 'name:string!' --models src/app/models

TIP

d1-record schema writes application-record.ts there, and the generators write Models that import it, so both need --models. A package.json script saves retyping it: "db:schema": "d1-record schema --models src/app/models".

Keeping the schema current ​

The generators regenerate src/db/schema.ts themselves. After writing or editing a migration by hand, regenerate it:

sh
npx d1-record schema
  • --watch regenerates it whenever a migration changes.
  • --check fails when the committed schema is out of date. Run it in CI.