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
134 changes: 134 additions & 0 deletions drizzle-kit/src/cli/commands/squash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import chalk from "chalk";
import { readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
assertV1OutFolder,
type Journal,
prepareMigrationFolder,
} from "../../utils";
import { prepareMigrationMetadata } from "../../utils/words";
import type { SquashConfig } from "./utils";

export const squashMigrations = async ({
start,
end,
out,
dialect,
prefix,
}: SquashConfig) => {
try {
assertV1OutFolder(out);

const { journal }: { journal: Journal } = prepareMigrationFolder(
out,
dialect,
);

// Filter entries within the specified range
const entriesToSquash = journal.entries.filter(
(entry) => entry.idx >= start && entry.idx <= end,
);
entriesToSquash.sort((a, b) => a.idx - b.idx);

if (entriesToSquash.length === 0) {
console.log(
`[${chalk.yellow("!")}] No migrations found in range ${start}-${end}`,
);
return;
}

if (entriesToSquash.length === 1) {
console.log(
`[${chalk.yellow("!")}] Only one migration found in range ${start}-${end}, nothing to squash`,
);
return;
}

console.log(
`[${chalk.green("✓")}] Found ${entriesToSquash.length} migrations to squash (${entriesToSquash[0].tag} through ${entriesToSquash[entriesToSquash.length - 1].tag})`,
);

// Combine all SQL statements
const combinedSql = entriesToSquash
.map(
(entry) =>
`-- Start migration ${entry.tag}\n${readFileSync(join(out, `${entry.tag}.sql`), "utf-8")}\n-- End migration ${entry.tag}`,
)
.join("\n");

// Use the last snapshot as the new snapshot
const lastEntry = entriesToSquash[entriesToSquash.length - 1];
const lastSnapshotPath = join(
out,
"meta",
`${lastEntry.tag.split("_")[0]}_snapshot.json`,
);
const lastSnapshot = readFileSync(lastSnapshotPath, "utf-8");

// Create new migration
const newMetadata = prepareMigrationMetadata(end, prefix);
const newTag = newMetadata.tag;
const newSnapshotPath = join(
out,
"meta",
`${newTag.split("_")[0]}_snapshot.json`,
);
const newSqlPath = join(out, `${newTag}.sql`);

// Write new files
writeFileSync(newSnapshotPath, lastSnapshot);
writeFileSync(newSqlPath, combinedSql);

// Update journal
const newJournalEntry = {
idx: entriesToSquash[0].idx,
version: journal.version,
when: Date.now(),
tag: newTag,
breakpoints: entriesToSquash[0].breakpoints,
};

// Keep entries outside the squash range
const keptEntries = journal.entries.filter(
(entry) => entry.idx < start || entry.idx > end,
);

// Insert new squashed entry at the correct position (maintaining index order)
const beforeSquash = keptEntries.filter((e) => e.idx < start);
const afterSquash = keptEntries.filter((e) => e.idx > end);

const updatedEntries = [...beforeSquash, newJournalEntry, ...afterSquash];

const updatedJournal: Journal = {
...journal,
entries: updatedEntries,
};

// Write updated journal
const metaFilePath = join(out, "meta", "_journal.json");
writeFileSync(metaFilePath, JSON.stringify(updatedJournal, null, 2));

// Remove old migration files
for (const entry of entriesToSquash) {
const oldSqlPath = join(out, `${entry.tag}.sql`);
const oldSnapshotPath = join(
out,
"meta",
`${entry.tag.split("_")[0]}_snapshot.json`,
);
rmSync(oldSqlPath);
rmSync(oldSnapshotPath);
}

console.log(
`[${chalk.green("✓")}] Successfully squashed ${entriesToSquash.length} migrations into ${newTag}`,
);
console.log(
`[${chalk.blue("i")}] Removed: ${entriesToSquash.map((e) => e.tag).join(", ")}`,
);
console.log(`[${chalk.blue("i")}] Created: ${newTag}`);
} catch (e) {
console.error(e);
process.exit(1);
}
};
46 changes: 46 additions & 0 deletions drizzle-kit/src/cli/commands/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ export type ExportConfig = {
sql: boolean;
};

