From edd45b317ad8783927bfe0a19392a0eaeff8e6c7 Mon Sep 17 00:00:00 2001 From: lana-k Date: Mon, 19 Apr 2021 15:01:40 +0200 Subject: [PATCH] add new sql.js module and tests #27 --- src/db.worker.js | 88 +++------------------ src/sql.js | 79 +++++++++++++++++++ tests/unit/sql.spec.js | 171 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 262 insertions(+), 76 deletions(-) create mode 100644 src/sql.js create mode 100644 tests/unit/sql.spec.js diff --git a/src/db.worker.js b/src/db.worker.js index 2be2009..7493889 100644 --- a/src/db.worker.js +++ b/src/db.worker.js @@ -1,98 +1,34 @@ import registerPromiseWorker from 'promise-worker/register' -import initSqlJs from 'sql.js/dist/sql-wasm.js' -import dbUtils from '@/dbUtils' +import Sql from '@/sql' -const sqlModuleReady = initSqlJs() -let db = null - -function onModuleReady (SQL) { - function createDb (data) { - if (db != null) db.close() - db = new SQL.Database(data) - return db - } +const sqlReady = Sql.build() +function processMsg (sql) { const data = this - switch (data && data.action) { case 'open': - const buff = data.buffer - createDb(buff && new Uint8Array(buff)) - return { - ready: true - } + return sql.open(data.buffer) case 'exec': - if (db === null) { - createDb() - } - if (!data.sql) { - throw new Error('exec: Missing query string') - } - return db.exec(data.sql, data.params) - case 'each': - if (db === null) { - createDb() - } - const callback = function callback (row) { - return { - row: row, - finished: false - } - } - const done = function done () { - return { - finished: true - } - } - return db.each(data.sql, data.params, callback, done) + return sql.exec(data.sql, data.params) case 'import': - createDb() - const values = data.values - const columns = data.columns - const chunkSize = 1500 - db.exec(dbUtils.getCreateStatement(columns, values)) - const chunks = dbUtils.generateChunks(values, chunkSize) - const chunksAmount = Math.ceil(values.length / chunkSize) - let count = 0 - const insertStr = dbUtils.getInsertStmt(columns) - const insertStmt = db.prepare(insertStr) - - postMessage({ progress: 0, id: data.progressCounterId }) - for (const chunk of chunks) { - db.exec('BEGIN') - for (const row of chunk) { - insertStmt.run(row) - } - db.exec('COMMIT') - count++ - postMessage({ progress: 100 * (count / chunksAmount), id: data.progressCounterId }) - } - - return { - finish: true - } + return sql.import(data.columns, data.values, data.progressCounterId, postMessage) case 'export': - return db.export() + return sql.export() case 'close': - if (db) { - db.close() - } - return { - finished: true - } + return sql.close() default: throw new Error('Invalid action : ' + (data && data.action)) } } -function onError (err) { +function onError (error) { return { - error: new Error(err.message) + error } } registerPromiseWorker(data => { - return sqlModuleReady - .then(onModuleReady.bind(data)) + return sqlReady + .then(processMsg.bind(data)) .catch(onError) }) diff --git a/src/sql.js b/src/sql.js new file mode 100644 index 0000000..f1f42b8 --- /dev/null +++ b/src/sql.js @@ -0,0 +1,79 @@ +import initSqlJs from 'sql.js/dist/sql-wasm.js' +import dbUtils from '@/dbUtils' + +let SQL = null +const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule }) + +export default class Sql { + constructor () { + this.db = null + } + + static build () { + return sqlModuleReady + .then(() => { + return new Sql() + }) + } + + createDb (buffer) { + if (this.db != null) this.db.close() + this.db = new SQL.Database(buffer) + return this.db + } + + open (buffer) { + this.createDb(buffer && new Uint8Array(buffer)) + return { + ready: true + } + } + + exec (sql, params) { + if (this.db === null) { + this.createDb() + } + if (!sql) { + throw new Error('exec: Missing query string') + } + return this.db.exec(sql, params) + } + + import (columns, values, progressCounterId, progressCallback, chunkSize = 1500) { + this.createDb() + this.db.exec(dbUtils.getCreateStatement(columns, values)) + const chunks = dbUtils.generateChunks(values, chunkSize) + const chunksAmount = Math.ceil(values.length / chunkSize) + let count = 0 + const insertStr = dbUtils.getInsertStmt(columns) + const insertStmt = this.db.prepare(insertStr) + + progressCallback({ progress: 0, id: progressCounterId }) + for (const chunk of chunks) { + this.db.exec('BEGIN') + for (const row of chunk) { + insertStmt.run(row) + } + this.db.exec('COMMIT') + count++ + progressCallback({ progress: 100 * (count / chunksAmount), id: progressCounterId }) + } + + return { + finish: true + } + } + + export () { + return this.db.export() + } + + close () { + if (this.db) { + this.db.close() + } + return { + finished: true + } + } +} diff --git a/tests/unit/sql.spec.js b/tests/unit/sql.spec.js new file mode 100644 index 0000000..5324d99 --- /dev/null +++ b/tests/unit/sql.spec.js @@ -0,0 +1,171 @@ +import chai from 'chai' +import sinon from 'sinon' +import chaiAsPromised from 'chai-as-promised' +import initSqlJs from 'sql.js' +import Sql from '@/sql' +chai.use(chaiAsPromised) +const expect = chai.expect +chai.should() + +const getSQL = initSqlJs() + +describe('sql.js', () => { + afterEach(() => { + sinon.restore() + }) + + it('returns a query result', async () => { + const SQL = await getSQL + const tempDb = new SQL.Database() + tempDb.run(` + CREATE TABLE test ( + id integer, + name varchar(100), + faculty varchar(100) + ); + INSERT INTO test (id, name, faculty) + VALUES + ( 1, 'Harry Potter', 'Griffindor'), + ( 2, 'Draco Malfoy', 'Slytherin'); + `) + + const data = tempDb.export() + const sql = await Sql.build() + sql.open(data) + const result = sql.exec('SELECT * from test') + expect(result).to.have.lengthOf(1) + expect(result[0].columns).to.eql(['id', 'name', 'faculty']) + expect(result[0].values).to.have.lengthOf(2) + expect(result[0].values[0]).to.eql([1, 'Harry Potter', 'Griffindor']) + expect(result[0].values[1]).to.eql([2, 'Draco Malfoy', 'Slytherin']) + }) + + it('throws an error if query is empty', async () => { + const SQL = await getSQL + const tempDb = new SQL.Database() + tempDb.run(` + CREATE TABLE test ( + id integer, + name varchar(100), + faculty varchar(100) + ); + INSERT INTO test (id, name, faculty) + VALUES + ( 1, 'Harry Potter', 'Griffindor'), + ( 2, 'Draco Malfoy', 'Slytherin'); + `) + + const data = tempDb.export() + const sql = await Sql.build() + sql.open(data) + expect(() => { sql.exec() }).to.throw('exec: Missing query string') + }) + + it('imports', async () => { + const data = { + columns: ['id', 'name'], + values: [ + [1, 'Harry Potter'], + [2, 'Draco Malfoy'], + [3, 'Hermione Granger'], + [4, 'Ron Weasley'] + ] + } + const progressCallback = sinon.stub() + const progressCounterId = 1 + const sql = await Sql.build() + sql.import(data.columns, data.values, progressCounterId, progressCallback, 2) + const result = sql.exec('SELECT * from csv_import') + expect(result).to.have.lengthOf(1) + expect(result[0].columns).to.eql(['id', 'name']) + expect(result[0].values).to.have.lengthOf(4) + expect(result[0].values[0]).to.eql([1, 'Harry Potter']) + expect(result[0].values[1]).to.eql([2, 'Draco Malfoy']) + expect(result[0].values[2]).to.eql([3, 'Hermione Granger']) + expect(result[0].values[3]).to.eql([4, 'Ron Weasley']) + + expect(progressCallback.calledThrice).to.equal(true) + expect(progressCallback.getCall(0).args[0]).to.eql({ progress: 0, id: 1 }) + expect(progressCallback.getCall(1).args[0]).to.eql({ progress: 50, id: 1 }) + expect(progressCallback.getCall(2).args[0]).to.eql({ progress: 100, id: 1 }) + }) + + it('exports', async () => { + const sql = await Sql.build() + sql.exec(` + CREATE TABLE test ( + id integer, + name varchar(100), + faculty varchar(100) + ); + INSERT INTO test (id, name, faculty) + VALUES + ( 1, 'Harry Potter', 'Griffindor'), + ( 2, 'Draco Malfoy', 'Slytherin'); + `) + const data = sql.export() + const anotherSql = await Sql.build() + anotherSql.open(data) + const result = anotherSql.exec('SELECT * from test') + expect(result).to.have.lengthOf(1) + expect(result[0].columns).to.eql(['id', 'name', 'faculty']) + expect(result[0].values).to.have.lengthOf(2) + expect(result[0].values[0]).to.eql([1, 'Harry Potter', 'Griffindor']) + expect(result[0].values[1]).to.eql([2, 'Draco Malfoy', 'Slytherin']) + }) + + it('closes', async () => { + const sql = await Sql.build() + + // nothing breaks if close empty db + sql.close() + + sql.exec(` + CREATE TABLE test ( + id integer, + name varchar(100) + ); + INSERT INTO test (id, name) + VALUES + ( 1, 'Harry Potter'), + ( 2, 'Draco Malfoy'); + `) + expect(sql.db.db).to.not.equal(null) + sql.close() + expect(sql.db.db).to.equal(null) + }) + + it('overwrites', async () => { + const sql = await Sql.build() + sql.exec(` + CREATE TABLE test ( + id integer, + name varchar(100) + ); + INSERT INTO test (id, name) + VALUES + ( 1, 'foo'), + ( 2, 'bar'); + `) + + let result = sql.exec('SELECT * from test') + expect(result[0].values).to.have.lengthOf(2) + + const data = { + columns: ['id', 'name'], + values: [ + [1, 'Harry Potter'], + [2, 'Draco Malfoy'], + [3, 'Hermione Granger'], + [4, 'Ron Weasley'] + ] + } + // rewrite the database by import + sql.import(data.columns, data.values, 1, sinon.stub(), 2) + result = sql.exec('SELECT * from csv_import') + expect(result[0].values).to.have.lengthOf(4) + + // test table oesn't exists anymore: the db was overwritten + expect(() => { sql.exec('SELECT * from test') }).to.throw('no such table: test') + }) +})