Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pg Client: improve Interval support #1462

Merged
merged 5 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions vertx-pg-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,44 @@
</plugins>
</build>

<profiles>
<profile>
<id>benchmarks</id>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>assemble-benchmarks</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<archive>
<manifest>
<mainClass>org.openjdk.jmh.Main</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>src/test/assembly/benchmarks.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
</profiles>

</project>
54 changes: 50 additions & 4 deletions vertx-pg-client/src/main/java/io/vertx/pgclient/data/Interval.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.vertx.pgclient.data;

import io.vertx.codegen.annotations.DataObject;
import io.vertx.core.json.JsonObject;
import java.time.Duration;

/**
* Postgres Interval is date and time based
Expand Down Expand Up @@ -84,6 +83,35 @@ public static Interval of(int years) {
return new Interval(years);
}

/**
* Creates an instance from the given {@link Duration}.
* <p>
* The conversion algorithm assumes a year lasts 12 months and a month lasts 30 days, as <a href="https://github.com/postgres/postgres/blob/5bbdfa8a18dc56d3e64aa723a68e02e897cb5ec3/src/include/datatype/timestamp.h#L116">Postgres does</a> and ISO 8601 suggests.
*
* @param duration the value to convert
* @return a new instance of {@link Interval}
*/
public static Interval of(Duration duration) {
long totalSeconds = duration.getSeconds();

int years = (int) (totalSeconds / 31104000);
long remainder = totalSeconds % 31104000;

int months = (int) (remainder / 2592000);
remainder = totalSeconds % 2592000;

int days = (int) (remainder / 86400);
remainder = remainder % 86400;

int hours = (int) (remainder / 3600);
remainder = remainder % 3600;

int minutes = (int) (remainder / 60);
remainder = remainder % 60;

return new Interval(years, months, days, hours, minutes, (int) remainder, duration.getNano() / 1000);
}

