@evolu/sqlite-wasm 2.2.4 → 3.53.4-build1
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/LICENSE +21 -0
- package/README.md +55 -218
- package/dist/src/CApi.d.ts +2108 -0
- package/dist/src/CApi.d.ts.map +1 -0
- package/dist/src/CApi.js +1919 -0
- package/dist/src/Constants.d.ts +868 -0
- package/dist/src/Constants.d.ts.map +1 -0
- package/dist/src/Constants.js +602 -0
- package/dist/src/Database.d.ts +641 -0
- package/dist/src/Database.d.ts.map +1 -0
- package/dist/src/Database.js +1177 -0
- package/dist/src/Memory.d.ts +119 -0
- package/dist/src/Memory.d.ts.map +1 -0
- package/dist/src/Memory.js +207 -0
- package/dist/src/Pointer.d.ts +100 -0
- package/dist/src/Pointer.d.ts.map +1 -0
- package/dist/src/Pointer.js +15 -0
- package/dist/src/SahPool.d.ts +744 -0
- package/dist/src/SahPool.d.ts.map +1 -0
- package/dist/src/SahPool.js +1985 -0
- package/dist/src/Wasm.d.ts +315 -0
- package/dist/src/Wasm.d.ts.map +1 -0
- package/dist/src/Wasm.js +756 -0
- package/dist/src/WasmUrl.d.ts +21 -0
- package/dist/src/WasmUrl.d.ts.map +1 -0
- package/dist/src/WasmUrl.js +20 -0
- package/dist/src/c-api/index.d.ts +9 -0
- package/dist/src/c-api/index.d.ts.map +1 -0
- package/dist/src/c-api/index.js +8 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +13 -0
- package/dist/wasm/sqlite3.wasm +0 -0
- package/package.json +60 -60
- package/src/CApi.test.ts +217 -0
- package/src/CApi.ts +3294 -0
- package/src/Constants.ts +803 -0
- package/src/Database.ts +1714 -0
- package/src/Memory.test.ts +319 -0
- package/src/Memory.ts +237 -0
- package/src/Pointer.ts +121 -0
- package/src/SahPool.test.ts +180 -0
- package/src/SahPool.ts +2627 -0
- package/src/Wasm.test.ts +444 -0
- package/src/Wasm.ts +1026 -0
- package/src/WasmUrl.ts +26 -0
- package/src/c-api/index.ts +9 -0
- package/src/index.ts +15 -0
- package/bin/index.js +0 -110
- package/index.d.ts +0 -8118
- package/index.mjs +0 -7
- package/node.mjs +0 -3
- package/sqlite-wasm/jswasm/sqlite3-bundler-friendly.mjs +0 -13659
- package/sqlite-wasm/jswasm/sqlite3-node.mjs +0 -11671
- package/sqlite-wasm/jswasm/sqlite3-opfs-async-proxy.js +0 -691
- package/sqlite-wasm/jswasm/sqlite3-worker1-bundler-friendly.mjs +0 -35
- package/sqlite-wasm/jswasm/sqlite3-worker1-promiser.js +0 -193
- package/sqlite-wasm/jswasm/sqlite3-worker1-promiser.mjs +0 -187
- package/sqlite-wasm/jswasm/sqlite3-worker1.js +0 -46
- package/sqlite-wasm/jswasm/sqlite3.js +0 -13697
- package/sqlite-wasm/jswasm/sqlite3.mjs +0 -13661
- package/sqlite-wasm/jswasm/sqlite3.wasm +0 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
assertEqual,
|
|
5
|
+
assertErr,
|
|
6
|
+
assertFalse,
|
|
7
|
+
assertOk,
|
|
8
|
+
assertTrue,
|
|
9
|
+
} from "@evolu/common";
|
|
10
|
+
import type { SqliteCExports } from "./CApi.ts";
|
|
11
|
+
import {
|
|
12
|
+
allocCString,
|
|
13
|
+
allocWasm,
|
|
14
|
+
copyWasmBytes,
|
|
15
|
+
createSqliteScratch,
|
|
16
|
+
isInt32,
|
|
17
|
+
isSqliteInt64,
|
|
18
|
+
maxInt32,
|
|
19
|
+
maxSqliteInt64,
|
|
20
|
+
minSqliteInt64,
|
|
21
|
+
readCString,
|
|
22
|
+
readUtf8,
|
|
23
|
+
writeUtf8,
|
|
24
|
+
writeWasmBytes,
|
|
25
|
+
} from "./Memory.ts";
|
|
26
|
+
import type { CStringPtr, SqliteOwnedPtr, WasmPtr } from "./Pointer.ts";
|
|
27
|
+
import type { SqliteWasmDep } from "./Wasm.ts";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A fake {@link SqliteWasmDep} over a real `WebAssembly.Memory`, whose
|
|
31
|
+
* `sqlite3_malloc` returns NULL for 0 bytes, as SQLite's does, and for the
|
|
32
|
+
* lengths `mallocFails` accepts, and otherwise allocates 8-byte aligned memory
|
|
33
|
+
* after growing the memory by a page, so that every allocation detaches the
|
|
34
|
+
* previous heap views.
|
|
35
|
+
*/
|
|
36
|
+
const setupFakeSqliteWasm = ({
|
|
37
|
+
mallocFails = () => false,
|
|
38
|
+
}: { mallocFails?: (byteLength: number) => boolean } = {}) => {
|
|
39
|
+
const memory = new WebAssembly.Memory({ initial: 1, maximum: 1000 });
|
|
40
|
+
const mallocCalls: Array<number> = [];
|
|
41
|
+
const freed: Array<number> = [];
|
|
42
|
+
let end = 8;
|
|
43
|
+
|
|
44
|
+
const sqlite3_malloc = (byteLength: number): SqliteOwnedPtr | 0 => {
|
|
45
|
+
mallocCalls.push(byteLength);
|
|
46
|
+
if (mallocFails(byteLength) || byteLength <= 0) return 0;
|
|
47
|
+
const ptr = Math.ceil(end / 8) * 8;
|
|
48
|
+
end = ptr + byteLength;
|
|
49
|
+
memory.grow(
|
|
50
|
+
Math.max(1, Math.ceil((end - memory.buffer.byteLength) / 65536)),
|
|
51
|
+
);
|
|
52
|
+
return ptr as SqliteOwnedPtr;
|
|
53
|
+
};
|
|
54
|
+
const sqlite3_free = (ptr: SqliteOwnedPtr | 0): void => {
|
|
55
|
+
freed.push(ptr);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const deps: SqliteWasmDep = {
|
|
59
|
+
sqliteWasm: {
|
|
60
|
+
exports: { sqlite3_malloc, sqlite3_free } as unknown as SqliteCExports,
|
|
61
|
+
functionTable: new WebAssembly.Table({ initial: 0, element: "anyfunc" }),
|
|
62
|
+
getHeapU8: () => new Uint8Array(memory.buffer),
|
|
63
|
+
getHeapDataView: () => new DataView(memory.buffer),
|
|
64
|
+
call: (fn) => fn(),
|
|
65
|
+
isBroken: () => false,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
return { deps, mallocCalls, freed };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const utf8 = (value: string): Uint8Array<ArrayBuffer> =>
|
|
72
|
+
new TextEncoder().encode(value);
|
|
73
|
+
|
|
74
|
+
test("maxInt32 is the largest C int", () => {
|
|
75
|
+
assertEqual(maxInt32, 2 ** 31 - 1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("isInt32 accepts exactly the integers a C int holds", () => {
|
|
79
|
+
for (const value of [0, 1, -1, maxInt32, -maxInt32 - 1, -0])
|
|
80
|
+
assertTrue(isInt32(value));
|
|
81
|
+
for (const value of [
|
|
82
|
+
maxInt32 + 1,
|
|
83
|
+
-maxInt32 - 2,
|
|
84
|
+
0.5,
|
|
85
|
+
2 ** 53,
|
|
86
|
+
Number.NaN,
|
|
87
|
+
Number.POSITIVE_INFINITY,
|
|
88
|
+
Number.NEGATIVE_INFINITY,
|
|
89
|
+
])
|
|
90
|
+
assertFalse(isInt32(value));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("isSqliteInt64 accepts exactly the integers from minSqliteInt64 to maxSqliteInt64", () => {
|
|
94
|
+
assertEqual(maxSqliteInt64, 2n ** 63n - 1n);
|
|
95
|
+
assertEqual(minSqliteInt64, -(2n ** 63n));
|
|
96
|
+
for (const value of [0n, -1n, maxSqliteInt64, minSqliteInt64])
|
|
97
|
+
assertTrue(isSqliteInt64(value));
|
|
98
|
+
for (const value of [maxSqliteInt64 + 1n, minSqliteInt64 - 1n, 2n ** 64n])
|
|
99
|
+
assertFalse(isSqliteInt64(value));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("allocWasm allocates at least one byte, so a zero-length value keeps a non-NULL address", () => {
|
|
103
|
+
const { deps, mallocCalls } = setupFakeSqliteWasm();
|
|
104
|
+
|
|
105
|
+
const result = allocWasm(deps)(0);
|
|
106
|
+
|
|
107
|
+
assertOk(result);
|
|
108
|
+
assertTrue(result.value !== 0);
|
|
109
|
+
assertEqual(mallocCalls, [1]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("allocWasm fails with SqliteNoMem when sqlite3_malloc returns NULL", () => {
|
|
113
|
+
const { deps } = setupFakeSqliteWasm({ mallocFails: () => true });
|
|
114
|
+
|
|
115
|
+
assertErr(allocWasm(deps)(10), { type: "SqliteNoMem", byteLength: 10 });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("allocWasm fails without calling sqlite3_malloc above maxInt32, which a C int would wrap", () => {
|
|
119
|
+
const { deps, mallocCalls } = setupFakeSqliteWasm();
|
|
120
|
+
|
|
121
|
+
assertErr(allocWasm(deps)(maxInt32 + 1), {
|
|
122
|
+
type: "SqliteNoMem",
|
|
123
|
+
byteLength: maxInt32 + 1,
|
|
124
|
+
});
|
|
125
|
+
assertEqual(mallocCalls, []);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("allocWasm calls sqlite3_malloc for exactly maxInt32", () => {
|
|
129
|
+
const { deps, mallocCalls } = setupFakeSqliteWasm({
|
|
130
|
+
mallocFails: (byteLength) => byteLength === maxInt32,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
assertErr(allocWasm(deps)(maxInt32), {
|
|
134
|
+
type: "SqliteNoMem",
|
|
135
|
+
byteLength: maxInt32,
|
|
136
|
+
});
|
|
137
|
+
assertEqual(mallocCalls, [maxInt32]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("allocCString copies a string into new memory as NUL-terminated UTF-8", () => {
|
|
141
|
+
const { deps, mallocCalls } = setupFakeSqliteWasm();
|
|
142
|
+
const value = "Žluťoučký kůň 🐴";
|
|
143
|
+
|
|
144
|
+
const result = allocCString(deps)(value);
|
|
145
|
+
|
|
146
|
+
assertOk(result);
|
|
147
|
+
const bytes = utf8(value);
|
|
148
|
+
assertEqual(mallocCalls, [bytes.length + 1]);
|
|
149
|
+
assertEqual(
|
|
150
|
+
deps.sqliteWasm
|
|
151
|
+
.getHeapU8()
|
|
152
|
+
.slice(result.value, result.value + bytes.length + 1),
|
|
153
|
+
Uint8Array.of(...bytes, 0),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("allocCString fails with SqliteNoMem for the encoded length and its NUL", () => {
|
|
158
|
+
const { deps } = setupFakeSqliteWasm({ mallocFails: () => true });
|
|
159
|
+
|
|
160
|
+
assertErr(allocCString(deps)("kůň"), { type: "SqliteNoMem", byteLength: 6 });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("writeUtf8 encodes into existing memory without a NUL and returns the byte length, cutting at a character boundary", () => {
|
|
164
|
+
const { deps } = setupFakeSqliteWasm();
|
|
165
|
+
const ptr = 64 as WasmPtr;
|
|
166
|
+
const heap = deps.sqliteWasm.getHeapU8();
|
|
167
|
+
heap.fill(0xff, ptr, ptr + 16);
|
|
168
|
+
|
|
169
|
+
assertEqual(writeUtf8(deps)("kůň🐴", ptr, 16), 9);
|
|
170
|
+
assertEqual(heap.slice(ptr, ptr + 10), Uint8Array.of(...utf8("kůň🐴"), 0xff));
|
|
171
|
+
|
|
172
|
+
heap.fill(0xff, ptr, ptr + 16);
|
|
173
|
+
assertEqual(writeUtf8(deps)("kůň🐴", ptr, 7), 5);
|
|
174
|
+
assertEqual(
|
|
175
|
+
heap.slice(ptr, ptr + 8),
|
|
176
|
+
Uint8Array.of(...utf8("kůň"), 0xff, 0xff, 0xff),
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const byteOrderMark = Uint8Array.of(0xef, 0xbb, 0xbf);
|
|
181
|
+
|
|
182
|
+
test("readCString decodes up to the NUL, keeping a leading byte order mark", () => {
|
|
183
|
+
const { deps } = setupFakeSqliteWasm();
|
|
184
|
+
const ptr = 64 as CStringPtr;
|
|
185
|
+
deps.sqliteWasm
|
|
186
|
+
.getHeapU8()
|
|
187
|
+
.set([...byteOrderMark, ...utf8("kůň"), 0, ...utf8("after")], ptr);
|
|
188
|
+
|
|
189
|
+
assertEqual(readCString(deps)(ptr), "\uFEFFkůň");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("readUtf8 decodes a byte length, keeping a leading byte order mark and embedded NUL characters", () => {
|
|
193
|
+
const { deps } = setupFakeSqliteWasm();
|
|
194
|
+
const ptr = 64 as WasmPtr;
|
|
195
|
+
const bytes = Uint8Array.of(...byteOrderMark, ...utf8("a"), 0, ...utf8("ž"));
|
|
196
|
+
deps.sqliteWasm.getHeapU8().set([...bytes, ...utf8("after")], ptr);
|
|
197
|
+
|
|
198
|
+
assertEqual(readUtf8(deps)(ptr, bytes.length), "\uFEFFa\0ž");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("copyWasmBytes copies bytes into their own ArrayBuffer", () => {
|
|
202
|
+
const { deps } = setupFakeSqliteWasm();
|
|
203
|
+
const ptr = 64 as WasmPtr;
|
|
204
|
+
const heap = deps.sqliteWasm.getHeapU8();
|
|
205
|
+
heap.set([1, 2, 3, 4], ptr);
|
|
206
|
+
|
|
207
|
+
const copy = copyWasmBytes(deps)(ptr, 3);
|
|
208
|
+
heap.fill(0, ptr, ptr + 4);
|
|
209
|
+
|
|
210
|
+
assertEqual(copy, Uint8Array.of(1, 2, 3));
|
|
211
|
+
assertEqual(copy.buffer.byteLength, 3);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("writeWasmBytes copies bytes into wasm memory", () => {
|
|
215
|
+
const { deps } = setupFakeSqliteWasm();
|
|
216
|
+
const ptr = 64 as WasmPtr;
|
|
217
|
+
|
|
218
|
+
writeWasmBytes(deps)(ptr, Uint8Array.of(1, 2, 3));
|
|
219
|
+
|
|
220
|
+
assertEqual(
|
|
221
|
+
deps.sqliteWasm.getHeapU8().slice(ptr - 1, ptr + 4),
|
|
222
|
+
Uint8Array.of(0, 1, 2, 3, 0),
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("createSqliteScratch allocates 16 bytes, 8-byte aligned, for output parameters", () => {
|
|
227
|
+
const { deps, mallocCalls } = setupFakeSqliteWasm();
|
|
228
|
+
|
|
229
|
+
const result = createSqliteScratch(deps);
|
|
230
|
+
|
|
231
|
+
assertOk(result);
|
|
232
|
+
assertEqual(result.value.out % 8, 0);
|
|
233
|
+
assertEqual(mallocCalls, [16]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("SqliteScratch.reserve reuses its memory and doubles it when a value does not fit", () => {
|
|
237
|
+
const { deps, mallocCalls, freed } = setupFakeSqliteWasm();
|
|
238
|
+
const result = createSqliteScratch(deps);
|
|
239
|
+
assertOk(result);
|
|
240
|
+
const scratch = result.value;
|
|
241
|
+
|
|
242
|
+
const first = scratch.reserve(100);
|
|
243
|
+
assertOk(first);
|
|
244
|
+
assertEqual(scratch.reserve(100), first);
|
|
245
|
+
assertEqual(scratch.reserve(1), first);
|
|
246
|
+
|
|
247
|
+
const second = scratch.reserve(150);
|
|
248
|
+
assertOk(second);
|
|
249
|
+
assertEqual(freed, [first.value]);
|
|
250
|
+
assertEqual(scratch.reserve(200), second);
|
|
251
|
+
|
|
252
|
+
const third = scratch.reserve(1000);
|
|
253
|
+
assertOk(third);
|
|
254
|
+
assertEqual(freed, [first.value, second.value]);
|
|
255
|
+
assertEqual(mallocCalls, [16, 100, 200, 1000]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("SqliteScratch.reserve fails with SqliteNoMem and recovers once memory is available", () => {
|
|
259
|
+
let outOfMemory = false;
|
|
260
|
+
const { deps, freed } = setupFakeSqliteWasm({
|
|
261
|
+
mallocFails: () => outOfMemory,
|
|
262
|
+
});
|
|
263
|
+
const result = createSqliteScratch(deps);
|
|
264
|
+
assertOk(result);
|
|
265
|
+
const scratch = result.value;
|
|
266
|
+
assertOk(scratch.reserve(100));
|
|
267
|
+
|
|
268
|
+
outOfMemory = true;
|
|
269
|
+
assertErr(scratch.reserve(1000), { type: "SqliteNoMem", byteLength: 1000 });
|
|
270
|
+
|
|
271
|
+
outOfMemory = false;
|
|
272
|
+
const recovered = scratch.reserve(100);
|
|
273
|
+
assertOk(recovered);
|
|
274
|
+
assertFalse(freed.includes(recovered.value));
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("SqliteScratch.reserve falls back to the requested length when twice its capacity is unavailable, and a failure leaves it empty", () => {
|
|
278
|
+
const { deps, mallocCalls } = setupFakeSqliteWasm({
|
|
279
|
+
mallocFails: (byteLength) => byteLength > 1000,
|
|
280
|
+
});
|
|
281
|
+
const result = createSqliteScratch(deps);
|
|
282
|
+
assertOk(result);
|
|
283
|
+
const scratch = result.value;
|
|
284
|
+
assertOk(scratch.reserve(600));
|
|
285
|
+
|
|
286
|
+
mallocCalls.length = 0;
|
|
287
|
+
assertOk(scratch.reserve(700));
|
|
288
|
+
assertEqual(mallocCalls, [1200, 700]);
|
|
289
|
+
|
|
290
|
+
mallocCalls.length = 0;
|
|
291
|
+
assertErr(scratch.reserve(1001), { type: "SqliteNoMem", byteLength: 1001 });
|
|
292
|
+
assertEqual(mallocCalls, [1400, 1001]);
|
|
293
|
+
|
|
294
|
+
mallocCalls.length = 0;
|
|
295
|
+
// Not twice a capacity it no longer has.
|
|
296
|
+
assertOk(scratch.reserve(16));
|
|
297
|
+
assertEqual(mallocCalls, [16]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("disposing a SqliteScratch frees its memory", () => {
|
|
301
|
+
const { deps, freed } = setupFakeSqliteWasm();
|
|
302
|
+
const result = createSqliteScratch(deps);
|
|
303
|
+
assertOk(result);
|
|
304
|
+
const reserved = result.value.reserve(100);
|
|
305
|
+
assertOk(reserved);
|
|
306
|
+
|
|
307
|
+
result.value[Symbol.dispose]();
|
|
308
|
+
|
|
309
|
+
assertEqual(
|
|
310
|
+
freed.toSorted((a, b) => a - b),
|
|
311
|
+
[result.value.out, reserved.value].toSorted((a, b) => a - b),
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("createSqliteScratch fails with SqliteNoMem without memory", () => {
|
|
316
|
+
const { deps } = setupFakeSqliteWasm({ mallocFails: () => true });
|
|
317
|
+
|
|
318
|
+
assertErr(createSqliteScratch(deps), { type: "SqliteNoMem", byteLength: 16 });
|
|
319
|
+
});
|
package/src/Memory.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone helpers for wasm memory: allocation, UTF-8, bytes, scratch memory,
|
|
3
|
+
* and integer ranges.
|
|
4
|
+
*
|
|
5
|
+
* Like the CApi functions, each helper that touches the instance takes
|
|
6
|
+
* {@link SqliteWasmDep} and returns the operation, so a database binds it once.
|
|
7
|
+
*
|
|
8
|
+
* Rules the helpers follow and their callers must too:
|
|
9
|
+
*
|
|
10
|
+
* - Allocate at least one byte. `sqlite3_malloc(0)` returns NULL, and binding a
|
|
11
|
+
* NULL address binds SQL NULL, so an empty string or blob would silently
|
|
12
|
+
* become NULL.
|
|
13
|
+
* - Take the heap view after allocating. Growing memory detaches the previous
|
|
14
|
+
* `ArrayBuffer`, and writing to a detached view does nothing.
|
|
15
|
+
* - Decode by byte length with a decoder that keeps a leading byte order mark, so
|
|
16
|
+
* text round-trips exactly, including embedded NUL characters. The default
|
|
17
|
+
* `TextDecoder` strips it.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { disposable, err, ok, type Result, type Typed } from "@evolu/common";
|
|
23
|
+
import type {
|
|
24
|
+
CStringPtr,
|
|
25
|
+
SqliteOwnedCStringPtr,
|
|
26
|
+
SqliteOwnedPtr,
|
|
27
|
+
WasmPtr,
|
|
28
|
+
} from "./Pointer.ts";
|
|
29
|
+
import type { SqliteWasmDep } from "./Wasm.ts";
|
|
30
|
+
|
|
31
|
+
const utf8Encoder = /*#__PURE__*/ new TextEncoder();
|
|
32
|
+
|
|
33
|
+
// Keeps a leading byte order mark, which the default decoder strips.
|
|
34
|
+
const utf8Decoder = /*#__PURE__*/ new TextDecoder("utf-8", { ignoreBOM: true });
|
|
35
|
+
|
|
36
|
+
/** `sqlite3_malloc` returned NULL. */
|
|
37
|
+
export interface SqliteNoMemError extends Typed<"SqliteNoMem"> {
|
|
38
|
+
readonly byteLength: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Allocates at least one byte with `sqlite3_malloc`, so a zero-length value
|
|
43
|
+
* keeps a non-NULL address.
|
|
44
|
+
*
|
|
45
|
+
* A request above {@link maxInt32} fails without calling `sqlite3_malloc`, which
|
|
46
|
+
* takes a C int and would wrap it.
|
|
47
|
+
*/
|
|
48
|
+
export const allocWasm =
|
|
49
|
+
(deps: SqliteWasmDep) =>
|
|
50
|
+
(byteLength: number): Result<SqliteOwnedPtr, SqliteNoMemError> => {
|
|
51
|
+
const ptr =
|
|
52
|
+
byteLength > maxInt32
|
|
53
|
+
? 0
|
|
54
|
+
: deps.sqliteWasm.exports.sqlite3_malloc(Math.max(1, byteLength));
|
|
55
|
+
return ptr === 0 ? err({ type: "SqliteNoMem", byteLength }) : ok(ptr);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Copies a string into new memory as a NUL-terminated UTF-8 C string, which the
|
|
60
|
+
* caller frees with `sqlite3_free`.
|
|
61
|
+
*
|
|
62
|
+
* UTF-8 cannot encode a lone surrogate, which becomes U+FFFD, as with
|
|
63
|
+
* `TextEncoder`, so a caller that needs the exact text checks
|
|
64
|
+
* `String.prototype.isWellFormed` first.
|
|
65
|
+
*/
|
|
66
|
+
export const allocCString =
|
|
67
|
+
(deps: SqliteWasmDep) =>
|
|
68
|
+
(value: string): Result<SqliteOwnedCStringPtr, SqliteNoMemError> => {
|
|
69
|
+
const bytes = utf8Encoder.encode(value);
|
|
70
|
+
const allocated = allocWasm(deps)(bytes.length + 1);
|
|
71
|
+
if (!allocated.ok) return allocated;
|
|
72
|
+
const ptr = allocated.value as SqliteOwnedCStringPtr;
|
|
73
|
+
const heap = deps.sqliteWasm.getHeapU8();
|
|
74
|
+
heap.set(bytes, ptr);
|
|
75
|
+
heap[ptr + bytes.length] = 0;
|
|
76
|
+
return ok(ptr);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Encodes a string as UTF-8 into existing memory and returns the byte length,
|
|
81
|
+
* without a NUL terminator.
|
|
82
|
+
*
|
|
83
|
+
* The memory needs up to three bytes per UTF-16 code unit; text that does not
|
|
84
|
+
* fit is cut at a character boundary, so reserve `value.length * 3` bytes. A
|
|
85
|
+
* lone surrogate becomes U+FFFD, as in {@link allocCString}.
|
|
86
|
+
*/
|
|
87
|
+
export const writeUtf8 =
|
|
88
|
+
(deps: SqliteWasmDep) =>
|
|
89
|
+
(value: string, ptr: WasmPtr, capacity: number): number =>
|
|
90
|
+
utf8Encoder.encodeInto(
|
|
91
|
+
value,
|
|
92
|
+
deps.sqliteWasm.getHeapU8().subarray(ptr, ptr + capacity),
|
|
93
|
+
).written;
|
|
94
|
+
|
|
95
|
+
/** Decodes a NUL-terminated C string, such as an error message or a column name. */
|
|
96
|
+
export const readCString =
|
|
97
|
+
(deps: SqliteWasmDep) =>
|
|
98
|
+
(ptr: CStringPtr): string => {
|
|
99
|
+
const heap = deps.sqliteWasm.getHeapU8();
|
|
100
|
+
return utf8Decoder.decode(heap.subarray(ptr, heap.indexOf(0, ptr)));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decodes UTF-8 of a known byte length, such as a text column, keeping a
|
|
105
|
+
* leading byte order mark and embedded NUL characters.
|
|
106
|
+
*/
|
|
107
|
+
export const readUtf8 =
|
|
108
|
+
(deps: SqliteWasmDep) =>
|
|
109
|
+
(ptr: WasmPtr, byteLength: number): string =>
|
|
110
|
+
utf8Decoder.decode(
|
|
111
|
+
deps.sqliteWasm.getHeapU8().subarray(ptr, ptr + byteLength),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Copies bytes out of wasm memory into a new `ArrayBuffer`, so the result can
|
|
116
|
+
* be transferred to another worker.
|
|
117
|
+
*/
|
|
118
|
+
export const copyWasmBytes =
|
|
119
|
+
(deps: SqliteWasmDep) =>
|
|
120
|
+
(ptr: WasmPtr, byteLength: number): Uint8Array<ArrayBuffer> =>
|
|
121
|
+
deps.sqliteWasm.getHeapU8().slice(ptr, ptr + byteLength);
|
|
122
|
+
|
|
123
|
+
/** Copies bytes into wasm memory. */
|
|
124
|
+
export const writeWasmBytes =
|
|
125
|
+
(deps: SqliteWasmDep) =>
|
|
126
|
+
(ptr: WasmPtr, bytes: Uint8Array): void => {
|
|
127
|
+
deps.sqliteWasm.getHeapU8().set(bytes, ptr);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Reusable memory for output parameters and bind values, allocated once per
|
|
132
|
+
* database instead of once per call.
|
|
133
|
+
*
|
|
134
|
+
* A callback inside a call, such as a collation-needed callback or a trace
|
|
135
|
+
* callback, can run another call on the same database, which reuses this
|
|
136
|
+
* memory. The outer call is unaffected, because SQLite copies a value bound
|
|
137
|
+
* with SQLITE_TRANSIENT at once and writes an output parameter after the
|
|
138
|
+
* callbacks of the call. What SQLite reads or writes before a callback and the
|
|
139
|
+
* caller uses after it needs memory of its own: SQL, which SQLite parses in
|
|
140
|
+
* place and copies only after the parse, and the size `sqlite3_serialize`
|
|
141
|
+
* writes before it finalizes its query.
|
|
142
|
+
*/
|
|
143
|
+
export interface SqliteScratch extends Disposable {
|
|
144
|
+
/**
|
|
145
|
+
* Sixteen bytes, 8-byte aligned, for output parameters such as `ppDb`,
|
|
146
|
+
* `ppStmt` and `pzTail`.
|
|
147
|
+
*/
|
|
148
|
+
readonly out: WasmPtr;
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Returns memory of at least the given length, growing it when needed. The
|
|
152
|
+
* previous contents are not kept, and the address is valid until the next
|
|
153
|
+
* call. Growth doubles the capacity, so reallocations stay rare as values
|
|
154
|
+
* grow, or, when that much memory is not available, allocates the length
|
|
155
|
+
* alone. A failure leaves the scratch empty, so later calls are unaffected.
|
|
156
|
+
*
|
|
157
|
+
* Binding text or a blob from here with SQLITE_TRANSIENT is faster than
|
|
158
|
+
* allocating per value: SQLite copies into a buffer it reuses.
|
|
159
|
+
*/
|
|
160
|
+
readonly reserve: (byteLength: number) => Result<WasmPtr, SqliteNoMemError>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Creates a {@link SqliteScratch}. */
|
|
164
|
+
export const createSqliteScratch = (
|
|
165
|
+
deps: SqliteWasmDep,
|
|
166
|
+
): Result<SqliteScratch, SqliteNoMemError> => {
|
|
167
|
+
const out = allocWasm(deps)(16);
|
|
168
|
+
if (!out.ok) return out;
|
|
169
|
+
|
|
170
|
+
let buffer: SqliteOwnedPtr | null = null;
|
|
171
|
+
let capacity = 0;
|
|
172
|
+
|
|
173
|
+
using disposer = new DisposableStack();
|
|
174
|
+
disposer.defer(() => {
|
|
175
|
+
deps.sqliteWasm.exports.sqlite3_free(out.value);
|
|
176
|
+
});
|
|
177
|
+
disposer.defer(() => {
|
|
178
|
+
if (buffer != null) deps.sqliteWasm.exports.sqlite3_free(buffer);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return ok(
|
|
182
|
+
disposable<SqliteScratch>(
|
|
183
|
+
{
|
|
184
|
+
out: out.value,
|
|
185
|
+
reserve: (byteLength) => {
|
|
186
|
+
if (buffer != null) {
|
|
187
|
+
if (byteLength <= capacity) return ok(buffer);
|
|
188
|
+
// The contents need not be kept, so the old memory goes first.
|
|
189
|
+
deps.sqliteWasm.exports.sqlite3_free(buffer);
|
|
190
|
+
buffer = null;
|
|
191
|
+
}
|
|
192
|
+
// Twice the capacity keeps reallocations rare, but when it is not
|
|
193
|
+
// available, the requested length alone may still be.
|
|
194
|
+
let newCapacity = Math.max(byteLength, capacity * 2);
|
|
195
|
+
let allocated = allocWasm(deps)(newCapacity);
|
|
196
|
+
if (!allocated.ok && newCapacity > byteLength) {
|
|
197
|
+
newCapacity = byteLength;
|
|
198
|
+
allocated = allocWasm(deps)(newCapacity);
|
|
199
|
+
}
|
|
200
|
+
if (!allocated.ok) {
|
|
201
|
+
capacity = 0;
|
|
202
|
+
return allocated;
|
|
203
|
+
}
|
|
204
|
+
buffer = allocated.value;
|
|
205
|
+
capacity = newCapacity;
|
|
206
|
+
return ok(buffer);
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
disposer,
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** The largest C int. */
|
|
215
|
+
export const maxInt32 = 0x7fff_ffff;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Whether a number is a 32-bit integer, which binds with `sqlite3_bind_int`
|
|
219
|
+
* without a BigInt.
|
|
220
|
+
*/
|
|
221
|
+
export const isInt32 = (value: number): boolean =>
|
|
222
|
+
Number.isInteger(value) && value >= -maxInt32 - 1 && value <= maxInt32;
|
|
223
|
+
|
|
224
|
+
/** The largest SQLite INTEGER, 2^63 - 1. */
|
|
225
|
+
export const maxSqliteInt64 = 2n ** 63n - 1n;
|
|
226
|
+
|
|
227
|
+
/** The smallest SQLite INTEGER, -2^63. */
|
|
228
|
+
export const minSqliteInt64 = -(2n ** 63n);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Whether a bigint fits a SQLite INTEGER.
|
|
232
|
+
*
|
|
233
|
+
* The wasm boundary converts a bigint to i64 modulo 2^64 without an error, so
|
|
234
|
+
* anything passed to `sqlite3_bind_int64` must be checked first.
|
|
235
|
+
*/
|
|
236
|
+
export const isSqliteInt64 = (value: bigint): boolean =>
|
|
237
|
+
BigInt.asIntN(64, value) === value;
|
package/src/Pointer.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Branded pointers into SQLite's wasm memory and function table.
|
|
3
|
+
*
|
|
4
|
+
* At runtime every pointer is a wasm32 address, a plain number. The brands keep
|
|
5
|
+
* C types apart, so a statement cannot be passed where a connection is
|
|
6
|
+
* expected, borrowed memory cannot be freed, and no JavaScript string converts
|
|
7
|
+
* to a C string implicitly. Only this package creates pointer values, from what
|
|
8
|
+
* the wasm returns.
|
|
9
|
+
*
|
|
10
|
+
* The loader caps memory at 2 GiB (32768 pages), so every address is a
|
|
11
|
+
* non-negative i32 and reads with `getUint32` need no `>>> 0`.
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Brand } from "@evolu/common";
|
|
17
|
+
import type {
|
|
18
|
+
SQLITE_STATIC,
|
|
19
|
+
SQLITE_TRANSIENT,
|
|
20
|
+
SQLITE_WASM_DEALLOC,
|
|
21
|
+
} from "./Constants.ts";
|
|
22
|
+
|
|
23
|
+
/** An address in the wasm heap. */
|
|
24
|
+
export type WasmPtr = number & Brand<"WasmPtr">;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The C NULL pointer.
|
|
28
|
+
*
|
|
29
|
+
* Functions that can return NULL include it in their result type, and a `=== 0`
|
|
30
|
+
* check narrows it away.
|
|
31
|
+
*/
|
|
32
|
+
export type NullPtr = 0;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A NUL-terminated UTF-8 string.
|
|
36
|
+
*
|
|
37
|
+
* Returned C strings are borrowed from SQLite: copy them before the next call
|
|
38
|
+
* on the same object and never free them.
|
|
39
|
+
*/
|
|
40
|
+
export type CStringPtr = WasmPtr & Brand<"CString">;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Memory allocated with `sqlite3_malloc`.
|
|
44
|
+
*
|
|
45
|
+
* The holder frees it with `sqlite3_free` or hands it to SQLite, for example as
|
|
46
|
+
* a bind value with {@link SQLITE_WASM_DEALLOC}.
|
|
47
|
+
*/
|
|
48
|
+
export type SqliteOwnedPtr = WasmPtr & Brand<"SqliteOwned">;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A NUL-terminated UTF-8 string in memory allocated with `sqlite3_malloc`,
|
|
52
|
+
* which the holder frees with `sqlite3_free`.
|
|
53
|
+
*/
|
|
54
|
+
export type SqliteOwnedCStringPtr = SqliteOwnedPtr & CStringPtr;
|
|
55
|
+
|
|
56
|
+
/** An open database connection, `sqlite3*`. */
|
|
57
|
+
export type SqliteDbPtr = WasmPtr & Brand<"SqliteDb">;
|
|
58
|
+
|
|
59
|
+
/** A prepared statement, `sqlite3_stmt*`. */
|
|
60
|
+
export type SqliteStmtPtr = WasmPtr & Brand<"SqliteStmt">;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A dynamically typed SQL value, `sqlite3_value*`, such as a column value or an
|
|
64
|
+
* argument of an application-defined function.
|
|
65
|
+
*/
|
|
66
|
+
export type SqliteValuePtr = WasmPtr & Brand<"SqliteValue">;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A protected value copied with `sqlite3_value_dup`, which the holder frees
|
|
70
|
+
* with `sqlite3_value_free`.
|
|
71
|
+
*
|
|
72
|
+
* Column values, function arguments and the other values SQLite passes are
|
|
73
|
+
* borrowed, so `sqlite3_value_free` rejects them.
|
|
74
|
+
*/
|
|
75
|
+
export type SqliteOwnedValuePtr = SqliteValuePtr & Brand<"SqliteOwnedValue">;
|
|
76
|
+
|
|
77
|
+
/** The context of an application-defined function call, `sqlite3_context*`. */
|
|
78
|
+
export type SqliteContextPtr = WasmPtr & Brand<"SqliteContext">;
|
|
79
|
+
|
|
80
|
+
/** A VFS struct, `sqlite3_vfs*`. */
|
|
81
|
+
export type SqliteVfsPtr = WasmPtr & Brand<"SqliteVfs">;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A file SQLite opened through a VFS, `sqlite3_file*`.
|
|
85
|
+
*
|
|
86
|
+
* SQLite allocates it with the VFS's `szOsFile` bytes before calling `xOpen`.
|
|
87
|
+
*/
|
|
88
|
+
export type SqliteFilePtr = WasmPtr & Brand<"SqliteFile">;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* A database filename SQLite created, `sqlite3_filename`, as `xOpen` receives
|
|
92
|
+
* it.
|
|
93
|
+
*
|
|
94
|
+
* SQLite stores the URI parameters after the name, where the `sqlite3_uri_*`
|
|
95
|
+
* functions read them, so no other C string can take its place.
|
|
96
|
+
*/
|
|
97
|
+
export type SqliteFilenamePtr = CStringPtr & Brand<"SqliteFilename">;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The query planner's exchange with a virtual table's `xBestIndex`,
|
|
101
|
+
* `sqlite3_index_info*`.
|
|
102
|
+
*/
|
|
103
|
+
export type SqliteIndexInfoPtr = WasmPtr & Brand<"SqliteIndexInfo">;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A C function pointer, which in wasm is an index into the function table, not
|
|
107
|
+
* a memory address.
|
|
108
|
+
*/
|
|
109
|
+
export type SqliteFunctionPtr = number & Brand<"SqliteFunctionPtr">;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The destructor argument of `sqlite3_bind_text`, `sqlite3_bind_blob`,
|
|
113
|
+
* `sqlite3_result_text` and `sqlite3_result_blob`.
|
|
114
|
+
*
|
|
115
|
+
* {@link SQLITE_STATIC} promises the memory outlives the value,
|
|
116
|
+
* {@link SQLITE_TRANSIENT} makes SQLite copy it, and a function pointer such as
|
|
117
|
+
* {@link SQLITE_WASM_DEALLOC} hands it to SQLite, which frees it even when the
|
|
118
|
+
* call fails.
|
|
119
|
+
*/
|
|
120
|
+
export type SqliteDestructor =
|
|
121
|
+
typeof SQLITE_STATIC | typeof SQLITE_TRANSIENT | SqliteFunctionPtr;
|