Skip to content

Commit 4becd31

Browse files
committed
Add registerSqlOperator() to BeamSqlEnv for custom SQL operators
Backports commit f53e1abe from dataflow/beam-spark. Adds a mutable, session-scoped ListSqlOperatorTable (extraOperatorTable) to JdbcConnection, chained at the front of the planner's operator table in CalciteQueryPlanner.defaultConfig. This allows registering custom SqlOperators (e.g. with VARIADIC operand checkers) that cannot be expressed through the schema-function auto-wrapping path. Adds BeamSqlEnvRegisterOperatorTest with tests verifying: - Operators are added to the extra operator table - Table starts empty on fresh env - Registered operators are resolvable by name
1 parent aaad9b4 commit 4becd31

4 files changed

Lines changed: 155 additions & 1 deletion

File tree

sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/BeamSqlEnv.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ public Map<String, String> getPipelineOptions() {
132132
return connection.getPipelineOptionsMap();
133133
}
134134

135+
/**
136+
* Registers a pre-built {@link
137+
* org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator} into the
138+
* session-scoped operator table chained at the front of the planner's operator table. Use this
139+
* (instead of the schema-function registration path) when the operator must declare a non-fixed
140+
* operand checker (e.g. {@code VARIADIC}) — the schema auto-wrapping path always produces a
141+
* fixed-parameter checker, which breaks SQL routine overload resolution for {@code ANY}-typed
142+
* parameters (the precedence comparison asserts on {@code ANY}).
143+
*/
144+
public void registerSqlOperator(
145+
org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator operator) {
146+
connection.getExtraOperatorTable().add(operator);
147+
}
148+
135149
public String explain(String sqlString) throws ParseException {
136150
try {
137151
return RelOptUtil.toString(planner.convertToBeamRel(sqlString, QueryParameters.ofNone()));

sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/CalciteQueryPlanner.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,17 @@ public FrameworkConfig defaultConfig(JdbcConnection connection, Collection<RuleS
158158
.ruleSets(ruleSets.toArray(new RuleSet[0]))
159159
.costFactory(BeamCostModel.FACTORY)
160160
.typeSystem(connection.getTypeFactory().getTypeSystem())
161-
.operatorTable(SqlOperatorTables.chain(opTab0, catalogReader))
161+
.operatorTable(
162+
SqlOperatorTables.chain(
163+
// Session-scoped custom operators (e.g. registered scalar Python UDFs that must
164+
// declare a non-fixed VARIADIC operand checker). Chained first and held by
165+
// reference, so operators added to the connection's table after the planner is
166+
// built
167+
// still resolve. Placing it ahead of the catalogReader avoids the duplicate
168+
// fixed-parameter overload that schema auto-wrapping would otherwise create.
169+
connection.getExtraOperatorTable(),
170+
opTab0,
171+
catalogReader))
162172
.sqlToRelConverterConfig(sqlToRelConfig)
163173
.build();
164174
}

sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/JdbcConnection.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,39 @@ public class JdbcConnection extends CalciteConnectionWrapper {
5151
private Map<String, String> pipelineOptionsMap;
5252
private @Nullable PipelineOptions pipelineOptions;
5353

54+
/**
55+
* A mutable, session-scoped operator table that callers can populate with custom {@link
56+
* org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator}s after the
57+
* connection (and its planner) is built. {@link CalciteQueryPlanner#defaultConfig} chains this
58+
* table at the front of the validator/converter operator table, so operators added here resolve
59+
* in subsequent SQL. This is the channel for functions that must declare a non-fixed (e.g. {@code
60+
* VARIADIC}) operand checker, which the schema-function auto-wrapping in {@code
61+
* CalciteCatalogReader.toOp} (always fixed-parameter {@code OperandMetadataImpl}) cannot express.
62+
* The list is held by reference in the planner config, so additions made after the planner is
63+
* built are visible.
64+
*/
65+
private final org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.util
66+
.ListSqlOperatorTable
67+
extraOperatorTable =
68+
new org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.util
69+
.ListSqlOperatorTable();
70+
5471
private JdbcConnection(CalciteConnection connection) throws SQLException {
5572
super(connection);
5673
this.pipelineOptionsMap = Collections.emptyMap();
5774
}
5875

76+
/**
77+
* The session-scoped, mutable operator table chained at the front of the planner's operator
78+
* table. Add custom {@link
79+
* org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator}s here to make them
80+
* resolvable in subsequent SQL.
81+
*/
82+
public org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.util.ListSqlOperatorTable
83+
getExtraOperatorTable() {
84+
return extraOperatorTable;
85+
}
86+
5987
/**
6088
* Wraps and initializes the initial connection created by Calcite.
6189
*
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.sdk.extensions.sql.impl;
19+
20+
import static org.junit.Assert.assertEquals;
21+
import static org.junit.Assert.assertFalse;
22+
import static org.junit.Assert.assertTrue;
23+
24+
import org.apache.beam.sdk.extensions.sql.impl.rel.BaseRelTest;
25+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlFunction;
26+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlFunctionCategory;
27+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlIdentifier;
28+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlKind;
29+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator;
30+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlSyntax;
31+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.parser.SqlParserPos;
32+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.type.OperandTypes;
33+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.type.ReturnTypes;
34+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.validate.SqlNameMatcher;
35+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.validate.SqlNameMatchers;
36+
import org.junit.Test;
37+
38+
/**
39+
* Tests for {@link BeamSqlEnv#registerSqlOperator(SqlOperator)} and the session-scoped operator
40+
* table in {@link JdbcConnection}.
41+
*/
42+
public class BeamSqlEnvRegisterOperatorTest extends BaseRelTest {
43+
44+
/** A simple custom function for testing, not present in the default operator table. */
45+
private static final String CUSTOM_FUNCTION_NAME = "BEAM_TEST_CUSTOM_FUNC";
46+
47+
private static SqlFunction createCustomFunction() {
48+
return new SqlFunction(
49+
CUSTOM_FUNCTION_NAME,
50+
SqlKind.OTHER_FUNCTION,
51+
ReturnTypes.INTEGER,
52+
null,
53+
OperandTypes.NUMERIC,
54+
SqlFunctionCategory.USER_DEFINED_FUNCTION);
55+
}
56+
57+
@Test
58+
public void testExtraOperatorTableStartsEmpty() {
59+
// A fresh env should have an empty extra operator table
60+
BeamSqlEnv freshEnv = BeamSqlEnv.readOnly("empty_test", new java.util.HashMap<>());
61+
assertTrue(
62+
"Extra operator table should be empty on a fresh env",
63+
freshEnv.connection.getExtraOperatorTable().getOperatorList().isEmpty());
64+
}
65+
66+
@Test
67+
public void testRegisterSqlOperator_addsToExtraOperatorTable() {
68+
SqlFunction customFunc = createCustomFunction();
69+
env.registerSqlOperator(customFunc);
70+
71+
// Verify the operator is in the extra operator table
72+
assertTrue(
73+
"Registered operator should be present in the extra operator table",
74+
env.connection.getExtraOperatorTable().getOperatorList().contains(customFunc));
75+
assertEquals(1, env.connection.getExtraOperatorTable().getOperatorList().size());
76+
}
77+
78+
@Test
79+
public void testRegisterSqlOperator_makesOperatorResolvableByName() {
80+
SqlFunction customFunc = createCustomFunction();
81+
env.registerSqlOperator(customFunc);
82+
83+
// Verify the operator is resolvable by name in the extra operator table
84+
SqlNameMatcher nameMatcher = SqlNameMatchers.withCaseSensitive(false);
85+
java.util.List<SqlOperator> resolvedOps = new java.util.ArrayList<>();
86+
env.connection
87+
.getExtraOperatorTable()
88+
.lookupOperatorOverloads(
89+
new SqlIdentifier(CUSTOM_FUNCTION_NAME, SqlParserPos.ZERO),
90+
SqlFunctionCategory.USER_DEFINED_FUNCTION,
91+
SqlSyntax.FUNCTION,
92+
resolvedOps,
93+
nameMatcher);
94+
95+
assertFalse(
96+
"Custom operator should be resolvable by name after registration",
97+
resolvedOps.isEmpty());
98+
assertTrue(
99+
"The resolved operator should be the one we registered",
100+
resolvedOps.contains(customFunc));
101+
}
102+
}

0 commit comments

Comments
 (0)