text: import { assign, clone, isRemote, toPromise, upsert } from 'pouchdb-utils';
  import Promise from 'pouchdb-promise';
  import { compare, filterInMemoryFields, getFieldFromDoc, getKey, getValue, massageSelector, parseField, setFieldInDoc } from 'pouchdb-selector-core';
  import abstractMapReduce from 'pouchdb-abstract-mapreduce';
  import { collate } from 'pouchdb-collate';
  import { stringMd5 } from 'pouchdb-md5';
  
  // we restucture the supplied JSON considerably, because the official
  // Mango API is very particular about a lot of this stuff, but we like
  // to be liberal with what we accept in order to prevent mental
  // breakdowns in our users
  function massageCreateIndexRequest(requestDef) {
    requestDef = clone(requestDef);
  
    if (!requestDef.index) {
      requestDef.index = {};
    }
  
    ['type', 'name', 'ddoc'].forEach(function (key) {
      if (requestDef.index[key]) {
        requestDef[key] = requestDef.index[key];
        delete requestDef.index[key];
      }
    });
  
    if (requestDef.fields) {
      requestDef.index.fields = requestDef.fields;
      delete requestDef.fields;
    }
  
    if (!requestDef.type) {
      requestDef.type = 'json';
    }
    return requestDef;
  }
  
  function createIndex(db, requestDef, callback) {
    requestDef = massageCreateIndexRequest(requestDef);
  
    db.request({
      method: 'POST',
      url: '_index',
      body: requestDef
    }, callback);
  }
  
  function find(db, requestDef, callback) {
    db.request({
      method: 'POST',
      url: '_find',
      body: requestDef
    }, callback);
  }
  
  function getIndexes(db, callback) {
    db.request({
      method: 'GET',
      url: '_index'
    }, callback);
  }
  
  function deleteIndex(db, indexDef, callback) {
  
  
    var ddoc = indexDef.ddoc;
    var type = indexDef.type || 'json';
    var name = indexDef.name;
  
    if (!ddoc) {
      return callback(new Error('you must provide an index\'s ddoc'));
    }
  
    if (!name) {
      return callback(new Error('you must provide an index\'s name'));
    }
  
    var url = '_index/' + [ddoc, type, name].map(encodeURIComponent).join('/');
  
    db.request({
      method: 'DELETE',
      url: url
    }, callback);
  }
  
  function getArguments(fun) {
    return function () {
      var len = arguments.length;
      var args = new Array(len);
      var i = -1;
      while (++i < len) {
        args[i] = arguments[i];
      }
      return fun.call(this, args);
    };
  }
  function callbackify(fun) {
    return getArguments(function (args) {
      var cb = args.pop();
      var promise = fun.apply(this, args);
      promisedCallback(promise, cb);
      return promise;
    });
  }
  
  function promisedCallback(promise, callback) {
    promise.then(function (res) {
      process.nextTick(function () {
        callback(null, res);
      });
    }, function (reason) {
      process.nextTick(function () {
        callback(reason);
      });
    });
    return promise;
  }
  
  var flatten = getArguments(function (args) {
    var res = [];
    for (var i = 0, len = args.length; i < len; i++) {
      var subArr = args[i];
      if (Array.isArray(subArr)) {
        res = res.concat(flatten.apply(null, subArr));
      } else {
        res.push(subArr);
      }
    }
    return res;
  });
  
  function mergeObjects(arr) {
    var res = {};
    for (var i = 0, len = arr.length; i < len; i++) {
      res = assign(res, arr[i]);
    }
    return res;
  }
  
  // Selects a list of fields defined in dot notation from one doc
  // and copies them to a new doc. Like underscore _.pick but supports nesting.
  function pick(obj, arr) {
    var res = {};
    for (var i = 0, len = arr.length; i < len; i++) {
      var parsedField = parseField(arr[i]);
      var value = getFieldFromDoc(obj, parsedField);
      if (typeof value !== 'undefined') {
        setFieldInDoc(res, parsedField, value);
      }
    }
    return res;
  }
  
  // e.g. ['a'], ['a', 'b'] is true, but ['b'], ['a', 'b'] is false
  function oneArrayIsSubArrayOfOther(left, right) {
  
    for (var i = 0, len = Math.min(left.length, right.length); i < len; i++) {
      if (left[i] !== right[i]) {
        return false;
      }
    }
    return true;
  }
  
  // e.g.['a', 'b', 'c'], ['a', 'b'] is false
  function oneArrayIsStrictSubArrayOfOther(left, right) {
  
    if (left.length > right.length) {
      return false;
    }
  
    return oneArrayIsSubArrayOfOther(left, right);
  }
  
  // same as above, but treat the left array as an unordered set
  // e.g. ['b', 'a'], ['a', 'b', 'c'] is true, but ['c'], ['a', 'b', 'c'] is false
  function oneSetIsSubArrayOfOther(left, right) {
    left = left.slice();
    for (var i = 0, len = right.length; i < len; i++) {
      var field = right[i];
      if (!left.length) {
        break;
      }
      var leftIdx = left.indexOf(field);
      if (leftIdx === -1) {
        return false;
      } else {
        left.splice(leftIdx, 1);
      }
    }
    return true;
  }
  
  function arrayToObject(arr) {
    var res = {};
    for (var i = 0, len = arr.length; i < len; i++) {
      res[arr[i]] = true;
    }
    return res;
  }
  
  function max(arr, fun) {
    var max = null;
    var maxScore = -1;
    for (var i = 0, len = arr.length; i < len; i++) {
      var element = arr[i];
      var score = fun(element);
      if (score > maxScore) {
        maxScore = score;
        max = element;
      }
    }
    return max;
  }
  
  function arrayEquals(arr1, arr2) {
    if (arr1.length !== arr2.length) {
      return false;
    }
    for (var i = 0, len = arr1.length; i < len; i++) {
      if (arr1[i] !== arr2[i]) {
        return false;
      }
    }
    return true;
  }
  
  function uniq(arr) {
    var obj = {};
    for (var i = 0; i < arr.length; i++) {
      obj['$' + arr[i]] = true;
    }
    return Object.keys(obj).map(function (key) {
      return key.substring(1);
    });
  }
  
  //
  // One thing about these mappers:
  //
  // Per the advice of John-David Dalton (http://youtu.be/NthmeLEhDDM),
  // what you want to do in this case is optimize for the smallest possible
  // function, since that's the thing that gets run over and over again.
  //
  // This code would be a lot simpler if all the if/elses were inside
  // the function, but it would also be a lot less performant.
  //
  
  
  function createDeepMultiMapper(fields, emit) {
    return function (doc) {
      var toEmit = [];
      for (var i = 0, iLen = fields.length; i < iLen; i++) {
        var parsedField = parseField(fields[i]);
        var value = doc;
        for (var j = 0, jLen = parsedField.length; j < jLen; j++) {
          var key = parsedField[j];
          value = value[key];
          if (!value) {
            return; // don't emit
          }
        }
        toEmit.push(value);
      }
      emit(toEmit);
    };
  }
  
  function createDeepSingleMapper(field, emit) {
    var parsedField = parseField(field);
    return function (doc) {
      var value = doc;
      for (var i = 0, len = parsedField.length; i < len; i++) {
        var key = parsedField[i];
        value = value[key];
        if (!value) {
          return; // do nothing
        }
      }
      emit(value);
    };
  }
  
  function createShallowSingleMapper(field, emit) {
    return function (doc) {
      emit(doc[field]);
    };
  }
  
  function createShallowMultiMapper(fields, emit) {
    return function (doc) {
      var toEmit = [];
      for (var i = 0, len = fields.length; i < len; i++) {
        toEmit.push(doc[fields[i]]);
      }
      emit(toEmit);
    };
  }
  
  function checkShallow(fields) {
    for (var i = 0, len = fields.length; i < len; i++) {
      var field = fields[i];
      if (field.indexOf('.') !== -1) {
        return false;
      }
    }
    return true;
  }
  
  function createMapper(fields, emit) {
    var isShallow = checkShallow(fields);
    var isSingle = fields.length === 1;
  
    // notice we try to optimize for the most common case,
    // i.e. single shallow indexes
    if (isShallow) {
      if (isSingle) {
        return createShallowSingleMapper(fields[0], emit);
      } else { // multi
        return createShallowMultiMapper(fields, emit);
      }
    } else { // deep
      if (isSingle) {
        return createDeepSingleMapper(fields[0], emit);
      } else { // multi
        return createDeepMultiMapper(fields, emit);
      }
    }
  }
  
  function mapper(mapFunDef, emit) {
    // mapFunDef is a list of fields
  
    var fields = Object.keys(mapFunDef.fields);
  
    return createMapper(fields, emit);
  }
  
  /* istanbul ignore next */
  function reducer(/*reduceFunDef*/) {
    throw new Error('reduce not supported');
  }
  
  function ddocValidator(ddoc, viewName) {
    var view = ddoc.views[viewName];
    // This doesn't actually need to be here apparently, but
    // I feel safer keeping it.
    /* istanbul ignore if */
    if (!view.map || !view.map.fields) {
      throw new Error('ddoc ' + ddoc._id +' with view ' + viewName +
        ' doesn\'t have map.fields defined. ' +
        'maybe it wasn\'t created by this plugin?');
    }
  }
  
  var abstractMapper = abstractMapReduce(
    /* localDocName */ 'indexes',
    mapper,
    reducer,
    ddocValidator
  );
  
  // normalize the "sort" value
  function massageSort(sort) {
    if (!Array.isArray(sort)) {
      throw new Error('invalid sort json - should be an array');
    }
    return sort.map(function (sorting) {
      if (typeof sorting === 'string') {
        var obj = {};
        obj[sorting] = 'asc';
        return obj;
      } else {
        return sorting;
      }
    });
  }
  
  function massageIndexDef(indexDef) {
    indexDef.fields = indexDef.fields.map(function (field) {
      if (typeof field === 'string') {
        var obj = {};
        obj[field] = 'asc';
        return obj;
      }
      return field;
    });
    return indexDef;
  }
  
  function getKeyFromDoc(doc, index) {
    var res = [];
    for (var i = 0; i < index.def.fields.length; i++) {
      var field = getKey(index.def.fields[i]);
      res.push(doc[field]);
    }
    return res;
  }
  
  // have to do this manually because REASONS. I don't know why
  // CouchDB didn't implement inclusive_start
  function filterInclusiveStart(rows, targetValue, index) {
    var indexFields = index.def.fields;
    for (var i = 0, len = rows.length; i < len; i++) {
      var row = rows[i];
  
      // shave off any docs at the beginning that are <= the
      // target value
  
      var docKey = getKeyFromDoc(row.doc, index);
      if (indexFields.length === 1) {
        docKey = docKey[0]; // only one field, not multi-field
      } else { // more than one field in index
        // in the case where e.g. the user is searching {$gt: {a: 1}}
        // but the index is [a, b], then we need to shorten the doc key
        while (docKey.length > targetValue.length) {
          docKey.pop();
        }
      }
      //ABS as we just looking for values that don't match
      if (Math.abs(collate(docKey, targetValue)) > 0) {
        // no need to filter any further; we're past the key
        break;
      }
    }
    return i > 0 ? rows.slice(i) : rows;
  }
  
  function reverseOptions(opts) {
    var newOpts = clone(opts);
    delete newOpts.startkey;
    delete newOpts.endkey;
    delete newOpts.inclusive_start;
    delete newOpts.inclusive_end;
  
    if ('endkey' in opts) {
      newOpts.startkey = opts.endkey;
    }
    if ('startkey' in opts) {
      newOpts.endkey = opts.startkey;
    }
    if ('inclusive_start' in opts) {
      newOpts.inclusive_end = opts.inclusive_start;
    }
    if ('inclusive_end' in opts) {
      newOpts.inclusive_start = opts.inclusive_end;
    }
    return newOpts;
  }
  
  function validateIndex(index) {
    var ascFields = index.fields.filter(function (field) {
      return getValue(field) === 'asc';
    });
    if (ascFields.length !== 0 && ascFields.length !== index.fields.length) {
      throw new Error('unsupported mixed sorting');
    }
  }
  
  function validateSort(requestDef, index) {
    if (index.defaultUsed && requestDef.sort) {
      var noneIdSorts = requestDef.sort.filter(function (sortItem) {
        return Object.keys(sortItem)[0] !== '_id';
      }).map(function (sortItem) {
        return Object.keys(sortItem)[0];
      });
  
      if (noneIdSorts.length > 0) {
        throw new Error('Cannot sort on field(s) "' + noneIdSorts.join(',') +
        '" when using the default index');
      }
    }
  
    if (index.defaultUsed) {
      return;
    }
  }
  
  function validateFindRequest(requestDef) {
    if (typeof requestDef.selector !== 'object') {
      throw new Error('you must provide a selector when you find()');
    }
  
    /*var selectors = requestDef.selector['$and'] || [requestDef.selector];
    for (var i = 0; i < selectors.length; i++) {
      var selector = selectors[i];
      var keys = Object.keys(selector);
      if (keys.length === 0) {
        throw new Error('invalid empty selector');
      }
      //var selection = selector[keys[0]];
      /*if (Object.keys(selection).length !== 1) {
        throw new Error('invalid selector: ' + JSON.stringify(selection) +
          ' - it must have exactly one key/value');
      }
    }*/
  }
  
  // determine the maximum number of fields
  // we're going to need to query, e.g. if the user
  // has selection ['a'] and sorting ['a', 'b'], then we
  // need to use the longer of the two: ['a', 'b']
  function getUserFields(selector, sort) {
    var selectorFields = Object.keys(selector);
    var sortFields = sort? sort.map(getKey) : [];
    var userFields;
    if (selectorFields.length >= sortFields.length) {
      userFields = selectorFields;
    } else {
      userFields = sortFields;
    }
  
    if (sortFields.length === 0) {
      return {
        fields: userFields
      };
    }
  
    // sort according to the user's preferred sorting
    userFields = userFields.sort(function (left, right) {
      var leftIdx = sortFields.indexOf(left);
      if (leftIdx === -1) {
        leftIdx = Number.MAX_VALUE;
      }
      var rightIdx = sortFields.indexOf(right);
      if (rightIdx === -1) {
        rightIdx = Number.MAX_VALUE;
      }
      return leftIdx < rightIdx ? -1 : leftIdx > rightIdx ? 1 : 0;
    });
  
    return {
      fields: userFields,
      sortOrder: sort.map(getKey)
    };
  }
  
  function createIndex$1(db, requestDef) {
    requestDef = massageCreateIndexRequest(requestDef);
    var originalIndexDef = clone(requestDef.index);
    requestDef.index = massageIndexDef(requestDef.index);
  
    validateIndex(requestDef.index);
  
    var md5 = stringMd5(JSON.stringify(requestDef));
  
    var viewName = requestDef.name || ('idx-' + md5);
  
    var ddocName = requestDef.ddoc || ('idx-' + md5);
    var ddocId = '_design/' + ddocName;
  
    var hasInvalidLanguage = false;
    var viewExists = false;
  
    function updateDdoc(doc) {
      if (doc._rev && doc.language !== 'query') {
        hasInvalidLanguage = true;
      }
      doc.language = 'query';
      doc.views = doc.views || {};
  
      viewExists = !!doc.views[viewName];
  
      if (viewExists) {
        return false;
      }
  
      doc.views[viewName] = {
        map: {
          fields: mergeObjects(requestDef.index.fields)
        },
        reduce: '_count',
        options: {
          def: originalIndexDef
        }
      };
  
      return doc;
    }
  
    db.constructor.emit('debug', ['find', 'creating index', ddocId]);
  
    return upsert(db, ddocId, updateDdoc).then(function () {
      if (hasInvalidLanguage) {
        throw new Error('invalid language for ddoc with id "' +
        ddocId +
        '" (should be "query")');
      }
    }).then(function () {
      // kick off a build
      // TODO: abstract-pouchdb-mapreduce should support auto-updating
      // TODO: should also use update_after, but pouchdb/pouchdb#3415 blocks me
      var signature = ddocName + '/' + viewName;
      return abstractMapper.query.call(db, signature, {
        limit: 0,
        reduce: false
      }).then(function () {
        return {
          id: ddocId,
          name: viewName,
          result: viewExists ? 'exists' : 'created'
        };
      });
    });
  }
  
  function getIndexes$1(db) {
    // just search through all the design docs and filter in-memory.
    // hopefully there aren't that many ddocs.
    return db.allDocs({
      startkey: '_design/',
      endkey: '_design/\uffff',
      include_docs: true
    }).then(function (allDocsRes) {
      var res = {
        indexes: [{
          ddoc: null,
          name: '_all_docs',
          type: 'special',
          def: {
            fields: [{_id: 'asc'}]
          }
        }]
      };
  
      res.indexes = flatten(res.indexes, allDocsRes.rows.filter(function (row) {
        return row.doc.language === 'query';
      }).map(function (row) {
        var viewNames = row.doc.views !== undefined ? Object.keys(row.doc.views) : [];
  
        return viewNames.map(function (viewName) {
          var view = row.doc.views[viewName];
          return {
            ddoc: row.id,
            name: viewName,
            type: 'json',
            def: massageIndexDef(view.options.def)
          };
        });
      }));
  
      // these are sorted by view name for some reason
      res.indexes.sort(function (left, right) {
        return compare(left.name, right.name);
      });
      res.total_rows = res.indexes.length;
      return res;
    });
  }
  
  // couchdb lowest collation value
  var COLLATE_LO = null;
  
  // couchdb highest collation value (TODO: well not really, but close enough amirite)
  var COLLATE_HI = {"\uffff": {}};
  
  // couchdb second-lowest collation value
  
  function checkFieldInIndex(index, field) {
    var indexFields = index.def.fields.map(getKey);
    for (var i = 0, len = indexFields.length; i < len; i++) {
      var indexField = indexFields[i];
      if (field === indexField) {
        return true;
      }
    }
    return false;
  }
  
  // so when you do e.g. $eq/$eq, we can do it entirely in the database.
  // but when you do e.g. $gt/$eq, the first part can be done
  // in the database, but the second part has to be done in-memory,
  // because $gt has forced us to lose precision.
  // so that's what this determines
  function userOperatorLosesPrecision(selector, field) {
    var matcher = selector[field];
    var userOperator = getKey(matcher);
  
    return userOperator !== '$eq';
  }
  
  // sort the user fields by their position in the index,
  // if they're in the index
  function sortFieldsByIndex(userFields, index) {
    var indexFields = index.def.fields.map(getKey);
  
    return userFields.slice().sort(function (a, b) {
      var aIdx = indexFields.indexOf(a);
      var bIdx = indexFields.indexOf(b);
      if (aIdx === -1) {
        aIdx = Number.MAX_VALUE;
      }
      if (bIdx === -1) {
        bIdx = Number.MAX_VALUE;
      }
      return compare(aIdx, bIdx);
    });
  }
  
  // first pass to try to find fields that will need to be sorted in-memory
  function getBasicInMemoryFields(index, selector, userFields) {
  
    userFields = sortFieldsByIndex(userFields, index);
  
    // check if any of the user selectors lose precision
    var needToFilterInMemory = false;
    for (var i = 0, len = userFields.length; i < len; i++) {
      var field = userFields[i];
      if (needToFilterInMemory || !checkFieldInIndex(index, field)) {
        return userFields.slice(i);
      }
      if (i < len - 1 && userOperatorLosesPrecision(selector, field)) {
        needToFilterInMemory = true;
      }
    }
    return [];
  }
  
  function getInMemoryFieldsFromNe(selector) {
    var fields = [];
    Object.keys(selector).forEach(function (field) {
      var matcher = selector[field];
      Object.keys(matcher).forEach(function (operator) {
        if (operator === '$ne') {
          fields.push(field);
        }
      });
    });
    return fields;
  }
  
  function getInMemoryFields(coreInMemoryFields, index, selector, userFields) {
    var result = flatten(
      // in-memory fields reported as necessary by the query planner
      coreInMemoryFields,
      // combine with another pass that checks for any we may have missed
      getBasicInMemoryFields(index, selector, userFields),
      // combine with another pass that checks for $ne's
      getInMemoryFieldsFromNe(selector)
    );
  
    return sortFieldsByIndex(uniq(result), index);
  }
  
  // check that at least one field in the user's query is represented
  // in the index. order matters in the case of sorts
  function checkIndexFieldsMatch(indexFields, sortOrder, fields) {
    if (sortOrder) {
      // array has to be a strict subarray of index array. furthermore,
      // the sortOrder fields need to all be represented in the index
      var sortMatches = oneArrayIsStrictSubArrayOfOther(sortOrder, indexFields);
      var selectorMatches = oneArrayIsSubArrayOfOther(fields, indexFields);
  
      return sortMatches && selectorMatches;
    }
  
    // all of the user's specified fields still need to be
    // on the left side of the index array, although the order
    // doesn't matter
    return oneSetIsSubArrayOfOther(fields, indexFields);
  }
  
  var logicalMatchers = ['$eq', '$gt', '$gte', '$lt', '$lte'];
  function isNonLogicalMatcher(matcher) {
    return logicalMatchers.indexOf(matcher) === -1;
  }
  
  // check all the index fields for usages of '$ne'
  // e.g. if the user queries {foo: {$ne: 'foo'}, bar: {$eq: 'bar'}},
  // then we can neither use an index on ['foo'] nor an index on
  // ['foo', 'bar'], but we can use an index on ['bar'] or ['bar', 'foo']
  function checkFieldsLogicallySound(indexFields, selector) {
    var firstField = indexFields[0];
    var matcher = selector[firstField];
  
    if (typeof matcher === 'undefined') {
      return true;
    }
  
    var hasLogicalOperator = Object.keys(matcher).some(function (matcherKey) {
      return !(isNonLogicalMatcher(matcherKey));
    });
  
    if (!hasLogicalOperator) {
      return false;
    }
  
    var isInvalidNe = Object.keys(matcher).length === 1 &&
      getKey(matcher) === '$ne';
  
    return !isInvalidNe;
  }
  
  function checkIndexMatches(index, sortOrder, fields, selector) {
  
    var indexFields = index.def.fields.map(getKey);
  
    var fieldsMatch = checkIndexFieldsMatch(indexFields, sortOrder, fields);
  
    if (!fieldsMatch) {
      return false;
    }
  
    return checkFieldsLogicallySound(indexFields, selector);
  }
  
  //
  // the algorithm is very simple:
  // take all the fields the user supplies, and if those fields
  // are a strict subset of the fields in some index,
  // then use that index
  //
  //
  function findMatchingIndexes(selector, userFields, sortOrder, indexes) {
  
    return indexes.reduce(function (res, index) {
      var indexMatches = checkIndexMatches(index, sortOrder, userFields, selector);
      if (indexMatches) {
        res.push(index);
      }
      return res;
    }, []);
  }
  
  // find the best index, i.e. the one that matches the most fields
  // in the user's query
  function findBestMatchingIndex(selector, userFields, sortOrder, indexes) {
  
    var matchingIndexes = findMatchingIndexes(selector, userFields, sortOrder, indexes);
  
    if (matchingIndexes.length === 0) {
      //return `all_docs` as a default index;
      //I'm assuming that _all_docs is always first
      var defaultIndex = indexes[0];
      defaultIndex.defaultUsed = true;
      return defaultIndex;
    }
    if (matchingIndexes.length === 1) {
      return matchingIndexes[0];
    }
  
    var userFieldsMap = arrayToObject(userFields);
  
    function scoreIndex(index) {
      var indexFields = index.def.fields.map(getKey);
      var score = 0;
      for (var i = 0, len = indexFields.length; i < len; i++) {
        var indexField = indexFields[i];
        if (userFieldsMap[indexField]) {
          score++;
        }
      }
      return score;
    }
  
    return max(matchingIndexes, scoreIndex);
  }
  
  function getSingleFieldQueryOptsFor(userOperator, userValue) {
    switch (userOperator) {
      case '$eq':
        return {key: userValue};
      case '$lte':
        return {endkey: userValue};
      case '$gte':
        return {startkey: userValue};
      case '$lt':
        return {
          endkey: userValue,
          inclusive_end: false
        };
      case '$gt':
        return {
          startkey: userValue,
          inclusive_start: false
        };
    }
  }
  
  function getSingleFieldCoreQueryPlan(selector, index) {
    var field = getKey(index.def.fields[0]);
    //ignoring this because the test to exercise the branch is skipped at the moment
    /* istanbul ignore next */
    var matcher = selector[field] || {};
    var inMemoryFields = [];
  
    var userOperators = Object.keys(matcher);
  
    var combinedOpts;
  
    userOperators.forEach(function (userOperator) {
  
      if (isNonLogicalMatcher(userOperator)) {
        inMemoryFields.push(field);
        return;
      }
  
      var userValue = matcher[userOperator];
  
      var newQueryOpts = getSingleFieldQueryOptsFor(userOperator, userValue);
  
      if (combinedOpts) {
        combinedOpts = mergeObjects([combinedOpts, newQueryOpts]);
      } else {
        combinedOpts = newQueryOpts;
      }
    });
  
    return {
      queryOpts: combinedOpts,
      inMemoryFields: inMemoryFields
    };
  }
  
  function getMultiFieldCoreQueryPlan(userOperator, userValue) {
    switch (userOperator) {
      case '$eq':
        return {
          startkey: userValue,
          endkey: userValue
        };
      case '$lte':
        return {
          endkey: userValue
        };
      case '$gte':
        return {
          startkey: userValue
        };
      case '$lt':
        return {
          endkey: userValue,
          inclusive_end: false
        };
      case '$gt':
        return {
          startkey: userValue,
          inclusive_start: false
        };
    }
  }
  
  function getMultiFieldQueryOpts(selector, index) {
  
    var indexFields = index.def.fields.map(getKey);
  
    var inMemoryFields = [];
    var startkey = [];
    var endkey = [];
    var inclusiveStart;
    var inclusiveEnd;
  
  
    function finish(i) {
  
      if (inclusiveStart !== false) {
        startkey.push(COLLATE_LO);
      }
      if (inclusiveEnd !== false) {
        endkey.push(COLLATE_HI);
      }
      // keep track of the fields where we lost specificity,
      // and therefore need to filter in-memory
      inMemoryFields = indexFields.slice(i);
    }
  
    for (var i = 0, len = indexFields.length; i < len; i++) {
      var indexField = indexFields[i];
  
      var matcher = selector[indexField];
  
      if (!matcher || !Object.keys(matcher).length) { // fewer fields in user query than in index
        finish(i);
        break;
      } else if (i > 0) {
        if (Object.keys(matcher).some(isNonLogicalMatcher)) { // non-logical are ignored 
          finish(i);
          break;
        }
        var usingGtlt = (
          '$gt' in matcher || '$gte' in matcher ||
          '$lt' in matcher || '$lte' in matcher);
        var previousKeys = Object.keys(selector[indexFields[i - 1]]);
        var previousWasEq = arrayEquals(previousKeys, ['$eq']);
        var previousWasSame = arrayEquals(previousKeys, Object.keys(matcher));
        var gtltLostSpecificity = usingGtlt && !previousWasEq && !previousWasSame;
        if (gtltLostSpecificity) {
          finish(i);
          break;
        }
      }
  
      var userOperators = Object.keys(matcher);
  
      var combinedOpts = null;
  
      for (var j = 0; j < userOperators.length; j++) {
        var userOperator = userOperators[j];
        var userValue = matcher[userOperator];
  
        var newOpts = getMultiFieldCoreQueryPlan(userOperator, userValue);
  
        if (combinedOpts) {
          combinedOpts = mergeObjects([combinedOpts, newOpts]);
        } else {
          combinedOpts = newOpts;
        }
      }
  
      startkey.push('startkey' in combinedOpts ? combinedOpts.startkey : COLLATE_LO);
      endkey.push('endkey' in combinedOpts ? combinedOpts.endkey : COLLATE_HI);
      if ('inclusive_start' in combinedOpts) {
        inclusiveStart = combinedOpts.inclusive_start;
      }
      if ('inclusive_end' in combinedOpts) {
        inclusiveEnd = combinedOpts.inclusive_end;
      }
    }
  
    var res = {
      startkey: startkey,
      endkey: endkey
    };
  
    if (typeof inclusiveStart !== 'undefined') {
      res.inclusive_start = inclusiveStart;
    }
    if (typeof inclusiveEnd !== 'undefined') {
      res.inclusive_end = inclusiveEnd;
    }
  
    return {
      queryOpts: res,
      inMemoryFields: inMemoryFields
    };
  }
  
  function getDefaultQueryPlan(selector) {
    //using default index, so all fields need to be done in memory
    return {
      queryOpts: {startkey: null},
      inMemoryFields: [Object.keys(selector)]
    };
  }
  
  function getCoreQueryPlan(selector, index) {
    if (index.defaultUsed) {
      return getDefaultQueryPlan(selector, index);
    }
  
    if (index.def.fields.length === 1) {
      // one field in index, so the value was indexed as a singleton
      return getSingleFieldCoreQueryPlan(selector, index);
    }
    // else index has multiple fields, so the value was indexed as an array
    return getMultiFieldQueryOpts(selector, index);
  }
  
  function planQuery(request, indexes) {
  
    var selector = request.selector;
    var sort = request.sort;
  
    var userFieldsRes = getUserFields(selector, sort);
  
    var userFields = userFieldsRes.fields;
    var sortOrder = userFieldsRes.sortOrder;
    var index = findBestMatchingIndex(selector, userFields, sortOrder, indexes);
  
    var coreQueryPlan = getCoreQueryPlan(selector, index);
    var queryOpts = coreQueryPlan.queryOpts;
    var coreInMemoryFields = coreQueryPlan.inMemoryFields;
  
    var inMemoryFields = getInMemoryFields(coreInMemoryFields, index, selector, userFields);
  
    var res = {
      queryOpts: queryOpts,
      index: index,
      inMemoryFields: inMemoryFields
    };
    return res;
  }
  
  function indexToSignature(index) {
    // remove '_design/'
    return index.ddoc.substring(8) + '/' + index.name;
  }
  
  function doAllDocs(db, originalOpts) {
    var opts = clone(originalOpts);
  
    // CouchDB responds in weird ways when you provide a non-string to _id;
    // we mimic the behavior for consistency. See issue66 tests for details.
  
    if (opts.descending) {
      if ('endkey' in opts && typeof opts.endkey !== 'string') {
        opts.endkey = '';
      }
      if ('startkey' in opts && typeof opts.startkey !== 'string') {
        opts.limit = 0;
      }
    } else {
      if ('startkey' in opts && typeof opts.startkey !== 'string') {
        opts.startkey = '';
      }
      if ('endkey' in opts && typeof opts.endkey !== 'string') {
        opts.limit = 0;
      }
    }
    if ('key' in opts && typeof opts.key !== 'string') {
      opts.limit = 0;
    }
  
    return db.allDocs(opts);
  }
  
  function find$1(db, requestDef) {
    if (requestDef.selector) {
      requestDef.selector = massageSelector(requestDef.selector);
    }
    if (requestDef.sort) {
      requestDef.sort = massageSort(requestDef.sort);
    }
  
    validateFindRequest(requestDef);
  
    return getIndexes$1(db).then(function (getIndexesRes) {
  
      db.constructor.emit('debug', ['find', 'planning query', requestDef]);
      var queryPlan = planQuery(requestDef, getIndexesRes.indexes);
      db.constructor.emit('debug', ['find', 'query plan', queryPlan]);
  
      var indexToUse = queryPlan.index;
  
      validateSort(requestDef, indexToUse);
  
      var opts = assign({
        include_docs: true,
        reduce: false
      }, queryPlan.queryOpts);
  
      if ('startkey' in opts && 'endkey' in opts &&
          collate(opts.startkey, opts.endkey) > 0) {
        // can't possibly return any results, startkey > endkey
        return {docs: []};
      }
  
      var isDescending = requestDef.sort &&
        typeof requestDef.sort[0] !== 'string' &&
        getValue(requestDef.sort[0]) === 'desc';
  
      if (isDescending) {
        // either all descending or all ascending
        opts.descending = true;
        opts = reverseOptions(opts);
      }
  
      if (!queryPlan.inMemoryFields.length) {
        // no in-memory filtering necessary, so we can let the
        // database do the limit/skip for us
        if ('limit' in requestDef) {
          opts.limit = requestDef.limit;
        }
        if ('skip' in requestDef) {
          opts.skip = requestDef.skip;
        }
      }
      
      return Promise.resolve().then(function () {
        if (indexToUse.name === '_all_docs') {
          return doAllDocs(db, opts);
        } else {
          var signature = indexToSignature(indexToUse);
          return abstractMapper.query.call(db, signature, opts);
        }
      }).then(function (res) {
        if (opts.inclusive_start === false) {
          // may have to manually filter the first one,
          // since couchdb has no true inclusive_start option
          res.rows = filterInclusiveStart(res.rows, opts.startkey, indexToUse);
        }
  
        if (queryPlan.inMemoryFields.length) {
          // need to filter some stuff in-memory
          res.rows = filterInMemoryFields(res.rows, requestDef, queryPlan.inMemoryFields);
        }
  
        var resp = {
          docs: res.rows.map(function (row) {
            var doc = row.doc;
            if (requestDef.fields) {
              return pick(doc, requestDef.fields);
            }
            return doc;
          })
        };
  
        if (indexToUse.defaultUsed) {
          resp.warning = 'no matching index found, create an index to optimize query time';
        }
  
        return resp;
      });
    });
  }
  
  function deleteIndex$1(db, index) {
  
    if (!index.ddoc) {
      throw new Error('you must supply an index.ddoc when deleting');
    }
  
    if (!index.name) {
      throw new Error('you must supply an index.name when deleting');
    }
  
    var docId = index.ddoc;
    var viewName = index.name;
  
    function deltaFun(doc) {
      if (Object.keys(doc.views).length === 1 && doc.views[viewName]) {
        // only one view in this ddoc, delete the whole ddoc
        return {_id: docId, _deleted: true};
      }
      // more than one view here, just remove the view
      delete doc.views[viewName];
      return doc;
    }
  
    return upsert(db, docId, deltaFun).then(function () {
      return abstractMapper.viewCleanup.apply(db);
    }).then(function () {
      return {ok: true};
    });
  }
  
  var createIndexAsCallback = callbackify(createIndex$1);
  var findAsCallback = callbackify(find$1);
  var getIndexesAsCallback = callbackify(getIndexes$1);
  var deleteIndexAsCallback = callbackify(deleteIndex$1);
  
  var plugin = {};
  plugin.createIndex = toPromise(function (requestDef, callback) {
  
    if (typeof requestDef !== 'object') {
      return callback(new Error('you must provide an index to create'));
    }
  
    var createIndex$$1 = isRemote(this) ?
      createIndex : createIndexAsCallback;
    createIndex$$1(this, requestDef, callback);
  });
  
  plugin.find = toPromise(function (requestDef, callback) {
  
    if (typeof callback === 'undefined') {
      callback = requestDef;
      requestDef = undefined;
    }
  
    if (typeof requestDef !== 'object') {
      return callback(new Error('you must provide search parameters to find()'));
    }
  
    var find$$1 = isRemote(this) ? find : findAsCallback;
    find$$1(this, requestDef, callback);
  });
  
  plugin.getIndexes = toPromise(function (callback) {
  
    var getIndexes$$1 = isRemote(this) ? getIndexes : getIndexesAsCallback;
    getIndexes$$1(this, callback);
  });
  
  plugin.deleteIndex = toPromise(function (indexDef, callback) {
  
    if (typeof indexDef !== 'object') {
      return callback(new Error('you must provide an index to delete'));
    }
  
    var deleteIndex$$1 = isRemote(this) ?
      deleteIndex : deleteIndexAsCallback;
    deleteIndex$$1(this, indexDef, callback);
  });
  
  export default plugin;
  