| 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; |