Skip to content

Commit 9df7153

Browse files
RyanL1997claude
andcommitted
Follow a leaf through a cast everywhere, not just where it is allowed
Reading a leaf as a number is allowed, so the check strips a cast before deciding that. Two other places ask the same question -- which column a read went into, and what to call it -- and neither stripped the cast. A cast leaf therefore reached pushdown untracked, where a script over a flat_object cannot be built, and the failure surfaced as HTTP 500, code UNKNOWN java.sql.SQLException: ... A flat_object field cannot be read by a pushed-down script: attributes for a query such as source=otel | eval d = cast(attributes.duration_ms as double) | sort d | fields service which now reports "Cannot sort by attributes.duration_ms" like the rest. The three callers share one stripCasts so they cannot drift again. Signed-off-by: Jialiang Liang <jiallian@amazon.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHurdipg7MHo61tquK7BpD Signed-off-by: Jialiang Liang <ryanleeang@gmail.com>
1 parent f5fb75e commit 9df7153

2 files changed

Lines changed: 31 additions & 7 deletions

File tree

‎core/src/main/java/org/opensearch/sql/calcite/FlatObjectScopeValidator.java‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ private static Set<Integer> leafColumns(RelNode node, Set<String> fields) {
158158
List<RelDataTypeField> below = inputFields(project);
159159
List<RexNode> projects = project.getProjects();
160160
for (int i = 0; i < projects.size(); i++) {
161-
RexNode expression = projects.get(i);
161+
RexNode expression = stripCasts(projects.get(i));
162162
boolean fromField =
163163
isLeafRef(expression, below, fields)
164164
|| (expression instanceof RexInputRef ref
@@ -178,16 +178,26 @@ private static Set<Integer> leafColumns(RelNode node, Set<String> fields) {
178178
* text, so converting it is the parse a caller would do anyway, the value comes from {@code
179179
* _source} either way, and it is how a query asks for a leaf as a number.
180180
*/
181-
private static boolean isRead(
182-
RexNode node, List<RelDataTypeField> inputFields, Set<String> fields, Set<Integer> leaves) {
183-
if (node instanceof RexInputRef) {
184-
return true;
185-
}
181+
/**
182+
* Unwraps the casts a read may wear. {@code isRead} and {@link #leafColumns} have to agree on
183+
* what counts as reading a leaf: a column one of them lets through while the other does not track
184+
* is a column that reaches pushdown, where a script over a flat_object cannot be built.
185+
*/
186+
private static RexNode stripCasts(RexNode node) {
186187
RexNode read = node;
187188
while (read instanceof RexCall cast
188189
&& (cast.getKind() == SqlKind.CAST || cast.getKind() == SqlKind.SAFE_CAST)) {
189190
read = cast.getOperands().get(0);
190191
}
192+
return read;
193+
}
194+
195+
private static boolean isRead(
196+
RexNode node, List<RelDataTypeField> inputFields, Set<String> fields, Set<Integer> leaves) {
197+
if (node instanceof RexInputRef) {
198+
return true;
199+
}
200+
RexNode read = stripCasts(node);
191201
return isLeafRef(read, inputFields, fields)
192202
|| (read instanceof RexInputRef ref && leaves.contains(ref.getIndex()));
193203
}
@@ -262,7 +272,7 @@ private static String displayName(
262272
RelNode node, List<RelDataTypeField> inputFields, int index, Set<String> fields) {
263273
if (!node.getInputs().isEmpty() && node.getInput(0) instanceof Project project) {
264274
List<RelDataTypeField> below = inputFields(project);
265-
RexNode expression = project.getProjects().get(index);
275+
RexNode expression = stripCasts(project.getProjects().get(index));
266276
if (isLeafRef(expression, below, fields)) {
267277
return leafName((RexCall) expression, below);
268278
}

‎core/src/test/java/org/opensearch/sql/calcite/FlatObjectScopeValidatorTest.java‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,20 @@ void sortingByALeafIsRejected() {
163163
"Cannot sort by attributes");
164164
}
165165

166+
// Reading a leaf as a number is allowed, so a cast is stripped when deciding that -- and it has to
167+
// be stripped just the same when noting which column the read went into. Otherwise the column
168+
// sorts by a leaf unchecked and the failure surfaces from pushdown, as a 500.
169+
@Test
170+
void sortingByACastLeafIsRejected() {
171+
RexNode asDouble =
172+
rexBuilder.makeCast(
173+
TYPE_FACTORY.createSqlType(SqlTypeName.DOUBLE), leaf("duration_ms"), true, true);
174+
rejected(
175+
LogicalSort.create(
176+
project(field(0), asDouble), RelCollations.of(new RelFieldCollation(1)), null, null),
177+
"Cannot sort by attributes.duration_ms");
178+
}
179+
166180
@Test
167181
void groupingByALeafIsRejected() {
168182
RelNode projected = project(field(0), leaf("status_code"));

0 commit comments

Comments
 (0)