Skip to content

Commit 4026e1d

Browse files
committed
fix(sandbox): cluster D — datetime stringify uses space, time has no epoch prefix
- ArrowValues: format Date/Time/Timestamp scalars and list-of-temporal elements with space separator; emit Time as HH:mm:ss[.fraction] without 1970-01-01 prefix; preserve nanosecond precision when present. - ArrowValues: post-process VarChar cells matching ISO timestamp pattern to swap T → ' ' so DataFusion's native CAST(timestamp AS VARCHAR) results (chart column-split, list_merge intermediates) render with a space. - CastToVarcharRewriter (new): rewrite CAST(boolean AS VARCHAR) to CASE WHEN v THEN 'TRUE' WHEN NOT v THEN 'FALSE' END so the engine emits upper-case literals matching tostring(boolean). - ListAggregateMultiTypeIT: update boolean / date / date_nanos assertions to match the new space-separator and upper-case TRUE output. Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com>
1 parent 389c581 commit 4026e1d

6 files changed

Lines changed: 369 additions & 76 deletions

File tree

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.be.datafusion;
10+
11+
import org.apache.calcite.rel.RelHomogeneousShuttle;
12+
import org.apache.calcite.rel.RelNode;
13+
import org.apache.calcite.rel.type.RelDataType;
14+
import org.apache.calcite.rex.RexBuilder;
15+
import org.apache.calcite.rex.RexCall;
16+
import org.apache.calcite.rex.RexNode;
17+
import org.apache.calcite.rex.RexShuttle;
18+
import org.apache.calcite.sql.SqlKind;
19+
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
20+
import org.apache.calcite.sql.type.SqlTypeName;
21+
22+
import java.util.List;
23+
24+
/**
25+
* Pre-isthmus pass that rewrites {@code CAST(boolean AS VARCHAR)} to
26+
* {@code CASE WHEN val THEN 'TRUE' WHEN NOT val THEN 'FALSE' END} so the engine emits the
27+
* upper-case literals PPL clients expect (matching {@code tostring(boolean)}).
28+
*
29+
* <p>Temporal casts to VARCHAR are intentionally NOT rewritten here — that would force a fixed
30+
* format pattern (lossy for nanosecond timestamps). The space-separator + nano-preservation fix
31+
* for temporals lives in {@code ArrowValues}, which post-processes the rendered string.
32+
*
33+
* @opensearch.internal
34+
*/
35+
final class CastToVarcharRewriter {
36+
37+
private CastToVarcharRewriter() {}
38+
39+
static RelNode rewrite(RelNode root) {
40+
return root.accept(new RelHomogeneousShuttle() {
41+
@Override
42+
public RelNode visit(RelNode other) {
43+
RelNode visited = super.visit(other);
44+
return visited.accept(new CastShuttle(visited.getCluster().getRexBuilder()));
45+
}
46+
});
47+
}
48+
49+
/** Package-visible for unit tests; production code goes through {@link #rewrite}. */
50+
static RexShuttle newShuttle(RexBuilder rexBuilder) {
51+
return new CastShuttle(rexBuilder);
52+
}
53+
54+
private static final class CastShuttle extends RexShuttle {
55+
56+
private final RexBuilder rexBuilder;
57+
58+
CastShuttle(RexBuilder rexBuilder) {
59+
this.rexBuilder = rexBuilder;
60+
}
61+
62+
@Override
63+
public RexNode visitCall(RexCall call) {
64+
RexCall recursed = (RexCall) super.visitCall(call);
65+
if (!isCastBooleanToVarchar(recursed)) {
66+
return recursed;
67+
}
68+
return rewriteBooleanCast(recursed, recursed.getOperands().get(0));
69+
}
70+
71+
/** True iff the call is CAST(boolean AS VARCHAR/CHAR). */
72+
private static boolean isCastBooleanToVarchar(RexCall call) {
73+
if (call.getKind() != SqlKind.CAST && call.getKind() != SqlKind.SAFE_CAST) {
74+
return false;
75+
}
76+
if (call.getOperands().size() != 1) {
77+
return false;
78+
}
79+
SqlTypeName target = call.getType().getSqlTypeName();
80+
if (target != SqlTypeName.VARCHAR && target != SqlTypeName.CHAR) {
81+
return false;
82+
}
83+
return call.getOperands().get(0).getType().getSqlTypeName() == SqlTypeName.BOOLEAN;
84+
}
85+
86+
/** boolean → CASE WHEN v THEN 'TRUE' WHEN NOT v THEN 'FALSE' END (matches tostring(boolean)). */
87+
private RexNode rewriteBooleanCast(RexCall original, RexNode value) {
88+
RelDataType varcharType = original.getType();
89+
RexNode trueLit = rexBuilder.makeLiteral("TRUE");
90+
RexNode falseLit = rexBuilder.makeLiteral("FALSE");
91+
RexNode notValue = rexBuilder.makeCall(SqlStdOperatorTable.NOT, value);
92+
RexNode nullLit = rexBuilder.makeNullLiteral(varcharType);
93+
return rexBuilder.makeCall(varcharType, SqlStdOperatorTable.CASE, List.of(value, trueLit, notValue, falseLit, nullLit));
94+
}
95+
}
96+
}

