Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
*/
package de.ii.xtraplatform.features.sql.app;

import de.ii.xtraplatform.cql.domain.And;
import de.ii.xtraplatform.cql.domain.BinaryScalarOperation;
import de.ii.xtraplatform.cql.domain.Cql2Expression;
import de.ii.xtraplatform.cql.domain.CqlNode;
import de.ii.xtraplatform.cql.domain.CqlVisitorCopy;
import de.ii.xtraplatform.cql.domain.ImmutableInResultSet;
import de.ii.xtraplatform.cql.domain.InResultSet;
import de.ii.xtraplatform.cql.domain.Not;
import de.ii.xtraplatform.cql.domain.Or;
import de.ii.xtraplatform.features.domain.ImmutableMultiFeatureQuery;
import de.ii.xtraplatform.features.domain.ImmutableSubQuery;
import de.ii.xtraplatform.features.domain.MultiFeatureQuery;
Expand Down Expand Up @@ -74,6 +77,7 @@ public MultiFeatureQuery materialize(MultiFeatureQuery query) {
}

Map<String, List<Object>> materialized = new HashMap<>();
int shortCircuited = 0;
// materialize level by level: within a level the producers are independent and run concurrently
// (bounded by the connection pool). SQL is built single-threaded between levels, so the filter
// encoder is never invoked concurrently and the materialized map is only mutated on this
Expand All @@ -83,6 +87,15 @@ public MultiFeatureQuery materialize(MultiFeatureQuery query) {
Map<String, SchemaBase.Type> valueTypes = new HashMap<>();
for (String name : level) {
InResultSet node = sets.get(name);
// if a dependency has already materialized to no members in a position that forces this
// producer's filter false, the producer can only be empty too — record the empty set and
// skip its query. Consumers encode IN () for it either way, so the output is unchanged.
if (node.getProducerFilter().isPresent()
&& isProvablyEmpty(node.getProducerFilter().get(), materialized)) {
materialized.put(name, List.of());
shortCircuited++;
continue;
}
InResultSet prepared =
node.getProducerFilter().isPresent()
? new ImmutableInResultSet.Builder()
Expand Down Expand Up @@ -121,6 +134,13 @@ public MultiFeatureQuery materialize(MultiFeatureQuery query) {
}
}

if (shortCircuited > 0 && LOGGER.isDebugEnabled()) {
LOGGER.debug(
"Short-circuited {} of {} result-set producer(s) with an empty dependency.",
shortCircuited,
sets.size());
}

List<SubQuery> rewritten =
query.getQueries().stream()
.map(
Expand Down Expand Up @@ -213,6 +233,69 @@ private static Cql2Expression applyMaterialized(
return (Cql2Expression) expression.accept(new ApplyMaterialized(materialized));
}

/**
* True if the filter can only ever be false given the result sets materialized so far, i.e. it
* references a set that materialized to no members in a boolean position that forces the whole
* filter false. Conservative: any leaf whose value is not pinned by an empty set is treated as
* indeterminate, so a producer is skipped only when boolean algebra guarantees an empty result.
*/
private static boolean isProvablyEmpty(
Cql2Expression filter, Map<String, List<Object>> materialized) {
return truth(filter, materialized) == Truth.FALSE;
}

private enum Truth {
TRUE,
FALSE,
UNKNOWN
}

private static Truth truth(CqlNode node, Map<String, List<Object>> materialized) {
if (node instanceof InResultSet) {
List<Object> values = materialized.get(((InResultSet) node).getSetName());
// a materialized-empty set makes the IN / A_OVERLAPS predicate always false; a non-empty or
// not-yet-materialized (oversized) set is indeterminate at the query level
return values != null && values.isEmpty() ? Truth.FALSE : Truth.UNKNOWN;
}
if (node instanceof And) {
Truth result = Truth.TRUE;
for (Cql2Expression child : ((And) node).getArgs()) {
Truth childTruth = truth(child, materialized);
if (childTruth == Truth.FALSE) {
return Truth.FALSE;
}
if (childTruth == Truth.UNKNOWN) {
result = Truth.UNKNOWN;
}
}
return result;
}
if (node instanceof Or) {
Truth result = Truth.FALSE;
for (Cql2Expression child : ((Or) node).getArgs()) {
Truth childTruth = truth(child, materialized);
if (childTruth == Truth.TRUE) {
return Truth.TRUE;
}
if (childTruth == Truth.UNKNOWN) {
result = Truth.UNKNOWN;
}
}
return result;
}
if (node instanceof Not) {
Truth child = truth(((Not) node).getArgs().get(0), materialized);
if (child == Truth.TRUE) {
return Truth.FALSE;
}
if (child == Truth.FALSE) {
return Truth.TRUE;
}
return Truth.UNKNOWN;
}
return Truth.UNKNOWN;
}

private static Object coerce(Object value, SchemaBase.Type type) {
if (!(value instanceof String)) {
return value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import de.ii.xtraplatform.cql.domain.Cql2Expression
import de.ii.xtraplatform.cql.domain.Eq
import de.ii.xtraplatform.cql.domain.ImmutableInResultSet
import de.ii.xtraplatform.cql.domain.InResultSet
import de.ii.xtraplatform.cql.domain.Or
import de.ii.xtraplatform.cql.domain.Property
import de.ii.xtraplatform.cql.domain.ScalarLiteral
import de.ii.xtraplatform.crs.domain.OgcCrs
Expand Down Expand Up @@ -116,6 +117,63 @@ class ResultSetMaterializerSpec extends Specification {
node.getMaterializedValues().isEmpty()
}

def 'a producer whose dependency materialized empty is short-circuited and not run'() {
given:
def s1 = resolved("s1", "simple", Eq.of(Property.of("id"), ScalarLiteral.of("foo")))
def s2 = resolved("s2", "simple", s1)
def sqlClient = Mock(SqlClient)
def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000)

when:
def result = materializer.materialize(query(s2))

then:
// only the s1 producer runs; s2 is skipped because its dependency s1 is empty
1 * sqlClient.run(_, _) >> CompletableFuture.completedFuture([])
def node = (InResultSet) result.getQueries().get(0).getFilters().get(0)
node.getMaterializedValues().get() == []
}

def 'a producer whose dependency is non-empty is still run'() {
given:
def s1 = resolved("s1", "simple", Eq.of(Property.of("id"), ScalarLiteral.of("foo")))
def s2 = resolved("s2", "simple", s1)
def sqlClient = Mock(SqlClient)
def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000)

when:
def result = materializer.materialize(query(s2))

then:
2 * sqlClient.run(_, _) >>> [
CompletableFuture.completedFuture([row("a")]),
CompletableFuture.completedFuture([row("b"), row("c")])
]
def node = (InResultSet) result.getQueries().get(0).getFilters().get(0)
node.getMaterializedValues().get() == ["b", "c"]
}

def 'an empty dependency in an OR with another term does not short-circuit'() {
given:
def s1 = resolved("s1", "simple", Eq.of(Property.of("id"), ScalarLiteral.of("foo")))
def s2 = resolved("s2", "simple",
Or.of([s1, Eq.of(Property.of("id"), ScalarLiteral.of("bar"))] as List<Cql2Expression>))
def sqlClient = Mock(SqlClient)
def materializer = new ResultSetMaterializer({ -> sqlClient } as Supplier, filterEncoder, 100000)

when:
def result = materializer.materialize(query(s2))

then:
// s1 is empty but the OR's other term is indeterminate, so s2 must still run
2 * sqlClient.run(_, _) >>> [
CompletableFuture.completedFuture([]),
CompletableFuture.completedFuture([row("b")])
]
def node = (InResultSet) result.getQueries().get(0).getFilters().get(0)
node.getMaterializedValues().get() == ["b"]
}

def 'a query without result sets is returned unchanged'() {
given:
def filter = Eq.of(Property.of("id"), ScalarLiteral.of("foo"))
Expand Down
Loading