com.hazelcast.org.apache.calcite.rel.rules.SpatialRules Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to you 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.hazelcast.org.apache.calcite.rel.rules;
import com.hazelcast.org.apache.calcite.plan.RelOptPredicateList;
import com.hazelcast.org.apache.calcite.plan.RelOptRule;
import com.hazelcast.org.apache.calcite.plan.RelOptRuleCall;
import com.hazelcast.org.apache.calcite.plan.RelOptUtil;
import com.hazelcast.org.apache.calcite.plan.RelRule;
import com.hazelcast.org.apache.calcite.rel.core.Filter;
import com.hazelcast.org.apache.calcite.rex.RexBuilder;
import com.hazelcast.org.apache.calcite.rex.RexCall;
import com.hazelcast.org.apache.calcite.rex.RexInputRef;
import com.hazelcast.org.apache.calcite.rex.RexLiteral;
import com.hazelcast.org.apache.calcite.rex.RexNode;
import com.hazelcast.org.apache.calcite.rex.RexUtil;
import com.hazelcast.org.apache.calcite.runtime.GeoFunctions;
import com.hazelcast.org.apache.calcite.runtime.Geometries;
import com.hazelcast.org.apache.calcite.runtime.HilbertCurve2D;
import com.hazelcast.org.apache.calcite.runtime.SpaceFillingCurve2D;
import com.hazelcast.org.apache.calcite.sql.SqlKind;
import com.hazelcast.org.apache.calcite.sql.fun.SqlStdOperatorTable;
import com.hazelcast.org.apache.calcite.tools.RelBuilder;
import com.hazelcast.com.esri.core.geometry.Envelope;
import com.hazelcast.com.esri.core.geometry.Point;
import com.hazelcast.com.google.common.collect.ImmutableList;
import com.hazelcast.org.checkerframework.checker.nullness.qual.Nullable;
import org.immutables.value.Value;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import static com.hazelcast.org.apache.calcite.rex.RexLiteral.value;
import static java.util.Objects.requireNonNull;
/**
* Collection of planner rules that convert
* calls to spatial functions into more efficient expressions.
*
* The rules allow Calcite to use spatial indexes. For example the following
* query:
*
*
SELECT ...
* FROM Restaurants AS r
* WHERE ST_DWithin(ST_Point(10, 20), ST_Point(r.longitude, r.latitude), 5)
*
*
* is rewritten to
*
*
SELECT ...
* FROM Restaurants AS r
* WHERE (r.h BETWEEN 100 AND 150
* OR r.h BETWEEN 170 AND 185)
* AND ST_DWithin(ST_Point(10, 20), ST_Point(r.longitude, r.latitude), 5)
*
*
* if there is the constraint
*
*
CHECK (h = Hilbert(8, r.longitude, r.latitude))
*
* If the {@code Restaurants} table is sorted on {@code h} then the latter
* query can be answered using two limited range-scans, and so is much more
* efficient.
*
*
Note that the original predicate
* {@code ST_DWithin(ST_Point(10, 20), ST_Point(r.longitude, r.latitude), 5)}
* is still present, but is evaluated after the approximate predicate has
* eliminated many potential matches.
*/
@Value.Enclosing
public abstract class SpatialRules {
private SpatialRules() {}
private static final RexUtil.RexFinder DWITHIN_FINDER =
RexUtil.find(EnumSet.of(SqlKind.ST_DWITHIN, SqlKind.ST_CONTAINS));
private static final RexUtil.RexFinder HILBERT_FINDER =
RexUtil.find(SqlKind.HILBERT);
public static final RelOptRule INSTANCE =
FilterHilbertRule.Config.DEFAULT.toRule();
/** Returns a geometry if an expression is constant, null otherwise. */
private static Geometries.@Nullable Geom constantGeom(RexNode e) {
switch (e.getKind()) {
case CAST:
return constantGeom(((RexCall) e).getOperands().get(0));
case LITERAL:
return (Geometries.Geom) ((RexLiteral) e).getValue();
default:
return null;
}
}
/** Rule that converts ST_DWithin in a Filter condition into a predicate on
* a Hilbert curve. */
@SuppressWarnings("WeakerAccess")
public static class FilterHilbertRule
extends RelRule {
protected FilterHilbertRule(Config config) {
super(config);
}
@Override public void onMatch(RelOptRuleCall call) {
final Filter filter = call.rel(0);
final List conjunctions = new ArrayList<>();
RelOptUtil.decomposeConjunction(filter.getCondition(), conjunctions);
// Match a predicate
// r.hilbert = hilbert(r.longitude, r.latitude)
// to one of the conjunctions
// ST_DWithin(ST_Point(x, y), ST_Point(r.longitude, r.latitude), d)
// and if it matches add a new conjunction before it,
// r.hilbert between h1 and h2
// or r.hilbert between h3 and h4
// where {[h1, h2], [h3, h4]} are the ranges of the Hilbert curve
// intersecting the square
// (r.longitude - d, r.latitude - d, r.longitude + d, r.latitude + d)
final RelOptPredicateList predicates =
call.getMetadataQuery().getAllPredicates(filter.getInput());
if (predicates == null) {
return;
}
int changeCount = 0;
for (RexNode predicate : predicates.pulledUpPredicates) {
final RelBuilder builder = call.builder();
if (predicate.getKind() == SqlKind.EQUALS) {
final RexCall eqCall = (RexCall) predicate;
if (eqCall.operands.get(0) instanceof RexInputRef
&& eqCall.operands.get(1).getKind() == SqlKind.HILBERT) {
final RexInputRef ref = (RexInputRef) eqCall.operands.get(0);
final RexCall hilbert = (RexCall) eqCall.operands.get(1);
final RexUtil.RexFinder finder = RexUtil.find(ref);
if (finder.anyContain(conjunctions)) {
// If the condition already contains "ref", it is probable that
// this rule has already fired once.
continue;
}
for (int i = 0; i < conjunctions.size();) {
final List replacements =
replaceSpatial(conjunctions.get(i), builder, ref, hilbert);
if (replacements != null) {
conjunctions.remove(i);
conjunctions.addAll(i, replacements);
i += replacements.size();
++changeCount;
} else {
++i;
}
}
}
}
if (changeCount > 0) {
call.transformTo(
builder.push(filter.getInput())
.filter(conjunctions)
.build());
return; // we found one useful constraint; don't look for more
}
}
}
/** Rewrites a spatial predicate to a predicate on a Hilbert curve.
*
* Returns null if the predicate cannot be rewritten;
* a 1-element list (new) if the predicate can be fully rewritten;
* returns a 2-element list (new, original) if the new predicate allows
* some false positives.
*
* @param conjunction Original predicate
* @param builder Builder
* @param ref Reference to Hilbert column
* @param hilbert Function call that populates Hilbert column
*
* @return List containing rewritten predicate and original, or null
*/
static @Nullable List replaceSpatial(RexNode conjunction, RelBuilder builder,
RexInputRef ref, RexCall hilbert) {
final RexNode op0;
final RexNode op1;
final Geometries.Geom g0;
switch (conjunction.getKind()) {
case ST_DWITHIN:
final RexCall within = (RexCall) conjunction;
op0 = within.operands.get(0);
g0 = constantGeom(op0);
op1 = within.operands.get(1);
final Geometries.Geom g1 = constantGeom(op1);
if (RexUtil.isLiteral(within.operands.get(2), true)) {
final Number distance = requireNonNull(
(Number) value(within.operands.get(2)),
() -> "distance for " + within);
switch (Double.compare(distance.doubleValue(), 0D)) {
case -1: // negative distance
return ImmutableList.of(builder.getRexBuilder().makeLiteral(false));
case 0: // zero distance
// Change "ST_DWithin(g, p, 0)" to "g = p"
conjunction = builder.equals(op0, op1);
// fall through
case 1:
if (g0 != null
&& op1.getKind() == SqlKind.ST_POINT
&& ((RexCall) op1).operands.equals(hilbert.operands)) {
// Add the new predicate before the existing predicate
// because it is cheaper to execute (albeit less selective).
return ImmutableList.of(
hilbertPredicate(builder.getRexBuilder(), ref, g0, distance),
conjunction);
} else if (g1 != null && op0.getKind() == SqlKind.ST_POINT
&& ((RexCall) op0).operands.equals(hilbert.operands)) {
// Add the new predicate before the existing predicate
// because it is cheaper to execute (albeit less selective).
return ImmutableList.of(
hilbertPredicate(builder.getRexBuilder(), ref, g1, distance),
conjunction);
}
return null; // cannot rewrite
default:
throw new AssertionError("invalid sign: " + distance);
}
}
return null; // cannot rewrite
case ST_CONTAINS:
final RexCall contains = (RexCall) conjunction;
op0 = contains.operands.get(0);
g0 = constantGeom(op0);
op1 = contains.operands.get(1);
if (g0 != null
&& op1.getKind() == SqlKind.ST_POINT
&& ((RexCall) op1).operands.equals(hilbert.operands)) {
// Add the new predicate before the existing predicate
// because it is cheaper to execute (albeit less selective).
return ImmutableList.of(
hilbertPredicate(builder.getRexBuilder(), ref, g0),
conjunction);
}
return null; // cannot rewrite
default:
return null; // cannot rewrite
}
}
/** Creates a predicate on the column that contains the index on the Hilbert
* curve.
*
* The predicate is a safe approximation. That is, it may allow some
* points that are not within the distance, but will never disallow a point
* that is within the distance.
*
*
Returns FALSE if the distance is negative (the ST_DWithin function
* would always return FALSE) and returns an {@code =} predicate if distance
* is 0. But usually returns a list of ranges,
* {@code ref BETWEEN c1 AND c2 OR ref BETWEEN c3 AND c4}. */
private static RexNode hilbertPredicate(RexBuilder rexBuilder,
RexInputRef ref, Geometries.Geom g, Number distance) {
if (distance.doubleValue() == 0D
&& Geometries.type(g.g()) == Geometries.Type.POINT) {
final Point p = (Point) g.g();
final HilbertCurve2D hilbert = new HilbertCurve2D(8);
final long index = hilbert.toIndex(p.getX(), p.getY());
return rexBuilder.makeCall(SqlStdOperatorTable.EQUALS, ref,
rexBuilder.makeExactLiteral(BigDecimal.valueOf(index)));
}
final Geometries.Geom g2 =
GeoFunctions.ST_Buffer(g, distance.doubleValue());
return hilbertPredicate(rexBuilder, ref, g2);
}
private static RexNode hilbertPredicate(RexBuilder rexBuilder,
RexInputRef ref, Geometries.Geom g2) {
final Geometries.Geom g3 = GeoFunctions.ST_Envelope(g2);
final Envelope env = (Envelope) g3.g();
final HilbertCurve2D hilbert = new HilbertCurve2D(8);
final List ranges =
hilbert.toRanges(env.getXMin(), env.getYMin(), env.getXMax(),
env.getYMax(), new SpaceFillingCurve2D.RangeComputeHints());
final List nodes = new ArrayList<>();
for (SpaceFillingCurve2D.IndexRange range : ranges) {
final BigDecimal lowerBd = BigDecimal.valueOf(range.lower());
final BigDecimal upperBd = BigDecimal.valueOf(range.upper());
nodes.add(
rexBuilder.makeCall(
SqlStdOperatorTable.AND,
rexBuilder.makeCall(SqlStdOperatorTable.GREATER_THAN_OR_EQUAL,
ref,
rexBuilder.makeExactLiteral(lowerBd)),
rexBuilder.makeCall(SqlStdOperatorTable.LESS_THAN_OR_EQUAL,
ref,
rexBuilder.makeExactLiteral(upperBd))));
}
return rexBuilder.makeCall(SqlStdOperatorTable.OR, nodes);
}
/** Rule configuration. */
@Value.Immutable
public interface Config extends RelRule.Config {
Config DEFAULT = ImmutableSpatialRules.Config.of()
.withOperandSupplier(b ->
b.operand(Filter.class)
.predicate(f -> DWITHIN_FINDER.inFilter(f)
&& !HILBERT_FINDER.inFilter(f))
.anyInputs())
.as(Config.class);
@Override default FilterHilbertRule toRule() {
return new FilterHilbertRule(this);
}
}
}
}