git-sqlite-vfs 0.0.0
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/README.md +56 -0
- package/c/Makefile +30 -0
- package/c/git-merge-sqlitevfs.c +230 -0
- package/c/gitvfs.c +526 -0
- package/c/gitvfs.h +15 -0
- package/c/main.c +111 -0
- package/c/output/git-merge-sqlitevfs +0 -0
- package/c/output/gitvfs.o +0 -0
- package/c/output/gitvfs.so +0 -0
- package/c/output/gitvfs_test +0 -0
- package/c/output/main.o +0 -0
- package/index.js +55 -0
- package/install.js +53 -0
- package/package.json +17 -0
- package/test.js +209 -0
package/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# git-sqlite-vfs
|
|
2
|
+
|
|
3
|
+
A Git-Versioned SQLite Database via a Custom Virtual File System (VFS).
|
|
4
|
+
|
|
5
|
+
This project bridges the mathematical robustness of SQLite's B-Tree engine with the distributed version control capabilities of Git, neutralizing the fundamental friction between binary databases and text-based source control.
|
|
6
|
+
|
|
7
|
+
## The Architecture
|
|
8
|
+
|
|
9
|
+
By default, standard monolithic SQLite databases undergo "cascading byte shifts" during standard operations (e.g., page splits, rebalancing). This destroys Git's ability to efficiently delta-compress the binary, causing massive repository bloat.
|
|
10
|
+
|
|
11
|
+
**The GitVFS Sharding Engine:**
|
|
12
|
+
We solve this by replacing the POSIX I/O layer with a custom SQLite Virtual File System (VFS) written in C. Instead of writing to a single `.db` file, `gitvfs` dynamically shards the database into isolated, deterministic 4KB hexadecimal `.bin` pages (e.g., `.db/pages/0A/1B/0A1B2C.bin`).
|
|
13
|
+
|
|
14
|
+
Because changes are mathematically isolated to specific physical files, Git's `xdelta` sliding window algorithm achieves near-perfect binary compression. Operations like `VACUUM` naturally trigger `xTruncate`, unlinking dead pages and shrinking the physical directory footprint.
|
|
15
|
+
|
|
16
|
+
## The Custom Merge Strategy
|
|
17
|
+
|
|
18
|
+
Standard Git auto-merges (`ort`) operate on a file-by-file basis. Merging isolated binary pages from divergent branches silently corrupts the mathematical integrity of a B-Tree graph.
|
|
19
|
+
|
|
20
|
+
This package provides a **Native Git Merge Strategy** (`git-merge-sqlitevfs`) that elevates the merge context from the file level to the database level. When Git encounters a branch merge, it delegates the entire operation to our C executable:
|
|
21
|
+
1. `git-merge-sqlitevfs` uses `git archive` to safely reconstruct `MERGE_HEAD` and the Ancestor database states without index-lock collisions.
|
|
22
|
+
2. It uses `ATTACH DATABASE` to instantly mount all three branches (Local, Remote, Ancestor) into a single unified SQLite VDBE engine.
|
|
23
|
+
3. Using the `EXCEPT` operator, it calculates full-row tuples and structural DDL schema diffs instantly.
|
|
24
|
+
4. It performs a true mathematically sound 3-Way Logical Merge—resolving schema evolutions, propagating insertions/deletions, and mitigating exact row-level conflicts (preferring `HEAD`)—then stages the physically reconciled `.bin` pages back to Git.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
Install the package via npm (requires `better-sqlite3` and `make`):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install git-sqlite-vfs
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Initialize your version-controlled connection in Node.js:
|
|
35
|
+
|
|
36
|
+
```javascript
|
|
37
|
+
const GitSQLite = require('git-sqlite-vfs');
|
|
38
|
+
|
|
39
|
+
// Configure Git optimizations and register the VFS merge driver
|
|
40
|
+
GitSQLite.setupGit();
|
|
41
|
+
|
|
42
|
+
// Open a connection. Our Node wrapper automatically loads the C extension
|
|
43
|
+
// and routes the URI query via better-sqlite3.
|
|
44
|
+
const db = GitSQLite.open('.db');
|
|
45
|
+
|
|
46
|
+
// Execute standard SQL natively
|
|
47
|
+
db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);");
|
|
48
|
+
db.exec("INSERT INTO users (name) VALUES ('Alice');");
|
|
49
|
+
|
|
50
|
+
const row = db.prepare("SELECT * FROM users WHERE name = ?").get('Alice');
|
|
51
|
+
console.log(row.name); // 'Alice'
|
|
52
|
+
|
|
53
|
+
db.close();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Because the underlying files are flawlessly tracked, you can seamlessly branch, commit, and `git reset --hard HEAD~1` to time travel instantly!
|
package/c/Makefile
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
CC = gcc
|
|
2
|
+
CFLAGS = -Wall -Wextra -g -O2 -std=c99 -D_POSIX_C_SOURCE=200809L
|
|
3
|
+
LDFLAGS = -lsqlite3
|
|
4
|
+
|
|
5
|
+
SRC_DIR = .
|
|
6
|
+
OUT_DIR = output
|
|
7
|
+
|
|
8
|
+
all: $(OUT_DIR)/gitvfs_test $(OUT_DIR)/git-merge-sqlitevfs $(OUT_DIR)/gitvfs.so
|
|
9
|
+
|
|
10
|
+
$(OUT_DIR)/gitvfs.so: $(SRC_DIR)/gitvfs.c $(SRC_DIR)/gitvfs.h | $(OUT_DIR)
|
|
11
|
+
$(CC) $(CFLAGS) -fPIC -shared -DCOMPILE_SQLITE_EXTENSION $(SRC_DIR)/gitvfs.c -o $(OUT_DIR)/gitvfs.so $(LDFLAGS)
|
|
12
|
+
|
|
13
|
+
$(OUT_DIR)/gitvfs.o: $(SRC_DIR)/gitvfs.c $(SRC_DIR)/gitvfs.h | $(OUT_DIR)
|
|
14
|
+
$(CC) $(CFLAGS) -c $(SRC_DIR)/gitvfs.c -o $(OUT_DIR)/gitvfs.o
|
|
15
|
+
|
|
16
|
+
$(OUT_DIR)/main.o: $(SRC_DIR)/main.c $(SRC_DIR)/gitvfs.h | $(OUT_DIR)
|
|
17
|
+
$(CC) $(CFLAGS) -c $(SRC_DIR)/main.c -o $(OUT_DIR)/main.o
|
|
18
|
+
|
|
19
|
+
$(OUT_DIR)/gitvfs_test: $(OUT_DIR)/main.o $(OUT_DIR)/gitvfs.o | $(OUT_DIR)
|
|
20
|
+
$(CC) $(CFLAGS) $(OUT_DIR)/main.o $(OUT_DIR)/gitvfs.o -o $(OUT_DIR)/gitvfs_test $(LDFLAGS)
|
|
21
|
+
|
|
22
|
+
$(OUT_DIR)/git-merge-sqlitevfs: $(SRC_DIR)/git-merge-sqlitevfs.c $(OUT_DIR)/gitvfs.o | $(OUT_DIR)
|
|
23
|
+
$(CC) $(CFLAGS) $(SRC_DIR)/git-merge-sqlitevfs.c $(OUT_DIR)/gitvfs.o -o $(OUT_DIR)/git-merge-sqlitevfs $(LDFLAGS)
|
|
24
|
+
|
|
25
|
+
$(OUT_DIR):
|
|
26
|
+
mkdir -p $(OUT_DIR)
|
|
27
|
+
|
|
28
|
+
clean:
|
|
29
|
+
rm -rf $(OUT_DIR)
|
|
30
|
+
rm -rf .db .git
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#include <stdio.h>
|
|
2
|
+
#include <stdlib.h>
|
|
3
|
+
#include <unistd.h>
|
|
4
|
+
#include <string.h>
|
|
5
|
+
#include <sqlite3.h>
|
|
6
|
+
#include "gitvfs.h"
|
|
7
|
+
|
|
8
|
+
int main(int argc, char *argv[]) {
|
|
9
|
+
// A Git Strategy receives: <base> -- <head> <remote>
|
|
10
|
+
if (argc < 5) {
|
|
11
|
+
fprintf(stderr, "Usage: %s <base> -- <head> <remote>\n", argv[0]);
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const char *base = argv[1];
|
|
16
|
+
const char *other = argv[4];
|
|
17
|
+
|
|
18
|
+
printf("VFS Merge Driver Invoked: Whole-DB Logical Merge Initiated...\n");
|
|
19
|
+
|
|
20
|
+
// 2. Extract MERGE_HEAD and Ancestor without triggering index-lock failures
|
|
21
|
+
system("rm -rf /tmp/gitvfs_other_db && mkdir -p /tmp/gitvfs_other_db");
|
|
22
|
+
|
|
23
|
+
char cmd_other[512];
|
|
24
|
+
snprintf(cmd_other, sizeof(cmd_other), "git archive %s .db/pages/ | tar -x -C /tmp/gitvfs_other_db", other);
|
|
25
|
+
int ret_other = system(cmd_other);
|
|
26
|
+
if (ret_other != 0) {
|
|
27
|
+
fprintf(stderr, "Failed to extract other database. Command: %s\n", cmd_other);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
system("rm -rf /tmp/gitvfs_ancestor_db && mkdir -p /tmp/gitvfs_ancestor_db");
|
|
31
|
+
char cmd_ancestor[512];
|
|
32
|
+
snprintf(cmd_ancestor, sizeof(cmd_ancestor), "git archive %s .db/pages/ | tar -x -C /tmp/gitvfs_ancestor_db", base);
|
|
33
|
+
int ret_ancestor = system(cmd_ancestor);
|
|
34
|
+
if (ret_ancestor != 0) {
|
|
35
|
+
fprintf(stderr, "Failed to extract ancestor database. Command: %s\n", cmd_ancestor);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 3. The Logical Merge via ATTACH
|
|
39
|
+
sqlite3 *db_local;
|
|
40
|
+
|
|
41
|
+
sqlite3_gitvfs_init_impl(NULL);
|
|
42
|
+
sqlite3_open_v2(".db", &db_local, SQLITE_OPEN_READWRITE, "gitvfs");
|
|
43
|
+
|
|
44
|
+
sqlite3_exec(db_local, "ATTACH DATABASE '/tmp/gitvfs_other_db/.db' AS other;", NULL, 0, NULL);
|
|
45
|
+
sqlite3_exec(db_local, "ATTACH DATABASE '/tmp/gitvfs_ancestor_db/.db' AS ancestor;", NULL, 0, NULL);
|
|
46
|
+
|
|
47
|
+
// 3-Way DDL Merge
|
|
48
|
+
// Phase 1 (Propagate Drops)
|
|
49
|
+
sqlite3_exec(db_local, "CREATE TEMP TABLE drops AS SELECT type, name FROM main.sqlite_schema WHERE name IN (SELECT name FROM ancestor.sqlite_schema EXCEPT SELECT name FROM other.sqlite_schema) AND name NOT LIKE 'sqlite_%';", NULL, 0, NULL);
|
|
50
|
+
|
|
51
|
+
sqlite3_stmt *drop_stmt;
|
|
52
|
+
char *drops_to_execute = calloc(1, 1024 * 1024); // 1MB buffer
|
|
53
|
+
if (sqlite3_prepare_v2(db_local, "SELECT type, name FROM drops;", -1, &drop_stmt, NULL) == SQLITE_OK) {
|
|
54
|
+
while (sqlite3_step(drop_stmt) == SQLITE_ROW) {
|
|
55
|
+
const char *type = (const char *)sqlite3_column_text(drop_stmt, 0);
|
|
56
|
+
const char *name = (const char *)sqlite3_column_text(drop_stmt, 1);
|
|
57
|
+
if (type && name) {
|
|
58
|
+
char drop_sql[512];
|
|
59
|
+
snprintf(drop_sql, sizeof(drop_sql), "DROP %s IF EXISTS \"%s\";\n", type, name);
|
|
60
|
+
strcat(drops_to_execute, drop_sql);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
sqlite3_finalize(drop_stmt);
|
|
64
|
+
}
|
|
65
|
+
if (strlen(drops_to_execute) > 0) {
|
|
66
|
+
printf("Propagating Drops:\n%s", drops_to_execute);
|
|
67
|
+
char *err_msg = NULL;
|
|
68
|
+
if (sqlite3_exec(db_local, drops_to_execute, NULL, 0, &err_msg) != SQLITE_OK) {
|
|
69
|
+
fprintf(stderr, "Error executing drop: %s\n", err_msg);
|
|
70
|
+
sqlite3_free(err_msg);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
free(drops_to_execute);
|
|
74
|
+
sqlite3_exec(db_local, "DROP TABLE drops;", NULL, 0, NULL);
|
|
75
|
+
|
|
76
|
+
// Phase 2 (Propagate Additions)
|
|
77
|
+
sqlite3_exec(db_local, "CREATE TEMP TABLE adds AS SELECT sql FROM other.sqlite_schema WHERE sql IS NOT NULL AND name IN (SELECT name FROM other.sqlite_schema EXCEPT SELECT name FROM ancestor.sqlite_schema) AND name NOT IN (SELECT name FROM main.sqlite_schema) ORDER BY CASE WHEN type='table' THEN 1 ELSE 2 END;", NULL, 0, NULL);
|
|
78
|
+
|
|
79
|
+
sqlite3_stmt *add_stmt;
|
|
80
|
+
char *adds_to_execute = calloc(1, 1024 * 1024); // 1MB buffer
|
|
81
|
+
if (sqlite3_prepare_v2(db_local, "SELECT sql FROM adds;", -1, &add_stmt, NULL) == SQLITE_OK) {
|
|
82
|
+
while (sqlite3_step(add_stmt) == SQLITE_ROW) {
|
|
83
|
+
const char *sql = (const char *)sqlite3_column_text(add_stmt, 0);
|
|
84
|
+
if (sql) {
|
|
85
|
+
strcat(adds_to_execute, sql);
|
|
86
|
+
strcat(adds_to_execute, ";\n");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
sqlite3_finalize(add_stmt);
|
|
90
|
+
}
|
|
91
|
+
if (strlen(adds_to_execute) > 0) {
|
|
92
|
+
printf("Propagating Additions:\n%s", adds_to_execute);
|
|
93
|
+
char *err_msg = NULL;
|
|
94
|
+
if (sqlite3_exec(db_local, adds_to_execute, NULL, 0, &err_msg) != SQLITE_OK) {
|
|
95
|
+
fprintf(stderr, "Error executing addition: %s\n", err_msg);
|
|
96
|
+
sqlite3_free(err_msg);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
free(adds_to_execute);
|
|
100
|
+
sqlite3_exec(db_local, "DROP TABLE adds;", NULL, 0, NULL);
|
|
101
|
+
|
|
102
|
+
// Dynamically discover and merge tables
|
|
103
|
+
sqlite3_stmt *stmt;
|
|
104
|
+
const char *query_schema = "SELECT name FROM main.sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%';";
|
|
105
|
+
|
|
106
|
+
if (sqlite3_prepare_v2(db_local, query_schema, -1, &stmt, NULL) == SQLITE_OK) {
|
|
107
|
+
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
|
108
|
+
const char *table_name = (const char *)sqlite3_column_text(stmt, 0);
|
|
109
|
+
if (table_name) {
|
|
110
|
+
printf("Merging table: %s\n", table_name);
|
|
111
|
+
|
|
112
|
+
char pragma_query[512];
|
|
113
|
+
snprintf(pragma_query, sizeof(pragma_query), "PRAGMA main.table_info(\"%s\");", table_name);
|
|
114
|
+
sqlite3_stmt *pragma_stmt;
|
|
115
|
+
char pk_col[256] = {0};
|
|
116
|
+
int has_pk = 0;
|
|
117
|
+
|
|
118
|
+
if (sqlite3_prepare_v2(db_local, pragma_query, -1, &pragma_stmt, NULL) == SQLITE_OK) {
|
|
119
|
+
while (sqlite3_step(pragma_stmt) == SQLITE_ROW) {
|
|
120
|
+
int pk = sqlite3_column_int(pragma_stmt, 5);
|
|
121
|
+
if (pk > 0) {
|
|
122
|
+
const char *col_name = (const char *)sqlite3_column_text(pragma_stmt, 1);
|
|
123
|
+
if (col_name) {
|
|
124
|
+
strncpy(pk_col, col_name, sizeof(pk_col) - 1);
|
|
125
|
+
has_pk = 1;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
sqlite3_finalize(pragma_stmt);
|
|
131
|
+
} else {
|
|
132
|
+
fprintf(stderr, "Failed to prepare PRAGMA query for table %s: %s\n", table_name, sqlite3_errmsg(db_local));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (has_pk) {
|
|
136
|
+
int exists_in_ancestor = 0;
|
|
137
|
+
char check_anc[256];
|
|
138
|
+
snprintf(check_anc, sizeof(check_anc), "SELECT 1 FROM ancestor.sqlite_schema WHERE type='table' AND name='%s';", table_name);
|
|
139
|
+
sqlite3_stmt *anc_stmt;
|
|
140
|
+
if (sqlite3_prepare_v2(db_local, check_anc, -1, &anc_stmt, NULL) == SQLITE_OK) {
|
|
141
|
+
if (sqlite3_step(anc_stmt) == SQLITE_ROW) exists_in_ancestor = 1;
|
|
142
|
+
sqlite3_finalize(anc_stmt);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (exists_in_ancestor) {
|
|
146
|
+
char drop_conflict[256];
|
|
147
|
+
snprintf(drop_conflict, sizeof(drop_conflict), "DROP TABLE IF EXISTS temp.\"conflicted_pks_%s\";", table_name);
|
|
148
|
+
sqlite3_exec(db_local, drop_conflict, NULL, 0, NULL);
|
|
149
|
+
|
|
150
|
+
char conflict_query[1024];
|
|
151
|
+
snprintf(conflict_query, sizeof(conflict_query),
|
|
152
|
+
"CREATE TEMP TABLE \"conflicted_pks_%s\" AS "
|
|
153
|
+
"SELECT \"%s\" FROM (SELECT * FROM main.\"%s\" EXCEPT SELECT * FROM ancestor.\"%s\") "
|
|
154
|
+
"INTERSECT "
|
|
155
|
+
"SELECT \"%s\" FROM (SELECT * FROM other.\"%s\" EXCEPT SELECT * FROM ancestor.\"%s\");",
|
|
156
|
+
table_name,
|
|
157
|
+
pk_col, table_name, table_name,
|
|
158
|
+
pk_col, table_name, table_name);
|
|
159
|
+
|
|
160
|
+
char *err_msg = NULL;
|
|
161
|
+
if (sqlite3_exec(db_local, conflict_query, NULL, 0, &err_msg) != SQLITE_OK) {
|
|
162
|
+
fprintf(stderr, "Error executing conflict query: %s\n", err_msg);
|
|
163
|
+
sqlite3_free(err_msg);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Identify Row-Level Clashes
|
|
167
|
+
sqlite3_stmt *conflict_stmt;
|
|
168
|
+
char select_conflict[256];
|
|
169
|
+
snprintf(select_conflict, sizeof(select_conflict), "SELECT * FROM \"conflicted_pks_%s\";", table_name);
|
|
170
|
+
if (sqlite3_prepare_v2(db_local, select_conflict, -1, &conflict_stmt, NULL) == SQLITE_OK) {
|
|
171
|
+
while (sqlite3_step(conflict_stmt) == SQLITE_ROW) {
|
|
172
|
+
const char *conflicted_pk_val = (const char *)sqlite3_column_text(conflict_stmt, 0);
|
|
173
|
+
printf("WARNING: True Row Conflict detected on table '%s', PK '%s'. Preserving HEAD state.\n", table_name, conflicted_pk_val ? conflicted_pk_val : "NULL");
|
|
174
|
+
}
|
|
175
|
+
sqlite3_finalize(conflict_stmt);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
char q1[1024];
|
|
179
|
+
char q2[1024];
|
|
180
|
+
|
|
181
|
+
// Query 1: Propagate Deletions from MERGE_HEAD
|
|
182
|
+
snprintf(q1, sizeof(q1),
|
|
183
|
+
"DELETE FROM main.\"%s\" WHERE \"%s\" IN (SELECT \"%s\" FROM ancestor.\"%s\" EXCEPT SELECT \"%s\" FROM other.\"%s\");",
|
|
184
|
+
table_name, pk_col, pk_col, table_name, pk_col, table_name);
|
|
185
|
+
if (sqlite3_exec(db_local, q1, NULL, 0, &err_msg) != SQLITE_OK) {
|
|
186
|
+
fprintf(stderr, "Error executing deletion merge query: %s\n", err_msg);
|
|
187
|
+
sqlite3_free(err_msg);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Query 2: Propagate Inserts & Updates from MERGE_HEAD, omitting row conflicts
|
|
191
|
+
snprintf(q2, sizeof(q2),
|
|
192
|
+
"REPLACE INTO main.\"%s\" SELECT * FROM other.\"%s\" WHERE \"%s\" IN (SELECT \"%s\" FROM (SELECT * FROM other.\"%s\" EXCEPT SELECT * FROM ancestor.\"%s\")) "
|
|
193
|
+
"AND \"%s\" NOT IN (SELECT \"%s\" FROM \"conflicted_pks_%s\");",
|
|
194
|
+
table_name, table_name, pk_col, pk_col, table_name, table_name, pk_col, pk_col, table_name);
|
|
195
|
+
if (sqlite3_exec(db_local, q2, NULL, 0, &err_msg) != SQLITE_OK) {
|
|
196
|
+
fprintf(stderr, "Error executing insert/update merge query: %s\n", err_msg);
|
|
197
|
+
sqlite3_free(err_msg);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
sqlite3_exec(db_local, drop_conflict, NULL, 0, NULL);
|
|
201
|
+
} else {
|
|
202
|
+
// Fallback to naive 2-way append (table is new)
|
|
203
|
+
char merge_query[512];
|
|
204
|
+
snprintf(merge_query, sizeof(merge_query),
|
|
205
|
+
"INSERT OR IGNORE INTO main.\"%s\" SELECT * FROM other.\"%s\";",
|
|
206
|
+
table_name, table_name);
|
|
207
|
+
sqlite3_exec(db_local, merge_query, NULL, 0, NULL);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
char merge_query[512];
|
|
211
|
+
snprintf(merge_query, sizeof(merge_query),
|
|
212
|
+
"INSERT OR IGNORE INTO main.\"%s\" SELECT * FROM other.\"%s\";",
|
|
213
|
+
table_name, table_name);
|
|
214
|
+
sqlite3_exec(db_local, merge_query, NULL, 0, NULL);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
sqlite3_finalize(stmt);
|
|
219
|
+
} else {
|
|
220
|
+
fprintf(stderr, "Failed to prepare schema query: %s\n", sqlite3_errmsg(db_local));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
sqlite3_close(db_local);
|
|
224
|
+
|
|
225
|
+
if (system("git add -A -f .db/pages/") != 0) {}
|
|
226
|
+
if (system("rm -rf /tmp/gitvfs_other_db /tmp/gitvfs_ancestor_db") != 0) {}
|
|
227
|
+
|
|
228
|
+
printf("Logical Merge Complete! VFS physical pages reconciled.\n");
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
package/c/gitvfs.c
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
#include "gitvfs.h"
|
|
2
|
+
#ifdef COMPILE_SQLITE_EXTENSION
|
|
3
|
+
#include <sqlite3ext.h>
|
|
4
|
+
SQLITE_EXTENSION_INIT1
|
|
5
|
+
#else
|
|
6
|
+
#include <sqlite3.h>
|
|
7
|
+
#endif
|
|
8
|
+
#include <stdio.h>
|
|
9
|
+
#include <stdlib.h>
|
|
10
|
+
#include <string.h>
|
|
11
|
+
#include <sys/stat.h>
|
|
12
|
+
#include <fcntl.h>
|
|
13
|
+
#include <unistd.h>
|
|
14
|
+
#include <errno.h>
|
|
15
|
+
|
|
16
|
+
#define GITVFS_PAGE_SIZE 4096
|
|
17
|
+
#define GITVFS_MAX_PATH 512
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Custom sqlite3_file subclass to hold our internal state.
|
|
21
|
+
* This structure tracks the file descriptor equivalents and the state
|
|
22
|
+
* of the highest page written for bounds checking.
|
|
23
|
+
*/
|
|
24
|
+
typedef struct gitvfs_file {
|
|
25
|
+
sqlite3_file base; /* Base class. Must be first. */
|
|
26
|
+
char base_dir[GITVFS_MAX_PATH]; /* The base directory for this database (e.g., ".db") */
|
|
27
|
+
sqlite3_int64 max_page_number; /* Highest page number written, used for xFileSize */
|
|
28
|
+
int is_main_db; /* 1 if main DB (sharded), 0 if temp/journal (flat) */
|
|
29
|
+
int flat_fd; /* POSIX file descriptor for temp/journal files */
|
|
30
|
+
} gitvfs_file;
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Utility: Recursively create directories (mkdir -p behavior)
|
|
34
|
+
* Helps ensure our nested sharded directory structure exists before writing.
|
|
35
|
+
*/
|
|
36
|
+
static int mkdir_p(const char *path, mode_t mode) {
|
|
37
|
+
char tmp[GITVFS_MAX_PATH];
|
|
38
|
+
char *p = NULL;
|
|
39
|
+
size_t len;
|
|
40
|
+
|
|
41
|
+
snprintf(tmp, sizeof(tmp), "%s", path);
|
|
42
|
+
len = strlen(tmp);
|
|
43
|
+
if (len > 0 && tmp[len - 1] == '/') {
|
|
44
|
+
tmp[len - 1] = 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (p = tmp + 1; *p; p++) {
|
|
48
|
+
if (*p == '/') {
|
|
49
|
+
*p = 0;
|
|
50
|
+
if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
|
|
51
|
+
return -1;
|
|
52
|
+
}
|
|
53
|
+
*p = '/';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
|
|
57
|
+
return -1;
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*
|
|
63
|
+
* Utility: Calculate the file path for a specific page number.
|
|
64
|
+
*
|
|
65
|
+
* Sharding logic:
|
|
66
|
+
* We map a page number to a hex string to create a deterministic path.
|
|
67
|
+
* 1. Convert page number to a zero-padded, uppercase 6-character hex string.
|
|
68
|
+
* 2. Extract the first two characters for the 1st level directory.
|
|
69
|
+
* 3. Extract the next two characters for the 2nd level directory.
|
|
70
|
+
*
|
|
71
|
+
* Example: Page 1048575 -> Hex "0FFFFF"
|
|
72
|
+
* Path: <base_dir>/pages/0F/FF/0FFFFF.bin
|
|
73
|
+
*/
|
|
74
|
+
static void get_page_filepath(const char *base_dir, sqlite3_int64 page_number, char *path_buffer, size_t buffer_size) {
|
|
75
|
+
char hex_str[32];
|
|
76
|
+
|
|
77
|
+
// Format page number as a zero-padded, 6-character hexadecimal string
|
|
78
|
+
snprintf(hex_str, sizeof(hex_str), "%06llX", (unsigned long long)page_number);
|
|
79
|
+
|
|
80
|
+
// Extract sharding prefixes
|
|
81
|
+
char dir1[3] = { hex_str[0], hex_str[1], '\0' };
|
|
82
|
+
char dir2[3] = { hex_str[2], hex_str[3], '\0' };
|
|
83
|
+
|
|
84
|
+
// Construct the full path
|
|
85
|
+
snprintf(path_buffer, buffer_size, "%s/pages/%s/%s/%s.bin", base_dir, dir1, dir2, hex_str);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/*
|
|
89
|
+
* Utility: Generate the .gitattributes file in the pages directory.
|
|
90
|
+
* Explicitly declares all .bin files as binary to optimize Git delta-compression
|
|
91
|
+
* and suppress meaningless text-based diff operations.
|
|
92
|
+
*/
|
|
93
|
+
static void generate_gitattributes(const char *base_dir) {
|
|
94
|
+
char pages_dir[GITVFS_MAX_PATH];
|
|
95
|
+
snprintf(pages_dir, sizeof(pages_dir), "%s/pages", base_dir);
|
|
96
|
+
|
|
97
|
+
// Ensure the root pages directory exists
|
|
98
|
+
if (mkdir_p(pages_dir, 0755) != 0) return;
|
|
99
|
+
|
|
100
|
+
char attr_path[GITVFS_MAX_PATH];
|
|
101
|
+
snprintf(attr_path, sizeof(attr_path), "%s/.gitattributes", pages_dir);
|
|
102
|
+
|
|
103
|
+
// Check if the file already exists to avoid unnecessary disk writes
|
|
104
|
+
if (access(attr_path, F_OK) != -1) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Create the .gitattributes file
|
|
109
|
+
FILE *f = fopen(attr_path, "w");
|
|
110
|
+
if (f) {
|
|
111
|
+
fprintf(f, "*.bin binary\nsize.meta binary\n");
|
|
112
|
+
fclose(f);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static void generate_gitignore(const char *base_dir) {
|
|
117
|
+
char attr_path[GITVFS_MAX_PATH];
|
|
118
|
+
snprintf(attr_path, sizeof(attr_path), "%s/.gitignore", base_dir);
|
|
119
|
+
|
|
120
|
+
if (access(attr_path, F_OK) != -1) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
FILE *f = fopen(attr_path, "w");
|
|
125
|
+
if (f) {
|
|
126
|
+
fprintf(f, "*-journal\n*-wal\n*-shm\n");
|
|
127
|
+
fclose(f);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* =====================================================================
|
|
132
|
+
* VFS I/O Methods (sqlite3_io_methods)
|
|
133
|
+
* Part 2 - Core I/O Logic
|
|
134
|
+
* ===================================================================== */
|
|
135
|
+
|
|
136
|
+
static int gitvfs_Close(sqlite3_file *pFile) {
|
|
137
|
+
gitvfs_file *p = (gitvfs_file*)pFile;
|
|
138
|
+
|
|
139
|
+
// If it's a temporary or journal file, close the POSIX file descriptor
|
|
140
|
+
if (!p->is_main_db && p->flat_fd >= 0) {
|
|
141
|
+
close(p->flat_fd);
|
|
142
|
+
p->flat_fd = -1;
|
|
143
|
+
}
|
|
144
|
+
return SQLITE_OK;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
static int gitvfs_Read(sqlite3_file *pFile, void *zBuf, int iAmt, sqlite3_int64 iOfst) {
|
|
148
|
+
gitvfs_file *p = (gitvfs_file*)pFile;
|
|
149
|
+
|
|
150
|
+
// Route to monolithic file read for temp/journal files
|
|
151
|
+
if (!p->is_main_db) {
|
|
152
|
+
ssize_t n = pread(p->flat_fd, zBuf, iAmt, iOfst);
|
|
153
|
+
if (n == iAmt) {
|
|
154
|
+
return SQLITE_OK;
|
|
155
|
+
} else if (n >= 0) {
|
|
156
|
+
memset((char*)zBuf + n, 0, iAmt - n);
|
|
157
|
+
return SQLITE_IOERR_SHORT_READ;
|
|
158
|
+
}
|
|
159
|
+
return SQLITE_IOERR_READ;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Sharded DB read logic
|
|
163
|
+
sqlite3_int64 page_number = iOfst / GITVFS_PAGE_SIZE;
|
|
164
|
+
sqlite3_int64 local_offset = iOfst % GITVFS_PAGE_SIZE;
|
|
165
|
+
|
|
166
|
+
char path[GITVFS_MAX_PATH];
|
|
167
|
+
get_page_filepath(p->base_dir, page_number, path, sizeof(path));
|
|
168
|
+
|
|
169
|
+
int fd = open(path, O_RDONLY);
|
|
170
|
+
if (fd < 0) {
|
|
171
|
+
// Unwritten page requested: Zero-fill the buffer entirely
|
|
172
|
+
memset(zBuf, 0, iAmt);
|
|
173
|
+
return SQLITE_IOERR_SHORT_READ;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Read exact intra-page payload using pread
|
|
177
|
+
ssize_t bytes_read = pread(fd, zBuf, iAmt, local_offset);
|
|
178
|
+
close(fd);
|
|
179
|
+
|
|
180
|
+
if (bytes_read == iAmt) {
|
|
181
|
+
return SQLITE_OK;
|
|
182
|
+
} else if (bytes_read >= 0) {
|
|
183
|
+
// Short read: file exists but has fewer bytes than requested. Zero-fill the remainder.
|
|
184
|
+
memset((char*)zBuf + bytes_read, 0, iAmt - bytes_read);
|
|
185
|
+
return SQLITE_IOERR_SHORT_READ;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return SQLITE_IOERR_READ;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static int gitvfs_Write(sqlite3_file *pFile, const void *zBuf, int iAmt, sqlite3_int64 iOfst) {
|
|
192
|
+
gitvfs_file *p = (gitvfs_file*)pFile;
|
|
193
|
+
|
|
194
|
+
// Route to monolithic file write for temp/journal files
|
|
195
|
+
if (!p->is_main_db) {
|
|
196
|
+
ssize_t n = pwrite(p->flat_fd, zBuf, iAmt, iOfst);
|
|
197
|
+
return (n == iAmt) ? SQLITE_OK : SQLITE_IOERR_WRITE;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Sharded DB write logic
|
|
201
|
+
sqlite3_int64 page_number = iOfst / GITVFS_PAGE_SIZE;
|
|
202
|
+
sqlite3_int64 local_offset = iOfst % GITVFS_PAGE_SIZE;
|
|
203
|
+
|
|
204
|
+
char path[GITVFS_MAX_PATH];
|
|
205
|
+
get_page_filepath(p->base_dir, page_number, path, sizeof(path));
|
|
206
|
+
|
|
207
|
+
// Ensure parent directories exist
|
|
208
|
+
char dir_path[GITVFS_MAX_PATH];
|
|
209
|
+
snprintf(dir_path, sizeof(dir_path), "%s", path);
|
|
210
|
+
char *last_slash = strrchr(dir_path, '/');
|
|
211
|
+
if (last_slash) {
|
|
212
|
+
*last_slash = '\0';
|
|
213
|
+
if (mkdir_p(dir_path, 0755) != 0) {
|
|
214
|
+
return SQLITE_IOERR_WRITE;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// CRITICAL: Open with O_RDWR | O_CREAT to modify existing page data without truncating
|
|
219
|
+
int fd = open(path, O_RDWR | O_CREAT, 0644);
|
|
220
|
+
if (fd < 0) {
|
|
221
|
+
return SQLITE_IOERR_WRITE;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Write exact intra-page payload using pwrite
|
|
225
|
+
ssize_t bytes_written = pwrite(fd, zBuf, iAmt, local_offset);
|
|
226
|
+
close(fd);
|
|
227
|
+
|
|
228
|
+
if (bytes_written != iAmt) {
|
|
229
|
+
return SQLITE_IOERR_WRITE;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// State Persistence: Update max_page_number and size.meta if this is a new high page
|
|
233
|
+
if (page_number > p->max_page_number) {
|
|
234
|
+
p->max_page_number = page_number;
|
|
235
|
+
|
|
236
|
+
char meta_path[GITVFS_MAX_PATH];
|
|
237
|
+
snprintf(meta_path, sizeof(meta_path), "%s/pages/size.meta", p->base_dir);
|
|
238
|
+
FILE *f = fopen(meta_path, "w");
|
|
239
|
+
if (f) {
|
|
240
|
+
fprintf(f, "%lld\n", (long long)p->max_page_number);
|
|
241
|
+
fclose(f);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return SQLITE_OK;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static int gitvfs_Truncate(sqlite3_file *pFile, sqlite3_int64 size) {
|
|
249
|
+
gitvfs_file *p = (gitvfs_file*)pFile;
|
|
250
|
+
|
|
251
|
+
// Route to ftruncate for temp/journal files
|
|
252
|
+
if (!p->is_main_db) {
|
|
253
|
+
return (ftruncate(p->flat_fd, size) == 0) ? SQLITE_OK : SQLITE_IOERR_TRUNCATE;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Sharded DB truncate logic
|
|
257
|
+
sqlite3_int64 new_max_page = (size == 0) ? -1 : (size - 1) / GITVFS_PAGE_SIZE;
|
|
258
|
+
|
|
259
|
+
// Clean up abandoned page files
|
|
260
|
+
for (sqlite3_int64 i = new_max_page + 1; i <= p->max_page_number; i++) {
|
|
261
|
+
char path[GITVFS_MAX_PATH];
|
|
262
|
+
get_page_filepath(p->base_dir, i, path, sizeof(path));
|
|
263
|
+
unlink(path);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Update max_page_number and size.meta
|
|
267
|
+
if (new_max_page != p->max_page_number) {
|
|
268
|
+
p->max_page_number = new_max_page;
|
|
269
|
+
|
|
270
|
+
char meta_path[GITVFS_MAX_PATH];
|
|
271
|
+
snprintf(meta_path, sizeof(meta_path), "%s/pages/size.meta", p->base_dir);
|
|
272
|
+
|
|
273
|
+
if (new_max_page == -1) {
|
|
274
|
+
unlink(meta_path); // DB is completely empty
|
|
275
|
+
} else {
|
|
276
|
+
FILE *f = fopen(meta_path, "w");
|
|
277
|
+
if (f) {
|
|
278
|
+
fprintf(f, "%lld\n", (long long)p->max_page_number);
|
|
279
|
+
fclose(f);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return SQLITE_OK;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
static int gitvfs_Sync(sqlite3_file *pFile, int flags) {
|
|
288
|
+
// Single-writer MVP relying on standard POSIX disk flushes.
|
|
289
|
+
// For temp files we could call fsync(p->flat_fd).
|
|
290
|
+
// Returning SQLITE_OK satisfies SQLite's expectation.
|
|
291
|
+
return SQLITE_OK;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
static int gitvfs_FileSize(sqlite3_file *pFile, sqlite3_int64 *pSize) {
|
|
295
|
+
gitvfs_file *p = (gitvfs_file*)pFile;
|
|
296
|
+
|
|
297
|
+
if (!p->is_main_db) {
|
|
298
|
+
struct stat st;
|
|
299
|
+
if (fstat(p->flat_fd, &st) == 0) {
|
|
300
|
+
*pSize = st.st_size;
|
|
301
|
+
return SQLITE_OK;
|
|
302
|
+
}
|
|
303
|
+
return SQLITE_IOERR_FSTAT;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// O(1) state lookup for the sharded DB size
|
|
307
|
+
*pSize = (p->max_page_number + 1) * GITVFS_PAGE_SIZE;
|
|
308
|
+
return SQLITE_OK;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
static int gitvfs_Lock(sqlite3_file *pFile, int eLock) {
|
|
312
|
+
return SQLITE_OK; // SQLite requires lock functions to succeed
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
static int gitvfs_Unlock(sqlite3_file *pFile, int eLock) {
|
|
316
|
+
(void)pFile; (void)eLock;
|
|
317
|
+
return SQLITE_OK;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
static int gitvfs_CheckReservedLock(sqlite3_file *pFile, int *pResOut) {
|
|
321
|
+
*pResOut = 0;
|
|
322
|
+
return SQLITE_OK;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
static int gitvfs_FileControl(sqlite3_file *pFile, int op, void *pArg) {
|
|
326
|
+
return SQLITE_NOTFOUND;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
static int gitvfs_SectorSize(sqlite3_file *pFile) {
|
|
330
|
+
return GITVFS_PAGE_SIZE;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
static int gitvfs_DeviceCharacteristics(sqlite3_file *pFile) {
|
|
334
|
+
return 0; // Standard characteristics
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
static const sqlite3_io_methods gitvfs_io_methods = {
|
|
338
|
+
1, /* iVersion */
|
|
339
|
+
gitvfs_Close, /* xClose */
|
|
340
|
+
gitvfs_Read, /* xRead */
|
|
341
|
+
gitvfs_Write, /* xWrite */
|
|
342
|
+
gitvfs_Truncate, /* xTruncate */
|
|
343
|
+
gitvfs_Sync, /* xSync */
|
|
344
|
+
gitvfs_FileSize, /* xFileSize */
|
|
345
|
+
gitvfs_Lock, /* xLock */
|
|
346
|
+
gitvfs_Unlock, /* xUnlock */
|
|
347
|
+
gitvfs_CheckReservedLock, /* xCheckReservedLock */
|
|
348
|
+
gitvfs_FileControl, /* xFileControl */
|
|
349
|
+
gitvfs_SectorSize, /* xSectorSize */
|
|
350
|
+
gitvfs_DeviceCharacteristics, /* xDeviceCharacteristics */
|
|
351
|
+
NULL, /* xShmMap */
|
|
352
|
+
NULL, /* xShmLock */
|
|
353
|
+
NULL, /* xShmBarrier */
|
|
354
|
+
NULL, /* xShmUnmap */
|
|
355
|
+
NULL, /* xFetch */
|
|
356
|
+
NULL /* xUnfetch */
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/* =====================================================================
|
|
360
|
+
* VFS Registration Methods (sqlite3_vfs)
|
|
361
|
+
* ===================================================================== */
|
|
362
|
+
|
|
363
|
+
static sqlite3_vfs *orig_vfs = NULL;
|
|
364
|
+
|
|
365
|
+
static int gitvfs_Open(sqlite3_vfs *pVfs, const char *zName, sqlite3_file *pFile, int flags, int *pOutFlags) {
|
|
366
|
+
if (!orig_vfs) orig_vfs = sqlite3_vfs_find(NULL);
|
|
367
|
+
if (!zName || strstr(zName, ".db") == NULL) {
|
|
368
|
+
return orig_vfs->xOpen(orig_vfs, zName, pFile, flags, pOutFlags);
|
|
369
|
+
}
|
|
370
|
+
gitvfs_file *p = (gitvfs_file*)pFile;
|
|
371
|
+
p->base.pMethods = &gitvfs_io_methods;
|
|
372
|
+
p->max_page_number = -1;
|
|
373
|
+
p->flat_fd = -1;
|
|
374
|
+
|
|
375
|
+
// Identify if opening the main database or a temporary/journal file
|
|
376
|
+
if (flags & SQLITE_OPEN_MAIN_DB) {
|
|
377
|
+
p->is_main_db = 1;
|
|
378
|
+
|
|
379
|
+
const char *base = (zName != NULL) ? zName : ".db";
|
|
380
|
+
|
|
381
|
+
// Strip URI parameters if they exist
|
|
382
|
+
char clean_base[GITVFS_MAX_PATH];
|
|
383
|
+
snprintf(clean_base, sizeof(clean_base), "%s", base);
|
|
384
|
+
char *qmark = strchr(clean_base, '?');
|
|
385
|
+
if (qmark) {
|
|
386
|
+
*qmark = '\0';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Also strip "file:" prefix if Python passes it raw
|
|
390
|
+
const char *actual_base = clean_base;
|
|
391
|
+
if (strncmp(actual_base, "file:", 5) == 0) {
|
|
392
|
+
actual_base += 5;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
snprintf(p->base_dir, sizeof(p->base_dir), "%s", actual_base);
|
|
396
|
+
|
|
397
|
+
// Initialize standard repository constraints
|
|
398
|
+
generate_gitattributes(p->base_dir);
|
|
399
|
+
generate_gitignore(p->base_dir);
|
|
400
|
+
|
|
401
|
+
// State Persistence: Load max_page_number from size.meta
|
|
402
|
+
char meta_path[GITVFS_MAX_PATH];
|
|
403
|
+
snprintf(meta_path, sizeof(meta_path), "%s/pages/size.meta", p->base_dir);
|
|
404
|
+
FILE *f = fopen(meta_path, "r");
|
|
405
|
+
if (f) {
|
|
406
|
+
long long max_page;
|
|
407
|
+
if (fscanf(f, "%lld", &max_page) == 1) {
|
|
408
|
+
p->max_page_number = (sqlite3_int64)max_page;
|
|
409
|
+
}
|
|
410
|
+
fclose(f);
|
|
411
|
+
}
|
|
412
|
+
} else {
|
|
413
|
+
// Handle temp/journal file natively as a monolithic file
|
|
414
|
+
p->is_main_db = 0;
|
|
415
|
+
|
|
416
|
+
int openFlags = 0;
|
|
417
|
+
if (flags & SQLITE_OPEN_READONLY) openFlags |= O_RDONLY;
|
|
418
|
+
if (flags & SQLITE_OPEN_READWRITE) openFlags |= O_RDWR;
|
|
419
|
+
if (flags & SQLITE_OPEN_CREATE) openFlags |= O_CREAT;
|
|
420
|
+
|
|
421
|
+
// Anonymous temp file handling
|
|
422
|
+
if (zName == NULL) {
|
|
423
|
+
char temp_name[GITVFS_MAX_PATH];
|
|
424
|
+
snprintf(temp_name, sizeof(temp_name), "/tmp/gitvfs_temp_%d_%p", getpid(), p);
|
|
425
|
+
p->flat_fd = open(temp_name, O_RDWR | O_CREAT | O_EXCL, 0644);
|
|
426
|
+
if (p->flat_fd >= 0) unlink(temp_name); // Clean up immediately on close
|
|
427
|
+
} else {
|
|
428
|
+
p->flat_fd = open(zName, openFlags, 0644);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (p->flat_fd < 0) {
|
|
432
|
+
return SQLITE_CANTOPEN;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (pOutFlags) {
|
|
437
|
+
*pOutFlags = flags;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return SQLITE_OK;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
static int gitvfs_Delete(sqlite3_vfs *pVfs, const char *zName, int syncDir) {
|
|
444
|
+
// Standard file deletion, primarily used for clearing out old journals
|
|
445
|
+
unlink(zName);
|
|
446
|
+
return SQLITE_OK;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
static int gitvfs_Access(sqlite3_vfs *pVfs, const char *zName, int flags, int *pResOut) {
|
|
450
|
+
// Check if the directory or file is accessible
|
|
451
|
+
*pResOut = (access(zName, F_OK) == 0) ? 1 : 0;
|
|
452
|
+
return SQLITE_OK;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
static int gitvfs_FullPathname(sqlite3_vfs *pVfs, const char *zName, int nOut, char *zOut) {
|
|
456
|
+
snprintf(zOut, nOut, "%s", zName);
|
|
457
|
+
return SQLITE_OK;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/* System calls to load extensions (stubbed) */
|
|
461
|
+
static void *gitvfs_DlOpen(sqlite3_vfs *pVfs, const char *zFilename) { return orig_vfs->xDlOpen(orig_vfs, zFilename); }
|
|
462
|
+
static void gitvfs_DlError(sqlite3_vfs *pVfs, int nByte, char *zErrMsg) { orig_vfs->xDlError(orig_vfs, nByte, zErrMsg); }
|
|
463
|
+
static void (*gitvfs_DlSym(sqlite3_vfs *pVfs, void *p, const char*zSymbol))(void) { return orig_vfs->xDlSym(orig_vfs, p, zSymbol); }
|
|
464
|
+
static void gitvfs_DlClose(sqlite3_vfs *pVfs, void *pHandle) { orig_vfs->xDlClose(orig_vfs, pHandle); }
|
|
465
|
+
static int gitvfs_Randomness(sqlite3_vfs *pVfs, int nByte, char *zOut) { return orig_vfs->xRandomness(orig_vfs, nByte, zOut); }
|
|
466
|
+
static int gitvfs_Sleep(sqlite3_vfs *pVfs, int microseconds) { return orig_vfs->xSleep(orig_vfs, microseconds); }
|
|
467
|
+
static int gitvfs_CurrentTime(sqlite3_vfs *pVfs, double *prNow) { return orig_vfs->xCurrentTime(orig_vfs, prNow); }
|
|
468
|
+
|
|
469
|
+
static int gitvfs_GetLastError(sqlite3_vfs *pVfs, int a, char *b) { return orig_vfs->xGetLastError ? orig_vfs->xGetLastError(orig_vfs, a, b) : 0; }
|
|
470
|
+
static int gitvfs_CurrentTimeInt64(sqlite3_vfs *pVfs, sqlite3_int64 *p) { return orig_vfs->xCurrentTimeInt64 ? orig_vfs->xCurrentTimeInt64(orig_vfs, p) : 0; }
|
|
471
|
+
static int gitvfs_SetSystemCall(sqlite3_vfs *pVfs, const char *zName, sqlite3_syscall_ptr pNew) { return orig_vfs->xSetSystemCall ? orig_vfs->xSetSystemCall(orig_vfs, zName, pNew) : SQLITE_ERROR; }
|
|
472
|
+
static sqlite3_syscall_ptr gitvfs_GetSystemCall(sqlite3_vfs *pVfs, const char *zName) { return orig_vfs->xGetSystemCall ? orig_vfs->xGetSystemCall(orig_vfs, zName) : NULL; }
|
|
473
|
+
static const char *gitvfs_NextSystemCall(sqlite3_vfs *pVfs, const char *zName) { return orig_vfs->xNextSystemCall ? orig_vfs->xNextSystemCall(orig_vfs, zName) : NULL; }
|
|
474
|
+
|
|
475
|
+
/*
|
|
476
|
+
* Entry point to register our Git VFS.
|
|
477
|
+
*/
|
|
478
|
+
int sqlite3_gitvfs_init_impl(const char *base_dir) {
|
|
479
|
+
(void)base_dir;
|
|
480
|
+
if (sqlite3_vfs_find("gitvfs") != NULL) {
|
|
481
|
+
return SQLITE_OK;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (!orig_vfs) orig_vfs = sqlite3_vfs_find(NULL);
|
|
485
|
+
|
|
486
|
+
static sqlite3_vfs git_vfs = {
|
|
487
|
+
3, /* iVersion */
|
|
488
|
+
0, /* szOsFile */
|
|
489
|
+
GITVFS_MAX_PATH, /* mxPathname */
|
|
490
|
+
NULL, /* pNext */
|
|
491
|
+
"gitvfs", /* zName */
|
|
492
|
+
NULL, /* pAppData */
|
|
493
|
+
gitvfs_Open, /* xOpen */
|
|
494
|
+
gitvfs_Delete, /* xDelete */
|
|
495
|
+
gitvfs_Access, /* xAccess */
|
|
496
|
+
gitvfs_FullPathname, /* xFullPathname */
|
|
497
|
+
gitvfs_DlOpen, /* xDlOpen */
|
|
498
|
+
gitvfs_DlError, /* xDlError */
|
|
499
|
+
gitvfs_DlSym, /* xDlSym */
|
|
500
|
+
gitvfs_DlClose, /* xDlClose */
|
|
501
|
+
gitvfs_Randomness, /* xRandomness */
|
|
502
|
+
gitvfs_Sleep, /* xSleep */
|
|
503
|
+
gitvfs_CurrentTime, /* xCurrentTime */
|
|
504
|
+
gitvfs_GetLastError, /* xGetLastError */
|
|
505
|
+
gitvfs_CurrentTimeInt64, /* xCurrentTimeInt64 */
|
|
506
|
+
gitvfs_SetSystemCall, /* xSetSystemCall */
|
|
507
|
+
gitvfs_GetSystemCall, /* xGetSystemCall */
|
|
508
|
+
gitvfs_NextSystemCall /* xNextSystemCall */
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
git_vfs.szOsFile = sizeof(gitvfs_file) > (size_t)orig_vfs->szOsFile ? (int)sizeof(gitvfs_file) : orig_vfs->szOsFile;
|
|
512
|
+
return sqlite3_vfs_register(&git_vfs, 1);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#ifdef COMPILE_SQLITE_EXTENSION
|
|
516
|
+
#ifdef _WIN32
|
|
517
|
+
__declspec(dllexport)
|
|
518
|
+
#endif
|
|
519
|
+
int sqlite3_gitvfs_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
|
|
520
|
+
(void)db; (void)pzErrMsg;
|
|
521
|
+
SQLITE_EXTENSION_INIT2(pApi);
|
|
522
|
+
int rc = sqlite3_gitvfs_init_impl(".db");
|
|
523
|
+
return (rc == SQLITE_OK) ? SQLITE_OK_LOAD_PERMANENTLY : rc;
|
|
524
|
+
}
|
|
525
|
+
#endif
|
|
526
|
+
|
package/c/gitvfs.h
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#ifndef GITVFS_H
|
|
2
|
+
#define GITVFS_H
|
|
3
|
+
|
|
4
|
+
#include <sqlite3.h>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Initializes and registers the Git VFS with SQLite.
|
|
8
|
+
*
|
|
9
|
+
* @param base_dir Optional. The base directory for the database.
|
|
10
|
+
* Can be passed in context or managed per connection.
|
|
11
|
+
* @return SQLITE_OK on success, or an SQLite error code.
|
|
12
|
+
*/
|
|
13
|
+
int sqlite3_gitvfs_init_impl(const char *base_dir);
|
|
14
|
+
|
|
15
|
+
#endif // GITVFS_H
|
package/c/main.c
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#include <stdio.h>
|
|
2
|
+
#include <stdlib.h>
|
|
3
|
+
#include <string.h>
|
|
4
|
+
#include <sqlite3.h>
|
|
5
|
+
#include "gitvfs.h"
|
|
6
|
+
|
|
7
|
+
// Callback to print SELECT results
|
|
8
|
+
static int select_callback(void *NotUsed, int argc, char **argv, char **azColName) {
|
|
9
|
+
(void)NotUsed;
|
|
10
|
+
for (int i = 0; i < argc; i++) {
|
|
11
|
+
printf("%s = %s ", azColName[i], argv[i] ? argv[i] : "NULL");
|
|
12
|
+
}
|
|
13
|
+
printf("\n");
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
int main(int argc, char *argv[]) {
|
|
18
|
+
sqlite3 *db;
|
|
19
|
+
char *err_msg = 0;
|
|
20
|
+
int rc;
|
|
21
|
+
|
|
22
|
+
if (argc < 2) {
|
|
23
|
+
fprintf(stderr, "Usage: %s [init | <sql_statement>]\n", argv[0]);
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 1. Initialize our custom Git VFS
|
|
28
|
+
// We pass ".db" as our base directory. This will map to .db/pages/...
|
|
29
|
+
rc = sqlite3_gitvfs_init_impl(".db");
|
|
30
|
+
if (rc != SQLITE_OK) {
|
|
31
|
+
fprintf(stderr, "Failed to initialize gitvfs: %d\n", rc);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. Open the database using our custom VFS
|
|
36
|
+
// Note the "gitvfs" parameter at the end
|
|
37
|
+
rc = sqlite3_open_v2(".db", &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, "gitvfs");
|
|
38
|
+
if (rc != SQLITE_OK) {
|
|
39
|
+
fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db));
|
|
40
|
+
sqlite3_close(db);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Set page size to 4096 to match our VFS assumptions
|
|
45
|
+
rc = sqlite3_exec(db, "PRAGMA page_size = 4096;", 0, 0, &err_msg);
|
|
46
|
+
if (rc != SQLITE_OK) {
|
|
47
|
+
fprintf(stderr, "PRAGMA error: %s\n", err_msg);
|
|
48
|
+
sqlite3_free(err_msg);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (strcmp(argv[1], "init") == 0) {
|
|
52
|
+
// 4. Create test tables
|
|
53
|
+
const char *sql_create = "CREATE TABLE IF NOT EXISTS test_data (id INTEGER PRIMARY KEY, name TEXT, value BLOB);";
|
|
54
|
+
rc = sqlite3_exec(db, sql_create, 0, 0, &err_msg);
|
|
55
|
+
if (rc != SQLITE_OK) {
|
|
56
|
+
fprintf(stderr, "SQL error (CREATE test_data): %s\n", err_msg);
|
|
57
|
+
sqlite3_free(err_msg);
|
|
58
|
+
} else {
|
|
59
|
+
printf("Table 'test_data' ensured.\n");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const char *sql_create_settings = "CREATE TABLE IF NOT EXISTS test_settings (config_key TEXT PRIMARY KEY, config_val TEXT);";
|
|
63
|
+
rc = sqlite3_exec(db, sql_create_settings, 0, 0, &err_msg);
|
|
64
|
+
if (rc != SQLITE_OK) {
|
|
65
|
+
fprintf(stderr, "SQL error (CREATE test_settings): %s\n", err_msg);
|
|
66
|
+
sqlite3_free(err_msg);
|
|
67
|
+
} else {
|
|
68
|
+
printf("Table 'test_settings' ensured.\n");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 5. Insert test data
|
|
72
|
+
sqlite3_exec(db, "BEGIN TRANSACTION;", 0, 0, &err_msg);
|
|
73
|
+
const char *sql_insert = "INSERT INTO test_data (name, value) VALUES ('Hello GitVFS', randomblob(100));";
|
|
74
|
+
|
|
75
|
+
// Let's insert a few rows to force some data across pages eventually
|
|
76
|
+
for (int i = 0; i < 10000; i++) {
|
|
77
|
+
rc = sqlite3_exec(db, sql_insert, 0, 0, &err_msg);
|
|
78
|
+
if (rc != SQLITE_OK) {
|
|
79
|
+
fprintf(stderr, "SQL error (INSERT test_data): %s\n", err_msg);
|
|
80
|
+
sqlite3_free(err_msg);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
sqlite3_exec(db, "COMMIT;", 0, 0, &err_msg);
|
|
85
|
+
printf("Inserted 10000 rows of test_data.\n");
|
|
86
|
+
|
|
87
|
+
const char *sql_insert_settings = "INSERT INTO test_settings (config_key, config_val) VALUES ('theme', 'dark');";
|
|
88
|
+
rc = sqlite3_exec(db, sql_insert_settings, 0, 0, &err_msg);
|
|
89
|
+
if (rc != SQLITE_OK) {
|
|
90
|
+
fprintf(stderr, "SQL error (INSERT test_settings): %s\n", err_msg);
|
|
91
|
+
sqlite3_free(err_msg);
|
|
92
|
+
} else {
|
|
93
|
+
printf("Inserted baseline row into test_settings.\n");
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
// Execute the provided SQL
|
|
97
|
+
// Include the select_callback to print output for queries
|
|
98
|
+
rc = sqlite3_exec(db, argv[1], select_callback, 0, &err_msg);
|
|
99
|
+
if (rc != SQLITE_OK) {
|
|
100
|
+
fprintf(stderr, "SQL error: %s\n", err_msg);
|
|
101
|
+
sqlite3_free(err_msg);
|
|
102
|
+
} else {
|
|
103
|
+
printf("Executed SQL: %s\n", argv[1]);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 6. Close the connection
|
|
108
|
+
sqlite3_close(db);
|
|
109
|
+
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/c/output/main.o
ADDED
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
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);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
module.exports = GitSQLite;
|
package/install.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
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');
|
|
6
|
+
|
|
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();
|
|
11
|
+
|
|
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
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
downloadAndExtract();
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-sqlite-vfs",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "A Git-Versioned SQLite Database via a Custom Virtual File System (VFS)",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "cd c && make",
|
|
8
|
+
"postinstall": "node install.js",
|
|
9
|
+
"pretest": "npm run build && rm -rf .db .git && git init --initial-branch=master",
|
|
10
|
+
"test": "node --test test.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"better-sqlite3": "^9.4.3"
|
|
14
|
+
},
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "ISC"
|
|
17
|
+
}
|
package/test.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
});
|