Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

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 push
import { 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-run
const 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 push
const 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-reset
const 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.

Next Steps

Was this page helpful?