@sproutboat/runtime 0.21.0 → 0.22.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/CHANGELOG.md +34 -0
- package/LTX_REPLICATION.md +124 -0
- package/README.md +7 -0
- package/package.json +2 -2
- package/src/native-fetch-prelude.js +38 -22
- package/src/transport-broker.js +5 -0
- package/src/transport-embedded.js +1082 -4
- package/test/native-ltx.ts +359 -0
- package/test/native-service-bindings.ts +39 -2
|
@@ -77,6 +77,1040 @@ static sqlite3* sb_dbs[SB_MAX_DB];
|
|
|
77
77
|
static char sb_db_names[SB_MAX_DB][256];
|
|
78
78
|
static int sb_db_count = 0;
|
|
79
79
|
|
|
80
|
+
// BEGIN LTX NATIVE: also compiled directly by the replication regression.
|
|
81
|
+
#include <dirent.h>
|
|
82
|
+
#include <errno.h>
|
|
83
|
+
#include <limits.h>
|
|
84
|
+
#include <sys/file.h>
|
|
85
|
+
|
|
86
|
+
// Public SQLite VFS ABI, from sqlite3.h (public domain). Keeping declarations
|
|
87
|
+
// here preserves standalone builds without an installed SQLite development SDK.
|
|
88
|
+
#ifndef SQLITE3_H
|
|
89
|
+
#ifndef _SQLITE3_H_
|
|
90
|
+
typedef long long sqlite3_int64;
|
|
91
|
+
typedef struct sqlite3_file sqlite3_file;
|
|
92
|
+
typedef struct sqlite3_io_methods sqlite3_io_methods;
|
|
93
|
+
typedef struct sqlite3_vfs sqlite3_vfs;
|
|
94
|
+
struct sqlite3_file {
|
|
95
|
+
const sqlite3_io_methods *pMethods;
|
|
96
|
+
};
|
|
97
|
+
struct sqlite3_io_methods {
|
|
98
|
+
int iVersion;
|
|
99
|
+
int (*xClose)(sqlite3_file *);
|
|
100
|
+
int (*xRead)(sqlite3_file *, void *, int, sqlite3_int64);
|
|
101
|
+
int (*xWrite)(sqlite3_file *, const void *, int, sqlite3_int64);
|
|
102
|
+
int (*xTruncate)(sqlite3_file *, sqlite3_int64);
|
|
103
|
+
int (*xSync)(sqlite3_file *, int);
|
|
104
|
+
int (*xFileSize)(sqlite3_file *, sqlite3_int64 *);
|
|
105
|
+
int (*xLock)(sqlite3_file *, int);
|
|
106
|
+
int (*xUnlock)(sqlite3_file *, int);
|
|
107
|
+
int (*xCheckReservedLock)(sqlite3_file *, int *);
|
|
108
|
+
int (*xFileControl)(sqlite3_file *, int, void *);
|
|
109
|
+
int (*xSectorSize)(sqlite3_file *);
|
|
110
|
+
int (*xDeviceCharacteristics)(sqlite3_file *);
|
|
111
|
+
int (*xShmMap)(sqlite3_file *, int, int, int, void volatile **);
|
|
112
|
+
int (*xShmLock)(sqlite3_file *, int, int, int);
|
|
113
|
+
void (*xShmBarrier)(sqlite3_file *);
|
|
114
|
+
int (*xShmUnmap)(sqlite3_file *, int);
|
|
115
|
+
int (*xFetch)(sqlite3_file *, sqlite3_int64, int, void **);
|
|
116
|
+
int (*xUnfetch)(sqlite3_file *, sqlite3_int64, void *);
|
|
117
|
+
};
|
|
118
|
+
struct sqlite3_vfs {
|
|
119
|
+
int iVersion, szOsFile, mxPathname;
|
|
120
|
+
sqlite3_vfs *pNext;
|
|
121
|
+
const char *zName;
|
|
122
|
+
void *pAppData;
|
|
123
|
+
int (*xOpen)(sqlite3_vfs *, const char *, sqlite3_file *, int, int *);
|
|
124
|
+
int (*xDelete)(sqlite3_vfs *, const char *, int);
|
|
125
|
+
int (*xAccess)(sqlite3_vfs *, const char *, int, int *);
|
|
126
|
+
int (*xFullPathname)(sqlite3_vfs *, const char *, int, char *);
|
|
127
|
+
void *(*xDlOpen)(sqlite3_vfs *, const char *);
|
|
128
|
+
void (*xDlError)(sqlite3_vfs *, int, char *);
|
|
129
|
+
void (*(*xDlSym)(sqlite3_vfs *, void *, const char *))(void);
|
|
130
|
+
void (*xDlClose)(sqlite3_vfs *, void *);
|
|
131
|
+
int (*xRandomness)(sqlite3_vfs *, int, char *);
|
|
132
|
+
int (*xSleep)(sqlite3_vfs *, int);
|
|
133
|
+
int (*xCurrentTime)(sqlite3_vfs *, double *);
|
|
134
|
+
int (*xGetLastError)(sqlite3_vfs *, int, char *);
|
|
135
|
+
int (*xCurrentTimeInt64)(sqlite3_vfs *, sqlite3_int64 *);
|
|
136
|
+
int (*xSetSystemCall)(sqlite3_vfs *, const char *, void (*)(void));
|
|
137
|
+
void (*(*xGetSystemCall)(sqlite3_vfs *, const char *))(void);
|
|
138
|
+
const char *(*xNextSystemCall)(sqlite3_vfs *, const char *);
|
|
139
|
+
};
|
|
140
|
+
#endif
|
|
141
|
+
#endif
|
|
142
|
+
extern sqlite3_vfs *sqlite3_vfs_find(const char *);
|
|
143
|
+
extern int sqlite3_vfs_register(sqlite3_vfs *, int);
|
|
144
|
+
extern int sqlite3_open_v2(const char *, sqlite3 **, int, const char *);
|
|
145
|
+
extern void *sqlite3_wal_hook(sqlite3 *, int (*)(void *, sqlite3 *, const char *, int), void *);
|
|
146
|
+
extern int sqlite3_wal_checkpoint_v2(sqlite3 *, const char *, int, int *, int *);
|
|
147
|
+
extern int sqlite3_set_authorizer(sqlite3 *,
|
|
148
|
+
int (*)(void *, int, const char *, const char *, const char *,
|
|
149
|
+
const char *),
|
|
150
|
+
void *);
|
|
151
|
+
|
|
152
|
+
#define SB_LTX_FLAG UINT64_C(0x8000000000000000)
|
|
153
|
+
#define SB_LTX_PATH 1024
|
|
154
|
+
static int sb_ltx_compat = 0;
|
|
155
|
+
static uint64_t sb_ltx_max_bytes = 16777216;
|
|
156
|
+
static sqlite3_vfs sb_ltx_vfs, *sb_ltx_parent;
|
|
157
|
+
typedef struct {
|
|
158
|
+
sqlite3_file base;
|
|
159
|
+
sqlite3_file *real;
|
|
160
|
+
int wal;
|
|
161
|
+
} sb_ltx_file;
|
|
162
|
+
static sqlite3_file *sb_ltx_real(sqlite3_file *f) { return ((sb_ltx_file *)f)->real; }
|
|
163
|
+
static int sb_ltx_vclose(sqlite3_file *f) {
|
|
164
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
165
|
+
return r->pMethods->xClose(r);
|
|
166
|
+
}
|
|
167
|
+
static int sb_ltx_vread(sqlite3_file *f, void *b, int n, sqlite3_int64 o) {
|
|
168
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
169
|
+
return r->pMethods->xRead(r, b, n, o);
|
|
170
|
+
}
|
|
171
|
+
static int sb_ltx_vwrite(sqlite3_file *f, const void *b, int n, sqlite3_int64 o) {
|
|
172
|
+
if (((sb_ltx_file *)f)->wal && (o < 0 || n < 0 || (uint64_t)o > sb_ltx_max_bytes ||
|
|
173
|
+
(uint64_t)n > sb_ltx_max_bytes - (uint64_t)o))
|
|
174
|
+
return 13; // SQLITE_FULL, before writing.
|
|
175
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
176
|
+
return r->pMethods->xWrite(r, b, n, o);
|
|
177
|
+
}
|
|
178
|
+
static int sb_ltx_vtruncate(sqlite3_file *f, sqlite3_int64 n) {
|
|
179
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
180
|
+
if (((sb_ltx_file *)f)->wal && (uint64_t)n > sb_ltx_max_bytes)
|
|
181
|
+
return 13;
|
|
182
|
+
return r->pMethods->xTruncate(r, n);
|
|
183
|
+
}
|
|
184
|
+
static int sb_ltx_vsync(sqlite3_file *f, int n) {
|
|
185
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
186
|
+
return r->pMethods->xSync(r, n);
|
|
187
|
+
}
|
|
188
|
+
static int sb_ltx_vsize(sqlite3_file *f, sqlite3_int64 *n) {
|
|
189
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
190
|
+
return r->pMethods->xFileSize(r, n);
|
|
191
|
+
}
|
|
192
|
+
static int sb_ltx_vlock(sqlite3_file *f, int n) {
|
|
193
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
194
|
+
return r->pMethods->xLock(r, n);
|
|
195
|
+
}
|
|
196
|
+
static int sb_ltx_vunlock(sqlite3_file *f, int n) {
|
|
197
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
198
|
+
return r->pMethods->xUnlock(r, n);
|
|
199
|
+
}
|
|
200
|
+
static int sb_ltx_vreserved(sqlite3_file *f, int *n) {
|
|
201
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
202
|
+
return r->pMethods->xCheckReservedLock(r, n);
|
|
203
|
+
}
|
|
204
|
+
static int sb_ltx_vcontrol(sqlite3_file *f, int n, void *p) {
|
|
205
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
206
|
+
if (((sb_ltx_file *)f)->wal) {
|
|
207
|
+
// SIZE_HINT can preallocate inside the parent VFS, bypassing xWrite and
|
|
208
|
+
// xTruncate. Ignore hints beyond the ceiling and disable chunk rounding.
|
|
209
|
+
if (n == 5 && (*(sqlite3_int64 *)p < 0 || (uint64_t)*(sqlite3_int64 *)p > sb_ltx_max_bytes))
|
|
210
|
+
return 0;
|
|
211
|
+
if (n == 6) {
|
|
212
|
+
int zero = 0;
|
|
213
|
+
return r->pMethods->xFileControl(r, n, &zero);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return r->pMethods->xFileControl(r, n, p);
|
|
217
|
+
}
|
|
218
|
+
static int sb_ltx_vsector(sqlite3_file *f) {
|
|
219
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
220
|
+
return r->pMethods->xSectorSize(r);
|
|
221
|
+
}
|
|
222
|
+
static int sb_ltx_vdevice(sqlite3_file *f) {
|
|
223
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
224
|
+
return r->pMethods->xDeviceCharacteristics(r);
|
|
225
|
+
}
|
|
226
|
+
static int sb_ltx_vmap(sqlite3_file *f, int p, int n, int w, void volatile **o) {
|
|
227
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
228
|
+
return r->pMethods->xShmMap(r, p, n, w, o);
|
|
229
|
+
}
|
|
230
|
+
static int sb_ltx_vshmlock(sqlite3_file *f, int o, int n, int v) {
|
|
231
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
232
|
+
return r->pMethods->xShmLock(r, o, n, v);
|
|
233
|
+
}
|
|
234
|
+
static void sb_ltx_vbarrier(sqlite3_file *f) {
|
|
235
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
236
|
+
r->pMethods->xShmBarrier(r);
|
|
237
|
+
}
|
|
238
|
+
static int sb_ltx_vunmap(sqlite3_file *f, int n) {
|
|
239
|
+
sqlite3_file *r = sb_ltx_real(f);
|
|
240
|
+
return r->pMethods->xShmUnmap(r, n);
|
|
241
|
+
}
|
|
242
|
+
// No mmap: bounds and RSS apply to reads as well as writes.
|
|
243
|
+
static int sb_ltx_vfetch(sqlite3_file *f, sqlite3_int64 o, int n, void **p) {
|
|
244
|
+
(void)f;
|
|
245
|
+
(void)o;
|
|
246
|
+
(void)n;
|
|
247
|
+
*p = 0;
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
static int sb_ltx_vunfetch(sqlite3_file *f, sqlite3_int64 o, void *p) {
|
|
251
|
+
(void)f;
|
|
252
|
+
(void)o;
|
|
253
|
+
(void)p;
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
static const sqlite3_io_methods sb_ltx_methods = {3,
|
|
257
|
+
sb_ltx_vclose,
|
|
258
|
+
sb_ltx_vread,
|
|
259
|
+
sb_ltx_vwrite,
|
|
260
|
+
sb_ltx_vtruncate,
|
|
261
|
+
sb_ltx_vsync,
|
|
262
|
+
sb_ltx_vsize,
|
|
263
|
+
sb_ltx_vlock,
|
|
264
|
+
sb_ltx_vunlock,
|
|
265
|
+
sb_ltx_vreserved,
|
|
266
|
+
sb_ltx_vcontrol,
|
|
267
|
+
sb_ltx_vsector,
|
|
268
|
+
sb_ltx_vdevice,
|
|
269
|
+
sb_ltx_vmap,
|
|
270
|
+
sb_ltx_vshmlock,
|
|
271
|
+
sb_ltx_vbarrier,
|
|
272
|
+
sb_ltx_vunmap,
|
|
273
|
+
sb_ltx_vfetch,
|
|
274
|
+
sb_ltx_vunfetch};
|
|
275
|
+
static int sb_ltx_vopen(sqlite3_vfs *v, const char *name, sqlite3_file *f, int flags, int *out) {
|
|
276
|
+
(void)v;
|
|
277
|
+
sb_ltx_file *w = (sb_ltx_file *)f;
|
|
278
|
+
memset(w, 0, sizeof(*w));
|
|
279
|
+
w->real = (sqlite3_file *)((char *)f + sizeof(*w));
|
|
280
|
+
w->wal = (flags & 0x80000) != 0;
|
|
281
|
+
int rc = sb_ltx_parent->xOpen(sb_ltx_parent, name, w->real, flags, out);
|
|
282
|
+
if (!rc)
|
|
283
|
+
w->base.pMethods = &sb_ltx_methods;
|
|
284
|
+
return rc;
|
|
285
|
+
}
|
|
286
|
+
static int sb_ltx_register(void) {
|
|
287
|
+
if (sb_ltx_parent)
|
|
288
|
+
return 0;
|
|
289
|
+
const char *n = getenv("SB_LTX_MAX_WAL_BYTES");
|
|
290
|
+
if (n && *n) {
|
|
291
|
+
char *end = 0;
|
|
292
|
+
unsigned long long value = strtoull(n, &end, 10);
|
|
293
|
+
if (*end || value < 131072 || value > 1073741824)
|
|
294
|
+
return -1;
|
|
295
|
+
sb_ltx_max_bytes = value;
|
|
296
|
+
}
|
|
297
|
+
sb_ltx_parent = sqlite3_vfs_find(0);
|
|
298
|
+
if (!sb_ltx_parent)
|
|
299
|
+
return -1;
|
|
300
|
+
sb_ltx_vfs = *sb_ltx_parent;
|
|
301
|
+
sb_ltx_vfs.zName = "sb-ltx-bounded";
|
|
302
|
+
sb_ltx_vfs.pNext = 0;
|
|
303
|
+
sb_ltx_vfs.szOsFile += (int)sizeof(sb_ltx_file);
|
|
304
|
+
sb_ltx_vfs.xOpen = sb_ltx_vopen;
|
|
305
|
+
return sqlite3_vfs_register(&sb_ltx_vfs, 0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
typedef struct {
|
|
309
|
+
sqlite3 *db;
|
|
310
|
+
char path[SB_LTX_PATH], root[SB_LTX_PATH], epoch[64];
|
|
311
|
+
int lock, failed, read_fd, checkpointed;
|
|
312
|
+
uint32_t page_size, commit, salt1, salt2, wal_c1, wal_c2;
|
|
313
|
+
uint64_t txid, offset, checksum;
|
|
314
|
+
} sb_ltx_state;
|
|
315
|
+
static sb_ltx_state sb_ltx_states[SB_MAX_DB];
|
|
316
|
+
static uint64_t sb_ltx_crc_table[256];
|
|
317
|
+
static int sb_ltx_crc_ready;
|
|
318
|
+
static uint32_t sb_ltx_be32(const unsigned char *p) {
|
|
319
|
+
return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3];
|
|
320
|
+
}
|
|
321
|
+
static void sb_ltx_put32(unsigned char *p, uint32_t v) {
|
|
322
|
+
for (int i = 3; i >= 0; i--) {
|
|
323
|
+
p[i] = (unsigned char)v;
|
|
324
|
+
v >>= 8;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
static void sb_ltx_put64(unsigned char *p, uint64_t v) {
|
|
328
|
+
for (int i = 7; i >= 0; i--) {
|
|
329
|
+
p[i] = (unsigned char)v;
|
|
330
|
+
v >>= 8;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
static uint64_t sb_ltx_crc(uint64_t crc, const void *buf, size_t n) {
|
|
334
|
+
if (!sb_ltx_crc_ready) {
|
|
335
|
+
for (int i = 0; i < 256; i++) {
|
|
336
|
+
uint64_t c = i;
|
|
337
|
+
for (int j = 0; j < 8; j++)
|
|
338
|
+
c = (c >> 1) ^ ((c & 1) ? UINT64_C(0xd800000000000000) : 0);
|
|
339
|
+
sb_ltx_crc_table[i] = c;
|
|
340
|
+
}
|
|
341
|
+
sb_ltx_crc_ready = 1;
|
|
342
|
+
}
|
|
343
|
+
const unsigned char *p = buf;
|
|
344
|
+
while (n--)
|
|
345
|
+
crc = sb_ltx_crc_table[(crc ^ *p++) & 255] ^ (crc >> 8);
|
|
346
|
+
return crc;
|
|
347
|
+
}
|
|
348
|
+
static uint64_t sb_ltx_page_crc(uint32_t pgno, const void *page, uint32_t size) {
|
|
349
|
+
unsigned char b[4];
|
|
350
|
+
sb_ltx_put32(b, pgno);
|
|
351
|
+
return SB_LTX_FLAG | ~sb_ltx_crc(sb_ltx_crc(UINT64_MAX, b, 4), page, size);
|
|
352
|
+
}
|
|
353
|
+
static int sb_ltx_read(int fd, void *buf, size_t n, uint64_t offset) {
|
|
354
|
+
size_t done = 0;
|
|
355
|
+
while (done < n) {
|
|
356
|
+
ssize_t r = pread(fd, (char *)buf + done, n - done, (off_t)(offset + done));
|
|
357
|
+
if (r < 0 && errno == EINTR)
|
|
358
|
+
continue;
|
|
359
|
+
if (r <= 0)
|
|
360
|
+
return -1;
|
|
361
|
+
done += (size_t)r;
|
|
362
|
+
}
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
static int sb_ltx_sync_dir(const char *path) {
|
|
366
|
+
int fd = open(path, O_RDONLY | O_DIRECTORY);
|
|
367
|
+
if (fd < 0)
|
|
368
|
+
return -1;
|
|
369
|
+
int rc = fsync(fd);
|
|
370
|
+
if (close(fd) && !rc)
|
|
371
|
+
rc = -1;
|
|
372
|
+
return rc;
|
|
373
|
+
}
|
|
374
|
+
static int sb_ltx_flush(FILE *f) { return fflush(f) || fsync(fileno(f)) ? -1 : 0; }
|
|
375
|
+
#ifdef SB_LTX_TESTING
|
|
376
|
+
static int sb_ltx_test_armed, sb_ltx_test_commits;
|
|
377
|
+
static void sb_ltx_phase(const char *phase) {
|
|
378
|
+
const char *p = getenv("SB_LTX_TEST_STOP");
|
|
379
|
+
if (sb_ltx_test_armed && p && strcmp(p, phase) == 0 &&
|
|
380
|
+
sb_ltx_test_commits ==
|
|
381
|
+
atoi(getenv("SB_LTX_TEST_COMMIT") ? getenv("SB_LTX_TEST_COMMIT") : "1"))
|
|
382
|
+
raise(SIGSTOP);
|
|
383
|
+
}
|
|
384
|
+
#else
|
|
385
|
+
static void sb_ltx_phase(const char *phase) { (void)phase; }
|
|
386
|
+
#endif
|
|
387
|
+
|
|
388
|
+
typedef struct {
|
|
389
|
+
FILE *out, *index;
|
|
390
|
+
uint64_t hash, n, index_n;
|
|
391
|
+
uint32_t page_size;
|
|
392
|
+
} sb_ltx_writer;
|
|
393
|
+
static int sb_ltx_write(sb_ltx_writer *w, const void *buf, size_t n, int hash) {
|
|
394
|
+
if (fwrite(buf, 1, n, w->out) != n)
|
|
395
|
+
return -1;
|
|
396
|
+
if (hash)
|
|
397
|
+
w->hash = sb_ltx_crc(w->hash, buf, n);
|
|
398
|
+
w->n += n;
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
static int sb_ltx_varint(FILE *f, uint64_t v, uint64_t *n) {
|
|
402
|
+
do {
|
|
403
|
+
unsigned char b = (unsigned char)(v & 127);
|
|
404
|
+
v >>= 7;
|
|
405
|
+
if (v)
|
|
406
|
+
b |= 128;
|
|
407
|
+
if (fwrite(&b, 1, 1, f) != 1)
|
|
408
|
+
return -1;
|
|
409
|
+
(*n)++;
|
|
410
|
+
} while (v);
|
|
411
|
+
return 0;
|
|
412
|
+
}
|
|
413
|
+
static int sb_ltx_begin(sb_ltx_writer *w, const char *path, sb_ltx_state *s, uint32_t commit,
|
|
414
|
+
int snapshot, uint64_t wal_size) {
|
|
415
|
+
memset(w, 0, sizeof(*w));
|
|
416
|
+
w->hash = UINT64_MAX;
|
|
417
|
+
w->page_size = s->page_size;
|
|
418
|
+
w->out = fopen(path, "wbx");
|
|
419
|
+
w->index = tmpfile();
|
|
420
|
+
if (!w->out || !w->index)
|
|
421
|
+
return -1;
|
|
422
|
+
unsigned char h[100] = {0};
|
|
423
|
+
memcpy(h, "LTX1", 4);
|
|
424
|
+
sb_ltx_put32(h + 8, s->page_size);
|
|
425
|
+
sb_ltx_put32(h + 12, commit);
|
|
426
|
+
sb_ltx_put64(h + 16, snapshot ? 1 : s->txid + 1);
|
|
427
|
+
sb_ltx_put64(h + 24, snapshot ? 1 : s->txid + 1);
|
|
428
|
+
sb_ltx_put64(h + 32, (uint64_t)time(0) * 1000);
|
|
429
|
+
sb_ltx_put64(h + 40, snapshot ? 0 : s->checksum);
|
|
430
|
+
if (!snapshot) {
|
|
431
|
+
sb_ltx_put64(h + 48, s->offset ? s->offset : 32);
|
|
432
|
+
sb_ltx_put64(h + 56, wal_size);
|
|
433
|
+
sb_ltx_put32(h + 64, s->salt1);
|
|
434
|
+
sb_ltx_put32(h + 68, s->salt2);
|
|
435
|
+
}
|
|
436
|
+
return sb_ltx_write(w, h, sizeof(h), 1);
|
|
437
|
+
}
|
|
438
|
+
// Literal-only LZ4 blocks: bounded, interoperable, no compression library.
|
|
439
|
+
// Compression can be added after crash qualification without changing the format.
|
|
440
|
+
static int sb_ltx_page(sb_ltx_writer *w, uint32_t pgno, const unsigned char *page) {
|
|
441
|
+
uint64_t offset = w->n;
|
|
442
|
+
unsigned char hdr[10] = {0};
|
|
443
|
+
sb_ltx_put32(hdr, pgno);
|
|
444
|
+
hdr[5] = 1;
|
|
445
|
+
uint32_t extra = (w->page_size - 15) / 255 + 1, size = 1 + extra + w->page_size;
|
|
446
|
+
sb_ltx_put32(hdr + 6, size);
|
|
447
|
+
if (sb_ltx_write(w, hdr, 10, 1))
|
|
448
|
+
return -1;
|
|
449
|
+
unsigned char literal = 0xf0;
|
|
450
|
+
if (sb_ltx_write(w, &literal, 1, 0))
|
|
451
|
+
return -1;
|
|
452
|
+
uint32_t remaining = w->page_size - 15;
|
|
453
|
+
while (remaining >= 255) {
|
|
454
|
+
literal = 255;
|
|
455
|
+
if (sb_ltx_write(w, &literal, 1, 0))
|
|
456
|
+
return -1;
|
|
457
|
+
remaining -= 255;
|
|
458
|
+
}
|
|
459
|
+
literal = (unsigned char)remaining;
|
|
460
|
+
if (sb_ltx_write(w, &literal, 1, 0) || sb_ltx_write(w, page, w->page_size, 0))
|
|
461
|
+
return -1;
|
|
462
|
+
w->hash = sb_ltx_crc(w->hash, page, w->page_size);
|
|
463
|
+
return sb_ltx_varint(w->index, pgno, &w->index_n) ||
|
|
464
|
+
sb_ltx_varint(w->index, offset, &w->index_n) ||
|
|
465
|
+
sb_ltx_varint(w->index, w->n - offset, &w->index_n)
|
|
466
|
+
? -1
|
|
467
|
+
: 0;
|
|
468
|
+
}
|
|
469
|
+
static int sb_ltx_finish(sb_ltx_writer *w, uint64_t checksum) {
|
|
470
|
+
unsigned char zero[6] = {0}, buf[65536];
|
|
471
|
+
if (sb_ltx_write(w, zero, 6, 1) || fflush(w->index))
|
|
472
|
+
return -1;
|
|
473
|
+
rewind(w->index);
|
|
474
|
+
size_t n;
|
|
475
|
+
while ((n = fread(buf, 1, sizeof(buf), w->index)))
|
|
476
|
+
if (sb_ltx_write(w, buf, n, 1))
|
|
477
|
+
return -1;
|
|
478
|
+
if (ferror(w->index) || sb_ltx_write(w, zero, 1, 1))
|
|
479
|
+
return -1;
|
|
480
|
+
sb_ltx_put64(buf, w->index_n + 1);
|
|
481
|
+
if (sb_ltx_write(w, buf, 8, 1))
|
|
482
|
+
return -1;
|
|
483
|
+
sb_ltx_put64(buf, checksum);
|
|
484
|
+
if (sb_ltx_write(w, buf, 8, 1))
|
|
485
|
+
return -1;
|
|
486
|
+
sb_ltx_put64(buf, SB_LTX_FLAG | ~w->hash);
|
|
487
|
+
if (sb_ltx_write(w, buf, 8, 0))
|
|
488
|
+
return -1;
|
|
489
|
+
sb_ltx_phase("ltx-written");
|
|
490
|
+
if (sb_ltx_flush(w->out))
|
|
491
|
+
return -1;
|
|
492
|
+
sb_ltx_phase("ltx-fsynced");
|
|
493
|
+
return 0;
|
|
494
|
+
}
|
|
495
|
+
static void sb_ltx_writer_close(sb_ltx_writer *w) {
|
|
496
|
+
if (w->out)
|
|
497
|
+
fclose(w->out);
|
|
498
|
+
if (w->index)
|
|
499
|
+
fclose(w->index);
|
|
500
|
+
}
|
|
501
|
+
static int sb_ltx_names(sb_ltx_state *s, uint64_t txid, char *tmp, char *final, char *dir) {
|
|
502
|
+
if (snprintf(dir, SB_LTX_PATH, "%s/%s", s->root, s->epoch) >= SB_LTX_PATH)
|
|
503
|
+
return -1;
|
|
504
|
+
if (snprintf(final, SB_LTX_PATH, "%s/%016llx-%016llx.ltx", dir, (unsigned long long)txid,
|
|
505
|
+
(unsigned long long)txid) >= SB_LTX_PATH)
|
|
506
|
+
return -1;
|
|
507
|
+
return snprintf(tmp, SB_LTX_PATH, "%s.tmp", final) >= SB_LTX_PATH ? -1 : 0;
|
|
508
|
+
}
|
|
509
|
+
static int sb_ltx_publish(const char *tmp, const char *final, const char *dir) {
|
|
510
|
+
// Never replace a finished file, including an orphan ahead of position.
|
|
511
|
+
if (link(tmp, final))
|
|
512
|
+
return -1; // Atomic no-replace publication on the same filesystem.
|
|
513
|
+
if (unlink(tmp) || sb_ltx_sync_dir(dir))
|
|
514
|
+
return -1;
|
|
515
|
+
sb_ltx_phase("ltx-published");
|
|
516
|
+
return 0;
|
|
517
|
+
}
|
|
518
|
+
static int sb_ltx_position(sb_ltx_state *s) {
|
|
519
|
+
char path[SB_LTX_PATH], tmp[SB_LTX_PATH], body[512];
|
|
520
|
+
if (snprintf(path, sizeof(path), "%s/position", s->root) >= sizeof(path) ||
|
|
521
|
+
snprintf(tmp, sizeof(tmp), "%s/position.tmp", s->root) >= sizeof(tmp))
|
|
522
|
+
return -1;
|
|
523
|
+
int n = snprintf(body, sizeof(body), "LTXPOS2\n%s\n%c %llu %u %u %llu %016llx %u %u %u %u\n",
|
|
524
|
+
s->epoch, s->checkpointed ? 'C' : 'W', (unsigned long long)s->txid, s->salt1,
|
|
525
|
+
s->salt2, (unsigned long long)s->offset, (unsigned long long)s->checksum,
|
|
526
|
+
s->wal_c1, s->wal_c2, s->page_size, s->commit);
|
|
527
|
+
if (n < 0 || n >= (int)sizeof(body))
|
|
528
|
+
return -1;
|
|
529
|
+
FILE *f = fopen(tmp, "wb");
|
|
530
|
+
if (!f)
|
|
531
|
+
return -1;
|
|
532
|
+
int rc = fwrite(body, 1, n, f) != (size_t)n ||
|
|
533
|
+
fprintf(f, "%016llx\n", (unsigned long long)~sb_ltx_crc(UINT64_MAX, body, n)) < 0 ||
|
|
534
|
+
sb_ltx_flush(f);
|
|
535
|
+
if (fclose(f))
|
|
536
|
+
rc = -1;
|
|
537
|
+
if (rc)
|
|
538
|
+
return -1;
|
|
539
|
+
sb_ltx_phase(s->checkpointed ? "checkpoint-record-fsynced" : "position-fsynced");
|
|
540
|
+
if (rename(tmp, path) || sb_ltx_sync_dir(s->root))
|
|
541
|
+
return -1;
|
|
542
|
+
sb_ltx_phase(s->checkpointed ? "checkpoint-record-published" : "position-published");
|
|
543
|
+
return 0;
|
|
544
|
+
}
|
|
545
|
+
static int sb_ltx_checkpoint(sb_ltx_state *s, int persist) {
|
|
546
|
+
int a, b;
|
|
547
|
+
sb_ltx_phase("before-checkpoint");
|
|
548
|
+
int rc = sqlite3_wal_checkpoint_v2(s->db, "main", 3, &a, &b);
|
|
549
|
+
if (!rc) {
|
|
550
|
+
s->offset = 0;
|
|
551
|
+
s->salt1 = s->salt2 = s->wal_c1 = s->wal_c2 = 0;
|
|
552
|
+
}
|
|
553
|
+
sb_ltx_phase("after-checkpoint");
|
|
554
|
+
// The WAL record remains authoritative until checkpoint has completed. A
|
|
555
|
+
// crash before this second atomic record write retains conservative recovery.
|
|
556
|
+
if (!rc && persist && !s->checkpointed) {
|
|
557
|
+
s->checkpointed = 1;
|
|
558
|
+
rc = sb_ltx_position(s);
|
|
559
|
+
}
|
|
560
|
+
return rc;
|
|
561
|
+
}
|
|
562
|
+
static int sb_ltx_snapshot(sb_ltx_state *s) {
|
|
563
|
+
if (sb_ltx_checkpoint(s, 0))
|
|
564
|
+
return -1;
|
|
565
|
+
// EXCLUSIVE SQLite locking mode excludes other connections. BEGIN pins the
|
|
566
|
+
// checkpointed live pages while they are read; no VACUUM or page rewriting.
|
|
567
|
+
if (sqlite3_exec(s->db, "BEGIN", 0, 0, 0))
|
|
568
|
+
return -1;
|
|
569
|
+
int fd = s->read_fd;
|
|
570
|
+
unsigned char head[100];
|
|
571
|
+
struct stat st;
|
|
572
|
+
int rc = -1;
|
|
573
|
+
if (fd < 0 || fstat(fd, &st) || sb_ltx_read(fd, head, 100, 0) ||
|
|
574
|
+
memcmp(head, "SQLite format 3", 15))
|
|
575
|
+
goto done;
|
|
576
|
+
s->page_size = ((uint32_t)head[16] << 8) | head[17];
|
|
577
|
+
if (s->page_size == 1)
|
|
578
|
+
s->page_size = 65536;
|
|
579
|
+
if (s->page_size < 512 || s->page_size > 65536 || (s->page_size & (s->page_size - 1)) ||
|
|
580
|
+
st.st_size % s->page_size)
|
|
581
|
+
goto done;
|
|
582
|
+
s->commit = (uint32_t)(st.st_size / s->page_size);
|
|
583
|
+
s->txid = 1;
|
|
584
|
+
s->checksum = SB_LTX_FLAG;
|
|
585
|
+
char dir[SB_LTX_PATH], tmp[SB_LTX_PATH], final[SB_LTX_PATH];
|
|
586
|
+
if (snprintf(dir, sizeof(dir), "%s/epoch-XXXXXX", s->root) >= sizeof(dir) || !mkdtemp(dir))
|
|
587
|
+
goto done;
|
|
588
|
+
snprintf(s->epoch, sizeof(s->epoch), "%s", strrchr(dir, '/') + 1);
|
|
589
|
+
if (sb_ltx_sync_dir(s->root) || sb_ltx_names(s, 1, tmp, final, dir))
|
|
590
|
+
goto done;
|
|
591
|
+
unsigned char *page = malloc(s->page_size);
|
|
592
|
+
if (!page)
|
|
593
|
+
goto done;
|
|
594
|
+
sb_ltx_writer w;
|
|
595
|
+
rc = sb_ltx_begin(&w, tmp, s, s->commit, 1, 0);
|
|
596
|
+
for (uint32_t p = 1; !rc && p <= s->commit; p++) {
|
|
597
|
+
if (p == (0x40000000u / s->page_size) + 1)
|
|
598
|
+
continue;
|
|
599
|
+
if (sb_ltx_read(fd, page, s->page_size, (uint64_t)(p - 1) * s->page_size) ||
|
|
600
|
+
sb_ltx_page(&w, p, page)) {
|
|
601
|
+
rc = -1;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
s->checksum = SB_LTX_FLAG | (s->checksum ^ sb_ltx_page_crc(p, page, s->page_size));
|
|
605
|
+
}
|
|
606
|
+
free(page);
|
|
607
|
+
if (!rc)
|
|
608
|
+
rc = sb_ltx_finish(&w, s->checksum);
|
|
609
|
+
sb_ltx_writer_close(&w);
|
|
610
|
+
if (!rc)
|
|
611
|
+
rc = sb_ltx_publish(tmp, final, dir);
|
|
612
|
+
if (!rc) {
|
|
613
|
+
s->checkpointed = 1;
|
|
614
|
+
rc = sb_ltx_position(s);
|
|
615
|
+
}
|
|
616
|
+
done:
|
|
617
|
+
if (sqlite3_exec(s->db, "COMMIT", 0, 0, 0))
|
|
618
|
+
rc = -1;
|
|
619
|
+
return rc;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
typedef struct {
|
|
623
|
+
uint32_t pgno, frame;
|
|
624
|
+
} sb_ltx_frame;
|
|
625
|
+
static int sb_ltx_frame_cmp(const void *a, const void *b) {
|
|
626
|
+
const sb_ltx_frame *x = a, *y = b;
|
|
627
|
+
return x->pgno < y->pgno ? -1
|
|
628
|
+
: x->pgno > y->pgno ? 1
|
|
629
|
+
: x->frame < y->frame ? -1
|
|
630
|
+
: x->frame > y->frame;
|
|
631
|
+
}
|
|
632
|
+
static void sb_ltx_wal_checksum(const unsigned char *p, size_t n, int big, uint32_t *a,
|
|
633
|
+
uint32_t *b) {
|
|
634
|
+
for (size_t i = 0; i < n; i += 8) {
|
|
635
|
+
uint32_t x, y;
|
|
636
|
+
if (big) {
|
|
637
|
+
x = sb_ltx_be32(p + i);
|
|
638
|
+
y = sb_ltx_be32(p + i + 4);
|
|
639
|
+
} else {
|
|
640
|
+
x = (uint32_t)p[i] | (uint32_t)p[i + 1] << 8 | (uint32_t)p[i + 2] << 16 |
|
|
641
|
+
(uint32_t)p[i + 3] << 24;
|
|
642
|
+
y = (uint32_t)p[i + 4] | (uint32_t)p[i + 5] << 8 | (uint32_t)p[i + 6] << 16 |
|
|
643
|
+
(uint32_t)p[i + 7] << 24;
|
|
644
|
+
}
|
|
645
|
+
*a += x + *b;
|
|
646
|
+
*b += y + *a;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
static int sb_ltx_capture(sb_ltx_state *s, uint32_t frames) {
|
|
650
|
+
char path[SB_LTX_PATH];
|
|
651
|
+
if (snprintf(path, sizeof(path), "%s-wal", s->path) >= sizeof(path))
|
|
652
|
+
return -1;
|
|
653
|
+
int wal = open(path, O_RDONLY), db = s->read_fd;
|
|
654
|
+
unsigned char head[32], fh[24];
|
|
655
|
+
unsigned char *page = 0;
|
|
656
|
+
sb_ltx_frame *map = 0;
|
|
657
|
+
int rc = -1;
|
|
658
|
+
if (wal < 0 || db < 0 || sb_ltx_read(wal, head, 32, 0))
|
|
659
|
+
goto done;
|
|
660
|
+
uint32_t magic = sb_ltx_be32(head), ps = sb_ltx_be32(head + 8), salt1 = sb_ltx_be32(head + 16),
|
|
661
|
+
salt2 = sb_ltx_be32(head + 20), a = 0, b = 0;
|
|
662
|
+
if ((magic != 0x377f0682 && magic != 0x377f0683) || sb_ltx_be32(head + 4) != 3007000 ||
|
|
663
|
+
ps != s->page_size || !frames || 32 + (uint64_t)frames * (ps + 24) > sb_ltx_max_bytes)
|
|
664
|
+
goto done;
|
|
665
|
+
sb_ltx_wal_checksum(head, 24, magic & 1, &a, &b);
|
|
666
|
+
if (a != sb_ltx_be32(head + 24) || b != sb_ltx_be32(head + 28))
|
|
667
|
+
goto done;
|
|
668
|
+
uint32_t prior = s->offset ? (uint32_t)((s->offset - 32) / (ps + 24)) : 0;
|
|
669
|
+
if (s->offset && (s->salt1 != salt1 || s->salt2 != salt2 || prior > frames))
|
|
670
|
+
goto done;
|
|
671
|
+
page = malloc(ps);
|
|
672
|
+
map = malloc((size_t)frames * sizeof(*map));
|
|
673
|
+
if (!page || !map)
|
|
674
|
+
goto done;
|
|
675
|
+
uint32_t commit = 0;
|
|
676
|
+
for (uint32_t f = 1; f <= frames; f++) {
|
|
677
|
+
uint64_t off = 32 + (uint64_t)(f - 1) * (ps + 24);
|
|
678
|
+
if (sb_ltx_read(wal, fh, 24, off) || sb_ltx_read(wal, page, ps, off + 24) ||
|
|
679
|
+
sb_ltx_be32(fh + 8) != salt1 || sb_ltx_be32(fh + 12) != salt2 || !sb_ltx_be32(fh))
|
|
680
|
+
goto done;
|
|
681
|
+
sb_ltx_wal_checksum(fh, 8, magic & 1, &a, &b);
|
|
682
|
+
sb_ltx_wal_checksum(page, ps, magic & 1, &a, &b);
|
|
683
|
+
if (a != sb_ltx_be32(fh + 16) || b != sb_ltx_be32(fh + 20))
|
|
684
|
+
goto done;
|
|
685
|
+
if (f == prior && (a != s->wal_c1 || b != s->wal_c2))
|
|
686
|
+
goto done;
|
|
687
|
+
map[f - 1] = (sb_ltx_frame){sb_ltx_be32(fh), f};
|
|
688
|
+
commit = sb_ltx_be32(fh + 4);
|
|
689
|
+
}
|
|
690
|
+
if (!commit)
|
|
691
|
+
goto done;
|
|
692
|
+
if (frames == prior) {
|
|
693
|
+
rc = 0;
|
|
694
|
+
goto done;
|
|
695
|
+
}
|
|
696
|
+
qsort(map, frames, sizeof(*map), sb_ltx_frame_cmp);
|
|
697
|
+
uint64_t checksum = s->checksum;
|
|
698
|
+
unsigned char *old = malloc(ps);
|
|
699
|
+
if (!old)
|
|
700
|
+
goto done;
|
|
701
|
+
// Remove truncated old pages, reading the last captured WAL version if present.
|
|
702
|
+
for (uint32_t p = commit + 1; p && p <= s->commit; p++) {
|
|
703
|
+
if (p == (0x40000000u / ps) + 1)
|
|
704
|
+
continue;
|
|
705
|
+
uint32_t prev = 0;
|
|
706
|
+
for (uint32_t i = 0; i < frames; i++)
|
|
707
|
+
if (map[i].pgno == p && map[i].frame <= prior)
|
|
708
|
+
prev = map[i].frame;
|
|
709
|
+
if (sb_ltx_read(prev ? wal : db, old, ps,
|
|
710
|
+
prev ? 32 + (uint64_t)(prev - 1) * (ps + 24) + 24 : (uint64_t)(p - 1) * ps)) {
|
|
711
|
+
free(old);
|
|
712
|
+
goto done;
|
|
713
|
+
}
|
|
714
|
+
checksum = SB_LTX_FLAG | (checksum ^ sb_ltx_page_crc(p, old, ps));
|
|
715
|
+
}
|
|
716
|
+
char tmp[SB_LTX_PATH], final[SB_LTX_PATH], dir[SB_LTX_PATH];
|
|
717
|
+
if (sb_ltx_names(s, s->txid + 1, tmp, final, dir)) {
|
|
718
|
+
free(old);
|
|
719
|
+
goto done;
|
|
720
|
+
}
|
|
721
|
+
s->salt1 = salt1;
|
|
722
|
+
s->salt2 = salt2;
|
|
723
|
+
sb_ltx_writer w;
|
|
724
|
+
rc = sb_ltx_begin(&w, tmp, s, commit, 0, (uint64_t)(frames - prior) * (ps + 24));
|
|
725
|
+
for (uint32_t i = 0; !rc && i < frames;) {
|
|
726
|
+
uint32_t p = map[i].pgno, latest = 0, prev = 0;
|
|
727
|
+
do {
|
|
728
|
+
if (map[i].frame <= prior)
|
|
729
|
+
prev = map[i].frame;
|
|
730
|
+
else
|
|
731
|
+
latest = map[i].frame;
|
|
732
|
+
i++;
|
|
733
|
+
} while (i < frames && map[i].pgno == p);
|
|
734
|
+
if (!latest || p > commit || p == (0x40000000u / ps) + 1)
|
|
735
|
+
continue;
|
|
736
|
+
if (p <= s->commit) {
|
|
737
|
+
if (sb_ltx_read(prev ? wal : db, old, ps,
|
|
738
|
+
prev ? 32 + (uint64_t)(prev - 1) * (ps + 24) + 24 : (uint64_t)(p - 1) * ps)) {
|
|
739
|
+
rc = -1;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
checksum = SB_LTX_FLAG | (checksum ^ sb_ltx_page_crc(p, old, ps));
|
|
743
|
+
}
|
|
744
|
+
if (sb_ltx_read(wal, page, ps, 32 + (uint64_t)(latest - 1) * (ps + 24) + 24) ||
|
|
745
|
+
sb_ltx_page(&w, p, page)) {
|
|
746
|
+
rc = -1;
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
checksum = SB_LTX_FLAG | (checksum ^ sb_ltx_page_crc(p, page, ps));
|
|
750
|
+
}
|
|
751
|
+
free(old);
|
|
752
|
+
if (!rc)
|
|
753
|
+
rc = sb_ltx_finish(&w, checksum);
|
|
754
|
+
sb_ltx_writer_close(&w);
|
|
755
|
+
if (!rc)
|
|
756
|
+
rc = sb_ltx_publish(tmp, final, dir);
|
|
757
|
+
if (!rc) {
|
|
758
|
+
s->checkpointed = 0;
|
|
759
|
+
s->txid++;
|
|
760
|
+
s->commit = commit;
|
|
761
|
+
s->offset = 32 + (uint64_t)frames * (ps + 24);
|
|
762
|
+
s->checksum = checksum;
|
|
763
|
+
s->wal_c1 = a;
|
|
764
|
+
s->wal_c2 = b;
|
|
765
|
+
rc = sb_ltx_position(s);
|
|
766
|
+
}
|
|
767
|
+
done:
|
|
768
|
+
if (wal >= 0)
|
|
769
|
+
close(wal);
|
|
770
|
+
free(page);
|
|
771
|
+
free(map);
|
|
772
|
+
return rc;
|
|
773
|
+
}
|
|
774
|
+
static int sb_ltx_hook(void *ctx, sqlite3 *db, const char *name, int frames) {
|
|
775
|
+
(void)db;
|
|
776
|
+
sb_ltx_state *s = ctx;
|
|
777
|
+
#ifdef SB_LTX_TESTING
|
|
778
|
+
sb_ltx_test_commits++;
|
|
779
|
+
#endif
|
|
780
|
+
sb_ltx_phase("wal-committed");
|
|
781
|
+
if (s->failed || strcmp(name, "main") || sb_ltx_capture(s, (uint32_t)frames)) {
|
|
782
|
+
s->failed = 1;
|
|
783
|
+
fprintf(stderr, "LTX capture failed for %s, restart required\n", s->path);
|
|
784
|
+
return 10;
|
|
785
|
+
}
|
|
786
|
+
// Checkpoint after the statement resets, so a subsequent transaction cannot
|
|
787
|
+
// reset the WAL salts before the in-memory cursor has been cleared.
|
|
788
|
+
return 0;
|
|
789
|
+
}
|
|
790
|
+
static int sb_ltx_authorize(void *ctx, int action, const char *a, const char *b, const char *c,
|
|
791
|
+
const char *d) {
|
|
792
|
+
(void)ctx;
|
|
793
|
+
(void)b;
|
|
794
|
+
(void)c;
|
|
795
|
+
(void)d;
|
|
796
|
+
if (action == 24 || action == 25)
|
|
797
|
+
return 1; // ATTACH / DETACH would escape capture.
|
|
798
|
+
if (action == 19 && a &&
|
|
799
|
+
(!strcasecmp(a, "wal_checkpoint") || !strcasecmp(a, "wal_autocheckpoint") ||
|
|
800
|
+
!strcasecmp(a, "journal_mode") || !strcasecmp(a, "locking_mode") ||
|
|
801
|
+
!strcasecmp(a, "synchronous") || !strcasecmp(a, "page_size") || !strcasecmp(a, "mmap_size")))
|
|
802
|
+
return 1;
|
|
803
|
+
return 0;
|
|
804
|
+
}
|
|
805
|
+
static int sb_ltx_after(int idx) {
|
|
806
|
+
sb_ltx_state *s = &sb_ltx_states[idx];
|
|
807
|
+
if (!s->db)
|
|
808
|
+
return 0;
|
|
809
|
+
if (s->failed)
|
|
810
|
+
return -1;
|
|
811
|
+
// SQLITE_BUSY can occur with an explicit transaction holding a read lock.
|
|
812
|
+
// Keep the validated WAL and position; the VFS still imposes the hard ceiling.
|
|
813
|
+
int rc = sb_ltx_checkpoint(s, 1);
|
|
814
|
+
if (rc && rc != 5 && rc != 6) {
|
|
815
|
+
s->failed = 1;
|
|
816
|
+
return -1;
|
|
817
|
+
}
|
|
818
|
+
return 0;
|
|
819
|
+
}
|
|
820
|
+
// Verify our literal-block file before trusting a position. Reads stay bounded
|
|
821
|
+
// even for a large base; CRC covers the logical uncompressed LTX contents.
|
|
822
|
+
static int sb_ltx_verify_last(sb_ltx_state *s) {
|
|
823
|
+
char tmp[SB_LTX_PATH], final[SB_LTX_PATH], dir[SB_LTX_PATH];
|
|
824
|
+
if (sb_ltx_names(s, s->txid, tmp, final, dir))
|
|
825
|
+
return 0;
|
|
826
|
+
int fd = open(final, O_RDONLY);
|
|
827
|
+
struct stat st;
|
|
828
|
+
unsigned char h[100], fh[10], tail[24];
|
|
829
|
+
unsigned char *page = malloc(65536);
|
|
830
|
+
int valid = 0;
|
|
831
|
+
if (fd < 0 || !page || fstat(fd, &st) || st.st_size < 131 || sb_ltx_read(fd, h, 100, 0) ||
|
|
832
|
+
sb_ltx_read(fd, tail, 24, (uint64_t)st.st_size - 24))
|
|
833
|
+
goto done;
|
|
834
|
+
uint64_t min = 0, max = 0, post = 0, file_crc = 0, index_size = 0;
|
|
835
|
+
for (int i = 0; i < 8; i++) {
|
|
836
|
+
min = (min << 8) | h[16 + i];
|
|
837
|
+
max = (max << 8) | h[24 + i];
|
|
838
|
+
post = (post << 8) | tail[8 + i];
|
|
839
|
+
file_crc = (file_crc << 8) | tail[16 + i];
|
|
840
|
+
index_size = (index_size << 8) | tail[i];
|
|
841
|
+
}
|
|
842
|
+
if (memcmp(h, "LTX1", 4) || sb_ltx_be32(h + 4) || sb_ltx_be32(h + 8) != s->page_size ||
|
|
843
|
+
sb_ltx_be32(h + 12) != s->commit || min != s->txid || max != s->txid || post != s->checksum)
|
|
844
|
+
goto done;
|
|
845
|
+
uint64_t offset = 100, hash = sb_ltx_crc(UINT64_MAX, h, 100);
|
|
846
|
+
uint32_t prev = 0;
|
|
847
|
+
for (;;) {
|
|
848
|
+
if (sb_ltx_read(fd, fh, 6, offset))
|
|
849
|
+
goto done;
|
|
850
|
+
hash = sb_ltx_crc(hash, fh, 6);
|
|
851
|
+
offset += 6;
|
|
852
|
+
uint32_t pgno = sb_ltx_be32(fh);
|
|
853
|
+
if (!pgno) {
|
|
854
|
+
if (fh[4] || fh[5])
|
|
855
|
+
goto done;
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
if (pgno <= prev || pgno > s->commit || fh[4] || fh[5] != 1)
|
|
859
|
+
goto done;
|
|
860
|
+
prev = pgno;
|
|
861
|
+
if (sb_ltx_read(fd, fh, 4, offset))
|
|
862
|
+
goto done;
|
|
863
|
+
hash = sb_ltx_crc(hash, fh, 4);
|
|
864
|
+
offset += 4;
|
|
865
|
+
uint32_t size = sb_ltx_be32(fh), literal = 15;
|
|
866
|
+
uint64_t end = offset + size;
|
|
867
|
+
unsigned char c;
|
|
868
|
+
if (size > s->page_size + s->page_size / 255 + 16 || sb_ltx_read(fd, &c, 1, offset++) ||
|
|
869
|
+
c != 0xf0)
|
|
870
|
+
goto done;
|
|
871
|
+
do {
|
|
872
|
+
if (sb_ltx_read(fd, &c, 1, offset++))
|
|
873
|
+
goto done;
|
|
874
|
+
literal += c;
|
|
875
|
+
if (literal > s->page_size)
|
|
876
|
+
goto done;
|
|
877
|
+
} while (c == 255);
|
|
878
|
+
if (literal != s->page_size || offset + s->page_size != end ||
|
|
879
|
+
sb_ltx_read(fd, page, s->page_size, offset))
|
|
880
|
+
goto done;
|
|
881
|
+
hash = sb_ltx_crc(hash, page, s->page_size);
|
|
882
|
+
offset = end;
|
|
883
|
+
}
|
|
884
|
+
if (index_size > (uint64_t)st.st_size || offset + index_size + 24 != (uint64_t)st.st_size)
|
|
885
|
+
goto done;
|
|
886
|
+
// Index bytes, its size field and post-apply checksum are all hashed.
|
|
887
|
+
while (offset < (uint64_t)st.st_size - 8) {
|
|
888
|
+
size_t n = (uint64_t)st.st_size - 8 - offset;
|
|
889
|
+
if (n > 65536)
|
|
890
|
+
n = 65536;
|
|
891
|
+
if (sb_ltx_read(fd, page, n, offset))
|
|
892
|
+
goto done;
|
|
893
|
+
hash = sb_ltx_crc(hash, page, n);
|
|
894
|
+
offset += n;
|
|
895
|
+
}
|
|
896
|
+
valid = (SB_LTX_FLAG | ~hash) == file_crc;
|
|
897
|
+
done:
|
|
898
|
+
if (fd >= 0)
|
|
899
|
+
close(fd);
|
|
900
|
+
free(page);
|
|
901
|
+
return valid;
|
|
902
|
+
}
|
|
903
|
+
static int sb_ltx_load_position(sb_ltx_state *s) {
|
|
904
|
+
char path[SB_LTX_PATH], text[768];
|
|
905
|
+
if (snprintf(path, sizeof(path), "%s/position", s->root) >= sizeof(path))
|
|
906
|
+
return 0;
|
|
907
|
+
FILE *f = fopen(path, "rb");
|
|
908
|
+
if (!f)
|
|
909
|
+
return 0;
|
|
910
|
+
size_t n = fread(text, 1, sizeof(text) - 1, f);
|
|
911
|
+
int bad = ferror(f) || !feof(f);
|
|
912
|
+
fclose(f);
|
|
913
|
+
if (bad)
|
|
914
|
+
return 0;
|
|
915
|
+
text[n] = 0;
|
|
916
|
+
sb_ltx_state candidate = *s;
|
|
917
|
+
unsigned long long txid, offset, checksum, digest;
|
|
918
|
+
char mode;
|
|
919
|
+
int used = 0, tail = 0;
|
|
920
|
+
if (sscanf(text, "LTXPOS2\n%63s\n%c %llu %u %u %llu %16llx %u %u %u %u\n%n", candidate.epoch,
|
|
921
|
+
&mode, &txid, &candidate.salt1, &candidate.salt2, &offset, &checksum,
|
|
922
|
+
&candidate.wal_c1, &candidate.wal_c2, &candidate.page_size, &candidate.commit,
|
|
923
|
+
&used) != 11)
|
|
924
|
+
return 0;
|
|
925
|
+
if (!used || sscanf(text + used, "%16llx\n%n", &digest, &tail) != 1 ||
|
|
926
|
+
(size_t)(used + tail) != n || digest != ~sb_ltx_crc(UINT64_MAX, text, (size_t)used))
|
|
927
|
+
return 0;
|
|
928
|
+
if (strncmp(candidate.epoch, "epoch-", 6) || strlen(candidate.epoch) != 12 ||
|
|
929
|
+
strspn(candidate.epoch, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") !=
|
|
930
|
+
12 ||
|
|
931
|
+
!txid || txid == UINT64_MAX || !(checksum & SB_LTX_FLAG) || !candidate.commit)
|
|
932
|
+
return 0;
|
|
933
|
+
uint32_t ps = candidate.page_size;
|
|
934
|
+
if (ps < 512 || ps > 65536 || (ps & (ps - 1)))
|
|
935
|
+
return 0;
|
|
936
|
+
if (mode == 'C') {
|
|
937
|
+
if (offset || candidate.salt1 || candidate.salt2 || candidate.wal_c1 || candidate.wal_c2)
|
|
938
|
+
return 0;
|
|
939
|
+
} else if (mode != 'W' || offset < 32 || offset > sb_ltx_max_bytes || (offset - 32) % (ps + 24))
|
|
940
|
+
return 0;
|
|
941
|
+
candidate.checkpointed = mode == 'C';
|
|
942
|
+
candidate.txid = txid;
|
|
943
|
+
candidate.offset = offset;
|
|
944
|
+
candidate.checksum = checksum;
|
|
945
|
+
if (!sb_ltx_verify_last(&candidate))
|
|
946
|
+
return 0;
|
|
947
|
+
// A published orphan ahead of the record is never overwritten. A new base
|
|
948
|
+
// captures the recovered source and preserves the old generation for restore.
|
|
949
|
+
char tmp[SB_LTX_PATH], final[SB_LTX_PATH], dir[SB_LTX_PATH];
|
|
950
|
+
if (sb_ltx_names(&candidate, txid + 1, tmp, final, dir) || access(final, F_OK) == 0)
|
|
951
|
+
return 0;
|
|
952
|
+
*s = candidate;
|
|
953
|
+
return 1;
|
|
954
|
+
}
|
|
955
|
+
// A clean marker means the checkpointed file is the entire captured state.
|
|
956
|
+
// Verify its full page checksum before trusting an absent/empty WAL. Opening no
|
|
957
|
+
// additional database fd preserves SQLite's process-level POSIX locks.
|
|
958
|
+
static int sb_ltx_verify_database(sb_ltx_state *s) {
|
|
959
|
+
struct stat st;
|
|
960
|
+
if (fstat(s->read_fd, &st) || (uint64_t)st.st_size != (uint64_t)s->commit * s->page_size)
|
|
961
|
+
return 0;
|
|
962
|
+
unsigned char *page = malloc(s->page_size);
|
|
963
|
+
if (!page)
|
|
964
|
+
return 0;
|
|
965
|
+
uint64_t checksum = SB_LTX_FLAG;
|
|
966
|
+
int valid = 1;
|
|
967
|
+
for (uint32_t p = 1; p <= s->commit; p++) {
|
|
968
|
+
if (p == (0x40000000u / s->page_size) + 1)
|
|
969
|
+
continue;
|
|
970
|
+
if (sb_ltx_read(s->read_fd, page, s->page_size, (uint64_t)(p - 1) * s->page_size)) {
|
|
971
|
+
valid = 0;
|
|
972
|
+
break;
|
|
973
|
+
}
|
|
974
|
+
checksum = SB_LTX_FLAG | (checksum ^ sb_ltx_page_crc(p, page, s->page_size));
|
|
975
|
+
}
|
|
976
|
+
free(page);
|
|
977
|
+
return valid && checksum == s->checksum;
|
|
978
|
+
}
|
|
979
|
+
static int sb_ltx_resume(sb_ltx_state *s) {
|
|
980
|
+
if (!sb_ltx_load_position(s))
|
|
981
|
+
return 0;
|
|
982
|
+
char path[SB_LTX_PATH];
|
|
983
|
+
if (snprintf(path, sizeof(path), "%s-wal", s->path) >= sizeof(path))
|
|
984
|
+
return 0;
|
|
985
|
+
if (s->checkpointed) {
|
|
986
|
+
struct stat wal;
|
|
987
|
+
if (stat(path, &wal) == 0) {
|
|
988
|
+
if (wal.st_size != 0)
|
|
989
|
+
return 0;
|
|
990
|
+
} else if (errno != ENOENT)
|
|
991
|
+
return 0;
|
|
992
|
+
return sb_ltx_verify_database(s);
|
|
993
|
+
}
|
|
994
|
+
int fd = open(path, O_RDONLY);
|
|
995
|
+
unsigned char h[32], fh[24];
|
|
996
|
+
unsigned char *page = malloc(s->page_size);
|
|
997
|
+
struct stat st;
|
|
998
|
+
int valid = 0;
|
|
999
|
+
uint32_t last = 0;
|
|
1000
|
+
if (fd < 0 || !page || fstat(fd, &st) || st.st_size < (off_t)s->offset ||
|
|
1001
|
+
st.st_size > (off_t)sb_ltx_max_bytes || sb_ltx_read(fd, h, 32, 0))
|
|
1002
|
+
goto done;
|
|
1003
|
+
uint32_t magic = sb_ltx_be32(h), a = 0, b = 0;
|
|
1004
|
+
if ((magic != 0x377f0682 && magic != 0x377f0683) || sb_ltx_be32(h + 4) != 3007000 ||
|
|
1005
|
+
sb_ltx_be32(h + 8) != s->page_size || sb_ltx_be32(h + 16) != s->salt1 ||
|
|
1006
|
+
sb_ltx_be32(h + 20) != s->salt2)
|
|
1007
|
+
goto done;
|
|
1008
|
+
sb_ltx_wal_checksum(h, 24, magic & 1, &a, &b);
|
|
1009
|
+
if (a != sb_ltx_be32(h + 24) || b != sb_ltx_be32(h + 28))
|
|
1010
|
+
goto done;
|
|
1011
|
+
uint32_t prior = (uint32_t)((s->offset - 32) / (s->page_size + 24));
|
|
1012
|
+
uint32_t count = (uint32_t)(((uint64_t)st.st_size - 32) / (s->page_size + 24));
|
|
1013
|
+
for (uint32_t i = 1; i <= count; i++) {
|
|
1014
|
+
uint64_t off = 32 + (uint64_t)(i - 1) * (s->page_size + 24);
|
|
1015
|
+
if (sb_ltx_read(fd, fh, 24, off) || sb_ltx_read(fd, page, s->page_size, off + 24) ||
|
|
1016
|
+
!sb_ltx_be32(fh) || sb_ltx_be32(fh + 8) != s->salt1 || sb_ltx_be32(fh + 12) != s->salt2)
|
|
1017
|
+
break;
|
|
1018
|
+
sb_ltx_wal_checksum(fh, 8, magic & 1, &a, &b);
|
|
1019
|
+
sb_ltx_wal_checksum(page, s->page_size, magic & 1, &a, &b);
|
|
1020
|
+
if (a != sb_ltx_be32(fh + 16) || b != sb_ltx_be32(fh + 20))
|
|
1021
|
+
break;
|
|
1022
|
+
if (sb_ltx_be32(fh + 4))
|
|
1023
|
+
last = i;
|
|
1024
|
+
if (i == prior)
|
|
1025
|
+
valid = a == s->wal_c1 && b == s->wal_c2 && sb_ltx_be32(fh + 4) == s->commit;
|
|
1026
|
+
}
|
|
1027
|
+
done:
|
|
1028
|
+
if (fd >= 0)
|
|
1029
|
+
close(fd);
|
|
1030
|
+
free(page);
|
|
1031
|
+
if (!valid || last < (s->offset - 32) / (s->page_size + 24))
|
|
1032
|
+
return 0;
|
|
1033
|
+
if (last > (s->offset - 32) / (s->page_size + 24)) {
|
|
1034
|
+
char tmp[SB_LTX_PATH], final[SB_LTX_PATH], dir[SB_LTX_PATH];
|
|
1035
|
+
if (sb_ltx_names(s, s->txid + 1, tmp, final, dir))
|
|
1036
|
+
return -1;
|
|
1037
|
+
if (unlink(tmp) && errno != ENOENT)
|
|
1038
|
+
return -1;
|
|
1039
|
+
if (sb_ltx_capture(s, last))
|
|
1040
|
+
return -1;
|
|
1041
|
+
}
|
|
1042
|
+
if (sb_ltx_checkpoint(s, 1))
|
|
1043
|
+
return -1;
|
|
1044
|
+
return 1;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
static int sb_ltx_start(int idx, sqlite3 *db, const char *path, int lock) {
|
|
1048
|
+
sb_ltx_state *s = &sb_ltx_states[idx];
|
|
1049
|
+
memset(s, 0, sizeof(*s));
|
|
1050
|
+
s->db = db;
|
|
1051
|
+
s->lock = lock;
|
|
1052
|
+
s->read_fd = -1;
|
|
1053
|
+
if (snprintf(s->path, sizeof(s->path), "%s", path) >= sizeof(s->path) ||
|
|
1054
|
+
snprintf(s->root, sizeof(s->root), "%s.ltx", path) >= sizeof(s->root))
|
|
1055
|
+
return -1;
|
|
1056
|
+
if (mkdir(s->root, 0700) && errno != EEXIST)
|
|
1057
|
+
return -1;
|
|
1058
|
+
char parent[SB_LTX_PATH];
|
|
1059
|
+
snprintf(parent, sizeof(parent), "%s", path);
|
|
1060
|
+
char *slash = strrchr(parent, '/');
|
|
1061
|
+
if (slash) {
|
|
1062
|
+
*slash = 0;
|
|
1063
|
+
if (sb_ltx_sync_dir(*parent ? parent : "/"))
|
|
1064
|
+
return -1;
|
|
1065
|
+
} else if (sb_ltx_sync_dir("."))
|
|
1066
|
+
return -1;
|
|
1067
|
+
if (sqlite3_exec(db,
|
|
1068
|
+
"PRAGMA wal_autocheckpoint=0; PRAGMA synchronous=FULL; PRAGMA "
|
|
1069
|
+
"locking_mode=EXCLUSIVE; BEGIN EXCLUSIVE; COMMIT;",
|
|
1070
|
+
0, 0, 0))
|
|
1071
|
+
return -1;
|
|
1072
|
+
sqlite3_stmt *mode = 0;
|
|
1073
|
+
if (sqlite3_prepare_v2(db, "PRAGMA journal_mode", -1, &mode, 0) || !mode)
|
|
1074
|
+
return -1;
|
|
1075
|
+
int is_wal =
|
|
1076
|
+
sqlite3_step(mode) == 100 && !strcmp((const char *)sqlite3_column_text(mode, 0), "wal");
|
|
1077
|
+
sqlite3_finalize(mode);
|
|
1078
|
+
if (!is_wal)
|
|
1079
|
+
return -1;
|
|
1080
|
+
struct stat st;
|
|
1081
|
+
if (stat(path, &st))
|
|
1082
|
+
return -1;
|
|
1083
|
+
if (st.st_size == 0 && sqlite3_exec(db, "PRAGMA user_version=0;", 0, 0, 0))
|
|
1084
|
+
return -1;
|
|
1085
|
+
// POSIX fcntl locks belong to the process, not its descriptors. Closing any
|
|
1086
|
+
// raw fd for this inode would silently release SQLite's locks. Keep this fd
|
|
1087
|
+
// for the database handle's entire lifetime, including failed captures.
|
|
1088
|
+
s->read_fd = open(path, O_RDONLY);
|
|
1089
|
+
if (s->read_fd < 0)
|
|
1090
|
+
return -1;
|
|
1091
|
+
int resumed = sb_ltx_resume(s);
|
|
1092
|
+
if (resumed < 0 || (resumed == 0 && sb_ltx_snapshot(s)))
|
|
1093
|
+
return -1;
|
|
1094
|
+
sqlite3_wal_hook(db, sb_ltx_hook, s);
|
|
1095
|
+
sqlite3_set_authorizer(db, sb_ltx_authorize, s);
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1098
|
+
static int sb_ltx_opted(const char *path) {
|
|
1099
|
+
const char *v = getenv("SB_LTX_ENABLE"), *name = getenv("SB_LTX_DATABASE");
|
|
1100
|
+
if (!sb_ltx_compat || !v || strcmp(v, "1"))
|
|
1101
|
+
return 0;
|
|
1102
|
+
if (!name || !*name ||
|
|
1103
|
+
strspn(name, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-") !=
|
|
1104
|
+
strlen(name))
|
|
1105
|
+
return -1;
|
|
1106
|
+
char suffix[300];
|
|
1107
|
+
if (snprintf(suffix, sizeof(suffix), "/d1/%s.sqlite", name) >= sizeof(suffix))
|
|
1108
|
+
return -1;
|
|
1109
|
+
size_t n = strlen(path), k = strlen(suffix);
|
|
1110
|
+
return n >= k && strcmp(path + n - k, suffix) == 0;
|
|
1111
|
+
}
|
|
1112
|
+
// END LTX NATIVE
|
|
1113
|
+
|
|
80
1114
|
// One handle per path, opened once and kept for the process lifetime — the same
|
|
81
1115
|
// lifetime the broker gives a Database. Returns an index, or -1.
|
|
82
1116
|
// Create every parent directory of path, like mkdir -p. A standalone binary
|
|
@@ -97,12 +1131,21 @@ static void sb_mkdirs(const char* path) {
|
|
|
97
1131
|
|
|
98
1132
|
static int sb_db_for(const char* path) {
|
|
99
1133
|
for (int i = 0; i < sb_db_count; i++) {
|
|
100
|
-
if (strcmp(sb_db_names[i], path) == 0) return i;
|
|
1134
|
+
if (strcmp(sb_db_names[i], path) == 0) return sb_ltx_states[i].failed ? -1 : i;
|
|
101
1135
|
}
|
|
102
1136
|
if (sb_db_count >= SB_MAX_DB) return -1;
|
|
103
1137
|
sqlite3* db = 0;
|
|
104
1138
|
sb_mkdirs(path);
|
|
105
|
-
|
|
1139
|
+
int replicated = sb_ltx_opted(path), lock = -1;
|
|
1140
|
+
if (replicated < 0 || (replicated && strlen(path) >= sizeof(sb_db_names[0]))) return -1;
|
|
1141
|
+
if (replicated) {
|
|
1142
|
+
char lockpath[SB_LTX_PATH];
|
|
1143
|
+
if (snprintf(lockpath, sizeof(lockpath), "%s.ltx.lock", path) >= sizeof(lockpath) || sb_ltx_register()) return -1;
|
|
1144
|
+
lock = open(lockpath, O_RDWR | O_CREAT, 0600);
|
|
1145
|
+
if (lock < 0 || flock(lock, LOCK_EX | LOCK_NB)) { if (lock >= 0) close(lock); return -1; }
|
|
1146
|
+
}
|
|
1147
|
+
int open_rc = replicated ? sqlite3_open_v2(path, &db, 6, "sb-ltx-bounded") : sqlite3_open(path, &db);
|
|
1148
|
+
if (open_rc != 0) { if (db) sqlite3_close(db); if (lock >= 0) close(lock); return -1; }
|
|
106
1149
|
// No mmap_size on purpose: mapping the database makes every page a read
|
|
107
1150
|
// touches count toward RSS, and measured on Linux that cost more than the
|
|
108
1151
|
// copy it saves once the file holds a few large objects. The page cache is
|
|
@@ -119,6 +1162,16 @@ static int sb_db_for(const char* path) {
|
|
|
119
1162
|
0, 0, 0);
|
|
120
1163
|
sb_dbs[sb_db_count] = db;
|
|
121
1164
|
snprintf(sb_db_names[sb_db_count], 256, "%s", path);
|
|
1165
|
+
if (replicated && sb_ltx_start(sb_db_count, db, path, lock)) {
|
|
1166
|
+
sb_ltx_states[sb_db_count].failed = 1;
|
|
1167
|
+
fprintf(stderr, "LTX initialization failed for %s\n", path);
|
|
1168
|
+
sqlite3_close(db);
|
|
1169
|
+
if (sb_ltx_states[sb_db_count].read_fd >= 0) close(sb_ltx_states[sb_db_count].read_fd);
|
|
1170
|
+
sb_ltx_states[sb_db_count].read_fd = -1;
|
|
1171
|
+
sb_dbs[sb_db_count] = 0;
|
|
1172
|
+
sb_db_count++;
|
|
1173
|
+
close(lock); return -1;
|
|
1174
|
+
}
|
|
122
1175
|
return sb_db_count++;
|
|
123
1176
|
}
|
|
124
1177
|
|
|
@@ -865,9 +1918,22 @@ static char* sb_sql_script(const char* path, const char* sql) {
|
|
|
865
1918
|
int idx = sb_db_for(path);
|
|
866
1919
|
if (idx < 0) { sb_puts(&b, "{\"ok\":false,\"error\":\"cannot open database\"}"); return b.p; }
|
|
867
1920
|
char* err = 0;
|
|
868
|
-
|
|
1921
|
+
int script_rc = 0;
|
|
1922
|
+
if (sb_ltx_states[idx].db) {
|
|
1923
|
+
const char *next = sql;
|
|
1924
|
+
while (*next && !script_rc) {
|
|
1925
|
+
sqlite3_stmt *st = 0; const char *tail = 0;
|
|
1926
|
+
script_rc = sqlite3_prepare_v2(sb_dbs[idx], next, -1, &st, &tail);
|
|
1927
|
+
if (!script_rc && st) { do { script_rc = sqlite3_step(st); } while (script_rc == SB_SQLITE_ROW); if (script_rc == SB_SQLITE_DONE) script_rc = 0; }
|
|
1928
|
+
if (st) sqlite3_finalize(st);
|
|
1929
|
+
if (sb_ltx_after(idx)) script_rc = 10;
|
|
1930
|
+
if (!tail || tail <= next) break;
|
|
1931
|
+
next = tail;
|
|
1932
|
+
}
|
|
1933
|
+
} else script_rc = sqlite3_exec(sb_dbs[idx], sql, 0, 0, &err);
|
|
1934
|
+
if (script_rc != 0) {
|
|
869
1935
|
sb_puts(&b, "{\"ok\":false,\"error\":");
|
|
870
|
-
sb_putjson_text(&b, err ? err :
|
|
1936
|
+
sb_putjson_text(&b, err ? err : sqlite3_errmsg(sb_dbs[idx]), err ? strlen(err) : strlen(sqlite3_errmsg(sb_dbs[idx])));
|
|
871
1937
|
sb_puts(&b, "}");
|
|
872
1938
|
return b.p;
|
|
873
1939
|
}
|
|
@@ -930,6 +1996,7 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
|
|
|
930
1996
|
// Reset, don't finalize: the statement stays compiled in sb_stmt_cache. Reset
|
|
931
1997
|
// now (not just on next reuse) so a SELECT stops holding its read transaction.
|
|
932
1998
|
sqlite3_reset(st);
|
|
1999
|
+
if (sb_ltx_after(idx)) rc = 10;
|
|
933
2000
|
if (rc != SB_SQLITE_DONE && rc != SB_SQLITE_ROW) {
|
|
934
2001
|
free(b.p);
|
|
935
2002
|
sb_buf e = { 0, 0, 0 };
|
|
@@ -2086,6 +3153,11 @@ function __sbHttpAsyncResult(asyncId) {
|
|
|
2086
3153
|
// placeholder timer cleared, the reply is dropped when it arrives.
|
|
2087
3154
|
function __sbCancelAsync() {}
|
|
2088
3155
|
|
|
3156
|
+
// sproutd ran fetch() off its loop, as a promise, at every compatibility date (B18).
|
|
3157
|
+
function __sbAsyncAtAnyDate() {
|
|
3158
|
+
return true;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
2089
3161
|
// Whether the host runs async requests (sb_async_enable was called).
|
|
2090
3162
|
function __sbHttpAsyncEnabled() {
|
|
2091
3163
|
let res = false;
|
|
@@ -2997,7 +4069,13 @@ globalThis.__sbAssetsGet = function (path) {
|
|
|
2997
4069
|
};
|
|
2998
4070
|
|
|
2999
4071
|
/** The transport contract: one request string in, one reply string out. */
|
|
4072
|
+
function __sbLtxConfigure() {
|
|
4073
|
+
// oxlint-disable-next-line no-unused-expressions -- configures the inline C transport.
|
|
4074
|
+
Porffor.c`sb_ltx_compat = 1;`;
|
|
4075
|
+
}
|
|
4076
|
+
|
|
3000
4077
|
function __sbCall(reqJson) {
|
|
4078
|
+
if (globalThis.__sbCompat >= "2026-10-22") __sbLtxConfigure();
|
|
3001
4079
|
try {
|
|
3002
4080
|
return JSON.stringify(__sbEmbeddedDispatch(JSON.parse(reqJson)));
|
|
3003
4081
|
} catch (err) {
|