public Interval years(int years) {
this.years = years;
return this;
Expand Down Expand Up @@ -203,7 +231,25 @@ public int hashCode() {

@Override
public String toString() {
return "Interval( " + years + " years " + months + " months " + days + " days " + hours + " hours " +
minutes + " minutes " + seconds + (microseconds == 0 ? "" : "." + Math.abs(microseconds)) + " seconds )";
return "Interval( "
+ years + " years "
+ months + " months "
+ days + " days "
+ hours + " hours "
+ minutes + " minutes "
+ seconds + " seconds "
+ microseconds + " microseconds )";
}

/**
* Convert this interval to an instance of {@link Duration}.
* <p>
* The conversion algorithm assumes a year lasts 12 months and a month lasts 30 days, as <a href="https://github.com/postgres/postgres/blob/5bbdfa8a18dc56d3e64aa723a68e02e897cb5ec3/src/include/datatype/timestamp.h#L116">Postgres does</a> and ISO 8601 suggests.
*
* @return an instance of {@link Duration} representing the same amount of time as this interval
*/
public Duration toDuration() {
return Duration.ofSeconds(((((years * 12L + months) * 30L + days) * 24L + hours) * 60 + minutes) * 60 + seconds)
.plusNanos(microseconds * 1000L);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@

import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE;
import static java.time.format.DateTimeFormatter.ISO_LOCAL_TIME;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

/**
* @author <a href="mailto:[email protected]">Julien Viet</a>
Expand Down Expand Up @@ -1303,32 +1302,49 @@ private static Circle binaryDecodeCircle(int index, int len, ByteBuf buff) {
}

private static void binaryEncodeINTERVAL(Interval interval, ByteBuf buff) {
Duration duration = Duration
.ofHours(interval.getHours())
.plusMinutes(interval.getMinutes())
.plusSeconds(interval.getSeconds())
.plus(interval.getMicroseconds(), ChronoUnit.MICROS);
// days won't be changed
Period monthYear = Period.of(interval.getYears(), interval.getMonths(), interval.getDays()).normalized();
binaryEncodeINT8(NANOSECONDS.toMicros(duration.toNanos()), buff);
binaryEncodeINT4(monthYear.getDays(), buff);
binaryEncodeINT4((int) monthYear.toTotalMonths(), buff);
// We decompose the interval in 3 parts: months, seconds and micros
int monthsPart = Math.addExact(Math.multiplyExact(interval.getYears(), 12), interval.getMonths());
// A long is big enough to store the maximum/minimum value of the seconds part
long secondsPart = interval.getDays() * 24 * 3600L
+ interval.getHours() * 3600L
+ interval.getMinutes() * 60L
+ interval.getSeconds()
+ interval.getMicroseconds() / 1000000;
int microsPart = interval.getMicroseconds() % 1000000;

// The actual number of months is the sum of the months part and the number of months present in the seconds part
int months = Math.addExact(monthsPart, Math.toIntExact(secondsPart / 2592000));
// The actual number of days is computed from the remainder of the previous division
// It's necessarily smaller than or equal to 29
int days = (int) secondsPart % 2592000 / 86400;
// The actual number of micros is the sum of the micros part and the remainder of previous divisions
// The remainder of previous divisions is necessarily smaller than or equal to a day less a second
// The microseconds part is smaller than a second
// Therefore, their sum is necessarily smaller than a day
long micros = microsPart + secondsPart % 2592000 % 86400 * 1000000;

binaryEncodeINT8(micros, buff);
binaryEncodeINT4(days, buff);
binaryEncodeINT4(months, buff);
}

private static Interval binaryDecodeINTERVAL(int index, int len, ByteBuf buff) {
Duration duration = Duration.of(buff.getLong(index), ChronoUnit.MICROS);
final long hours = duration.toHours();
duration = duration.minusHours(hours);
final long minutes = duration.toMinutes();
duration = duration.minusMinutes(minutes);
final long seconds = NANOSECONDS.toSeconds(duration.toNanos());
duration = duration.minusSeconds(seconds);
final long microseconds = NANOSECONDS.toMicros(duration.toNanos());
int days = buff.getInt(index + 8);
int months = buff.getInt(index + 12);
Period monthYear = Period.of(0, months, days).normalized();
return new Interval(monthYear.getYears(), monthYear.getMonths(), monthYear.getDays(),
(int) hours, (int) minutes, (int) seconds, (int) microseconds);
long micros = buff.getLong(index);
long seconds = micros / 1000000;
micros -= seconds * 1000000;
long minutes = seconds / 60;
seconds -= minutes * 60;
long hours = minutes / 60;
minutes -= hours * 60;
long days = hours / 24;
hours -= days * 24;
days += buff.getInt(index + 8);
long months = days / 30;
days -= months * 30;
months += buff.getInt(index + 12);
long years = months / 12;
months -= years * 12;
return new Interval((int) years, (int) months, (int) days, (int) hours, (int) minutes, (int) seconds, (int) micros);
}

private static UUID binaryDecodeUUID(int index, int len, ByteBuf buff) {
Expand Down
33 changes: 33 additions & 0 deletions vertx-pg-client/src/test/assembly/benchmarks.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!--
~ Copyright (c) 2011-2022 Contributors to the Eclipse Foundation
~
~ This program and the accompanying materials are made available under the
~ terms of the Eclipse Public License 2.0 which is available at
~ http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
~ which is available at https://www.apache.org/licenses/LICENSE-2.0.
~
~ SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
-->

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.1 http://maven.apache.org/xsd/assembly-1.1.1.xsd">
<id>benchmarks</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.build.testOutputDirectory}</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<unpack>true</unpack>
<scope>test</scope>
</dependencySet>
</dependencySets>
</assembly>
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/

package io.vertx.pgclient.benchmarks;

import io.vertx.pgclient.data.Interval;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

import java.io.IOException;
import java.time.Duration;
import java.time.Period;
import java.util.concurrent.TimeUnit;

import static java.time.temporal.ChronoUnit.MICROS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

@Threads(1)
@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 20, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 10, time = 2, timeUnit = TimeUnit.SECONDS)
@Fork(value = 3, jvmArgs = {"-Xms8g", "-Xmx8g", "-Xmn7g"})
public class IntervalBenchmarks {

private Interval interval;

@Setup
public void setup() throws IOException, InterruptedException {
interval = new Interval(-2, 3, 15, -13, 2, -57, -426994);
}

@Benchmark
public void encodeWithDurationAndPeriod(Blackhole blackhole) {
Duration duration = Duration
.ofHours(interval.getHours())
.plusMinutes(interval.getMinutes())
.plusSeconds(interval.getSeconds())
.plus(interval.getMicroseconds(), MICROS);
// days won't be changed
Period monthYear = Period.of(interval.getYears(), interval.getMonths(), interval.getDays()).normalized();

blackhole.consume(NANOSECONDS.toMicros(duration.toNanos()));
blackhole.consume(monthYear.getDays());
blackhole.consume((int) monthYear.toTotalMonths());
}

@Benchmark
public void encodeWithParts(Blackhole blackhole) {
// We decompose the interval in 3 parts: months, seconds and micros
int monthsPart = Math.addExact(Math.multiplyExact(interval.getYears(), 12), interval.getMonths());
// A long is big enough to store the maximum/minimum value of the seconds part
long secondsPart = interval.getDays() * 24 * 3600L
+ interval.getHours() * 3600L
+ interval.getMinutes() * 60L
+ interval.getSeconds()
+ interval.getMicroseconds() / 1000000;
int microsPart = interval.getMicroseconds() % 1000000;

// The actual number of months is the sum of the months part and the number of months present in the seconds part
int months = Math.addExact(monthsPart, Math.toIntExact(secondsPart / 2592000));
// The actual number of days is computed from the remainder of the previous division
// It's necessarily smaller than or equal to 29
int days = (int) secondsPart % 2592000 / 86400;
// The actual number of micros is the sum of the micros part and the remainder of previous divisions
// The remainder of previous divisions is necessarily smaller than or equal to a day less a second
// The microseconds part is smaller than a second
// Therefore, their sum is necessarily smaller than a day
long micros = microsPart + secondsPart % 2592000 % 86400 * 1000000;

blackhole.consume(micros);
blackhole.consume(days);
blackhole.consume(months);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.vertx.pgclient.data;

import io.vertx.pgclient.PgTestBase;
import io.vertx.core.Vertx;
import io.vertx.ext.unit.TestContext;
import io.vertx.pgclient.PgTestBase;
import io.vertx.sqlclient.ColumnChecker;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
Expand Down Expand Up @@ -34,7 +34,7 @@ public abstract class DataTypeTestBase extends PgTestBase {
protected static final OffsetTime dt = OffsetTime.parse("17:55:04.90512+03:00");
protected static final OffsetDateTime odt = OffsetDateTime.parse("2017-05-15T02:59:59.237666Z");
protected static final Interval[] intervals = new Interval[] {
Interval.of().years(10).months(3).days(332).hours(20).minutes(20).seconds(20).microseconds(999991),
Interval.of().years(11).months(2).days(2).hours(20).minutes(20).seconds(20).microseconds(999991),
Interval.of().minutes(20).seconds(20).microseconds(123456),
Interval.of().years(-2).months(-6)
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
package io.vertx.pgclient.data;

import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import io.vertx.pgclient.PgConnection;
import io.vertx.sqlclient.ColumnChecker;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.Tuple;
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import org.junit.Test;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.*;
import java.time.format.DateTimeFormatter;

public class DateTimeTypesExtendedCodecTest extends ExtendedQueryDataTypeCodecTestBase {
Expand Down Expand Up @@ -365,9 +361,9 @@ public void testEncodeTimestampTzAfterPgEpoch(TestContext ctx) {
@Test
public void testDecodeInterval(TestContext ctx) {
Interval interval = Interval.of()
.years(10)
.months(3)
.days(332)
.years(11)
.months(2)
.days(2)
.hours(20)
.minutes(20)
.seconds(20)
Expand Down Expand Up @@ -397,12 +393,12 @@ public void testEncodeInterval(TestContext ctx) {
PgConnection.connect(vertx, options, ctx.asyncAssertSuccess(conn -> {
conn.prepare("UPDATE \"TemporalDataType\" SET \"Interval\" = $1 WHERE \"id\" = $2 RETURNING \"Interval\"",
ctx.asyncAssertSuccess(p -> {
// 2000 years 1 months 403 days 59 hours 35 minutes 13.999998 seconds
// 2001 years 2 months 15 days 11 hours 35 minutes 13.999998 seconds
Interval expected = Interval.of()
.years(2000)
.months(1)
.days(403)
.hours(59)
.years(2001)
.months(2)
.days(15)
.hours(11)
.minutes(35)
.seconds(13)
.microseconds(999998);
Expand Down Expand Up @@ -568,7 +564,7 @@ public void testEncodeIntervalArray(TestContext ctx) {
conn.prepare("UPDATE \"ArrayDataType\" SET \"Interval\" = $1 WHERE \"id\" = $2 RETURNING \"Interval\"",
ctx.asyncAssertSuccess(p -> {
Interval[] intervals = new Interval[]{
Interval.of().years(10).months(3).days(332).hours(20).minutes(20).seconds(20).microseconds(999991),
Interval.of().years(11).months(2).days(2).hours(20).minutes(20).seconds(20).microseconds(999991),
Interval.of().minutes(20).seconds(20).microseconds(123456),
Interval.of().years(-2).months(-6),
Interval.of()
Expand Down
Loading
Loading