[jOOQ/jOOQ#18905] PostgreSQL UDT array not deserialized correctly when

embedded in multiset
This commit is contained in:
Lukas Eder 2025-08-26 11:21:38 +02:00
parent eaae21f47e
commit da91884b57
3 changed files with 75 additions and 8 deletions

View File

@ -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<T> extends DefaultDataType<T[]> {
);
}
@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());

View File

@ -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<R extends Record> extends DefaultDataType<R> {
@SuppressWarnings("unchecked")
@Override
final R convert(Object object, ConverterContext cc) {
return convert0(object, cc, getRecordType(), row, super::convert);
}
static final <R extends Record> R convert0(
Object object,
ConverterContext cc,
Class<? extends R> recordType,
AbstractRow<R> row,
Function2<? super Object, ? super ConverterContext, ? extends R> 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<R extends Record> extends DefaultDataType<R> {
|| 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<String, ?>) 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<String, ?>) map);
else
r.from(((Map<String, ?>) object).entrySet().stream().sorted(comparing(Entry::getKey)).map(Entry::getValue).collect(toList()));
}
else
r.from(object);
@ -189,6 +210,6 @@ final class RecordDataType<R extends Record> extends DefaultDataType<R> {
});
}
else
return super.convert(object, cc);
return delegate.apply(object, cc);
}
}

View File

@ -68,7 +68,21 @@ final class UDTDataType<R extends UDTRecord<R>> extends DefaultDataType<R> {
}
@Override
public final Class<? extends Record> getRecordType() {
public final Class<? extends R> 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<R>) getRow(),
super::convert
);
}
}