Push
Synchronize your schema directly to the database without migration files — compares models against database state and applies changes
When to Use Push
- Development environment - Rapid iteration without migration files
- Prototyping - Quick schema experiments
- CI test databases - Fresh schema for each test run
- Non-versioned workflows - When you don’t need migration history
Basic Usage
# Push schema changes to database
npx viborm pushimport { createMigrationClient } from "viborm/migrations";
import { client } from "./client";
const migrations = createMigrationClient(client);
const result = await migrations.push();
if (result.applied) {
console.log(`Applied ${result.operations.length} changes`);
}Dry Run Mode
Preview changes without applying them:
# Preview changes without applying
npx viborm push --dry-runconst result = await migrations.push({ dryRun: true });
console.log("Would apply:");
for (const sql of result.sql) {
console.log(sql);
}
// result.applied === falseResolving Changes
When push detects destructive changes (dropping tables/columns), ambiguous changes (potential renames), or enum value removals, it calls the resolve callback for each one.
# CLI prompts interactively for each change
npx viborm push
# Auto-accept all changes without prompting
npx viborm push --force
# Always ask for approval before executing SQL statements
npx viborm push --strictconst result = await migrations.push({
resolve: async (change) => {
console.log(change.description);
if (change.type === "destructive") {
// Destructive changes have: proceed(), reject()
return confirm("Accept data loss?") ? change.proceed() : change.reject();
}
if (change.type === "ambiguous") {
// Ambiguous changes have: rename(), addAndDrop(), reject()
return change.rename();
}
if (change.type === "enumValueRemoval") {
// Enum value removals have: mapValues(), reject()
return change.mapValues({
'OLD_VALUE': 'NEW_VALUE',
'DEPRECATED': null, // Set to NULL
});
}
},
});ResolveChange Interface
Each change type has specific methods available:
// Destructive changes (dropTable, dropColumn, alterColumn)
interface DestructiveResolveChange {
type: "destructive";
operation: "dropTable" | "dropColumn" | "alterColumn";
table: string;
column?: string;
description: string;
proceed(): ResolveResult; // Accept the data loss
reject(): ResolveResult; // Abort the operation
}
// Ambiguous changes (renameTable, renameColumn)
interface AmbiguousResolveChange {
type: "ambiguous";
operation: "renameTable" | "renameColumn";
table: string;
column?: string;
oldName?: string;
newName?: string;
oldType?: string;
newType?: string;
description: string;
rename(): ResolveResult; // Treat as rename (preserves data)
addAndDrop(): ResolveResult; // Treat as separate add + drop (data loss)
reject(): ResolveResult; // Abort the operation
}
// Enum value removal changes (per-column)
interface EnumValueRemovalChange {
type: "enumValueRemoval";
enumName: string;
tableName: string; // Table containing the column
columnName: string; // Column using the enum
isNullable: boolean; // Whether the column is nullable
removedValues: string[]; // Values being removed
availableValues: string[]; // Values to map to
description: string;
mapValues(replacements: Record<string, string | null>): ResolveResult;
useNull(): ResolveResult; // Set all removed values to NULL (nullable columns only)
reject(): ResolveResult; // Abort the operation
}
Built-in Resolvers
import { lenientResolver, addDropResolver, rejectAllResolver } from "viborm/migrations";
// Accept destructive, rename ambiguous, map enums to NULL
await migrations.push({ resolve: lenientResolver });
// Accept destructive, add+drop for ambiguous, map enums to NULL
await migrations.push({ resolve: addDropResolver });
// Reject all changes (useful for CI)
await migrations.push({ resolve: rejectAllResolver });
The built-in resolvers handle all change types:
// lenientResolver implementation
const lenientResolver = async (change) => {
if (change.type === "destructive") return change.proceed();
if (change.type === "ambiguous") return change.rename();
// enumValueRemoval: set all to null
return change.useNull();
};
// addDropResolver implementation
const addDropResolver = async (change) => {
if (change.type === "destructive") return change.proceed();
if (change.type === "ambiguous") return change.addAndDrop();
// enumValueRemoval: set all to null
return change.useNull();
};
// rejectAllResolver implementation
const rejectAllResolver = async (change) => change.reject();
Force Mode
Skip all resolution prompts and auto-accept everything:
- Destructive changes: Proceeds (accepts data loss)
- Ambiguous changes: Treats as add+drop (not rename, accepts data loss)
- Enum value removals: Sets all to NULL
npx viborm push --forceawait migrations.push({ force: true });Combining Force with Resolver
You can use both force: true and a resolve callback together. The resolver takes precedence - if it returns a result, that’s used. If it returns undefined (doesn’t handle the change), force mode kicks in automatically.
This is useful for protecting specific tables or columns while auto-accepting everything else:
await migrations.push({
force: true,
resolve: async (change) => {
// Protect the users table from being dropped
if (change.type === "destructive" && change.table === "users") {
return change.reject();
}
// Protect specific renames from being treated as add+drop
if (change.type === "ambiguous" && change.table === "accounts") {
return change.rename();
}
// Return undefined to let force handle everything else
},
});
force |
resolve |
Behavior |
|---|---|---|
false |
undefined |
Throw error if unresolved changes |
true |
undefined |
Auto-accept all changes |
false |
provided | Resolver must handle all changes |
true |
provided | Resolver handles what it returns, force handles the rest |
Force Reset
Reset the database before pushing (drops all tables and pushes fresh schema):
# Drop all tables and push fresh schema
npx viborm push --force-reset// forceReset drops all tables, then pushes fresh schema
await migrations.push({ forceReset: true });CLI Options
| Option | Description |
|---|---|
--dry-run |
Preview SQL without executing |
--force |
Skip all resolution prompts |
--force-reset |
Drop all tables before pushing |
--strict |
Always ask for approval before executing SQL statements |
--verbose |
Print all SQL statements prior to execution |
--config <path> |
Path to config file |
API Options
interface PushOptions {
force?: boolean;
forceReset?: boolean;
dryRun?: boolean;
skipValidation?: boolean;
resolve?: ResolveCallback;
}
| Option | Type | Default | Description |
|---|---|---|---|
force |
boolean |
false |
Auto-accept all changes. Can be combined with resolve to auto-accept unhandled changes. |
forceReset |
boolean |
false |
Drop all tables before pushing |
dryRun |
boolean |
false |
Preview SQL without executing |
skipValidation |
boolean |
false |
Skip schema validation before pushing. Validation catches definition errors that would otherwise corrupt data silently — only skip it when deliberately pushing a shape the validator flags. |
resolve |
ResolveCallback |
- | Callback for resolving changes. Return undefined to let force handle it. |
API Result
interface PushResult {
operations: DiffOperation[];
applied: boolean;
sql: string[];
}
| Property | Type | Description |
|---|---|---|
operations |
DiffOperation[] |
All operations that were applied |
applied |
boolean |
Whether changes were actually applied |
sql |
string[] |
Generated SQL statements |
Enum Value Removal
When removing enum values, you must specify what to do with existing data. Each column using the enum is resolved separately, allowing different mappings per column:
const result = await migrations.push({
resolve: async (change) => {
if (change.type === "enumValueRemoval") {
// Each call is for a specific column
console.log(`Column: ${change.tableName}.${change.columnName}`);
console.log(`Removing: ${change.removedValues.join(", ")}`);
console.log(`Available: ${change.availableValues.join(", ")}`);
// For nullable columns, can use useNull() for convenience
if (change.isNullable) {
return change.useNull();
}
// Map removed values to new values
return change.mapValues({
'PENDING': 'INACTIVE', // Map to another value
'OLD_STATUS': null, // Set to NULL (only if column is nullable)
});
}
// Handle other change types...
if (change.type === "destructive") return change.proceed();
if (change.type === "ambiguous") return change.rename();
},
});
See the ResolveChange interface above for the full EnumValueRemovalChange contract.