@developer-ayushanand/dbgit 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +33 -0
  2. package/.github/PULL_REQUEST_TEMPLATE.md +20 -0
  3. package/.github-issue-bug.md +57 -0
  4. package/.github-workflows-ci.yml +41 -0
  5. package/CODE_OF_CONDUCT.md +45 -0
  6. package/CONTRIBUTING.md +29 -0
  7. package/LICENSE +21 -0
  8. package/README.md +115 -0
  9. package/SECURITY.md +15 -0
  10. package/dbgit-0.1.0-beta.1.tgz +0 -0
  11. package/dist/cli.d.ts +2 -0
  12. package/dist/cli.js +173 -0
  13. package/dist/commands/branch.d.ts +3 -0
  14. package/dist/commands/branch.js +34 -0
  15. package/dist/commands/checkout.d.ts +1 -0
  16. package/dist/commands/checkout.js +59 -0
  17. package/dist/commands/commit.d.ts +3 -0
  18. package/dist/commands/commit.js +86 -0
  19. package/dist/commands/diff.d.ts +1 -0
  20. package/dist/commands/diff.js +91 -0
  21. package/dist/commands/doctor.d.ts +1 -0
  22. package/dist/commands/doctor.js +115 -0
  23. package/dist/commands/init.d.ts +4 -0
  24. package/dist/commands/init.js +84 -0
  25. package/dist/commands/log.d.ts +1 -0
  26. package/dist/commands/log.js +18 -0
  27. package/dist/commands/merge.d.ts +1 -0
  28. package/dist/commands/merge.js +101 -0
  29. package/dist/commands/purge.d.ts +1 -0
  30. package/dist/commands/purge.js +85 -0
  31. package/dist/commands/restore-backup.d.ts +1 -0
  32. package/dist/commands/restore-backup.js +14 -0
  33. package/dist/commands/restore.d.ts +1 -0
  34. package/dist/commands/restore.js +33 -0
  35. package/dist/commands/rollback.d.ts +6 -0
  36. package/dist/commands/rollback.js +187 -0
  37. package/dist/core/backup.d.ts +5 -0
  38. package/dist/core/backup.js +40 -0
  39. package/dist/core/connector.d.ts +13 -0
  40. package/dist/core/connector.js +39 -0
  41. package/dist/core/dependencyGraph.d.ts +5 -0
  42. package/dist/core/dependencyGraph.js +87 -0
  43. package/dist/core/differ.d.ts +3 -0
  44. package/dist/core/differ.js +218 -0
  45. package/dist/core/generator.d.ts +3 -0
  46. package/dist/core/generator.js +104 -0
  47. package/dist/core/ignore.d.ts +3 -0
  48. package/dist/core/ignore.js +20 -0
  49. package/dist/core/schemaLock.d.ts +3 -0
  50. package/dist/core/schemaLock.js +18 -0
  51. package/dist/core/snapshot.d.ts +4 -0
  52. package/dist/core/snapshot.js +121 -0
  53. package/dist/core/store.d.ts +19 -0
  54. package/dist/core/store.js +102 -0
  55. package/dist/core/transaction.d.ts +2 -0
  56. package/dist/core/transaction.js +17 -0
  57. package/dist/core/validator.d.ts +4 -0
  58. package/dist/core/validator.js +18 -0
  59. package/dist/types/changes.d.ts +27 -0
  60. package/dist/types/changes.js +14 -0
  61. package/dist/types/commits.d.ts +21 -0
  62. package/dist/types/commits.js +1 -0
  63. package/dist/types/schema.d.ts +40 -0
  64. package/dist/types/schema.js +1 -0
  65. package/package.json +38 -0
  66. package/tsconfig.json +16 -0
