Calcite's spatial operator table cannot initialize under Calcite 1.43's own model class allowlist, so every consumer of this package has to name two of Calcite's own classes before a spatial query will run. This package is the right place to name them once.
What happens without it
SELECT ST_Point(1, 2) -- fun=spatial, conformance=LENIENT
com.google.common.util.concurrent.UncheckedExecutionException:
java.lang.SecurityException: Class 'org.apache.calcite.runtime.SpatialTypeFunctions' rejected by the allowlist.
Validation succeeds — ST_POINT resolves — and it fails at execution, which reads like a runtime fault rather than a configuration one.
Why
1.43 added ClassNameFilter and calcite.model.classes.allowed, so a model JSON cannot name an arbitrary class. The allowlist defaults to empty and check throws when nothing matches. SqlSpatialTypeOperatorTable's constructor then registers Calcite's own spatial classes through ModelHandler.addFunctions(SchemaPlus, ...), the overload that consults ClassNameFilter.standard():
// core/src/main/java/org/apache/calcite/sql/SqlSpatialTypeOperatorTable.java, lines 56 and 60
ModelHandler.addFunctions(schema, null, ImmutableList.of(),
SpatialTypeFunctions.class.getName(), "*", true);
ModelHandler.addFunctions(schema, null, ImmutableList.of(),
SqlSpatialTypeFunctions.class.getName(), "*", true);
So the operator table asks the deployer's permission to load two classes that ship inside calcite-core. Arguably an upstream bug — the allowlist exists to constrain what a model file can name, and no model names these — but it is 1.43's behaviour, and something has to name them for spatial to work at all.
Why here
Two classes, both Calcite's own, neither user-supplied. Naming them takes nothing away from what the allowlist is for: the denylist that stops JNDI, Runtime, script engines and the rest is untouched, and any pattern the hosting application set is preserved by appending.
The alternative is that every consumer does it, and doing it correctly is harder than it looks. ClassNameFilter.STANDARD is a static final built from the property the first time the class initializes, so setting the property later is silently ignored. That makes the failure order-dependent rather than deterministic: measured while adding NetTopologySuite support to the EF Core provider, setting the property inside the connection factory gave nine failures on one run of a test assembly and zero on the next, from the same build, purely on whether some earlier test had planned a query first. A module initializer in this package runs at assembly load and has no such race.
Suggested shape
The same shape the EF Core provider already uses for its own namespace (CalciteModelAllowlistInitializer in Apache.Calcite.EntityFrameworkCore): a [ModuleInitializer] that appends rather than replaces, and never throws.
[ModuleInitializer]
internal static void Initialize()
{
try
{
const string property = "calcite.model.classes.allowed";
const string spatial = "org.apache.calcite.runtime.SpatialTypeFunctions,org.apache.calcite.sql.fun.SqlSpatialTypeFunctions";
var allowed = java.lang.System.getProperty(property) ?? "";
if (allowed.Contains("SpatialTypeFunctions", StringComparison.Ordinal) == false)
java.lang.System.setProperty(property, allowed.Length > 0 ? allowed + "," + spatial : spatial);
}
catch
{
// never fail assembly load
}
}
What this does not cover
Two other things are required for a spatial query and are not bugs, listed so the three do not get conflated. They are the caller's connection configuration and should stay that way:
fun must name spatial. fun=all does not include it — SqlLibrary.ALL.children() is BigQuery, Calcite, Hive, MSSQL, MySQL, Oracle, PostgreSQL, Redshift, Snowflake, Spark, ClickHouse — and spatial functions under fun=all fail validation with No match found for function signature ST_POINT(<NUMERIC>, <NUMERIC>).
- The conformance must allow the
GEOMETRY type, or DDL naming it fails to parse with Geo-spatial extensions and the GEOMETRY data type are not enabled. SqlConformanceEnum.allowGeometry() is true for BABEL, LENIENT, MYSQL_5, SQL_SERVER_2008, PRESTO.
Verified
With the two classes named, and fun=spatial plus conformance=LENIENT on the connection, measured on 1.43.0-SNAPSHOT:
|
|
SELECT ST_Point(1, 2) |
POINT (1 2) |
SELECT ST_Distance(ST_Point(0, 0), ST_Point(3, 4)) |
5 |
SELECT ST_Contains(ST_GeomFromText('POLYGON((0 0, 0 4, 4 4, 4 0, 0 0))'), ST_Point(1, 1)) |
true |
CREATE TABLE/INSERT/SELECT over a GEOMETRY column |
round-trips, and GetCalciteValue hands back the JTS geometry |
Related: ikvmnet/calcite-efcore#64 records the same finding from the provider's side, and #153 here is a separate matter about GetFieldValue<Geometry>.
Calcite's spatial operator table cannot initialize under Calcite 1.43's own model class allowlist, so every consumer of this package has to name two of Calcite's own classes before a spatial query will run. This package is the right place to name them once.
What happens without it
Validation succeeds —
ST_POINTresolves — and it fails at execution, which reads like a runtime fault rather than a configuration one.Why
1.43 added
ClassNameFilterandcalcite.model.classes.allowed, so a model JSON cannot name an arbitrary class. The allowlist defaults to empty andcheckthrows when nothing matches.SqlSpatialTypeOperatorTable's constructor then registers Calcite's own spatial classes throughModelHandler.addFunctions(SchemaPlus, ...), the overload that consultsClassNameFilter.standard():So the operator table asks the deployer's permission to load two classes that ship inside calcite-core. Arguably an upstream bug — the allowlist exists to constrain what a model file can name, and no model names these — but it is 1.43's behaviour, and something has to name them for spatial to work at all.
Why here
Two classes, both Calcite's own, neither user-supplied. Naming them takes nothing away from what the allowlist is for: the denylist that stops JNDI,
Runtime, script engines and the rest is untouched, and any pattern the hosting application set is preserved by appending.The alternative is that every consumer does it, and doing it correctly is harder than it looks.
ClassNameFilter.STANDARDis astatic finalbuilt from the property the first time the class initializes, so setting the property later is silently ignored. That makes the failure order-dependent rather than deterministic: measured while adding NetTopologySuite support to the EF Core provider, setting the property inside the connection factory gave nine failures on one run of a test assembly and zero on the next, from the same build, purely on whether some earlier test had planned a query first. A module initializer in this package runs at assembly load and has no such race.Suggested shape
The same shape the EF Core provider already uses for its own namespace (
CalciteModelAllowlistInitializerinApache.Calcite.EntityFrameworkCore): a[ModuleInitializer]that appends rather than replaces, and never throws.What this does not cover
Two other things are required for a spatial query and are not bugs, listed so the three do not get conflated. They are the caller's connection configuration and should stay that way:
funmust namespatial.fun=alldoes not include it —SqlLibrary.ALL.children()is BigQuery, Calcite, Hive, MSSQL, MySQL, Oracle, PostgreSQL, Redshift, Snowflake, Spark, ClickHouse — and spatial functions underfun=allfail validation withNo match found for function signature ST_POINT(<NUMERIC>, <NUMERIC>).GEOMETRYtype, or DDL naming it fails to parse withGeo-spatial extensions and the GEOMETRY data type are not enabled.SqlConformanceEnum.allowGeometry()is true forBABEL,LENIENT,MYSQL_5,SQL_SERVER_2008,PRESTO.Verified
With the two classes named, and
fun=spatialplusconformance=LENIENTon the connection, measured on 1.43.0-SNAPSHOT:SELECT ST_Point(1, 2)POINT (1 2)SELECT ST_Distance(ST_Point(0, 0), ST_Point(3, 4))5SELECT ST_Contains(ST_GeomFromText('POLYGON((0 0, 0 4, 4 4, 4 0, 0 0))'), ST_Point(1, 1))trueCREATE TABLE/INSERT/SELECTover aGEOMETRYcolumnGetCalciteValuehands back the JTS geometryRelated: ikvmnet/calcite-efcore#64 records the same finding from the provider's side, and #153 here is a separate matter about
GetFieldValue<Geometry>.