From da91884b572f3e7e66a2ee9ee84b934dd1d9cbb6 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Tue, 26 Aug 2025 11:21:38 +0200 Subject: [PATCH] [jOOQ/jOOQ#18905] PostgreSQL UDT array not deserialized correctly when embedded in multiset --- .../java/org/jooq/impl/ArrayDataType.java | 32 +++++++++++++++++ .../java/org/jooq/impl/RecordDataType.java | 35 +++++++++++++++---- .../main/java/org/jooq/impl/UDTDataType.java | 16 ++++++++- 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java b/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java index ec6c70b8a3..2029c0bec7 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java @@ -39,9 +39,14 @@ package org.jooq.impl; import static org.jooq.impl.Tools.CONFIG; +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.List; + import org.jooq.CharacterSet; import org.jooq.Collation; import org.jooq.Configuration; +import org.jooq.ConverterContext; import org.jooq.DataType; import org.jooq.Field; import org.jooq.Generator; @@ -128,6 +133,33 @@ final class ArrayDataType extends DefaultDataType { ); } + @SuppressWarnings("unchecked") + @Override + final T[] convert(Object object, ConverterContext cc) { + + // [#1441] Avoid unneeded type conversions to improve performance + if (object == null) + return null; + else if (object.getClass() == getType()) + return (T[]) object; + + Object[] array = + object instanceof Object[] ? (Object[]) object + : object instanceof Collection ? ((Collection) object).toArray() + : null; + + if (array != null && elementType.getType() != Object.class) { + T[] result = (T[]) Array.newInstance(elementType.getType(), array.length); + + for (int i = 0; i < array.length; i++) + result[i] = convert0(elementType, array[i], cc); + + return result; + } + else + return super.convert(object, cc); + } + @Override public final String getTypeName() { return getTypeName(CONFIG.get()); diff --git a/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java b/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java index 5ef4c75650..75c900d970 100644 --- a/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java @@ -39,7 +39,7 @@ package org.jooq.impl; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; -import static org.jooq.impl.Tools.CONFIG; +import static org.jooq.impl.Tools.allMatch; import static org.jooq.impl.Tools.newRecord; import static org.jooq.impl.Tools.recordType; @@ -52,13 +52,17 @@ import org.jooq.CharacterSet; import org.jooq.Collation; import org.jooq.ConverterContext; import org.jooq.Field; +import org.jooq.Function2; import org.jooq.Generator; import org.jooq.Nullability; +import org.jooq.QualifiedRecord; import org.jooq.Record; import org.jooq.Row; import org.jooq.impl.QOM.GenerationLocation; import org.jooq.impl.QOM.GenerationOption; +import org.jetbrains.annotations.Nullable; + /** * A wrapper for anonymous row data types. * @@ -160,6 +164,16 @@ final class RecordDataType extends DefaultDataType { @SuppressWarnings("unchecked") @Override final R convert(Object object, ConverterContext cc) { + return convert0(object, cc, getRecordType(), row, super::convert); + } + + static final R convert0( + Object object, + ConverterContext cc, + Class recordType, + AbstractRow row, + Function2 delegate + ) { // [#12269] [#13403] Don't re-copy perfectly fine results. if (object instanceof Record && ((Record) object).fieldsRow().equals(row)) @@ -172,16 +186,23 @@ final class RecordDataType extends DefaultDataType { || object instanceof List || object instanceof Struct ) { - return newRecord(true, cc.configuration(), getRecordType(), row) + return newRecord(true, cc.configuration(), recordType, row) .operate(r -> { // [#12014] TODO: Fix this and remove workaround - if (object instanceof Record) + if (object instanceof Record) { ((AbstractRecord) r).fromArray(((Record) object).intoArray()); + } - // This sort is required if we use the JSONFormat.RecordFormat.OBJECT encoding (e.g. in SQL Server) - else if (object instanceof Map) - r.from(((Map) object).entrySet().stream().sorted(comparing(Entry::getKey)).map(Entry::getValue).collect(toList())); + // [#18681] [#18905] The Map encoding of nested ROW values can happen for 2 reasons: + // - We create it ourselves with "v1", "v2", ... keys because json objects work better than arrays in some RDBMS + // - It's a UDT or similar, serialised into a JSON object, where keys are attribute names, not in order! + else if (object instanceof Map map) { + if (QualifiedRecord.class.isAssignableFrom(recordType) && allMatch(row.fields.fields, f -> map.containsKey(f.getName()))) + r.fromMap((Map) map); + else + r.from(((Map) object).entrySet().stream().sorted(comparing(Entry::getKey)).map(Entry::getValue).collect(toList())); + } else r.from(object); @@ -189,6 +210,6 @@ final class RecordDataType extends DefaultDataType { }); } else - return super.convert(object, cc); + return delegate.apply(object, cc); } } diff --git a/jOOQ/src/main/java/org/jooq/impl/UDTDataType.java b/jOOQ/src/main/java/org/jooq/impl/UDTDataType.java index 326186ecdb..837f96ba80 100644 --- a/jOOQ/src/main/java/org/jooq/impl/UDTDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/UDTDataType.java @@ -68,7 +68,21 @@ final class UDTDataType> extends DefaultDataType { } @Override - public final Class getRecordType() { + public final Class getRecordType() { return udt.getRecordType(); } + + @SuppressWarnings("unchecked") + @Override + final R convert(Object object, ConverterContext cc) { + + // [#18905] Re-use the untyped record conversion logic, mostly for MULTISET usage + return RecordDataType.convert0( + object, + cc, + getRecordType(), + (AbstractRow) getRow(), + super::convert + ); + } }