Skip to content

Commit 87b1de6

Browse files
committed
Register PPL ADDDATE/SUBDATE with integer-days overload
ADDDATE / SUBDATE were unbound in the analytics-engine scalar registry, so PPL queries using either UDF returned `No backend supports scalar function [ADDDATE]`. Both alias DATE_ADD / DATE_SUB but additionally accept an integer second operand (days), which the existing DateAddSubAdapter rejected. Add ADDDATE / SUBDATE to ScalarFunction, register them against the shared DateAddSubAdapter, and extend the adapter to rebuild integer operands as INTERVAL N DAY before the existing interval-lowering path. Cluster verified — 10/10 affected queries return HTTP 200 with expected values; null-row semantics preserved.
1 parent 5ee2f94 commit 87b1de6

3 files changed

Lines changed: 123 additions & 15 deletions

File tree

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

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131

3232
/**
3333
* Rewrites PPL {@code DATE_ADD(base, INTERVAL n unit)} / {@code DATE_SUB(base, INTERVAL n unit)}
34-
* into {@code DATETIME_PLUS(CAST(base AS TIMESTAMP), interval)}, which lowers to Substrait's
34+
* (and the alias forms {@code ADDDATE} / {@code SUBDATE}) into
35+
* {@code DATETIME_PLUS(CAST(base AS TIMESTAMP), interval)}, which lowers to Substrait's
3536
* {@code add(timestamp, interval)} that DataFusion executes natively. The raw PPL UDFs have no
3637
* Substrait binding, so isthmus rejects them.
3738
*
@@ -45,6 +46,10 @@
4546
* {@link TimeUnit#DAY} or {@link TimeUnit#MONTH} qualifier) and folds the {@code DATE_SUB} sign in,
4647
* the same way {@link EarliestLatestAdapter} does.
4748
*
49+
* <p>{@code ADDDATE(base, n_days)} / {@code SUBDATE(base, n_days)} share the same lowering: the
50+
* integer second operand is rebuilt as an {@code INTERVAL n DAY} literal before the standard
51+
* interval path runs. This matches the SQL plugin's {@code AddSubDateFunction} semantics.
52+
*
4853
* <p>DATE / TIMESTAMP / TIME / character bases are all lowered. TIME anchors to today-UTC at plan
4954
* time, which can drift from the PPL UDF's query-start anchor across UTC midnight or cached plans
5055
* (TODO: thread {@code FunctionProperties#getQueryStartClock} through to adapters). MICROSECOND
@@ -74,21 +79,14 @@ public RexNode adapt(RexCall original, List<FieldStorageInfo> fieldStorage, RelO
7479
}
7580
RexBuilder rexBuilder = cluster.getRexBuilder();
7681
RexNode base = original.getOperands().get(0);
77-
RexNode intervalOperand = stripOperatorAnnotation(original.getOperands().get(1));
78-
79-
SqlIntervalQualifier qualifier;
80-
BigDecimal leadingValue;
81-
if (intervalOperand instanceof RexLiteral intervalLiteral
82-
&& SqlTypeName.INTERVAL_TYPES.contains(intervalLiteral.getType().getSqlTypeName())) {
83-
qualifier = intervalLiteral.getType().getIntervalQualifier();
84-
leadingValue = intervalLiteral.getValueAs(BigDecimal.class);
85-
} else if (SqlTypeFamily.NUMERIC.contains(intervalOperand.getType()) && intervalOperand instanceof RexLiteral numericLiteral) {
86-
// ADDDATE/SUBDATE integer form: ADDDATE(base, N) is treated as INTERVAL N DAY.
87-
qualifier = new SqlIntervalQualifier(TimeUnit.DAY, null, SqlParserPos.ZERO);
88-
leadingValue = numericLiteral.getValueAs(BigDecimal.class);
89-
} else {
82+
RexNode rawSecond = stripOperatorAnnotation(original.getOperands().get(1));
83+
// ADDDATE/SUBDATE accept INTEGER days; rebuild as INTERVAL n DAY so the interval path runs.
84+
RexLiteral intervalLiteral = asIntervalLiteral(rawSecond, rexBuilder);
85+
if (intervalLiteral == null) {
9086
return original;
9187
}
88+
SqlIntervalQualifier qualifier = intervalLiteral.getType().getIntervalQualifier();
89+
BigDecimal leadingValue = intervalLiteral.getValueAs(BigDecimal.class);
9290
if (qualifier == null || leadingValue == null) {
9391
return original;
9492
}
@@ -194,4 +192,25 @@ private static RexNode stripOperatorAnnotation(RexNode node) {
194192
}
195193
return node;
196194
}
195+
196+
/**
197+
* Returns {@code node} when it's already an interval literal; for an integer literal returns a
198+
* synthetic {@code INTERVAL n DAY} literal (the ADDDATE/SUBDATE integer-days form). Anything
199+
* else returns null so the caller can pass the call through to the UDF path.
200+
*/
201+
private static RexLiteral asIntervalLiteral(RexNode node, RexBuilder rexBuilder) {
202+
if (node instanceof RexLiteral lit) {
203+
if (SqlTypeName.INTERVAL_TYPES.contains(lit.getType().getSqlTypeName())) {
204+
return lit;
205+
}
206+
if (SqlTypeName.INT_TYPES.contains(lit.getType().getSqlTypeName())) {
207+
BigDecimal days = lit.getValueAs(BigDecimal.class);
208+
if (days == null) {
209+
return null;
210+
}
211+
return rexBuilder.makeIntervalLiteral(days, new SqlIntervalQualifier(TimeUnit.DAY, null, SqlParserPos.ZERO));
212+
}
213+
}
214+
return null;
215+
}
197216
}

