git-sqlite-vfs 0.0.1 → 0.0.6

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 (64) hide show
  1. package/README.md +67 -32
  2. package/bin/cli.js +53 -0
  3. package/c/Makefile +31 -16
  4. package/c/download-sqlite.cjs +17 -0
  5. package/c/git-merge-sqlitevfs.c +210 -186
  6. package/c/gitvfs.c +69 -21
  7. package/c/output/git-merge-sqlitevfs.exe +0 -0
  8. package/c/output/gitvfs.dll +0 -0
  9. package/c/output/gitvfs.o +0 -0
  10. package/c/output/gitvfs_test.exe +0 -0
  11. package/c/output/main.o +0 -0
  12. package/c/output/sqlite3.o +0 -0
  13. package/c/sqlite-autoconf-3450200/INSTALL +370 -0
  14. package/c/sqlite-autoconf-3450200/Makefile.am +20 -0
  15. package/c/sqlite-autoconf-3450200/Makefile.fallback +19 -0
  16. package/c/sqlite-autoconf-3450200/Makefile.in +1050 -0
  17. package/c/sqlite-autoconf-3450200/Makefile.msc +1069 -0
  18. package/c/sqlite-autoconf-3450200/README.txt +113 -0
  19. package/c/sqlite-autoconf-3450200/Replace.cs +223 -0
  20. package/c/sqlite-autoconf-3450200/aclocal.m4 +10204 -0
  21. package/c/sqlite-autoconf-3450200/compile +348 -0
  22. package/c/sqlite-autoconf-3450200/config.guess +1754 -0
  23. package/c/sqlite-autoconf-3450200/config.sub +1890 -0
  24. package/c/sqlite-autoconf-3450200/configure +16887 -0
  25. package/c/sqlite-autoconf-3450200/configure.ac +270 -0
  26. package/c/sqlite-autoconf-3450200/depcomp +791 -0
  27. package/c/sqlite-autoconf-3450200/install-sh +541 -0
  28. package/c/sqlite-autoconf-3450200/ltmain.sh +11251 -0
  29. package/c/sqlite-autoconf-3450200/missing +215 -0
  30. package/c/sqlite-autoconf-3450200/shell.c +29659 -0
  31. package/c/sqlite-autoconf-3450200/sqlite3.1 +161 -0
  32. package/c/sqlite-autoconf-3450200/sqlite3.c +255811 -0
  33. package/c/sqlite-autoconf-3450200/sqlite3.h +13357 -0
  34. package/c/sqlite-autoconf-3450200/sqlite3.pc.in +13 -0
  35. package/c/sqlite-autoconf-3450200/sqlite3.rc +83 -0
  36. package/c/sqlite-autoconf-3450200/sqlite3ext.h +719 -0
  37. package/c/sqlite-autoconf-3450200/sqlite3rc.h +3 -0
  38. package/c/sqlite-autoconf-3450200/tea/Makefile.in +475 -0
  39. package/c/sqlite-autoconf-3450200/tea/README +36 -0
  40. package/c/sqlite-autoconf-3450200/tea/aclocal.m4 +9 -0
  41. package/c/sqlite-autoconf-3450200/tea/configure +10179 -0
  42. package/c/sqlite-autoconf-3450200/tea/configure.ac +227 -0
  43. package/c/sqlite-autoconf-3450200/tea/doc/sqlite3.n +15 -0
  44. package/c/sqlite-autoconf-3450200/tea/generic/tclsqlite3.c +4080 -0
  45. package/c/sqlite-autoconf-3450200/tea/license.terms +6 -0
  46. package/c/sqlite-autoconf-3450200/tea/pkgIndex.tcl.in +10 -0
  47. package/c/sqlite-autoconf-3450200/tea/tclconfig/install-sh +528 -0
  48. package/c/sqlite-autoconf-3450200/tea/tclconfig/tcl.m4 +4067 -0
  49. package/c/sqlite-autoconf-3450200/tea/win/makefile.vc +430 -0
  50. package/c/sqlite-autoconf-3450200/tea/win/nmakehlp.c +815 -0
  51. package/c/sqlite-autoconf-3450200/tea/win/rules.vc +711 -0
  52. package/c/sqlite-autoconf-3450200.tar.gz +0 -0
  53. package/c/sqlite3.c +255811 -0
  54. package/c/sqlite3.h +13357 -0
  55. package/c/sqlite3ext.h +719 -0
  56. package/downloader.js +63 -0
  57. package/index.d.ts +14 -0
  58. package/index.js +103 -51
  59. package/install.js +13 -49
  60. package/package.json +22 -3
  61. package/c/output/git-merge-sqlitevfs +0 -0
  62. package/c/output/gitvfs.so +0 -0
  63. package/c/output/gitvfs_test +0 -0
  64. package/test.js +0 -209
