Eric Pugh created CALCITE-7730:
----------------------------------
Summary: Provide a hook in the JDBC driver to inject a custom
SqlOperatorTable without mutating the global SqlStdOperatorTable singleton
Key: CALCITE-7730
URL: https://issues.apache.org/jira/browse/CALCITE-7730
Project: Calcite
Issue Type: Improvement
Affects Versions: 1.42.0
Reporter: Eric Pugh
This was discovered during the upgrade of Solr's use of Calcite! Here is the
PR where I discovered the gap: [https://github.com/apache/solr/pull/3119]
We can't eliminate our last use of deprecated code ;-(.
## Problem
`ReflectiveSqlOperatorTable.register()` is deprecated (since CALCITE-6024) in
favour of
`SqlOperatorTables.of()` / `SqlOperatorTables.chain()`. However, for projects
that use
Calcite through its standard JDBC driver (`org.apache.calcite.jdbc.Driver`),
there is
currently no non-deprecated way to make custom `SqlFunction` operators visible
to the SQL
validator.
The validator's operator table is assembled inside
`CalcitePrepareImpl.createSqlValidator()`, which is `private static`. It chains:
1. `context.config().fun(SqlOperatorTable.class,
SqlStdOperatorTable.instance())`
2. `CalciteCatalogReader`
The `fun()` path only supports named built-in libraries (`SqlLibrary`), not
arbitrary
`SqlOperatorTable` instances. There is no protected method in
`CalcitePrepareImpl` or
`CalcitePreparingStmt` (a private inner class) to override.
`Driver.withPrepareFactory()`
exists, but subclassing `CalcitePreparingStmt` to change `createSqlValidator`
is not
possible without access to the private inner class.
## Consequence
JDBC-based Calcite adapters that need custom SQL-level functions (e.g.
`SqlFunction`
subclasses) are forced to mutate the global `SqlStdOperatorTable` singleton via
the
deprecated `register()` call. This is a global side-effect and cannot be scoped
to a
single connection or driver instance.
Projects using the `Frameworks` API (Apache Flink, etc.) do not have this
problem because
they construct their own `SqlValidator` directly and can pass any
`SqlOperatorTable` they
like. The JDBC path has no equivalent injection point.
Note: Flink's `FlinkSqlOperatorTable` also extends `ReflectiveSqlOperatorTable`
and calls
`register()` for its dynamic functions, suggesting this is a common pain point
beyond just
Solr.
## Suggested Fix
One or more of the following would resolve the issue:
1. **Add a protected `createOperatorTable()` method to `CalcitePrepareImpl`**
that
subclasses can override to return a chained `SqlOperatorTable`.
2. **Add a `CalciteConnectionProperty` (or connection URL parameter) that
accepts a
`SqlOperatorTable` instance** (or a factory class name) so JDBC users can
supply custom
operators without touching the global singleton.
3. **Make `CalcitePreparingStmt` a top-level or protected class** so that
`createSqlValidator()` can be overridden by subclasses returned from
`getPreparingStmt()`.
Any of these would allow the non-deprecated `SqlOperatorTables.of()` /
`chain()` API to
be used end-to-end in a JDBC context.
## Workaround
Currently the only viable workaround is:
```java
@SuppressWarnings("deprecation")
private void registerUDFs() {
// register() is deprecated but there is no non-deprecated injection point
// for custom SqlFunction operators in the JDBC driver path.
SqlStdOperatorTable.instance().register(new MyCustomFunction());
}
Environment
- Calcite version: 1.42.0
- Usage: JDBC driver (org.apache.calcite.jdbc.Driver subclass)
- Affected class: CalcitePrepareImpl.createSqlValidator (private static, not
overridable)
--
This message was sent by Atlassian Jira
(v8.20.10#820010)