A simple, forward-only migration tool for PostgreSQL ported from postgres.js to TypeScript and extended to have more features, and ported to @alcyone-labs/arg-parser as a CLI handler, making it MCP-compatible out-of-the-box.
- Forward-only migrations (no rollbacks)
- Schema-based organization
- Both SQL and JavaScript migrations
- CLI tool and programmatic API
- Built specifically for postgres.js
- TypeScript support
- MCP support
pnpm add @alcyone-labs/postgres-shift-ts
# or
npm install @alcyone-labs/postgres-shift-ts
# or
yarn add @alcyone-labs/postgres-shift-ts
# or
bun add @alcyone-labs/postgres-shift-ts
# or
deno add npm:@alcyone-labs/postgres-shift-ts- Check options
npx migrate --help
# or
pnpx migrate --help- Set your database connection string:
export DB_CONNECTION_STRING="postgres://username:password@localhost:5432/database"- Create migration directories:
src/db/migrations/
├── public/
│ ├── 00001_create_users_table/
│ │ └── index.sql
│ └── 00002_add_email_index/
│ └── index.sql
└── analytics/
└── 00001_create_events_table/
└── index.sql
- Run migrations:
npx migrate --path src/db/migrations
# or
pnpx migrate --path src/db/migrationsimport postgres from "postgres";
import shift from "@ophiuchus/postgres-shift";
const sql = postgres("postgres://username:password@localhost:5432/database");
await shift({
sql,
path: "./migrations/public",
schema: "public",
before: (migration) => console.log(`Running: ${migration.name}`),
after: (migration) => console.log(`Completed: ${migration.name}`),
});Migrations are organized by schema, with each migration in its own numbered directory:
migrations/
├── public/ # Schema name
│ ├── 00001_initial_schema/ # Migration directory (5-digit prefix)
│ │ └── index.sql # SQL migration
│ ├── 00002_add_users/
│ │ └── index.sql
│ └── 00003_complex_migration/
│ └── index.js # JavaScript migration
└── analytics/
└── 00001_create_tables/
└── index.sql
- Migration directories must start with a 5-digit number:
00001_,00002_, etc. - Numbers must be consecutive (no gaps)
- Use descriptive names after the number:
00001_create_users_table - Underscores in names are converted to spaces in the migration log
Create an index.sql file in your migration directory:
-- 00001_create_users_table/index.sql
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);Create an index.js file that exports a default function:
// 00002_seed_data/index.js
export default async function (sql) {
await sql`
INSERT INTO users (email) VALUES
('admin@example.com'),
('user@example.com')
`;
// You can perform complex logic here
const users = await sql`SELECT * FROM users`;
console.log(`Seeded ${users.length} users`);
}Run database migrations for all schemas in the specified directory.
npx migrate [options]--path, -p, --migrations <path>- Path to migrations directory (default:src/db/migrations)
DB_CONNECTION_STRING- PostgreSQL connection string (required)
# Use default path
npx migrate
# Specify custom path
npx migrate --path ./db/migrations
# Using environment file
DB_CONNECTION_STRING="postgres://localhost/mydb" npx migrateMain migration function.
sql(Sql) - postgres.js database connectionpath(string) - Path to migration files for a specific schemaschema(string) - PostgreSQL schema name (default: 'public')before(function, optional) - Callback called before each migrationafter(function, optional) - Callback called after each migration
Promise that resolves when all migrations are complete.
import postgres from "postgres";
import shift from "@ophiuchus/postgres-shift";
const sql = postgres(process.env.DATABASE_URL);
try {
await shift({
sql,
path: "./migrations/public",
schema: "public",
before: ({ migration_id, name, path }) => {
console.log(`Starting migration ${migration_id}: ${name}`);
},
after: ({ migration_id, name, path }) => {
console.log(`Completed migration ${migration_id}: ${name}`);
},
});
console.log("All migrations completed successfully");
} catch (error) {
console.error("Migration failed:", error);
process.exit(1);
}The migration object passed to before and after callbacks:
type TMigration = {
path: string; // Full path to migration directory
migration_id: number; // Numeric ID from directory name
name: string; // Migration name (underscores converted to spaces)
};- Discovery: Scans the specified directory for migration folders matching the pattern
/^[0-9]{5}_/ - Validation: Ensures migration numbers are consecutive with no gaps
- Tracking: Creates a
migrationstable in the target schema to track completed migrations - Execution: Runs migrations in order, skipping those already completed
- Recording: Records each successful migration in the tracking table
CREATE TABLE migrations (
migration_id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
name TEXT
);- Missing consecutive numbers: Throws error if migration numbers have gaps
- Schema creation: Automatically creates the target schema if it doesn't exist
- Transaction safety: Each migration runs in its own transaction
- Rollback: Failed migrations are automatically rolled back
- Never modify completed migrations - Always create new migrations for changes
- Use descriptive names - Make migration purposes clear from the directory name
- Keep migrations small - One logical change per migration
- Test migrations - Run against a copy of production data
- Backup before running - Always backup production databases first
pnpm test:runpnpm build:tsupMIT
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
Originally forked from postgres-shift by Rasmus Porsager.