Repository navigation
Fix Beam SQL UNNEST of array elements with a nested row through JDBC - #40841
mxtymoshyk wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
📋 Review Summary
This PR fixes an IllegalArgumentException in BeamUnnestRel.UnnestFn when running CROSS JOIN UNNEST over an ARRAY<ROW<...>> containing nested ROW fields via the JDBC driver or SQL shell, as well as when flattening null nested rows via SqlTransform. The implementation cleanly aligns UnnestFn value extraction with outputSchema and includes unit tests covering both the JDBC and SqlTransform paths.
🔍 General Feedback
- Checking the corresponding
outputSchemacolumn type inaddElementValuesaccurately handles both the unflattened (JdbcDriver) and flattened (SqlTransform) Calcite planner outputs, while properly paddingnullvalues for flattenednullnested rows.
💡 Tip: Comment /gemini review on this pull request to trigger a new Gemini code review.
utafrali
left a comment
There was a problem hiding this comment.
The positional-check approach in addElementValues is a clean and correct way to handle both planner outputs without duplicating logic. The fix is well-documented in the Javadoc and the two new tests cover the cases that previously crashed. The comments above are minor polish items that do not block merging.
| for (int i = 0; i < elementSchema.getFieldCount(); i++) { | ||
| Schema.FieldType fieldType = elementSchema.getField(i).getType(); | ||
| Object value = null; | ||
| if (Objects.nonNull(element)) { |
There was a problem hiding this comment.
Objects.nonNull(element) is equivalent to element != null but less idiomatic in Java. Prefer the direct null check: if (element != null). The rest of the file uses direct null comparisons (e.g. rawValues == null on line 184).
There was a problem hiding this comment.
Done in 390bfad. Switched to element != null to match the rest of the module.
| } | ||
| } | ||
|
|
||
| private boolean isRowColumn(int index) { |
There was a problem hiding this comment.
isRowColumn will throw IndexOutOfBoundsException if index >= outputSchema.getFieldCount(). This can only happen when the element schema and the output schema disagree on field count, which indicates a planning bug rather than user error. A bounds guard with a clear message would make that failure much easier to diagnose than a raw IOOBE:
private boolean isRowColumn(int index) {
if (index >= outputSchema.getFieldCount()) {
throw new IllegalStateException(
"Element schema produces more fields than output schema at index " + index);
}
return outputSchema.getField(index).getType().getTypeName().equals(Schema.TypeName.ROW);
}There was a problem hiding this comment.
Added a guard in 7a2a631. Instead of throwing, isRowColumn returns false past the last output column. The extra values then reach Row.Builder#build(), which fails with the same Row expected N fields ... initialized with M fields error as in #26911. That message lists the output schema fields, so a planner mismatch is easier to trace. Happy to switch to an IllegalStateException if you would rather have that.
| assertThat(resultRows, containsInAnyOrder(row(1L, "aaa"), row(2L, "bbb"))); | ||
| } | ||
|
|
||
| @Test |
There was a problem hiding this comment.
The new JDBC test only exercises the non-null nested row path. The null-nested-row fix in addElementValues (adding a null per flattened column) is covered by BeamSqlDslUnnestRowsTest#testUnnestArrayWithNullNestedRow, but that test runs through SqlTransform, not the JDBC planner. On the JDBC path the row is kept as a single ROW column, so a null nestedRow would be added as a single null value via values.add(value) at line 160, which is a different branch. Consider adding a second entry in tableProvider.addRows with a null nested row and asserting that the corresponding result row has null for the nested field.
There was a problem hiding this comment.
Added in 7a2a631. The JDBC test now has a third element whose nestedRow is null, and the query returns null for item.nestedRow.name on that row.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #40841 +/- ##
=============================================
+ Coverage 0 64.17% +64.17%
- Complexity 0 2357 +2357
=============================================
Files 0 321 +321
Lines 0 15599 +15599
Branches 0 2030 +2030
=============================================
+ Hits 0 10011 +10011
- Misses 0 4822 +4822
- Partials 0 766 +766
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Assigning reviewers: R: @chamikaramj for label java. Note: If you would like to opt out of this review, comment Available commands:
The PR bot will only process comments in the main thread (not review comments). |
Fixes #26911
CROSS JOIN UNNESTover anARRAY<ROW<...>>whose element has a nested ROW field fails when the query runs through the JDBC driver or the SQL shell:Beam plans SQL with two different Calcite planners, and they give different unnest output for the same query. The planner behind
SqlTransformflattens the element's nested row into leaf columns (id, name, email). The planner behind the JDBC driver keeps it as one ROW column (id, author).UnnestFnalways flattened, so it only matchedSqlTransform.UnnestFnnow checks each nested row field of the element against the output column at the same position. If that column is a ROW, the row goes in as is. Otherwise the row's values go in flattened.The same change fixes a null nested row on the flattened path. The old code added no values for it, so the row came out short and hit the same field count error. It now fills one null per flattened column.
Tests:
JdbcDriverTest#testUnnestArrayOfRowsWithNestedRowcovers the JDBC path, including an element whose nested row is null.BeamSqlDslUnnestRowsTest#testUnnestArrayWithNullNestedRowcovers a null nested row on theSqlTransformpath.BeamUnnestRel. The existingtestUnnestArrayWithNestedRowsstill passes and covers the flattened path../gradlew :sdks:java:extensions:sql:checkpasses locally.