@sproutboat/runtime 0.34.4 → 0.35.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 +23 -0
- package/package.json +1 -1
- package/src/d1-shim.test.js +120 -16
- package/src/native-fetch-prelude.js +198 -80
- package/src/transport-embedded.js +159 -42
- package/test/native-d1.ts +38 -8
- package/test/native-headers.ts +182 -0
- package/test/native-sqlite-lock.ts +94 -0
|
@@ -1195,6 +1195,17 @@ static unsigned long sb_db_tick = 0;
|
|
|
1195
1195
|
static int sb_is_do_sql(const char* path) { return strstr(path, "/do-sql/") != 0; }
|
|
1196
1196
|
static void sb_db_evict(int idx);
|
|
1197
1197
|
|
|
1198
|
+
// SQLite allocates 20 pages in one block for every page cache it creates, and a statement that
|
|
1199
|
+
// builds a temporary table (INSERT ... RETURNING, a sort) creates one: an 88 KiB mmap and munmap
|
|
1200
|
+
// per such statement. A page count of 0 turns the bulk allocation off; the cache then takes
|
|
1201
|
+
// its pages one at a time, as it always does past the first 20. Must run before SQLite starts.
|
|
1202
|
+
extern int sqlite3_config(int, ...);
|
|
1203
|
+
static int sb_sqlite_booted = 0;
|
|
1204
|
+
static void sb_sqlite_boot(void) {
|
|
1205
|
+
if (sb_sqlite_booted) return;
|
|
1206
|
+
sb_sqlite_booted = 1;
|
|
1207
|
+
sqlite3_config(7 /* SQLITE_CONFIG_PAGECACHE */, (void*)0, 0, 0);
|
|
1208
|
+
}
|
|
1198
1209
|
static int sb_db_for(const char* path) {
|
|
1199
1210
|
for (int i = 0; i < sb_db_count; i++) {
|
|
1200
1211
|
if (sb_dbs[i] && strcmp(sb_db_names[i], path) == 0) {
|
|
@@ -1202,6 +1213,7 @@ static int sb_db_for(const char* path) {
|
|
|
1202
1213
|
return sb_ltx_states[i].failed ? -1 : i;
|
|
1203
1214
|
}
|
|
1204
1215
|
}
|
|
1216
|
+
sb_sqlite_boot();
|
|
1205
1217
|
const int do_sql = sb_is_do_sql(path);
|
|
1206
1218
|
// A name that would not fit could never be found again.
|
|
1207
1219
|
if (do_sql && strlen(path) >= sizeof(sb_db_names[0])) return -1;
|
|
@@ -1240,9 +1252,30 @@ static int sb_db_for(const char* path) {
|
|
|
1240
1252
|
// copies of the binary behind SO_REUSEPORT (#154 scaling recipe): WAL takes
|
|
1241
1253
|
// concurrent readers plus one writer across processes, and this makes a
|
|
1242
1254
|
// writer wait for the lock instead of failing SQLITE_BUSY.
|
|
1255
|
+
// SB_SQLITE_EXCLUSIVE=1 holds the database's lock for the life of the process: no fcntl lock
|
|
1256
|
+
// and unlock around each transaction, and the WAL index lives in heap memory instead of a shared
|
|
1257
|
+
// -shm file. Set before journal_mode, because the heap index is only chosen when WAL starts in
|
|
1258
|
+
// exclusive mode. No other process can open the database while the binary runs, which also rules
|
|
1259
|
+
// out the several-copies-behind-SO_REUSEPORT recipe above. Off unless set (see below), and never for a
|
|
1260
|
+
// database under LTX replication: capture reads the WAL through its shared index, and in
|
|
1261
|
+
// exclusive mode (no -shm file) every capture failed (native-d1's LTX database).
|
|
1262
|
+
// The transport itself only reads the variable, so a host that embeds it (sproutd, the broker) stays
|
|
1263
|
+
// in shared mode unless it is set. A standalone binary sets it to 1 at startup (patch.ts, the shim's
|
|
1264
|
+
// constructor) unless the operator has set it, so SB_SQLITE_EXCLUSIVE=0 opts a standalone binary out.
|
|
1265
|
+
// SB_SQLITE_CACHE_MB sizes each database's page cache in MiB (default 2, about -2000 KiB).
|
|
1266
|
+
{
|
|
1267
|
+
const char* excl = getenv("SB_SQLITE_EXCLUSIVE");
|
|
1268
|
+
if (excl && excl[0] == '1' && !excl[1] && !replicated) sqlite3_exec(db, "PRAGMA locking_mode=EXCLUSIVE", 0, 0, 0);
|
|
1269
|
+
char cache[64];
|
|
1270
|
+
const char* mb = getenv("SB_SQLITE_CACHE_MB");
|
|
1271
|
+
long mib = mb && *mb ? strtol(mb, 0, 10) : 0;
|
|
1272
|
+
if (mib >= 1 && mib <= 4096) snprintf(cache, sizeof(cache), "PRAGMA cache_size=-%ld", mib * 1024);
|
|
1273
|
+
else snprintf(cache, sizeof(cache), "PRAGMA cache_size=-2000");
|
|
1274
|
+
sqlite3_exec(db, cache, 0, 0, 0);
|
|
1275
|
+
}
|
|
1243
1276
|
sqlite3_exec(db,
|
|
1244
1277
|
"PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;"
|
|
1245
|
-
"PRAGMA
|
|
1278
|
+
"PRAGMA journal_size_limit=16777216;",
|
|
1246
1279
|
0, 0, 0);
|
|
1247
1280
|
// Durable Object SQLite enforces foreign keys, as in workerd.
|
|
1248
1281
|
if (do_sql) sqlite3_exec(db, "PRAGMA foreign_keys=ON", 0, 0, 0);
|
|
@@ -2162,13 +2195,35 @@ static char* sb_sql_error(sqlite3* db, const char* message, int code, int skip)
|
|
|
2162
2195
|
// A strict D1 query can hand its rows back as cells instead of JSON text: the caller turns
|
|
2163
2196
|
// them into JS arrays, with no JSON written here and none parsed there. t is 0 NULL,
|
|
2164
2197
|
// 1 number, 2 TEXT (UTF-8 in p), 3 BLOB (bytes in p).
|
|
2165
|
-
|
|
2166
|
-
|
|
2198
|
+
// The text and blob bytes of every cell share one arena (a cell holds an offset into it), and a
|
|
2199
|
+
// TEXT cell says whether it is all ASCII: that one becomes a JS string with a single copy.
|
|
2200
|
+
typedef struct { unsigned char t; unsigned char ascii; double d; size_t off; int n; } sb_cell;
|
|
2201
|
+
// hdr: how many of the first cells are the column names, not row data; changes and rowid are the
|
|
2202
|
+
// statement's counts, so a native reply needs no JSON header at all.
|
|
2203
|
+
typedef struct { sb_cell* c; size_t n; size_t cap; int ncol; int used; char* a; size_t alen; size_t acap; int hdr; long long changes; long long rowid; } sb_cells;
|
|
2167
2204
|
static void sb_cells_free(sb_cells* cs) {
|
|
2168
2205
|
if (!cs) return;
|
|
2169
|
-
for (size_t i = 0; i < cs->n; i++) free(cs->c[i].p);
|
|
2170
2206
|
free(cs->c);
|
|
2171
|
-
cs->
|
|
2207
|
+
free(cs->a);
|
|
2208
|
+
cs->c = 0; cs->a = 0; cs->n = cs->cap = cs->alen = cs->acap = 0;
|
|
2209
|
+
}
|
|
2210
|
+
// Copies n bytes into the arena and returns their offset, or (size_t)-1 when out of memory.
|
|
2211
|
+
static size_t sb_cells_bytes(sb_cells* cs, const unsigned char* src, size_t n, unsigned char* ascii) {
|
|
2212
|
+
if (cs->alen + n + 1 > cs->acap) {
|
|
2213
|
+
size_t cap = cs->acap ? cs->acap * 2 : 1024;
|
|
2214
|
+
while (cap < cs->alen + n + 1) cap *= 2;
|
|
2215
|
+
char* grown = (char*)realloc(cs->a, cap);
|
|
2216
|
+
if (!grown) return (size_t)-1;
|
|
2217
|
+
cs->a = grown; cs->acap = cap;
|
|
2218
|
+
}
|
|
2219
|
+
const size_t off = cs->alen;
|
|
2220
|
+
unsigned char high = 0;
|
|
2221
|
+
for (size_t i = 0; i < n; i++) high |= src[i];
|
|
2222
|
+
if (n) memcpy(cs->a + off, src, n);
|
|
2223
|
+
cs->a[off + n] = 0;
|
|
2224
|
+
cs->alen = off + n + 1;
|
|
2225
|
+
*ascii = high < 0x80;
|
|
2226
|
+
return off;
|
|
2172
2227
|
}
|
|
2173
2228
|
static sb_cell* sb_cells_add(sb_cells* cs) {
|
|
2174
2229
|
if (cs->n == cs->cap) {
|
|
@@ -2178,7 +2233,7 @@ static sb_cell* sb_cells_add(sb_cells* cs) {
|
|
|
2178
2233
|
cs->c = grown; cs->cap = cap;
|
|
2179
2234
|
}
|
|
2180
2235
|
sb_cell* cell = &cs->c[cs->n++];
|
|
2181
|
-
cell->t = 0; cell->d = 0; cell->
|
|
2236
|
+
cell->t = 0; cell->ascii = 0; cell->d = 0; cell->off = 0; cell->n = 0;
|
|
2182
2237
|
return cell;
|
|
2183
2238
|
}
|
|
2184
2239
|
static char* sb_sql_run_x(const char* path, const char* sql, const char* params, sb_cells* cells) {
|
|
@@ -2202,17 +2257,30 @@ static char* sb_sql_run_x(const char* path, const char* sql, const char* params,
|
|
|
2202
2257
|
return sb_sql_error(db, "Wrong number of parameter bindings for SQL query.", 0, 0);
|
|
2203
2258
|
}
|
|
2204
2259
|
|
|
2205
|
-
sb_puts(&b, "{\"ok\":true,\"cols\":[");
|
|
2206
2260
|
int ncol = sqlite3_column_count(st);
|
|
2207
|
-
for
|
|
2208
|
-
|
|
2209
|
-
const char* name = sqlite3_column_name(st, i);
|
|
2210
|
-
sb_putjson_text(&b, name ? name : "", name ? strlen(name) : 0);
|
|
2211
|
-
}
|
|
2212
|
-
sb_puts(&b, "],\"rows\":[");
|
|
2213
|
-
// Cells only for a strict D1 statement: its BLOBs are byte arrays either way.
|
|
2261
|
+
// Cells only for a strict D1 statement: its BLOBs are byte arrays either way. The column names
|
|
2262
|
+
// are its first cells and the reply is no JSON text: success returns NULL, with the counts in cells.
|
|
2214
2263
|
const int native = cells && strict && d1;
|
|
2215
|
-
if (native) {
|
|
2264
|
+
if (native) {
|
|
2265
|
+
cells->ncol = ncol; cells->used = 1; cells->hdr = ncol;
|
|
2266
|
+
for (int i = 0; i < ncol && cells->used > 0; i++) {
|
|
2267
|
+
sb_cell* cell = sb_cells_add(cells);
|
|
2268
|
+
if (!cell) { cells->used = -1; break; }
|
|
2269
|
+
const char* name = sqlite3_column_name(st, i);
|
|
2270
|
+
cell->t = 2;
|
|
2271
|
+
cell->n = name ? (int)strlen(name) : 0;
|
|
2272
|
+
cell->off = sb_cells_bytes(cells, (const unsigned char*)(name ? name : ""), (size_t)cell->n, &cell->ascii);
|
|
2273
|
+
if (cell->off == (size_t)-1) { cells->used = -1; break; }
|
|
2274
|
+
}
|
|
2275
|
+
} else {
|
|
2276
|
+
sb_puts(&b, "{\"ok\":true,\"cols\":[");
|
|
2277
|
+
for (int i = 0; i < ncol; i++) {
|
|
2278
|
+
if (i) sb_puts(&b, ",");
|
|
2279
|
+
const char* name = sqlite3_column_name(st, i);
|
|
2280
|
+
sb_putjson_text(&b, name ? name : "", name ? strlen(name) : 0);
|
|
2281
|
+
}
|
|
2282
|
+
sb_puts(&b, "],\"rows\":[");
|
|
2283
|
+
}
|
|
2216
2284
|
int rc, first = 1;
|
|
2217
2285
|
while ((rc = sqlite3_step(st)) == SB_SQLITE_ROW) {
|
|
2218
2286
|
if (native) {
|
|
@@ -2226,10 +2294,8 @@ static char* sb_sql_run_x(const char* path, const char* sql, const char* params,
|
|
|
2226
2294
|
int n = sqlite3_column_bytes(st, i);
|
|
2227
2295
|
cell->t = t == 4 ? 3 : 2;
|
|
2228
2296
|
cell->n = txt ? n : 0;
|
|
2229
|
-
cell->
|
|
2230
|
-
if (
|
|
2231
|
-
if (cell->n) memcpy(cell->p, txt, (size_t)cell->n);
|
|
2232
|
-
cell->p[cell->n] = 0;
|
|
2297
|
+
cell->off = sb_cells_bytes(cells, txt, (size_t)cell->n, &cell->ascii);
|
|
2298
|
+
if (cell->off == (size_t)-1) { cells->used = -1; break; }
|
|
2233
2299
|
}
|
|
2234
2300
|
if (cells->used < 0) break;
|
|
2235
2301
|
continue;
|
|
@@ -2262,10 +2328,15 @@ static char* sb_sql_run_x(const char* path, const char* sql, const char* params,
|
|
|
2262
2328
|
}
|
|
2263
2329
|
sb_puts(&b, "]");
|
|
2264
2330
|
}
|
|
2265
|
-
char tail[96];
|
|
2266
2331
|
// A read-only statement changed nothing: sqlite3_changes still holds the last write's count.
|
|
2267
|
-
|
|
2268
|
-
|
|
2332
|
+
if (native) {
|
|
2333
|
+
cells->changes = sqlite3_stmt_readonly(st) ? 0 : sqlite3_changes(db);
|
|
2334
|
+
cells->rowid = (long long)sqlite3_last_insert_rowid(db);
|
|
2335
|
+
} else {
|
|
2336
|
+
char tail[96];
|
|
2337
|
+
int k = snprintf(tail, 96, "],\"changes\":%d,\"rowid\":%lld}", sqlite3_stmt_readonly(st) ? 0 : sqlite3_changes(db), (long long)sqlite3_last_insert_rowid(db));
|
|
2338
|
+
sb_put(&b, tail, (size_t)k);
|
|
2339
|
+
}
|
|
2269
2340
|
// Reset, don't finalize: the statement stays compiled in sb_stmt_cache. Reset
|
|
2270
2341
|
// now (not just on next reuse) so a SELECT stops holding its read transaction.
|
|
2271
2342
|
sqlite3_reset(st);
|
|
@@ -4011,6 +4082,22 @@ function __sbDirBytesRaw(dir) {
|
|
|
4011
4082
|
return res;
|
|
4012
4083
|
}
|
|
4013
4084
|
|
|
4085
|
+
// "captured" of __sbD1DbRaw as a number: no text made in C and parsed here, on every D1 query.
|
|
4086
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
4087
|
+
function __sbD1Captured_(path) {
|
|
4088
|
+
let status = -1;
|
|
4089
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
4090
|
+
Porffor.c`
|
|
4091
|
+
const char* __p; size_t __pl; char* __po = 0;
|
|
4092
|
+
porf_native_fetch_read_value(path, &__p, &__pl, &__po);
|
|
4093
|
+
char* __path = (char*)malloc(__pl + 1); memcpy(__path, __p, __pl); __path[__pl] = 0;
|
|
4094
|
+
if (__po) free(__po);
|
|
4095
|
+
status = (f64)sb_db_quota_freeze(__path, -1);
|
|
4096
|
+
free(__path);
|
|
4097
|
+
`;
|
|
4098
|
+
return status;
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4014
4101
|
// sb_db_page_size and sb_db_quota_freeze, for the quota (a path and a "0"/"1" in, a number out).
|
|
4015
4102
|
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
4016
4103
|
function __sbD1DbRaw(path, what) {
|
|
@@ -4217,10 +4304,10 @@ function __sbSqlRaw(path, sql, paramsJson) {
|
|
|
4217
4304
|
return res;
|
|
4218
4305
|
}
|
|
4219
4306
|
|
|
4220
|
-
// One strict D1 statement: [header
|
|
4221
|
-
//
|
|
4222
|
-
//
|
|
4223
|
-
//
|
|
4307
|
+
// One strict D1 statement: [header, rows, columns, changes, rowid]. A result has the empty string
|
|
4308
|
+
// for its header, rows an array of row arrays built here from the cells (BLOBs as byte arrays),
|
|
4309
|
+
// columns the array of names, and changes and rowid numbers: no JSON is written or parsed. Any
|
|
4310
|
+
// other reply is the error JSON in the header, with nothing after it.
|
|
4224
4311
|
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
4225
4312
|
function __sbSqlRowsRaw(path, sql, paramsJson) {
|
|
4226
4313
|
let res = "";
|
|
@@ -4244,29 +4331,38 @@ function __sbSqlRowsRaw(path, sql, paramsJson) {
|
|
|
4244
4331
|
sb_cells __cells = { 0, 0, 0, 0, 0 };
|
|
4245
4332
|
char* __out = sb_sql_run_x(__path, __sql, __args, &__cells);
|
|
4246
4333
|
free(__path); free(__sql); free(__args);
|
|
4247
|
-
const u32 __pair = porf_arr_new(
|
|
4334
|
+
const u32 __pair = porf_arr_new(5, 5);
|
|
4248
4335
|
res = porf_box((f64)__pair, 72);
|
|
4249
|
-
|
|
4250
|
-
|
|
4336
|
+
const int __native = !__out && __cells.used == 1;
|
|
4337
|
+
if (!__out && !__native) __out = strdup("{\"ok\":false,\"error\":\"no result\"}");
|
|
4338
|
+
porf_arr_set(__pair, 0, porf_box((f64)porf_native_fetch_alloc_bytestring(__out ? __out : "", __out ? strlen(__out) : 0), 195));
|
|
4251
4339
|
free(__out);
|
|
4252
|
-
if (
|
|
4340
|
+
if (__native) {
|
|
4253
4341
|
const size_t __ncol = (size_t)__cells.ncol;
|
|
4254
|
-
const size_t
|
|
4342
|
+
const size_t __hdr = (size_t)__cells.hdr;
|
|
4343
|
+
const size_t __nrow = __ncol ? (__cells.n - __hdr) / __ncol : 0;
|
|
4255
4344
|
const u32 __rows = porf_arr_new(0, (i32)(__nrow ? __nrow : 1));
|
|
4256
4345
|
porf_arr_set(__pair, 1, porf_box((f64)__rows, 72));
|
|
4257
|
-
|
|
4346
|
+
porf_arr_set(__pair, 3, porf_box_num((f64)__cells.changes));
|
|
4347
|
+
porf_arr_set(__pair, 4, porf_box_num((f64)__cells.rowid));
|
|
4348
|
+
if (!__ncol) porf_arr_set(__pair, 2, porf_box((f64)porf_arr_new(0, 1), 72));
|
|
4349
|
+
// Row index -1 is the column names; the data rows follow.
|
|
4350
|
+
for (long long __r = (__ncol ? -1 : 0); __r < (long long)__nrow; __r++) {
|
|
4258
4351
|
const u32 __row = porf_arr_new((i32)__ncol, (i32)__ncol);
|
|
4259
|
-
porf_arr_set(
|
|
4352
|
+
if (__r < 0) porf_arr_set(__pair, 2, porf_box((f64)__row, 72));
|
|
4353
|
+
else porf_arr_set(__rows, (u32)__r, porf_box((f64)__row, 72));
|
|
4260
4354
|
for (size_t __c = 0; __c < __ncol; __c++) {
|
|
4261
|
-
const sb_cell* __cell = &__cells.c[__r * __ncol + __c];
|
|
4355
|
+
const sb_cell* __cell = &__cells.c[(__r < 0 ? 0 : __hdr + (size_t)__r * __ncol) + __c];
|
|
4262
4356
|
jsval __v = porf_box(0.0, 7);
|
|
4263
4357
|
if (__cell->t == 1) __v = porf_box_num(__cell->d);
|
|
4264
|
-
else if (__cell->t == 2) {
|
|
4358
|
+
else if (__cell->t == 2 && __cell->ascii) {
|
|
4359
|
+
__v = porf_box((f64)porf_native_fetch_alloc_bytestring(__cells.a + __cell->off, (size_t)__cell->n), 195);
|
|
4360
|
+
} else if (__cell->t == 2) {
|
|
4265
4361
|
const size_t __n = (size_t)__cell->n;
|
|
4266
4362
|
uint16_t* __u = (uint16_t*)malloc(__n * 2 + 2);
|
|
4267
4363
|
if (__u) {
|
|
4268
4364
|
int __wide = 0;
|
|
4269
|
-
const size_t __k = sb_text_units((const unsigned char*)__cell->
|
|
4365
|
+
const size_t __k = sb_text_units((const unsigned char*)(__cells.a + __cell->off), __n, __u, &__wide);
|
|
4270
4366
|
if (__wide) {
|
|
4271
4367
|
const u32 __str = porf_native_fetch_alloc((u32)(4 + __k * 2), 67);
|
|
4272
4368
|
*(u32*)(MEM + __str) = (u32)__k;
|
|
@@ -4284,7 +4380,7 @@ function __sbSqlRowsRaw(path, sql, paramsJson) {
|
|
|
4284
4380
|
}
|
|
4285
4381
|
} else if (__cell->t == 3) {
|
|
4286
4382
|
const u32 __bytes = porf_arr_new((i32)__cell->n, (i32)__cell->n);
|
|
4287
|
-
for (int __i = 0; __i < __cell->n; __i++) porf_arr_set(__bytes, (u32)__i, porf_box_num((f64)(unsigned char)__cell->
|
|
4383
|
+
for (int __i = 0; __i < __cell->n; __i++) porf_arr_set(__bytes, (u32)__i, porf_box_num((f64)(unsigned char)__cells.a[__cell->off + __i]));
|
|
4288
4384
|
__v = porf_box((f64)__bytes, 72);
|
|
4289
4385
|
}
|
|
4290
4386
|
porf_arr_set(__row, (u32)__c, __v);
|
|
@@ -4366,7 +4462,7 @@ function __sbD1Quota(path) {
|
|
|
4366
4462
|
// Whether capture owns this database, from the private helper (no SQL), before anything
|
|
4367
4463
|
// below depends on it. A database that cannot open (or whose capture failed) is left to
|
|
4368
4464
|
// the call itself, which reports why.
|
|
4369
|
-
const status =
|
|
4465
|
+
const status = __sbD1Captured_(path);
|
|
4370
4466
|
if (status < 0) return;
|
|
4371
4467
|
__sbD1Captured[path] = status === 1;
|
|
4372
4468
|
let full = __sbQuotaFull();
|
|
@@ -4414,8 +4510,16 @@ function __sbDOSqlPath(cls, file) {
|
|
|
4414
4510
|
if (!__sbDOSqlName(c, false) || !__sbDOSqlName(f, true)) throw new Error("invalid Durable Object SQL database");
|
|
4415
4511
|
return __sbDir() + "/do-sql/" + c + "/" + f + ".sqlite";
|
|
4416
4512
|
}
|
|
4513
|
+
// The last database's path: an app has one or two, and every query asks, so three concatenations
|
|
4514
|
+
// are done once. The directory cannot change mid-run (see __sbDir).
|
|
4515
|
+
var __sbD1PathName = "";
|
|
4516
|
+
var __sbD1PathValue = "";
|
|
4417
4517
|
function __sbD1Path(name) {
|
|
4418
|
-
|
|
4518
|
+
if (name === __sbD1PathName) return __sbD1PathValue;
|
|
4519
|
+
const path = __sbDir() + "/d1/" + name + ".sqlite";
|
|
4520
|
+
__sbD1PathName = name;
|
|
4521
|
+
__sbD1PathValue = path;
|
|
4522
|
+
return path;
|
|
4419
4523
|
}
|
|
4420
4524
|
|
|
4421
4525
|
function __sbSql(path, sql, params) {
|
|
@@ -5336,10 +5440,12 @@ function __sbD1Run(path, sql, params, strict) {
|
|
|
5336
5440
|
if (strict) {
|
|
5337
5441
|
// Rows come back as arrays, built in C (see __sbSqlRowsRaw); the header is small JSON.
|
|
5338
5442
|
const pair = __sbSqlRowsRaw(path, "/*sb-d1-strict*/" + String(sql), JSON.stringify(params || []));
|
|
5339
|
-
|
|
5340
|
-
|
|
5443
|
+
if (pair[0] !== "") {
|
|
5444
|
+
const head = JSON.parse(pair[0]);
|
|
5445
|
+
if (head.ok === false) throw __sbD1Error(head);
|
|
5446
|
+
}
|
|
5341
5447
|
const rows = pair[1];
|
|
5342
|
-
return { columns:
|
|
5448
|
+
return { columns: pair[2], rows, native: true, meta: { changes: pair[3], last_row_id: pair[4], rows_read: rows.length } };
|
|
5343
5449
|
}
|
|
5344
5450
|
const r = JSON.parse(__sbSqlRaw(path, (strict ? "/*sb-d1-strict*/" : "") + String(sql), JSON.stringify(params || [])));
|
|
5345
5451
|
if (r.ok === false) throw strict ? __sbD1Error(r) : new Error("sqlite: " + r.error);
|
|
@@ -5504,6 +5610,17 @@ function __sbCall(reqJson) {
|
|
|
5504
5610
|
}
|
|
5505
5611
|
}
|
|
5506
5612
|
|
|
5613
|
+
// A strict D1 query without the generic dispatch: no request object, no copy of its fields, no walk
|
|
5614
|
+
// down the op list and no result object. The same steps __sbEmbeddedDispatch runs for "d1.query".
|
|
5615
|
+
__sbD1QueryDirect = function (db, sql, params) {
|
|
5616
|
+
if (__sbCompatDate() >= "2026-10-22") __sbLtxConfigure();
|
|
5617
|
+
__sbEnsureSchema();
|
|
5618
|
+
const path = __sbD1Path(String(db));
|
|
5619
|
+
__sbD1Open(path, true);
|
|
5620
|
+
__sbD1Quota(path);
|
|
5621
|
+
return __sbD1Run(path, sql, params, true);
|
|
5622
|
+
};
|
|
5623
|
+
|
|
5507
5624
|
// The same dispatch without the JSON text either side: D1 calls take this (see __sbRpcDirect).
|
|
5508
5625
|
__sbCallDirect = function (req) {
|
|
5509
5626
|
if (__sbCompatDate() >= "2026-10-22") __sbLtxConfigure();
|
package/test/native-d1.ts
CHANGED
|
@@ -56,23 +56,29 @@ int main(void) {
|
|
|
56
56
|
if (!strncmp(line, ":cells ", 7)) {
|
|
57
57
|
sb_cells cs = { 0, 0, 0, 0, 0 };
|
|
58
58
|
char* head = sb_sql_run_x(line + 7, sql, params, &cs);
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// A result has no JSON header: its column names are the first cells and its counts are in cs.
|
|
60
|
+
if (head) printf("{\\"head\\":%s,\\"rows\\":[", head);
|
|
61
|
+
else {
|
|
62
|
+
printf("{\\"head\\":{\\"ok\\":true,\\"cols\\":[");
|
|
63
|
+
for (int c = 0; c < cs.hdr; c++) printf(c ? ",\\"%.*s\\"" : "\\"%.*s\\"", cs.c[c].n, cs.a + cs.c[c].off);
|
|
64
|
+
printf("],\\"changes\\":%lld,\\"rowid\\":%lld},\\"rows\\":[", cs.changes, cs.rowid);
|
|
65
|
+
}
|
|
66
|
+
size_t nrow = cs.ncol ? (cs.n - (size_t)cs.hdr) / (size_t)cs.ncol : 0;
|
|
61
67
|
for (size_t r = 0; r < nrow; r++) {
|
|
62
68
|
printf(r ? ",[" : "[");
|
|
63
69
|
for (int c = 0; c < cs.ncol; c++) {
|
|
64
|
-
sb_cell* x = &cs.c[r * (size_t)cs.ncol + (size_t)c];
|
|
70
|
+
sb_cell* x = &cs.c[(size_t)cs.hdr + r * (size_t)cs.ncol + (size_t)c];
|
|
65
71
|
if (c) printf(",");
|
|
66
72
|
if (x->t == 0) printf("null");
|
|
67
73
|
else if (x->t == 1) printf("%.17g", x->d);
|
|
68
74
|
else if (x->t == 2) {
|
|
69
75
|
uint16_t* u = (uint16_t*)malloc((size_t)x->n * 2 + 2); int wide = 0;
|
|
70
|
-
size_t k = sb_text_units((const unsigned char*)x->
|
|
76
|
+
size_t k = sb_text_units((const unsigned char*)(cs.a + x->off), (size_t)x->n, u, &wide);
|
|
71
77
|
printf("\\"");
|
|
72
78
|
for (size_t i = 0; i < k; i++) printf("\\\\u%04x", u[i]);
|
|
73
79
|
printf("\\"");
|
|
74
80
|
free(u);
|
|
75
|
-
} else { printf("{\\"$blob\\":\\""); for (int i = 0; i < x->n; i++) printf("%02x", (unsigned char)x->
|
|
81
|
+
} else { printf("{\\"$blob\\":\\""); for (int i = 0; i < x->n; i++) printf("%02x", (unsigned char)cs.a[x->off + i]); printf("\\"}"); }
|
|
76
82
|
}
|
|
77
83
|
printf("]");
|
|
78
84
|
}
|
|
@@ -162,6 +168,7 @@ int main(void) {
|
|
|
162
168
|
assert.deepEqual(cells.rows, json.rows, `cells equal the JSON rows for ${t}`);
|
|
163
169
|
assert.deepEqual(cells.head.cols, json.cols);
|
|
164
170
|
assert.equal(cells.head.changes, json.changes);
|
|
171
|
+
assert.equal(cells.head.rowid, json.rowid);
|
|
165
172
|
}
|
|
166
173
|
const empty = cellRun("SELECT a FROM t WHERE 0");
|
|
167
174
|
assert.deepEqual(empty.cells.rows, []);
|
|
@@ -252,15 +259,16 @@ int main(void) {
|
|
|
252
259
|
};
|
|
253
260
|
// oxlint-disable-next-line no-function-constructor -- lifting a block out of the text-only transport
|
|
254
261
|
const scriptRaw = (path: string, text: string) => sqlRaw(":script " + path, text, "[]");
|
|
255
|
-
// __sbSqlRowsRaw as the transport's C block gives it: [header JSON
|
|
262
|
+
// __sbSqlRowsRaw as the transport's C block gives it: an error is [header JSON], a result is
|
|
263
|
+
// ["", rows, columns, changes, rowid], BLOBs as byte arrays.
|
|
256
264
|
const rowsOf =
|
|
257
265
|
(raw: (path: string, text: string, params: string) => string) => (path: string, text: string, params: string) => {
|
|
258
266
|
const r = JSON.parse(raw(":cells " + path, text, params));
|
|
259
|
-
if (r.head.ok === false) return [JSON.stringify(r.head)
|
|
267
|
+
if (r.head.ok === false) return [JSON.stringify(r.head)];
|
|
260
268
|
const rows = r.rows.map((row: unknown[]) =>
|
|
261
269
|
row.map((c) => (c instanceof Object && "$blob" in c ? [...Buffer.from(String(c.$blob), "hex")] : c)),
|
|
262
270
|
);
|
|
263
|
-
return [
|
|
271
|
+
return ["", rows, r.head.cols, r.head.changes, r.head.rowid];
|
|
264
272
|
};
|
|
265
273
|
const ops = new Function(
|
|
266
274
|
"__sbSqlRaw",
|
|
@@ -279,6 +287,7 @@ int main(void) {
|
|
|
279
287
|
"__sbDir",
|
|
280
288
|
"__sbDirBytesRaw",
|
|
281
289
|
"__sbD1DbRaw",
|
|
290
|
+
"__sbD1Captured_",
|
|
282
291
|
"__sbSql",
|
|
283
292
|
"__sbSqlRaw",
|
|
284
293
|
`${transport.slice(quotaStart, quotaEnd)}\nreturn { __sbQuotaCheck, __sbD1Quota, __sbQuotaFull, expire() { __sbQuotaAt = -1; } };`,
|
|
@@ -287,6 +296,7 @@ int main(void) {
|
|
|
287
296
|
() => dir,
|
|
288
297
|
(d: string) => String(JSON.parse(sqlRaw(":bytes " + d, "", "")).n),
|
|
289
298
|
(path: string, what: string) => String(JSON.parse(sqlRaw(":db " + path, what, "")).n),
|
|
299
|
+
(path: string) => JSON.parse(sqlRaw(":db " + path, "c", "")).n,
|
|
290
300
|
sql,
|
|
291
301
|
sqlRaw,
|
|
292
302
|
);
|
|
@@ -334,6 +344,7 @@ int main(void) {
|
|
|
334
344
|
"__sbDir",
|
|
335
345
|
"__sbDirBytesRaw",
|
|
336
346
|
"__sbD1DbRaw",
|
|
347
|
+
"__sbD1Captured_",
|
|
337
348
|
"__sbSql",
|
|
338
349
|
"__sbSqlRaw",
|
|
339
350
|
`${transport.slice(quotaStart, quotaEnd)}\nreturn { __sbQuotaCheck, __sbD1Quota, __sbQuotaFull };`,
|
|
@@ -342,6 +353,7 @@ int main(void) {
|
|
|
342
353
|
() => dir,
|
|
343
354
|
(d: string) => String(JSON.parse(raw(":bytes " + d, "", "")).n),
|
|
344
355
|
(path: string, what: string) => String(JSON.parse(raw(":db " + path, what, "")).n),
|
|
356
|
+
(path: string) => JSON.parse(raw(":db " + path, "c", "")).n,
|
|
345
357
|
run,
|
|
346
358
|
raw,
|
|
347
359
|
);
|
|
@@ -629,6 +641,24 @@ int main(void) {
|
|
|
629
641
|
assert.match(failedOver.error, /^LTX capture failed/);
|
|
630
642
|
third.close();
|
|
631
643
|
await third.exited;
|
|
644
|
+
// Locking mode. This driver is the transport on its own, as sproutd and the broker embed it (no
|
|
645
|
+
// standalone shim): unset it must stay shared, so those hosts behave as before. SB_SQLITE_EXCLUSIVE=1 is
|
|
646
|
+
// exclusive for a plain database; anything else, 0 included, is shared. A database under LTX replication
|
|
647
|
+
// is never exclusive.
|
|
648
|
+
const lockingMode = (env: Record<string, string>, path: string) => {
|
|
649
|
+
const e = Object.fromEntries(Object.entries(process.env).filter(([key]) => key !== "SB_SQLITE_EXCLUSIVE"));
|
|
650
|
+
const p = Bun.spawnSync([binary], {
|
|
651
|
+
stdin: Buffer.from(`${path}\tPRAGMA locking_mode\t[]\n`),
|
|
652
|
+
env: { ...e, ...env },
|
|
653
|
+
});
|
|
654
|
+
assert.equal(p.exitCode, 0, p.stderr.toString());
|
|
655
|
+
return JSON.parse(p.stdout.toString().trim()).rows[0][0];
|
|
656
|
+
};
|
|
657
|
+
const lockDb = join(work, "data/d1/LOCK.sqlite");
|
|
658
|
+
assert.equal(lockingMode({}, lockDb), "normal", "the bare transport keeps shared locking unless asked");
|
|
659
|
+
assert.equal(lockingMode({ SB_SQLITE_EXCLUSIVE: "0" }, lockDb), "normal");
|
|
660
|
+
assert.equal(lockingMode({ SB_SQLITE_EXCLUSIVE: "1" }, lockDb), "exclusive");
|
|
661
|
+
assert.equal(lockingMode({ SB_SQLITE_EXCLUSIVE: "1" }, join(work, "data/d1/LOCK2.sqlite")), "exclusive");
|
|
632
662
|
console.log(
|
|
633
663
|
"native d1: sb_sql_run and the D1 ops pass (blob binds, atomic batch, strict counts, foreign keys, error text)",
|
|
634
664
|
);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { ensurePorffor } from "../../toolchain/src/acquire";
|
|
6
|
+
import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
|
|
7
|
+
|
|
8
|
+
// Headers validates a name and trims a value in C before it falls back to the regular expression and
|
|
9
|
+
// the JS loops. Every kind of name and value must come out as the Fetch spec says: valid names
|
|
10
|
+
// lowercased, invalid ones a TypeError, values trimmed, forbidden characters refused, and a name
|
|
11
|
+
// that is not a plain bytestring (a wide character) still judged by the expression.
|
|
12
|
+
const handler = `export default {fetch(request) {
|
|
13
|
+
const out = {};
|
|
14
|
+
const names = ["a", "Content-Type", "X-Sb-Test", "x_y", "a.b", "a~b", "a|b", "a^b", "a+b", "a*b", "a!b", "a#b", "a$b", "a%b", "a&b", "a'b", "a-b", "9", "A9z", "",
|
|
15
|
+
"a b", "a:b", "a/b", "a(b", "a@b", "a[b", "a\\u0000b", "a\\nb", "\\u00e9", "caf\\u00e9", "a\\u0100b", "a,b", "a;b", "a=b", "a?b", "a\\"b", "a\\\\b", "a<b", "a>b", "a{b", "a}b"];
|
|
16
|
+
const h = new Headers();
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
try { h.append(name, "v"); out["n:" + name] = h.has(name.toLowerCase()) ? "ok" : "stored?"; }
|
|
19
|
+
catch (e) { out["n:" + name] = e instanceof TypeError ? "TypeError" : String(e); }
|
|
20
|
+
}
|
|
21
|
+
const values = ["plain", "", " lead", "trail ", "\\t both \\t", "\\r\\nx", "a\\u0000b", "a\\nb", "a\\rb", "in ner", "caf\\u00e9", "\\u4e2d", " \\u4e2d ", "x\\ty"];
|
|
22
|
+
for (const value of values) {
|
|
23
|
+
const g = new Headers();
|
|
24
|
+
try { g.set("x", value); out["v:" + JSON.stringify(value)] = g.get("x"); }
|
|
25
|
+
catch (e) { out["v:" + JSON.stringify(value)] = e instanceof TypeError ? "TypeError" : String(e); }
|
|
26
|
+
}
|
|
27
|
+
const dup = new Headers();
|
|
28
|
+
dup.append("Accept", "a"); dup.append("accept", "b");
|
|
29
|
+
out.dup = dup.get("ACCEPT");
|
|
30
|
+
out.fromObject = new Headers({ "Content-Type": "text/plain", "X-A": " 1 " }).get("x-a");
|
|
31
|
+
out.getCase = new Headers({ "x-a": "1" }).get("X-A");
|
|
32
|
+
out.trigger = request.headers.get("X-Sb-Trigger");
|
|
33
|
+
out.custom = request.headers.get("X-Custom");
|
|
34
|
+
return new Response(JSON.stringify(out), { headers: { "Content-Type": "application/json", "X-Case": " kept " } });
|
|
35
|
+
}};`;
|
|
36
|
+
|
|
37
|
+
function freePort(): number {
|
|
38
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
39
|
+
const port = listener.port;
|
|
40
|
+
listener.stop(true);
|
|
41
|
+
return port;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// What the Fetch spec (and workerd) say, from the names and values above.
|
|
45
|
+
function expectedName(name: string): string {
|
|
46
|
+
const valid = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(name);
|
|
47
|
+
return valid ? "ok" : "TypeError";
|
|
48
|
+
}
|
|
49
|
+
function expectedValue(value: string): string {
|
|
50
|
+
const trimmed = value.replace(/^[\t\n\r ]+|[\t\n\r ]+$/g, "");
|
|
51
|
+
return /[\0\n\r]/.test(trimmed) ? "TypeError" : trimmed;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-headers-"));
|
|
55
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
56
|
+
try {
|
|
57
|
+
const porffor = await ensurePorffor();
|
|
58
|
+
const node = Bun.which("node");
|
|
59
|
+
if (!node) throw new Error("native headers test needs node on PATH");
|
|
60
|
+
|
|
61
|
+
const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
|
|
62
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
63
|
+
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
64
|
+
const generated = join(workdir, "headers.generated.js");
|
|
65
|
+
const binary = join(workdir, "headers-native");
|
|
66
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
67
|
+
await rm(binary, { force: true });
|
|
68
|
+
|
|
69
|
+
const compile = Bun.spawn(
|
|
70
|
+
[node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
|
|
71
|
+
{
|
|
72
|
+
cwd: workdir,
|
|
73
|
+
env: process.env,
|
|
74
|
+
stdout: "pipe",
|
|
75
|
+
stderr: "pipe",
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
79
|
+
compile.exited,
|
|
80
|
+
new Response(compile.stdout).text(),
|
|
81
|
+
new Response(compile.stderr).text(),
|
|
82
|
+
]);
|
|
83
|
+
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
84
|
+
|
|
85
|
+
const port = freePort();
|
|
86
|
+
server = Bun.spawn([binary], {
|
|
87
|
+
cwd: workdir,
|
|
88
|
+
env: { ...process.env, PORT: String(port) },
|
|
89
|
+
stdout: "ignore",
|
|
90
|
+
stderr: "inherit",
|
|
91
|
+
});
|
|
92
|
+
let response: Response | undefined;
|
|
93
|
+
for (let attempt = 0; attempt < 50 && !response; attempt++) {
|
|
94
|
+
try {
|
|
95
|
+
response = await fetch(`http://127.0.0.1:${port}/`, {
|
|
96
|
+
headers: { "X-Custom": "custom" },
|
|
97
|
+
signal: AbortSignal.timeout(2000),
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
if (server.exitCode !== null) break;
|
|
101
|
+
await Bun.sleep(100);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
assert(response, "compiled handler did not start");
|
|
105
|
+
// SAFETY: the handler above answers a flat JSON object of strings (and null), read key by key below.
|
|
106
|
+
const out = (await response.json()) as Record<string, string>;
|
|
107
|
+
const names = [
|
|
108
|
+
"a",
|
|
109
|
+
"Content-Type",
|
|
110
|
+
"X-Sb-Test",
|
|
111
|
+
"x_y",
|
|
112
|
+
"a.b",
|
|
113
|
+
"a~b",
|
|
114
|
+
"a|b",
|
|
115
|
+
"a^b",
|
|
116
|
+
"a+b",
|
|
117
|
+
"a*b",
|
|
118
|
+
"a!b",
|
|
119
|
+
"a#b",
|
|
120
|
+
"a$b",
|
|
121
|
+
"a%b",
|
|
122
|
+
"a&b",
|
|
123
|
+
"a'b",
|
|
124
|
+
"a-b",
|
|
125
|
+
"9",
|
|
126
|
+
"A9z",
|
|
127
|
+
"",
|
|
128
|
+
"a b",
|
|
129
|
+
"a:b",
|
|
130
|
+
"a/b",
|
|
131
|
+
"a(b",
|
|
132
|
+
"a@b",
|
|
133
|
+
"a[b",
|
|
134
|
+
"a\u0000b",
|
|
135
|
+
"a\nb",
|
|
136
|
+
"é",
|
|
137
|
+
"café",
|
|
138
|
+
"aĀb",
|
|
139
|
+
"a,b",
|
|
140
|
+
"a;b",
|
|
141
|
+
"a=b",
|
|
142
|
+
"a?b",
|
|
143
|
+
'a"b',
|
|
144
|
+
"a\\b",
|
|
145
|
+
"a<b",
|
|
146
|
+
"a>b",
|
|
147
|
+
"a{b",
|
|
148
|
+
"a}b",
|
|
149
|
+
];
|
|
150
|
+
for (const name of names) assert.equal(out["n:" + name], expectedName(name), `header name ${JSON.stringify(name)}`);
|
|
151
|
+
const values = [
|
|
152
|
+
"plain",
|
|
153
|
+
"",
|
|
154
|
+
" lead",
|
|
155
|
+
"trail ",
|
|
156
|
+
"\t both \t",
|
|
157
|
+
"\r\nx",
|
|
158
|
+
"a\u0000b",
|
|
159
|
+
"a\nb",
|
|
160
|
+
"a\rb",
|
|
161
|
+
"in ner",
|
|
162
|
+
"café",
|
|
163
|
+
"中",
|
|
164
|
+
" 中 ",
|
|
165
|
+
"x\ty",
|
|
166
|
+
];
|
|
167
|
+
for (const value of values)
|
|
168
|
+
assert.equal(out["v:" + JSON.stringify(value)], expectedValue(value), `header value ${JSON.stringify(value)}`);
|
|
169
|
+
assert.equal(out.dup, "a, b");
|
|
170
|
+
assert.equal(out.fromObject, "1");
|
|
171
|
+
assert.equal(out.getCase, "1");
|
|
172
|
+
assert.equal(out.trigger, null);
|
|
173
|
+
assert.equal(out.custom, "custom");
|
|
174
|
+
assert.equal(response.headers.get("x-case"), "kept");
|
|
175
|
+
console.log("Native Headers: names and values validated in C match the Fetch spec for every class of input");
|
|
176
|
+
} finally {
|
|
177
|
+
if (server) {
|
|
178
|
+
server.kill(9);
|
|
179
|
+
await server.exited;
|
|
180
|
+
}
|
|
181
|
+
await rm(workdir, { recursive: true, force: true });
|
|
182
|
+
}
|