├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE.txt ├── Makefile ├── README.md ├── benchmark ├── dblite.js └── sqlite3.js ├── build └── dblite.node.js ├── index.html ├── package.json ├── src └── dblite.js ├── template ├── amd.after ├── amd.before ├── copyright ├── license.after ├── license.before ├── md.after ├── md.before ├── node.after ├── node.before ├── var.after └── var.before └── test ├── .test.js └── dblite.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | package-lock.json 3 | node_modules/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | benchmark/ 4 | template/ 5 | node_modules/ 6 | build/*.amd.js 7 | Makefile 8 | index.html 9 | package-lock.json 10 | .gitignore 11 | .travis.yml 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 8 4 | - stable 5 | git: 6 | depth: 1 7 | branches: 8 | only: 9 | - master 10 | before_script: 11 | - "npm install wru" 12 | - "which sqlite3" 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 - present by WebReflection 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: bench build var node amd size hint clean test web preview pages dependencies 2 | 3 | # repository name 4 | REPO = dblite 5 | 6 | # make var files 7 | VAR = src/$(REPO).js 8 | 9 | # make node files 10 | NODE = $(VAR) 11 | 12 | # make amd files 13 | AMD = $(VAR) 14 | 15 | # README constant 16 | 17 | 18 | # default build task 19 | build: 20 | make clean 21 | # make var 22 | make node 23 | # make amd 24 | make test 25 | # make hint 26 | make bench 27 | # make size 28 | 29 | # build generic version 30 | var: 31 | mkdir -p build 32 | cat template/var.before $(VAR) template/var.after >build/no-copy.$(REPO).max.js 33 | node node_modules/uglify-js/bin/uglifyjs --verbose build/no-copy.$(REPO).max.js >build/no-copy.$(REPO).js 34 | cat template/license.before LICENSE.txt template/license.after build/no-copy.$(REPO).max.js >build/$(REPO).max.js 35 | cat template/copyright build/no-copy.$(REPO).js >build/$(REPO).js 36 | rm build/no-copy.$(REPO).max.js 37 | rm build/no-copy.$(REPO).js 38 | 39 | # build node.js version 40 | node: 41 | mkdir -p build 42 | cat template/license.before LICENSE.txt template/license.after template/node.before $(NODE) template/node.after >build/$(REPO).node.js 43 | 44 | # build AMD version 45 | amd: 46 | mkdir -p build 47 | cat template/amd.before $(AMD) template/amd.after >build/no-copy.$(REPO).max.amd.js 48 | node node_modules/uglify-js/bin/uglifyjs --verbose build/no-copy.$(REPO).max.amd.js >build/no-copy.$(REPO).amd.js 49 | cat template/license.before LICENSE.txt template/license.after build/no-copy.$(REPO).max.amd.js >build/$(REPO).max.amd.js 50 | cat template/copyright build/no-copy.$(REPO).amd.js >build/$(REPO).amd.js 51 | rm build/no-copy.$(REPO).max.amd.js 52 | rm build/no-copy.$(REPO).amd.js 53 | 54 | bench: 55 | echo '' 56 | node benchmark/dblite.js 1 57 | rm bench.dblite.db 58 | 59 | 60 | size: 61 | wc -c build/$(REPO).max.js 62 | gzip -c build/$(REPO).js | wc -c 63 | 64 | # hint built file 65 | hint: 66 | node node_modules/jshint/bin/jshint build/$(REPO).node.js 67 | 68 | # clean/remove build folder 69 | clean: 70 | rm -rf build 71 | 72 | # tests, as usual and of course 73 | test: 74 | npm test 75 | 76 | # launch polpetta (ctrl+click to open the page) 77 | web: 78 | node node_modules/polpetta/build/polpetta ./ 79 | 80 | # markdown the readme and view it 81 | preview: 82 | node_modules/markdown/bin/md2html.js README.md >README.md.htm 83 | cat template/md.before README.md.htm template/md.after >README.md.html 84 | open README.md.html 85 | sleep 3 86 | rm README.md.htm README.md.html 87 | 88 | pages: 89 | git pull --rebase 90 | make var 91 | mkdir -p ~/tmp 92 | mkdir -p ~/tmp/$(REPO) 93 | cp -rf src ~/tmp/$(REPO) 94 | cp -rf build ~/tmp/$(REPO) 95 | cp -rf test ~/tmp/$(REPO) 96 | cp index.html ~/tmp/$(REPO) 97 | git checkout gh-pages 98 | mkdir -p test 99 | rm -rf test 100 | cp -rf ~/tmp/$(REPO) test 101 | git add test 102 | git add test/. 103 | git commit -m 'automatic test generator' 104 | git push 105 | git checkout master 106 | rm -r ~/tmp/$(REPO) 107 | 108 | # modules used in this repo 109 | dependencies: 110 | rm -rf node_modules 111 | mkdir node_modules 112 | npm install wru 113 | # npm install sqlite3 114 | # npm install polpetta 115 | # npm install uglify-js@1 116 | npm install jshint@2.1.6 117 | npm install markdown 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dblite 2 | ====== 3 | 4 | [![Build Status](https://travis-ci.org/WebReflection/dblite.svg?branch=master)](https://travis-ci.org/WebReflection/dblite) 5 | 6 | # Deprecated 7 | 8 | This module served me well to date but it's overly complicated for very little real-world gain or reasons to be so. 9 | 10 | Please consider **[sqlite-tag-spawned](https://github.com/WebReflection/sqlite-tag-spawned#readme)** as modern, safe, as fast as this module is, alternative, as I am not planning to improve much in here from now on, thank you! 11 | 12 | - - - - - - 13 | 14 | a zero hassle wrapper for sqlite 15 | ```javascript 16 | var dblite = require('dblite'), 17 | db = dblite('file.name'); 18 | 19 | // Asynchronous, fast, and ... 20 | db.query('SELECT * FROM table', function(err, rows) { 21 | // ... that easy! 22 | }); 23 | ``` 24 | More in [the related blogpost](http://webreflection.blogspot.com/2013/07/dblite-sqlite3-for-nodejs-made-easy.html) and here too :-) 25 | 26 | 27 | ### Updates 28 | Version `0.7.5` forces `-noheader` flag if there is no explicit `-header` flag so that no matter what, headers will **not** be used. 29 | 30 | This will eventually overwrite the `.sqliterc` but will make the library behavior more consistent across platforms. 31 | 32 | Please check [issue 35](https://github.com/WebReflection/dblite/issues/35) to know more. 33 | 34 | - - - - - - 35 | 36 | Previously, in **sqlite3** version `3.8.6` you need a "_new line agnostic_" version of `dblite`, used in dblite version `0.6.0`. 37 | 38 | This **breaks** compatibility with older version of the database cli but this problem should have been fixed in `0.7.0`. 39 | 40 | ```js 41 | // old version 42 | var dblite = require('dblite'); 43 | 44 | // 3.8.6 version 45 | var dblite = require('dblite').withSQLite('3.8.6+'); 46 | 47 | // new version, same as old one 48 | var dblite = require('dblite'); 49 | 50 | ``` 51 | 52 | It seems that sqlite3 version `3.8.8+` introduced a new line `\n` on Windows machines too so the whole initialization is now performed asynchronously and through features detection. 53 | 54 | This should fix the annoying `EOL` problem "_fore`var`_" 55 | 56 | 57 | ### The What And The Why 58 | I've created `dblite` module because there's still not a simple and straight forward or standard way to have [sqlite](http://www.sqlite.org) in [node.js](http://nodejs.org) without requiring to re-compile, re-build, download sources a part or install dependencies instead of simply `apt-get install sqlite3` or `pacman -S sqlite3` in your \*nix system. 59 | 60 | `dblite` has been created with portability, simplicity, and reasonable performance for **embedded Hardware** such ARM boards, Raspberry Pi, Arduino Yun, Atmel MIPS CPUs or Linino boards in mind. 61 | 62 | Generally speaking all linux based distributions like [Arch Linux](https://www.archlinux.org), where is not always that easy to `node-gyp` a module and add dependencies that work, can now use this battle tested wrap and perform basic to advanced sqlite operations. 63 | 64 | 65 | ### Bootstrap 66 | To install dblite simply `npm install dblite` then in node: 67 | ```javascript 68 | var dblite = require('dblite'), 69 | db = dblite('/folder/to/file.sqlite'); 70 | 71 | // ready to go, i.e. 72 | db.query('.databases'); 73 | db.query( 74 | 'SELECT * FROM users WHERE pass = ?', 75 | [pass], 76 | function (err, rows) { 77 | var user = rows.length && rows[0]; 78 | } 79 | ); 80 | ``` 81 | By default the `dblite` function uses **sqlite3 as executable**. If you need to change the path simply update `dblite.bin = "/usr/local/bin/sqlite3";` before invoking the function. 82 | 83 | 84 | ### API 85 | Right now a created `EventEmitter` `db` instance has 3 extra methods: `.query()`, `.lastRowID()`, and `.close()`. 86 | 87 | The `.lastRowID(table, callback(rowid))` helper simplifies a common operation with SQL tables after inserts, handful as shortcut for the following query: 88 | `SELECT ROWID FROM ``table`` ORDER BY ROWID DESC LIMIT 1`. 89 | 90 | The method `.close()` does exactly what it suggests: it closes the database connection. 91 | Please note that it is **not possible to perform other operations once it has been closed**. 92 | 93 | Being an `EventEmitter` instance, the database variable will be notified with the `close` listener, if any. 94 | 95 | 96 | ### Understanding The .query() Method 97 | The main role in this module is played by the `db.query()` method, a method rich in overloads all with perfect and natural meaning. 98 | 99 | The amount of parameters goes from one to four, left to right, where left is the input going through the right which is the eventual output. 100 | 101 | All parameters are optionals except the SQL one. 102 | 103 | ### db.query() Possible Combinations 104 | ```javascript 105 | db.query(SQL) 106 | db.query(SQL, callback:Function) 107 | db.query(SQL, params:Array|Object) 108 | db.query(SQL, fields:Array|Object) 109 | db.query(SQL, params:Array|Object, callback:Function) 110 | db.query(SQL, fields:Array|Object, callback:Function) 111 | db.query(SQL, params:Array|Object, fields:Array|Object) 112 | db.query(SQL, params:Array|Object, fields:Array|Object, callback:Function) 113 | ``` 114 | All above combinations are [tested properly in this file](test/dblite.js) together with many other tests able to make `dblite` robust enough and ready to be used. 115 | 116 | Please note how `params` is always before `fields` and/or `callback` if `fields` is missing, just as reminder that order is left to right accordingly with what we are trying to do. 117 | 118 | Following detailed explanation per each parameter. 119 | 120 | #### The SQL:string 121 | This string [accepts any query understood by SQLite](http://www.sqlite.org/lang.html) plus it accepts all commands that regular SQLite shell would accept such `.databases`, `.tables`, `.show` and all others passing through the specified `info` listener, if any, using just the console as fallback otherwise. 122 | ```javascript 123 | var dblite = require('dblite'), 124 | db = dblite('./db.sqlite'); 125 | 126 | // will call the implicit `info` console.log 127 | db.query('.show'); 128 | /* will console.log something like: 129 | 130 | echo: off 131 | explain: off 132 | headers: off 133 | mode: csv 134 | nullvalue: "" 135 | output: stdout 136 | separator: "," 137 | stats: off 138 | width: 139 | */ 140 | 141 | // normal query 142 | db.query('CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, value TEXT)'); 143 | db.query('INSERT INTO test VALUES(null, ?)', ['some text']); 144 | db.query('SELECT * FROM test'); 145 | // will implicitly log the following 146 | // [ [ '1', 'some text' ] ] 147 | ``` 148 | 149 | ##### Warning! 150 | This library heavily relies on strings and it normalizes them through special escaping which aim is to make passed data safe and secure which should be goal #1 of each db oriented API. 151 | 152 | Please **do not pass datamanually** like `INSERT INTO table VALUES (null, 'my@email.com')` and always use, specially for any field that contains strings, the provided API: `INSERT INTO table VALUES (null, '@email')` and `{emaiL: 'my@email.com'}`. These kind of operations are described in the following paragraphs. 153 | 154 | 155 | #### The params:Array|Object 156 | If the SQL string **contains special chars** such `?`, `:key`, `$key`, or `@key` properties, these will be replaced accordingly with the `params` `Array` or `Object` that, in this case, MUST be present. 157 | ```javascript 158 | // params as Array 159 | db.query('SELECT * FROM test WHERE id = ?', [1]); 160 | 161 | // params as Object 162 | db.query('SELECT * FROM test WHERE id = :id', {id:1}); 163 | // same as 164 | db.query('SELECT * FROM test WHERE id = $id', {id:1}); 165 | // same as 166 | db.query('SELECT * FROM test WHERE id = @id', {id:1}); 167 | ``` 168 | 169 | #### The fields:Array|Object 170 | By default, results are returned as an `Array` where all rows are the outer `Array` and each single row is another `Array`. 171 | ```javascript 172 | db.query('SELECT * FROM test'); 173 | // will log something like: 174 | [ 175 | [ '1', 'some text' ], // row1 176 | [ '2', 'something else' ] // rowN 177 | ] 178 | ``` 179 | If we specify a fields parameter we can have each row represented by an object, instead of an array. 180 | ```javascript 181 | // same query using fields as Array 182 | db.query('SELECT * FROM test', ['key', 'value']); 183 | // will log something like: 184 | [ 185 | {key: '1', value: 'some text'}, // row1 186 | {key: '2', value: 'something else'} // rowN 187 | ] 188 | ``` 189 | 190 | #### Parsing Through The fields:Object 191 | [SQLite Datatypes](http://www.sqlite.org/datatype3.html) are different from JavaScript plus SQLite works via affinity. 192 | This module also parses sqlite3 output which is **always a string** and as string every result will always be returned **unless** we specify `fields` parameter as object, suggesting validation per each field. 193 | ```javascript 194 | // same query using fields as Object 195 | db.query('SELECT * FROM test', { 196 | key: Number, 197 | value: String 198 | }); 199 | // note the key as integer! 200 | [ 201 | {key: 1, value: 'some text'}, // row1 202 | {key: 2, value: 'something else'} // rowN 203 | ] 204 | ``` 205 | More complex validators/transformers can be passed without problems: 206 | ```javascript 207 | // same query using fields as Object 208 | db.query('SELECT * FROM `table.users`', { 209 | id: Number, 210 | name: String, 211 | adult: Boolean, 212 | skills: JSON.parse, 213 | birthday: Date, 214 | cube: function (fieldValue) { 215 | return fieldValue * 3; 216 | } 217 | }); 218 | ``` 219 | 220 | #### The params:Array|Object AND The fields:Array|Object 221 | Not a surprise we can combine both params, using the left to right order input to output so **params first**! 222 | ```javascript 223 | // same query using params AND fields 224 | db.query('SELECT * FROM test WHERE id = :id', { 225 | id: 1 226 | },{ 227 | key: Number, 228 | value: String 229 | }); 230 | 231 | // same as... 232 | db.query('SELECT * FROM test WHERE id = ?', [1], ['key', 'value']); 233 | // same as... 234 | db.query('SELECT * FROM test WHERE id = ?', [1], { 235 | key: Number, 236 | value: String 237 | }); 238 | // same as... 239 | db.query('SELECT * FROM test WHERE id = :id', { 240 | id: 1 241 | }, [ 242 | 'key', 'value' 243 | ]); 244 | ``` 245 | 246 | #### The callback:Function 247 | When a `SELECT` or a `PRAGMA` `SQL` is executed the module puts itself in a *waiting for results* state. 248 | 249 | **Update** - Starting from `0.4.0` the callback will be invoked with `err` and `data` if the callback length is greater than one. `function(err, data){}` VS `function(data){}`. However, latter mode will keep working in order to not break backward compatibility. 250 | **Update** - Starting from `0.3.3` every other `SQL` statement will invoke the callback after the operation has been completed. 251 | 252 | As soon as results are fully pushed to the output the module parses this result, if any, and send it to the specified callback. 253 | 254 | The callback is **always the last specified parameter**, if any, or the implicit equivalent of `console.log.bind(console)`. 255 | Latter case is simply helpful to operate directly via `node` **console** and see results without bothering writing a callback each `.query()` call. 256 | 257 | #### Extra Bonus: JSON Serialization With fields:Array|Object 258 | If one field value is not scalar (boolean, number, string, null) `JSON.stringify` is performed in order to save data. 259 | This helps lazy developers that don't want to pre parse every field and let `dblite` do the magic. 260 | ```javascript 261 | // test has two fields, id and value 262 | db.query('INSERT INTO test VALUES(?, ?)', [ 263 | 123, 264 | {name: 'dblite', rate: 'awesome'} // value serialized 265 | ]); 266 | 267 | // use the fields to parse back the object 268 | db.query('SELECT * FROM test WHERE id = ?', [123], { 269 | id: Number, 270 | value: JSON.parse // value unserialized 271 | }, function (err, rows) { 272 | var record = rows[0]; 273 | console.log(record.id); // 123 274 | console.log(record.value.name); // "dblite" 275 | console.log(record.value.rate); // "awesome"" 276 | }); 277 | ``` 278 | 279 | ### Automatic Fields Through Headers 280 | Since version `0.3.0` it is possible to enable automatic fields parsing either through initialization (suggested) or at runtime. 281 | ```javascript 282 | var dblite = require('dblite'), 283 | // passing extra argument at creation 284 | db = dblite('file.name', '-header'); 285 | 286 | db.query('SELECT * FROM table', function(err, rows) { 287 | rows[0]; // {header0: value0, headerN: valueN} 288 | }); 289 | 290 | // at runtime 291 | db 292 | .query('.headers ON') 293 | .query('SELECT * FROM table', function(err, rows) { 294 | rows[0]; // {header0: value0, headerN: valueN} 295 | }) 296 | .query('.headers OFF') 297 | ; 298 | ``` 299 | 300 | In version `0.3.2` a smarter approach for combined _headers/fields_ is used where the right key order is granted by headers but it's possible to validate known fields too. 301 | 302 | ```javascript 303 | var db = require('dblite')('file.name', '-header'); 304 | 305 | db.query('SELECT 1 as one, 2 as two', {two:Number}, function(err, rows) { 306 | rows[0]; // {one: "1", two: 2} // note "1" as String 307 | }); 308 | ``` 309 | In this way these two options can be supplementary when and if necessary. 310 | 311 | 312 | ### Handling Infos And Errors - Listeners 313 | The `EventEmitter` will notify any listener attached to `info`, `error`, or `close` accordingly with the current status. 314 | ```javascript 315 | db.on('info', function (data) { 316 | // show data returned by special syntax 317 | // such: .databases .tables .show and others 318 | console.log(data); 319 | // by default, it does the same 320 | }); 321 | 322 | db.on('error', function (err) { 323 | // same as `info` but for errors 324 | console.error(err.toString()); 325 | // by default, it does the same 326 | }); 327 | 328 | db.on('close', function (code) { 329 | // by default, it logs "bye bye" 330 | // invoked once the database has been closed 331 | // and every statement in the queue executed 332 | // the code is the exit code returned via SQLite3 333 | // usually 0 if everything was OK 334 | console.log('safe to get out of here ^_^_'); 335 | }); 336 | ``` 337 | Please **note** that error is invoked only if the callback is not handling it already via double argument. 338 | 339 | The `close` event ensures that all operations have been successfully performed and your app is ready to exit or move next. 340 | 341 | Please note that after invoking `db.close()` any other query will be ignored and the instance will be put in a _waiting to complete_ state which will invoke the `close` listener once operations have been completed. 342 | 343 | 344 | ### Raspberry Pi Performance 345 | This is the output generated after a `make test` call in this repo folder within Arch Linux for RPi. 346 | ``` 347 | npm test 348 | 349 | > dblite@0.1.2 test /home/dblite 350 | > node test/.test.js 351 | 352 | /home/dblite/dblite.test.sqlite 353 | ------------------------------ 354 | main 355 | passes: 1, fails: 0, errors: 0 356 | ------------------------------ 357 | create table if not exists 358 | passes: 1, fails: 0, errors: 0 359 | ------------------------------ 360 | 100 sequential inserts 361 | 100 records in 3.067 seconds 362 | passes: 1, fails: 0, errors: 0 363 | ------------------------------ 364 | 1 transaction with 100 inserts 365 | 200 records in 0.178 seconds 366 | passes: 1, fails: 0, errors: 0 367 | ------------------------------ 368 | auto escape 369 | passes: 1, fails: 0, errors: 0 370 | ------------------------------ 371 | auto field 372 | fetched 201 rows as objects in 0.029 seconds 373 | passes: 1, fails: 0, errors: 0 374 | ------------------------------ 375 | auto parsing field 376 | fetched 201 rows as normalized objects in 0.038 seconds 377 | passes: 1, fails: 0, errors: 0 378 | ------------------------------ 379 | many selects at once 380 | different selects in 0.608 seconds 381 | passes: 1, fails: 0, errors: 0 382 | ------------------------------ 383 | db.query() arguments 384 | [ [ '1' ] ] 385 | [ [ '2' ] ] 386 | [ { id: 1 } ] 387 | [ { id: 2 } ] 388 | passes: 5, fails: 0, errors: 0 389 | ------------------------------ 390 | utf-8 391 | ¥ · £ · € · $ · ¢ · ₡ · ₢ · ₣ · ₤ · ₥ · ₦ · ₧ · ₨ · ₩ · ₪ · ₫ · ₭ · ₮ · ₯ · ₹ 392 | passes: 1, fails: 0, errors: 0 393 | ------------------------------ 394 | erease file 395 | passes: 1, fails: 0, errors: 0 396 | 397 | ------------------------------ 398 | 15 Passes 399 | ------------------------------ 400 | ``` 401 | If an SD card can do this good, I guess any other environment should not have problems here ;-) 402 | 403 | ### F.A.Q. 404 | Here a list of probably common Q&A about this module. Please do not hesitate to ask me more, if necessary, thanks. 405 | 406 | * **How Does It Work?** `dblite` uses a [spawned](http://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options) version of the `sqlite3` executable. It could theoretically work with any other SQL like database but it's tested with `sqlite3-shell` only 407 | * **Does It Spawn Per Each Query?** this is a quick one: **NO**! `dblite` spawns once per each database file where usually there is only one database file opened per time. 408 | * **How About Memory And Performance?** Accordingly with `node` manual: 409 | 410 | > These child Nodes are still whole new instances of V8. 411 | > Assume at least 30ms startup and 10mb memory for each new Node. 412 | > That is, you cannot create many thousands of them. 413 | 414 | Since `dblite` spawns only once, there is a little overhead during the database initialization but that's pretty much it, the amount of RAM increases with the amount of data we save or retrieve from the database. The above **Raspberry Pi Benchmark** should ensure that with most common operation, and using transactions where possible, latency and RAM aren't a real issue. 415 | * **Why Not The Native One?** I had some difficulty installing this [node-sqlite3 module](https://github.com/developmentseed/node-sqlite3#name) due `node-gyp` incompatibilities with some **ARM** based device in both *Debian* and *ArchLinux*. Since I really needed an sqlite manager for the next version of [polpetta](https://github.com/WebReflection/polpetta#က-polpetta) which aim is to have a complete, lightweight, and super fast web server in many embedded hardware such RPi, Cubieboard, and others, and since I needed something able to work with multiple core too, I've decided to try this road wrapping the native, easy to install and update, `sqlite3` shell client and do everything I need. So far, so good I would say ;-) 416 | * **Isn't `params` and `fields` an ambiguous choice?** At the very beginning I wasn't sure myself if that would have worked as API choice but later on I've changed my mind. First of all, it's very easy to spot special chars in the `SQL` statement. If present, params is mandatory and used, as easy as that. Secondly, if an object has functions as value, it's obviously a `fields` object, 'cause `params` cannot contains functions since these are not compatible with `JSON` serialization, neither meaningful for the database. The only case where `fields` might be confused with `params` is when no `params` has been specified, and `fields` is an `Array`. In this case I believe you are the same one that wrote the SQL too and know upfront if there are fields to retrieve from `params` or not so this is actually a *non real-world* problem and as soon as you try this API you'll realize it feels intuitive and right. 417 | * **Are Transactions Supported?** ... **YES**, transactions are supported simply performing multiple queries as you would do in *sqlite3* shell: 418 | ```javascript 419 | db.query('BEGIN TRANSACTION'); 420 | for(var i = 0; i < 100; i++) { 421 | db.query('INSERT INTO table VALUES(?, ?)', [null, Math.random()]); 422 | } 423 | db.query('COMMIT'); 424 | ``` 425 | The test file has a transaction with 100 records in it, [have a look](test/dblite.js). 426 | * **Can I Connect To A `:memory:` Database?** well, you can do anything you would do with `sqlite3` shell so **YES** 427 | ```javascript 428 | var db = dblite(':memory:'); // that's it! 429 | ``` 430 | 431 | ### License 432 | The usual Mit Style, thinking about the [WTFPL](http://en.wikipedia.org/wiki/WTFPL) though ... stay tuned for updates. 433 | 434 | Copyright (C) 2013 by WebReflection 435 | 436 | Permission is hereby granted, free of charge, to any person obtaining a copy 437 | of this software and associated documentation files (the "Software"), to deal 438 | in the Software without restriction, including without limitation the rights 439 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 440 | copies of the Software, and to permit persons to whom the Software is 441 | furnished to do so, subject to the following conditions: 442 | 443 | The above copyright notice and this permission notice shall be included in 444 | all copies or substantial portions of the Software. 445 | 446 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 447 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 448 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 449 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 450 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 451 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 452 | THE SOFTWARE. 453 | -------------------------------------------------------------------------------- /benchmark/dblite.js: -------------------------------------------------------------------------------- 1 | var startTime = Date.now(), 2 | db = require('../build/dblite.node.js')('./bench.dblite.db'); 3 | // db = require('../build/dblite.node.js')(':memory:'); 4 | 5 | db.query('BEGIN'); 6 | db.query('CREATE TABLE IF NOT EXISTS users_login (id INTEGER PRIMARY KEY, name TEXT, pass TEXT)'); 7 | db.query('CREATE TABLE IF NOT EXISTS users_info (id INTEGER PRIMARY KEY, email TEXT, birthday INTEGER)'); 8 | for (var i = 0; i < 1000; i++) { 9 | db.query('INSERT INTO users_login VALUES (:id, :name, :pass)', { 10 | id: null, 11 | name: 'user_' + i, 12 | pass: ('pass_' + i + '_' + Math.random()).slice(0, 11) 13 | }); 14 | } 15 | db.query('COMMIT'); 16 | 17 | db.query('SELECT * FROM users_login', function (rows) { 18 | var total = rows.length, 19 | lastValidRow; 20 | db.query('BEGIN'); 21 | rows.forEach(function (row) { 22 | db.query( 23 | this, { 24 | id: row[0], 25 | email: 'user_' + row[0] + '@email.com', 26 | bday: parseInt(Math.random() * startTime) 27 | } 28 | ); 29 | },[ 30 | 'REPLACE INTO users_info (id, email, birthday)', 31 | 'VALUES (', 32 | ':id,', 33 | 'COALESCE((SELECT users_info.email FROM users_info WHERE id = :id), :email),', 34 | 'COALESCE((SELECT users_info.birthday FROM users_info WHERE id = :id), :bday)', 35 | ')' 36 | ].join(' ') 37 | ); 38 | db.query('COMMIT'); 39 | 40 | function onUserInfo(rows) { 41 | if (rows.length) { 42 | found++; 43 | lastValidRow = rows[0]; 44 | } 45 | if (!--i) { 46 | db.on('close', function () { 47 | if (process.argv[2] == 1) { 48 | console.log('completed in ' + ((Date.now() - startTime) / 1000) + ' seconds'); 49 | console.log('found ' + found + ' random matches out of ' + total + ' possibilities'); 50 | if (found) { 51 | console.log('last row looked like this'); 52 | console.log(lastValidRow); 53 | } 54 | } 55 | }).close(); 56 | } 57 | } 58 | 59 | for (var found = 0, i = 0; i < 100; i++) { 60 | db.query([ 61 | 'SELECT users_login.name, users_info.email, users_info.birthday', 62 | 'FROM users_login', 63 | 'LEFT JOIN users_info', 64 | 'ON (users_login.id = users_info.id)', 65 | 'WHERE users_login.name = :name AND users_login.pass = :pass' 66 | ].join(' '), 67 | { 68 | name: 'user_' + i, 69 | pass: ('pass_' + i + '_' + Math.random()).slice(0, 11) 70 | }, 71 | { 72 | name: String, 73 | email: String, 74 | bday: Date 75 | }, 76 | onUserInfo 77 | ); 78 | } 79 | 80 | }); -------------------------------------------------------------------------------- /benchmark/sqlite3.js: -------------------------------------------------------------------------------- 1 | var startTime = Date.now(), 2 | sqlite3 = require('sqlite3'), 3 | db = new sqlite3.Database('./bench.sqlite3.db'); 4 | //db = new sqlite3.Database(':memory:'); 5 | 6 | db.serialize(function() { 7 | db.run('BEGIN'); 8 | db.run('CREATE TABLE IF NOT EXISTS users_login (id INTEGER PRIMARY KEY, name TEXT, pass TEXT)'); 9 | db.run('CREATE TABLE IF NOT EXISTS users_info (id INTEGER PRIMARY KEY, email TEXT, birthday INTEGER)'); 10 | stmt = db.prepare("INSERT INTO users_login VALUES (:id, :name, :pass)"); 11 | for (var stmt, i = 0; i < 1000; i++) { 12 | stmt.run({ 13 | ':id': null, 14 | ':name': 'user_' + i, 15 | ':pass': ('pass_' + i + '_' + Math.random()).slice(0, 11) 16 | }); 17 | } 18 | stmt.finalize(); 19 | db.run('COMMIT', function () { 20 | db.all('SELECT * FROM users_login', function (err, rows) { 21 | var total = rows.length; 22 | db.serialize(function() { 23 | db.run('BEGIN'); 24 | var stmt = db.prepare([ 25 | 'REPLACE INTO users_info (id, email, birthday)', 26 | 'VALUES (', 27 | ':id,', 28 | 'COALESCE((SELECT users_info.email FROM users_info WHERE id = :id), :email),', 29 | 'COALESCE((SELECT users_info.birthday FROM users_info WHERE id = :id), :bday)', 30 | ')' 31 | ].join(' ') 32 | ); 33 | rows.forEach(function (row) { 34 | stmt.run( 35 | { 36 | ':id': row.id, 37 | ':email': 'user_' + row.id + '@email.com', 38 | ':bday': parseInt(Math.random() * startTime) 39 | } 40 | ); 41 | }); 42 | stmt.finalize(); 43 | db.run('COMMIT', function () { 44 | function onUserInfo(err, row) { 45 | if (!--i) { 46 | if (process.argv[2] == 1) { 47 | console.log('completed in ' + ((Date.now() - startTime) / 1000) + ' seconds'); 48 | console.log('found ' + found + ' random matches out of ' + total + ' possibilities'); 49 | if (found) { 50 | console.log('last row looked like this'); 51 | console.log(lastValidRow); 52 | } 53 | } 54 | db.close(); 55 | } 56 | } 57 | 58 | function onRun(err, row) { 59 | // I am not even adding validation ... 60 | if (row) { 61 | found++; 62 | lastValidRow = row; 63 | row.bday = new Date(row.birthday); 64 | delete row.birthday; 65 | } 66 | } 67 | 68 | for (var found = 0, i = 0; i < 100; i++) { 69 | db.each([ 70 | 'SELECT users_login.name, users_info.email, users_info.birthday', 71 | 'FROM users_login', 72 | 'LEFT JOIN users_info', 73 | 'ON (users_login.id = users_info.id)', 74 | 'WHERE users_login.name = :name AND users_login.pass = :pass' 75 | ].join(' '), 76 | { 77 | ':name': 'user_' + i, 78 | ':pass': ('pass_' + i + '_' + Math.random()).slice(0, 11) 79 | }, 80 | onRun, 81 | onUserInfo 82 | ); 83 | } 84 | }); 85 | }); 86 | }); 87 | }); 88 | 89 | }); -------------------------------------------------------------------------------- /build/dblite.node.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (C) 2013 - present by WebReflection 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | */ 23 | /*! 24 | Copyright (C) 2015 by WebReflection 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining a copy 27 | of this software and associated documentation files (the "Software"), to deal 28 | in the Software without restriction, including without limitation the rights 29 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 30 | copies of the Software, and to permit persons to whom the Software is 31 | furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in 34 | all copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 38 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 39 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 40 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 41 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 42 | THE SOFTWARE. 43 | 44 | */ 45 | /*! a zero hassle wrapper for sqlite by Andrea Giammarchi !*/ 46 | var 47 | isArray = Array.isArray, 48 | // used to generate unique "end of the query" identifiers 49 | crypto = require('crypto'), 50 | // relative, absolute, and db paths are normalized anyway 51 | path = require('path'), 52 | // each dblite(fileName) instance is an EventEmitter 53 | EventEmitter = require('events').EventEmitter, 54 | // used to perform some fallback 55 | WIN32 = process.platform === 'win32', 56 | // what kind of Path Separator we have here ? 57 | PATH_SEP = path.sep || ( 58 | WIN32 ? '\\' : '/' 59 | ), 60 | // each dblite instance spawns a process once 61 | // and interact with that shell for the whole session 62 | // one spawn per database and no more (on avg 1 db is it) 63 | spawn = require('child_process').spawn, 64 | // use to re-generate Date objects 65 | DECIMAL = /^[1-9][0-9]*$/, 66 | // verify if it's a select or not 67 | SELECT = /^\s*(?:select|SELECT|pragma|PRAGMA|with|WITH) /, 68 | // for simple query replacements: WHERE field = ? 69 | REPLACE_QUESTIONMARKS = /\?/g, 70 | // for named replacements: WHERE field = :data 71 | REPLACE_PARAMS = /(?:\:|\@|\$)([a-zA-Z0-9_$]+)/g, 72 | // the way CSV threats double quotes 73 | DOUBLE_DOUBLE_QUOTES = /""/g, 74 | // to escape strings 75 | SINGLE_QUOTES = /'/g, 76 | // to use same escaping logic for double quotes 77 | // except it makes escaping easier for JSON data 78 | // which usually is full of " 79 | SINGLE_QUOTES_DOUBLED = "''", 80 | // to verify there are named fields/parametes 81 | HAS_PARAMS = /(?:\?|(?:(?:\:|\@|\$)[a-zA-Z0-9_$]+))/, 82 | // shortcut used as deafault notifier 83 | log = console.log.bind(console), 84 | // the default binary as array of paths 85 | bin = ['sqlite3'], 86 | // private shared variables 87 | // avoid creation of N functions 88 | // keeps memory low and improves performance 89 | paramsIndex, // which index is the current 90 | paramsArray, // which value when Array 91 | paramsObject, // which value when Object (named parameters) 92 | IS_NODE_06 = false, // dirty things to do there ... 93 | // defned later on 94 | EOL, EOL_LENGTH, 95 | SANITIZER, SANITIZER_REPLACER, 96 | waitForEOLToBeDefined = [], 97 | createProgram = function () { 98 | for (var args = [], i = 0; i < arguments.length; i++) { 99 | args[i] = arguments[i]; 100 | } 101 | return spawn( 102 | // executable only, folder needs to be specified a part 103 | bin.length === 1 ? bin[0] : ('.' + PATH_SEP + bin[bin.length - 1]), 104 | // normalize file path if not :memory: 105 | normalizeFirstArgument( 106 | // it is possible to eventually send extra sqlite3 args 107 | // so all arguments are passed 108 | args 109 | ).concat('-csv') // but the output MUST be csv 110 | .reverse(), // see https://github.com/WebReflection/dblite/pull/12 111 | // be sure the dir is the right one 112 | { 113 | // the right folder is important or sqlite3 won't work 114 | cwd: bin.slice(0, -1).join(PATH_SEP) || process.cwd(), 115 | env: process.env, // same env is OK 116 | encoding: 'utf8', // utf8 is OK 117 | stdio: ['pipe', 'pipe', 'pipe'] // handled here 118 | } 119 | ); 120 | }, 121 | defineCSVEOL = function (fn) { 122 | 123 | if (waitForEOLToBeDefined.push(fn) === 1) { 124 | 125 | var 126 | program = createProgram(':memory:', '-noheader'), 127 | ondata = function (data) { 128 | setUpAndGo(data.toString().slice(1)); 129 | }, 130 | // save the Mac, SAVE THE MAC!!! 131 | // error could occur due \r\n 132 | // so clearly that won't be the right EOL 133 | onerror = function () { 134 | setUpAndGo('\n'); 135 | }, 136 | setUpAndGo = function (eol) { 137 | 138 | // habemus EOL \o/ 139 | EOL = eol; 140 | 141 | // what's EOL length? Used to properly parse data 142 | EOL_LENGTH = EOL.length; 143 | 144 | // makes EOL safe for strings passed to the shell 145 | SANITIZER = new RegExp("[;" + EOL.split('').map(function(c) { 146 | return '\\x' + ('0' + c.charCodeAt(0).toString(16)).slice(-2); 147 | }).join('') + "]+$"); 148 | 149 | // used to mark the end of each line passed to the shell 150 | SANITIZER_REPLACER = ';' + EOL; 151 | 152 | // once closed, reassign this helper 153 | // and trigger all queued functions 154 | program.on('close', function () { 155 | defineCSVEOL = function (fn) { fn(); }; 156 | waitForEOLToBeDefined 157 | .splice(0, waitForEOLToBeDefined.length) 158 | .forEach(defineCSVEOL); 159 | }); 160 | 161 | program.kill(); 162 | } 163 | ; 164 | 165 | program.stdout.on('data', ondata); 166 | program.stdin.on('error', onerror); 167 | program.stdout.on('error', onerror); 168 | program.stderr.on('error', onerror); 169 | 170 | program.stdin.write('SELECT 1;\r\n'); 171 | 172 | } 173 | 174 | } 175 | ; 176 | 177 | /** 178 | * var db = dblite('filename.sqlite'):EventEmitter; 179 | * 180 | * db.query( thismethod has **many** overloads where almost everything is optional 181 | * 182 | * SQL:string, only necessary field. Accepts a query or a command such `.databases` 183 | * 184 | * params:Array|Object, optional, if specified replaces SQL parts with this object 185 | * db.query('INSERT INTO table VALUES(?, ?)', [null, 'content']); 186 | * db.query('INSERT INTO table VALUES(:id, :value)', {id:null, value:'content'}); 187 | * 188 | * fields:Array|Object, optional, if specified is used to normalize the query result with named fields. 189 | * db.query('SELECT table.a, table.other FROM table', ['a', 'b']); 190 | * [{a:'first value', b:'second'},{a:'row2 value', b:'row2'}] 191 | * 192 | * 193 | * db.query('SELECT table.a, table.other FROM table', ['a', 'b']); 194 | * [{a:'first value', b:'second'},{a:'row2 value', b:'row2'}] 195 | * callback:Function 196 | * ); 197 | */ 198 | function dblite() { 199 | var 200 | args = arguments, 201 | // the current dblite "instance" 202 | self = new EventEmitter(), 203 | // the incrementally concatenated buffer 204 | // cleaned up as soon as the current command has been completed 205 | selectResult = '', 206 | // sqlite3 shell can produce one output per time 207 | // every operation performed through this wrapper 208 | // should not bother the program until next 209 | // available slot. This queue helps keeping 210 | // requests ordered without stressing the system 211 | // once things will be ready, callbacks will be notified 212 | // accordingly. As simple as that ^_^ 213 | queue = [], 214 | // set as true only once db.close() has been called 215 | notWorking = false, 216 | // marks the shell busy or not 217 | // initially we need to wait for the EOL 218 | // so it's busy by default 219 | busy = true, 220 | // tells if current output needs to be processed 221 | wasSelect = false, 222 | wasNotSelect = false, 223 | wasError = false, 224 | // forces the output not to be processed 225 | // might be handy in some case where it's passed around 226 | // as string instread of needing to serialize/unserialize 227 | // the list of already arrays or objects 228 | dontParseCSV = false, 229 | // one callback per time will be notified 230 | $callback, 231 | // recycled variable for fields operation 232 | $fields, 233 | // this will be the delimiter of each sqlite3 shell command 234 | SUPER_SECRET, 235 | SUPER_SECRET_SELECT, 236 | SUPER_SECRET_LENGTH, 237 | // the spawned program 238 | program 239 | ; 240 | 241 | defineCSVEOL(function () { 242 | 243 | // this is the delimiter of each sqlite3 shell command 244 | SUPER_SECRET = '---' + 245 | crypto.randomBytes(64).toString('base64') + 246 | '---'; 247 | // ... I wish .print was introduced before SQLite 3.7.10 ... 248 | // this is a weird way to get rid of the header, if enabled 249 | SUPER_SECRET_SELECT = '"' + SUPER_SECRET + '" AS "' + SUPER_SECRET + '";' + EOL; 250 | // used to check the end of a buffer 251 | SUPER_SECRET_LENGTH = -(SUPER_SECRET.length + EOL_LENGTH); 252 | // add the EOL to the secret 253 | SUPER_SECRET += EOL; 254 | 255 | // define the spawn program 256 | program = createProgram.apply(null, args); 257 | 258 | // and all its IO handled here 259 | program.stderr.on('data', onerror); 260 | program.stdin.on('error', onerror); 261 | program.stdout.on('error', onerror); 262 | program.stderr.on('error', onerror); 263 | 264 | // invoked each time the sqlite3 shell produces an output 265 | program.stdout.on('data', function (data) { 266 | /*jshint eqnull: true*/ 267 | // big output might require more than a call 268 | var str, result, callback, fields, headers, wasSelectLocal, rows, dpcsv; 269 | if (wasError) { 270 | selectResult = ''; 271 | wasError = false; 272 | if (self.ignoreErrors) { 273 | busy = false; 274 | next(); 275 | } 276 | return; 277 | } 278 | // the whole output is converted into a string here 279 | selectResult += data; 280 | // if the end of the output is the serapator 281 | if (selectResult.slice(SUPER_SECRET_LENGTH) === SUPER_SECRET) { 282 | // time to move forward since sqlite3 has done 283 | str = selectResult.slice(0, SUPER_SECRET_LENGTH); 284 | // drop the secret header if present 285 | headers = str.slice(SUPER_SECRET_LENGTH) === SUPER_SECRET; 286 | if (headers) str = str.slice(0, SUPER_SECRET_LENGTH); 287 | // clean up the outer variabls 288 | selectResult = ''; 289 | // makes the spawned program not busy anymore 290 | busy = false; 291 | // if it was a select 292 | if (wasSelect || wasNotSelect) { 293 | wasSelectLocal = wasSelect; 294 | dpcsv = dontParseCSV; 295 | // set as false all conditions 296 | // only here dontParseCSV could have been true 297 | // set to false that too 298 | wasSelect = wasNotSelect = dontParseCSV = busy; 299 | // which callback should be invoked? 300 | // last expected one for this round 301 | callback = $callback; 302 | // same as fields 303 | fields = $fields; 304 | // parse only if it was a select/pragma 305 | if (wasSelectLocal) { 306 | // unless specified, process the string 307 | // converting the CSV into an Array of rows 308 | result = dpcsv ? str : parseCSV(str); 309 | // if there were headers/fields and we have a result ... 310 | if (headers && isArray(result) && result.length) { 311 | // ... and fields is not defined 312 | if (fields == null) { 313 | // fields is the row 0 314 | fields = result[0]; 315 | } else if(!isArray(fields)) { 316 | // per each non present key, enrich the fields object 317 | // it is then possible to have automatic headers 318 | // with some known field validated/parsed 319 | // e.g. {id:Number} will be {id:Number, value:String} 320 | // if the query was SELECT id, value FROM table 321 | // and the fields object was just {id:Number} 322 | // but headers were active 323 | result[0].forEach(enrichFields, fields); 324 | } 325 | // drop the first row with headers 326 | result.shift(); 327 | } 328 | } 329 | // but next query, should not have 330 | // previously set callbacks or fields 331 | $callback = $fields = null; 332 | // the spawned program can start a new job without current callback 333 | // being able to push another job as soon as executed. This makes 334 | // the queue fair for everyone without granting priority to anyone. 335 | next(); 336 | // if there was actually a callback to call 337 | if (callback) { 338 | rows = fields ? ( 339 | // and if there was a need to parse each row 340 | isArray(fields) ? 341 | // as object with properties 342 | result.map(row2object, fields) : 343 | // or object with validated properties 344 | result.map(row2parsed, parseFields(fields)) 345 | ) : 346 | // go for it ... otherwise returns the result as it is: 347 | // an Array of Arrays 348 | result 349 | ; 350 | // if there was an error signature 351 | if (1 < callback.length) { 352 | callback.call(self, null, rows); 353 | } else { 354 | // invoke it with the db object as context 355 | callback.call(self, rows); 356 | } 357 | } 358 | } else { 359 | // not a select, just a special command 360 | // such .databases or .tables 361 | next(); 362 | // if there is something to notify 363 | if (str.length) { 364 | // and if there was an 'info' listener 365 | if (self.listeners('info').length) { 366 | // notify 367 | self.emit('info', EOL + str); 368 | } else { 369 | // otherwise log 370 | log(EOL + str); 371 | } 372 | } 373 | } 374 | } 375 | }); 376 | 377 | // detach the program from this one 378 | // node 0.6 has not unref 379 | if (program.unref) { 380 | program.on('close', close); 381 | } else { 382 | IS_NODE_06 = true; 383 | program.stdout.on('close', close); 384 | } 385 | 386 | // let's begin 387 | busy = false; 388 | next(); 389 | 390 | }); 391 | 392 | // when program is killed or closed for some reason 393 | // the dblite object needs to be notified too 394 | function close(code) { 395 | if (self.listeners('close').length) { 396 | self.emit('close', code); 397 | if (program.unref) 398 | program.unref(); 399 | } else { 400 | log('bye bye'); 401 | } 402 | } 403 | 404 | // as long as there's something else to do ... 405 | function next() { 406 | if (queue.length) { 407 | // ... do that and wait for next check 408 | self.query.apply(self, queue.shift()); 409 | } 410 | } 411 | 412 | // common error helper 413 | function onerror(data) { 414 | if($callback && 1 < $callback.length) { 415 | // there is a callback waiting 416 | // and there is more than an argument in there 417 | // the callback is waiting for errors too 418 | var callback = $callback; 419 | wasSelect = wasNotSelect = dontParseCSV = false; 420 | $callback = $fields = null; 421 | wasError = true; 422 | // should the next be called ? next(); 423 | callback.call(self, new Error(data.toString()), null); 424 | } else if(self.listeners('error').length) { 425 | // notify listeners 426 | self.emit('error', '' + data); 427 | } else { 428 | // log the output avoiding exit 1 429 | // if no listener was added 430 | console.error('' + data); 431 | } 432 | } 433 | 434 | // WARNING: this can be very unsafe !!! 435 | // if there is an error and this 436 | // property is explicitly set to false 437 | // it keeps running queries no matter what 438 | self.ignoreErrors = false; 439 | // - - - - - - - - - - - - - - - - - - - - 440 | 441 | // safely closes the process 442 | // will emit 'close' once done 443 | self.close = function() { 444 | // close can happen only once 445 | if (!notWorking) { 446 | // this should gently terminate the program 447 | // only once everything scheduled has been completed 448 | self.query('.exit'); 449 | notWorking = true; 450 | // the hardly killed version was like this: 451 | // program.stdin.end(); 452 | // program.kill(); 453 | } 454 | }; 455 | 456 | // SELECT last_insert_rowid() FROM table might not work as expected 457 | // This method makes the operation atomic and reliable 458 | self.lastRowID = function(table, callback) { 459 | self.query( 460 | 'SELECT ROWID FROM `' + table + '` ORDER BY ROWID DESC LIMIT 1', 461 | function(result){ 462 | var row = result[0], k; 463 | // if headers are switched on 464 | if (!(row instanceof Array)) { 465 | for (k in row) { 466 | if (row.hasOwnProperty(k)) { 467 | row = [row[k]]; 468 | break; 469 | } 470 | } 471 | } 472 | (callback || log).call(self, row[0]); 473 | } 474 | ); 475 | return self; 476 | }; 477 | 478 | // Handy if for some reason data has to be passed around 479 | // as string instead of being serialized and deserialized 480 | // as Array of Arrays. Don't use if not needed. 481 | self.plain = function() { 482 | dontParseCSV = true; 483 | return self.query.apply(self, arguments); 484 | }; 485 | 486 | // main logic/method/entry point 487 | self.query = function(string, params, fields, callback) { 488 | // recognizes sql-template-strings objects 489 | if (typeof string === 'object' && 'sql' in string && 'values' in string) { 490 | switch (arguments.length) { 491 | case 3: return self.query(string.sql, string.values, params, fields); 492 | case 2: return self.query(string.sql, string.values, params); 493 | } 494 | return self.query(string.sql, string.values); 495 | } 496 | if ( 497 | // notWorking is set once .close() has been called 498 | // it does not make sense to execute anything after 499 | // the program is being closed 500 | notWorking && 501 | // however, since at that time the program could also be busy 502 | // let's be sure than this is either the exit call 503 | // or that the last call is still the exit one 504 | !(string === '.exit' || queue[queue.length - 1][0] === '.exit') 505 | ) return onerror('closing'), self; 506 | // if something is still going on in the sqlite3 shell 507 | // the progcess is flagged as busy. Just queue other operations 508 | if (busy) return queue.push(arguments), self; 509 | // if a SELECT or a PRAGMA ... 510 | wasSelect = SELECT.test(string); 511 | if (wasSelect) { 512 | // SELECT and PRAGMA makes `dblite` busy 513 | busy = true; 514 | switch(arguments.length) { 515 | // all arguments passed, nothing to do 516 | case 4: 517 | $callback = callback; 518 | $fields = fields; 519 | string = replaceString(string, params); 520 | break; 521 | // 3 arguments passed ... 522 | case 3: 523 | // is the last one the callback ? 524 | if (typeof fields == 'function') { 525 | // assign it 526 | $callback = fields; 527 | // has string parameters to repalce 528 | // such ? or :id and others ? 529 | if (HAS_PARAMS.test(string)) { 530 | // no objectification and/or validation needed 531 | $fields = null; 532 | // string replaced wit parameters 533 | string = replaceString(string, params); 534 | } else { 535 | // no replacement in the SQL needed 536 | // objectification with validation 537 | // if specified, will manage the result 538 | $fields = params; 539 | } 540 | } else { 541 | // no callback specified at all, probably in "dev mode" 542 | $callback = log; // just log the result 543 | $fields = fields; // use objectification 544 | string = replaceString(string, params); // replace parameters 545 | } 546 | break; 547 | // in this case ... 548 | case 2: 549 | // simple query with a callback 550 | if (typeof params == 'function') { 551 | // no objectification 552 | $fields = null; 553 | // callback is params argument 554 | $callback = params; 555 | } else { 556 | // "dev mode", just log 557 | $callback = log; 558 | // if there's something to replace 559 | if (HAS_PARAMS.test(string)) { 560 | // no objectification 561 | $fields = null; 562 | string = replaceString(string, params); 563 | } else { 564 | // nothing to replace 565 | // objectification with eventual validation 566 | $fields = params; 567 | } 568 | } 569 | break; 570 | default: 571 | // 1 argument, the SQL string and nothing else 572 | // "dev mode" log will do 573 | $callback = log; 574 | $fields = null; 575 | break; 576 | } 577 | // ask the sqlite3 shell ... 578 | program.stdin.write( 579 | // trick to always know when the console is not busy anymore 580 | // specially for those cases where no result is shown 581 | sanitize(string) + 'SELECT ' + SUPER_SECRET_SELECT 582 | ); 583 | } else { 584 | // if db.plain() was used but this is not a SELECT or PRAGMA 585 | // something is wrong with the logic since no result 586 | // was expected anyhow 587 | if (dontParseCSV) { 588 | dontParseCSV = false; 589 | throw new Error('not a select'); 590 | } else if (string[0] === '.') { 591 | // .commands are special queries .. so 592 | // .commands make `dblite` busy 593 | busy = true; 594 | // same trick with the secret to emit('info', resultAsString) 595 | // once everything is done 596 | program.stdin.write(string + EOL + 'SELECT ' + SUPER_SECRET_SELECT); 597 | } else { 598 | switch(arguments.length) { 599 | case 1: 600 | /* falls through */ 601 | case 2: 602 | if (typeof params !== 'function') { 603 | // no need to make the shell busy 604 | // since no output is shown at all (errors ... eventually) 605 | // sqlite3 shell will take care of the order 606 | // same as writing in a linux shell while something else is going on 607 | // who cares, will show when possible, after current job ^_^ 608 | program.stdin.write(sanitize(HAS_PARAMS.test(string) ? 609 | replaceString(string, params) : 610 | string 611 | )); 612 | // keep checking for possible following operations 613 | process.nextTick(next); 614 | break; 615 | } 616 | fields = params; 617 | // not necessary but guards possible wrong replaceString 618 | params = null; 619 | /* falls through */ 620 | case 3: 621 | // execute a non SELECT/PRAGMA statement 622 | // and be notified once it's done. 623 | // set state as busy 624 | busy = wasNotSelect = true; 625 | $callback = fields; 626 | program.stdin.write( 627 | (sanitize( 628 | HAS_PARAMS.test(string) ? 629 | replaceString(string, params) : 630 | string 631 | )) + 632 | EOL + 'SELECT ' + SUPER_SECRET_SELECT 633 | ); 634 | } 635 | } 636 | } 637 | // chainability just useful here for multiple queries at once 638 | return self; 639 | }; 640 | return self; 641 | } 642 | 643 | // enrich possible fields object with extra headers 644 | function enrichFields(key) { 645 | var had = this.hasOwnProperty(key), 646 | callback = had && this[key]; 647 | delete this[key]; 648 | this[key] = had ? callback : String; 649 | } 650 | 651 | // if not a memory database 652 | // the file path should be resolved as absolute 653 | function normalizeFirstArgument(args) { 654 | var file = args[0]; 655 | if (file !== ':memory:') { 656 | args[0] = path.resolve(args[0]); 657 | } 658 | return args.indexOf('-header') < 0 ? args.concat('-noheader') : args; 659 | } 660 | 661 | // assuming generated CSV is always like 662 | // 1,what,everEOL 663 | // with double quotes when necessary 664 | // 2,"what's up",everEOL 665 | // this parser works like a charm 666 | function parseCSV(output) { 667 | /*jshint eqnull: true*/ 668 | if (EOL == null) throw new Error( 669 | 'SQLite EOL not found. Please connect to a database first' 670 | ); 671 | for(var 672 | fields = [], 673 | rows = [], 674 | index = 0, 675 | rindex = 0, 676 | length = output.length, 677 | i = 0, 678 | j, loop, 679 | current, 680 | endLine, 681 | iNext, 682 | str; 683 | i < length; i++ 684 | ) { 685 | switch(output[i]) { 686 | case '"': 687 | loop = true; 688 | j = i; 689 | do { 690 | iNext = output.indexOf('"', current = j + 1); 691 | switch(output[j = iNext + 1]) { 692 | case EOL[0]: 693 | if (EOL_LENGTH === 2 && output[j + 1] !== EOL[1]) { 694 | break; 695 | } 696 | /* falls through */ 697 | case ',': 698 | loop = false; 699 | } 700 | } while(loop); 701 | str = output.slice(i + 1, iNext++).replace(DOUBLE_DOUBLE_QUOTES, '"'); 702 | break; 703 | default: 704 | iNext = output.indexOf(',', i); 705 | endLine = output.indexOf(EOL, i); 706 | if (iNext < 0) iNext = length - EOL_LENGTH; 707 | str = output.slice(i, endLine < iNext ? (iNext = endLine) : iNext); 708 | break; 709 | } 710 | fields[index++] = str; 711 | if (output[i = iNext] === EOL[0] && ( 712 | EOL_LENGTH === 1 || ( 713 | output[i + 1] === EOL[1] && ++i 714 | ) 715 | ) 716 | ) { 717 | rows[rindex++] = fields; 718 | fields = []; 719 | index = 0; 720 | } 721 | } 722 | return rows; 723 | } 724 | 725 | // create an object with right validation 726 | // and right fields to simplify the parsing 727 | // NOTE: this is based on ordered key 728 | // which is not specified by old ES specs 729 | // but it works like this in V8 730 | function parseFields($fields) { 731 | for (var 732 | current, 733 | fields = Object.keys($fields), 734 | parsers = [], 735 | length = fields.length, 736 | i = 0; i < length; i++ 737 | ) { 738 | current = $fields[fields[i]]; 739 | parsers[i] = current === Boolean ? 740 | $Boolean : ( 741 | current === Date ? 742 | $Date : 743 | current || String 744 | ) 745 | ; 746 | } 747 | return {f: fields, p: parsers}; 748 | } 749 | 750 | // transform SQL strings using parameters 751 | function replaceString(string, params) { 752 | // if params is an array 753 | if (isArray(params)) { 754 | // replace all ? occurence ? with right 755 | // incremental params[index++] 756 | paramsIndex = 0; 757 | paramsArray = params; 758 | string = string.replace(REPLACE_QUESTIONMARKS, replaceQuestions); 759 | } else { 760 | // replace :all @fields with the right 761 | // object.all or object.fields occurrences 762 | paramsObject = params; 763 | string = string.replace(REPLACE_PARAMS, replaceParams); 764 | } 765 | paramsArray = paramsObject = null; 766 | return string; 767 | } 768 | 769 | // escape the property found in the SQL 770 | function replaceParams(match, key) { 771 | return escape(paramsObject[key]); 772 | } 773 | 774 | // escape the value found for that ? in the SQL 775 | function replaceQuestions() { 776 | return escape(paramsArray[paramsIndex++]); 777 | } 778 | 779 | // objectification: makes an Array an object 780 | // assuming the context is an array of ordered fields 781 | function row2object(row) { 782 | for (var 783 | out = {}, 784 | length = this.length, 785 | i = 0; i < length; i++ 786 | ) { 787 | out[this[i]] = row[i]; 788 | } 789 | return out; 790 | } 791 | 792 | // objectification with validation: 793 | // makes an Array a validated object 794 | // assuming the context is an object 795 | // produced via parseFields() function 796 | function row2parsed(row) { 797 | for (var 798 | out = {}, 799 | fields = this.f, 800 | parsers = this.p, 801 | length = fields.length, 802 | i = 0; i < length; i++ 803 | ) { 804 | out[fields[i]] = parsers[i](row[i]); 805 | } 806 | return out; 807 | } 808 | 809 | // escape in a smart way generic values 810 | // making them compatible with SQLite types 811 | // or useful for JavaScript once retrieved back 812 | function escape(what) { 813 | /*jshint eqnull: true*/ 814 | if (EOL == null) throw new Error( 815 | 'SQLite EOL not found. Please connect to a database first' 816 | ); 817 | switch (typeof what) { 818 | case 'string': 819 | return "'" + what.replace( 820 | SINGLE_QUOTES, SINGLE_QUOTES_DOUBLED 821 | ) + "'"; 822 | case 'undefined': 823 | what = null; 824 | /* falls through */ 825 | case 'object': 826 | return what == null ? 827 | 'null' : 828 | ("'" + JSON.stringify(what).replace( 829 | SINGLE_QUOTES, SINGLE_QUOTES_DOUBLED 830 | ) + "'") 831 | ; 832 | // SQLite has no Boolean type 833 | case 'boolean': 834 | return what ? '1' : '0'; // 1 => true, 0 => false 835 | case 'number': 836 | // only finite numbers can be stored 837 | if (isFinite(what)) return '' + what; 838 | } 839 | // all other cases 840 | throw new Error('unsupported data'); 841 | } 842 | 843 | // makes an SQL statement OK for dblite <=> sqlite communications 844 | function sanitize(string) { 845 | return string.replace(SANITIZER, '') + SANITIZER_REPLACER; 846 | } 847 | 848 | // no Boolean type in SQLite 849 | // this will replace the possible Boolean validator 850 | // returning the right expected value 851 | function $Boolean(field) { 852 | switch(field.toLowerCase()) { 853 | case '0': 854 | case 'false': 855 | case 'null': 856 | case '': 857 | return false; 858 | } 859 | return true; 860 | } 861 | 862 | // no Date in SQLite, this will 863 | // take care of validating/creating Dates 864 | // when the field is retrieved with a Date validator 865 | function $Date(field) { 866 | return new Date( 867 | DECIMAL.test(field) ? parseInt(field, 10) : field 868 | ); 869 | } 870 | 871 | // which sqlite3 executable ? 872 | // it is possible to specify a different 873 | // sqlite3 executable even in relative paths 874 | // be sure the file exists and is usable as executable 875 | Object.defineProperty( 876 | dblite, 877 | 'bin', 878 | { 879 | get: function () { 880 | // normalized string if was a path 881 | return bin.join(PATH_SEP); 882 | }, 883 | set: function (value) { 884 | var isPath = -1 < value.indexOf(PATH_SEP); 885 | if (isPath) { 886 | // resolve the path 887 | value = path.resolve(value); 888 | // verify it exists 889 | if (!require(IS_NODE_06 ? 'path' : 'fs').existsSync(value)) { 890 | throw 'invalid executable: ' + value; 891 | } 892 | } 893 | // assign as Array in any case 894 | bin = value.split(PATH_SEP); 895 | } 896 | } 897 | ); 898 | 899 | // starting from v0.6.0 sqlite version shuold be specified 900 | // specially if SQLite version is 3.8.6 or greater 901 | // var dblite = require('dblite').withSQLite('3.8.6') 902 | dblite.withSQLite = function (sqliteVersion) { 903 | dblite.sqliteVersion = sqliteVersion; 904 | return dblite; 905 | }; 906 | 907 | // to manually parse CSV data if necessary 908 | // mainly to be able to use db.plain(SQL) 909 | // without parsing it right away and pass the string 910 | // around instead of serializing and de-serializing it 911 | // all the time. Ideally this is a scenario for clusters 912 | // no need to usually do manually anything otherwise. 913 | dblite.parseCSV = parseCSV; 914 | 915 | // how to manually escape data 916 | // might be handy to write directly SQL strings 917 | // instead of using handy paramters Array/Object 918 | // usually you don't want to do this 919 | dblite.escape = escape; 920 | 921 | // helps writing queries 922 | dblite.SQL = (function () { 923 | 'use strict'; 924 | 925 | const lsp = str => (/^[\s\n\r]/.test(str) ? str : ' ' + str); 926 | 927 | const push = (str, val, statement, spaced) => { 928 | const {strings, values} = statement; 929 | str[str.length - 1] += spaced ? lsp(strings[0]) : strings[0]; 930 | str.push(...strings.slice(1)); 931 | val.push(...values); 932 | }; 933 | 934 | class SQLStatement { 935 | constructor(strings, values) { 936 | this.strings = strings; 937 | this.values = values; 938 | } 939 | append(statement) { 940 | const {strings, values} = this; 941 | if (statement instanceof SQLStatement) 942 | push(strings, values, statement, true); 943 | else 944 | strings[strings.length - 1] += lsp(statement); 945 | return this; 946 | } 947 | named() { 948 | return this; 949 | } 950 | get sql() { 951 | return this.strings.join('?'); 952 | } 953 | } 954 | 955 | const SQL = (tpl, ...val) => { 956 | const strings = [tpl[0]]; 957 | const values = []; 958 | for (let {length} = tpl, prev = tpl[0], j = 0, i = 1; i < length; i++) { 959 | const current = tpl[i]; 960 | const value = val[i - 1]; 961 | if (/^('|")/.test(current) && RegExp.$1 === prev.slice(-1)) { 962 | strings[j] = [ 963 | strings[j].slice(0, -1), 964 | value, 965 | current.slice(1) 966 | ].join('`'); 967 | } else { 968 | if (value instanceof SQLStatement) { 969 | push(strings, values, value, false); 970 | j = strings.length - 1; 971 | strings[j] += current; 972 | } else { 973 | values.push(value); 974 | j = strings.push(current) - 1; 975 | } 976 | prev = strings[j]; 977 | } 978 | } 979 | return new SQLStatement(strings, values); 980 | }; 981 | 982 | return SQL; 983 | 984 | }()); 985 | 986 | // that's it! 987 | module.exports = dblite; 988 | 989 | /** some simple example 990 | var db = 991 | require('./build/dblite.node.js')('./test/dblite.test.sqlite'). 992 | on('info', console.log.bind(console)). 993 | on('error', console.error.bind(console)). 994 | on('close', console.log.bind(console)); 995 | 996 | // CORE FUNCTIONS: http://www.sqlite.org/lang_corefunc.html 997 | 998 | // PRAGMA: http://www.sqlite.org/pragma.html 999 | db.query('PRAGMA table_info(kvp)'); 1000 | 1001 | // to test memory database 1002 | var db = require('./build/dblite.node.js')(':memory:'); 1003 | db.query('CREATE TABLE test (key INTEGER PRIMARY KEY, value TEXT)') && undefined; 1004 | db.query('INSERT INTO test VALUES(null, "asd")') && undefined; 1005 | db.query('SELECT * FROM test') && undefined; 1006 | // db.close(); 1007 | */ 1008 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | wru test 5 | 6 | 7 | 8 | 9 | 13 | 44 | 45 | 46 | 78 | 79 | 80 |
81 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.1.5", 3 | "license": "MIT", 4 | "name": "dblite", 5 | "description": "a zero hassle wrapper for sqlite", 6 | "homepage": "https://github.com/WebReflection/dblite", 7 | "keywords": [ 8 | "sqlite", 9 | "sqlite3", 10 | "shell", 11 | "query", 12 | "embedded", 13 | "arch linux", 14 | "raspberry pi", 15 | "cubieboard", 16 | "cubieboard2", 17 | "simple", 18 | "no gyp", 19 | "no compile", 20 | "no hassle", 21 | "wrap", 22 | "spawn" 23 | ], 24 | "author": "Andrea Giammarchi", 25 | "repository": { 26 | "type": "git", 27 | "url": "git://github.com/WebReflection/dblite.git" 28 | }, 29 | "main": "./build/dblite.node.js", 30 | "scripts": { 31 | "test": "node test/.test.js" 32 | }, 33 | "dependencies": {}, 34 | "devDependencies": { 35 | "jshint": "^2.12.0", 36 | "wru": "^0.3.0" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/WebReflection/dblite/issues" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/dblite.js: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (C) 2015 by WebReflection 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | */ 23 | /*! a zero hassle wrapper for sqlite by Andrea Giammarchi !*/ 24 | var 25 | isArray = Array.isArray, 26 | // used to generate unique "end of the query" identifiers 27 | crypto = require('crypto'), 28 | // relative, absolute, and db paths are normalized anyway 29 | path = require('path'), 30 | // each dblite(fileName) instance is an EventEmitter 31 | EventEmitter = require('events').EventEmitter, 32 | // used to perform some fallback 33 | WIN32 = process.platform === 'win32', 34 | // what kind of Path Separator we have here ? 35 | PATH_SEP = path.sep || ( 36 | WIN32 ? '\\' : '/' 37 | ), 38 | // each dblite instance spawns a process once 39 | // and interact with that shell for the whole session 40 | // one spawn per database and no more (on avg 1 db is it) 41 | spawn = require('child_process').spawn, 42 | // use to re-generate Date objects 43 | DECIMAL = /^[1-9][0-9]*$/, 44 | // verify if it's a select or not 45 | SELECT = /^\s*(?:select|SELECT|pragma|PRAGMA|with|WITH) /, 46 | // for simple query replacements: WHERE field = ? 47 | REPLACE_QUESTIONMARKS = /\?/g, 48 | // for named replacements: WHERE field = :data 49 | REPLACE_PARAMS = /(?:\:|\@|\$)([a-zA-Z0-9_$]+)/g, 50 | // the way CSV threats double quotes 51 | DOUBLE_DOUBLE_QUOTES = /""/g, 52 | // to escape strings 53 | SINGLE_QUOTES = /'/g, 54 | // to use same escaping logic for double quotes 55 | // except it makes escaping easier for JSON data 56 | // which usually is full of " 57 | SINGLE_QUOTES_DOUBLED = "''", 58 | // to verify there are named fields/parametes 59 | HAS_PARAMS = /(?:\?|(?:(?:\:|\@|\$)[a-zA-Z0-9_$]+))/, 60 | // shortcut used as deafault notifier 61 | log = console.log.bind(console), 62 | // the default binary as array of paths 63 | bin = ['sqlite3'], 64 | // private shared variables 65 | // avoid creation of N functions 66 | // keeps memory low and improves performance 67 | paramsIndex, // which index is the current 68 | paramsArray, // which value when Array 69 | paramsObject, // which value when Object (named parameters) 70 | IS_NODE_06 = false, // dirty things to do there ... 71 | // defned later on 72 | EOL, EOL_LENGTH, 73 | SANITIZER, SANITIZER_REPLACER, 74 | waitForEOLToBeDefined = [], 75 | createProgram = function () { 76 | for (var args = [], i = 0; i < arguments.length; i++) { 77 | args[i] = arguments[i]; 78 | } 79 | return spawn( 80 | // executable only, folder needs to be specified a part 81 | bin.length === 1 ? bin[0] : ('.' + PATH_SEP + bin[bin.length - 1]), 82 | // normalize file path if not :memory: 83 | normalizeFirstArgument( 84 | // it is possible to eventually send extra sqlite3 args 85 | // so all arguments are passed 86 | args 87 | ).concat('-csv') // but the output MUST be csv 88 | .reverse(), // see https://github.com/WebReflection/dblite/pull/12 89 | // be sure the dir is the right one 90 | { 91 | // the right folder is important or sqlite3 won't work 92 | cwd: bin.slice(0, -1).join(PATH_SEP) || process.cwd(), 93 | env: process.env, // same env is OK 94 | encoding: 'utf8', // utf8 is OK 95 | stdio: ['pipe', 'pipe', 'pipe'] // handled here 96 | } 97 | ); 98 | }, 99 | defineCSVEOL = function (fn) { 100 | 101 | if (waitForEOLToBeDefined.push(fn) === 1) { 102 | 103 | var 104 | program = createProgram(':memory:', '-noheader'), 105 | ondata = function (data) { 106 | setUpAndGo(data.toString().slice(1)); 107 | }, 108 | // save the Mac, SAVE THE MAC!!! 109 | // error could occur due \r\n 110 | // so clearly that won't be the right EOL 111 | onerror = function () { 112 | setUpAndGo('\n'); 113 | }, 114 | setUpAndGo = function (eol) { 115 | 116 | // habemus EOL \o/ 117 | EOL = eol; 118 | 119 | // what's EOL length? Used to properly parse data 120 | EOL_LENGTH = EOL.length; 121 | 122 | // makes EOL safe for strings passed to the shell 123 | SANITIZER = new RegExp("[;" + EOL.split('').map(function(c) { 124 | return '\\x' + ('0' + c.charCodeAt(0).toString(16)).slice(-2); 125 | }).join('') + "]+$"); 126 | 127 | // used to mark the end of each line passed to the shell 128 | SANITIZER_REPLACER = ';' + EOL; 129 | 130 | // once closed, reassign this helper 131 | // and trigger all queued functions 132 | program.on('close', function () { 133 | defineCSVEOL = function (fn) { fn(); }; 134 | waitForEOLToBeDefined 135 | .splice(0, waitForEOLToBeDefined.length) 136 | .forEach(defineCSVEOL); 137 | }); 138 | 139 | program.kill(); 140 | } 141 | ; 142 | 143 | program.stdout.on('data', ondata); 144 | program.stdin.on('error', onerror); 145 | program.stdout.on('error', onerror); 146 | program.stderr.on('error', onerror); 147 | 148 | program.stdin.write('SELECT 1;\r\n'); 149 | 150 | } 151 | 152 | } 153 | ; 154 | 155 | /** 156 | * var db = dblite('filename.sqlite'):EventEmitter; 157 | * 158 | * db.query( thismethod has **many** overloads where almost everything is optional 159 | * 160 | * SQL:string, only necessary field. Accepts a query or a command such `.databases` 161 | * 162 | * params:Array|Object, optional, if specified replaces SQL parts with this object 163 | * db.query('INSERT INTO table VALUES(?, ?)', [null, 'content']); 164 | * db.query('INSERT INTO table VALUES(:id, :value)', {id:null, value:'content'}); 165 | * 166 | * fields:Array|Object, optional, if specified is used to normalize the query result with named fields. 167 | * db.query('SELECT table.a, table.other FROM table', ['a', 'b']); 168 | * [{a:'first value', b:'second'},{a:'row2 value', b:'row2'}] 169 | * 170 | * 171 | * db.query('SELECT table.a, table.other FROM table', ['a', 'b']); 172 | * [{a:'first value', b:'second'},{a:'row2 value', b:'row2'}] 173 | * callback:Function 174 | * ); 175 | */ 176 | function dblite() { 177 | var 178 | args = arguments, 179 | // the current dblite "instance" 180 | self = new EventEmitter(), 181 | // the incrementally concatenated buffer 182 | // cleaned up as soon as the current command has been completed 183 | selectResult = '', 184 | // sqlite3 shell can produce one output per time 185 | // every operation performed through this wrapper 186 | // should not bother the program until next 187 | // available slot. This queue helps keeping 188 | // requests ordered without stressing the system 189 | // once things will be ready, callbacks will be notified 190 | // accordingly. As simple as that ^_^ 191 | queue = [], 192 | // set as true only once db.close() has been called 193 | notWorking = false, 194 | // marks the shell busy or not 195 | // initially we need to wait for the EOL 196 | // so it's busy by default 197 | busy = true, 198 | // tells if current output needs to be processed 199 | wasSelect = false, 200 | wasNotSelect = false, 201 | wasError = false, 202 | // forces the output not to be processed 203 | // might be handy in some case where it's passed around 204 | // as string instread of needing to serialize/unserialize 205 | // the list of already arrays or objects 206 | dontParseCSV = false, 207 | // one callback per time will be notified 208 | $callback, 209 | // recycled variable for fields operation 210 | $fields, 211 | // this will be the delimiter of each sqlite3 shell command 212 | SUPER_SECRET, 213 | SUPER_SECRET_SELECT, 214 | SUPER_SECRET_LENGTH, 215 | // the spawned program 216 | program 217 | ; 218 | 219 | defineCSVEOL(function () { 220 | 221 | // this is the delimiter of each sqlite3 shell command 222 | SUPER_SECRET = '---' + 223 | crypto.randomBytes(64).toString('base64') + 224 | '---'; 225 | // ... I wish .print was introduced before SQLite 3.7.10 ... 226 | // this is a weird way to get rid of the header, if enabled 227 | SUPER_SECRET_SELECT = '"' + SUPER_SECRET + '" AS "' + SUPER_SECRET + '";' + EOL; 228 | // used to check the end of a buffer 229 | SUPER_SECRET_LENGTH = -(SUPER_SECRET.length + EOL_LENGTH); 230 | // add the EOL to the secret 231 | SUPER_SECRET += EOL; 232 | 233 | // define the spawn program 234 | program = createProgram.apply(null, args); 235 | 236 | // and all its IO handled here 237 | program.stderr.on('data', onerror); 238 | program.stdin.on('error', onerror); 239 | program.stdout.on('error', onerror); 240 | program.stderr.on('error', onerror); 241 | 242 | // invoked each time the sqlite3 shell produces an output 243 | program.stdout.on('data', function (data) { 244 | /*jshint eqnull: true*/ 245 | // big output might require more than a call 246 | var str, result, callback, fields, headers, wasSelectLocal, rows, dpcsv; 247 | if (wasError) { 248 | selectResult = ''; 249 | wasError = false; 250 | if (self.ignoreErrors) { 251 | busy = false; 252 | next(); 253 | } 254 | return; 255 | } 256 | // the whole output is converted into a string here 257 | selectResult += data; 258 | // if the end of the output is the serapator 259 | if (selectResult.slice(SUPER_SECRET_LENGTH) === SUPER_SECRET) { 260 | // time to move forward since sqlite3 has done 261 | str = selectResult.slice(0, SUPER_SECRET_LENGTH); 262 | // drop the secret header if present 263 | headers = str.slice(SUPER_SECRET_LENGTH) === SUPER_SECRET; 264 | if (headers) str = str.slice(0, SUPER_SECRET_LENGTH); 265 | // clean up the outer variabls 266 | selectResult = ''; 267 | // makes the spawned program not busy anymore 268 | busy = false; 269 | // if it was a select 270 | if (wasSelect || wasNotSelect) { 271 | wasSelectLocal = wasSelect; 272 | dpcsv = dontParseCSV; 273 | // set as false all conditions 274 | // only here dontParseCSV could have been true 275 | // set to false that too 276 | wasSelect = wasNotSelect = dontParseCSV = busy; 277 | // which callback should be invoked? 278 | // last expected one for this round 279 | callback = $callback; 280 | // same as fields 281 | fields = $fields; 282 | // parse only if it was a select/pragma 283 | if (wasSelectLocal) { 284 | // unless specified, process the string 285 | // converting the CSV into an Array of rows 286 | result = dpcsv ? str : parseCSV(str); 287 | // if there were headers/fields and we have a result ... 288 | if (headers && isArray(result) && result.length) { 289 | // ... and fields is not defined 290 | if (fields == null) { 291 | // fields is the row 0 292 | fields = result[0]; 293 | } else if(!isArray(fields)) { 294 | // per each non present key, enrich the fields object 295 | // it is then possible to have automatic headers 296 | // with some known field validated/parsed 297 | // e.g. {id:Number} will be {id:Number, value:String} 298 | // if the query was SELECT id, value FROM table 299 | // and the fields object was just {id:Number} 300 | // but headers were active 301 | result[0].forEach(enrichFields, fields); 302 | } 303 | // drop the first row with headers 304 | result.shift(); 305 | } 306 | } 307 | // but next query, should not have 308 | // previously set callbacks or fields 309 | $callback = $fields = null; 310 | // the spawned program can start a new job without current callback 311 | // being able to push another job as soon as executed. This makes 312 | // the queue fair for everyone without granting priority to anyone. 313 | next(); 314 | // if there was actually a callback to call 315 | if (callback) { 316 | rows = fields ? ( 317 | // and if there was a need to parse each row 318 | isArray(fields) ? 319 | // as object with properties 320 | result.map(row2object, fields) : 321 | // or object with validated properties 322 | result.map(row2parsed, parseFields(fields)) 323 | ) : 324 | // go for it ... otherwise returns the result as it is: 325 | // an Array of Arrays 326 | result 327 | ; 328 | // if there was an error signature 329 | if (1 < callback.length) { 330 | callback.call(self, null, rows); 331 | } else { 332 | // invoke it with the db object as context 333 | callback.call(self, rows); 334 | } 335 | } 336 | } else { 337 | // not a select, just a special command 338 | // such .databases or .tables 339 | next(); 340 | // if there is something to notify 341 | if (str.length) { 342 | // and if there was an 'info' listener 343 | if (self.listeners('info').length) { 344 | // notify 345 | self.emit('info', EOL + str); 346 | } else { 347 | // otherwise log 348 | log(EOL + str); 349 | } 350 | } 351 | } 352 | } 353 | }); 354 | 355 | // detach the program from this one 356 | // node 0.6 has not unref 357 | if (program.unref) { 358 | program.on('close', close); 359 | } else { 360 | IS_NODE_06 = true; 361 | program.stdout.on('close', close); 362 | } 363 | 364 | // let's begin 365 | busy = false; 366 | next(); 367 | 368 | }); 369 | 370 | // when program is killed or closed for some reason 371 | // the dblite object needs to be notified too 372 | function close(code) { 373 | if (self.listeners('close').length) { 374 | self.emit('close', code); 375 | if (program.unref) 376 | program.unref(); 377 | } else { 378 | log('bye bye'); 379 | } 380 | } 381 | 382 | // as long as there's something else to do ... 383 | function next() { 384 | if (queue.length) { 385 | // ... do that and wait for next check 386 | self.query.apply(self, queue.shift()); 387 | } 388 | } 389 | 390 | // common error helper 391 | function onerror(data) { 392 | if($callback && 1 < $callback.length) { 393 | // there is a callback waiting 394 | // and there is more than an argument in there 395 | // the callback is waiting for errors too 396 | var callback = $callback; 397 | wasSelect = wasNotSelect = dontParseCSV = false; 398 | $callback = $fields = null; 399 | wasError = true; 400 | // should the next be called ? next(); 401 | callback.call(self, new Error(data.toString()), null); 402 | } else if(self.listeners('error').length) { 403 | // notify listeners 404 | self.emit('error', '' + data); 405 | } else { 406 | // log the output avoiding exit 1 407 | // if no listener was added 408 | console.error('' + data); 409 | } 410 | } 411 | 412 | // WARNING: this can be very unsafe !!! 413 | // if there is an error and this 414 | // property is explicitly set to false 415 | // it keeps running queries no matter what 416 | self.ignoreErrors = false; 417 | // - - - - - - - - - - - - - - - - - - - - 418 | 419 | // safely closes the process 420 | // will emit 'close' once done 421 | self.close = function() { 422 | // close can happen only once 423 | if (!notWorking) { 424 | // this should gently terminate the program 425 | // only once everything scheduled has been completed 426 | self.query('.exit'); 427 | notWorking = true; 428 | // the hardly killed version was like this: 429 | // program.stdin.end(); 430 | // program.kill(); 431 | } 432 | }; 433 | 434 | // SELECT last_insert_rowid() FROM table might not work as expected 435 | // This method makes the operation atomic and reliable 436 | self.lastRowID = function(table, callback) { 437 | self.query( 438 | 'SELECT ROWID FROM `' + table + '` ORDER BY ROWID DESC LIMIT 1', 439 | function(result){ 440 | var row = result[0], k; 441 | // if headers are switched on 442 | if (!(row instanceof Array)) { 443 | for (k in row) { 444 | if (row.hasOwnProperty(k)) { 445 | row = [row[k]]; 446 | break; 447 | } 448 | } 449 | } 450 | (callback || log).call(self, row[0]); 451 | } 452 | ); 453 | return self; 454 | }; 455 | 456 | // Handy if for some reason data has to be passed around 457 | // as string instead of being serialized and deserialized 458 | // as Array of Arrays. Don't use if not needed. 459 | self.plain = function() { 460 | dontParseCSV = true; 461 | return self.query.apply(self, arguments); 462 | }; 463 | 464 | // main logic/method/entry point 465 | self.query = function(string, params, fields, callback) { 466 | // recognizes sql-template-strings objects 467 | if (typeof string === 'object' && 'sql' in string && 'values' in string) { 468 | switch (arguments.length) { 469 | case 3: return self.query(string.sql, string.values, params, fields); 470 | case 2: return self.query(string.sql, string.values, params); 471 | } 472 | return self.query(string.sql, string.values); 473 | } 474 | if ( 475 | // notWorking is set once .close() has been called 476 | // it does not make sense to execute anything after 477 | // the program is being closed 478 | notWorking && 479 | // however, since at that time the program could also be busy 480 | // let's be sure than this is either the exit call 481 | // or that the last call is still the exit one 482 | !(string === '.exit' || queue[queue.length - 1][0] === '.exit') 483 | ) return onerror('closing'), self; 484 | // if something is still going on in the sqlite3 shell 485 | // the progcess is flagged as busy. Just queue other operations 486 | if (busy) return queue.push(arguments), self; 487 | // if a SELECT or a PRAGMA ... 488 | wasSelect = SELECT.test(string); 489 | if (wasSelect) { 490 | // SELECT and PRAGMA makes `dblite` busy 491 | busy = true; 492 | switch(arguments.length) { 493 | // all arguments passed, nothing to do 494 | case 4: 495 | $callback = callback; 496 | $fields = fields; 497 | string = replaceString(string, params); 498 | break; 499 | // 3 arguments passed ... 500 | case 3: 501 | // is the last one the callback ? 502 | if (typeof fields == 'function') { 503 | // assign it 504 | $callback = fields; 505 | // has string parameters to repalce 506 | // such ? or :id and others ? 507 | if (HAS_PARAMS.test(string)) { 508 | // no objectification and/or validation needed 509 | $fields = null; 510 | // string replaced wit parameters 511 | string = replaceString(string, params); 512 | } else { 513 | // no replacement in the SQL needed 514 | // objectification with validation 515 | // if specified, will manage the result 516 | $fields = params; 517 | } 518 | } else { 519 | // no callback specified at all, probably in "dev mode" 520 | $callback = log; // just log the result 521 | $fields = fields; // use objectification 522 | string = replaceString(string, params); // replace parameters 523 | } 524 | break; 525 | // in this case ... 526 | case 2: 527 | // simple query with a callback 528 | if (typeof params == 'function') { 529 | // no objectification 530 | $fields = null; 531 | // callback is params argument 532 | $callback = params; 533 | } else { 534 | // "dev mode", just log 535 | $callback = log; 536 | // if there's something to replace 537 | if (HAS_PARAMS.test(string)) { 538 | // no objectification 539 | $fields = null; 540 | string = replaceString(string, params); 541 | } else { 542 | // nothing to replace 543 | // objectification with eventual validation 544 | $fields = params; 545 | } 546 | } 547 | break; 548 | default: 549 | // 1 argument, the SQL string and nothing else 550 | // "dev mode" log will do 551 | $callback = log; 552 | $fields = null; 553 | break; 554 | } 555 | // ask the sqlite3 shell ... 556 | program.stdin.write( 557 | // trick to always know when the console is not busy anymore 558 | // specially for those cases where no result is shown 559 | sanitize(string) + 'SELECT ' + SUPER_SECRET_SELECT 560 | ); 561 | } else { 562 | // if db.plain() was used but this is not a SELECT or PRAGMA 563 | // something is wrong with the logic since no result 564 | // was expected anyhow 565 | if (dontParseCSV) { 566 | dontParseCSV = false; 567 | throw new Error('not a select'); 568 | } else if (string[0] === '.') { 569 | // .commands are special queries .. so 570 | // .commands make `dblite` busy 571 | busy = true; 572 | // same trick with the secret to emit('info', resultAsString) 573 | // once everything is done 574 | program.stdin.write(string + EOL + 'SELECT ' + SUPER_SECRET_SELECT); 575 | } else { 576 | switch(arguments.length) { 577 | case 1: 578 | /* falls through */ 579 | case 2: 580 | if (typeof params !== 'function') { 581 | // no need to make the shell busy 582 | // since no output is shown at all (errors ... eventually) 583 | // sqlite3 shell will take care of the order 584 | // same as writing in a linux shell while something else is going on 585 | // who cares, will show when possible, after current job ^_^ 586 | program.stdin.write(sanitize(HAS_PARAMS.test(string) ? 587 | replaceString(string, params) : 588 | string 589 | )); 590 | // keep checking for possible following operations 591 | process.nextTick(next); 592 | break; 593 | } 594 | fields = params; 595 | // not necessary but guards possible wrong replaceString 596 | params = null; 597 | /* falls through */ 598 | case 3: 599 | // execute a non SELECT/PRAGMA statement 600 | // and be notified once it's done. 601 | // set state as busy 602 | busy = wasNotSelect = true; 603 | $callback = fields; 604 | program.stdin.write( 605 | (sanitize( 606 | HAS_PARAMS.test(string) ? 607 | replaceString(string, params) : 608 | string 609 | )) + 610 | EOL + 'SELECT ' + SUPER_SECRET_SELECT 611 | ); 612 | } 613 | } 614 | } 615 | // chainability just useful here for multiple queries at once 616 | return self; 617 | }; 618 | return self; 619 | } 620 | 621 | // enrich possible fields object with extra headers 622 | function enrichFields(key) { 623 | var had = this.hasOwnProperty(key), 624 | callback = had && this[key]; 625 | delete this[key]; 626 | this[key] = had ? callback : String; 627 | } 628 | 629 | // if not a memory database 630 | // the file path should be resolved as absolute 631 | function normalizeFirstArgument(args) { 632 | var file = args[0]; 633 | if (file !== ':memory:') { 634 | args[0] = path.resolve(args[0]); 635 | } 636 | return args.indexOf('-header') < 0 ? args.concat('-noheader') : args; 637 | } 638 | 639 | // assuming generated CSV is always like 640 | // 1,what,everEOL 641 | // with double quotes when necessary 642 | // 2,"what's up",everEOL 643 | // this parser works like a charm 644 | function parseCSV(output) { 645 | /*jshint eqnull: true*/ 646 | if (EOL == null) throw new Error( 647 | 'SQLite EOL not found. Please connect to a database first' 648 | ); 649 | for(var 650 | fields = [], 651 | rows = [], 652 | index = 0, 653 | rindex = 0, 654 | length = output.length, 655 | i = 0, 656 | j, loop, 657 | current, 658 | endLine, 659 | iNext, 660 | str; 661 | i < length; i++ 662 | ) { 663 | switch(output[i]) { 664 | case '"': 665 | loop = true; 666 | j = i; 667 | do { 668 | iNext = output.indexOf('"', current = j + 1); 669 | switch(output[j = iNext + 1]) { 670 | case EOL[0]: 671 | if (EOL_LENGTH === 2 && output[j + 1] !== EOL[1]) { 672 | break; 673 | } 674 | /* falls through */ 675 | case ',': 676 | loop = false; 677 | } 678 | } while(loop); 679 | str = output.slice(i + 1, iNext++).replace(DOUBLE_DOUBLE_QUOTES, '"'); 680 | break; 681 | default: 682 | iNext = output.indexOf(',', i); 683 | endLine = output.indexOf(EOL, i); 684 | if (iNext < 0) iNext = length - EOL_LENGTH; 685 | str = output.slice(i, endLine < iNext ? (iNext = endLine) : iNext); 686 | break; 687 | } 688 | fields[index++] = str; 689 | if (output[i = iNext] === EOL[0] && ( 690 | EOL_LENGTH === 1 || ( 691 | output[i + 1] === EOL[1] && ++i 692 | ) 693 | ) 694 | ) { 695 | rows[rindex++] = fields; 696 | fields = []; 697 | index = 0; 698 | } 699 | } 700 | return rows; 701 | } 702 | 703 | // create an object with right validation 704 | // and right fields to simplify the parsing 705 | // NOTE: this is based on ordered key 706 | // which is not specified by old ES specs 707 | // but it works like this in V8 708 | function parseFields($fields) { 709 | for (var 710 | current, 711 | fields = Object.keys($fields), 712 | parsers = [], 713 | length = fields.length, 714 | i = 0; i < length; i++ 715 | ) { 716 | current = $fields[fields[i]]; 717 | parsers[i] = current === Boolean ? 718 | $Boolean : ( 719 | current === Date ? 720 | $Date : 721 | current || String 722 | ) 723 | ; 724 | } 725 | return {f: fields, p: parsers}; 726 | } 727 | 728 | // transform SQL strings using parameters 729 | function replaceString(string, params) { 730 | // if params is an array 731 | if (isArray(params)) { 732 | // replace all ? occurence ? with right 733 | // incremental params[index++] 734 | paramsIndex = 0; 735 | paramsArray = params; 736 | string = string.replace(REPLACE_QUESTIONMARKS, replaceQuestions); 737 | } else { 738 | // replace :all @fields with the right 739 | // object.all or object.fields occurrences 740 | paramsObject = params; 741 | string = string.replace(REPLACE_PARAMS, replaceParams); 742 | } 743 | paramsArray = paramsObject = null; 744 | return string; 745 | } 746 | 747 | // escape the property found in the SQL 748 | function replaceParams(match, key) { 749 | return escape(paramsObject[key]); 750 | } 751 | 752 | // escape the value found for that ? in the SQL 753 | function replaceQuestions() { 754 | return escape(paramsArray[paramsIndex++]); 755 | } 756 | 757 | // objectification: makes an Array an object 758 | // assuming the context is an array of ordered fields 759 | function row2object(row) { 760 | for (var 761 | out = {}, 762 | length = this.length, 763 | i = 0; i < length; i++ 764 | ) { 765 | out[this[i]] = row[i]; 766 | } 767 | return out; 768 | } 769 | 770 | // objectification with validation: 771 | // makes an Array a validated object 772 | // assuming the context is an object 773 | // produced via parseFields() function 774 | function row2parsed(row) { 775 | for (var 776 | out = {}, 777 | fields = this.f, 778 | parsers = this.p, 779 | length = fields.length, 780 | i = 0; i < length; i++ 781 | ) { 782 | out[fields[i]] = parsers[i](row[i]); 783 | } 784 | return out; 785 | } 786 | 787 | // escape in a smart way generic values 788 | // making them compatible with SQLite types 789 | // or useful for JavaScript once retrieved back 790 | function escape(what) { 791 | /*jshint eqnull: true*/ 792 | if (EOL == null) throw new Error( 793 | 'SQLite EOL not found. Please connect to a database first' 794 | ); 795 | switch (typeof what) { 796 | case 'string': 797 | return "'" + what.replace( 798 | SINGLE_QUOTES, SINGLE_QUOTES_DOUBLED 799 | ) + "'"; 800 | case 'undefined': 801 | what = null; 802 | /* falls through */ 803 | case 'object': 804 | return what == null ? 805 | 'null' : 806 | ("'" + JSON.stringify(what).replace( 807 | SINGLE_QUOTES, SINGLE_QUOTES_DOUBLED 808 | ) + "'") 809 | ; 810 | // SQLite has no Boolean type 811 | case 'boolean': 812 | return what ? '1' : '0'; // 1 => true, 0 => false 813 | case 'number': 814 | // only finite numbers can be stored 815 | if (isFinite(what)) return '' + what; 816 | } 817 | // all other cases 818 | throw new Error('unsupported data'); 819 | } 820 | 821 | // makes an SQL statement OK for dblite <=> sqlite communications 822 | function sanitize(string) { 823 | return string.replace(SANITIZER, '') + SANITIZER_REPLACER; 824 | } 825 | 826 | // no Boolean type in SQLite 827 | // this will replace the possible Boolean validator 828 | // returning the right expected value 829 | function $Boolean(field) { 830 | switch(field.toLowerCase()) { 831 | case '0': 832 | case 'false': 833 | case 'null': 834 | case '': 835 | return false; 836 | } 837 | return true; 838 | } 839 | 840 | // no Date in SQLite, this will 841 | // take care of validating/creating Dates 842 | // when the field is retrieved with a Date validator 843 | function $Date(field) { 844 | return new Date( 845 | DECIMAL.test(field) ? parseInt(field, 10) : field 846 | ); 847 | } 848 | 849 | // which sqlite3 executable ? 850 | // it is possible to specify a different 851 | // sqlite3 executable even in relative paths 852 | // be sure the file exists and is usable as executable 853 | Object.defineProperty( 854 | dblite, 855 | 'bin', 856 | { 857 | get: function () { 858 | // normalized string if was a path 859 | return bin.join(PATH_SEP); 860 | }, 861 | set: function (value) { 862 | var isPath = -1 < value.indexOf(PATH_SEP); 863 | if (isPath) { 864 | // resolve the path 865 | value = path.resolve(value); 866 | // verify it exists 867 | if (!require(IS_NODE_06 ? 'path' : 'fs').existsSync(value)) { 868 | throw 'invalid executable: ' + value; 869 | } 870 | } 871 | // assign as Array in any case 872 | bin = value.split(PATH_SEP); 873 | } 874 | } 875 | ); 876 | 877 | // starting from v0.6.0 sqlite version shuold be specified 878 | // specially if SQLite version is 3.8.6 or greater 879 | // var dblite = require('dblite').withSQLite('3.8.6') 880 | dblite.withSQLite = function (sqliteVersion) { 881 | dblite.sqliteVersion = sqliteVersion; 882 | return dblite; 883 | }; 884 | 885 | // to manually parse CSV data if necessary 886 | // mainly to be able to use db.plain(SQL) 887 | // without parsing it right away and pass the string 888 | // around instead of serializing and de-serializing it 889 | // all the time. Ideally this is a scenario for clusters 890 | // no need to usually do manually anything otherwise. 891 | dblite.parseCSV = parseCSV; 892 | 893 | // how to manually escape data 894 | // might be handy to write directly SQL strings 895 | // instead of using handy paramters Array/Object 896 | // usually you don't want to do this 897 | dblite.escape = escape; 898 | 899 | // helps writing queries 900 | dblite.SQL = (function () { 901 | 'use strict'; 902 | 903 | const lsp = str => (/^[\s\n\r]/.test(str) ? str : ' ' + str); 904 | 905 | const push = (str, val, statement, spaced) => { 906 | const {strings, values} = statement; 907 | str[str.length - 1] += spaced ? lsp(strings[0]) : strings[0]; 908 | str.push(...strings.slice(1)); 909 | val.push(...values); 910 | }; 911 | 912 | class SQLStatement { 913 | constructor(strings, values) { 914 | this.strings = strings; 915 | this.values = values; 916 | } 917 | append(statement) { 918 | const {strings, values} = this; 919 | if (statement instanceof SQLStatement) 920 | push(strings, values, statement, true); 921 | else 922 | strings[strings.length - 1] += lsp(statement); 923 | return this; 924 | } 925 | named() { 926 | return this; 927 | } 928 | get sql() { 929 | return this.strings.join('?'); 930 | } 931 | } 932 | 933 | const SQL = (tpl, ...val) => { 934 | const strings = [tpl[0]]; 935 | const values = []; 936 | for (let {length} = tpl, prev = tpl[0], j = 0, i = 1; i < length; i++) { 937 | const current = tpl[i]; 938 | const value = val[i - 1]; 939 | if (/^('|")/.test(current) && RegExp.$1 === prev.slice(-1)) { 940 | strings[j] = [ 941 | strings[j].slice(0, -1), 942 | value, 943 | current.slice(1) 944 | ].join('`'); 945 | } else { 946 | if (value instanceof SQLStatement) { 947 | push(strings, values, value, false); 948 | j = strings.length - 1; 949 | strings[j] += current; 950 | } else { 951 | values.push(value); 952 | j = strings.push(current) - 1; 953 | } 954 | prev = strings[j]; 955 | } 956 | } 957 | return new SQLStatement(strings, values); 958 | }; 959 | 960 | return SQL; 961 | 962 | }()); 963 | 964 | // that's it! 965 | module.exports = dblite; 966 | 967 | /** some simple example 968 | var db = 969 | require('./build/dblite.node.js')('./test/dblite.test.sqlite'). 970 | on('info', console.log.bind(console)). 971 | on('error', console.error.bind(console)). 972 | on('close', console.log.bind(console)); 973 | 974 | // CORE FUNCTIONS: http://www.sqlite.org/lang_corefunc.html 975 | 976 | // PRAGMA: http://www.sqlite.org/pragma.html 977 | db.query('PRAGMA table_info(kvp)'); 978 | 979 | // to test memory database 980 | var db = require('./build/dblite.node.js')(':memory:'); 981 | db.query('CREATE TABLE test (key INTEGER PRIMARY KEY, value TEXT)') && undefined; 982 | db.query('INSERT INTO test VALUES(null, "asd")') && undefined; 983 | db.query('SELECT * FROM test') && undefined; 984 | // db.close(); 985 | */ 986 | -------------------------------------------------------------------------------- /template/amd.after: -------------------------------------------------------------------------------- 1 | ); -------------------------------------------------------------------------------- /template/amd.before: -------------------------------------------------------------------------------- 1 | define( -------------------------------------------------------------------------------- /template/copyright: -------------------------------------------------------------------------------- 1 | /*! (C) WebReflection Mit Style License */ 2 | -------------------------------------------------------------------------------- /template/license.after: -------------------------------------------------------------------------------- 1 | 2 | */ 3 | -------------------------------------------------------------------------------- /template/license.before: -------------------------------------------------------------------------------- 1 | /*! 2 | -------------------------------------------------------------------------------- /template/md.after: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /template/md.before: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 31 | 32 | -------------------------------------------------------------------------------- /template/node.after: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/dblite/64dc64f0fc0d411f99a4a43819448d2e43f92260/template/node.after -------------------------------------------------------------------------------- /template/node.before: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WebReflection/dblite/64dc64f0fc0d411f99a4a43819448d2e43f92260/template/node.before -------------------------------------------------------------------------------- /template/var.after: -------------------------------------------------------------------------------- 1 | ; -------------------------------------------------------------------------------- /template/var.before: -------------------------------------------------------------------------------- 1 | var main = -------------------------------------------------------------------------------- /test/.test.js: -------------------------------------------------------------------------------- 1 | var 2 | fs = require('fs'), 3 | path = require('path'), 4 | spawn = require('child_process').spawn, 5 | modules = path.join(__dirname, '..', 'node_modules', 'wru', 'node', 'program.js'), 6 | tests = [], 7 | ext = /\.js$/, 8 | code = 0, 9 | many = 0; 10 | 11 | function exit($code) { 12 | if ($code) { 13 | code = $code; 14 | } 15 | if (!--many) { 16 | if (!code) { 17 | fs.writeFileSync( 18 | path.join(__dirname, '..', 'index.html'), 19 | fs.readFileSync( 20 | path.join(__dirname, '..', 'index.html'), 21 | 'utf-8' 22 | ).replace(/var TESTS = \[.*?\];/, 'var TESTS = ' + JSON.stringify(tests) + ';'), 23 | 'utf-8' 24 | ); 25 | } 26 | process.exit(code); 27 | } 28 | } 29 | 30 | fs.readdirSync(__dirname).filter(function(file){ 31 | if (ext.test(file) && (fs.existsSync || path.existsSync)(path.join(__dirname, '..', 'src', file))) { 32 | ++many; 33 | tests.push(file.replace(ext, '')); 34 | spawn( 35 | 'node', [modules, path.join('test', file)], { 36 | detached: false, 37 | stdio: [process.stdin, process.stdout, process.stderr] 38 | }).on('exit', exit); 39 | } 40 | }); -------------------------------------------------------------------------------- /test/dblite.js: -------------------------------------------------------------------------------- 1 | //remove: 2 | var dblite = require('../build/dblite.node.js'), 3 | file = require('path').join( 4 | (require('os').tmpdir || function(){return '.'})(), 5 | 'dblite.test.sqlite' 6 | ), 7 | SQL = dblite.SQL, 8 | db; 9 | 10 | // dblite.bin = '/Users/agiammarchi/Downloads/sqlite3'; 11 | 12 | if (typeof wru === 'undefined') wru = require('wru'); 13 | //:remove 14 | wru.log(file); 15 | wru.test([ 16 | { 17 | name: "main", 18 | test: function () { 19 | wru.assert(typeof dblite == "function"); 20 | db = dblite(file); 21 | } 22 | },{ 23 | name: 'create table if not exists', 24 | test: function () { 25 | db.query('CREATE TABLE IF NOT EXISTS `kvp` (id INTEGER PRIMARY KEY, key TEXT, value TEXT)'); 26 | db.on('info', wru.async(function (data) { 27 | db.removeListener('table exists', arguments.callee); 28 | wru.assert('table exists', /^kvp\b/m.test('' + data)); 29 | })); 30 | db.query('.tables'); 31 | } 32 | },{ 33 | name: '100 sequential inserts', 34 | test: function () { 35 | var timeout = wru.timeout; 36 | wru.timeout = 30000; // might be very slow 37 | var start = Date.now(), many = 0; 38 | db.on('error', wru.log); 39 | while(many++ < 100) { 40 | db.query(SQL`INSERT INTO kvp VALUES(null, ${'k' + many}, ${'v' + many})`); 41 | } 42 | db.lastRowID('kvp', wru.async(function(data){ 43 | wru.log(data + ' records in ' + ((Date.now() - start) / 1000) + ' seconds'); 44 | wru.assert(100 == data); 45 | wru.timeout = timeout; 46 | })); 47 | } 48 | },{ 49 | name: '1 transaction with 100 inserts', 50 | test: function () { 51 | var start = Date.now(), many = 0; 52 | db.on('error', wru.log); 53 | db.query('BEGIN TRANSACTION'); 54 | while(many++ < 100) { 55 | db.query('INSERT INTO kvp VALUES(null, "k' + many + '", "v' + many + '")'); 56 | } 57 | db.query('COMMIT'); 58 | db.lastRowID('kvp', wru.async(function(data){ 59 | wru.log(data + ' records in ' + ((Date.now() - start) / 1000) + ' seconds'); 60 | wru.assert(200 == data); 61 | })); 62 | } 63 | },{ 64 | name: 'auto escape', 65 | test: function () { 66 | var uniqueKey = 'key' + Math.random(); 67 | db.query('INSERT INTO kvp VALUES(?, ?, ?)', [null, uniqueKey, 'unique value']); 68 | db.query('SELECT * FROM kvp WHERE key = ?', [uniqueKey], wru.async(function (rows) { 69 | wru.assert('all good', rows.length === 1 && rows[0][2] === 'unique value' && rows[0][1] === uniqueKey); 70 | })); 71 | } 72 | },{ 73 | name: 'auto field', 74 | test: function () { 75 | var start = Date.now(); 76 | db.query('SELECT * FROM kvp', ['id', 'key', 'value'], wru.async(function (rows) { 77 | start = Date.now() - start; 78 | wru.log('fetched ' + rows.length + ' rows as objects in ' + (start / 1000) + ' seconds'); 79 | wru.assert( 80 | 'all good', 81 | rows[0].hasOwnProperty('id') && 82 | rows[0].hasOwnProperty('key') && 83 | rows[0].hasOwnProperty('value') && 84 | rows[rows.length - 1].hasOwnProperty('id') && 85 | rows[rows.length - 1].hasOwnProperty('key') && 86 | rows[rows.length - 1].hasOwnProperty('value') 87 | ); 88 | })); 89 | } 90 | },{ 91 | name: 'auto parsing field', 92 | test: function () { 93 | var start = Date.now(); 94 | db.query('SELECT * FROM kvp', { 95 | num: parseInt, 96 | whatsoever: String, 97 | whatever: String 98 | }, wru.async(function (rows) { 99 | start = Date.now() - start; 100 | wru.log('fetched ' + rows.length + ' rows as normalized objects in ' + (start / 1000) + ' seconds'); 101 | wru.assert( 102 | 'all good', 103 | rows[0].hasOwnProperty('num') && typeof rows[0].num === 'number' && 104 | rows[0].hasOwnProperty('whatsoever') && 105 | rows[0].hasOwnProperty('whatever') && 106 | rows[rows.length - 1].hasOwnProperty('num') && typeof rows[rows.length - 1].num === 'number' && 107 | rows[rows.length - 1].hasOwnProperty('whatsoever') && 108 | rows[rows.length - 1].hasOwnProperty('whatever') 109 | ); 110 | })); 111 | } 112 | },{ 113 | name: 'many selects at once', 114 | test: function () { 115 | for(var 116 | start = Date.now(), 117 | length = 0xFF, 118 | done = wru.async(function() { 119 | wru.log(length + ' different selects in ' + ((Date.now() - start) / 1000) + ' seconds'); 120 | wru.assert(true); 121 | }), 122 | f = function(j) { 123 | return function(r) { 124 | if (j != r[0][0]) { 125 | throw new Error(j + ':' + r[0][0]); 126 | } else if (i == length && j == i - 1) { 127 | done(); 128 | } 129 | } 130 | }, 131 | i = 0; 132 | i < length; i++ 133 | ) { 134 | db.query('SELECT '+i,f(i)); 135 | } 136 | } 137 | },{ 138 | name: 'db.query() arguments', 139 | test: function () { 140 | db.query('SELECT 1', wru.async(function (data) { 141 | wru.assert('just one', data[0][0] == 1); 142 | db.query('SELECT ?', [2], wru.async(function (data) { 143 | wru.assert('just two', data[0][0] == 2); 144 | db.query('SELECT 1', {id:Number}, wru.async(function (data) { 145 | wru.assert('now one', data[0].id === 1); 146 | db.query('SELECT ?', [3], {id:Number}, wru.async(function (data) { 147 | wru.assert('now three', data[0].id === 3); 148 | // implicit output via bound console.log 149 | db.query('SELECT 1'); 150 | db.query('SELECT ?', [2]); 151 | db.query('SELECT 1', {id:Number}); 152 | db.query('SELECT ?', [2], {id:Number}); 153 | setTimeout(wru.async(function(){ 154 | wru.assert('check the output, should be like the following'); 155 | 156 | // [ [ '1' ] ] 157 | // [ [ '2' ] ] 158 | // [ { id: 1 } ] 159 | // [ { id: 2 } ] 160 | 161 | }), 500); 162 | })); 163 | })); 164 | })); 165 | })); 166 | } 167 | },{ 168 | name: 'utf-8', 169 | test: function () { 170 | var utf8 = '¥ · £ · € · $ · ¢ · ₡ · ₢ · ₣ · ₤ · ₥ · ₦ · ₧ · ₨ · ₩ · ₪ · ₫ · ₭ · ₮ · ₯ · ₹'; 171 | db.query('INSERT INTO kvp VALUES(null, ?, ?)', [utf8, utf8]); 172 | db.query('SELECT `value` FROM `kvp` WHERE `key` = ? AND `value` = ?', [utf8, utf8], wru.async(function(rows){ 173 | console.log(utf8); 174 | wru.assert(rows.length === 1 && rows[0][0] === utf8); 175 | })); 176 | } 177 | },{ 178 | name: 'new lines and disturbing queries', 179 | test: function () { 180 | // beware SQLite converts \r\n into \n 181 | var wut = '"\'\n\'\'\\\\;\n;\'"\'";"\'\r\'"\n\r@\n--'; // \r\n 182 | db.query('INSERT INTO kvp VALUES(null, ?, ?)', [wut, wut]); 183 | db.query('SELECT value FROM kvp WHERE key = ? AND value = ?', [wut, wut], wru.async(function(rows){ 184 | wru.assert(rows.length === 1 && rows[0][0] === wut); 185 | })); 186 | } 187 | },{ 188 | name: 'erease file', 189 | test: function () { 190 | db.on('close', wru.async(function () { 191 | wru.assert('bye bye'); 192 | require('fs').unlinkSync(file); 193 | })).close(); 194 | } 195 | },{ 196 | name: 'does not create :memory: file', 197 | test: function () { 198 | dblite(':memory:') 199 | .query('CREATE TABLE test (id INTEGER PRIMARY KEY)') 200 | .query('INSERT INTO test VALUES (null)') 201 | .query('SELECT * FROM test', wru.async(function () { 202 | this.close(); 203 | wru.assert('file was NOT created', !(require('fs').existsSync || require('path').existsSync)(':memory:')); 204 | })) 205 | .on('close', Object) // silent operation: don't show "bye bye" 206 | ; 207 | } 208 | },{ 209 | name: 'cannot close twice', 210 | test: function () { 211 | var times = 0; 212 | var db = dblite(':memory:'); 213 | db.on('close', function () { 214 | times++; 215 | }); 216 | db.close(); 217 | db.close(); 218 | setTimeout(wru.async(function () { 219 | wru.assert(times === 1); 220 | }), 500); 221 | } 222 | },{ 223 | name: '-header flag', 224 | test: function () { 225 | dblite(':memory:', '-header') 226 | .query('CREATE TABLE test (a INTEGER PRIMARY KEY, b TEXT, c TEXT)') 227 | .query('INSERT INTO test VALUES (null, 1, 2)') 228 | .query('INSERT INTO test VALUES (null, 3, 4)') 229 | .query('SELECT * FROM test', wru.async(function (rows) { 230 | this.close(); 231 | wru.assert('correct length', rows.length === 2); 232 | wru.assert('correct result', 233 | JSON.stringify({a: '1', b: '1', c: '2'}) === JSON.stringify(rows[0]) && 234 | JSON.stringify({a: '2', b: '3', c: '4'}) === JSON.stringify(rows[1]) 235 | ); 236 | })) 237 | .on('close', Object) // silent operation: don't show "bye bye" 238 | ; 239 | } 240 | },{ 241 | // fields have priority if specified 242 | name: '-header flag with fields too', 243 | test: function () { 244 | dblite(':memory:', '-header') 245 | .query('CREATE TABLE test (a INTEGER PRIMARY KEY, b TEXT, c TEXT)') 246 | .query('INSERT INTO test VALUES (null, 1, 2)') 247 | .query('INSERT INTO test VALUES (null, 3, 4)') 248 | // testing only one random item with a validation 249 | // to be sure b will be used as second validation property 250 | // headers are mandatory. Without headers b woul dbe used as `a` 251 | // because the parsing is based on fields order (supported in V8) 252 | .query('SELECT * FROM test', {b:Number}, wru.async(function (rows) { 253 | this.close(); 254 | wru.assert('correct length', rows.length === 2); 255 | wru.assert('correct result', 256 | JSON.stringify({a: '1', b: 1, c: '2'}) === JSON.stringify(rows[0]) && 257 | JSON.stringify({a: '2', b: 3, c: '4'}) === JSON.stringify(rows[1]) 258 | ); 259 | })) 260 | .on('close', Object) // silent operation: don't show "bye bye" 261 | ; 262 | } 263 | },{ 264 | name: 'runtime headers', 265 | test: function () { 266 | dblite(':memory:') 267 | .query('.headers ON') 268 | .query('CREATE TABLE test (a INTEGER PRIMARY KEY, b TEXT, c TEXT)') 269 | .query('INSERT INTO test VALUES (null, 1, 2)') 270 | .query('INSERT INTO test VALUES (null, 3, 4)') 271 | .query('SELECT * FROM test', wru.async(function (rows) { 272 | this.close(); 273 | wru.assert('correct length', rows.length === 2); 274 | wru.assert('correct result', 275 | JSON.stringify({a: '1', b: '1', c: '2'}) === JSON.stringify(rows[0]) && 276 | JSON.stringify({a: '2', b: '3', c: '4'}) === JSON.stringify(rows[1]) 277 | ); 278 | })) 279 | .query('.headers OFF') 280 | .on('close', Object) // silent operation: don't show "bye bye" 281 | ; 282 | } 283 | },{ 284 | name: 'runtime headers with fields too', 285 | test: function () { 286 | dblite(':memory:') 287 | .query('.headers ON') 288 | .query('CREATE TABLE test (a INTEGER PRIMARY KEY, b TEXT, c TEXT)') 289 | .query('INSERT INTO test VALUES (null, 1, 2)') 290 | .query('INSERT INTO test VALUES (null, 3, 4)') 291 | .query('SELECT * FROM test', {b:Number}, wru.async(function (rows) { 292 | this.close(); 293 | wru.assert('correct length', rows.length === 2); 294 | wru.assert('correct result', 295 | JSON.stringify({a: '1', b: 1, c: '2'}) === JSON.stringify(rows[0]) && 296 | JSON.stringify({a: '2', b: 3, c: '4'}) === JSON.stringify(rows[1]) 297 | ); 298 | })) 299 | .on('close', Object) // silent operation: don't show "bye bye" 300 | ; 301 | } 302 | },{ 303 | name: 'single count on header', 304 | test: function () { 305 | dblite(':memory:') 306 | .query('.headers ON') 307 | .query('CREATE TABLE test (id INTEGER PRIMARY KEY)') 308 | .query('INSERT INTO test VALUES (null)') 309 | .query('SELECT COUNT(id) AS total FROM test', wru.async(function (rows) { 310 | this.close(); 311 | wru.assert('right amount of rows', rows.length === 1 && rows[0].total == 1); 312 | })) 313 | .query('.headers OFF') 314 | .on('close', Object) 315 | ; 316 | } 317 | },{ 318 | name: 'null value test', 319 | test: function () { 320 | dblite(':memory:') 321 | .query('CREATE TABLE test (v TEXT)') 322 | .query('INSERT INTO test VALUES (null)') 323 | .query('SELECT * FROM test', wru.async(function (rows) { 324 | wru.assert('as Array', rows[0][0] === ''); 325 | this 326 | .query('.headers ON') 327 | .query('SELECT * FROM test', wru.async(function (rows) { 328 | this.close(); 329 | wru.assert('as Object', rows[0].v === ''); 330 | })) 331 | ; 332 | })) 333 | .on('close', Object) 334 | ; 335 | } 336 | },{ 337 | name: 'right order of events', 338 | test: function () { 339 | dblite(':memory:') 340 | .query('.headers ON') 341 | .query('CREATE TABLE test (v TEXT)') 342 | .query('INSERT INTO test VALUES ("value")') 343 | .query('SELECT * FROM test', wru.async(function (rows) { 344 | wru.assert('as Object', rows[0].v === 'value'); 345 | // now it should not have headers 346 | this 347 | .query('SELECT * FROM test', wru.async(function (rows) { 348 | this.close(); 349 | wru.assert('as Array', rows[0][0] === 'value'); 350 | })) 351 | ; 352 | })) 353 | .query('.headers OFF') // before the next query 354 | .on('close', Object) 355 | ; 356 | } 357 | },{ 358 | name: 'combined fields and headers', 359 | test: function () { 360 | dblite(':memory:') 361 | .query('.headers ON') 362 | .query('SELECT 1 as one, 2 as two', {two:Number}, wru.async(function (rows) { 363 | this.close(); 364 | wru.assert('right validation', rows[0].one === '1' && rows[0].two === 2); 365 | })) 366 | .on('close', Object) 367 | ; 368 | } 369 | },{ 370 | name: 'selecting empty results with manual fields works as expected', 371 | test: function () { 372 | dblite(':memory:') 373 | .query('CREATE TABLE whatever (v TEXT)') 374 | .query('SELECT * FROM whatever', {v:String}, wru.async(function (rows) { 375 | wru.assert('invoked corectly', rows.length === 0); 376 | this.close(); 377 | })) 378 | .on('close', Object) 379 | ; 380 | } 381 | },{ 382 | name: 'selecting empty results with automatic fields works as expected', 383 | test: function () { 384 | dblite(':memory:') 385 | .query('.headers ON') 386 | .query('CREATE TABLE whatever (v TEXT)') 387 | .query('SELECT * FROM whatever', wru.async(function (rows) { 388 | wru.assert('invoked corectly', rows.length === 0); 389 | this.close(); 390 | })) 391 | .on('close', Object) 392 | ; 393 | } 394 | },{ 395 | name: 'notifies inserts or other operations too', 396 | test: function () { 397 | var many = 0, db = dblite(':memory:').on('close', Object); 398 | db.query('CREATE TABLE IF NOT EXISTS `kvp` (id INTEGER PRIMARY KEY, key TEXT, value TEXT)'); 399 | db.query('BEGIN TRANSACTION'); 400 | while(many++ < 100) { 401 | db.query('INSERT INTO kvp VALUES(null, "k' + many + '", "v' + many + '")'); 402 | } 403 | db.query('COMMIT', wru.async(function () { 404 | wru.assert('so far, so good'); 405 | db.query('SELECT COUNT(id) FROM kvp', wru.async(function (rows) { 406 | db.close(); 407 | wru.assert('exact number of rows', +rows[0][0] === --many); 408 | })); 409 | })); 410 | } 411 | },{ 412 | name: 'automagic serialization', 413 | test: function () { 414 | dblite(':memory:') 415 | .query('CREATE TABLE IF NOT EXISTS `kv` (id INTEGER PRIMARY KEY, value TEXT)') 416 | .query('INSERT INTO `kv` VALUES(?, ?)', [null, {some:'text'}]) 417 | .query('SELECT * FROM `kv`', { 418 | id: Number, 419 | value: JSON.parse 420 | }, wru.async(function(rows) { 421 | this.close(); 422 | wru.assert('it did JSON.parse correctly', rows[0].value.some === 'text'); 423 | })).on('close', Object) 424 | ; 425 | } 426 | },{ 427 | name: 'lastRowID with headers too', 428 | test: function () { 429 | dblite(':memory:') 430 | .query('.headers ON') 431 | .query('CREATE TABLE test (id INTEGER PRIMARY KEY)') 432 | .query('INSERT INTO test VALUES (null)') 433 | .lastRowID('test', wru.async(function (id) { 434 | this.close(); 435 | wru.assert(id == 1); 436 | })).on('close', Object) 437 | ; 438 | } 439 | }, { 440 | name: 'dual arguments with error behavior', 441 | test: function () { 442 | var done = wru.async(function (err, data) { 443 | wru.assert('there is an error', err instanceof Error); 444 | wru.assert('there is no data', data === null); 445 | }); 446 | dblite(':memory:').query('CAUSING ERROR', function(err, data){ 447 | this.close(); 448 | done(err, data); 449 | }).on('close', Object); 450 | } 451 | }, { 452 | name: 'dual arguments with data behavior', 453 | test: function () { 454 | var done = wru.async(function (err, data) { 455 | wru.assert('there is no error', !err); 456 | wru.assert('there is some data', data == 123); 457 | }); 458 | dblite(':memory:').query('SELECT 123', function(err, data){ 459 | this.close(); 460 | done(err, data); 461 | }).on('close', Object); 462 | } 463 | }, { 464 | name: 'using unsafe .ignoreErrors property', 465 | test: function () { 466 | var done = wru.async(function (err, data) { 467 | wru.assert('there is no error', !err); 468 | wru.assert('there is some data', data.length === 2); 469 | }); 470 | var q = 'SELECT * FROM album ORDER BY name asc'; 471 | var db = dblite(':memory:'); 472 | db.ignoreErrors = true; 473 | db 474 | .query(q, function result(err, data) { 475 | if (err) { 476 | this 477 | .query('CREATE TABLE album (id INTEGER PRIMARY KEY, name TEXT)') 478 | .query('INSERT INTO album VALUES (null, "a")') 479 | .query('INSERT INTO album VALUES (null, "b")') 480 | .query(q, result) 481 | ; 482 | } else { 483 | done(err, data); 484 | } 485 | }) 486 | .on('close', Object) 487 | ; 488 | } 489 | },{ 490 | name: 'no problems on ABORT', 491 | test: function () { 492 | 493 | var 494 | done = wru.async(function (message, test) { 495 | wru.assert(message, test); 496 | done = Object; 497 | } 498 | ); 499 | 500 | db = dblite(file); 501 | db 502 | .query('CREATE TABLE users (id INTEGER PRIMARY KEY ON CONFLICT REPLACE, email TEXT UNIQUE ON CONFLICT ABORT)') 503 | .query('INSERT INTO users VALUES(?, ?)', [1, 'foo@example.com']) 504 | .query('INSERT INTO users VALUES(?, ?)', [2, 'bar@example.com'], function(err, data) { 505 | if (err) { 506 | return done('it failed to insert data', err); 507 | } 508 | db 509 | .query('INSERT INTO users VALUES(?, ?)', [1, 'bar@example.com'], function(err, data){ 510 | setTimeout(done, 1500, 'should have produced an error', err); 511 | }) 512 | .query('SELECT * FROM users WHERE id = ?', [1], function(err, data) { 513 | done('the error did not trigger', false); 514 | }) 515 | ; 516 | }) 517 | .on('close', Object) 518 | ; 519 | 520 | } 521 | }, { 522 | name: 'json_each(json_array(20170318,20170319,20170329))', 523 | test: function () { 524 | dblite(':memory:') 525 | .query('SELECT sqlite_version()', wru.async(function (rows) { 526 | var version = parseFloat(rows[0][0].replace(/\.\d+$/, '')); 527 | wru.assert(version); 528 | // to test this locally, drop the ! 529 | if (!version) { 530 | this.query('select * from json_each(json_array(20170318,20170319,20170329))', wru.async(function (rows) { 531 | wru.assert('[["0","20170318","integer","20170318","1","","$[0]","$"],["1","20170319","integer","20170319","2","","$[1]","$"],["2","20170329","integer","20170329","3","","$[2]","$"]]' === JSON.stringify(rows)); 532 | })); 533 | } else { 534 | console.log('skipped for ' + version); 535 | this.close(); 536 | } 537 | })) 538 | .on('close', Object); 539 | } 540 | } 541 | ]); --------------------------------------------------------------------------------