/**
*
* Copyright (c) 2006-2017, Speedment, Inc. All Rights Reserved.
*
* 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:
*
* http://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.speedment.runtime.core.internal.component.sql.optimizer;
import com.speedment.runtime.config.identifier.ColumnIdentifier;
import com.speedment.runtime.core.component.sql.Metrics;
import com.speedment.runtime.core.component.sql.SqlStreamOptimizer;
import com.speedment.runtime.core.component.sql.SqlStreamOptimizerInfo;
import com.speedment.runtime.core.db.AsynchronousQueryResult;
import com.speedment.runtime.core.db.DbmsType;
import static com.speedment.runtime.core.db.DbmsType.SkipLimitSupport.NONE;
import static com.speedment.runtime.core.db.DbmsType.SkipLimitSupport.ONLY_AFTER_SORTED;
import com.speedment.runtime.core.internal.stream.builder.action.reference.FilterAction;
import com.speedment.runtime.core.internal.stream.builder.action.reference.LimitAction;
import com.speedment.runtime.core.internal.stream.builder.action.reference.SkipAction;
import com.speedment.runtime.core.internal.stream.builder.action.reference.SortedComparatorAction;
import com.speedment.runtime.core.internal.stream.builder.streamterminator.StreamTerminatorUtil;
import com.speedment.runtime.core.internal.stream.builder.streamterminator.StreamTerminatorUtil.RenderResult;
import static com.speedment.runtime.core.internal.stream.builder.streamterminator.StreamTerminatorUtil.isContainingOnlyFieldPredicate;
import static com.speedment.runtime.core.internal.stream.builder.streamterminator.StreamTerminatorUtil.isSortedActionWithFieldPredicate;
import com.speedment.runtime.core.stream.Pipeline;
import com.speedment.runtime.core.stream.action.Action;
import com.speedment.runtime.field.comparator.FieldComparator;
import com.speedment.runtime.field.comparator.NullOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import static java.util.Objects.requireNonNull;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.function.Predicate;
import static java.util.stream.Collectors.toList;
/**
* This Optimizer takes care of the following case:
* <br>
* <ul>
* <li> a) Zero or more filter() operations
* <li> b) Zero or more sorted() operations
* <li> c) Zero or more skip() operations
* <li> d) Zero or more limit() operations
*
* <em>No other operations</em> must be in the sequence a-d or within the
* individual items a-d. <em>All</em> parameters in a and b must be obtained via
* fields. Failure to any of these rules will make the Optimizer reject
* optimization. Steps a) and b) may swap places.
*
* Thus, this optimizer can handle a (FILTER*, SORTED*, SKIP*, LIMIT*) or
* (SORTED*, LIMIT*, SKIP*, LIMIT*) pattern where all non-primitive parameters
* are all Field derived
*
*
* @author Per Minborg
* @param <ENTITY> entity type
*/
public final class FilterSortedSkipOptimizer<ENTITY> implements SqlStreamOptimizer<ENTITY> {
private final FilterOperation FILTER_OPERATION = new FilterOperation();
private final SortedOperation SORTED_OPERATION = new SortedOperation();
private final SkipOperation SKIP_OPERATION = new SkipOperation();
private final LimitOperation LIMIT_OPERATION = new LimitOperation();
private final List<Operation<ENTITY>> FILTER_SORTED_SKIP_PATH = Arrays.asList(
FILTER_OPERATION,
SORTED_OPERATION,
SKIP_OPERATION,
LIMIT_OPERATION
);
private final List<Operation<ENTITY>> SORTED_FILTER_SKIP_PATH = Arrays.asList(
SORTED_OPERATION,
FILTER_OPERATION,
SKIP_OPERATION,
LIMIT_OPERATION
);
// FILTER <-> SORTED
// This optimizer can handle a (FILTER*,SORTED*,SKIP*, LIMIT*) pattern where filter and sorted parameters are all Field derived
@Override
public Metrics metrics(Pipeline initialPipeline, DbmsType dbmsType) {
requireNonNull(initialPipeline);
requireNonNull(dbmsType);
final DbmsType.SkipLimitSupport skipLimitSupport = dbmsType.getSkipLimitSupport();
final AtomicInteger filterCounter = new AtomicInteger();
final AtomicInteger orderCounter = new AtomicInteger();
final AtomicInteger skipCounter = new AtomicInteger();
final AtomicInteger limitCounter = new AtomicInteger();
traverse(initialPipeline,
$ -> filterCounter.incrementAndGet(),
$ -> orderCounter.incrementAndGet(),
$ -> skipCounter.incrementAndGet(),
$ -> limitCounter.incrementAndGet()
);
if (skipLimitSupport == ONLY_AFTER_SORTED && orderCounter.get() == 0) {
// Just decline. There are other optimizer that handles just filtering better
return Metrics.empty();
}
if (skipLimitSupport == NONE) {
return Metrics.of(filterCounter.get() + orderCounter.get(), filterCounter.get(), orderCounter.get(), 0, 0);
}
return Metrics.of(
filterCounter.get() + orderCounter.get() + skipCounter.get() + limitCounter.get(),
filterCounter.get(),
orderCounter.get(),
skipCounter.get() > 0 ? 1 : 0,
limitCounter.get() > 0 ? 1 : 0
);
}
@Override
public <P extends Pipeline> P optimize(
final P initialPipeline,
final SqlStreamOptimizerInfo<ENTITY> info,
final AsynchronousQueryResult<ENTITY> query
) {
requireNonNull(initialPipeline);
requireNonNull(info);
requireNonNull(query);
final DbmsType dbmsType = info.getDbmsType();
final DbmsType.SkipLimitSupport skipLimitSupport = dbmsType.getSkipLimitSupport();
final List<FilterAction<ENTITY>> filters = new ArrayList<>();
final List<SortedComparatorAction<ENTITY>> sorteds = new ArrayList<>();
final List<SkipAction<ENTITY>> skips = new ArrayList<>();
final List<LimitAction<ENTITY>> limits = new ArrayList<>();
traverse(initialPipeline, filters::add, sorteds::add, skips::add, limits::add);
final List<Object> values = new ArrayList<>();
final StringBuilder sql = new StringBuilder();
sql.append(info.getSqlSelect());
if (!filters.isEmpty()) {
@SuppressWarnings("unchecked")
List<Predicate<ENTITY>> predicates = filters.stream()
.map(FilterAction::getPredicate)
.map(p -> (Predicate<ENTITY>) p)
.collect(toList());
final RenderResult rr = StreamTerminatorUtil.renderSqlWhere(
dbmsType,
info.getSqlColumnNamer(),
info.getSqlDatabaseTypeFunction(),
predicates
);
sql.append(" WHERE ").append(rr.getSql());
values.addAll(rr.getValues());
}
if (!sorteds.isEmpty()) {
sql.append(" ORDER BY ");
// Iterate backwards
final Set<ColumnIdentifier<ENTITY>> columns = new HashSet<>();
for (int i = sorteds.size() - 1; i >= 0; i--) {
final SortedComparatorAction<ENTITY> sortedAction = sorteds.get(i);
@SuppressWarnings("unchecked")
final FieldComparator<ENTITY, ?> fieldComparator = (FieldComparator<ENTITY, ?>) sortedAction.getComparator();
final ColumnIdentifier<ENTITY> columnIdentifier = fieldComparator.getField().identifier();
// Some databases (e.g. SQL Server) only allows distinct columns in ORDER BY
if (columns.add(columnIdentifier)) {
if (!(i == (sorteds.size() - 1))) {
sql.append(", ");
}
boolean isReversed = fieldComparator.isReversed();
String fieldName = info.getSqlColumnNamer().apply(fieldComparator.getField());
final NullOrder effectiveNullOrder = isReversed
? fieldComparator.getNullOrder().reversed()
: fieldComparator.getNullOrder();
// Specify NullOrder pre column if nulls are first
if (effectiveNullOrder == NullOrder.FIRST) {
if (dbmsType.getSortByNullOrderInsertion() == DbmsType.SortByNullOrderInsertion.PRE) {
sql.append(fieldName).append("IS NOT NULL, ");
}
if (dbmsType.getSortByNullOrderInsertion() == DbmsType.SortByNullOrderInsertion.PRE_WITH_CASE) {
sql.append("CASE WHEN ").append(fieldName).append(" IS NULL THEN 0 ELSE 1 END, ");
}
}
sql.append(fieldName);
if (isReversed) {
sql.append(" DESC");
} else {
sql.append(" ASC");
}
// Specify NullOrder post column
if (effectiveNullOrder == NullOrder.FIRST && dbmsType.getSortByNullOrderInsertion() == DbmsType.SortByNullOrderInsertion.POST) {
sql.append(" NULLS FIRST");
}
}
}
}
final String finalSql;
if (skipLimitSupport == NONE) {
finalSql = sql.toString();
initialPipeline.removeIf(a -> filters.contains(a) || sorteds.contains(a));
} else {
final long sumSkip = skips.stream().mapToLong(SkipAction::getSkip).sum();
final long minLimit = limits.stream().mapToLong(LimitAction::getLimit).min().orElse(Long.MAX_VALUE);
finalSql = dbmsType
.applySkipLimit(sql.toString(), values, sumSkip, minLimit);
initialPipeline.removeIf(a -> filters.contains(a) || sorteds.contains(a) || skips.contains(a) || limits.contains(a));
}
query.setSql(finalSql);
query.setValues(values);
return initialPipeline;
}
private void traverse(Pipeline pipeline,
final Consumer<? super FilterAction<ENTITY>> filterConsumer,
final Consumer<? super SortedComparatorAction<ENTITY>> sortedConsumer,
final Consumer<? super SkipAction<ENTITY>> skipConsumer,
final Consumer<? super LimitAction<ENTITY>> limitConsumer
) {
if (pipeline.isEmpty()) {
return;
}
final Consumers<ENTITY> consumers = new Consumers<>(filterConsumer, sortedConsumer, skipConsumer, limitConsumer);
final Action<?, ?> firstAction = pipeline.getFirst();
final List<Operation<ENTITY>> path;
if (isFilterActionAndContainingOnlyFieldPredicate(firstAction)) {
path = FILTER_SORTED_SKIP_PATH;
} else {
path = SORTED_FILTER_SKIP_PATH;
}
Operation<ENTITY> operation = path.get(0);
for (Action<?, ?> action : pipeline) {
if (operation == path.get(0)) {
if (operation.is(action)) {
operation.consume(action, consumers);
} else {
if (path.get(1).is(action)) {
operation = path.get(1);
} else {
if (path.get(2).is(action)) {
operation = path.get(2);
} else {
if (path.get(3).is(action)) {
operation = path.get(3);
} else {
return;
}
}
}
}
}
if (operation == path.get(1)) {
if (operation.is(action)) {
operation.consume(action, consumers);
} else {
if (path.get(2).is(action)) {
operation = path.get(2);
} else {
if (path.get(3).is(action)) {
operation = path.get(3);
} else {
return;
}
}
}
}
if (operation == path.get(2)) {
if (operation.is(action)) {
operation.consume(action, consumers);
} else {
if (path.get(3).is(action)) {
operation = path.get(3);
} else {
return;
}
}
}
if (operation == path.get(3)) {
if (operation.is(action)) {
operation.consume(action, consumers);
} else {
return;
}
}
}
}
private boolean isFilterActionAndContainingOnlyFieldPredicate(Action<?, ?> action) {
if (action instanceof FilterAction) {
@SuppressWarnings("unchecked")
final FilterAction<ENTITY> filterAction = (FilterAction<ENTITY>) action;
return isContainingOnlyFieldPredicate(filterAction.getPredicate());
}
return false;
}
private static class Consumers<ENTITY> {
private final Consumer<? super FilterAction<ENTITY>> filterConsumer;
private final Consumer<? super SortedComparatorAction<ENTITY>> sortedConsumer;
private final Consumer<? super SkipAction<ENTITY>> skipConsumer;
private final Consumer<? super LimitAction<ENTITY>> limitConsumer;
public Consumers(
final Consumer<? super FilterAction<ENTITY>> filterConsumer,
final Consumer<? super SortedComparatorAction<ENTITY>> sortedConsumer,
final Consumer<? super SkipAction<ENTITY>> skipConsumer,
final Consumer<? super LimitAction<ENTITY>> limitConsumer
) {
this.filterConsumer = requireNonNull(filterConsumer);
this.sortedConsumer = requireNonNull(sortedConsumer);;
this.skipConsumer = requireNonNull(skipConsumer);
this.limitConsumer = requireNonNull(limitConsumer);
}
public Consumer<? super FilterAction<ENTITY>> getFilterConsumer() {
return filterConsumer;
}
public Consumer<? super SortedComparatorAction<ENTITY>> getSortedConsumer() {
return sortedConsumer;
}
public Consumer<? super SkipAction<ENTITY>> getSkipConsumer() {
return skipConsumer;
}
public Consumer<? super LimitAction<ENTITY>> getLimitConsumer() {
return limitConsumer;
}
}
private interface Operation<ENTITY> {
boolean is(Action<?, ?> action);
void consume(Action<?, ?> action, Consumers<ENTITY> consumers);
}
private class FilterOperation implements Operation<ENTITY> {
@Override
public boolean is(Action<?, ?> action) {
return isFilterActionAndContainingOnlyFieldPredicate(action);
}
@Override
public void consume(Action<?, ?> action, Consumers<ENTITY> consumers) {
@SuppressWarnings("unchecked")
final FilterAction<ENTITY> filterAction = (FilterAction<ENTITY>) action;
consumers.getFilterConsumer().accept(filterAction);
}
}
private class SortedOperation implements Operation<ENTITY> {
@Override
public boolean is(Action<?, ?> action) {
return isSortedActionWithFieldPredicate(action);
}
@Override
public void consume(Action<?, ?> action, Consumers<ENTITY> consumers) {
@SuppressWarnings("unchecked")
final SortedComparatorAction<ENTITY> sortedAction = (SortedComparatorAction<ENTITY>) action;
consumers.getSortedConsumer().accept(sortedAction);
}
}
private class SkipOperation implements Operation<ENTITY> {
@Override
public boolean is(Action<?, ?> action) {
return action instanceof SkipAction;
}
@Override
public void consume(Action<?, ?> action, Consumers<ENTITY> consumers) {
@SuppressWarnings("unchecked")
final SkipAction<ENTITY> skipAction = (SkipAction<ENTITY>) action;
consumers.getSkipConsumer().accept(skipAction);
}
}
private class LimitOperation implements Operation<ENTITY> {
@Override
public boolean is(Action<?, ?> action) {
return action instanceof LimitAction;
}
@Override
public void consume(Action<?, ?> action, Consumers<ENTITY> consumers) {
@SuppressWarnings("unchecked")
final LimitAction<ENTITY> limitAction = (LimitAction<ENTITY>) action;
consumers.getLimitConsumer().accept(limitAction);
}
}
}