SchemaDiffer compares your model definitions against the schema the databaseMigrationGenerator turns that diff into up / down SQL.sync({ alter: true }), whichimport { Prorm, createSchemaDiffer, createMigrationGenerator } from 'ts-prorm-orm';
const prorm = new Prorm({ dialect: 'postgres', url: process.env.DATABASE_URL });
await prorm.connect();
// … define or import your models here …
const dialect = prorm.getDialectInstance()!;
const differ = createSchemaDiffer(dialect, { compareIndexes: true });
const generator = createMigrationGenerator(dialect, { generateDown: true });
for (const model of Object.values(prorm.models)) {
const diff = await differ.diffModel(model);
if (!diff.hasChanges) continue;
const script = generator.generateMigration(diff);
console.log(script.name);
console.log(script.up.join(';\n'));
}| Call | Returns |
|---|---|
diffModel(Model) | A SchemaDiff for one model. |
diffAll() | A SchemaDiff for each registered model that has changes. |
getColumnChanges(...) | Column-level changes only. |
getIndexChanges(...) | Index-level changes only. |
getConstraintChanges(...) | Constraint-level changes only. |
getForeignKeyChanges(...) | Foreign-key changes only. |
SchemaDiff is:{
modelName: 'User',
tableName: 'users',
schemaName: undefined,
changes: {
columns: [{ type: 'column', changeType: 'create' | 'modify' | 'delete',
columnName, modelDefinition?, databaseDefinition?, changedProperties? }],
indexes: [{ type: 'index', changeType, indexName, … }],
constraints: [{ type: 'constraint', changeType, constraintName, constraintType, … }],
foreignKeys: [{ type: 'foreignKey', changeType, … }],
},
hasChanges: true,
generatedAt: Date,
}changeType of create (in the model, not in themodify (in both but different — changedProperties says which) ordelete (in the database, not in the model).diffAll() finds the models through the Prorm instance that created thediffAll(models)) or the instancecreateSchemaDiffer(dialect, { prorm })). Before 4.9.2 it always threwDialect is not associated with a Prorm instance.| Option | Default | Effect |
|---|---|---|
compareIndexes | on | Include index differences. |
compareConstraints | on | Include constraint differences. |
compareForeignKeys | on | Include foreign-key differences. |
serializeColumnType | dialect default | Override how a DataType is rendered for comparison. |
skipColumnProperties | [] | Column properties to leave out of the comparison, such as ['defaultValue']. |
VARCHAR(255) may report a collation you nevermodify. skipColumnProperties leaves those properties out of theimport { createSchemaDiffer } from 'ts-prorm-orm';
const differ = createSchemaDiffer(prorm.getDialectInstance()!, {
skipColumnProperties: ['defaultValue'],
});
const diff = await differ.diffModel(prorm.model('User'));[ 'modify', 'email', [ 'defaultValue' ] ] into no change at all, and id[ 'modify', 'id', [ 'allowNull' ] ].import { createMigrationGenerator } from 'ts-prorm-orm';
const generator = createMigrationGenerator(prorm.getDialectInstance()!);
const script = generator.generateMigration(diff, {
namePrefix: 'auto',
generateDown: true,
includeComments: true,
});{
name: 'auto_1757110000000_users',
modelName: 'User',
tableName: 'users',
up: ['ALTER TABLE "users" ADD COLUMN "nickname" VARCHAR(50)', 'CREATE INDEX …'],
down: ['DROP INDEX …', 'ALTER TABLE "users" DROP COLUMN "nickname"'],
generatedAt: Date,
}up is emitted in order — columns, then indexes, then constraints, thendown is that list reversed, so a rollback undoes thegenerateDown: false leavesdown empty.generateAllMigrations(diffs) maps the whole set in one call.| Option | Default | Effect |
|---|---|---|
namePrefix | 'migration' | Leading segment of the migration name. |
generateDown | true | Produce rollback statements. |
includeComments | true | Prepend -- Migration: <table>, a timestamp and a blank line to up. |
dialectOptions | — | Passed through to the dialect. |
import { writeFileSync } from 'node:fs';
import { createMigrationGenerator, createSchemaDiffer, type Prorm } from 'ts-prorm-orm';
declare const prorm: Prorm; // your connected instance, models defined
const differ = createSchemaDiffer(prorm.getDialectInstance()!);
const generator = createMigrationGenerator(prorm.getDialectInstance()!);
for (const model of Object.values(prorm.models)) {
const diff = await differ.diffModel(model);
if (!diff.hasChanges) continue;
const script = generator.generateMigration(diff, { includeComments: false });
writeFileSync(`./migrations/${script.name}.js`, `
module.exports = {
async up(queryInterface) {
${script.up.map((s) => ` await queryInterface.prormQuery(${JSON.stringify(s)});`).join('\n')}
},
async down(queryInterface) {
${script.down.map((s) => ` await queryInterface.prormQuery(${JSON.stringify(s)});`).join('\n')}
},
};
`);
}users table was created with a legacyCode column. The model has sincenickname and an index: prorm.define('User', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true },
email: { type: DataTypes.STRING(255), allowNull: false },
- legacyCode: { type: DataTypes.STRING(20) },
+ nickname: { type: DataTypes.STRING(50) },
}, {
tableName: 'users',
timestamps: false,
+ indexes: [{ name: 'idx_users_nickname', fields: ['nickname'] }],
});import { createSchemaDiffer } from 'ts-prorm-orm';
const dialect = prorm.getDialectInstance()!;
const differ = createSchemaDiffer(dialect, { compareConstraints: false });
const diff = await differ.diffModel(prorm.model('User'));
console.log(diff.changes.columns.map((c) => [c.changeType, c.columnName, c.changedProperties]));[ [ 'modify', 'id', [ 'allowNull', 'defaultValue' ] ],
[ 'modify', 'email', [ 'defaultValue' ] ],
[ 'create', 'nickname', undefined ],
[ 'delete', 'legacyCode', undefined ] ]id and email "modifications" are theskipColumnProperties: ['defaultValue'] removes. diff.changes.indexes holds[ [ 'create', 'idx_users_nickname' ] ].import { createMigrationGenerator } from 'ts-prorm-orm';
const script = createMigrationGenerator(prorm.getDialectInstance()!, { namePrefix: 'auto' })
.generateMigration(diff);DROP INDEX "undefined" for the primary key's index, and neither enginescript.name is auto_<timestamp>_users, and down reverses the work: itlegacyCode (as VARCHAR(20) on SQLite) andnickname.| Statement | Verdict |
|---|---|
ALTER COLUMN "id" DROP NOT NULL / DROP DEFAULT | Noise from the drift above. On PostgreSQL it would strip the SERIAL default from the key; SQLite has no ALTER COLUMN at all. Delete. |
ALTER COLUMN "email" DROP DEFAULT | Noise. Delete. |
ADD COLUMN "nickname" STRING | Right intent, wrong type: the generator wrote the portable key, not VARCHAR(50). Fix the type. |
DROP COLUMN "legacyCode" | Destroys every value in the column. Keep only if that is the decision — or rename it aside instead. |
CREATE INDEX "idx_users_nickname" | Right. Keep it, after the column it indexes. |
ADD CONSTRAINT "users_pkey" | The table already has its key. Delete. |
$ npx prorm migration:run
Database connection established
Running 1 pending migration(s)...
Running migration: 20260926181500-users-nickname
Migration 20260926181500-users-nickname completed
Executed 1 migration(s)delete on a column is a data loss. The differ cannot know whether thename became full_name; applied as generated, that dropsmodify on a type may not be safe. Narrowing VARCHAR(255) toVARCHAR(50) compiles fine and fails, or truncates, on real rows.ALTER TABLE … ALTER COLUMN andADD CONSTRAINT, which SQLite does not support, and may render a type by its[!CAUTION]
Never pipescript.upstraight into the database. A generatedDROP COLUMN
runs as happily as anADD COLUMN, and in the example above it would have
removedlegacyCodefrom every row with no backup and no prompt.
sync({ alter: true })sync({ alter: true }) | SchemaDiffer + MigrationGenerator | |
|---|---|---|
| Applies changes | immediately | never — you decide |
| Rollback | none | generated down |
| Reviewable | no | yes — plain SQL strings |
| Drops columns | only with { drop: true } | reports them; you choose |
| Good for | local development | staging and production change management |