├── cjs ├── package.json ├── utils.js └── index.js ├── .npmrc ├── .gitignore ├── .npmignore ├── test ├── concurrent ├── cluster.js ├── bench-sqlite-tag-spawned.js ├── persistent.js ├── concurrent.cjs ├── bench-better-sqlite.js ├── benchmark.js └── index.js ├── LICENSE ├── .github └── workflows │ └── node.js.yml ├── package.json ├── esm ├── utils.js └── index.js └── README.md /cjs/package.json: -------------------------------------------------------------------------------- 1 | {"type":"commonjs"} -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | package-lock=true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | coverage/ 4 | node_modules/ 5 | test/sqlite.* 6 | test/counter.* 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .nyc_output 3 | .eslintrc.json 4 | .travis.yml 5 | coverage/ 6 | node_modules/ 7 | rollup/ 8 | test/ 9 | -------------------------------------------------------------------------------- /test/concurrent: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | for i in {0..999}; do 4 | curl -s http://localhost:8080/ & 5 | done 6 | 7 | sleep 5 8 | 9 | exit 0 10 | -------------------------------------------------------------------------------- /test/cluster.js: -------------------------------------------------------------------------------- 1 | import cluster from 'cluster'; 2 | import {cpus} from 'os'; 3 | import process from 'process'; 4 | 5 | if (cluster.isPrimary) { 6 | for (let {length} = cpus(), i = 0; i < length; i++) 7 | cluster.fork(); 8 | } 9 | else { 10 | globalThis.DO_NOT_DELETE_FROM = true; 11 | await import('./index.js'); 12 | setTimeout(() => process.exit(0), 1000); 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2022, Andrea Giammarchi, @WebReflection 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 11 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 14 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /test/bench-sqlite-tag-spawned.js: -------------------------------------------------------------------------------- 1 | import { bench, run } from "mitata"; 2 | import SQLiteTagSpawned from '../esm/index.js'; 3 | 4 | // DB https://github.com/jpwhite3/northwind-SQLite3/blob/master/Northwind_large.sqlite.zip 5 | const db = SQLiteTagSpawned('/tmp/northwind.sqlite', { 6 | persistent: true 7 | }); 8 | 9 | /* 10 | console.time('bootstrap'); 11 | await Promise.all([ 12 | db.all`SELECT * FROM "OrderDetail"`, 13 | db.all`SELECT * FROM "OrderDetail"` 14 | ]).then(() => console.timeEnd('bootstrap')); 15 | */ 16 | 17 | bench('SELECT * FROM "Order" (objects)', async () => { 18 | await db.all`SELECT * FROM "Order"`; 19 | }); 20 | 21 | bench('SELECT * FROM "Product" (objects)', async () => { 22 | await db.all`SELECT * FROM "Product"`; 23 | }); 24 | 25 | bench('SELECT * FROM "OrderDetail" (objects)', async () => { 26 | await db.all`SELECT * FROM "OrderDetail" LIMIT 10000`; 27 | }); 28 | 29 | run({ gc: true }); 30 | -------------------------------------------------------------------------------- /test/persistent.js: -------------------------------------------------------------------------------- 1 | import SQLiteTag from '../esm/index.js'; 2 | 3 | let {all, get, query, close} = SQLiteTag('./test/sqlite.db', {persistent: true, timeout: 1000}); 4 | 5 | console.assert(void 0 === await get`SELECT * FROM lorem LIMIT ${0}`); 6 | 7 | console.time('persistent get'); 8 | const row = await get`SELECT * FROM lorem`; 9 | console.timeEnd('persistent get'); 10 | 11 | console.time('persistent all'); 12 | const rows = await all`SELECT * FROM lorem`; 13 | console.timeEnd('persistent all'); 14 | 15 | console.assert('[{"1":1}]' === (await query`SELECT 1`).trim(), 'should be the same'); 16 | 17 | console.assert(JSON.stringify(rows[0]) === JSON.stringify(row)); 18 | close(); 19 | 20 | await new Promise($ => setTimeout($)); 21 | 22 | ({all, get, query, close} = SQLiteTag('./test/sqlite.db', {persistent: true})); 23 | 24 | try { 25 | await query`SHENANIGANS`; 26 | console.assert(!'nope'); 27 | } 28 | catch ({message}) { 29 | console.assert(!!message); 30 | } 31 | 32 | for (let i = 0; i < 10; i++) 33 | query`INSERT INTO lorem VALUES (${'Ipsum ' + Math.random()})`; 34 | 35 | console.log(await get`SELECT COUNT(*) AS total FROM lorem`); 36 | 37 | close(); 38 | -------------------------------------------------------------------------------- /test/concurrent.cjs: -------------------------------------------------------------------------------- 1 | const SQLiteTag = require('../cjs/index.js'); 2 | 3 | const {get, query, close} = SQLiteTag( 4 | __dirname + '/counter.db', 5 | {persistent: true, timeout: 1000} 6 | ); 7 | 8 | (async () => { 9 | await query`CREATE TABLE IF NOT EXISTS counter (total INTEGER)`; 10 | if ((await get`SELECT total FROM counter`) === void 0) { 11 | await query`PRAGMA journal_mode=WAL`; 12 | await query`INSERT INTO counter VALUES (0)`; 13 | } 14 | })(); 15 | 16 | require('http').createServer( 17 | async (_, res) => { 18 | await query`UPDATE counter SET total = total + 1`; 19 | res.writeHead(200, {'content-type': 'application/json'}); 20 | res.end(JSON.stringify(await get`SELECT total FROM counter`) + '\n'); 21 | } 22 | ).listen(8080); 23 | 24 | console.log('http://localhost:8080/'); 25 | 26 | process 27 | .on('exit', () => { 28 | close(); 29 | console.log('bye bye'); 30 | }) 31 | .on("SIGTERM", () => { 32 | close(); 33 | process.exit(0); 34 | }) 35 | .on("SIGINT", () => { 36 | close(); 37 | process.exit(0); 38 | }) 39 | .on("uncaughtException", () => { 40 | close(); 41 | process.exit(1); 42 | }) 43 | ; 44 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16] 16 | 17 | steps: 18 | - name: Build modern SQLite3 19 | run: wget https://sqlite.org/2022/sqlite-autoconf-3370200.tar.gz && tar -xvf sqlite-autoconf-3370200.tar.gz && cd sqlite-autoconf-3370200 && ./configure && make && sudo make install && export PATH="/usr/local/lib:$PATH" && sqlite3 --version 20 | - uses: actions/checkout@v2 21 | - name: Use Node.js ${{ matrix.node-version }} 22 | uses: actions/setup-node@v2 23 | with: 24 | node-version: ${{ matrix.node-version }} 25 | cache: 'npm' 26 | - run: npm ci 27 | - run: npm run build --if-present 28 | - run: npm test 29 | - run: npm run coverage --if-present 30 | - name: Coveralls 31 | uses: coverallsapp/github-action@master 32 | with: 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /test/bench-better-sqlite.js: -------------------------------------------------------------------------------- 1 | import { bench, run } from "mitata"; 2 | import { createRequire } from "module"; 3 | 4 | // DB https://github.com/jpwhite3/northwind-SQLite3/blob/master/Northwind_large.sqlite.zip 5 | const db = createRequire(import.meta.url)("better-sqlite3")( 6 | "/tmp/northwind.sqlite" 7 | ); 8 | 9 | { 10 | // const sql = db.prepare(`SELECT * FROM "Order"`); 11 | 12 | bench('SELECT * FROM "Order" (objects)', async () => { 13 | await db.prepare(`SELECT * FROM "Order"`).all(); 14 | }); 15 | 16 | // bench('SELECT * FROM "Order" (nothing)', () => { 17 | // sql.run(); 18 | // }); 19 | } 20 | 21 | { 22 | // const sql = db.prepare(`SELECT * FROM "Product"`); 23 | 24 | bench('SELECT * FROM "Product" (objects)', async () => { 25 | await db.prepare(`SELECT * FROM "Product"`).all(); 26 | }); 27 | 28 | // bench('SELECT * FROM "Product" (nothing)', () => { 29 | // sql.run(); 30 | // }); 31 | } 32 | 33 | { 34 | // const sql = db.prepare(`SELECT * FROM "OrderDetail"`); 35 | 36 | bench('SELECT * FROM "OrderDetail" (objects)', async () => { 37 | await db.prepare(`SELECT * FROM "OrderDetail" LIMIT 10000`).all(); 38 | }); 39 | 40 | // bench('SELECT * FROM "OrderDetail" (nothing)', () => { 41 | // sql.run(); 42 | // }); 43 | } 44 | 45 | run({ gc: true }); 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sqlite-tag-spawned", 3 | "version": "0.7.1", 4 | "description": "Same as sqlite-tag but without the native sqlite3 module dependency", 5 | "main": "./cjs/index.js", 6 | "scripts": { 7 | "benchmark": "node test/benchmark.js; rm -f test/{better,bindings,spawned}.db", 8 | "build": "npm run cjs && npm run test", 9 | "cjs": "ascjs --no-default esm cjs", 10 | "test": "c8 node test/index.js", 11 | "coverage": "mkdir -p ./coverage; c8 report --reporter=text-lcov > ./coverage/lcov.info" 12 | }, 13 | "keywords": [ 14 | "sqlite", 15 | "sqlite3", 16 | "spawn", 17 | "lightweight" 18 | ], 19 | "author": "Andrea Giammarchi", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "ascjs": "^6.0.2", 23 | "better-sqlite3": "^9.0.0", 24 | "c8": "^8.0.1", 25 | "mitata": "^0.1.6", 26 | "sqlite-tag": "^1.3.2", 27 | "sqlite3": "^5.1.6" 28 | }, 29 | "module": "./esm/index.js", 30 | "type": "module", 31 | "exports": { 32 | ".": { 33 | "import": "./esm/index.js", 34 | "default": "./cjs/index.js" 35 | }, 36 | "./utils": { 37 | "import": "./esm/utils.js", 38 | "default": "./cjs/utils.js" 39 | }, 40 | "./package.json": "./package.json" 41 | }, 42 | "dependencies": { 43 | "plain-tag": "^0.1.3", 44 | "static-params": "^0.4.0" 45 | }, 46 | "repository": { 47 | "type": "git", 48 | "url": "git+https://github.com/WebReflection/sqlite-tag-spawned.git" 49 | }, 50 | "bugs": { 51 | "url": "https://github.com/WebReflection/sqlite-tag-spawned/issues" 52 | }, 53 | "homepage": "https://github.com/WebReflection/sqlite-tag-spawned#readme" 54 | } 55 | -------------------------------------------------------------------------------- /esm/utils.js: -------------------------------------------------------------------------------- 1 | import plain from 'plain-tag'; 2 | import {asStatic, asParams} from 'static-params/sql'; 3 | 4 | export const error = (rej, reason) => { 5 | const code = 'SQLITE_ERROR'; 6 | const error = new Error(code + ': ' + reason); 7 | error.code = code; 8 | rej(error); 9 | return ''; 10 | }; 11 | 12 | export const raw = (..._) => asStatic(plain(..._)); 13 | 14 | const {from} = Array; 15 | const quote = /'/g; 16 | const hex = x => x.toString(16).padStart(2, '0'); 17 | const x = typed => `x'${from(typed, hex).join('')}'`; 18 | export const asValue = value => { 19 | switch (typeof value) { 20 | case 'string': 21 | return "'" + value.replace(quote, "''") + "'"; 22 | case 'number': 23 | if (!isFinite(value)) 24 | return; 25 | case 'boolean': 26 | return +value; 27 | case 'object': 28 | case 'undefined': 29 | switch (true) { 30 | case !value: 31 | return 'NULL'; 32 | case value instanceof Date: 33 | return "'" + value.toISOString() + "'"; 34 | case value instanceof Buffer: 35 | case value instanceof ArrayBuffer: 36 | value = new Uint8Array(value); 37 | case value instanceof Uint8Array: 38 | case value instanceof Uint8ClampedArray: 39 | return x(value); 40 | } 41 | } 42 | }; 43 | 44 | export const sql = (rej, _) => { 45 | const [template, ...values] = asParams(..._); 46 | const sql = [template[0]]; 47 | for (let i = 0; i < values.length; i++) { 48 | const value = asValue(values[i]); 49 | if (value === void 0) 50 | return error(rej, 'incompatible ' + (typeof value) + 'value'); 51 | sql.push(value, template[i + 1]); 52 | } 53 | const query = sql.join('').trim(); 54 | return query.length ? query : error(rej, 'empty query'); 55 | }; 56 | 57 | export const sql2array = sql => { 58 | const re = /(([:$@](\w+))|(\$\{\s*(\w+)\s*\}))/g; 59 | const out = []; 60 | let i = 0; 61 | let match; 62 | while (match = re.exec(sql)) { 63 | out.push(sql.slice(i, match.index), match[3] || match[5]); 64 | i = match.index + match[0].length; 65 | } 66 | out.push(sql.slice(i)); 67 | return out; 68 | }; 69 | 70 | // WARNING: this changes the incoming array value @ holes 71 | // useful only when sql2array results are stored, 72 | // and revived, as JSON ... watch out side effects 73 | // if used with same array more than once! 74 | export const array2sql = (chunks, data = null) => { 75 | for (let i = 1; i < chunks.length; i += 2) { 76 | const value = asValue(data[chunks[i]]); 77 | if (value === void 0) 78 | return ''; 79 | chunks[i] = value; 80 | } 81 | return chunks.join(''); 82 | }; 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sqlite-tag-spawned 2 | 3 | [![build status](https://github.com/WebReflection/sqlite-tag-spawned/actions/workflows/node.js.yml/badge.svg)](https://github.com/WebReflection/sqlite-tag-spawned/actions) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/sqlite-tag-spawned/badge.svg?branch=main)](https://coveralls.io/github/WebReflection/sqlite-tag-spawned?branch=main) 4 | 5 | **Social Media Photo by [Tomas Kirvėla](https://unsplash.com/@tomkirvela) on [Unsplash](https://unsplash.com/)** 6 | 7 | 8 | The same [sqlite-tag](https://github.com/WebReflection/sqlite-tag#readme) ease but without the native [sqlite3](https://www.npmjs.com/package/sqlite3) dependency, aiming to replace [dblite](https://github.com/WebReflection/dblite#readme). 9 | 10 | ```js 11 | import SQLiteTagSpawned from 'sqlite-tag-spawned'; 12 | // const SQLiteTagSpawned = require('sqlite-tag-spawned'); 13 | 14 | const {query, get, all, raw, transaction} = SQLiteTagSpawned('./db.sql'); 15 | 16 | // single query as any info 17 | console.log(await query`.databases`); 18 | 19 | // single query as SQL 20 | await query`CREATE TABLE IF NOT EXISTS names ( 21 | id INTEGER PRIMARY KEY, 22 | name TEXT 23 | )`; 24 | 25 | // transaction (requires .commit() to execute) 26 | const populate = transaction(); 27 | for (let i = 0; i < 2; i++) 28 | populate`INSERT INTO names (name) VALUES (${'Name' + i})`; 29 | await populate.commit(); 30 | 31 | // get single row (works with LIMIT 1 too, of course) 32 | await get`SELECT name FROM names`; 33 | // { name: 'Name0' } 34 | 35 | // get all results, if any, or an empty array 36 | await all`SELECT * FROM names`; 37 | // [ { id: 1, name: 'Name0' }, { id: 2, name: 'Name1' } ] 38 | 39 | // use the IN clause through arrays 40 | const list = ['Name 0', 'Name 1']; 41 | await all`SELECT * FROM names WHERE name IN (${list})`; 42 | ``` 43 | 44 | 45 | ### Differently from dblite 46 | 47 | * requires **SQLite 3.33** or higher (it uses the `-json` output mode) 48 | * each query is a spawn call except for transactions, grouped as single spawned query 49 | * performance still similar to sqlite3 native module 50 | * `:memory:` database is based on an always same, yet runtime-once created temporary file, and it requires NodeJS 16+ 51 | 52 | 53 | ## API: SQLiteTagSpawned(fileName[, options]) 54 | 55 | While the `fileName` is just a string pointing at the db file or the string `:memory:`, optional options can contain the following fields: 56 | 57 | * `readonly` to run queries in read only mode 58 | * `bin` to specify a different `sqlite3` executable 59 | * `timeout` to drop the spawned process after *N* milliseconds 60 | * `persistent` to open a DB in persistent mode (kept alive spawned command) 61 | * `exec` to specify a different way to execute spawned process and results, mostly used for internal purpose 62 | -------------------------------------------------------------------------------- /cjs/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const plain = (m => /* c8 ignore start */ m.__esModule ? m.default : m /* c8 ignore stop */)(require('plain-tag')); 3 | const {asStatic, asParams} = require('static-params/sql'); 4 | 5 | const error = (rej, reason) => { 6 | const code = 'SQLITE_ERROR'; 7 | const error = new Error(code + ': ' + reason); 8 | error.code = code; 9 | rej(error); 10 | return ''; 11 | }; 12 | exports.error = error; 13 | 14 | const raw = (..._) => asStatic(plain(..._)); 15 | exports.raw = raw; 16 | 17 | const {from} = Array; 18 | const quote = /'/g; 19 | const hex = x => x.toString(16).padStart(2, '0'); 20 | const x = typed => `x'${from(typed, hex).join('')}'`; 21 | const asValue = value => { 22 | switch (typeof value) { 23 | case 'string': 24 | return "'" + value.replace(quote, "''") + "'"; 25 | case 'number': 26 | if (!isFinite(value)) 27 | return; 28 | case 'boolean': 29 | return +value; 30 | case 'object': 31 | case 'undefined': 32 | switch (true) { 33 | case !value: 34 | return 'NULL'; 35 | case value instanceof Date: 36 | return "'" + value.toISOString() + "'"; 37 | case value instanceof Buffer: 38 | case value instanceof ArrayBuffer: 39 | value = new Uint8Array(value); 40 | case value instanceof Uint8Array: 41 | case value instanceof Uint8ClampedArray: 42 | return x(value); 43 | } 44 | } 45 | }; 46 | exports.asValue = asValue; 47 | 48 | const sql = (rej, _) => { 49 | const [template, ...values] = asParams(..._); 50 | const sql = [template[0]]; 51 | for (let i = 0; i < values.length; i++) { 52 | const value = asValue(values[i]); 53 | if (value === void 0) 54 | return error(rej, 'incompatible ' + (typeof value) + 'value'); 55 | sql.push(value, template[i + 1]); 56 | } 57 | const query = sql.join('').trim(); 58 | return query.length ? query : error(rej, 'empty query'); 59 | }; 60 | exports.sql = sql; 61 | 62 | const sql2array = sql => { 63 | const re = /(([:$@](\w+))|(\$\{\s*(\w+)\s*\}))/g; 64 | const out = []; 65 | let i = 0; 66 | let match; 67 | while (match = re.exec(sql)) { 68 | out.push(sql.slice(i, match.index), match[3] || match[5]); 69 | i = match.index + match[0].length; 70 | } 71 | out.push(sql.slice(i)); 72 | return out; 73 | }; 74 | exports.sql2array = sql2array; 75 | 76 | // WARNING: this changes the incoming array value @ holes 77 | // useful only when sql2array results are stored, 78 | // and revived, as JSON ... watch out side effects 79 | // if used with same array more than once! 80 | const array2sql = (chunks, data = null) => { 81 | for (let i = 1; i < chunks.length; i += 2) { 82 | const value = asValue(data[chunks[i]]); 83 | if (value === void 0) 84 | return ''; 85 | chunks[i] = value; 86 | } 87 | return chunks.join(''); 88 | }; 89 | exports.array2sql = array2sql; 90 | -------------------------------------------------------------------------------- /test/benchmark.js: -------------------------------------------------------------------------------- 1 | import sqlite3 from 'sqlite3'; 2 | import betterSQLite3 from 'better-sqlite3'; 3 | import SQLiteTag from 'sqlite-tag'; 4 | 5 | import SQLiteTagSpawned from '../esm/index.js'; 6 | 7 | await bench('sqlite3 bindings', () => { 8 | const db = new sqlite3.Database('./test/bindings.db'); 9 | return {db, ...SQLiteTag(db)}; 10 | }); 11 | 12 | await new Promise($ => setTimeout($, 500)); 13 | 14 | await bench('better-sqlite3 bindings', () => { 15 | const db = betterSQLite3('./test/better.db'); 16 | return {db, ...SQLiteTag({ 17 | run(sql, params, callback) { 18 | try { 19 | callback(void 0, db.prepare(sql).run(...params)); 20 | } 21 | catch (error) { 22 | callback(error); 23 | } 24 | }, 25 | get(sql, params, callback) { 26 | try { 27 | callback(void 0, db.prepare(sql).get(...params)); 28 | } 29 | catch (error) { 30 | callback(error); 31 | } 32 | }, 33 | all(sql, params, callback) { 34 | try { 35 | callback(void 0, db.prepare(sql).all(...params)); 36 | } 37 | catch (error) { 38 | callback(error); 39 | } 40 | } 41 | })}; 42 | }); 43 | 44 | await new Promise($ => setTimeout($, 500)); 45 | 46 | await bench('sqlite3 spawned', () => { 47 | return {...SQLiteTagSpawned('./test/spawned.db')}; 48 | }); 49 | 50 | await new Promise($ => setTimeout($, 500)); 51 | 52 | await bench('sqlite3 spawned persistent', () => { 53 | const db = SQLiteTagSpawned('./test/spawned.db', {persistent: true}); 54 | return {db, ...db}; 55 | }); 56 | 57 | async function bench(title, init) { 58 | const bindings = title !== 'sqlite3 spawned'; 59 | console.log(`\x1b[1m${title}\x1b[0m`); 60 | console.time('total'); 61 | 62 | console.time('initialization'); 63 | const {db, all, get, query, raw, transaction} = init(); 64 | console.timeEnd('initialization'); 65 | 66 | console.time('table creation'); 67 | await query`CREATE TABLE lorem (info TEXT)`; 68 | console.timeEnd('table creation'); 69 | 70 | console.time('1K inserts (transaction)'); 71 | // TODO: make this use best approach for sqlite3 72 | const insert = transaction(); 73 | for (let i = 0; i < 1000; i++) 74 | insert`INSERT INTO lorem VALUES (${'Ipsum ' + i})`; 75 | await insert.commit(); 76 | console.timeEnd('1K inserts (transaction)'); 77 | 78 | console.time('single select return'); 79 | let rows = await get`SELECT COUNT(info) AS rows FROM lorem`; 80 | if (bindings) 81 | rows = JSON.parse(JSON.stringify(rows)); 82 | console.timeEnd('single select return'); 83 | 84 | const list = ['Ipsum 2', 'Ipsum 3']; 85 | console.time('multiple select return'); 86 | let multi = await all`SELECT * FROM lorem WHERE info IN (${list})`; 87 | if (bindings) 88 | multi = JSON.parse(JSON.stringify(multi)); 89 | console.timeEnd('multiple select return'); 90 | 91 | console.time('select 1K rows'); 92 | let oneK = await all`SELECT * FROM lorem`; 93 | if (bindings) 94 | oneK = JSON.parse(JSON.stringify(oneK)); 95 | console.timeEnd('select 1K rows'); 96 | 97 | console.time('table removal'); 98 | await query`DROP TABLE ${raw`lorem`}`; 99 | console.timeEnd('table removal'); 100 | 101 | if (db) db.close(); 102 | 103 | console.timeEnd('total'); 104 | console.log(rows); 105 | console.log(''); 106 | } 107 | 108 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import SQLiteTag from '../esm/index.js'; 2 | import {array2sql, sql2array} from '../esm/utils.js'; 3 | 4 | const {all, get, query, raw, transaction, close} = SQLiteTag('./test/sqlite.db', {timeout: 1000}); 5 | 6 | console.time('sqlite-tag-spawned'); 7 | 8 | console.log('✔', 'table creation'); 9 | await query`CREATE TABLE IF NOT EXISTS bin_data (id INTEGER PRIMARY KEY, data BLOB)`; 10 | console.log('✔', 'inserting blobs'); 11 | const one_two_three = new Uint8Array([1, 2, 3]); 12 | await query`INSERT INTO bin_data (data) VALUES (${one_two_three})`; 13 | await query`INSERT INTO bin_data (data) VALUES (${one_two_three.buffer})`; 14 | 15 | const asBuffer = hex => Buffer.from(hex, 'hex'); 16 | const resumed = asBuffer((await get`SELECT hex(data) AS data FROM bin_data`).data); 17 | console.assert([].join.call(resumed) === [].join.call(one_two_three) && '1,2,3' === [].join.call(one_two_three)); 18 | await query`DELETE FROM bin_data`; 19 | await query`INSERT INTO bin_data (data) VALUES (${resumed})`; 20 | const re_resumed = asBuffer((await get`SELECT hex(data) AS data FROM bin_data`).data); 21 | console.assert([].join.call(re_resumed) === [].join.call(one_two_three)); 22 | console.log('✔', 'getting blobs'); 23 | 24 | await query`CREATE TABLE IF NOT EXISTS ${raw`lorem`} (info TEXT)`; 25 | if (!globalThis.DO_NOT_DELETE_FROM) 26 | await query`DELETE FROM lorem`; 27 | 28 | console.log('✔', 'getting nothing from DB'); 29 | console.log( 30 | ' ', 31 | await get`SELECT * FROM lorem WHERE info=${'test'}` === void 0, 32 | (await all`SELECT * FROM lorem WHERE info=${'test'}`).length === 0 33 | ); 34 | 35 | console.log('✔', 'transaction'); 36 | const insert = transaction(); 37 | for (let i = 0; i < 9; i++) 38 | insert`INSERT INTO lorem VALUES (${'Ipsum ' + i})`; 39 | await insert.commit(); 40 | 41 | console.log('✔', 'SQL null'); 42 | await query`INSERT INTO lorem VALUES (${null})`; 43 | 44 | console.log('✔', 'SQL null via undefined'); 45 | await query`INSERT INTO lorem VALUES (${void 0})`; 46 | 47 | console.log('✔', 'SQL dates'); 48 | await query`INSERT INTO lorem VALUES (${new Date})`; 49 | 50 | console.log('✔', 'Single row'); 51 | const row = await get` 52 | SELECT rowid AS id, info 53 | FROM ${raw`lorem`} 54 | WHERE info = ${'Ipsum 5'} 55 | `; 56 | console.log(' ', row.id + ": " + row.info); 57 | 58 | console.log('✔', 'Multiple rows'); 59 | const TABLE = 'lorem'; 60 | const rows = await all`SELECT rowid AS id, info FROM ${raw`${TABLE}`} LIMIT ${0}, ${20}`; 61 | for (let row of rows) 62 | console.log(' ', row.id + ": " + row.info); 63 | 64 | const utf8 = '¥ · £ · € · $ · ¢ · ₡ · ₢ · ₣ · ₤ · ₥ · ₦ · ₧ · ₨ · ₩ · ₪ · ₫ · ₭ · ₮ · ₯ · ₹'; 65 | console.log('✔', 'Safe utf8'); 66 | await query`INSERT INTO lorem VALUES (${utf8})`; 67 | console.assert((await get`SELECT info FROM lorem WHERE info = ${utf8}`).info === utf8); 68 | 69 | console.log('✔', 'IN clause'); 70 | console.log(' ', await all`SELECT * FROM lorem WHERE info IN (${['Ipsum 2', 'Ipsum 3']})`); 71 | 72 | console.log('✔', 'Temporary db as :memory:'); 73 | console.log(' ', await SQLiteTag(':memory:').query`.databases`); 74 | 75 | console.log('✔', 'Error handling'); 76 | try { 77 | await query`INSERT INTO shenanigans VALUES (1, 2, 3)`; 78 | } 79 | catch ({message}) { 80 | console.log(' ', message); 81 | } 82 | 83 | console.log('✔', 'Empty SQL in transaction'); 84 | try { 85 | const empty = transaction(); 86 | empty``; 87 | await empty.commit(); 88 | } 89 | catch ({message}) { 90 | console.log(' ', message); 91 | } 92 | 93 | console.log('✔', 'SQL injection safe'); 94 | try { 95 | await query`INSERT INTO shenanigans VALUES (?, ${2}, ${3})`; 96 | } 97 | catch ({message}) { 98 | console.log(' ', message); 99 | } 100 | 101 | console.log('✔', 'SQL syntax'); 102 | try { 103 | await query`SHENANIGANS`; 104 | } 105 | catch({message}) { 106 | console.log(' ', message); 107 | } 108 | 109 | console.log('✔', 'SQL values'); 110 | try { 111 | await query`INSERT INTO lorem VALUES (${{no:'pe'}})`; 112 | } 113 | catch({message}) { 114 | console.log(' ', message); 115 | } 116 | 117 | console.log('✔', 'SQL invalid numbers'); 118 | try { 119 | await query`INSERT INTO lorem VALUES (${Infinity})`; 120 | } 121 | catch({message}) { 122 | console.log(' ', message); 123 | } 124 | 125 | console.log('✔', 'SQL invalid empty query'); 126 | try { 127 | await query``; 128 | } 129 | catch({message}) { 130 | console.log(' ', message); 131 | } 132 | 133 | const {query: ro} = SQLiteTag('./test/sqlite.db', {readonly: true, timeout: 1000}); 134 | console.log('✔', 'Readonly mode'); 135 | try { 136 | await ro`INSERT INTO lorem VALUES (${'nope'})`; 137 | } 138 | catch({message}) { 139 | console.log(' ', message); 140 | } 141 | 142 | console.log('✔', 'Non SQL query'); 143 | console.log(' ', await ro`.databases`); 144 | 145 | console.timeEnd('sqlite-tag-spawned'); 146 | 147 | const array = sql2array('SELECT * FROM table WHERE value = @value AND age = ${age} LIMIT 1'); 148 | console.assert( 149 | JSON.stringify(array) === '["SELECT * FROM table WHERE value = ","value"," AND age = ","age"," LIMIT 1"]', 150 | 'sql2array does not produces the expected result' 151 | ); 152 | 153 | console.assert(array2sql(array, {value: {}, age: {}}) === '', 'invalid values should not pass'); 154 | 155 | console.assert( 156 | array2sql(array, {value: 'test', age: 123}) === 157 | "SELECT * FROM table WHERE value = 'test' AND age = 123 LIMIT 1", 158 | 'array2sql does not produce the expected result' 159 | ); 160 | 161 | close(); 162 | 163 | import('./persistent.js'); 164 | -------------------------------------------------------------------------------- /esm/index.js: -------------------------------------------------------------------------------- 1 | import {spawn} from 'child_process'; 2 | import {randomUUID} from 'crypto'; 3 | import {tmpdir} from 'os'; 4 | import {join} from 'path'; 5 | 6 | import {error, raw, sql} from './utils.js'; 7 | 8 | const UNIQUE_ID = randomUUID(); 9 | const UNIQUE_ID_LINE = `[{"_":"${UNIQUE_ID}"}]\n`; 10 | 11 | const {isArray} = Array; 12 | const {parse} = JSON; 13 | const {defineProperty} = Object; 14 | 15 | const noop = () => {}; 16 | 17 | const defaultExec = (res, rej, type, bin, args, opts) => { 18 | const out = []; 19 | 20 | const {stdout, stderr} = spawn(bin, args, opts).on( 21 | 'close', 22 | code => { 23 | if (errored || code !== 0) { 24 | if (code !== 0) 25 | error(rej, 'busy DB or query too slow'); 26 | return; 27 | } 28 | 29 | const result = out.join('').trim(); 30 | if (type === 'query') 31 | res(result); 32 | else { 33 | const json = parse(result || '[]'); 34 | res(type === 'get' && isArray(json) ? json.shift() : json); 35 | } 36 | } 37 | ); 38 | 39 | stdout.on('data', data => { out.push(data); }); 40 | 41 | let errored = false; 42 | stderr.on('data', data => { 43 | errored = true; 44 | error(rej, ''.trim.call(data)); 45 | }); 46 | }; 47 | 48 | const interactiveExec = (bin, db, timeout) => { 49 | const {stdin, stdout, stderr} = spawn(bin, [db]); 50 | stdin.write('.mode json\n'); 51 | if (timeout) 52 | stdin.write(`.timeout ${timeout}\n`); 53 | let next = Promise.resolve(); 54 | return (res, rej, type, _, args) => { 55 | if (type === 'close') { 56 | stdin.write('.quit\n'); 57 | next = null; 58 | } 59 | else if (next) { 60 | next = next.then( 61 | () => new Promise(done => { 62 | let out = ''; 63 | const $ = data => { 64 | out += data; 65 | let process = false; 66 | while (out.endsWith(UNIQUE_ID_LINE)) { 67 | process = true; 68 | out = out.slice(0, -UNIQUE_ID_LINE.length); 69 | } 70 | if (process) { 71 | dropListeners(); 72 | // this one is funny *but* apparently possible 73 | /* c8 ignore next 2 */ 74 | while (out.startsWith(UNIQUE_ID_LINE)) 75 | out = out.slice(UNIQUE_ID_LINE.length); 76 | 77 | if (type === 'query') 78 | res(out); 79 | else { 80 | const json = parse(out || '[]'); 81 | res(type === 'get' ? json.shift() : json); 82 | } 83 | } 84 | }; 85 | const _ = data => { 86 | dropListeners(); 87 | rej(new Error(data)); 88 | }; 89 | const dropListeners = () => { 90 | done(); 91 | stdout.removeListener('data', $); 92 | stderr.removeListener('data', _); 93 | }; 94 | stdout.on('data', $); 95 | stderr.once('data', _); 96 | stdin.write(`${args[args.length - 1]};\n`); 97 | stdin.write(`SELECT '${UNIQUE_ID}' as _;\n`); 98 | }) 99 | ); 100 | } 101 | }; 102 | }; 103 | 104 | /** 105 | * Returns a template literal tag function usable to await `get`, `all`, or 106 | * `query` SQL statements. The tag will return a Promise with results. 107 | * In case of `all`, an Array is always resolved, if no error occurs, while with 108 | * `get` the result or undefined is returned instead. The `query` returns whatever 109 | * output the spawned command produced. 110 | * @param {string} type the query type 111 | * @param {string} bin the sqlite3 executable 112 | * @param {function} exec the logic to spawn and parse the output 113 | * @param {string[]} args spawned arguments for sqlite3 114 | * @param {object} opts spawned options 115 | * @returns {function} 116 | */ 117 | const sqlite = (type, bin, exec, args, opts) => (..._) => new Promise((res, rej) => { 118 | let query = sql(rej, _); 119 | if (!query.length) 120 | return; 121 | if ( 122 | type === 'get' && 123 | /^SELECT\s+/i.test(query) && 124 | !/\s+LIMIT\s+\d+$/i.test(query) 125 | ) { 126 | query += ' LIMIT 1'; 127 | } 128 | exec(res, rej, type, bin, args.concat(query), opts); 129 | }); 130 | 131 | let memory = ''; 132 | 133 | /** 134 | * @typedef {object} SQLiteOptions optional options 135 | * @property {boolean?} readonly opens the database in readonly mode 136 | * @property {string?} bin the sqlite3 executable path 137 | * @property {number?} timeout optional db/spawn timeout in milliseconds 138 | * @property {boolean} [persistent=false] optional flag to keep the db persistent 139 | * @property {function} [exec=defaultExec] the logic to spawn and parse the output 140 | */ 141 | 142 | /** 143 | * Returns `all`, `get`, `query`, and `raw` template literal tag utilities, 144 | * plus a `transaction` one that, once invoked, returns also a template literal 145 | * tag utility with a special `.commit()` method, to execute all queries used 146 | * within such returned tag function. 147 | * @param {string} db the database file to create or `:memory:` for a temp file 148 | * @param {SQLiteOptions?} options optional extra options 149 | * @returns 150 | */ 151 | export default function SQLiteTag(db, options = {}) { 152 | if (db === ':memory:') 153 | db = memory || (memory = join(tmpdir(), randomUUID())); 154 | 155 | const timeout = options.timeout || 0; 156 | const bin = options.bin || 'sqlite3'; 157 | 158 | const args = [db, '-bail']; 159 | const opts = {timeout}; 160 | 161 | if (options.readonly) 162 | args.push('-readonly'); 163 | 164 | if (timeout) 165 | args.push('-cmd', '.timeout ' + timeout); 166 | 167 | const json = args.concat('-json'); 168 | const exec = options.exec || ( 169 | options.persistent ? 170 | interactiveExec(bin, db, timeout) : 171 | defaultExec 172 | ); 173 | 174 | return { 175 | /** 176 | * Returns a template literal tag function where all queries part of the 177 | * transactions should be written, and awaited through `tag.commit()`. 178 | * @returns {function} 179 | */ 180 | transaction() { 181 | const params = []; 182 | return defineProperty( 183 | (..._) => { params.push(_); }, 184 | 'commit', 185 | {value() { 186 | return new Promise((res, rej) => { 187 | const multi = ['BEGIN TRANSACTION']; 188 | for (const _ of params) { 189 | const query = sql(rej, _); 190 | if (!query.length) 191 | return; 192 | multi.push(query); 193 | } 194 | multi.push('COMMIT'); 195 | exec(res, rej, 'query', bin, args.concat(multi.join(';')), opts); 196 | }); 197 | }} 198 | ); 199 | }, 200 | query: sqlite('query', bin, exec, args, opts), 201 | get: sqlite('get', bin, exec, json, opts), 202 | all: sqlite('all', bin, exec, json, opts), 203 | close: options.persistent ? (() => exec(null, null, 'close')) : noop, 204 | raw 205 | }; 206 | }; 207 | -------------------------------------------------------------------------------- /cjs/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {spawn} = require('child_process'); 3 | const {randomUUID} = require('crypto'); 4 | const {tmpdir} = require('os'); 5 | const {join} = require('path'); 6 | 7 | const {error, raw, sql} = require('./utils.js'); 8 | 9 | const UNIQUE_ID = randomUUID(); 10 | const UNIQUE_ID_LINE = `[{"_":"${UNIQUE_ID}"}]\n`; 11 | 12 | const {isArray} = Array; 13 | const {parse} = JSON; 14 | const {defineProperty} = Object; 15 | 16 | const noop = () => {}; 17 | 18 | const defaultExec = (res, rej, type, bin, args, opts) => { 19 | const out = []; 20 | 21 | const {stdout, stderr} = spawn(bin, args, opts).on( 22 | 'close', 23 | code => { 24 | if (errored || code !== 0) { 25 | if (code !== 0) 26 | error(rej, 'busy DB or query too slow'); 27 | return; 28 | } 29 | 30 | const result = out.join('').trim(); 31 | if (type === 'query') 32 | res(result); 33 | else { 34 | const json = parse(result || '[]'); 35 | res(type === 'get' && isArray(json) ? json.shift() : json); 36 | } 37 | } 38 | ); 39 | 40 | stdout.on('data', data => { out.push(data); }); 41 | 42 | let errored = false; 43 | stderr.on('data', data => { 44 | errored = true; 45 | error(rej, ''.trim.call(data)); 46 | }); 47 | }; 48 | 49 | const interactiveExec = (bin, db, timeout) => { 50 | const {stdin, stdout, stderr} = spawn(bin, [db]); 51 | stdin.write('.mode json\n'); 52 | if (timeout) 53 | stdin.write(`.timeout ${timeout}\n`); 54 | let next = Promise.resolve(); 55 | return (res, rej, type, _, args) => { 56 | if (type === 'close') { 57 | stdin.write('.quit\n'); 58 | next = null; 59 | } 60 | else if (next) { 61 | next = next.then( 62 | () => new Promise(done => { 63 | let out = ''; 64 | const $ = data => { 65 | out += data; 66 | let process = false; 67 | while (out.endsWith(UNIQUE_ID_LINE)) { 68 | process = true; 69 | out = out.slice(0, -UNIQUE_ID_LINE.length); 70 | } 71 | if (process) { 72 | dropListeners(); 73 | // this one is funny *but* apparently possible 74 | /* c8 ignore next 2 */ 75 | while (out.startsWith(UNIQUE_ID_LINE)) 76 | out = out.slice(UNIQUE_ID_LINE.length); 77 | 78 | if (type === 'query') 79 | res(out); 80 | else { 81 | const json = parse(out || '[]'); 82 | res(type === 'get' ? json.shift() : json); 83 | } 84 | } 85 | }; 86 | const _ = data => { 87 | dropListeners(); 88 | rej(new Error(data)); 89 | }; 90 | const dropListeners = () => { 91 | done(); 92 | stdout.removeListener('data', $); 93 | stderr.removeListener('data', _); 94 | }; 95 | stdout.on('data', $); 96 | stderr.once('data', _); 97 | stdin.write(`${args[args.length - 1]};\n`); 98 | stdin.write(`SELECT '${UNIQUE_ID}' as _;\n`); 99 | }) 100 | ); 101 | } 102 | }; 103 | }; 104 | 105 | /** 106 | * Returns a template literal tag function usable to await `get`, `all`, or 107 | * `query` SQL statements. The tag will return a Promise with results. 108 | * In case of `all`, an Array is always resolved, if no error occurs, while with 109 | * `get` the result or undefined is returned instead. The `query` returns whatever 110 | * output the spawned command produced. 111 | * @param {string} type the query type 112 | * @param {string} bin the sqlite3 executable 113 | * @param {function} exec the logic to spawn and parse the output 114 | * @param {string[]} args spawned arguments for sqlite3 115 | * @param {object} opts spawned options 116 | * @returns {function} 117 | */ 118 | const sqlite = (type, bin, exec, args, opts) => (..._) => new Promise((res, rej) => { 119 | let query = sql(rej, _); 120 | if (!query.length) 121 | return; 122 | if ( 123 | type === 'get' && 124 | /^SELECT\s+/i.test(query) && 125 | !/\s+LIMIT\s+\d+$/i.test(query) 126 | ) { 127 | query += ' LIMIT 1'; 128 | } 129 | exec(res, rej, type, bin, args.concat(query), opts); 130 | }); 131 | 132 | let memory = ''; 133 | 134 | /** 135 | * @typedef {object} SQLiteOptions optional options 136 | * @property {boolean?} readonly opens the database in readonly mode 137 | * @property {string?} bin the sqlite3 executable path 138 | * @property {number?} timeout optional db/spawn timeout in milliseconds 139 | * @property {boolean} [persistent=false] optional flag to keep the db persistent 140 | * @property {function} [exec=defaultExec] the logic to spawn and parse the output 141 | */ 142 | 143 | /** 144 | * Returns `all`, `get`, `query`, and `raw` template literal tag utilities, 145 | * plus a `transaction` one that, once invoked, returns also a template literal 146 | * tag utility with a special `.commit()` method, to execute all queries used 147 | * within such returned tag function. 148 | * @param {string} db the database file to create or `:memory:` for a temp file 149 | * @param {SQLiteOptions?} options optional extra options 150 | * @returns 151 | */ 152 | function SQLiteTag(db, options = {}) { 153 | if (db === ':memory:') 154 | db = memory || (memory = join(tmpdir(), randomUUID())); 155 | 156 | const timeout = options.timeout || 0; 157 | const bin = options.bin || 'sqlite3'; 158 | 159 | const args = [db, '-bail']; 160 | const opts = {timeout}; 161 | 162 | if (options.readonly) 163 | args.push('-readonly'); 164 | 165 | if (timeout) 166 | args.push('-cmd', '.timeout ' + timeout); 167 | 168 | const json = args.concat('-json'); 169 | const exec = options.exec || ( 170 | options.persistent ? 171 | interactiveExec(bin, db, timeout) : 172 | defaultExec 173 | ); 174 | 175 | return { 176 | /** 177 | * Returns a template literal tag function where all queries part of the 178 | * transactions should be written, and awaited through `tag.commit()`. 179 | * @returns {function} 180 | */ 181 | transaction() { 182 | const params = []; 183 | return defineProperty( 184 | (..._) => { params.push(_); }, 185 | 'commit', 186 | {value() { 187 | return new Promise((res, rej) => { 188 | const multi = ['BEGIN TRANSACTION']; 189 | for (const _ of params) { 190 | const query = sql(rej, _); 191 | if (!query.length) 192 | return; 193 | multi.push(query); 194 | } 195 | multi.push('COMMIT'); 196 | exec(res, rej, 'query', bin, args.concat(multi.join(';')), opts); 197 | }); 198 | }} 199 | ); 200 | }, 201 | query: sqlite('query', bin, exec, args, opts), 202 | get: sqlite('get', bin, exec, json, opts), 203 | all: sqlite('all', bin, exec, json, opts), 204 | close: options.persistent ? (() => exec(null, null, 'close')) : noop, 205 | raw 206 | }; 207 | } 208 | module.exports = SQLiteTag; 209 | --------------------------------------------------------------------------------