From f5d19372384378c381f1a2e7bbd1105ca41843ad Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Mon, 21 Feb 2022 15:27:31 +0100 Subject: [PATCH] [jOOQ/jOOQ#13117] Bad deserialisation of nested record TIMESTAMP value in PostgreSQL --- jOOQ/src/main/java/org/jooq/impl/Convert.java | 34 ++++---- .../java/org/jooq/impl/DefaultBinding.java | 77 ++++++++++--------- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/impl/Convert.java b/jOOQ/src/main/java/org/jooq/impl/Convert.java index 048b753267..fa4d208279 100644 --- a/jOOQ/src/main/java/org/jooq/impl/Convert.java +++ b/jOOQ/src/main/java/org/jooq/impl/Convert.java @@ -1377,23 +1377,6 @@ final class Convert { }; } - private static final String patchIso8601Time(String string) { - // [#12158] Support Db2's 15.30.45 format - return string.length() == 8 - ? string.replace('.', ':') - : string; - } - - private static final String patchIso8601Timestamp(String string, boolean t) { - if (string.length() > 11) - if (t && string.charAt(10) == ' ') - return string.substring(0, 10) + "T" + string.substring(11); - else if (!t && string.charAt(10) == 'T') - return string.substring(0, 10) + " " + string.substring(11); - - return string; - } - @Override public Object to(U to) { return to; @@ -1490,4 +1473,21 @@ final class Convert { return new DataTypeException(message); } } + + static final String patchIso8601Time(String string) { + // [#12158] Support Db2's 15.30.45 format + return string.length() == 8 + ? string.replace('.', ':') + : string; + } + + static final String patchIso8601Timestamp(String string, boolean t) { + if (string.length() > 11) + if (t && string.charAt(10) == ' ') + return string.substring(0, 10) + "T" + string.substring(11); + else if (!t && string.charAt(10) == 'T') + return string.substring(0, 10) + " " + string.substring(11); + + return string; + } } diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java index 440a0a341b..222f2ef54d 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java @@ -84,6 +84,7 @@ import static org.jooq.SQLDialect.SQLITE; import static org.jooq.SQLDialect.YUGABYTEDB; import static org.jooq.conf.ParamType.INLINED; import static org.jooq.impl.Convert.convert; +import static org.jooq.impl.Convert.patchIso8601Timestamp; import static org.jooq.impl.DSL.cast; import static org.jooq.impl.DSL.field; import static org.jooq.impl.DSL.inline; @@ -158,6 +159,7 @@ import static org.jooq.tools.StringUtils.leftPad; import static org.jooq.tools.jdbc.JDBCUtils.safeFree; import static org.jooq.tools.jdbc.JDBCUtils.wasNull; import static org.jooq.tools.reflect.Reflect.onClass; +import static org.jooq.tools.reflect.Reflect.wrapper; import static org.jooq.util.postgres.PostgresUtils.toPGArray; import static org.jooq.util.postgres.PostgresUtils.toPGArrayString; import static org.jooq.util.postgres.PostgresUtils.toPGInterval; @@ -3800,92 +3802,91 @@ public class DefaultBinding implements Binding { } @SuppressWarnings("unchecked") - private static final T pgFromString(Scope ctx, Field field, String string) { - Converter converter = field.getConverter(); - Class type = Reflect.wrapper(converter.toType()); + private static final U pgFromString(Scope ctx, Field field, String string) { + Converter converter = (Converter) field.getConverter(); + Class type = wrapper(converter.fromType()); if (string == null) return null; else if (type == Blob.class) ; // Not supported else if (type == Boolean.class) - return (T) Convert.convert(string, Boolean.class); + return converter.from((T) Convert.convert(string, Boolean.class)); else if (type == BigInteger.class) - return (T) new BigInteger(string); + return converter.from((T) new BigInteger(string)); else if (type == BigDecimal.class) - return (T) new BigDecimal(string); + return converter.from((T) new BigDecimal(string)); else if (type == Byte.class) - return (T) Byte.valueOf(string); + return converter.from((T) Byte.valueOf(string)); else if (type == byte[].class) - return (T) PostgresUtils.toBytes(string); + return converter.from((T) PostgresUtils.toBytes(string)); else if (type == Clob.class) ; // Not supported else if (type == Date.class) - return (T) Date.valueOf(string); + return converter.from((T) Date.valueOf(string)); else if (type == Double.class) - return (T) Double.valueOf(string); + return converter.from((T) Double.valueOf(string)); else if (type == Float.class) - return (T) Float.valueOf(string); + return converter.from((T) Float.valueOf(string)); else if (type == Integer.class) - return (T) Integer.valueOf(string); + return converter.from((T) Integer.valueOf(string)); else if (type == Long.class) - return (T) Long.valueOf(string); + return converter.from((T) Long.valueOf(string)); else if (type == Short.class) - return (T) Short.valueOf(string); + return converter.from((T) Short.valueOf(string)); else if (type == String.class) - return (T) string; + return converter.from((T) string); else if (type == Time.class) - return (T) Time.valueOf(string); + return converter.from((T) Time.valueOf(string)); else if (type == Timestamp.class) - return (T) Timestamp.valueOf(string); + return converter.from((T) Timestamp.valueOf(patchIso8601Timestamp(string, false))); else if (type == LocalTime.class) - return (T) LocalTime.parse(string); + return converter.from((T) LocalTime.parse(string)); else if (type == LocalDate.class) - return (T) LocalDate.parse(string); + return converter.from((T) LocalDate.parse(string)); else if (type == LocalDateTime.class) - return (T) LocalDateTime.parse(string); + return converter.from((T) LocalDateTime.parse(patchIso8601Timestamp(string, true))); else if (type == OffsetTime.class) - return (T) OffsetDateTimeParser.offsetTime(string); + return converter.from((T) OffsetDateTimeParser.offsetTime(string)); else if (type == OffsetDateTime.class) - return (T) OffsetDateTimeParser.offsetDateTime(string); + return converter.from((T) OffsetDateTimeParser.offsetDateTime(string)); else if (type == Instant.class) - return (T) OffsetDateTimeParser.offsetDateTime(string).toInstant(); + return converter.from((T) OffsetDateTimeParser.offsetDateTime(string).toInstant()); else if (type == UByte.class) - return (T) UByte.valueOf(string); + return converter.from((T) UByte.valueOf(string)); else if (type == UShort.class) - return (T) UShort.valueOf(string); + return converter.from((T) UShort.valueOf(string)); else if (type == UInteger.class) - return (T) UInteger.valueOf(string); + return converter.from((T) UInteger.valueOf(string)); else if (type == ULong.class) - return (T) ULong.valueOf(string); + return converter.from((T) ULong.valueOf(string)); else if (type == UUID.class) - return (T) UUID.fromString(string); + return converter.from((T) UUID.fromString(string)); else if (type.isArray()) - return (T) pgNewArray(ctx, field, type, string); + return converter.from((T) pgNewArray(ctx, field, type, string)); else if (EnumType.class.isAssignableFrom(type)) - return (T) DefaultEnumTypeBinding.getEnumType((Class) type, string); + return converter.from((T) DefaultEnumTypeBinding.getEnumType((Class) type, string)); else if (Result.class.isAssignableFrom(type)) if (string.startsWith("<")) - return (T) readMultisetXML(ctx, (AbstractRow) field.getDataType().getRow(), (Class) field.getDataType().getRecordType(), string); + return converter.from((T) readMultisetXML(ctx, (AbstractRow) field.getDataType().getRow(), (Class) field.getDataType().getRecordType(), string)); else - return (T) readMultisetJSON(ctx, (AbstractRow) field.getDataType().getRow(), (Class) field.getDataType().getRecordType(), string); + return converter.from((T) readMultisetJSON(ctx, (AbstractRow) field.getDataType().getRow(), (Class) field.getDataType().getRecordType(), string)); else if (Record.class.isAssignableFrom(type) // [#11812] UDTRecords/TableRecords or InternalRecords that don't have an explicit converter && (!InternalRecord.class.isAssignableFrom(type) || type == converter.fromType())) - return (T) pgNewRecord(ctx, type, (AbstractRow) field.getDataType().getRow(), string); + return converter.from((T) pgNewRecord(ctx, type, (AbstractRow) field.getDataType().getRow(), string)); else if (type == Object.class) - return (T) string; + return converter.from((T) string); // [#4964] [#6058] Recurse only if we have a meaningful converter, not the identity converter, // which would cause a StackOverflowError, here! - else if (type != converter.fromType()) { - Converter c = (Converter) converter; - return c.from(pgFromString(ctx, field("converted_field", ((ConvertedDataType) field.getDataType()).delegate()), string)); + else if (type != wrapper(converter.toType())) { + return converter.from((T) pgFromString(ctx, field("converted_field", ((ConvertedDataType) field.getDataType()).delegate()), string)); } throw new UnsupportedOperationException("Class " + type + " is not supported"); @@ -3953,7 +3954,7 @@ public class DefaultBinding implements Binding { try { return Tools.map( toPGArray(string), - v -> pgFromString(ctx, field("array_element", field.getDataType().getArrayComponentDataType()), v), + v -> pgFromString(ctx, field("array_element", type.getComponentType()), v), size -> (Object[]) java.lang.reflect.Array.newInstance(type.getComponentType(), size) ); }