Skip to content

Commit

Permalink
Fix ordering
Browse files Browse the repository at this point in the history
  • Loading branch information
Torch3333 committed Dec 18, 2024
1 parent 152e196 commit e79630c
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 29 deletions.
50 changes: 31 additions & 19 deletions core/src/main/java/com/scalar/db/storage/ColumnEncodingUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
import java.time.LocalTime;
import java.time.ZoneOffset;

/**
* This class provides utility methods for encoding and decoding time related column values for
* DynamoDB, CosmosDB and SQLite
*/
public final class ColumnEncodingUtils {
private ColumnEncodingUtils() {}

Expand Down Expand Up @@ -37,46 +41,54 @@ public static long encode(TimestampColumn column) {
}

public static LocalDateTime decodeTimestamp(long longTimestamp) {
long nanoOfSeconds = longTimestamp % 1000 * 1_000_000;
// Invert the nanoOfSeconds when the encoded instant is negative, that is for a date before 1970
long milliOfSecond = longTimestamp % 1000;
if (longTimestamp < 0) {
nanoOfSeconds *= -1;
// Convert the complement of the millisecondOfSecond to the actual millisecondOfSecond
milliOfSecond += 1000 - 1;
}
return LocalDateTime.ofEpochSecond(
longTimestamp / 1000, Math.toIntExact(nanoOfSeconds), ZoneOffset.UTC);
longTimestamp / 1000, Math.toIntExact(milliOfSecond * 1_000_000), ZoneOffset.UTC);
}

public static long encode(TimestampTZColumn column) {
assert column.getTimestampTZValue() != null;
return encodeInstant(column.getTimestampTZValue());
}

public static Instant decodeTimestampTZ(long longTimestampTZ) {
long milliOfSecond = longTimestampTZ % 1000;

if (longTimestampTZ < 0) {
// Convert the complement of the millisecondOfSecond to the actual millisecondOfSecond
milliOfSecond += 1000 - 1;
}
return Instant.ofEpochSecond(longTimestampTZ / 1000, milliOfSecond * 1_000_000);
}

@SuppressWarnings("JavaInstantGetSecondsGetNano")
private static long encodeInstant(Instant instant) {
// Encoding format : <epochSecond><millisecondOfSecond>
// Encoding format on a long : <epochSecond><millisecondOfSecond>
// The rightmost three digits are the number of milliseconds from the start of the
// second, the other digits on the left are the epochSecond
// second, the other digits on the left are the epochSecond.
// If the epochSecond is negative (for a date before 1970), to preserve the timestamp ordering
// the
// millisecondOfSecond is converted to its complement
// with the formula "complementOfN = 1000 - 1 - N", where N is the millisecondOfSecond.
//
// For example:
// - if epochSecond=12345 and millisecondOfSecond=789, then the encoded value will be 12345789
// - if epochSecond=-12345 and millisecondOfSecond=789, then the encoded value will be -12345789
// - if epochSecond=-12345 and millisecondOfSecond=789, then
// millisecondOfSecondComplement = 1000 - 1 - 789 = 210. So the encoded value will be
// -12345210.

long encoded = instant.getEpochSecond() * 1000;
// Subtract the millisecondOfSeconds when the epochSecond is negative, that is for a date before
// 1970
if (encoded < 0) {
encoded -= instant.getNano() / 1_000_000;
// Convert the nanosecondOfSecond to millisecondOfSecond, compute its complement and subtract
// it
encoded -= 1000 - 1 - instant.getNano() / 1_000_000;
} else {
encoded += instant.getNano() / 1_000_000;
}
return encoded;
}

