Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 287 additions & 21 deletions drizzle-kit/src/cli/commands/migrate.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions drizzle-kit/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export type Journal = {
when: number;
tag: string;
breakpoints: boolean;
hasDown?: boolean;
}[];
};

Expand Down
212 changes: 212 additions & 0 deletions drizzle-kit/tests/migrate/down-sql.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import { embeddedMigrations, writeResult } from 'src/cli/commands/migrate';
import type { Journal } from 'src/utils';

// Minimal CommonSchema stub accepted by writeResult
const minimalSchema: any = {
version: '7',
dialect: 'sqlite',
tables: {},
views: {},
enums: {},
_meta: { columns: {}, schemas: {}, tables: {} },
};

let tmpDir: string;

beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'drizzle-down-sql-test-'));
fs.mkdirSync(path.join(tmpDir, 'meta'));
});

afterEach(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

function makeJournal(dialect: string = 'sqlite'): Journal {
return {
version: '7',
dialect: dialect as any,
entries: [],
};
}

describe('writeResult — down SQL file generation', () => {
test('writes .down.sql file when downSqlStatements are provided', () => {
const journal = makeJournal();
writeResult({
cur: minimalSchema,
sqlStatements: ['CREATE TABLE users (id INTEGER PRIMARY KEY)'],
downSqlStatements: ['DROP TABLE users'],
journal,
outFolder: tmpDir,
breakpoints: true,
prefixMode: 'index',
name: 'create_users',
});

const entries = journal.entries;
expect(entries).toHaveLength(1);
const tag = entries[0]!.tag;

expect(fs.existsSync(path.join(tmpDir, `${tag}.sql`))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, `${tag}.down.sql`))).toBe(true);

const downContent = fs.readFileSync(path.join(tmpDir, `${tag}.down.sql`), 'utf8');
expect(downContent).toContain('DROP TABLE users');
});

test('does NOT write .down.sql file when downSqlStatements is undefined', () => {
const journal = makeJournal();
writeResult({
cur: minimalSchema,
sqlStatements: ['CREATE TABLE users (id INTEGER PRIMARY KEY)'],
journal,
outFolder: tmpDir,
breakpoints: true,
prefixMode: 'index',
name: 'test_migration',
});

const tag = journal.entries[0]!.tag;
expect(fs.existsSync(path.join(tmpDir, `${tag}.sql`))).toBe(true);
expect(fs.existsSync(path.join(tmpDir, `${tag}.down.sql`))).toBe(false);
});

test('sets hasDown: true in journal entry when downSqlStatements are provided', () => {
const journal = makeJournal();
writeResult({
cur: minimalSchema,
sqlStatements: ['CREATE TABLE t (id INTEGER)'],
downSqlStatements: ['DROP TABLE t'],
journal,
outFolder: tmpDir,
breakpoints: true,
prefixMode: 'index',
name: 'test_migration',
});

const entry = journal.entries[0]!;
expect(entry.hasDown).toBe(true);
});

test('does NOT set hasDown when downSqlStatements is undefined', () => {
const journal = makeJournal();
writeResult({
cur: minimalSchema,
sqlStatements: ['CREATE TABLE t (id INTEGER)'],
journal,
outFolder: tmpDir,
breakpoints: true,
prefixMode: 'index',
name: 'test_migration',
});

const entry = journal.entries[0]!;
expect(entry.hasDown).toBeUndefined();
});

test('does NOT set hasDown when downSqlStatements is empty array', () => {
const journal = makeJournal();
writeResult({
cur: minimalSchema,
sqlStatements: ['CREATE TABLE t (id INTEGER)'],
downSqlStatements: [],
journal,
outFolder: tmpDir,
breakpoints: true,
prefixMode: 'index',
name: 'test_migration',
});

const entry = journal.entries[0]!;
expect(entry.hasDown).toBeUndefined();

const tag = entry.tag;
expect(fs.existsSync(path.join(tmpDir, `${tag}.down.sql`))).toBe(false);
});

test('respects breakpoints delimiter in .down.sql', () => {
const journal = makeJournal();
writeResult({
cur: minimalSchema,
sqlStatements: ['CREATE TABLE a (id INTEGER)', 'CREATE TABLE b (id INTEGER)'],
downSqlStatements: ['DROP TABLE b', 'DROP TABLE a'],
journal,
outFolder: tmpDir,
breakpoints: true,
prefixMode: 'index',
name: 'test_migration',
});

const tag = journal.entries[0]!.tag;
const downContent = fs.readFileSync(path.join(tmpDir, `${tag}.down.sql`), 'utf8');
expect(downContent).toContain('--> statement-breakpoint');
});
});

