git-sqlite-vfs 0.0.2 → 0.0.7

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.
@@ -6,225 +6,249 @@
6
6
  #include "gitvfs.h"
7
7
 
8
8
  int main(int argc, char *argv[]) {
9
- // A Git Strategy receives: <base> -- <head> <remote>
9
+ // A Git Driver receives: %O %A %B %P
10
10
  if (argc < 5) {
11
- fprintf(stderr, "Usage: %s <base> -- <head> <remote>\n", argv[0]);
11
+ fprintf(stderr, "Usage: %s %%O %%A %%B %%P\n", argv[0]);
12
12
  return 1;
13
13
  }
14
14
 
15
- const char *base = argv[1];
16
- const char *other = argv[4];
15
+ const char *path_out = argv[2]; // %A
16
+ const char *path_repo = argv[4]; // %P
17
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);
18
+ char base_dir[512];
19
+ strncpy(base_dir, path_repo, sizeof(base_dir) - 1);
20
+ base_dir[sizeof(base_dir) - 1] = '\0';
21
+ char *pages_ptr = strstr(base_dir, "/pages/");
22
+ if (!pages_ptr) {
23
+ fprintf(stderr, "Failed to parse base_dir from %s\n", path_repo);
24
+ return 1;
28
25
  }
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);
26
+ *pages_ptr = '\0';
27
+
28
+ // We only need to run the logical merge once per merge operation.
29
+ // We can use a lock file.
30
+ char lock_file[512];
31
+ snprintf(lock_file, sizeof(lock_file), "/tmp/gitvfs_merge_%s.lock", base_dir);
32
+ for (size_t i=0; i<sizeof(lock_file); i++) {
33
+ if (lock_file[i] == '/') lock_file[i] = '_';
36
34
  }
