Skip to content

Commit a9570fd

Browse files
committed
Floor-divide DATEDIFF day-numbers for pre-1970 correctness
SQL `/` truncates toward zero, so naive `to_unixtime(x) / 86400` rounds negative epoch seconds the wrong way and puts e.g. `1969-12-31 12:00` on the same epoch-day as `1970-01-01 00:00`. Express the floor in pure integer arithmetic — `(x - ((x MOD d + d) MOD d)) / d` — since isthmus has no `FLOOR(i64)` signature in the bound catalog. Pin with two DateTimeScalarFunctionsIT cases that span and sit below the epoch. Signed-off-by: Vinay Krishna Pudyodu <vinkrish.neo@gmail.com>
1 parent 8feacbe commit a9570fd

2 files changed

Lines changed: 29 additions & 11 deletions

File tree

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

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,14 @@ public RexNode adapt(RexCall original, List<FieldStorageInfo> fieldStorage, RelO
5858
}
5959

6060
/**
61-
* {@code to_unixtime(x) / 86400} — the epoch-day index of {@code x}. {@code to_unixtime} returns
62-
* BIGINT whole seconds, so the divide is integer division (truncates toward zero); for the UTC
63-
* epoch seconds of any post-1970 date that equals {@code floor}, giving the calendar-day index.
64-
* No explicit FLOOR — isthmus has no {@code FLOOR(i64)} signature.
65-
*
66-
* <p>A bare {@code TIME} operand (e.g. {@code DATEDIFF(TIME('23:59:59'), TIME('00:00:00'))}) is
67-
* anchored to a TIMESTAMP first via {@link DatePartAdapters#coerceCharacterOperandToTimestamp},
68-
* because {@code to_unixtime} rejects an Arrow {@code Time64}. Both TIME operands anchor to the
69-
* same (today's) date, so their day-numbers cancel — matching MySQL's {@code DATEDIFF} on times
70-
* returning 0.
61+
* Epoch-day index of {@code x}: floor-divide {@code to_unixtime(x)} by 86400. SQL {@code /}
62+
* truncates toward zero, which differs from floor for negative epoch seconds (pre-1970) — and
63+
* isthmus has no {@code FLOOR(i64)} in the bound catalog, so the floor is expressed in pure
64+
* integer arithmetic:
65+
* <pre>{@code floor(x / d) = (x - ((x MOD d + d) MOD d)) / d}</pre>
66+
* Bare {@code TIME} operands are anchored to today's TIMESTAMP first ({@code to_unixtime}
67+
* rejects {@code Time64}); both operands anchor to the same date so their day-numbers cancel,
68+
* matching MySQL's {@code DATEDIFF}-on-times returning 0.
7169
*/
7270
private static RexNode dayNumber(RexNode operand, RelOptCluster cluster) {
7371
RexBuilder rexBuilder = cluster.getRexBuilder();
@@ -77,6 +75,10 @@ private static RexNode dayNumber(RexNode operand, RelOptCluster cluster) {
7775
BigDecimal.valueOf(TimeOfDayLowering.SECONDS_PER_DAY),
7876
rexBuilder.getTypeFactory().createSqlType(SqlTypeName.BIGINT)
7977
);
80-
return rexBuilder.makeCall(SqlStdOperatorTable.DIVIDE, epochSeconds, secondsPerDay);
78+
RexNode mod1 = rexBuilder.makeCall(SqlStdOperatorTable.MOD, epochSeconds, secondsPerDay);
79+
RexNode plusDay = rexBuilder.makeCall(SqlStdOperatorTable.PLUS, mod1, secondsPerDay);
80+
RexNode floorMod = rexBuilder.makeCall(SqlStdOperatorTable.MOD, plusDay, secondsPerDay);
81+
RexNode numerator = rexBuilder.makeCall(SqlStdOperatorTable.MINUS, epochSeconds, floorMod);
82+
return rexBuilder.makeCall(SqlStdOperatorTable.DIVIDE, numerator, secondsPerDay);
8183
}
8284
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,22 @@ public void testDateDiffIgnoresTimeOfDay() throws IOException {
458458
);
459459
}
460460

461+
/** DATEDIFF spanning the epoch — pins floor-divide; truncate-toward-zero would return 0. */
462+
public void testDateDiffAcrossEpochBoundary() throws IOException {
463+
assertFirstRowLong(
464+
oneRow("key00") + "| eval d = datediff('1970-01-01 00:00:00', '1969-12-31 12:00:00') | fields d",
465+
1L
466+
);
467+
}
468+
469+
/** DATEDIFF on a pre-1970 pair — both operands negative-epoch, floor-divide preserves the calendar delta. */
470+
public void testDateDiffPre1970Pair() throws IOException {
471+
assertFirstRowLong(
472+
oneRow("key00") + "| eval d = datediff('1969-12-31 00:00:00', '1969-12-30 00:00:00') | fields d",
473+
1L
474+
);
475+
}
476+
461477
/**
462478
* DATEDIFF on two TIME operands: both anchor to the same (today's UTC) date, so their day-counts
463479
* cancel — matches MySQL's documented behavior of returning 0 even when the time-of-day values

0 commit comments

Comments
 (0)