Skip to main content

generate:migration

Use the built-in CLI command for generating migrations to easily get up and running. Psychic will automatically timestamp the filename for your migrations, keeping them in sequential order, and will generate boilerplate to get you started with writing a migration.

pnpm psy g:migration --help
pnpm psy g:migration add-deleted-at-to-posts deleted_at:datetime:optional

Usage​

pnpm psy g:migration [options] <migrationName> [columnsWithTypes...]

Arguments​

  • <migrationName>: A kebab-case name describing the change. Include -to-<table_name> (add columns) or -from-<table_name> (remove columns) to prepopulate an alterTable command for that table.

  • [columnsWithTypes...]: Space separated snake-case (except for belongs_to model name) properties like this: title:citext subtitle:string body_markdown:text style:enum:post_styles:formal,informal User:belongs_to

    All properties default to not nullable; null can be allowed by appending :optional: subtitle:string:optional

    Column shorthand supplies a column default on its own only for non-optional boolean (false), array ('{}'), and json/jsonb ('{}') columns. Any other NOT NULL column added to a table that already has rows needs a hand-edited default or backfill — see NOT NULL columns and defaults.

    Supported types​

    • uuid, uuid[]: a column optimized for storing UUIDs
    • citext, citext[]: case insensitive text (indexes and queries are automatically case insensitive)
    • encrypted: encrypted text (used in conjunction with the @deco.Encrypted decorator)
    • string, string[]: varchar; allowed length defaults to 255, but may be customized, e.g.: subtitle:string:128 or subtitle:string:128:optional
    • text, text[]
    • boolean, boolean[]
    • date, date[]
    • datetime, datetime[]
    • time, time[]
    • timetz, timetz[]
    • integer, integer[]
    • decimal, decimal[]: precision,scale is required, e.g.: volume:decimal:3,2 or volume:decimal:3,2:optional
      • Leveraging arrays, add the "[]" suffix, e.g.: volume:decimal[]:3,2
    • jsonb: prefer this over json unless you specifically need Postgres's non-normalized json storage. Reach for either only when the data genuinely can't be modeled as columns or an associated table — see Column Types — JSON for when a jsonb column is (and isn't) the right call.
    • enum, enum[]: include the enum name to automatically create the enum: type:enum:room_types:bathroom,kitchen,bedroom or type:enum:room_types:bathroom,kitchen,bedroom:optional
      • Omit the enum values to leverage an existing enum (omits the enum type creation): type:enum:room_types or type:enum:room_types:optional
      • Leveraging arrays, add the "[]" suffix, e.g.: type:enum[]:room_types:bathroom,kitchen,bedroom
    • belongs_to: adds a foreign key to migration
      • Include the fully qualified model name, e.g., if the Coach model is in src/app/models/Health/Coach: Health/Coach:belongs_to
      • Aliased FK shorthand — append @alias to generate a foreign key with a custom column name. This is the canonical pattern for _by columns (e.g. canceled_by_id) and for multiple foreign keys to the same model: InternalUser@canceled_by:belongs_to:optional produces the canceled_by_id column and its index from one token. A standalone g:migration writes a migration and nothing else, so the canceledById property and the canceledBy association are hand-added to the model afterward; g:model and g:resource write them for you. Examples:
        • User@created_by:belongs_to — created_by_id FK, createdBy association
        • Message@last_inbound:belongs_to:optional — last_inbound_id FK, lastInbound association
        • Message@last_outbound:belongs_to:optional — last_outbound_id FK, lastOutbound association

Options​

  • --connection-name <connectionName>: The connection name you wish to use for your migration.
  • -h, --help: Display help for command.

A -to-<table_name> marker anywhere in the migration name — not just at the end — makes the generator write an alterTable for that table that adds the columns you pass, and a -from-<table_name> marker makes up drop them instead (down re-adds them). The text after the marker is snake-cased and pluralized into the table name, which is how the generator finds the table: add-phone-to-guests phone:encrypted alters guests. When a name contains both markers, -to- wins regardless of where -from- appears; -from- only takes effect when the name has no -to- at all. A name with no marker — including an underscore spelling such as add_phone_to_guests — resolves no table: with columns passed, it leaves an alterTable('<table-name>') stub to edit by hand, and with none, empty up and down bodies.

Because of this, a hand-written migration name (one with no columns passed, meant to be edited by hand) must avoid -to- and -from- entirely — either substring resolves the trailing text into a real table name and the generator fails when it can't find columns to pair with that table.

If you want to also generate a model for this migration, you can simply use the g:model or g:resource commands instead.

For schema changes that just add columns (or a foreign key) to an existing table, g:migration accepts the same column shorthand shown above — including Model:belongs_to and :optional. Hand-edit the generated migration only when the change isn't expressible as column shorthand, such as check constraints, enum alterations, or a custom backfill; see migrations for that reference.