blob: 26cc3f166d9778cbfb4d543f2cd8058c89b994e0 [file] [log] [blame]
'use strict';
var utils = require('../../utils');
var errors = require('../../deps/errors');
var websqlConstants = require('./constants');
var BY_SEQ_STORE = websqlConstants.BY_SEQ_STORE;
var ATTACH_STORE = websqlConstants.ATTACH_STORE;
var ATTACH_AND_SEQ_STORE = websqlConstants.ATTACH_AND_SEQ_STORE;
// escapeBlob and unescapeBlob are workarounds for a websql bug:
// https://code.google.com/p/chromium/issues/detail?id=422690
// https://bugs.webkit.org/show_bug.cgi?id=137637
// The goal is to never actually insert the \u0000 character
// in the database.
function escapeBlob(str) {
return str
.replace(/\u0002/g, '\u0002\u0002')
.replace(/\u0001/g, '\u0001\u0002')
.replace(/\u0000/g, '\u0001\u0001');
}
function unescapeBlob(str) {
return str
.replace(/\u0001\u0001/g, '\u0000')
.replace(/\u0001\u0002/g, '\u0001')
.replace(/\u0002\u0002/g, '\u0002');
}
function stringifyDoc(doc) {
// don't bother storing the id/rev. it uses lots of space,
// in persistent map/reduce especially
delete doc._id;
delete doc._rev;
return JSON.stringify(doc);
}
function unstringifyDoc(doc, id, rev) {
doc = JSON.parse(doc);
doc._id = id;
doc._rev = rev;
return doc;
}
// question mark groups IN queries, e.g. 3 -> '(?,?,?)'
function qMarks(num) {
var s = '(';
while (num--) {
s += '?';
if (num) {
s += ',';
}
}
return s + ')';
}
function select(selector, table, joiner, where, orderBy) {
return 'SELECT ' + selector + ' FROM ' +
(typeof table === 'string' ? table : table.join(' JOIN ')) +
(joiner ? (' ON ' + joiner) : '') +
(where ? (' WHERE ' +
(typeof where === 'string' ? where : where.join(' AND '))) : '') +
(orderBy ? (' ORDER BY ' + orderBy) : '');
}
function compactRevs(revs, docId, tx) {
if (!revs.length) {
return;
}
var numDone = 0;
var seqs = [];
function checkDone() {
if (++numDone === revs.length) { // done
deleteOrphans();
}
}
function deleteOrphans() {
// find orphaned attachment digests
if (!seqs.length) {
return;
}
var sql = 'SELECT DISTINCT digest AS digest FROM ' +
ATTACH_AND_SEQ_STORE + ' WHERE seq IN ' + qMarks(seqs.length);
tx.executeSql(sql, seqs, function (tx, res) {
var digestsToCheck = [];
for (var i = 0; i < res.rows.length; i++) {
digestsToCheck.push(res.rows.item(i).digest);
}
if (!digestsToCheck.length) {
return;
}
var sql = 'DELETE FROM ' + ATTACH_AND_SEQ_STORE +
' WHERE seq IN (' +
seqs.map(function () { return '?'; }).join(',') +
')';
tx.executeSql(sql, seqs, function (tx) {
var sql = 'SELECT digest FROM ' + ATTACH_AND_SEQ_STORE +
' WHERE digest IN (' +
digestsToCheck.map(function () { return '?'; }).join(',') +
')';
tx.executeSql(sql, digestsToCheck, function (tx, res) {
var nonOrphanedDigests = new utils.Set();
for (var i = 0; i < res.rows.length; i++) {
nonOrphanedDigests.add(res.rows.item(i).digest);
}
digestsToCheck.forEach(function (digest) {
if (nonOrphanedDigests.has(digest)) {
return;
}
tx.executeSql(
'DELETE FROM ' + ATTACH_AND_SEQ_STORE + ' WHERE digest=?',
[digest]);
tx.executeSql(
'DELETE FROM ' + ATTACH_STORE + ' WHERE digest=?', [digest]);
});
});
});
});
}
// update by-seq and attach stores in parallel
revs.forEach(function (rev) {
var sql = 'SELECT seq FROM ' + BY_SEQ_STORE +
' WHERE doc_id=? AND rev=?';
tx.executeSql(sql, [docId, rev], function (tx, res) {
if (!res.rows.length) { // already deleted
return checkDone();
}
var seq = res.rows.item(0).seq;
seqs.push(seq);
tx.executeSql(
'DELETE FROM ' + BY_SEQ_STORE + ' WHERE seq=?', [seq], checkDone);
});
});
}
function unknownError(callback) {
return function (event) {
// event may actually be a SQLError object, so report is as such
var errorNameMatch = event && event.constructor.toString()
.match(/function ([^\(]+)/);
var errorName = (errorNameMatch && errorNameMatch[1]) || event.type;
var errorReason = event.target || event.message;
callback(errors.error(errors.WSQ_ERROR, errorReason, errorName));
};
}
function getSize(opts) {
if ('size' in opts) {
// triggers immediate popup in iOS, fixes #2347
// e.g. 5000001 asks for 5 MB, 10000001 asks for 10 MB,
return opts.size * 1000000;
}
// In iOS, doesn't matter as long as it's <= 5000000.
// Except that if you request too much, our tests fail
// because of the native "do you accept?" popup.
// In Android <=4.3, this value is actually used as an
// honest-to-god ceiling for data, so we need to
// set it to a decently high number.
var isAndroid = /Android/.test(window.navigator.userAgent);
return isAndroid ? 5000000 : 1; // in PhantomJS, if you use 0 it will crash
}
function createOpenDBFunction() {
if (typeof sqlitePlugin !== 'undefined') {
// The SQLite Plugin started deviating pretty heavily from the
// standard openDatabase() function, as they started adding more features.
// It's better to just use their "new" format and pass in a big ol'
// options object.
return sqlitePlugin.openDatabase.bind(sqlitePlugin);
}
if (typeof openDatabase !== 'undefined') {
return function openDB(opts) {
// Traditional WebSQL API
return openDatabase(opts.name, opts.version, opts.description, opts.size);
};
}
}
var cachedDatabases = {};
function openDB(opts) {
var openDBFunction = createOpenDBFunction();
var db = cachedDatabases[opts.name];
if (!db) {
db = cachedDatabases[opts.name] = openDBFunction(opts);
db._sqlitePlugin = typeof sqlitePlugin !== 'undefined';
}
return db;
}
function valid() {
// SQLitePlugin leaks this global object, which we can use
// to detect if it's installed or not. The benefit is that it's
// declared immediately, before the 'deviceready' event has fired.
return typeof openDatabase !== 'undefined' ||
typeof SQLitePlugin !== 'undefined';
}
module.exports = {
escapeBlob: escapeBlob,
unescapeBlob: unescapeBlob,
stringifyDoc: stringifyDoc,
unstringifyDoc: unstringifyDoc,
qMarks: qMarks,
select: select,
compactRevs: compactRevs,
unknownError: unknownError,
getSize: getSize,
openDB: openDB,
valid: valid
};