com.google.appengine.api.datastore.MultiQueryBuilder Maven / Gradle / Ivy
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.appengine.api.datastore;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.appengine.api.datastore.MultiQueryComponent.Order;
import com.google.appengine.api.datastore.Query.FilterPredicate;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
/**
* A class that generates multiple lists of query filters to execute to satisfy the {@link
* MultiQueryComponent}s given to it.
*
* Each component contains multiple {@link List}s of {@link FilterPredicate}s and an order in
* which MultiQuery should produce queries that contain these filters.
*
*
If the order of a {@link MultiQueryComponent} is set to {@link Order#SERIAL} the given filters
* are applied to sequentially generated sets of queries. This denotes that the result of the
* sequentially generated queries can be concatenated to produce a valid result set. However this
* class makes no guarantees to this effect and this assurance must come from the code that
* constructs this class.
*
*
If the order of a {@link MultiQueryComponent} is set to {@link Order#PARALLEL} the given
* filters are used to generate multiple queries (one for each list of filters) that are returned
* together. This is used to denote that results must be merged in memory to create a valid result
* set. However this class makes no guarantees to this effect and this assurance must come from the
* code that constructs this class.
*
*
{@link MultiQueryBuilder#iterator()} will provide an iterator that generates the filters for
* these queries in the given order. The iterator generates each list of filters as they are needed.
* In this way, if the user request is satisfied the first few sets of queries, the remaining
* queries need never be created. This is important because the total number of queries generated by
* this class is mult_i(|component_i.filters|).
*
*
This class preserves the order in which {@link MultiQueryComponent} are given to it when
* applying the filters. However filters are generated most optimally when {@link Order#PARALLEL}
* are the last to be applied. Thus to provide a convenient way to push components with {@link
* Order#PARALLEL} to the back, {@link MultiQueryComponent} are sortable.
*
*
{@link MultiQueryComponent}s with different {@link Order}s can be added to the same {@link
* MultiQueryBuilder} in any configuration.
*/
// TODO: consider allowing the order in which components are
// process be separate from the order of the filters appear in the
// resulting query. It is unclear to me whether or not this would help
// users optimize for specific queries.
class MultiQueryBuilder implements Iterable>> {
final List baseFilters;
final List components;
final int parallelQuerySize;
/* @VisibleForTesting */
MultiQueryBuilder(
List baseFilters,
List components,
int parallelQuerySize) {
this.baseFilters = checkNotNull(baseFilters);
this.components = components;
this.parallelQuerySize = parallelQuerySize;
}
public MultiQueryBuilder(
List baseFilters,
List splitComponents,
boolean hasSort) {
this.baseFilters = checkNotNull(baseFilters);
if (splitComponents.isEmpty()) {
components = Collections.emptyList();
parallelQuerySize = 1;
} else {
components = Lists.newArrayListWithCapacity(splitComponents.size());
// First we need to sort the components. This orders sequential components
// by sortIndex and pushes all arbitrary components to end
//
// TODO: is this a stable sort? a stable sort would probably be
// better as it would give the user some control of the ordering in which
// these queries are produced.
Collections.sort(splitComponents);
// If there are no sorts we can do everything serially
MultiQueryComponent.Order applyToRemaining =
hasSort ? null : MultiQueryComponent.Order.SERIAL;
int currentSortIndex = 0;
int totalParallelQueries = 1;
for (QuerySplitComponent component : splitComponents) {
if ((applyToRemaining == null) && (component.getSortIndex() != currentSortIndex)) {
if (component.getSortIndex() == currentSortIndex + 1) {
// We can safely serialize these components as there can only be one
// inequality filter and that filter must match the first sort order
// so this component must only contain equality filters. In this way
// we are assured that we are maintaining the correct sort order
// of the resultSet when running the queries in serial
++currentSortIndex;
} else {
// If we have jumped over a sort order or hit an arbitrary sorting
// component we must run all the remaining components in parallel
// NOTE: a component with an arbitrary ordering will
// always land here as they have a sortIndex of -1.
applyToRemaining = MultiQueryComponent.Order.PARALLEL;
// TODO: if there are no inequality filters and we have no
// more sorts we can technically run the rest in serial.
}
}
// Converting the current component and adding it to the result
components.add(
new MultiQueryComponent(
applyToRemaining != null ? applyToRemaining : MultiQueryComponent.Order.SERIAL,
component.getFilters()));
if (applyToRemaining == MultiQueryComponent.Order.PARALLEL) {
totalParallelQueries *= component.getFilters().size();
}
}
this.parallelQuerySize = totalParallelQueries;
}
}
@Override
public Iterator>> iterator() {
if (components.isEmpty()) {
return Iterators.singletonIterator(Collections.singletonList(baseFilters));
}
return new MultiQueryIterator(baseFilters, components);
}
public List getBaseFilters() {
return baseFilters;
}
public boolean isSingleton() {
return components.isEmpty();
}
/** Returns the parallelQuerySize. */
public int getParallelQuerySize() {
return parallelQuerySize;
}
@Override
public String toString() {
return "MultiQueryBuilder [baseFilters=" + baseFilters + ", components=" + components + "]";
}
}