This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 6b4838d56dbdedce9a1fe3c2ccd8dc71d5504513
Author: Andrus Adamchik <[email protected]>
AuthorDate: Sun Jun 28 15:28:54 2026 -0400

    Refactoring row descriptors:
    
    * ColumnDescriptor: ExtendedType type property
    * Getting rid of RowDescriptor ... ColumnDescriptor[] now gives us a full 
row
    * Inline RowDescriptorBuilder into ColumnDescriptor
---
 .../reader/CryptoRowReaderFactoryDecorator.java    |  86 +++---
 .../apache/cayenne/velocity/ResultDirective.java   |   8 +-
 .../VelocitySQLTemplateTranslator_SelectTest.java  |   8 +-
 .../java/org/apache/cayenne/access/DataNode.java   |  29 ---
 .../apache/cayenne/access/jdbc/BaseSQLAction.java  |   4 +-
 .../apache/cayenne/access/jdbc/BatchAction.java    |  20 +-
 .../cayenne/access/jdbc/ColumnDescriptor.java      | 264 ++++++++++++++++++-
 .../cayenne/access/jdbc/ProcedureAction.java       |  25 +-
 .../apache/cayenne/access/jdbc/RowDescriptor.java  |  71 -----
 .../cayenne/access/jdbc/RowDescriptorBuilder.java  | 287 ---------------------
 .../cayenne/access/jdbc/SQLTemplateAction.java     |  24 +-
 .../apache/cayenne/access/jdbc/SelectAction.java   |   8 +-
 .../cayenne/access/jdbc/reader/BaseRowReader.java  |   8 +-
 .../jdbc/reader/DefaultRowReaderFactory.java       |  44 ++--
 .../access/jdbc/reader/EmbeddableRowReader.java    |   7 +-
 .../access/jdbc/reader/EntityRowReader.java        |   7 +-
 .../cayenne/access/jdbc/reader/FullRowReader.java  |   8 +-
 .../cayenne/access/jdbc/reader/IdRowReader.java    |   7 +-
 .../reader/InheritanceAwareEntityRowReader.java    |   6 +-
 .../jdbc/reader/InheritanceAwareRowReader.java     |   6 +-
 .../access/jdbc/reader/RowReaderFactory.java       |   4 +-
 .../access/jdbc/reader/ScalarRowReader.java        |   8 +-
 .../translator/select/ColumnDescriptorStage.java   |   4 +-
 .../access/translator/sqltemplate/Context.java     |  10 +
 .../translator/sqltemplate/directive/Result.java   |   2 +-
 .../apache/cayenne/dba/db2/DB2ProcedureAction.java |   6 +-
 .../cayenne/dba/mysql/MySQLProcedureAction.java    |   6 +-
 .../cayenne/dba/oracle/OracleProcedureAction.java  |   6 +-
 .../dba/oracle/OracleSQLTemplateAction.java        |  24 +-
 .../dba/sqlserver/SQLServerProcedureAction.java    |   6 +-
 .../access/jdbc/JDBCResultIteratorTest.java        |   4 +-
 .../select/MockSelectTranslatorContext.java        |  12 +-
 32 files changed, 442 insertions(+), 577 deletions(-)

diff --git 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
index b48cdb692..d8a6893fd 100644
--- 
a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
+++ 
b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
@@ -22,7 +22,6 @@ import java.sql.ResultSet;
 import java.util.Map;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
 import org.apache.cayenne.access.jdbc.reader.DefaultRowReaderFactory;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
 import org.apache.cayenne.access.types.ExtendedType;
@@ -60,80 +59,63 @@ public class CryptoRowReaderFactoryDecorator extends 
DefaultRowReaderFactory {
     }
 
     @Override
