Skip to content

Commit fcfec9a

Browse files
Support global sort and improve UDF overload resolution in Beam SQL (#38832)
* Support global sort and improve UDF overload resolution in Beam SQL - Enable ORDER BY without LIMIT (global sort) in BeamSortRel by sorting in-memory. - Add AssertSorted test helper and testOrderBy_noLimit to verify global sort. - Map common Java classes to Calcite SqlTypeName in CalciteUtils.sqlTypeWithAutoCast, and add testSqlTypeWithAutoCast. - Prioritize overloaded methods with maximum parameter count in UdfImpl lookup, and add UdfImplTest. - Update LazyAggregateCombineFnTest to expect SQL BIGINT type instead of Java Long class. TAG=agy CONV=0df243da-2867-4795-9889-6334ba7d1599 * Address PR comments: simplify CalciteUtils type mapping and ensure deterministic UdfImpl lookup - Simplify Java class to Calcite type mapping in CalciteUtils using a switch statement and dynamic nullability check. - Use deterministic tie-breaker in UdfImpl.findMethod when resolving overloaded methods with the same parameter count. TAG=agy CONV=0df243da-2867-4795-9889-6334ba7d1599 * Optimize Java to SQL type mapping in CalciteUtils Use a static ImmutableMap for Java to SQL type mapping instead of a switch statement, improving lookup performance and code readability. TAG=agy CONV=0df243da-2867-4795-9889-6334ba7d1599 * Apply suggestion from @damccorm * Update sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/UdfImpl.java Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Run spotless to fix formatting in UdfImpl.java --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 0856c22 commit fcfec9a

7 files changed

Lines changed: 268 additions & 16 deletions

File tree

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,28 @@ public static Function create(Method method) {
7070
}
7171

7272
/*
73-
* Finds a method in a given class by name.
73+
* Finds a method in a given class by name. In case of overloaded methods with the same name,
74+
* this prioritizes the overload with the maximum number of parameters. This ensures Calcite
75+
* can resolve optional/default trailing parameters correctly when binding UDF overloads.
76+
*
7477
* @param clazz class to search method in
7578
* @param name name of the method to find
76-
* @return the first method with matching name or null when no method found
79+
* @return the matching method with the highest parameter count or null when no method found
7780
*/
7881
static @Nullable Method findMethod(Class<?> clazz, String name) {
82+
Method bestMethod = null;
7983
for (Method method : clazz.getMethods()) {
8084
if (method.getName().equals(name) && !method.isBridge()) {
81-
return method;
85+
if (bestMethod == null) {
86+
bestMethod = method;
87+
} else {
88+
int cmp = Integer.compare(method.getParameterCount(), bestMethod.getParameterCount());
89+
if (cmp > 0 || (cmp == 0 && method.toString().compareTo(bestMethod.toString()) < 0)) {
90+
bestMethod = method;
91+
}
92+
}
8293
}
8394
}
84-
return null;
95+
return bestMethod;
8596
}
8697
}

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

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.apache.beam.sdk.state.ValueState;
4141
import org.apache.beam.sdk.transforms.DoFn;
4242
import org.apache.beam.sdk.transforms.Flatten;
43+
import org.apache.beam.sdk.transforms.GroupByKey;
4344
import org.apache.beam.sdk.transforms.PTransform;
4445
import org.apache.beam.sdk.transforms.ParDo;
4546
import org.apache.beam.sdk.transforms.Top;
@@ -74,15 +75,12 @@
7475
* <pre>{@code
7576
* SELECT * FROM t ORDER BY id DESC LIMIT 10;
7677
* SELECT * FROM t ORDER BY id DESC LIMIT 10 OFFSET 5;
77-
* }</pre>
78-
*
79-
* <p>but an ORDER BY without a LIMIT is NOT supported. For example, the following will throw an
80-
* exception:
81-
*
82-
* <pre>{@code
8378
* SELECT * FROM t ORDER BY id DESC;
8479
* }</pre>
8580
*
81+
* <p>Note: ORDER BY without a LIMIT is supported by keying all rows to a single key and sorting
82+
* them in memory. This can be memory-intensive and may fail for large datasets.
83+
*
8684
* <h3>Constraints</h3>
8785
*
8886
* <ul>
@@ -134,12 +132,12 @@ public BeamSortRel(
134132
}
135133

136134
if (fetch == null) {
137-
throw new UnsupportedOperationException("ORDER BY without a LIMIT is not supported!");
135+
count = -1;
136+
} else {
137+
RexLiteral fetchLiteral = (RexLiteral) fetch;
138+
count = ((BigDecimal) fetchLiteral.getValue()).intValue();
138139
}
139140

