Skip to content

Commit 2434619

Browse files
authored
Add registerSqlOperator() to BeamSqlEnv for custom SQL operators (#39432)
* 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 * format imports
1 parent 39c0dde commit 2434619

4 files changed

Lines changed: 144 additions & 1 deletion

File tree

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.plan.RelOptUtil;
5050
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.Function;
5151
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlKind;
52+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator;
5253
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.tools.RuleSet;
5354
import org.checkerframework.checker.nullness.qual.Nullable;
5455

@@ -132,6 +133,18 @@ public Map<String, String> getPipelineOptions() {
132133
return connection.getPipelineOptionsMap();
133134
}
134135

136+
/**
137+
* Registers a custom {@link SqlOperator} into the current session's operator table. This allows
138+
* registering functions with non-fixed operand types (e.g. VARIADIC), which the schema-function
139+
* auto-wrapping mechanism cannot express.
140+
*
141+
* <p>Only safe to call before the first SQL query is planned in this environment (otherwise the
142+
* parser/validator operator table may have already been built without it).
143+
*/
144+
public void registerSqlOperator(SqlOperator operator) {
145+
connection.getExtraOperatorTable().add(operator);
146+
}
147+
135148
public String explain(String sqlString) throws ParseException {
136149
try {
137150
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: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,15 @@ 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(), opTab0, catalogReader))
162170
.sqlToRelConverterConfig(sqlToRelConfig)
163171
.build();
164172
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.jdbc.CalciteSchema;
3232
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.Schema;
3333
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.SchemaPlus;
34+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperator;
35+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.util.ListSqlOperatorTable;
3436
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
3537
import org.checkerframework.checker.nullness.qual.Nullable;
3638

@@ -51,11 +53,31 @@ public class JdbcConnection extends CalciteConnectionWrapper {
5153
private Map<String, String> pipelineOptionsMap;
5254
private @Nullable PipelineOptions pipelineOptions;
5355

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

73+
/**
74+
* The session-scoped, mutable operator table chained at the front of the planner's operator
75+
* table. Add custom {@link SqlOperator}s here to make them resolvable in subsequent SQL.
76+
*/
77+
public ListSqlOperatorTable getExtraOperatorTable() {
78+
return extraOperatorTable;
79+
}
80+
5981
/**
6082
* Wraps and initializes the initial connection created by Calcite.
6183
*
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
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", resolvedOps.isEmpty());
97+
assertTrue(
98+
"The resolved operator should be the one we registered", resolvedOps.contains(customFunc));
99+
}
100+
}

0 commit comments

Comments
 (0)