diff --git a/src/93sqljs.js b/src/93sqljs.js index 294082168d..66adfc26c3 100755 --- a/src/93sqljs.js +++ b/src/93sqljs.js @@ -6,6 +6,57 @@ var SQLITE = (alasql.engines.SQLITE = function () {}); +function getSqlJs(cb, errorcb) { + var sqljs = SQLITE.sqljs; + var sqljsPromise = SQLITE.sqljsPromise; + var globalSQL = alasql.utils.global.SQL; + + if (sqljs && sqljs.Database) { + cb(sqljs); + return; + } + + if (globalSQL && globalSQL.Database) { + SQLITE.sqljs = sqljs = globalSQL; + cb(sqljs); + return; + } + + var initSqlJs = (typeof globalSQL === 'function' && globalSQL) || alasql.utils.global.initSqlJs; + + if (!initSqlJs) { + var err = new Error('SQL.js library is not loaded'); + if (errorcb) { + errorcb(err); + return; + } + throw err; + } + + if (!sqljsPromise) { + var initResult = initSqlJs(); + var initPromise = + initResult && typeof initResult.then === 'function' + ? initResult + : Promise.resolve(initResult); + SQLITE.sqljsPromise = sqljsPromise = initPromise + .then(function (sqljsModule) { + if (!sqljsModule || !sqljsModule.Database) { + throw new Error('SQL.js library did not expose a Database constructor'); + } + SQLITE.sqljs = sqljs = sqljsModule; + return sqljsModule; + }) + .catch(function (err) { + SQLITE.sqljs = null; + SQLITE.sqljsPromise = null; + throw err; + }); + } + + sqljsPromise.then(cb, errorcb); +} + SQLITE.createDatabase = function (wdbid, args, ifnotexists, dbid, cb) { throw new Error('Connot create SQLITE database in memory. Attach it.'); }; @@ -30,29 +81,38 @@ SQLITE.attachDatabase = function (sqldbid, dbid, args, params, cb) { value, true, function (data) { - var db = new alasql.Database(dbid || sqldbid); - db.engineid = 'SQLITE'; - db.sqldbid = sqldbid; - var sqldb = (db.sqldb = new SQL.Database(data)); - db.tables = []; - var tables = sqldb.exec("SELECT * FROM sqlite_master WHERE type='table'")[0].values; - - tables.forEach(function (tbl) { - db.tables[tbl[1]] = {}; - var columns = (db.tables[tbl[1]].columns = []); - var ast = alasql.parse(tbl[4]); - var coldefs = ast.statements[0].columns; - if (coldefs && coldefs.length > 0) { - coldefs.forEach(function (cd) { - columns.push(cd); + getSqlJs( + function (SQL) { + var db = new alasql.Database(dbid || sqldbid); + db.engineid = 'SQLITE'; + db.sqldbid = sqldbid; + var sqldb = (db.sqldb = new SQL.Database(data)); + db.tables = []; + var tables = sqldb.exec("SELECT * FROM sqlite_master WHERE type='table'")[0].values; + + tables.forEach(function (tbl) { + db.tables[tbl[1]] = {}; + var columns = (db.tables[tbl[1]].columns = []); + var ast = alasql.parse(tbl[4]); + var coldefs = ast.statements[0].columns; + if (coldefs && coldefs.length > 0) { + coldefs.forEach(function (cd) { + columns.push(cd); + }); + } }); - } - }); - cb(1); + cb(1); + }, + function (err) { + cb(null, err); + } + ); }, function (err) { - throw new Error('Cannot open SQLite database file "' + args[0].value + '"'); + var fileError = new Error('Cannot open SQLite database file "' + args[0].value + '"'); + fileError.cause = err; + cb(null, fileError); } ); return res; diff --git a/test/test1284.js b/test/test1284.js new file mode 100644 index 0000000000..2cb77ee206 --- /dev/null +++ b/test/test1284.js @@ -0,0 +1,169 @@ +if (typeof exports === 'object') { + var assert = require('assert'); + var alasql = require('..'); +} + +describe('Test 1284 - ATTACH SQLITE DATABASE with async sql.js initialization', function () { + var originalLoadBinaryFile; + var originalSQL; + var originalInitSqlJs; + + class FakeSqlDatabase { + constructor(data) { + this.data = data; + } + + exec(sql) { + if (sql === "SELECT * FROM sqlite_master WHERE type='table'") { + return [ + { + values: [[null, 'signatures', null, null, 'CREATE TABLE signatures (name TEXT)']], + }, + ]; + } + + if (sql === 'SELECT * FROM signatures') { + return [ + { + columns: ['name'], + values: [['Ada']], + }, + ]; + } + + throw new Error('Unexpected SQL: ' + sql); + } + } + + beforeEach(function () { + originalLoadBinaryFile = alasql.utils.loadBinaryFile; + originalSQL = global.SQL; + originalInitSqlJs = global.initSqlJs; + alasql.engines.SQLITE.sqljs = null; + alasql.engines.SQLITE.sqljsPromise = null; + + alasql.utils.loadBinaryFile = function (path, runAsync, success) { + setTimeout(function () { + success('fake sqlite data'); + }, 0); + }; + + global.SQL = function () { + return Promise.resolve({Database: FakeSqlDatabase}); + }; + delete global.initSqlJs; + }); + + afterEach(function () { + alasql.utils.loadBinaryFile = originalLoadBinaryFile; + + if (typeof originalSQL === 'undefined') { + delete global.SQL; + } else { + global.SQL = originalSQL; + } + + if (typeof originalInitSqlJs === 'undefined') { + delete global.initSqlJs; + } else { + global.initSqlJs = originalInitSqlJs; + } + + if (alasql.databases.inscriptions) { + alasql('DETACH DATABASE inscriptions'); + } + alasql.engines.SQLITE.sqljs = null; + alasql.engines.SQLITE.sqljsPromise = null; + }); + + it('supports promise-based sql.js initialization for ATTACH/USE/SELECT', async function () { + const res = await alasql([ + 'ATTACH SQLITE DATABASE inscriptions("mydb.sqlite3")', + 'USE inscriptions', + 'SELECT * FROM signatures', + ]); + + assert.deepStrictEqual(res, [1, 1, [{name: 'Ada'}]]); + }); + + it('supports initSqlJs globals used by browser builds', async function () { + delete global.SQL; + global.initSqlJs = function () { + return Promise.resolve({Database: FakeSqlDatabase}); + }; + + const res = await alasql([ + 'ATTACH SQLITE DATABASE inscriptions("mydb.sqlite3")', + 'USE inscriptions', + 'SELECT * FROM signatures', + ]); + + assert.deepStrictEqual(res, [1, 1, [{name: 'Ada'}]]); + }); + + it('rejects when async sql.js initialization fails', async function () { + delete global.SQL; + global.initSqlJs = function () { + return Promise.reject(new Error('sql.js init failed')); + }; + + await assert.rejects(function () { + return alasql(['ATTACH SQLITE DATABASE inscriptions("mydb.sqlite3")']); + }, /sql\.js init failed/); + }); + + it('retries sql.js initialization after a failure', async function () { + delete global.SQL; + var shouldFail = true; + global.initSqlJs = function () { + if (shouldFail) { + return Promise.reject(new Error('sql.js init failed')); + } + return Promise.resolve({Database: FakeSqlDatabase}); + }; + + await assert.rejects(function () { + return alasql(['ATTACH SQLITE DATABASE inscriptions("mydb.sqlite3")']); + }, /sql\.js init failed/); + + shouldFail = false; + + const res = await alasql([ + 'ATTACH SQLITE DATABASE inscriptions("mydb.sqlite3")', + 'USE inscriptions', + 'SELECT * FROM signatures', + ]); + + assert.deepStrictEqual(res, [1, 1, [{name: 'Ada'}]]); + }); + + it('rejects invalid async sql.js module shapes', async function () { + delete global.SQL; + global.initSqlJs = function () { + return Promise.resolve({}); + }; + + await assert.rejects(function () { + return alasql(['ATTACH SQLITE DATABASE inscriptions("mydb.sqlite3")']); + }, /did not expose a Database constructor/); + }); + + it('rejects when the SQLite file cannot be loaded', async function () { + alasql.utils.loadBinaryFile = function (path, runAsync, success, error) { + setTimeout(function () { + error(new Error('load failed')); + }, 0); + }; + + await assert.rejects( + function () { + return alasql(['ATTACH SQLITE DATABASE inscriptions("mydb.sqlite3")']); + }, + function (err) { + assert.match(err.message, /Cannot open SQLite database file "mydb\.sqlite3"/); + assert.strictEqual(err.cause.message, 'load failed'); + return true; + } + ); + }); +});