Add IntervalDescriptor and change DurationConverter to use Interval datatype (#653)

* Add use of interval data type

* Add support for Millis

* Use Java-object type

* Change column type for relock_duration

* add years and months

* Add tests for hours, minutes, and seconds

* Add javadoc describing how joda duration is stored

* Add test for lots of days
This commit is contained in:
sarahcaseybot 2020-08-03 12:42:34 -04:00 committed by GitHub
parent 0e6bc91861
commit 4a2215e88d
14 changed files with 248 additions and 31 deletions

View file

@ -258,7 +258,7 @@ dependencies {
compile deps['org.joda:joda-money'] compile deps['org.joda:joda-money']
compile deps['org.json:json'] compile deps['org.json:json']
testCompile deps['org.mortbay.jetty:jetty'] testCompile deps['org.mortbay.jetty:jetty']
runtimeOnly deps['org.postgresql:postgresql'] compile deps['org.postgresql:postgresql']
testCompile deps['org.seleniumhq.selenium:selenium-api'] testCompile deps['org.seleniumhq.selenium:selenium-api']
testCompile deps['org.seleniumhq.selenium:selenium-chrome-driver'] testCompile deps['org.seleniumhq.selenium:selenium-chrome-driver']
testCompile deps['org.seleniumhq.selenium:selenium-java'] testCompile deps['org.seleniumhq.selenium:selenium-java']

View file

@ -228,6 +228,7 @@ org.ow2.asm:asm-commons:7.1
org.ow2.asm:asm-tree:8.0.1 org.ow2.asm:asm-tree:8.0.1
org.ow2.asm:asm-util:8.0.1 org.ow2.asm:asm-util:8.0.1
org.ow2.asm:asm:8.0.1 org.ow2.asm:asm:8.0.1
org.postgresql:postgresql:42.2.14
org.rnorth.duct-tape:duct-tape:1.0.8 org.rnorth.duct-tape:duct-tape:1.0.8
org.rnorth.visible-assertions:visible-assertions:2.1.2 org.rnorth.visible-assertions:visible-assertions:2.1.2
org.rnorth:tcp-unix-socket-proxy:1.0.2 org.rnorth:tcp-unix-socket-proxy:1.0.2

View file

@ -223,6 +223,7 @@ org.ow2.asm:asm-commons:7.1
org.ow2.asm:asm-tree:8.0.1 org.ow2.asm:asm-tree:8.0.1
org.ow2.asm:asm-util:8.0.1 org.ow2.asm:asm-util:8.0.1
org.ow2.asm:asm:8.0.1 org.ow2.asm:asm:8.0.1
org.postgresql:postgresql:42.2.14
org.rnorth.duct-tape:duct-tape:1.0.8 org.rnorth.duct-tape:duct-tape:1.0.8
org.rnorth.visible-assertions:visible-assertions:2.1.2 org.rnorth.visible-assertions:visible-assertions:2.1.2
org.rnorth:tcp-unix-socket-proxy:1.0.2 org.rnorth:tcp-unix-socket-proxy:1.0.2

View file

@ -228,6 +228,7 @@ org.ow2.asm:asm-commons:7.1
org.ow2.asm:asm-tree:8.0.1 org.ow2.asm:asm-tree:8.0.1
org.ow2.asm:asm-util:8.0.1 org.ow2.asm:asm-util:8.0.1
org.ow2.asm:asm:8.0.1 org.ow2.asm:asm:8.0.1
org.postgresql:postgresql:42.2.14
org.rnorth.duct-tape:duct-tape:1.0.8 org.rnorth.duct-tape:duct-tape:1.0.8
org.rnorth.visible-assertions:visible-assertions:2.1.2 org.rnorth.visible-assertions:visible-assertions:2.1.2
org.rnorth:tcp-unix-socket-proxy:1.0.2 org.rnorth:tcp-unix-socket-proxy:1.0.2

View file

@ -226,6 +226,7 @@ org.ow2.asm:asm-commons:7.1
org.ow2.asm:asm-tree:8.0.1 org.ow2.asm:asm-tree:8.0.1
org.ow2.asm:asm-util:8.0.1 org.ow2.asm:asm-util:8.0.1
org.ow2.asm:asm:8.0.1 org.ow2.asm:asm:8.0.1
org.postgresql:postgresql:42.2.14
org.rnorth.duct-tape:duct-tape:1.0.8 org.rnorth.duct-tape:duct-tape:1.0.8
org.rnorth.visible-assertions:visible-assertions:2.1.2 org.rnorth.visible-assertions:visible-assertions:2.1.2
org.rnorth:tcp-unix-socket-proxy:1.0.2 org.rnorth:tcp-unix-socket-proxy:1.0.2

View file

@ -266,6 +266,7 @@ org.ow2.asm:asm-commons:7.1
org.ow2.asm:asm-tree:8.0.1 org.ow2.asm:asm-tree:8.0.1
org.ow2.asm:asm-util:8.0.1 org.ow2.asm:asm-util:8.0.1
org.ow2.asm:asm:8.0.1 org.ow2.asm:asm:8.0.1
org.postgresql:postgresql:42.2.14
org.rnorth.duct-tape:duct-tape:1.0.8 org.rnorth.duct-tape:duct-tape:1.0.8
org.rnorth.visible-assertions:visible-assertions:2.1.2 org.rnorth.visible-assertions:visible-assertions:2.1.2
org.rnorth:tcp-unix-socket-proxy:1.0.2 org.rnorth:tcp-unix-socket-proxy:1.0.2

View file

@ -264,6 +264,7 @@ org.ow2.asm:asm-commons:7.1
org.ow2.asm:asm-tree:8.0.1 org.ow2.asm:asm-tree:8.0.1
org.ow2.asm:asm-util:8.0.1 org.ow2.asm:asm-util:8.0.1
org.ow2.asm:asm:8.0.1 org.ow2.asm:asm:8.0.1
org.postgresql:postgresql:42.2.14
org.rnorth.duct-tape:duct-tape:1.0.8 org.rnorth.duct-tape:duct-tape:1.0.8
org.rnorth.visible-assertions:visible-assertions:2.1.2 org.rnorth.visible-assertions:visible-assertions:2.1.2
org.rnorth:tcp-unix-socket-proxy:1.0.2 org.rnorth:tcp-unix-socket-proxy:1.0.2

View file

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
package google.registry.persistence; package google.registry.persistence;
import google.registry.persistence.converter.IntervalDescriptor;
import google.registry.persistence.converter.StringCollectionDescriptor; import google.registry.persistence.converter.StringCollectionDescriptor;
import google.registry.persistence.converter.StringMapDescriptor; import google.registry.persistence.converter.StringMapDescriptor;
import java.sql.Types; import java.sql.Types;
@ -30,6 +31,7 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
registerColumnType(StringMapDescriptor.COLUMN_TYPE, StringMapDescriptor.COLUMN_NAME); registerColumnType(StringMapDescriptor.COLUMN_TYPE, StringMapDescriptor.COLUMN_NAME);
registerColumnType( registerColumnType(
StringCollectionDescriptor.COLUMN_TYPE, StringCollectionDescriptor.COLUMN_DDL_NAME); StringCollectionDescriptor.COLUMN_TYPE, StringCollectionDescriptor.COLUMN_DDL_NAME);
registerColumnType(IntervalDescriptor.COLUMN_TYPE, IntervalDescriptor.COLUMN_NAME);
} }
@Override @Override
@ -40,5 +42,7 @@ public class NomulusPostgreSQLDialect extends PostgreSQL95Dialect {
typeContributions.contributeSqlTypeDescriptor(StringCollectionDescriptor.getInstance()); typeContributions.contributeSqlTypeDescriptor(StringCollectionDescriptor.getInstance());
typeContributions.contributeJavaTypeDescriptor(StringMapDescriptor.getInstance()); typeContributions.contributeJavaTypeDescriptor(StringMapDescriptor.getInstance());
typeContributions.contributeSqlTypeDescriptor(StringMapDescriptor.getInstance()); typeContributions.contributeSqlTypeDescriptor(StringMapDescriptor.getInstance());
typeContributions.contributeJavaTypeDescriptor(IntervalDescriptor.getInstance());
typeContributions.contributeSqlTypeDescriptor(IntervalDescriptor.getInstance());
} }
} }

