prorm.sync(), which reconciles tables directly from your models,Migrator + QueryInterface), which recordssync() is convenient for local development and tests. Migrations are what you want forprorm.sync()sync() walks every model registered with prorm.define(...) and makes the databaseSyncOptions:import { Prorm, DataTypes } from 'ts-prorm-orm';
const prorm = new Prorm({
dialect: 'sqlite',
storage: ':memory:',
});
const User = prorm.define('User', {
name: { type: DataTypes.STRING, allowNull: false },
email: { type: DataTypes.STRING, unique: true },
});
// Default mode: create any missing tables, leave existing ones untouched.
await prorm.sync();sync() returns the Prorm instance, so it is chainable, and it runs beforeSync /afterSync hooks (globally and per model) unless you pass hooks: false.force wins over alter, and with neither set// Default: only CREATE TABLE for tables that don't exist yet. Never drops data.
await prorm.sync();
// force: DROP every table (in reverse order, CASCADE) and recreate from scratch.
// Destroys all data — great for tests, never for production.
await prorm.sync({ force: true });
// alter: keep existing tables and data, but add/remove/modify columns to match models.
await prorm.sync({ alter: true });[!CAUTION] sync({ force: true })drops every model's table, withCASCADE, before
recreating it. Pointed at the wrongDATABASE_URLit empties production in one
call. Guard it withmatch(below) and never run it from application start-up.
alter also accepts an object to control what happens to columns that are absentalter: true never drops: it logs a warning naming each unknown// The same as alter: true — add and change columns, warn about unknown ones.
await prorm.sync({ alter: { drop: false } });
// Really drop columns that are no longer in the model.
await prorm.sync({ alter: { drop: true } });sync({ alter }) whatever drop says.alter: { drop: 'soft' } keeps the data while getting the column out of the_prorm_dropped_<col>_<YYYYMMDD>,prorm_dropped_columnsalter: { drop: 'hard' }true) really drops.await prorm.sync({ alter: { drop: 'soft' } });
const parked = await prorm.listSoftDroppedColumns('users');
// [{ table: 'users', archivedName: '_prorm_dropped_nickname_20260926',
// originalName: 'nickname', droppedAt: Date, dialect: 'sqlite' }]
await prorm.restoreSoftDroppedColumn('users', 'nickname'); // rename it back
await prorm.purgeSoftDroppedColumns('users'); // DROP the parked columns for good{ id: 1, email: 'a@x.test', _prorm_dropped_nickname_20260926: 'ace', createdAt: …, updatedAt: … }// Drop only what was parked more than 30 days ago; returns what it dropped.
const purged = await prorm.purgeSoftDroppedColumns(undefined, { olderThan: '30d' });
for (const c of purged) console.log(`dropped ${c.table}.${c.archivedName}`);[!WARNING] restoreSoftDroppedColumn()refuses when the table has a column with the
original name again — a model that re-declarednicknameand synced has
created a new, empty one. Rename or drop that column first, or you would have
to choose which of the two to lose.
sync({ alter }) takes for a column the model'hard' branch (and purge) destroysalter: true warn-and-keep behaviour is.SyncOptions| Option | Type | Default | Purpose |
|---|---|---|---|
force | boolean | false | Drop and recreate all tables. |
alter | boolean | { drop?: boolean | 'soft' | 'hard' } | false | Reconcile columns in place, preserving data. true warns about unknown columns and keeps them; 'soft' / 'hard' are 4.9.2+. |
match | RegExp | – | Only sync if the configured database name matches. |
logging | boolean | (sql, timing?) => void | false | Log the SQL that sync emits. |
hooks | boolean | true | Run beforeSync / afterSync hooks. |
indexes | boolean | true | Also sync indexes. |
constraints | boolean | true | Also sync constraints. |
match is a safety valve: sync silently no-ops unless the database name matches theforce runs.// Only ever wipe databases whose name contains "test".
await prorm.sync({ force: true, match: /test/ });up and down. Each receivesQueryInterface (the schema-manipulation API) and the Prorm instance:// migrations/20240315120000-create-users-table.js
module.exports = {
up: async (queryInterface, prorm) => {
await queryInterface.createTable('users', {
id: { type: 'INTEGER', primaryKey: true, autoIncrement: true },
name: { type: 'VARCHAR(255)', allowNull: false },
email: { type: 'VARCHAR(255)', allowNull: false },
createdAt: { type: 'DATETIME', allowNull: false },
});
await queryInterface.addIndex('users', 'users_email_idx', ['email'], { unique: true });
},
down: async (queryInterface, prorm) => {
await queryInterface.dropTable('users');
},
};down should undo exactly what up did, in reverse. If a migration has no meaningfuldown, but then reverting it only removes the tracking row without<timestamp>-<name> where the timestamp is a 14-digitYYYYMMDDHHMMSS value, e.g. 20240315120000-create-users-table.js. Migrations are run inlocaleCompare), so the timestamp prefix is what.js and .ts migrations are both supported. TypeScript files are loaded throughts-node, which is registered lazily the first time a .ts migration is loaded; ifts-node is not installed you'll get a clear error telling you to install it or use .js.up and down directly; the loader acceptsmodule.exports = { up, down }, named exports and a default export alike.import { DataTypes, type Prorm, type QueryInterface } from 'ts-prorm-orm';
export async function up(queryInterface: QueryInterface, prorm: Prorm): Promise<void> {
await queryInterface.createTable('orders', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
userId: {
type: DataTypes.INTEGER,
allowNull: false,
references: { model: 'users', key: 'id', onDelete: 'CASCADE' },
},
totalCents: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0 },
placedAt: { type: DataTypes.DATE, allowNull: false },
});
await queryInterface.addIndex('orders', 'orders_user_placed_idx', ['userId', 'placedAt']);
}
export async function down(queryInterface: QueryInterface): Promise<void> {
await queryInterface.dropTable('orders');
}my-app/
.env # DATABASE_URL for the CLI
config/
config.ts # scaffolded by prorm init
migrations/
20260926175708-create-users-table.js
20260926175719-add-users-display-name.js
20260926120500-create-orders.ts # sorts first: its timestamp is earlier
models/
index.ts
seeders/
1790460065308-roles.jsusers table, then a column added to it — from anDATABASE_URL first, then DB_DIALECT with DB_NAME, DB_HOST,DB_PORT, DB_USER and DB_PASSWORD. With nothing set it opens a SQLite filedatabase.db in the current directory.DATABASE_URL=postgres://app:secret@localhost:5432/app_devDATABASE_URL=mysql://app:secret@localhost:3306/app_devDB_DIALECT=sqlite
DB_NAME=./app.db.env by itself: export the variables, or run it throughnode --env-file=.env, dotenv -e .env --, your process$ npx prorm migration:create -- --name create-users-table
Created migrations directory: /srv/my-app/migrations
Migration created: /srv/my-app/migrations/20260926175708-create-users-table.js
Migration created successfully: 20260926175708-create-users-table.jsup and down. Fill both in:$ npx prorm migration:create -- --name add-users-display-name
Migration created: /srv/my-app/migrations/20260926175719-add-users-display-name.js
Migration created successfully: 20260926175719-add-users-display-name.jsNOT NULL column with no$ npx prorm migration:status
Database connection established
Migration Status:
================
Pending Migrations:
[DOWN] 20260926175708-create-users-table
[DOWN] 20260926175719-add-users-display-name
----------------
Total: 2 (0 executed, 2 pending)--verbose lists what ran; without it you get the count.$ npx prorm migration:run --verbose
Database connection established
Running 2 pending migration(s)...
Running migration: 20260926175708-create-users-table
Migration 20260926175708-create-users-table completed
Running migration: 20260926175719-add-users-display-name
Migration 20260926175719-add-users-display-name completed
Executed 2 migration(s)
- 20260926175708-create-users-table
- 20260926175719-add-users-display-name$ DATABASE_URL="$PROD_DATABASE_URL" npx prorm migration:run
Database connection established
Running 2 pending migration(s)...
Running migration: 20260926175708-create-users-table
Migration 20260926175708-create-users-table completed
Running migration: 20260926175719-add-users-display-name
Migration 20260926175719-add-users-display-name completed
Executed 2 migration(s)$ npx prorm migration:run
Database connection established
No pending migrations to runmigration:revert with no flags undoes the most recent migration:$ npx prorm migration:revert
Database connection established
Reverting last migration...
Reverting migration: 20260926175719-add-users-display-name
Migration 20260926175719-add-users-display-name reverted
Reverted 1 migration(s)
$ npx prorm migration:status
Database connection established
Migration Status:
================
Executed Migrations:
[UP] 20260926175708-create-users-table [Batch 1]
Pending Migrations:
[DOWN] 20260926175719-add-users-display-name
----------------
Total: 2 (1 executed, 1 pending)--steps goes further back, newest first:$ npx prorm migration:revert -- --steps 2 --verbose
Database connection established
Reverting 2 migration(s)...
Reverting migration: 20260926175719-add-users-display-name
Migration 20260926175719-add-users-display-name reverted
Reverting migration: 20260926175708-create-users-table
Migration 20260926175708-create-users-table reverted
Reverted 2 migration(s)
- 20260926175719-add-users-display-name
- 20260926175708-create-users-table[!CAUTION]
Revertingadd-users-display-nameruns itsdown(), which drops the column
and every value in it. A rollback is only as safe as thedown()it runs:
read it before you revert in production, and take a backup first.
up() (or down()) and itsPrormMeta row in one transaction. A failure part-way rolls back the$ npx prorm migration:run
Database connection established
Running 1 pending migration(s)...
Running migration: 20260926120000-create-orders
Migration 20260926120000-create-orders failed: SqliteError: no such column: "customerId" - should this be a string literal in single-quotes?
Error: no such column: "customerId" - should this be a string literal in single-quotes?orders table that up() created first is gone again, and PrormMeta hasCREATE INDEX CONCURRENTLY,ALTER TYPE … ADD VALUE before PostgreSQL 12. Opt such a migration out in// In the migration file:
export const transaction = false;
// Or for every migration the migrator runs:
const migrator = new Migrator({ prorm, path: './migrations', transaction: false });[!IMPORTANT]
MySQL, MariaDB and Oracle commit every DDL statement implicitly, so there the
migrator cannot roll a half-applied migration back, and neither can an
opted-out migration anywhere. Keep one logical change per migration and make
each statement safe to repeat (createTableemitsCREATE TABLE IF NOT EXISTS; anaddColumnfails the second time, and you remove the
half-applied change by hand first).
[!NOTE]
Before 4.9.2 the migrator never opened a transaction, on any engine: a
failure left the earlier statements applied. A migration that relies on a
statement PostgreSQL refuses inside a transaction (CREATE INDEX CONCURRENTLY) now fails until you addexport const transaction = false.
CREATE TABLEqueryInterface a migration is handed covers tables, columns, indexes,prorm argument hands you:import type { CreateQueryInterface, Prorm, QueryInterface } from 'ts-prorm-orm';
export async function up(queryInterface: QueryInterface, prorm: Prorm) {
const qi: CreateQueryInterface = prorm.getQueryInterface();
await qi.addEnumValue('order_status', 'refunded', { after: 'delivered' });
}qi.await qi.addEnumValue('order_status', 'refunded', { after: 'delivered', ifNotExists: true });ifNotExists makes the migration safe to re-runWarning: PostgreSQL cannot run ALTER TYPE … ADD VALUEinside a transaction
block on versions before 12, and the migrator runs each migration in one. On
those versions, putexport const transaction = false;in the migration.
await qi.createExtension('uuid-ossp');
await qi.createExtension('pg_trgm');uuid_generate_v4() as a columnawait qi.bulkInsert('users', rowsWithExplicitIds);
// Without this, the next ordinary insert collides on the primary key.
await qi.setSequenceValue('users_id_seq', 10_000);await qi.addIndex('events', ['occurred_at']);
await qi.analyze('events');await qi.commentOnTable('events', 'Append-only; see the retention job');
await qi.commentOnColumn('events', 'payload', 'Raw webhook body, unvalidated');down() is where most of them areup() | down() |
|---|---|
addEnumValue() | No inverse. PostgreSQL cannot remove an enum value; recreating the type is the only route. |
createExtension() | dropExtension() — but only if nothing else came to depend on it. |
createDomain() | dropDomain() |
commentOnTable(t, 'x') | commentOnTable(t, null) |
setSequenceValue() | Record the old value first; there is nothing to read it back from afterwards. |
Note: an irreversible up()is not a reason to skipdown(). Throw an error
explaining why, so a rollback fails loudly at the migration that cannot be
undone rather than half-way through the one after it.
[UP] and every migration older than it stays applied.Migrator discovers migration files, tracks which have run, and runs or reverts them.import { Prorm } from 'ts-prorm-orm';
import { Migrator } from 'ts-prorm-orm'; // also: createMigrator(options)
const prorm = new Prorm({ dialect: 'sqlite', storage: './app.db' });
await prorm.connect();
const migrator = new Migrator({
prorm,
path: './migrations', // default './migrations'
tableName: 'PrormMeta', // tracking table, default 'PrormMeta'
});
// Discover files on disk before inspecting/running.
await migrator.findMigrations();
// Run every pending migration.
const result = await migrator.up();
console.log(`Ran ${result.executed}:`, result.migrations);
// Run just one, by name.
await migrator.up({ migration: '20240315120000-create-users-table' });Migrator also loads files automatically the first time you call up() / down() iffindMigrations() is only strictly required when youpending(), executed(), status()) beforehand.// Revert the most recently executed migration.
await migrator.down();
// Revert the last N migrations (reverse order).
await migrator.down({ steps: 3 });
// Revert one specific migration.
await migrator.down({ migration: '20240315120000-create-users-table' });
// Revert every migration in a batch above the target batch number.
await migrator.downToBatch(2);MigratorResult — { executed, reverted, migrations }.const pending = await migrator.pending(); // Migration[] not yet run
const done = await migrator.executed(); // Migration[] already run
const status = await migrator.status(); // MigrationStatus[]
for (const s of status) {
console.log(s.name, s.status, s.batch); // status: 'up' | 'pending' | 'down'
}PrormMeta; override it with MigratorOptions.tableName (andschema). PrormMigration.ensureMigrationsTable() creates it with:name — the migration name (unique)batch — an integer grouping migrationstimestamp — unix seconds when it was appliedMigrator.up() — and so prorm migration:run — records every migration ofdownToBatch() undoes a whole deploy.migration:revert with no flags still undoes only the newest migration. Aftername batch timestamp
20260926175708-create-users-table 1 1790485935
20260926175719-add-users-display-name 1 1790485935downToBatch(target) reverts every migration whose batch is above target,const before = await migrator.status();
const lastGood = Math.max(0, ...before.filter((s) => s.status === 'up').map((s) => s.batch ?? 0));
const result = await migrator.up();
try {
await runSmokeTests();
} catch (err) {
// Undo exactly what this deploy applied, then fail the deploy.
await migrator.downToBatch(lastGood);
throw err;
}MigratorOptions alsoextension (default .js, .ts always allowed) and a custom filter(filename)prorm binary (also installed as orm-cli, the same file) wraps the sameMigrator. It reads its connection from DATABASE_URL, or from DB_DIALECT,DB_NAME, DB_USER, DB_PASSWORD, DB_HOST and DB_PORT — only from theconfig/config.ts that prorm init writes is for your$ npx prorm init
$ npx prorm migration:create -- --name create-users-table
$ npx prorm migration:run
$ npx prorm migration:run -- --name 20240315120000-create-users-table
$ npx prorm migration:revert
$ npx prorm migration:revert -- --steps 3
$ npx prorm migration:revert -- --name 20240315120000-create-users-table
$ npx prorm migration:status
$ npx prorm migration:run -- --path ./db/migrations| Flag | Used by | Does |
|---|---|---|
--name <name> | migration:create, migration:run, migration:revert | The new file's name, or the one migration to run or revert. |
--steps <n> | migration:revert | Revert the last n, newest first. |
--path <dir> | all | Migrations directory; default ./migrations. |
--verbose | migration:run, migration:revert | List each migration after the count. |
--quiet, -q | all | Print errors only. |
--no-color / NO_COLOR | all | Plain output for logs. |
-- separator matters under npm run and npx: flags after it go to thenpx prorm --help for the full reference. Newlymodule.exports = { up, down }) with.js extension.needs: migrate keeps theQueryInterfaceQueryInterface passed into migrations is a full schema-manipulation andmigrator.getQueryInterface().const qi = migrator.getQueryInterface();
// Introspection
const tables = await qi.showTables(); // string[]
const exists = await qi.tableExists('users'); // boolean
const hasCol = await qi.columnExists('users', 'email');
const columns = await qi.describeTable('users'); // { [col]: ColumnDescription }
const indexes = await qi.showIndexes('users'); // IndexDescription[]
const fks = await qi.getForeignKeysForTable('users');
const version = await qi.getDatabaseVersion();QueryInterface include:| Category | Methods |
|---|---|
| Tables | createTable, dropTable, dropAllTables, renameTable |
| Columns | addColumn, removeColumn, changeColumn, renameColumn |
| Indexes | addIndex, removeIndex |
| Constraints | addConstraint, removeConstraint |
| Foreign keys | addForeignKey, removeForeignKey |
| Partitions | createPartitionedTable, createPartition, attachPartition, detachPartition, dropPartition |
| Data | bulkInsert, bulkUpdate, bulkDelete |
| Raw | prormQuery(sql, options) |
// A data-migration example using the bulk + raw helpers.
export const up = async (queryInterface, prorm) => {
await queryInterface.addColumn('users', 'status', {
type: 'VARCHAR(20)',
allowNull: false,
defaultValue: 'active',
});
await queryInterface.bulkUpdate('users', { status: 'legacy' }, { createdAt: null });
const { rows } = await queryInterface.prormQuery('SELECT COUNT(*) AS n FROM users');
console.log('backfilled', rows[0].n, 'rows');
};QueryInterface's constraint support is intentionally narrow: addConstraintunique constraints (as a unique index); primaryKey, foreignKey, andcheck are best expressed at table-creation time or via addForeignKey / raw SQL.prorm.sync({ force: true }) in tests and throwaway local databases — fast, no filesprorm.sync({ alter: true }) for quick iteration on a dev database you want to keep.PrormMeta, so every environment can be brought to the same