├── .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 | [](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 |