@@ -0,0 +1,6 @@
1
+ export declare function rollbackCommand(hash: string, options: {
2
+ safe: boolean;
3
+ force: boolean;
4
+ dryRun: boolean;
5
+ softDelete: boolean;
6
+ }): Promise<void>;
@@ -0,0 +1,187 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import prompts from 'prompts';
4
+ import crypto from 'crypto';
5
+ import boxen from 'boxen';
6
+ import { isInitialized, getHead, loadCommit, loadSnapshot, setHead, loadConfig, saveCommit, saveSnapshot, saveBranch, loadBranch } from '../core/store.js';
7
+ import { createPool, getConnectionConfig, query } from '../core/connector.js';
8
+ import { captureSnapshot } from '../core/snapshot.js';
9
+ import { diffSnapshots } from '../core/differ.js';
10
+ import { validateSchemaNotDrifted } from '../core/validator.js';
11
+ import { acquireLock, releaseLock } from '../core/schemaLock.js';
12
+ import { createBackup } from '../core/backup.js';
13
+ import { orderChanges } from '../core/dependencyGraph.js';
14
+ import { generateForwardSQL } from '../core/generator.js';
15
+ import { runInTransaction } from '../core/transaction.js';
16
+ import { loadIgnoreList } from '../core/ignore.js';
17
+ import { ChangeType } from '../types/changes.js';
18
+ export async function rollbackCommand(hash, options) {
19
+ if (!isInitialized()) {
20
+ console.error(chalk.red("Not a DBGit repository. Run 'dbgit init' first."));
21
+ process.exit(1);
22
+ }
23
+ const head = getHead();
24
+ if (!head.commit) {
25
+ console.error(chalk.red("No commits to rollback from."));
26
+ process.exit(1);
27
+ }
28
+ const ignoreList = loadIgnoreList();
29
+ const config = getConnectionConfig();
30
+ const pool = createPool(config);
31
+ const dbConfig = loadConfig();
32
+ const isProd = dbConfig.DBGIT_MODE === 'prod';
33
+ try {
34
+ const targetCommit = loadCommit(hash);
35
+ const headCommit = loadCommit(head.commit);
36
+ const targetSnapshot = loadSnapshot(targetCommit.snapshotHash);
37
+ const spinner = ora('Validating schema...').start();
38
+ try {
39
+ await validateSchemaNotDrifted(pool, headCommit, ignoreList);
40
+ }
41
+ catch (e) {
42
+ spinner.fail(chalk.red(e.message));
43
+ await pool.end();
44
+ process.exit(1);
45
+ }
46
+ spinner.succeed('Schema validation passed.');
47
+ const liveSnapshot = await captureSnapshot(pool, ignoreList);
48
+ const changeset = diffSnapshots(liveSnapshot, targetSnapshot);
49
+ if (changeset.changes.length === 0) {
50
+ console.log(chalk.gray("ℹ Database schema is already at target state."));
51
+ await pool.end();
52
+ return;
53
+ }
54
+ if (changeset.hasDestructive) {
55
+ const destructiveChanges = changeset.changes.filter(c => c.isDestructive);
56
+ const allFks = Object.values(liveSnapshot.tables).flatMap(t => t.foreignKeys);
57
+ let impactReport = chalk.yellow('⚠ DESTRUCTIVE CHANGES DETECTED\n\n');
58
+ for (const change of destructiveChanges) {
59
+ let impact = '';
60
+ let dependencies = [];
61
+ if (change.type === ChangeType.DROP_TABLE) {
62
+ const rows = await query(pool, `SELECT count(*) FROM "${change.table}"`);
63
+ impact = `${rows[0].count} rows`;
64
+ dependencies = allFks
65
+ .filter(fk => fk.targetTable === change.table)
66
+ .map(fk => `${fk.sourceTable}.${fk.sourceColumn}`);
67
+ }
68
+ else if (change.type === ChangeType.DROP_COLUMN) {
69
+ const rows = await query(pool, `SELECT count(*) FROM "${change.table}" WHERE "${change.objectName}" IS NOT NULL`);
70
+ impact = `${rows[0].count} non-null rows`;
71
+ dependencies = allFks
72
+ .filter(fk => fk.targetTable === change.table && fk.targetColumn === change.objectName)
73
+ .map(fk => `${fk.sourceTable}.${fk.sourceColumn}`);
74
+ }
75
+ impactReport += chalk.red(`✗ ${change.type}: ${change.table}${change.objectName ? '.' + change.objectName : ''}\n`);
76
+ impactReport += chalk.gray(` Impact: ${impact}\n`);
77
+ if (dependencies.length > 0) {
78
+ impactReport += chalk.red(` Referenced by: ${dependencies.join(', ')}\n`);
79
+ }
80
+ impactReport += '\n';
81
+ }
82
+ console.log(boxen(impactReport.trim(), { padding: 1, borderColor: 'red', title: 'Rollback Risk: HIGH' }));
83
+ if (isProd) {
84
+ if (!options.safe) {
85
+ console.error(chalk.red("\nError: Production mode. Backup is mandatory for destructive operations. Use --safe."));
86
+ process.exit(1);
87
+ }
88
+ }
89
+ else {
90
+ if (!options.force && !options.safe) {
91
+ const response = await prompts({
92
+ type: 'confirm',
93
+ name: 'value',
94
+ message: 'Continue with destructive rollback?',
95
+ initial: false
96
+ });
97
+ if (!response.value) {
98
+ console.log(chalk.gray('Rollback aborted.'));
99
+ process.exit(1);
100
+ }
101
+ }
102
+ }
103
+ if (options.force && !isProd) {
104
+ const response = await prompts({
105
+ type: 'text',
106
+ name: 'confirm',
107
+ message: 'Type DESTROY DATA to continue:'
108
+ });
109
+ if (response.confirm !== 'DESTROY DATA') {
110
+ console.log(chalk.gray('Rollback aborted.'));
111
+ process.exit(1);
112
+ }
113
+ }
114
+ if (options.safe) {
115
+ const backupSpinner = ora('Creating backup...').start();
116
+ const backupPath = await createBackup(head.commit, config);
117
+ if (backupPath) {
118
+ backupSpinner.succeed(`Backup created: ${backupPath}`);
119
+ }
120
+ else {
121
+ backupSpinner.warn('Backup skipped or failed.');
122
+ }
123
+ }
124
+ }
125
+ const orderedChanges = orderChanges(changeset.changes, liveSnapshot);
126
+ let sqlStatements = generateForwardSQL({ ...changeset, changes: orderedChanges });
127
+ if (options.softDelete) {
128
+ const timestamp = Date.now();
129
+ sqlStatements = sqlStatements.map(sql => {
130
+ if (sql.startsWith('DROP TABLE')) {
131
+ const tableName = sql.replace('DROP TABLE ', '').trim();
132
+ return `ALTER TABLE ${tableName} RENAME TO _dbgit_deleted_${tableName.replace(/"/g, '')}_${timestamp}`;
133
+ }
134
+ if (sql.includes('DROP COLUMN')) {
135
+ const match = sql.match(/ALTER TABLE (.*) DROP COLUMN (.*)/);
136
+ if (match) {
137
+ return `ALTER TABLE ${match[1].trim()} RENAME COLUMN ${match[2].trim()} TO _dbgit_deleted_${match[2].trim().replace(/"/g, '')}_${timestamp}`;
138
+ }
139
+ }
140
+ return sql;
141
+ });
142
+ }
143
+ if (options.dryRun) {
144
+ console.log(chalk.blue('\nDry run: SQL statements that would be executed:'));
145
+ sqlStatements.forEach(sql => console.log(chalk.gray(sql)));
146
+ console.log(chalk.green('\nDry run complete. Nothing executed.'));
147
+ await pool.end();
148
+ return;
149
+ }
150
+ await acquireLock(pool);
151
+ const applySpinner = ora('Applying rollback...').start();
152
+ try {
153
+ await runInTransaction(pool, sqlStatements);
154
+ // Create a NEW commit for the rollback
155
+ const newCommitHash = crypto.createHash('sha256').update(Date.now().toString() + hash).digest('hex').substring(0, 7);
156
+ const rollbackCommit = {
157
+ commitHash: newCommitHash,
158
+ snapshotHash: targetCommit.snapshotHash,
159
+ parent: head.commit,
160
+ timestamp: new Date().toISOString(),
161
+ message: `rollback: restore schema state from ${hash.substring(0, 7)}`,
162
+ branch: head.branch,
163
+ schemaHash: targetCommit.schemaHash,
164
+ type: 'rollback',
165
+ targetCommit: hash
166
+ };
167
+ saveSnapshot(targetCommit.snapshotHash, targetSnapshot); // Ensure snapshot is saved for the new commit
168
+ saveCommit(rollbackCommit);
169
+ const newHead = { ...head, commit: newCommitHash };
170
+ setHead(newHead);
171
+ if (head.branch) {
172
+ const branch = loadBranch(head.branch);
173
+ branch.headCommit = newCommitHash;
174
+ saveBranch(branch);
175
+ }
176
+ applySpinner.succeed(chalk.green(`Rolled back to ${hash.substring(0, 7)}. New commit ${newCommitHash} created.`));
177
+ }
178
+ finally {
179
+ await releaseLock(pool);
180
+ }
181
+ await pool.end();
182
+ }
183
+ catch (e) {
184
+ console.error(chalk.red(`Rollback failed: ${e.message}`));
185
+ process.exit(1);
186
+ }
187
+ }
@@ -0,0 +1,5 @@
1
+ import { ConnectionConfig } from './connector.js';
2
+ export declare function createBackup(hash: string, config: ConnectionConfig): Promise<string>;
3
+ export declare function listBackups(): string[];
4
+ export declare function backupExists(hash: string): boolean;
5
+ export declare function getRestoreCommand(hash: string, config: ConnectionConfig): string;
@@ -0,0 +1,40 @@
1
+ import { execSync } from 'child_process';
2
+ import path from 'path';
3
+ import fs from 'fs';
4
+ import { getBackupsDir } from './store.js';
5
+ export async function createBackup(hash, config) {
6
+ const backupDir = path.join(getBackupsDir(), hash);
7
+ if (!fs.existsSync(backupDir)) {
8
+ fs.mkdirSync(backupDir, { recursive: true });
9
+ }
10
+ const backupFile = path.join(backupDir, 'backup.sql');
11
+ const env = {
12
+ ...process.env,
13
+ PGPASSWORD: config.password
14
+ };
15
+ const command = `pg_dump -h ${config.host} -p ${config.port} -U ${config.user} -d ${config.database} -f "${backupFile}"`;
16
+ try {
17
+ execSync(command, { env, stdio: 'pipe' });
18
+ return backupFile;
19
+ }
20
+ catch (error) {
21
+ if (error.message.includes('pg_dump: not found') || error.message.includes('command not found')) {
22
+ console.warn("pg_dump not found. Install PostgreSQL client tools and ensure pg_dump is in your PATH. Skipping backup.");
23
+ return '';
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+ export function listBackups() {
29
+ const backupsDir = getBackupsDir();
30
+ if (!fs.existsSync(backupsDir))
31
+ return [];
32
+ return fs.readdirSync(backupsDir);
33
+ }
34
+ export function backupExists(hash) {
35
+ return fs.existsSync(path.join(getBackupsDir(), hash, 'backup.sql'));
36
+ }
37
+ export function getRestoreCommand(hash, config) {
38
+ const backupFile = path.join(getBackupsDir(), hash, 'backup.sql');
39
+ return `psql -h ${config.host} -p ${config.port} -U ${config.user} -d ${config.database} -f "${backupFile}"`;
40
+ }
@@ -0,0 +1,13 @@
1
+ import pg from 'pg';
2
+ export interface ConnectionConfig {
3
+ host?: string;
4
+ port?: number;
5
+ database?: string;
6
+ user?: string;
7
+ password?: string;
8
+ ssl?: boolean;
9
+ connectionString?: string;
10
+ }
11
+ export declare function getConnectionConfig(): ConnectionConfig;
12
+ export declare function createPool(config: ConnectionConfig): pg.Pool;
13
+ export declare function query<T>(pool: pg.Pool, sql: string, params?: unknown[]): Promise<T[]>;
@@ -0,0 +1,39 @@
1
+ import pg from 'pg';
2
+ const { Pool } = pg;
3
+ import { loadConfig } from './store.js';
4
+ export function getConnectionConfig() {
5
+ const storedConfig = loadConfig();
6
+ // 1. DATABASE_URL
7
+ if (process.env.DATABASE_URL) {
8
+ return { connectionString: process.env.DATABASE_URL };
9
+ }
10
+ // 2. DBGIT_DATABASE_URL
11
+ if (process.env.DBGIT_DATABASE_URL) {
12
+ return { connectionString: process.env.DBGIT_DATABASE_URL };
13
+ }
14
+ // 3. .dbgit/config databaseUrl (if we support it there in the future, for now it might be in individual fields)
15
+ if (storedConfig.databaseUrl) {
16
+ return { connectionString: storedConfig.databaseUrl };
17
+ }
18
+ // 4. Legacy individual variables (Env first, then config)
19
+ const host = process.env.DBGIT_HOST || storedConfig.DBGIT_HOST || 'localhost';
20
+ const port = parseInt(process.env.DBGIT_PORT || storedConfig.DBGIT_PORT || '5432', 10);
21
+ const database = process.env.DBGIT_DATABASE || storedConfig.DBGIT_DATABASE || '';
22
+ const user = process.env.DBGIT_USER || storedConfig.DBGIT_USER || '';
23
+ const password = process.env.DBGIT_PASSWORD || storedConfig.DBGIT_PASSWORD;
24
+ const ssl = (process.env.DBGIT_SSL === 'true') || (storedConfig.DBGIT_SSL === 'true') || false;
25
+ return { host, port, database, user, password, ssl };
26
+ }
27
+ export function createPool(config) {
28
+ if (config.connectionString) {
29
+ return new Pool({ connectionString: config.connectionString });
30
+ }
31
+ if (!config.database || !config.user) {
32
+ throw new Error('Database name and user are required. Set DATABASE_URL or individual DBGIT_* env vars.');
33
+ }
34
+ return new Pool(config);
35
+ }
36
+ export async function query(pool, sql, params) {
37
+ const res = await pool.query(sql, params);
38
+ return res.rows;
39
+ }
@@ -0,0 +1,5 @@
1
+ import { ForeignKey, SchemaSnapshot } from '../types/schema.js';
2
+ import { Change } from '../types/changes.js';
3
+ export declare function orderForDeletion(tables: string[], foreignKeys: ForeignKey[]): string[];
4
+ export declare function orderForCreation(tables: string[], foreignKeys: ForeignKey[]): string[];
5
+ export declare function orderChanges(changes: Change[], snapshot: SchemaSnapshot): Change[];
@@ -0,0 +1,87 @@
1
+ import { ChangeType } from '../types/changes.js';
2
+ export function orderForDeletion(tables, foreignKeys) {
3
+ // Kahn's algorithm for topological sort
4
+ // For deletion, we want to delete tables that are NOT referenced by others first.
5
+ // Dependencies: targetTable -> sourceTable (target must exist for source to exist)
6
+ // For deletion: sourceTable must be deleted before targetTable if there is an FK from source to target.
7
+ const adj = new Map();
8
+ const inDegree = new Map();
9
+ tables.forEach(t => {
10
+ adj.set(t, []);
11
+ inDegree.set(t, 0);
12
+ });
13
+ foreignKeys.forEach(fk => {
14
+ if (adj.has(fk.sourceTable) && adj.has(fk.targetTable)) {
15
+ adj.get(fk.sourceTable).push(fk.targetTable);
16
+ inDegree.set(fk.targetTable, (inDegree.get(fk.targetTable) || 0) + 1);
17
+ }
18
+ });
19
+ const queue = [];
20
+ inDegree.forEach((degree, table) => {
21
+ if (degree === 0)
22
+ queue.push(table);
23
+ });
24
+ const result = [];
25
+ while (queue.length > 0) {
26
+ const u = queue.shift();
27
+ result.push(u);
28
+ adj.get(u)?.forEach(v => {
29
+ inDegree.set(v, inDegree.get(v) - 1);
30
+ if (inDegree.get(v) === 0)
31
+ queue.push(v);
32
+ });
33
+ }
34
+ // Any tables not in result are part of a cycle (shouldn't happen with FKs normally)
35
+ const remaining = tables.filter(t => !result.includes(t));
36
+ return [...result, ...remaining];
37
+ }
38
+ export function orderForCreation(tables, foreignKeys) {
39
+ return orderForDeletion(tables, foreignKeys).reverse();
40
+ }
41
+ export function orderChanges(changes, snapshot) {
42
+ // Order:
43
+ // 1. DROP_FOREIGN_KEY
44
+ // 2. DROP_CONSTRAINT
45
+ // 3. DROP_INDEX
46
+ // 4. DROP_COLUMN
47
+ // 5. DROP_TABLE (topologically ordered)
48
+ // 6. MODIFY_COLUMN
49
+ // 7. ADD_TABLE (topologically ordered)
50
+ // 8. ADD_COLUMN
51
+ // 9. ADD_INDEX
52
+ // 10. ADD_CONSTRAINT
53
+ // 11. ADD_FOREIGN_KEY
54
+ const dropFks = changes.filter(c => c.type === ChangeType.DROP_FOREIGN_KEY);
55
+ const dropConstraints = changes.filter(c => c.type === ChangeType.DROP_CONSTRAINT);
56
+ const dropIndexes = changes.filter(c => c.type === ChangeType.DROP_INDEX);
57
+ const dropColumns = changes.filter(c => c.type === ChangeType.DROP_COLUMN);
58
+ const dropTables = changes.filter(c => c.type === ChangeType.DROP_TABLE);
59
+ const modifyColumns = changes.filter(c => c.type === ChangeType.MODIFY_COLUMN);
60
+ const addTables = changes.filter(c => c.type === ChangeType.ADD_TABLE);
61
+ const addColumns = changes.filter(c => c.type === ChangeType.ADD_COLUMN);
62
+ const addIndexes = changes.filter(c => c.type === ChangeType.ADD_INDEX);
63
+ const addConstraints = changes.filter(c => c.type === ChangeType.ADD_CONSTRAINT);
64
+ const addFks = changes.filter(c => c.type === ChangeType.ADD_FOREIGN_KEY);
65
+ // Topologically sort dropTables
66
+ const allFks = Object.values(snapshot.tables).flatMap(t => t.foreignKeys);
67
+ const dropTableNames = dropTables.map(c => c.table);
68
+ const sortedDropTableNames = orderForDeletion(dropTableNames, allFks);
69
+ const sortedDropTables = sortedDropTableNames.map(name => dropTables.find(c => c.table === name));
70
+ // Topologically sort addTables
71
+ const addTableNames = addTables.map(c => c.table);
72
+ const sortedAddTableNames = orderForCreation(addTableNames, allFks);
73
+ const sortedAddTables = sortedAddTableNames.map(name => addTables.find(c => c.table === name));
74
+ return [
75
+ ...dropFks,
76
+ ...dropConstraints,
77
+ ...dropIndexes,
78
+ ...dropColumns,
79
+ ...sortedDropTables,
80
+ ...modifyColumns,
81
+ ...sortedAddTables,
82
+ ...addColumns,
83
+ ...addIndexes,
84
+ ...addConstraints,
85
+ ...addFks
86
+ ];
87
+ }
@@ -0,0 +1,3 @@
1
+ import { SchemaSnapshot } from '../types/schema.js';
2
+ import { ChangeSet } from '../types/changes.js';
3
+ export declare function diffSnapshots(from: SchemaSnapshot, to: SchemaSnapshot): ChangeSet;
@@ -0,0 +1,218 @@
1
+ import { ChangeType } from '../types/changes.js';
2
+ export function diffSnapshots(from, to) {
3
+ const changes = [];
4
+ const fromTables = Object.keys(from.tables);
5
+ const toTables = Object.keys(to.tables);
6
+ // Tables to drop
7
+ for (const tableName of fromTables) {
8
+ if (!to.tables[tableName]) {
9
+ changes.push({
10
+ type: ChangeType.DROP_TABLE,
11
+ table: tableName,
12
+ before: from.tables[tableName],
13
+ isDestructive: true
14
+ });
15
+ }
16
+ }
17
+ // Tables to add or modify
18
+ for (const tableName of toTables) {
19
+ const fromTable = from.tables[tableName];
20
+ const toTable = to.tables[tableName];
21
+ if (!fromTable) {
22
+ changes.push({
23
+ type: ChangeType.ADD_TABLE,
24
+ table: tableName,
25
+ after: toTable,
26
+ isDestructive: false
27
+ });
28
+ // All columns, indexes etc in this table are part of ADD_TABLE
29
+ continue;
30
+ }
31
+ // Diff columns
32
+ diffColumns(fromTable, toTable, changes);
33
+ // Diff indexes
34
+ diffIndexes(fromTable, toTable, changes);
35
+ // Diff constraints
36
+ diffConstraints(fromTable, toTable, changes);
37
+ // Diff foreign keys
38
+ diffForeignKeys(fromTable, toTable, changes);
39
+ }
40
+ return {
41
+ changes,
42
+ hasDestructive: changes.some(c => c.isDestructive)
43
+ };
44
+ }
45
+ function diffColumns(from, to, changes) {
46
+ const fromCols = new Map(from.columns.map(c => [c.name, c]));
47
+ const toCols = new Map(to.columns.map(c => [c.name, c]));
48
+ for (const [name, col] of fromCols) {
49
+ if (!toCols.has(name)) {
50
+ changes.push({
51
+ type: ChangeType.DROP_COLUMN,
52
+ table: from.name,
53
+ objectName: name,
54
+ before: col,
55
+ isDestructive: true
56
+ });
57
+ }
58
+ }
59
+ for (const [name, col] of toCols) {
60
+ const fromCol = fromCols.get(name);
61
+ if (!fromCol) {
62
+ changes.push({
63
+ type: ChangeType.ADD_COLUMN,
64
+ table: to.name,
65
+ objectName: name,
66
+ after: col,
67
+ isDestructive: false
68
+ });
69
+ }
70
+ else if (fromCol.type !== col.type || fromCol.nullable !== col.nullable || fromCol.default !== col.default) {
71
+ changes.push({
72
+ type: ChangeType.MODIFY_COLUMN,
73
+ table: to.name,
74
+ objectName: name,
75
+ before: fromCol,
76
+ after: col,
77
+ isDestructive: false // Usually not destructive, but can be
78
+ });
79
+ }
80
+ }
81
+ }
82
+ function diffIndexes(from, to, changes) {
83
+ const fromIdx = new Map(from.indexes.map(i => [i.name, i]));
84
+ const toIdx = new Map(to.indexes.map(i => [i.name, i]));
85
+ for (const [name, idx] of fromIdx) {
86
+ if (!toIdx.has(name)) {
87
+ changes.push({
88
+ type: ChangeType.DROP_INDEX,
89
+ table: from.name,
90
+ objectName: name,
91
+ before: idx,
92
+ isDestructive: false
93
+ });
94
+ }
95
+ }
96
+ for (const [name, idx] of toIdx) {
97
+ const fromIdxObj = fromIdx.get(name);
98
+ if (!fromIdxObj) {
99
+ changes.push({
100
+ type: ChangeType.ADD_INDEX,
101
+ table: to.name,
102
+ objectName: name,
103
+ after: idx,
104
+ isDestructive: false
105
+ });
106
+ }
107
+ else if (JSON.stringify(fromIdxObj) !== JSON.stringify(idx)) {
108
+ changes.push({
109
+ type: ChangeType.DROP_INDEX,
110
+ table: from.name,
111
+ objectName: name,
112
+ before: fromIdxObj,
113
+ isDestructive: false
114
+ });
115
+ changes.push({
116
+ type: ChangeType.ADD_INDEX,
117
+ table: to.name,
118
+ objectName: name,
119
+ after: idx,
120
+ isDestructive: false
121
+ });
122
+ }
123
+ }
124
+ }
125
+ function diffConstraints(from, to, changes) {
126
+ const fromCons = new Map(from.constraints.map(c => [c.name, c]));
127
+ const toCons = new Map(to.constraints.map(c => [c.name, c]));
128
+ for (const [name, con] of fromCons) {
129
+ if (!toCons.has(name)) {
130
+ changes.push({
131
+ type: ChangeType.DROP_CONSTRAINT,
132
+ table: from.name,
133
+ objectName: name,
134
+ before: con,
135
+ beforeTable: from,
136
+ isDestructive: false
137
+ });
138
+ }
139
+ }
140
+ for (const [name, con] of toCons) {
141
+ const fromCon = fromCons.get(name);
142
+ if (!fromCon) {
143
+ changes.push({
144
+ type: ChangeType.ADD_CONSTRAINT,
145
+ table: to.name,
146
+ objectName: name,
147
+ after: con,
148
+ afterTable: to,
149
+ isDestructive: false
150
+ });
151
+ }
152
+ else if (JSON.stringify(fromCon) !== JSON.stringify(con)) {
153
+ changes.push({
154
+ type: ChangeType.DROP_CONSTRAINT,
155
+ table: from.name,
156
+ objectName: name,
157
+ before: fromCon,
158
+ beforeTable: from,
159
+ isDestructive: false
160
+ });
161
+ changes.push({
162
+ type: ChangeType.ADD_CONSTRAINT,
163
+ table: to.name,
164
+ objectName: name,
165
+ after: con,
166
+ afterTable: to,
167
+ isDestructive: false
168
+ });
169
+ }
170
+ }
171
+ }
172
+ function diffForeignKeys(from, to, changes) {
173
+ const fromFks = new Map(from.foreignKeys.map(f => [f.name, f]));
174
+ const toFks = new Map(to.foreignKeys.map(f => [f.name, f]));
175
+ for (const [name, fk] of fromFks) {
176
+ if (!toFks.has(name)) {
177
+ changes.push({
178
+ type: ChangeType.DROP_FOREIGN_KEY,
179
+ table: from.name,
180
+ objectName: name,
181
+ before: fk,
182
+ beforeTable: from,
183
+ isDestructive: true
184
+ });
185
+ }
186
+ }
187
+ for (const [name, fk] of toFks) {
188
+ const fromFk = fromFks.get(name);
189
+ if (!fromFk) {
190
+ changes.push({
191
+ type: ChangeType.ADD_FOREIGN_KEY,
192
+ table: to.name,
193
+ objectName: name,
194
+ after: fk,
195
+ afterTable: to,
196
+ isDestructive: false
197
+ });
198
+ }
199
+ else if (JSON.stringify(fromFk) !== JSON.stringify(fk)) {
200
+ changes.push({
201
+ type: ChangeType.DROP_FOREIGN_KEY,
202
+ table: from.name,
203
+ objectName: name,
204
+ before: fromFk,
205
+ beforeTable: from,
206
+ isDestructive: true
207
+ });
208
+ changes.push({
209
+ type: ChangeType.ADD_FOREIGN_KEY,
210
+ table: to.name,
211
+ objectName: name,
212
+ after: fk,
213
+ afterTable: to,
214
+ isDestructive: false
215
+ });
216
+ }
217
+ }
218
+ }
@@ -0,0 +1,3 @@
1
+ import { ChangeSet } from '../types/changes.js';
2
+ export declare function generateForwardSQL(changeset: ChangeSet): string[];
3
+ export declare function generateInverseSQL(changeset: ChangeSet): string[];