Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 79 additions & 19 deletions src/93sqljs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
};
Expand All @@ -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;
Expand Down
169 changes: 169 additions & 0 deletions test/test1284.js
Original file line number Diff line number Diff line change
@@ -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;
}
);
});
});