├── tsconfig.json ├── .editorconfig ├── .gitignore ├── tasks ├── northwind.sh ├── memory.js ├── benchmark.js └── blob.js ├── src ├── constants.ts ├── types.ts ├── utils.ts └── index.ts ├── license ├── package.json ├── readme.md └── test └── index.js /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsex/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.err 3 | *.log 4 | ._* 5 | .cache 6 | .fseventsd 7 | .DocumentRevisions* 8 | .DS_Store 9 | .TemporaryItems 10 | .Trashes 11 | Thumbs.db 12 | 13 | dist 14 | node_modules 15 | package-lock.json 16 | northwind.sqlite 17 | -------------------------------------------------------------------------------- /tasks/northwind.sh: -------------------------------------------------------------------------------- 1 | 2 | # UPDATE 3 | wget https://github.com/jpwhite3/northwind-SQLite3/blob/591cd3253c327b1eed7155c1fec57464565c0932/Northwind_large.sqlite.zip?raw=true -O northwind.zip 4 | unzip -p northwind.zip Northwind_large.sqlite > tasks/northwind.sqlite 5 | rm northwind.zip 6 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | const MEMORY_DATABASE = ':memory:'; 5 | const TEMPORARY_DATABASE = ''; 6 | 7 | const PAGE_SIZE = ( 2 ** 14 ); // 16KB 8 | const PAGES_COUNT = ( 2 ** 30 ) - 1; 9 | 10 | /* EXPORT */ 11 | 12 | export {MEMORY_DATABASE, TEMPORARY_DATABASE, PAGE_SIZE, PAGES_COUNT}; 13 | -------------------------------------------------------------------------------- /tasks/memory.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Database from '../dist/index.js'; 5 | 6 | /* MAIN */ 7 | 8 | const dbs = []; 9 | const limit = 10_000; 10 | 11 | console.time ( 'create' ); 12 | for ( let i = 0; i < limit; i++ ) { 13 | dbs[i] = new Database ( ':memory:' ); 14 | dbs[i].query ( 'SELECT 1' ); 15 | } 16 | console.timeEnd ( 'create' ); 17 | 18 | console.log ( process.memoryUsage () ); 19 | 20 | console.time ( 'close' ); 21 | for ( let i = 0; i < limit; i++ ) { 22 | dbs[i].close (); 23 | } 24 | console.timeEnd ( 'close' ); 25 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | 2 | /* MAIN */ 3 | 4 | type Callback = { 5 | (): void 6 | }; 7 | 8 | type Dict = { 9 | [key: string]: T 10 | }; 11 | 12 | type Disposer = { 13 | (): void 14 | }; 15 | 16 | type DatabaseOptions = { 17 | bin?: string, // Path to "better-sqlite3.node" 18 | page?: number, // Bytes 19 | readonly?: boolean, 20 | size?: number, // Bytes 21 | timeout?: number, // Milliseconds 22 | wal?: boolean 23 | }; 24 | 25 | type FunctionOptions = { 26 | deterministic?: boolean, 27 | direct?: boolean, 28 | variadic?: boolean 29 | }; 30 | 31 | type In = null | undefined | string | number | bigint | Uint8Array; 32 | 33 | type Out = null | string | number | bigint | Uint8Array; 34 | 35 | /* EXPORT */ 36 | 37 | export type {Callback, Dict, Disposer, DatabaseOptions, FunctionOptions, In, Out}; 38 | -------------------------------------------------------------------------------- /tasks/benchmark.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Database from '../dist/index.js'; 5 | 6 | /* MAIN */ 7 | 8 | const northwind = new Database ( './tasks/northwind.sqlite' ); 9 | const test = new Database ( '', { wal: true } ); 10 | 11 | console.time ( 'northwind' ); 12 | northwind.query ( 'SELECT * FROM "Order"' ); 13 | northwind.query ( 'SELECT * FROM "Product"' ); 14 | northwind.query ( 'SELECT * FROM "OrderDetail" LIMIT 10000' ); 15 | console.timeEnd ( 'northwind' ); 16 | 17 | console.time ( 'general' ); 18 | test.execute ( 'CREATE TABLE lorem (info TEXT)' ); 19 | test.transaction ( () => { 20 | for ( let i = 0; i < 1000; i++ ) { 21 | test.execute ( `INSERT INTO lorem VALUES ('Ipsum${i}')` ); 22 | } 23 | }); 24 | test.query ( 'SELECT COUNT(info) AS rows FROM lorem' ); 25 | test.query ( 'SELECT * FROM lorem WHERE info IN (?, ?)', ['Ipsum 2', 'Ipsum 3'] ); 26 | test.query ( 'SELECT * FROM lorem' ); 27 | test.execute ( 'DROP TABLE lorem' ); 28 | console.timeEnd ( 'general' ); 29 | 30 | northwind.close (); 31 | test.close (); 32 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023-present Fabio Spampinato 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a 6 | copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 21 | DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tiny-sqlite3", 3 | "repository": "github:fabiospampinato/tiny-sqlite3", 4 | "description": "A tiny convenience Node client for SQLite3, based on better-sqlite3", 5 | "version": "2.0.0", 6 | "type": "module", 7 | "main": "dist/index.js", 8 | "exports": "./dist/index.js", 9 | "types": "./dist/index.d.ts", 10 | "scripts": { 11 | "benchmark": "tsex benchmark", 12 | "benchmark:watch": "tsex benchmark --watch", 13 | "blob": "tsex task --name blob", 14 | "blob:watch": "tsex task --name blob --watch", 15 | "clean": "tsex clean", 16 | "compile": "tsex compile", 17 | "compile:watch": "tsex compile --watch", 18 | "memory": "tsex task --name memory", 19 | "memory:watch": "tsex task --name memory --watch", 20 | "northwind": "bash tasks/northwind.sh", 21 | "test": "tsex test", 22 | "test:watch": "tsex test --watch", 23 | "prepublishOnly": "tsex prepare" 24 | }, 25 | "keywords": [ 26 | "sqlite3", 27 | "node", 28 | "tiny" 29 | ], 30 | "dependencies": { 31 | "better-sqlite3": "^8.5.1", 32 | "buffer2uint8": "^1.0.0", 33 | "stubborn-fs": "^1.2.5", 34 | "when-exit": "^2.1.1", 35 | "zeptoid": "^1.0.1" 36 | }, 37 | "devDependencies": { 38 | "@types/better-sqlite3": "^7.6.4", 39 | "@types/node": "^20.5.3", 40 | "fava": "^0.3.0", 41 | "tsex": "^3.0.1", 42 | "typescript": "^5.1.6" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tasks/blob.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import Database from '../dist/index.js'; 5 | 6 | /* MAIN */ 7 | 8 | const db = new Database ( '', { wal: true } ); 9 | 10 | db.query ( 'CREATE TABLE IF NOT EXISTS example ( id INTEGER PRIMARY KEY, data BLOB )' ); 11 | 12 | const sizes = [1_000, 10_000, 100_000, 250_000, 500_000, 1_000_000, 2_000_000, 5_000_000, 10_000_000, 25_000_000]; 13 | const blobs = sizes.map ( size => new TextEncoder ().encode ( 'a'.repeat ( size ) ) ); 14 | 15 | console.time ( 'roundtrips' ); 16 | 17 | for ( let i = 0, l = sizes.length; i < l; i++ ) { 18 | 19 | const size = sizes[i]; 20 | const blob = blobs[i]; 21 | 22 | console.log ( `\n[${size}]` ); 23 | console.time ( 'roundtrip' ); 24 | 25 | console.time ( 'write' ); 26 | db.query ( 'INSERT INTO example VALUES( ?, ? )', [1, blob] ); 27 | console.timeEnd ( 'write' ); 28 | 29 | console.time ( 'read' ); 30 | db.query ( 'SELECT data FROM example WHERE id=?', [1] ); 31 | db.query ( 'SELECT data FROM example WHERE id=?', [1] ); 32 | db.query ( 'SELECT data FROM example WHERE id=?', [1] ); 33 | db.query ( 'SELECT data FROM example WHERE id=?', [1] ); 34 | db.query ( 'SELECT data FROM example WHERE id=?', [1] ); 35 | console.timeEnd ( 'read' ); 36 | 37 | console.time ( 'delete' ); 38 | db.query ( 'DELETE FROM example WHERE id=?', [1] ); 39 | console.timeEnd ( 'delete' ); 40 | 41 | console.time ( 'vacuum' ); 42 | db.vacuum (); 43 | console.timeEnd ( 'vacuum' ); 44 | 45 | console.timeEnd ( 'roundtrip' ); 46 | 47 | } 48 | 49 | console.log ( '\n[total]' ); 50 | console.timeEnd ( 'roundtrips' ); 51 | 52 | db.close (); 53 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import os from 'node:os'; 5 | import path from 'node:path'; 6 | import fs from 'stubborn-fs'; 7 | import zeptoid from 'zeptoid'; 8 | import {MEMORY_DATABASE, TEMPORARY_DATABASE} from './constants'; 9 | 10 | /* MAIN */ 11 | 12 | const ensureFileSync = ( filePath: string, content: Uint8Array | string = '' ): void => { 13 | 14 | if ( fs.attempt.existsSync ( filePath ) ) return; 15 | 16 | const folderPath = path.dirname ( filePath ); 17 | 18 | try { 19 | 20 | ensureFolderSync ( folderPath ); 21 | writeFileSync ( filePath, content ); 22 | 23 | } catch {} 24 | 25 | }; 26 | 27 | const ensureFileUnlinkSync = ( filePath: string ): void => { 28 | 29 | return fs.attempt.unlinkSync ( filePath ); 30 | 31 | }; 32 | 33 | const ensureFolderSync = ( folderPath: string ): void => { 34 | 35 | fs.attempt.mkdirSync ( folderPath, { recursive: true } ); 36 | 37 | }; 38 | 39 | const getDatabasePath = ( db: Uint8Array | string ): string => { 40 | 41 | if ( db === MEMORY_DATABASE ) { 42 | 43 | return db; 44 | 45 | } else if ( db === TEMPORARY_DATABASE ) { 46 | 47 | return getTempPath (); 48 | 49 | } else if ( isUint8Array ( db ) ) { 50 | 51 | return getTempPath ( db ); 52 | 53 | } else { 54 | 55 | return path.resolve ( db ); 56 | 57 | } 58 | 59 | }; 60 | 61 | const getTempPath = ( content?: Uint8Array | string ): string => { 62 | 63 | const tempPath = path.join ( os.tmpdir (), `sqlite-${zeptoid ()}.db` ); 64 | 65 | ensureFileSync ( tempPath, content ); 66 | 67 | return tempPath; 68 | 69 | }; 70 | 71 | const isUint8Array = ( value: unknown ): value is Uint8Array => { 72 | 73 | return value instanceof Uint8Array; 74 | 75 | }; 76 | 77 | const noop = (): void => { 78 | 79 | return; 80 | 81 | }; 82 | 83 | const writeFileSync = ( filePath: string, content: Uint8Array | string ): void => { 84 | 85 | return fs.retry.writeFileSync ( 1000 )( filePath, content ); 86 | 87 | }; 88 | 89 | /* EXPORT */ 90 | 91 | export {ensureFileSync, ensureFileUnlinkSync, ensureFolderSync, getDatabasePath, getTempPath, isUint8Array, noop, writeFileSync}; 92 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Tiny SQLite3 2 | 3 | A tiny convenience Node client for SQLite3, based on [better-sqlite3](https://github.com/WiseLibs/better-sqlite3). 4 | 5 | ## Install 6 | 7 | ```sh 8 | npm install --save tiny-sqlite3 9 | ``` 10 | 11 | ## Usage 12 | 13 | ```ts 14 | import Database from 'tiny-sqlite3'; 15 | 16 | // Create an in-memory database 17 | 18 | const mem = new Database ( ':memory:' ); 19 | 20 | // Create a permament on-disk database 21 | 22 | const db = new Database ( 'foo.db' ); 23 | 24 | // Create a temporary on-disk database, which is automatically deleted when the database is closed 25 | 26 | const temp = new Database ( '' ); 27 | 28 | // Create a database with custom options 29 | 30 | const custom = new Database ( 'bar.db', { 31 | bin: '/path/to/better-sqlite3.node', // Custom path to the native module, for bundling purposes 32 | page: 16_384, // Custom page size, in bytes 33 | size: 1_000_000, // Maximum allowed size of the database, in bytes 34 | readonly: true, // Opening the database in read-only mode 35 | timeout: 60_000, // Maximum allowed time for a query to run, in milliseconds 36 | wal: true // Using the WAL journaling mode, rather than the default one 37 | }); 38 | 39 | // Read the various properties attached to the database instance 40 | 41 | db.path // => full path to the main file containing the data for the database, or ":memory:" if it's an in-memory database 42 | db.memory // => whether it's in an in-memory database or not 43 | db.readonly // => whether the database is opened in read-only mode or not 44 | db.temporary // => whether it's a temporary database or not, temporary databases are automatically deleted from disk on close 45 | 46 | db.changes // => number of rows modified, inserted or deleted by the most recent query 47 | db.lastInsertRowId // => the id of the row that was last inserted 48 | db.size // => the size of the database, in bytes 49 | db.totalChanges // => the total number of rows modified, inserted or deleted since the database was last opened 50 | db.transacting // => whether you are currently inside a transaction block or not 51 | 52 | // Perform a SQL query, without requesting any output 53 | 54 | db.execute ( 'CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )' ); 55 | 56 | // Perform a SQL query, requesting resulting rows as objects 57 | 58 | const limit = 1; 59 | const rows = db.query ( 'SELECT * FROM example LIMIT 1' ); // No interpolation 60 | const rows = db.query ( 'SELECT * FROM example LIMIT ?', [limit] ); // Array interpolation 61 | const rows = db.query ( 'SELECT * FROM example LIMIT :limit', {limit} ); // Object interpolation 62 | const rows = db.sql`SELECT * FROM example LIMIT ${limit}`; // Inline interpolation 63 | 64 | // Perform a type-aware SQL query, where both input parameters and output rows are typed 65 | 66 | type Row = { id: number, title: string, description: string }; 67 | type ParametersArray = [number, string, string]; 68 | type ParametersObject = Row; 69 | 70 | const rows = db.query ( 'SELECT * FROM example LIMIT 1' ); // No interpolation 71 | const rows = db.query ( 'SELECT * FROM example LIMIT ?', [limit] ); // Array interpolation 72 | const rows = db.query ( 'SELECT * FROM example LIMIT :limit', {limit} ); // Object interpolation 73 | 74 | // Perform a query using a precompiled query, which can be cleaner. Internally regular queries use cached precompiled queries also, for performance 75 | 76 | const getRows = db.prepare ( 'SELECT * FROM example LIMIT ?' ); 77 | const rows = getRows ([ 1 ]); 78 | 79 | // Register a custom function 80 | 81 | const sum = ( ...numbers ) => numbers.reduce ( ( sum, number ) => sum + number, 0 ); 82 | const dispose = db.function ( 'sum', sum ); 83 | const summed = db.query ( `SELECT sum(1, 2) as a, sum(1,2,3,4) as b` ); // => [{ a: 3, b: 10 }] 84 | 85 | dispose (); // Unregister the function 86 | 87 | // Backup a database to a provided path 88 | 89 | await db.backup ( 'backup.db' ); 90 | 91 | // Serialize the database to a Uint8Array, and create a new temporary database from that Uint8Array 92 | 93 | const serialized = db.serialize (); 94 | const deserialized = new Database ( serialized ); 95 | 96 | // Vacuum the database, potentially shrinking its size by reducing fragmentation caused by deleted pages 97 | 98 | db.vacuum (); 99 | 100 | // Start a transaction, which is executed immediately and rolled back automatically if the function passed to the "transaction" method throws at any point 101 | 102 | db.transaction ( () => { 103 | db.query ( 'INSERT INTO example VALUES( ?, ?, ? )', [1, 'title1', 'description1'] ); 104 | db.query ( 'INSERT INTO example VALUES( ?, ?, ? )', [2, 'title2', 'description2'] ); 105 | db.query ( 'INSERT INTO example VALUES( ?, ?, ? )', [1, 'title1', 'description1'] ); // This will cause the transaction to be rolled back automatically 106 | }); 107 | 108 | // Manually close the connection to the database 109 | // The connection is automatically re-opened if you execute another query, for convenience 110 | 111 | db.close (); 112 | ``` 113 | 114 | ## License 115 | 116 | MIT © Fabio Spampinato 117 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import sqlite3 from 'better-sqlite3'; 5 | import buffer2uint8 from 'buffer2uint8'; 6 | import whenExit from 'when-exit'; 7 | import {MEMORY_DATABASE, TEMPORARY_DATABASE, PAGE_SIZE, PAGES_COUNT} from './constants'; 8 | import {getDatabasePath, isUint8Array, ensureFileSync, ensureFileUnlinkSync, noop} from './utils'; 9 | import type {Callback, Dict, Disposer, DatabaseOptions, FunctionOptions, In, Out} from './types'; 10 | 11 | /* MAIN */ 12 | 13 | //TODO: ttl -> autocloser (maybe on a PooledDatabase class or something) 14 | //TODO: .dump 15 | //TODO: .recover 16 | 17 | class Database { 18 | 19 | /* VARIABLES */ 20 | 21 | public path: string; 22 | public memory: boolean; 23 | public readonly: boolean; 24 | public temporary: boolean; 25 | 26 | private options: DatabaseOptions; 27 | private sqlite3?: sqlite3.Database; 28 | private statements: Record any[]> = {}; 29 | private exitDisposer: Disposer = noop; 30 | 31 | /* CONSTRUCTOR */ 32 | 33 | constructor ( path: Uint8Array | string, options: DatabaseOptions = {} ) { 34 | 35 | this.path = getDatabasePath ( path ); 36 | this.memory = ( path === MEMORY_DATABASE ); 37 | this.readonly = !!options.readonly; 38 | this.temporary = ( path === TEMPORARY_DATABASE || isUint8Array ( path ) ); 39 | this.options = options; 40 | 41 | } 42 | 43 | /* GETTERS API */ 44 | 45 | private get db (): sqlite3.Database { 46 | 47 | return this.open (); 48 | 49 | } 50 | 51 | get changes (): number { 52 | 53 | return this.query<{ value: number }>( 'SELECT changes() as value' )[0].value; 54 | 55 | } 56 | 57 | get lastInsertRowId (): number { 58 | 59 | return this.query<{ value: number }>( 'SELECT last_insert_rowid() as value' )[0].value; 60 | 61 | } 62 | 63 | get size (): number { 64 | 65 | return this.query<{ value: number }>( 'SELECT page_count * page_size as value FROM pragma_page_count(), pragma_page_size()' )[0].value; 66 | 67 | } 68 | 69 | get totalChanges (): number { 70 | 71 | return this.query<{ value: number }>( 'SELECT total_changes() as value' )[0].value; 72 | 73 | } 74 | 75 | get transacting (): boolean { 76 | 77 | return this.db.inTransaction; 78 | 79 | } 80 | 81 | /* API */ 82 | 83 | backup ( path: string ): Promise { 84 | 85 | return this.db.backup ( path ).then ( noop ); 86 | 87 | } 88 | 89 | close (): void { 90 | 91 | if ( !this.sqlite3 ) return; 92 | 93 | this.exitDisposer (); 94 | 95 | this.db.close (); 96 | 97 | this.sqlite3 = undefined; 98 | 99 | if ( this.temporary ) { 100 | 101 | ensureFileUnlinkSync ( this.path ); 102 | 103 | } 104 | 105 | } 106 | 107 | execute ( sql: string ): void { 108 | 109 | this.db.exec ( sql ); 110 | 111 | } 112 | 113 | function ( name: string, fn: ( ...args: unknown[] ) => unknown, options: FunctionOptions = {} ): Disposer { 114 | 115 | const config: sqlite3.RegistrationOptions = { 116 | deterministic: !!options.deterministic, 117 | directOnly: !!options.direct, 118 | varargs: !!options.variadic 119 | }; 120 | 121 | this.db.function ( name, config, fn ); 122 | 123 | return (): void => { 124 | 125 | this.db.function ( name, config, () => { 126 | 127 | throw new Error ( `no such function: ${name}` ); 128 | 129 | }); 130 | 131 | }; 132 | 133 | } 134 | 135 | open (): sqlite3.Database { 136 | 137 | if ( this.sqlite3 ) return this.sqlite3; 138 | 139 | if ( !this.memory ) { 140 | ensureFileSync ( this.path ); 141 | } 142 | 143 | const db = this.sqlite3 = new sqlite3 ( this.path, { 144 | nativeBinding: this.options.bin, 145 | readonly: this.readonly, 146 | timeout: this.options.timeout ?? 600_000 147 | }); 148 | 149 | if ( this.options.page || this.options.size ) { 150 | const page = this.options.page || PAGE_SIZE; 151 | const size = this.options.size || ( page * PAGES_COUNT ); 152 | const maxPageCount = Math.ceil ( size / page ); 153 | db.exec ( `PRAGMA page_size=${page}` ); 154 | db.exec ( `PRAGMA max_page_count=${maxPageCount}` ); 155 | } else { 156 | db.exec ( `PRAGMA page_size=${PAGE_SIZE}` ); 157 | db.exec ( `PRAGMA max_page_count=${PAGES_COUNT}` ); 158 | } 159 | 160 | if ( this.options.wal ) { 161 | db.exec ( 'PRAGMA synchronous=NORMAL' ); 162 | db.exec ( 'PRAGMA journal_mode=WAL' ); 163 | } 164 | 165 | db.exec ( 'PRAGMA mmap_size=30000000000' ); 166 | db.exec ( 'PRAGMA temp_store=MEMORY' ); 167 | 168 | this.exitDisposer = whenExit ( () => this.close () ); 169 | 170 | return db; 171 | 172 | } 173 | 174 | prepare = Dict, P extends Array | Dict = Array | Dict> ( sql: string ): (( params?: P ) => R[]) { 175 | 176 | return this.statements[sql] ||= (() => { 177 | 178 | const statement = this.db.prepare( sql ); 179 | 180 | return ( params?: P ) => { 181 | 182 | if ( statement.reader ) { 183 | 184 | return params ? statement.all ( params ) : statement.all (); 185 | 186 | } else { 187 | 188 | params ? statement.run ( params ) : statement.run (); 189 | 190 | return []; 191 | 192 | } 193 | 194 | }; 195 | 196 | })(); 197 | 198 | } 199 | 200 | query = Dict, P extends Array | Dict = Array | Dict> ( sql: string, params?: P ): R[] { 201 | 202 | return this.prepare( sql )( params ); 203 | 204 | } 205 | 206 | serialize (): Uint8Array { 207 | 208 | return buffer2uint8 ( this.db.serialize () ); 209 | 210 | } 211 | 212 | sql = Dict, P extends Array = Array> ( statics: TemplateStringsArray, ...params: P ): R[] { 213 | 214 | const sql = statics.join ( '?' ); 215 | 216 | return this.prepare( sql )( params ); 217 | 218 | } 219 | 220 | transaction ( fn: Callback ): void { 221 | 222 | this.db.transaction ( fn )(); 223 | 224 | } 225 | 226 | vacuum (): void { 227 | 228 | this.execute ( 'VACUUM' ); 229 | 230 | } 231 | 232 | } 233 | 234 | /* EXPORT */ 235 | 236 | export default Database; 237 | export type {DatabaseOptions, FunctionOptions}; 238 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | /* IMPORT */ 3 | 4 | import {describe} from 'fava'; 5 | import {Buffer} from 'node:buffer'; 6 | import fs from 'node:fs'; 7 | import os from 'node:os'; 8 | import {setTimeout as delay} from 'node:timers/promises'; 9 | import Database from '../dist/index.js'; 10 | 11 | /* MAIN */ 12 | 13 | describe ( 'tiny-sqlite3', () => { 14 | 15 | describe ( 'constructor', it => { 16 | 17 | it ( 'can create an in-disk database', t => { 18 | 19 | const db = new Database ( 'test.db' ); 20 | 21 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 22 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 23 | 24 | const rows = db.query ( `SELECT * FROM example LIMIT 1` ); 25 | 26 | t.deepEqual ( rows, [{ id: 1, title: 'title1', description: 'description1' }] ); 27 | 28 | t.true ( fs.existsSync ( 'test.db' ) ); 29 | t.true ( fs.existsSync ( db.path ) ); 30 | 31 | t.false ( db.memory ); 32 | t.false ( db.readonly ); 33 | t.false ( db.temporary ); 34 | 35 | db.close (); 36 | 37 | t.true ( fs.existsSync ( 'test.db' ) ); 38 | t.true ( fs.existsSync ( db.path ) ); 39 | 40 | fs.rmSync ( 'test.db' ); 41 | 42 | }); 43 | 44 | it ( 'can create an in-memory database', t => { 45 | 46 | const db = new Database ( ':memory:' ); 47 | 48 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 49 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 50 | 51 | const rows = db.query ( `SELECT * FROM example LIMIT 1` ); 52 | 53 | t.deepEqual ( rows, [{ id: 1, title: 'title1', description: 'description1' }] ); 54 | 55 | t.is ( db.path, ':memory:' ); 56 | 57 | t.true ( db.memory ); 58 | t.false ( db.readonly ); 59 | t.false ( db.temporary ); 60 | 61 | db.close (); 62 | 63 | }); 64 | 65 | it ( 'can create an in-temporary database', t => { 66 | 67 | const db = new Database ( '' ); 68 | 69 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 70 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 71 | 72 | const rows = db.query ( `SELECT * FROM example LIMIT 1` ); 73 | 74 | t.deepEqual ( rows, [{ id: 1, title: 'title1', description: 'description1' }] ); 75 | 76 | t.true ( fs.existsSync ( db.path ) ); 77 | t.true ( db.path.startsWith ( os.tmpdir () ) ); 78 | 79 | t.false ( db.memory ); 80 | t.false ( db.readonly ); 81 | t.true ( db.temporary ); 82 | 83 | db.close (); 84 | 85 | t.false ( fs.existsSync ( db.path ) ); 86 | 87 | }); 88 | 89 | it ( 'can create multiple in-disk databases that share the same underlying files as another', t => { 90 | 91 | const db1 = new Database ( '' ); 92 | const db2 = new Database ( db1.path ); 93 | 94 | db1.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 95 | db1.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 96 | 97 | const rows1 = db1.query ( `SELECT * FROM example LIMIT 1` ); 98 | const rows2 = db2.query ( `SELECT * FROM example LIMIT 1` ); 99 | 100 | t.is ( db1.path, db2.path ); 101 | 102 | t.deepEqual ( rows1, [{ id: 1, title: 'title1', description: 'description1' }] ); 103 | t.deepEqual ( rows2, [{ id: 1, title: 'title1', description: 'description1' }] ); 104 | 105 | db1.close (); 106 | db2.close (); 107 | 108 | }); 109 | 110 | it ( 'can create multiple different in-memory databases', t => { 111 | 112 | const db1 = new Database ( ':memory:' ); 113 | const db2 = new Database ( ':memory:' ); 114 | 115 | db1.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 116 | db1.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 117 | 118 | db2.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 119 | db2.query ( `INSERT INTO example VALUES( 2, 'title2', 'description2' )` ); 120 | 121 | const rows1 = db1.query ( `SELECT * FROM example LIMIT 1` ); 122 | const rows2 = db2.query ( `SELECT * FROM example LIMIT 1` ); 123 | 124 | t.deepEqual ( rows1, [{ id: 1, title: 'title1', description: 'description1' }] ); 125 | t.deepEqual ( rows2, [{ id: 2, title: 'title2', description: 'description2' }] ); 126 | 127 | db1.close (); 128 | db2.close (); 129 | 130 | }); 131 | 132 | it ( 'can create an in-temporary database from a serialized one', t => { 133 | 134 | const db = new Database ( ':memory:' ); 135 | 136 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 137 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 138 | 139 | const serialized = db.serialize (); 140 | const deserialized = new Database ( serialized ); 141 | 142 | const rows1 = db.query ( `SELECT * FROM example LIMIT 1` ); 143 | const rows2 = deserialized.query ( `SELECT * FROM example LIMIT 1` ); 144 | 145 | t.deepEqual ( rows1, [{ id: 1, title: 'title1', description: 'description1' }] ); 146 | t.deepEqual ( rows2, [{ id: 1, title: 'title1', description: 'description1' }] ); 147 | 148 | t.false ( deserialized.memory ); 149 | t.false ( deserialized.readonly ); 150 | t.true ( deserialized.temporary ); 151 | 152 | db.close (); 153 | deserialized.close (); 154 | 155 | }); 156 | 157 | it ( 'can create a readonly database', t => { 158 | 159 | const db = new Database ( '' ); 160 | const rodb = new Database ( db.path, { readonly: true } ); 161 | 162 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 163 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 164 | 165 | const rows = rodb.query ( `SELECT * FROM example LIMIT 1` ); 166 | 167 | t.deepEqual ( rows, [{ id: 1, title: 'title1', description: 'description1' }] ); 168 | 169 | t.false ( db.readonly ); 170 | t.true ( rodb.readonly ); 171 | 172 | t.throws ( () => { 173 | return rodb.query ( `INSERT INTO example VALUES( 2, 'title2', 'description2' )` ); 174 | }, { message: 'attempt to write a readonly database' } ); 175 | 176 | db.close (); 177 | rodb.close (); 178 | 179 | }); 180 | 181 | it ( 'can use the "wal" journal mode', t => { 182 | 183 | const db = new Database ( '', { wal: true } ); 184 | 185 | const rows = db.query ( `PRAGMA journal_mode` ); 186 | 187 | t.deepEqual ( rows, [{ journal_mode: 'wal' }] ); 188 | 189 | db.close (); 190 | 191 | }); 192 | 193 | it ( 'defaults to the "delete" journal mode', t => { 194 | 195 | const db = new Database ( '' ); 196 | 197 | const rows = db.query ( `PRAGMA journal_mode` ); 198 | 199 | t.deepEqual ( rows, [{ journal_mode: 'delete' }] ); 200 | 201 | db.close (); 202 | 203 | }); 204 | 205 | it ( 'can set the size of a page', t => { 206 | 207 | const db1 = new Database ( ':memory:' ); 208 | 209 | const count1 = db1.query ( `PRAGMA page_size` ); 210 | 211 | t.deepEqual ( count1, [{ page_size: 16384 }] ); 212 | 213 | const db2 = new Database ( ':memory:', { page: 8192 } ); 214 | 215 | const count2 = db2.query ( `PRAGMA page_size` ); 216 | 217 | t.deepEqual ( count2, [{ page_size: 8192 }] ); 218 | 219 | db1.close (); 220 | db2.close (); 221 | 222 | }); 223 | 224 | it ( 'can set the max size of a database', t => { 225 | 226 | const db1 = new Database ( ':memory:' ); 227 | 228 | const count1 = db1.query ( `PRAGMA max_page_count` ); 229 | 230 | t.deepEqual ( count1, [{ max_page_count: 1073741823 }] ); 231 | 232 | const db2 = new Database ( ':memory:', { size: 16384000 } ); 233 | 234 | const count2 = db2.query ( `PRAGMA max_page_count` ); 235 | 236 | t.deepEqual ( count2, [{ max_page_count: 1000 }] ); 237 | 238 | db1.close (); 239 | db2.close (); 240 | 241 | }); 242 | 243 | }); 244 | 245 | describe ( 'getters', it => { 246 | 247 | it ( 'can retrieve some metadata about the database', t => { 248 | 249 | const db = new Database ( ':memory:' ); 250 | 251 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 252 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 253 | db.query ( `INSERT INTO example VALUES( 2, 'title2', 'description2' )` ); 254 | db.query ( `INSERT INTO example VALUES( 3, 'title3', 'description3' )` ); 255 | 256 | t.is ( db.changes, 1 ); 257 | t.is ( db.lastInsertRowId, 3 ); 258 | t.is ( db.totalChanges, 3 ); 259 | t.is ( db.transacting, false ); 260 | 261 | db.close (); 262 | 263 | }); 264 | 265 | it ( 'can retrieve the size of the database', t => { 266 | 267 | const db = new Database ( ':memory:' ); 268 | 269 | const size1 = db.size; 270 | 271 | t.is ( size1, 0 ); 272 | 273 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 274 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 275 | 276 | const size2 = db.size; 277 | 278 | t.is ( size2, 32768 ); 279 | 280 | db.close (); 281 | 282 | }); 283 | 284 | }); 285 | 286 | describe ( 'backup', it => { 287 | 288 | it ( 'can backup a database', async t => { 289 | 290 | const db = new Database ( '' ); 291 | 292 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 293 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 294 | 295 | await db.backup ( 'backup.db' ); 296 | 297 | const db2 = new Database ( 'backup.db' ); 298 | 299 | const rows1 = db.query ( `SELECT * FROM example LIMIT 1` ); 300 | const rows2 = db2.query ( `SELECT * FROM example LIMIT 1` ); 301 | 302 | t.deepEqual ( rows1, [{ id: 1, title: 'title1', description: 'description1' }] ); 303 | t.deepEqual ( rows2, [{ id: 1, title: 'title1', description: 'description1' }] ); 304 | 305 | db.close (); 306 | db2.close (); 307 | 308 | fs.unlinkSync ( 'backup.db' ); 309 | 310 | }); 311 | 312 | }); 313 | 314 | describe ( 'close', it => { 315 | 316 | it ( 'can be closed multiple times without throwing', t => { 317 | 318 | const db = new Database ( ':memory:' ); 319 | 320 | db.close (); 321 | db.close (); 322 | db.close (); 323 | 324 | t.pass (); 325 | 326 | }); 327 | 328 | it ( 'unlinks the in-temporary database after close', t => { 329 | 330 | const db = new Database ( '' ); 331 | 332 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 333 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 334 | 335 | t.true ( fs.existsSync ( db.path ) ); 336 | 337 | db.close (); 338 | 339 | t.false ( fs.existsSync ( db.path ) ); 340 | 341 | }); 342 | 343 | it ( 'can re-open the connection automatically after closing', t => { 344 | 345 | const db = new Database ( ':memory:' ); 346 | 347 | const result1 = db.query ( `SELECT 1 AS value` ); 348 | 349 | t.deepEqual ( result1, [{ value: 1 }] ); 350 | 351 | db.close (); 352 | 353 | const result2 = db.query ( `SELECT 2 AS value` ); 354 | 355 | t.deepEqual ( result2, [{ value: 2 }] ); 356 | 357 | db.close (); 358 | 359 | }); 360 | 361 | }); 362 | 363 | describe ( 'execute', it => { 364 | 365 | it ( 'can execute queries ignoring the result', t => { 366 | 367 | const db = new Database ( ':memory:' ); 368 | 369 | db.execute (` 370 | CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT ); 371 | INSERT INTO example VALUES( 1, 'title1', 'description1' ); 372 | INSERT INTO example VALUES( 2, 'title2', 'description2' ); 373 | `); 374 | 375 | const rowsExecute = db.execute ( `SELECT * FROM example` ); 376 | const rowsQuery = db.query ( `SELECT * FROM example` ); 377 | 378 | t.is ( rowsExecute, undefined ); 379 | t.deepEqual ( rowsQuery, [{ id: 1, title: 'title1', description: 'description1' }, { id: 2, title: 'title2', description: 'description2' }] ); 380 | 381 | db.close (); 382 | 383 | }); 384 | 385 | }); 386 | 387 | describe ( 'function', it => { 388 | 389 | it ( 'can register and unregister a function', t => { 390 | 391 | const db = new Database ( ':memory:' ); 392 | 393 | t.throws ( () => { 394 | db.query ( `SELECT custom_sum(1, 2) as a, sum(1,2,3,4) as b` ); 395 | }, { message: 'no such function: custom_sum' }); 396 | 397 | const sum = ( ...numbers ) => numbers.reduce ( ( sum, number ) => sum + number, 0 ); 398 | const dispose = db.function ( 'custom_sum', sum, { variadic: true } ); 399 | const summed = db.query ( `SELECT custom_sum(1, 2) as a, custom_sum(1,2,3,4) as b` ); 400 | 401 | t.deepEqual ( summed, [{ a: 3, b: 10 }] ); 402 | 403 | dispose (); 404 | 405 | t.throws ( () => { 406 | db.query ( `SELECT custom_sum(1, 2) as a, custom_sum(1,2,3,4) as b` ); 407 | }, { message: 'no such function: custom_sum' }); 408 | 409 | db.close (); 410 | 411 | }); 412 | 413 | }); 414 | 415 | describe ( 'prepare', it => { 416 | 417 | it ( 'can prepare a query taking an array of values to execute later', t => { 418 | 419 | const db = new Database ( ':memory:' ); 420 | 421 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 422 | 423 | const insert1 = db.prepare ( `INSERT INTO example VALUES( ?, ?, ? )` ); 424 | const insert2 = db.prepare ( `INSERT INTO example VALUES( :id, :title, :description )` ); 425 | 426 | insert1 ([ 1, 'title1', 'description1' ]); 427 | insert1 ([ 2, 'title2', 'description2' ]); 428 | 429 | insert2 ({ id: 3, title: 'title3', description: 'description3' }); 430 | insert2 ({ id: 4, title: 'title4', description: 'description4' }); 431 | 432 | const rows = db.query ( `SELECT * FROM example` ); 433 | 434 | t.deepEqual ( rows[0], { id: 1, title: 'title1', description: 'description1' } ); 435 | t.deepEqual ( rows[1], { id: 2, title: 'title2', description: 'description2' } ); 436 | t.deepEqual ( rows[2], { id: 3, title: 'title3', description: 'description3' } ); 437 | t.deepEqual ( rows[3], { id: 4, title: 'title4', description: 'description4' } ); 438 | 439 | db.close (); 440 | 441 | }); 442 | 443 | }); 444 | 445 | describe ( 'query', it => { 446 | 447 | it ( 'can interpolate an array of values', t => { 448 | 449 | const db = new Database ( ':memory:' ); 450 | 451 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 452 | db.query ( `INSERT INTO example VALUES( ?, ?, ? )`, [1, 'title1', 'description1'] ); 453 | 454 | const rows = db.query ( `SELECT * FROM example LIMIT 1` ); 455 | 456 | t.deepEqual ( rows, [{ id: 1, title: 'title1', description: 'description1' }] ); 457 | 458 | db.close (); 459 | 460 | }); 461 | 462 | it ( 'can interpolate an object of values', t => { 463 | 464 | const db = new Database ( ':memory:' ); 465 | 466 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 467 | db.query ( `INSERT INTO example VALUES( :id, :title, :description )`, { id: 1, title: 'title1', description: 'description1' } ); 468 | db.query ( `INSERT INTO example VALUES( @id, @title, @description )`, { id: 2, title: 'title2', description: 'description2' } ); 469 | db.query ( `INSERT INTO example VALUES( $id, $title, $description )`, { id: 3, title: 'title3', description: 'description3' } ); 470 | 471 | const rows = db.query ( `SELECT * FROM example` ); 472 | 473 | t.deepEqual ( rows[0], { id: 1, title: 'title1', description: 'description1' } ); 474 | t.deepEqual ( rows[1], { id: 2, title: 'title2', description: 'description2' } ); 475 | t.deepEqual ( rows[2], { id: 3, title: 'title3', description: 'description3' } ); 476 | 477 | db.close (); 478 | 479 | }); 480 | 481 | }); 482 | 483 | describe ( 'sql', it => { 484 | 485 | it.skip ( 'can interpolate a bigint', t => { 486 | 487 | const db = new Database ( ':memory:' ); 488 | 489 | const rowsSmall = db.sql`SELECT ${123n} AS value`; 490 | 491 | t.deepEqual ( rowsSmall, [{ value: 123 }] ); 492 | 493 | const rowsBig = db.sql`SELECT ${1152735103331642317n} AS value`; 494 | 495 | t.deepEqual ( rowsBig, [{ value: 1152735103331642317n }] ); 496 | 497 | db.close (); 498 | 499 | }); 500 | 501 | it ( 'can interpolate a null', t => { 502 | 503 | const db = new Database ( ':memory:' ); 504 | 505 | const rows = db.sql`SELECT ${null} AS value`; 506 | 507 | t.deepEqual ( rows, [{ value: null }] ); 508 | 509 | db.close (); 510 | 511 | }); 512 | 513 | it ( 'can interpolate an undefined', t => { 514 | 515 | const db = new Database ( ':memory:' ); 516 | 517 | const rows = db.sql`SELECT ${undefined} AS value`; 518 | 519 | t.deepEqual ( rows, [{ value: null }] ); 520 | 521 | db.close (); 522 | 523 | }); 524 | 525 | it ( 'can interpolate an Infinity', t => { 526 | 527 | const db = new Database ( ':memory:' ); 528 | 529 | const rowsPositive = db.sql`SELECT ${Infinity} AS value`; 530 | 531 | t.deepEqual ( rowsPositive, [{ value: Infinity }] ); 532 | 533 | const rowsNegative = db.sql`SELECT ${-Infinity} AS value`; 534 | 535 | t.deepEqual ( rowsNegative, [{ value: -Infinity }] ); 536 | 537 | db.close (); 538 | 539 | }); 540 | 541 | it ( 'can interpolate a NaN', t => { 542 | 543 | const db = new Database ( ':memory:' ); 544 | 545 | const rows = db.sql`SELECT ${NaN} AS value`; 546 | 547 | t.deepEqual ( rows, [{ value: null }] ); 548 | 549 | db.close (); 550 | 551 | }); 552 | 553 | it ( 'can interpolate a number', t => { 554 | 555 | const db = new Database ( ':memory:' ); 556 | 557 | const rows = db.sql`SELECT ${123} AS value`; 558 | 559 | t.deepEqual ( rows, [{ value: 123 }] ); 560 | 561 | db.close (); 562 | 563 | }); 564 | 565 | it ( 'can interpolate a string', t => { 566 | 567 | const db = new Database ( ':memory:' ); 568 | 569 | const rows = db.sql`SELECT ${'foo'} AS value`; 570 | 571 | t.deepEqual ( rows, [{ value: 'foo' }] ); 572 | 573 | db.close (); 574 | 575 | }); 576 | 577 | it ( 'can interpolate a string with automatic escaping', t => { 578 | 579 | const db = new Database ( ':memory:' ); 580 | 581 | const rows = db.sql`SELECT ${"f''o'o"} AS value`; 582 | 583 | t.deepEqual ( rows, [{ value: "f''o'o" }] ); 584 | 585 | db.close (); 586 | 587 | }); 588 | 589 | it.skip ( 'can interpolate an ArrayBuffer', t => { 590 | 591 | const db = new Database ( ':memory:' ); 592 | 593 | const data = new Uint8Array ([ 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33 ]); 594 | 595 | const rows = db.sql`SELECT ${data.buffer} AS value`; 596 | 597 | t.deepEqual ( rows, [{ value: data.buffer }] ); 598 | 599 | db.close (); 600 | 601 | }); 602 | 603 | it ( 'can interpolate a Uint8Array', t => { 604 | 605 | const db = new Database ( ':memory:' ); 606 | 607 | const data = new Uint8Array ([ 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33 ]); 608 | 609 | const rows = db.sql`SELECT ${data} AS value`; 610 | 611 | t.deepEqual ( rows, [{ value: Buffer.from ( data ) }] ); //TODO: Avoid using Buffers 612 | 613 | db.close (); 614 | 615 | }); 616 | 617 | it ( 'throws when interpolating a boolean', t => { 618 | 619 | const db = new Database ( ':memory:' ); 620 | 621 | t.throws ( () => { 622 | db.sql`SELECT ${true} as value`; 623 | }); 624 | 625 | t.throws ( () => { 626 | db.sql`SELECT ${false} as value`; 627 | }); 628 | 629 | db.close (); 630 | 631 | }); 632 | 633 | it ( 'throws when interpolating a date', t => { 634 | 635 | const db = new Database ( ':memory:' ); 636 | 637 | t.throws ( () => { 638 | db.sql`SELECT ${true} as value`; 639 | }); 640 | 641 | db.close (); 642 | 643 | }); 644 | 645 | it ( 'throws when interpolating a plain object', t => { 646 | 647 | const db = new Database ( ':memory:' ); 648 | 649 | t.throws ( () => { 650 | db.sql`SELECT ${{}} as value`; 651 | }); 652 | 653 | db.close (); 654 | 655 | }); 656 | 657 | it ( 'throws when interpolating a symbol', t => { 658 | 659 | const db = new Database ( ':memory:' ); 660 | 661 | t.throws ( () => { 662 | db.sql`SELECT ${Symbol ()} as value`; 663 | }); 664 | 665 | db.close (); 666 | 667 | }); 668 | 669 | it ( 'throws when interpolating an array', t => { 670 | 671 | const db = new Database ( ':memory:' ); 672 | 673 | t.throws ( () => { 674 | db.sql`SELECT ${[]} as value`; 675 | }); 676 | 677 | db.close (); 678 | 679 | }); 680 | 681 | }); 682 | 683 | describe ( 'transaction', it => { 684 | 685 | it ( 'supports empty transactions', t => { 686 | 687 | const db = new Database ( ':memory:' ); 688 | 689 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 690 | 691 | t.false ( db.transacting ); 692 | 693 | db.transaction ( () => { 694 | t.true ( db.transacting ); 695 | }); 696 | 697 | t.false ( db.transacting ); 698 | 699 | const rows = db.query ( `SELECT * FROM example` ); 700 | 701 | t.deepEqual ( rows, [] ); 702 | 703 | db.close (); 704 | 705 | }); 706 | 707 | it ( 'supports successful transactions', t => { 708 | 709 | const db = new Database ( ':memory:' ); 710 | 711 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 712 | 713 | t.false ( db.transacting ); 714 | 715 | db.transaction ( () => { 716 | t.true ( db.transacting ); 717 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 718 | db.query ( `INSERT INTO example VALUES( 2, 'title2', 'description2' )` ); 719 | db.query ( `INSERT INTO example VALUES( 3, 'title3', 'description3' )` ); 720 | t.true ( db.transacting ); 721 | }); 722 | 723 | t.false ( db.transacting ); 724 | 725 | const rows = db.query ( `SELECT * FROM example` ); 726 | 727 | const expected = [ 728 | { id: 1, title: 'title1', description: 'description1' }, 729 | { id: 2, title: 'title2', description: 'description2' }, 730 | { id: 3, title: 'title3', description: 'description3' } 731 | ]; 732 | 733 | t.deepEqual ( rows, expected ); 734 | 735 | db.close (); 736 | 737 | }); 738 | 739 | it ( 'supports failing transactions', t => { 740 | 741 | const db = new Database ( ':memory:' ); 742 | 743 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 744 | 745 | t.false ( db.transacting ); 746 | 747 | t.throws ( () => { 748 | db.transaction ( () => { 749 | t.true ( db.transacting ); 750 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 751 | db.query ( `INSERT INTO example VALUES( 2, 'title2', 'description2' )` ); 752 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 753 | t.true ( db.transacting ); 754 | }); 755 | }); 756 | 757 | t.false ( db.transacting ); 758 | 759 | const rows = db.query ( `SELECT * FROM example` ); 760 | 761 | t.deepEqual ( rows, [] ); 762 | 763 | db.close (); 764 | 765 | }); 766 | 767 | it ( 'supports empty nested transactions', t => { 768 | 769 | const db = new Database ( ':memory:' ); 770 | 771 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 772 | 773 | t.false ( db.transacting ); 774 | 775 | db.transaction ( () => { 776 | t.true ( db.transacting ); 777 | db.transaction ( () => { 778 | t.true ( db.transacting ); 779 | }); 780 | t.true ( db.transacting ); 781 | }); 782 | 783 | t.false ( db.transacting ); 784 | 785 | const rows = db.query ( `SELECT * FROM example` ); 786 | 787 | t.deepEqual ( rows, [] ); 788 | 789 | db.close (); 790 | 791 | }); 792 | 793 | it ( 'supports successful nested transactions', t => { 794 | 795 | const db = new Database ( ':memory:' ); 796 | 797 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 798 | 799 | t.false ( db.transacting ); 800 | 801 | db.transaction ( () => { 802 | t.true ( db.transacting ); 803 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 804 | db.transaction ( () => { 805 | t.true ( db.transacting ); 806 | db.query ( `INSERT INTO example VALUES( 2, 'title2', 'description2' )` ); 807 | t.true ( db.transacting ); 808 | }); 809 | db.query ( `INSERT INTO example VALUES( 3, 'title3', 'description3' )` ); 810 | t.true ( db.transacting ); 811 | }); 812 | 813 | t.false ( db.transacting ); 814 | 815 | const rows = db.query ( `SELECT * FROM example` ); 816 | 817 | const expected = [ 818 | { id: 1, title: 'title1', description: 'description1' }, 819 | { id: 2, title: 'title2', description: 'description2' }, 820 | { id: 3, title: 'title3', description: 'description3' } 821 | ]; 822 | 823 | t.deepEqual ( rows, expected ); 824 | 825 | db.close (); 826 | 827 | }); 828 | 829 | it ( 'supports failing nested transactions', t => { 830 | 831 | const db = new Database ( ':memory:' ); 832 | 833 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 834 | 835 | t.false ( db.transacting ); 836 | 837 | t.throws ( () => { 838 | db.transaction ( () => { 839 | t.true ( db.transacting ); 840 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 841 | db.transaction ( () => { 842 | t.true ( db.transacting ); 843 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 844 | t.true ( db.transacting ); 845 | }); 846 | db.query ( `INSERT INTO example VALUES( 3, 'title3', 'description3' )` ); 847 | t.true ( db.transacting ); 848 | }); 849 | }); 850 | 851 | t.false ( db.transacting ); 852 | 853 | const rows = db.query ( `SELECT * FROM example` ); 854 | 855 | t.deepEqual ( rows, [] ); 856 | 857 | db.close (); 858 | 859 | }); 860 | 861 | }); 862 | 863 | describe ( 'vacuum', it => { 864 | 865 | it ( 'can shrink a database by vacuuming freed pages', t => { 866 | 867 | const db = new Database ( ':memory:' ); 868 | 869 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 870 | db.query ( `INSERT INTO example VALUES( 1, 'title1', '${'description1'.repeat ( 10_000 )}' )` ); 871 | 872 | const size1 = db.size; 873 | 874 | db.query ( `DELETE FROM example WHERE id=${1}` ); 875 | 876 | const size2 = db.size; 877 | 878 | db.vacuum (); 879 | 880 | const size3 = db.size; 881 | 882 | db.vacuum (); 883 | 884 | const size4 = db.size; 885 | 886 | t.is ( size1, size2 ); 887 | t.true ( size3 < size2 ); 888 | t.is ( size3, size4 ); 889 | 890 | db.close (); 891 | 892 | }); 893 | 894 | }); 895 | 896 | describe ( 'todo', it => { 897 | 898 | it.skip ( 'supports closing the process automatically after a ttl', t => { 899 | 900 | const db = new Database ( ':memory:', { ttl: 250 } ); 901 | 902 | const pid1 = db.pid (); 903 | 904 | db.query ( `SELECT 1 AS value` ); 905 | 906 | const pid2 = db.pid (); 907 | 908 | delay ( 200 ); 909 | 910 | db.query ( `SELECT 1 AS value` ); 911 | 912 | const pid3 = db.pid (); 913 | 914 | delay ( 200 ); 915 | 916 | const pid4 = db.pid (); 917 | 918 | delay ( 200 ); 919 | 920 | const pid5 = db.pid (); 921 | 922 | t.is ( pid1, undefined ); 923 | t.true ( pid2 > 0 ); 924 | t.is ( pid2, pid3 ); 925 | t.is ( pid3, pid4 ); 926 | t.is ( pid5, undefined ); 927 | 928 | db.close (); 929 | 930 | }); 931 | 932 | it.skip ( 'can dump the contents of a database', t => { 933 | 934 | const db = new Database ( ':memory:' ); 935 | 936 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 937 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 938 | 939 | const sql = db.dump (); 940 | 941 | const expected = ( 942 | `PRAGMA foreign_keys=OFF;\n` + 943 | `BEGIN TRANSACTION;\n` + 944 | `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT );\n` + 945 | `INSERT INTO example VALUES(1,'title1','description1');\n` + 946 | `COMMIT;\n` 947 | ); 948 | 949 | t.is ( sql, expected ); 950 | 951 | db.close (); 952 | 953 | }); 954 | 955 | it.skip ( 'can recover the contents of a database', t => { 956 | 957 | const db = new Database ( ':memory:' ); 958 | 959 | db.query ( `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT )` ); 960 | db.query ( `INSERT INTO example VALUES( 1, 'title1', 'description1' )` ); 961 | 962 | const sql = db.recover (); 963 | 964 | const expected = ( 965 | `BEGIN;\n` + 966 | `PRAGMA writable_schema = on;\n` + 967 | `PRAGMA encoding = 'UTF-8';\n` + 968 | `PRAGMA page_size = '4096';\n` + 969 | `PRAGMA auto_vacuum = '0';\n` + 970 | `PRAGMA user_version = '0';\n` + 971 | `PRAGMA application_id = '0';\n` + 972 | `CREATE TABLE example ( id INTEGER PRIMARY KEY, title TEXT, description TEXT );\n` + 973 | `INSERT OR IGNORE INTO 'example'('id', 'title', 'description') VALUES (1, 'title1', 'description1');\n` + 974 | `PRAGMA writable_schema = off;\n` + 975 | `COMMIT;\n` 976 | ); 977 | 978 | t.is ( sql, expected ); 979 | 980 | db.close (); 981 | 982 | }); 983 | 984 | }); 985 | 986 | }); 987 | --------------------------------------------------------------------------------