@sproutboat/runtime 0.34.2 → 0.34.4
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 +20 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +142 -6
- package/src/r2-object.test.js +1 -1
- package/src/transport-embedded.js +181 -2
- package/test/native-d1.ts +97 -3
- package/test/native-http.ts +280 -0
- package/test/native-json-strings.ts +131 -0
- package/test/native-jwt.ts +333 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.34.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5bd4a25: Cheaper JWT verification. `atob` decodes in C for the usual input (no whitespace, valid characters, correct padding) and falls back to the existing JavaScript for everything else, so what throws and what it throws is unchanged. `TextEncoder.encode` sizes and fills the Uint8Array in two C passes instead of two `charCodeAt` loops (a lone surrogate is still U+FFFD). The ArrayBuffer returned by `crypto.subtle.sign` and `digest` is copied in with one `memcpy`, and the constant-time compare behind `verify` runs in C. A typical `authenticate()` (three base64url decodes, two `JSON.parse`, one `TextEncoder.encode`, one HMAC verify) goes from 21.5 to 7 us on an M-series host. New native test `test:native-jwt`: RFC 4231 HMAC vectors, the RFC 7515 and jwt.io tokens, wrong key, tampered header, payload and signature, short, long and missing signatures, bad base64url, typed-array windows and ArrayBuffers as verify input, and seeded random `atob`, `TextEncoder` and HMAC (SHA-256, 384, 512) inputs compared with Bun and node:crypto.
|
|
8
|
+
|
|
9
|
+
## 0.34.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 49ac7df: Faster `atob`: each character is looked up in a 128-entry table instead of `indexOf` over the alphabet, and the same pass validates, so the regex test over the whole input is gone. Same output and same `InvalidCharacterError` cases (checked against the platform `atob` on 9,000 random inputs and edge cases).
|
|
14
|
+
|
|
15
|
+
A JWT check decodes three base64url parts. On one pinned x86 core at a fixed rate, CPU per request: like 854 to 761 us, create 922 to 866 us; feed and post reads do not call `atob`.
|
|
16
|
+
- 745f94c: Faster D1 in standalone builds: a D1 call no longer goes through `JSON.stringify` and `JSON.parse` on both sides of the in-process dispatch. The request object is handed straight to the dispatcher and the reply object comes straight back (the C layer still returns its own JSON, parsed once). Same results, same error messages, no change under the broker transport.
|
|
17
|
+
|
|
18
|
+
On one pinned x86 core at a fixed rate, CPU per request against a 500,000-row feed API: feed (20 rows) 1,418 to 1,133 us, post read 394 to 332 us, like 854 to 728 us, create 922 to 857 us, the realistic mix 670 to 580 us.
|
|
19
|
+
- 88c7bff: Faster strict D1 queries in standalone builds (compatibility date 2026-10-24 and later): the rows of a result are built as JS arrays in C, straight from the SQLite cells, instead of being written as JSON text in C and parsed again in JS. NULL, numbers, text (UTF-8 decoded exactly as the JSON path decoded it, including invalid sequences), BLOBs (byte arrays) and empty results are unchanged. Only a small header (columns, changes, last row id) still goes through JSON. Older compatibility dates and non-D1 operations keep the JSON path.
|
|
20
|
+
|
|
21
|
+
On one pinned x86 core at a fixed rate, CPU per request against a 500,000-row feed API, see the PR for numbers.
|
|
22
|
+
|
|
3
23
|
## 0.34.2
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -3639,16 +3639,29 @@ if (globalThis.crypto.getRandomValues == null) {
|
|
|
3639
3639
|
// so a string held as UTF-16 (one from JSON.parse or TextDecoder, say) decoded to the
|
|
3640
3640
|
// wrong bytes, and btoa encoded its UTF-16 units.
|
|
3641
3641
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
3642
|
+
// Sextet per character code (255 = not base64): a table read rather than an indexOf over the
|
|
3643
|
+
// alphabet, and one pass that also validates, so no regex test over the whole input.
|
|
3644
|
+
const sextet = [];
|
|
3645
|
+
for (let i = 0; i < 128; i++) sextet.push(255);
|
|
3646
|
+
for (let i = 0; i < alphabet.length; i++) sextet[alphabet.charCodeAt(i)] = i;
|
|
3642
3647
|
globalThis.atob = function (data) {
|
|
3643
|
-
|
|
3648
|
+
// One C pass for the usual input (no whitespace, valid characters); the JS below
|
|
3649
|
+
// takes everything else, so it alone decides what throws.
|
|
3650
|
+
const input = String(data);
|
|
3651
|
+
const fast = __sbAtobNative(input);
|
|
3652
|
+
if (fast !== "") return fast;
|
|
3653
|
+
let text = input.replace(/[\t\n\f\r ]/g, "");
|
|
3644
3654
|
if (text.length % 4 === 0) text = text.replace(/==?$/, "");
|
|
3645
|
-
if (text.length % 4 === 1
|
|
3655
|
+
if (text.length % 4 === 1) throw invalidCharacter("atob: the string is not valid base64");
|
|
3646
3656
|
const parts = [];
|
|
3647
3657
|
let chunk = "";
|
|
3648
3658
|
let bits = 0;
|
|
3649
3659
|
let count = 0;
|
|
3650
3660
|
for (let i = 0; i < text.length; i++) {
|
|
3651
|
-
|
|
3661
|
+
const code = text.charCodeAt(i);
|
|
3662
|
+
const value = code < 128 ? sextet[code] : 255;
|
|
3663
|
+
if (value === 255) throw invalidCharacter("atob: the string is not valid base64");
|
|
3664
|
+
bits = (bits << 6) | value;
|
|
3652
3665
|
count += 6;
|
|
3653
3666
|
if (count >= 8) {
|
|
3654
3667
|
count -= 8;
|
|
@@ -4050,6 +4063,7 @@ function __sbToBytes(input) {
|
|
|
4050
4063
|
}
|
|
4051
4064
|
function __sbBufFrom(latin1) {
|
|
4052
4065
|
const b = new Uint8Array(latin1.length);
|
|
4066
|
+
if (__sbFillBytes(latin1, b)) return b.buffer;
|
|
4053
4067
|
for (let i = 0; i < latin1.length; i++) b[i] = latin1.charCodeAt(i) & 0xff;
|
|
4054
4068
|
return b.buffer;
|
|
4055
4069
|
}
|
|
@@ -4182,6 +4196,8 @@ function __sbRawBodyResponse(body, init) {
|
|
|
4182
4196
|
// Constant-time compare of two latin1 byte strings.
|
|
4183
4197
|
function __sbEqCt(a, b) {
|
|
4184
4198
|
if (a.length !== b.length) return false;
|
|
4199
|
+
const native = __sbEqCtNative(a, b);
|
|
4200
|
+
if (native >= 0) return native === 1;
|
|
4185
4201
|
let diff = 0;
|
|
4186
4202
|
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
4187
4203
|
return diff === 0;
|
|
@@ -4797,6 +4813,21 @@ static size_t sb_utf8_encode_units(const uint16_t* s, size_t len, unsigned char*
|
|
|
4797
4813
|
return k;
|
|
4798
4814
|
}
|
|
4799
4815
|
|
|
4816
|
+
// Unit I of a string held as UTF-16 (WIDE) or one unit per byte.
|
|
4817
|
+
static unsigned sb_unit(const unsigned char* s, int wide, size_t i) {
|
|
4818
|
+
return wide ? (unsigned)((const uint16_t*)s)[i] : (unsigned)s[i];
|
|
4819
|
+
}
|
|
4820
|
+
|
|
4821
|
+
// Base64 sextet of a character, or -1.
|
|
4822
|
+
static int sb_b64_val(unsigned c) {
|
|
4823
|
+
if (c >= 'A' && c <= 'Z') return (int)(c - 'A');
|
|
4824
|
+
if (c >= 'a' && c <= 'z') return (int)(c - 'a') + 26;
|
|
4825
|
+
if (c >= '0' && c <= '9') return (int)(c - '0') + 52;
|
|
4826
|
+
if (c == '+') return 62;
|
|
4827
|
+
if (c == '/') return 63;
|
|
4828
|
+
return -1;
|
|
4829
|
+
}
|
|
4830
|
+
|
|
4800
4831
|
`;
|
|
4801
4832
|
|
|
4802
4833
|
// sproutd #22: __sbFromUtf8 for a string of bytes (one byte per unit) or a Uint8Array, in C.
|
|
@@ -4897,6 +4928,91 @@ function __sbToBytesNative(input) {
|
|
|
4897
4928
|
return out;
|
|
4898
4929
|
}
|
|
4899
4930
|
|
|
4931
|
+
// Constant-time equality of two equal-length bytestrings in C: 1 equal, 0 different, -1 when
|
|
4932
|
+
// either is not a bytestring (the caller then compares in JS).
|
|
4933
|
+
// oxlint-disable-next-line no-unused-vars -- `a` and `b` are read inside the RawC block below, not by JS.
|
|
4934
|
+
function __sbEqCtNative(a, b) {
|
|
4935
|
+
let result = -1;
|
|
4936
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
4937
|
+
Porffor.c`
|
|
4938
|
+
if (a.type == 195 && b.type == 195) {
|
|
4939
|
+
const size_t __n = (size_t)*(u32*)(MEM + (u32)a.val);
|
|
4940
|
+
if (__n == (size_t)*(u32*)(MEM + (u32)b.val)) {
|
|
4941
|
+
const unsigned char* __x = (const unsigned char*)(MEM + (u32)a.val + 4);
|
|
4942
|
+
const unsigned char* __y = (const unsigned char*)(MEM + (u32)b.val + 4);
|
|
4943
|
+
unsigned char __diff = 0;
|
|
4944
|
+
for (size_t __i = 0; __i < __n; __i++) __diff |= __x[__i] ^ __y[__i];
|
|
4945
|
+
result = (f64)(__diff == 0 ? 1 : 0);
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
`;
|
|
4949
|
+
return result;
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
// Copies a bytestring (one unit per byte) into the front of a Uint8Array.
|
|
4953
|
+
// Returns false (writing nothing) when it cannot.
|
|
4954
|
+
// oxlint-disable-next-line no-unused-vars -- `bytes` and `view` are read inside the RawC block below, not by JS.
|
|
4955
|
+
function __sbFillBytes(bytes, view) {
|
|
4956
|
+
let ok = 0;
|
|
4957
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
4958
|
+
Porffor.c`
|
|
4959
|
+
if (bytes.type == 195 && view.type == 81 && *(int32_t*)(MEM + (u32)view.val) >= 0) {
|
|
4960
|
+
const u32 __p = (u32)bytes.val;
|
|
4961
|
+
const u32 __v = (u32)view.val;
|
|
4962
|
+
const size_t __n = (size_t)*(u32*)(MEM + __p);
|
|
4963
|
+
if (__n <= (size_t)*(int32_t*)(MEM + __v)) {
|
|
4964
|
+
memcpy(MEM + *(u32*)(MEM + __v + 4) + 4, MEM + __p + 4, __n);
|
|
4965
|
+
ok = (f64)1;
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
`;
|
|
4969
|
+
return ok === 1;
|
|
4970
|
+
}
|
|
4971
|
+
|
|
4972
|
+
// atob for the common case: no whitespace, only base64 characters, `=` padding
|
|
4973
|
+
// only where the spec allows it. Returns the decoded bytestring, or "" for
|
|
4974
|
+
// anything else (including an empty result), so the caller's JS path decides
|
|
4975
|
+
// and throws.
|
|
4976
|
+
// oxlint-disable-next-line no-unused-vars -- `text` is read inside the RawC block below, not by JS.
|
|
4977
|
+
function __sbAtobNative(text) {
|
|
4978
|
+
let out = "";
|
|
4979
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
4980
|
+
Porffor.c`
|
|
4981
|
+
if (text.type == 195 || text.type == 67) {
|
|
4982
|
+
const u32 __p = (u32)text.val;
|
|
4983
|
+
const int __wide = text.type == 67;
|
|
4984
|
+
size_t __n = (size_t)*(u32*)(MEM + __p);
|
|
4985
|
+
const unsigned char* __s = (const unsigned char*)(MEM + __p + 4);
|
|
4986
|
+
if (__n % 4 == 0) {
|
|
4987
|
+
if (__n > 0 && sb_unit(__s, __wide, __n - 1) == '=') __n--;
|
|
4988
|
+
if (__n > 0 && sb_unit(__s, __wide, __n - 1) == '=' && __n % 4 != 0) __n--;
|
|
4989
|
+
}
|
|
4990
|
+
if (__n > 0 && __n % 4 != 1) {
|
|
4991
|
+
const size_t __cap = __n / 4 * 3 + 3;
|
|
4992
|
+
unsigned char __stack[768];
|
|
4993
|
+
unsigned char* __o = __cap <= sizeof(__stack) ? __stack : (unsigned char*)malloc(__cap);
|
|
4994
|
+
if (__o) {
|
|
4995
|
+
size_t __k = 0, __i = 0;
|
|
4996
|
+
int __bad = 0;
|
|
4997
|
+
uint32_t __acc = 0;
|
|
4998
|
+
int __bits = 0;
|
|
4999
|
+
for (; __i < __n; __i++) {
|
|
5000
|
+
const unsigned __c = sb_unit(__s, __wide, __i);
|
|
5001
|
+
const int __v = __c < 128 ? sb_b64_val(__c) : -1;
|
|
5002
|
+
if (__v < 0) { __bad = 1; break; }
|
|
5003
|
+
__acc = (__acc << 6) | (uint32_t)__v;
|
|
5004
|
+
__bits += 6;
|
|
5005
|
+
if (__bits >= 8) { __bits -= 8; __o[__k++] = (unsigned char)((__acc >> __bits) & 0xff); }
|
|
5006
|
+
}
|
|
5007
|
+
if (!__bad) out = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__o, __k), 195);
|
|
5008
|
+
if (__o != __stack) free(__o);
|
|
5009
|
+
}
|
|
5010
|
+
}
|
|
5011
|
+
}
|
|
5012
|
+
`;
|
|
5013
|
+
return out;
|
|
5014
|
+
}
|
|
5015
|
+
|
|
4900
5016
|
// TRANSPORT: wrap.ts splices one of transport-broker.js / transport-embedded.js here.
|
|
4901
5017
|
|
|
4902
5018
|
// `nStr` is the decimal byte count as a string (same string-param pattern as
|
|
@@ -5173,11 +5289,30 @@ function __sbD1Param(value, strict) {
|
|
|
5173
5289
|
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- workerd's D1_TYPE_ERROR names the value's typeof
|
|
5174
5290
|
throw new Error("D1_TYPE_ERROR: Type '" + typeof value + "' not supported for value '" + String(value) + "'");
|
|
5175
5291
|
}
|
|
5292
|
+
// An embedded (standalone) build sets this to hand the request object straight to its
|
|
5293
|
+
// dispatcher. D1 replies are the biggest ones a handler sees (20 rows is 100 cells), and
|
|
5294
|
+
// the JSON.stringify and JSON.parse around the in-process call cost more than SQLite did.
|
|
5295
|
+
// The broker transport leaves it unset and keeps the string hop. (No initializer: the transport is placed ahead of the prelude, and `= null` would run after its assignment.)
|
|
5296
|
+
// oxlint-disable-next-line no-unassigned-vars -- the embedded transport assigns it; the broker transport does not.
|
|
5297
|
+
var __sbCallDirect;
|
|
5298
|
+
function __sbRpcDirect(op, extra) {
|
|
5299
|
+
if (!__sbCallDirect) return __sbRpc(op, extra);
|
|
5300
|
+
const req = { v: 1, id: ++__sbReqId, op };
|
|
5301
|
+
if (extra) for (const k in extra) req[k] = extra[k];
|
|
5302
|
+
let reply;
|
|
5303
|
+
try {
|
|
5304
|
+
reply = __sbCallDirect(req);
|
|
5305
|
+
} catch (err) {
|
|
5306
|
+
throw new Error(`sproutboat ${op}: ${String((err && err.message) || err) || "failed"}`);
|
|
5307
|
+
}
|
|
5308
|
+
if (reply && reply.ok === false) throw new Error(`sproutboat ${op}: ${reply.error || "failed"}`);
|
|
5309
|
+
return reply;
|
|
5310
|
+
}
|
|
5176
5311
|
function __sbD1Rpc(op, extra, strict) {
|
|
5177
|
-
if (!strict) return
|
|
5312
|
+
if (!strict) return __sbRpcDirect(op, extra);
|
|
5178
5313
|
extra.strict = true;
|
|
5179
5314
|
try {
|
|
5180
|
-
return
|
|
5315
|
+
return __sbRpcDirect(op, extra);
|
|
5181
5316
|
} catch (err) {
|
|
5182
5317
|
// "sproutboat d1.query: D1_ERROR: ..." -> "D1_ERROR: ...", as workerd words it.
|
|
5183
5318
|
const message = String((err && err.message) || err);
|
|
@@ -5240,7 +5375,8 @@ function __sbD1Meta(meta, started) {
|
|
|
5240
5375
|
}
|
|
5241
5376
|
// Columns and rows of a strict reply (an older broker answers with objects only).
|
|
5242
5377
|
function __sbD1Table(r) {
|
|
5243
|
-
|
|
5378
|
+
// rows built in C (r.native) already hold BLOBs as byte arrays
|
|
5379
|
+
if (r.columns) return { columns: r.columns, rows: r.native === true ? r.rows : __sbD1Rows(r) };
|
|
5244
5380
|
const objects = r.results || [];
|
|
5245
5381
|
const columns = objects.length ? Object.keys(objects[0]) : [];
|
|
5246
5382
|
const rows = [];
|
package/src/r2-object.test.js
CHANGED
|
@@ -31,7 +31,7 @@ const load = new Function(
|
|
|
31
31
|
"__sbRpc",
|
|
32
32
|
"__sbR2MultipartPut",
|
|
33
33
|
// __sbIsStr and __sbToBytes (for uploadPart's values), with the C fast paths stubbed out.
|
|
34
|
-
`function __sbFromUtf8Native() { return ""; }\nfunction __sbToBytesNative() { return ""; }\n${src.slice(isStrStart, isStrEnd)}\n${src.slice(bytesStart, utf8Start)}\n${src.slice(utf8Start, utf8End)}\n${src.slice(rawStart, rawEnd)}\n${src.slice(r2Start, r2End)}\nreturn { __sbR2Object, __sbR2Multipart };`,
|
|
34
|
+
`function __sbFromUtf8Native() { return ""; }\nfunction __sbToBytesNative() { return ""; }\nfunction __sbFillBytes() { return false; }\n${src.slice(isStrStart, isStrEnd)}\n${src.slice(bytesStart, utf8Start)}\n${src.slice(utf8Start, utf8End)}\n${src.slice(rawStart, rawEnd)}\n${src.slice(r2Start, r2End)}\nreturn { __sbR2Object, __sbR2Multipart };`,
|
|
35
35
|
);
|
|
36
36
|
const calls = [];
|
|
37
37
|
const rpc = (op, value) => {
|
|
@@ -1352,6 +1352,43 @@ static void sb_putjson(sb_buf* b, const char* s, size_t n) {
|
|
|
1352
1352
|
// doesn't start a valid sequence passes through as \u00XX, the same rule as
|
|
1353
1353
|
// the prelude's __sbFromUtf8. Raw byte producers (the HTTP client's body,
|
|
1354
1354
|
// BLOB columns) keep sb_putjson.
|
|
1355
|
+
// The UTF-16 units sb_putjson_text would write as u-escapes, for a TEXT cell handed over as
|
|
1356
|
+
// a JS string: same validity rules (an invalid lead byte is its own unit, an overlong form, a
|
|
1357
|
+
// surrogate or a value above U+10FFFF is not decoded). OUT has room for n units. *wide is set
|
|
1358
|
+
// when a unit is above 0xff.
|
|
1359
|
+
static size_t sb_text_units(const unsigned char* s, size_t n, uint16_t* out, int* wide) {
|
|
1360
|
+
size_t k = 0;
|
|
1361
|
+
*wide = 0;
|
|
1362
|
+
for (size_t i = 0; i < n; i++) {
|
|
1363
|
+
unsigned char c = s[i];
|
|
1364
|
+
if (c < 0x80) { out[k++] = c; continue; }
|
|
1365
|
+
unsigned int cp = 0, min = 0;
|
|
1366
|
+
size_t len = 0;
|
|
1367
|
+
if ((c & 0xe0) == 0xc0) { cp = c & 0x1f; len = 2; min = 0x80; }
|
|
1368
|
+
else if ((c & 0xf0) == 0xe0) { cp = c & 0x0f; len = 3; min = 0x800; }
|
|
1369
|
+
else if ((c & 0xf8) == 0xf0) { cp = c & 0x07; len = 4; min = 0x10000; }
|
|
1370
|
+
int ok = len > 0 && i + len <= n;
|
|
1371
|
+
for (size_t j = 1; ok && j < len; j++) {
|
|
1372
|
+
unsigned char cc = s[i + j];
|
|
1373
|
+
if ((cc & 0xc0) != 0x80) ok = 0;
|
|
1374
|
+
else cp = (cp << 6) | (cc & 0x3f);
|
|
1375
|
+
}
|
|
1376
|
+
if (ok && (cp < min || cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff))) ok = 0;
|
|
1377
|
+
if (!ok) { out[k++] = c; if (c > 0xff) *wide = 1; continue; }
|
|
1378
|
+
if (cp >= 0x10000) {
|
|
1379
|
+
cp -= 0x10000;
|
|
1380
|
+
out[k++] = (uint16_t)(0xd800 + (cp >> 10));
|
|
1381
|
+
out[k++] = (uint16_t)(0xdc00 + (cp & 0x3ff));
|
|
1382
|
+
*wide = 1;
|
|
1383
|
+
} else {
|
|
1384
|
+
out[k++] = (uint16_t)cp;
|
|
1385
|
+
if (cp > 0xff) *wide = 1;
|
|
1386
|
+
}
|
|
1387
|
+
i += len - 1;
|
|
1388
|
+
}
|
|
1389
|
+
return k;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1355
1392
|
static void sb_putjson_text(sb_buf* b, const char* s, size_t n) {
|
|
1356
1393
|
sb_put(b, "\"", 1);
|
|
1357
1394
|
for (size_t i = 0; i < n; i++) {
|
|
@@ -2122,7 +2159,29 @@ static char* sb_sql_error(sqlite3* db, const char* message, int code, int skip)
|
|
|
2122
2159
|
// {"ok":true,"cols":[...],"rows":[[...]],"changes":n,"rowid":n}
|
|
2123
2160
|
// or {"ok":false,"error":"...","code":n,"offset":n}. In a D1 database (a path under
|
|
2124
2161
|
// /d1/) a BLOB comes back as {"$blob":"HEX"}; elsewhere as one char per byte.
|
|
2125
|
-
|
|
2162
|
+
// A strict D1 query can hand its rows back as cells instead of JSON text: the caller turns
|
|
2163
|
+
// them into JS arrays, with no JSON written here and none parsed there. t is 0 NULL,
|
|
2164
|
+
// 1 number, 2 TEXT (UTF-8 in p), 3 BLOB (bytes in p).
|
|
2165
|
+
typedef struct { unsigned char t; double d; char* p; int n; } sb_cell;
|
|
2166
|
+
typedef struct { sb_cell* c; size_t n; size_t cap; int ncol; int used; } sb_cells;
|
|
2167
|
+
static void sb_cells_free(sb_cells* cs) {
|
|
2168
|
+
if (!cs) return;
|
|
2169
|
+
for (size_t i = 0; i < cs->n; i++) free(cs->c[i].p);
|
|
2170
|
+
free(cs->c);
|
|
2171
|
+
cs->c = 0; cs->n = cs->cap = 0;
|
|
2172
|
+
}
|
|
2173
|
+
static sb_cell* sb_cells_add(sb_cells* cs) {
|
|
2174
|
+
if (cs->n == cs->cap) {
|
|
2175
|
+
size_t cap = cs->cap ? cs->cap * 2 : 64;
|
|
2176
|
+
sb_cell* grown = (sb_cell*)realloc(cs->c, cap * sizeof(sb_cell));
|
|
2177
|
+
if (!grown) return 0;
|
|
2178
|
+
cs->c = grown; cs->cap = cap;
|
|
2179
|
+
}
|
|
2180
|
+
sb_cell* cell = &cs->c[cs->n++];
|
|
2181
|
+
cell->t = 0; cell->d = 0; cell->p = 0; cell->n = 0;
|
|
2182
|
+
return cell;
|
|
2183
|
+
}
|
|
2184
|
+
static char* sb_sql_run_x(const char* path, const char* sql, const char* params, sb_cells* cells) {
|
|
2126
2185
|
sb_buf b = { 0, 0, 0 };
|
|
2127
2186
|
int idx = sb_db_for(path);
|
|
2128
2187
|
if (idx < 0 && sb_db_capture_failed(path)) return sb_sql_error(0, SB_LTX_FAILED, 10, 0);
|
|
@@ -2151,8 +2210,30 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
|
|
|
2151
2210
|
sb_putjson_text(&b, name ? name : "", name ? strlen(name) : 0);
|
|
2152
2211
|
}
|
|
2153
2212
|
sb_puts(&b, "],\"rows\":[");
|
|
2213
|
+
// Cells only for a strict D1 statement: its BLOBs are byte arrays either way.
|
|
2214
|
+
const int native = cells && strict && d1;
|
|
2215
|
+
if (native) { cells->ncol = ncol; cells->used = 1; }
|
|
2154
2216
|
int rc, first = 1;
|
|
2155
2217
|
while ((rc = sqlite3_step(st)) == SB_SQLITE_ROW) {
|
|
2218
|
+
if (native) {
|
|
2219
|
+
for (int i = 0; i < ncol; i++) {
|
|
2220
|
+
sb_cell* cell = sb_cells_add(cells);
|
|
2221
|
+
if (!cell) { cells->used = -1; break; }
|
|
2222
|
+
int t = sqlite3_column_type(st, i);
|
|
2223
|
+
if (t == 5) continue;
|
|
2224
|
+
if (t == 1 || t == 2) { cell->t = 1; cell->d = sqlite3_column_double(st, i); continue; }
|
|
2225
|
+
const unsigned char* txt = t == 4 ? (const unsigned char*)sqlite3_column_blob(st, i) : sqlite3_column_text(st, i);
|
|
2226
|
+
int n = sqlite3_column_bytes(st, i);
|
|
2227
|
+
cell->t = t == 4 ? 3 : 2;
|
|
2228
|
+
cell->n = txt ? n : 0;
|
|
2229
|
+
cell->p = (char*)malloc((size_t)cell->n + 1);
|
|
2230
|
+
if (!cell->p) { cells->used = -1; break; }
|
|
2231
|
+
if (cell->n) memcpy(cell->p, txt, (size_t)cell->n);
|
|
2232
|
+
cell->p[cell->n] = 0;
|
|
2233
|
+
}
|
|
2234
|
+
if (cells->used < 0) break;
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2156
2237
|
if (!first) sb_puts(&b, ",");
|
|
2157
2238
|
first = 0;
|
|
2158
2239
|
sb_puts(&b, "[");
|
|
@@ -2188,6 +2269,7 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
|
|
|
2188
2269
|
// Reset, don't finalize: the statement stays compiled in sb_stmt_cache. Reset
|
|
2189
2270
|
// now (not just on next reuse) so a SELECT stops holding its read transaction.
|
|
2190
2271
|
sqlite3_reset(st);
|
|
2272
|
+
if (cells && cells->used < 0) { free(b.p); sb_cells_free(cells); return sb_sql_error(db, "out of memory", 7, 0); }
|
|
2191
2273
|
// The statement's own error, kept before LTX capture runs: capture executes statements
|
|
2192
2274
|
// of its own, after which SQLite reports "not an error".
|
|
2193
2275
|
char failure[512] = { 0 };
|
|
@@ -2200,11 +2282,15 @@ static char* sb_sql_run(const char* path, const char* sql, const char* params) {
|
|
|
2200
2282
|
if (capture_failed) rc = 10;
|
|
2201
2283
|
if (rc != SB_SQLITE_DONE && rc != SB_SQLITE_ROW) {
|
|
2202
2284
|
free(b.p);
|
|
2285
|
+
if (cells) sb_cells_free(cells);
|
|
2203
2286
|
if (capture_failed) return sb_sql_error(db, SB_LTX_FAILED, 10, 0);
|
|
2204
2287
|
return sb_sql_error(db, failure, failure_code, 0);
|
|
2205
2288
|
}
|
|
2206
2289
|
return b.p;
|
|
2207
2290
|
}
|
|
2291
|
+
static char* sb_sql_run(const char* path, const char* sql, const char* params) {
|
|
2292
|
+
return sb_sql_run_x(path, sql, params, 0);
|
|
2293
|
+
}
|
|
2208
2294
|
|
|
2209
2295
|
// The storage quota (docs: an app's data directory is capped): bytes in regular files under
|
|
2210
2296
|
// dir, symlinks not followed. Bounded by depth; an unreadable entry counts as 0.
|
|
@@ -4131,6 +4217,85 @@ function __sbSqlRaw(path, sql, paramsJson) {
|
|
|
4131
4217
|
return res;
|
|
4132
4218
|
}
|
|
4133
4219
|
|
|
4220
|
+
// One strict D1 statement: [header JSON, rows]. The header is {"ok":true,"cols":[...],"changes":n,
|
|
4221
|
+
// "rowid":n} (or the error reply) and rows is an array of row arrays built here from the
|
|
4222
|
+
// cells, BLOBs as byte arrays, so no JSON for the rows is written or parsed. rows is
|
|
4223
|
+
// undefined when the reply is not a result.
|
|
4224
|
+
// oxlint-disable-next-line no-unused-vars -- read inside the RawC block below, not by JS.
|
|
4225
|
+
function __sbSqlRowsRaw(path, sql, paramsJson) {
|
|
4226
|
+
let res = "";
|
|
4227
|
+
// oxlint-disable-next-line no-unused-expressions -- Porffor.c`...` is inline C the compiler consumes, not a JS expression.
|
|
4228
|
+
Porffor.c`
|
|
4229
|
+
const char* __p; size_t __pl; char* __po = 0;
|
|
4230
|
+
porf_native_fetch_read_value(path, &__p, &__pl, &__po);
|
|
4231
|
+
char* __path = (char*)malloc(__pl + 1); memcpy(__path, __p, __pl); __path[__pl] = 0;
|
|
4232
|
+
if (__po) free(__po);
|
|
4233
|
+
|
|
4234
|
+
const char* __s; size_t __sl; char* __so = 0;
|
|
4235
|
+
porf_native_fetch_read_value(sql, &__s, &__sl, &__so);
|
|
4236
|
+
char* __sql = (char*)malloc(__sl + 1); memcpy(__sql, __s, __sl); __sql[__sl] = 0;
|
|
4237
|
+
if (__so) free(__so);
|
|
4238
|
+
|
|
4239
|
+
const char* __a; size_t __al; char* __ao = 0;
|
|
4240
|
+
porf_native_fetch_read_value(paramsJson, &__a, &__al, &__ao);
|
|
4241
|
+
char* __args = (char*)malloc(__al + 1); memcpy(__args, __a, __al); __args[__al] = 0;
|
|
4242
|
+
if (__ao) free(__ao);
|
|
4243
|
+
|
|
4244
|
+
sb_cells __cells = { 0, 0, 0, 0, 0 };
|
|
4245
|
+
char* __out = sb_sql_run_x(__path, __sql, __args, &__cells);
|
|
4246
|
+
free(__path); free(__sql); free(__args);
|
|
4247
|
+
const u32 __pair = porf_arr_new(2, 2);
|
|
4248
|
+
res = porf_box((f64)__pair, 72);
|
|
4249
|
+
if (!__out) __out = strdup("{\"ok\":false,\"error\":\"no result\"}");
|
|
4250
|
+
porf_arr_set(__pair, 0, porf_box((f64)porf_native_fetch_alloc_bytestring(__out, strlen(__out)), 195));
|
|
4251
|
+
free(__out);
|
|
4252
|
+
if (__cells.used == 1) {
|
|
4253
|
+
const size_t __ncol = (size_t)__cells.ncol;
|
|
4254
|
+
const size_t __nrow = __ncol ? __cells.n / __ncol : 0;
|
|
4255
|
+
const u32 __rows = porf_arr_new(0, (i32)(__nrow ? __nrow : 1));
|
|
4256
|
+
porf_arr_set(__pair, 1, porf_box((f64)__rows, 72));
|
|
4257
|
+
for (size_t __r = 0; __r < __nrow; __r++) {
|
|
4258
|
+
const u32 __row = porf_arr_new((i32)__ncol, (i32)__ncol);
|
|
4259
|
+
porf_arr_set(__rows, (u32)__r, porf_box((f64)__row, 72));
|
|
4260
|
+
for (size_t __c = 0; __c < __ncol; __c++) {
|
|
4261
|
+
const sb_cell* __cell = &__cells.c[__r * __ncol + __c];
|
|
4262
|
+
jsval __v = porf_box(0.0, 7);
|
|
4263
|
+
if (__cell->t == 1) __v = porf_box_num(__cell->d);
|
|
4264
|
+
else if (__cell->t == 2) {
|
|
4265
|
+
const size_t __n = (size_t)__cell->n;
|
|
4266
|
+
uint16_t* __u = (uint16_t*)malloc(__n * 2 + 2);
|
|
4267
|
+
if (__u) {
|
|
4268
|
+
int __wide = 0;
|
|
4269
|
+
const size_t __k = sb_text_units((const unsigned char*)__cell->p, __n, __u, &__wide);
|
|
4270
|
+
if (__wide) {
|
|
4271
|
+
const u32 __str = porf_native_fetch_alloc((u32)(4 + __k * 2), 67);
|
|
4272
|
+
*(u32*)(MEM + __str) = (u32)__k;
|
|
4273
|
+
memcpy(MEM + __str + 4, __u, __k * 2);
|
|
4274
|
+
__v = porf_box((f64)__str, 67);
|
|
4275
|
+
} else {
|
|
4276
|
+
unsigned char* __b = (unsigned char*)malloc(__k + 1);
|
|
4277
|
+
if (__b) {
|
|
4278
|
+
for (size_t __i = 0; __i < __k; __i++) __b[__i] = (unsigned char)__u[__i];
|
|
4279
|
+
__v = porf_box((f64)porf_native_fetch_alloc_bytestring((const char*)__b, __k), 195);
|
|
4280
|
+
free(__b);
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
free(__u);
|
|
4284
|
+
}
|
|
4285
|
+
} else if (__cell->t == 3) {
|
|
4286
|
+
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->p[__i]));
|
|
4288
|
+
__v = porf_box((f64)__bytes, 72);
|
|
4289
|
+
}
|
|
4290
|
+
porf_arr_set(__row, (u32)__c, __v);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
sb_cells_free(&__cells);
|
|
4295
|
+
`;
|
|
4296
|
+
return res;
|
|
4297
|
+
}
|
|
4298
|
+
|
|
4134
4299
|
// --- the op dispatch, in JS -------------------------------------------------
|
|
4135
4300
|
// Deliberately the same SQL and the same partition keys as broker.ts. When one
|
|
4136
4301
|
// changes the other has to, and the conformance suite is what says so.
|
|
@@ -4621,7 +4786,7 @@ function __sbEmbeddedDispatch(msg) {
|
|
|
4621
4786
|
__sbD1Quota(path);
|
|
4622
4787
|
if (op === "d1.batch") return { ok: true, results: __sbD1Batch(path, msg.statements || [], strict) };
|
|
4623
4788
|
const one = __sbD1Run(path, msg.sql, msg.params, strict);
|
|
4624
|
-
return { ok: true, results: one.results, columns: one.columns, rows: one.rows, meta: one.meta, success: true };
|
|
4789
|
+
return { ok: true, results: one.results, columns: one.columns, rows: one.rows, native: one.native, meta: one.meta, success: true };
|
|
4625
4790
|
}
|
|
4626
4791
|
|
|
4627
4792
|
// Durable Objects storage.sql (compatibility date 2026-10-29): one database per object,
|
|
@@ -5168,6 +5333,14 @@ function __sbD1Bytes(cell) {
|
|
|
5168
5333
|
return out;
|
|
5169
5334
|
}
|
|
5170
5335
|
function __sbD1Run(path, sql, params, strict) {
|
|
5336
|
+
if (strict) {
|
|
5337
|
+
// Rows come back as arrays, built in C (see __sbSqlRowsRaw); the header is small JSON.
|
|
5338
|
+
const pair = __sbSqlRowsRaw(path, "/*sb-d1-strict*/" + String(sql), JSON.stringify(params || []));
|
|
5339
|
+
const head = JSON.parse(pair[0]);
|
|
5340
|
+
if (head.ok === false) throw __sbD1Error(head);
|
|
5341
|
+
const rows = pair[1];
|
|
5342
|
+
return { columns: head.cols, rows, native: true, meta: { changes: head.changes, last_row_id: head.rowid, rows_read: rows.length } };
|
|
5343
|
+
}
|
|
5171
5344
|
const r = JSON.parse(__sbSqlRaw(path, (strict ? "/*sb-d1-strict*/" : "") + String(sql), JSON.stringify(params || [])));
|
|
5172
5345
|
if (r.ok === false) throw strict ? __sbD1Error(r) : new Error("sqlite: " + r.error);
|
|
5173
5346
|
const meta = { changes: r.changes, last_row_id: r.rowid, rows_read: r.rows.length };
|
|
@@ -5331,6 +5504,12 @@ function __sbCall(reqJson) {
|
|
|
5331
5504
|
}
|
|
5332
5505
|
}
|
|
5333
5506
|
|
|
5507
|
+
// The same dispatch without the JSON text either side: D1 calls take this (see __sbRpcDirect).
|
|
5508
|
+
__sbCallDirect = function (req) {
|
|
5509
|
+
if (__sbCompatDate() >= "2026-10-22") __sbLtxConfigure();
|
|
5510
|
+
return __sbEmbeddedDispatch(req);
|
|
5511
|
+
};
|
|
5512
|
+
|
|
5334
5513
|
// --- local triggers ---------------------------------------------------------
|
|
5335
5514
|
// A deployed sprout is driven by the broker: it POSTs x-sb-trigger for cron
|
|
5336
5515
|
// ticks, queue batches and DO alarms. An embedded binary has no broker, so the
|
package/test/native-d1.ts
CHANGED
|
@@ -51,6 +51,33 @@ int main(void) {
|
|
|
51
51
|
char* params = strchr(sql, '\\t'); if (!params) return 2; *params++ = 0;
|
|
52
52
|
if (!strncmp(line, ":db ", 4)) { int r = sql[0] == 'p' ? sb_db_page_size(line + 4) : sb_db_quota_freeze(line + 4, sql[0] == 'c' ? -1 : sql[0] == 'f'); printf("{\\"n\\":%d}\\n", r); fflush(stdout); continue; }
|
|
53
53
|
if (!strncmp(line, ":bytes ", 7)) { line[strcspn(line, "\\t")] = 0; printf("{\\"n\\":%lld}\\n", sb_dir_bytes(line + 7, 0)); fflush(stdout); continue; }
|
|
54
|
+
// ":cells PATH": the same statement through sb_sql_run_x with cells, printed as JSON the test
|
|
55
|
+
// can compare with sb_sql_run's rows: TEXT as \\uXXXX units (sb_text_units), BLOB as {"$blob":hex}.
|
|
56
|
+
if (!strncmp(line, ":cells ", 7)) {
|
|
57
|
+
sb_cells cs = { 0, 0, 0, 0, 0 };
|
|
58
|
+
char* head = sb_sql_run_x(line + 7, sql, params, &cs);
|
|
59
|
+
printf("{\\"head\\":%s,\\"rows\\":[", head);
|
|
60
|
+
size_t nrow = cs.ncol ? cs.n / (size_t)cs.ncol : 0;
|
|
61
|
+
for (size_t r = 0; r < nrow; r++) {
|
|
62
|
+
printf(r ? ",[" : "[");
|
|
63
|
+
for (int c = 0; c < cs.ncol; c++) {
|
|
64
|
+
sb_cell* x = &cs.c[r * (size_t)cs.ncol + (size_t)c];
|
|
65
|
+
if (c) printf(",");
|
|
66
|
+
if (x->t == 0) printf("null");
|
|
67
|
+
else if (x->t == 1) printf("%.17g", x->d);
|
|
68
|
+
else if (x->t == 2) {
|
|
69
|
+
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->p, (size_t)x->n, u, &wide);
|
|
71
|
+
printf("\\"");
|
|
72
|
+
for (size_t i = 0; i < k; i++) printf("\\\\u%04x", u[i]);
|
|
73
|
+
printf("\\"");
|
|
74
|
+
free(u);
|
|
75
|
+
} else { printf("{\\"$blob\\":\\""); for (int i = 0; i < x->n; i++) printf("%02x", (unsigned char)x->p[i]); printf("\\"}"); }
|
|
76
|
+
}
|
|
77
|
+
printf("]");
|
|
78
|
+
}
|
|
79
|
+
printf("]}\\n"); fflush(stdout); sb_cells_free(&cs); free(head); continue;
|
|
80
|
+
}
|
|
54
81
|
// ":script PATH" runs SQL as a script (sb_sql_script, what exec() uses).
|
|
55
82
|
char* out = !strncmp(line, ":script ", 8) ? sb_sql_script(line + 8, sql) : sb_sql_run(line, sql, params); puts(out); fflush(stdout); free(out);
|
|
56
83
|
}
|
|
@@ -91,6 +118,59 @@ int main(void) {
|
|
|
91
118
|
);
|
|
92
119
|
assert.equal(other.rows[0][0], String(other.rows[0][0]), "outside D1 a BLOB is still one char per byte, not hex");
|
|
93
120
|
|
|
121
|
+
// Strict rows handed over as cells (the JS arrays are built from them in C) hold exactly what
|
|
122
|
+
// the JSON rows hold: NULL, integers, floats, ASCII, Latin-1, BMP and astral text, every kind
|
|
123
|
+
// of invalid UTF-8, a BLOB, and no rows at all.
|
|
124
|
+
const cellRun = (sql: string, params: unknown[] = []) => {
|
|
125
|
+
const input = `:cells ${d1}\t/*sb-d1-strict*/${sql}\t${JSON.stringify(params)}\n${d1}\t/*sb-d1-strict*/${sql}\t${JSON.stringify(params)}\n`;
|
|
126
|
+
const out = Bun.spawnSync([binary], { stdin: Buffer.from(input) });
|
|
127
|
+
assert.equal(out.exitCode, 0, out.stderr.toString());
|
|
128
|
+
const [cells, json] = out.stdout
|
|
129
|
+
.toString()
|
|
130
|
+
.trim()
|
|
131
|
+
.split("\n")
|
|
132
|
+
.map((l) => JSON.parse(l));
|
|
133
|
+
return { cells, json };
|
|
134
|
+
};
|
|
135
|
+
const bad = [
|
|
136
|
+
"x'80'",
|
|
137
|
+
"x'c0af'",
|
|
138
|
+
"x'c3'",
|
|
139
|
+
"x'e282'",
|
|
140
|
+
"x'f09f9880'",
|
|
141
|
+
"x'f0'",
|
|
142
|
+
"x'eda080'",
|
|
143
|
+
"x'f4908080'",
|
|
144
|
+
"x'e0809a'",
|
|
145
|
+
"x'41c342'",
|
|
146
|
+
"x'ffff'",
|
|
147
|
+
"x'c3a9e282'",
|
|
148
|
+
];
|
|
149
|
+
const texts = [
|
|
150
|
+
"''",
|
|
151
|
+
"'plain ascii'",
|
|
152
|
+
"'caf\u00e9'",
|
|
153
|
+
"'\u4e1c\u4eac'",
|
|
154
|
+
"'x\u{1F680}y'",
|
|
155
|
+
"'\u00ff\u0100'",
|
|
156
|
+
"char(113, 34, 92, 10, 9, 1, 127)",
|
|
157
|
+
...bad.map((b) => `CAST(${b} AS TEXT)`),
|
|
158
|
+
];
|
|
159
|
+
for (const t of texts) {
|
|
160
|
+
const { cells, json } = cellRun(`SELECT ${t} AS t, 7 AS i, 2.5 AS f, NULL AS n, x'00ff7f' AS b, ${t} || 'z' AS u`);
|
|
161
|
+
assert.equal(cells.head.ok, true);
|
|
162
|
+
assert.deepEqual(cells.rows, json.rows, `cells equal the JSON rows for ${t}`);
|
|
163
|
+
assert.deepEqual(cells.head.cols, json.cols);
|
|
164
|
+
assert.equal(cells.head.changes, json.changes);
|
|
165
|
+
}
|
|
166
|
+
const empty = cellRun("SELECT a FROM t WHERE 0");
|
|
167
|
+
assert.deepEqual(empty.cells.rows, []);
|
|
168
|
+
assert.deepEqual(empty.json.rows, []);
|
|
169
|
+
const wide = cellRun("SELECT * FROM t");
|
|
170
|
+
assert.deepEqual(wide.cells.rows, wide.json.rows, "a table with a BLOB and NULLs");
|
|
171
|
+
const failed = cellRun("SELECT nope FROM nowhere");
|
|
172
|
+
assert.equal(failed.cells.head.ok, false);
|
|
173
|
+
|
|
94
174
|
// An object that is not a blob binds NULL and the parse carries on (a "}" in a string included).
|
|
95
175
|
const [, kept] = run([
|
|
96
176
|
[d1, "INSERT INTO t VALUES (?1, ?2, ?3)", [{ a: "}{" }, 5, "x"]],
|
|
@@ -172,12 +252,23 @@ int main(void) {
|
|
|
172
252
|
};
|
|
173
253
|
// oxlint-disable-next-line no-function-constructor -- lifting a block out of the text-only transport
|
|
174
254
|
const scriptRaw = (path: string, text: string) => sqlRaw(":script " + path, text, "[]");
|
|
255
|
+
// __sbSqlRowsRaw as the transport's C block gives it: [header JSON, rows], BLOBs as byte arrays.
|
|
256
|
+
const rowsOf =
|
|
257
|
+
(raw: (path: string, text: string, params: string) => string) => (path: string, text: string, params: string) => {
|
|
258
|
+
const r = JSON.parse(raw(":cells " + path, text, params));
|
|
259
|
+
if (r.head.ok === false) return [JSON.stringify(r.head), undefined];
|
|
260
|
+
const rows = r.rows.map((row: unknown[]) =>
|
|
261
|
+
row.map((c) => (c instanceof Object && "$blob" in c ? [...Buffer.from(String(c.$blob), "hex")] : c)),
|
|
262
|
+
);
|
|
263
|
+
return [JSON.stringify(r.head), rows];
|
|
264
|
+
};
|
|
175
265
|
const ops = new Function(
|
|
176
266
|
"__sbSqlRaw",
|
|
177
267
|
"__sbSql",
|
|
178
268
|
"__sbSqlScriptRaw",
|
|
269
|
+
"__sbSqlRowsRaw",
|
|
179
270
|
`${transport.slice(opsStart, opsEnd)}\nreturn { __sbD1Run, __sbD1Batch, __sbD1Open, __sbD1ExecLines };`,
|
|
180
|
-
)(sqlRaw, sql, scriptRaw);
|
|
271
|
+
)(sqlRaw, sql, scriptRaw, rowsOf(sqlRaw));
|
|
181
272
|
// The storage quota, lifted from "The storage quota" to __sbStore, for DIR and LIMIT.
|
|
182
273
|
const quotaStart = transport.indexOf("// The storage quota: an app's data directory");
|
|
183
274
|
const quotaEnd = transport.indexOf("function __sbStore() {");
|
|
@@ -233,8 +324,9 @@ int main(void) {
|
|
|
233
324
|
"__sbSqlRaw",
|
|
234
325
|
"__sbSql",
|
|
235
326
|
"__sbSqlScriptRaw",
|
|
327
|
+
"__sbSqlRowsRaw",
|
|
236
328
|
`${transport.slice(opsStart, opsEnd)}\nreturn { __sbD1Run, __sbD1Batch, __sbD1Open, __sbD1ExecLines };`,
|
|
237
|
-
)(raw, run, (path: string, text: string) => raw(":script " + path, text, "[]"));
|
|
329
|
+
)(raw, run, (path: string, text: string) => raw(":script " + path, text, "[]"), rowsOf(raw));
|
|
238
330
|
const lift = (dir: string, limit: number) =>
|
|
239
331
|
// oxlint-disable-next-line no-function-constructor -- lifting a block out of the text-only transport
|
|
240
332
|
new Function(
|
|
@@ -312,7 +404,7 @@ int main(void) {
|
|
|
312
404
|
"D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT (extended: SQLITE_CONSTRAINT_FOREIGNKEY)",
|
|
313
405
|
);
|
|
314
406
|
const strictRow = ops.__sbD1Run(db2, "SELECT 1 AS a, ?1 AS a", [{ $blob: "0a" }], true);
|
|
315
|
-
assert.deepEqual([strictRow.columns, strictRow.rows], [["a", "a"], [[1,
|
|
407
|
+
assert.deepEqual([strictRow.columns, strictRow.rows], [["a", "a"], [[1, [10]]]]);
|
|
316
408
|
assert.throws(
|
|
317
409
|
() => ops.__sbD1Run(db2, "SELEC 1", [], true),
|
|
318
410
|
(e: Error) => e.message === 'D1_ERROR: near "SELEC": syntax error at offset 0: SQLITE_ERROR',
|
|
@@ -413,6 +505,7 @@ int main(void) {
|
|
|
413
505
|
"__sbSqlRaw",
|
|
414
506
|
"__sbSql",
|
|
415
507
|
"__sbSqlScriptRaw",
|
|
508
|
+
"__sbSqlRowsRaw",
|
|
416
509
|
`${transport.slice(opsStart, opsEnd)}\nreturn { __sbD1Run, __sbD1Batch };`,
|
|
417
510
|
)(
|
|
418
511
|
(path: string, text: string, params: string) => {
|
|
@@ -421,6 +514,7 @@ int main(void) {
|
|
|
421
514
|
},
|
|
422
515
|
sql,
|
|
423
516
|
scriptRaw,
|
|
517
|
+
rowsOf(sqlRaw),
|
|
424
518
|
);
|
|
425
519
|
const LTX_FAILED =
|
|
426
520
|
"D1_ERROR: LTX capture failed: the transaction may be committed locally but is not durable in the replication log; writes are refused until restart: SQLITE_IOERR";
|