Skip to content

Commit 885a3da

Browse files
committedJul 28, 2020
[CALCITE-2160] Spatial: Add functions ST_MakeGrid and ST_MakeGridPoints
These will be the foundations of a spatial grid index, to accelerate polygon-to-polygon spatial joins. Add "states" and "parks" data sets. Fix lateral references to fields.
1 parent 19edf52 commit 885a3da

File tree

9 files changed

+656
-17
lines changed

9 files changed

+656
-17
lines changed
 

‎core/src/main/java/org/apache/calcite/prepare/CalciteCatalogReader.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -272,15 +272,18 @@ public void lookupOperatorOverloads(final SqlIdentifier opName,
272272
.forEachOrdered(operatorList::add);
273273
}
274274

275-
/** Creates an operator table that contains functions in the given class.
275+
/** Creates an operator table that contains functions in the given class
276+
* or classes.
276277
*
277278
* @see ModelHandler#addFunctions */
278-
public static SqlOperatorTable operatorTable(String className) {
279+
public static SqlOperatorTable operatorTable(String... classNames) {
279280
// Dummy schema to collect the functions
280281
final CalciteSchema schema =
281282
CalciteSchema.createRootSchema(false, false);
282-
ModelHandler.addFunctions(schema.plus(), null, ImmutableList.of(),
283-
className, "*", true);
283+
for (String className : classNames) {
284+
ModelHandler.addFunctions(schema.plus(), null, ImmutableList.of(),
285+
className, "*", true);
286+
}
284287

285288
// The following is technical debt; see [CALCITE-2082] Remove
286289
// RelDataTypeFactory argument from SqlUserDefinedAggFunction constructor

‎core/src/main/java/org/apache/calcite/runtime/GeoFunctions.java

+96
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package org.apache.calcite.runtime;
1818

19+
import org.apache.calcite.linq4j.AbstractEnumerable;
20+
import org.apache.calcite.linq4j.Enumerator;
1921
import org.apache.calcite.linq4j.function.Deterministic;
2022
import org.apache.calcite.linq4j.function.Experimental;
2123
import org.apache.calcite.linq4j.function.SemiStrict;
@@ -179,6 +181,22 @@ public static Geom ST_MPolyFromText(String wkt, int srid) {
179181

180182
// Geometry creation functions ==============================================
181183

184+
/** Calculates a regular grid of polygons based on {@code geom}. */
185+
private static void ST_MakeGrid(final Geom geom,
186+
final BigDecimal deltaX, final BigDecimal deltaY) {
187+
// This is a dummy function. We cannot include table functions in this
188+
// package, because they have too many dependencies. See the real definition
189+
// in SqlGeoFunctions.
190+
}
191+
192+
/** Calculates a regular grid of points based on {@code geom}. */
193+
private static void ST_MakeGridPoints(final Geom geom,
194+
final BigDecimal deltaX, final BigDecimal deltaY) {
195+
// This is a dummy function. We cannot include table functions in this
196+
// package, because they have too many dependencies. See the real definition
197+
// in SqlGeoFunctions.
198+
}
199+
182200
/** Creates a line-string from the given POINTs (or MULTIPOINTs). */
183201
public static Geom ST_MakeLine(Geom geom1, Geom geom2) {
184202
return makeLine(geom1, geom2);
@@ -655,4 +673,82 @@ enum Type {
655673
this.code = code;
656674
}
657675
}
676+
677+
/** Used at run time by the {@link #ST_MakeGrid} and
678+
* {@link #ST_MakeGridPoints} functions. */
679+
public static class GridEnumerable extends AbstractEnumerable<Object[]> {
680+
private final Envelope envelope;
681+
private final boolean point;
682+
private final double deltaX;
683+
private final double deltaY;
684+
private final double minX;
685+
private final double minY;
686+
private final int baseX;
687+
private final int baseY;
688+
private final int spanX;
689+
private final int spanY;
690+
private final int area;
691+
692+
public GridEnumerable(Envelope envelope, BigDecimal deltaX,
693+
BigDecimal deltaY, boolean point) {
694+
this.envelope = envelope;
695+
this.deltaX = deltaX.doubleValue();
696+
this.deltaY = deltaY.doubleValue();
697+
this.point = point;
698+
this.spanX = (int) Math.floor((envelope.getXMax() - envelope.getXMin())
699+
/ this.deltaX) + 1;
700+
this.baseX = (int) Math.floor(envelope.getXMin() / this.deltaX);
701+
this.minX = this.deltaX * baseX;
702+
this.spanY = (int) Math.floor((envelope.getYMax() - envelope.getYMin())
703+
/ this.deltaY) + 1;
704+
this.baseY = (int) Math.floor(envelope.getYMin() / this.deltaY);
705+
this.minY = this.deltaY * baseY;
706+
this.area = this.spanX * this.spanY;
707+
}
708+
709+
@Override public Enumerator<Object[]> enumerator() {
710+
return new Enumerator<Object[]>() {
711+
int id = -1;
712+
713+
@Override public Object[] current() {
714+
final Geom geom;
715+
final int x = id % spanX;
716+
final int y = id / spanX;
717+
if (point) {
718+
final double xCurrent = minX + (x + 0.5D) * deltaX;
719+
final double yCurrent = minY + (y + 0.5D) * deltaY;
720+
geom = ST_MakePoint(BigDecimal.valueOf(xCurrent),
721+
BigDecimal.valueOf(yCurrent));
722+
} else {
723+
final Polygon polygon = new Polygon();
724+
final double left = minX + x * deltaX;
725+
final double right = left + deltaX;
726+
final double bottom = minY + y * deltaY;
727+
final double top = bottom + deltaY;
728+
729+
final Polyline polyline = new Polyline();
730+
polyline.addSegment(new Line(left, bottom, right, bottom), true);
731+
polyline.addSegment(new Line(right, bottom, right, top), false);
732+
polyline.addSegment(new Line(right, top, left, top), false);
733+
polyline.addSegment(new Line(left, top, left, bottom), false);
734+
polygon.add(polyline, false);
735+
geom = new SimpleGeom(polygon);
736+
}
737+
return new Object[] {geom, id, x + 1, y + 1, baseX + x, baseY + y};
738+
}
739+
740+
public boolean moveNext() {
741+
return ++id < area;
742+
}
743+
744+
public void reset() {
745+
id = -1;
746+
}
747+
748+
public void close() {
749+
}
750+
};
751+
}
752+
}
753+
658754
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.calcite.sql.fun;
18+
19+
import org.apache.calcite.DataContext;
20+
import org.apache.calcite.config.CalciteConnectionConfig;
21+
import org.apache.calcite.linq4j.Enumerable;
22+
import org.apache.calcite.linq4j.Linq4j;
23+
import org.apache.calcite.rel.type.RelDataType;
24+
import org.apache.calcite.rel.type.RelDataTypeFactory;
25+
import org.apache.calcite.runtime.GeoFunctions;
26+
import org.apache.calcite.schema.ScannableTable;
27+
import org.apache.calcite.schema.Schema;
28+
import org.apache.calcite.schema.Statistic;
29+
import org.apache.calcite.schema.Statistics;
30+
import org.apache.calcite.sql.SqlCall;
31+
import org.apache.calcite.sql.SqlNode;
32+
import org.apache.calcite.sql.type.SqlTypeName;
33+
import org.apache.calcite.util.ImmutableBitSet;
34+
35+
import com.esri.core.geometry.Envelope;
36+
import com.esri.core.geometry.Geometry;
37+
import com.google.common.collect.ImmutableList;
38+
39+
import java.math.BigDecimal;
40+
41+
/**
42+
* Utilities for Geo/Spatial functions.
43+
*
44+
* <p>Includes some table functions, and may in future include other functions
45+
* that have dependencies beyond the {@code org.apache.calcite.runtime} package.
46+
*/
47+
public class SqlGeoFunctions {
48+
private SqlGeoFunctions() {}
49+
50+
// Geometry table functions =================================================
51+
52+
/** Calculates a regular grid of polygons based on {@code geom}.
53+
*
54+
* @see GeoFunctions#ST_MakeGrid */
55+
@SuppressWarnings({"WeakerAccess", "unused"})
56+
public static ScannableTable ST_MakeGrid(final GeoFunctions.Geom geom,
57+
final BigDecimal deltaX, final BigDecimal deltaY) {
58+
return new GridTable(geom, deltaX, deltaY, false);
59+
}
60+
61+
/** Calculates a regular grid of points based on {@code geom}.
62+
*
63+
* @see GeoFunctions#ST_MakeGridPoints */
64+
@SuppressWarnings({"WeakerAccess", "unused"})
65+
public static ScannableTable ST_MakeGridPoints(final GeoFunctions.Geom geom,
66+
final BigDecimal deltaX, final BigDecimal deltaY) {
67+
return new GridTable(geom, deltaX, deltaY, true);
68+
}
69+
70+
/** Returns the points or rectangles in a grid that covers a given
71+
* geometry. */
72+
public static class GridTable implements ScannableTable {
73+
private final GeoFunctions.Geom geom;
74+
private final BigDecimal deltaX;
75+
private final BigDecimal deltaY;
76+
private boolean point;
77+
78+
GridTable(GeoFunctions.Geom geom, BigDecimal deltaX, BigDecimal deltaY,
79+
boolean point) {
80+
this.geom = geom;
81+
this.deltaX = deltaX;
82+
this.deltaY = deltaY;
83+
this.point = point;
84+
}
85+
86+
@Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
87+
return typeFactory.builder()
88+
// a point (for ST_MakeGridPoints) or a rectangle (for ST_MakeGrid)
89+
.add("THE_GEOM", SqlTypeName.GEOMETRY)
90+
// in [0, width * height)
91+
.add("ID", SqlTypeName.INTEGER)
92+
// in [1, width]
93+
.add("ID_COL", SqlTypeName.INTEGER)
94+
// in [1, height]
95+
.add("ID_ROW", SqlTypeName.INTEGER)
96+
// absolute column, with 0 somewhere near the origin; not standard
97+
.add("ABS_COL", SqlTypeName.INTEGER)
98+
// absolute row, with 0 somewhere near the origin; not standard
99+
.add("ABS_ROW", SqlTypeName.INTEGER)
100+
.build();
101+
}
102+
103+
public Enumerable<Object[]> scan(DataContext root) {
104+
if (geom != null && deltaX != null && deltaY != null) {
105+
final Geometry geometry = geom.g();
106+
final Envelope envelope = new Envelope();
107+
geometry.queryEnvelope(envelope);
108+
if (deltaX.compareTo(BigDecimal.ZERO) > 0
109+
&& deltaY.compareTo(BigDecimal.ZERO) > 0) {
110+
return new GeoFunctions.GridEnumerable(envelope, deltaX, deltaY,
111+
point);
112+
}
113+
}
114+
return Linq4j.emptyEnumerable();
115+
}
116+
117+
public Statistic getStatistic() {
118+
return Statistics.of(100d, ImmutableList.of(ImmutableBitSet.of(0, 1)));
119+
}
120+
121+
public Schema.TableType getJdbcTableType() {
122+
return Schema.TableType.OTHER;
123+
}
124+
125+
public boolean isRolledUp(String column) {
126+
return false;
127+
}
128+
129+
public boolean rolledUpColumnValidInsideAgg(String column, SqlCall call,
130+
SqlNode parent, CalciteConnectionConfig config) {
131+
return false;
132+
}
133+
}
134+
}

‎core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperatorTableFactory.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ private SqlOperatorTable create(ImmutableSet<SqlLibrary> librarySet) {
8686
break;
8787
case SPATIAL:
8888
list.addAll(
89-
CalciteCatalogReader.operatorTable(GeoFunctions.class.getName())
90-
.getOperatorList());
89+
CalciteCatalogReader.operatorTable(GeoFunctions.class.getName(),
90+
SqlGeoFunctions.class.getName()).getOperatorList());
9191
break;
9292
default:
9393
custom = true;

‎core/src/test/java/org/apache/calcite/test/CalciteAssert.java

+29-6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import org.apache.calcite.schema.impl.ViewTableMacro;
5454
import org.apache.calcite.sql.SqlDialect;
5555
import org.apache.calcite.sql.SqlExplainLevel;
56+
import org.apache.calcite.sql.fun.SqlGeoFunctions;
5657
import org.apache.calcite.sql.type.SqlTypeName;
5758
import org.apache.calcite.sql.validate.SqlConformanceEnum;
5859
import org.apache.calcite.sql.validate.SqlValidatorException;
@@ -750,6 +751,7 @@ public static SchemaPlus addSchema(SchemaPlus rootSchema, SchemaSpec schema) {
750751
final SchemaPlus scott;
751752
final ConnectionSpec cs;
752753
final DataSource dataSource;
754+
final ImmutableList<String> emptyPath = ImmutableList.of();
753755
switch (schema) {
754756
case REFLECTIVE_FOODMART:
755757
return rootSchema.add(schema.schemaName,
@@ -793,16 +795,37 @@ public static SchemaPlus addSchema(SchemaPlus rootSchema, SchemaSpec schema) {
793795
foodmart = addSchemaIfNotExists(rootSchema, SchemaSpec.JDBC_FOODMART);
794796
return rootSchema.add("foodmart2", new CloneSchema(foodmart));
795797
case GEO:
796-
ModelHandler.addFunctions(rootSchema, null, ImmutableList.of(),
798+
ModelHandler.addFunctions(rootSchema, null, emptyPath,
797799
GeoFunctions.class.getName(), "*", true);
800+
ModelHandler.addFunctions(rootSchema, null, emptyPath,
801+
SqlGeoFunctions.class.getName(), "*", true);
798802
final SchemaPlus s =
799803
rootSchema.add(schema.schemaName, new AbstractSchema());
800-
ModelHandler.addFunctions(s, "countries", ImmutableList.of(),
804+
ModelHandler.addFunctions(s, "countries", emptyPath,
801805
CountriesTableFunction.class.getName(), null, false);
802806
final String sql = "select * from table(\"countries\"(true))";
803807
final ViewTableMacro viewMacro = ViewTable.viewMacro(rootSchema, sql,
804-
ImmutableList.of("GEO"), ImmutableList.of(), false);
808+
ImmutableList.of("GEO"), emptyPath, false);
805809
s.add("countries", viewMacro);
810+
811+
ModelHandler.addFunctions(s, "states", emptyPath,
812+
StatesTableFunction.class.getName(), "states", false);
813+
final String sql2 = "select \"name\",\n"
814+
+ " ST_PolyFromText(\"geom\") as \"geom\"\n"
815+
+ "from table(\"states\"(true))";
816+
final ViewTableMacro viewMacro2 = ViewTable.viewMacro(rootSchema, sql2,
817+
ImmutableList.of("GEO"), emptyPath, false);
818+
s.add("states", viewMacro2);
819+
820+
ModelHandler.addFunctions(s, "parks", emptyPath,
821+
StatesTableFunction.class.getName(), "parks", false);
822+
final String sql3 = "select \"name\",\n"
823+
+ " ST_PolyFromText(\"geom\") as \"geom\"\n"
824+
+ "from table(\"parks\"(true))";
825+
final ViewTableMacro viewMacro3 = ViewTable.viewMacro(rootSchema, sql3,
826+
ImmutableList.of("GEO"), emptyPath, false);
827+
s.add("parks", viewMacro3);
828+
806829
return s;
807830
case HR:
808831
return rootSchema.add(schema.schemaName,
@@ -835,7 +858,7 @@ public static SchemaPlus addSchema(SchemaPlus rootSchema, SchemaSpec schema) {
835858
+ " ('Grace', 60, 'F'),\n"
836859
+ " ('Wilma', cast(null as integer), 'F'))\n"
837860
+ " as t(ename, deptno, gender)",
838-
ImmutableList.of(), ImmutableList.of("POST", "EMP"),
861+
emptyPath, ImmutableList.of("POST", "EMP"),
839862
null));
840863
post.add("DEPT",
841864
ViewTable.viewMacro(post,
@@ -844,7 +867,7 @@ public static SchemaPlus addSchema(SchemaPlus rootSchema, SchemaSpec schema) {
844867
+ " (20, 'Marketing'),\n"
845868
+ " (30, 'Engineering'),\n"
846869
+ " (40, 'Empty')) as t(deptno, dname)",
847-
ImmutableList.of(), ImmutableList.of("POST", "DEPT"),
870+
emptyPath, ImmutableList.of("POST", "DEPT"),
848871
null));
849872
post.add("DEPT30",
850873
ViewTable.viewMacro(post,
@@ -860,7 +883,7 @@ public static SchemaPlus addSchema(SchemaPlus rootSchema, SchemaSpec schema) {
860883
+ " (120, 'Wilma', 20, 'F', CAST(NULL AS VARCHAR(20)), 1, 5, UNKNOWN, TRUE, DATE '2005-09-07'),\n"
861884
+ " (130, 'Alice', 40, 'F', 'Vancouver', 2, CAST(NULL AS INT), FALSE, TRUE, DATE '2007-01-01'))\n"
862885
+ " as t(empno, name, deptno, gender, city, empid, age, slacker, manager, joinedat)",
863-
ImmutableList.of(), ImmutableList.of("POST", "EMPS"),
886+
emptyPath, ImmutableList.of("POST", "EMPS"),
864887
null));
865888
post.add("TICKER",
866889
ViewTable.viewMacro(post,

0 commit comments

Comments
 (0)
Please sign in to comment.