140-
RexLiteral fetchLiteral = (RexLiteral) fetch;
141-
count = ((BigDecimal) fetchLiteral.getValue()).intValue();
142-
143141
if (offset != null) {
144142
RexLiteral offsetLiteral = (RexLiteral) offset;
145143
startIndex = ((BigDecimal) offsetLiteral.getValue()).intValue();
@@ -209,6 +207,21 @@ public PCollection<Row> expand(PCollectionList<Row> pinput) {
209207
GlobalWindows.class.getSimpleName(), windowingStrategy));
210208
}
211209

210+
// When no limit is specified (count == -1), we must sort the entire dataset.
211+
// To achieve this globally, we key all rows by a single dummy key, group them together
212+
// using GroupByKey to ensure they are processed together, and then sort them in-memory
213+
// via SortInMemoryFn. Note: This can be memory-intensive for large datasets. It should
214+
// only be done as a final step when the remaining data is small
215+
if (count == -1) {
216+
BeamSqlRowComparator comparator =
217+
new BeamSqlRowComparator(fieldIndices, orientation, nullsFirst);
218+
return upstream
219+
.apply("WithDummyKey", WithKeys.of("DummyKey"))
220+
.apply("GroupByKey", GroupByKey.create())
221+
.apply("SortInMemory", ParDo.of(new SortInMemoryFn(comparator)))
222+
.setRowSchema(CalciteUtils.toSchema(getRowType()));
223+
}
224+
212225
ReversedBeamSqlRowComparator comparator =
213226
new ReversedBeamSqlRowComparator(fieldIndices, orientation, nullsFirst);
214227

@@ -303,6 +316,31 @@ public void processElement(ProcessContext ctx) {
303316
}
304317
}
305318

