2

I came across this issue while writing a test case where I had to get a range of records between a range of timestamps –– using H2 embedded database with spring-data-jpa.

The original issue is located at: Fetching records BETWEEN two java.time.Instant instances in Spring Data Query

I have the timestamps as java.time.Instant instances.

If the user gives no start-end timestamps, I go ahead and plug in Instant.MIN and Instant.MAX respectively.

What perplexes me is that the following test-case passes:

  @Test
  public void test_date_min_max_instants_timestamps() {
    Timestamp past = new Timestamp(Long.MIN_VALUE); 
    Timestamp future = new Timestamp(Long.MAX_VALUE); 
    Timestamp present = Timestamp.from(Instant.now()); 

    assertTrue(present.after(past));
    assertTrue(future.after(past));
    assertTrue(future.after(present));

    assertTrue(present.before(future));
    assertTrue(past.before(present));
    assertTrue(past.before(future));
  }

but, the following test-case fails:

 @Test
  public void test_instant_ranges() throws InterruptedException {
    Timestamp past = Timestamp.from(Instant.MIN);
    Timestamp future = Timestamp.from(Instant.MAX);
    Timestamp present = Timestamp.from(Instant.now());

    assertTrue(present.after(past));
    assertTrue(future.after(past));
    assertTrue(future.after(present));

    assertTrue(present.before(future));
    assertTrue(past.before(present));
    assertTrue(past.before(future));
  }

Furthermore, if the past and future are not MIN/MAX values, but normal values instead, the result is as expected.

Any idea why java.sql.Timestamp behaves like this?

Also, if the time represented by Instant is too big for Timestamp shouldn't it fail instead?

P.S. If this question has already been asked, could someone link the original since I haven't been able to find it.

Edit: Added the debug information I mentioned in the comment section, so that we have everything in one place.

For the Timestamp instances made from Instant.MIN and Instant.MAX, I had the following values:

past = 169108098-07-03 21:51:43.0 
future = 169104627-12-11 11:08:15.999999999 
present = 2018-07-23 10:46:50.842 

and for the Timestamp instances made from Long.MIN_VALUE and Long.MAX_VALUE, I got:

past = 292278994-08-16 23:12:55.192 
future = 292278994-08-16 23:12:55.807 
present = 2018-07-23 10:49:54.281

To clarify my question, instead of failing silently or using using a different value internally, the Timestamp should fail explicitly. Currently it doesn't.

sidmishraw
  • 390
  • 4
  • 15
  • 1
    *Also, if the time represented by Instant is too big for Timestamp shouldn't it fail instead?* Instead of what? – shmosel Jul 23 '18 at 17:30
  • [What does your step debugger tell you?](http://stackoverflow.com/questions/25385173/what-is-a-debugger-and-how-can-it-help-me-diagnose-problems). Your question can be answered very quickly and easily with your step-debugger. You should always try and solve your problems with a step debugger before coming to StackOverflow. –  Jul 23 '18 at 17:35
  • For the Timestamp instances made from Instant.MIN and Instant.MAX -- past = 169108098-07-03 21:51:43.0 future = 169104627-12-11 11:08:15.999999999 present = 2018-07-23 10:46:50.842 and for Timestamp instances made from Long.MIN_VALUE and Long.MAX_VALUE, I get: past = 292278994-08-16 23:12:55.192 future = 292278994-08-16 23:12:55.807 present = 2018-07-23 10:49:54.281 P.S. The reason I've posted this question is because I wasn't able to solve it myself. – sidmishraw Jul 23 '18 at 17:54
  • @shmosel Instead of failing silently or using using a different value internally. Basically, what I'm trying to get at is: the conversion should fail explicitly if not possible. – sidmishraw Jul 23 '18 at 18:03
  • What do you mean? Does it not throw an exception? – shmosel Jul 23 '18 at 18:04
  • No, it doesn't. – sidmishraw Jul 23 '18 at 18:23
  • 2
    Thanks, @sidmishraw, for adding more information. Please do so *in the question* ([edit it](https://stackoverflow.com/posts/51484091/edit)) rather than in comments so we have everything in one place. Thx. – Ole V.V. Jul 25 '18 at 07:55

1 Answers1

4

This is a known bug in the Timestamp class and its conversion from Instant. It was registered in the Java bug database in January 2015, three and a half years ago (and is still open with no decided fix version). See the link to the official bug report at the bottom.

Expected behaviour is clear

The documentation of Timestamp.from(Instant) is pretty clear about this:

Instant can store points on the time-line further in the future and further in the past than Date. In this scenario, this method will throw an exception.

So yes, an exception should be thrown.

It’s straightforward to reproduce the bug

On my Java 10 I have reproduced a couple of examples where the conversion silently gives an incorrect result rather than throwing an exception. One example is:

        Instant i = LocalDate.of(-400_000_000, Month.JUNE, 14)
                .atStartOfDay(ZoneId.of("Africa/Cairo"))
                .toInstant();
        Timestamp ts = Timestamp.from(i);
        System.out.println("" + i + " -> " + ts + " -> " + ts.toInstant());

This prints:

-400000000-06-13T21:54:51Z -> 184554049-09-14 14:20:42.0 -> +184554049-09-14T12:20:42Z

The former conversion is very obviously wrong: a time in the far past has been converted into a time in the far (though not quite as far) future (the conversion back to Instant seems to be correct).

Appendix: JDK source code

For the curious here is the implementation of the conversion method:

public static Timestamp from(Instant instant) {
    try {
        Timestamp stamp = new Timestamp(instant.getEpochSecond() * MILLIS_PER_SECOND);
        stamp.nanos = instant.getNano();
        return stamp;
    } catch (ArithmeticException ex) {
        throw new IllegalArgumentException(ex);
    }
}

It may seem that the author had expected that an arithmetic overflow in the multiplication would cause an ArithmeticException. It does not.

Links

Ole V.V.
  • 81,772
  • 15
  • 137
  • 161