sandbox/plugins/analytics-backend-datafusion/src/test/java/org/opensearch/be/datafusion/DateAddSubAdapterTests.java

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import java.math.BigDecimal;
3232
import java.util.List;
3333

34-
/** Covers {@link DateAddSubAdapter}: TIME base anchors to today UTC before DATETIME_PLUS. */
34+
/** Covers {@link DateAddSubAdapter}: TIME base anchors to today UTC; integer-days form rebuilds as INTERVAL DAY. */
3535
public class DateAddSubAdapterTests extends OpenSearchTestCase {
3636

3737
private final RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl();
@@ -47,6 +47,24 @@ public class DateAddSubAdapterTests extends OpenSearchTestCase {
4747
SqlFunctionCategory.TIMEDATE
4848
);
4949

50+
private static final SqlFunction ADDDATE_OP = new SqlFunction(
51+
"ADDDATE",
52+
SqlKind.OTHER_FUNCTION,
53+
ReturnTypes.TIMESTAMP,
54+
null,
55+
OperandTypes.VARIADIC,
56+
SqlFunctionCategory.TIMEDATE
57+
);
58+
59+
private static final SqlFunction SUBDATE_OP = new SqlFunction(
60+
"SUBDATE",
61+
SqlKind.OTHER_FUNCTION,
62+
ReturnTypes.TIMESTAMP,
63+
null,
64+
OperandTypes.VARIADIC,
65+
SqlFunctionCategory.TIMEDATE
66+
);
67+
5068
/** DATE_ADD(TIME-col, INTERVAL 1 DAY) → DATETIME_PLUS(CAST(CONCAT(today,' ',CAST time AS VARCHAR)) AS TIMESTAMP), 86400000 millis). */
5169
public void testDateAddTimeOperandAnchoredToToday() {
5270
RelDataType timeType = typeFactory.createSqlType(SqlTypeName.TIME);
@@ -70,4 +88,43 @@ public void testDateAddTimeOperandAnchoredToToday() {
7088
RexCall concat = (RexCall) castNode.getOperands().get(0);
7189
assertEquals("||", concat.getOperator().getName());
7290
}
91+
92+
/** ADDDATE(DATE-col, 1) → DATETIME_PLUS(CAST(date AS …), INTERVAL 1 DAY). The integer 1 is rebuilt as a DAY interval. */
93+
public void testAddDateIntegerDaysOnDateRebuiltAsIntervalDay() {
94+
RelDataType dateType = typeFactory.createSqlType(SqlTypeName.DATE);
95+
RexNode dateCol = rexBuilder.makeInputRef(dateType, 0);
96+
RexNode oneInt = rexBuilder.makeLiteral(1, typeFactory.createSqlType(SqlTypeName.INTEGER), false);
97+
RexCall original = (RexCall) rexBuilder.makeCall(ADDDATE_OP, List.of(dateCol, oneInt));
98+
99+
RexNode adapted = new DateAddSubAdapter(true).adapt(original, List.of(), cluster);
100+
101+
RexCall outer = adapted instanceof RexCall && ((RexCall) adapted).getKind() == SqlKind.CAST
102+
? (RexCall) ((RexCall) adapted).getOperands().get(0)
103+
: (RexCall) adapted;
104+
assertSame(SqlStdOperatorTable.DATETIME_PLUS, outer.getOperator());
105+
RexNode shiftedInterval = outer.getOperands().get(1);
106+
assertTrue(SqlTypeName.INTERVAL_TYPES.contains(shiftedInterval.getType().getSqlTypeName()));
107+
assertEquals(TimeUnit.DAY, shiftedInterval.getType().getIntervalQualifier().getUnit());
108+
// INTERVAL DAY values are stored in millis after the unit-rebuild step; +1 day = +86400000.
109+
long signed = ((org.apache.calcite.rex.RexLiteral) shiftedInterval).getValueAs(Long.class);
110+
assertEquals(86_400_000L, signed);
111+
}
112+
113+
/** SUBDATE(TIMESTAMP-col, 5) → DATETIME_PLUS(ts, INTERVAL -5 DAY). Sign folded for SUB. */
114+
public void testSubDateIntegerDaysFoldsSign() {
115+
RelDataType tsType = typeFactory.createSqlType(SqlTypeName.TIMESTAMP);
116+
RexNode tsCol = rexBuilder.makeInputRef(tsType, 0);
117+
RexNode fiveInt = rexBuilder.makeLiteral(5, typeFactory.createSqlType(SqlTypeName.INTEGER), false);
118+
RexCall original = (RexCall) rexBuilder.makeCall(SUBDATE_OP, List.of(tsCol, fiveInt));
119+
120+
RexNode adapted = new DateAddSubAdapter(false).adapt(original, List.of(), cluster);
121+
122+
RexCall outer = adapted instanceof RexCall && ((RexCall) adapted).getKind() == SqlKind.CAST
123+
? (RexCall) ((RexCall) adapted).getOperands().get(0)
124+
: (RexCall) adapted;
125+
assertSame(SqlStdOperatorTable.DATETIME_PLUS, outer.getOperator());
126+
long signed = ((org.apache.calcite.rex.RexLiteral) outer.getOperands().get(1)).getValueAs(Long.class);
127+
// 5 days in millis, negated for SUB.
128+
assertEquals(-5L * 86_400_000L, signed);
129+
}
73130
}

sandbox/qa/analytics-engine-rest/src/test/java/org/opensearch/analytics/qa/DatetimeCoverageIT.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,38 @@ public void testClusterB_dateSubOnTime() throws IOException {
181181
assertEquals("date_sub(time, -30min) wall time must be 08:30:00", "08:30:00", firstRowFirstCell(response));
182182
}
183183

184+
/** Cluster B: ADDDATE(DATE, integer days) lowers via DateAddSubAdapter integer-days form. */
185+
public void testClusterB_addDateIntegerDaysOnDate() throws IOException {
186+
assertFirstRowString(
187+
oneRow() + "| eval f = adddate(date('2020-08-26'), 1) | fields f",
188+
"2020-08-27"
189+
);
190+
}
191+
192+
/** Cluster B: SUBDATE(DATE, integer days) — sign folded by adapter. */
193+
public void testClusterB_subDateIntegerDaysOnDate() throws IOException {
194+
assertFirstRowString(
195+
oneRow() + "| eval f = subdate(date('2020-08-26'), 1) | fields f",
196+
"2020-08-25"
197+
);
198+
}
199+
200+
/** Cluster B: ADDDATE(TIMESTAMP, integer days) — promotes to TIMESTAMP wall time. */
201+
public void testClusterB_addDateIntegerDaysOnTimestamp() throws IOException {
202+
assertFirstRowString(
203+
oneRow() + "| eval f = adddate(timestamp('2020-09-16 17:30:00'), 1) | fields f",
204+
"2020-09-17 17:30:00"
205+
);
206+
}
207+
208+
/** Cluster B: ADDDATE(DATE, INTERVAL n DAY) — interval form sharing the DATE_ADD path. */
209+
public void testClusterB_addDateIntervalOnDate() throws IOException {
210+
assertFirstRowString(
211+
oneRow() + "| eval f = adddate(date('2020-08-26'), interval 3 day) | fields f",
212+
"2020-08-29 00:00:00"
213+
);
214+
}
215+
184216
/** Cluster B: TIMESTAMPADD standalone string-overload resolves. */
185217
public void testClusterB_timestampAddStandalone() throws IOException {
186218
assertFirstRowString(

0 commit comments

Comments
 (0)