public static Instant decodeTimestampTZ(long longTimestampTZ) {
long nanoOfSeconds = longTimestampTZ % 1000 * 1_000_000;
// Invert the nanoOfSeconds when the encoded instant is negative, that is for a date before 1970
if (longTimestampTZ < 0) {
nanoOfSeconds *= -1;
}
return Instant.ofEpochSecond(longTimestampTZ / 1000, nanoOfSeconds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.junit.jupiter.api.Test;

class ColumnEncodingUtilsTest {
Expand Down Expand Up @@ -67,8 +71,8 @@ public void encodeTimestamp_ShouldWorkProperly() {
// Assert
assertThat(actualPositiveEpochSecondWithNano).isEqualTo(1696163696789L);
assertThat(actualPositiveEpochSecondWithZeroNano).isEqualTo(1696163696000L);
assertThat(actualNegativeEpochSecondWithNano).isEqualTo(-23202242704456L);
assertThat(actualNegativeEpochSecondWithZeroNano).isEqualTo(-23202242704000L);
assertThat(actualNegativeEpochSecondWithNano).isEqualTo(-23202242704543L);
assertThat(actualNegativeEpochSecondWithZeroNano).isEqualTo(-23202242704999L);
assertThat(actualEpoch).isEqualTo(0L);
}

Expand Down Expand Up @@ -109,8 +113,8 @@ public void encodeTimestampTZ_ShouldWorkProperly() {
// Assert
assertThat(actualPositiveEpochSecondWithNano).isEqualTo(1696163696789L);
assertThat(actualPositiveEpochSecondWithZeroNano).isEqualTo(1696163696000L);
assertThat(actualNegativeEpochSecondWithNano).isEqualTo(-23202242704456L);
assertThat(actualNegativeEpochSecondWithZeroNano).isEqualTo(-23202242704000L);
assertThat(actualNegativeEpochSecondWithNano).isEqualTo(-23202242704543L);
assertThat(actualNegativeEpochSecondWithZeroNano).isEqualTo(-23202242704999L);
assertThat(actualEpoch).isEqualTo(0L);
}

Expand Down Expand Up @@ -140,9 +144,9 @@ public void decodeTimestamp_ShouldWorkProperly() {
LocalDateTime positiveEpochSecondWithZeroNano =
ColumnEncodingUtils.decodeTimestamp(1696163696000L);
LocalDateTime negativeEpochSecondWithNano =
ColumnEncodingUtils.decodeTimestamp(-23202242704456L);
ColumnEncodingUtils.decodeTimestamp(-23202242704543L);
LocalDateTime negativeEpochSecondWithZeroNano =
ColumnEncodingUtils.decodeTimestamp(-23202242704000L);
ColumnEncodingUtils.decodeTimestamp(-23202242704999L);
LocalDateTime epoch = ColumnEncodingUtils.decodeTimestamp(0L);

// Act assert
Expand All @@ -162,9 +166,9 @@ public void decodeTimestampTZ_ShouldWorkProperly() {
// Arrange
Instant positiveEpochSecondWithNano = ColumnEncodingUtils.decodeTimestampTZ(1696163696789L);
Instant positiveEpochSecondWithZeroNano = ColumnEncodingUtils.decodeTimestampTZ(1696163696000L);
Instant negativeEpochSecondWithNano = ColumnEncodingUtils.decodeTimestampTZ(-23202242704456L);
Instant negativeEpochSecondWithNano = ColumnEncodingUtils.decodeTimestampTZ(-23202242704543L);
Instant negativeEpochSecondWithZeroNano =
ColumnEncodingUtils.decodeTimestampTZ(-23202242704000L);
ColumnEncodingUtils.decodeTimestampTZ(-23202242704999L);
Instant epoch = ColumnEncodingUtils.decodeTimestampTZ(0);

// Act assert
Expand All @@ -182,7 +186,7 @@ public void decodeTimestampTZ_ShouldWorkProperly() {
}

@Test
public void encodeThenDecodeTimestamp_WithMinAndMaxValues_ShouldWorkProperly() {
public void encodeThenDecodeTimestamp_ShouldPreserverDataIntegrity() {
// Arrange
TimestampColumn min = TimestampColumn.of("timestamp", TimestampColumn.MIN_VALUE);
TimestampColumn max = TimestampColumn.of("timestamp", TimestampColumn.MAX_VALUE);
Expand All @@ -192,10 +196,18 @@ public void encodeThenDecodeTimestamp_WithMinAndMaxValues_ShouldWorkProperly() {
.isEqualTo(TimestampColumn.MIN_VALUE);
assertThat(ColumnEncodingUtils.decodeTimestamp(ColumnEncodingUtils.encode(max)))
.isEqualTo(TimestampColumn.MAX_VALUE);
LocalDateTime start = LocalDateTime.ofEpochSecond(-2, 0, ZoneOffset.UTC);
LocalDateTime end = LocalDateTime.ofEpochSecond(3, 0, ZoneOffset.UTC);
for (LocalDateTime dt = start; dt.isBefore(end); dt = dt.plusNanos(1_000_000)) {
assertThat(
ColumnEncodingUtils.decodeTimestamp(
ColumnEncodingUtils.encode(TimestampColumn.of("ts", dt))))
.isEqualTo(dt);
}
}

@Test
public void encodeThenDecodeTimestampTZ_WithMinAndMaxValues_ShouldWorkProperly() {
public void encodeThenDecodeTimestampTZ_ShouldPreserverDataIntegrity() {
// Arrange
TimestampTZColumn min = TimestampTZColumn.of("timestampTZ", TimestampTZColumn.MIN_VALUE);
TimestampTZColumn max = TimestampTZColumn.of("timestampTZ", TimestampTZColumn.MAX_VALUE);
Expand All @@ -205,5 +217,51 @@ public void encodeThenDecodeTimestampTZ_WithMinAndMaxValues_ShouldWorkProperly()
.isEqualTo(TimestampTZColumn.MIN_VALUE);
assertThat(ColumnEncodingUtils.decodeTimestampTZ(ColumnEncodingUtils.encode(max)))
.isEqualTo(TimestampTZColumn.MAX_VALUE);
Instant start = Instant.ofEpochSecond(-2, 0);
Instant end = Instant.ofEpochSecond(3, 0);
for (Instant instant = start; instant.isBefore(end); instant = instant.plusNanos(1_000_000)) {
assertThat(
ColumnEncodingUtils.decodeTimestampTZ(
ColumnEncodingUtils.encode(TimestampTZColumn.of("ts", instant))))
.isEqualTo(instant);
}
}

@Test
public void encodeTimestamp_ShouldPreserveOrder() {
// Arrange
List<Long> expectedTimestamps = new ArrayList<>();
LocalDateTime start = LocalDateTime.ofEpochSecond(-2, 0, ZoneOffset.UTC);
LocalDateTime end = LocalDateTime.ofEpochSecond(3, 0, ZoneOffset.UTC);
for (LocalDateTime dt = start; dt.isBefore(end); dt = dt.plusNanos(1_000_000)) {
expectedTimestamps.add(ColumnEncodingUtils.encode(TimestampColumn.of("ts", dt)));
}

// Act
List<Long> shuffleThenSorted = new ArrayList<>(expectedTimestamps);
Collections.shuffle(shuffleThenSorted);
shuffleThenSorted.sort(Comparator.naturalOrder());

// Assert
assertThat(shuffleThenSorted).containsExactlyElementsOf(expectedTimestamps);
}

@Test
public void encodeTimestampTZ_ShouldPreserveOrder() {
// Arrange
List<Long> expectedTimestamps = new ArrayList<>();
Instant start = Instant.ofEpochSecond(-2, 0);
Instant end = Instant.ofEpochSecond(3, 0);
for (Instant instant = start; instant.isBefore(end); instant = instant.plusNanos(1_000_000)) {
expectedTimestamps.add(ColumnEncodingUtils.encode(TimestampTZColumn.of("ts", instant)));
}

// Act
List<Long> shuffleThenSorted = new ArrayList<>(expectedTimestamps);
Collections.shuffle(shuffleThenSorted);
shuffleThenSorted.sort(Comparator.naturalOrder());

// Assert
assertThat(shuffleThenSorted).containsExactlyElementsOf(expectedTimestamps);
}
}

0 comments on commit e79630c

Please sign in to comment.