View file

@ -14,24 +14,67 @@
package google.registry.persistence.converter; package google.registry.persistence.converter;
import java.sql.SQLException;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.persistence.AttributeConverter; import javax.persistence.AttributeConverter;
import javax.persistence.Converter; import javax.persistence.Converter;
import org.joda.time.Duration; import org.joda.time.Duration;
import org.joda.time.Period;
import org.postgresql.util.PGInterval;
/** JPA converter to for storing/retrieving {@link org.joda.time.DateTime} objects. */ /**
* JPA converter to for storing/retrieving {@link org.joda.time.Duration} objects.
*
* <p>The Joda Time Duration is simply a number of milliseconds representing a length of time. This
* can be converted into a PGInterval, but only for the fields that have a standard number of
* milliseconds. Therefore, there is no way to populate the months or years field of a PGInterval
* and be confident that it is representing the exact number of milliseconds it was intended to
* represent.
*/
@Converter(autoApply = true) @Converter(autoApply = true)
public class DurationConverter implements AttributeConverter<Duration, Long> { public class DurationConverter implements AttributeConverter<Duration, PGInterval> {
@Override @Override
@Nullable @Nullable
public Long convertToDatabaseColumn(@Nullable Duration duration) { public PGInterval convertToDatabaseColumn(@Nullable Duration duration) {
return duration == null ? null : duration.getMillis(); if (duration == null) {
return new PGInterval();
}
PGInterval interval = new PGInterval();
Period period = new Period(duration);
// For some reason when the period is created from the duration, it does not set days, but
// instead just a total number of hours. Years and months are not created because those can
// differ in length of milliseconds.
interval.setDays(period.getHours() / 24);
interval.setHours(period.getHours() % 24);
interval.setMinutes(period.getMinutes());
double millis = (double) period.getMillis() / 1000;
interval.setSeconds(period.getSeconds() + millis);
return interval;
} }
@Override @Override
@Nullable @Nullable
public Duration convertToEntityAttribute(@Nullable Long dbData) { public Duration convertToEntityAttribute(@Nullable PGInterval dbData) {
return dbData == null ? null : new Duration(dbData); if (dbData == null) {
return null;
}
PGInterval interval = null;
try {
interval = new PGInterval(dbData.toString());
} catch (SQLException e) {
throw new RuntimeException(e);
}
if (interval.equals(new PGInterval())) {
return null;
}
final int days = interval.getDays();
final int hours = interval.getHours();
final int mins = interval.getMinutes();
final int secs = (int) interval.getSeconds();
final int millis = interval.getMicroSeconds() / 1000;
return new Period(0, 0, 0, days, hours, mins, secs, millis).toStandardDuration();
} }
} }

