blob: 3e53523bb0cc6379dbbecd841ef760ebd6d91d5a [file] [log] [blame]
import jsExtend from 'js-extend'; var extend = jsExtend.extend;
import {
clone,
uuid,
pick,
filterChange,
isDeleted,
isLocalId,
parseHexString,
binaryStringToBlobOrBuffer as binStringToBlob,
hasLocalStorage,
collectConflicts,
traverseRevTree,
safeJsonParse,
safeJsonStringify,
changesHandler as Changes,
toPromise,
btoa
} from 'pouchdb-utils';
import websqlBulkDocs from './bulkDocs';
import {
MISSING_DOC,
REV_CONFLICT,
createError
} from 'pouchdb-errors';
import {
ADAPTER_VERSION,
DOC_STORE,
BY_SEQ_STORE,
ATTACH_STORE,
LOCAL_STORE,
META_STORE,
ATTACH_AND_SEQ_STORE
} from './constants';
import {
qMarks,
stringifyDoc,
unstringifyDoc,
select,
compactRevs,
websqlError,
getSize,
unescapeBlob
} from './utils';
import openDB from './openDatabase';
var websqlChanges = new Changes();
function fetchAttachmentsIfNecessary(doc, opts, api, txn, cb) {
var attachments = Object.keys(doc._attachments || {});
if (!attachments.length) {
return cb && cb();
}
var numDone = 0;
function checkDone() {
if (++numDone === attachments.length && cb) {
cb();
}
}
function fetchAttachment(doc, att) {
var attObj = doc._attachments[att];
var attOpts = {binary: opts.binary, ctx: txn};
api._getAttachment(attObj, attOpts, function (_, data) {
doc._attachments[att] = extend(
pick(attObj, ['digest', 'content_type']),
{ data: data }
);
checkDone();
});
}
attachments.forEach(function (att) {
if (opts.attachments && opts.include_docs) {
fetchAttachment(doc, att);
} else {
doc._attachments[att].stub = true;
checkDone();
}
});
}
var POUCH_VERSION = 1;
// these indexes cover the ground for most allDocs queries
var BY_SEQ_STORE_DELETED_INDEX_SQL =
'CREATE INDEX IF NOT EXISTS \'by-seq-deleted-idx\' ON ' +
BY_SEQ_STORE + ' (seq, deleted)';
var BY_SEQ_STORE_DOC_ID_REV_INDEX_SQL =
'CREATE UNIQUE INDEX IF NOT EXISTS \'by-seq-doc-id-rev\' ON ' +
BY_SEQ_STORE + ' (doc_id, rev)';
var DOC_STORE_WINNINGSEQ_INDEX_SQL =
'CREATE INDEX IF NOT EXISTS \'doc-winningseq-idx\' ON ' +
DOC_STORE + ' (winningseq)';
var ATTACH_AND_SEQ_STORE_SEQ_INDEX_SQL =
'CREATE INDEX IF NOT EXISTS \'attach-seq-seq-idx\' ON ' +
ATTACH_AND_SEQ_STORE + ' (seq)';
var ATTACH_AND_SEQ_STORE_ATTACH_INDEX_SQL =
'CREATE UNIQUE INDEX IF NOT EXISTS \'attach-seq-digest-idx\' ON ' +
ATTACH_AND_SEQ_STORE + ' (digest, seq)';
var DOC_STORE_AND_BY_SEQ_JOINER = BY_SEQ_STORE +
'.seq = ' + DOC_STORE + '.winningseq';
var SELECT_DOCS = BY_SEQ_STORE + '.seq AS seq, ' +
BY_SEQ_STORE + '.deleted AS deleted, ' +
BY_SEQ_STORE + '.json AS data, ' +
BY_SEQ_STORE + '.rev AS rev, ' +
DOC_STORE + '.json AS metadata';
function WebSqlPouch(opts, callback) {
var api = this;
var instanceId = null;
var size = getSize(opts);
var idRequests = [];
var encoding;
api._docCount = -1; // cache sqlite count(*) for performance
api._name = opts.name;
// extend the options here, because sqlite plugin has a ton of options
// and they are constantly changing, so it's more prudent to allow anything
var websqlOpts = extend({}, opts, {size: size, version: POUCH_VERSION});
var openDBResult = openDB(websqlOpts);
if (openDBResult.error) {
return websqlError(callback)(openDBResult.error);
}
var db = openDBResult.db;
if (typeof db.readTransaction !== 'function') {
// doesn't exist in sqlite plugin
db.readTransaction = db.transaction;
}
function dbCreated() {
// note the db name in case the browser upgrades to idb
if (hasLocalStorage()) {
window.localStorage['_pouch__websqldb_' + api._name] = true;
}
callback(null, api);
}
// In this migration, we added the 'deleted' and 'local' columns to the
// by-seq and doc store tables.
// To preserve existing user data, we re-process all the existing JSON
// and add these values.
// Called migration2 because it corresponds to adapter version (db_version) #2
function runMigration2(tx, callback) {
// index used for the join in the allDocs query
tx.executeSql(DOC_STORE_WINNINGSEQ_INDEX_SQL);
tx.executeSql('ALTER TABLE ' + BY_SEQ_STORE +
' ADD COLUMN deleted TINYINT(1) DEFAULT 0', [], function () {
tx.executeSql(BY_SEQ_STORE_DELETED_INDEX_SQL);
tx.executeSql('ALTER TABLE ' + DOC_STORE +
' ADD COLUMN local TINYINT(1) DEFAULT 0', [], function () {
tx.executeSql('CREATE INDEX IF NOT EXISTS \'doc-store-local-idx\' ON ' +
DOC_STORE + ' (local, id)');
var sql = 'SELECT ' + DOC_STORE + '.winningseq AS seq, ' + DOC_STORE +
'.json AS metadata FROM ' + BY_SEQ_STORE + ' JOIN ' + DOC_STORE +
' ON ' + BY_SEQ_STORE + '.seq = ' + DOC_STORE + '.winningseq';
tx.executeSql(sql, [], function (tx, result) {
var deleted = [];
var local = [];
for (var i = 0; i < result.rows.length; i++) {
var item = result.rows.item(i);
var seq = item.seq;
var metadata = JSON.parse(item.metadata);
if (isDeleted(metadata)) {
deleted.push(seq);
}
if (isLocalId(metadata.id)) {
local.push(metadata.id);
}
}
tx.executeSql('UPDATE ' + DOC_STORE + 'SET local = 1 WHERE id IN ' +
qMarks(local.length), local, function () {
tx.executeSql('UPDATE ' + BY_SEQ_STORE +
' SET deleted = 1 WHERE seq IN ' +
qMarks(deleted.length), deleted, callback);
});
});
});
});
}
// in this migration, we make all the local docs unversioned
function runMigration3(tx, callback) {
var local = 'CREATE TABLE IF NOT EXISTS ' + LOCAL_STORE +
' (id UNIQUE, rev, json)';
tx.executeSql(local, [], function () {
var sql = 'SELECT ' + DOC_STORE + '.id AS id, ' +
BY_SEQ_STORE + '.json AS data ' +
'FROM ' + BY_SEQ_STORE + ' JOIN ' +
DOC_STORE + ' ON ' + BY_SEQ_STORE + '.seq = ' +
DOC_STORE + '.winningseq WHERE local = 1';
tx.executeSql(sql, [], function (tx, res) {
var rows = [];
for (var i = 0; i < res.rows.length; i++) {
rows.push(res.rows.item(i));
}
function doNext() {
if (!rows.length) {
return callback(tx);
}
var row = rows.shift();
var rev = JSON.parse(row.data)._rev;
tx.executeSql('INSERT INTO ' + LOCAL_STORE +
' (id, rev, json) VALUES (?,?,?)',
[row.id, rev, row.data], function (tx) {
tx.executeSql('DELETE FROM ' + DOC_STORE + ' WHERE id=?',
[row.id], function (tx) {
tx.executeSql('DELETE FROM ' + BY_SEQ_STORE + ' WHERE seq=?',
[row.seq], function () {
doNext();
});
});
});
}
doNext();
});
});
}
// in this migration, we remove doc_id_rev and just use rev
function runMigration4(tx, callback) {
function updateRows(rows) {
function doNext() {
if (!rows.length) {
return callback(tx);
}
var row = rows.shift();
var doc_id_rev = parseHexString(row.hex, encoding);
var idx = doc_id_rev.lastIndexOf('::');
var doc_id = doc_id_rev.substring(0, idx);
var rev = doc_id_rev.substring(idx + 2);
var sql = 'UPDATE ' + BY_SEQ_STORE +
' SET doc_id=?, rev=? WHERE doc_id_rev=?';
tx.executeSql(sql, [doc_id, rev, doc_id_rev], function () {
doNext();
});
}
doNext();
}
var sql = 'ALTER TABLE ' + BY_SEQ_STORE + ' ADD COLUMN doc_id';
tx.executeSql(sql, [], function (tx) {
var sql = 'ALTER TABLE ' + BY_SEQ_STORE + ' ADD COLUMN rev';
tx.executeSql(sql, [], function (tx) {
tx.executeSql(BY_SEQ_STORE_DOC_ID_REV_INDEX_SQL, [], function (tx) {
var sql = 'SELECT hex(doc_id_rev) as hex FROM ' + BY_SEQ_STORE;
tx.executeSql(sql, [], function (tx, res) {
var rows = [];
for (var i = 0; i < res.rows.length; i++) {
rows.push(res.rows.item(i));
}
updateRows(rows);
});
});
});
});
}
// in this migration, we add the attach_and_seq table
// for issue #2818
function runMigration5(tx, callback) {
function migrateAttsAndSeqs(tx) {
// need to actually populate the table. this is the expensive part,
// so as an optimization, check first that this database even
// contains attachments
var sql = 'SELECT COUNT(*) AS cnt FROM ' + ATTACH_STORE;
tx.executeSql(sql, [], function (tx, res) {
var count = res.rows.item(0).cnt;
if (!count) {
return callback(tx);
}
var offset = 0;
var pageSize = 10;
function nextPage() {
var sql = select(
SELECT_DOCS + ', ' + DOC_STORE + '.id AS id',
[DOC_STORE, BY_SEQ_STORE],
DOC_STORE_AND_BY_SEQ_JOINER,
null,
DOC_STORE + '.id '
);
sql += ' LIMIT ' + pageSize + ' OFFSET ' + offset;
offset += pageSize;
tx.executeSql(sql, [], function (tx, res) {
if (!res.rows.length) {
return callback(tx);
}
var digestSeqs = {};
function addDigestSeq(digest, seq) {
// uniq digest/seq pairs, just in case there are dups
var seqs = digestSeqs[digest] = (digestSeqs[digest] || []);
if (seqs.indexOf(seq) === -1) {
seqs.push(seq);
}
}
for (var i = 0; i < res.rows.length; i++) {
var row = res.rows.item(i);
var doc = unstringifyDoc(row.data, row.id, row.rev);
var atts = Object.keys(doc._attachments || {});
for (var j = 0; j < atts.length; j++) {
var att = doc._attachments[atts[j]];
addDigestSeq(att.digest, row.seq);
}
}
var digestSeqPairs = [];
Object.keys(digestSeqs).forEach(function (digest) {
var seqs = digestSeqs[digest];
seqs.forEach(function (seq) {
digestSeqPairs.push([digest, seq]);
});
});
if (!digestSeqPairs.length) {
return nextPage();
}
var numDone = 0;
digestSeqPairs.forEach(function (pair) {
var sql = 'INSERT INTO ' + ATTACH_AND_SEQ_STORE +
' (digest, seq) VALUES (?,?)';
tx.executeSql(sql, pair, function () {
if (++numDone === digestSeqPairs.length) {
nextPage();
}
});
});
});
}
nextPage();
});
}
var attachAndRev = 'CREATE TABLE IF NOT EXISTS ' +
ATTACH_AND_SEQ_STORE + ' (digest, seq INTEGER)';
tx.executeSql(attachAndRev, [], function (tx) {
tx.executeSql(
ATTACH_AND_SEQ_STORE_ATTACH_INDEX_SQL, [], function (tx) {
tx.executeSql(
ATTACH_AND_SEQ_STORE_SEQ_INDEX_SQL, [],
migrateAttsAndSeqs);
});
});
}
// in this migration, we use escapeBlob() and unescapeBlob()
// instead of reading out the binary as HEX, which is slow
function runMigration6(tx, callback) {
var sql = 'ALTER TABLE ' + ATTACH_STORE +
' ADD COLUMN escaped TINYINT(1) DEFAULT 0';
tx.executeSql(sql, [], callback);
}
// issue #3136, in this migration we need a "latest seq" as well
// as the "winning seq" in the doc store
function runMigration7(tx, callback) {
var sql = 'ALTER TABLE ' + DOC_STORE +
' ADD COLUMN max_seq INTEGER';
tx.executeSql(sql, [], function (tx) {
var sql = 'UPDATE ' + DOC_STORE + ' SET max_seq=(SELECT MAX(seq) FROM ' +
BY_SEQ_STORE + ' WHERE doc_id=id)';
tx.executeSql(sql, [], function (tx) {
// add unique index after filling, else we'll get a constraint
// error when we do the ALTER TABLE
var sql =
'CREATE UNIQUE INDEX IF NOT EXISTS \'doc-max-seq-idx\' ON ' +
DOC_STORE + ' (max_seq)';
tx.executeSql(sql, [], callback);
});
});
}
function checkEncoding(tx, cb) {
// UTF-8 on chrome/android, UTF-16 on safari < 7.1
tx.executeSql('SELECT HEX("a") AS hex', [], function (tx, res) {
var hex = res.rows.item(0).hex;
encoding = hex.length === 2 ? 'UTF-8' : 'UTF-16';
cb();
}
);
}
function onGetInstanceId() {
while (idRequests.length > 0) {
var idCallback = idRequests.pop();
idCallback(null, instanceId);
}
}
function onGetVersion(tx, dbVersion) {
if (dbVersion === 0) {
// initial schema
var meta = 'CREATE TABLE IF NOT EXISTS ' + META_STORE +
' (dbid, db_version INTEGER)';
var attach = 'CREATE TABLE IF NOT EXISTS ' + ATTACH_STORE +
' (digest UNIQUE, escaped TINYINT(1), body BLOB)';
var attachAndRev = 'CREATE TABLE IF NOT EXISTS ' +
ATTACH_AND_SEQ_STORE + ' (digest, seq INTEGER)';
// TODO: migrate winningseq to INTEGER
var doc = 'CREATE TABLE IF NOT EXISTS ' + DOC_STORE +
' (id unique, json, winningseq, max_seq INTEGER UNIQUE)';
var seq = 'CREATE TABLE IF NOT EXISTS ' + BY_SEQ_STORE +
' (seq INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, ' +
'json, deleted TINYINT(1), doc_id, rev)';
var local = 'CREATE TABLE IF NOT EXISTS ' + LOCAL_STORE +
' (id UNIQUE, rev, json)';
// creates
tx.executeSql(attach);
tx.executeSql(local);
tx.executeSql(attachAndRev, [], function () {
tx.executeSql(ATTACH_AND_SEQ_STORE_SEQ_INDEX_SQL);
tx.executeSql(ATTACH_AND_SEQ_STORE_ATTACH_INDEX_SQL);
});
tx.executeSql(doc, [], function () {
tx.executeSql(DOC_STORE_WINNINGSEQ_INDEX_SQL);
tx.executeSql(seq, [], function () {
tx.executeSql(BY_SEQ_STORE_DELETED_INDEX_SQL);
tx.executeSql(BY_SEQ_STORE_DOC_ID_REV_INDEX_SQL);
tx.executeSql(meta, [], function () {
// mark the db version, and new dbid
var initSeq = 'INSERT INTO ' + META_STORE +
' (db_version, dbid) VALUES (?,?)';
instanceId = uuid();
var initSeqArgs = [ADAPTER_VERSION, instanceId];
tx.executeSql(initSeq, initSeqArgs, function () {
onGetInstanceId();
});
});
});
});
} else { // version > 0
var setupDone = function () {
var migrated = dbVersion < ADAPTER_VERSION;
if (migrated) {
// update the db version within this transaction
tx.executeSql('UPDATE ' + META_STORE + ' SET db_version = ' +
ADAPTER_VERSION);
}
// notify db.id() callers
var sql = 'SELECT dbid FROM ' + META_STORE;
tx.executeSql(sql, [], function (tx, result) {
instanceId = result.rows.item(0).dbid;
onGetInstanceId();
});
};
// would love to use promises here, but then websql
// ends the transaction early
var tasks = [
runMigration2,
runMigration3,
runMigration4,
runMigration5,
runMigration6,
runMigration7,
setupDone
];
// run each migration sequentially
var i = dbVersion;
var nextMigration = function (tx) {
tasks[i - 1](tx, nextMigration);
i++;
};
nextMigration(tx);
}
}
function setup() {
db.transaction(function (tx) {
// first check the encoding
checkEncoding(tx, function () {
// then get the version
fetchVersion(tx);
});
}, websqlError(callback), dbCreated);
}
function fetchVersion(tx) {
var sql = 'SELECT sql FROM sqlite_master WHERE tbl_name = ' + META_STORE;
tx.executeSql(sql, [], function (tx, result) {
if (!result.rows.length) {
// database hasn't even been created yet (version 0)
onGetVersion(tx, 0);
} else if (!/db_version/.test(result.rows.item(0).sql)) {
// table was created, but without the new db_version column,
// so add it.
tx.executeSql('ALTER TABLE ' + META_STORE +
' ADD COLUMN db_version INTEGER', [], function () {
// before version 2, this column didn't even exist
onGetVersion(tx, 1);
});
} else { // column exists, we can safely get it
tx.executeSql('SELECT db_version FROM ' + META_STORE,
[], function (tx, result) {
var dbVersion = result.rows.item(0).db_version;
onGetVersion(tx, dbVersion);
});
}
});
}
setup();
api.type = function () {
return 'websql';
};
api._id = toPromise(function (callback) {
callback(null, instanceId);
});
api._info = function (callback) {
db.readTransaction(function (tx) {
countDocs(tx, function (docCount) {
var sql = 'SELECT MAX(seq) AS seq FROM ' + BY_SEQ_STORE;
tx.executeSql(sql, [], function (tx, res) {
var updateSeq = res.rows.item(0).seq || 0;
callback(null, {
doc_count: docCount,
update_seq: updateSeq,
// for debugging
sqlite_plugin: db._sqlitePlugin,
websql_encoding: encoding
});
});
});
}, websqlError(callback));
};
api._bulkDocs = function (req, reqOpts, callback) {
websqlBulkDocs(opts, req, reqOpts, api, db, websqlChanges, callback);
};
api._get = function (id, opts, callback) {
var doc;
var metadata;
var err;
var tx = opts.ctx;
if (!tx) {
return db.readTransaction(function (txn) {
api._get(id, extend({ctx: txn}, opts), callback);
});
}
function finish() {
callback(err, {doc: doc, metadata: metadata, ctx: tx});
}
var sql;
var sqlArgs;
if (opts.rev) {
sql = select(
SELECT_DOCS,
[DOC_STORE, BY_SEQ_STORE],
DOC_STORE + '.id=' + BY_SEQ_STORE + '.doc_id',
[BY_SEQ_STORE + '.doc_id=?', BY_SEQ_STORE + '.rev=?']);
sqlArgs = [id, opts.rev];
} else {
sql = select(
SELECT_DOCS,
[DOC_STORE, BY_SEQ_STORE],
DOC_STORE_AND_BY_SEQ_JOINER,
DOC_STORE + '.id=?');
sqlArgs = [id];
}
tx.executeSql(sql, sqlArgs, function (a, results) {
if (!results.rows.length) {
err = createError(MISSING_DOC, 'missing');
return finish();
}
var item = results.rows.item(0);
metadata = safeJsonParse(item.metadata);
if (item.deleted && !opts.rev) {
err = createError(MISSING_DOC, 'deleted');
return finish();
}
doc = unstringifyDoc(item.data, metadata.id, item.rev);
finish();
});
};
function countDocs(tx, callback) {
if (api._docCount !== -1) {
return callback(api._docCount);
}
// count the total rows
var sql = select(
'COUNT(' + DOC_STORE + '.id) AS \'num\'',
[DOC_STORE, BY_SEQ_STORE],
DOC_STORE_AND_BY_SEQ_JOINER,
BY_SEQ_STORE + '.deleted=0');
tx.executeSql(sql, [], function (tx, result) {
api._docCount = result.rows.item(0).num;
callback(api._docCount);
});
}
api._allDocs = function (opts, callback) {
var results = [];
var totalRows;
var start = 'startkey' in opts ? opts.startkey : false;
var end = 'endkey' in opts ? opts.endkey : false;
var key = 'key' in opts ? opts.key : false;
var descending = 'descending' in opts ? opts.descending : false;
var limit = 'limit' in opts ? opts.limit : -1;
var offset = 'skip' in opts ? opts.skip : 0;
var inclusiveEnd = opts.inclusive_end !== false;
var sqlArgs = [];
var criteria = [];
if (key !== false) {
criteria.push(DOC_STORE + '.id = ?');
sqlArgs.push(key);
} else if (start !== false || end !== false) {
if (start !== false) {
criteria.push(DOC_STORE + '.id ' + (descending ? '<=' : '>=') + ' ?');
sqlArgs.push(start);
}
if (end !== false) {
var comparator = descending ? '>' : '<';
if (inclusiveEnd) {
comparator += '=';
}
criteria.push(DOC_STORE + '.id ' + comparator + ' ?');
sqlArgs.push(end);
}
if (key !== false) {
criteria.push(DOC_STORE + '.id = ?');
sqlArgs.push(key);
}
}
if (opts.deleted !== 'ok') {
// report deleted if keys are specified
criteria.push(BY_SEQ_STORE + '.deleted = 0');
}
db.readTransaction(function (tx) {
// first count up the total rows
countDocs(tx, function (count) {
totalRows = count;
if (limit === 0) {
return;
}
// then actually fetch the documents
var sql = select(
SELECT_DOCS,
[DOC_STORE, BY_SEQ_STORE],
DOC_STORE_AND_BY_SEQ_JOINER,
criteria,
DOC_STORE + '.id ' + (descending ? 'DESC' : 'ASC')
);
sql += ' LIMIT ' + limit + ' OFFSET ' + offset;
tx.executeSql(sql, sqlArgs, function (tx, result) {
for (var i = 0, l = result.rows.length; i < l; i++) {
var item = result.rows.item(i);
var metadata = safeJsonParse(item.metadata);
var id = metadata.id;
var data = unstringifyDoc(item.data, id, item.rev);
var winningRev = data._rev;
var doc = {
id: id,
key: id,
value: {rev: winningRev}
};
if (opts.include_docs) {
doc.doc = data;
doc.doc._rev = winningRev;
if (opts.conflicts) {
doc.doc._conflicts = collectConflicts(metadata);
}
fetchAttachmentsIfNecessary(doc.doc, opts, api, tx);
}
if (item.deleted) {
if (opts.deleted === 'ok') {
doc.value.deleted = true;
doc.doc = null;
} else {
continue;
}
}
results.push(doc);
}
});
});
}, websqlError(callback), function () {
callback(null, {
total_rows: totalRows,
offset: opts.skip,
rows: results
});
});
};
api._changes = function (opts) {
opts = clone(opts);
if (opts.continuous) {
var id = api._name + ':' + uuid();
websqlChanges.addListener(api._name, id, api, opts);
websqlChanges.notify(api._name);
return {
cancel: function () {
websqlChanges.removeListener(api._name, id);
}
};
}
var descending = opts.descending;
// Ignore the `since` parameter when `descending` is true
opts.since = opts.since && !descending ? opts.since : 0;
var limit = 'limit' in opts ? opts.limit : -1;
if (limit === 0) {
limit = 1; // per CouchDB _changes spec
}
var returnDocs;
if ('return_docs' in opts) {
returnDocs = opts.return_docs;
} else if ('returnDocs' in opts) {
// TODO: Remove 'returnDocs' in favor of 'return_docs' in a future release
returnDocs = opts.returnDocs;
} else {
returnDocs = true;
}
var results = [];
var numResults = 0;
function fetchChanges() {
var selectStmt =
DOC_STORE + '.json AS metadata, ' +
DOC_STORE + '.max_seq AS maxSeq, ' +
BY_SEQ_STORE + '.json AS winningDoc, ' +
BY_SEQ_STORE + '.rev AS winningRev ';
var from = DOC_STORE + ' JOIN ' + BY_SEQ_STORE;
var joiner = DOC_STORE + '.id=' + BY_SEQ_STORE + '.doc_id' +
' AND ' + DOC_STORE + '.winningseq=' + BY_SEQ_STORE + '.seq';
var criteria = ['maxSeq > ?'];
var sqlArgs = [opts.since];
if (opts.doc_ids) {
criteria.push(DOC_STORE + '.id IN ' + qMarks(opts.doc_ids.length));
sqlArgs = sqlArgs.concat(opts.doc_ids);
}
var orderBy = 'maxSeq ' + (descending ? 'DESC' : 'ASC');
var sql = select(selectStmt, from, joiner, criteria, orderBy);
var filter = filterChange(opts);
if (!opts.view && !opts.filter) {
// we can just limit in the query
sql += ' LIMIT ' + limit;
}
var lastSeq = opts.since || 0;
db.readTransaction(function (tx) {
tx.executeSql(sql, sqlArgs, function (tx, result) {
function reportChange(change) {
return function () {
opts.onChange(change);
};
}
for (var i = 0, l = result.rows.length; i < l; i++) {
var item = result.rows.item(i);
var metadata = safeJsonParse(item.metadata);
lastSeq = item.maxSeq;
var doc = unstringifyDoc(item.winningDoc, metadata.id,
item.winningRev);
var change = opts.processChange(doc, metadata, opts);
change.seq = item.maxSeq;
var filtered = filter(change);
if (typeof filtered === 'object') {
return opts.complete(filtered);
}
if (filtered) {
numResults++;
if (returnDocs) {
results.push(change);
}
// process the attachment immediately
// for the benefit of live listeners
if (opts.attachments && opts.include_docs) {
fetchAttachmentsIfNecessary(doc, opts, api, tx,
reportChange(change));
} else {
reportChange(change)();
}
}
if (numResults === limit) {
break;
}
}
});
}, websqlError(opts.complete), function () {
if (!opts.continuous) {
opts.complete(null, {
results: results,
last_seq: lastSeq
});
}
});
}
fetchChanges();
};
api._close = function (callback) {
//WebSQL databases do not need to be closed
callback();
};
api._getAttachment = function (attachment, opts, callback) {
var res;
var tx = opts.ctx;
var digest = attachment.digest;
var type = attachment.content_type;
var sql = 'SELECT escaped, ' +
'CASE WHEN escaped = 1 THEN body ELSE HEX(body) END AS body FROM ' +
ATTACH_STORE + ' WHERE digest=?';
tx.executeSql(sql, [digest], function (tx, result) {
// websql has a bug where \u0000 causes early truncation in strings
// and blobs. to work around this, we used to use the hex() function,
// but that's not performant. after migration 6, we remove \u0000
// and add it back in afterwards
var item = result.rows.item(0);
var data = item.escaped ? unescapeBlob(item.body) :
parseHexString(item.body, encoding);
if (opts.binary) {
res = binStringToBlob(data, type);
} else {
res = btoa(data);
}
callback(null, res);
});
};
api._getRevisionTree = function (docId, callback) {
db.readTransaction(function (tx) {
var sql = 'SELECT json AS metadata FROM ' + DOC_STORE + ' WHERE id = ?';
tx.executeSql(sql, [docId], function (tx, result) {
if (!result.rows.length) {
callback(createError(MISSING_DOC));
} else {
var data = safeJsonParse(result.rows.item(0).metadata);
callback(null, data.rev_tree);
}
});
});
};
api._doCompaction = function (docId, revs, callback) {
if (!revs.length) {
return callback();
}
db.transaction(function (tx) {
// update doc store
var sql = 'SELECT json AS metadata FROM ' + DOC_STORE + ' WHERE id = ?';
tx.executeSql(sql, [docId], function (tx, result) {
var metadata = safeJsonParse(result.rows.item(0).metadata);
traverseRevTree(metadata.rev_tree, function (isLeaf, pos,
revHash, ctx, opts) {
var rev = pos + '-' + revHash;
if (revs.indexOf(rev) !== -1) {
opts.status = 'missing';
}
});
var sql = 'UPDATE ' + DOC_STORE + ' SET json = ? WHERE id = ?';
tx.executeSql(sql, [safeJsonStringify(metadata), docId]);
});
compactRevs(revs, docId, tx);
}, websqlError(callback), function () {
callback();
});
};
api._getLocal = function (id, callback) {
db.readTransaction(function (tx) {
var sql = 'SELECT json, rev FROM ' + LOCAL_STORE + ' WHERE id=?';
tx.executeSql(sql, [id], function (tx, res) {
if (res.rows.length) {
var item = res.rows.item(0);
var doc = unstringifyDoc(item.json, id, item.rev);
callback(null, doc);
} else {
callback(createError(MISSING_DOC));
}
});
});
};
api._putLocal = function (doc, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
delete doc._revisions; // ignore this, trust the rev
var oldRev = doc._rev;
var id = doc._id;
var newRev;
if (!oldRev) {
newRev = doc._rev = '0-1';
} else {
newRev = doc._rev = '0-' + (parseInt(oldRev.split('-')[1], 10) + 1);
}
var json = stringifyDoc(doc);
var ret;
function putLocal(tx) {
var sql;
var values;
if (oldRev) {
sql = 'UPDATE ' + LOCAL_STORE + ' SET rev=?, json=? ' +
'WHERE id=? AND rev=?';
values = [newRev, json, id, oldRev];
} else {
sql = 'INSERT INTO ' + LOCAL_STORE + ' (id, rev, json) VALUES (?,?,?)';
values = [id, newRev, json];
}
tx.executeSql(sql, values, function (tx, res) {
if (res.rowsAffected) {
ret = {ok: true, id: id, rev: newRev};
if (opts.ctx) { // return immediately
callback(null, ret);
}
} else {
callback(createError(REV_CONFLICT));
}
}, function () {
callback(createError(REV_CONFLICT));
return false; // ack that we handled the error
});
}
if (opts.ctx) {
putLocal(opts.ctx);
} else {
db.transaction(putLocal, websqlError(callback), function () {
if (ret) {
callback(null, ret);
}
});
}
};
api._removeLocal = function (doc, opts, callback) {
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
var ret;
function removeLocal(tx) {
var sql = 'DELETE FROM ' + LOCAL_STORE + ' WHERE id=? AND rev=?';
var params = [doc._id, doc._rev];
tx.executeSql(sql, params, function (tx, res) {
if (!res.rowsAffected) {
return callback(createError(MISSING_DOC));
}
ret = {ok: true, id: doc._id, rev: '0-0'};
if (opts.ctx) { // return immediately
callback(null, ret);
}
});
}
if (opts.ctx) {
removeLocal(opts.ctx);
} else {
db.transaction(removeLocal, websqlError(callback), function () {
if (ret) {
callback(null, ret);
}
});
}
};
api._destroy = function (opts, callback) {
websqlChanges.removeAllListeners(api._name);
db.transaction(function (tx) {
var stores = [DOC_STORE, BY_SEQ_STORE, ATTACH_STORE, META_STORE,
LOCAL_STORE, ATTACH_AND_SEQ_STORE];
stores.forEach(function (store) {
tx.executeSql('DROP TABLE IF EXISTS ' + store, []);
});
}, websqlError(callback), function () {
if (hasLocalStorage()) {
delete window.localStorage['_pouch__websqldb_' + api._name];
delete window.localStorage[api._name];
}
callback(null, {'ok': true});
});
};
}
export default WebSqlPouch;