@sproutboat/runtime 0.36.0 → 0.37.1
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 +29 -0
- package/package.json +1 -1
- package/src/d1-shim.test.js +215 -0
- package/src/native-fetch-prelude.js +855 -73
- package/src/queues.test.js +136 -0
- package/src/transport-embedded.js +156 -35
- package/test/native-d1.ts +71 -0
- package/test/native-domexception.ts +114 -0
- package/test/native-getrandomvalues.ts +117 -0
- package/test/native-textdecoder-encodings.ts +123 -0
package/src/queues.test.js
CHANGED
|
@@ -157,3 +157,139 @@ test("before 2026-10-31 without settings, the outcome has only ack and retry", a
|
|
|
157
157
|
expect(legacy).toEqual(["dlq", true, 5000]);
|
|
158
158
|
expect(out).toEqual({ ack: [], retry: ["a"] });
|
|
159
159
|
});
|
|
160
|
+
|
|
161
|
+
// ---- 2026-11-05 ----
|
|
162
|
+
|
|
163
|
+
const failure = (p) =>
|
|
164
|
+
p.then(
|
|
165
|
+
() => "accepted",
|
|
166
|
+
(e) => e.name + ": " + e.message,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
test("2026-11-05: send() checks arguments and delays as workerd does", async () => {
|
|
170
|
+
const { QUEUE } = env("2026-11-05");
|
|
171
|
+
expect(await failure(QUEUE.send())).toBe(
|
|
172
|
+
"TypeError: Failed to execute 'send' on 'WorkerQueue': parameter 1 is not of type 'JsValue'.",
|
|
173
|
+
);
|
|
174
|
+
expect(await failure(QUEUE.send(1, "x"))).toBe(
|
|
175
|
+
"TypeError: Failed to execute 'send' on 'WorkerQueue': parameter 2 is not of type 'SendOptions'.",
|
|
176
|
+
);
|
|
177
|
+
expect(await failure(QUEUE.send(1, null))).toBe("accepted");
|
|
178
|
+
expect(await failure(QUEUE.send(1, { delaySeconds: -1 }))).toStartWith("Error: Queue send failed: delaySeconds");
|
|
179
|
+
expect(await failure(QUEUE.send(1, { delaySeconds: 86401 }))).toStartWith("Error: Queue send failed: delaySeconds");
|
|
180
|
+
expect(await failure(QUEUE.send(1, { delaySeconds: 86400 }))).toBe("accepted");
|
|
181
|
+
expect(await failure(QUEUE.send(1, { delaySeconds: 1.5 }))).toBe("accepted");
|
|
182
|
+
expect(await failure(QUEUE.sendBatch([{ body: 1 }], { delaySeconds: 86401 }))).toStartWith(
|
|
183
|
+
"Error: Queue send failed",
|
|
184
|
+
);
|
|
185
|
+
expect(await failure(QUEUE.sendBatch([{ body: 1, delaySeconds: -5 }]))).toStartWith("Error: Queue send failed");
|
|
186
|
+
expect(await failure(QUEUE.sendBatch([1, 2]))).toBe(
|
|
187
|
+
"TypeError: Incorrect type: the provided value is not of type 'MessageSendRequest'.",
|
|
188
|
+
);
|
|
189
|
+
expect(await failure(QUEUE.send(new Uint8Array([1]).buffer, { contentType: "bytes" }))).toBe(
|
|
190
|
+
'TypeError: Content Type "bytes" requires a value of type ArrayBufferView, but received: object',
|
|
191
|
+
);
|
|
192
|
+
expect(await failure(QUEUE.send(new Uint8Array([1]), { contentType: "bytes" }))).toBe("accepted");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("before 2026-11-05 the same arguments are accepted or answer as before", async () => {
|
|
196
|
+
const { QUEUE } = env("2026-11-01");
|
|
197
|
+
expect(await failure(QUEUE.send(1, { delaySeconds: 86401 }))).toBe("accepted");
|
|
198
|
+
expect(await failure(QUEUE.send(1, "x"))).toBe("accepted");
|
|
199
|
+
expect(await failure(QUEUE.sendBatch([{ body: 1, delaySeconds: -5 }]))).toBe("accepted");
|
|
200
|
+
expect(await failure(QUEUE.send(new Uint8Array([1]).buffer, { contentType: "bytes" }))).toBe("accepted");
|
|
201
|
+
expect(await failure(QUEUE.send())).toBe("TypeError: Message body cannot be undefined");
|
|
202
|
+
expect(sent.find((r) => r.delaySeconds === 86401)).toBeDefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const roundTrip = async (compat, value) => {
|
|
206
|
+
const { QUEUE } = env(compat);
|
|
207
|
+
await QUEUE.send(value, { contentType: "v8" });
|
|
208
|
+
const body = sent[sent.length - 1].body;
|
|
209
|
+
let seen;
|
|
210
|
+
await run(
|
|
211
|
+
{
|
|
212
|
+
queue(batch) {
|
|
213
|
+
seen = batch.messages[0];
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{ queue: "QUEUE", messages: [message("a", body)] },
|
|
217
|
+
ctx,
|
|
218
|
+
);
|
|
219
|
+
return { body, seen };
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
test("2026-11-05: the v8 content type keeps shared and circular references, holes and invalid dates", async () => {
|
|
223
|
+
const shared = { z: 1 };
|
|
224
|
+
const circular = { n: 1 };
|
|
225
|
+
circular.self = circular;
|
|
226
|
+
const holes = [1];
|
|
227
|
+
holes[3] = 4;
|
|
228
|
+
const { body, seen } = await roundTrip("2026-11-05", {
|
|
229
|
+
a: shared,
|
|
230
|
+
b: shared,
|
|
231
|
+
c: circular,
|
|
232
|
+
holes,
|
|
233
|
+
d: new Date(NaN),
|
|
234
|
+
e: new TypeError("boom"),
|
|
235
|
+
m: new Map([[shared, new Set([shared])]]),
|
|
236
|
+
});
|
|
237
|
+
expect(body.startsWith("#sbq1:v8r:")).toBe(true);
|
|
238
|
+
const v = seen.body;
|
|
239
|
+
expect(v.a).toBe(v.b);
|
|
240
|
+
expect(v.c.self).toBe(v.c);
|
|
241
|
+
expect(v.holes.length).toBe(4);
|
|
242
|
+
expect(1 in v.holes).toBe(false);
|
|
243
|
+
expect(v.holes[3]).toBe(4);
|
|
244
|
+
expect(Number.isNaN(v.d.getTime())).toBe(true);
|
|
245
|
+
expect(v.e).toBeInstanceOf(TypeError);
|
|
246
|
+
expect(v.e.message).toBe("boom");
|
|
247
|
+
const [key, set] = [...v.m][0];
|
|
248
|
+
expect(key).toBe(v.a);
|
|
249
|
+
expect([...set][0]).toBe(v.a);
|
|
250
|
+
expect(Object.getOwnPropertyDescriptor(seen, "body").writable).toBe(false);
|
|
251
|
+
expect(Object.getOwnPropertyDescriptor(seen, "id").writable).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("before 2026-11-05 the v8 content type is the earlier codec, and v8r messages decode at any date", async () => {
|
|
255
|
+
const { body, seen } = await roundTrip("2026-11-01", { d: new Date(1000), m: new Map([[1, 2]]) });
|
|
256
|
+
expect(body.startsWith("#sbq1:v8:")).toBe(true);
|
|
257
|
+
expect(seen.body.m.get(1)).toBe(2);
|
|
258
|
+
const circular = {};
|
|
259
|
+
circular.self = circular;
|
|
260
|
+
const later = await roundTrip("2026-11-05", circular);
|
|
261
|
+
globalThis.__sbCompat = "2026-10-31";
|
|
262
|
+
let again;
|
|
263
|
+
await run(
|
|
264
|
+
{
|
|
265
|
+
queue(batch) {
|
|
266
|
+
again = batch.messages[0].body;
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{ queue: "QUEUE", messages: [message("a", later.body)] },
|
|
270
|
+
ctx,
|
|
271
|
+
);
|
|
272
|
+
expect(again.self).toBe(again);
|
|
273
|
+
expect(Object.getOwnPropertyDescriptor(seen, "body").writable).toBe(true);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("2026-11-05: a consumer announces its queue to the store before the handler runs", async () => {
|
|
277
|
+
globalThis.__sbCompat = "2026-11-05";
|
|
278
|
+
sent = [];
|
|
279
|
+
let at;
|
|
280
|
+
await run(
|
|
281
|
+
{
|
|
282
|
+
queue() {
|
|
283
|
+
at = sent.length;
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
{ queue: "QUEUE", messages: [message("a", "1")] },
|
|
287
|
+
ctx,
|
|
288
|
+
);
|
|
289
|
+
expect(sent[0]).toMatchObject({ op: "queue.consumer", queue: "QUEUE" });
|
|
290
|
+
expect(at).toBe(1);
|
|
291
|
+
globalThis.__sbCompat = "2026-11-01";
|
|
292
|
+
sent = [];
|
|
293
|
+
await run({ queue() {} }, { queue: "QUEUE", messages: [message("a", "1")] }, ctx);
|
|
294
|
+
expect(sent).toEqual([]);
|
|
295
|
+
});
|
|
@@ -61,6 +61,8 @@ extern int sqlite3_bind_parameter_count(sqlite3_stmt*);
|
|
|
61
61
|
extern int sqlite3_stmt_readonly(sqlite3_stmt*);
|
|
62
62
|
extern int sqlite3_extended_errcode(sqlite3*);
|
|
63
63
|
extern int sqlite3_error_offset(sqlite3*);
|
|
64
|
+
extern const char* sqlite3_sql(sqlite3_stmt*);
|
|
65
|
+
extern int sqlite3_total_changes(sqlite3*);
|
|
64
66
|
|
|
65
67
|
// baronunread/sproutboat#176 — defined in render.js, alongside
|
|
66
68
|
// porf_native_fetch_read_value (which every inline-C block below already
|
|
@@ -2138,7 +2140,7 @@ static char* sb_sql_script(const char* path, const char* sql) {
|
|
|
2138
2140
|
if (idx < 0 && sb_db_capture_failed(path)) { sb_puts(&b, "{\"ok\":false,\"error\":\"" SB_LTX_FAILED "\",\"code\":10}"); return b.p; }
|
|
2139
2141
|
if (idx < 0) { sb_puts(&b, "{\"ok\":false,\"error\":\"cannot open database\"}"); return b.p; }
|
|
2140
2142
|
char* err = 0;
|
|
2141
|
-
int script_rc = 0, capture_failed = 0, quota_full = 0, failure_code = 0;
|
|
2143
|
+
int script_rc = 0, capture_failed = 0, quota_full = 0, failure_code = 0, failure_offset = -1;
|
|
2142
2144
|
char failure[512] = { 0 };
|
|
2143
2145
|
if (sb_ltx_states[idx].db) {
|
|
2144
2146
|
const char *next = sql;
|
|
@@ -2149,19 +2151,25 @@ static char* sb_sql_script(const char* path, const char* sql) {
|
|
|
2149
2151
|
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; }
|
|
2150
2152
|
// Keep the statement's error before capture runs statements of its own (it would
|
|
2151
2153
|
// read "not an error" after them).
|
|
2152
|
-
if (script_rc && !failure[0]) { snprintf(failure, sizeof(failure), "%s", sqlite3_errmsg(sb_dbs[idx])); failure_code = sqlite3_extended_errcode(sb_dbs[idx]); }
|
|
2154
|
+
if (script_rc && !failure[0]) { snprintf(failure, sizeof(failure), "%s", sqlite3_errmsg(sb_dbs[idx])); failure_code = sqlite3_extended_errcode(sb_dbs[idx]); failure_offset = sqlite3_error_offset(sb_dbs[idx]); }
|
|
2153
2155
|
if (st) sqlite3_finalize(st);
|
|
2154
2156
|
if (sb_ltx_after(idx)) { script_rc = 10; capture_failed = 1; }
|
|
2155
2157
|
if (!tail || tail <= next) break;
|
|
2156
2158
|
next = tail;
|
|
2159
|
+
// as sqlite3_exec does: the next statement starts at its first non-space character, so an
|
|
2160
|
+
// error offset counts from there
|
|
2161
|
+
while (*next == ' ' || *next == '\t' || *next == '\n' || *next == '\r' || *next == '\f') next++;
|
|
2157
2162
|
}
|
|
2158
|
-
} else
|
|
2163
|
+
} else {
|
|
2164
|
+
script_rc = sqlite3_exec(sb_dbs[idx], sql, 0, 0, &err);
|
|
2165
|
+
if (script_rc) { failure_code = sqlite3_extended_errcode(sb_dbs[idx]); failure_offset = sqlite3_error_offset(sb_dbs[idx]); }
|
|
2166
|
+
}
|
|
2159
2167
|
if (script_rc != 0) {
|
|
2160
2168
|
const char* m = quota_full ? SB_QUOTA_FULL : capture_failed ? SB_LTX_FAILED : failure[0] ? failure : err ? err : sqlite3_errmsg(sb_dbs[idx]);
|
|
2161
2169
|
sb_puts(&b, "{\"ok\":false,\"error\":");
|
|
2162
2170
|
sb_putjson_text(&b, m, strlen(m));
|
|
2163
2171
|
char code[32];
|
|
2164
|
-
snprintf(code, sizeof(code), ",\"code\":%d}", quota_full ? 13 : capture_failed ? 10 :
|
|
2172
|
+
snprintf(code, sizeof(code), ",\"code\":%d,\"offset\":%d}", quota_full ? 13 : capture_failed ? 10 : failure_code, quota_full || capture_failed ? -1 : failure_offset);
|
|
2165
2173
|
sb_puts(&b, code);
|
|
2166
2174
|
return b.p;
|
|
2167
2175
|
}
|
|
@@ -2187,6 +2195,39 @@ static char* sb_sql_error(sqlite3* db, const char* message, int code, int skip)
|
|
|
2187
2195
|
// D1 calls from compatibility date 2026-10-24 put this comment first: the statement must
|
|
2188
2196
|
// get exactly as many parameters as it has, like D1.
|
|
2189
2197
|
#define SB_D1_STRICT "/*sb-d1-strict*/"
|
|
2198
|
+
// 2026-11-03: a statement with more behind it runs all of them, as workerd does (the marker follows
|
|
2199
|
+
// SB_D1_STRICT). A query without a tail never looks past its first statement.
|
|
2200
|
+
#define SB_D1_MULTI "/*sb-d1-multi*/"
|
|
2201
|
+
#define SB_MULTI_PARAMS "When executing multiple SQL statements in a single call, only the last statement can have parameters."
|
|
2202
|
+
#define SB_NO_STATEMENT "SQL code did not contain a statement."
|
|
2203
|
+
// Runs st (the first statement, cached) and every statement after it but the last, inside one
|
|
2204
|
+
// savepoint: they all take effect or none does. Returns the last statement, prepared here (the
|
|
2205
|
+
// caller finalizes it and ends the savepoint), or NULL with *error set (savepoint rolled back).
|
|
2206
|
+
static sqlite3_stmt* sb_multi_open(int idx, sqlite3* db, sqlite3_stmt* st, const char* rest, char** error) {
|
|
2207
|
+
sqlite3_stmt* cur = st;
|
|
2208
|
+
int cached = 1;
|
|
2209
|
+
*error = 0;
|
|
2210
|
+
sqlite3_exec(db, "SAVEPOINT sb_d1_multi", 0, 0, 0);
|
|
2211
|
+
for (;;) {
|
|
2212
|
+
rest = sb_skip_ws(rest);
|
|
2213
|
+
if (!*rest) return cur;
|
|
2214
|
+
if (sqlite3_bind_parameter_count(cur) > 0) { *error = sb_sql_error(db, SB_MULTI_PARAMS, 0, 0); break; }
|
|
2215
|
+
if (sb_quota_frozen[idx] && !sqlite3_stmt_readonly(cur)) { *error = sb_sql_error(db, SB_QUOTA_FULL, 13, 0); break; }
|
|
2216
|
+
int rc;
|
|
2217
|
+
while ((rc = sqlite3_step(cur)) == SB_SQLITE_ROW) {}
|
|
2218
|
+
if (rc != SB_SQLITE_DONE) { *error = sb_sql_error(db, 0, -1, 0); break; }
|
|
2219
|
+
if (cached) sqlite3_reset(cur); else sqlite3_finalize(cur);
|
|
2220
|
+
cur = 0;
|
|
2221
|
+
sqlite3_stmt* next = 0; const char* tail = 0;
|
|
2222
|
+
if (sqlite3_prepare_v2(db, rest, -1, &next, &tail)) { *error = sb_sql_error(db, 0, -1, 0); break; }
|
|
2223
|
+
if (!next) { *error = sb_sql_error(db, SB_NO_STATEMENT, 0, 0); break; }
|
|
2224
|
+
cur = next; cached = 0; rest = tail ? tail : "";
|
|
2225
|
+
}
|
|
2226
|
+
if (cur) { if (cached) sqlite3_reset(cur); else sqlite3_finalize(cur); }
|
|
2227
|
+
sqlite3_exec(db, "ROLLBACK TO sb_d1_multi", 0, 0, 0);
|
|
2228
|
+
sqlite3_exec(db, "RELEASE sb_d1_multi", 0, 0, 0);
|
|
2229
|
+
return 0;
|
|
2230
|
+
}
|
|
2190
2231
|
|
|
2191
2232
|
// Run one statement. Returns malloc'd JSON:
|
|
2192
2233
|
// {"ok":true,"cols":[...],"rows":[[...]],"changes":n,"rowid":n}
|
|
@@ -2243,18 +2284,38 @@ static char* sb_sql_run_x(const char* path, const char* sql, const char* params,
|
|
|
2243
2284
|
if (idx < 0) { sb_puts(&b, "{\"ok\":false,\"error\":\"cannot open database\"}"); return b.p; }
|
|
2244
2285
|
sqlite3* db = sb_dbs[idx];
|
|
2245
2286
|
const int strict = strncmp(sql, SB_D1_STRICT, sizeof(SB_D1_STRICT) - 1) == 0;
|
|
2287
|
+
const int multi = strict && strncmp(sql + sizeof(SB_D1_STRICT) - 1, SB_D1_MULTI, sizeof(SB_D1_MULTI) - 1) == 0;
|
|
2288
|
+
const int skip = strict ? (int)sizeof(SB_D1_STRICT) - 1 + (multi ? (int)sizeof(SB_D1_MULTI) - 1 : 0) : 0;
|
|
2246
2289
|
// Durable Object SQL databases return BLOBs as D1 does.
|
|
2247
2290
|
const int d1 = strstr(path, "/d1/") != 0 || sb_is_do_sql(path);
|
|
2248
2291
|
sqlite3_stmt* st = sb_stmt_get(idx, db, sql);
|
|
2249
|
-
if (!st) return sb_sql_error(db, 0, -1,
|
|
2292
|
+
if (!st) return sb_sql_error(db, 0, -1, skip);
|
|
2293
|
+
// More than one statement: workerd runs them all and answers with the last one's rows; only the last
|
|
2294
|
+
// can take parameters. (sqlite3_sql is the first statement's text, a prefix of sql.)
|
|
2295
|
+
sqlite3_stmt* owned = 0;
|
|
2296
|
+
long long multi_before = 0;
|
|
2297
|
+
int in_multi = 0;
|
|
2298
|
+
if (multi) {
|
|
2299
|
+
const char* own = sqlite3_sql(st);
|
|
2300
|
+
size_t olen = own ? strlen(own) : 0;
|
|
2301
|
+
if (own && strncmp(sql, own, olen) == 0 && *sb_skip_ws(sql + olen)) {
|
|
2302
|
+
char* merr = 0;
|
|
2303
|
+
multi_before = sqlite3_total_changes(db);
|
|
2304
|
+
owned = sb_multi_open(idx, db, st, sql + olen, &merr);
|
|
2305
|
+
if (!owned) return merr;
|
|
2306
|
+
st = owned; in_multi = 1;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2250
2309
|
int bound = params && *params ? sb_bind_params(st, params) : 0;
|
|
2251
2310
|
if (sb_quota_frozen[idx] && !sqlite3_stmt_readonly(st)) {
|
|
2252
|
-
|
|
2253
|
-
|
|
2311
|
+
char* e = sb_sql_error(db, SB_QUOTA_FULL, 13, 0);
|
|
2312
|
+
if (owned) { sqlite3_finalize(owned); sqlite3_exec(db, "ROLLBACK TO sb_d1_multi", 0, 0, 0); sqlite3_exec(db, "RELEASE sb_d1_multi", 0, 0, 0); } else sqlite3_reset(st);
|
|
2313
|
+
return e;
|
|
2254
2314
|
}
|
|
2255
2315
|
if (strict && bound != sqlite3_bind_parameter_count(st)) {
|
|
2256
|
-
|
|
2257
|
-
|
|
2316
|
+
char* e = sb_sql_error(db, "Wrong number of parameter bindings for SQL query.", 0, 0);
|
|
2317
|
+
if (owned) { sqlite3_finalize(owned); sqlite3_exec(db, "ROLLBACK TO sb_d1_multi", 0, 0, 0); sqlite3_exec(db, "RELEASE sb_d1_multi", 0, 0, 0); } else sqlite3_reset(st);
|
|
2318
|
+
return e;
|
|
2258
2319
|
}
|
|
2259
2320
|
|
|
2260
2321
|
int ncol = sqlite3_column_count(st);
|
|
@@ -2330,25 +2391,41 @@ static char* sb_sql_run_x(const char* path, const char* sql, const char* params,
|
|
|
2330
2391
|
}
|
|
2331
2392
|
// A read-only statement changed nothing: sqlite3_changes still holds the last write's count.
|
|
2332
2393
|
if (native) {
|
|
2333
|
-
cells->changes = sqlite3_stmt_readonly(st) ? 0 : sqlite3_changes(db);
|
|
2394
|
+
cells->changes = in_multi ? (int)(sqlite3_total_changes(db) - multi_before) : sqlite3_stmt_readonly(st) ? 0 : sqlite3_changes(db);
|
|
2334
2395
|
cells->rowid = (long long)sqlite3_last_insert_rowid(db);
|
|
2335
2396
|
} else {
|
|
2336
2397
|
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));
|
|
2398
|
+
int k = snprintf(tail, 96, "],\"changes\":%d,\"rowid\":%lld}", in_multi ? (int)(sqlite3_total_changes(db) - multi_before) : sqlite3_stmt_readonly(st) ? 0 : sqlite3_changes(db), (long long)sqlite3_last_insert_rowid(db));
|
|
2338
2399
|
sb_put(&b, tail, (size_t)k);
|
|
2339
2400
|
}
|
|
2340
2401
|
// Reset, don't finalize: the statement stays compiled in sb_stmt_cache. Reset
|
|
2341
2402
|
// now (not just on next reuse) so a SELECT stops holding its read transaction.
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
// The statement's own error, kept before LTX capture runs: capture executes statements
|
|
2345
|
-
// of its own, after which SQLite reports "not an error".
|
|
2403
|
+
// The statement's own error, kept before anything runs another statement (a rollback, LTX capture):
|
|
2404
|
+
// after those SQLite reports "not an error".
|
|
2346
2405
|
char failure[512] = { 0 };
|
|
2347
2406
|
int failure_code = 0;
|
|
2348
2407
|
if (rc != SB_SQLITE_DONE && rc != SB_SQLITE_ROW) {
|
|
2349
2408
|
snprintf(failure, sizeof(failure), "%s", sqlite3_errmsg(db));
|
|
2350
2409
|
failure_code = sqlite3_extended_errcode(db);
|
|
2351
2410
|
}
|
|
2411
|
+
sqlite3_reset(st);
|
|
2412
|
+
if (owned) {
|
|
2413
|
+
sqlite3_finalize(owned);
|
|
2414
|
+
if (rc == SB_SQLITE_DONE || rc == SB_SQLITE_ROW) {
|
|
2415
|
+
// a deferred constraint can fail here, as it does at COMMIT
|
|
2416
|
+
if (sqlite3_exec(db, "RELEASE sb_d1_multi", 0, 0, 0)) {
|
|
2417
|
+
snprintf(failure, sizeof(failure), "%s", sqlite3_errmsg(db));
|
|
2418
|
+
failure_code = sqlite3_extended_errcode(db);
|
|
2419
|
+
rc = 1;
|
|
2420
|
+
sqlite3_exec(db, "ROLLBACK TO sb_d1_multi", 0, 0, 0);
|
|
2421
|
+
sqlite3_exec(db, "RELEASE sb_d1_multi", 0, 0, 0);
|
|
2422
|
+
}
|
|
2423
|
+
} else {
|
|
2424
|
+
sqlite3_exec(db, "ROLLBACK TO sb_d1_multi", 0, 0, 0);
|
|
2425
|
+
sqlite3_exec(db, "RELEASE sb_d1_multi", 0, 0, 0);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
if (cells && cells->used < 0) { free(b.p); sb_cells_free(cells); return sb_sql_error(db, "out of memory", 7, 0); }
|
|
2352
2429
|
const int capture_failed = sb_ltx_after(idx) != 0;
|
|
2353
2430
|
if (capture_failed) rc = 10;
|
|
2354
2431
|
if (rc != SB_SQLITE_DONE && rc != SB_SQLITE_ROW) {
|
|
@@ -4878,7 +4955,7 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
4878
4955
|
__sbD1Open(path, msg.strict === true);
|
|
4879
4956
|
__sbD1Quota(path);
|
|
4880
4957
|
if (msg.strict === true && Array.isArray(msg.lines)) {
|
|
4881
|
-
__sbD1ExecLines(path, msg.lines);
|
|
4958
|
+
__sbD1ExecLines(path, msg.lines, msg.parity === true);
|
|
4882
4959
|
return { ok: true };
|
|
4883
4960
|
}
|
|
4884
4961
|
const reply = JSON.parse(__sbSqlScriptRaw(path, String(msg.sql)));
|
|
@@ -4888,8 +4965,8 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
4888
4965
|
const strict = msg.strict === true;
|
|
4889
4966
|
__sbD1Open(path, strict);
|
|
4890
4967
|
__sbD1Quota(path);
|
|
4891
|
-
if (op === "d1.batch") return { ok: true, results: __sbD1Batch(path, msg.statements || [], strict) };
|
|
4892
|
-
const one = __sbD1Run(path, msg.sql, msg.params, strict);
|
|
4968
|
+
if (op === "d1.batch") return { ok: true, results: __sbD1Batch(path, msg.statements || [], strict, msg.parity === true) };
|
|
4969
|
+
const one = __sbD1Run(path, msg.sql, msg.params, strict, msg.parity === true);
|
|
4893
4970
|
return { ok: true, results: one.results, columns: one.columns, rows: one.rows, native: one.native, meta: one.meta, success: true };
|
|
4894
4971
|
}
|
|
4895
4972
|
|
|
@@ -5211,6 +5288,10 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
5211
5288
|
|
|
5212
5289
|
if (op === "queue.metrics") return { ok: true, metrics: __sbQueueBacklog(store, msg.queue) };
|
|
5213
5290
|
|
|
5291
|
+
// A consumer announces itself (the broker uses it to pick the delivery rules of the app's
|
|
5292
|
+
// compatibility date); the embedded store reads the date itself.
|
|
5293
|
+
if (op === "queue.consumer") return { ok: true };
|
|
5294
|
+
|
|
5214
5295
|
if (op === "do.storage.get") {
|
|
5215
5296
|
const r = __sbSql(store, "SELECT value FROM do_storage WHERE cls = ? AND id = ? AND key = ?", [
|
|
5216
5297
|
msg.cls,
|
|
@@ -5436,10 +5517,10 @@ function __sbD1Bytes(cell) {
|
|
|
5436
5517
|
for (let i = 0; i + 1 < hex.length; i += 2) out += String.fromCharCode(parseInt(hex.slice(i, i + 2), 16));
|
|
5437
5518
|
return out;
|
|
5438
5519
|
}
|
|
5439
|
-
function __sbD1Run(path, sql, params, strict) {
|
|
5520
|
+
function __sbD1Run(path, sql, params, strict, multi) {
|
|
5440
5521
|
if (strict) {
|
|
5441
5522
|
// Rows come back as arrays, built in C (see __sbSqlRowsRaw); the header is small JSON.
|
|
5442
|
-
const pair = __sbSqlRowsRaw(path, "/*sb-d1-strict*/" + String(sql), JSON.stringify(params || []));
|
|
5523
|
+
const pair = __sbSqlRowsRaw(path, multi ? "/*sb-d1-strict*//*sb-d1-multi*/" + String(sql) : "/*sb-d1-strict*/" + String(sql), JSON.stringify(params || []));
|
|
5443
5524
|
if (pair[0] !== "") {
|
|
5444
5525
|
const head = JSON.parse(pair[0]);
|
|
5445
5526
|
if (head.ok === false) throw __sbD1Error(head);
|
|
@@ -5480,11 +5561,11 @@ function __sbD1Undo(path, savepoint) {
|
|
|
5480
5561
|
return null;
|
|
5481
5562
|
}
|
|
5482
5563
|
// One transaction, like D1: a statement that fails undoes the ones before it.
|
|
5483
|
-
function __sbD1Batch(path, list, strict) {
|
|
5564
|
+
function __sbD1Batch(path, list, strict, multi) {
|
|
5484
5565
|
const results = [];
|
|
5485
5566
|
__sbD1Control(path, "SAVEPOINT sb_d1_batch", strict);
|
|
5486
5567
|
try {
|
|
5487
|
-
for (let i = 0; i < list.length; i++) results.push(__sbD1Run(path, list[i].sql, list[i].params, strict));
|
|
5568
|
+
for (let i = 0; i < list.length; i++) results.push(__sbD1Run(path, list[i].sql, list[i].params, strict, multi));
|
|
5488
5569
|
} catch (err) {
|
|
5489
5570
|
__sbD1Undo(path, "sb_d1_batch");
|
|
5490
5571
|
throw err;
|
|
@@ -5502,13 +5583,15 @@ function __sbD1Release(path, savepoint, strict) {
|
|
|
5502
5583
|
throw strict ? __sbD1Error(r) : new Error("sqlite: " + r.error);
|
|
5503
5584
|
}
|
|
5504
5585
|
// exec() from 2026-10-24: line by line as workerd runs it, all or nothing.
|
|
5505
|
-
function __sbD1ExecLines(path, lines) {
|
|
5586
|
+
function __sbD1ExecLines(path, lines, parity) {
|
|
5506
5587
|
__sbD1Control(path, "SAVEPOINT sb_d1_exec", true);
|
|
5507
5588
|
for (let i = 0; i < lines.length; i++) {
|
|
5508
5589
|
const reply = JSON.parse(__sbSqlScriptRaw(path, String(lines[i])));
|
|
5509
5590
|
if (reply.ok === false) {
|
|
5510
5591
|
if (Number(reply.code) !== 10) __sbD1Undo(path, "sb_d1_exec");
|
|
5511
|
-
|
|
5592
|
+
// parity (2026-11-03): D1's own wording after the line, " at offset N: SQLITE_ERROR" and all
|
|
5593
|
+
const detail = parity ? __sbD1Error(reply).message.slice(10) : reply.error;
|
|
5594
|
+
throw new Error("D1_EXEC_ERROR: Error in line " + (i + 1) + ": " + lines[i] + ": " + detail);
|
|
5512
5595
|
}
|
|
5513
5596
|
}
|
|
5514
5597
|
__sbD1Release(path, "sb_d1_exec", true);
|
|
@@ -5618,7 +5701,7 @@ __sbD1QueryDirect = function (db, sql, params) {
|
|
|
5618
5701
|
const path = __sbD1Path(String(db));
|
|
5619
5702
|
__sbD1Open(path, true);
|
|
5620
5703
|
__sbD1Quota(path);
|
|
5621
|
-
return __sbD1Run(path, sql, params, true);
|
|
5704
|
+
return __sbD1Run(path, sql, params, true, __sbCompatDate() >= "2026-11-03");
|
|
5622
5705
|
};
|
|
5623
5706
|
|
|
5624
5707
|
// The same dispatch without the JSON text either side: D1 calls take this (see __sbRpcDirect).
|
|
@@ -5695,7 +5778,9 @@ function __sbQueueBacklog(store, queue) {
|
|
|
5695
5778
|
}
|
|
5696
5779
|
|
|
5697
5780
|
function __sbQueueAttemptCap(settings) {
|
|
5698
|
-
|
|
5781
|
+
const retries = (settings.maxRetries !== undefined ? Number(settings.maxRetries) : 3) + 1;
|
|
5782
|
+
// From 2026-11-05 a lost delivery counts as a retry, so there is no floor of 5.
|
|
5783
|
+
return __sbQueuesFixed() ? retries : Math.max(5, retries);
|
|
5699
5784
|
}
|
|
5700
5785
|
|
|
5701
5786
|
// How long a queue's due messages wait to fill a batch (max_batch_timeout), in ms.
|
|
@@ -5722,11 +5807,13 @@ async function __sbQueuePass(handlers, queues) {
|
|
|
5722
5807
|
try {
|
|
5723
5808
|
// A message whose deliveries never came back (the worker died or timed out each time)
|
|
5724
5809
|
// stops after 5 attempts, or max_retries + 1 when that is more.
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5729
|
-
|
|
5810
|
+
if (__sbQueuesFixed()) __sbQueueRetire(store, name, settings, now);
|
|
5811
|
+
else
|
|
5812
|
+
__sbSql(store, "UPDATE mq SET dead=1 WHERE queue=? AND visible_at<=? AND attempts>=?", [
|
|
5813
|
+
name,
|
|
5814
|
+
now,
|
|
5815
|
+
__sbQueueAttemptCap(settings),
|
|
5816
|
+
]);
|
|
5730
5817
|
due = __sbSql(
|
|
5731
5818
|
store,
|
|
5732
5819
|
"SELECT id,body,attempts,sent_at,visible_at FROM mq WHERE queue=? AND dead=0 AND visible_at<=? ORDER BY visible_at LIMIT ?",
|
|
@@ -5763,13 +5850,45 @@ async function __sbQueuePass(handlers, queues) {
|
|
|
5763
5850
|
if (p && __sbIsFn(p.then)) __sbTasks.push(p);
|
|
5764
5851
|
},
|
|
5765
5852
|
};
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5853
|
+
// From 2026-11-05 a consumer that runs past the 30 s lease keeps its messages: the lease
|
|
5854
|
+
// is renewed while it runs, so they are not delivered a second time or lose their ack.
|
|
5855
|
+
const beat = __sbQueuesFixed()
|
|
5856
|
+
? setInterval(() => {
|
|
5857
|
+
__sbSql(store, "UPDATE mq SET visible_at=? WHERE queue=? AND claim_token=? AND visible_at>?", [Date.now() + 30000, name, claim, Date.now()]);
|
|
5858
|
+
}, 10000)
|
|
5859
|
+
: null;
|
|
5860
|
+
let result;
|
|
5861
|
+
try {
|
|
5862
|
+
result = __sbRunQueueBatch(handlers, { queue: name, messages, metadata }, ctx);
|
|
5863
|
+
if (result && __sbIsFn(result.then)) result = await result;
|
|
5864
|
+
if (__sbTasks.length) await __sbDrainWaitUntil(__sbTasks);
|
|
5865
|
+
} finally {
|
|
5866
|
+
if (beat !== null) clearInterval(beat);
|
|
5867
|
+
}
|
|
5769
5868
|
__sbQueueSettle(store, name, claim, result);
|
|
5770
5869
|
}
|
|
5771
5870
|
}
|
|
5772
5871
|
|
|
5872
|
+
// From 2026-11-05, the messages whose last allowed delivery never came back (the worker died or
|
|
5873
|
+
// timed out): they leave the queue as a message past max_retries does, into the dead letter
|
|
5874
|
+
// queue when there is one, or dropped. Runs inside the claim transaction.
|
|
5875
|
+
function __sbQueueRetire(store, name, settings, now) {
|
|
5876
|
+
const rows = __sbSql(store, "SELECT id FROM mq WHERE queue=? AND dead=0 AND visible_at<=? AND attempts>=?", [
|
|
5877
|
+
name,
|
|
5878
|
+
now,
|
|
5879
|
+
__sbQueueAttemptCap(settings),
|
|
5880
|
+
]).rows;
|
|
5881
|
+
if (rows.length === 0) return;
|
|
5882
|
+
if (settings.deadLetterQueue === undefined) {
|
|
5883
|
+
__sbSql(store, "UPDATE mq SET dead=1 WHERE queue=? AND visible_at<=? AND attempts>=?", [name, now, __sbQueueAttemptCap(settings)]);
|
|
5884
|
+
return;
|
|
5885
|
+
}
|
|
5886
|
+
const part = __sbQueuePart(settings.deadLetterQueue);
|
|
5887
|
+
for (let i = 0; i < rows.length; i++)
|
|
5888
|
+
__sbSql(store, "UPDATE mq SET queue=?,visible_at=?,attempts=0,claim_token='',dead=0 WHERE queue=? AND id=?", [part, now, name, rows[i][0]]);
|
|
5889
|
+
if (__sbLocal !== null && __sbLocal.queues.indexOf(part) !== -1) __sbHostDue(now);
|
|
5890
|
+
}
|
|
5891
|
+
|
|
5773
5892
|
// Applies a batch's outcome to the claimed rows. `dead` (past max_retries) move to the dead
|
|
5774
5893
|
// letter queue `deadLetter` as new messages, or are marked dead; `delays` gives each retry's
|
|
5775
5894
|
// delay in seconds (5 when the runtime sends none). A row whose claim expired meanwhile was
|
|
@@ -5863,7 +5982,9 @@ async function __sbAlarmPass() {
|
|
|
5863
5982
|
try {
|
|
5864
5983
|
const instance = __sbGetDOInstance(cls, id);
|
|
5865
5984
|
if (__sbIsFn(instance.alarm)) {
|
|
5866
|
-
const res = __sbDOGated()
|
|
5985
|
+
const res = __sbDOGated()
|
|
5986
|
+
? __sbDOEnter(instance.__sbGate, () => __sbDOAlarmCall(instance, cls, id, due.rows[i][2], null))
|
|
5987
|
+
: __sbDOAlarmCall(instance, cls, id, due.rows[i][2], null);
|
|
5867
5988
|
if (res && __sbIsFn(res.then)) await res;
|
|
5868
5989
|
if (instance.__sbTasks.length) await __sbDrainWaitUntil(instance.__sbTasks);
|
|
5869
5990
|
}
|
package/test/native-d1.ts
CHANGED
|
@@ -373,6 +373,77 @@ int main(void) {
|
|
|
373
373
|
(e: Error) => e.message === 'D1_EXEC_ERROR: Error in line 2: SELEC 2: near "SELEC": syntax error',
|
|
374
374
|
);
|
|
375
375
|
assert.equal(ops.__sbD1Run(ex, "SELECT count(*) AS n FROM x", [], false).results[0].n, 2, "exec rolled back");
|
|
376
|
+
// From 2026-11-03 (parity): D1's text, with the failing statement's own offset (counted from its first
|
|
377
|
+
// non-space character) and SQLite's result code.
|
|
378
|
+
const parityError = (lines: string[]) => {
|
|
379
|
+
try {
|
|
380
|
+
ops.__sbD1ExecLines(ex, lines, true);
|
|
381
|
+
} catch (e) {
|
|
382
|
+
return e instanceof Error ? e.message : String(e);
|
|
383
|
+
}
|
|
384
|
+
return "did not throw";
|
|
385
|
+
};
|
|
386
|
+
const parityCases: [string[], string][] = [
|
|
387
|
+
[
|
|
388
|
+
["INSERT INTO x VALUES (3); SELEC 2"],
|
|
389
|
+
'Error in line 1: INSERT INTO x VALUES (3); SELEC 2: near "SELEC": syntax error at offset 0: SQLITE_ERROR',
|
|
390
|
+
],
|
|
391
|
+
[["SELEC 2"], 'Error in line 1: SELEC 2: near "SELEC": syntax error at offset 0: SQLITE_ERROR'],
|
|
392
|
+
[["SELECT 1;", " SELEC 3"], 'Error in line 2: SELEC 3: near "SELEC": syntax error at offset 2: SQLITE_ERROR'],
|
|
393
|
+
[["/*c*/ SELEC 1"], 'Error in line 1: /*c*/ SELEC 1: near "SELEC": syntax error at offset 6: SQLITE_ERROR'],
|
|
394
|
+
[["SELECT * FROM nope"], "Error in line 1: SELECT * FROM nope: no such table: nope: SQLITE_ERROR"],
|
|
395
|
+
[["SELECT 1,"], "Error in line 1: SELECT 1,: incomplete input: SQLITE_ERROR"],
|
|
396
|
+
];
|
|
397
|
+
for (const [lines, text] of parityCases) assert.equal(parityError(lines), "D1_EXEC_ERROR: " + text);
|
|
398
|
+
ops.__sbD1ExecLines(ex, ["CREATE TABLE ux (a UNIQUE)", "INSERT INTO ux VALUES (1)"], true);
|
|
399
|
+
assert.equal(
|
|
400
|
+
parityError(["INSERT INTO ux VALUES (1)"]),
|
|
401
|
+
"D1_EXEC_ERROR: Error in line 1: INSERT INTO ux VALUES (1): UNIQUE constraint failed: ux.a: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_UNIQUE)",
|
|
402
|
+
);
|
|
403
|
+
// From 2026-11-03 (multi): a statement with more behind it runs them all, in one savepoint, and answers with the last.
|
|
404
|
+
const mdb = join(work, "data/d1/MULTI.sqlite");
|
|
405
|
+
const multi = (sql: string, params: unknown[] = []) => ops.__sbD1Run(mdb, sql, params, true, true);
|
|
406
|
+
const multiError = (sql: string, params: unknown[] = []) => {
|
|
407
|
+
try {
|
|
408
|
+
multi(sql, params);
|
|
409
|
+
} catch (e) {
|
|
410
|
+
return e instanceof Error ? e.message : String(e);
|
|
411
|
+
}
|
|
412
|
+
return "did not throw";
|
|
413
|
+
};
|
|
414
|
+
multi("CREATE TABLE m (id INTEGER PRIMARY KEY, name TEXT, v TEXT UNIQUE)");
|
|
415
|
+
const two = multi("INSERT INTO m (id) VALUES (7); INSERT INTO m (id) VALUES (8)");
|
|
416
|
+
assert.deepEqual([two.meta.changes, two.meta.last_row_id, two.rows.length], [2, 8, 0]);
|
|
417
|
+
assert.equal(multi("INSERT INTO m (id) VALUES (10); UPDATE m SET name = 'x'").meta.changes, 4);
|
|
418
|
+
assert.deepEqual(multi("SELECT 1 AS a; SELECT 2 AS b").columns, ["b"]);
|
|
419
|
+
assert.deepEqual(multi("SELECT 1; SELECT ? AS p", [5]).rows, [[5]]);
|
|
420
|
+
assert.equal(multi("INSERT INTO m (id) VALUES (30); INSERT INTO m (id) VALUES (31); ").meta.changes, 2);
|
|
421
|
+
const mcount = () => ops.__sbD1Run(mdb, "SELECT count(*) FROM m", [], true, true).rows[0][0];
|
|
422
|
+
const mbefore = mcount();
|
|
423
|
+
assert.equal(
|
|
424
|
+
multiError("INSERT INTO m (id) VALUES (40); garbage"),
|
|
425
|
+
'D1_ERROR: near "garbage": syntax error at offset 0: SQLITE_ERROR',
|
|
426
|
+
);
|
|
427
|
+
assert.match(
|
|
428
|
+
multiError("INSERT INTO m (id, v) VALUES (41, 'a'); INSERT INTO m (id, v) VALUES (42, 'a')"),
|
|
429
|
+
/^D1_ERROR: UNIQUE constraint failed: m\.v: SQLITE_CONSTRAINT/,
|
|
430
|
+
);
|
|
431
|
+
assert.equal(
|
|
432
|
+
multiError("INSERT INTO m (id) VALUES (?); INSERT INTO m (id) VALUES (?)", [50, 51]),
|
|
433
|
+
"D1_ERROR: When executing multiple SQL statements in a single call, only the last statement can have parameters.",
|
|
434
|
+
);
|
|
435
|
+
assert.equal(multiError("SELECT 1;;"), "D1_ERROR: SQL code did not contain a statement.");
|
|
436
|
+
assert.equal(multiError("SELECT 1; -- c"), "D1_ERROR: SQL code did not contain a statement.");
|
|
437
|
+
assert.equal(
|
|
438
|
+
multiError("SELECT 1; SELECT ? AS p", [1, 2]),
|
|
439
|
+
"D1_ERROR: Wrong number of parameter bindings for SQL query.",
|
|
440
|
+
);
|
|
441
|
+
assert.equal(mcount(), mbefore, "a failing script left nothing behind");
|
|
442
|
+
assert.equal(
|
|
443
|
+
ops.__sbD1Run(mdb, "INSERT INTO m (id) VALUES (60); INSERT INTO m (id) VALUES (61)", [], true, false).meta.changes,
|
|
444
|
+
1,
|
|
445
|
+
"without multi only the first runs",
|
|
446
|
+
);
|
|
376
447
|
const db2 = join(work, "data/d1/OPS.sqlite");
|
|
377
448
|
ops.__sbD1Run(db2, "CREATE TABLE p (id INTEGER PRIMARY KEY)", [], false);
|
|
378
449
|
ops.__sbD1Run(db2, "CREATE TABLE c (id INTEGER PRIMARY KEY, p INTEGER REFERENCES p(id))", [], false);
|