Skip to content

Commit 6b04a01

Browse files
Refactor EXPAND and MVEXPAND and fix its unittest
Signed-off-by: Srikanth Padakanti <srikanth_padakanti@apple.com>
1 parent 8a13f44 commit 6b04a01

2 files changed

Lines changed: 96 additions & 21 deletions

File tree

core/src/main/java/org/opensearch/sql/calcite/CalciteRelNodeVisitor.java

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3139,29 +3139,72 @@ private UnnestResult correlateUnnest(
31393139
*
31403140
* <p>This helper tolerates unknown/ANY element types and any runtime exceptions thrown while
31413141
* introspecting the type. It returns an empty list when it cannot obtain concrete field
3142-
* information.
3142+
* information. It also avoids calling getFieldList() on scalar types (e.g. VARCHAR) which for
3143+
* some RelDataType implementations can assert/fail.
31433144
*
31443145
* @param elemRef element reference RexNode
31453146
* @return list of element RelDataTypeField when available, otherwise empty list
31463147
*/
31473148
private List<RelDataTypeField> getElementFieldsSafely(RexNode elemRef) {
31483149
try {
31493150
RelDataType elemType = elemRef.getType();
3150-
if (elemType == null || elemType.getFamily() == SqlTypeFamily.ANY) {
3151+
if (elemType == null) {
31513152
return List.of();
31523153
}
3154+
3155+
// If the declared family is ANY, treat as unknown and return empty.
3156+
try {
3157+
if (elemType.getFamily() == SqlTypeFamily.ANY) {
3158+
return List.of();
3159+
}
3160+
} catch (Exception ignored) {
3161+
// Some exotic types may throw here — fall back to safe empty result.
3162+
return List.of();
3163+
}
3164+
3165+
// Defensive checks: avoid calling getFieldList() on scalar types (VARCHAR, INTEGER, etc.)
3166+
// which may cause assertions in some RelDataType implementations.
3167+
try {
3168+
SqlTypeName sqlTypeName = elemType.getSqlTypeName();
3169+
if (sqlTypeName != null) {
3170+
// If element type is not ROW (struct-like) then it has no named sub-fields.
3171+
// For MAP/ARRAY shape we rely on concrete MapSqlType/ArraySqlType handling below.
3172+
if (sqlTypeName != SqlTypeName.ROW) {
3173+
// If type object itself is a MapSqlType (map value is struct) allow further inspection.
3174+
if (!(elemType instanceof MapSqlType) && !(elemType instanceof ArraySqlType)) {
3175+
return List.of();
3176+
}
3177+
}
3178+
}
3179+
} catch (Exception ignored) {
3180+
// If we can't safely determine SQL type name, fall through and use guarded getFieldList.
3181+
}
3182+
3183+
// If field count is explicitly zero, return empty early.
3184+
try {
3185+
if (elemType.getFieldCount() == 0) {
3186+
return List.of();
3187+
}
3188+
} catch (Exception ignored) {
3189+
// getFieldCount may throw for some implementations; fall through to guarded getFieldList.
3190+
}
3191+
3192+
// Finally, attempt to retrieve the field list but guard against runtime exceptions.
31533193
try {
31543194
List<RelDataTypeField> fl = elemType.getFieldList();
3155-
if (fl != null && !fl.isEmpty()) {
3156-
return fl;
3195+
if (fl == null || fl.isEmpty()) {
3196+
return List.of();
31573197
}
3198+
return fl;
31583199
} catch (RuntimeException ignored) {
3159-
// Fall through and return empty list as a safe fallback.
3200+
// Some RelDataType implementations assert/throw from getFieldList() for scalar types.
3201+
// Return empty as the safe fallback.
3202+
return List.of();
31603203
}
31613204
} catch (Exception ignored) {
3162-
// Introspection not possible; return empty.
3205+
// Any unexpected failure: be defensive and return empty list.
3206+
return List.of();
31633207
}
3164-
return List.of();
31653208
}
31663209

31673210
/**

ppl/src/test/java/org/opensearch/sql/ppl/calcite/CalcitePPLMvExpandTest.java

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
* Calcite tests for the mvexpand command.
3737
*
3838
* <p>Planner tests for mvexpand; kept minimal and consistent with other Calcite planner tests.
39+
*
40+
* <p>NOTE: - Updated expected Spark-SQL strings to match the new Calcite -> Spark SQL translation
41+
* emitted by the current CalciteRelNodeVisitor implementation (uses UNNEST subquery form).
3942
*/
4043
public class CalcitePPLMvExpandTest extends CalcitePPLAbstractTest {
4144

@@ -63,7 +66,15 @@ protected Frameworks.ConfigBuilder config(CalciteAssert.SchemaSpec... schemaSpec
6366
@Test
6467
public void testMvExpandBasic() {
6568
String ppl = "source=USERS | mvexpand skills";
66-
RelNode root = getRelNode(ppl);
69+
RelNode root;
70+
try {
71+
root = getRelNode(ppl);
72+
// Ensure planner didn't throw and returned a plan
73+
assertNotNull(root);
74+
} catch (Exception e) {
75+
fail("mvexpand basic planning should not throw, but got: " + e.getMessage());
76+
return;
77+
}
6778

6879
String expectedLogical =
6980
"LogicalProject(USERNAME=[$0], skills.name=[$2])\n"
@@ -75,17 +86,28 @@ public void testMvExpandBasic() {
7586
+ " LogicalValues(tuples=[[{ 0 }]])\n";
7687
verifyLogical(root, expectedLogical);
7788

89+
// Updated expectation: Calcite's current Spark SQL translator emits an UNNEST-style lateral
90+
// subquery rather than a "LATERAL VIEW EXPLODE(...)" expression. Match that output.
7891
String expectedSparkSql =
79-
"SELECT `USERNAME`, exploded.skills.name\n"
80-
+ "FROM `scott`.`USERS`\n"
81-
+ "LATERAL VIEW EXPLODE(skills) exploded AS skills";
92+
"SELECT `$cor0`.`USERNAME`, `t1`.`skills.name`\n"
93+
+ "FROM `scott`.`USERS` `$cor0`,\n"
94+
+ "LATERAL (SELECT `name` `skills.name`\n"
95+
+ "FROM UNNEST((SELECT `$cor0`.`skills`\n"
96+
+ "FROM (VALUES (0)) `t` (`ZERO`))) `t0` (`name`, `level`)) `t1`";
8297
verifyPPLToSparkSQL(root, expectedSparkSql);
8398
}
8499

85100
@Test
86101
public void testMvExpandWithLimit() {
87102
String ppl = "source=USERS | mvexpand skills | head 1";
88-
RelNode root = getRelNode(ppl);
103+
RelNode root;
104+
try {
105+
root = getRelNode(ppl);
106+
assertNotNull(root);
107+
} catch (Exception e) {
108+
fail("mvexpand with limit planning should not throw, but got: " + e.getMessage());
109+
return;
110+
}
89111

90112
String expectedLogical =
91113
"LogicalSort(fetch=[1])\n"
@@ -98,19 +120,28 @@ public void testMvExpandWithLimit() {
98120
+ " LogicalValues(tuples=[[{ 0 }]])\n";
99121
verifyLogical(root, expectedLogical);
100122

101-
// Spark SQL expectation includes a LIMIT
123+
// Same UNNEST-style translation with LIMIT appended
102124
String expectedSparkSql =
103-
"SELECT `USERNAME`, exploded.skills.name\n"
104-
+ "FROM `scott`.`USERS`\n"
105-
+ "LATERAL VIEW EXPLODE(skills) exploded AS skills\n"
125+
"SELECT `$cor0`.`USERNAME`, `t1`.`skills.name`\n"
126+
+ "FROM `scott`.`USERS` `$cor0`,\n"
127+
+ "LATERAL (SELECT `name` `skills.name`\n"
128+
+ "FROM UNNEST((SELECT `$cor0`.`skills`\n"
129+
+ "FROM (VALUES (0)) `t` (`ZERO`))) `t0` (`name`, `level`)) `t1`\n"
106130
+ "LIMIT 1";
107131
verifyPPLToSparkSQL(root, expectedSparkSql);
108132
}
109133

110134
@Test
111135
public void testMvExpandProjectNested() {
112136
String ppl = "source=USERS | mvexpand skills | fields USERNAME, skills.name";
113-
RelNode root = getRelNode(ppl);
137+
RelNode root;
138+
try {
139+
root = getRelNode(ppl);
140+
assertNotNull(root);
141+
} catch (Exception e) {
142+
fail("mvexpand project nested planning should not throw, but got: " + e.getMessage());
143+
return;
144+
}
114145

115146
String expectedLogical =
116147
"LogicalProject(USERNAME=[$0], skills.name=[$2])\n"
@@ -122,11 +153,12 @@ public void testMvExpandProjectNested() {
122153
+ " LogicalValues(tuples=[[{ 0 }]])\n";
123154
verifyLogical(root, expectedLogical);
124155

125-
// Verify Spark SQL translation for projected nested attribute
126156
String expectedSparkSql =
127-
"SELECT `USERNAME`, exploded.skills.name\n"
128-
+ "FROM `scott`.`USERS`\n"
129-
+ "LATERAL VIEW EXPLODE(skills) exploded AS skills";
157+
"SELECT `$cor0`.`USERNAME`, `t1`.`skills.name`\n"
158+
+ "FROM `scott`.`USERS` `$cor0`,\n"
159+
+ "LATERAL (SELECT `name` `skills.name`\n"
160+
+ "FROM UNNEST((SELECT `$cor0`.`skills`\n"
161+
+ "FROM (VALUES (0)) `t` (`ZERO`))) `t0` (`name`, `level`)) `t1`";
130162
verifyPPLToSparkSQL(root, expectedSparkSql);
131163
}
132164

0 commit comments

Comments
 (0)