sandbox/plugins/analytics-backend-datafusion/src/main/java/org/opensearch/be/datafusion/DataFusionFragmentConvertor.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,7 @@ private byte[] convertToSubstrait(RelNode fragment) {
458458
preprocessed = PplAggregateCallRewriter.rewrite(preprocessed);
459459
preprocessed = PplWindowCallRewriter.rewrite(preprocessed);
460460
preprocessed = ItemTypeRebuilder.rewrite(preprocessed);
461+
preprocessed = CastToVarcharRewriter.rewrite(preprocessed);
461462
RelRoot root = RelRoot.of(preprocessed, SqlKind.SELECT);
462463
SubstraitRelVisitor visitor = createVisitor(preprocessed);
463464
Rel substraitRel;
@@ -488,6 +489,7 @@ private Rel convertStandalone(RelNode operator) {
488489
preprocessed = PplAggregateCallRewriter.rewrite(preprocessed);
489490
preprocessed = PplWindowCallRewriter.rewrite(preprocessed);
490491
preprocessed = ItemTypeRebuilder.rewrite(preprocessed);
492+
preprocessed = CastToVarcharRewriter.rewrite(preprocessed);
491493
SubstraitRelVisitor visitor = createVisitor(preprocessed);
492494
return visitor.apply(preprocessed);
493495
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.be.datafusion;
10+
11+
import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
12+
import org.apache.calcite.rel.type.RelDataType;
13+
import org.apache.calcite.rel.type.RelDataTypeFactory;
14+
import org.apache.calcite.rex.RexBuilder;
15+
import org.apache.calcite.rex.RexCall;
16+
import org.apache.calcite.rex.RexLiteral;
17+
import org.apache.calcite.rex.RexNode;
18+
import org.apache.calcite.sql.SqlKind;
19+
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
20+
import org.apache.calcite.sql.type.SqlTypeName;
21+
import org.opensearch.test.OpenSearchTestCase;
22+
23+
public class CastToVarcharRewriterTests extends OpenSearchTestCase {
24+
25+
/** CAST(boolean AS VARCHAR) → CASE WHEN val THEN 'TRUE' WHEN NOT val THEN 'FALSE' END. */
26+
public void testBooleanCastRewritesToCaseUpper() {
27+
Cluster c = newCluster();
28+
RexNode operand = c.inputRef(0, SqlTypeName.BOOLEAN);
29+
RexNode cast = c.castTo(operand, SqlTypeName.VARCHAR);
30+
31+
RexCall out = (RexCall) cast.accept(CastToVarcharRewriter.newShuttle(c.rexBuilder));
32+
33+
assertEquals(SqlKind.CASE, out.getKind());
34+
assertEquals("TRUE", ((RexLiteral) out.getOperands().get(1)).getValueAs(String.class));
35+
assertEquals("FALSE", ((RexLiteral) out.getOperands().get(3)).getValueAs(String.class));
36+
}
37+
38+
/** Temporal casts pass through — handled by ArrowValues post-processing instead. */
39+
public void testTimestampCastPassesThrough() {
40+
Cluster c = newCluster();
41+
RexNode operand = c.inputRef(0, SqlTypeName.TIMESTAMP);
42+
RexNode cast = c.castTo(operand, SqlTypeName.VARCHAR);
43+
44+
RexNode out = cast.accept(CastToVarcharRewriter.newShuttle(c.rexBuilder));
45+
46+
assertEquals(SqlKind.CAST, out.getKind());
47+
}
48+
49+
/** Unrelated casts (INT → VARCHAR) pass through. */
50+
public void testIntegerCastPassesThrough() {
51+
Cluster c = newCluster();
52+
RexNode operand = c.inputRef(0, SqlTypeName.INTEGER);
53+
RexNode cast = c.castTo(operand, SqlTypeName.VARCHAR);
54+
55+
RexNode out = cast.accept(CastToVarcharRewriter.newShuttle(c.rexBuilder));
56+
57+
assertEquals(SqlKind.CAST, out.getKind());
58+
}
59+
60+
/** Non-cast calls pass through. */
61+
public void testNonCastCallPassesThrough() {
62+
Cluster c = newCluster();
63+
RexNode left = c.inputRef(0, SqlTypeName.INTEGER);
64+
RexNode right = c.inputRef(1, SqlTypeName.INTEGER);
65+
RexNode plus = c.rexBuilder.makeCall(SqlStdOperatorTable.PLUS, left, right);
66+
67+
RexNode out = plus.accept(CastToVarcharRewriter.newShuttle(c.rexBuilder));
68+
69+
assertSame(plus, out);
70+
}
71+
72+
private static Cluster newCluster() {
73+
RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl();
74+
RexBuilder rexBuilder = new RexBuilder(typeFactory);
75+
return new Cluster(typeFactory, rexBuilder);
76+
}
77+
78+
private static final class Cluster {
79+
final RelDataTypeFactory typeFactory;
80+
final RexBuilder rexBuilder;
81+
82+
Cluster(RelDataTypeFactory typeFactory, RexBuilder rexBuilder) {
83+
this.typeFactory = typeFactory;
84+
this.rexBuilder = rexBuilder;
85+
}
86+
87+
RexNode inputRef(int index, SqlTypeName type) {
88+
return rexBuilder.makeInputRef(typeFactory.createTypeWithNullability(typeFactory.createSqlType(type), true), index);
89+
}
90+
91+
RexNode castTo(RexNode operand, SqlTypeName target) {
92+
RelDataType targetType = typeFactory.createTypeWithNullability(typeFactory.createSqlType(target), true);
93+
return rexBuilder.makeCast(targetType, operand);
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)