35
+
36
+ if (access(lock_file, F_OK) != 0) {
37
+ // Create lock
38
+ FILE *lf = fopen(lock_file, "w");
39
+ if (lf) fclose(lf);
40
+
41
+ printf("VFS Merge Driver Invoked: Whole-DB Logical Merge Initiated for %s...\n", base_dir);
42
+
43
+ system("env > /tmp/gitvfs_env.txt");
44
+ system("ls -la .git > /tmp/gitvfs_ls.txt");
45
+
46
+ char merge_head_hash[64] = "";
47
+ extern char **environ;
48
+ for (char **env = environ; *env != 0; env++) {
49
+ if (strncmp(*env, "GITHEAD_", 8) == 0) {
50
+ strncpy(merge_head_hash, *env + 8, 40);
51
+ merge_head_hash[40] = '\0';
52
+ break;
53
+ }
54
+ }
55
+
56
+ if (strlen(merge_head_hash) == 0) {
57
+ fprintf(stderr, "Could not find GITHEAD_ environment variable to determine other commit.\n");
58
+ return 1;
59
+ }
37
60
 
38
- // 3. The Logical Merge via ATTACH
39
- sqlite3 *db_local;
61
+ char cmd_other[1024];
62
+ snprintf(cmd_other, sizeof(cmd_other), "git archive %s \"%s/pages/\" | tar -x -C /tmp/gitvfs_other_db", merge_head_hash, base_dir);
63
+ if (system(cmd_other) != 0) {
64
+ fprintf(stderr, "Failed to extract other database. Command: %s\n", cmd_other);
65
+ }
40
66
 
41
- sqlite3_gitvfs_init_impl(NULL);
42
- sqlite3_open_v2(".db", &db_local, SQLITE_OPEN_READWRITE, "gitvfs");
67
+ system("rm -rf /tmp/gitvfs_ancestor_db && mkdir -p /tmp/gitvfs_ancestor_db");
68
+ char cmd_ancestor[1024];
69
+ snprintf(cmd_ancestor, sizeof(cmd_ancestor), "git archive $(git merge-base HEAD %s) \"%s/pages/\" | tar -x -C /tmp/gitvfs_ancestor_db", merge_head_hash, base_dir);
70
+ if (system(cmd_ancestor) != 0) {
71
+ fprintf(stderr, "Failed to extract ancestor database. Command: %s\n", cmd_ancestor);
72
+ }
43
73
 
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);
74
+ // The Logical Merge via ATTACH
75
+ sqlite3 *db_local;
76
+ sqlite3_gitvfs_init_impl(NULL);
77
+
78
+ // Ensure VFS env var is set for gitvfs_Open
79
+ #ifdef _WIN32
80
+ char env_str[1024];
81
+ snprintf(env_str, sizeof(env_str), "GIT_SQLITE_VFS_DIR=%s", base_dir);
82
+ _putenv(env_str);
83
+ #else
84
+ setenv("GIT_SQLITE_VFS_DIR", base_dir, 1);
85
+ #endif
86
+
87
+ if (sqlite3_open_v2(base_dir, &db_local, SQLITE_OPEN_READWRITE, "gitvfs") != SQLITE_OK) {
88
+ fprintf(stderr, "Failed to open local DB: %s\n", sqlite3_errmsg(db_local));
89
+ }
46
90
 
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);
91
+ char attach_other[1024];
92
+ snprintf(attach_other, sizeof(attach_other), "ATTACH DATABASE '/tmp/gitvfs_other_db/%s' AS other;", base_dir);
93
+ sqlite3_exec(db_local, attach_other, NULL, 0, NULL);
94
+
95
+ char attach_ancestor[1024];
96
+ snprintf(attach_ancestor, sizeof(attach_ancestor), "ATTACH DATABASE '/tmp/gitvfs_ancestor_db/%s' AS ancestor;", base_dir);
97
+ sqlite3_exec(db_local, attach_ancestor, NULL, 0, NULL);
98
+
99
+ // Phase 1 (Propagate Drops)
100
+ 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);
101
+
102
+ sqlite3_stmt *drop_stmt;
103
+ char *drops_to_execute = calloc(1, 1024 * 1024); // 1MB buffer
104
+ if (sqlite3_prepare_v2(db_local, "SELECT type, name FROM drops;", -1, &drop_stmt, NULL) == SQLITE_OK) {
105
+ while (sqlite3_step(drop_stmt) == SQLITE_ROW) {
106
+ const char *type = (const char *)sqlite3_column_text(drop_stmt, 0);
107
+ const char *name = (const char *)sqlite3_column_text(drop_stmt, 1);
108
+ if (type && name) {
109
+ char drop_sql[512];
110
+ snprintf(drop_sql, sizeof(drop_sql), "DROP %s IF EXISTS \"%s\";\n", type, name);
111
+ strcat(drops_to_execute, drop_sql);
112
+ }
61
113
  }
114
+ sqlite3_finalize(drop_stmt);
62
115
  }
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);
116
+ if (strlen(drops_to_execute) > 0) {
117
+ sqlite3_exec(db_local, drops_to_execute, NULL, 0, NULL);
71
118
  }
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");
119
+ free(drops_to_execute);
120
+ sqlite3_exec(db_local, "DROP TABLE drops;", NULL, 0, NULL);
121
+
122
+ // Phase 2 (Propagate Additions)
123
+ 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);
124
+
125
+ sqlite3_stmt *add_stmt;
126
+ char *adds_to_execute = calloc(1, 1024 * 1024); // 1MB buffer
127
+ if (sqlite3_prepare_v2(db_local, "SELECT sql FROM adds;", -1, &add_stmt, NULL) == SQLITE_OK) {
128
+ while (sqlite3_step(add_stmt) == SQLITE_ROW) {
129
+ const char *sql = (const char *)sqlite3_column_text(add_stmt, 0);
130
+ if (sql) {
131
+ strcat(adds_to_execute, sql);
132
+ strcat(adds_to_execute, ";\n");
133
+ }
87
134
  }
135
+ sqlite3_finalize(add_stmt);
88
136
  }
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);
137
+ if (strlen(adds_to_execute) > 0) {
138
+ sqlite3_exec(db_local, adds_to_execute, NULL, 0, NULL);
97
139
  }
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;
140
+ free(adds_to_execute);
141
+ sqlite3_exec(db_local, "DROP TABLE adds;", NULL, 0, NULL);
142
+
143
+ // Dynamically discover and merge tables
144
+ sqlite3_stmt *stmt;
145
+ const char *query_schema = "SELECT name FROM main.sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%';";
146
+
147
+ if (sqlite3_prepare_v2(db_local, query_schema, -1, &stmt, NULL) == SQLITE_OK) {
148
+ while (sqlite3_step(stmt) == SQLITE_ROW) {
149
+ const char *table_name = (const char *)sqlite3_column_text(stmt, 0);
150
+ if (table_name) {
151
+ char pragma_query[512];
152
+ snprintf(pragma_query, sizeof(pragma_query), "PRAGMA main.table_info(\"%s\");", table_name);
153
+ sqlite3_stmt *pragma_stmt;
154
+ char pk_col[256] = {0};
155
+ int has_pk = 0;
156
+
157
+ if (sqlite3_prepare_v2(db_local, pragma_query, -1, &pragma_stmt, NULL) == SQLITE_OK) {
158
+ while (sqlite3_step(pragma_stmt) == SQLITE_ROW) {
159
+ int pk = sqlite3_column_int(pragma_stmt, 5);
160
+ if (pk > 0) {
161
+ const char *col_name = (const char *)sqlite3_column_text(pragma_stmt, 1);
162
+ if (col_name) {
163
+ strncpy(pk_col, col_name, sizeof(pk_col) - 1);
164
+ has_pk = 1;
165
+ break;
166
+ }
127
167
  }
128
168
  }
169
+ sqlite3_finalize(pragma_stmt);
129
170
  }
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
171
 
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);
172
+ if (has_pk) {
173
+ int exists_in_ancestor = 0;
174
+ char check_anc[256];
175
+ snprintf(check_anc, sizeof(check_anc), "SELECT 1 FROM ancestor.sqlite_schema WHERE type='table' AND name='%s';", table_name);
176
+ sqlite3_stmt *anc_stmt;
177
+ if (sqlite3_prepare_v2(db_local, check_anc, -1, &anc_stmt, NULL) == SQLITE_OK) {
178
+ if (sqlite3_step(anc_stmt) == SQLITE_ROW) exists_in_ancestor = 1;
179
+ sqlite3_finalize(anc_stmt);
188
180
  }
189
181
 
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);
182
+ if (exists_in_ancestor) {
183
+ char drop_conflict[256];
184
+ snprintf(drop_conflict, sizeof(drop_conflict), "DROP TABLE IF EXISTS temp.\"conflicted_pks_%s\";", table_name);
185
+ sqlite3_exec(db_local, drop_conflict, NULL, 0, NULL);
186
+
187
+ char conflict_query[1024];
188
+ snprintf(conflict_query, sizeof(conflict_query),
189
+ "CREATE TEMP TABLE \"conflicted_pks_%s\" AS "
190
+ "SELECT \"%s\" FROM (SELECT * FROM main.\"%s\" EXCEPT SELECT * FROM ancestor.\"%s\") "
191
+ "INTERSECT "
192
+ "SELECT \"%s\" FROM (SELECT * FROM other.\"%s\" EXCEPT SELECT * FROM ancestor.\"%s\");",
193
+ table_name,
194
+ pk_col, table_name, table_name,
195
+ pk_col, table_name, table_name);
196
+
197
+ sqlite3_exec(db_local, conflict_query, NULL, 0, NULL);
198
+
199
+ char q1[1024];
200
+ char q2[1024];
201
+
202
+ // Query 1: Propagate Deletions from MERGE_HEAD
203
+ snprintf(q1, sizeof(q1),
204
+ "DELETE FROM main.\"%s\" WHERE \"%s\" IN (SELECT \"%s\" FROM ancestor.\"%s\" EXCEPT SELECT \"%s\" FROM other.\"%s\");",
205
+ table_name, pk_col, pk_col, table_name, pk_col, table_name);
206
+ sqlite3_exec(db_local, q1, NULL, 0, NULL);
207
+
208
+ // Query 2: Propagate Inserts & Updates from MERGE_HEAD, omitting row conflicts
209
+ snprintf(q2, sizeof(q2),
210
+ "REPLACE INTO main.\"%s\" SELECT * FROM other.\"%s\" WHERE \"%s\" IN (SELECT \"%s\" FROM (SELECT * FROM other.\"%s\" EXCEPT SELECT * FROM ancestor.\"%s\")) "
211
+ "AND \"%s\" NOT IN (SELECT \"%s\" FROM \"conflicted_pks_%s\");",
212
+ table_name, table_name, pk_col, pk_col, table_name, table_name, pk_col, pk_col, table_name);
213
+ sqlite3_exec(db_local, q2, NULL, 0, NULL);
214
+
215
+ sqlite3_exec(db_local, drop_conflict, NULL, 0, NULL);
216
+ } else {
217
+ char merge_query[512];
218
+ snprintf(merge_query, sizeof(merge_query), "INSERT OR IGNORE INTO main.\"%s\" SELECT * FROM other.\"%s\";", table_name, table_name);
219
+ sqlite3_exec(db_local, merge_query, NULL, 0, NULL);
198
220
  }
199
-
200
- sqlite3_exec(db_local, drop_conflict, NULL, 0, NULL);
201
221
  } else {
202
- // Fallback to naive 2-way append (table is new)
203
222
  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);
223
+ snprintf(merge_query, sizeof(merge_query), "INSERT OR IGNORE INTO main.\"%s\" SELECT * FROM other.\"%s\";", table_name, table_name);
207
224
  sqlite3_exec(db_local, merge_query, NULL, 0, NULL);
208
225
  }
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
226
  }