View file

@ -0,0 +1,139 @@
// Copyright 2020 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence.converter;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.AbstractTypeDescriptor;
import org.hibernate.type.descriptor.java.JavaTypeDescriptor;
import org.hibernate.type.descriptor.spi.JdbcRecommendedSqlTypeMappingContext;
import org.hibernate.type.descriptor.sql.BasicBinder;
import org.hibernate.type.descriptor.sql.BasicExtractor;
import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
import org.postgresql.util.PGInterval;
/**
* The {@link JavaTypeDescriptor} and {@link SqlTypeDescriptor} for {@link PGInterval}.
*
* @see <a
* href="https://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#basic-jpa-convert">JPA
* 2.1 AttributeConverters</a>
*/
public class IntervalDescriptor extends AbstractTypeDescriptor<PGInterval>
implements SqlTypeDescriptor {
public static final int COLUMN_TYPE = Types.JAVA_OBJECT;
public static final String COLUMN_NAME = "interval";
private static final IntervalDescriptor INSTANCE = new IntervalDescriptor();
private IntervalDescriptor() {
super(PGInterval.class);
}
public static IntervalDescriptor getInstance() {
return INSTANCE;
}
@Override
public PGInterval fromString(String string) {
throw new UnsupportedOperationException(
"Constructing IntervalDescriptor from string is not allowed");
}
@Override
public <X> X unwrap(PGInterval value, Class<X> type, WrapperOptions options) {
if (value == null) {
return null;
}
if (PGInterval.class.isAssignableFrom(type)) {
return (X) value;
}
throw unknownUnwrap(type);
}
@Override
public <X> PGInterval wrap(X value, WrapperOptions options) {
if (value == null) {
return null;
}
if (value instanceof PGInterval) {
try {
return new PGInterval(value.toString());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
throw unknownWrap(value.getClass());
}
@Override
public int getSqlType() {
return COLUMN_TYPE;
}
@Override
public SqlTypeDescriptor getJdbcRecommendedSqlType(JdbcRecommendedSqlTypeMappingContext context) {
return this;
}
@Override
public boolean canBeRemapped() {
return false;
}
@Override
public <X> ValueBinder<X> getBinder(JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicBinder<X>(javaTypeDescriptor, this) {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options)
throws SQLException {
st.setObject(index, new PGInterval(value.toString()));
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options)
throws SQLException {
st.setObject(name, new PGInterval(value.toString()));
}
};
}
@Override
public <X> ValueExtractor<X> getExtractor(JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicExtractor<X>(javaTypeDescriptor, this) {
@Override
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap(rs.getObject(name), options);
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options)
throws SQLException {
return javaTypeDescriptor.wrap(statement.getObject(index), options);
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options)
throws SQLException {
return javaTypeDescriptor.wrap(statement.getObject(name), options);
}
};
}
}

View file

