From f2580592dd657d9d3411fbdd7e814219126644f5 Mon Sep 17 00:00:00 2001 From: Lukas Eder Date: Mon, 3 May 2021 13:07:57 +0200 Subject: [PATCH] [jOOQ/jOOQ#11829] Add DataType.getRow() to track nested ROW types and allow access to their Field[] --- jOOQ/src/main/java/org/jooq/DataType.java | 17 +++ .../java/org/jooq/impl/AbstractDataType.java | 15 +++ .../java/org/jooq/impl/ArrayDataType.java | 6 +- .../java/org/jooq/impl/ConvertedDataType.java | 16 ++- .../java/org/jooq/impl/DataTypeProxy.java | 5 + .../java/org/jooq/impl/DefaultBinding.java | 38 +++--- .../java/org/jooq/impl/DefaultDataType.java | 29 +++-- .../java/org/jooq/impl/RecordDataType.java | 120 ++++++++++++++++++ .../src/main/java/org/jooq/impl/RowField.java | 3 +- 9 files changed, 212 insertions(+), 37 deletions(-) create mode 100644 jOOQ/src/main/java/org/jooq/impl/RecordDataType.java diff --git a/jOOQ/src/main/java/org/jooq/DataType.java b/jOOQ/src/main/java/org/jooq/DataType.java index 9b3f23ff47..880894959e 100644 --- a/jOOQ/src/main/java/org/jooq/DataType.java +++ b/jOOQ/src/main/java/org/jooq/DataType.java @@ -75,6 +75,7 @@ import java.util.function.Function; import org.jooq.Converters.UnknownType; import org.jooq.exception.DataTypeException; +import org.jooq.impl.DSL; import org.jooq.impl.SQLDataType; import org.jooq.tools.Convert; import org.jooq.types.DayToSecond; @@ -148,6 +149,13 @@ public interface DataType extends Named { @Nullable Domain getDomain(); + /** + * Get the nested record's {@link Row} definition, if this is a + * {@link #isRecord()}, or NULL otherwise. + */ + @Nullable + Row getRow(); + /** * Retrieve the Java type associated with ARRAYs of this data type. */ @@ -889,6 +897,15 @@ public interface DataType extends Named { */ boolean isUDT(); + /** + * Whether this data type is a nested record type. + *

+ * This is true for anonymous, structural nested record types constructed + * with {@link DSL#row(SelectField...)} or for nominal nested record types, + * such as {@link #isUDT()} or {@link #isEmbeddable()}. + */ + boolean isRecord(); + /** * Whether this data type is an enum type. */ diff --git a/jOOQ/src/main/java/org/jooq/impl/AbstractDataType.java b/jOOQ/src/main/java/org/jooq/impl/AbstractDataType.java index a02ce7ea83..ab2287fdad 100644 --- a/jOOQ/src/main/java/org/jooq/impl/AbstractDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/AbstractDataType.java @@ -85,13 +85,17 @@ import org.jooq.Name; import org.jooq.Nullability; // ... import org.jooq.QualifiedRecord; +import org.jooq.RecordType; import org.jooq.Result; +import org.jooq.Row; import org.jooq.SQLDialect; import org.jooq.XML; import org.jooq.tools.Convert; import org.jooq.types.Interval; import org.jooq.types.UNumber; +import org.jetbrains.annotations.Nullable; + /** * @author Lukas Eder */ @@ -478,6 +482,11 @@ abstract class AbstractDataType extends AbstractNamed implements DataType return null; } + @Override + public /* non-final */ Row getRow() { + return null; + } + @@ -646,6 +655,11 @@ abstract class AbstractDataType extends AbstractNamed implements DataType return QualifiedRecord.class.isAssignableFrom(tType0()); } + @Override + public final boolean isRecord() { + return Record.class.isAssignableFrom(tType0()); + } + @Override public final boolean isEnum() { return EnumType.class.isAssignableFrom(tType0()); @@ -676,6 +690,7 @@ abstract class AbstractDataType extends AbstractNamed implements DataType abstract String castTypeSuffix0(); abstract String castTypeName0(); abstract Class tType0(); + abstract Class uType0(); abstract Integer precision0(); abstract Integer scale0(); abstract Integer length0(); diff --git a/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java b/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java index 42d9b0b823..db1b793b5b 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/ArrayDataType.java @@ -51,7 +51,7 @@ import org.jooq.Nullability; */ final class ArrayDataType extends DefaultDataType { - final DataType elementType; + final DataType elementType; public ArrayDataType(DataType elementType) { super(null, elementType.getArrayType(), elementType.getTypeName(), elementType.getCastTypeName()); @@ -63,7 +63,7 @@ final class ArrayDataType extends DefaultDataType { * [#3225] Performant constructor for creating derived types. */ ArrayDataType( - DefaultDataType t, + AbstractDataType t, DataType elementType, Integer precision, Integer scale, @@ -94,7 +94,7 @@ final class ArrayDataType extends DefaultDataType { ) { return new ArrayDataType<>( this, - (DefaultDataType) elementType, + (AbstractDataType) elementType, newPrecision, newScale, newLength, diff --git a/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java b/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java index 33009fa055..d82a6a5f29 100644 --- a/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/ConvertedDataType.java @@ -45,6 +45,8 @@ import org.jooq.Converter; import org.jooq.DataType; import org.jooq.Field; import org.jooq.Nullability; +import org.jooq.RecordType; +import org.jooq.Row; import org.jooq.SQLDialect; /** @@ -55,8 +57,8 @@ import org.jooq.SQLDialect; @SuppressWarnings({ "rawtypes", "unchecked" }) final class ConvertedDataType extends AbstractDataTypeX { - private final AbstractDataTypeX delegate; - private final Binding binding; + final AbstractDataTypeX delegate; + final Binding binding; ConvertedDataType(AbstractDataTypeX delegate, Binding binding) { super(delegate.getQualifiedName(), delegate.getCommentPart()); @@ -92,6 +94,11 @@ final class ConvertedDataType extends AbstractDataTypeX { ).asConvertedDataType(binding); } + @Override + public final Row getRow() { + return delegate.getRow(); + } + @Override public final DataType getSQLDataType() { return (DataType) delegate.getSQLDataType(); @@ -167,6 +174,11 @@ final class ConvertedDataType extends AbstractDataTypeX { return delegate.tType0(); } + @Override + final Class uType0() { + return binding.converter().toType(); + } + @Override final Integer precision0() { return delegate.precision0(); diff --git a/jOOQ/src/main/java/org/jooq/impl/DataTypeProxy.java b/jOOQ/src/main/java/org/jooq/impl/DataTypeProxy.java index 2f6961713c..60b6c6f7a0 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DataTypeProxy.java +++ b/jOOQ/src/main/java/org/jooq/impl/DataTypeProxy.java @@ -258,6 +258,11 @@ final class DataTypeProxy extends AbstractDataType { return type.tType0(); } + @Override + final Class uType0() { + return type.uType0(); + } + @Override final Integer precision0() { return defaultIfNull(overridePrecision, type.precision0()); diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java index 40d4975811..c3e5d86495 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultBinding.java @@ -232,6 +232,8 @@ import org.jooq.types.YearToMonth; import org.jooq.types.YearToSecond; import org.jooq.util.postgres.PostgresUtils; +import org.jetbrains.annotations.Nullable; + // ... // ... @@ -3430,12 +3432,9 @@ public class DefaultBinding implements Binding { render.sql("::").visit(((TableRecord) value).getTable().getQualifiedName()); } - private static final T pgFromString(Class type, String string) { - return pgFromString(Converters.identity(type), string); - } - @SuppressWarnings("unchecked") - private static final T pgFromString(Converter converter, String string) { + private static final T pgFromString(Field field, String string) { + Converter converter = field.getConverter(); Class type = Reflect.wrapper(converter.toType()); if (string == null) @@ -3495,7 +3494,7 @@ public class DefaultBinding implements Binding { else if (type == UUID.class) return (T) UUID.fromString(string); else if (type.isArray()) - return (T) pgNewArray(type, string); + return (T) pgNewArray(field, type, string); @@ -3506,7 +3505,7 @@ public class DefaultBinding implements Binding { // [#11812] UDTRecords/TableRecords or InternalRecords that don't have an explicit converter && (!InternalRecord.class.isAssignableFrom(type) || type == converter.fromType())) - return (T) pgNewRecord(type, null, string); + return (T) pgNewRecord(type, (AbstractRow) field.getDataType().getRow(), string); else if (type == Object.class) return (T) string; @@ -3514,7 +3513,7 @@ public class DefaultBinding implements Binding { // which would cause a StackOverflowError, here! else if (type != converter.fromType()) { Converter c = (Converter) converter; - return c.from(pgFromString(c.fromType(), string)); + return c.from(pgFromString(field("converted_field", ((ConvertedDataType) field.getDataType()).delegate), string)); } throw new UnsupportedOperationException("Class " + type + " is not supported"); @@ -3532,7 +3531,7 @@ public class DefaultBinding implements Binding { * @return The converted {@link UDTRecord} */ @SuppressWarnings("unchecked") - static final Record pgNewRecord(Class type, AbstractRow fields, final Object object) { + static final Record pgNewRecord(Class type, AbstractRow fields, final Object object) { if (object == null) return null; @@ -3548,9 +3547,9 @@ public class DefaultBinding implements Binding { // - Temporal data // - Everything else: VARCHAR if (fields == null && Record.class.isAssignableFrom(type)) - fields = (AbstractRow) Tools.row0(Tools.fields(values.size(), SQLDataType.VARCHAR)); + fields = Tools.row0(Tools.fields(values.size(), SQLDataType.VARCHAR)); - return Tools.newRecord(true, (Class) type, fields) + return Tools.newRecord(true, (Class) type, (AbstractRow) fields) .operate(record -> { Row row = record.fieldsRow(); @@ -3562,7 +3561,7 @@ public class DefaultBinding implements Binding { } private static final void pgSetValue(Record record, Field field, String value) { - record.set(field, pgFromString(field.getConverter(), value)); + record.set(field, pgFromString(field, value)); } /** @@ -3574,21 +3573,24 @@ public class DefaultBinding implements Binding { * @param string A String representation of an array * @return The converted array */ - private static final Object[] pgNewArray(Class type, String string) { + private static final Object[] pgNewArray(Field field, Class type, String string) { if (string == null) return null; try { - Class component = type.getComponentType(); - return Tools.map( toPGArray(string), - v -> pgFromString(component, v), - size -> (Object[]) java.lang.reflect.Array.newInstance(component, size) + v -> pgFromString(field("array_element", field.getDataType().getArrayComponentDataType()), v), + size -> (Object[]) java.lang.reflect.Array.newInstance(type.getComponentType(), size) ); } catch (Exception e) { - throw new DataTypeException("Error while creating array", e); + + // [#11823] + if (type.getComponentType().getSimpleName().equals("UnknownType")) + throw new DataTypeException("Error while creating array for UnknownType. Please provide an explicit Class type to your converter, see https://github.com/jOOQ/jOOQ/issues/11823", e); + else + throw new DataTypeException("Error while creating array", e); } } } diff --git a/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java b/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java index 8562a64baa..c2de843d5d 100644 --- a/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java +++ b/jOOQ/src/main/java/org/jooq/impl/DefaultDataType.java @@ -360,7 +360,7 @@ public class DefaultDataType extends AbstractDataTypeX { * [#3225] Performant constructor for creating derived types. */ DefaultDataType( - DefaultDataType t, + AbstractDataType t, Integer precision, Integer scale, Integer length, @@ -372,14 +372,14 @@ public class DefaultDataType extends AbstractDataTypeX { ) { super(t.getQualifiedName(), NO_COMMENT); - this.dialect = t.dialect; - this.sqlDataType = t.sqlDataType; - this.uType = t.uType; - this.tType = t.tType; - this.typeName = t.typeName; - this.castTypeName = t.castTypeName; - this.castTypePrefix = t.castTypePrefix; - this.castTypeSuffix = t.castTypeSuffix; + this.dialect = t.getDialect(); + this.sqlDataType = t.getSQLDataType(); + this.uType = t.uType0(); + this.tType = t.tType0(); + this.typeName = t.typeName0(); + this.castTypeName = t.castTypeName0(); + this.castTypePrefix = t.castTypePrefix0(); + this.castTypeSuffix = t.castTypeSuffix0(); this.nullability = nullability; this.collation = collation; @@ -392,9 +392,9 @@ public class DefaultDataType extends AbstractDataTypeX { // [#10362] User bindings and/or converters need to be retained this.binding = - t.binding instanceof org.jooq.impl.DefaultBinding.AbstractBinding - ? binding(this, (Converter) t.binding.converter()) - : t.binding; + t.getBinding() instanceof org.jooq.impl.DefaultBinding.AbstractBinding + ? binding(this, (Converter) t.getBinding().converter()) + : t.getBinding(); } private static final Integer integerPrecision(Class type, Integer precision) { @@ -528,6 +528,11 @@ public class DefaultDataType extends AbstractDataTypeX { return tType; } + @Override + final Class uType0() { + return uType; + } + @Override public final SQLDialect getDialect() { return dialect; diff --git a/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java b/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java new file mode 100644 index 0000000000..3872be4bba --- /dev/null +++ b/jOOQ/src/main/java/org/jooq/impl/RecordDataType.java @@ -0,0 +1,120 @@ +/* + * 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. + * + * Other licenses: + * ----------------------------------------------------------------------------- + * Commercial licenses for this work are available. These replace the above + * ASL 2.0 and offer limited warranties, support, maintenance, and commercial + * database integrations. + * + * For more information, please visit: http://www.jooq.org/licenses + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + */ +package org.jooq.impl; + +import static org.jooq.impl.Tools.recordType; + +import org.jooq.CharacterSet; +import org.jooq.Collation; +import org.jooq.Field; +import org.jooq.Nullability; +import org.jooq.Record; +import org.jooq.RecordType; +import org.jooq.Row; + +import org.jetbrains.annotations.Nullable; + +/** + * A wrapper for anonymous array data types + * + * @author Lukas Eder + */ +final class RecordDataType extends DefaultDataType { + + final Row row; + + @SuppressWarnings("unchecked") + public RecordDataType(Row row) { + // [#11829] TODO: Implement this correctly for UDTRecord, TableRecord, and EmbeddableRecord + super(null, (Class) recordType(row.size()), "record", "record"); + + this.row = row; + } + + /** + * [#3225] Performant constructor for creating derived types. + */ + RecordDataType( + DefaultDataType t, + Row row, + Integer precision, + Integer scale, + Integer length, + Nullability nullability, + Collation collation, + CharacterSet characterSet, + boolean identity, + Field defaultValue + ) { + super(t, precision, scale, length, nullability, collation, characterSet, identity, defaultValue); + + this.row = row; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Override + DefaultDataType construct( + Integer newPrecision, + Integer newScale, + Integer newLength, + Nullability + newNullability, + Collation newCollation, + CharacterSet newCharacterSet, + boolean newIdentity, + Field newDefaultValue + ) { + return new RecordDataType<>( + this, + row, + newPrecision, + newScale, + newLength, + newNullability, + newCollation, + newCharacterSet, + newIdentity, + (Field) newDefaultValue + ); + } + + @Override + public final Row getRow() { + return row; + } +} diff --git a/jOOQ/src/main/java/org/jooq/impl/RowField.java b/jOOQ/src/main/java/org/jooq/impl/RowField.java index fdeb21038d..bb477ea56e 100644 --- a/jOOQ/src/main/java/org/jooq/impl/RowField.java +++ b/jOOQ/src/main/java/org/jooq/impl/RowField.java @@ -76,7 +76,6 @@ import static org.jooq.impl.Tools.BooleanDataKey.DATA_LIST_ALREADY_INDENTED; import java.util.Set; import org.jooq.Context; -import org.jooq.DataType; import org.jooq.Field; import org.jooq.Name; import org.jooq.Record; @@ -99,7 +98,7 @@ final class RowField extends AbstractField< @SuppressWarnings({ "unchecked", "rawtypes" }) RowField(final ROW row, Name as) { - super(as, (DataType) SQLDataType.RECORD, CommentImpl.NO_COMMENT, binding(fromNullable( + super(as, new RecordDataType<>(row), CommentImpl.NO_COMMENT, binding(fromNullable( Object.class, (Class) Tools.recordType(row.size()),