export type SquashConfig = {
dialect: Dialect;
out: string;
prefix: Prefix;
start: number;
end: number;
};

export const prepareGenerateConfig = async (
options: {
config?: string;
Expand Down Expand Up @@ -238,6 +246,44 @@ export const prepareExportConfig = async (
};
};

export const prepareSquashConfig = async (
options: {
config?: string;
dialect?: Dialect;
out?: string;
prefix?: Prefix;
start: number;
end: number;
},
from: "config" | "cli",
): Promise<SquashConfig> => {
const config =
from === "config"
? await drizzleConfigFromFile(options.config, true)
: options;

const { dialect, out } = config;

if (!dialect) {
console.log(error("Please provide required params:"));
console.log(wrapParam("dialect", dialect));
process.exit(1);
}

const prefix =
("migrations" in config
? config.migrations?.prefix
: (config.prefix as Prefix | undefined)) || "index";

return {
dialect,
out: out || "drizzle",
prefix,
start: options.start,
end: options.end,
};
};

export const flattenDatabaseCredentials = (config: any) => {
if ('dbCredentials' in config) {
const { dbCredentials, ...rest } = config;
Expand Down
4 changes: 2 additions & 2 deletions drizzle-kit/src/cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { command, run } from '@drizzle-team/brocli';
import chalk from 'chalk';
import { check, drop, exportRaw, generate, migrate, pull, push, studio, up } from './schema';
import { check, drop, exportRaw, generate, migrate, pull, push, squash, studio, up } from './schema';
import { ormCoreVersions } from './utils';

const version = async () => {
Expand Down Expand Up @@ -42,7 +42,7 @@ const legacy = [
legacyCommand('check:sqlite', 'check'),
];

run([generate, migrate, pull, push, studio, up, check, drop, exportRaw, ...legacy], {
run([generate, migrate, pull, push, studio, up, check, drop, exportRaw, squash, ...legacy], {
name: 'drizzle-kit',
version: version,
});
36 changes: 36 additions & 0 deletions drizzle-kit/src/cli/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ import {
prepareMigrateConfig,
preparePullConfig,
preparePushConfig,
prepareSquashConfig,
prepareStudioConfig,
} from './commands/utils';
import { assertOrmCoreVersion, assertPackages, assertStudioNodeVersion, ormVersionGt } from './utils';
import { assertCollisions, drivers, prefixes } from './validations/common';
import { withStyle } from './validations/outputs';
import { error, grey, MigrateProgress } from './views';
import { squashMigrations } from './commands/squash';

const optionDialect = string('dialect')
.enum(...dialects)
Expand Down Expand Up @@ -852,3 +854,37 @@ export const exportRaw = command({
}
},
});

export const squash = command({
name: 'squash',
desc: 'Combine a range of consecutive migrations into a single migration file',
options: {
config: optionConfig,
dialect: optionDialect,
out: optionOut,
prefix: string()
.enum(...prefixes)
.default('index'),
start: number()
.int()
.min(0).required(),
end: number()
.int()
.min(0).required(),
},
transform: async (opts) => {
const from = assertCollisions(
'squash',
opts,
['start', 'end', 'prefix'],
['out', 'dialect'],
);
return prepareSquashConfig(opts, from);
},
handler: async (opts) => {
await assertOrmCoreVersion();
await assertPackages('drizzle-orm');

await squashMigrations(opts);
}
})
3 changes: 2 additions & 1 deletion drizzle-kit/src/cli/validations/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export type Commands =
| 'up'
| 'drop'
| 'push'
| 'export';
| 'export'
| 'squash';

type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
type IsUnion<T> = [T] extends [UnionToIntersection<T>] ? false : true;
Expand Down
Loading