-    public RowReader<?> rowReader(RowDescriptor descriptor, QueryMetadata 
queryMetadata, DbAdapter adapter) {
-        RowDescriptor encryptedRowDescriptor = 
encryptedRowDescriptor(descriptor, adapter.getExtendedTypes());
-        return super.rowReader(encryptedRowDescriptor, queryMetadata, adapter);
+    public RowReader<?> rowReader(ColumnDescriptor[] columns, QueryMetadata 
queryMetadata, DbAdapter adapter) {
+        return super.rowReader(encryptedColumns(columns, 
adapter.getExtendedTypes()), queryMetadata, adapter);
     }
 
     @Override
-    protected RowReader<?> createScalarRowReader(RowDescriptor descriptor, 
QueryMetadata queryMetadata,
+    protected RowReader<?> createScalarRowReader(ColumnDescriptor[] columns, 
QueryMetadata queryMetadata,
                                                  ScalarResultSegment segment) {
         RowReader<?> scalarRowReader = super
-                .createScalarRowReader(descriptor, queryMetadata, segment);
-        return new 
DecoratedScalarRowReader(descriptor.getColumns()[segment.getColumnOffset()], 
scalarRowReader);
+                .createScalarRowReader(columns, queryMetadata, segment);
+        return new 
DecoratedScalarRowReader(columns[segment.getColumnOffset()], scalarRowReader);
     }
 
     @Override
-    protected RowReader<?> createEntityRowReader(RowDescriptor descriptor, 
QueryMetadata queryMetadata,
+    protected RowReader<?> createEntityRowReader(ColumnDescriptor[] columns, 
QueryMetadata queryMetadata,
                                                  EntityResultSegment 
resultMetadata) {
         RowReader<?> entityRowReader = super
-                .createEntityRowReader(descriptor, queryMetadata, 
resultMetadata);
-        return new DecoratedEntityRowReader(descriptor, entityRowReader, 
resultMetadata);
+                .createEntityRowReader(columns, queryMetadata, resultMetadata);
+        return new DecoratedEntityRowReader(columns, entityRowReader, 
resultMetadata);
     }
 
     @Override
-    protected RowReader<?> createFullRowReader(RowDescriptor descriptor, 
QueryMetadata queryMetadata) {
+    protected RowReader<?> createFullRowReader(ColumnDescriptor[] columns, 
QueryMetadata queryMetadata) {
         RowReader<?> fullRowReader = super
-                .createFullRowReader(descriptor, queryMetadata);
-        return new DecoratedFullRowReader(descriptor, fullRowReader);
+                .createFullRowReader(columns, queryMetadata);
+        return new DecoratedFullRowReader(columns, fullRowReader);
     }
 
-    protected RowDescriptor encryptedRowDescriptor(RowDescriptor descriptor, 
ExtendedTypeMap typeMap) {
+    protected ColumnDescriptor[] encryptedColumns(ColumnDescriptor[] columns, 
ExtendedTypeMap typeMap) {
 
-        // need to tweak the original descriptor to ensure binary columns are 
read as binary, eben if the plain Java
-        // type is not a byte[]
+        // need to tweak the columns to ensure encrypted columns are read as 
binary or char, even if the plain Java
+        // type is not a byte[] / String
 
-        ColumnDescriptor[] originalColumns = descriptor.getColumns();
-        int len = originalColumns.length;
+        ColumnDescriptor[] encrypted = new ColumnDescriptor[columns.length];
 
-        ExtendedType[] originalConverters = descriptor.getConverters();
-        ExtendedType[] encryptedConverters = new ExtendedType[len];
+        for (int i = 0; i < columns.length; i++) {
+            ColumnDescriptor column = columns[i];
+            DbAttribute attribute = column.attribute();
 
-        for (int i = 0; i < len; i++) {
-            DbAttribute attribute = originalColumns[i].attribute();
-
-            ExtendedType t = originalConverters[i];
+            ExtendedType type = column.type();
 
             if (attribute != null && columnMapper.isEncrypted(attribute)) {
 
                 // only char or binary columns can store encrypted data
                 if (TypesMapping.isBinary(attribute.getType())) {
-                    t = typeMap.getRegisteredType(byte[].class);
+                    type = typeMap.getRegisteredType(byte[].class);
                 } else if (TypesMapping.isCharacter(attribute.getType())) {
-                    t = typeMap.getRegisteredType(String.class);
+                    type = typeMap.getRegisteredType(String.class);
                 }
                 // else - warning?
             }
 
-            encryptedConverters[i] = t;
-        }
-
-        return new DecoratedRowDescriptor(descriptor, originalColumns, 
encryptedConverters);
-    }
-
-    private static class DecoratedRowDescriptor extends RowDescriptor {
-
-        private final RowDescriptor original;
-
-        DecoratedRowDescriptor(RowDescriptor rowDescriptor, ColumnDescriptor[] 
columns, ExtendedType[] converters) {
-            this.original = rowDescriptor;
-            this.columns = columns;
-            this.converters = converters;
+            encrypted[i] = type == column.type()
+                    ? column
+                    : new ColumnDescriptor(column.name(), column.dataRowKey(), 
column.jdbcType(), type, attribute);
         }
 
-        public RowDescriptor unwrap() {
-            return original;
-        }
+        return encrypted;
     }
 
     private class DecoratedScalarRowReader implements RowReader<Object> {
@@ -164,16 +146,16 @@ public class CryptoRowReaderFactoryDecorator extends 
DefaultRowReaderFactory {
 
     private abstract class DecoratedEntityFullRowReader implements 
RowReader<Object> {
 
-        final RowDescriptor descriptor;
+        final ColumnDescriptor[] columns;
         final RowReader<?> delegateReader;
         final EntityResultSegment resultMetadata;
         boolean decryptorCompiled;
         MapTransformer decryptor;
 
-        DecoratedEntityFullRowReader(RowDescriptor descriptor,
+        DecoratedEntityFullRowReader(ColumnDescriptor[] columns,
                                  RowReader<?> delegateReader,
                                  EntityResultSegment resultMetadata) {
-            this.descriptor = descriptor;
+            this.columns = columns;
             this.delegateReader = delegateReader;
             this.resultMetadata = resultMetadata;
         }
@@ -198,10 +180,10 @@ public class CryptoRowReaderFactoryDecorator extends 
DefaultRowReaderFactory {
 
     private class DecoratedEntityRowReader extends 
DecoratedEntityFullRowReader {
 
-        DecoratedEntityRowReader(RowDescriptor descriptor,
+        DecoratedEntityRowReader(ColumnDescriptor[] columns,
                                  RowReader<?> delegateReader,
                                  EntityResultSegment resultMetadata) {
-            super(descriptor, delegateReader, resultMetadata);
+            super(columns, delegateReader, resultMetadata);
         }
 
         void ensureDecryptorCompiled(Object row) {
@@ -211,7 +193,7 @@ public class CryptoRowReaderFactoryDecorator extends 
DefaultRowReaderFactory {
                 ColumnDescriptor[] columnDescriptors =
                         new ColumnDescriptor[fieldsSize];
                 for(int i = offset, j = 0; i < offset + fieldsSize; i++) {
-                    columnDescriptors[j++] = descriptor.getColumns()[i];
+                    columnDescriptors[j++] = columns[i];
                 }
                 decryptor = transformerFactory.decryptor(columnDescriptors, 
row);
                 decryptorCompiled = true;
@@ -226,14 +208,14 @@ public class CryptoRowReaderFactoryDecorator extends 
DefaultRowReaderFactory {
 
     private class DecoratedFullRowReader extends DecoratedEntityFullRowReader {
 
-        DecoratedFullRowReader(RowDescriptor descriptor,
+        DecoratedFullRowReader(ColumnDescriptor[] columns,
                                RowReader<?> delegateReader) {
-            super(descriptor, delegateReader, null);
+            super(columns, delegateReader, null);
         }
 
         void ensureDecryptorCompiled(Object row) {
             if (!decryptorCompiled) {
-                decryptor = 
transformerFactory.decryptor(descriptor.getColumns(), row);
+                decryptor = transformerFactory.decryptor(columns, row);
                 decryptorCompiled = true;
             }
         }
diff --git 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java
 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java
index c8a869160..cb7b5be88 100644
--- 
a/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java
+++ 
b/cayenne-velocity/src/main/java/org/apache/cayenne/velocity/ResultDirective.java
@@ -31,6 +31,8 @@ import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.util.Util;
 import org.apache.velocity.context.InternalContextAdapter;
 import org.apache.velocity.exception.MethodInvocationException;
@@ -129,6 +131,10 @@ public class ResultDirective extends Directive {
                String type = getChildAsString(context, node, 1);
                String javaClass = (type != null) ? guessType(type) : null;
 
+               DbAdapter adapter = (DbAdapter) context.getInternalUserContext()
+                               .get(VelocitySQLTemplateTranslator.ADAPTER_KEY);
+               ExtendedType extendedType = 
adapter.getExtendedTypes().getRegisteredType(javaClass);
+
                // TODO: andrus 6/27/2007 - this is an unofficial jdbcType 
parameter
                // that is added
                // temporarily pending CAY-813 implementation for the sake of 
EJBQL
@@ -136,7 +142,7 @@ public class ResultDirective extends Directive {
                Object jdbcTypeChild = getChild(context, node, 4);
                int jdbcType = (jdbcTypeChild instanceof Number) ? ((Number) 
jdbcTypeChild).intValue() : 0;
 
-               ColumnDescriptor columnDescriptor = new 
ColumnDescriptor(column, label, jdbcType, javaClass, null);
+               ColumnDescriptor columnDescriptor = new 
ColumnDescriptor(column, label, jdbcType, extendedType, null);
 
                writer.write(column);
 
diff --git 
a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateTranslator_SelectTest.java
 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateTranslator_SelectTest.java
index 01d659d52..1f8578bcf 100644
--- 
a/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateTranslator_SelectTest.java
+++ 
b/cayenne-velocity/src/test/java/org/apache/cayenne/velocity/VelocitySQLTemplateTranslator_SelectTest.java
@@ -20,7 +20,7 @@
 package org.apache.cayenne.velocity;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
@@ -68,7 +68,7 @@ public class VelocitySQLTemplateTranslator_SelectTest {
                assertEquals(0, compiled.bindings().length);
                assertEquals(1, compiled.resultColumns().length);
                assertEquals("A", compiled.resultColumns()[0].name());
-               assertNull(compiled.resultColumns()[0].javaClass());
+               assertSame(adapter.getExtendedTypes().getDefaultType(), 
compiled.resultColumns()[0].type());
        }
 
        @Test
@@ -82,7 +82,7 @@ public class VelocitySQLTemplateTranslator_SelectTest {
 
                assertEquals(1, compiled.resultColumns().length);
                assertEquals("A", compiled.resultColumns()[0].name());
-               assertEquals("java.lang.String", 
compiled.resultColumns()[0].javaClass());
+               
assertSame(adapter.getExtendedTypes().getRegisteredType(String.class), 
compiled.resultColumns()[0].type());
        }
 
        @Test
@@ -98,7 +98,7 @@ public class VelocitySQLTemplateTranslator_SelectTest {
                ColumnDescriptor column = compiled.resultColumns()[0];
                assertEquals("A", column.name());
                assertEquals("B", column.dataRowKey());
-               assertEquals("java.lang.String", column.javaClass());
+               
assertSame(adapter.getExtendedTypes().getRegisteredType(String.class), 
column.type());
        }
 
        @Test
diff --git a/cayenne/src/main/java/org/apache/cayenne/access/DataNode.java 
b/cayenne/src/main/java/org/apache/cayenne/access/DataNode.java
index 22c9273c8..6f496b821 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/DataNode.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/DataNode.java
@@ -21,9 +21,6 @@ package org.apache.cayenne.access;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.dbsync.SchemaUpdateStrategy;
-import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
-import org.apache.cayenne.access.jdbc.reader.RowReader;
 import org.apache.cayenne.access.jdbc.reader.RowReaderFactory;
 import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslator;
@@ -36,11 +33,9 @@ import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.log.NoopJdbcEventLogger;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.ObjAttribute;
 import org.apache.cayenne.query.DeleteBatchQuery;
 import org.apache.cayenne.query.InsertBatchQuery;
 import org.apache.cayenne.query.Query;
-import org.apache.cayenne.query.QueryMetadata;
 import org.apache.cayenne.query.UpdateBatchQuery;
 import org.apache.cayenne.tx.BaseTransaction;
 import org.apache.cayenne.tx.Transaction;
@@ -317,30 +312,6 @@ public class DataNode {
         return new ToStringBuilder(this).append("name", getName()).toString();
     }
 
-
-    /**
-     * Creates a {@link RowReader} using internal {@link RowReaderFactory}.
-     *
-     * @since 4.0
-     * @deprecated in favor of obtaining the factory via {@link 
#getRowReaderFactory()} and calling it directly.
-     */
-    @Deprecated(since = "5.0", forRemoval = true)
-    public RowReader<?> rowReader(RowDescriptor descriptor, QueryMetadata 
queryMetadata) {
-        return rowReaderFactory.rowReader(descriptor, queryMetadata, 
getAdapter());
-    }
-
-    /**
-     * Creates a {@link RowReader} using internal {@link RowReaderFactory}.
-     *
-     * @since 4.0
-     * @deprecated in favor of obtaining the factory via {@link 
#getRowReaderFactory()} and calling it directly.
-     */
-    @Deprecated(since = "5.0", forRemoval = true)
-    public RowReader<?> rowReader(RowDescriptor descriptor, QueryMetadata 
queryMetadata,
-                                  Map<ObjAttribute, ColumnDescriptor> 
attributeOverrides) {
-        return rowReader(descriptor, queryMetadata);
-    }
-
     /**
      * @since 4.0
      */
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BaseSQLAction.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BaseSQLAction.java
index 1a63b8610..6f484f41b 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BaseSQLAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BaseSQLAction.java
@@ -49,14 +49,14 @@ public abstract class BaseSQLAction implements SQLAction {
      * Helper method to process a ResultSet.
      */
     @SuppressWarnings({ "rawtypes", "unchecked" })
-    protected void readResultSet(ResultSet resultSet, RowDescriptor 
descriptor, Query query, OperationObserver delegate)
+    protected void readResultSet(ResultSet resultSet, ColumnDescriptor[] 
columns, Query query, OperationObserver delegate)
             throws Exception {
 
         long t1 = System.currentTimeMillis();
 
         QueryMetadata metadata = 
query.getMetaData(dataNode.getEntityResolver());
 
-        RowReader<?> rowReader = 
dataNode.getRowReaderFactory().rowReader(descriptor, metadata, 
dataNode.getAdapter());
+        RowReader<?> rowReader = 
dataNode.getRowReaderFactory().rowReader(columns, metadata, 
dataNode.getAdapter());
 
         JDBCResultIterator resultReader = new JDBCResultIterator(null, 
resultSet, rowReader);
 
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
index a5bb47938..15c6e8ace 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
@@ -27,6 +27,7 @@ import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.OptimisticLockException;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
 import org.apache.cayenne.access.translator.ParameterBinding;
+import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.translator.batch.TranslatedBatch;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.log.JdbcEventLogger;
@@ -57,7 +58,7 @@ public class BatchAction extends BaseSQLAction {
 
        protected boolean runningAsBatch;
        protected BatchQuery query;
-       protected RowDescriptor keyRowDescriptor;
+       protected ColumnDescriptor[] keyColumns;
 
        /**
         * @since 4.0
@@ -267,9 +268,9 @@ public class BatchAction extends BaseSQLAction {
                // (this way we can support multiple columns..
                // although need to check how well this works with most common 
drivers)
 
-               RowDescriptorBuilder builder = new RowDescriptorBuilder();
+               ColumnDescriptor.RowBuilder rowBuilder = 
ColumnDescriptor.rowBuilder();
 
-               if (this.keyRowDescriptor == null) {
+               if (this.keyColumns == null) {
                        // attempt to figure out the right descriptor from the 
mapping...
                        Collection<DbAttribute> generated = 
query.getDbEntity().getGeneratedAttributes();
                        if (generated.size() == 1 && 
keysRS.getMetaData().getColumnCount() == 1) {
@@ -286,17 +287,18 @@ public class BatchAction extends BaseSQLAction {
                                                columnName = "column_1";
                                        }
                                }
-                               columns[0] = new ColumnDescriptor(columnName, 
columnName, key.getType(), typeForGeneratedPK(key), null);
-                               builder.setColumns(columns);
+                               ExtendedType type = 
dataNode.getAdapter().getExtendedTypes().getRegisteredType(typeForGeneratedPK(key));
+                               columns[0] = new ColumnDescriptor(columnName, 
columnName, key.getType(), type, null);
+                               rowBuilder.columns(columns);
                        } else {
-                               builder.setResultSet(keysRS);
+                               rowBuilder.resultSet(keysRS);
                        }
 
-                       this.keyRowDescriptor = 
builder.getDescriptor(dataNode.getAdapter().getExtendedTypes());
+                       this.keyColumns = 
rowBuilder.build(dataNode.getAdapter().getExtendedTypes());
                }
 
                RowReader<?> rowReader = dataNode.getRowReaderFactory()
-                               .rowReader(keyRowDescriptor, 
query.getMetaData(dataNode.getEntityResolver()), dataNode.getAdapter());
+                               .rowReader(keyColumns, 
query.getMetaData(dataNode.getEntityResolver()), dataNode.getAdapter());
                ResultIterator iterator = new JDBCResultIterator(null, keysRS, 
rowReader);
 
                List<ObjectId> objectIds = new ArrayList<>(rows.size());
@@ -307,7 +309,7 @@ public class BatchAction extends BaseSQLAction {
        }
 
        private String typeForGeneratedPK(DbAttribute key) {
-               String entityName = 
getQuery().getRows().get(0).getObjectId().getEntityName();
+               String entityName = 
getQuery().getRows().getFirst().getObjectId().getEntityName();
                ObjEntity objEntity = 
dataNode.getEntityResolver().getObjEntity(entityName);
                if(objEntity != null) {
                        ObjAttribute attributeForDbAttribute = 
objEntity.getAttributeForDbAttribute(key);
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
index 8b1df6876..0664f7ee2 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
@@ -19,14 +19,31 @@
 
 package org.apache.cayenne.access.jdbc;
 
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.access.types.ExtendedTypeMap;
+import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.map.DbAttribute;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import java.sql.ResultSet;
+import java.sql.ResultSetMetaData;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import java.util.function.Function;
 
 /**
- * A descriptor of a ResultSet column.
+ * A descriptor of a ResultSet column, carrying the {@link ExtendedType} used 
to materialize the column value.
  */
-public record ColumnDescriptor(String name, String dataRowKey, int jdbcType, 
String javaClass, DbAttribute attribute) {
+public record ColumnDescriptor(String name, String dataRowKey, int jdbcType, 
ExtendedType<?> type,
+                               DbAttribute attribute) {
 
     public ColumnDescriptor {
         if (dataRowKey == null) {
@@ -55,4 +72,247 @@ public record ColumnDescriptor(String name, String 
dataRowKey, int jdbcType, Str
     public int hashCode() {
         return Objects.hash(name, dataRowKey);
     }
+
+    /**
+     * Creates a {@link RowBuilder} that assembles the array of {@link 
ColumnDescriptor}s describing a result row.
+     *
+     * @since 5.0
+     */
+    public static RowBuilder rowBuilder() {
+        return new RowBuilder();
+    }
+
+    public static class RowBuilder {
+
+        private static final Logger LOGGER = 
LoggerFactory.getLogger(RowBuilder.class);
+
+        private static final Function<String, String> UPPERCASE_TRANSFORMER = 
input ->
+                input != null ? input.toUpperCase() : null;
+
+        private static final Function<String, String> LOWERCASE_TRANSFORMER = 
input ->
+                input != null ? input.toLowerCase() : null;
+
+        protected ColumnDescriptor[] columns;
+        protected ResultSetMetaData resultSetMetadata;
+
+        protected Function<String, String> caseTransformer;
+        protected Map<String, String> typeOverrides;
+
+        private boolean mergeColumnsWithRsMetadata;
+
+        protected boolean validateDuplicateColumnNames;
+
+        /**
+         * Returns the array of {@link ColumnDescriptor}s describing the 
result row, with an {@link ExtendedType}
+         * resolved for each column.
+         */
+        public ColumnDescriptor[] build(ExtendedTypeMap typeMap) throws 
SQLException, IllegalStateException {
+
+            ColumnDescriptor[] columnsForRD;
+
+            if (this.resultSetMetadata != null) {
+                // do merge between explicitly-set columns and 
ResultSetMetadata
+                // explicitly-set columns take precedence
+                columnsForRD = mergeResultSetAndPresetColumns(typeMap);
+            } else if (this.columns != null) {
+                // use explicitly-set columns
+                columnsForRD = this.columns;
+            } else {
+                throw new IllegalStateException(
+                        "Can't build row descriptor, both 'columns' and 
'resultSetMetadata' are null");
+            }
+
+            return performTransformAndTypeOverride(columnsForRD, typeMap);
+        }
+
+        /**
+         * @return array of columns for ResultSet with overriding 
ColumnDescriptors from
+         * 'columns' Note: column will be overlooked, if column name is empty
+         */
+        protected ColumnDescriptor[] 
mergeResultSetAndPresetColumns(ExtendedTypeMap typeMap) throws SQLException {
+
+            int rsLen = resultSetMetadata.getColumnCount();
+            if (rsLen == 0) {
+                throw new CayenneRuntimeException("'ResultSetMetadata' is 
empty.");
+            }
+
+            int columnLen = (columns != null) ? columns.length : 0;
+
+            if (mergeColumnsWithRsMetadata && rsLen != columnLen) {
+                throw new CayenneRuntimeException("Size of 'ResultSetMetadata' 
not equals to size of 'columns'.");
+            } else if (rsLen < columnLen) {
+                throw new CayenneRuntimeException("'ResultSetMetadata' has 
less elements then 'columns'.");
+            } else if (rsLen == columnLen && !mergeColumnsWithRsMetadata) {
+                // 'columns' contains ColumnDescriptor for every column
+                // in resultSetMetadata. This return is for optimization.
+                return columns;
+            }
+
+            ColumnDescriptor[] rsColumns = new ColumnDescriptor[rsLen];
+            List<String> duplicates = null;
+            Set<String> uniqueNames = null;
+
+            if (validateDuplicateColumnNames) {
+                duplicates = new ArrayList<>();
+                uniqueNames = new HashSet<>();
+            }
+
+            int outputLen = 0;
+            for (int i = 0; i < rsLen; i++) {
+                String rowkey = resolveDataRowKeyFromResultSet(i + 1);
+
+                // resolve column descriptor from 'columns' or create new
+                ColumnDescriptor column = getOrCreateColumn(rowkey, columns, i 
+ 1, typeMap);
+
+                // validate uniqueness of names
+                if (validateDuplicateColumnNames) {
+                    if (!uniqueNames.add(column.dataRowKey())) {
+                        duplicates.add(column.dataRowKey());
+                    }
+                }
+                rsColumns[outputLen] = column;
+                outputLen++;
+            }
+
+            if (validateDuplicateColumnNames && !duplicates.isEmpty()) {
+                LOGGER.warn("Found duplicated columns '{}' in row descriptor. 
" +
+                                "This can lead to errors when converting 
result to persistent objects.",
+                        String.join("', '", duplicates));
+            }
+
+            if (outputLen < rsLen) {
+                // cut ColumnDescriptor array
+                ColumnDescriptor[] rsColumnsCut = new 
ColumnDescriptor[outputLen];
+                System.arraycopy(rsColumns, 0, rsColumnsCut, 0, outputLen);
+                return rsColumnsCut;
+            }
+
+            return rsColumns;
+        }
+
+        private ColumnDescriptor getOrCreateColumn(
+                String rowKey,
+                ColumnDescriptor[] columnArray,
+                int position,
+                ExtendedTypeMap typeMap) throws SQLException {
+            int len = (columnArray != null) ? columnArray.length : 0;
+            // go through columnArray to find ColumnDescriptor for specified 
column
+            for (int i = 0; i < len; i++) {
+                if (columnArray[i] != null) {
+                    if (mergeColumnsWithRsMetadata) {
+                        return new ColumnDescriptor(rowKey, rowKey, 
resultSetMetadata.getColumnType(position), columnArray[position - 1].type(), 
null);
+                    } else {
+                        String columnRowKey = columnArray[i].dataRowKey();
+
+                        // TODO: andrus, 10/14/2009 - 'equalsIgnoreCase' check 
can result in
+                        // subtle bugs in DBs with case-sensitive column names 
(or when quotes are
+                        // used to force case sensitivity). Alternatively 
using 'equals' may miss
+                        // columns in case-insensitive situations.
+                        if (columnRowKey != null && 
columnRowKey.equalsIgnoreCase(rowKey)) {
+                            return columnArray[i];
+                        }
+                    }
+                }
+            }
+            // columnArray doesn't contain ColumnDescriptor for specified 
column
+            int jdbcType = resultSetMetadata.getColumnType(position);
+            ExtendedType<?> type = 
typeMap.getRegisteredType(TypesMapping.getJavaBySqlType(jdbcType));
+            return new ColumnDescriptor(rowKey, rowKey, jdbcType, type, null);
+        }
+
+        /**
+         * Return not empty string with ColumnLabel or ColumnName or "column_" 
+ position for
+         * for specified (by it's position) column in ResultSetMetaData.
+         */
+        private String resolveDataRowKeyFromResultSet(int position) throws 
SQLException {
+            String name = resultSetMetadata.getColumnLabel(position);
+            if (name == null || name.isEmpty()) {
+                name = resultSetMetadata.getColumnName(position);
+                if (name == null) {
+                    name = "";
+                }
+            }
+            return name;
+        }
+
+        private ColumnDescriptor[] 
performTransformAndTypeOverride(ColumnDescriptor[] columnArray, ExtendedTypeMap 
typeMap) {
+            if (caseTransformer == null && typeOverrides == null) {
+                return columnArray;
+            }
+
+            ColumnDescriptor[] result = new 
ColumnDescriptor[columnArray.length];
+            for (int i = 0; i < columnArray.length; i++) {
+                ColumnDescriptor column = columnArray[i];
+
+                String name = column.name();
+                String dataRowKey = column.dataRowKey();
+                ExtendedType<?> type = column.type();
+
+                if (caseTransformer != null) {
+                    name = caseTransformer.apply(name);
+                    dataRowKey = caseTransformer.apply(dataRowKey);
+                }
+
+                // type overrides are keyed by the (possibly transformed) 
column name
+                if (typeOverrides != null) {
+                    String javaClass = typeOverrides.get(name);
+                    if (javaClass != null) {
+                        type = typeMap.getRegisteredType(javaClass);
+                    }
+                }
+
+                result[i] = new ColumnDescriptor(name, dataRowKey, 
column.jdbcType(), type, column.attribute());
+            }
+            return result;
+        }
+
+        /**
+         * Sets an explicit set of columns. The builder may replace these with 
new instances to enforce the column
+         * capitalization policy and column Java type overrides.
+         */
+        public RowBuilder columns(ColumnDescriptor[] columns) {
+            this.columns = columns;
+            return this;
+        }
+
+        public RowBuilder resultSet(ResultSet resultSet) throws SQLException {
+            this.resultSetMetadata = resultSet.getMetaData();
+            return this;
+        }
+
+        public RowBuilder useLowercaseColumnNames() {
+            this.caseTransformer = LOWERCASE_TRANSFORMER;
+            return this;
+        }
+
+        public RowBuilder useUppercaseColumnNames() {
+            this.caseTransformer = UPPERCASE_TRANSFORMER;
+            return this;
+        }
+
+        /**
+         * Registers a Java type override for the named column. The first 
override registered for a given column wins:
+         * subsequent calls for the same column are ignored, so callers can 
register higher-priority overrides first and
+         * follow with fallback defaults without having to check what is 
already registered.
+         */
+        public RowBuilder overrideColumnType(String columnName, String type) {
+
+            if (typeOverrides == null) {
+                typeOverrides = new HashMap<>();
+            }
+
+            typeOverrides.putIfAbsent(columnName, type);
+            return this;
+        }
+
+        public RowBuilder validateDuplicateColumnNames() {
+            this.validateDuplicateColumnNames = true;
+            return this;
+        }
+
+        public RowBuilder mergeColumnsWithRsMetadata() {
+            this.mergeColumnsWithRsMetadata = true;
+            return this;
+        }
+    }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ProcedureAction.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ProcedureAction.java
index cb5914825..1f6d1da1f 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ProcedureAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/ProcedureAction.java
@@ -26,6 +26,7 @@ import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.procedure.TranslatedProcedure;
 import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.map.Procedure;
@@ -106,8 +107,8 @@ public class ProcedureAction extends BaseSQLAction {
                                if (statement.getMoreResults()) {
 
                                        try (ResultSet rs = 
statement.getResultSet()) {
-                                               RowDescriptor descriptor = 
describeResultSet(rs, processedResultSets++);
-                                               readResultSet(rs, descriptor, 
query, observer);
+                                               ColumnDescriptor[] columns = 
describeResultSet(rs, processedResultSets++);
+                                               readResultSet(rs, columns, 
query, observer);
                                        }
                                } else {
                                        int updateCount = 
statement.getUpdateCount();
@@ -151,25 +152,25 @@ public class ProcedureAction extends BaseSQLAction {
        }
 
        /**
-        * Creates a RowDescriptor for result set.
-        * 
+        * Describes the result set columns, resolving an {@link ExtendedType} 
for each.
+        *
         * @param resultSet
         *            JDBC ResultSet
         * @param setIndex
         *            a zero-based index of the ResultSet in the query results.
         */
-       protected RowDescriptor describeResultSet(ResultSet resultSet, int 
setIndex) throws SQLException {
+       protected ColumnDescriptor[] describeResultSet(ResultSet resultSet, int 
setIndex) throws SQLException {
 
                if (setIndex < 0) {
                        throw new IllegalArgumentException("Expected a 
non-negative result set index. Got: " + setIndex);
                }
 
-               RowDescriptorBuilder builder = new RowDescriptorBuilder();
+               ColumnDescriptor.RowBuilder builder = 
ColumnDescriptor.rowBuilder();
 
                List<ProcedureColumn[]> descriptors = 
query.getResultDescriptors();
 
                if (descriptors.isEmpty()) {
-                       builder.setResultSet(resultSet);
+                       builder.resultSet(resultSet);
                } else {
 
                        // if one result is described, all of them must be 
present...
@@ -177,7 +178,8 @@ public class ProcedureAction extends BaseSQLAction {
                                throw new CayenneRuntimeException("No 
descriptor for result set at index '%d' configured.", setIndex);
                        }
 
-                       
builder.setColumns(toColumnDescriptors(descriptors.get(setIndex)));
+                       
builder.columns(toColumnDescriptors(descriptors.get(setIndex),
+                                       
dataNode.getAdapter().getExtendedTypes()));
                }
 
                switch (query.getColumnNamesCapitalization()) {
@@ -189,7 +191,7 @@ public class ProcedureAction extends BaseSQLAction {
                        break;
                }
 
-               return 
builder.getDescriptor(dataNode.getAdapter().getExtendedTypes());
+               return builder.build(dataNode.getAdapter().getExtendedTypes());
        }
 
        /**
@@ -198,11 +200,12 @@ public class ProcedureAction extends BaseSQLAction {
         *
         * @since 5.0
         */
-       private static ColumnDescriptor[] toColumnDescriptors(ProcedureColumn[] 
columns) {
+       private static ColumnDescriptor[] toColumnDescriptors(ProcedureColumn[] 
columns, ExtendedTypeMap typeMap) {
                ColumnDescriptor[] result = new 
ColumnDescriptor[columns.length];
                for (int i = 0; i < columns.length; i++) {
                        ProcedureColumn c = columns[i];
-                       result[i] = new ColumnDescriptor(c.name(), 
c.dataRowKey(), c.jdbcType(), c.javaClass(), null);
+                       ExtendedType type = 
typeMap.getRegisteredType(c.javaClass());
+                       result[i] = new ColumnDescriptor(c.name(), 
c.dataRowKey(), c.jdbcType(), type, null);
                }
                return result;
        }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/RowDescriptor.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/RowDescriptor.java
deleted file mode 100644
index 57a07c99a..000000000
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/RowDescriptor.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you 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
- *
- *    https://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 org.apache.cayenne.access.jdbc;
-
-import org.apache.cayenne.access.types.ExtendedType;
-
-/**
- * A descriptor of a result row obtained from a database.
- * 
- * @since 1.2
- */
-public class RowDescriptor {
-
-    protected ColumnDescriptor[] columns;
-    protected ExtendedType[] converters;
-
-    /**
-     * Creates an empty RowDescriptor. Intended mainly for testing and use by 
subclasses.
-     */
-    protected RowDescriptor() {
-
-    }
-
-    /**
-     * Creates a fully initialized RowDescriptor.
-     * 
-     * @since 3.0
-     */
-    public RowDescriptor(ColumnDescriptor[] columns, ExtendedType[] 
converters) {
-        this.columns = columns;
-        this.converters = converters;
-    }
-
-    /**
-     * Returns a number of columns in a row.
-     */
-    public int getWidth() {
-        return columns.length;
-    }
-
-    /**
-     * Returns column descriptors.
-     */
-    public ColumnDescriptor[] getColumns() {
-        return columns;
-    }
-
-    /**
-     * Returns extended types for columns.
-     */
-    public ExtendedType[] getConverters() {
-        return converters;
-    }
-}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/RowDescriptorBuilder.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/RowDescriptorBuilder.java
deleted file mode 100644
index 68c6a7b28..000000000
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/RowDescriptorBuilder.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you 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
- *
- *    https://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 org.apache.cayenne.access.jdbc;
-
-import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.types.ExtendedType;
-import org.apache.cayenne.access.types.ExtendedTypeMap;
-import org.apache.cayenne.dba.TypesMapping;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.sql.ResultSet;
-import java.sql.ResultSetMetaData;
-import java.sql.SQLException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.Function;
-
-/**
- * A builder class that helps to assemble {@link RowDescriptor} instances from 
various
- * types of inputs.
- * 
- * @since 3.0
- */
-public class RowDescriptorBuilder {
-
-    private static final Logger LOGGER = 
LoggerFactory.getLogger(RowDescriptorBuilder.class);
-
-    private static final Function<String, String> UPPERCASE_TRANSFORMER = 
input ->
-            input != null ? input.toUpperCase() : null;
-
-    private static final Function<String, String> LOWERCASE_TRANSFORMER = 
input ->
-            input != null ? input.toLowerCase() : null;
-
-    protected ColumnDescriptor[] columns;
-    protected ResultSetMetaData resultSetMetadata;
-
-    protected Function<String, String> caseTransformer;
-    protected Map<String, String> typeOverrides;
-
-    private boolean mergeColumnsWithRsMetadata;
-
-    protected boolean validateDuplicateColumnNames;
-
-    /**
-     * Returns a RowDescriptor built based on the builder internal state.
-     */
-    public RowDescriptor getDescriptor(ExtendedTypeMap typeMap) throws 
SQLException, IllegalStateException {
-
-        ColumnDescriptor[] columnsForRD;
-
-        if (this.resultSetMetadata != null) {
-            // do merge between explicitly-set columns and ResultSetMetadata
-            // explicitly-set columns take precedence
-            columnsForRD = mergeResultSetAndPresetColumns();
-        } else if (this.columns != null) {
-            // use explicitly-set columns
-            columnsForRD = this.columns;
-        } else {
-            throw new IllegalStateException(
-                    "Can't build RowDescriptor, both 'columns' and 
'resultSetMetadata' are null");
-        }
-
-        columnsForRD = performTransformAndTypeOverride(columnsForRD);
-        ExtendedType[] converters = new ExtendedType[columnsForRD.length];
-        for (int i = 0; i < columnsForRD.length; i++) {
-            converters[i] = 
typeMap.getRegisteredType(columnsForRD[i].javaClass());
-        }
-
-        return new RowDescriptor(columnsForRD, converters);
-    }
-
-    /**
-     * @return array of columns for ResultSet with overriding 
ColumnDescriptors from
-     *         'columns' Note: column will be overlooked, if column name is 
empty
-     */
-    protected ColumnDescriptor[] mergeResultSetAndPresetColumns() throws 
SQLException {
-
-        int rsLen = resultSetMetadata.getColumnCount();
-        if (rsLen == 0) {
-            throw new CayenneRuntimeException("'ResultSetMetadata' is empty.");
-        }
-
-        int columnLen = (columns != null) ? columns.length : 0;
-
-        if(mergeColumnsWithRsMetadata && rsLen != columnLen) {
-            throw new CayenneRuntimeException("Size of 'ResultSetMetadata' not 
equals to size of 'columns'.");
-        } else if (rsLen < columnLen) {
-            throw new CayenneRuntimeException("'ResultSetMetadata' has less 
elements then 'columns'.");
-        } else if(rsLen == columnLen && !mergeColumnsWithRsMetadata) {
-            // 'columns' contains ColumnDescriptor for every column
-            // in resultSetMetadata. This return is for optimization.
-            return columns;
-        }
-
-        ColumnDescriptor[] rsColumns = new ColumnDescriptor[rsLen];
-        List<String> duplicates = null;
-        Set<String> uniqueNames = null;
-        if(validateDuplicateColumnNames) {
-            duplicates = new ArrayList<>();
-            uniqueNames = new HashSet<>();
-        }
-
-        int outputLen = 0;
-        for (int i = 0; i < rsLen; i++) {
-            String rowkey = resolveDataRowKeyFromResultSet(i + 1);
-            
-            // resolve column descriptor from 'columns' or create new
-            ColumnDescriptor descriptor = getColumnDescriptor(rowkey, columns, 
i + 1);
-
-            // validate uniqueness of names
-            if(validateDuplicateColumnNames) {
-                if(!uniqueNames.add(descriptor.dataRowKey())) {
-                    duplicates.add(descriptor.dataRowKey());
-                }
-            }
-            rsColumns[outputLen] = descriptor;
-            outputLen++;
-        }
-
-        if(validateDuplicateColumnNames && !duplicates.isEmpty()) {
-            LOGGER.warn("Found duplicated columns '{}' in row descriptor. " +
-                    "This can lead to errors when converting result to 
persistent objects.",
-                    String.join("', '", duplicates));
-        }
-
-        if (outputLen < rsLen) {
-            // cut ColumnDescriptor array
-            ColumnDescriptor[] rsColumnsCut = new ColumnDescriptor[outputLen];
-            System.arraycopy(rsColumns, 0, rsColumnsCut, 0, outputLen);
-            return rsColumnsCut;
-        }
-
-        return rsColumns;
-    }
-
-    /**
-     * @return ColumnDescriptor from columnArray, if columnArray contains 
descriptor for
-     *         this column, or new ColumnDescriptor.
-     */
-    private ColumnDescriptor getColumnDescriptor(
-            String rowKey,
-            ColumnDescriptor[] columnArray,
-            int position) throws SQLException {
-        int len = (columnArray != null) ? columnArray.length : 0;
-        // go through columnArray to find ColumnDescriptor for specified column
-        for (int i = 0; i < len; i++) {
-            if (columnArray[i] != null) {
-                if(mergeColumnsWithRsMetadata) {
-                    return new ColumnDescriptor(rowKey, rowKey, 
resultSetMetadata.getColumnType(position), columnArray[position - 
1].javaClass(), null);
-                } else {
-                    String columnRowKey = columnArray[i].dataRowKey();
-
-                    // TODO: andrus, 10/14/2009 - 'equalsIgnoreCase' check can 
result in
-                    // subtle bugs in DBs with case-sensitive column names (or 
when quotes are
-                    // used to force case sensitivity). Alternatively using 
'equals' may miss
-                    // columns in case-insensitive situations.
-                    if (columnRowKey != null && 
columnRowKey.equalsIgnoreCase(rowKey)) {
-                        return columnArray[i];
-                    }
-                }
-            }
-        }
-        // columnArray doesn't contain ColumnDescriptor for specified column
-        int jdbcType = resultSetMetadata.getColumnType(position);
-        return new ColumnDescriptor(rowKey, rowKey, jdbcType, 
TypesMapping.getJavaBySqlType(jdbcType), null);
-    }
-
-    /**
-     * Return not empty string with ColumnLabel or ColumnName or "column_" + 
position for
-     * for specified (by it's position) column in ResultSetMetaData.
-     */
-    private String resolveDataRowKeyFromResultSet(int position) throws 
SQLException {
-        String name = resultSetMetadata.getColumnLabel(position);
-        if (name == null || name.length() == 0) {
-            name = resultSetMetadata.getColumnName(position);
-            if (name == null) {
-                name = "";
-            }
-        }
-        return name;
-    }
-
-    private ColumnDescriptor[] 
performTransformAndTypeOverride(ColumnDescriptor[] columnArray) {
-        if (caseTransformer == null && typeOverrides == null) {
-            return columnArray;
-        }
-
-        ColumnDescriptor[] result = new ColumnDescriptor[columnArray.length];
-        for (int i = 0; i < columnArray.length; i++) {
-            ColumnDescriptor column = columnArray[i];
-
-            String name = column.name();
-            String dataRowKey = column.dataRowKey();
-            String javaClass = column.javaClass();
-
-            if (caseTransformer != null) {
-                name = caseTransformer.apply(name);
-                dataRowKey = caseTransformer.apply(dataRowKey);
-            }
-
-            // type overrides are keyed by the (possibly transformed) column 
name
-            if (typeOverrides != null) {
-                String type = typeOverrides.get(name);
-                if (type != null) {
-                    javaClass = type;
-                }
-            }
-
-            result[i] = new ColumnDescriptor(name, dataRowKey, 
column.jdbcType(), javaClass, column.attribute());
-        }
-        return result;
-    }
-
-    /**
-     * Sets an explicit set of columns. Note that the array passed as an 
argument can
-     * later be modified by the build to enforce column capitalization policy 
and columns
-     * Java types overrides.
-     */
-    public RowDescriptorBuilder setColumns(ColumnDescriptor[] columns) {
-        this.columns = columns;
-        return this;
-    }
-
-    public RowDescriptorBuilder setResultSet(ResultSet resultSet) throws 
SQLException {
-        this.resultSetMetadata = resultSet.getMetaData();
-        return this;
-    }
-
-    public RowDescriptorBuilder useLowercaseColumnNames() {
-        this.caseTransformer = LOWERCASE_TRANSFORMER;
-        return this;
-    }
-
-    public RowDescriptorBuilder useUppercaseColumnNames() {
-        this.caseTransformer = UPPERCASE_TRANSFORMER;
-        return this;
-    }
-
-    public RowDescriptorBuilder overrideColumnType(String columnName, String 
type) {
-
-        if (typeOverrides == null) {
-            typeOverrides = new HashMap<>();
-        }
-
-        typeOverrides.put(columnName, type);
-        return this;
-    }
-
-    /**
-     * Validate and report duplicate names of columns.
-     * @return this builder
-     */
-    public RowDescriptorBuilder validateDuplicateColumnNames() {
-        this.validateDuplicateColumnNames = true;
-        return this;
-    }
-
-    public boolean isOverriden(String columnName) {
-        return typeOverrides != null && typeOverrides.containsKey(columnName);
-    }
-
-    public RowDescriptorBuilder mergeColumnsWithRsMetadata() {
-        this.mergeColumnsWithRsMetadata = true;
-        return this;
-    }
-}
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
index a5afa378f..7044cae0a 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SQLTemplateAction.java
@@ -26,6 +26,7 @@ import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.sqltemplate.TranslatedSQL;
+import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
@@ -239,10 +240,10 @@ public class SQLTemplateAction implements SQLAction {
 
         boolean iteratedResult = callback.isIteratedResult();
         ExtendedTypeMap types = dataNode.getAdapter().getExtendedTypes();
-        RowDescriptorBuilder builder = configureRowDescriptorBuilder(compiled, 
resultSet);
+        ColumnDescriptor.RowBuilder rowBuilder = rowBuilder(compiled, 
resultSet);
         recreateQueryMetadata(resultSet);
         RowReader<?> rowReader = dataNode.getRowReaderFactory()
-                .rowReader(builder.getDescriptor(types), queryMetadata, 
dataNode.getAdapter());
+                .rowReader(rowBuilder.build(types), queryMetadata, 
dataNode.getAdapter());
         ResultIterator<?> it = new JDBCResultIterator<>(statement, resultSet, 
rowReader);
 
         if (iteratedResult) {
@@ -303,11 +304,13 @@ public class SQLTemplateAction implements SQLAction {
             return null;
         }
 
+        ExtendedTypeMap extendedTypes = 
dataNode.getAdapter().getExtendedTypes();
         int size = query.getResultColumnsTypes().size();
         ColumnDescriptor[] columnDescriptors = new ColumnDescriptor[size];
         for (int i = 0; i < size; i++) {
-            // only the Java class is known here; name and type are resolved 
later from ResultSet metadata
-            columnDescriptors[i] = new ColumnDescriptor(null, null, 0, 
query.getResultColumnsTypes().get(i).getCanonicalName(), null);
+            // only the Java class is known here; name and jdbcType are 
resolved later from ResultSet metadata
+            ExtendedType type = 
extendedTypes.getRegisteredType(query.getResultColumnsTypes().get(i));
+            columnDescriptors[i] = new ColumnDescriptor(null, null, 0, type, 
null);
         }
         return columnDescriptors;
     }
@@ -315,11 +318,11 @@ public class SQLTemplateAction implements SQLAction {
     /**
      * @since 3.0
      */
-    protected RowDescriptorBuilder configureRowDescriptorBuilder(TranslatedSQL 
compiled, ResultSet resultSet)
+    protected ColumnDescriptor.RowBuilder rowBuilder(TranslatedSQL compiled, 
ResultSet resultSet)
             throws SQLException {
-        RowDescriptorBuilder builder = new RowDescriptorBuilder()
-                .setResultSet(resultSet)
-                .setColumns(createColumnDescriptors(compiled))
+        ColumnDescriptor.RowBuilder builder = ColumnDescriptor.rowBuilder()
+                .resultSet(resultSet)
+                .columns(createColumnDescriptors(compiled))
                 .validateDuplicateColumnNames();
 
         if (query.getResultColumnsTypes() != null) {
@@ -340,10 +343,11 @@ public class SQLTemplateAction implements SQLAction {
 
         // override numeric Java types based on JDBC defaults for 
DbAttributes, as Oracle
         // ResultSetMetadata is not very precise about NUMERIC distinctions...
-        // (BigDecimal vs Long vs. Integer)
+        // (BigDecimal vs Long vs. Integer). These are fallbacks: the 
ObjAttribute overrides
+        // registered above take precedence, as the builder keeps the first 
override per column.
         if (dbEntity != null && isResultColumnTypesEmpty()) {
             for (DbAttribute attribute : dbEntity.getAttributes()) {
-                if (!builder.isOverriden(attribute.getName()) && 
TypesMapping.isNumeric(attribute.getType())) {
+                if (TypesMapping.isNumeric(attribute.getType())) {
                     builder.overrideColumnType(attribute.getName(), 
TypesMapping.getJavaBySqlType(attribute));
                 }
             }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
index feac78c3a..135d3d8c7 100644
--- a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/SelectAction.java
@@ -107,11 +107,11 @@ public class SelectAction extends BaseSQLAction {
             throw ex;
         }
 
-        RowDescriptor descriptor = new RowDescriptorBuilder()
-                .setColumns(translated.resultColumns())
-                .getDescriptor(dataNode.getAdapter().getExtendedTypes());
+        ColumnDescriptor[] columns = ColumnDescriptor.rowBuilder()
+                .columns(translated.resultColumns())
+                .build(dataNode.getAdapter().getExtendedTypes());
 
-        RowReader<?> rowReader = 
dataNode.getRowReaderFactory().rowReader(descriptor, queryMetadata, 
dataNode.getAdapter());
+        RowReader<?> rowReader = 
dataNode.getRowReaderFactory().rowReader(columns, queryMetadata, 
dataNode.getAdapter());
 
         ResultIterator<?> it = new JDBCResultIterator<>(statement, rs, 
rowReader);
         it = forIteratedResult(it, observer, connection, t1, translated.sql());
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/BaseRowReader.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/BaseRowReader.java
index fb26058dd..d1d5ae2a4 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/BaseRowReader.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/BaseRowReader.java
@@ -21,7 +21,6 @@ package org.apache.cayenne.access.jdbc.reader;
 import java.sql.ResultSet;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.query.QueryMetadata;
@@ -36,20 +35,19 @@ abstract class BaseRowReader<T> implements RowReader<T> {
     int[] types;
     String entityName;
 
-    BaseRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata) {
+    BaseRowReader(ColumnDescriptor[] columns, QueryMetadata queryMetadata) {
         ObjEntity rootObjEntity = queryMetadata.getObjEntity();
         if (rootObjEntity != null) {
             this.entityName = rootObjEntity.getName();
         }
 
-        this.converters = descriptor.getConverters();
-
-        ColumnDescriptor[] columns = descriptor.getColumns();
         int width = columns.length;
+        this.converters = new ExtendedType[width];
         this.labels = new String[width];
         this.types = new int[width];
 
         for (int i = 0; i < width; i++) {
+            converters[i] = columns[i].type();
             labels[i] = columns[i].dataRowKey();
             types[i] = columns[i].jdbcType();
         }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
index fdca01cdb..c2800b2b6 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.access.jdbc.reader;
 import java.util.List;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.query.EmbeddableResultSegment;
 import org.apache.cayenne.query.EntityResultSegment;
@@ -34,16 +34,16 @@ import org.apache.cayenne.query.ScalarResultSegment;
 public class DefaultRowReaderFactory implements RowReaderFactory {
 
        @Override
-       public RowReader<?> rowReader(RowDescriptor descriptor, QueryMetadata 
queryMetadata, DbAdapter adapter) {
+       public RowReader<?> rowReader(ColumnDescriptor[] columns, QueryMetadata 
queryMetadata, DbAdapter adapter) {
 
                List<Object> rsMapping = queryMetadata.getResultSetMapping();
                if (rsMapping == null) {
-                       return createFullRowReader(descriptor, queryMetadata);
+                       return createFullRowReader(columns, queryMetadata);
                }
 
                int resultWidth = rsMapping.size();
                if (resultWidth == 0) {
-                       throw new CayenneRuntimeException("Empty result 
descriptor");
+                       throw new CayenneRuntimeException("Empty result 
columns");
                }
 
                if (queryMetadata.isSingleResultSetMapping()) {
@@ -51,11 +51,11 @@ public class DefaultRowReaderFactory implements 
RowReaderFactory {
                        Object segment = rsMapping.get(0);
 
                        if (segment instanceof EntityResultSegment) {
-                               return createEntityRowReader(descriptor, 
queryMetadata, (EntityResultSegment) segment);
+                               return createEntityRowReader(columns, 
queryMetadata, (EntityResultSegment) segment);
                        } else if (segment instanceof EmbeddableResultSegment) {
-                               return createEmbeddableRowReader(descriptor, 
queryMetadata, (EmbeddableResultSegment) segment);
+                               return createEmbeddableRowReader(columns, 
queryMetadata, (EmbeddableResultSegment) segment);
                        } else {
-                               return createScalarRowReader(descriptor, 
queryMetadata, (ScalarResultSegment) segment);
+                               return createScalarRowReader(columns, 
queryMetadata, (ScalarResultSegment) segment);
                        }
                } else {
                        CompoundRowReader reader = new 
CompoundRowReader(resultWidth);
@@ -65,11 +65,11 @@ public class DefaultRowReaderFactory implements 
RowReaderFactory {
 
                                if (segment instanceof EntityResultSegment) {
                                        reader.addRowReader(i,
-                                                       
createEntityRowReader(descriptor, queryMetadata, (EntityResultSegment) 
segment));
+                                                       
createEntityRowReader(columns, queryMetadata, (EntityResultSegment) segment));
                                } else if(segment instanceof 
EmbeddableResultSegment) {
-                                       reader.addRowReader(i, 
createEmbeddableRowReader(descriptor, queryMetadata, (EmbeddableResultSegment) 
segment));
+                                       reader.addRowReader(i, 
createEmbeddableRowReader(columns, queryMetadata, (EmbeddableResultSegment) 
segment));
                                } else {
-                                       reader.addRowReader(i, 
createScalarRowReader(descriptor, queryMetadata, (ScalarResultSegment) 
segment));
+                                       reader.addRowReader(i, 
createScalarRowReader(columns, queryMetadata, (ScalarResultSegment) segment));
                                }
                        }
 
@@ -77,34 +77,34 @@ public class DefaultRowReaderFactory implements 
RowReaderFactory {
                }
        }
 
-       private RowReader<?> createEmbeddableRowReader(RowDescriptor 
descriptor, QueryMetadata queryMetadata, EmbeddableResultSegment segment) {
-               return new EmbeddableRowReader(descriptor, queryMetadata, 
segment);
+       private RowReader<?> createEmbeddableRowReader(ColumnDescriptor[] 
columns, QueryMetadata queryMetadata, EmbeddableResultSegment segment) {
+               return new EmbeddableRowReader(columns, queryMetadata, segment);
        }
 
-       protected RowReader<?> createScalarRowReader(RowDescriptor descriptor, 
QueryMetadata queryMetadata, ScalarResultSegment segment) {
-               return new ScalarRowReader<>(descriptor, segment);
+       protected RowReader<?> createScalarRowReader(ColumnDescriptor[] 
columns, QueryMetadata queryMetadata, ScalarResultSegment segment) {
+               return new ScalarRowReader<>(columns, segment);
        }
 
-       protected RowReader<?> createEntityRowReader(RowDescriptor descriptor, 
QueryMetadata queryMetadata,
+       protected RowReader<?> createEntityRowReader(ColumnDescriptor[] 
columns, QueryMetadata queryMetadata,
                        EntityResultSegment resultMetadata) {
 
                if (queryMetadata.getPageSize() > 0) {
-                       return new IdRowReader<>(descriptor, queryMetadata, 
resultMetadata);
+                       return new IdRowReader<>(columns, queryMetadata, 
resultMetadata);
                } else if (resultMetadata.getClassDescriptor() != null && 
resultMetadata.getClassDescriptor().hasSubclasses()) {
-                       return new InheritanceAwareEntityRowReader(descriptor, 
resultMetadata);
+                       return new InheritanceAwareEntityRowReader(columns, 
resultMetadata);
                } else {
-                       return new EntityRowReader(descriptor, resultMetadata);
+                       return new EntityRowReader(columns, resultMetadata);
                }
        }
 
-       protected RowReader<?> createFullRowReader(RowDescriptor descriptor, 
QueryMetadata queryMetadata) {
+       protected RowReader<?> createFullRowReader(ColumnDescriptor[] columns, 
QueryMetadata queryMetadata) {
 
                if (queryMetadata.getPageSize() > 0) {
-                       return new IdRowReader<>(descriptor, queryMetadata, 
null);
+                       return new IdRowReader<>(columns, queryMetadata, null);
                } else if (queryMetadata.getClassDescriptor() != null && 
queryMetadata.getClassDescriptor().hasSubclasses()) {
-                       return new InheritanceAwareRowReader(descriptor, 
queryMetadata);
+                       return new InheritanceAwareRowReader(columns, 
queryMetadata);
                } else {
-                       return new FullRowReader(descriptor, queryMetadata);
+                       return new FullRowReader(columns, queryMetadata);
                }
        }
 
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/EmbeddableRowReader.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/EmbeddableRowReader.java
index c08025669..79e77ca4e 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/EmbeddableRowReader.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/EmbeddableRowReader.java
@@ -24,7 +24,6 @@ import java.sql.ResultSet;
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.query.EmbeddableResultSegment;
 import org.apache.cayenne.query.QueryMetadata;
@@ -41,17 +40,15 @@ class EmbeddableRowReader implements RowReader<DataRow> {
     private final String[] labels;
     private final int[] types;
 
-    EmbeddableRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, 
EmbeddableResultSegment segment) {
+    EmbeddableRowReader(ColumnDescriptor[] columns, QueryMetadata 
queryMetadata, EmbeddableResultSegment segment) {
         int segmentWidth = segment.getFields().size();
         this.startIndex = segment.getColumnOffset();
         this.converters = new ExtendedType[segmentWidth];
         this.types = new int[segmentWidth];
         this.labels = new String[segmentWidth];
 
-        ExtendedType[] converters = descriptor.getConverters();
-        ColumnDescriptor[] columns = descriptor.getColumns();
         for (int i = 0; i < segmentWidth; i++) {
-            this.converters[i] = converters[startIndex + i];
+            this.converters[i] = columns[startIndex + i].type();
             types[i] = columns[startIndex + i].jdbcType();
             labels[i] = segment.getFields().get(columns[startIndex +i].name());
         }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/EntityRowReader.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/EntityRowReader.java
index 4edb97a5f..e6b1bd2e4 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/EntityRowReader.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/EntityRowReader.java
@@ -23,7 +23,6 @@ import java.sql.ResultSet;
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.query.EntityResultSegment;
 import org.apache.cayenne.reflect.ClassDescriptor;
@@ -42,7 +41,7 @@ class EntityRowReader implements RowReader<DataRow> {
     private int mapCapacity;
     private int startIndex;
 
-    EntityRowReader(RowDescriptor descriptor, EntityResultSegment 
segmentMetadata) {
+    EntityRowReader(ColumnDescriptor[] columns, EntityResultSegment 
segmentMetadata) {
 
         ClassDescriptor classDescriptor = segmentMetadata.getClassDescriptor();
 
@@ -56,10 +55,8 @@ class EntityRowReader implements RowReader<DataRow> {
         this.types = new int[segmentWidth];
         this.labels = new String[segmentWidth];
 
-        ExtendedType[] converters = descriptor.getConverters();
-        ColumnDescriptor[] columns = descriptor.getColumns();
         for (int i = 0; i < segmentWidth; i++) {
-            this.converters[i] = converters[startIndex + i];
+            this.converters[i] = columns[startIndex + i].type();
             types[i] = columns[startIndex + i].jdbcType();
 
             // query translator may change the order of fields compare to the 
entity
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/FullRowReader.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/FullRowReader.java
index cafab68aa..6634b6276 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/FullRowReader.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/FullRowReader.java
@@ -22,7 +22,7 @@ import java.sql.ResultSet;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataRow;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.query.QueryMetadata;
 import org.apache.cayenne.util.Util;
 
@@ -33,9 +33,9 @@ class FullRowReader extends BaseRowReader<DataRow> {
 
     int mapCapacity;
 
-    FullRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata) {
-        super(descriptor, queryMetadata);
-        this.mapCapacity = (int) Math.ceil((descriptor.getWidth()) / 0.75);
+    FullRowReader(ColumnDescriptor[] columns, QueryMetadata queryMetadata) {
+        super(columns, queryMetadata);
+        this.mapCapacity = (int) Math.ceil(columns.length / 0.75);
     }
 
     @Override
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java
index af004af66..cb5add474 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/IdRowReader.java
@@ -23,7 +23,6 @@ import java.sql.ResultSet;
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.query.EntityResultSegment;
@@ -37,8 +36,8 @@ class IdRowReader<T> extends BaseRowReader<T> {
 
     protected int[] pkIndices;
 
-    public IdRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, 
EntityResultSegment resultMetadata) {
-        super(descriptor, queryMetadata);
+    public IdRowReader(ColumnDescriptor[] columns, QueryMetadata 
queryMetadata, EntityResultSegment resultMetadata) {
+        super(columns, queryMetadata);
 
         DbEntity dbEntity = resultMetadata == null
                 ? queryMetadata.getDbEntity()
@@ -54,8 +53,6 @@ class IdRowReader<T> extends BaseRowReader<T> {
             throw new CayenneRuntimeException("Root DBEntity has no PK 
defined: %s", dbEntity);
         }
 
-        ColumnDescriptor[] columns = descriptor.getColumns();
-
         int[] pk = new int[len];
         int offset = resultMetadata != null ?
                 resultMetadata.getColumnOffset() :
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/InheritanceAwareEntityRowReader.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/InheritanceAwareEntityRowReader.java
index f51ad058e..2afe70fc4 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/InheritanceAwareEntityRowReader.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/InheritanceAwareEntityRowReader.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.access.jdbc.reader;
 import java.sql.ResultSet;
 
 import org.apache.cayenne.DataRow;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.map.EntityInheritanceTree;
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.query.EntityResultSegment;
@@ -33,9 +33,9 @@ class InheritanceAwareEntityRowReader extends EntityRowReader 
{
 
     private EntityInheritanceTree entityInheritanceTree;
 
-    public InheritanceAwareEntityRowReader(RowDescriptor descriptor, 
EntityResultSegment segmentMetadata) {
+    public InheritanceAwareEntityRowReader(ColumnDescriptor[] columns, 
EntityResultSegment segmentMetadata) {
 
-        super(descriptor, segmentMetadata);
+        super(columns, segmentMetadata);
         this.entityInheritanceTree = 
segmentMetadata.getClassDescriptor().getEntityInheritanceTree();
     }
 
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/InheritanceAwareRowReader.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/InheritanceAwareRowReader.java
index 06c61a20b..fdcdc3262 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/InheritanceAwareRowReader.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/InheritanceAwareRowReader.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.access.jdbc.reader;
 import java.sql.ResultSet;
 
 import org.apache.cayenne.DataRow;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.map.EntityInheritanceTree;
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.query.QueryMetadata;
@@ -33,8 +33,8 @@ class InheritanceAwareRowReader extends FullRowReader {
 
     private EntityInheritanceTree entityInheritanceTree;
 
-    InheritanceAwareRowReader(RowDescriptor descriptor, QueryMetadata 
queryMetadata) {
-        super(descriptor, queryMetadata);
+    InheritanceAwareRowReader(ColumnDescriptor[] columns, QueryMetadata 
queryMetadata) {
+        super(columns, queryMetadata);
         this.entityInheritanceTree = 
queryMetadata.getClassDescriptor().getEntityInheritanceTree();
     }
 
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/RowReaderFactory.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/RowReaderFactory.java
index 75a9b21b5..39bdc792c 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/RowReaderFactory.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/RowReaderFactory.java
@@ -18,7 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.access.jdbc.reader;
 
-import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.query.QueryMetadata;
 
@@ -29,5 +29,5 @@ import org.apache.cayenne.query.QueryMetadata;
  */
 public interface RowReaderFactory {
 
-    RowReader<?> rowReader(RowDescriptor descriptor, QueryMetadata 
queryMetadata, DbAdapter adapter);
+    RowReader<?> rowReader(ColumnDescriptor[] columns, QueryMetadata 
queryMetadata, DbAdapter adapter);
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/ScalarRowReader.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/ScalarRowReader.java
index bb36b6e86..4f6db5cbe 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/ScalarRowReader.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/jdbc/reader/ScalarRowReader.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.access.jdbc.reader;
 import java.sql.ResultSet;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.query.ScalarResultSegment;
 import org.apache.cayenne.util.Util;
@@ -36,10 +36,10 @@ class ScalarRowReader<T> implements RowReader<T> {
     private final int type;
 
     @SuppressWarnings("unchecked")
-    ScalarRowReader(RowDescriptor descriptor, ScalarResultSegment 
segmentMetadata) {
+    ScalarRowReader(ColumnDescriptor[] columns, ScalarResultSegment 
segmentMetadata) {
         int scalarIndex = segmentMetadata.getColumnOffset();
-        this.converter = descriptor.getConverters()[scalarIndex];
-        this.type = descriptor.getColumns()[scalarIndex].jdbcType();
+        this.converter = (ExtendedType<T>) columns[scalarIndex].type();
+        this.type = columns[scalarIndex].jdbcType();
         this.index = scalarIndex + 1;
     }
 
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStage.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStage.java
index eb43855aa..0ec865f86 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStage.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/select/ColumnDescriptorStage.java
@@ -20,6 +20,7 @@
 package org.apache.cayenne.access.translator.select;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
+import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.map.DbAttribute;
 
 /**
@@ -46,8 +47,9 @@ class ColumnDescriptorStage implements TranslationStage {
                 name = "__c" + i++;
             }
 
+            ExtendedType type = 
context.getAdapter().getExtendedTypes().getRegisteredType(resultNode.getJavaType());
             ColumnDescriptor descriptor = new ColumnDescriptor(
-                    name, resultNode.getDataRowKey(), 
resultNode.getJdbcType(), resultNode.getJavaType(), attribute);
+                    name, resultNode.getDataRowKey(), 
resultNode.getJdbcType(), type, attribute);
 
             context.getColumnDescriptors().add(descriptor);
         }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/sqltemplate/Context.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/sqltemplate/Context.java
index 61bac2832..53d5a71ee 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/sqltemplate/Context.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/sqltemplate/Context.java
@@ -68,6 +68,16 @@ public class Context {
         return adapter.preferredBindingType(jdbcType);
     }
 
+    /**
+     * Resolves the {@link ExtendedType} for the given Java class name via the 
target adapter. A null class name resolves
+     * to the adapter's default type.
+     *
+     * @since 5.0
+     */
+    public ExtendedType<?> getExtendedType(String javaClassName) {
+        return adapter.getExtendedTypes().getRegisteredType(javaClassName);
+    }
+
     public Directive getDirective(String name) {
         return directives.get(name);
     }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/sqltemplate/directive/Result.java
 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/sqltemplate/directive/Result.java
index 19c7cb978..96337f2ff 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/access/translator/sqltemplate/directive/Result.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/access/translator/sqltemplate/directive/Result.java
@@ -100,7 +100,7 @@ public class Result implements Directive {
             jdbcType = (int) expressions[4].evaluateAsLong(context);
         }
 
-        context.addColumnDescriptor(new ColumnDescriptor(column, label, 
jdbcType, javaClass, null));
+        context.addColumnDescriptor(new ColumnDescriptor(column, label, 
jdbcType, context.getExtendedType(javaClass), null));
 
         context.getBuilder().append(column);
         if (!Util.isEmptyString(alias) && !alias.equals(column)) {
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/db2/DB2ProcedureAction.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/db2/DB2ProcedureAction.java
index 95848f97e..0d13c7010 100644
--- a/cayenne/src/main/java/org/apache/cayenne/dba/db2/DB2ProcedureAction.java
+++ b/cayenne/src/main/java/org/apache/cayenne/dba/db2/DB2ProcedureAction.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.dba.db2;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.ProcedureAction;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.translator.procedure.TranslatedProcedure;
 import org.apache.cayenne.query.ProcedureQuery;
 
@@ -64,8 +64,8 @@ class DB2ProcedureAction extends ProcedureAction {
                                if (hasResultSet) {
 
                                        try (ResultSet rs = 
statement.getResultSet();) {
-                                               RowDescriptor descriptor = 
describeResultSet(rs, processedResultSets++);
-                                               readResultSet(rs, descriptor, 
query, observer);
+                                               ColumnDescriptor[] columns = 
describeResultSet(rs, processedResultSets++);
+                                               readResultSet(rs, columns, 
query, observer);
                                        }
                                } else {
                                        int updateCount = 
statement.getUpdateCount();
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java 
b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java
index 10982a2f5..55df3c3a2 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/mysql/MySQLProcedureAction.java
@@ -21,7 +21,7 @@ package org.apache.cayenne.dba.mysql;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.ProcedureAction;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.translator.procedure.TranslatedProcedure;
 import org.apache.cayenne.query.ProcedureQuery;
 
@@ -86,8 +86,8 @@ class MySQLProcedureAction extends ProcedureAction {
                ResultSet rs = Objects.requireNonNull(statement.getResultSet());
 
                try {
-                       RowDescriptor descriptor = describeResultSet(rs, 
processedResultSets++);
-                       readResultSet(rs, descriptor, query, observer);
+                       ColumnDescriptor[] columns = describeResultSet(rs, 
processedResultSets++);
+                       readResultSet(rs, columns, query, observer);
                } finally {
                        try {
                                rs.close();
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleProcedureAction.java
 
b/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleProcedureAction.java
index 7d25a2cf8..37db32592 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleProcedureAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleProcedureAction.java
@@ -28,8 +28,8 @@ import java.util.List;
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.jdbc.ProcedureAction;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.map.ProcedureParameter;
@@ -69,8 +69,8 @@ class OracleProcedureAction extends ProcedureAction {
                        if (parameter.getType() == 
OracleAdapter.getOracleCursorType()) {
 
                                try (ResultSet rs = (ResultSet) 
statement.getObject(i + 1);) {
-                                       RowDescriptor rsDescriptor = 
describeResultSet(rs, processedResultSets++);
-                                       readResultSet(rs, rsDescriptor, query, 
delegate);
+                                       ColumnDescriptor[] rsColumns = 
describeResultSet(rs, processedResultSets++);
+                                       readResultSet(rs, rsColumns, query, 
delegate);
                                }
                        }
                        // ==== end Oracle-specific part
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTemplateAction.java
 
b/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTemplateAction.java
index cb8c39c46..e301a0784 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTemplateAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTemplateAction.java
@@ -18,19 +18,17 @@
  ****************************************************************/
 package org.apache.cayenne.dba.oracle;
 
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
-import org.apache.cayenne.access.jdbc.RowDescriptorBuilder;
-import org.apache.cayenne.access.translator.sqltemplate.TranslatedSQL;
 import org.apache.cayenne.access.jdbc.SQLTemplateAction;
+import org.apache.cayenne.access.translator.sqltemplate.TranslatedSQL;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.query.SQLTemplate;
 
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.Statement;
+
 /**
  * @since 3.0
  */
@@ -57,16 +55,4 @@ class OracleSQLTemplateAction extends SQLTemplateAction {
 
                super.processSelectResult(compiled, connection, statement, 
resultSet, callback, startTime);
        }
-
-       /**
-        * @since 3.0
-        */
-       @Override
-       protected RowDescriptorBuilder 
configureRowDescriptorBuilder(TranslatedSQL compiled, ResultSet resultSet)
-                       throws SQLException {
-
-               RowDescriptorBuilder builder = 
super.configureRowDescriptorBuilder(compiled, resultSet);
-
-               return builder;
-       }
 }
diff --git 
a/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerProcedureAction.java
 
b/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerProcedureAction.java
index 705df6a3d..35674b832 100644
--- 
a/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerProcedureAction.java
+++ 
b/cayenne/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerProcedureAction.java
@@ -24,7 +24,7 @@ import org.apache.cayenne.ResultIterator;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.OperationObserver;
 import org.apache.cayenne.access.jdbc.ProcedureAction;
-import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.translator.procedure.TranslatedProcedure;
 import org.apache.cayenne.query.ProcedureQuery;
 import org.apache.cayenne.query.Query;
@@ -83,8 +83,8 @@ public class SQLServerProcedureAction extends ProcedureAction 
{
                                if (hasResultSet) {
 
                                        try (ResultSet rs = 
statement.getResultSet();) {
-                                               RowDescriptor descriptor = 
describeResultSet(rs, processedResultSets++);
-                                               readResultSet(rs, descriptor, 
query, localObserver);
+                                               ColumnDescriptor[] columns = 
describeResultSet(rs, processedResultSets++);
+                                               readResultSet(rs, columns, 
query, localObserver);
                                        }
                                } else {
                                        int updateCount = 
statement.getUpdateCount();
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/access/jdbc/JDBCResultIteratorTest.java
 
b/cayenne/src/test/java/org/apache/cayenne/access/jdbc/JDBCResultIteratorTest.java
index 077dffcfa..f88d9ddae 100644
--- 
a/cayenne/src/test/java/org/apache/cayenne/access/jdbc/JDBCResultIteratorTest.java
+++ 
b/cayenne/src/test/java/org/apache/cayenne/access/jdbc/JDBCResultIteratorTest.java
@@ -48,8 +48,8 @@ public class JDBCResultIteratorTest {
                TestResultSet rs = new TestResultSet("rs");
                rs.addColumn("a", new Object[] { "1", "2", "3" });
 
-               RowDescriptor descriptor = new 
RowDescriptorBuilder().setResultSet(rs).getDescriptor(new ExtendedTypeMap());
-               RowReader<?> rowReader = new 
DefaultRowReaderFactory().rowReader(descriptor, new MockQueryMetadata(),
+               ColumnDescriptor[] columns = 
ColumnDescriptor.rowBuilder().resultSet(rs).build(new ExtendedTypeMap());
+               RowReader<?> rowReader = new 
DefaultRowReaderFactory().rowReader(columns, new MockQueryMetadata(),
                                mock(DbAdapter.class));
 
                JDBCResultIterator it = new JDBCResultIterator(s, rs, 
rowReader);
diff --git 
a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/MockSelectTranslatorContext.java
 
b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/MockSelectTranslatorContext.java
index 4bb1b61d3..e7cb994bf 100644
--- 
a/cayenne/src/test/java/org/apache/cayenne/access/translator/select/MockSelectTranslatorContext.java
+++ 
b/cayenne/src/test/java/org/apache/cayenne/access/translator/select/MockSelectTranslatorContext.java
@@ -19,17 +19,25 @@
 
 package org.apache.cayenne.access.translator.select;
 
+import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.map.EntityResolver;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class MockSelectTranslatorContext extends SelectTranslatorContext {
     MockSelectTranslatorContext(TranslatableQueryWrapper query) {
-        super(query, mock(DbAdapter.class), null, null);
+        super(query, mockAdapter(), null, null);
     }
 
     MockSelectTranslatorContext(TranslatableQueryWrapper query, EntityResolver 
resolver) {
-        super(query, mock(DbAdapter.class), resolver, null);
+        super(query, mockAdapter(), resolver, null);
+    }
+
+    private static DbAdapter mockAdapter() {
+        DbAdapter adapter = mock(DbAdapter.class);
+        when(adapter.getExtendedTypes()).thenReturn(new ExtendedTypeMap());
+        return adapter;
     }
 }


Reply via email to