@ -21,58 +21,65 @@ import google.registry.model.ImmutableObject;
import google.registry.persistence.transaction.JpaTestRules; import google.registry.persistence.transaction.JpaTestRules;
import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestExtension; import google.registry.persistence.transaction.JpaTestRules.JpaUnitTestExtension;
import google.registry.schema.replay.EntityTest.EntityForTesting; import google.registry.schema.replay.EntityTest.EntityForTesting;
import java.math.BigInteger;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import org.joda.time.Duration; import org.joda.time.Duration;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.postgresql.util.PGInterval;
/** Unit tests for {@link DurationConverter}. */ /** Unit tests for {@link DurationConverter}. */
public class DurationConverterTest { public class DurationConverterTest {
@RegisterExtension @RegisterExtension
public final JpaUnitTestExtension jpaExtension = public final JpaUnitTestExtension jpaExtension =
new JpaTestRules.Builder().withEntityClass(TestEntity.class).buildUnitTestRule(); new JpaTestRules.Builder().withEntityClass(DurationTestEntity.class).buildUnitTestRule();
private final DurationConverter converter = new DurationConverter(); private final DurationConverter converter = new DurationConverter();
@Test @Test
void testNulls() { public void testNulls() {
assertThat(converter.convertToDatabaseColumn(null)).isNull(); assertThat(converter.convertToDatabaseColumn(null)).isEqualTo(new PGInterval());
assertThat(converter.convertToEntityAttribute(null)).isNull(); assertThat(converter.convertToEntityAttribute(new PGInterval())).isNull();
} }
@Test @Test
void testRoundTrip() { void testRoundTrip() {
TestEntity entity = new TestEntity(Duration.standardDays(6)); Duration testDuration =
Duration.standardDays(6)
.plus(Duration.standardHours(10))
.plus(Duration.standardMinutes(30))
.plus(Duration.standardSeconds(15))
.plus(Duration.millis(7));
DurationTestEntity entity = new DurationTestEntity(testDuration);
jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity)); jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity));
assertThat( DurationTestEntity persisted =
jpaTm() jpaTm().transact(() -> jpaTm().getEntityManager().find(DurationTestEntity.class, "id"));
.transact( assertThat(persisted.duration.getMillis()).isEqualTo(testDuration.getMillis());
() -> }
jpaTm()
.getEntityManager() @Test
.createNativeQuery( void testRoundTripLargeNumberOfDays() {
"SELECT duration FROM \"TestEntity\" WHERE name = 'id'") Duration testDuration =
.getResultList())) Duration.standardDays(10001).plus(Duration.standardHours(100)).plus(Duration.millis(790));
.containsExactly(BigInteger.valueOf(Duration.standardDays(6).getMillis())); DurationTestEntity entity = new DurationTestEntity(testDuration);
TestEntity persisted = jpaTm().transact(() -> jpaTm().getEntityManager().persist(entity));
jpaTm().transact(() -> jpaTm().getEntityManager().find(TestEntity.class, "id")); DurationTestEntity persisted =
assertThat(persisted.duration).isEqualTo(Duration.standardDays(6)); jpaTm().transact(() -> jpaTm().getEntityManager().find(DurationTestEntity.class, "id"));
assertThat(persisted.duration.getMillis()).isEqualTo(testDuration.getMillis());
} }
@Entity(name = "TestEntity") // Override entity name to avoid the nested class reference. @Entity(name = "TestEntity") // Override entity name to avoid the nested class reference.
@EntityForTesting @EntityForTesting
public static class TestEntity extends ImmutableObject { public static class DurationTestEntity extends ImmutableObject {
@Id String name = "id"; @Id String name = "id";
Duration duration; Duration duration;
public TestEntity() {} public DurationTestEntity() {}
TestEntity(Duration duration) { DurationTestEntity(Duration duration) {
this.duration = duration; this.duration = duration;
} }
} }

View file

@ -0,0 +1,18 @@
-- Copyright 2020 The Nomulus Authors. All Rights Reserved.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
ALTER TABLE "RegistryLock" DROP COLUMN relock_duration;
ALTER TABLE "RegistryLock" ADD COLUMN relock_duration interval;

View file

@ -459,7 +459,7 @@ create sequence history_id_sequence start 1 increment 1;
lock_request_timestamp timestamptz not null, lock_request_timestamp timestamptz not null,
registrar_id text not null, registrar_id text not null,
registrar_poc_id text, registrar_poc_id text,
relock_duration int8, relock_duration interval,
repo_id text not null, repo_id text not null,
unlock_completion_timestamp timestamptz, unlock_completion_timestamp timestamptz,
unlock_request_timestamp timestamptz, unlock_request_timestamp timestamptz,

View file

@ -670,7 +670,7 @@ CREATE TABLE public."RegistryLock" (
unlock_completion_timestamp timestamp with time zone, unlock_completion_timestamp timestamp with time zone,
last_update_timestamp timestamp with time zone, last_update_timestamp timestamp with time zone,
relock_revision_id bigint, relock_revision_id bigint,
relock_duration bigint relock_duration interval
); );