@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.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +33 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +20 -0
- package/.github-issue-bug.md +57 -0
- package/.github-workflows-ci.yml +41 -0
- package/CODE_OF_CONDUCT.md +45 -0
- package/CONTRIBUTING.md +29 -0
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/SECURITY.md +15 -0
- package/dbgit-0.1.0-beta.1.tgz +0 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +173 -0
- package/dist/commands/branch.d.ts +3 -0
- package/dist/commands/branch.js +34 -0
- package/dist/commands/checkout.d.ts +1 -0
- package/dist/commands/checkout.js +59 -0
- package/dist/commands/commit.d.ts +3 -0
- package/dist/commands/commit.js +86 -0
- package/dist/commands/diff.d.ts +1 -0
- package/dist/commands/diff.js +91 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +115 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +84 -0
- package/dist/commands/log.d.ts +1 -0
- package/dist/commands/log.js +18 -0
- package/dist/commands/merge.d.ts +1 -0
- package/dist/commands/merge.js +101 -0
- package/dist/commands/purge.d.ts +1 -0
- package/dist/commands/purge.js +85 -0
- package/dist/commands/restore-backup.d.ts +1 -0
- package/dist/commands/restore-backup.js +14 -0
- package/dist/commands/restore.d.ts +1 -0
- package/dist/commands/restore.js +33 -0
- package/dist/commands/rollback.d.ts +6 -0
- package/dist/commands/rollback.js +187 -0
- package/dist/core/backup.d.ts +5 -0
- package/dist/core/backup.js +40 -0
- package/dist/core/connector.d.ts +13 -0
- package/dist/core/connector.js +39 -0
- package/dist/core/dependencyGraph.d.ts +5 -0
- package/dist/core/dependencyGraph.js +87 -0
- package/dist/core/differ.d.ts +3 -0
- package/dist/core/differ.js +218 -0
- package/dist/core/generator.d.ts +3 -0
- package/dist/core/generator.js +104 -0
- package/dist/core/ignore.d.ts +3 -0
- package/dist/core/ignore.js +20 -0
- package/dist/core/schemaLock.d.ts +3 -0
- package/dist/core/schemaLock.js +18 -0
- package/dist/core/snapshot.d.ts +4 -0
- package/dist/core/snapshot.js +121 -0
- package/dist/core/store.d.ts +19 -0
- package/dist/core/store.js +102 -0
- package/dist/core/transaction.d.ts +2 -0
- package/dist/core/transaction.js +17 -0
- package/dist/core/validator.d.ts +4 -0
- package/dist/core/validator.js +18 -0
- package/dist/types/changes.d.ts +27 -0
- package/dist/types/changes.js +14 -0
- package/dist/types/commits.d.ts +21 -0
- package/dist/types/commits.js +1 -0
- package/dist/types/schema.d.ts +40 -0
- package/dist/types/schema.js +1 -0
- package/package.json +38 -0
- 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,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,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,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,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
|
+
}
|