216
227
  }
228
+ sqlite3_finalize(stmt);
217
229
  }
218
- sqlite3_finalize(stmt);
219
- } else {
220
- fprintf(stderr, "Failed to prepare schema query: %s\n", sqlite3_errmsg(db_local));
221
- }
222
230
 
223
- sqlite3_close(db_local);
231
+ sqlite3_close(db_local);
232
+
233
+ char git_add_cmd[1024];
234
+ snprintf(git_add_cmd, sizeof(git_add_cmd), "git add -A -f \"%s/pages/\"", base_dir);
235
+ if (system(git_add_cmd) != 0) {}
236
+
237
+ printf("Logical Merge Complete! VFS physical pages reconciled.\n");
238
+ }
224
239
 
225
- if (system("git add -A -f .db/pages/") != 0) {}
226
- if (system("rm -rf /tmp/gitvfs_other_db /tmp/gitvfs_ancestor_db") != 0) {}
240
+ // Now, copy the resulting file from the working tree to path_out (%A) so Git considers this file resolved!
241
+ // If the file doesn't exist anymore (deleted), we can just create an empty file or remove it.
242
+ char cp_cmd[1024];
243
+ if (access(path_repo, F_OK) == 0) {
244
+ snprintf(cp_cmd, sizeof(cp_cmd), "cp \"%s\" \"%s\"", path_repo, path_out);
245
+ if (system(cp_cmd) != 0) {
246
+ fprintf(stderr, "Failed to copy %s to %s\n", path_repo, path_out);
247
+ }
248
+ } else {
249
+ snprintf(cp_cmd, sizeof(cp_cmd), "rm -f \"%s\"", path_out);
250
+ if (system(cp_cmd) != 0) {}
251
+ }
227
252
 
