@sproutboat/runtime 0.35.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 +36 -0
- package/package.json +1 -1
- package/src/d1-shim.test.js +215 -0
- package/src/native-fetch-prelude.js +989 -87
- package/src/queues.test.js +136 -0
- package/src/transport-embedded.js +156 -35
- package/test/concurrent-driver.ts +291 -0
- package/test/fixtures/als-app.js +136 -0
- package/test/fixtures/concurrent-app.js +229 -0
- package/test/native-async-local-storage.ts +97 -0
- package/test/native-concurrent-requests.ts +87 -0
- package/test/native-cpu-requests.ts +156 -0
- 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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.37.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 378f1a2: `crypto.getRandomValues` fills every byte of the view, as WebCrypto does. It filled only the low byte of each element, so a `Uint32Array` came back with values below 256 and any code that draws an integer from `getRandomValues(new Uint32Array(1))` got the same bucket every time. It now also throws a `TypeMismatchError` for a `Float32Array`, a `Float64Array`, a `DataView` or a non-view, and the 65536 limit counts bytes, not elements. Found while testing third-party packages.
|
|
8
|
+
|
|
9
|
+
## 0.37.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 4c4c25d: D1 answers like workerd in four more ways, from compatibility date 2026-11-03 (earlier dates answer exactly as before). A PRAGMA outside D1's allow-list (`table_list`, `table_info`, `table_xinfo`, `index_list`, `index_info`, `index_xinfo`, `foreign_keys`, `foreign_key_list`, `foreign_key_check`, `quick_check`, `optimize`, `defer_foreign_keys`, `case_sensitive_like`, `recursive_triggers`, `data_version`, `legacy_alter_table`, `reverse_unordered_selects`, `ignore_check_constraints`; the name as written, schema `main` only) fails with `D1_ERROR: not authorized: SQLITE_AUTH`, in a query, a batch statement or an `exec()` line, on every transport; the check runs in the shim and only looks at a statement that starts with P (or a comment or `;`). `PRAGMA foreign_keys = OFF` no longer sticks in a standalone binary (D1 runs every call in a transaction, where it is a no-op). An `exec()` error reads `... : near "SELEC": syntax error at offset 0: SQLITE_ERROR`, with the offset counted from the failing statement and SQLite's result code (the embedded `sb_sql_script` now returns both; the broker counts statement starts itself, because bun:sqlite reports the offset in the whole string). A statement with nothing in it (`;`, a comment) fails with `SQL code did not contain a statement.`, and `exec("")` with `D1_EXEC_ERROR: Error in line 1: : No SQL statements detected.`. `prepare("A; B")` runs every statement and answers with the last one (all or nothing, `meta.changes` summed, only the last statement may take parameters, a tail of comments or `;;` is "SQL code did not contain a statement."); the embedded transport does it in C only when the first statement has a non-blank tail, so a single statement costs one `strlen`. On the broker transport every whole-number parameter binds as a REAL, as in workerd (`typeof(?)` is real, `7 / 2` is 3.5, a number in a TEXT column reads "7.0"), by wrapping the placeholder in `CAST(? AS REAL)`. `meta.rows_read` is unchanged: workerd counts rows scanned with Cloudflare's SQLite fork, which upstream SQLite cannot reproduce.
|
|
14
|
+
- 2c19adf: Durable Objects follow workerd more closely from compatibility date 2026-11-04; earlier dates answer as before.
|
|
15
|
+
|
|
16
|
+
- `alarm(alarmInfo)` gets `{ retryCount, isRetry }`. The count lives in the prelude, per object and scheduled time (the broker, the embedded timer and sproutd all deliver through it), and starts over when a run succeeds or the alarm is set for a new time. A failed alarm now answers the broker's trigger with 500 instead of 204, so the broker retries it (it used to treat the failure as delivered).
|
|
17
|
+
- `ctx.storage.sql.exec()` runs every statement of a script and returns the last one's cursor, takes parameters only in the last statement, refuses an empty script, and gives workerd's errors for the statements its authorizer refuses (other PRAGMAs, ATTACH, temporary tables, VACUUM, `sqlite_version()`). Bindings coerce as workerd's do (a boolean binds as the text "true", `undefined` as NULL, a BigInt is a TypeError). A cursor's iterator is a new iterator over the same rows.
|
|
18
|
+
- `deleteAll()` on a SQLite-backed object drops its tables, views and triggers as well as its keys.
|
|
19
|
+
- `ctx.storage.kv` (the synchronous key-value API); `getCurrentBookmark`, `getBookmarkForTime` and `onNextSessionRestoreBookmark` exist and reject (point-in-time recovery is not available); `ctx.abort()`; `ctx.props`.
|
|
20
|
+
- Concurrent `storage.transaction()` calls on one object run one after another, so a read-modify-write counter no longer loses updates. `txn.deleteAll()` throws as workerd's does. A transaction used after `txn.rollback()` or after its closure finished rejects with workerd's messages ("Cannot get() on rolled back transaction", "Cannot call put() on transaction that has already committed: did you move `txn` outside of the closure?"); `rollback()` itself stays allowed.
|
|
21
|
+
- Structured clone in storage keeps shared and circular references, restores Error subclasses (`RangeError`, ...) and invalid Dates.
|
|
22
|
+
- `storage.get([...])` returns its Map in key order; `put({ k: undefined })` skips the entry; `put(["a", 1])` and `put()` throw as workerd's do; `list()` refuses `start` with `startAfter` and takes a fractional `limit`; `setAlarm()` refuses NaN, an invalid Date, a time at or before 0, and a class with no `alarm()` method.
|
|
23
|
+
- `DurableObjectId` is a class (`constructor.name`, `instanceof`), `id.equals()` and `namespace.get()` refuse what is not an id, `blockConcurrencyWhile()` refuses a non-function.
|
|
24
|
+
- 1f0622b: Queues parity, from compatibility date 2026-11-05 (earlier dates answer as before). The v8 content type keeps shared and circular references, array holes and invalid dates, and an Error keeps its TypeError or RangeError type (it used to throw "a circular value could not be cloned", copy a shared object, fill holes with undefined and turn an invalid Date into 1970). A message's id, timestamp, body and attempts are read-only, as in workerd. send() and sendBatch() check arguments as workerd does: no arguments, an options argument that is not an object, a sendBatch item that is not an object, `bytes` given an ArrayBuffer (workerd takes an ArrayBufferView), and a delaySeconds outside 0 to 86400, which was clamped or kept. A delivery that never comes back (the worker died or timed out) now counts as a retry: the message is delivered at most max_retries + 1 times, and then goes to the dead letter queue, where it used to be retried up to 5 times and marked dead without reaching it. The consumer announces itself to the broker (`queue.consumer`) so the broker applies this only to apps on the new date. On the embedded transport a consumer that runs past the 30 s lease renews it, so its messages are no longer delivered twice with their acks lost.
|
|
25
|
+
|
|
26
|
+
Over the broker (`sproutboat dev` and deployed apps) a queue delivery is now decoded as UTF-8 from 2026-11-05: a message body with non-ASCII text ("héllo ☃") used to reach the consumer as mojibake, because the native server hands the delivery over one character per byte.
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- 354a405: `DOMException` is now a real, constructible global. Porffor's own `DOMException` reports `typeof` "function" but throws "not a constructor", so `new DOMException("m", "AbortError")` failed and any library that tests `error instanceof DOMException` or builds one could not run. The prelude installs a class that extends Error with the legacy numeric `code`, the `ABORT_ERR`-style constants, the `{ name, cause }` options form, and `AbortSignal` abort and timeout reasons and WebCrypto errors are instances of it. Found while testing third-party packages (timers, queue, rpc, websockets).
|
|
31
|
+
|
|
32
|
+
## 0.36.0
|
|
33
|
+
|
|
34
|
+
### Minor Changes
|
|
35
|
+
|
|
36
|
+
- 7a9c1f9: `AsyncLocalStorage` (`import { AsyncLocalStorage } from "node:async_hooks"`, once the CLI maps the import), as Workers has it: `run`, `exit`, `getStore` and the static `snapshot`. `enterWith` and `disable` throw, as there. The static `bind` is not provided: Porffor compiles a `.bind(` call on a function value to `Function.prototype.bind`, so use `snapshot()`. The toolchain patches Porffor's job queue and timers to carry one current-context value: a promise reaction (then, await, a coroutine resume, a native response callback) and a timer record it when they are created and set it when they run, so the store follows a request across awaits, microtasks, timers, intervals, async fetch and WebSocket callbacks and `ctx.waitUntil`, and requests interleaved in one process never see each other's. Cost: a reaction is 56 bytes, not 40, and each job does one load. The context is only ever set by `AsyncLocalStorage`. Nothing an existing app observes changes, so there is no new compatibility date: `node:async_hooks` did not work at any date before (the import built and `new AsyncLocalStorage()` threw). New native test `test:native-async-local-storage` (needs `SB_CLI`).
|
|
37
|
+
- a68d923: `x-sb-cpu-ms` is per request when requests interleave in one process. A request is charged the thread CPU time of the slices in which it ran (the toolchain's async context carries a request id through awaits, reactions and timers, and the clock is read only when the running request changes, so a request that runs alone costs two reads). Before, the stamp was the process CPU delta across the handler, which is right only for a handler that answers synchronously, so an async handler was never stamped. From compatibility date `2026-11-02` an async handler is stamped too (its response is a string with no Set-Cookie, as for a sync one); earlier dates still send no stamp for an async handler. A synchronous handler's stamp is now thread time, which leaves out the egress threads. New native test `test:native-cpu-requests`: a 200 ms spinner among eight waiters is charged its 200 ms and the waiters under 60 ms, two spinners are each charged their own, and the date gate.
|
|
38
|
+
|
|
3
39
|
## 0.35.0
|
|
4
40
|
|
|
5
41
|
### Minor Changes
|
package/package.json
CHANGED
package/src/d1-shim.test.js
CHANGED
|
@@ -256,3 +256,218 @@ test("a statement is a transaction keyword exactly as the regular expression it
|
|
|
256
256
|
];
|
|
257
257
|
for (const text of cases) expect([text, isTx(text)]).toEqual([text, old(text)]);
|
|
258
258
|
});
|
|
259
|
+
|
|
260
|
+
// From 2026-11-03: workerd's PRAGMA allow-list (the strict path only; exec lines and batch statements too).
|
|
261
|
+
const ALLOWED_PRAGMAS = [
|
|
262
|
+
"table_list",
|
|
263
|
+
"table_info(t)",
|
|
264
|
+
"table_xinfo(t)",
|
|
265
|
+
"index_list(t)",
|
|
266
|
+
"index_info(i)",
|
|
267
|
+
"index_xinfo(i)",
|
|
268
|
+
"foreign_keys",
|
|
269
|
+
"foreign_key_list(t)",
|
|
270
|
+
"foreign_key_check",
|
|
271
|
+
"quick_check",
|
|
272
|
+
"optimize",
|
|
273
|
+
"defer_foreign_keys",
|
|
274
|
+
"case_sensitive_like",
|
|
275
|
+
"recursive_triggers",
|
|
276
|
+
"data_version",
|
|
277
|
+
"legacy_alter_table",
|
|
278
|
+
"reverse_unordered_selects",
|
|
279
|
+
"ignore_check_constraints",
|
|
280
|
+
];
|
|
281
|
+
const NOT_AUTHORIZED = "D1_ERROR: not authorized: SQLITE_AUTH";
|
|
282
|
+
test("from 2026-11-03: PRAGMAs outside D1's allow-list are refused, the allowed ones run", async () => {
|
|
283
|
+
const DB = fresh("2026-11-03");
|
|
284
|
+
await DB.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, a TEXT);\nCREATE INDEX i ON t (a)");
|
|
285
|
+
for (const pragma of ALLOWED_PRAGMAS)
|
|
286
|
+
expect(await failure(() => DB.prepare(`PRAGMA ${pragma}`).all())).not.toBe(NOT_AUTHORIZED);
|
|
287
|
+
expect((await DB.prepare("PRAGMA table_info(t)").raw()).map((c) => c[1])).toEqual(["id", "a"]);
|
|
288
|
+
expect(await DB.prepare("PRAGMA foreign_keys").first("foreign_keys")).toBe(1);
|
|
289
|
+
for (const sql of [
|
|
290
|
+
"PRAGMA user_version",
|
|
291
|
+
"PRAGMA user_version = 3",
|
|
292
|
+
"pragma integrity_check",
|
|
293
|
+
"PRAGMA journal_mode",
|
|
294
|
+
"PRAGMA page_count",
|
|
295
|
+
"PRAGMA TABLE_INFO(t)",
|
|
296
|
+
"PRAGMA Foreign_Keys",
|
|
297
|
+
"PRAGMA nonexistent",
|
|
298
|
+
"PRAGMA temp.table_info(t)",
|
|
299
|
+
"PRAGMA main.user_version",
|
|
300
|
+
' PRAGMA "user_version"',
|
|
301
|
+
"/* x */ PRAGMA user_version",
|
|
302
|
+
"-- x\nPRAGMA user_version",
|
|
303
|
+
])
|
|
304
|
+
expect(await failure(() => DB.prepare(sql).run())).toBe(NOT_AUTHORIZED);
|
|
305
|
+
expect(await failure(() => DB.prepare("PRAGMA main.table_info(t)").all())).not.toBe(NOT_AUTHORIZED);
|
|
306
|
+
expect(await failure(() => DB.prepare('PRAGMA "table_info"(t)').all())).not.toBe(NOT_AUTHORIZED);
|
|
307
|
+
expect(await failure(() => DB.prepare("PRAGMA nosuch.user_version").all())).not.toBe(NOT_AUTHORIZED);
|
|
308
|
+
// not a PRAGMA at all
|
|
309
|
+
expect((await DB.prepare("SELECT 'PRAGMA user_version' AS p").first()).p).toBe("PRAGMA user_version");
|
|
310
|
+
expect(await failure(() => DB.batch([DB.prepare("SELECT 1"), DB.prepare("PRAGMA user_version")]))).toBe(
|
|
311
|
+
NOT_AUTHORIZED,
|
|
312
|
+
);
|
|
313
|
+
expect(await failure(() => DB.exec("SELECT 1;\nPRAGMA user_version"))).toBe(
|
|
314
|
+
"D1_EXEC_ERROR: Error in line 2: PRAGMA user_version: not authorized: SQLITE_AUTH",
|
|
315
|
+
);
|
|
316
|
+
expect(await failure(() => DB.exec("PRAGMA foreign_keys = ON; PRAGMA user_version = 3"))).toBe(
|
|
317
|
+
"D1_EXEC_ERROR: Error in line 1: PRAGMA foreign_keys = ON; PRAGMA user_version = 3: not authorized: SQLITE_AUTH",
|
|
318
|
+
);
|
|
319
|
+
// a ';' inside a string does not start a statement
|
|
320
|
+
expect((await DB.exec("SELECT ';PRAGMA user_version'")).count).toBe(1);
|
|
321
|
+
expect((await DB.exec("PRAGMA foreign_keys = ON; PRAGMA table_info(t)")).count).toBe(1);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("before 2026-11-03: every PRAGMA still runs", async () => {
|
|
325
|
+
const DB = fresh("2026-11-02");
|
|
326
|
+
expect(await DB.prepare("PRAGMA user_version").first("user_version")).toBe(0);
|
|
327
|
+
expect((await DB.exec("PRAGMA user_version = 3")).count).toBe(1);
|
|
328
|
+
expect(await DB.prepare("PRAGMA user_version").first("user_version")).toBe(3);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("exec errors read as D1's: the statement's offset and SQLite's code, from 2026-11-03", async () => {
|
|
332
|
+
const DB = fresh("2026-11-03");
|
|
333
|
+
const text = (sql) => failure(() => DB.exec(sql));
|
|
334
|
+
await DB.exec("CREATE TABLE t (id INTEGER)");
|
|
335
|
+
expect(await text("INSERT INTO t (id) VALUES (1); SELEC 2")).toBe(
|
|
336
|
+
'D1_EXEC_ERROR: Error in line 1: INSERT INTO t (id) VALUES (1); SELEC 2: near "SELEC": syntax error at offset 0: SQLITE_ERROR',
|
|
337
|
+
);
|
|
338
|
+
expect(await text("SELEC 2")).toBe(
|
|
339
|
+
'D1_EXEC_ERROR: Error in line 1: SELEC 2: near "SELEC": syntax error at offset 0: SQLITE_ERROR',
|
|
340
|
+
);
|
|
341
|
+
expect(await text("SELECT 1;\nSELEC 3")).toBe(
|
|
342
|
+
'D1_EXEC_ERROR: Error in line 2: SELEC 3: near "SELEC": syntax error at offset 0: SQLITE_ERROR',
|
|
343
|
+
);
|
|
344
|
+
expect(await text("SELECT ';'; SELEC 1")).toBe(
|
|
345
|
+
`D1_EXEC_ERROR: Error in line 1: SELECT ';'; SELEC 1: near "SELEC": syntax error at offset 0: SQLITE_ERROR`,
|
|
346
|
+
);
|
|
347
|
+
expect(await text("SELECT * FROM nope")).toBe(
|
|
348
|
+
"D1_EXEC_ERROR: Error in line 1: SELECT * FROM nope: no such table: nope: SQLITE_ERROR",
|
|
349
|
+
);
|
|
350
|
+
expect(await text("SELECT 1,")).toBe("D1_EXEC_ERROR: Error in line 1: SELECT 1,: incomplete input: SQLITE_ERROR");
|
|
351
|
+
await DB.exec("CREATE TABLE u (a UNIQUE); INSERT INTO u VALUES (1)");
|
|
352
|
+
expect(await text("INSERT INTO u VALUES (1)")).toBe(
|
|
353
|
+
"D1_EXEC_ERROR: Error in line 1: INSERT INTO u VALUES (1): UNIQUE constraint failed: u.a: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_UNIQUE)",
|
|
354
|
+
);
|
|
355
|
+
// earlier dates keep the bare message
|
|
356
|
+
const old = fresh("2026-11-02");
|
|
357
|
+
expect(await failure(() => old.exec("SELEC 2"))).toBe(
|
|
358
|
+
'D1_EXEC_ERROR: Error in line 1: SELEC 2: near "SELEC": syntax error',
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("from 2026-11-03: a statement with no statement in it, an empty exec, and PRAGMA foreign_keys = OFF", async () => {
|
|
363
|
+
const DB = fresh("2026-11-03");
|
|
364
|
+
const NONE = "D1_ERROR: SQL code did not contain a statement.";
|
|
365
|
+
for (const sql of [";", " ; ", "-- hi", "/* x */", "-- a\n/* b */;"])
|
|
366
|
+
expect(await failure(() => DB.prepare(sql).all())).toBe(NONE);
|
|
367
|
+
expect((await DB.prepare("-- c\nSELECT 1 AS a").first()).a).toBe(1);
|
|
368
|
+
expect(await failure(() => DB.exec(""))).toBe("D1_EXEC_ERROR: Error in line 1: : No SQL statements detected.");
|
|
369
|
+
expect(await failure(() => DB.exec(";"))).toBe(
|
|
370
|
+
"D1_EXEC_ERROR: Error in line 1: ;: SQL code did not contain a statement.",
|
|
371
|
+
);
|
|
372
|
+
expect(await failure(() => DB.exec("SELECT 1;\n-- x"))).toBe(
|
|
373
|
+
"D1_EXEC_ERROR: Error in line 2: -- x: SQL code did not contain a statement.",
|
|
374
|
+
);
|
|
375
|
+
await DB.exec("CREATE TABLE p (id INTEGER PRIMARY KEY);\nCREATE TABLE c (a REFERENCES p(id))");
|
|
376
|
+
await DB.prepare("PRAGMA foreign_keys = OFF").run();
|
|
377
|
+
expect(await DB.prepare("PRAGMA foreign_keys").first("foreign_keys")).toBe(1);
|
|
378
|
+
expect(await failure(() => DB.prepare("INSERT INTO c VALUES (5)").run())).toMatch(
|
|
379
|
+
/^D1_ERROR: FOREIGN KEY constraint failed/,
|
|
380
|
+
);
|
|
381
|
+
// earlier dates keep the old answers
|
|
382
|
+
const old = fresh("2026-11-02");
|
|
383
|
+
expect(await failure(() => old.exec(""))).toBe("D1_ERROR: No SQL statements detected.");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("from 2026-11-03: prepare('A; B') runs every statement and answers with the last", async () => {
|
|
387
|
+
const DB = fresh("2026-11-03");
|
|
388
|
+
await DB.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT, v TEXT UNIQUE)");
|
|
389
|
+
const r = await DB.prepare("INSERT INTO t (id) VALUES (7); INSERT INTO t (id) VALUES (8)").run();
|
|
390
|
+
expect([r.meta.changes, r.meta.last_row_id, r.results]).toEqual([2, 8, []]);
|
|
391
|
+
expect((await DB.prepare("INSERT INTO t (id) VALUES (10); UPDATE t SET name = 'x'").run()).meta.changes).toBe(4);
|
|
392
|
+
expect((await DB.prepare("SELECT 1 AS a; SELECT 2 AS b").all()).results).toEqual([{ b: 2 }]);
|
|
393
|
+
expect((await DB.prepare("UPDATE t SET name = 'y'; SELECT id FROM t WHERE id = 10").all()).results).toEqual([
|
|
394
|
+
{ id: 10 },
|
|
395
|
+
]);
|
|
396
|
+
expect(await DB.prepare("SELECT 1; SELECT 2, 3").raw()).toEqual([[2, 3]]);
|
|
397
|
+
expect(
|
|
398
|
+
(await DB.prepare("INSERT INTO t (id) VALUES (30); INSERT INTO t (id) VALUES (31); ").run()).meta.changes,
|
|
399
|
+
).toBe(2);
|
|
400
|
+
expect((await DB.prepare("SELECT 1; SELECT ? AS p").bind(5).first()).p).toBe(5);
|
|
401
|
+
// all or nothing
|
|
402
|
+
const count = () => DB.prepare("SELECT count(*) AS c FROM t").first("c");
|
|
403
|
+
const before = await count();
|
|
404
|
+
expect(await failure(() => DB.prepare("INSERT INTO t (id) VALUES (40); garbage").run())).toBe(
|
|
405
|
+
'D1_ERROR: near "garbage": syntax error at offset 0: SQLITE_ERROR',
|
|
406
|
+
);
|
|
407
|
+
expect(
|
|
408
|
+
await failure(() =>
|
|
409
|
+
DB.prepare("INSERT INTO t (id, v) VALUES (41, 'a'); INSERT INTO t (id, v) VALUES (42, 'a')").run(),
|
|
410
|
+
),
|
|
411
|
+
).toMatch(/^D1_ERROR: UNIQUE constraint failed: t\.v: SQLITE_CONSTRAINT/);
|
|
412
|
+
expect(await count()).toBe(before);
|
|
413
|
+
expect(
|
|
414
|
+
await failure(() => DB.prepare("INSERT INTO t (id) VALUES (?); INSERT INTO t (id) VALUES (?)").bind(50, 51).run()),
|
|
415
|
+
).toBe(
|
|
416
|
+
"D1_ERROR: When executing multiple SQL statements in a single call, only the last statement can have parameters.",
|
|
417
|
+
);
|
|
418
|
+
expect(await failure(() => DB.prepare("SELECT 1;;").all())).toBe("D1_ERROR: SQL code did not contain a statement.");
|
|
419
|
+
expect(await failure(() => DB.prepare("SELECT 1; -- c").all())).toBe(
|
|
420
|
+
"D1_ERROR: SQL code did not contain a statement.",
|
|
421
|
+
);
|
|
422
|
+
expect(await failure(() => DB.prepare("SELECT 1; PRAGMA user_version").all())).toBe(NOT_AUTHORIZED);
|
|
423
|
+
expect(await failure(() => DB.prepare("SELECT 1; SELECT ? AS p").bind(1, 2).all())).toBe(WRONG_COUNT);
|
|
424
|
+
expect(await count()).toBe(before);
|
|
425
|
+
// a trigger body is one statement
|
|
426
|
+
await DB.prepare("CREATE TRIGGER tr AFTER INSERT ON t BEGIN UPDATE t SET name = 'trig' WHERE id = new.id; END").run();
|
|
427
|
+
await DB.prepare("INSERT INTO t (id) VALUES (60)").run();
|
|
428
|
+
expect(await DB.prepare("SELECT name FROM t WHERE id = 60").first("name")).toBe("trig");
|
|
429
|
+
// before the date only the first statement runs
|
|
430
|
+
const old = fresh("2026-11-02");
|
|
431
|
+
await old.exec("CREATE TABLE t (id INTEGER PRIMARY KEY)");
|
|
432
|
+
expect((await old.prepare("INSERT INTO t (id) VALUES (1); INSERT INTO t (id) VALUES (2)").run()).meta.changes).toBe(
|
|
433
|
+
1,
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("from 2026-11-03: every number binds as a REAL, as in workerd", async () => {
|
|
438
|
+
const DB = fresh("2026-11-03");
|
|
439
|
+
expect(
|
|
440
|
+
await DB.prepare("SELECT typeof(?1) AS a, typeof(?2) AS b, typeof(?3) AS c, typeof(?4) AS d, typeof(?5) AS e")
|
|
441
|
+
.bind(1, 1.5, "1", true, null)
|
|
442
|
+
.first(),
|
|
443
|
+
).toEqual({ a: "real", b: "real", c: "text", d: "real", e: "null" });
|
|
444
|
+
expect(await DB.prepare("SELECT ? / ? AS d").bind(7, 2).first("d")).toBe(3.5);
|
|
445
|
+
expect(await DB.prepare("SELECT :a + :a AS d, @b AS b").bind(2, 3).first()).toEqual({ d: 4, b: 3 });
|
|
446
|
+
await DB.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, n INT, name TEXT)");
|
|
447
|
+
const r = await DB.prepare("INSERT INTO t (id, n, name) VALUES (?, ?, ?)").bind(70, 7, 7).run();
|
|
448
|
+
expect([r.meta.changes, r.meta.last_row_id]).toEqual([1, 70]);
|
|
449
|
+
expect(
|
|
450
|
+
await DB.prepare("SELECT typeof(id) AS a, typeof(n) AS b, name, typeof(name) AS c FROM t WHERE id = ?")
|
|
451
|
+
.bind(70)
|
|
452
|
+
.first(),
|
|
453
|
+
).toEqual({
|
|
454
|
+
a: "integer",
|
|
455
|
+
b: "integer",
|
|
456
|
+
name: "7.0",
|
|
457
|
+
c: "text",
|
|
458
|
+
});
|
|
459
|
+
expect(
|
|
460
|
+
(await DB.prepare("SELECT id FROM t WHERE id IN (?, ?) LIMIT ? OFFSET ?").bind(70, 71, 5, 0).all()).results,
|
|
461
|
+
).toEqual([{ id: 70 }]);
|
|
462
|
+
// a '?' inside a string or a comment is not a parameter
|
|
463
|
+
expect(await DB.prepare("SELECT '?' AS q, ? AS p -- ?").bind(2).first()).toEqual({ q: "?", p: 2 });
|
|
464
|
+
// an error's offset counts the SQL as written
|
|
465
|
+
expect(await failure(() => DB.prepare("SELECT ?, zz FROM t").bind(1).all())).toBe(
|
|
466
|
+
"D1_ERROR: no such column: zz at offset 10: SQLITE_ERROR",
|
|
467
|
+
);
|
|
468
|
+
expect(await failure(() => DB.prepare("SELECT ?, ?, zz FROM t").bind(1, 2).all())).toBe(
|
|
469
|
+
"D1_ERROR: no such column: zz at offset 13: SQLITE_ERROR",
|
|
470
|
+
);
|
|
471
|
+
const old = fresh("2026-11-02");
|
|
472
|
+
expect(await old.prepare("SELECT typeof(?1) AS a").bind(1).first("a")).toBe("integer");
|
|
473
|
+
});
|