Skip to content
VibORM
Esc
navigateopen⌘Jpreview
On this page

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 push
import { 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-run
const result = await migrations.push({ dryRun: true });

console.log("Would apply:");
for (const sql of result.sql) {
  console.log(sql);
}
// result.applied === false

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

# Auto-accept all changes without prompting
npx viborm push --force

# Always ask for approval before executing SQL statements
npx viborm push --strict
const 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 --force
await 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.

Next Steps

Was this page helpful?