319+
/**
320+
* A {@link DoFn} that sorts all elements in-memory. Expects input grouped by a dummy key, sorts
321+
* the iterable values, and outputs them.
322+
*/
323+
private static class SortInMemoryFn extends DoFn<KV<String, Iterable<Row>>, Row> {
324+
private final BeamSqlRowComparator comparator;
325+
326+
public SortInMemoryFn(BeamSqlRowComparator comparator) {
327+
this.comparator = comparator;
328+
}
329+
330+
@ProcessElement
331+
public void processElement(ProcessContext ctx) {
332+
Iterable<Row> input = ctx.element().getValue();
333+
List<Row> list = new ArrayList<>();
334+
for (Row r : input) {
335+
list.add(r);
336+
}
337+
list.sort(comparator);
338+
for (Row r : list) {
339+
ctx.output(r);
340+
}
341+
}
342+
}
343+
306344
@Override
307345
public Sort copy(
308346
RelTraitSet traitSet,

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,25 @@ public static boolean isStringType(FieldType fieldType) {
170170
FieldType.DATETIME, SqlTypeName.TIMESTAMP,
171171
FieldType.STRING, SqlTypeName.VARCHAR);
172172

173+
private static final Map<Class<?>, SqlTypeName> JAVA_TO_SQL_TYPE_MAPPING =
174+
ImmutableMap.<Class<?>, SqlTypeName>builder()
175+
.put(String.class, SqlTypeName.VARCHAR)
176+
.put(Integer.class, SqlTypeName.INTEGER)
177+
.put(int.class, SqlTypeName.INTEGER)
178+
.put(Long.class, SqlTypeName.BIGINT)
179+
.put(long.class, SqlTypeName.BIGINT)
180+
.put(Double.class, SqlTypeName.DOUBLE)
181+
.put(double.class, SqlTypeName.DOUBLE)
182+
.put(Float.class, SqlTypeName.FLOAT)
183+
.put(float.class, SqlTypeName.FLOAT)
184+
.put(Short.class, SqlTypeName.SMALLINT)
185+
.put(short.class, SqlTypeName.SMALLINT)
186+
.put(Byte.class, SqlTypeName.TINYINT)
187+
.put(byte.class, SqlTypeName.TINYINT)
188+
.put(Boolean.class, SqlTypeName.BOOLEAN)
189+
.put(boolean.class, SqlTypeName.BOOLEAN)
190+
.build();
191+
173192
// Associating FieldType to generated RelDataType objects for Beam logical types. Used for
174193
// recovering the original type in output schema after full Beam FieldType->Calcite Type->Beam
175194
// FieldType trip
@@ -365,7 +384,9 @@ private static RelDataType toRelDataType(
365384
* SQL-Java type mapping, with specified Beam rules: <br>
366385
* 1. redirect {@link AbstractInstant} to {@link Date} so Calcite can recognize it. <br>
367386
* 2. For a list, the component type is needed to create a Sql array type. <br>
368-
* 3. For a Map, the component type is needed to create a Sql map type.
387+
* 3. For a Map, the component type is needed to create a Sql map type. <br>
388+
* 4. For standard Java classes (String, Integer, etc.), map them to corresponding Calcite SQL
389+
* type with appropriate nullability.
369390
*
370391
* @param type
371392
* @return Calcite RelDataType
@@ -396,6 +417,14 @@ public static RelDataType sqlTypeWithAutoCast(RelDataTypeFactory typeFactory, Ty
396417
+ ". This is currently unsupported, use List instead "
397418
+ "of Array.");
398419
}
420+
if (type instanceof Class) {
421+
Class<?> clazz = (Class<?>) type;
422+
SqlTypeName sqlTypeName = JAVA_TO_SQL_TYPE_MAPPING.get(clazz);
423+
if (sqlTypeName != null) {
424+
return typeFactory.createTypeWithNullability(
425+
typeFactory.createSqlType(sqlTypeName), !clazz.isPrimitive());
426+
}
427+
}
399428
return typeFactory.createJavaType((Class) type);
400429
}
401430
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.type.RelDataTypeSystem;
3535
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.AggregateFunction;
3636
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.schema.FunctionParameter;
37+
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.sql.type.SqlTypeName;
3738
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
3839
import org.checkerframework.checker.nullness.qual.Nullable;
3940
import org.junit.Rule;
@@ -77,7 +78,9 @@ public void subclassGetUdafImpl() {
7778
LazyAggregateCombineFn<?, ?, ?> combiner = new LazyAggregateCombineFn<>(aggregateFn);
7879
AggregateFunction aggregateFunction = combiner.getUdafImpl();
7980
RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
80-
RelDataType expectedType = typeFactory.createJavaType(Long.class);
81+
RelDataType expectedType =
82+
typeFactory.createTypeWithNullability(
83+
typeFactory.createSqlType(SqlTypeName.BIGINT), true);
8184

8285
List<FunctionParameter> params = aggregateFunction.getParameters();
8386
assertThat(params, hasSize(1));
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.assertNotNull;
22+
23+
import java.lang.reflect.Method;
24+
import org.junit.Test;
25+
import org.junit.runner.RunWith;
26+
import org.junit.runners.JUnit4;
27+
28+
/** Tests for {@link UdfImpl}. */
29+
@RunWith(JUnit4.class)
30+
public class UdfImplTest {
31+
32+
@Test
33+
public void testFindMethod_overloaded_prioritizesMaxParams() {
34+
Method method = UdfImpl.findMethod(OverloadedFn.class, "eval");
35+
assertNotNull(method);
36+
assertEquals(3, method.getParameterTypes().length);
37+
}
38+
39+
@Test
40+
public void testFindMethod_singleMethod() {
41+
Method method = UdfImpl.findMethod(SingleFn.class, "eval");
42+
assertNotNull(method);
43+
assertEquals(1, method.getParameterTypes().length);
44+
}
45+
46+
public static class OverloadedFn {
47+
public String eval(String a) {
48+
return a;
49+
}
50+
51+
public String eval(String a, String b) {
52+
return a + b;
53+
}
54+
55+
public String eval(String a, String b, String c) {
56+
return a + b + c;
57+
}
58+
}
59+
60+
public static class SingleFn {
61+
public String eval(String a) {
62+
return a;
63+
}
64+
}
65+
}

sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSortRelTest.java

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
*/
1818
package org.apache.beam.sdk.extensions.sql.impl.rel;
1919

20+
import java.util.ArrayList;
21+
import java.util.List;
2022
import org.apache.beam.sdk.extensions.sql.TestUtils;
2123
import org.apache.beam.sdk.extensions.sql.impl.planner.BeamRelMetadataQuery;
2224
import org.apache.beam.sdk.extensions.sql.impl.planner.NodeStats;
2325
import org.apache.beam.sdk.extensions.sql.meta.provider.test.TestBoundedTable;
2426
import org.apache.beam.sdk.schemas.Schema;
2527
import org.apache.beam.sdk.testing.PAssert;
2628
import org.apache.beam.sdk.testing.TestPipeline;
29+
import org.apache.beam.sdk.transforms.SerializableFunction;
2730
import org.apache.beam.sdk.values.PCollection;
2831
import org.apache.beam.sdk.values.Row;
2932
import org.apache.beam.vendor.calcite.v1_40_0.org.apache.calcite.rel.RelNode;
@@ -316,4 +319,44 @@ public void testNodeStatsEstimation() {
316319
Assert.assertEquals(10., estimate.getRowCount(), 0.01);
317320
Assert.assertEquals(10., estimate.getWindow(), 0.01);
318321
}
322+
323+
@Test
324+
public void testOrderBy_noLimit() {
325+
String sql =
326+
"SELECT order_id, site_id, price "
327+
+ "FROM ORDER_DETAILS "
328+
+ "ORDER BY order_id asc, site_id desc";
329+
330+
PCollection<Row> rows = compilePipeline(sql, pipeline);
331+
PAssert.that(rows).satisfies(new AssertSorted());
332+
pipeline.run().waitUntilFinish();
333+
}
334+
335+
private static class AssertSorted implements SerializableFunction<Iterable<Row>, Void> {
336+
@Override
337+
public Void apply(Iterable<Row> input) {
338+
List<Row> list = new ArrayList<>();
339+
for (Row r : input) {
340+
list.add(r);
341+
}
342+
Assert.assertEquals(10, list.size());
343+
for (int i = 0; i < list.size() - 1; i++) {
344+
Row r1 = list.get(i);
345+
Row r2 = list.get(i + 1);
346+
Long id1 = r1.getInt64("order_id");
347+
Long id2 = r2.getInt64("order_id");
348+
int comp = id1.compareTo(id2);
349+
if (comp > 0) {
350+
Assert.fail("Rows not sorted by order_id asc: " + list);
351+
} else if (comp == 0) {
352+
Integer site1 = r1.getInt32("site_id");
353+
Integer site2 = r2.getInt32("site_id");
354+
if (site1 < site2) {
355+
Assert.fail("Rows not sorted by site_id desc when order_id is equal: " + list);
356+
}
357+
}
358+
}
359+
return null;
360+
}
361+
}
319362
}

