Push
Synchronize models directly with the live database without creating migration history
When to Use Push
- Development environment - Rapid iteration without migration history
- 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.outcome === "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 statement of result.statements) {
console.log(statement.sql);
}
// result.outcome === "planned" | "noop"Resolving 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 pushconst preview = await migrations.push({
dryRun: true,
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
});
}
},
});
// The callback's decisions are closed into this exact preview.
await migrations.push({ consent: preview.consent });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
The public package exports three reusable policies. Use them during preview; destructive decisions still require the preview’s exact consent before any effect:
import {
lenientResolver,
rejectAllResolver,
} from "viborm/migrations";
const preview = await migrations.push({
dryRun: true,
resolve: lenientResolver,
});
await migrations.push({ consent: preview.consent });
// In CI, this policy refuses as soon as a decision is required.
const ciPushOptions = {
dryRun: true,
resolve: rejectAllResolver,
} as const;
| Resolver | Destructive | Ambiguous | Removed enum values |
|---|---|---|---|
rejectAllResolver |
Reject | Reject | Reject |
lenientResolver |
Proceed | Rename | Set to NULL |
addDropResolver |
Proceed | Add and drop | Set to NULL |
The last two policies are intentionally data-destructive. Their enum policy is valid only when every affected column is nullable; otherwise the migration is refused. Prefer an explicit callback when the mapping depends on the column or the old value.
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-resetconst preview = await migrations.push({
dryRun: true,
forceReset: true,
});
// Review preview.target, preview.operations, and preview.statements first.
await migrations.push({ consent: preview.consent });CLI Options
| Option | Description |
|---|---|
--dry-run |
Preview SQL without executing |
--force-reset |
Plan a rebuild from empty; requires exact preview consent |
-y, --yes |
Apply the displayed preview without an interactive prompt |
--json |
Print machine-readable preview or result JSON; applying a non-empty plan also requires --yes |
--config <path> |
Path to config file |
API Options
Planning and consent are intentionally different calls. A consent call cannot
also change forceReset, validation, or resolution decisions:
interface PushPlanningOptions {
forceReset?: boolean;
skipValidation?: boolean;
resolve?: ResolveCallback;
}
type PushOptions =
| (PushPlanningOptions & { dryRun: true })
| (PushPlanningOptions & {
dryRun?: false;
consent?: never;
})
| {
consent: PushConsent;
dryRun?: false;
};
| Option | Type | Default | Description |
|---|---|---|---|
forceReset |
boolean |
false |
Drop managed tables, then rebuild from empty. Requires exact preview consent. |
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 destructive or ambiguous changes. |
consent |
PushConsent |
- | The only option in the apply-after-preview arm. Required for destructive and force-reset work. |
A safe non-destructive plan may execute directly. A destructive or force-reset plan always requires a dry preview followed by its exact consent. The second call reacquires the target lock, replans from the live catalog, and refuses if the target or plan changed.
API Result
interface PushPreview {
outcome: "planned" | "noop";
target: PushTargetIdentity;
planHash: string;
schemaHash: string;
fingerprint: string;
destructive: boolean;
operations: readonly PushOperation[];
statements: readonly PushStatementPreview[];
consent: PushConsent;
}
interface PushApplyResult {
outcome: "applied" | "noop";
target: PushTargetIdentity;
planHash: string;
operations: readonly PushOperation[];
statements: readonly PushStatementPreview[];
attestation: {
pathHash: string;
planHash: string;
schemaHash: string;
fingerprint: string;
};
}
| Property | Type | Description |
|---|---|---|
outcome |
"planned" | "noop" | "applied" |
Whether the plan would change the database, or did |
operations |
PushOperation[] |
Review labels for the accepted plan |
statements |
PushStatementPreview[] |
Structured SQL plus typed parameters |
schemaHash, fingerprint |
string |
Desired-schema identity and preview-time live fingerprint |
consent |
PushConsent |
Inert preview token; required to apply a destructive or force-reset plan |
attestation |
PushAttestation |
Apply-time path, plan, schema, and final live-fingerprint proof |
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 preview = await migrations.push({
dryRun: true,
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();
},
});
await migrations.push({ consent: preview.consent });
See the ResolveChange interface above for the full EnumValueRemovalChange contract.