228
- printf("Logical Merge Complete! VFS physical pages reconciled.\n");
229
253
  return 0;
230
254
  }
package/c/gitvfs.c CHANGED
@@ -29,11 +29,34 @@ typedef struct gitvfs_file {
29
29
  int flat_fd; /* POSIX file descriptor for temp/journal files */
30
30
  } gitvfs_file;
31
31
 
32
+ #ifdef _WIN32
33
+ #include <direct.h>
34
+ #include <io.h>
35
+ #define MKDIR(p, m) _mkdir(p)
36
+
37
+ ssize_t pread(int fd, void *buf, size_t count, off_t offset) {
38
+ off_t current = lseek(fd, 0, SEEK_CUR);
39
+ if (lseek(fd, offset, SEEK_SET) == (off_t)-1) return -1;
40
+ ssize_t ret = read(fd, buf, count);
41
+ lseek(fd, current, SEEK_SET);
42
+ return ret;
43
+ }
44
+
45
+ ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset) {
46
+ off_t current = lseek(fd, 0, SEEK_CUR);
47
+ if (lseek(fd, offset, SEEK_SET) == (off_t)-1) return -1;
48
+ ssize_t ret = write(fd, buf, count);
49
+ lseek(fd, current, SEEK_SET);
50
+ return ret;
51
+ }
52
+ #else
53
+ #define MKDIR(p, m) mkdir(p, m)
54
+ #endif
32
55
  /*
33
- * Utility: Recursively create directories (mkdir -p behavior)
34
56
  * Helps ensure our nested sharded directory structure exists before writing.
35
57
  */