sdks/java/extensions/sql/src/test/java/org/apache/beam/sdk/extensions/sql/impl/utils/CalciteUtilsTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,4 +198,67 @@ public void testToRelDataTypeWithRowBackedLogicalType() {
198198
assertEquals(1, relDataType.getFieldCount());
199199
assertEquals("nested_f1", relDataType.getFieldList().get(0).getName());
200200
}
201+
202+
@Test
203+
public void testSqlTypeWithAutoCast() {
204+
RelDataType type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, String.class);
205+
assertEquals(SqlTypeName.VARCHAR, type.getSqlTypeName());
206+
assertTrue(type.isNullable());
207+
208+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Integer.class);
209+
assertEquals(SqlTypeName.INTEGER, type.getSqlTypeName());
210+
assertTrue(type.isNullable());
211+
212+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, int.class);
213+
assertEquals(SqlTypeName.INTEGER, type.getSqlTypeName());
214+
assertFalse(type.isNullable());
215+
216+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Long.class);
217+
assertEquals(SqlTypeName.BIGINT, type.getSqlTypeName());
218+
assertTrue(type.isNullable());
219+
220+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, long.class);
221+
assertEquals(SqlTypeName.BIGINT, type.getSqlTypeName());
222+
assertFalse(type.isNullable());
223+
224+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Double.class);
225+
assertEquals(SqlTypeName.DOUBLE, type.getSqlTypeName());
226+
assertTrue(type.isNullable());
227+
228+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, double.class);
229+
assertEquals(SqlTypeName.DOUBLE, type.getSqlTypeName());
230+
assertFalse(type.isNullable());
231+
232+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Float.class);
233+
assertEquals(SqlTypeName.FLOAT, type.getSqlTypeName());
234+
assertTrue(type.isNullable());
235+
236+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, float.class);
237+
assertEquals(SqlTypeName.FLOAT, type.getSqlTypeName());
238+
assertFalse(type.isNullable());
239+
240+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Short.class);
241+
assertEquals(SqlTypeName.SMALLINT, type.getSqlTypeName());
242+
assertTrue(type.isNullable());
243+
244+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, short.class);
245+
assertEquals(SqlTypeName.SMALLINT, type.getSqlTypeName());
246+
assertFalse(type.isNullable());
247+
248+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Byte.class);
249+
assertEquals(SqlTypeName.TINYINT, type.getSqlTypeName());
250+
assertTrue(type.isNullable());
251+
252+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, byte.class);
253+
assertEquals(SqlTypeName.TINYINT, type.getSqlTypeName());
254+
assertFalse(type.isNullable());
255+
256+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, Boolean.class);
257+
assertEquals(SqlTypeName.BOOLEAN, type.getSqlTypeName());
258+
assertTrue(type.isNullable());
259+
260+
type = CalciteUtils.sqlTypeWithAutoCast(dataTypeFactory, boolean.class);
261+
assertEquals(SqlTypeName.BOOLEAN, type.getSqlTypeName());
262+
assertFalse(type.isNullable());
263+
}
201264
}

0 commit comments

Comments
 (0)