VibORM

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

Not for Production

Push doesn't create migration files, so changes aren't versioned. Use file-based migrations for production deployments.

Basic Usage

# Push schema changes to database
npx viborm push
src/db/push.ts
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

# Fail on any changes requiring resolution
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
}

Resolution Methods

Change TypeAvailable MethodsDescription
destructivechange.proceed()Accept the data loss
destructivechange.reject()Abort the operation
ambiguouschange.rename()Treat as rename (preserves data)
ambiguouschange.addAndDrop()Treat as separate add + drop (data loss)
ambiguouschange.reject()Abort the operation
enumValueRemovalchange.mapValues({...})Map old values to new values or NULL
enumValueRemovalchange.useNull()Set all removed values to NULL
enumValueRemovalchange.reject()Abort the operation

Per-Column Resolution

Enum value removals are resolved per-column, allowing different mappings for different columns using the same enum. This lets you map PENDING to INACTIVE in one column but to NULL in another.

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 });

Data Loss

Force mode accepts all data loss. Use only when you're certain you don't need the data.

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
  },
});
forceresolveBehavior
falseundefinedThrow error if unresolved changes
trueundefinedAuto-accept all changes
falseprovidedResolver must handle all changes
trueprovidedResolver 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 });

Destructive Operation

Force reset drops all tables and data. Use with extreme caution and never in production.

Storage-Aware

When a storage driver is configured, forceReset also clears the migration journal to keep it in sync. Without a storage driver, it only drops tables.

CLI Options

OptionDescription
--dry-runPreview SQL without executing
--forceSkip all resolution prompts
--force-resetDrop all tables before pushing
--strictRequire confirmation before executing
--verboseShow detailed output
--config <path>Path to config file

API Options

interface PushOptions {
  force?: boolean;
  forceReset?: boolean;
  dryRun?: boolean;
  resolve?: ResolveCallback;
}
OptionTypeDefaultDescription
forcebooleanfalseAuto-accept all changes. Can be combined with resolve to auto-accept unhandled changes.
forceResetbooleanfalseDrop all tables before pushing
dryRunbooleanfalsePreview SQL without executing
resolveResolveCallback-Callback for resolving changes. Return undefined to let force handle it.

API Result

interface PushResult {
  operations: DiffOperation[];
  applied: boolean;
  sql: string[];
}
PropertyTypeDescription
operationsDiffOperation[]All operations that were applied
appliedbooleanWhether changes were actually applied
sqlstring[]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();
  },
});

The EnumValueRemovalChange provides:

  • enumName: The enum being modified
  • tableName: The table containing the column
  • columnName: The column using this enum
  • isNullable: Whether the column allows NULL
  • removedValues: Array of values being removed
  • availableValues: Array of values you can map to
  • mapValues(replacements): Method to specify the mapping
  • useNull(): Set all removed values to NULL (for nullable columns)

Next Steps

On this page