package/downloader.js ADDED
@@ -0,0 +1,63 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import os from 'node:os';
6
+ import process from 'node:process';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ export async function downloadOrBuild(targetDir) {
12
+ let pkgVersion = '0.0.2';
13
+ try {
14
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf-8'));
15
+ pkgVersion = pkg.version;
16
+ } catch(e) {}
17
+
18
+ const platform = os.platform();
19
+ const arch = os.arch();
20
+
21
+ const repo = 'fur-tea-laser/git-sqlite-vfs';
22
+ const assetName = `git-sqlite-vfs-${platform}-${arch}.tar.gz`;
23
+ const url = `https://github.com/${repo}/releases/download/v${pkgVersion}/${assetName}`;
24
+
25
+ try {
26
+ console.log(`Attempting to download prebuilt binary: ${url}`);
27
+
28
+ if (!fs.existsSync(targetDir)) {
29
+ fs.mkdirSync(targetDir, { recursive: true });
30
+ }
31
+
32
+ const res = await fetch(url);
33
+ if (!res.ok) {
34
+ throw new Error(`Failed to fetch: ${res.status} ${res.statusText}`);
35
+ }
36
+
37
+ const arrayBuffer = await res.arrayBuffer();
38
+ const buffer = Buffer.from(arrayBuffer);
39
+
40
+ const tarballPath = path.join(targetDir, 'temp.tar.gz');
41
+ fs.writeFileSync(tarballPath, buffer);
42
+
43
+ execSync(`tar -xzf temp.tar.gz`, { cwd: targetDir });
44
+ fs.unlinkSync(tarballPath);
45
+
46
+ console.log('Successfully downloaded and extracted prebuilt binary.');
47
+ } catch (err) {
48
+ console.warn(`Download failed: ${err.message}`);
49
+ console.log('Falling back to building from source...');
50
+ try {
51
+ execSync('npm run build', { stdio: 'inherit', cwd: __dirname });
52
+
53
+ const defaultOutDir = path.join(__dirname, 'c', 'output');
54
+ if (path.resolve(defaultOutDir) !== path.resolve(targetDir)) {
55
+ fs.cpSync(defaultOutDir, targetDir, { recursive: true });
56
+ }
57
+
58
+ console.log('Successfully built from source.');
59
+ } catch (buildErr) {
60
+ console.error('Failed to build from source.', buildErr.message);
61
+ }
62
+ }
63
+ }
package/index.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export const GITVFS_EXTENSION_PATH: string;
2
+
3
+ export interface BootstrapOptions {
4
+ dir?: string;
5
+ }
6
+
7
+ export function bootstrapGitVFS(options?: BootstrapOptions): Promise<void>;
8
+
9
+ export interface ConfigureGitOptions {
10
+ repoDir: string;
11
+ vfsDir: string;
12
+ }
13
+
14
+ export function configureGitIntegration(options: ConfigureGitOptions): Promise<void>;
package/index.js CHANGED
@@ -1,55 +1,107 @@
1
- const Database = require('better-sqlite3');
2
- const { execSync } = require('child_process');
3
- const path = require('path');
4
- const fs = require('fs');
5
-
6
- class GitSQLite {
7
- /**
8
- * Opens a SQLite database utilizing the custom Git Virtual File System (VFS).
9
- * By sharding the SQLite B-Tree into 4KB binary pages, it neutralizes cascading
10
- * byte shifts, allowing native Git xdelta to achieve near-perfect compression.
11
- *
12
- * @param {string} dbPath - The path to the sharded database directory (e.g., '.db')
13
- * @returns {Database} - A native better-sqlite3 database connection
14
- */
15
- static open(dbPath) {
16
- // 1. Open a temporary in-memory database to act as an extension loader
17
- // better-sqlite3 requires an active connection to load an extension.
18
- const tempDb = new Database(':memory:');
19
-
20
- // 2. Enable extensions and load our compiled C VFS extension (.so)
21
- tempDb.loadExtension(path.resolve(__dirname, 'c/output/gitvfs'));
22
-
23
- // 3. Close tempDb. The SQLite runtime inside the Node process
24
- // will permanently retain the global 'gitvfs' registration!
25
- tempDb.close();
26
-
27
- // 4. Instantiate and return the actual database connection.
28
- // Because our compiled extension registers itself as the default VFS,
29
- // better-sqlite3 will automatically route all physical I/O for this DB
30
- // through our Git-sharded C engine!
31
- return new Database(dbPath);
32
- }
33
-
34
- /**
35
- * Configures the local Git repository with optimized binary thresholds
36
- * and strictly wires up our custom C engine as a Git Merge Strategy.
37
- */
38
- static setupGit() {
39
- try {
40
- // Optimize Git for 4KB binary pages to guarantee xdelta works nicely
41
- // without prematurely terminating delta compression loops
42
- execSync('git config core.bigFileThreshold 10m', { stdio: 'ignore' });
43
-
44
- // Wire up the custom merge strategy driver with absolute paths
45
- // This natively binds our C executable to Git's conflict resolution pipeline
46
- const driverPath = path.resolve(__dirname, 'c/output/git-merge-sqlitevfs');
47
- execSync(`git config merge.sqlite_logical.name "SQLite Logical Merge Driver"`, { stdio: 'ignore' });
48
- execSync(`git config merge.sqlite_logical.driver "${driverPath} %O %A %B %P"`, { stdio: 'ignore' });
49
- } catch (err) {
50
- console.warn("Warning: Could not configure git attributes automatically.", err.message);
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { execSync } from 'node:child_process';
5
+ import fs from 'node:fs';
6
+ import process from 'node:process';
7
+ import { downloadOrBuild } from './downloader.js';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+
12
+ // Determine the correct extension based on OS
13
+ const platform = os.platform();
14
+ let ext = 'so';
15
+ if (platform === 'darwin') {
16
+ ext = 'dylib';
17
+ } else if (platform === 'win32') {
18
+ ext = 'dll';
19
+ }
20
+
21
+ const extensionPath = path.resolve(__dirname, 'c', 'output', `gitvfs.${ext}`);
22
+
23
+ export const GITVFS_EXTENSION_PATH = extensionPath;
24
+
25
+ export async function bootstrapGitVFS(options = {}) {
26
+ if (options.dir) {
27
+ if (typeof Deno !== 'undefined') {
28
+ Deno.env.set('GIT_SQLITE_VFS_DIR', options.dir);
29
+ } else {
30
+ process.env.GIT_SQLITE_VFS_DIR = options.dir;
51
31
  }
52
32
  }
33
+
34
+ let currentExtPath = extensionPath;
35
+ if (!fs.existsSync(currentExtPath)) {
36
+ const writableDir = path.join(process.cwd(), '.git-sqlite-vfs-bin');
37
+ await downloadOrBuild(writableDir);
38
+ currentExtPath = path.join(writableDir, `gitvfs.${ext}`);
39
+ }
40
+
41
+ // Dynamically import libsql so that we load the extension into its isolated native memory space.
42
+ let Database;
43
+ if (typeof Deno !== 'undefined') {
44
+ // Deno environment
45
+ const lib = await import('npm:libsql');
46
+ Database = lib.default || lib.Database || lib;
47
+ } else {
48
+ // Node.js environment
49
+ const lib = await import('libsql');
50
+ Database = lib.default || lib.Database || lib;
51
+ }
52
+
53
+ const db = new Database(':memory:');
54
+ db.loadExtension(currentExtPath);
55
+ db.close();
53
56
  }
54
57
 
55
- module.exports = GitSQLite;
58
+ export async function configureGitIntegration({ repoDir, vfsDir }) {
59
+ let driverDir = path.resolve(__dirname, 'c', 'output');
60
+ let driverPath = path.join(driverDir, 'git-merge-sqlitevfs');
61
+ if (platform === 'win32' && !fs.existsSync(driverPath) && fs.existsSync(driverPath + '.exe')) {
62
+ driverPath += '.exe';
63
+ }
64
+
65
+ if (!fs.existsSync(driverPath)) {
66
+ driverDir = path.join(process.cwd(), '.git-sqlite-vfs-bin');
67
+ await downloadOrBuild(driverDir);
68
+ driverPath = path.join(driverDir, 'git-merge-sqlitevfs');
69
+ if (platform === 'win32' && !fs.existsSync(driverPath) && fs.existsSync(driverPath + '.exe')) {
70
+ driverPath += '.exe';
71
+ }
72
+ }
73
+
74
+ // Set the merge driver
75
+ execSync(`git config merge.sqlitevfs.name "SQLite VFS Merge Driver"`, { cwd: repoDir, stdio: 'ignore' });
76
+ execSync(`git config merge.sqlitevfs.driver "${driverPath} %O %A %B %P"`, { cwd: repoDir, stdio: 'ignore' });
77
+
78
+ // Append to .gitattributes
79
+ const gitattributesPath = path.join(repoDir, '.gitattributes');
80
+ const attributeLine = `${vfsDir}/* merge=sqlitevfs\n`;
81
+
82
+ let content = '';
83
+ if (fs.existsSync(gitattributesPath)) {
84
+ content = fs.readFileSync(gitattributesPath, 'utf-8');
85
+ }
86
+ if (!content.includes(attributeLine.trim())) {
87
+ fs.appendFileSync(gitattributesPath, attributeLine);
88
+ }
89
+
90
+ // Create or update .gitignore in the repo root to ignore SQLite transient files
91
+ const gitignorePath = path.join(repoDir, '.gitignore');
92
+ const ignoreLines = [
93
+ `${vfsDir}/*-journal`,
94
+ `${vfsDir}/*-wal`,
95
+ `${vfsDir}/*-shm`
96
+ ];
97
+
98
+ let gitignoreContent = '';
99
+ if (fs.existsSync(gitignorePath)) {
100
+ gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
101
+ }
102
+
103
+ const linesToAdd = ignoreLines.filter(line => !gitignoreContent.includes(line));
104
+ if (linesToAdd.length > 0) {
105
+ fs.appendFileSync(gitignorePath, (gitignoreContent.endsWith('\n') || gitignoreContent === '' ? '' : '\n') + linesToAdd.join('\n') + '\n');
106
+ }
107
+ }
package/install.js CHANGED
@@ -1,53 +1,17 @@
1
- const os = require('os');
2
- const fs = require('fs');
3
- const path = require('path');
4
- const { execSync } = require('child_process');
5
- const pkg = require('./package.json');
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import process from 'node:process';
4
+ import { downloadOrBuild } from './downloader.js';
6
5
 
7
- const REPO = 'fur-tea-laser/git-sqlite-vfs';
8
- const VERSION = `v${pkg.version}`;
9
- const PLATFORM = os.platform();
10
- const ARCH = os.arch();
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
11
8
 
12
- // Target filename from GitHub Release: e.g., git-sqlite-vfs-v1.0.0-linux-x64.tar.gz
13
- const ASSET_NAME = `git-sqlite-vfs-${VERSION}-${PLATFORM}-${ARCH}.tar.gz`;
14
- const DOWNLOAD_URL = `https://github.com/${REPO}/releases/download/${VERSION}/${ASSET_NAME}`;
15
-
16
- const OUT_DIR = path.join(__dirname, 'c', 'output');
17
-
18
- function buildFromSource() {
19
- console.log('Building from source as fallback...');
20
- try {
21
- execSync('npm run build', { stdio: 'inherit', cwd: __dirname });
22
- console.log('Successfully built from source.');
23
- } catch (e) {
24
- console.error('Failed to build from source.', e.message);
25
- process.exit(1);
26
- }
27
- }
28
-
29
- function downloadAndExtract() {
30
- // If the SKIP_DOWNLOAD env var is set, or if we are building locally from the repo root
31
- // we should just build from source.
32
- if (process.env.SKIP_DOWNLOAD || !fs.existsSync(path.join(__dirname, 'node_modules'))) {
33
- return buildFromSource();
34
- }
35
-
36
- console.log(`Attempting to download prebuilt binary: ${DOWNLOAD_URL}`);
37
-
38
- try {
39
- if (!fs.existsSync(OUT_DIR)) {
40
- fs.mkdirSync(OUT_DIR, { recursive: true });
41
- }
42
-
43
- // Use native curl and tar to download and extract without requiring NPM dependencies.
44
- // This is supported out-of-the-box on modern Linux, macOS, and Windows 10+
45
- execSync(`curl -sLf ${DOWNLOAD_URL} | tar -xz -C "${OUT_DIR}"`, { stdio: 'inherit' });
46
- console.log('Prebuilt binary successfully downloaded and extracted!');
47
- } catch (err) {
48
- console.log('Prebuilt binary not found or download failed. Falling back to source compilation...');
49
- buildFromSource();
50
- }
9
+ async function run() {
10
+ const targetDir = path.join(__dirname, 'c', 'output');
11
+ await downloadOrBuild(targetDir);
51
12
  }
52
13
 
53
- downloadAndExtract();
14
+ run().catch(err => {
15
+ console.error(err);
16
+ process.exit(1);
17
+ });
package/package.json CHANGED
@@ -1,15 +1,34 @@
1
1
  {
2
2
  "name": "git-sqlite-vfs",
3
- "version": "0.0.1",
3
+ "version": "0.0.6",
4
4
  "description": "A Git-Versioned SQLite Database via a Custom Virtual File System (VFS)",
5
5
  "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "type": "module",
8
+ "bin": {
9
+ "git-sqlite-setup": "bin/cli.js"
10
+ },
11
+ "files": [
12
+ "bin/",
13
+ "c/",
14
+ "index.js",
15
+ "index.d.ts",
16
+ "downloader.js",
17
+ "install.js"
18
+ ],
6
19
  "scripts": {
7
20
  "build": "cd c && make",
8
21
  "postinstall": "node install.js",
9
- "pretest": "npm run build && rm -rf .db .git && git init --initial-branch=master",
10
- "test": "node --test test.js"
22
+ "pretest": "npm run build && rm -rf .db test.db .test-db .compaction-db .git && git init --initial-branch=master",
23
+ "test": "node --test test.node.js test.compaction.node.js test.e2e.node.js",
24
+ "test:deno": "npm run build && rm -rf .db test.db .test-db .git && git init --initial-branch=master && deno test -A test.deno.ts"
11
25
  },
12
26
  "dependencies": {
27
+ "@libsql/client": "^0.14.0",
28
+ "drizzle-orm": "^0.33.0",
29
+ "libsql": "^0.4.5"
30
+ },
31
+ "devDependencies": {
13
32
  "better-sqlite3": "^9.4.3"
14
33
  },
15
34
  "author": "",
Binary file
Binary file
Binary file
package/test.js DELETED
@@ -1,209 +0,0 @@
1
- const { describe, it, before } = require('node:test');
2
- const assert = require('node:assert');
3
- const { execSync } = require('child_process');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const GitSQLite = require('./index.js');
7
-
8
- // Helper to run git commands synchronously
9
- const runGit = (cmd) => execSync(cmd, { stdio: 'pipe' }).toString().trim();
10
-
11
- // Helper to recursively calculate total directory size
12
- const getDirSize = (dirPath) => {
13
- let total = 0;
14
- if (!fs.existsSync(dirPath)) return 0;
15
-
16
- const files = fs.readdirSync(dirPath, { withFileTypes: true });
17
- for (const file of files) {
18
- const fullPath = path.join(dirPath, file.name);
19
- if (file.isDirectory()) {
20
- total += getDirSize(fullPath);
21
- } else {
22
- total += fs.statSync(fullPath).size;
23
- }
24
- }
25
- return total;
26
- };
27
-
28
- describe('GitSQLite Architecture Validation', () => {
29
- let db;
30
-
31
- before(() => {
32
- // Initialize Git repo optimizations and register driver configurations
33
- GitSQLite.setupGit();
34
- });
35
-
36
- it('Test 1: Initialization & VFS Sharding', () => {
37
- db = GitSQLite.open('.db');
38
-
39
- // Create initial schema
40
- db.exec(`
41
- CREATE TABLE test_data (id INTEGER PRIMARY KEY, name TEXT, value BLOB);
42
- CREATE TABLE test_settings (config_key TEXT PRIMARY KEY, config_val TEXT);
43
- `);
44
-
45
- // Insert initial baseline data using parameterized transactions
46
- const insertData = db.prepare("INSERT INTO test_data (name, value) VALUES (?, randomblob(100))");
47
- const insertSettings = db.prepare("INSERT INTO test_settings (config_key, config_val) VALUES (?, ?)");
48
-
49
- db.exec('BEGIN TRANSACTION;');
50
- for (let i = 1; i <= 50; i++) {
51
- insertData.run(`Initial ${i}`);
52
- }
53
- insertSettings.run('theme', 'dark');
54
- db.exec('COMMIT;');
55
-
56
- // Close to flush SQLite connections (VFS sync)
57
- db.close();
58
-
59
- // Assert our C VFS dynamically intercepted physical I/O and sharded the B-Tree!
60
- assert.ok(fs.existsSync('.db/pages'), 'Pages directory should exist');
61
- assert.ok(fs.existsSync('.db/pages/size.meta'), 'size.meta persistence state should exist');
62
-
63
- // Verify git tracking structure
64
- assert.ok(fs.existsSync('.db/.gitignore'), '.gitignore should be generated');
65
- assert.ok(fs.existsSync('.db/pages/.gitattributes'), '.gitattributes should be generated');
66
-
67
- // Stage and Commit Snapshot 1
68
- runGit('git add -A -f .db/');
69
- runGit('git commit -m "Snapshot 1: Initial DB state"');
70
- });
71
-
72
- it('Test 2: VACUUM and Physical B-Tree Shrinkage', () => {
73
- db = GitSQLite.open('.db');
74
-
75
- // Insert a massive amount of rows to forcibly expand the B-Tree footprint
76
- db.exec('BEGIN TRANSACTION;');
77
- const insertData = db.prepare("INSERT INTO test_data (name, value) VALUES ('Bulk', randomblob(100))");
78
- for (let i = 0; i < 10000; i++) {
79
- insertData.run();
80
- }
81
- db.exec('COMMIT;');
82
- db.close();
83
-
84
- // Capture total file system footprint of the expanded sharded VFS
85
- const sizeBefore = getDirSize('.db/pages');
86
-
87
- // Reopen, execute a massive deletion, and trigger a vacuum
88
- db = GitSQLite.open('.db');
89
- db.exec("DELETE FROM test_data WHERE id > 50;");
90
- db.exec("VACUUM;");
91
- db.close();
92
-
93
- // Capture total file system footprint post-vacuum
94
- const sizeAfter = getDirSize('.db/pages');
95
-
96
- // Assert mathematical shrinkage: our xTruncate implementation successfully unlinked dead pages!
97
- assert.ok(sizeAfter < sizeBefore, `Directory size must shrink after VACUUM. Before: ${sizeBefore}, After: ${sizeAfter}`);
98
-
99
- // Stage and Commit Snapshot 2 (Unlinked files must be natively staged as Git deletions)
100
- runGit('git add -A -f .db/');
101
- runGit('git commit -m "Snapshot 2: Vacuumed database"');
102
- });
103
-
104
- it('Test 3: Git Branching & True 3-Way Merge (DDL + Row Conflicts)', () => {
105
- // --- CONFLICT BRANCH MUTATIONS ---
106
- runGit('git checkout -b conflict_branch');
107
- db = GitSQLite.open('.db');
108
- db.exec(`
109
- INSERT INTO test_data (id, name, value) VALUES (10001, 'conflict_branch', randomblob(100));
110
- INSERT INTO test_settings (config_key, config_val) VALUES ('plugin', 'enabled');
111
- UPDATE test_data SET name = 'branch_update' WHERE id = 10;
112
- DELETE FROM test_data WHERE id = 15;
113
- UPDATE test_data SET name = 'branch_wins' WHERE id = 50;
114
- CREATE TABLE new_feature (id INTEGER PRIMARY KEY, feature_name TEXT);
115
- INSERT INTO new_feature (id, feature_name) VALUES (1, 'version_control');
116
- CREATE INDEX idx_test_name ON test_data(name);
117
- DROP TABLE test_settings;
118
- `);
119
- db.close();
120
- runGit('git add -A -f .db/');
121
- runGit('git commit -m "conflict_branch: Schema evolution and row updates"');
122
-
123
- // --- MASTER BRANCH MUTATIONS ---
124
- runGit('git checkout master');
125
- db = GitSQLite.open('.db');
126
- db.exec(`
127
- INSERT INTO test_data (id, name, value) VALUES (10002, 'master', randomblob(100));
128
- CREATE TABLE unrelated_table (id INTEGER);
129
- UPDATE test_data SET name = 'master_update' WHERE id = 20;
130
- UPDATE test_data SET name = 'master_wins' WHERE id = 50;
131
- `);
132
- db.close();
133
- runGit('git add -A -f .db/');
134
- runGit('git commit -m "master: Insertions and row updates"');
135
-
136
- // --- THE CUSTOM GIT MERGE STRATEGY ---
137
- const binPath = path.resolve(__dirname, 'c/output');
138
- try {
139
- // By utilizing the -s sqlitevfs strategy and augmenting our PATH, Git natively
140
- // delegates the entire branch resolution to our SQLite C engine!
141
- execSync(`PATH=$PATH:${binPath} git merge -s sqlitevfs conflict_branch -m "Merge conflict_branch into master"`, { stdio: 'pipe' });
142
- } catch (e) {
143
- console.error("Merge failed:\n", e.stdout?.toString(), e.stderr?.toString());
144
- throw e;
145
- }
146
-
147
- // --- ASSERTIONS (Mathematical verification of 3-Way Logical Merge) ---
148
- db = GitSQLite.open('.db');
149
-
150
- // Assert True Row Conflict Resolution (Master Wins)
151
- const row50 = db.prepare("SELECT name FROM test_data WHERE id = 50").get();
152
- assert.strictEqual(row50.name, 'master_wins', 'Master must win true row-level collisions by Custom Merge Strategy logic');
153
-
154
- // Assert standard branch updates
155
- const row10 = db.prepare("SELECT name FROM test_data WHERE id = 10").get();
156
- assert.strictEqual(row10.name, 'branch_update');
157
-
158
- const row20 = db.prepare("SELECT name FROM test_data WHERE id = 20").get();
159
- assert.strictEqual(row20.name, 'master_update');
160
-
161
- // Assert branch deletions
162
- const row15 = db.prepare("SELECT name FROM test_data WHERE id = 15").get();
163
- assert.strictEqual(row15, undefined, 'Row 15 must have been deleted by conflict_branch');
164
-
165
- // Assert 3-Way Schema Evolution (DDL Merge)
166
- const settingsTable = db.prepare("SELECT count(*) as cnt FROM sqlite_schema WHERE name='test_settings'").get();
167
- assert.strictEqual(settingsTable.cnt, 0, 'test_settings table must be mathematically DROPPED');
168
-
169
- const newFeatureRow = db.prepare("SELECT feature_name FROM new_feature WHERE id = 1").get();
170
- assert.strictEqual(newFeatureRow.feature_name, 'version_control', 'new_feature table and its row data must exist');
171
-
172
- const idx = db.prepare("SELECT name FROM sqlite_schema WHERE type='index' AND name='idx_test_name'").get();
173
- assert.ok(idx, 'idx_test_name index must have been created');
174
-
175
- db.close();
176
- });
177
-
178
- it('Test 4: Time Travel (Disaster Recovery via Git)', () => {
179
- db = GitSQLite.open('.db');
180
-
181
- // Assert initial baseline existence
182
- let featureTable = db.prepare("SELECT count(*) as cnt FROM sqlite_schema WHERE name='new_feature'").get();
183
- assert.strictEqual(featureTable.cnt, 1);
184
-
185
- // Execute a catastrophic, destructive operation
186
- db.exec("DROP TABLE new_feature;");
187
- featureTable = db.prepare("SELECT count(*) as cnt FROM sqlite_schema WHERE name='new_feature'").get();
188
- assert.strictEqual(featureTable.cnt, 0, 'Table must be completely dropped from SQLite');
189
- db.close();
190
-
191
- // Commit the disaster
192
- runGit('git add -A -f .db/');
193
- runGit('git commit -m "Oops, accidentally dropped new_feature"');
194
-
195
- // Initiate Time Travel (Git Reset)
196
- // Because the database is perfectly versioned, Git instantly restores the .bin files
197
- runGit('git reset --hard HEAD~1');
198
-
199
- // Reopen DB and Verify absolute recovery
200
- db = GitSQLite.open('.db');
201
- featureTable = db.prepare("SELECT count(*) as cnt FROM sqlite_schema WHERE name='new_feature'").get();
202
- assert.strictEqual(featureTable.cnt, 1, 'Table schema must be fully resurrected natively by Git!');
203
-
204
- const row = db.prepare("SELECT feature_name FROM new_feature WHERE id = 1").get();
205
- assert.strictEqual(row.feature_name, 'version_control', 'Physical row data must be fully intact after time travel!');
206
-
207
- db.close();
208
- });
209
- });