function prepareQuery(thisStorage, filter, order) {
  order = normalizeKeyPath(order)
  
  let expectedSortingDirection = order[0].charAt(0) === "!"
  let canSortingBeOptimized
  canSortingBeOptimized = canOptimizeSorting(expectedSortingDirection, order)
  
  let storages = new Map()
  storages.set(normalizeKeyPath(thisStorage.keyPath), {
    storage: thisStorage,
    score: 1 // traversing storage is faster than fetching records by index
  })
  
  for (let indexName of thisStorage.indexNames) {
    let index = thisStorage.getIndex(indexName)
    if (!index.multiEntry) {
      storages.set(normalizeKeyPath(index.keyPath), {
        storage: index,
        score: 0
      })
    }
  }
  
  let simplifiedOrderFieldPaths = simplifyOrderingFieldPaths(order)
  
  if (canSortingBeOptimized) {
    prepareSortingOptimization(storages, simplifiedOrderFieldPaths)
  }

  prepareFilteringOptimization(storages, filter)
  
  return chooseStorageForQuery(
    storages,
    order,
    simplifiedOrderFieldPaths,
    canSortingBeOptimized,
    expectedSortingDirection
  )
}
function prepareQuery(thisStorage, filter, order) {
  order = normalizeKeyPath(order)
  
  let expectedSortingDirection = order[0].charAt(0) === "!"
  let canSortingBeOptimized
  canSortingBeOptimized = canOptimizeSorting(expectedSortingDirection, order)
  
  let storages = new Map()
  storages.set(normalizeKeyPath(thisStorage.keyPath), {
    storage: thisStorage,
    score: 1 // traversing storage is faster than fetching records by index
  })
  
  for (let indexName of thisStorage.indexNames) {
    let index = thisStorage.getIndex(indexName)
    if (!index.multiEntry) {
      storages.set(normalizeKeyPath(index.keyPath), {
        storage: index,
        score: 0
      })
    }
  }
  
  let simplifiedOrderFieldPaths = simplifyOrderingFieldPaths(order)
  
  if (canSortingBeOptimized) {
    prepareSortingOptimization(storages, simplifiedOrderFieldPaths)
  }

  prepareFilteringOptimization(storages, filter)
  
  return chooseStorageForQuery(
    storages,
    order,
    simplifiedOrderFieldPaths,
    canSortingBeOptimized,
    expectedSortingDirection
  )
}
function prepareQuery ( thisStorage , filter , order ) { order = normalizeKeyPath ( order ) let expectedSortingDirection = order [ 0 ] . charAt ( 0 ) === "!" let canSortingBeOptimized canSortingBeOptimized = canOptimizeSorting ( expectedSortingDirection , order ) let storages = new Map ( ) storages . set ( normalizeKeyPath ( thisStorage . keyPath ) , { storage : thisStorage , score : 1 // traversing storage is faster than fetching records by index } ) for ( let indexName of thisStorage . indexNames ) { let index = thisStorage . getIndex ( indexName ) if ( ! index . multiEntry ) { storages . set ( normalizeKeyPath ( index . keyPath ) , { storage : index , score : 0 } ) } } let simplifiedOrderFieldPaths = simplifyOrderingFieldPaths ( order ) if ( canSortingBeOptimized ) { prepareSortingOptimization ( storages , simplifiedOrderFieldPaths ) } prepareFilteringOptimization ( storages , filter ) return chooseStorageForQuery ( storages , order , simplifiedOrderFieldPaths , canSortingBeOptimized , expectedSortingDirection ) }
Prepares the query that uses the specified filter and record order for
execution on this storage.

The method attempts uses a heuristic to determine whether the query should
be run directly on this object store or on one of its indexes for maximum
performance.

If the method cannot optimize sorting and filtering, it prefers optimizing
sorting to optimizing filtering, as optimizing sorting allows the query
executor to skip the requested amount of records and the records following
the last record that fills the required amount of records.

If the sorting cannot be optimized, the method attempts to optimize
filtering so that the matching records can be selected by providing a key
range to cursor, so that only the matching records will be traversed.

The method constructs the filtering predicate and sorting comparator if
neither the sorting nor filtering can be optimized, and prepares the query
to be executed directly on the object store.

@param {ReadOnlyObjectStore} thisStorage This object store.
@param {?(undefined|number|string|Date|Array|IDBKeyRange|Object<string, (number|string|Date|Array|IDBKeyRange)>|function(*, (number|string|Date|Array)): boolean)=} filter
The filter, restricting the records returned by this method. If a
function is provided, the first argument will be set to the record
and the second argument will be set to the primary key of the record.
@param {(string|string[])} order Field paths by which the records should be
sorted. A field path may be prefixed by an exclamation mark
({@code "!"}) for descending order.
@return {{storage: AbstractReadOnlyStorage, direction: CursorDirection, comparator: ?function(*, *): number, keyRange: (undefined|IDBKeyRange), filter: (undefined|function(*, (number|string|Date|Array)): boolean)}}
The storage on which the query should be executed, the direction in
which the cursor should be opened and the record comparator to use
to additionally sort the fetched records matching the filter.
Finally, the returned object has the {@code keyRange} and
{@code filter} fields set to the key range and custom filter to use
with the storage to run the query.
Prepares the query that uses the specified filter and record order for execution on this storage .