@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,104 @@
1
+ import { ChangeType } from '../types/changes.js';
2
+ export function generateForwardSQL(changeset) {
3
+ return changeset.changes.flatMap(change => generateChangeSQL(change, false));
4
+ }
5
+ export function generateInverseSQL(changeset) {
6
+ // To generate inverse, we process changes in reverse order
7
+ // and flip the operations.
8
+ return [...changeset.changes]
9
+ .reverse()
10
+ .flatMap(change => generateChangeSQL(change, true));
11
+ }
12
+ function generateChangeSQL(change, inverse) {
13
+ const type = inverse ? flipType(change.type) : change.type;
14
+ switch (type) {
15
+ case ChangeType.ADD_TABLE: {
16
+ const table = (inverse ? change.before : change.after);
17
+ const columnsSql = table.columns.map(renderColumn).join(', ');
18
+ let sql = `CREATE TABLE ${table.name} (${columnsSql})`;
19
+ return [sql];
20
+ }
21
+ case ChangeType.DROP_TABLE:
22
+ return [`DROP TABLE ${change.table}`];
23
+ case ChangeType.ADD_COLUMN: {
24
+ const col = (inverse ? change.before : change.after);
25
+ return [`ALTER TABLE ${change.table} ADD COLUMN ${renderColumn(col)}`];
26
+ }
27
+ case ChangeType.DROP_COLUMN:
28
+ return [`ALTER TABLE ${change.table} DROP COLUMN ${change.objectName}`];
29
+ case ChangeType.MODIFY_COLUMN: {
30
+ const col = (inverse ? change.before : change.after);
31
+ const statements = [];
32
+ statements.push(`ALTER TABLE ${change.table} ALTER COLUMN ${change.objectName} TYPE ${col.type} USING ${change.objectName}::${col.type}`);
33
+ if (col.nullable) {
34
+ statements.push(`ALTER TABLE ${change.table} ALTER COLUMN ${change.objectName} DROP NOT NULL`);
35
+ }
36
+ else {
37
+ statements.push(`ALTER TABLE ${change.table} ALTER COLUMN ${change.objectName} SET NOT NULL`);
38
+ }
39
+ if (col.default) {
40
+ statements.push(`ALTER TABLE ${change.table} ALTER COLUMN ${change.objectName} SET DEFAULT ${col.default}`);
41
+ }
42
+ else {
43
+ statements.push(`ALTER TABLE ${change.table} ALTER COLUMN ${change.objectName} DROP DEFAULT`);
44
+ }
45
+ return statements;
46
+ }
47
+ case ChangeType.ADD_INDEX: {
48
+ const idx = (inverse ? change.before : change.after);
49
+ const unique = idx.unique ? 'UNIQUE ' : '';
50
+ return [`CREATE ${unique}INDEX ${idx.name} ON ${idx.table} (${idx.columns.join(', ')})`];
51
+ }
52
+ case ChangeType.DROP_INDEX:
53
+ return [`DROP INDEX ${change.objectName}`];
54
+ case ChangeType.ADD_CONSTRAINT: {
55
+ const con = (inverse ? change.before : change.after);
56
+ // Note: definition is expected to be something like "CHECK (price > 0)"
57
+ let typeClause = con.type;
58
+ if (con.type === 'PRIMARY KEY') {
59
+ const table = (inverse ? change.beforeTable : change.afterTable);
60
+ const pkCols = table?.columns.filter(c => c.isPrimaryKey).map(c => c.name).join(', ');
61
+ if (pkCols) {
62
+ return [`ALTER TABLE ${change.table} ADD CONSTRAINT ${con.name} PRIMARY KEY (${pkCols})`];
63
+ }
64
+ }
65
+ return [`ALTER TABLE ${change.table} ADD CONSTRAINT ${con.name} ${typeClause} ${con.definition}`];
66
+ }
67
+ case ChangeType.DROP_CONSTRAINT:
68
+ return [`ALTER TABLE ${change.table} DROP CONSTRAINT ${change.objectName}`];
69
+ case ChangeType.ADD_FOREIGN_KEY: {
70
+ const fk = (inverse ? change.before : change.after);
71
+ return [`ALTER TABLE ${change.table} ADD CONSTRAINT ${fk.name} FOREIGN KEY (${fk.sourceColumn}) REFERENCES ${fk.targetTable}(${fk.targetColumn}) ON DELETE ${fk.onDelete} ON UPDATE ${fk.onUpdate}`];
72
+ }
73
+ case ChangeType.DROP_FOREIGN_KEY:
74
+ return [`ALTER TABLE ${change.table} DROP CONSTRAINT ${change.objectName}`];
75
+ default:
76
+ return [];
77
+ }
78
+ }
79
+ function renderColumn(col) {
80
+ let sql = `${col.name} ${col.type}`;
81
+ if (!col.nullable)
82
+ sql += ' NOT NULL';
83
+ if (col.default)
84
+ sql += ` DEFAULT ${col.default}`;
85
+ if (col.isPrimaryKey)
86
+ sql += ' PRIMARY KEY';
87
+ return sql;
88
+ }
89
+ function flipType(type) {
90
+ switch (type) {
91
+ case ChangeType.ADD_TABLE: return ChangeType.DROP_TABLE;
92
+ case ChangeType.DROP_TABLE: return ChangeType.ADD_TABLE;
93
+ case ChangeType.ADD_COLUMN: return ChangeType.DROP_COLUMN;
94
+ case ChangeType.DROP_COLUMN: return ChangeType.ADD_COLUMN;
95
+ case ChangeType.ADD_INDEX: return ChangeType.DROP_INDEX;
96
+ case ChangeType.DROP_INDEX: return ChangeType.ADD_INDEX;
97
+ case ChangeType.ADD_CONSTRAINT: return ChangeType.DROP_CONSTRAINT;
98
+ case ChangeType.DROP_CONSTRAINT: return ChangeType.ADD_CONSTRAINT;
99
+ case ChangeType.ADD_FOREIGN_KEY: return ChangeType.DROP_FOREIGN_KEY;
100
+ case ChangeType.DROP_FOREIGN_KEY: return ChangeType.ADD_FOREIGN_KEY;
101
+ case ChangeType.MODIFY_COLUMN: return ChangeType.MODIFY_COLUMN;
102
+ default: return type;
103
+ }
104
+ }
@@ -0,0 +1,3 @@
1
+ export declare function loadIgnoreList(): string[];
2
+ export declare function shouldIgnore(tableName: string, patterns: string[]): boolean;
3
+ export declare function isSoftDeleted(name: string): boolean;
@@ -0,0 +1,20 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { minimatch } from 'minimatch';
4
+ export function loadIgnoreList() {
5
+ const ignoreFile = path.join(process.cwd(), '.dbgitignore');
6
+ if (!fs.existsSync(ignoreFile))
7
+ return [];
8
+ return fs.readFileSync(ignoreFile, 'utf-8')
9
+ .split('\n')
10
+ .map(line => line.trim())
11
+ .filter(line => line && !line.startsWith('#'));
12
+ }
13
+ export function shouldIgnore(tableName, patterns) {
14
+ if (tableName.startsWith('_dbgit_deleted_'))
15
+ return true;
16
+ return patterns.some(pattern => minimatch(tableName, pattern));
17
+ }
18
+ export function isSoftDeleted(name) {
19
+ return name.startsWith('_dbgit_deleted_');
20
+ }
@@ -0,0 +1,3 @@
1
+ import pg from 'pg';
2
+ export declare function acquireLock(pool: pg.Pool): Promise<void>;
3
+ export declare function releaseLock(pool: pg.Pool): Promise<void>;
@@ -0,0 +1,18 @@
1
+ import { query } from './connector.js';
2
+ // Simple hash function for the lock key
3
+ function hashCode(str) {
4
+ let hash = 0;
5
+ for (let i = 0; i < str.length; i++) {
6
+ const char = str.charCodeAt(i);
7
+ hash = ((hash << 5) - hash) + char;
8
+ hash = hash & hash; // Convert to 32bit integer
9
+ }
10
+ return Math.abs(hash);
11
+ }
12
+ const LOCK_ID = hashCode('dbgit');
13
+ export async function acquireLock(pool) {
14
+ await query(pool, 'SELECT pg_advisory_lock($1)', [LOCK_ID]);
15
+ }
16
+ export async function releaseLock(pool) {
17
+ await query(pool, 'SELECT pg_advisory_unlock($1)', [LOCK_ID]);
18
+ }
@@ -0,0 +1,4 @@
1
+ import pg from 'pg';
2
+ import { SchemaSnapshot } from '../types/schema.js';
3
+ export declare function captureSnapshot(pool: pg.Pool, ignoreList: string[]): Promise<SchemaSnapshot>;
4
+ export declare function hashSnapshot(snapshot: SchemaSnapshot): string;
@@ -0,0 +1,121 @@
1
+ import crypto from 'crypto';
2
+ import { query } from './connector.js';
3
+ import { shouldIgnore } from './ignore.js';
4
+ export async function captureSnapshot(pool, ignoreList) {
5
+ const tables = {};
6
+ const tableRows = await query(pool, `
7
+ SELECT table_name FROM information_schema.tables
8
+ WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
9
+ `);
10
+ for (const tableRow of tableRows) {
11
+ const tableName = tableRow.table_name;
12
+ if (shouldIgnore(tableName, ignoreList))
13
+ continue;
14
+ const columns = await query(pool, `
15
+ SELECT column_name, data_type, character_maximum_length,
16
+ is_nullable, column_default, udt_name
17
+ FROM information_schema.columns
18
+ WHERE table_schema = 'public' AND table_name = $1
19
+ ORDER BY ordinal_position
20
+ `, [tableName]);
21
+ const primaryKeys = await query(pool, `
22
+ SELECT kcu.column_name
23
+ FROM information_schema.table_constraints tc
24
+ JOIN information_schema.key_column_usage kcu
25
+ ON tc.constraint_name = kcu.constraint_name
26
+ AND tc.table_schema = kcu.table_schema
27
+ WHERE tc.constraint_type = 'PRIMARY KEY'
28
+ AND tc.table_schema = 'public'
29
+ AND tc.table_name = $1
30
+ `, [tableName]);
31
+ const pkColumns = new Set(primaryKeys.map(pk => pk.column_name));
32
+ const indexes = await query(pool, `
33
+ SELECT indexname, indexdef
34
+ FROM pg_indexes
35
+ WHERE schemaname = 'public' AND tablename = $1
36
+ `, [tableName]);
37
+ const constraints = await query(pool, `
38
+ SELECT tc.constraint_name, tc.constraint_type, cc.check_clause
39
+ FROM information_schema.table_constraints tc
40
+ LEFT JOIN information_schema.check_constraints cc
41
+ ON tc.constraint_name = cc.constraint_name
42
+ WHERE tc.table_schema = 'public' AND tc.table_name = $1
43
+ AND tc.constraint_type IN ('PRIMARY KEY', 'UNIQUE', 'CHECK')
44
+ `, [tableName]);
45
+ const foreignKeys = await query(pool, `
46
+ SELECT
47
+ kcu.constraint_name,
48
+ kcu.column_name as source_column,
49
+ ccu.table_name as target_table,
50
+ ccu.column_name as target_column,
51
+ rc.delete_rule as on_delete,
52
+ rc.update_rule as on_update
53
+ FROM information_schema.key_column_usage kcu
54
+ JOIN information_schema.referential_constraints rc
55
+ ON kcu.constraint_name = rc.constraint_name
56
+ JOIN information_schema.constraint_column_usage ccu
57
+ ON rc.unique_constraint_name = ccu.constraint_name
58
+ WHERE kcu.table_schema = 'public' AND kcu.table_name = $1
59
+ `, [tableName]);
60
+ tables[tableName] = {
61
+ name: tableName,
62
+ columns: columns
63
+ .filter((c) => !c.column_name.startsWith('_dbgit_deleted_'))
64
+ .map((c) => ({
65
+ name: c.column_name,
66
+ type: c.character_maximum_length ? `${c.data_type}(${c.character_maximum_length})` : c.data_type,
67
+ nullable: c.is_nullable === 'YES',
68
+ default: c.column_default,
69
+ isPrimaryKey: pkColumns.has(c.column_name)
70
+ })),
71
+ indexes: indexes.map((i) => {
72
+ // Extract columns from indexdef: "CREATE UNIQUE INDEX idx_name ON table USING btree (col1, col2)"
73
+ const match = i.indexdef.match(/\((.*)\)/);
74
+ const cols = match ? match[1].split(',').map((s) => s.trim()) : [];
75
+ return {
76
+ name: i.indexname,
77
+ table: tableName,
78
+ columns: cols,
79
+ unique: i.indexdef.includes('UNIQUE')
80
+ };
81
+ }),
82
+ constraints: constraints.map((c) => ({
83
+ name: c.constraint_name,
84
+ table: tableName,
85
+ type: c.constraint_type,
86
+ definition: c.check_clause || '' // Simplification, standard definition is harder to get exactly
87
+ })),
88
+ foreignKeys: foreignKeys.map((fk) => ({
89
+ name: fk.constraint_name,
90
+ sourceTable: tableName,
91
+ sourceColumn: fk.source_column,
92
+ targetTable: fk.target_table,
93
+ targetColumn: fk.target_column,
94
+ onDelete: fk.on_delete,
95
+ onUpdate: fk.on_update
96
+ }))
97
+ };
98
+ }
99
+ const snapshot = {
100
+ tables,
101
+ capturedAt: new Date().toISOString(),
102
+ schemaHash: ''
103
+ };
104
+ snapshot.schemaHash = hashSnapshot(snapshot);
105
+ return snapshot;
106
+ }
107
+ export function hashSnapshot(snapshot) {
108
+ const sortedTables = Object.keys(snapshot.tables).sort().reduce((acc, key) => {
109
+ const table = snapshot.tables[key];
110
+ acc[key] = {
111
+ ...table,
112
+ columns: [...table.columns].sort((a, b) => a.name.localeCompare(b.name)),
113
+ indexes: [...table.indexes].sort((a, b) => a.name.localeCompare(b.name)),
114
+ constraints: [...table.constraints].sort((a, b) => a.name.localeCompare(b.name)),
115
+ foreignKeys: [...table.foreignKeys].sort((a, b) => a.name.localeCompare(b.name)),
116
+ };
117
+ return acc;
118
+ }, {});
119
+ const data = JSON.stringify(sortedTables);
120
+ return crypto.createHash('sha256').update(data).digest('hex');
121
+ }
@@ -0,0 +1,19 @@
1
+ import { Commit, Branch, HEAD } from '../types/commits.js';
2
+ import { SchemaSnapshot } from '../types/schema.js';
3
+ export declare function setRepoRoot(root: string): void;
4
+ export declare function getRepoRoot(): string;
5
+ export declare function initStore(): void;
6
+ export declare function isInitialized(): boolean;
7
+ export declare function saveCommit(commit: Commit): void;
8
+ export declare function loadCommit(hash: string): Commit;
9
+ export declare function saveSnapshot(hash: string, snapshot: SchemaSnapshot): void;
10
+ export declare function loadSnapshot(hash: string): SchemaSnapshot;
11
+ export declare function getHead(): HEAD;
12
+ export declare function setHead(head: HEAD): void;
13
+ export declare function saveBranch(branch: Branch): void;
14
+ export declare function loadBranch(name: string): Branch;
15
+ export declare function listBranches(): string[];
16
+ export declare function listCommits(branchName: string | null, headCommitHash: string | null): Commit[];
17
+ export declare function saveConfig(config: Record<string, string>): void;
18
+ export declare function loadConfig(): Record<string, string>;
19
+ export declare function getBackupsDir(): string;
@@ -0,0 +1,102 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ let repoRoot = process.cwd();
4
+ export function setRepoRoot(root) {
5
+ repoRoot = root;
6
+ }
7
+ export function getRepoRoot() {
8
+ return repoRoot;
9
+ }
10
+ const getDbgitDir = () => path.join(getRepoRoot(), '.dbgit');
11
+ const getCommitsDir = () => path.join(getDbgitDir(), 'commits');
12
+ const getSnapshotsDir = () => path.join(getDbgitDir(), 'snapshots');
13
+ const getBranchesDir = () => path.join(getDbgitDir(), 'branches');
14
+ const getBackupsDirInternal = () => path.join(getDbgitDir(), 'backups');
15
+ const getHeadFile = () => path.join(getDbgitDir(), 'HEAD');
16
+ const getConfigFile = () => path.join(getDbgitDir(), 'config');
17
+ export function initStore() {
18
+ const dbgitDir = getDbgitDir();
19
+ if (!fs.existsSync(dbgitDir))
20
+ fs.mkdirSync(dbgitDir, { recursive: true });
21
+ if (!fs.existsSync(getCommitsDir()))
22
+ fs.mkdirSync(getCommitsDir(), { recursive: true });
23
+ if (!fs.existsSync(getSnapshotsDir()))
24
+ fs.mkdirSync(getSnapshotsDir(), { recursive: true });
25
+ if (!fs.existsSync(getBranchesDir()))
26
+ fs.mkdirSync(getBranchesDir(), { recursive: true });
27
+ if (!fs.existsSync(getBackupsDirInternal()))
28
+ fs.mkdirSync(getBackupsDirInternal(), { recursive: true });
29
+ }
30
+ export function isInitialized() {
31
+ return fs.existsSync(getDbgitDir());
32
+ }
33
+ export function saveCommit(commit) {
34
+ fs.writeFileSync(path.join(getCommitsDir(), `${commit.commitHash}.json`), JSON.stringify(commit, null, 2));
35
+ }
36
+ export function loadCommit(hash) {
37
+ const filePath = path.join(getCommitsDir(), `${hash}.json`);
38
+ if (!fs.existsSync(filePath))
39
+ throw new Error(`Commit ${hash} not found.`);
40
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
41
+ }
42
+ export function saveSnapshot(hash, snapshot) {
43
+ fs.writeFileSync(path.join(getSnapshotsDir(), `${hash}.json`), JSON.stringify(snapshot, null, 2));
44
+ }
45
+ export function loadSnapshot(hash) {
46
+ const filePath = path.join(getSnapshotsDir(), `${hash}.json`);
47
+ if (!fs.existsSync(filePath))
48
+ throw new Error(`Snapshot ${hash} not found.`);
49
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
50
+ }
51
+ export function getHead() {
52
+ const headFile = getHeadFile();
53
+ if (!fs.existsSync(headFile))
54
+ return { branch: 'main', commit: null };
55
+ return JSON.parse(fs.readFileSync(headFile, 'utf-8'));
56
+ }
57
+ export function setHead(head) {
58
+ fs.writeFileSync(getHeadFile(), JSON.stringify(head, null, 2));
59
+ }
60
+ export function saveBranch(branch) {
61
+ fs.writeFileSync(path.join(getBranchesDir(), `${branch.name}.json`), JSON.stringify(branch, null, 2));
62
+ }
63
+ export function loadBranch(name) {
64
+ const filePath = path.join(getBranchesDir(), `${name}.json`);
65
+ if (!fs.existsSync(filePath))
66
+ throw new Error(`Branch ${name} not found.`);
67
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
68
+ }
69
+ export function listBranches() {
70
+ return fs.readdirSync(getBranchesDir()).map(f => f.replace('.json', ''));
71
+ }
72
+ export function listCommits(branchName, headCommitHash) {
73
+ const commits = [];
74
+ let currentHash = headCommitHash;
75
+ if (!currentHash && branchName) {
76
+ try {
77
+ const branch = loadBranch(branchName);
78
+ currentHash = branch.headCommit;
79
+ }
80
+ catch (e) {
81
+ return [];
82
+ }
83
+ }
84
+ while (currentHash) {
85
+ const commit = loadCommit(currentHash);
86
+ commits.push(commit);
87
+ currentHash = commit.parent;
88
+ }
89
+ return commits;
90
+ }
91
+ export function saveConfig(config) {
92
+ fs.writeFileSync(getConfigFile(), JSON.stringify(config, null, 2));
93
+ }
94
+ export function loadConfig() {
95
+ const configFile = getConfigFile();
96
+ if (!fs.existsSync(configFile))
97
+ return {};
98
+ return JSON.parse(fs.readFileSync(configFile, 'utf-8'));
99
+ }
100
+ export function getBackupsDir() {
101
+ return getBackupsDirInternal();
102
+ }
@@ -0,0 +1,2 @@
1
+ import pg from 'pg';
2
+ export declare function runInTransaction(pool: pg.Pool, statements: string[]): Promise<void>;
@@ -0,0 +1,17 @@
1
+ export async function runInTransaction(pool, statements) {
2
+ const client = await pool.connect();
3
+ try {
4
+ await client.query('BEGIN');
5
+ for (const sql of statements) {
6
+ await client.query(sql);
7
+ }
8
+ await client.query('COMMIT');
9
+ }
10
+ catch (e) {
11
+ await client.query('ROLLBACK');
12
+ throw new Error(`Transaction failed: ${e.message}`);
13
+ }
14
+ finally {
15
+ client.release();
16
+ }
17
+ }
@@ -0,0 +1,4 @@
1
+ import pg from 'pg';
2
+ import { Commit } from '../types/commits.js';
3
+ export declare function computeSchemaHash(pool: pg.Pool, ignoreList: string[]): Promise<string>;
4
+ export declare function validateSchemaNotDrifted(pool: pg.Pool, commit: Commit, ignoreList: string[]): Promise<void>;
@@ -0,0 +1,18 @@
1
+ import { captureSnapshot } from './snapshot.js';
2
+ export async function computeSchemaHash(pool, ignoreList) {
3
+ const snapshot = await captureSnapshot(pool, ignoreList);
4
+ return snapshot.schemaHash;
5
+ }
6
+ export async function validateSchemaNotDrifted(pool, commit, ignoreList) {
7
+ const currentHash = await computeSchemaHash(pool, ignoreList);
8
+ if (currentHash !== commit.schemaHash) {
9
+ throw new Error(`Schema drift detected.
10
+ Database has been modified outside of DBGit since the last commit.
11
+ HEAD: ${commit.schemaHash.substring(0, 8)}
12
+ Live: ${currentHash.substring(0, 8)}
13
+
14
+ Rollback aborted for safety.
15
+ If you want to keep live changes, 'dbgit commit' them first.
16
+ If you want to discard them, you may need to manually sync or use --force (not recommended).`);
17
+ }
18
+ }
@@ -0,0 +1,27 @@
1
+ export declare enum ChangeType {
2
+ ADD_TABLE = "ADD_TABLE",
3
+ DROP_TABLE = "DROP_TABLE",
4
+ ADD_COLUMN = "ADD_COLUMN",
5
+ DROP_COLUMN = "DROP_COLUMN",
6
+ MODIFY_COLUMN = "MODIFY_COLUMN",
7
+ ADD_INDEX = "ADD_INDEX",
8
+ DROP_INDEX = "DROP_INDEX",
9
+ ADD_CONSTRAINT = "ADD_CONSTRAINT",
10
+ DROP_CONSTRAINT = "DROP_CONSTRAINT",
11
+ ADD_FOREIGN_KEY = "ADD_FOREIGN_KEY",
12
+ DROP_FOREIGN_KEY = "DROP_FOREIGN_KEY"
13
+ }
14
+ export interface Change {
15
+ type: ChangeType;
16
+ table: string;
17
+ objectName?: string;
18
+ before?: unknown;
19
+ after?: unknown;
20
+ beforeTable?: unknown;
21
+ afterTable?: unknown;
22
+ isDestructive: boolean;
23
+ }
24
+ export interface ChangeSet {
25
+ changes: Change[];
26
+ hasDestructive: boolean;
27
+ }
@@ -0,0 +1,14 @@
1
+ export var ChangeType;
2
+ (function (ChangeType) {
3
+ ChangeType["ADD_TABLE"] = "ADD_TABLE";
4
+ ChangeType["DROP_TABLE"] = "DROP_TABLE";
5
+ ChangeType["ADD_COLUMN"] = "ADD_COLUMN";
6
+ ChangeType["DROP_COLUMN"] = "DROP_COLUMN";
7
+ ChangeType["MODIFY_COLUMN"] = "MODIFY_COLUMN";
8
+ ChangeType["ADD_INDEX"] = "ADD_INDEX";
9
+ ChangeType["DROP_INDEX"] = "DROP_INDEX";
10
+ ChangeType["ADD_CONSTRAINT"] = "ADD_CONSTRAINT";
11
+ ChangeType["DROP_CONSTRAINT"] = "DROP_CONSTRAINT";
12
+ ChangeType["ADD_FOREIGN_KEY"] = "ADD_FOREIGN_KEY";
13
+ ChangeType["DROP_FOREIGN_KEY"] = "DROP_FOREIGN_KEY";
14
+ })(ChangeType || (ChangeType = {}));
@@ -0,0 +1,21 @@
1
+ export interface Commit {
2
+ commitHash: string;
3
+ snapshotHash: string;
4
+ parent: string | null;
5
+ timestamp: string;
6
+ message: string;
7
+ branch: string | null;
8
+ schemaHash: string;
9
+ type?: 'commit' | 'rollback';
10
+ targetCommit?: string;
11
+ }
12
+ export interface Branch {
13
+ name: string;
14
+ headCommit: string;
15
+ createdAt: string;
16
+ basedOn: string | null;
17
+ }
18
+ export interface HEAD {
19
+ branch: string | null;
20
+ commit: string | null;
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ export interface Column {
2
+ name: string;
3
+ type: string;
4
+ nullable: boolean;
5
+ default: string | null;
6
+ isPrimaryKey: boolean;
7
+ }
8
+ export interface Index {
9
+ name: string;
10
+ table: string;
11
+ columns: string[];
12
+ unique: boolean;
13
+ }
14
+ export interface Constraint {
15
+ name: string;
16
+ table: string;
17
+ type: 'PRIMARY KEY' | 'UNIQUE' | 'CHECK' | 'FOREIGN KEY';
18
+ definition: string;
19
+ }
20
+ export interface ForeignKey {
21
+ name: string;
22
+ sourceTable: string;
23
+ sourceColumn: string;
24
+ targetTable: string;
25
+ targetColumn: string;
26
+ onDelete: string;
27
+ onUpdate: string;
28
+ }
29
+ export interface TableSchema {
30
+ name: string;
31
+ columns: Column[];
32
+ indexes: Index[];
33
+ constraints: Constraint[];
34
+ foreignKeys: ForeignKey[];
35
+ }
36
+ export interface SchemaSnapshot {
37
+ tables: Record<string, TableSchema>;
38
+ capturedAt: string;
39
+ schemaHash: string;
40
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@developer-ayushanand/dbgit",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Git for your database schema — branch, diff, commit, rollback.",
5
+ "bin": {
6
+ "dbgit": "./dist/cli.js"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "tsx src/cli.ts",
12
+ "test": "vitest run",
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "eslint src/**/*.ts",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "dependencies": {
18
+ "boxen": "^8.0.1",
19
+ "chalk": "^5.3.0",
20
+ "commander": "^11.1.0",
21
+ "minimatch": "^9.0.3",
22
+ "ora": "^7.0.1",
23
+ "pg": "^8.11.3",
24
+ "prompts": "^2.4.2"
25
+ },
26
+ "devDependencies": {
27
+ "@electric-sql/pglite": "^0.5.1",
28
+ "@types/node": "^20.11.16",
29
+ "@types/pg": "^8.20.0",
30
+ "@types/prompts": "^2.4.9",
31
+ "@typescript-eslint/eslint-plugin": "^6.21.0",
32
+ "@typescript-eslint/parser": "^6.21.0",
33
+ "eslint": "^8.56.0",
34
+ "tsx": "^4.7.0",
35
+ "typescript": "^5.3.3",
36
+ "vitest": "^1.2.2"
37
+ }
38
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist", "tests"]
16
+ }