Skip to content

Commit ce39a8f

Browse files
committed
SQL API Extensions, Usability Improvements, and Subquery Decorrelation
1 parent d521df8 commit ce39a8f

12 files changed

Lines changed: 566 additions & 29 deletions

File tree

sdks/java/extensions/sql/src/main/codegen/includes/parserImpls.ftl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,7 @@ SqlCreate SqlCreateDatabase(Span s, boolean replace) :
381381
}
382382

383383
/**
384-
* USE DATABASE ( catalog_name '.' )? database_name
384+
* USE [ DATABASE ] ( catalog_name '.' )? database_name
385385
*/
386386
SqlCall SqlUseDatabase(Span s, String scope) :
387387
{
@@ -391,7 +391,7 @@ SqlCall SqlUseDatabase(Span s, String scope) :
391391
<USE> {
392392
s.add(this);
393393
}
394-
<DATABASE>
394+
[ <DATABASE> ]
395395
databaseName = CompoundIdentifier()
396396
{
397397
return new SqlUseDatabase(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class BeamCalciteTable extends AbstractQueryableTable
5353
private final Map<String, String> pipelineOptionsMap;
5454
private @Nullable PipelineOptions pipelineOptions;
5555

56-
BeamCalciteTable(
56+
public BeamCalciteTable(
5757
BeamSqlTable beamTable,
5858
Map<String, String> pipelineOptionsMap,
5959
@Nullable PipelineOptions pipelineOptions) {

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,23 @@
4747
import org.apache.beam.sdk.transforms.SerializableFunction;
4848
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.jdbc.CalcitePrepare;
4949
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.plan.RelOptUtil;
50+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.RelNode;
5051
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.Function;
5152
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlKind;
53+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.tools.RelBuilder;
5254
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.tools.RuleSet;
5355
import org.checkerframework.checker.nullness.qual.Nullable;
56+
import org.slf4j.Logger;
57+
import org.slf4j.LoggerFactory;
5458

5559
/**
5660
* Contains the metadata of tables/UDF functions, and exposes APIs to
5761
* query/validate/optimize/translate SQL statements.
5862
*/
5963
@Internal
6064
public class BeamSqlEnv {
65+
private static final Logger LOG = LoggerFactory.getLogger(BeamSqlEnv.class);
66+
6167
JdbcConnection connection;
6268
QueryPlanner planner;
6369

@@ -116,6 +122,31 @@ public BeamRelNode parseQuery(String query, QueryParameters queryParameters)
116122
return planner.convertToBeamRel(query, queryParameters);
117123
}
118124

125+
public QueryPlanner getPlanner() {
126+
return planner;
127+
}
128+
129+
public RelBuilder getRelBuilder() {
130+
return planner.getRelBuilder();
131+
}
132+
133+
public BeamRelNode convertToBeamRel(RelNode relNode) {
134+
return planner.convertToBeamRel(relNode, QueryParameters.ofNone());
135+
}
136+
137+
public RelNode parseLogicalPlan(String query) throws ParseException {
138+
return planner.parseToRel(query, QueryParameters.ofNone());
139+
}
140+
141+
public void registerSchemaFunction(String name, Function function) {
142+
connection.getCurrentSchemaPlus().add(name, function);
143+
}
144+
145+
public org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperatorTable
146+
getOperatorTable() {
147+
return planner.getOperatorTable();
148+
}
149+
119150
public boolean isDdl(String sqlStatement) throws ParseException {
120151
return planner.parse(sqlStatement).getKind().belongsTo(SqlKind.DDL);
121152
}
@@ -196,6 +227,7 @@ public BeamSqlEnvBuilder setCurrentSchema(String name) {
196227

197228
/** Set the ruleSet used for query optimizer. */
198229
public BeamSqlEnvBuilder setRuleSets(Collection<RuleSet> ruleSets) {
230+
LOG.info("Setting BeamSqlEnv rulesets to: {}", ruleSets);
199231
this.ruleSets = ruleSets;
200232
return this;
201233
}
@@ -262,6 +294,7 @@ public BeamSqlEnv build() {
262294

263295
configureSchemas(jdbcConnection);
264296

297+
LOG.info("Instantiating planner with ruleSets: {}", ruleSets);
265298
QueryPlanner planner = instantiatePlanner(jdbcConnection, ruleSets);
266299

267300
// The planner may choose to add its own builtin functions to the schema, so load user-defined

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

Lines changed: 302 additions & 21 deletions
Large diffs are not rendered by default.

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import java.util.List;
2323
import java.util.Map;
2424
import org.apache.beam.sdk.extensions.sql.impl.rel.BeamRelNode;
25+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.RelNode;
2526
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlNode;
27+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.tools.RelBuilder;
2628
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.tools.RuleSet;
2729
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
2830
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
@@ -39,9 +41,29 @@ public interface QueryPlanner {
3941
BeamRelNode convertToBeamRel(String sqlStatement, QueryParameters queryParameters)
4042
throws ParseException, SqlConversionException;
4143

44+
/** It parses and validate the input query, then convert into a {@link BeamRelNode} tree. */
45+
BeamRelNode convertToBeamRel(RelNode sqlStatement, QueryParameters queryParameters)
46+
throws SqlConversionException;
47+
48+
/**
49+
* Parses and validates {@code sqlStatement} and returns the logical {@link RelNode} (standard
50+
* Calcite convention), WITHOUT converting to Beam physical rels. This lets callers rewrite the
51+
* logical plan (e.g. inject custom rels) before {@link #convertToBeamRel(RelNode,
52+
* QueryParameters)}.
53+
*/
54+
default RelNode parseToRel(String sqlStatement, QueryParameters queryParameters)
55+
throws ParseException, SqlConversionException {
56+
throw new UnsupportedOperationException(
57+
"parseToRel is not implemented by " + getClass().getName());
58+
}
59+
4260
/** Parse input SQL query, and return a {@link SqlNode} as grammar tree. */
4361
SqlNode parse(String sqlStatement) throws ParseException;
4462

63+
RelBuilder getRelBuilder();
64+
65+
org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.SqlOperatorTable getOperatorTable();
66+
4567
@AutoOneOf(QueryParameters.Kind.class)
4668
abstract class QueryParameters {
4769
public enum Kind {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
<!--
2+
Licensed to the Apache Software Foundation (ASF) under one or more contributor
3+
license agreements. See the NOTICE file distributed with this work for additional
4+
information regarding copyright ownership. The ASF licenses this file to you under
5+
the Apache License, Version 2.0 (the "License"); you may not use this file except
6+
in compliance with the License. You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software distributed
11+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
specific language governing permissions and limitations under the License.
14+
-->
15+
16+
# Correlated sub-query decorrelation pre-pass
17+
18+
## Context
19+
20+
`CalciteQueryPlanner.convertToBeamRel(RelNode, ...)` is the single chokepoint
21+
through which every SQL query the Beam SQL extension plans is converted from a
22+
Calcite logical tree into a `BeamRelNode` tree by a single Volcano program.
23+
24+
The SqlToRel converter (driven by `Planner.rel`) already decorrelates most
25+
queries: `SqlToRelConverter` is configured with `withExpand(true)`, and the
26+
default `Planner.rel` path calls `RelDecorrelator.decorrelateQuery` once on the
27+
post-`SqlToRelConverter` tree. For the common correlated-scalar shape this is
28+
enough — the result is a `Join` + `Aggregate[SINGLE_VALUE]` that existing Beam
29+
rules already cover.
30+
31+
However, some correlated shapes survive that first pass:
32+
33+
- An un-expanded `RexSubQuery` inside a PROJECT/JOIN/FILTER condition (e.g. some
34+
correlated `EXISTS`/`IN` forms), or
35+
- a residual `LogicalCorrelate` the SqlToRel decorrelate pass could not lower.
36+
37+
The Beam Volcano ruleset (`BeamRuleSets.LOGICAL_OPTIMIZATIONS` /
38+
`BEAM_CONVERTERS`) has **no** converter rule for a general `LogicalCorrelate`.
39+
The only consumer is `BeamUnnestRule`, which matches strictly
40+
`LogicalCorrelate(_, Uncollect)` (the `UNNEST` shape). So any other residual
41+
correlate reaching Volcano fails planning with a `CannotPlanException`, surfaced
42+
as `SqlConversionException: Unable to convert relNode to Beam: ...`.
43+
44+
## Design
45+
46+
Add a private `normalizeForVolcano(RelNode)` pre-pass that runs at the very top
47+
of `convertToBeamRel(RelNode, ...)`, **strictly before** both:
48+
49+
1. the Volcano `program.run(...)`, and
50+
2. the metadata-provider swap (`setMetadataProvider` / `setMetadataQuerySupplier`
51+
/ `RRelMetadataQuery.THREAD_PROVIDERS`).
52+
53+
Running it before the metadata swap is deliberate: the pass uses stock Calcite
54+
metadata only, so it cannot trigger the Beam cost path (`BeamCostModel`,
55+
`RelMdNodeStats`, `BeamRelMetadataQuery`) and the cost recursion the Volcano
56+
search guards against.
57+
58+
The pass does two things:
59+
60+
1. A short-lived `HepPlanner` with `FILTER_SUB_QUERY_TO_CORRELATE`,
61+
`PROJECT_SUB_QUERY_TO_CORRELATE`, and `JOIN_SUB_QUERY_TO_CORRELATE` to turn any
62+
residual `RexSubQuery` into a `LogicalCorrelate`.
63+
2. `RelDecorrelator.decorrelateQuery(rel, RelBuilder.create(config))` to lower
64+
correlates into standard `Join`/`Aggregate`/`Project`/`Filter` nodes, using the
65+
planner's configured `RelBuilder` so produced rels share the cluster type
66+
factory and traits.
67+
68+
### Pre-flight safety gate
69+
70+
The entire pass is gated on the tree actually **referencing** a correlation
71+
variable: `RelOptUtil.getVariablesUsed(rel).isEmpty()` ⇒ return the input
72+
unchanged. This makes the pass a strict no-op on trees without a referenced
73+
correlate.
74+
75+
The gate intentionally uses `getVariablesUsed`, **not** `getVariablesSet`:
76+
77+
| shape | `getVariablesSet` | `getVariablesUsed` |
78+
|------------------------------------|-------------------|--------------------|
79+
| `LogicalCorrelate(_, Uncollect)` (UNNEST) | non-empty (defines an id) | empty (body refs none) |
80+
| correlated scalar / EXISTS / IN | non-empty | non-empty |
81+
82+
Because UNNEST *defines* a correlation id but does not *reference* one in its
83+
body, `getVariablesUsed` is empty for it and the pass is skipped, leaving the
84+
`LogicalCorrelate(_, Uncollect)` intact for `BeamUnnestRule`. A gate keyed on
85+
`getVariablesSet` would wrongly run the decorrelator on UNNEST trees. This
86+
structural guard is preferred over relying on test-only verification.
87+
88+
## Increments
89+
90+
This change is the smallest shippable increment:
91+
92+
- Single edit in `CalciteQueryPlanner.convertToBeamRel(RelNode, ...)` plus one
93+
private helper. No ruleset changes, no SparkConnect-side changes.
94+
- Deferred (explicitly out of scope): adding the PROJECT/JOIN
95+
`*_SUB_QUERY_TO_CORRELATE` variants into the Volcano `LOGICAL_OPTIMIZATIONS`
96+
ruleset; non-equi semi/anti-join lowering for correlated `EXISTS` without an
97+
equi key (still blocked at `BeamJoinRel.extractJoinRexNodes`).
98+
99+
## Risks
100+
101+
1. **UNNEST regression.** Mitigated by the `getVariablesUsed` gate above and
102+
verified by the `*Unnest*` test subset (and `BeamSqlDslArrayTest`).
103+
2. **Idempotency.** For trees the SqlToRel pass already fully decorrelated, the
104+
gate skips the pass entirely (no referenced correl var remains), so there is
105+
no redundant second decorrelate and no shape churn.
106+
3. **Cost-hang trap.** Avoided by placement strictly before the metadata-provider
107+
swap and Volcano; the pass never touches the Beam cost model.
108+
4. **Hand-built `RelNode` test fixtures** (join/aggregation DSL tests) flow
109+
through this method too; with no referenced correl var they hit the gate and
110+
are untouched. The filtered `*Join*` / `*CalciteQueryPlanner*` subset guards
111+
this.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public static SqlNode column(
5555
}
5656

5757
/** Returns the schema in which to create an object. */
58-
static Pair<CalciteSchema, String> schema(
58+
public static Pair<CalciteSchema, String> schema(
5959
CalcitePrepare.Context context, boolean mutable, SqlIdentifier id) {
6060
CalciteSchema rootSchema = mutable ? context.getMutableRootSchema() : context.getRootSchema();
6161
@Nullable CalciteSchema schema = null;
@@ -72,7 +72,7 @@ static Pair<CalciteSchema, String> schema(
7272
return Pair.of(checkStateNotNull(schema, "Got null sub-schema for path '%s'", path), name(id));
7373
}
7474

75-
private static @Nullable CalciteSchema childSchema(CalciteSchema rootSchema, List<String> path) {
75+
public static @Nullable CalciteSchema childSchema(CalciteSchema rootSchema, List<String> path) {
7676
@Nullable CalciteSchema schema = rootSchema;
7777
for (String p : path) {
7878
if (schema == null) {

sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/catalog/InMemoryCatalog.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
package org.apache.beam.sdk.extensions.sql.meta.catalog;
1919

2020
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkArgument;
21-
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkState;
2221

2322
import java.util.Collection;
2423
import java.util.Collections;
@@ -111,12 +110,16 @@ public Collection<String> databases() {
111110

112111
@Override
113112
public boolean dropDatabase(String database, boolean cascade) {
114-
checkState(!cascade, "%s does not support CASCADE.", getClass().getSimpleName());
113+
MetaStore metaStore = metaStores.get(database);
114+
if (!cascade && metaStore != null && !metaStore.getTables().isEmpty()) {
115+
throw new IllegalStateException("Database '" + database + "' is not empty.");
116+
}
115117

116118
boolean removed = databases.remove(database);
117119
if (database.equals(currentDatabase)) {
118120
currentDatabase = null;
119121
}
122+
metaStores.remove(database);
120123
return removed;
121124
}
122125

sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/meta/provider/text/TextTableProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ public PCollection<String> expand(PCollection<Row> input) {
238238

239239
/** Write-side converter for {@link TextTable} with format {@code 'csv'}. */
240240
@VisibleForTesting
241-
static class RowToCsv extends PTransform<PCollection<Row>, PCollection<String>>
241+
public static class RowToCsv extends PTransform<PCollection<Row>, PCollection<String>>
242242
implements Serializable {
243243

244244
private CSVFormat csvFormat;

sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/BeamSqlCliDatabaseTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ public void testUseDatabase() {
8383
assertEquals("my_database2", catalogManager.currentCatalog().currentDatabase());
8484
}
8585

86+
@Test
87+
public void testUseDatabaseWithoutDatabaseKeyword() {
88+
assertEquals(DEFAULT, catalogManager.currentCatalog().currentDatabase());
89+
cli.execute("CREATE DATABASE my_database");
90+
assertEquals(DEFAULT, catalogManager.currentCatalog().currentDatabase());
91+
cli.execute("USE my_database");
92+
assertEquals("my_database", catalogManager.currentCatalog().currentDatabase());
93+
}
94+
8695
@Test
8796
public void testUseDatabase_doesNotExist() {
8897
assertEquals(DEFAULT, catalogManager.currentCatalog().currentDatabase());
@@ -126,6 +135,33 @@ public void testDropDatabase_nonexistent() {
126135
cli.execute("DROP DATABASE my_database");
127136
}
128137

138+
@Test
139+
public void testDropDatabase_notEmpty_restrict() {
140+
cli.execute("CREATE DATABASE db_1");
141+
cli.execute("USE db_1");
142+
143+
TestTableProvider testTableProvider = new TestTableProvider();
144+
catalogManager.registerTableProvider(testTableProvider);
145+
cli.execute("CREATE EXTERNAL TABLE person(id int, name varchar, age int) TYPE 'test'");
146+
147+
thrown.expect(RuntimeException.class);
148+
thrown.expectMessage("Database 'db_1' is not empty.");
149+
cli.execute("DROP DATABASE db_1");
150+
}
151+
152+
@Test
153+
public void testDropDatabase_notEmpty_cascade() {
154+
cli.execute("CREATE DATABASE db_1");
155+
cli.execute("USE db_1");
156+
157+
TestTableProvider testTableProvider = new TestTableProvider();
158+
catalogManager.registerTableProvider(testTableProvider);
159+
cli.execute("CREATE EXTERNAL TABLE person(id int, name varchar, age int) TYPE 'test'");
160+
161+
cli.execute("DROP DATABASE db_1 CASCADE");
162+
assertFalse(catalogManager.currentCatalog().databaseExists("db_1"));
163+
}
164+
129165
@Test
130166
public void testCreateInsertDropTableUsingDefaultDatabase() {
131167
Catalog catalog = catalogManager.currentCatalog();

0 commit comments

Comments
 (0)