36
58
  static int mkdir_p(const char *path, mode_t mode) {
59
+ (void)mode;
37
60
  char tmp[GITVFS_MAX_PATH];
38
61
  char *p = NULL;
39
62
  size_t len;
@@ -47,13 +70,13 @@ static int mkdir_p(const char *path, mode_t mode) {
47
70
  for (p = tmp + 1; *p; p++) {
48
71
  if (*p == '/') {
49
72
  *p = 0;
50
- if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
73
+ if (MKDIR(tmp, mode) != 0 && errno != EEXIST) {
51
74
  return -1;
52
75
  }
53
76
  *p = '/';
54
77
  }
55
78
  }
56
- if (mkdir(tmp, mode) != 0 && errno != EEXIST) {
79
+ if (MKDIR(tmp, mode) != 0 && errno != EEXIST) {
57
80
  return -1;
58
81
  }
59
82
  return 0;
@@ -108,7 +131,7 @@ static void generate_gitattributes(const char *base_dir) {
108
131
  // Create the .gitattributes file
109
132
  FILE *f = fopen(attr_path, "w");
110
133
  if (f) {
111
- fprintf(f, "*.bin binary\nsize.meta binary\n");
134
+ fprintf(f, "*.bin -text -diff merge=sqlitevfs\nsize.meta -text -diff merge=sqlitevfs\n");
112
135
  fclose(f);
113
136
  }
114
137
  }
@@ -271,7 +294,7 @@ static int gitvfs_Truncate(sqlite3_file *pFile, sqlite3_int64 size) {
271
294
  snprintf(meta_path, sizeof(meta_path), "%s/pages/size.meta", p->base_dir);
272
295
 
273
296
  if (new_max_page == -1) {
274
- unlink(meta_path); // DB is completely empty
297
+ remove(meta_path); // DB is completely empty
275
298
  } else {
276
299
  FILE *f = fopen(meta_path, "w");
277
300
  if (f) {
@@ -285,6 +308,7 @@ static int gitvfs_Truncate(sqlite3_file *pFile, sqlite3_int64 size) {
285
308
  }
286
309
 
287
310
  static int gitvfs_Sync(sqlite3_file *pFile, int flags) {
311
+ (void)pFile; (void)flags;
288
312
  // Single-writer MVP relying on standard POSIX disk flushes.
289
313
  // For temp files we could call fsync(p->flat_fd).
290
314
  // Returning SQLITE_OK satisfies SQLite's expectation.
@@ -309,6 +333,7 @@ static int gitvfs_FileSize(sqlite3_file *pFile, sqlite3_int64 *pSize) {
309
333
  }
310
334
 
311
335
  static int gitvfs_Lock(sqlite3_file *pFile, int eLock) {
336
+ (void)pFile; (void)eLock;
312
337
  return SQLITE_OK; // SQLite requires lock functions to succeed
313
338
  }
314
339
 
@@ -318,19 +343,23 @@ static int gitvfs_Unlock(sqlite3_file *pFile, int eLock) {
318
343
  }
319
344
 
320
345
  static int gitvfs_CheckReservedLock(sqlite3_file *pFile, int *pResOut) {
346
+ (void)pFile;
321
347
  *pResOut = 0;
322
348
  return SQLITE_OK;
323
349
  }
324
350
 
325
351
  static int gitvfs_FileControl(sqlite3_file *pFile, int op, void *pArg) {
352
+ (void)pFile; (void)op; (void)pArg;
326
353
  return SQLITE_NOTFOUND;
327
354
  }
328
355
 
329
356
  static int gitvfs_SectorSize(sqlite3_file *pFile) {
357
+ (void)pFile;
330
358
  return GITVFS_PAGE_SIZE;
331
359
  }
332
360
 
333
361
  static int gitvfs_DeviceCharacteristics(sqlite3_file *pFile) {
362
+ (void)pFile;
334
363
  return 0; // Standard characteristics
335
364
  }
336
365
 
@@ -363,8 +392,13 @@ static const sqlite3_io_methods gitvfs_io_methods = {
363
392
  static sqlite3_vfs *orig_vfs = NULL;
364
393
 
365
394
  static int gitvfs_Open(sqlite3_vfs *pVfs, const char *zName, sqlite3_file *pFile, int flags, int *pOutFlags) {
395
+ (void)pVfs;
366
396
  if (!orig_vfs) orig_vfs = sqlite3_vfs_find(NULL);
367
- if (!zName || strstr(zName, ".db") == NULL) {
397
+ const char *vfs_dir = getenv("GIT_SQLITE_VFS_DIR");
398
+ if (!vfs_dir) {
399
+ vfs_dir = ".db";
400
+ }
401
+ if (!zName || strstr(zName, vfs_dir) == NULL) {
368
402
  return orig_vfs->xOpen(orig_vfs, zName, pFile, flags, pOutFlags);
369
403
  }
370
404
  gitvfs_file *p = (gitvfs_file*)pFile;
@@ -376,7 +410,7 @@ static int gitvfs_Open(sqlite3_vfs *pVfs, const char *zName, sqlite3_file *pFile
376
410
  if (flags & SQLITE_OPEN_MAIN_DB) {
377
411
  p->is_main_db = 1;
378
412
 
379
- const char *base = (zName != NULL) ? zName : ".db";
413
+ const char *base = (zName != NULL) ? zName : vfs_dir;
380
414
 
381
415
  // Strip URI parameters if they exist
382
416
  char clean_base[GITVFS_MAX_PATH];
@@ -441,36 +475,39 @@ static int gitvfs_Open(sqlite3_vfs *pVfs, const char *zName, sqlite3_file *pFile
441
475
  }
442
476
 
443
477
  static int gitvfs_Delete(sqlite3_vfs *pVfs, const char *zName, int syncDir) {
478
+ (void)pVfs; (void)syncDir;
444
479
  // Standard file deletion, primarily used for clearing out old journals
445
480
  unlink(zName);
446
481
  return SQLITE_OK;
447
482
  }
448
483
 
449
484
  static int gitvfs_Access(sqlite3_vfs *pVfs, const char *zName, int flags, int *pResOut) {
485
+ (void)pVfs; (void)flags;
450
486
  // Check if the directory or file is accessible
451
487
  *pResOut = (access(zName, F_OK) == 0) ? 1 : 0;
452
488
  return SQLITE_OK;
453
489
  }
454
490
 
455
491
  static int gitvfs_FullPathname(sqlite3_vfs *pVfs, const char *zName, int nOut, char *zOut) {
492
+ (void)pVfs;
456
493
  snprintf(zOut, nOut, "%s", zName);
457
494
  return SQLITE_OK;
458
495
  }
459
496
 
460
497
  /* 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; }
498
+ static void *gitvfs_DlOpen(sqlite3_vfs *pVfs, const char *zFilename) { (void)pVfs; return orig_vfs->xDlOpen(orig_vfs, zFilename); }
499
+ static void gitvfs_DlError(sqlite3_vfs *pVfs, int nByte, char *zErrMsg) { (void)pVfs; orig_vfs->xDlError(orig_vfs, nByte, zErrMsg); }
500
+ static void (*gitvfs_DlSym(sqlite3_vfs *pVfs, void *p, const char*zSymbol))(void) { (void)pVfs; return orig_vfs->xDlSym(orig_vfs, p, zSymbol); }
501
+ static void gitvfs_DlClose(sqlite3_vfs *pVfs, void *pHandle) { (void)pVfs; orig_vfs->xDlClose(orig_vfs, pHandle); }
502
+ static int gitvfs_Randomness(sqlite3_vfs *pVfs, int nByte, char *zOut) { (void)pVfs; return orig_vfs->xRandomness(orig_vfs, nByte, zOut); }
503
+ static int gitvfs_Sleep(sqlite3_vfs *pVfs, int microseconds) { (void)pVfs; return orig_vfs->xSleep(orig_vfs, microseconds); }
504
+ static int gitvfs_CurrentTime(sqlite3_vfs *pVfs, double *prNow) { (void)pVfs; return orig_vfs->xCurrentTime(orig_vfs, prNow); }
505
+
506
+ static int gitvfs_GetLastError(sqlite3_vfs *pVfs, int a, char *b) { (void)pVfs; return orig_vfs->xGetLastError ? orig_vfs->xGetLastError(orig_vfs, a, b) : 0; }
507
+ static int gitvfs_CurrentTimeInt64(sqlite3_vfs *pVfs, sqlite3_int64 *p) { (void)pVfs; return orig_vfs->xCurrentTimeInt64 ? orig_vfs->xCurrentTimeInt64(orig_vfs, p) : 0; }
508
+ static int gitvfs_SetSystemCall(sqlite3_vfs *pVfs, const char *zName, sqlite3_syscall_ptr pNew) { (void)pVfs; return orig_vfs->xSetSystemCall ? orig_vfs->xSetSystemCall(orig_vfs, zName, pNew) : SQLITE_ERROR; }
509
+ static sqlite3_syscall_ptr gitvfs_GetSystemCall(sqlite3_vfs *pVfs, const char *zName) { (void)pVfs; return orig_vfs->xGetSystemCall ? orig_vfs->xGetSystemCall(orig_vfs, zName) : NULL; }
510
+ static const char *gitvfs_NextSystemCall(sqlite3_vfs *pVfs, const char *zName) { (void)pVfs; return orig_vfs->xNextSystemCall ? orig_vfs->xNextSystemCall(orig_vfs, zName) : NULL; }
474
511
 
475
512
  /*
476
513
  * Entry point to register our Git VFS.
@@ -516,11 +553,22 @@ int sqlite3_gitvfs_init_impl(const char *base_dir) {
516
553
  #ifdef _WIN32
517
554
  __declspec(dllexport)
518
555
  #endif
519
- int sqlite3_gitvfs_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
556
+ int sqlite3_extension_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
520
557
  (void)db; (void)pzErrMsg;
521
558
  SQLITE_EXTENSION_INIT2(pApi);
522
559
  int rc = sqlite3_gitvfs_init_impl(".db");
523
560
  return (rc == SQLITE_OK) ? SQLITE_OK_LOAD_PERMANENTLY : rc;
524
561
  }
562
+
563
+ #ifdef _WIN32
564
+ __declspec(dllexport)
565
+ #endif
566
+ int sqlite3_gitvfs_init(sqlite3 *db, char **pzErrMsg, const sqlite3_api_routines *pApi) {
567
+ return sqlite3_extension_init(db, pzErrMsg, pApi);
568
+ }
525
569
  #endif
526
570
 
571
+
572
+
573
+
574
+