Skip to content
File

Blob: src/workerd/api/tests/sql-test.js

javascript1753 lines
1// Copyright (c) 2023 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import * as assert from 'node:assert';
6import { DurableObject } from 'cloudflare:workers';
7 
8// A collection of functions that can be triggered by name from the DO, to make
9// it easier to keep the DO and test functions together in the test file:
10let actorFuncs = {};
11 
12async function test(state) {
13 const storage = state.storage;
14 const sql = storage.sql;
15 // Test numeric results
16 const resultNumber = [...sql.exec('SELECT 123')];
17 assert.equal(resultNumber.length, 1);
18 assert.equal(resultNumber[0]['123'], 123);
19 
20 // Test raw results
21 const resultNumberRaw = [...sql.exec('SELECT 123').raw()];
22 assert.equal(resultNumberRaw.length, 1);
23 assert.equal(resultNumberRaw[0].length, 1);
24 assert.equal(resultNumberRaw[0][0], 123);
25 
26 sql.exec('SELECT 123');
27 sql.exec('SELECT 123');
28 sql.exec('SELECT 123');
29 
30 // Test string results
31 const resultStr = [...sql.exec("SELECT 'hello'")];
32 assert.equal(resultStr.length, 1);
33 assert.equal(resultStr[0]["'hello'"], 'hello');
34 
35 // Test blob results
36 const resultBlob = [...sql.exec("SELECT x'ff' as blob")];
37 assert.equal(resultBlob.length, 1);
38 const blob = new Uint8Array(resultBlob[0].blob);
39 assert.equal(blob.length, 1);
40 assert.equal(blob[0], 255);
41 
42 {
43 // Test binding values
44 const result = [...sql.exec('SELECT ?', 456)];
45 assert.equal(result.length, 1);
46 assert.equal(result[0]['?'], 456);
47 }
48 
49 {
50 // Test multiple binding values
51 const result = [...sql.exec('SELECT ? + ?', 123, 456)];
52 assert.equal(result.length, 1);
53 assert.equal(result[0]['? + ?'], 579);
54 }
55 
56 {
57 // Test multiple rows
58 const result = [
59 ...sql.exec(
60 'SELECT 1 AS value\n' +
61 'UNION ALL\n' +
62 'SELECT 2 AS value\n' +
63 'UNION ALL\n' +
64 'SELECT 3 AS value;'
65 ),
66 ];
67 assert.equal(result.length, 3);
68 assert.equal(result[0]['value'], 1);
69 assert.equal(result[1]['value'], 2);
70 assert.equal(result[2]['value'], 3);
71 }
72 
73 {
74 // Test multiple rows, manual iteration with next().
75 const cursor = sql.exec(
76 'SELECT 1 AS col\n' +
77 'UNION ALL\n' +
78 'SELECT "foo" AS col\n' +
79 'UNION ALL\n' +
80 'SELECT 3 AS col;'
81 );
82 assert.deepEqual(cursor.next(), { done: false, value: { col: 1 } });
83 assert.deepEqual(cursor.next(), { done: false, value: { col: 'foo' } });
84 assert.deepEqual(cursor.next(), { done: false, value: { col: 3 } });
85 assert.deepEqual(cursor.next(), { done: true });
86 }
87 
88 {
89 // Test multiple rows using .toArray()
90 const cursor = sql.exec(
91 'SELECT 1 AS value\n' +
92 'UNION ALL\n' +
93 'SELECT "foo" AS value\n' +
94 'UNION ALL\n' +
95 'SELECT 3 AS value;'
96 );
97 assert.deepEqual(cursor.toArray(), [
98 { value: 1 },
99 { value: 'foo' },
100 { value: 3 },
101 ]);
102 }
103 
104 {
105 // Test one row with .one()
106 let cursor = sql.exec('SELECT 123 AS foo, "abc" AS bar');
107 assert.deepEqual(cursor.one(), { foo: 123, bar: 'abc' });
108 // Cursor has been consumed.
109 assert.deepEqual([...cursor], []);
110 
111 // Multiple results throws.
112 assert.throws(
113 () =>
114 sql
115 .exec(
116 'SELECT 1 AS value\n' + 'UNION ALL\n' + 'SELECT "foo" AS value;'
117 )
118 .one(),
119 /Expected exactly one result from SQL query, but got multiple results/
120 );
121 
122 // No results throws.
123 sql.exec('CREATE TABLE IF NOT EXISTS empty (x INTEGER)');
124 assert.throws(
125 () => sql.exec('SELECT * from empty;').one(),
126 /Expected exactly one result from SQL query, but got no results/
127 );
128 sql.exec('DROP TABLE empty');
129 }
130 
131 // Test partial query ingestion
132 assert.deepEqual(sql.ingest(`SELECT 123; SELECT 456; `).remainder, ' ');
133 assert.deepEqual(sql.ingest(`SELECT 123; SELECT 456;`).remainder, '');
134 assert.deepEqual(
135 sql.ingest(`SELECT 123; SELECT 456`).remainder,
136 ' SELECT 456'
137 );
138 assert.deepEqual(sql.ingest(`SELECT 123; SELECT 45`).remainder, ' SELECT 45');
139 assert.deepEqual(sql.ingest(`SELECT 123; SELECT 4`).remainder, ' SELECT 4');
140 assert.deepEqual(sql.ingest(`SELECT 123; SELECT `).remainder, ' SELECT ');
141 assert.deepEqual(sql.ingest(`SELECT 123; SELECT`).remainder, ' SELECT');
142 assert.deepEqual(sql.ingest(`SELECT 123; SELEC`).remainder, ' SELEC');
143 assert.deepEqual(sql.ingest(`SELECT 123; SELE`).remainder, ' SELE');
144 assert.deepEqual(sql.ingest(`SELECT 123; SEL`).remainder, ' SEL');
145 assert.deepEqual(sql.ingest(`SELECT 123; SE`).remainder, ' SE');
146 assert.deepEqual(sql.ingest(`SELECT 123; S`).remainder, ' S');
147 assert.deepEqual(sql.ingest(`SELECT 123; `).remainder, ' ');
148 assert.deepEqual(sql.ingest(`SELECT 123;`).remainder, '');
149 assert.deepEqual(sql.ingest(`SELECT 123`).remainder, 'SELECT 123');
150 assert.deepEqual(sql.ingest(`SELECT 12`).remainder, 'SELECT 12');
151 assert.deepEqual(sql.ingest(`SELECT 1`).remainder, 'SELECT 1');
152 
153 // Exec throws with trailing comments
154 assert.throws(
155 () => sql.exec('SELECT 123; SELECT 456; -- trailing comment'),
156 /SQL code did not contain a statement/
157 );
158 // Ingest does not
159 assert.deepEqual(
160 sql.ingest(`SELECT 123; SELECT 456; -- trailing comment`).remainder,
161 ' -- trailing comment'
162 );
163 
164 // Ingest throws if statement looks "complete" but is actually a syntax error:
165 assert.throws(
166 () => sql.ingest(`SELECT * bunk;`),
167 /Error: near "bunk": syntax error at offset/
168 );
169 assert.throws(
170 () => sql.ingest(`INSER INTO xyz VALUES ('a'),('b');`),
171 /Error: near "INSER": syntax error/
172 );
173 assert.throws(
174 () => sql.ingest(`INSERT INTO xyz VALUES ('a')('b');`),
175 /Error: near "\(": syntax error/
176 );
177 
178 // Test execution of ingested queries by taking an input of 6 INSERT statements, that all
179 // add 6 rows of data, then splitting that into a bunch of chunks, then ingesting them all
180 {
181 sql.exec(`CREATE TABLE streaming(val TEXT);`);
182 
183 // Convert to binary otherwise .split can cause corruption for multi-byte chars
184 const inputBytes = new TextEncoder().encode(INSERT_36_ROWS);
185 const decoder = new TextDecoder();
186 
187 // Use a chunk size 1, 3, 9, 27, 81, ... bytes
188 for (let length = 1; length < inputBytes.length; length = length * 3) {
189 let totalRowsWritten = 0;
190 let totalSqlStatements = 0;
191 let buffer = '';
192 for (let offset = 0; offset < inputBytes.length; offset += length) {
193 // Simulate a single "chunk" arriving
194 const chunk = inputBytes.slice(offset, offset + length);
195 
196 // Append the new chunk to the existing buffer
197 buffer += decoder.decode(chunk, { stream: true });
198 
199 // Ingest any complete statements and snip those chars off the buffer
200 let result = sql.ingest(buffer);
201 buffer = result.remainder;
202 totalRowsWritten += result.rowsWritten;
203 totalSqlStatements += result.statementCount;
204 
205 // Simulate awaiting next chunk
206 await scheduler.wait(1);
207 }
208 
209 // Verify exactly 36 rows were added
210 assert.deepEqual(Array.from(sql.exec(`SELECT count(*) FROM streaming`)), [
211 { 'count(*)': 36 },
212 ]);
213 
214 // Ensure our precious emoji were preserved, even if their bytes occur across split points
215 assert.deepEqual(
216 Array.from(sql.exec(`SELECT * FROM streaming WHERE val LIKE 'f%'`)),
217 [
218 { val: 'f: ๐Ÿ˜ณ' },
219 { val: 'f: ๐Ÿซ ' },
220 { val: 'f: ๐Ÿ™ƒ' },
221 { val: 'f: ๐Ÿคก' },
222 { val: 'f: ๐Ÿฅบ' },
223 { val: 'f: ๐Ÿ”ฅ๐Ÿ˜Ž๐Ÿ”ฅ' },
224 ]
225 );
226 
227 // Verify that all 36 rows we inserted were accounted for.
228 assert.equal(totalRowsWritten, 36);
229 assert.equal(totalSqlStatements, 6);
230 
231 sql.exec(`DELETE FROM streaming`);
232 await scheduler.wait(1);
233 }
234 sql.exec(`DROP TABLE streaming;`);
235 }
236 
237 // Test count
238 {
239 const result = [
240 ...sql.exec(
241 'SELECT count(value) from (SELECT 1 AS value\n' +
242 'UNION ALL\n' +
243 'SELECT 2 AS value\n' +
244 'UNION ALL\n' +
245 'SELECT 3 AS value);'
246 ),
247 ];
248 assert.equal(result.length, 1);
249 assert.equal(result[0]['count(value)'], 3);
250 }
251 
252 // Test sum
253 {
254 const result = [
255 ...sql.exec(
256 'SELECT sum(value) from (SELECT 1 AS value\n' +
257 'UNION ALL\n' +
258 'SELECT 2 AS value\n' +
259 'UNION ALL\n' +
260 'SELECT 3 AS value);'
261 ),
262 ];
263 assert.equal(result.length, 1);
264 assert.equal(result[0]['sum(value)'], 6);
265 }
266 
267 // Test math functions enabled
268 {
269 const result = [...sql.exec('SELECT cos(0)')];
270 assert.equal(result.length, 1);
271 assert.equal(result[0]['cos(0)'], 1);
272 }
273 
274 // Empty statements
275 assert.throws(() => sql.exec(''), 'SQL code did not contain a statement');
276 assert.throws(() => sql.exec(';'), 'SQL code did not contain a statement');
277 
278 // Invalid statements
279 assert.throws(() => sql.exec('SELECT ;'), /syntax error at offset 7/);
280 assert.throws(() => sql.exec('SELECT -;'), /syntax error at offset 8/);
281 
282 // Data type mismatch
283 sql.exec(`CREATE TABLE test_error_codes (name TEXT);`);
284 assert.throws(
285 () =>
286 sql.exec(
287 `INSERT INTO test_error_codes(rowid, name) values ('yeah','nah');`
288 ),
289 /Error: datatype mismatch: SQLITE_MISMATCH/
290 );
291 sql.exec(`DROP TABLE test_error_codes;`);
292 
293 // Incorrect number of binding values
294 assert.throws(
295 () => sql.exec('SELECT ?'),
296 'Error: Wrong number of parameter bindings for SQL query.'
297 );
298 
299 // Prepared statement
300 const prepared = sql.prepare('SELECT 789');
301 const resultPrepared = [...prepared()];
302 assert.equal(resultPrepared.length, 1);
303 assert.equal(resultPrepared[0]['789'], 789);
304 
305 // Running the same query twice, overlapping, works just fine.
306 let result1 = prepared();
307 let result2 = prepared();
308 // Iterate result2 before result1.
309 assert.equal([...result2][0]['789'], 789);
310 assert.equal([...result1][0]['789'], 789);
311 
312 // That said if a cursor was already done before the statement was re-run, it's not considered
313 // canceled.
314 prepared();
315 assert.equal([...result2].length, 0);
316 
317 // Prepared statement with binding values
318 const preparedWithBinding = sql.prepare('SELECT ?');
319 const resultPreparedWithBinding = [...preparedWithBinding(789)];
320 assert.equal(resultPreparedWithBinding.length, 1);
321 assert.equal(resultPreparedWithBinding[0]['?'], 789);
322 
323 // Prepared statement (incorrect number of binding values)
324 assert.throws(
325 () => preparedWithBinding(),
326 'Error: Wrong number of parameter bindings for SQL query.'
327 );
328 
329 // Prepared statement with whitespace
330 const whitespace = [' ', '\t', '\n', '\r', '\v', '\f', '\r\n'];
331 
332 for (const char of whitespace) {
333 const prepared = sql.prepare(`SELECT 1;${char}`);
334 const result = [...prepared()];
335 
336 assert.equal(result.length, 1);
337 }
338 
339 // Prepared statement with multiple statements
340 assert.deepEqual([...sql.prepare('SELECT 1; SELECT 2;')()], [{ 2: 2 }]);
341 
342 // Accessing a hidden _cf_ table
343 assert.throws(
344 () => sql.exec('CREATE TABLE _cf_invalid (name TEXT)'),
345 /not authorized/
346 );
347 storage.put('blah', 123); // force creation of _cf_KV table
348 assert.throws(
349 () => sql.exec('SELECT * FROM _cf_KV'),
350 /access to _cf_KV.key is prohibited/
351 );
352 
353 // Some pragmas are completely not allowed
354 assert.throws(
355 () => sql.exec('PRAGMA hard_heap_limit = 1024'),
356 /not authorized/
357 );
358 
359 // Test reading read-only pragmas
360 {
361 const result = [...sql.exec('pragma data_version;')];
362 assert.equal(result.length, 1);
363 assert.equal(result[0]['data_version'], 2);
364 }
365 
366 // Trying to write to read-only pragmas is not allowed
367 assert.throws(
368 () => sql.exec('PRAGMA data_version = 5'),
369 /not authorized: SQLITE_AUTH/
370 );
371 assert.throws(
372 () => sql.exec('PRAGMA max_page_count = 65536'),
373 /not authorized/
374 );
375 assert.throws(
376 () => sql.exec('PRAGMA page_size = 8192'),
377 /not authorized: SQLITE_AUTH/
378 );
379 
380 // PRAGMA table_info and PRAGMA table_xinfo are allowed.
381 sql.exec('CREATE TABLE myTable (foo TEXT, bar INTEGER)');
382 {
383 let info = [...sql.exec('PRAGMA table_info(myTable)')];
384 assert.equal(info.length, 2);
385 assert.equal(info[0].name, 'foo');
386 assert.equal(info[1].name, 'bar');
387 
388 let xInfo = [...sql.exec('PRAGMA table_xinfo(myTable)')];
389 assert.equal(xInfo.length, 2);
390 assert.equal(xInfo[0].name, 'foo');
391 assert.equal(xInfo[1].name, 'bar');
392 }
393 
394 // Can't get table_info for _cf_KV.
395 assert.throws(() => sql.exec('PRAGMA table_info(_cf_KV)'), /not authorized/);
396 
397 // Testing the three valid types of inputs for quick_check
398 assert.deepEqual(Array.from(sql.exec('pragma quick_check;')), [
399 { quick_check: 'ok' },
400 ]);
401 assert.deepEqual(Array.from(sql.exec('pragma quick_check(1);')), [
402 { quick_check: 'ok' },
403 ]);
404 assert.deepEqual(Array.from(sql.exec('pragma quick_check(100);')), [
405 { quick_check: 'ok' },
406 ]);
407 assert.deepEqual(Array.from(sql.exec('pragma quick_check(myTable);')), [
408 { quick_check: 'ok' },
409 ]);
410 // But that private tables are again restricted
411 assert.throws(() => sql.exec('PRAGMA quick_check(_cf_KV)'), /not authorized/);
412 
413 // PRAGMA optimize and ANALYZE are allowed.
414 //
415 // The following sequence of calls is mentioned by https://www.sqlite.org/lang_analyze.html as how
416 // one might optmize the query planner.
417 {
418 // Dry-run. SQLite's documentation uses -1 as the debug example.
419 assert.deepEqual(Array.from(sql.exec('PRAGMA optimize(-1);')), [
420 { optimize: 'ANALYZE "main"."myTable"' },
421 { optimize: 'ANALYZE "main"."_cf_KV"' },
422 ]);
423 }
424 {
425 // Dry-run. This sets all of the bits except for the sign bit because SQLite's optimize
426 // function doesn't parse hex numbers into negative integers.
427 assert.deepEqual(Array.from(sql.exec('PRAGMA optimize(0x7fffffff);')), [
428 { optimize: 'ANALYZE "main"."myTable"' },
429 { optimize: 'ANALYZE "main"."_cf_KV"' },
430 ]);
431 }
432 {
433 let info = [...sql.exec('PRAGMA optimize=0x10002;')];
434 assert.equal(info.length, 0);
435 }
436 {
437 let info = [...sql.exec('ANALYZE;')];
438 assert.equal(info.length, 0);
439 }
440 {
441 let info = [...sql.exec('PRAGMA optimize;')];
442 assert.equal(info.length, 0);
443 }
444 
445 // Basic functions like abs() work.
446 assert.equal([...sql.exec('SELECT abs(-123)').raw()][0][0], 123);
447 
448 // We don't permit sqlite_*() functions.
449 assert.throws(
450 () => sql.exec('SELECT sqlite_version()'),
451 /not authorized to use function: sqlite_version/
452 );
453 
454 // JSON -> operator works
455 const jsonResult = [
456 ...sql.exec('SELECT \'{"a":2,"c":[4,5,{"f":7}]}\' -> \'$.c\' AS value'),
457 ][0].value;
458 assert.equal(jsonResult, '[4,5,{"f":7}]');
459 
460 // current_{date,time,timestamp} functions work
461 const resultDate = [...sql.exec('SELECT current_date')];
462 assert.equal(resultDate.length, 1);
463 // Should match results in the format "2023-06-01"
464 assert.match(resultDate[0]['current_date'], /^\d{4}-\d{2}-\d{2}$/);
465 
466 const resultTime = [...sql.exec('SELECT current_time')];
467 assert.equal(resultTime.length, 1);
468 // Should match results in the format "15:30:03"
469 assert.match(resultTime[0]['current_time'], /^\d{2}:\d{2}:\d{2}$/);
470 
471 const resultTimestamp = [...sql.exec('SELECT current_timestamp')];
472 assert.equal(resultTimestamp.length, 1);
473 // Should match results in the format "2023-06-01 15:30:03"
474 assert.match(
475 resultTimestamp[0]['current_timestamp'],
476 /^\d{4}-\d{2}-\d{2}\s{1}\d{2}:\d{2}:\d{2}$/
477 );
478 
479 // Validate that the SQLITE_LIMIT_COMPOUND_SELECT limit is enforced as expected
480 const compoundWithinLimits = [
481 ...sql.exec(
482 'SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5'
483 ),
484 ];
485 assert.equal(compoundWithinLimits.length, 5);
486 assert.throws(
487 () =>
488 sql.exec(
489 'SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6'
490 ),
491 /too many terms in compound SELECT/
492 );
493 
494 // Can't start transactions or savepoints.
495 assert.throws(
496 () => sql.exec('BEGIN TRANSACTION'),
497 /please use the state.storage.transaction\(\) or state.storage.transactionSync\(\) APIs/
498 );
499 assert.throws(
500 () => sql.exec('SAVEPOINT foo'),
501 /please use the state.storage.transaction\(\) or state.storage.transactionSync\(\) APIs/
502 );
503 
504 // Virtual tables
505 // Only fts5 and fts5vocab modules are allowed
506 assert.throws(
507 () => sql.exec(`CREATE VIRTUAL TABLE test_fts USING fts5abcd(id);`),
508 /not authorized/
509 );
510 
511 // Full text search extension
512 sql.exec(`
513 CREATE TABLE documents (
514 id INTEGER PRIMARY KEY,
515 title TEXT NOT NULL,
516 content TEXT NOT NULL
517 );
518 `);
519 
520 // Module names are case-insensitive
521 sql.exec(`
522 CREATE VIRTUAL TABLE documents_fts USING FtS5(id, title, content, tokenize = porter);
523 `);
524 sql.exec(`
525 CREATE VIRTUAL TABLE documents_fts_v_col USING fTs5VoCaB(documents_fts, col);
526 `);
527 sql.exec(`
528 CREATE VIRTUAL TABLE documents_fts_v_row USING FtS5vOcAb(documents_fts, row);
529 `);
530 sql.exec(`
531 CREATE VIRTUAL TABLE documents_fts_v_instance USING fTs5VoCaB(documents_fts, instance);
532 `);
533 
534 sql.exec(`
535 CREATE TRIGGER documents_fts_insert
536 AFTER INSERT ON documents
537 BEGIN
538 INSERT INTO documents_fts(id, title, content)
539 VALUES(new.id, new.title, new.content);
540 END;
541 `);
542 sql.exec(`
543 CREATE TRIGGER documents_fts_update
544 AFTER UPDATE ON documents
545 BEGIN
546 UPDATE documents_fts SET title=new.title, content=new.content WHERE id=old.id;
547 END;
548 `);
549 sql.exec(`
550 CREATE TRIGGER documents_fts_delete
551 AFTER DELETE ON documents
552 BEGIN
553 DELETE FROM documents_fts WHERE id=old.id;
554 END;
555 `);
556 sql.exec(`
557 INSERT INTO documents (title, content) VALUES ('Document 1', 'This is the contents of document 1 (of 2).');
558 `);
559 sql.exec(`
560 INSERT INTO documents (title, content) VALUES ('Document 2', 'This is the content of document 2 (of 2).');
561 `);
562 // Porter stemming makes 'contents' and 'content' the same
563 {
564 let results = Array.from(
565 sql.exec(`
566 SELECT * FROM documents_fts WHERE documents_fts MATCH 'content' ORDER BY rank;
567 `)
568 );
569 assert.equal(results.length, 2);
570 assert.equal(results[0].id, 1); // Stemming makes doc 1 match first
571 assert.equal(results[1].id, 2);
572 }
573 // Ranking functions
574 {
575 let results = Array.from(
576 sql.exec(`
577 SELECT *, bm25(documents_fts) FROM documents_fts WHERE documents_fts MATCH '2' ORDER BY rank;
578 `)
579 );
580 assert.equal(results.length, 2);
581 assert.equal(
582 results[0]['bm25(documents_fts)'] < results[1]['bm25(documents_fts)'],
583 true
584 ); // Better matches have lower bm25 (since they're all negative
585 assert.equal(results[0].id, 2); // Doc 2 comes first (sorted by rank)
586 assert.equal(results[1].id, 1);
587 }
588 // highlight() function
589 {
590 let results = Array.from(
591 sql.exec(`
592 SELECT highlight(documents_fts, 2, '<b>', '</b>') as output FROM documents_fts WHERE documents_fts MATCH '2' ORDER BY rank;
593 `)
594 );
595 assert.equal(results.length, 2);
596 assert.equal(
597 results[0].output,
598 `This is the content of document <b>2</b> (of <b>2</b>).`
599 ); // two matches, two highlights
600 assert.equal(
601 results[1].output,
602 `This is the contents of document 1 (of <b>2</b>).`
603 );
604 }
605 // snippet() function
606 {
607 let results = Array.from(
608 sql.exec(`
609 SELECT snippet(documents_fts, 2, '<b>', '</b>', '...', 4) as output FROM documents_fts WHERE documents_fts MATCH '2' ORDER BY rank;
610 `)
611 );
612 assert.equal(results.length, 2);
613 assert.equal(results[0].output, `...document <b>2</b> (of <b>2</b>).`); // two matches, two highlights
614 assert.equal(results[1].output, `...document 1 (of <b>2</b>).`);
615 }
616 
617 // Complex queries
618 
619 // List table info
620 {
621 let result = [
622 ...sql.exec(`
623 SELECT name as tbl_name,
624 ncol as num_columns
625 FROM pragma_table_list
626 WHERE TYPE = "table"
627 AND tbl_name NOT LIKE "sqlite_%"
628 AND tbl_name NOT LIKE "d1_%"
629 AND tbl_name NOT LIKE "_cf_%"
630 ORDER BY tbl_name desc`),
631 ];
632 assert.equal(result.length, 2);
633 assert.equal(result[0].tbl_name, 'myTable');
634 assert.equal(result[0].num_columns, 2);
635 assert.equal(result[1].tbl_name, 'documents');
636 assert.equal(result[1].num_columns, 3);
637 }
638 
639 // Similar query using JSON objects
640 {
641 const jsonResult = JSON.parse(
642 Array.from(
643 sql.exec(
644 `SELECT json_group_array(json_object(
645 'type', type,
646 'name', name,
647 'tbl_name', tbl_name,
648 'rootpage', rootpage,
649 'sql', sql,
650 'columns', (SELECT json_group_object(name, type) from pragma_table_info(tbl_name))
651 )) as data
652 FROM sqlite_master
653 WHERE type = "table" AND tbl_name != "_cf_KV";`
654 )
655 )[0].data
656 );
657 assert.equal(jsonResult.length, 12);
658 assert.equal(
659 jsonResult.map((r) => r.name).join(','),
660 'myTable,sqlite_stat1,documents,documents_fts,documents_fts_data,documents_fts_idx,documents_fts_content,documents_fts_docsize,documents_fts_config,documents_fts_v_col,documents_fts_v_row,documents_fts_v_instance'
661 );
662 assert.equal(jsonResult[0].columns.foo, 'TEXT');
663 assert.equal(jsonResult[0].columns.bar, 'INTEGER');
664 assert.equal(jsonResult[2].columns.id, 'INTEGER');
665 assert.equal(jsonResult[2].columns.title, 'TEXT');
666 assert.equal(jsonResult[2].columns.content, 'TEXT');
667 }
668 
669 let assertValidBool = (name, val) => {
670 sql.exec('PRAGMA defer_foreign_keys = ' + name + ';');
671 assert.equal(
672 [...sql.exec('PRAGMA defer_foreign_keys;')][0].defer_foreign_keys,
673 val
674 );
675 };
676 let assertInvalidBool = (name, msg) => {
677 assert.throws(
678 () => sql.exec('PRAGMA defer_foreign_keys = ' + name + ';'),
679 msg || /not authorized/
680 );
681 };
682 
683 assertValidBool('true', 1);
684 assertValidBool('false', 0);
685 assertValidBool('on', 1);
686 assertValidBool('off', 0);
687 assertValidBool('yes', 1);
688 assertValidBool('no', 0);
689 assertValidBool('1', 1);
690 assertValidBool('0', 0);
691 
692 // case-insensitive
693 assertValidBool('tRuE', 1);
694 assertValidBool('NO', 0);
695 
696 // quoted
697 assertValidBool("'true'", 1);
698 assertValidBool('"yes"', 1);
699 assertValidBool('"0"', 0);
700 
701 // whitespace is trimmed by sqlite before passing to authorizer
702 assertValidBool(' true ', 1);
703 
704 // Don't accept anything invalid...
705 assertInvalidBool('abcd');
706 assertInvalidBool('"foo"');
707 assertInvalidBool("'yes", 'unrecognized token');
708 
709 // Test database size interface.
710 assert.equal(sql.databaseSize, 40960);
711 sql.exec(`CREATE TABLE should_make_one_more_page(VALUE text);`);
712 assert.equal(sql.databaseSize, 40960 + 4096);
713 sql.exec(`DROP TABLE should_make_one_more_page;`);
714 assert.equal(sql.databaseSize, 40960);
715 
716 storage.put('txnTest', 0);
717 
718 // Try a transaction while no implicit transaction is open.
719 await scheduler.wait(1); // finish implicit txn
720 let txnResult = await storage.transaction(async () => {
721 storage.put('txnTest', 1);
722 assert.equal(await storage.get('txnTest'), 1);
723 return 'foo';
724 });
725 assert.equal(await storage.get('txnTest'), 1);
726 assert.equal(txnResult, 'foo');
727 
728 // Try a transaction while an implicit transaction is open first.
729 storage.put('txnTest', 2);
730 await storage.transaction(async () => {
731 storage.put('txnTest', 3);
732 assert.equal(await storage.get('txnTest'), 3);
733 });
734 assert.equal(await storage.get('txnTest'), 3);
735 
736 // Try a transaction that is explicitly rolled back.
737 await storage.transaction(async (txn) => {
738 storage.put('txnTest', 4);
739 assert.equal(await storage.get('txnTest'), 4);
740 txn.rollback();
741 });
742 assert.equal(await storage.get('txnTest'), 3);
743 
744 // Try a transaction that is implicitly rolled back by throwing an exception.
745 try {
746 await storage.transaction(async (txn) => {
747 storage.put('txnTest', 5);
748 assert.equal(await storage.get('txnTest'), 5);
749 throw new Error('txn failure');
750 });
751 throw new Error('expected error');
752 } catch (err) {
753 assert.equal(err.message, 'txn failure');
754 }
755 assert.equal(await storage.get('txnTest'), 3);
756 
757 // Try a nested transaction.
758 await storage.transaction(async (txn) => {
759 storage.put('txnTest', 6);
760 assert.equal(await storage.get('txnTest'), 6);
761 await storage.transaction(async (txn2) => {
762 storage.put('txnTest', 7);
763 assert.equal(await storage.get('txnTest'), 7);
764 // Let's even do an await in here for good measure.
765 await scheduler.wait(1);
766 });
767 assert.equal(await storage.get('txnTest'), 7);
768 txn.rollback();
769 });
770 assert.equal(await storage.get('txnTest'), 3);
771 
772 // Test transactionSync, success
773 {
774 await scheduler.wait(1);
775 const result = storage.transactionSync(() => {
776 sql.exec('CREATE TABLE IF NOT EXISTS should_succeed (VALUE text);');
777 return 'some data';
778 });
779 
780 assert.equal(result, 'some data');
781 
782 const results = Array.from(
783 sql.exec(`
784 SELECT * FROM sqlite_master WHERE tbl_name = 'should_succeed'
785 `)
786 );
787 assert.equal(results.length, 1);
788 }
789 
790 // Test transactionSync, failure
791 {
792 await scheduler.wait(1);
793 
794 assert.throws(
795 () =>
796 storage.transactionSync(() => {
797 sql.exec('CREATE TABLE should_be_rolled_back (VALUE text);');
798 sql.exec('SELECT * FROM misspelled_table_name;');
799 }),
800 'Error: no such table: misspelled_table_name'
801 );
802 
803 const results = Array.from(
804 sql.exec(`
805 SELECT * FROM sqlite_master WHERE tbl_name = 'should_be_rolled_back'
806 `)
807 );
808 assert.equal(results.length, 0);
809 }
810 
811 // Test transactionSync, nested
812 {
813 sql.exec('CREATE TABLE txnTest (i INTEGER)');
814 sql.exec('INSERT INTO txnTest VALUES (1)');
815 
816 let setI = sql.prepare('UPDATE txnTest SET i = ?');
817 let getIStmt = sql.prepare('SELECT i FROM txnTest');
818 let getI = () => [...getIStmt()][0].i;
819 
820 assert.equal(getI(), 1);
821 storage.transactionSync(() => {
822 setI(2);
823 assert.equal(getI(), 2);
824 
825 assert.throws(
826 () =>
827 storage.transactionSync(() => {
828 setI(3);
829 assert.equal(getI(), 3);
830 throw new Error('foo');
831 }),
832 'Error: foo'
833 );
834 
835 assert.equal(getI(), 2);
836 });
837 assert.equal(getI(), 2);
838 }
839 
840 // Test joining two tables with overlapping names
841 {
842 sql.exec(`CREATE TABLE abc (a INT, b INT, c INT);`);
843 sql.exec(`CREATE TABLE cde (c INT, d INT, e INT);`);
844 sql.exec(`INSERT INTO abc VALUES (1,2,3),(4,5,6);`);
845 sql.exec(`INSERT INTO cde VALUES (7,8,9),(1,2,3);`);
846 
847 const stmt = sql.prepare(`SELECT * FROM abc, cde`);
848 
849 // In normal iteration, data is lost
850 const objResults = Array.from(stmt());
851 assert.equal(Object.values(objResults[0]).length, 5); // duplicate column 'c' dropped
852 assert.equal(Object.values(objResults[1]).length, 5); // duplicate column 'c' dropped
853 assert.equal(Object.values(objResults[2]).length, 5); // duplicate column 'c' dropped
854 assert.equal(Object.values(objResults[3]).length, 5); // duplicate column 'c' dropped
855 
856 assert.equal(objResults[0].c, 7); // Value of 'c' is the second in the join
857 assert.equal(objResults[1].c, 1); // Value of 'c' is the second in the join
858 assert.equal(objResults[2].c, 7); // Value of 'c' is the second in the join
859 assert.equal(objResults[3].c, 1); // Value of 'c' is the second in the join
860 
861 // Iterator has a 'columnNames' property, with .raw() that lets us get the full data
862 const iterator = stmt();
863 assert.deepEqual(iterator.columnNames, ['a', 'b', 'c', 'c', 'd', 'e']);
864 const rawResults = Array.from(iterator.raw());
865 assert.equal(rawResults.length, 4);
866 assert.deepEqual(rawResults[0], [1, 2, 3, 7, 8, 9]);
867 assert.deepEqual(rawResults[1], [1, 2, 3, 1, 2, 3]);
868 assert.deepEqual(rawResults[2], [4, 5, 6, 7, 8, 9]);
869 assert.deepEqual(rawResults[3], [4, 5, 6, 1, 2, 3]);
870 
871 // After an iterator is consumed, columnNames can still be accessed.
872 assert.deepEqual(iterator.columnNames, ['a', 'b', 'c', 'c', 'd', 'e']);
873 
874 // Also works with cursors returned from .exec
875 const execIterator = sql.exec(`SELECT * FROM abc, cde`);
876 assert.deepEqual(execIterator.columnNames, ['a', 'b', 'c', 'c', 'd', 'e']);
877 assert.equal(Array.from(execIterator.raw())[0].length, 6);
878 
879 // Execute some sort of statement that returns no results, check that we can read the column
880 // names (which is empty).
881 const oneIterator = sql.exec(`UPDATE abc SET a = 1 WHERE b = 123542`);
882 assert.deepEqual(oneIterator.columnNames, []);
883 }
884 
885 await scheduler.wait(1);
886 
887 // Test for bug where a cursor constructed from a prepared statement didn't have a strong ref
888 // to the statement object.
889 {
890 sql.exec('CREATE TABLE iteratorTest (i INTEGER)');
891 sql.exec('INSERT INTO iteratorTest VALUES (0), (1)');
892 
893 let q = sql.prepare('SELECT * FROM iteratorTest')();
894 let i = 0;
895 for (let row of q) {
896 assert.equal(row.i, i++);
897 gc();
898 }
899 }
900 
901 {
902 // Test binding blobs & nulls
903 sql.exec(`CREATE TABLE test_blob (id INTEGER PRIMARY KEY, data BLOB);`);
904 sql.prepare(
905 `INSERT INTO test_blob(data) VALUES(?),(ZEROBLOB(10)),(null),(?);`
906 )(crypto.getRandomValues(new Uint8Array(12)), null);
907 const results = Array.from(sql.exec(`SELECT * FROM test_blob`));
908 assert.equal(results.length, 4);
909 assert.equal(results[0].data instanceof ArrayBuffer, true);
910 assert.equal(results[0].data.byteLength, 12);
911 assert.equal(results[1].data instanceof ArrayBuffer, true);
912 assert.equal(results[1].data.byteLength, 10);
913 assert.equal(results[2].data, null);
914 assert.equal(results[3].data, null);
915 }
916 
917 // Can rename tables
918 sql.exec(`
919 CREATE TABLE beforerename (
920 id INTEGER
921 );
922 `);
923 sql.exec(`
924 ALTER TABLE beforerename
925 RENAME TO afterrename;
926 `);
927 
928 sql.exec(`
929 CREATE TABLE altercolumns (
930 meta TEXT
931 );
932 `);
933 // Can add columns
934 sql.exec(`
935 ALTER TABLE altercolumns
936 ADD COLUMN tobedeleted TEXT;
937 `);
938 // Can rename columns within a table
939 sql.exec(`
940 ALTER TABLE altercolumns
941 RENAME COLUMN meta TO metadata
942 `);
943 // Can drop columns
944 sql.exec(`
945 ALTER TABLE altercolumns
946 DROP COLUMN tobedeleted
947 `);
948 
949 // Can add columns with a CHECK
950 sql.exec(`
951 ALTER TABLE altercolumns
952 ADD COLUMN checked_col TEXT CHECK(checked_col IN ('A','B'));
953 `);
954 
955 // The CHECK is enforced unless `ignore_check_constraints` is on
956 sql.exec(`INSERT INTO altercolumns(checked_col) VALUES ('A')`);
957 assert.throws(
958 () => sql.exec(`INSERT INTO altercolumns(checked_col) VALUES ('C')`),
959 /Error: CHECK constraint failed: checked_col IN \('A','B'\)/
960 );
961 
962 // Because there's already a row, adding another column with a CHECK
963 // but no default value will fail
964 assert.throws(
965 () =>
966 sql.exec(`
967 ALTER TABLE altercolumns
968 ADD COLUMN second_col TEXT CHECK(second_col IS NOT NULL);
969 `),
970 /Error: CHECK constraint failed/
971 );
972 
973 // ignore_check_constraints lets us bypass this for adding bad data
974 sql.exec(`PRAGMA ignore_check_constraints=ON;`);
975 sql.exec(`INSERT INTO altercolumns(checked_col) VALUES ('C')`);
976 assert.deepEqual(
977 [...sql.exec(`SELECT * FROM altercolumns`)],
978 [
979 { checked_col: 'A', metadata: null },
980 { checked_col: 'C', metadata: null },
981 ]
982 );
983 
984 // Or even adding columns that start broken (because second_col is NULL)
985 sql.exec(`
986 ALTER TABLE altercolumns
987 ADD COLUMN second_col TEXT CHECK(second_col IS NOT NULL);
988 `);
989 
990 // Turning check constraints back on doesn't actually do any checking, eagerly
991 sql.exec(`PRAGMA ignore_check_constraints=OFF;`);
992 
993 // But anything else that CHECKs that table will now fail, like adding another CHECK
994 assert.throws(
995 () =>
996 sql.exec(`
997 ALTER TABLE altercolumns
998 ADD COLUMN third_col TEXT DEFAULT 'E' CHECK(third_col IN ('E','F'));
999 `),
1000 /Error: CHECK constraint failed/
1001 );
1002 
1003 // And we can use quick_check to list out that there are now errors
1004 // (although these messages aren't great):
1005 assert.deepEqual(
1006 [...sql.exec(`PRAGMA quick_check;`)],
1007 [
1008 { quick_check: 'CHECK constraint failed in altercolumns' },
1009 { quick_check: 'CHECK constraint failed in altercolumns' },
1010 ]
1011 );
1012 
1013 // Can't create another temp table
1014 assert.throws(
1015 () =>
1016 sql.exec(`
1017 CREATE TEMP TABLE tempy AS
1018 SELECT * FROM sqlite_master;
1019 `),
1020 'Error: not authorized'
1021 );
1022 
1023 // Assert foreign keys can be truly turned off, not just deferred
1024 await state.blockConcurrencyWhile(async () => {
1025 sql.exec(`PRAGMA foreign_keys = OFF;`);
1026 });
1027 storage.transactionSync(() => {
1028 sql.exec(`
1029 CREATE TABLE A (
1030 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
1031 bId INTEGER NOT NULL REFERENCES B (id) ON DELETE RESTRICT ON UPDATE CASCADE
1032 );
1033 INSERT INTO A VALUES(1,1); -- this would throw a parse error with foreign keys on
1034 CREATE TABLE B (
1035 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT
1036 );
1037 `);
1038 });
1039 
1040 // Until we've inserted the row into B, we can detect our
1041 // foreign key violation (even with foreign_keys=OFF)
1042 assert.deepEqual(Array.from(sql.exec(`pragma foreign_key_check;`)), [
1043 { table: 'A', rowid: 1, parent: 'B', fkid: 0 },
1044 ]);
1045 sql.exec(`INSERT INTO B VALUES (1);`);
1046 assert.deepEqual(Array.from(sql.exec(`pragma foreign_key_check;`)), []);
1047 
1048 // Restore foreign keys for the rest of the tests
1049 await state.blockConcurrencyWhile(async () => {
1050 sql.exec(`PRAGMA foreign_keys = ON;`);
1051 });
1052 
1053 // Verify caching.
1054 {
1055 let isCached = (q) => {
1056 let cursor = sql.exec(q);
1057 cursor.toArray();
1058 return cursor.reusedCachedQueryForTest;
1059 };
1060 
1061 // Query based on literal string is cached.
1062 assert.equal(false, isCached('SELECT 179321'));
1063 assert.equal(true, isCached('SELECT 179321'));
1064 assert.equal(true, isCached('SELECT 179321'));
1065 
1066 // Query based on computed string is cached.
1067 assert.equal(false, isCached('SELECT "' + 'x'.repeat(4) + '"'));
1068 assert.equal(true, isCached('SELECT "' + 'x'.repeat(4) + '"'));
1069 assert.equal(true, isCached('SELECT "' + 'x'.repeat(4) + '"'));
1070 }
1071 
1072 // Verify that if we alter a table, cached statements continue to work.
1073 {
1074 sql.exec('CREATE TABLE alterTableTest (a INTEGER)');
1075 sql.exec('INSERT INTO alterTableTest VALUES (?)', 1);
1076 assert.deepStrictEqual(sql.exec('SELECT * FROM alterTableTest').toArray(), [
1077 { a: 1 },
1078 ]);
1079 sql.exec('ALTER TABLE alterTableTest ADD COLUMN b INTEGER').toArray();
1080 assert.deepStrictEqual(sql.exec('SELECT * FROM alterTableTest').toArray(), [
1081 { a: 1, b: null },
1082 ]);
1083 sql.exec('INSERT INTO alterTableTest VALUES (?, ?)', 2, 2);
1084 assert.deepStrictEqual(sql.exec('SELECT * FROM alterTableTest').toArray(), [
1085 { a: 1, b: null },
1086 { a: 2, b: 2 },
1087 ]);
1088 sql.exec('INSERT INTO alterTableTest VALUES (?, ?)', 3, 3);
1089 assert.deepStrictEqual(sql.exec('SELECT * FROM alterTableTest').toArray(), [
1090 { a: 1, b: null },
1091 { a: 2, b: 2 },
1092 { a: 3, b: 3 },
1093 ]);
1094 }
1095}
1096 
1097async function testIoStats(storage) {
1098 const sql = storage.sql;
1099 
1100 sql.exec(`CREATE TABLE tbl (id INTEGER PRIMARY KEY, value TEXT)`);
1101 sql.exec(
1102 `INSERT INTO tbl (id, value) VALUES (?, ?)`,
1103 100000,
1104 'arbitrary-initial-value'
1105 );
1106 await scheduler.wait(1);
1107 
1108 // When writing, the rowsWritten count goes up.
1109 {
1110 const cursor = sql.exec(
1111 `INSERT INTO tbl (id, value) VALUES (?, ?)`,
1112 1,
1113 'arbitrary-value'
1114 );
1115 Array.from(cursor); // Consume all the results
1116 assert.equal(cursor.rowsWritten, 1);
1117 }
1118 
1119 // When reading, the rowsRead count goes up.
1120 {
1121 const cursor = sql.exec(`SELECT * FROM tbl`);
1122 Array.from(cursor); // Consume all the results
1123 assert.equal(cursor.rowsRead, 2);
1124 }
1125 
1126 // Each invocation of a prepared statement gets its own counters.
1127 {
1128 const id1 = 101;
1129 const id2 = 202;
1130 
1131 const prepared = sql.prepare(`INSERT INTO tbl (id, value) VALUES (?, ?)`);
1132 const cursor123 = prepared(id1, 'value1');
1133 Array.from(cursor123);
1134 assert.equal(cursor123.rowsWritten, 1);
1135 
1136 const cursor456 = prepared(id2, 'value2');
1137 Array.from(cursor456);
1138 assert.equal(cursor456.rowsWritten, 1);
1139 assert.equal(cursor123.rowsWritten, 1); // remained unchanged
1140 }
1141 
1142 // Row counters are updated as you consume the cursor.
1143 {
1144 sql.exec(`DELETE FROM tbl`);
1145 const prepared = sql.prepare(`INSERT INTO tbl (id, value) VALUES (?, ?)`);
1146 for (let i = 1; i <= 10; i++) {
1147 Array.from(prepared(i, 'value' + i));
1148 }
1149 
1150 const cursor = sql.exec(`SELECT * FROM tbl`);
1151 const resultsIterator = cursor[Symbol.iterator]();
1152 let rowsSeen = 0;
1153 while (true) {
1154 const result = resultsIterator.next();
1155 if (result.done) {
1156 assert.equal(10, cursor.rowsRead);
1157 break;
1158 }
1159 // + 1 because the cursor is always one result ahead of what has been returned -- but there
1160 // are only 10 rows total.
1161 assert.equal(Math.min(++rowsSeen + 1, 10), cursor.rowsRead);
1162 }
1163 }
1164 
1165 // Row counters can track interleaved cursors
1166 {
1167 const join = [];
1168 const colCounts = [];
1169 // In-JS joining of two tables should be possible:
1170 const rows = sql.exec(`SELECT * FROM abc`);
1171 for (let row of rows) {
1172 const cols = sql.exec(`SELECT * FROM cde`);
1173 for (let col of cols) {
1174 join.push({ row, col });
1175 }
1176 colCounts.push(cols.rowsRead);
1177 }
1178 assert.deepEqual(join, [
1179 { col: { c: 7, d: 8, e: 9 }, row: { a: 1, b: 2, c: 3 } },
1180 { col: { c: 1, d: 2, e: 3 }, row: { a: 1, b: 2, c: 3 } },
1181 { col: { c: 7, d: 8, e: 9 }, row: { a: 4, b: 5, c: 6 } },
1182 { col: { c: 1, d: 2, e: 3 }, row: { a: 4, b: 5, c: 6 } },
1183 ]);
1184 assert.deepEqual(rows.rowsRead, 2);
1185 assert.deepEqual(colCounts, [2, 2]);
1186 }
1187 
1188 // Temporary tables (i.e. for IN clauses) don't contribute to rowsWritten
1189 {
1190 const cursor = sql.exec(`SELECT * FROM abc WHERE a IN (1,2,3,4,5,6)`);
1191 const _rows = Array.from(cursor);
1192 assert.deepEqual(cursor.rowsRead, 2);
1193 assert.deepEqual(cursor.rowsWritten, 0);
1194 }
1195}
1196 
1197async function testForeignKeys(storage) {
1198 const sql = storage.sql;
1199 
1200 // Test defer_foreign_keys
1201 {
1202 sql.exec(`CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);`);
1203 sql.exec(
1204 `CREATE TABLE posts (id INTEGER PRIMARY KEY, user_id INTEGER, content TEXT, FOREIGN KEY(user_id) REFERENCES users(id));`
1205 );
1206 
1207 await scheduler.wait(1);
1208 
1209 // By default, primary keys are enforced:
1210 assert.throws(
1211 () =>
1212 sql.exec(
1213 `INSERT INTO posts (user_id, content) VALUES (?, ?)`,
1214 1,
1215 'Post 1'
1216 ),
1217 /Error: FOREIGN KEY constraint failed/
1218 );
1219 
1220 // Transactions fail immediately too
1221 let passed_first_statement = false;
1222 assert.throws(
1223 () =>
1224 storage.transactionSync(() => {
1225 sql.exec(
1226 `INSERT INTO posts (user_id, content) VALUES (?, ?)`,
1227 1,
1228 'Post 1'
1229 );
1230 passed_first_statement = true;
1231 }),
1232 /Error: FOREIGN KEY constraint failed/
1233 );
1234 assert.equal(passed_first_statement, false);
1235 
1236 await scheduler.wait(1);
1237 
1238 // With defer_foreign_keys, we can insert things out-of-order within transactions,
1239 // as long as the data is valid by the end.
1240 storage.transactionSync(() => {
1241 sql.exec(`PRAGMA defer_foreign_keys=ON;`);
1242 sql.exec(
1243 `INSERT INTO posts (user_id, content) VALUES (?, ?)`,
1244 1,
1245 'Post 1'
1246 );
1247 sql.exec(`INSERT INTO users VALUES (?, ?)`, 1, 'Alice');
1248 });
1249 
1250 await scheduler.wait(1);
1251 
1252 // But if we use defer_foreign_keys but try to commit, it resets the DO
1253 storage.transactionSync(() => {
1254 sql.exec(`PRAGMA defer_foreign_keys=ON;`);
1255 sql.exec(
1256 `INSERT INTO posts (user_id, content) VALUES (?, ?)`,
1257 2,
1258 'Post 2'
1259 );
1260 });
1261 }
1262}
1263 
1264async function testStreamingIngestion(request, storage) {
1265 const { sql } = storage;
1266 
1267 sql.exec(`CREATE TABLE streaming(val TEXT);`);
1268 
1269 await storage.transaction(async () => {
1270 const stream = request.body.pipeThrough(new TextDecoderStream());
1271 let buffer = '';
1272 
1273 for await (const chunk of stream) {
1274 // Append the new chunk to the existing buffer
1275 buffer += chunk;
1276 
1277 // Ingest any complete statements and snip those chars off the buffer
1278 buffer = sql.ingest(buffer).remainder;
1279 }
1280 });
1281 
1282 // Verify exactly 36 rows were added
1283 assert.deepEqual(Array.from(sql.exec(`SELECT count(*) FROM streaming`)), [
1284 { 'count(*)': 36 },
1285 ]);
1286 assert.deepEqual(
1287 Array.from(sql.exec(`SELECT * FROM streaming WHERE val LIKE 'f%'`)),
1288 [
1289 { val: 'f: ๐Ÿ˜ณ' },
1290 { val: 'f: ๐Ÿซ ' },
1291 { val: 'f: ๐Ÿ™ƒ' },
1292 { val: 'f: ๐Ÿคก' },
1293 { val: 'f: ๐Ÿฅบ' },
1294 { val: 'f: ๐Ÿ”ฅ๐Ÿ˜Ž๐Ÿ”ฅ' },
1295 ]
1296 );
1297}
1298 
1299export class DurableObjectExample extends DurableObject {
1300 constructor(state, env) {
1301 super(state, env);
1302 this.state = state;
1303 }
1304 
1305 async fetch(req) {
1306 if (req.url.endsWith('/sql-test')) {
1307 await test(this.state);
1308 return Response.json({ ok: true });
1309 } else if (req.url.endsWith('/sql-test-foreign-keys')) {
1310 await testForeignKeys(this.state.storage);
1311 return Response.json({ ok: true });
1312 } else if (req.url.endsWith('/increment')) {
1313 let val = (await this.state.storage.get('counter')) || 0;
1314 ++val;
1315 this.state.storage.put('counter', val);
1316 return Response.json(val);
1317 } else if (req.url.endsWith('/break')) {
1318 // This `put()` should be discarded due to the actor aborting immediately after.
1319 this.state.storage.put('counter', 888);
1320 
1321 // Abort the actor, which also cancels unflushed writes.
1322 this.state.abort('test broken');
1323 
1324 // abort() always throws.
1325 throw new Error("can't get here");
1326 } else if (req.url.endsWith('/sql-test-io-stats')) {
1327 await testIoStats(this.state.storage);
1328 return Response.json({ ok: true });
1329 } else if (req.url.endsWith('/streaming-ingestion')) {
1330 await testStreamingIngestion(req, this.state.storage);
1331 return Response.json({ ok: true });
1332 } else if (req.url.endsWith('/deleteAll')) {
1333 this.state.storage.put('counter', 888); // will be deleted
1334 this.state.storage.deleteAll();
1335 assert.strictEqual(await this.state.storage.get('counter'), undefined);
1336 return Response.json({ ok: true });
1337 }
1338 
1339 throw new Error('unknown url: ' + req.url);
1340 }
1341 
1342 async testRollbackKvInit() {
1343 // Test what happens if initialization of the _cf_KV table gets rolled back.
1344 
1345 try {
1346 this.state.storage.transactionSync(() => {
1347 // Cause KV table to be initialized.
1348 this.state.storage.put('foo', 123);
1349 
1350 // Roll back the transaction by throwing.
1351 throw new Error('bar');
1352 });
1353 throw new Error('expected error');
1354 } catch (err) {
1355 if (err.message != 'bar') throw err;
1356 }
1357 
1358 // Now try to put to KV again. This will create the `_cf_KV` table again.
1359 await this.state.storage.put('foo', 456);
1360 }
1361 
1362 async testRollbackAlarmInit() {
1363 // Much like testRollbackKvInit() but for alarms.
1364 
1365 try {
1366 this.state.storage.transactionSync(() => {
1367 // Cause KV table to be initialized.
1368 this.state.storage.setAlarm(Date.now() + 86400 * 365);
1369 
1370 // Roll back the transaction by throwing.
1371 throw new Error('bar');
1372 });
1373 throw new Error('expected error');
1374 } catch (err) {
1375 if (err.message != 'bar') throw err;
1376 }
1377 
1378 assert.strictEqual(await this.state.storage.getAlarm(), null);
1379 await this.state.storage.setAlarm(Date.now() + 86400 * 365);
1380 }
1381 
1382 async alarm() {}
1383 
1384 async testMultiStatement() {
1385 // Performing this PRAGMA will cause sqlite to invalidate prepared statements and re-compile
1386 // them the next time they are executed. (Probably, many other pragmas would have the same
1387 // effect, but this is the one that we observed causing issues.)
1388 //
1389 // In particular, the prepared statement ActorSqlite::beginTxn, which is simply
1390 // `BEGIN TRANSACTION`, will be invalidated and recompiled on the next invocation.
1391 //
1392 // When we perform our multi-statement exec below, the first line will invoke the
1393 // `ActorSqlite::onWrite` callback, which will invoke `beginTxn`. Because `BEGIN TRANSACTION`
1394 // must be recompiled, the SQLite authorizer callback will be invoked to check if it is
1395 // authorized. But we use the authorizer callback to detect when SQLite has parsed a statement
1396 // as a transaction statement. At one point, we had a bug where we incorrectly thought that
1397 // the authorizer was being called on behalf of the statement we were trying to parse and
1398 // execute, namely, `CREATE TABLE items...`. We therefore incorrectly made note that this
1399 // statement was beginning a transaction. This led the transaction state tracking to become
1400 // all wrong!
1401 //
1402 // This only turned out to be an issue when performing a multi-statement exec(), because in
1403 // this case all statements except the last are executed inside the parse loop, which is why
1404 // we misinterpreted the authorizer callback.
1405 this.state.storage.sql.exec('PRAGMA case_sensitive_like = TRUE');
1406 
1407 let cursor = this.state.storage.sql.exec(`
1408 CREATE TABLE items(i INTEGER, s TEXT);
1409 CREATE INDEX itemsIdx ON items(s);
1410 INSERT INTO items VALUES (123, "abc");
1411 INSERT INTO items VALUES (456, "def");
1412 SELECT i FROM items WHERE s = "abc";
1413 `);
1414 
1415 assert.deepEqual([...cursor], [{ i: 123 }]);
1416 }
1417 
1418 async testSessionsAPIBookmark(previousBookmark) {
1419 if (previousBookmark) {
1420 await this.state.storage.waitForBookmark(previousBookmark);
1421 }
1422 let bookmark = await this.state.storage.getCurrentBookmark();
1423 if (previousBookmark) {
1424 assert.ok(previousBookmark < bookmark, "new bookmark didn't advance!");
1425 }
1426 return bookmark;
1427 }
1428 
1429 async createStringTable() {
1430 this.state.storage.sql.exec(
1431 'CREATE TABLE IF NOT EXISTS string_table (id INTEGER PRIMARY KEY, data BLOB)'
1432 );
1433 }
1434 
1435 async getStringTableIds() {
1436 return Array.from(
1437 this.state.storage.sql.exec('SELECT id FROM string_table'),
1438 (x) => x.id
1439 );
1440 }
1441 
1442 async runActorFunc(name) {
1443 return actorFuncs[name](this.state);
1444 }
1445}
1446 
1447export default {
1448 async test(ctrl, env, ctx) {
1449 let id = env.ns.idFromName('A');
1450 let obj = env.ns.get(id);
1451 
1452 // Now let's test persistence through breakage and atomic write coalescing.
1453 let doReq = async (path, init = {}) => {
1454 let resp = await obj.fetch('http://foo/' + path, init);
1455 return await resp.json();
1456 };
1457 
1458 // Test SQL API
1459 assert.deepEqual(await doReq('sql-test'), { ok: true });
1460 
1461 // Test SQL IO stats
1462 assert.deepEqual(await doReq('sql-test-io-stats'), { ok: true });
1463 
1464 // Test SQL streaming ingestion
1465 assert.deepEqual(
1466 await doReq('streaming-ingestion', {
1467 method: 'POST',
1468 body: new ReadableStream({
1469 async start(controller) {
1470 const data = new TextEncoder().encode(INSERT_36_ROWS);
1471 
1472 // Pick a value for chunkSize that splits the first emoji in half
1473 const chunkSize = INSERT_36_ROWS.indexOf('๐Ÿ˜ณ') + 1;
1474 assert.equal(chunkSize, 35); // Validate we're getting the value we expect
1475 
1476 // Send each chunk with a wait of 1ms in between
1477 for (
1478 let offset = 0;
1479 offset < data.length - 1;
1480 offset += chunkSize
1481 ) {
1482 controller.enqueue(data.slice(offset, offset + chunkSize));
1483 await scheduler.wait(1);
1484 }
1485 
1486 controller.close();
1487 },
1488 }),
1489 }),
1490 { ok: true }
1491 );
1492 
1493 // Test defer_foreign_keys (explodes the DO)
1494 await assert.rejects(async () => {
1495 await doReq('sql-test-foreign-keys');
1496 }, /constraints were violated: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT/);
1497 
1498 // Since the DO was exploded, reusing the stub dosen't work.
1499 await assert.rejects(async () => {
1500 await doReq('increment');
1501 }, /constraints were violated: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT/);
1502 
1503 // Get a new stub.
1504 obj = env.ns.get(id);
1505 
1506 // Some increments.
1507 assert.equal(await doReq('increment'), 1);
1508 assert.equal(await doReq('increment'), 2);
1509 
1510 // Now induce a failure.
1511 await assert.rejects(
1512 async () => {
1513 await doReq('break');
1514 },
1515 (err) => err.message === 'test broken' && err.durableObjectReset
1516 );
1517 
1518 // Get a new stub.
1519 obj = env.ns.get(id);
1520 
1521 // Everything's still consistent.
1522 assert.equal(await doReq('increment'), 3);
1523 
1524 // Delete all: increments start over
1525 await doReq('deleteAll');
1526 assert.equal(await doReq('increment'), 1);
1527 assert.equal(await doReq('increment'), 2);
1528 },
1529};
1530 
1531export let testRollbackKvInit = {
1532 async test(ctrl, env, ctx) {
1533 let stub = env.ns.get(env.ns.idFromName('rollback-kv-test'));
1534 await stub.testRollbackKvInit();
1535 await stub.testRollbackAlarmInit();
1536 },
1537};
1538 
1539export let testMultiStatement = {
1540 async test(ctrl, env, ctx) {
1541 let stub = env.ns.get(env.ns.idFromName('multi-statement-test'));
1542 await stub.testMultiStatement();
1543 },
1544};
1545 
1546const INSERT_36_ROWS = ['a', 'b', 'c', 'd', 'e', 'f']
1547 .map(
1548 (prefix) =>
1549 `INSERT INTO streaming VALUES ${['๐Ÿ˜ณ', '๐Ÿซ ', '๐Ÿ™ƒ', '๐Ÿคก', '๐Ÿฅบ', '๐Ÿ”ฅ๐Ÿ˜Ž๐Ÿ”ฅ']
1550 .map((suffix) => `('${prefix}: ${suffix}')`)
1551 .join(',')};`
1552 )
1553 .join(' ');
1554 
1555export let testSessionsAPIBookmark = {
1556 async test(ctrl, env, ctx) {
1557 let stub = env.ns.get(env.ns.idFromName('sessions-api-bookmark-test'));
1558 let bookmark = undefined;
1559 for (let i = 0; i < 20; ++i) {
1560 bookmark = await stub.testSessionsAPIBookmark(bookmark);
1561 }
1562 },
1563};
1564 
1565export let testAutoRollBackOnCriticalError = {
1566 async test(ctrl, env, ctx) {
1567 let id = env.ns.idFromName('auto-rollback-on-critical-error-test');
1568 let stub = env.ns.get(id);
1569 await stub.createStringTable();
1570 
1571 // Even though the DO function catches and handles all exceptions, we still expect it to fail
1572 // with the critical exception, due to the output gate being broken with it.
1573 await assert.rejects(async () => {
1574 await stub.runActorFunc('doAutoRollBackOnCriticalError');
1575 }, /^Error: database or disk is full: SQLITE_FULL/);
1576 
1577 // Get a new stub since the old stub is broken due to critical error
1578 stub = env.ns.get(id);
1579 // We expect only the first, committed row to be present:
1580 assert.deepStrictEqual(await stub.getStringTableIds(), [1]);
1581 },
1582};
1583actorFuncs.doAutoRollBackOnCriticalError = async (state) => {
1584 // Limit size of db so we can trigger a SQLITE_FULL error
1585 state.storage.sql.setMaxPageCountForTest(10);
1586 
1587 // Add a row as part of an implicit transaction, and wait for it to commit.
1588 state.storage.sql.exec('INSERT INTO string_table VALUES (?, ?)', 1, 'a');
1589 await state.storage.sync();
1590 
1591 // Add another row as part of a new implicit transaction
1592 state.storage.sql.exec('INSERT INTO string_table VALUES (?, ?)', 2, 'a');
1593 
1594 // Try to add a row that is too big for the database. We expect this to fail with a critical
1595 // error that rolls back the current implicit transaction and breaks the output gate:
1596 assert.throws(() => {
1597 state.storage.sql.exec(
1598 'INSERT INTO string_table VALUES (?, ?)',
1599 3,
1600 'a'.repeat(1000000)
1601 );
1602 }, /^Error: database or disk is full: SQLITE_FULL/);
1603 
1604 // Further storage ops are expected to fail because we've cached the critical error:
1605 assert.throws(() => {
1606 state.storage.sql.exec('INSERT INTO string_table VALUES (?, ?)', 4, 'a');
1607 }, /^Error: database or disk is full: SQLITE_FULL/);
1608};
1609 
1610export let testCriticalErrorOnTransactionSyncRollback = {
1611 async test(ctrl, env, ctx) {
1612 let id = env.ns.idFromName('critical-error-on-transaction-sync-rollback');
1613 let stub = env.ns.get(id);
1614 await stub.createStringTable();
1615 
1616 await assert.rejects(async () => {
1617 await stub.runActorFunc('doCriticalErrorOnTransactionSyncRollback');
1618 }, /^Error: database or disk is full: SQLITE_FULL/);
1619 
1620 // Get a new stub since the old stub is broken due to critical error
1621 stub = env.ns.get(id);
1622 // We expect only the first, committed row to be present:
1623 assert.deepStrictEqual(await stub.getStringTableIds(), [1]);
1624 },
1625};
1626actorFuncs.doCriticalErrorOnTransactionSyncRollback = async (state) => {
1627 // Limit size of db so we can trigger a SQLITE_FULL error
1628 state.storage.sql.setMaxPageCountForTest(10);
1629 
1630 // Add a row as part of an implicit transaction, and wait for it to commit.
1631 state.storage.sql.exec('INSERT INTO string_table VALUES (?, ?)', 1, 'a');
1632 await state.storage.sync();
1633 
1634 // Add another row as part of a new implicit transaction. We expect this to also get rolled
1635 // back when the subsequent transactionSync() fails.
1636 state.storage.sql.exec('INSERT INTO string_table VALUES (?, ?)', 2, 'a');
1637 
1638 // Try to add a row that is too big for the database, within an explicit synchronous
1639 // transaction. We expect this to fail with a critical error that rolls back the explicit
1640 // transaction and breaks the output gate. Earlier versions of the code failed here with
1641 // an internal error "no such savepoint: _cf_sync_savepoint_0" when trying to roll back.
1642 assert.throws(() => {
1643 state.storage.transactionSync(() => {
1644 assert.throws(() => {
1645 state.storage.sql.exec(
1646 'INSERT INTO string_table VALUES (?, ?)',
1647 3,
1648 'a'.repeat(1000000)
1649 );
1650 }, /^Error: database or disk is full: SQLITE_FULL/);
1651 
1652 // Throw an exception to make transactionSync() attempt to roll back the transaction:
1653 throw new Error('an_escaping_exception_to_trigger_rollback');
1654 });
1655 }, /^Error: an_escaping_exception_to_trigger_rollback/);
1656};
1657 
1658export let testCriticalErrorOnTransactionSyncCommit = {
1659 async test(ctrl, env, ctx) {
1660 let id = env.ns.idFromName('critical-error-on-transaction-sync-commit');
1661 let stub = env.ns.get(id);
1662 await stub.createStringTable();
1663 
1664 await assert.rejects(async () => {
1665 await stub.runActorFunc('doCriticalErrorOnTransactionSyncCommit');
1666 }, /^Error: database or disk is full: SQLITE_FULL/);
1667 
1668 // Get a new stub since the old stub is broken due to critical error
1669 stub = env.ns.get(id);
1670 // We expect only the first, committed row to be present:
1671 assert.deepStrictEqual(await stub.getStringTableIds(), [1]);
1672 },
1673};
1674actorFuncs.doCriticalErrorOnTransactionSyncCommit = async (state) => {
1675 // Limit size of db so we can trigger a SQLITE_FULL error
1676 state.storage.sql.setMaxPageCountForTest(10);
1677 
1678 // Add a row as part of an implicit transaction, and wait for it to commit.
1679 state.storage.sql.exec('INSERT INTO string_table VALUES (?, ?)', 1, 'a');
1680 await state.storage.sync();
1681 
1682 // Add another row as part of a new implicit transaction. We expect this to also get rolled
1683 // back when the subsequent transactionSync() fails.
1684 state.storage.sql.exec('INSERT INTO string_table VALUES (?, ?)', 2, 'a');
1685 
1686 // Try to add a row that is too big for the database, within an explicit synchronous
1687 // transaction. We expect this to fail with a critical error that rolls back the explicit
1688 // transaction and breaks the output gate. Earlier versions of the code failed here with
1689 // internal errors "no such savepoint: _cf_sync_savepoint_0" when trying to commit, then roll
1690 // back.
1691 assert.throws(() => {
1692 state.storage.transactionSync(() => {
1693 assert.throws(() => {
1694 state.storage.sql.exec(
1695 'INSERT INTO string_table VALUES (?, ?)',
1696 3,
1697 'a'.repeat(1000000)
1698 );
1699 }, /^Error: database or disk is full: SQLITE_FULL/);
1700 // Because the lambda completes successfully, transactionSync() will still try to commit the
1701 // transaction.
1702 });
1703 }, /^Error: Cannot commit transaction due to an earlier SQL critical error/);
1704};
1705 
1706export let testCriticalErrorOnTransactionRollback = {
1707 async test(ctrl, env, ctx) {
1708 let id = env.ns.idFromName('critical-error-on-transaction-rollback');
1709 let stub = env.ns.get(id);
1710 await stub.createStringTable();
1711 
1712 await assert.rejects(async () => {
1713 await stub.runActorFunc('doCriticalErrorOnTransactionRollback');
1714 }, /^Error: database or disk is full: SQLITE_FULL/);
1715 
1716 // Get a new stub since the old stub is broken due to critical error
1717 stub = env.ns.get(id);
1718 // We expect only the first two committed rows to be present:
1719 assert.deepStrictEqual(await stub.getStringTableIds(), [1, 2]);
1720 },
1721};
1722actorFuncs.doCriticalErrorOnTransactionRollback = async (state) => {
1723 // Limit size of db so we can trigger a SQLITE_FULL error
1724 state.storage.sql.setMaxPageCountForTest(10);
1725 
1726 // Add a row as part of an implicit transaction, and wait for it to commit.
1727 state.storage.sql.exec('INSERT INTO string_table VALUES (?, ?)', 1, 'a');
1728 await state.storage.sync();
1729 
1730 // Add another row as part of a new implicit transaction. We expect this to be committed
1731 // prior to the failing explicit transaction.
1732 state.storage.sql.exec('INSERT INTO string_table VALUES (?, ?)', 2, 'a');
1733 
1734 // Try to add a row that is too big for the database, within an explicit asynchronous
1735 // transaction. We expect this to fail with a critical error that rolls back the explicit
1736 // transaction and breaks the output gate.
1737 await assert.rejects(async () => {
1738 await state.storage.transaction(async (txn) => {
1739 assert.throws(() => {
1740 state.storage.sql.exec(
1741 'INSERT INTO string_table VALUES (?, ?)',
1742 3,
1743 'a'.repeat(1000000)
1744 );
1745 }, /^Error: database or disk is full: SQLITE_FULL/);
1746 
1747 // Explicitly roll back transaction. In earlier versions of the code, this could throw
1748 // "no such savepoint: _cf_savepoint_0" due to a missing brokenness check.
1749 txn.rollback();
1750 });
1751 }, /^Error: database or disk is full: SQLITE_FULL/);
1752};