describe('embeddedMigrations — down SQL bundling', () => {
test('includes downMigrations block when entries have hasDown', () => {
const journal: Journal = {
version: '7',
dialect: 'sqlite',
entries: [
{ idx: 0, version: '7', when: 1000, tag: '0000_test', breakpoints: true, hasDown: true },
],
};

const output = embeddedMigrations(journal);

expect(output).toContain("import d0000 from './0000_test.down.sql'");
expect(output).toContain('downMigrations');
expect(output).toContain('m0000: d0000');
});

test('omits downMigrations block when no entries have hasDown', () => {
const journal: Journal = {
version: '7',
dialect: 'sqlite',
entries: [
{ idx: 0, version: '7', when: 1000, tag: '0000_test', breakpoints: true },
],
};

const output = embeddedMigrations(journal);

expect(output).not.toContain('downMigrations');
expect(output).not.toContain('.down.sql');
});

test('only imports down SQL for entries that have hasDown', () => {
const journal: Journal = {
version: '7',
dialect: 'sqlite',
entries: [
{ idx: 0, version: '7', when: 1000, tag: '0000_no_down', breakpoints: true },
{ idx: 1, version: '7', when: 2000, tag: '0001_has_down', breakpoints: true, hasDown: true },
],
};

const output = embeddedMigrations(journal);

expect(output).toContain("import d0001 from './0001_has_down.down.sql'");
expect(output).not.toContain("import d0000 from './0000_no_down.down.sql'");
expect(output).toContain('downMigrations');
expect(output).toContain('m0001: d0001');
expect(output).not.toContain('d0000');
});

test('adds expo header for expo driver', () => {
const journal: Journal = {
version: '7',
dialect: 'sqlite',
entries: [],
};

const output = embeddedMigrations(journal, 'expo');
expect(output).toContain('Expo/React Native');
});
});
9 changes: 9 additions & 0 deletions drizzle-orm/src/aws-data-api/pg/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ export async function migrate<TSchema extends Record<string, unknown>>(
const migrations = readMigrationFiles(config);
await db.dialect.migrate(migrations, db.session, config);
}

export async function rollback<TSchema extends Record<string, unknown>>(
db: AwsDataApiPgDatabase<TSchema>,
config: MigrationConfig,
steps?: number,
) {
const migrations = readMigrationFiles(config);
await db.dialect.rollback(migrations, db.session, config, steps);
}
9 changes: 9 additions & 0 deletions drizzle-orm/src/better-sqlite3/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ export function migrate<TSchema extends Record<string, unknown>>(
const migrations = readMigrationFiles(config);
db.dialect.migrate(migrations, db.session, config);
}

export function rollback<TSchema extends Record<string, unknown>>(
db: BetterSQLite3Database<TSchema>,
config: MigrationConfig,
steps?: number,
) {
const migrations = readMigrationFiles(config);
db.dialect.rollback(migrations, db.session, config, steps);
}
9 changes: 9 additions & 0 deletions drizzle-orm/src/bun-sql/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ export async function migrate<TSchema extends Record<string, unknown>>(
const migrations = readMigrationFiles(config);
await db.dialect.migrate(migrations, db.session, config);
}

export async function rollback<TSchema extends Record<string, unknown>>(
db: BunSQLDatabase<TSchema>,
config: MigrationConfig,
steps?: number,
) {
const migrations = readMigrationFiles(config);
await db.dialect.rollback(migrations, db.session, config, steps);
}
9 changes: 9 additions & 0 deletions drizzle-orm/src/bun-sqlite/migrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,12 @@ export function migrate<TSchema extends Record<string, unknown>>(
const migrations = readMigrationFiles(config);
db.dialect.migrate(migrations, db.session, config);
}

export function rollback<TSchema extends Record<string, unknown>>(
db: BunSQLiteDatabase<TSchema>,
config: MigrationConfig,
steps?: number,
) {
const migrations = readMigrationFiles(config);
db.dialect.rollback(migrations, db.session, config, steps);
}
49 changes: 49 additions & 0 deletions drizzle-orm/src/d1/migrator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DrizzleError } from '~/errors.ts';
import type { MigrationConfig } from '~/migrator.ts';
import { readMigrationFiles } from '~/migrator.ts';
import { sql } from '~/sql/sql.ts';
Expand Down Expand Up @@ -47,3 +48,51 @@ export async function migrate<TSchema extends Record<string, unknown>>(
await db.session.batch(statementToBatch);
}
}

export async function rollback<TSchema extends Record<string, unknown>>(
db: DrizzleD1Database<TSchema>,
config: MigrationConfig,
steps: number = 1,
) {
const migrations = readMigrationFiles(config);
const migrationsTable = config.migrationsTable ?? '__drizzle_migrations';

const dbMigrations = await db.values<[number, string, string]>(
sql`SELECT id, hash, created_at FROM ${
sql.identifier(migrationsTable)
} ORDER BY created_at DESC LIMIT ${sql.raw(String(steps))}`,
);

if (dbMigrations.length === 0) {
return;
}

const statementToBatch = [];
for (const dbMigration of dbMigrations) {
const meta = migrations.find((m) => m.hash === dbMigration[1]);
if (!meta) {
throw new DrizzleError({
message: `Cannot rollback migration with hash ${dbMigration[1]}: migration file not found`,
});
}
if (!meta.downSql || meta.downSql.length === 0) {
throw new DrizzleError({
message: `Cannot rollback migration ${dbMigration[1]}: no down SQL available. Add a .down.sql file alongside the migration.`,
});
}
for (const stmt of [...meta.downSql].reverse()) {
statementToBatch.push(db.run(sql.raw(stmt)));
}
statementToBatch.push(
db.run(
sql`DELETE FROM ${sql.identifier(migrationsTable)} WHERE hash = ${
sql.raw(`'${dbMigration[1]}'`)
}`,
),
);
}

if (statementToBatch.length > 0) {
await db.session.batch(statementToBatch);
}
}
Loading