Skip to content

Commit 1f80bd9

Browse files
committed
Pivot to having a Conn class
I didn't want to do this, but having explored lots of other things it feels like the best way to go. Having a single sql function was a cool idea but would have been trouble when mixed with pooling / vfs / transactions. Just too many things need access to the sqlite3 ptr. And at least as written we can update the pointer contained inside the Conn which means commands are still possible.
1 parent 8e9a4e6 commit 1f80bd9

File tree

7 files changed

+409
-344
lines changed

7 files changed

+409
-344
lines changed

dist/conn.mjs

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import { OutOfMemError, Trait } from './util.mjs';
2+
import { default as sqlite_initialized, main_ptr, sqlite3, mem8, memdv, read_str, alloc_str, encoder, decoder, handle_error } from './sqlite.mjs';
3+
import {
4+
SQLITE_ROW, SQLITE_DONE,
5+
SQLITE_OPEN_URI, SQLITE_OPEN_CREATE, SQLITE_OPEN_EXRESCODE, SQLITE_OPEN_READWRITE,
6+
SQLITE_INTEGER, SQLITE_FLOAT, SQLITE3_TEXT, SQLITE_BLOB, SQLITE_NULL
7+
} from "./sqlite_def.mjs";
8+
9+
export const SqlCommand = new Trait("This trait marks special commands which can be used inside template literals tagged with the Conn.sql tag.");
10+
11+
export class OpenParams {
12+
pathname = ":memory:";
13+
flags = SQLITE_OPEN_URI | SQLITE_OPEN_CREATE | SQLITE_OPEN_EXRESCODE | SQLITE_OPEN_READWRITE
14+
vfs = "";
15+
async [SqlCommand](conn) {
16+
await conn.open(this);
17+
}
18+
}
19+
// This function doesn't actually open a database, it just fills out an OpenParams object from a template
20+
export function open(strings, ...args) {
21+
let seen_int = false;
22+
const ret = new OpenParams();
23+
ret.pathname = strings[0];
24+
for (let i = 0; i < args.length; ++i) {
25+
ret.pathname += strings[i + 1];
26+
const arg = args[i];
27+
if (typeof arg == 'number') {
28+
if (!seen_int) {
29+
ret.flags = 0;
30+
seen_int = true;
31+
}
32+
ret.flags |= arg;
33+
}
34+
else if (typeof arg == 'string') {
35+
ret.vfs = arg;
36+
}
37+
}
38+
return ret;
39+
}
40+
41+
function is_safe(int) {
42+
return (BigInt(Number.MIN_SAFE_INTEGER) < int) &&
43+
(int < BigInt(Number.MAX_SAFE_INTEGER));
44+
}
45+
class Bindings {
46+
inner = [];
47+
strings_from_args(strings, args) {
48+
let ret = strings[0];
49+
for (let i = 0; i < args.length; ++i) {
50+
const arg = args[i];
51+
this.inner.push(arg);
52+
if ((typeof arg != 'object' && typeof arg != 'function') || arg instanceof ArrayBuffer || ArrayBuffer.isView(arg)) {
53+
ret += '?';
54+
}
55+
ret += strings[i + 1];
56+
}
57+
return ret;
58+
}
59+
next_anon() {
60+
for (let i = 0; i < this.inner.length; ++i) {
61+
const arg = this.inner[i];
62+
if ((typeof arg != 'object' && typeof arg != 'function') || arg instanceof ArrayBuffer || ArrayBuffer.isView(arg)) {
63+
return this.inner.splice(i, 1)[0];
64+
}
65+
}
66+
}
67+
next_named() {
68+
for (let i = 0; i < this.inner.length; ++i) {
69+
const arg = this.inner[i];
70+
if (typeof arg == 'object' && !(arg instanceof ArrayBuffer) && !ArrayBuffer.isView(arg) && !(arg instanceof SqlCommand)) {
71+
return this.inner.splice(i, 1)[0];
72+
}
73+
}
74+
}
75+
command() {
76+
if (this.inner[0] instanceof SqlCommand) {
77+
return this.inner.shift();
78+
}
79+
}
80+
bind(stmt) {
81+
const num_params = sqlite3.sqlite3_bind_parameter_count(stmt);
82+
let named;
83+
for (let i = 1; i <= num_params; ++i) {
84+
const name_ptr = sqlite3.sqlite3_bind_parameter_name(stmt, i);
85+
let arg;
86+
if (name_ptr == 0) {
87+
arg = this.next_anon();
88+
} else {
89+
const name = read_str(name_ptr);
90+
const key = name.slice(1);
91+
named ??= this.next_named();
92+
arg = named[key]
93+
}
94+
const kind = typeof arg;
95+
if (kind == 'boolean') {
96+
arg = Number(arg);
97+
}
98+
if (arg instanceof ArrayBuffer) {
99+
arg = new Uint8Array(arg);
100+
}
101+
if (arg === null || typeof arg == 'undefined') {
102+
sqlite3.sqlite3_bind_null(stmt, i);
103+
}
104+
else if (kind == 'bigint') {
105+
sqlite3.sqlite3_bind_int64(stmt, i, arg);
106+
}
107+
else if (kind == 'number') {
108+
sqlite3.sqlite3_bind_double(stmt, i, arg);
109+
}
110+
else if (kind == 'string') {
111+
const encoded = encoder.encode(arg);
112+
const ptr = sqlite3.malloc(encoded.byteLength);
113+
if (!ptr) throw new OutOfMemError();
114+
mem8(ptr, encoded.byteLength).set(encoded);
115+
sqlite3.sqlite3_bind_text(stmt, i, ptr, encoded.byteLength, sqlite3.free_ptr());
116+
}
117+
else if (ArrayBuffer.isView(arg)) {
118+
const ptr = sqlite3.malloc(arg.byteLength);
119+
if (!ptr) throw new OutOfMemError();
120+
mem8(ptr, arg.byteLength).set(new Uint8Array(arg.buffer, arg.byteOffset, arg.byteLength));
121+
sqlite3.sqlite3_bind_blob(stmt, i, ptr, arg.byteLength, sqlite3.free_ptr());
122+
}
123+
else {
124+
throw new Error('Unknown parameter type.');
125+
}
126+
}
127+
}
128+
}
129+
130+
class Row {
131+
#column_names = [];
132+
constructor(stmt) {
133+
const data_count = sqlite3.sqlite3_data_count(stmt);
134+
135+
for (let i = 0; i < data_count; ++i) {
136+
const type = sqlite3.sqlite3_column_type(stmt, i);
137+
let val;
138+
if (type == SQLITE_INTEGER) {
139+
const int = sqlite3.sqlite3_column_int64(stmt, i);
140+
val = is_safe(int) ? Number(int) : int;
141+
}
142+
else if (type == SQLITE_FLOAT) {
143+
val = sqlite3.sqlite3_column_double(stmt, i);
144+
}
145+
else if (type == SQLITE3_TEXT) {
146+
const len = sqlite3.sqlite3_column_bytes(stmt, i);
147+
val = read_str(sqlite3.sqlite3_column_text(stmt, i), len);
148+
}
149+
else if (type == SQLITE_BLOB) {
150+
const len = sqlite3.sqlite3_column_bytes(stmt, i);
151+
val = mem8(sqlite3.sqlite3_column_blob(stmt, i), len).slice();
152+
}
153+
else if (type == SQLITE_NULL) {
154+
val = null;
155+
}
156+
const column_name = read_str(sqlite3.sqlite3_column_name(stmt, i));
157+
this.#column_names[i] = column_name;
158+
this[column_name] = val;
159+
}
160+
}
161+
get column_names() {
162+
return this.#column_names;
163+
}
164+
*[Symbol.iterator]() {
165+
for (const key of this.#column_names) {
166+
yield this[key];
167+
}
168+
}
169+
}
170+
171+
export class Conn {
172+
ptr = 0;
173+
// Lifecycle
174+
async open(params = new OpenParams()) {
175+
await sqlite_initialized;
176+
177+
let pathname_ptr, conn_ptr;
178+
let conn = 0;
179+
let vfs_ptr = 0;
180+
try {
181+
pathname_ptr = alloc_str(params.pathname);
182+
conn_ptr = sqlite3.malloc(4);
183+
if (params.vfs) {
184+
vfs_ptr = alloc_str(params.vfs);
185+
if (!vfs_ptr) throw new OutOfMemError();
186+
}
187+
if (!pathname_ptr || !conn_ptr) throw new OutOfMemError();
188+
189+
let res = await sqlite3.sqlite3_open_v2(pathname_ptr, conn_ptr, params.flags, vfs_ptr);
190+
conn = memdv().getInt32(conn_ptr, true);
191+
handle_error(res);
192+
} catch(e) {
193+
sqlite3.sqlite3_close_v2(conn);
194+
throw e;
195+
} finally {
196+
sqlite3.free(pathname_ptr);
197+
sqlite3.free(conn_ptr);
198+
sqlite3.free(vfs_ptr);
199+
}
200+
201+
if (this.ptr) {
202+
this.close();
203+
}
204+
this.ptr = conn;
205+
}
206+
close() {
207+
const old = this.ptr;
208+
this.ptr = 0;
209+
sqlite3.sqlite3_close_v2(old);
210+
}
211+
// Meta
212+
get filename() {
213+
if (!this.ptr) return '[no db open]';
214+
const filename_ptr = sqlite3.sqlite3_db_filename(this.ptr, main_ptr);
215+
return read_str(filename_ptr) || ':memory:';
216+
}
217+
get interrupted() {
218+
if (!this.ptr) return false;
219+
return Boolean(sqlite3.sqlite3_is_interrupted(this.ptr));
220+
}
221+
interrupt() {
222+
if (this.ptr) {
223+
sqlite3.sqlite3_interrupt(this.ptr);
224+
}
225+
}
226+
// Useful things:
227+
async *stmts(sql) {
228+
if (!sql) return; // Fast path empty sql (useful if you send a single command using Conn.sql)
229+
230+
await sqlite_initialized;
231+
232+
const sql_ptr = alloc_str(sql);
233+
const sql_len = sqlite3.strlen(sql_ptr);
234+
const sql_end_ptr = sqlite3.malloc(4);
235+
const stmt_ptr = sqlite3.malloc(4);
236+
try {
237+
if (!sql_ptr || !sql_end_ptr || !stmt_ptr) throw new OutOfMemError();
238+
memdv().setInt32(sql_end_ptr, sql_ptr, true);
239+
const sql_end = sql_ptr + sql_len;
240+
241+
while (1) {
242+
const sql_ptr = memdv().getInt32(sql_end_ptr, true);
243+
const remainder = sql_end - sql_ptr;
244+
if (remainder <= 1) break;
245+
246+
let stmt;
247+
try {
248+
// If we don't have any connection open, then connect.
249+
if (!this.ptr) await this.open();
250+
251+
const res = await sqlite3.sqlite3_prepare_v2(this.ptr, sql_ptr, remainder, stmt_ptr, sql_end_ptr);
252+
stmt = memdv().getInt32(stmt_ptr, true);
253+
handle_error(res, this.ptr);
254+
255+
if (stmt) yield stmt;
256+
} finally {
257+
sqlite3.sqlite3_finalize(stmt);
258+
}
259+
}
260+
} finally {
261+
sqlite3.free(sql_ptr);
262+
sqlite3.free(sql_end_ptr);
263+
sqlite3.free(stmt_ptr);
264+
}
265+
}
266+
async *sql(strings, ...args) {
267+
const bindings = new Bindings();
268+
const concat = bindings.strings_from_args(strings, args);
269+
270+
let command = bindings.command();
271+
if (command instanceof SqlCommand) {
272+
await command[SqlCommand](this);
273+
}
274+
for await (const stmt of this.stmts(concat)) {
275+
bindings.bind(stmt);
276+
while (1) {
277+
const res = await sqlite3.sqlite3_step(stmt);
278+
handle_error(res, this.ptr);
279+
280+
if (res == SQLITE_DONE) break;
281+
if (res != SQLITE_ROW) throw new Error("wat?");
282+
283+
yield new Row(stmt);
284+
}
285+
286+
let command = bindings.command();
287+
if (command instanceof SqlCommand) {
288+
await command[SqlCommand](this);
289+
}
290+
}
291+
}
292+
}
293+
294+
export async function exec(sql) {
295+
let last_row;
296+
for await (const row of sql) { last_row = row; }
297+
return last_row;
298+
}
299+
300+
export async function rows(sql) {
301+
const ret = [];
302+
for await(const row of sql) {
303+
ret.push(row);
304+
}
305+
return ret;
306+
}

dist/index.mjs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import { default as sqlite_initialized, sqlite3 } from './sqlite.mjs';
2+
import * as vfs from './vfs/index.mjs';
3+
14
export * from './asyncify.mjs';
2-
export * from './sqlite.mjs';
35
export * from './sqlite_def.mjs';
46
export * from './func.mjs';
5-
export * from './sql.mjs';
6-
export * from './blob.mjs';
7-
export * from './pool.mjs';
7+
export * from './conn.mjs';
8+
export {sqlite3, vfs};
9+
10+
export const initialized = (async () => {
11+
await sqlite_initialized;
812

9-
export * from './vfs/index.mjs';
13+
vfs.register_vfs(vfs.opfs, true);
14+
vfs.register_vfs(vfs.picker);
15+
})();

0 commit comments

Comments
 (0)