diff --git a/CHANGELOG.md b/CHANGELOG.md index 935543d..16ee828 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] + +## [2.2.0] - 2018-04-02 +### Added +- Query class for interfacing with custom queries (like statements, but only for + onetime use). +- Statement class with same API as query class. + +### Deprecated +- SqlQuery class, renamed to BuildQuery + ## [2.1.0] - 2018-03-16 ### Added - Collate option in schema for columns diff --git a/lib/index.js b/lib/index.js index 87886e8..95bf16d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -3,6 +3,8 @@ import {SqlQuery, buildQuery} from './sqlquery'; import {SqlReadStream} from './sqlreadstream'; import {DataType, Table} from './table'; import {MapTable} from './maptable'; +import {query} from './query'; +import {statement} from './statement'; module.exports = { DataType, @@ -11,5 +13,7 @@ module.exports = { SqlQuery, Table, MapTable, - buildQuery + buildQuery, + query, + statement }; diff --git a/lib/query.js b/lib/query.js new file mode 100644 index 0000000..a97c699 --- /dev/null +++ b/lib/query.js @@ -0,0 +1,40 @@ +import _ from 'lodash'; + +import {SqlReadStream} from './sqlreadstream'; + +class Query { + constructor(db, sql, params) { + this.db = db; + this.sql = sql; + this.params = params; + } + + all(params) { + return this.db.all(this.sql, params || this.params); + } + + get(params) { + return this.db.get(this.sql, params || this.params); + } + + each(params, callback) { + if (_.isFunction(params)) { + callback = params; + params = undefined; + } + + return this.db.each(this.sql, params || this.params, callback); + } + + prepare(params) { + return this.db.prepare(this.sql, params || this.params); + } + + stream(params) { + return new SqlReadStream(this.db.prepare(this.sql, params || this.params)); + } +} + +export function query(db, sql, params) { + return new Query(db, sql, params); +} diff --git a/lib/sqlquery.js b/lib/sqlquery.js index a80de25..06fa641 100644 --- a/lib/sqlquery.js +++ b/lib/sqlquery.js @@ -3,6 +3,7 @@ import _ from 'lodash'; import assert from 'assert'; import {SqlReadStream} from './sqlreadstream'; +import {query} from './query'; class SqlStatementParameters { constructor() { @@ -38,7 +39,7 @@ class SqlStatementParameters { } -export class SqlQuery { +class BuildQuery { constructor(db) { this.db = db; this.selectColumnList = null; @@ -333,23 +334,23 @@ export class SqlQuery { } all() { - const query = this._buildSelectQuery(); - return this.db.all(query.sql, query.params); + const rawQuery = this._buildSelectQuery(); + return query(this.db, rawQuery.sql, rawQuery.params).all(); } get() { - const query = this._buildSelectQuery(); - return this.db.get(query.sql, query.params); + const rawQuery = this._buildSelectQuery(); + return query(this.db, rawQuery.sql, rawQuery.params).get(); } each(callback) { - const query = this._buildSelectQuery(); - return this.db.each(query.sql, query.params, callback); + const rawQuery = this._buildSelectQuery(); + return query(this.db, rawQuery.sql, rawQuery.params).each(callback); } prepareSelect() { - const query = this._buildSelectQuery(); - return this.db.prepare(query.sql, query.params); + const rawQuery = this._buildSelectQuery(); + return query(this.db, rawQuery.sql, rawQuery.params).prepare(); } stream() { @@ -430,6 +431,11 @@ export class SqlQuery { } } +/** + * @deprecated Use #buildQuery function instead + */ +export const SqlQuery = BuildQuery; + export function buildQuery(db) { return new SqlQuery(db); } diff --git a/lib/sqlreadstream.js b/lib/sqlreadstream.js index 98a9cd4..f0df30a 100644 --- a/lib/sqlreadstream.js +++ b/lib/sqlreadstream.js @@ -4,26 +4,7 @@ import Promise from 'bluebird'; import _ from 'lodash'; import assert from 'assert'; -// Like promisifyAll, but with specific list of functions (sqlite3 functions -// are not iterable for the native classes). -function promisifyFunctions(klass, promiseFunctions) { - for (let i = 0; i < promiseFunctions.length; i++) { - let fnc = promiseFunctions[i]; - let asyncFnc = `${fnc}Async`; - - if (klass.prototype[fnc] && !klass.prototype[asyncFnc]) { - klass.prototype[asyncFnc] = function (...params) { - return Promise.fromCallback(klass.prototype[fnc].bind(this, ...params)); - }; - } - } -} - -export function promisifyStatement() { - promisifyFunctions(sqlite3.Statement, [ - 'all', 'bind', 'get', 'getMultiple' - ]); -} +import {promisifyStatement} from './statement'; promisifyStatement(); diff --git a/lib/statement.js b/lib/statement.js new file mode 100644 index 0000000..60d0311 --- /dev/null +++ b/lib/statement.js @@ -0,0 +1,60 @@ +import sqlite3 from 'sqlite3'; +import Promise from 'bluebird'; +import _ from 'lodash'; + +import {SqlReadStream} from './sqlreadstream'; + +// Like promisifyAll, but with specific list of functions (sqlite3 functions +// are not iterable for the native classes). +function promisifyFunctions(klass, promiseFunctions) { + for (let i = 0; i < promiseFunctions.length; i++) { + let fnc = promiseFunctions[i]; + let asyncFnc = `${fnc}Async`; + + if (klass.prototype[fnc] && !klass.prototype[asyncFnc]) { + klass.prototype[asyncFnc] = function (...params) { + return Promise.fromCallback(klass.prototype[fnc].bind(this, ...params)); + }; + } + } +} + +export function promisifyStatement() { + promisifyFunctions(sqlite3.Statement, [ + 'all', 'bind', 'get', 'getMultiple' + ]); +} + +promisifyStatement(); + +export class Statement { + constructor(db, statement) { + this.statement = statement; + } + + all(...params) { + return this.statement.allAsync(...params); + } + + /** + * It is important that we don't send any param values, even undefined, if not + * used, as this would restart the statement. + */ + get(...params) { + return this.statement.getAsync(...params); + } + + each(...params) { + return this.statement.eachAsync(...params); + } + + stream() { + return new SqlReadStream(this.statement); + } +} + +export function statement(db, sql, params) { + return db.prepare(sql, params).then(statement => { + return new Statement(db, statement); + }); +} diff --git a/package-lock.json b/package-lock.json index 342011b..99792f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "sqlutil", - "version": "1.0.0", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -795,9 +795,9 @@ } }, "babel-preset-env": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.0.tgz", - "integrity": "sha512-OVgtQRuOZKckrILgMA5rvctvFZPv72Gua9Rt006AiPoB0DJKGN07UmaQA+qRrYgK71MVct8fFhT0EyNWYorVew==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.6.1.tgz", + "integrity": "sha512-W6VIyA6Ch9ePMI7VptNn2wBM6dbG0eSz25HEiL40nQXCsXGTGZSTZu1Iap+cj3Q0S5a7T9+529l/5Bkvd+afNA==", "dev": true, "requires": { "babel-plugin-check-es2015-constants": "6.22.0", @@ -827,9 +827,9 @@ "babel-plugin-transform-es2015-unicode-regex": "6.24.1", "babel-plugin-transform-exponentiation-operator": "6.24.1", "babel-plugin-transform-regenerator": "6.26.0", - "browserslist": "2.5.1", + "browserslist": "2.11.3", "invariant": "2.2.2", - "semver": "5.4.1" + "semver": "5.5.0" } }, "babel-preset-stage-2": { @@ -977,13 +977,13 @@ "dev": true }, "browserslist": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.5.1.tgz", - "integrity": "sha512-jAvM2ku7YDJ+leAq3bFH1DE0Ylw+F+EQDq4GkqZfgPEqpWYw9ofQH85uKSB9r3Tv7XDbfqVtE+sdvKJW7IlPJA==", + "version": "2.11.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-2.11.3.tgz", + "integrity": "sha512-yWu5cXT7Av6mVwzWc8lMsJMHWn4xyjSuGYi4IozbVTLUOEYPSagUB8kiMDUHA1fS3zjr8nkxkn9jdvug4BBRmA==", "dev": true, "requires": { - "caniuse-lite": "1.0.30000746", - "electron-to-chromium": "1.3.26" + "caniuse-lite": "1.0.30000819", + "electron-to-chromium": "1.3.40" } }, "caller-path": { @@ -1002,9 +1002,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30000746", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000746.tgz", - "integrity": "sha1-xk+Vo5Jc/TAgejCO12wa6W6gnqA=", + "version": "1.0.30000819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000819.tgz", + "integrity": "sha512-9i1d8eiKA6dLvsMrVrXOTP9/1sd9iIv4iC/UbPbIa9iQd9Gcnozi2sQ0d69TiQY9l7Alt7YIWISOBwyGSM6H0Q==", "dev": true }, "chai": { @@ -1219,9 +1219,9 @@ } }, "electron-to-chromium": { - "version": "1.3.26", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.26.tgz", - "integrity": "sha1-mWQnKUhhp02cfIK5Jg6jAejALWY=", + "version": "1.3.40", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.40.tgz", + "integrity": "sha1-H71tl779crim+SHcONIkE9L2/d8=", "dev": true }, "es5-ext": { @@ -2623,9 +2623,9 @@ "dev": true }, "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", "dev": true }, "set-immediate-shim": { diff --git a/package.json b/package.json index a973ae6..a5f3b1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sqlutil", - "version": "2.1.0", + "version": "2.2.0", "description": "Wrapper around sqlite for structured queries", "homepage": "https://github.com/erikman/sqlutil", "main": "dist/index.js", @@ -11,7 +11,7 @@ "build": "babel --compact true --minified --source-maps false -d dist/ lib", "build:debug": "babel -d dist/ lib", "lint": "eslint lib test", - "test": "mocha --compilers js:babel-core/register test", + "test": "mocha --require babel-polyfill --compilers js:babel-core/register test", "prepare": "npm run build" }, "keywords": [ @@ -27,11 +27,13 @@ "license": "MIT", "repository": "erikman/sqlutil", "devDependencies": { - "babel-cli": "^6.24.1", + "babel-cli": "^6.26.0", "babel-core": "^6.26.0", "babel-eslint": "^7.1.1", - "babel-preset-env": "^1.6.0", + "babel-polyfill": "^6.26.0", + "babel-preset-env": "^1.6.1", "babel-preset-stage-2": "^6.22.0", + "babel-register": "^6.26.0", "chai": "^3.5.0", "chai-as-promised": "^6.0.0", "eslint": "^3.15.0", diff --git a/test/sqlutil.js b/test/sqlutil.js index ea4749a..7002247 100644 --- a/test/sqlutil.js +++ b/test/sqlutil.js @@ -36,6 +36,171 @@ describe('sqlutil', () => { }); }); + describe('query', () => { + let table; + + beforeEach(() => { + db = new sqlutil.Db(); + + table = new sqlutil.Table(db, { + name: 'testtable', + columns: { + id: {type: sqlutil.DataType.INTEGER, primaryKey: true}, + name: {type: sqlutil.DataType.TEXT, unique: true}, + value: {type: sqlutil.DataType.FLOAT} + } + }); + + return db.open(':memory:') + .then(() => table.createTable()); + }); + + function insertSomeData() { + return table.insert({name: 'key1', value: 42}) + .then(() => table.insert({name: 'key2', value: 42})) + .then(() => table.insert({name: 'key3', value: 43})); + } + + it('allows custom queries with get interface', async () => { + await insertSomeData(); + + let q = sqlutil.query(db, 'SELECT * from testtable WHERE name = "key1"'); + let row = await q.get(); + expect(row).to.deep.equal({id: 1, name: 'key1', value: 42}); + }); + + it('allows custom queries with params', async () => { + await insertSomeData(); + + let q = sqlutil.query(db, 'SELECT * from testtable WHERE name = $name', { + $name: 'key1' + }); + let row = await q.get(); + expect(row).to.deep.equal({id: 1, name: 'key1', value: 42}); + }); + + it('can change params when getting', async () => { + await insertSomeData(); + + let q = sqlutil.query(db, 'SELECT * from testtable WHERE name = $name', { + $name: 'key1' + }); + let row = await q.get({$name: 'key3'}); + expect(row).to.deep.equal({id: 3, name: 'key3', value: 43}); + }); + + it('can retrieve all results', async () => { + await insertSomeData(); + + let q = sqlutil.query(db, 'SELECT * from testtable WHERE value = $value', { + $value: 42 + }); + let row = await q.all(); + expect(row).to.deep.equal([ + {id: 1, name: 'key1', value: 42}, + {id: 2, name: 'key2', value: 42} + ]); + }); + + it('can prepare statement', async () => { + await insertSomeData(); + + let q = sqlutil.query(db, 'SELECT * from testtable WHERE name = $name', { + $name: 'key1' + }); + let sqlStatement = await q.prepare(); + let row = await sqlStatement.getAsync(); + expect(row).to.deep.equal({id: 1, name: 'key1', value: 42}); + }); + + it('can stream results', async () => { + await insertSomeData(); + + let q = sqlutil.query(db, 'SELECT * from testtable WHERE value = $value', { + $value: 42 + }); + let sqlStream = q.stream(); + return assert.eventually.lengthOf(streamutil.streamToArray(sqlStream), 2); + }); + }); + + describe('statement', () => { + let table; + + beforeEach(() => { + db = new sqlutil.Db(); + + table = new sqlutil.Table(db, { + name: 'testtable', + columns: { + id: {type: sqlutil.DataType.INTEGER, primaryKey: true}, + name: {type: sqlutil.DataType.TEXT, unique: true}, + value: {type: sqlutil.DataType.FLOAT} + } + }); + + return db.open(':memory:') + .then(() => table.createTable()); + }); + + function insertSomeData() { + return table.insert({name: 'key1', value: 42}) + .then(() => table.insert({name: 'key2', value: 42})) + .then(() => table.insert({name: 'key3', value: 43})); + } + + it('allows custom queries with get interface', async () => { + await insertSomeData(); + + let q = await sqlutil.statement(db, 'SELECT * from testtable WHERE name = "key1"'); + let row = await q.get(); + expect(row).to.deep.equal({id: 1, name: 'key1', value: 42}); + }); + + it('allows custom queries with params', async () => { + await insertSomeData(); + + let q = await sqlutil.statement(db, 'SELECT * from testtable WHERE name = $name', { + $name: 'key1' + }); + let row = await q.get(); + expect(row).to.deep.equal({id: 1, name: 'key1', value: 42}); + }); + + it('can change params when getting', async () => { + await insertSomeData(); + + let q = await sqlutil.statement(db, 'SELECT * from testtable WHERE name = $name', { + $name: 'key1' + }); + let row = await q.get({$name: 'key3'}); + expect(row).to.deep.equal({id: 3, name: 'key3', value: 43}); + }); + + it('can retrieve all results', async () => { + await insertSomeData(); + + let q = await sqlutil.statement(db, 'SELECT * from testtable WHERE value = $value', { + $value: 42 + }); + let row = await q.all(); + expect(row).to.deep.equal([ + {id: 1, name: 'key1', value: 42}, + {id: 2, name: 'key2', value: 42} + ]); + }); + + it('can stream results', async () => { + await insertSomeData(); + + let q = await sqlutil.statement(db, 'SELECT * from testtable WHERE value = $value', { + $value: 42 + }); + let sqlStream = q.stream(); + return assert.eventually.lengthOf(streamutil.streamToArray(sqlStream), 2); + }); + }); + describe('table', () => { let table;