Skip to content

Commit ea38f7c

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 ea38f7c

2 files changed

Lines changed: 36 additions & 5 deletions

File tree

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

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,15 @@ 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.
61+
* Epoch-day index of {@code x} via floor-division of {@code to_unixtime(x) / 86400}.
62+
* {@code to_unixtime} returns BIGINT whole seconds; SQL integer division truncates toward zero,
63+
* which differs from floor for negative epoch seconds (pre-1970 dates) — so naïve {@code x / d}
64+
* would put e.g. {@code 1969-12-31 12:00:00} on the same day index as {@code 1970-01-01 00:00:00}
65+
* and skew {@code DATEDIFF} across the epoch. Isthmus has no {@code FLOOR(i64)} signature, so
66+
* the floor is expressed in pure integer arithmetic:
67+
* <pre>{@code floor(x / d) = (x - ((x MOD d + d) MOD d)) / d}</pre>
68+
* The bracketed term is the non-negative remainder; subtracting it makes the numerator a
69+
* multiple of {@code d}, so the trailing divide is exact regardless of {@code x}'s sign.
6570
*
6671
* <p>A bare {@code TIME} operand (e.g. {@code DATEDIFF(TIME('23:59:59'), TIME('00:00:00'))}) is
6772
* anchored to a TIMESTAMP first via {@link DatePartAdapters#coerceCharacterOperandToTimestamp},
@@ -77,6 +82,12 @@ private static RexNode dayNumber(RexNode operand, RelOptCluster cluster) {
7782
BigDecimal.valueOf(TimeOfDayLowering.SECONDS_PER_DAY),
7883
rexBuilder.getTypeFactory().createSqlType(SqlTypeName.BIGINT)
7984
);
80-
return rexBuilder.makeCall(SqlStdOperatorTable.DIVIDE, epochSeconds, secondsPerDay);
85+
// floorMod = ((x MOD d) + d) MOD d — non-negative remainder.
86+
RexNode mod1 = rexBuilder.makeCall(SqlStdOperatorTable.MOD, epochSeconds, secondsPerDay);
87+
RexNode plusDay = rexBuilder.makeCall(SqlStdOperatorTable.PLUS, mod1, secondsPerDay);
88+
RexNode floorMod = rexBuilder.makeCall(SqlStdOperatorTable.MOD, plusDay, secondsPerDay);
89+
// numerator = x - floorMod, which is exactly divisible by d.
90+
RexNode numerator = rexBuilder.makeCall(SqlStdOperatorTable.MINUS, epochSeconds, floorMod);
91+
return rexBuilder.makeCall(SqlStdOperatorTable.DIVIDE, numerator, secondsPerDay);
8192
}
8293
}

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

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

461+
/**
462+
* DATEDIFF across the Unix epoch boundary — pins the floor-divide. Naïve integer division
463+
* (truncating toward zero) would put both operands on day 0 and return 0; floor-divide
464+
* correctly assigns 1969-12-31 to day -1, giving the expected 1-day delta.
465+
*/
466+
public void testDateDiffAcrossEpochBoundary() throws IOException {
467+
assertFirstRowLong(
468+
oneRow("key00") + "| eval d = datediff('1970-01-01 00:00:00', '1969-12-31 12:00:00') | fields d",
469+
1L
470+
);
471+
}
472+
473+
/** DATEDIFF on a pre-1970 pair — both operands negative-epoch, floor-divide preserves the calendar delta. */
474+
public void testDateDiffPre1970Pair() throws IOException {
475+
assertFirstRowLong(
476+
oneRow("key00") + "| eval d = datediff('1969-12-31 00:00:00', '1969-12-30 00:00:00') | fields d",
477+
1L
478+
);
479+
}
480+
461481
/**
462482
* DATEDIFF on two TIME operands: both anchor to the same (today's UTC) date, so their day-counts
463483
* cancel — matches MySQL's documented behavior of returning 0 even when the time-of-day values

0 commit comments

Comments
 (0)