=== modified file 'src/com/goldencode/p2j/jmx/FwdServerJMX.java'
--- old/src/com/goldencode/p2j/jmx/FwdServerJMX.java	2025-11-11 13:00:24 +0000
+++ new/src/com/goldencode/p2j/jmx/FwdServerJMX.java	2026-06-10 08:13:45 +0000
@@ -248,6 +248,29 @@
       OrmDataSetParam,
       /** Instrumentation for <code>DataSet.createDynamicDataSet</code>. */
       OrmDynamicDataSetParam,
+      
+      /** Lazily hydrated an new record (not rehydrated from cache) */
+      HydratorNewRecord,
+      /** Lazily hydrated a record (including rehydration from cache) */
+      HydratorLazyRecord,
+      /** Fully or partially hydrated a record, but not lazy */
+      HydratorNonLazyRecord,
+      /** The hydrator token matched the result-set token */
+      HydratorHit,
+      /** The hydrator token mismatched the result-set token */
+      HydratorMiss,
+      /** The hydrator fallbacked to refresh and succeeded */
+      HyratorSuccessfullRefresh,
+      /** The hydrator fallbacked to refresh and failed */
+      HyratorUnsuccessfullRefresh,
+      /** Created a {@code ScrollableResults} instance with lazy results */
+      HydratorLazyScrollable,
+      /** Created a {@code ScrollableResults} instance without lazy results */
+      HydratorNonLazyScrollable,
+      /** Executed uniqueResult instance with lazy results */
+      HydratorLazyUnique,
+      /** Executed uniqueResult instance without lazy results */
+      HydratorNonLazyUnique,
 
       /** Instrumentation for <code>OutputTableCopier.finished</code> */
       OutputTableCopier,
@@ -489,6 +512,45 @@
    public static enum Counter
    implements OptionalCounter
    {
+      TempTotalFields,
+      PersistentTotalFields,
+      TempHydratedRecordsWhenSessionCloses,
+      PersistentHydratedRecordsWhenSessionCloses,
+      TempHydratedFieldsWhenSessionCloses,
+      PersistentHydratedFieldsWhenSessionCloses,
+      PersistentHydratedFieldsWithLZS,
+      PersistentHydratedFieldsWithLoader,
+      TempHydratedFieldsWithLZS,
+      TempHydratedFieldsWithLoader,
+      
+      
+      /** Lazily hydrated an new record (not rehydrated from cache) */
+      HydratorNewRecord,
+      /** Lazily hydrated a record (including rehydration from cache) */
+      HydratorLazyRecord,
+      /** Fully or partially hydrated a record, but not lazy */
+      HydratorNonLazyRecord,
+      /** The hydrator token matched the result-set token */
+      HydratorHit,
+      /** The hydrator token mismatched the result-set token */
+      HydratorMiss,
+      /** The hydrator fallbacked to refresh and succeeded */
+      HyratorSuccessfullRefresh,
+      /** The hydrator fallbacked to refresh and failed */
+      HyratorUnsuccessfullRefresh,
+      /** Created a {@code ScrollableResults} instance with lazy results */
+      HydratorLazyScrollable,
+      /** Created a {@code ScrollableResults} instance without lazy results */
+      HydratorNonLazyScrollable,
+      /** Executed uniqueResult instance with lazy results */
+      HydratorLazyUnique,
+      /** Executed uniqueResult instance without lazy results */
+      HydratorNonLazyUnique,
+      /** A call-site's field-usage profile locked in the EAGER hydration verdict */
+      HydrationVerdictEager,
+      /** A call-site's field-usage profile locked in the LAZY hydration verdict */
+      HydrationVerdictLazy,
+
       /** Incomning network traffic */
       NetworkReads,
       /** Outgoing network traffic */

=== modified file 'src/com/goldencode/p2j/persist/Persistence.java'
--- old/src/com/goldencode/p2j/persist/Persistence.java	2025-11-06 15:39:50 +0000
+++ new/src/com/goldencode/p2j/persist/Persistence.java	2026-06-10 08:15:57 +0000
@@ -751,6 +751,7 @@
 import java.util.function.*;
 import java.util.logging.*;
 import com.goldencode.cache.*;
+import com.goldencode.p2j.jmx.*;
 import com.goldencode.p2j.persist.deploy.*;
 import com.goldencode.p2j.persist.dialect.*;
 import com.goldencode.p2j.persist.dirty.DirtyShareSupport;
@@ -925,7 +926,17 @@
     * name.
     */
    private final ConcurrentHashMap<String, Tenant> tenantData = new ConcurrentHashMap<>();
-   
+
+   /**
+    * Bounded per-database map of runtime field-usage profiles, keyed by FQL. Used by the
+    * lazy-feedback hydration layer to decide eager vs lazy hydration per call-site. Hosted on the
+    * per-database {@code Persistence} singleton (not the per-session context) so that all sessions
+    * against this database share the learned verdicts, while different databases stay isolated. The
+    * map is capped (see {@code FieldUsageProfile.maxProfiles()}): dynamic queries generate unbounded
+    * distinct FQL strings, so once the cap is hit new call-sites get no profile and hydrate eagerly.
+    */
+   private final ConcurrentHashMap<String, FieldUsageProfile> usageProfiles = new ConcurrentHashMap<>();
+
    /**
     * A container class which stores tenant-specific objects. Although the persistence tracks all these
     * {@code Tenant}, at any given moment, only one of is active in each user context.
@@ -1036,7 +1047,42 @@
    {
       return PersistenceFactory.getInstance(name);
    }
-   
+
+   /**
+    * Resolve (creating if necessary) the runtime field-usage profile for the given FQL call-site.
+    * Used by the lazy-feedback hydration layer to decide eager vs lazy hydration per call-site.
+    *
+    * @param   fql
+    *          The FQL identifying the call-site.
+    *
+    * @return  The shared profile for the call-site, or {@code null} when the feature is disabled or
+    *          the per-database profile cap has been reached (in which case the call-site hydrates
+    *          eagerly, the safe default).
+    */
+   public FieldUsageProfile getOrCreateProfile(String fql)
+   {
+      if (!FieldUsageProfile.isEnabled() || fql == null)
+      {
+         return null;
+      }
+
+      FieldUsageProfile profile = usageProfiles.get(fql);
+      if (profile != null)
+      {
+         return profile;
+      }
+
+      // hard create-cap: dynamic queries produce unbounded distinct FQL, so refuse new profiles once
+      // full rather than evicting (a missing profile simply means EAGER). The size check races with
+      // concurrent creators, but only ever overshoots the cap by the number of racing threads.
+      if (usageProfiles.size() >= FieldUsageProfile.maxProfiles())
+      {
+         return null;
+      }
+
+      return usageProfiles.computeIfAbsent(fql, FieldUsageProfile::new);
+   }
+
    /**
     * Initialize various infrastructure components of the persistence framework:
     * <ul>
@@ -1947,15 +1993,14 @@
       {
          local.closeSession(true);
          ErrorEntry ee = exc.getErrorEntry();
-         ErrorManager.recordOrShowError(
-                  new int[] {ee.num}, 
-                  new String[] {ee.text}, 
-                  true, 
-                  ee.prefix, 
-                  false,
-                  false,
-                  false,
-                  true);
+         ErrorManager.recordOrShowError(new int[] {ee.num}, 
+                                        new String[] {ee.text},
+                                        true,
+                                        ee.prefix,
+                                        false,
+                                        false,
+                                        false,
+                                        true);
          handleException(ErrorManager.buildErrorText(ee.num, ee.text, ee.prefix, false), exc);
       }
       catch (PersistenceException exc)
@@ -5248,6 +5293,9 @@
       /** FQL statement */
       private final String fql;
       
+      /** Database dialect */
+      private final Dialect dialect;
+      
       /** Does the query was generated with maximum results option? */
       private final boolean hasMaxResults;
       
@@ -5268,21 +5316,25 @@
        * 
        * @param   hql
        *          FQL statement
+       * @param   dialect
+       *          Database dialect.
        * @param   maxResults
        *          Maximum results.
        * @param   startOffset
        *          Starting row offset.
        */
-      QueryKey(String hql, int maxResults, int startOffset, DataRelation relation, boolean lazyMode)
+      QueryKey(String hql, Dialect dialect, int maxResults, int startOffset, DataRelation relation, boolean lazyMode)
       {
          this.fql = hql;
+         this.dialect = dialect;
          this.hasMaxResults = maxResults > 0;
          this.hasStartOffset = startOffset > 0;
          this.whereStr = relation == null ? "" : relation.getWhereString().toJavaType();
          this.lazyMode = lazyMode;
          
          int result = 17;
-         result = 37 * result + hql.hashCode();
+         result = 37 * result + fql.hashCode();
+         result = 37 * result + dialect.hashCode();
          result = 37 * result + (hasMaxResults ? 1 : 0) + (hasStartOffset ? 2 : 0);
          result = 37 * result + whereStr.hashCode();
          result = 37 * result + (lazyMode ? 1 : 0);
@@ -5308,6 +5360,7 @@
          QueryKey that = (QueryKey) o;
          
          return this.hash == that.hash                     &&
+                this.dialect.equals(that.dialect)          &&
                 this.hasMaxResults == that.hasMaxResults   &&
                 this.hasStartOffset == that.hasStartOffset &&
                 this.fql.equals(that.fql) &&
@@ -5334,7 +5387,7 @@
       @Override
       public String toString()
       {
-         return fql + "[" + hasMaxResults + ":" + hasStartOffset + "];" + whereStr;
+         return fql + "[" + hasMaxResults + ":" + hasStartOffset + "] (" + dialect + ") " + whereStr;
       }
    }
    
@@ -5561,28 +5614,21 @@
          Query query = null;
          fql = fql.trim(); // just in case
          
-         // TODO: use the dialect also? I.e: is it possible to have different SQL for same FQL
-         //       if different dialects are used?
-         QueryKey key = new QueryKey(fql, maxResults, startOffset, relation, lazyMode);
+         QueryKey key = new QueryKey(fql, dialect, maxResults, startOffset, relation, lazyMode);
          
-         synchronized (staticQueryCache)
+         query = staticQueryCache.get(key);
+         if (query == null)
          {
-            query = staticQueryCache.get(key);
-            if (query == null)
+            Consumer<Query> c = (q) ->
             {
-               query = Session.createQuery(q ->
-               {
-                  synchronized (staticQueryCache)
-                  {
-                     // NOTE: the FQL string is not converted to SQL at this time. The conversion
-                     //       is performed once, lazily, when the first navigation is required by a
-                     //       call of list(), scroll(), or uniqueResult() on the new query
-                     
-                     staticQueryCache.put(key, q);
-                  }
-               },
-               fql);
-            }
+               // NOTE: the FQL string is not converted to SQL at this time. The conversion
+               //       is performed once, lazily, when the first navigation is required by a
+               //       call of list(), scroll(), or uniqueResult() on the new query
+               
+               staticQueryCache.put(key, q);
+            };
+            
+            query = Session.createQuery(c, fql);
          }
          
          // update [maxResults] and [startOffset] because the cached query might have set 
@@ -5979,6 +6025,7 @@
                   try
                   {
                      session.associate(record);
+                     record.refreshHydrator(session);
                   }
                   catch (StaleRecordException exc)
                   {
@@ -6142,7 +6189,7 @@
          try
          {
             closingSession = true;
-            
+            hydrateActiveRecords();
             if (isTransactionOpen())
             {
                if (LOG.isLoggable(Level.WARNING))
@@ -6221,6 +6268,24 @@
          }
       }
       
+      private void hydrateActiveRecords()
+      {
+         Iterator<RecordBuffer> iter = bufferManager.activeBuffers(Persistence.this);
+         
+         Record record;
+         
+         while (iter.hasNext())
+         {
+            record = iter.next().getCurrentRecord();
+            if (record.primaryKey() < 0)
+            {
+               continue;
+            }
+            
+            record.hydrateRemainingFields();
+         }
+      }
+      
       /**
        * Notify all registered session listeners that the current ORM session is about to
        * close or the current, implicit transaction is being committed.  Listeners which were set

=== modified file 'src/com/goldencode/p2j/persist/Record.java'
--- old/src/com/goldencode/p2j/persist/Record.java	2026-05-15 11:20:28 +0000
+++ new/src/com/goldencode/p2j/persist/Record.java	2026-06-10 06:43:53 +0000
@@ -102,6 +102,7 @@
 import java.util.*;
 import java.util.function.*;
 
+import com.goldencode.p2j.jmx.*;
 import com.goldencode.p2j.oo.lang.BaseObject;
 import com.goldencode.p2j.persist.orm.*;
 import com.goldencode.p2j.util.*;
@@ -115,6 +116,21 @@
 extends BaseRecord
 implements PropertiesDescriptor
 {
+   private static final SimpleCounter HYDRATE_RECORDS_WHEN_SESSION_CLOSE =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentHydratedRecordsWhenSessionCloses);
+   
+   private static final SimpleCounter TOTAL_FIELDS =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentTotalFields);
+   
+   private static final SimpleCounter HYDRATE_FIELD_WHEN_SESSION_CLOSES =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentHydratedFieldsWhenSessionCloses);
+
+   private static final SimpleCounter HYDRATED_FIELDS_LZS =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentHydratedFieldsWithLZS);
+   
+   private static final SimpleCounter HYDRATED_FIELDS_LOADER =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentHydratedFieldsWithLoader);
+   
    /**
     * Default constructor needed by DMOs extending this class.
     */
@@ -163,7 +179,7 @@
    {
       checkIncomplete(offset);
 
-      Integer datum = (Integer) data[offset];
+      Integer datum = (Integer) getDatum(offset);
       
       return datum != null ? new integer(datum) : new integer();
    }
@@ -272,7 +288,7 @@
    {
       checkIncomplete(offset);
 
-      OffsetDateTime datum = (OffsetDateTime) data[offset];
+      OffsetDateTime datum = (OffsetDateTime) getDatum(offset);
       
       return datum != null ? new datetimetz(datum) : new datetimetz();
    }
@@ -362,6 +378,36 @@
       }
    }
    
+   @Override
+   protected void countHydrateRecordWhenSessionCloses(long count)
+   {
+      HYDRATE_RECORDS_WHEN_SESSION_CLOSE.update(count);
+   }
+   
+   @Override
+   protected void countTotalFields(long count)
+   {
+      TOTAL_FIELDS.update(count);
+   }
+   
+   @Override
+   protected void countHydratedFieldsWithLoader(long count)
+   {
+      HYDRATED_FIELDS_LOADER.update(count);
+   }
+   
+   @Override
+   protected void countHydratedFieldsWithLZS(long count)
+   {
+      HYDRATED_FIELDS_LZS.update(count);
+   }
+   
+   @Override
+   protected void countHydratedFieldWhenSessionCloses(long count)
+   {
+      HYDRATE_FIELD_WHEN_SESSION_CLOSES.update(count);
+   }
+   
    /**
     * Get an {@code datetime} value for the datum at the given offset in the property array.
     * <p>
@@ -378,7 +424,7 @@
    {
       checkIncomplete(offset);
 
-      Timestamp datum = (Timestamp) data[offset];
+      Timestamp datum = (Timestamp) getDatum(offset);
       
       return datum != null ? new datetime(datum) : new datetime();
    }
@@ -483,7 +529,7 @@
    {
       checkIncomplete(offset);
 
-      Date datum = (Date) data[offset];
+      Date datum = (Date) getDatum(offset);
       
       return datum != null ? new date(datum) : new date();
    }
@@ -588,7 +634,7 @@
    {
       checkIncomplete(offset);
 
-      Boolean datum = (Boolean) data[offset];
+      Boolean datum = (Boolean) getDatum(offset);
       
       return datum != null ? new logical(datum) : new logical();
    }
@@ -691,7 +737,9 @@
     */
    public final int64 _getInt64(int offset)
    {
-      Long datum = (Long) data[offset];
+      checkIncomplete(offset);
+      
+      Long datum = (Long) getDatum(offset);
       
       return datum != null ? new int64(datum) : new int64();
    }
@@ -800,7 +848,7 @@
    {
       checkIncomplete(offset);
 
-      BigDecimal datum = (BigDecimal) data[offset];
+      BigDecimal datum = (BigDecimal) getDatum(offset);
       
       return datum != null ? new decimal(datum) : new decimal();
    }
@@ -917,8 +965,8 @@
    public final character _getCharacter(int offset)
    {
       checkIncomplete(offset);
-
-      String datum = (String) data[offset];
+      
+      String datum = (String) getDatum(offset);
       
       if (datum != null)
       {
@@ -1030,7 +1078,7 @@
    {
       checkIncomplete(offset);
 
-      Long datum = (Long) data[offset];
+      Long datum = (Long) getDatum(offset);
       
       return datum != null ? new rowid(datum) : new rowid();
    }
@@ -1137,7 +1185,7 @@
    {
       checkIncomplete(offset);
 
-      Long datum = (Long) data[offset];
+      Long datum = (Long) getDatum(offset);
       
       return datum != null ? handle.fromResourceId(datum, false) : new handle();
    }
@@ -1244,7 +1292,7 @@
    {
       checkIncomplete(offset);
 
-      Long datum = (Long) data[offset];
+      Long datum = (Long) getDatum(offset);
       
       return datum != null ? new recid(datum) : new recid();
    }
@@ -1350,7 +1398,7 @@
    {
       checkIncomplete(offset);
 
-      byte[] datum = (byte[]) data[offset];
+      byte[] datum = (byte[]) getDatum(offset);
       
       return datum != null ? new blob(datum) : new blob();
    }
@@ -1457,7 +1505,7 @@
    {
       checkIncomplete(offset);
 
-      return new clob((String) data[offset], getCodePage(offset));
+      return new clob((String) getDatum(offset), getCodePage(offset));
    }
    
    /**
@@ -1560,7 +1608,7 @@
    {
       checkIncomplete(offset);
 
-      String datum = (String) data[offset];
+      String datum = (String) getDatum(offset);
       
       return datum != null ? comhandle.fromString(datum) : new comhandle();
    }
@@ -1665,7 +1713,7 @@
    {
       checkIncomplete(offset);
 
-      Long datum = (Long) data[offset];
+      Long datum = (Long) getDatum(offset);
       
       // the instance must be typed to Progress.Lang.Object
       object<?> res = new object<>(BaseObject.class);
@@ -1781,7 +1829,7 @@
    {
       checkIncomplete(offset);
 
-      byte[] datum = (byte[]) data[offset];
+      byte[] datum = (byte[]) getDatum(offset);
       
       return datum != null ? new raw(datum) : raw.instantiateUnknownRaw();
    }
@@ -1956,7 +2004,7 @@
       RecordMeta meta = _recordMeta();
       if (oneLine)
       {
-         int last = data.length;
+         int last = getLength();
          int start = (this instanceof TempTableRecord) ? 5 : 0;
          sb.append(meta.legacyName).append('/').append(meta.tables[0]).append(':').append(id).append(" {");
          for (int i = start; i < last; i++)
@@ -1966,7 +2014,7 @@
                sb.append(", ");
             }
             
-            boolean isChar = data[i] instanceof String;
+            boolean isChar = getSimpleDatum(i) instanceof String;
             if (isChar)
             {
                sb.append("\"");
@@ -1974,7 +2022,8 @@
             
             boolean incomplete = readFields != null && !readFields.get(i);
             
-            sb.append(data[i] == null ? incomplete ? "n/a" : "?" : data[i]);
+            Object datum = getSimpleDatum(i);
+            sb.append(datum == null ? incomplete ? "n/a" : "?" : datum);
             if (isChar)
             {
                sb.append("\"");
@@ -1997,14 +2046,14 @@
             int extent = pms[i].getExtent();
             if (extent == 0)
             {
-               sb.append(data[i]);
+               sb.append(getSimpleDatum(i));
             }
             else
             {
                for (int k = 0; k < extent; k++)
                {
                   sb.append(k == 0 ? "[" : "");
-                  sb.append(data[i + k]);
+                  sb.append(getSimpleDatum(i + k));
                }
                
                i += extent - 1;
@@ -2042,6 +2091,7 @@
          Object val = properties.get(prop.getLegacyName());
          val = f.apply(prop, val);
 
+         Object[] data = getData();
          if (prop.getExtent() > 0)
          {
             for (int j = 0; j < prop.getExtent(); j++, i++)
@@ -2050,11 +2100,13 @@
                
                BaseDataType fval = (BaseDataType) Array.get(val, j);
                prop.getDataHandler().setField(data, fval, i, prop);
+               setLivePropsAtOffset(i);
             }
          }
          else
          {
             prop.getDataHandler().setField(data, (BaseDataType) val, i, prop);
+            setLivePropsAtOffset(i);
          }
       }
    }
@@ -2127,23 +2179,6 @@
    }
    
    /**
-    * Check if two {@link Record} objects are equal from the point of view of the {@code data} array.
-    * <p>
-    * <strong>To be noted</strong>, that this method accesses the {@code data} arrays directly without using 
-    * any getter. This provides faster access, however should be changed if not all fields are hydrated and
-    * access is mandatory from a getter.
-    * 
-    * @param  otherRecord
-    *         The instance to compare against.
-    *        
-    * @return True if the {@code data} arrays are equal, false - otherwise
-    */
-   public boolean equalData(Record otherRecord)
-   {
-      return Arrays.equals(this.data, otherRecord.data);
-   }
-   
-   /**
     * Converts this {@code Record} into a POJO, if POJO class is available.
     * To be implemented by DMO implementations.
     * 

=== modified file 'src/com/goldencode/p2j/persist/RecordBuffer.java'
--- old/src/com/goldencode/p2j/persist/RecordBuffer.java	2026-05-15 11:20:28 +0000
+++ new/src/com/goldencode/p2j/persist/RecordBuffer.java	2026-06-10 08:17:33 +0000
@@ -12658,6 +12658,10 @@
             // don't count usages of template records and don't register with persistence
             if (oldRecord.primaryKey() != null && oldRecord.primaryKey() > 0)
             {
+               // the buffer is leaving this record: report the field demand observed during the visit
+               // to the runtime-feedback hydration layer (no-op unless the record was probing)
+               oldRecord.reportDemandSample();
+
                decrementDMOUseCount(oldRecord, false);
             }
          }

=== modified file 'src/com/goldencode/p2j/persist/TempRecord.java'
--- old/src/com/goldencode/p2j/persist/TempRecord.java	2024-10-21 07:34:01 +0000
+++ new/src/com/goldencode/p2j/persist/TempRecord.java	2026-06-10 06:43:53 +0000
@@ -17,6 +17,7 @@
 ** 004 CA  20240918 Removed default methods from interfaces and moved them as concrete implementations, as
 **                  default methods cause the Java metaspace to spike one order of magnitude.
 ** 005 AS  20241009 Refactored activeOffset into activeOffsets(Bitset).
+** 006 AL2 20231102 Added computeIndexForHydrator for lazy hydration.
 */
 
 /*
@@ -74,6 +75,7 @@
 
 package com.goldencode.p2j.persist;
 
+import com.goldencode.p2j.jmx.*;
 import com.goldencode.p2j.persist.orm.*;
 import com.goldencode.p2j.util.object;
 
@@ -87,6 +89,21 @@
 extends Record
 implements TempTableRecord
 {
+   private static final SimpleCounter HYDRATE_RECORDS_WHEN_SESSION_CLOSE =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.TempHydratedRecordsWhenSessionCloses);
+   
+   private static final SimpleCounter TOTAL_FIELDS =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.TempTotalFields);
+   
+   private static final SimpleCounter HYDRATE_FIELD_WHEN_SESSION_CLOSES =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.TempHydratedFieldsWhenSessionCloses);
+   
+   private static final SimpleCounter HYDRATED_FIELDS_LZS =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.TempHydratedFieldsWithLZS);
+   
+   private static final SimpleCounter HYDRATED_FIELDS_LOADER =
+      SimpleCounter.getInstance(FwdServerJMX.Counter.TempHydratedFieldsWithLoader);
+   
    /** The property/column name of "__error-flag__" field. */
    public static final String _ERROR_FLAG = "_errorFlag";
    
@@ -197,7 +214,7 @@
    @Override
    public final Long _peerRowid()
    {
-      return (Long) data[_PEER_ROWID_DATA_INDEX];
+      return (Long) getDatum(_PEER_ROWID_DATA_INDEX);
    }
    
    /**
@@ -237,7 +254,7 @@
    @Override
    public final Integer _rowState()
    {
-      return (Integer) data[_ROW_STATE_DATA_INDEX];
+      return (Integer) getDatum(_ROW_STATE_DATA_INDEX);
    }
    
    /**
@@ -278,7 +295,7 @@
    @Override
    public final Long _originRowid()
    {
-      return (Long) data[_ORIGIN_ROWID_DATA_INDEX];
+      return (Long) getDatum(_ORIGIN_ROWID_DATA_INDEX);
    }
    
    /**
@@ -318,7 +335,7 @@
    @Override
    public final Long _datasourceRowid()
    {
-      return (Long) data[_DATASOURCE_ROWID_INDEX];
+      return (Long) getDatum(_DATASOURCE_ROWID_INDEX);
    }
    
    /**
@@ -356,7 +373,7 @@
    @Override
    public final Integer _errorFlags()
    {
-      return (Integer) data[_ERROR_FLAG_DATA_INDEX];
+      return (Integer) getDatum(_ERROR_FLAG_DATA_INDEX);
    }
    
    /**
@@ -392,7 +409,7 @@
    @Override
    public final String _errorString()
    {
-      return (String) data[_ERROR_STRING_DATA_INDEX];
+      return (String) getDatum(_ERROR_STRING_DATA_INDEX);
    }
    
    /**
@@ -507,4 +524,40 @@
       Class<?> implIFace = _recordMeta().getDmoMeta().getAnnotatedInterface();
       TemporaryBuffer.updateObjectRefCount(implIFace, oldVal, newVal);
    }
+   
+   @Override
+   protected int getHydratorRowOffset()
+   {
+      return 2;
+   }
+   
+   @Override
+   protected void countHydrateRecordWhenSessionCloses(long count)
+   {
+      HYDRATE_RECORDS_WHEN_SESSION_CLOSE.update(count);
+   }
+   
+   @Override
+   protected void countTotalFields(long count)
+   {
+      TOTAL_FIELDS.update(count);
+   }
+   
+   @Override
+   protected void countHydratedFieldsWithLZS(long count)
+   {
+      HYDRATED_FIELDS_LZS.update(count);
+   }
+   
+   @Override
+   protected void countHydratedFieldsWithLoader(long count)
+   {
+      HYDRATED_FIELDS_LOADER.update(count);
+   }
+   
+   @Override
+   protected void countHydratedFieldWhenSessionCloses(long count)
+   {
+      HYDRATE_FIELD_WHEN_SESSION_CLOSES.update(count);
+   }
 }

=== modified file 'src/com/goldencode/p2j/persist/dialect/Dialect.java'
--- old/src/com/goldencode/p2j/persist/dialect/Dialect.java	2026-04-15 05:49:39 +0000
+++ new/src/com/goldencode/p2j/persist/dialect/Dialect.java	2026-06-10 08:51:59 +0000
@@ -213,6 +213,7 @@
 import java.util.logging.*;
 import com.goldencode.p2j.cfg.*;
 import com.goldencode.p2j.convert.*;
+import com.goldencode.p2j.directory.*;
 import com.goldencode.p2j.persist.*;
 import com.goldencode.p2j.persist.deploy.*;
 import com.goldencode.p2j.persist.orm.*;
@@ -264,7 +265,18 @@
    
    /** Logger */
    private static final CentralLogger log = CentralLogger.get(Dialect.class.getName());
-   
+
+   /** Directory path for the lazy-hydration master switch. */
+   private static final String CFG_LAZY_HYDRATION = "persistence/lazy-hydration/enabled";
+
+   /**
+    * Master switch for lazy (just-in-time) hydration of DMOs, read once from configuration. When
+    * {@code false}, hydration is always eager and the runtime-feedback layer is moot (no call-site is
+    * ever wrapped lazily). Defaults to enabled.
+    */
+   private static final boolean lazyHydrationEnabled =
+      DirectoryManager.getInstance().getBoolean(Directory.ID_RELATIVE_SERVER, CFG_LAZY_HYDRATION, false);
+
    /** Map of known/loaded dialect instances */
    private static final Map<String, Dialect> knownDialects = new ConcurrentHashMap<>();
    
@@ -605,8 +617,8 @@
    }
    
    /**
-    * Create the error descriptor from the SQLException message.
-    *
+    * Create the error descriptor from the {@code SQLException} message.
+    * 
     * @param   message
     *          The SQLException message.
     *
@@ -620,7 +632,7 @@
    /**
     * Check if Common Table Expressions should be used for converted CONTAINS operator
     * 
-    * @return <code>true</code> if Common Table Expressions should be used for converted CONTAINS operator
+    * @return  <code>true</code> if Common Table Expressions should be used for converted CONTAINS operator
     */
    public boolean useCTE4Contains()
    {
@@ -718,7 +730,7 @@
    public abstract void generateWordTablesDDLImpl(String dbName,
                                                   PrintStream out,
                                                   String eoln,
-                                                  Collection<WordTable>  wordTables);
+                                                  Collection<WordTable> wordTables);
    
    /**
     * Create a data object, the purpose of which is specific to a particular
@@ -2189,6 +2201,18 @@
    {
       return null;
    }
+
+   /**
+    * Indicate whether this dialect is likely to benefit from lazy hydration of DMOs when reading a record
+    * from the database. This serves as a hint, but may be overridden by configuration or runtime conditions.
+    *
+    * @return  {@code true} unless disabled via the {@code persistence/lazy-hydration/enabled}
+    *          configuration or overridden by subclasses.
+    */
+   public boolean useLazyHydration()
+   {
+      return lazyHydrationEnabled;
+   }
    
    /**
     * Use the provided database connection to extract the collation used by the database and report it

=== modified file 'src/com/goldencode/p2j/persist/dirty/DefaultDirtyShareManager.java'
--- old/src/com/goldencode/p2j/persist/dirty/DefaultDirtyShareManager.java	2026-04-22 06:58:45 +0000
+++ new/src/com/goldencode/p2j/persist/dirty/DefaultDirtyShareManager.java	2026-06-10 06:43:53 +0000
@@ -834,8 +834,7 @@
                indexUpdateStates.remove(ident);
             }
             
-            String hql = "delete from " + entity + 
-                         " where " + DatabaseManager.PRIMARY_KEY + " = ?";
+            String fql = "delete from " + entity + " where " + Session.PK + " = ?";
             Session session = null;
             boolean inTx = false;
             
@@ -843,7 +842,7 @@
             {
                session = new Session(dirtyDatabase);
                inTx = session.beginTransaction();
-               Query query = Session.createQuery(hql);
+               Query query = Session.createQuery(fql);
                query.setParameter(0, id);
                query.executeUpdate(session);
                session.commit();
@@ -1223,7 +1222,7 @@
    }
    
    /**
-    * Execute an HQL query and return the results as a list. If the query returns multiple results
+    * Execute an FQL query and return the results as a list. If the query returns multiple results
     * per row, each element in the list will be an array of {@code Object}s.
     * <p>
     * No locking is attempted.

=== modified file 'src/com/goldencode/p2j/persist/orm/AbstractRowStructure.java'
--- old/src/com/goldencode/p2j/persist/orm/AbstractRowStructure.java	2025-03-24 08:47:18 +0000
+++ new/src/com/goldencode/p2j/persist/orm/AbstractRowStructure.java	2026-06-10 06:43:53 +0000
@@ -12,6 +12,7 @@
 ** 003 LS  20241105 Added hasOnlyPK().
 ** 004 OM  20240701 Replaced + concatenation with append in StringBuilder.
 **     SP  20250321 Removed usage of prop.index.
+** 005 AL2 20231214 Fixed hydrate extents to avoid honoring recOffset if set on -1.
 */
 
 /*
@@ -177,7 +178,8 @@
     * @throws  PersistenceException
     *          thrown when hydrating extents fails (as it requires extra SQL queries to be run).
     */
-   protected void hydrateExtents(Session session, BaseRecord r, int recOffset)
+   @Override
+   public void hydrateExtents(Session session, BaseRecord r, int recOffset)
    throws PersistenceException
    {
       // TODO: collect extent properties and do a new query from SQL, see Loader.readExtentData()
@@ -188,12 +190,12 @@
          PropertyMeta[] propsMeta = recordMeta.getPropertyMeta(false);
          String[] loadSqls = recordMeta.loadSql;
          int[] loadSqlsIndexes = recordMeta.loadSqlIndexes;
-         boolean incomplete = isIncomplete();
+         boolean arbitrary = isIncomplete() || recOffset == -1;
          
          // only use the extent queries, skip the loadSqls[0] which contains the simple properties
          for (int i = 1; i < loadSqls.length; i++) // skip "plain" columns 
          {            
-            if (incomplete)
+            if (arbitrary)
             {
                // resolve the offset in the 'props' array where this sql starts.  
                // all non-expanded extent fields are loaded even for incomplete records.

=== modified file 'src/com/goldencode/p2j/persist/orm/AdaptiveRowStructure.java'
--- old/src/com/goldencode/p2j/persist/orm/AdaptiveRowStructure.java	2025-08-14 09:57:08 +0000
+++ new/src/com/goldencode/p2j/persist/orm/AdaptiveRowStructure.java	2026-06-10 06:43:53 +0000
@@ -10,6 +10,7 @@
 **     AL2 20231212 Use dmoMeta directly, without getter.
 ** 002 OM  20240715 Fixed copy/paste bug.
 ** 003 LS  20250714 Added 'getNeededFields' implementation.
+** 004 AL2 20231214 Added implementation for hydrateAt.
 */
 
 /*
@@ -69,8 +70,7 @@
 
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.BitSet;
-import java.util.Iterator;
+import java.util.*;
 
 import com.goldencode.p2j.persist.*;
 
@@ -228,6 +228,50 @@
       return delegate.getNeededFields();
    }
 
+
+   /**
+    * Hydrate only one property in the base-record. This is largely used by the lazy hydrator,
+    * allowing it to hydrate only the property that is accessed now. The other hydration method 
+    * ({@link #hydrate}) eagerly hydrates all properties, is a slow process and may do useless
+    * hydrations on properties that are not accessed.
+    * 
+    * @param   resultSet
+    *          The ResultSet from which the hydration should be done
+    * @param   rsOffset
+    *          The result set offset from which the reading should be done. This is relevant as
+    *          the result set may contain a joined row, but we are interested only in a specific
+    *          record that may start at a specific offset.
+    * @param   r
+    *          The record that should be hydrated. 
+    * @param   propOffset
+    *          The property that should be hydrated. The caller is responsible in assuring that
+    *          this property wasn't hydrated before.
+    * 
+    * @return  {@code true} if this worked due to the fact that it was a scalar value. {@code false}
+    *          in case the property was a non-expanded 
+    *          
+    * @throws  PersistenceException
+    *          This is thrown when reading the scalar property fails.
+    */
+   @Override
+   public boolean hydrateAt(ResultSet resultSet, int rsOffset, BaseRecord r, int dataIndex)
+   throws PersistenceException
+   {
+      return delegate.hydrateAt(resultSet, rsOffset, r, dataIndex);
+   }
+   
+   /**
+    * Retrieve the fields that were loaded from the database.
+    *
+    * @return A BitSet object, where true means that the field associated with that position in the {@code
+    * data} array has been loaded from database, and false - otherwise.
+    */
+   @Override
+   public BitSet getLoadedFields()
+   {
+      return delegate.getLoadedFields();
+   }
+   
    /**
     * Invalidation logic that will pick up everything that was appended until now and moved into
     * an un-ordered implementation. This routine should be avoided as much as possible in the 

=== modified file 'src/com/goldencode/p2j/persist/orm/BaseRecord.java'
--- old/src/com/goldencode/p2j/persist/orm/BaseRecord.java	2025-11-28 08:30:04 +0000
+++ new/src/com/goldencode/p2j/persist/orm/BaseRecord.java	2026-06-10 08:17:13 +0000
@@ -2,7 +2,7 @@
 ** Module   : BaseRecord.java
 ** Abstract : The abstract base class for all business data model object classes.
 **
-** Copyright (c) 2019-2025, Golden Code Development Corporation.
+** Copyright (c) 2019-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
 ** 001 ECF 20191001 Created initial version with basic data and accessors.
@@ -112,12 +112,12 @@
 
 package com.goldencode.p2j.persist.orm;
 
+import com.goldencode.p2j.jmx.*;
 import com.goldencode.p2j.persist.*;
 import com.goldencode.p2j.rest.serializers.*;
 import com.goldencode.p2j.schema.*;
 import com.goldencode.p2j.util.*;
 import com.goldencode.p2j.util.logging.*;
-
 import java.lang.reflect.*;
 import java.sql.*;
 import java.util.*;
@@ -162,9 +162,6 @@
    /** Surrogate primary key of the record */
    protected Long id = null;
    
-   /** The data represented by this record, in it's low-level (database friendly) form */
-   protected final Object[] data;
-   
    /** For an incomplete record, this holds the fields which were read or assigned in this record. */
    protected BitSet readFields = null;
    
@@ -178,6 +175,9 @@
     */
    protected long references = 0;
    
+   /** The data represented by this record, in it's low-level (database friendly) form */
+   private final Object[] data;
+   
    /** Map of properties touched, each bit corresponds with a property in the data array */
    private final BitSet dirtyProps;
    
@@ -193,6 +193,37 @@
    /** Record identifier (lock variant, which uses table name) for this record (created lazily) */
    private RecordIdentifier<String> recid = null;
    
+   /** Hydrator used to extract data from a result set positioned on a particular target row */
+   private Hydrator hydrator = null;
+   
+   /** Map of "live" properties (read or assigned), each bit corresponds with a property in the data array */
+   // TODO: resolve redundancy with readFields
+   private BitSet liveProps;
+   
+   /** The number of bits set in {@link #liveProps} */
+   private int livePropsCardinality;
+
+   /**
+    * Process-wide fast-path gate for the runtime-feedback hydration probe. {@code true} while the
+    * feature is enabled and at least one {@link FieldUsageProfile} is still UNKNOWN. When {@code
+    * false}, {@link #getDatum} pays only this single volatile read per field access and never touches
+    * the per-record probe state, so steady-state hydration cost is unchanged. Maintained by
+    * {@link FieldUsageProfile} as profiles are created and as verdicts lock in.
+    */
+   static volatile boolean PROBING_ACTIVE = false;
+
+   /**
+    * The field-usage profile this record is probing for, or {@code null} if not probing. Distinct
+    * from {@link #hydrator}, so it survives the hydrator being nulled out in {@link #getDatum}.
+    */
+   private FieldUsageProfile usageProfile;
+
+   /**
+    * While probing, the set of field offsets read via {@link #getDatum} during the current buffer
+    * visit; {@code null} except while a probe is armed (see {@link #armUsageProbe}).
+    */
+   private BitSet accessed;
+
    /** The currently active buffer when state is changing */
    private RecordBuffer activeBuffer = null;
    
@@ -298,13 +329,16 @@
          }
          
          // TODO: LOB data is mutable, needs to be duplicated explicitly
-         boolean change = to.data[i] != from.data[i];
+         Object toDatum = to.getDatum(i);
+         Object fromDatum = from.getDatum(i);
+         boolean change = toDatum != fromDatum; 
+
          if (change)
          {
-            to.data[i] = from.data[i];
+            to.setSimpleDatum(i, fromDatum);
             
             // update the null property bitset and mark the property as changed to be able to save it
-            to.nullProps.set(i, to.data[i] == null);
+            to.nullProps.set(i, fromDatum == null);
             to.dirtyProps.set(i);
          }
       }
@@ -488,10 +522,63 @@
    public void copy(BaseRecord from)
    {
       // TODO: BLOB data is mutable, needs to be duplicated explicitly
-      int len = this.data.length;
+      int len = this.getLength();
       this.id = from.id;
-      System.arraycopy(from.data, 0, this.data, 0, len);
+      copyArray(from.getData());
       dirtyProps.set(0, len); // TODO: nullProps?
+      
+      hydrator = from.hydrator;
+      liveProps = from.liveProps != null ? (BitSet) from.liveProps.clone() : null;
+      livePropsCardinality = from.livePropsCardinality;
+   }
+   
+   /**
+    * Check if two {@link BaseRecord} objects are equal from the point of view of the {@code data} array.
+    * <p>
+    * <strong>To be noted</strong>, that this method accesses the {@code data} arrays directly without using 
+    * any getter. This provides faster access, however should be changed if not all fields are hydrated and
+    * access is mandatory from a getter.
+    *
+    * @param  otherRecord
+    *         The instance to compare against.
+    *
+    * @return True if the {@code data} arrays are equal, false - otherwise
+    */
+   public boolean equalData(BaseRecord otherRecord)
+   {
+      if (this.getLength() != otherRecord.getLength())
+      {
+         return false;
+      }
+      for  (int i = 0; i < this.getLength(); i++)
+      {
+         if (this.areAllPropertiesLive() && otherRecord.areAllPropertiesLive())
+         {
+            return Arrays.equals(this.getData(), otherRecord.getData());
+         }
+      }
+      
+      for  (int i = 0; i < this.getLength(); i++)
+      {
+         if (!Objects.equals(getDatum(i), otherRecord.getDatum(i)))
+         {
+            return false;
+         }
+      }
+      
+      return true;
+   }
+   
+   
+   /**
+    * Copy an array of objects into data.
+    * 
+    * @param   from
+    *          The source to copy from.
+    */
+   public void copyArray(Object[] from)
+   {
+      System.arraycopy(from, 0, data, 0, getLength());
    }
    
    /**
@@ -695,7 +782,7 @@
       //       previous state and roll this back as well
       for (int i = activeOffsets.nextSetBit(0); i != -1; i = activeOffsets.nextSetBit(i + 1))
       {
-         data[i] = activePropPreviousValues[i];
+         setSimpleDatum(i, activePropPreviousValues[i]);
       }
    }
    
@@ -728,12 +815,14 @@
       buf.append(':').append(primaryKey()).append(sep)
          .append(state).append(sep)
          .append("references: ").append(references).append(sep)
+         .append("read: ").append(readFields).append(sep)
+         .append("live: ").append(liveProps).append(sep)
          .append("dirty: ").append(dirtyProps).append(sep)
          .append("unvalidated: ").append(unvalidated);
       
       if (includeData)
       {
-         int len = data.length;
+         int len = getLength();
          buf.append(sep).append("data: {");
          for (int i = 0; i < len; i++)
          {
@@ -742,7 +831,7 @@
                buf.append(", ");
             }
             
-            buf.append(data[i]);
+            buf.append(getSimpleDatum(i));
          }
          
          buf.append('}');
@@ -772,7 +861,7 @@
    {
       Objects.requireNonNull(pm);
       
-      return data;
+      return getData();
    }
    
    /**
@@ -791,9 +880,29 @@
    public Object[] getData(RecordBuffer rb)
    {
       Objects.requireNonNull(rb);
-      
+
+      return getData();
+   }
+   
+   /**
+    * Get the data property.
+    * 
+    * @return   The value of the data property.
+    */
+   public Object[] getData()
+   {
       return data;
    }
+       
+   /**
+    * Get the size of the data property.
+    * 
+    * @return  Size of the data property.
+    */
+   public int getLength()
+   {
+      return data.length;
+   }
    
    /**
     * Indicate whether any of this record's validatable properties are dirty and have not been validated.
@@ -860,12 +969,14 @@
    public Object[] getActiveUpdateDiff(int ormIndex, int extentIndex)
    {
       int actualIndex = ormIndex + extentIndex;
-      if (dirtyProps.get(actualIndex) && !Objects.equals(activePropPreviousValues[actualIndex], 
-                                                        data[actualIndex]))
+      Object activeOffsetDatum = getDatum(actualIndex);
+      
+      if (dirtyProps.get(actualIndex) &&
+         !Objects.equals(activePropPreviousValues[actualIndex], activeOffsetDatum))
       {
          Object[] res = new Object[2];
          res[0] = activePropPreviousValues[actualIndex];
-         res[1] = data[actualIndex];
+         res[1] = activeOffsetDatum;
          return res;
       }
       return null;
@@ -890,8 +1001,60 @@
    public BitSet _getReadFields()
    {
       return readFields;
-   }
-
+   }  
+   
+   /**
+    * This should be called when the record is re-attached to a new session. This way, we refresh the 
+    * underlying session in hydrator (if any) to properly refresh the record when needed. If this is 
+    * not used, we may face {@link LazyHydrationException} due to stale sessions in the hydrator. 
+    * 
+    * First time a hydrator is created, it will take the session from the generator result-set, thus,
+    * there is no need to call this method in that case.
+    * 
+    * @param   session
+    *          The session to which this record is re-attached.
+    */
+   public void refreshHydrator(Session session)
+   {
+      if (hydrator != null)
+      {
+         hydrator.refresh(session);
+      }
+   }
+   
+   /**
+    * Sets the bit in {@link BaseRecord#liveProps} to true, meaning that the field at that index has been 
+    * read or written.
+    * 
+    * @param offset
+    *        Zero-based offset of the datum in the data array, which corresponds to the index in 
+    *        {@link BaseRecord#liveProps}
+    */
+   protected void setLivePropsAtOffset(int offset)
+   {
+      if (liveProps != null)
+      {
+         if (!liveProps.get(offset))
+         {
+            livePropsCardinality++;
+         }
+         
+         liveProps.set(offset);
+      }
+   }
+   
+   protected void setAllLiveProps()
+   {
+      if (liveProps == null)
+      {
+         return;
+      }
+      for (int i = 0; i < data.length; i++)
+      {
+         liveProps.set(i);
+      }
+   }
+   
    /**
     * Set a single datum into the data array, and update the record and property state accordingly.
     * 
@@ -901,12 +1064,7 @@
     *          Value to be set into the data array.
     */
    protected void setDatum(int offset, Object datum)
-   {
-      if (readFields != null)
-      {
-         readFields.set(offset);
-      }
-
+   {      
       if (activeBuffer != null)
       {
          activeOffsets.set(offset);
@@ -915,15 +1073,22 @@
       // update the bitset of dirty (touched) properties
       dirtyProps.set(offset);
       
+      Object offsetDatum = getDatum(offset);
+      
       // temporarily save the old value (even if it is the same as the new value)
-      activePropPreviousValues[offset] = data[offset];
+      activePropPreviousValues[offset] = offsetDatum;
+
+      if (readFields != null)
+      {
+         readFields.set(offset);
+      }
       
       // for BigDecimal, equals checks the scale - we need compareTo, to check if the values are really the 
       // same, regardless of the scale value.
-      if (!(Objects.equals(data[offset], datum) || 
+      if (!(Objects.equals(offsetDatum, datum) || 
             (datum instanceof Comparable && 
-             data[offset] != null        && 
-             ((Comparable) datum).compareTo(data[offset]) == 0)))
+             offsetDatum != null        && 
+             ((Comparable) datum).compareTo(offsetDatum) == 0)))
       {
          // ensure we have an exclusive lock on the record
          if (!lockForUpdate())
@@ -942,7 +1107,7 @@
          }
          
          // update the value in the data array
-         data[offset] = datum;
+         setSimpleDatum(offset, datum);
       }
    }
    
@@ -980,8 +1145,7 @@
             continue;
          }
          
-         if (fromProp.getType()   != toProp.getType() || 
-             fromProp.getExtent() != toProp.getExtent())
+         if (fromProp.getType() != toProp.getType() || fromProp.getExtent() != toProp.getExtent())
          {
             return false;
          }
@@ -1012,10 +1176,11 @@
          
          // for BigDecimal, equals checks the scale - we need compareTo, to check if the values are really the 
          // same, regardless of the scale value.
-         if (!(Objects.equals(data[toOffset], datum) || 
+         Object offsetDatum = getDatum(toOffset);
+         if (!(Objects.equals(offsetDatum, datum) || 
               (datum instanceof Comparable && 
-               data[toOffset] != null && 
-               ((Comparable) datum).compareTo(data[toOffset]) == 0)))
+               offsetDatum != null && 
+               ((Comparable) datum).compareTo(offsetDatum) == 0)))
          {
             if (updatedOffsets == null)
             {
@@ -1056,24 +1221,144 @@
       {
          int offset = updatedOffsets.get(idx);
          Object datum = updatedDatums.get(idx);
-         data[offset] = datum; 
+         setSimpleDatum(offset, datum);
       }
       
       return true;
    }
    
    /**
-    * Get the field's value on the specified offset.
-    * 
-    * @param    offset
-    *           Zero-based offset of the datum in the data array.
-    * 
-    * @return   The field's value.
+    * Get the field's value at the specified offset, with {@code null} indicating unknown value.
+    * <p>
+    * N.B.: if lazy hydration can be active for this record, callers must catch and handle the unchecked
+    * {@code LazyHydrationException}.
+    * 
+    * @param   offset
+    *          Zero-based offset of the datum in the data array.
+    * 
+    * @return  The field's value, with {@code null} indicating unknown value.
+    * 
+    * @throws  LazyHydrationException
+    *          if this record is enabled for lazy hydration, but the attached hydrator was no longer valid.
+    *          Although this is an unchecked exception to allow the DMO to be generated with simple getter
+    *          methods, callers which invoke this method when lazy hydration could be active MUST catch and
+    *          handle this exception.
     */
    protected Object getDatum(int offset)
+   throws LazyHydrationException
    {
+      // runtime-feedback probe: record that this field was demanded during the current visit. Placed
+      // before the hydrator block because eager records have hydrator == null and would otherwise be
+      // invisible. Gated on a single volatile read so steady state (feature off / all verdicts
+      // locked) costs nothing more.
+      if (PROBING_ACTIVE)
+      {
+         BitSet acc = accessed;
+         if (acc != null)
+         {
+            acc.set(offset);
+         }
+      }
+
+      if (hydrator != null && liveProps != null && !liveProps.get(offset))
+      {
+         boolean success = hydrator.hydrate(this, offset, getHydratorRowOffset(), true);
+         if (success)
+         {
+            countHydratedFieldsWithLZS(1);
+         }
+         if (!success)
+         {
+            BitSet unhydratedFields = getUnhydratedFields();
+            hydrator.refresh(this, id, unhydratedFields);
+            countHydratedFieldsWithLoader(unhydratedFields.cardinality());
+         }
+         if (!success || livePropsCardinality == data.length)
+         {
+            hydrator = null;
+            liveProps = null;
+            livePropsCardinality = data.length;
+         }
+      }
+      
       return data[offset];
    }
+
+   /**
+    * Switch the process-wide probing fast-path gate on or off. Called by {@link FieldUsageProfile}
+    * as profiles are created and as their verdicts lock in.
+    *
+    * @param   active
+    *          The new value of {@link #PROBING_ACTIVE}.
+    */
+   static void setProbingActive(boolean active)
+   {
+      PROBING_ACTIVE = active;
+   }
+
+   /**
+    * Arm the runtime-feedback hydration probe for the given profile: while the profile is still
+    * learning, this record records which fields are read (via {@link #getDatum}) and reports the
+    * sample when the buffer leaves it (see {@link #reportDemandSample}). Hydration itself stays eager
+    * &mdash; only the demand is observed. No-op when the profile is {@code null} or already locked.
+    *
+    * @param   profile
+    *          The call-site profile to probe for, or {@code null}.
+    */
+   void armUsageProbe(FieldUsageProfile profile)
+   {
+      if (profile == null || !profile.isUnknown())
+      {
+         return;
+      }
+
+      this.usageProfile = profile;
+      this.accessed = new BitSet(data.length);
+      profile.onProbeArmed();
+   }
+
+   /**
+    * Report the field demand observed during the current buffer visit to the attached profile, and
+    * release the probe state. Called when the buffer leaves this record &mdash; the precise moment
+    * the demand for this visit is complete. No-op when no probe is armed.
+    */
+   public void reportDemandSample()
+   {
+      FieldUsageProfile profile = usageProfile;
+      BitSet acc = accessed;
+      if (profile != null && acc != null)
+      {
+         profile.recordSample(acc.cardinality(), data.length);
+         this.accessed = null;
+      }
+   }
+
+   public void hydrateRemainingFields()
+   {
+      BitSet unhydratedFields = getUnhydratedFields();
+      if (unhydratedFields == null)
+      {
+         return;
+      }
+      int unhydratedIndex = unhydratedFields.nextSetBit(0);
+      if (unhydratedIndex != -1)
+      {
+         countHydrateRecordWhenSessionCloses(1);
+         countHydratedFieldWhenSessionCloses(unhydratedFields.cardinality());
+      }
+      while (unhydratedIndex != -1 && hydrator != null)
+      {
+         getDatum(unhydratedIndex);
+         unhydratedIndex = unhydratedFields.nextSetBit(unhydratedIndex + 1);
+      }
+   }
+
+   public void clearHydrator()
+   {
+      hydrator = null;
+      liveProps = null;
+      livePropsCardinality = data.length;
+   }
    
    /**
     * Get the static record metadata for the concrete subclass.
@@ -1161,17 +1446,85 @@
          }
       }
    }
-
+   
+   /**
+    * Retrieve the row offset required by the hydrator to skip any eventual fields that may
+    * reside the in the result set and not in {@link #data}. For instance, recid is not part
+    * of the data array, but is part of the result-set.
+    * 
+    * @return   Always 1 as we need to skip only the leading recid.
+    */
+   protected int getHydratorRowOffset()
+   {
+      return 1;
+   }
+
+   /**
+    * Retrieve the fields that are not live due to lazy hydration. This is used to identify which fields
+    * are left to hydrate by the hydrator. This array is used only when a refresh is needed (the record
+    * should be re-loaded from the database). Therefore, don't use this for any hydrate call. Let the
+    * hydrator decide when this is needed.
+    * 
+    * @return   A bit-set representing the fields that still require hydration.
+    */
+   protected BitSet getUnhydratedFields()
+   {
+      if (liveProps == null)
+      {
+         return null;
+      }
+      
+      BitSet partialFields;
+      if (readFields != null)
+      {
+         partialFields = (BitSet) readFields.clone();
+         partialFields.andNot(liveProps);
+      }
+      else
+      {
+         partialFields = (BitSet) liveProps.clone();
+         partialFields.flip(0, data.length);
+      }
+      return partialFields;
+   }
+   
+   /**
+    * Checks if a field at the specified index has been hydrated.
+    *
+    * @param   index
+    *          The index of the field to check.
+    * 
+    * @return {@code true} if the field has been hydrated, {@code false} otherwise.
+    */
+   protected boolean isFieldHydrated(int index)
+   {
+      return liveProps.get(index);
+   }
+   
+   protected abstract void countHydrateRecordWhenSessionCloses(long count);
+
+   protected abstract void countTotalFields(long count);
+   
+   protected abstract void countHydratedFieldsWithLZS(long count);
+   protected abstract void countHydratedFieldsWithLoader(long count);
+   
+   
+   protected abstract void countHydratedFieldWhenSessionCloses(long count);
+   
+   
    /**
     * Mark this record as incomplete.
     * 
     * @param    session
     *           Database session (used for UNDO purposes).
+    *           
+    * @param    loadedFields
+    *           Fields that were loaded from database.
     */
-   void markIncomplete(Session session)
+   void markIncomplete(Session session, BitSet loadedFields)
    {
       updateState(session, DmoState.INCOMPLETE, true);
-      readFields = new BitSet(data.length);
+      readFields = loadedFields;
    }
 
    /**
@@ -1387,7 +1740,7 @@
     *          The result set to read from.
     * @param   rsOffset
     *          The index in {@code ResultSet} to read from. If the property is a multiple column
-    *          then more than one column will ve read from {@code ResultSet}.
+    *          then more than one column will be read from {@code ResultSet}.
     *
     * @return  The number of columns actually read from the {@code ResultSet}.
     */
@@ -1424,14 +1777,21 @@
       
       try
       {
-         if (readFields != null)
+         int count = propertyReader.readProperty(rs, rsOffset, data, propOffset);
+         
+         if (liveProps != null)
          {
-            readFields.set(propOffset);
+            // this should always hold; we shouldn't ever read a field twice
+            // otherwise, the cardinality will fail
+            livePropsCardinality++;
+            
+            // mark this property as live
+            liveProps.set(propOffset);
          }
          
-         return propertyReader.readProperty(rs, rsOffset, data, propOffset);
+         return count;
       }
-      catch (java.sql.SQLException sqle)
+      catch (SQLException sqle)
       {
          throw new PersistenceException(sqle);
       }
@@ -1529,6 +1889,40 @@
    }
    
    /**
+    * Indicate whether all properties are live; that is, have all properties been initialized, either by
+    * hydration from a result set, explicitly setting them, or initializing them to their default values.
+    * 
+    * @return  {@code true} if all properties are live, else {@code false}.
+    */
+   boolean areAllPropertiesLive()
+   {
+      return hydrator == null;
+   }
+   
+   /**
+    * Create a lazy hydration helper object for the given result set, at the given offset into the column
+    * results, for the current result set row.
+    * 
+    * @param   rowStruct
+    *          The row structure of the of the result-set.
+    * @param   lazyResultSet
+    *          Backing result set for lazy hydration.
+    * @param   rsOffset
+    *          Offset of the primary key column for this record in the current row of the result set.
+    */
+   void createHydrator(RowStructure rowStruct, LazyResultSet lazyResultSet, int rsOffset)
+   {
+      hydrator = lazyResultSet.createHydrator(rowStruct, rsOffset, _recordMeta());
+      // don't reset liveProps as we want to avoid hydrating properties that were already hydrated
+      if (liveProps == null)
+      {
+         liveProps = new BitSet(data.length);
+         livePropsCardinality = 0;
+      }
+      countTotalFields(data.length);
+   }
+   
+   /**
     * Perform a sub-transaction level changeset reset of this record
     * @param   txLevel
     *          The nesting level of sub-transactions of the block being rolled back, where 0 represents
@@ -1668,6 +2062,7 @@
       int txLevel = prepareChangeSet(activeBuffer.getSession());
       if (txLevel >= 0)
       {
+         changeSet.updateBaseline(offset, getDatum(offset));
          changeSet.logStateChange(flags, txLevel);
          changeSet.logDataChange(txLevel, offset, datum);
       }
@@ -1715,6 +2110,7 @@
          for (int i = 0; i < offsets.size(); i++)
          {
             int offset = offsets.get(i);
+            changeSet.updateBaseline(offset, getDatum(offset));
             Object datum = datums.get(i);
             changeSet.logDataChange(txLevel, offset, datum);
          }
@@ -1792,9 +2188,8 @@
          // create a change set if one with the same transaction ID does not already exist
          if (changeSet == null || !changeSet.verify(session, txId))
          {
-            changeSet = new ChangeSet(session, txId, data, state.state, txLevel);
+            changeSet = new ChangeSet(session, txId, data, liveProps, state.state, txLevel);
          }
-         
          // register this undoable record with the savepoint manager at the current transaction nesting
          // level
          session.trackUndoable(this);
@@ -1871,4 +2266,49 @@
       }
       return size;
    }
+   
+   /**
+    * Simple method to retrieve the effective value at a specified offset.
+    * Note that this doesn't honor the lazy hydration technique and should be used
+    * ONLY for logging / debugging purposes. This method shouldn't be used functionally
+    * in the orm architecture. 
+    * 
+    * @param   offset
+    *          Zero-based offset of the datum in the data array.
+    */
+   protected Object getSimpleDatum(int offset)
+   {
+      return data[offset];
+   }
+   
+   /**
+    * Set a single datum into the data array, and update the record.
+    * This is a simple version of {@link #setDatum(int, Object)} used
+    * by BaseRecord.
+    * 
+    * @param   offset
+    *          Zero-based offset of the datum in the data array.
+    * @param   datum
+    *          Value to be set into the data array.
+    */
+   public void setSimpleDatum(int offset, Object datum)
+   {
+      if (readFields != null)
+      {
+         readFields.set(offset);
+      }
+      
+      if (liveProps != null)
+      {
+         if (!liveProps.get(offset))
+         {
+            livePropsCardinality++;
+         }
+         
+         liveProps.set(offset);
+      }
+      
+      data[offset] = datum;
+   }
+
 }

=== modified file 'src/com/goldencode/p2j/persist/orm/ChangeSet.java'
--- old/src/com/goldencode/p2j/persist/orm/ChangeSet.java	2025-11-18 12:38:12 +0000
+++ new/src/com/goldencode/p2j/persist/orm/ChangeSet.java	2026-06-10 06:43:53 +0000
@@ -145,7 +145,7 @@
    
    /** Baseline snapshot of data before first change is made */
    private final Object[] baselineData;
-   
+   private final BitSet activeFieldsInBaseline;
    /** Baseline state before first change is made */
    private final int baselineState;
    
@@ -180,13 +180,14 @@
     *          Anticipated maximum number of transaction nesting levels to record. It will be increased
     *          dynamically later, if this capacity is exceeded.
     */
-   ChangeSet(Session session, int txId, Object[] data, int state, int capacity)
+   ChangeSet(Session session, int txId, Object[] data, BitSet liveProps, int state, int capacity)
    {
       this.session = session;
       this.txId = txId;
       int len = data.length;
       this.baselineData = new Object[len];
       System.arraycopy(data, 0, this.baselineData, 0, len);
+      this.activeFieldsInBaseline = liveProps == null ? new BitSet(len) : (BitSet) liveProps.clone();
       this.baselineState = state;
       
       ensureCapacity(capacity);
@@ -263,9 +264,6 @@
          return;
       }
       
-      // live data to be updated by rollback
-      Object[] liveData = dmo.data;
-      
       // gather the positions of all the data elements that were changed at this block and in more
       // deeply nested blocks and clear those data structures after collecting this information
       BitSet touched = new BitSet(baselineData.length);
@@ -314,8 +312,7 @@
          for (int j = copy.nextSetBit(0); j >= 0; j = copy.nextSetBit(j + 1))
          {
             // overlay the live data
-            liveData[j] = changes[i][j];
-            
+            dmo.setSimpleDatum(j, changes[i][j]);
             // remember that we've rolled back this data element
             touched.clear(j);
          }
@@ -326,7 +323,7 @@
       {
          for (int j = touched.nextSetBit(0); j >= 0; j = touched.nextSetBit(j + 1))
          {
-            liveData[j] = baselineData[j];
+            dmo.setSimpleDatum(j, baselineData[j]);
          }
       }
       
@@ -406,6 +403,17 @@
       }
    }
    
+   public void updateBaseline(int offset, Object datum)
+   {
+      if (activeFieldsInBaseline.get(offset))
+      {
+         return;
+      }
+      
+      baselineData[offset] = datum;
+      activeFieldsInBaseline.set(offset);
+   }
+   
    /**
     * Prepare for a log entry into the change set by updating the outermost and innermost transaction
     * nesting levels at which changes occur, and by ensuring the internal data structures have enough

=== modified file 'src/com/goldencode/p2j/persist/orm/DirectAccessHelper.java'
--- old/src/com/goldencode/p2j/persist/orm/DirectAccessHelper.java	2025-11-18 12:38:12 +0000
+++ new/src/com/goldencode/p2j/persist/orm/DirectAccessHelper.java	2026-06-10 08:21:57 +0000
@@ -524,7 +524,7 @@
     *          
     */
    private static Record interpretResponse(Session session, DmoMeta meta, DirectAccessResponse response)
-   throws PersistenceException, 
+   throws PersistenceException,
           SQLException
    {
       if (response.recid == null)
@@ -540,10 +540,19 @@
       }
       
       ResultSet rs = response.getResultSet();
+      if (Query.getDialect(session).useLazyHydration())
+      {
+         rs = new LazyResultSet(rs.getStatement(), rs, session);
+      }
+
       if (rs.next())
       {
          RowStructure rowStructure = new FullRowStructure(meta);
-         return (Record) SQLQuery.hydrateRecord(rs, rowStructure, 1, session);
+         // no Query/call-site profile is available on this direct-access path, so the runtime-feedback
+         // layer does not apply here: lazy hydration stays gated on the dialect switch alone (above),
+         // and no demand probe is armed (null profile). This path is not part of the regression
+         // scenario; revisit if profiling shows otherwise.
+         return (Record) SQLQuery.hydrateRecord(rs, rowStructure, 1, session, null);
       }
       else
       {

=== modified file 'src/com/goldencode/p2j/persist/orm/DmoMeta.java'
--- old/src/com/goldencode/p2j/persist/orm/DmoMeta.java	2025-11-10 10:38:44 +0000
+++ new/src/com/goldencode/p2j/persist/orm/DmoMeta.java	2026-06-10 06:43:53 +0000
@@ -679,8 +679,8 @@
     * Obtain the list of properties for this DMO.
     * 
     * @param   extra
-    *          If {@code true} the surrogate/hidden  properties ({@code id}, {@code _multiplex})
-     *         are also added, otherwise only the original fields are returned.
+    *          If {@code true} the surrogate/hidden properties ({@code id}, {@code _multiplex})
+    *          are also added, otherwise only the original fields are returned.
     * 
     * @return  An iterator for list of properties for this DMO.
     */

=== modified file 'src/com/goldencode/p2j/persist/orm/DmoMetadataManager.java'
--- old/src/com/goldencode/p2j/persist/orm/DmoMetadataManager.java	2025-04-02 14:49:50 +0000
+++ new/src/com/goldencode/p2j/persist/orm/DmoMetadataManager.java	2026-06-10 06:43:53 +0000
@@ -94,6 +94,7 @@
 import com.goldencode.p2j.persist.Record;
 import com.goldencode.p2j.persist.annotation.*;
 import com.goldencode.p2j.persist.meta.*;
+import com.goldencode.p2j.persist.dialect.*;
 import com.goldencode.p2j.util.*;
 import com.goldencode.p2j.util.logging.*;
 import java.util.*;
@@ -560,7 +561,6 @@
     *
     * @return  Unmodifiable map.
     *
-    *
     * @throws  PersistenceException
     *          if there is an error querying index information from the JDBC driver.
     */

=== added file 'src/com/goldencode/p2j/persist/orm/FieldUsageProfile.java'
--- old/src/com/goldencode/p2j/persist/orm/FieldUsageProfile.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/persist/orm/FieldUsageProfile.java	2026-06-10 08:39:37 +0000
@@ -0,0 +1,309 @@
+/*
+** Module   : FieldUsageProfile.java
+** Abstract : Per-call-site (per-FQL) record of how many fields the converted 4GL actually reads,
+**            used to decide eager vs lazy hydration at runtime.
+**
+** Copyright (c) 2026, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ----------------------------------------Description---------------------------------------
+** 001 TG  20260610 Initial version. Runtime-feedback hydration (6720 follow-up): observe the read
+**                  fraction of a call-site and flip it to lazy only when few fields are read.
+*/
+
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+*/
+
+package com.goldencode.p2j.persist.orm;
+
+import java.util.concurrent.atomic.*;
+import java.util.logging.*;
+
+import com.goldencode.p2j.directory.*;
+import com.goldencode.p2j.jmx.*;
+import com.goldencode.p2j.util.logging.*;
+
+/**
+ * Records, for a single query call-site (keyed by its FQL), how large a fraction of the selected
+ * fields the converted 4GL actually reads, and from that derives a sticky {@link Verdict} of whether
+ * that call-site should hydrate its records eagerly or lazily.
+ * <p>
+ * The default is {@link Verdict#EAGER}: lazy hydration is opt-in, earned by evidence. While a profile
+ * is {@link Verdict#UNKNOWN} the framework hydrates eagerly (no regression) but tags the records as
+ * "probing"; {@link BaseRecord#getDatum} sets a bit per accessed field, and when the buffer leaves the
+ * record {@link BaseRecord#reportDemandSample} classifies the visit into this profile. After
+ * {@code warmup-count} samples the verdict locks: a per-sample, EAGER-biased quorum (NOT a pooled
+ * mean) flips to LAZY only when at least {@code lazy-quorum} of the samples each read fewer than
+ * {@code eager-threshold} of the fields. The verdict is then fixed for the process lifetime.
+ * <p>
+ * <b>Known measurement blind spots</b> (documented, not "fixed" &mdash; all of them bias the verdict
+ * toward EAGER, the safe direction, or are neutral):
+ * <ul>
+ *   <li>Raw bulk {@code getData()} reads (flush/validation of <i>dirty</i> records) bypass
+ *       {@code getDatum} and are invisible to the probe &mdash; neutral for read-site profiling.</li>
+ *   <li>Framework-internal {@code getDatum} reads (validation, {@code equalData} fallback,
+ *       CURRENT-CHANGED comparison) inflate measured demand, biasing toward EAGER.</li>
+ *   <li>A record current in two buffers samples only at the first buffer leave, possibly
+ *       under-measuring one visit; mitigated by re-arming the probe on each cache-hit revisit while
+ *       the verdict is still UNKNOWN.</li>
+ *   <li>Records that escape the buffer (returned, handle-held) are never sampled &mdash; safe
+ *       under-sampling.</li>
+ * </ul>
+ * Profiles are created and held in a bounded per-database map on {@code Persistence}; when the cap is
+ * reached new call-sites get no profile and therefore hydrate EAGER.
+ */
+public class FieldUsageProfile
+{
+   /** Logger. */
+   private static final CentralLogger LOG = CentralLogger.get(FieldUsageProfile.class);
+
+   /** Root of the runtime-configuration sub-tree for this feature. */
+   private static final String CFG_ROOT = "persistence/lazy-feedback/";
+
+   /** {@code true} if the feedback layer is active (probing + verdict-driven gating). */
+   private static final boolean ENABLED;
+
+   /** Number of demand samples required before a verdict is computed. */
+   private static final int WARMUP_COUNT;
+
+   /** Read fraction (accessed / total) below which a single sample counts as "lazy-friendly". */
+   private static final double EAGER_THRESHOLD;
+
+   /** Fraction of lazy-friendly samples (of all samples) required to lock in the LAZY verdict. */
+   private static final double LAZY_QUORUM;
+
+   /** Maximum number of distinct profiles held per database (bounds unbounded dynamic FQL). */
+   private static final int MAX_PROFILES;
+
+   /** Probe-arming cap: stop probing and lock EAGER once a call-site arms this many probes without
+    *  completing warm-up (a liveness guard for call-sites that almost always hit the session cache). */
+   private static final long PROBE_ARM_CAP;
+
+   /** Instrumentation: a profile locked in the EAGER verdict. */
+   private static final SimpleCounter VERDICT_EAGER =
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydrationVerdictEager);
+
+   /** Instrumentation: a profile locked in the LAZY verdict. */
+   private static final SimpleCounter VERDICT_LAZY =
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydrationVerdictLazy);
+
+   /**
+    * Number of profiles still {@link Verdict#UNKNOWN} across all databases. While this is positive
+    * (and the feature is enabled) {@link BaseRecord#PROBING_ACTIVE} is kept {@code true}; when it
+    * drops to zero probing is switched off process-wide so steady-state field access pays nothing
+    * but a single volatile read.
+    */
+   private static final AtomicLong probingProfiles = new AtomicLong(0);
+
+   static
+   {
+      Directory dir = DirectoryManager.getInstance();
+      ENABLED         = dir.getBoolean(Directory.ID_RELATIVE_SERVER, CFG_ROOT + "enabled", true);
+      WARMUP_COUNT    = dir.getInt(Directory.ID_RELATIVE_SERVER, CFG_ROOT + "warmup-count", 32);
+      // Calibrated from the wide6720 (48-field) perf harness: lazy only marginally beats eager even
+      // when reading 1/48 fields (~0.02) and clearly loses by full read, putting the break-even at
+      // ~0.10. Set conservatively at the crossover since wrong-LAZY is the regressing direction.
+      EAGER_THRESHOLD = dir.getDouble(Directory.ID_RELATIVE_SERVER, CFG_ROOT + "eager-threshold", 0.10);
+      LAZY_QUORUM     = dir.getDouble(Directory.ID_RELATIVE_SERVER, CFG_ROOT + "lazy-quorum", 0.90);
+      MAX_PROFILES    = dir.getInt(Directory.ID_RELATIVE_SERVER, CFG_ROOT + "max-profiles", 4096);
+      PROBE_ARM_CAP   = 50L * WARMUP_COUNT;
+
+      if (LOG.isLoggable(Level.CONFIG))
+      {
+         LOG.config("Lazy-feedback hydration: enabled=" + ENABLED + ", warmup-count=" + WARMUP_COUNT
+                    + ", eager-threshold=" + EAGER_THRESHOLD + ", lazy-quorum=" + LAZY_QUORUM
+                    + ", max-profiles=" + MAX_PROFILES);
+      }
+   }
+
+   /** The hydration decision for a call-site. */
+   public enum Verdict
+   {
+      /** Not yet enough evidence; hydrate eagerly while probing. */
+      UNKNOWN,
+      /** Hydrate all fields up front (default; the call-site reads most of what it selects). */
+      EAGER,
+      /** Hydrate fields on first access (the call-site reads only a few of the selected fields). */
+      LAZY
+   }
+
+   /** The FQL identifying the call-site (used only for diagnostics; logged as a hash). */
+   private final String fql;
+
+   /** Number of demand samples collected so far. */
+   private final AtomicLong recordsSampled = new AtomicLong(0);
+
+   /** Number of those samples that were lazy-friendly (read fraction below {@link #EAGER_THRESHOLD}). */
+   private final AtomicLong lowDemandSamples = new AtomicLong(0);
+
+   /** Number of probes armed for this profile (drives the {@link #PROBE_ARM_CAP} liveness guard). */
+   private final AtomicLong probesArmed = new AtomicLong(0);
+
+   /** The current, sticky verdict. */
+   private volatile Verdict verdict = Verdict.UNKNOWN;
+
+   /**
+    * Create a profile for the given call-site. Only called (via
+    * {@code Persistence.getOrCreateProfile}) when the feature is enabled and the per-database cap
+    * has not been reached.
+    *
+    * @param   fql
+    *          The FQL identifying the call-site.
+    */
+   public FieldUsageProfile(String fql)
+   {
+      this.fql = fql;
+      probingProfiles.incrementAndGet();
+      BaseRecord.setProbingActive(true);
+   }
+
+   /**
+    * @return  {@code true} if the runtime-feedback layer is enabled.
+    */
+   public static boolean isEnabled()
+   {
+      return ENABLED;
+   }
+
+   /**
+    * @return  The maximum number of profiles to hold per database.
+    */
+   public static int maxProfiles()
+   {
+      return MAX_PROFILES;
+   }
+
+   /**
+    * @return  The current verdict for this call-site.
+    */
+   public Verdict verdict()
+   {
+      return verdict;
+   }
+
+   /**
+    * @return  {@code true} if no verdict has been locked yet (i.e. probing is still useful).
+    */
+   public boolean isUnknown()
+   {
+      return verdict == Verdict.UNKNOWN;
+   }
+
+   /**
+    * @return  {@code true} if this call-site has locked in the LAZY verdict.
+    */
+   public boolean isLazy()
+   {
+      return verdict == Verdict.LAZY;
+   }
+
+   /**
+    * Note that a probe was armed for a record of this call-site. Used to detect call-sites that keep
+    * arming probes (e.g. repeated session-cache hits) without ever completing warm-up, in which case
+    * the verdict is locked EAGER to stop probing.
+    */
+   void onProbeArmed()
+   {
+      long armed = probesArmed.incrementAndGet();
+      if (armed >= PROBE_ARM_CAP && verdict == Verdict.UNKNOWN && recordsSampled.get() < WARMUP_COUNT)
+      {
+         lockIn(Verdict.EAGER);
+      }
+   }
+
+   /**
+    * Record a single demand sample for one visit of a record of this call-site.
+    *
+    * @param   accessed
+    *          The number of distinct fields read during the visit.
+    * @param   total
+    *          The total number of fields in the record.
+    */
+   void recordSample(int accessed, int total)
+   {
+      if (verdict != Verdict.UNKNOWN || total <= 0)
+      {
+         return;
+      }
+
+      long sampled = recordsSampled.incrementAndGet();
+      if ((double) accessed / total < EAGER_THRESHOLD)
+      {
+         lowDemandSamples.incrementAndGet();
+      }
+
+      if (sampled >= WARMUP_COUNT)
+      {
+         computeVerdict();
+      }
+   }
+
+   /**
+    * Compute and lock the verdict once warm-up has completed, using the EAGER-biased quorum rule.
+    */
+   private void computeVerdict()
+   {
+      if (verdict != Verdict.UNKNOWN)
+      {
+         return;
+      }
+
+      long sampled = recordsSampled.get();
+      if (sampled < WARMUP_COUNT)
+      {
+         return;
+      }
+
+      double lowRatio = (double) lowDemandSamples.get() / sampled;
+      lockIn(lowRatio >= LAZY_QUORUM ? Verdict.LAZY : Verdict.EAGER);
+   }
+
+   /**
+    * Lock in the given verdict (if not already locked), emitting the diagnostic log line and the JMX
+    * counter, and switching off process-wide probing once the last UNKNOWN profile resolves.
+    *
+    * @param   v
+    *          The verdict to lock in.
+    */
+   private synchronized void lockIn(Verdict v)
+   {
+      if (verdict != Verdict.UNKNOWN)
+      {
+         return;
+      }
+
+      verdict = v;
+
+      if (v == Verdict.LAZY)
+      {
+         VERDICT_LAZY.update(1);
+      }
+      else
+      {
+         VERDICT_EAGER.update(1);
+      }
+
+      if (LOG.isLoggable(Level.CONFIG))
+      {
+         LOG.config("Hydration verdict locked: fql#" + Integer.toHexString(fql.hashCode()) + " -> " + v
+                    + " (sampled=" + recordsSampled.get() + ", lowDemand=" + lowDemandSamples.get()
+                    + ", armed=" + probesArmed.get() + ")");
+      }
+
+      if (probingProfiles.decrementAndGet() <= 0)
+      {
+         BaseRecord.setProbingActive(false);
+      }
+   }
+}

=== modified file 'src/com/goldencode/p2j/persist/orm/FullRowStructure.java'
--- old/src/com/goldencode/p2j/persist/orm/FullRowStructure.java	2025-07-16 14:01:50 +0000
+++ new/src/com/goldencode/p2j/persist/orm/FullRowStructure.java	2026-06-10 06:43:53 +0000
@@ -12,6 +12,7 @@
 ** 002 OM  20240712 Added support for hydrating expanded extent fields.
 **     SP  20250321 Removed usage of property.index. Fixed typos.
 ** 003 LS  20250714 Added 'getNeededFields' implementation.
+** 004 AL2 20231214 Added implementation for hydrateAt
 */
 
 /*
@@ -71,8 +72,7 @@
 
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.BitSet;
-import java.util.Iterator;
+import java.util.*;
 import com.goldencode.p2j.persist.PersistenceException;
 import com.goldencode.p2j.persist.TempRecord;
 
@@ -241,11 +241,62 @@
       {
          hydrateExtents(session, r, recOffset);
       }
+
+      r.clearHydrator();
       
       return rsOffset;
    }
    
    /**
+    * Hydrate only one property in the base-record. This is largely used by the lazy hydrator,
+    * allowing it to hydrate only the property that is accessed now. The other hydration method 
+    * ({@link #hydrate}) eagerly hydrates all properties, is a slow process and may do useless
+    * hydrations on properties that are not accessed.
+    * 
+    * @param   resultSet
+    *          The ResultSet from which the hydration should be done
+    * @param   rsOffset
+    *          The result set offset from which the reading should be done. This is relevant as
+    *          the result set may contain a joined row, but we are interested only in a specific
+    *          record that may start at a specific offset.
+    * @param   r
+    *          The record that should be hydrated. 
+    * @param   propOffset
+    *          The property that should be hydrated. The caller is responsible in assuring that
+    *          this property wasn't hydrated before.
+    * 
+    * @return  {@code true} if this worked due to the fact that it was a scalar value. {@code false}
+    *          in case the property was a non-expanded 
+    *          
+    * @throws  PersistenceException
+    *          This is thrown when reading the scalar property fails.
+    */
+   @Override
+   public boolean hydrateAt(ResultSet resultSet, int rsOffset, BaseRecord r, int propOffset)
+   throws PersistenceException
+   {
+      // we know the result-set if properly formatted and matched the data in the dataIndex
+      PropertyMeta pm = getPropertyMeta()[propOffset];
+      if (pm.getExtent() == 0)
+      {
+         r.readProperty(propOffset, resultSet, (rsOffset - 1) + pm.columnIndex, false);
+         return true;
+      }
+      else if(pm.dmoProperty.expanded)
+      {
+         int extentIndex = (propOffset - pm.offset) * pm.columnCount; 
+         // pm.columnIndex refers to the index of the 1st element in the extent, 
+         // so we need to add the current extent index
+         // Multiplication by pm.columnCount is needed for cases when the extent element is represented on 
+         // more SQL columns
+         r.readProperty(propOffset, resultSet, (rsOffset - 1) + pm.columnIndex + extentIndex);
+         return true;
+      }
+      
+      return false;
+   }
+   
+   /**
     * Check if this structure is used for an incomplete record. 
     * 
     * This specific row structure (full) will always retrieve the whole record.
@@ -266,4 +317,18 @@
    {
       return null;
    }
+   
+   /**
+    * Retrieve the fields that were loaded from the database.
+    *
+    * @return A BitSet object, where true means that the field associated with that position in the {@code
+    * data} array has been loaded from database, and false - otherwise.
+    */
+   @Override
+   public BitSet getLoadedFields()
+   {
+      BitSet fields = new BitSet(getPropertyMeta().length);
+      fields.flip(0, fields.size());
+      return fields;
+   }
 }

=== added file 'src/com/goldencode/p2j/persist/orm/Hydrator.java'
--- old/src/com/goldencode/p2j/persist/orm/Hydrator.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/persist/orm/Hydrator.java	2026-06-10 08:01:22 +0000
@@ -0,0 +1,324 @@
+/*
+** Module   : Hydrator.java
+** Abstract : Stores a lazy result set to enable lazy (just-in-time) hydration of a DMO.
+**
+** Copyright (c) 2023, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
+** 001 ECF 20230102 Created initial version.
+**     AL2 20231102 Added a loader as a fallback if the result-set is closed.
+**     AL2 20231103 Added a JMX to track lazy hydration
+**     AL2 20231106 Allow refresh with partial fields.
+**     AL2 20231110 Support hydration of normalized extent field in this class. The record will rely on this
+**                  instance to handle the whole hydration boilerplate.
+*/
+
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+**
+** Additional terms under GNU Affero GPL version 3 section 7:
+**
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+**
+**   0. Attribution Requirement.
+**
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+**
+**   1. No License To Use Trademarks.
+**
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+**
+**   2. No Misrepresentation of Affiliation.
+**
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+**
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+**
+**   3. No Misrepresentation of Source or Origin.
+**
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+*/
+
+package com.goldencode.p2j.persist.orm;
+
+import java.lang.ref.*;
+import java.sql.*;
+import java.util.BitSet;
+
+import com.goldencode.p2j.jmx.*;
+import com.goldencode.p2j.persist.PersistenceException;
+
+/**
+ * This class stores a lazy result set to enable deferred/lazy (just-in-time) hydration of a DMO. One or
+ * more instances of this class is created when a scrollable result set is positioned at a new row, when
+ * reading through the results of a query. One instance is created per DMO that needs to be hydrated from
+ * that result row. At that moment, a token is retrieved from the result set. When the result set is needed
+ * for lazy hydration later, the token is compared to the internal state of the result set. If any state has
+ * changed which prevents lazy hydration (e.g., the result set was closed, repositioned to a new row, etc.),
+ * {@link #getResultSet()} will return {@code null} instead of the result set. It is expected that the use
+ * pattern for this object is to invoke {@link #getResultSet()} and then immediately retrieve data from the
+ * returned result set (if not {@code null}).
+ */
+class Hydrator
+{
+   /** Instrumentation for {@link #getResultSet}. */
+   private static final SimpleCounter VALID_HYDRATOR = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydratorHit);
+   
+   /** Instrumentation for {@link #getResultSet}. */
+   private static final SimpleCounter HYDRATOR_MISS = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydratorMiss);
+   
+   /** Instrumentation for {@link #refresh}. */
+   private static final SimpleCounter SUCCESSFULL_REFRESH = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HyratorSuccessfullRefresh);
+   
+   /** Instrumentation for {@link #refresh}. */
+   private static final SimpleCounter UNSUCCESSFULL_REFRESH = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HyratorUnsuccessfullRefresh);
+   
+   /** Token which is used to track validity of the hydrator vis-a-vis the backing result set */
+   final long token;
+   
+   /** Offset of the starting point in a result set row from which to begin reading DMO data */
+   final int rsOffset;
+   
+   /** Weak reference to the backing result set */
+   private final WeakReference<LazyResultSet> rsRef;
+   
+   /** Weak reference to the backing session used for refreshing */
+   private WeakReference<Session> sessionRef;
+   
+   /** Weak reference to the loader which should be used for refreshing*/
+   private WeakReference<Loader> loaderRef;
+   
+   /** The row structure of the result-set */
+   private RowStructure rowStruct;
+   
+   /**
+    * Constructor.
+    * 
+    * @param   token
+    *          Validity token for the backing result set.
+    * @param   rowStruct
+    *          The row-structure of the result-set.
+    * @param   resultSet
+    *          The result set to be stored for lazy hydration use.
+    * @param   rsOffset
+    *          The offset of the start of the data for the current record. Should point to the primary key.
+    * @param   meta
+    *          The record meta of the record which is meant to be hydrated by this hydrator. 
+    * @param   sessionRef
+    *          A weak reference to the session that backs this hydrator.
+    * @param   loaderRef
+    *          A weak reference to the loader that backs this hydrator.
+    */
+   Hydrator(long token,
+            RowStructure rowStruct,
+            LazyResultSet resultSet, 
+            int rsOffset, 
+            RecordMeta meta, 
+            WeakReference<Session> sessionRef,
+            WeakReference<Loader> loaderRef)
+   {
+      this.token = token;
+      this.rsRef = new WeakReference<>(resultSet);
+      this.rsOffset = rsOffset;
+      this.rowStruct = rowStruct;
+      this.sessionRef = sessionRef;
+      this.loaderRef = loaderRef;
+   }
+   
+   /**
+    * Get the result set backing this hydrator, or {@code null} if the result set is no longer valid for
+    * lazy hydration, based on the token that was stored when this hydrator was created. To be considered
+    * valid, the result set must be positioned on the same record it was when the hydrator was created, and
+    * its state must be such that data can still be fetched from it (e.g., it must not have been closed).
+    * A weak reference to the result set is maintained, so it can be garbage collected if is no longer in
+    * use.
+    * <p>
+    * The result set returned from this method is meant to be accessed immediately upon return from this
+    * method. If such access is deferred, the result set may become invalid (closed, repositioned on
+    * another record, etc.). In such a case, an error or undefined behavior might result.
+    * 
+    * @return  Result set backing this hydrator, or {@code null} if it has become invalid for lazy hydration
+    *          purposes.
+    */
+   LazyResultSet getResultSet()
+   {
+      LazyResultSet rs = rsRef.get();
+      if (rs != null && rs.isHydratorValid(this))
+      {
+         VALID_HYDRATOR.update(1);
+         return rs;
+      }
+
+      HYDRATOR_MISS.update(1);
+      return null;
+   }
+
+   /**
+    * Allow refreshing a dmo based on the fallback loader here. this may fail if the loader is no longer
+    * available. This is because the session may be closed.
+    * 
+    * It is enough to do a quick load without doing any session cache work.
+    * 
+    * @param   dmo
+    *          The dmo to be refreshed
+    * @param   recid
+    *          The id based on which the refreshing should be done.
+    *          
+    * @return  {@code true} if the loading succeeded. This may fail if the session was closed.
+    */
+   public void refresh(BaseRecord dmo, Long recid, BitSet partialFields)
+   {
+      Loader loader;
+      if (loaderRef == null || (loader = loaderRef.get()) == null)
+      {
+         UNSUCCESSFULL_REFRESH.update(1);
+         throw new LazyHydrationException(recid, -1);
+      }
+      
+      try
+      {
+         // don't mark as incomplete; the other fields were hydrated on-time
+         loader.load(dmo, recid, partialFields);
+      }
+      catch (PersistenceException e)
+      {         
+         UNSUCCESSFULL_REFRESH.update(1);
+         throw new LazyHydrationException(recid, -1, e);
+      }
+      
+      SUCCESSFULL_REFRESH.update(1);
+   }
+
+   /**
+    * Hydrate a specific field in a record based on the underlying result-set. This is used 
+    * to honor lazy hydration. When a record field is accessed and not yet hydrated, this method
+    * will read from the result-set. If the result-set was closed, attempt to reload from database
+    * using the loader. This is an expensive call, but empirically it happens very rarely.
+    * 
+    * @param   r
+    *          The record to be hydrated.
+    * @param   propOffset
+    *          The property which should be hydrated. This has the index of the data.
+    * @param   rowOffset
+    *          An eventual offset to skip leading fields (i.e. recid or multiplex).
+    * @param   allowOtherImplicitHydration
+    *          Allow other implicit hydration (e.g. hydrate the other fields in unique indices).
+    *          
+    * @return  {@code true} if this requires a refresh or {@code false} otherwise. This information
+    *          is used by the record to know if the hydrator should be dropped or not. If a refresh
+    *          happened, lazy hydration is no longer needed.
+    */
+   public boolean hydrate(BaseRecord r, int propOffset, int rowOffset, boolean allowOtherImplicitHydration)
+   { 
+      ResultSet rs = getResultSet();
+      Long id = r.primaryKey();
+      
+      if (rs != null)
+      {
+         try
+         {
+            boolean wasScalar = rowStruct.hydrateAt(rs, rsOffset + rowOffset, r, propOffset);
+            if (!wasScalar)
+            {
+               Session session = null;
+               if (sessionRef == null || (session = sessionRef.get()) == null)
+               {
+                  throw new LazyHydrationException(r.primaryKey(), -1);
+               }
+               rowStruct.hydrateExtents(session, r, -1);
+            }
+            
+            if (allowOtherImplicitHydration)
+            {
+               hydrateRemainingFieldsInUniqueIndexes(r, propOffset, rowOffset);
+            }
+         }
+         catch (PersistenceException e)
+         {
+            throw new LazyHydrationException(id, propOffset, e);
+         }
+         
+         return true;
+      }
+      
+      return false;
+   }
+   
+   /**
+    * Hydrates other unhydrated fields that belong to the same unique indexes as the specified property.
+    * 
+    *
+    * @param   r
+    *          The record to be hydrated.
+    * @param   propOffset
+    *          The property whose unique indices should be checked. This has the index of the data.
+    * @param   rowOffset
+    *          An eventual offset to skip leading fields (i.e. recid or multiplex).
+    */
+   private void hydrateRemainingFieldsInUniqueIndexes(BaseRecord r, int propOffset, int rowOffset)
+   {
+      BitSet[] uniqueIndices = r._recordMeta().getUniqueIndices();
+      
+      for(BitSet uIndex : uniqueIndices)
+      {
+         if (uIndex.get(propOffset))
+         {
+            for (int i = uIndex.nextSetBit(0); i >= 0; i = uIndex.nextSetBit(i + 1))
+            {
+               // don't hydrate if the field is the current one or if the field has already been hydrated
+               if (i == propOffset || r.isFieldHydrated(i))
+               {
+                  continue;
+               }
+               
+               hydrate(r, i, rowOffset, false);
+            }
+         }
+      }
+   }
+   
+   
+   /**
+    * Always attempt to refresh the session to the new one. Otherwise, this could still reference
+    * a stale session which waits to be garbage collected (or reclaimed). 
+    * 
+    * @param   session
+    *          The session to which we attempt to attach this hydrator.
+    */
+   public void refresh(Session session)
+   {
+      this.sessionRef = new WeakReference<>(session);
+      this.loaderRef = session.getLoader();
+   }
+}

=== added file 'src/com/goldencode/p2j/persist/orm/LazyHydrationException.java'
--- old/src/com/goldencode/p2j/persist/orm/LazyHydrationException.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/persist/orm/LazyHydrationException.java	2026-06-10 08:01:22 +0000
@@ -0,0 +1,136 @@
+/*
+** Module   : LazyHydrationException.java
+** Abstract : An exception type for lazy hydration runtime errors.
+**
+** Copyright (c) 2023, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
+** 001 ECF 20230619 Created initial version.
+**     AL2 20231102 Added constructor with clause.
+*/
+
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+**
+** Additional terms under GNU Affero GPL version 3 section 7:
+**
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+**
+**   0. Attribution Requirement.
+**
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+**
+**   1. No License To Use Trademarks.
+**
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+**
+**   2. No Misrepresentation of Affiliation.
+**
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+**
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+**
+**   3. No Misrepresentation of Source or Origin.
+**
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+*/
+
+package com.goldencode.p2j.persist.orm;
+
+/**
+ * This exception type is used with lazy hydration of DMOs from result sets. It is raised when a {@link
+ * Hydrator} associated with a {@link BaseRecord} is no longer valid, or when a database error occurs during
+ * lazy hydration of a DMO's data.
+ * <p>
+ * This class extends {@code RuntimeException} rather than {@code Exception} to avoid complicating all
+ * generated DMO interface getter and setter methods. Lazy hydration is used only by the persistence runtime,
+ * while DMO interfaces are used in proxy form by application logic. It is undesirable to clutter application
+ * logic with checked exception handling, since these exceptions will be caught and handled at lower levels
+ * within the runtime environment and must not propagate beyond the DMO proxy invocation handlers to the
+ * application level.
+ */
+@SuppressWarnings("serial")
+public class LazyHydrationException
+extends RuntimeException
+{
+   private final Long primaryKey;
+   
+   private final int offset;
+   
+   /**
+    * Constructor used when hydrating a datum lazily and the hydrator is no longer valid.
+    * 
+    * @param   primaryKey
+    *          Primary key of record which failed lazy hydration.
+    * @param   offset
+    *          Starting offset of the datum which could not be retrieved, in the record's data array.
+    */
+   LazyHydrationException(Long primaryKey, int offset)
+   {
+      this(primaryKey, offset, null);
+   }
+   
+   /**
+    * Constructor used when hydrating a datum lazily and the hydrator is no longer valid.
+    * 
+    * @param   primaryKey
+    *          Primary key of record which failed lazy hydration.
+    * @param   offset
+    *          Starting offset of the datum which could not be retrieved, in the record's data array.
+    */
+   LazyHydrationException(Long primaryKey, int offset, Throwable cause)
+   {
+      super(cause);
+      this.primaryKey = primaryKey;
+      this.offset = offset;
+   }
+   
+   public long getPrimaryKey()
+   {
+      return primaryKey;
+   }
+   
+   public int getOffset()
+   {
+      return offset;
+   }
+   
+   /**
+    * Since this exception is only thrown from one location, override the default implementation to avoid
+    * the overhead of filling in the stack trace.
+    * 
+    * @return  This object.
+    */
+   @Override
+   public LazyHydrationException fillInStackTrace()
+   {
+      return this;
+   }
+}

=== added file 'src/com/goldencode/p2j/persist/orm/LazyResultSet.java'
--- old/src/com/goldencode/p2j/persist/orm/LazyResultSet.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/persist/orm/LazyResultSet.java	2026-06-10 08:01:39 +0000
@@ -0,0 +1,2097 @@
+/*
+** Module   : LazyResultSet.java
+** Abstract : Delegating result set wrapper which tracks validity for lazy hydration.
+**
+** Copyright (c) 2023, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
+** 001 ECF 20230804 Created initial version.
+**     AL2 20231102 Added loader to the hydrator to fall-back in case the result-set expires.
+**     AL2 20231110 Store a weak reference to the session instead of loader. Allow this to store the statement
+**                  to eventually return the statement that created this result-set, not the one that created
+**                  the underlying result-set (see UnclosablePreparedStatement).
+*/
+
+/*
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU Affero General Public License as
+** published by the Free Software Foundation, either version 3 of the
+** License, or (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+** GNU Affero General Public License for more details.
+**
+** You may find a copy of the GNU Affero GPL version 3 at the following
+** location: https://www.gnu.org/licenses/agpl-3.0.en.html
+**
+** Additional terms under GNU Affero GPL version 3 section 7:
+**
+**   Under Section 7 of the GNU Affero GPL version 3, the following additional
+**   terms apply to the works covered under the License.  These additional terms
+**   are non-permissive additional terms allowed under Section 7 of the GNU
+**   Affero GPL version 3 and may not be removed by you.
+**
+**   0. Attribution Requirement.
+**
+**     You must preserve all legal notices or author attributions in the covered
+**     work or Appropriate Legal Notices displayed by works containing the covered
+**     work.  You may not remove from the covered work any author or developer
+**     credit already included within the covered work.
+**
+**   1. No License To Use Trademarks.
+**
+**     This license does not grant any license or rights to use the trademarks
+**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
+**     of Golden Code Development Corporation. You are not authorized to use the
+**     name Golden Code, FWD, or the names of any author or contributor, for
+**     publicity purposes without written authorization.
+**
+**   2. No Misrepresentation of Affiliation.
+**
+**     You may not represent yourself as Golden Code Development Corporation or FWD.
+**
+**     You may not represent yourself for publicity purposes as associated with
+**     Golden Code Development Corporation, FWD, or any author or contributor to
+**     the covered work, without written authorization.
+**
+**   3. No Misrepresentation of Source or Origin.
+**
+**     You may not represent the covered work as solely your work.  All modified
+**     versions of the covered work must be marked in a reasonable way to make it
+**     clear that the modified work is not originating from Golden Code Development
+**     Corporation or FWD.  All modified versions must contain the notices of
+**     attribution required in this license.
+*/
+
+package com.goldencode.p2j.persist.orm;
+
+import java.io.*;
+import java.lang.ref.WeakReference;
+import java.math.*;
+import java.net.*;
+import java.sql.*;
+import java.sql.Date;
+import java.util.*;
+
+/**
+ * A wrapper for a {@code ResultSet} which delegates all methods to the wrapped result set and which
+ * additionally can create and validate {@link Hydrator} objects. A hydrator can extract data from a
+ * particular row in the result set, but becomes invalid if that row is no longer accessible.
+ * <p>
+ * Hydrator validity is determined by comparing a counter's value at the time the hydrator instance is
+ * created with that counter's value at the time validity is checked. When {@link #createHydrator}
+ * creates a new hydrator instance, the counter's current value is saved as a token in the hydrator object.
+ * The counter is incremented every time the wrapped result set's state is changed to move to another row,
+ * or to otherwise make the result set unreadable (e.g., it is closed).
+ * <p>
+ * If the current value of the counter does not match a hydrator's stored token at the moment {@link
+ * #isHydratorValid(Hydrator)} is invoked, the validation fails.
+ *
+ */
+public final class LazyResultSet
+implements ResultSet
+{   
+   /** Delegate statement */
+   private final Statement stmt;
+   
+   /** Delegate result set */
+   private final ResultSet rs;
+   
+   /** Counter which is updated for every change which could invalidate a hydrator */
+   private long changes = 0L;
+   
+   /** The fallback loader to help generated hydrators to recover from invalidated result-sets */
+   private WeakReference<Session> sessionRef;
+   
+   /**
+    * Constructor which accepts result set delegate.
+    * 
+    * @param   stmt
+    *          The statement that generated this result-set. This is not always the statement of 
+    *          the provided rs.
+    * @param   rs
+    *          Result set to which all operations are delegated.
+    * @param   session
+    *          The session under which this result-set was generated. This is relevant to support
+    *          the changing of ownership for the underlying result-set.
+    */
+   public LazyResultSet(Statement stmt, ResultSet rs, Session session)
+   {
+      this.stmt = stmt;
+      this.rs = rs;
+      this.sessionRef = new WeakReference<>(session);
+   }
+   
+   /**
+    * Create a new {@link Hydrator} instance, suitable for reading a single DMO's data values from the
+    * current row of the wrapped result set. The hydrator will only remain valid until the result set is
+    * moved to another row, or it is closed or otherwise made invalid by some method of this class.
+    * 
+    * @param   rowStruct
+    *          The row structure of the of the result-set.
+    * @param   rsOffset
+    *          The offset at which the hydrator should operate. This helps for the cases when the result-set
+    *          was generated by a join statement. 
+    * @param   rm
+    *          The record meta used by the hydrating process.
+    *           
+    * @return  A hydator used by the record to do lazy hydration.
+    */
+   public Hydrator createHydrator(RowStructure rowStruct, int rsOffset, RecordMeta rm)
+   {
+      Session session;
+      if (sessionRef == null || (session = sessionRef.get()) == null)
+      {
+         // no loader could be achieved
+         return new Hydrator(changes, rowStruct, this, rsOffset, rm, sessionRef, null);
+      }
+      
+      return new Hydrator(changes, rowStruct, this, rsOffset, rm, sessionRef, session.getLoader());
+   }
+   
+   /**
+    * Indicate whether the given hydrator is valid. A hydrator is valid if the token it was created with
+    * equals the current change count. If the change count is not the same, it indicates the result set's
+    * state has changed and it may no longer be positioned on the result row which the hydrator needs to
+    * read its data, or the result set was closed.
+    * 
+    * @param   hydrator
+    *          Hydrator to test for validity. Must not be {@code null}.
+    * 
+    * @return  {@code true} if the hydrator can still be used with the backing result set.
+    */
+   public boolean isHydratorValid(Hydrator hydrator)
+   {
+      return hydrator.token == changes;
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public <T> T unwrap(Class<T> iface)
+   throws SQLException
+   {
+      return rs.unwrap(iface);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean isWrapperFor(Class<?> iface)
+   throws SQLException
+   {
+      return rs.isWrapperFor(iface);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean next()
+   throws SQLException
+   {
+      changes++;
+      return rs.next();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void close()
+   throws SQLException
+   {
+      changes++;
+      rs.close();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean wasNull()
+   throws SQLException
+   {
+      return rs.wasNull();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public String getString(int columnIndex)
+   throws SQLException
+   {
+      return rs.getString(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean getBoolean(int columnIndex)
+   throws SQLException
+   {
+      return rs.getBoolean(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public byte getByte(int columnIndex)
+   throws SQLException
+   {
+      return rs.getByte(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public short getShort(int columnIndex)
+   throws SQLException
+   {
+      return rs.getShort(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public int getInt(int columnIndex)
+   throws SQLException
+   {
+      return rs.getInt(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public long getLong(int columnIndex)
+   throws SQLException
+   {
+      return rs.getLong(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public float getFloat(int columnIndex)
+   throws SQLException
+   {
+      return rs.getFloat(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public double getDouble(int columnIndex)
+   throws SQLException
+   {
+      return rs.getDouble(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   @Deprecated
+   public BigDecimal getBigDecimal(int columnIndex, int scale)
+   throws SQLException
+   {
+      return rs.getBigDecimal(columnIndex, scale);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public byte[] getBytes(int columnIndex)
+   throws SQLException
+   {
+      return rs.getBytes(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Date getDate(int columnIndex)
+   throws SQLException
+   {
+      return rs.getDate(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Time getTime(int columnIndex)
+   throws SQLException
+   {
+      return rs.getTime(columnIndex);
+   }
+
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Timestamp getTimestamp(int columnIndex)
+   throws SQLException
+   {
+      return rs.getTimestamp(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public InputStream getAsciiStream(int columnIndex)
+   throws SQLException
+   {
+      return rs.getAsciiStream(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   @Deprecated
+   public InputStream getUnicodeStream(int columnIndex)
+   throws SQLException
+   {
+      return rs.getUnicodeStream(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public InputStream getBinaryStream(int columnIndex)
+   throws SQLException
+   {
+      return rs.getBinaryStream(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public String getString(String columnLabel)
+   throws SQLException
+   {
+      return rs.getString(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean getBoolean(String columnLabel)
+   throws SQLException
+   {
+      return rs.getBoolean(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public byte getByte(String columnLabel)
+   throws SQLException
+   {
+      return rs.getByte(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public short getShort(String columnLabel)
+   throws SQLException
+   {
+      return rs.getShort(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public int getInt(String columnLabel)
+   throws SQLException
+   {
+      return rs.getInt(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public long getLong(String columnLabel)
+   throws SQLException
+   {
+      return rs.getLong(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public float getFloat(String columnLabel)
+   throws SQLException
+   {
+      return rs.getFloat(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public double getDouble(String columnLabel)
+      throws SQLException
+   {
+      return rs.getDouble(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   @Deprecated
+   public BigDecimal getBigDecimal(String columnLabel, int scale)
+   throws SQLException
+   {
+      return rs.getBigDecimal(columnLabel, scale);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public byte[] getBytes(String columnLabel)
+   throws SQLException
+   {
+      return rs.getBytes(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Date getDate(String columnLabel)
+   throws SQLException
+   {
+      return rs.getDate(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Time getTime(String columnLabel)
+   throws SQLException
+   {
+      return rs.getTime(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Timestamp getTimestamp(String columnLabel)
+   throws SQLException
+   {
+      return rs.getTimestamp(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public InputStream getAsciiStream(String columnLabel)
+   throws SQLException
+   {
+      return rs.getAsciiStream(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   @Deprecated
+   public InputStream getUnicodeStream(String columnLabel)
+   throws SQLException
+   {
+      return rs.getUnicodeStream(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public InputStream getBinaryStream(String columnLabel)
+   throws SQLException
+   {
+      return rs.getBinaryStream(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public SQLWarning getWarnings()
+   throws SQLException
+   {
+      return rs.getWarnings();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void clearWarnings()
+   throws SQLException
+   {
+      rs.clearWarnings();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public String getCursorName()
+   throws SQLException
+   {
+      return rs.getCursorName();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public ResultSetMetaData getMetaData()
+   throws SQLException
+   {
+      return rs.getMetaData();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Object getObject(int columnIndex)
+   throws SQLException
+   {
+      return rs.getObject(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Object getObject(String columnLabel)
+   throws SQLException
+   {
+      return rs.getObject(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public int findColumn(String columnLabel)
+   throws SQLException
+   {
+      return rs.findColumn(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Reader getCharacterStream(int columnIndex)
+   throws SQLException
+   {
+      return rs.getCharacterStream(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Reader getCharacterStream(String columnLabel)
+   throws SQLException
+   {
+      return rs.getCharacterStream(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public BigDecimal getBigDecimal(int columnIndex)
+   throws SQLException
+   {
+      return rs.getBigDecimal(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public BigDecimal getBigDecimal(String columnLabel)
+   throws SQLException
+   {
+      return rs.getBigDecimal(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean isBeforeFirst()
+   throws SQLException
+   {
+      return rs.isBeforeFirst();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean isAfterLast()
+   throws SQLException
+   {
+      return rs.isAfterLast();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean isFirst()
+   throws SQLException
+   {
+      return rs.isFirst();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean isLast()
+   throws SQLException
+   {
+      return rs.isLast();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void beforeFirst()
+   throws SQLException
+   {
+      changes++;
+      rs.beforeFirst();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void afterLast()
+   throws SQLException
+   {
+      changes++;
+      rs.afterLast();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean first()
+   throws SQLException
+   {
+      changes++;
+      return rs.first();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean last()
+   throws SQLException
+   {
+      changes++;
+      return rs.last();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public int getRow()
+   throws SQLException
+   {
+      return rs.getRow();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean absolute(int row)
+   throws SQLException
+   {
+      if (row != rs.getRow())
+      {
+         changes++;
+      }
+      return rs.absolute(row);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean relative(int rows)
+   throws SQLException
+   {
+      if (rows != 0)
+      {
+         changes++;
+      }
+      return rs.relative(rows);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean previous()
+   throws SQLException
+   {
+      changes++;
+      return rs.previous();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void setFetchDirection(int direction)
+   throws SQLException
+   {
+      rs.setFetchDirection(direction);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public int getFetchDirection()
+   throws SQLException
+   {
+      return rs.getFetchDirection();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void setFetchSize(int rows)
+   throws SQLException
+   {
+      rs.setFetchSize(rows);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public int getFetchSize()
+   throws SQLException
+   {
+      return rs.getFetchSize();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public int getType()
+   throws SQLException
+   {
+      return rs.getType();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public int getConcurrency()
+   throws SQLException
+   {
+      return rs.getConcurrency();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean rowUpdated()
+   throws SQLException
+   {
+      return rs.rowUpdated();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean rowInserted()
+   throws SQLException
+   {
+      return rs.rowInserted();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean rowDeleted()
+   throws SQLException
+   {
+      return rs.rowDeleted();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNull(int columnIndex)
+   throws SQLException
+   {
+      rs.updateNull(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBoolean(int columnIndex, boolean x)
+   throws SQLException
+   {
+      rs.updateBoolean(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateByte(int columnIndex, byte x)
+   throws SQLException
+   {
+      rs.updateByte(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateShort(int columnIndex, short x)
+   throws SQLException
+   {
+      rs.updateShort(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateInt(int columnIndex, int x)
+   throws SQLException
+   {
+      rs.updateInt(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateLong(int columnIndex, long x)
+   throws SQLException
+   {
+      rs.updateLong(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateFloat(int columnIndex, float x)
+   throws SQLException
+   {
+      rs.updateFloat(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateDouble(int columnIndex, double x)
+   throws SQLException
+   {
+      rs.updateDouble(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBigDecimal(int columnIndex, BigDecimal x)
+   throws SQLException
+   {
+      rs.updateBigDecimal(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateString(int columnIndex, String x)
+   throws SQLException
+   {
+      rs.updateString(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBytes(int columnIndex, byte[] x)
+   throws SQLException
+   {
+      rs.updateBytes(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateDate(int columnIndex, Date x)
+   throws SQLException
+   {
+      rs.updateDate(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateTime(int columnIndex, Time x)
+   throws SQLException
+   {
+      rs.updateTime(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateTimestamp(int columnIndex, Timestamp x)
+   throws SQLException
+   {
+      rs.updateTimestamp(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateAsciiStream(int columnIndex, InputStream x, int length)
+   throws SQLException
+   {
+      rs.updateAsciiStream(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBinaryStream(int columnIndex, InputStream x, int length)
+   throws SQLException
+   {
+      rs.updateBinaryStream(columnIndex, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateCharacterStream(int columnIndex, Reader x, int length)
+   throws SQLException
+   {
+      rs.updateCharacterStream(columnIndex, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateObject(int columnIndex, Object x, int scaleOrLength)
+   throws SQLException
+   {
+      rs.updateObject(columnIndex, x, scaleOrLength);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateObject(int columnIndex, Object x)
+   throws SQLException
+   {
+      rs.updateObject(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNull(String columnLabel)
+   throws SQLException
+   {
+      rs.updateNull(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBoolean(String columnLabel, boolean x)
+   throws SQLException
+   {
+      rs.updateBoolean(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateByte(String columnLabel, byte x)
+   throws SQLException
+   {
+      rs.updateByte(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateShort(String columnLabel, short x)
+   throws SQLException
+   {
+      rs.updateShort(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateInt(String columnLabel, int x)
+   throws SQLException
+   {
+      rs.updateInt(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateLong(String columnLabel, long x)
+   throws SQLException
+   {
+      rs.updateLong(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateFloat(String columnLabel, float x)
+   throws SQLException
+   {
+      rs.updateFloat(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateDouble(String columnLabel, double x)
+   throws SQLException
+   {
+      rs.updateDouble(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBigDecimal(String columnLabel, BigDecimal x)
+   throws SQLException
+   {
+      rs.updateBigDecimal(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateString(String columnLabel, String x)
+   throws SQLException
+   {
+      rs.updateString(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBytes(String columnLabel, byte[] x)
+   throws SQLException
+   {
+      rs.updateBytes(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateDate(String columnLabel, Date x)
+   throws SQLException
+   {
+      rs.updateDate(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateTime(String columnLabel, Time x)
+   throws SQLException
+   {
+      rs.updateTime(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateTimestamp(String columnLabel, Timestamp x)
+   throws SQLException
+   {
+      rs.updateTimestamp(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateAsciiStream(String columnLabel, InputStream x, int length)
+   throws SQLException
+   {
+      rs.updateAsciiStream(columnLabel, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBinaryStream(String columnLabel, InputStream x, int length)
+   throws SQLException
+   {
+      rs.updateBinaryStream(columnLabel, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateCharacterStream(String columnLabel, Reader reader, int length)
+   throws SQLException
+   {
+      rs.updateCharacterStream(columnLabel, reader, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateObject(String columnLabel, Object x, int scaleOrLength)
+   throws SQLException
+   {
+      rs.updateObject(columnLabel, x, scaleOrLength);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateObject(String columnLabel, Object x)
+   throws SQLException
+   {
+      rs.updateObject(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void insertRow()
+   throws SQLException
+   {
+      rs.insertRow();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateRow()
+   throws SQLException
+   {
+      rs.updateRow();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void deleteRow()
+   throws SQLException
+   {
+      rs.deleteRow();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void refreshRow()
+   throws SQLException
+   {
+      rs.refreshRow();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void cancelRowUpdates()
+   throws SQLException
+   {
+      rs.cancelRowUpdates();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void moveToInsertRow()
+   throws SQLException
+   {
+      rs.moveToInsertRow();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void moveToCurrentRow()
+   throws SQLException
+   {
+      rs.moveToCurrentRow();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Statement getStatement()
+   throws SQLException
+   {
+      return stmt;
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Object getObject(int columnIndex, Map<String, Class<?>> map)
+   throws SQLException
+   {
+      return rs.getObject(columnIndex, map);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Ref getRef(int columnIndex)
+   throws SQLException
+   {
+      return rs.getRef(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Blob getBlob(int columnIndex)
+   throws SQLException
+   {
+      return rs.getBlob(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Clob getClob(int columnIndex)
+   throws SQLException
+   {
+      return rs.getClob(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Array getArray(int columnIndex)
+   throws SQLException
+   {
+      return rs.getArray(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Object getObject(String columnLabel, Map<String, Class<?>> map)
+   throws SQLException
+   {
+      return rs.getObject(columnLabel, map);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Ref getRef(String columnLabel)
+   throws SQLException
+   {
+      return rs.getRef(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Blob getBlob(String columnLabel)
+   throws SQLException
+   {
+      return rs.getBlob(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Clob getClob(String columnLabel)
+   throws SQLException
+   {
+      return rs.getClob(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Array getArray(String columnLabel)
+   throws SQLException
+   {
+      return rs.getArray(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Date getDate(int columnIndex, Calendar cal)
+   throws SQLException
+   {
+      return rs.getDate(columnIndex, cal);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Date getDate(String columnLabel, Calendar cal)
+   throws SQLException
+   {
+      return rs.getDate(columnLabel, cal);
+   }
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Time getTime(int columnIndex, Calendar cal)
+   throws SQLException
+   {
+      return rs.getTime(columnIndex, cal);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Time getTime(String columnLabel, Calendar cal)
+   throws SQLException
+   {
+      return rs.getTime(columnLabel, cal);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Timestamp getTimestamp(int columnIndex, Calendar cal)
+   throws SQLException
+   {
+      return rs.getTimestamp(columnIndex, cal);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Timestamp getTimestamp(String columnLabel, Calendar cal)
+   throws SQLException
+   {
+      return rs.getTimestamp(columnLabel, cal);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public URL getURL(int columnIndex)
+   throws SQLException
+   {
+      return rs.getURL(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public URL getURL(String columnLabel)
+   throws SQLException
+   {
+      return rs.getURL(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateRef(int columnIndex, Ref x)
+   throws SQLException
+   {
+      rs.updateRef(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateRef(String columnLabel, Ref x)
+   throws SQLException
+   {
+      rs.updateRef(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBlob(int columnIndex, Blob x)
+   throws SQLException
+   {
+      rs.updateBlob(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBlob(String columnLabel, Blob x)
+   throws SQLException
+   {
+      rs.updateBlob(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateClob(int columnIndex, Clob x)
+   throws SQLException
+   {
+      rs.updateClob(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateClob(String columnLabel, Clob x)
+   throws SQLException
+   {
+      rs.updateClob(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateArray(int columnIndex, Array x)
+   throws SQLException
+   {
+      rs.updateArray(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateArray(String columnLabel, Array x)
+   throws SQLException
+   {
+      rs.updateArray(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public RowId getRowId(int columnIndex)
+   throws SQLException
+   {
+      return rs.getRowId(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public RowId getRowId(String columnLabel)
+   throws SQLException
+   {
+      return rs.getRowId(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateRowId(int columnIndex, RowId x)
+   throws SQLException
+   {
+      rs.updateRowId(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateRowId(String columnLabel, RowId x)
+   throws SQLException
+   {
+      rs.updateRowId(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public int getHoldability()
+   throws SQLException
+   {
+      return rs.getHoldability();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public boolean isClosed()
+   throws SQLException
+   {
+      return rs.isClosed();
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNString(int columnIndex, String nString)
+   throws SQLException
+   {
+      rs.updateNString(columnIndex, nString);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNString(String columnLabel, String nString)
+   throws SQLException
+   {
+      rs.updateNString(columnLabel, nString);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNClob(int columnIndex, NClob nClob)
+   throws SQLException
+   {
+      rs.updateNClob(columnIndex, nClob);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNClob(String columnLabel, NClob nClob)
+   throws SQLException
+   {
+      rs.updateNClob(columnLabel, nClob);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public NClob getNClob(int columnIndex)
+   throws SQLException
+   {
+      return rs.getNClob(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public NClob getNClob(String columnLabel)
+   throws SQLException
+   {
+      return rs.getNClob(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public SQLXML getSQLXML(int columnIndex)
+   throws SQLException
+   {
+      return rs.getSQLXML(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public SQLXML getSQLXML(String columnLabel)
+   throws SQLException
+   {
+      return rs.getSQLXML(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateSQLXML(int columnIndex, SQLXML xmlObject)
+   throws SQLException
+   {
+      rs.updateSQLXML(columnIndex, xmlObject);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateSQLXML(String columnLabel, SQLXML xmlObject)
+   throws SQLException
+   {
+      rs.updateSQLXML(columnLabel, xmlObject);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public String getNString(int columnIndex)
+   throws SQLException
+   {
+      return rs.getNString(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public String getNString(String columnLabel)
+   throws SQLException
+   {
+      return rs.getNString(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Reader getNCharacterStream(int columnIndex)
+   throws SQLException
+   {
+      return rs.getNCharacterStream(columnIndex);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public Reader getNCharacterStream(String columnLabel)
+   throws SQLException
+   {
+      return rs.getNCharacterStream(columnLabel);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNCharacterStream(int columnIndex, Reader x, long length)
+   throws SQLException
+   {
+      rs.updateNCharacterStream(columnIndex, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNCharacterStream(String columnLabel, Reader reader, long length)
+   throws SQLException
+   {
+      rs.updateNCharacterStream(columnLabel, reader, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateAsciiStream(int columnIndex, InputStream x, long length)
+   throws SQLException
+   {
+      rs.updateAsciiStream(columnIndex, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBinaryStream(int columnIndex, InputStream x, long length)
+   throws SQLException
+   {
+      rs.updateBinaryStream(columnIndex, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateCharacterStream(int columnIndex, Reader x, long length)
+   throws SQLException
+   {
+      rs.updateCharacterStream(columnIndex, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateAsciiStream(String columnLabel, InputStream x, long length)
+   throws SQLException
+   {
+      rs.updateAsciiStream(columnLabel, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBinaryStream(String columnLabel, InputStream x, long length)
+   throws SQLException
+   {
+      rs.updateBinaryStream(columnLabel, x, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateCharacterStream(String columnLabel, Reader reader, long length)
+   throws SQLException
+   {
+      rs.updateCharacterStream(columnLabel, reader, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBlob(int columnIndex, InputStream inputStream, long length)
+   throws SQLException
+   {
+      rs.updateBlob(columnIndex, inputStream, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBlob(String columnLabel, InputStream inputStream, long length)
+   throws SQLException
+   {
+      rs.updateBlob(columnLabel, inputStream, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateClob(int columnIndex, Reader reader, long length)
+   throws SQLException
+   {
+      rs.updateClob(columnIndex, reader, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateClob(String columnLabel, Reader reader, long length)
+   throws SQLException
+   {
+      rs.updateClob(columnLabel, reader, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNClob(int columnIndex, Reader reader, long length)
+   throws SQLException
+   {
+      rs.updateNClob(columnIndex, reader, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNClob(String columnLabel, Reader reader, long length)
+   throws SQLException
+   {
+      rs.updateNClob(columnLabel, reader, length);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNCharacterStream(int columnIndex, Reader x)
+   throws SQLException
+   {
+      rs.updateNCharacterStream(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNCharacterStream(String columnLabel, Reader reader)
+   throws SQLException
+   {
+      rs.updateNCharacterStream(columnLabel, reader);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateAsciiStream(int columnIndex, InputStream x)
+   throws SQLException
+   {
+      rs.updateAsciiStream(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBinaryStream(int columnIndex, InputStream x)
+   throws SQLException
+   {
+      rs.updateBinaryStream(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateCharacterStream(int columnIndex, Reader x)
+   throws SQLException
+   {
+      rs.updateCharacterStream(columnIndex, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateAsciiStream(String columnLabel, InputStream x)
+   throws SQLException
+   {
+      rs.updateAsciiStream(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBinaryStream(String columnLabel, InputStream x)
+   throws SQLException
+   {
+      rs.updateBinaryStream(columnLabel, x);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateCharacterStream(String columnLabel, Reader reader)
+   throws SQLException
+   {
+      rs.updateCharacterStream(columnLabel, reader);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBlob(int columnIndex, InputStream inputStream)
+   throws SQLException
+   {
+      rs.updateBlob(columnIndex, inputStream);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateBlob(String columnLabel, InputStream inputStream)
+   throws SQLException
+   {
+      rs.updateBlob(columnLabel, inputStream);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateClob(int columnIndex, Reader reader)
+   throws SQLException
+   {
+      rs.updateClob(columnIndex, reader);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateClob(String columnLabel, Reader reader)
+   throws SQLException
+   {
+      rs.updateClob(columnLabel, reader);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNClob(int columnIndex, Reader reader)
+   throws SQLException
+   {
+      rs.updateNClob(columnIndex, reader);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public void updateNClob(String columnLabel, Reader reader)
+   throws SQLException
+   {
+      rs.updateNClob(columnLabel, reader);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public <T> T getObject(int columnIndex, Class<T> type)
+   throws SQLException
+   {
+      return rs.getObject(columnIndex, type);
+   }
+   
+   /** 
+    * {@inheritDoc}
+    */
+   @Override
+   public <T> T getObject(String columnLabel, Class<T> type)
+   throws SQLException
+   {
+      return rs.getObject(columnLabel, type);
+   }
+}

=== modified file 'src/com/goldencode/p2j/persist/orm/Loader.java'
--- old/src/com/goldencode/p2j/persist/orm/Loader.java	2025-07-31 10:28:13 +0000
+++ new/src/com/goldencode/p2j/persist/orm/Loader.java	2026-06-10 06:43:53 +0000
@@ -498,6 +498,17 @@
       {
          T dmo = dmoClass.getDeclaredConstructor().newInstance();
          dmo.primaryKey(id);
+
+         // mark as incomplete; as only some fields are hydrated on the new instance
+         if (partialFields != null)
+         {
+            dmo.markIncomplete(session, partialFields);
+
+            if (partialFields.isEmpty())
+            {
+               return dmo;
+            }
+         }
          
          return load(dmo, id, partialFields);
       }
@@ -543,15 +554,6 @@
                                  (props[0].getExtent() == 0 || props[0].dmoProperty.expanded);
          
          int start = 0;
-         if (partialFields != null)
-         {
-            dmo.markIncomplete(session);
-
-            if (partialFields.isEmpty())
-            {
-               return dmo;
-            }
-         }
          
          for (String sql : loadSql)
          {
@@ -606,6 +608,12 @@
       }
    }
    
+   <T extends BaseRecord> ResultSet loadResults(T dmo)
+   throws PersistenceException
+   {
+      return null;
+   }
+   
    /**
     * Read all values from the given result set for the series of scalar fields. Assumes the
     * {@code ResultSet}'s cursor is positioned on the right row.

=== modified file 'src/com/goldencode/p2j/persist/orm/OrderedRowStructure.java'
--- old/src/com/goldencode/p2j/persist/orm/OrderedRowStructure.java	2025-08-14 09:57:08 +0000
+++ new/src/com/goldencode/p2j/persist/orm/OrderedRowStructure.java	2026-06-10 08:08:06 +0000
@@ -11,6 +11,7 @@
 **     AL2 20231212 Fixed method used to retrieve propetry index.
 ** 002 LS  20250714 Added 'getNeededFields' implementation.
 ** 003 AS  20250814 Return null if hasExtents in getNeededFields.
+** 004 AL2 20231214 Added extraColumn and hydrateAt.
 */
 
 /*
@@ -85,7 +86,23 @@
 {
    /** The collected fields for select query */
    private BitSet fields;
-   
+
+   /** Mark if some fields require an extra column or are represented on a single column */
+   private BitSet extraColumn;
+
+   /**
+    * For each appended field (indexed by property offset), the cumulative number of SQL columns
+    * consumed by the fields appended before it. This is the {@code rsOffset} delta used by
+    * {@link #hydrateAt}, precomputed incrementally in {@link #addProperty} so a single-field lazy
+    * hydration is O(1) instead of rescanning {@link #fields} from the start (which made hydrating
+    * all N fields O(N&sup2;)). Only the positions corresponding to set bits in {@link #fields} are
+    * meaningful. Allocated lazily together with {@link #fields}.
+    */
+   private int[] columnOffsets;
+
+   /** Running count of SQL columns consumed by appended fields, used to populate {@link #columnOffsets}. */
+   private int fieldColumns = 0;
+
    /** The last field (not recid or multiplex) that was appended. */
    private int lastFieldSet = -1;
    
@@ -183,15 +200,37 @@
          // we are not setting the fields in order
          return false;
       }
+
+      int columnCount = getPropertyMeta()[fieldId].columnCount;
+      if (columnCount == 2)
+      {
+         if (extraColumn == null)
+         {
+            this.extraColumn = new BitSet();
+         }
+         this.extraColumn.set(fieldId);
+      }
+      else if (columnCount != 1)
+      {
+         // can't handle fields that are represented on more than 2 columns
+         return false;
+      }
       
       if (fields == null)
       {
          this.fields = new BitSet();
+         this.columnOffsets = new int[getPropertyMeta().length];
       }
-      
+
+      // record the column offset of this field before accounting for its own columns; since fields
+      // are appended in ascending order, this yields the same cumulative offset hydrateAt needs,
+      // but precomputed (O(1) lookup at hydration time)
+      this.columnOffsets[fieldId] = this.fieldColumns;
+      this.fieldColumns += columnCount;
+
       fields.set(fieldId);
       this.lastFieldSet = fieldId;
-      this.count += getPropertyMeta()[fieldId].columnCount;
+      this.count += columnCount;
       return true;
    }
 
@@ -353,6 +392,8 @@
          // this is incomplete, so set the record offset to -1
          hydrateExtents(session, r, -1);
       }
+
+      r.clearHydrator();
       
       return rsOffset;
    }
@@ -368,4 +409,59 @@
    {
       return hasExtents ? null : fields;
    }
+
+   /**
+    * Hydrate only one property in the base-record. This is largely used by the lazy hydrator,
+    * allowing it to hydrate only the property that is accessed now. The other hydration method 
+    * ({@link #hydrate}) eagerly hydrates all properties, is a slow process and may do useless
+    * hydrations on properties that are not accessed.
+    * 
+    * @param   resultSet
+    *          The ResultSet from which the hydration should be done
+    * @param   rsOffset
+    *          The result set offset from which the reading should be done. This is relevant as
+    *          the result set may contain a joined row, but we are interested only in a specific
+    *          record that may start at a specific offset.
+    * @param   r
+    *          The record that should be hydrated. 
+    * @param   propOffset
+    *          The property that should be hydrated. The caller is responsible in assuring that
+    *          this property wasn't hydrated before.
+    * 
+    * @return  {@code true} if this worked due to the fact that it was a scalar value. {@code false}
+    *          in case the property was a non-expanded 
+    *          
+    * @throws  PersistenceException
+    *          This is thrown when reading the scalar property fails.
+    */
+   @Override
+   public boolean hydrateAt(ResultSet resultSet, int rsOffset, BaseRecord r, int propOffset)
+   throws PersistenceException
+   {
+      PropertyMeta pm = getPropertyMeta()[propOffset];
+      if (pm.getExtent() == 0 || pm.dmoProperty.expanded)
+      {
+         if (!fields.get(propOffset))
+         {
+            throw new PersistenceException("Can't hydrate property that was not selected into the result-set.");
+         }
+         int pos = columnOffsets[propOffset];
+         r.readProperty(propOffset, resultSet, rsOffset + pos, false);
+         return true;
+      }
+
+      return false;
+   }
+   
+   /**
+   * Retrieve the fields that were loaded from the database.
+   *
+   * @return A BitSet object, where true means that the field associated with that position in the {@code
+   *         data} array has been loaded from database, and false - otherwise.
+   */
+   @Override
+   public BitSet getLoadedFields()
+   {
+      return fields == null ? null : (BitSet) fields.clone();
+   }
 }

=== modified file 'src/com/goldencode/p2j/persist/orm/OrmUtils.java'
--- old/src/com/goldencode/p2j/persist/orm/OrmUtils.java	2024-11-28 03:26:23 +0000
+++ new/src/com/goldencode/p2j/persist/orm/OrmUtils.java	2026-06-10 06:43:53 +0000
@@ -132,7 +132,7 @@
             
             for (int k = uIndex.nextSetBit(0); k >= 0; k = uIndex.nextSetBit(k + 1))
             {
-               Object datum = buffer.getCurrentRecord().data[k];
+               Object datum = buffer.getCurrentRecord().getDatum(k);
                if (props[k].getType() == character.class)
                {
                   datum = TextOps.trim((String) datum, " ", false, true);

=== modified file 'src/com/goldencode/p2j/persist/orm/Persister.java'
--- old/src/com/goldencode/p2j/persist/orm/Persister.java	2025-05-20 11:19:03 +0000
+++ new/src/com/goldencode/p2j/persist/orm/Persister.java	2026-06-10 06:43:53 +0000
@@ -814,11 +814,11 @@
                         typeHandler = TypeManager.getTypeHandler(Integer.class);
                         break;
                      default:
-                        if (fieldId < 0 || fieldId > dmo.data.length)
+                        if (fieldId < 0 || fieldId > dmo.getData().length)
                         {
                            LOG.severe("Something went awfully wrong, array bounds exception");
                         }
-                        par = dmo.data[fieldId];
+                        par = dmo.getDatum(fieldId);
                         typeHandler = properties[fieldId].getDataHandler();
                   }
                   
@@ -930,10 +930,10 @@
          int i = 0;
          int count = 0;
          Long id = dmo.primaryKey();
-         Object[] data = dmo.data;
+         Object[] data = dmo.getData();
          boolean onlyExtentProps = (props.length == 0) || 
                                    (props[0].getExtent() > 0 && !props[0].dmoProperty.expanded);
-         
+
          if (onlyExtentProps)
          {
             // if the table has only extent fields or no fields at all, then insert the 'empty record' into

=== modified file 'src/com/goldencode/p2j/persist/orm/Query.java'
--- old/src/com/goldencode/p2j/persist/orm/Query.java	2025-07-05 21:30:12 +0000
+++ new/src/com/goldencode/p2j/persist/orm/Query.java	2026-06-10 08:53:24 +0000
@@ -108,7 +108,8 @@
 import com.goldencode.p2j.persist.meta.*;
 
 /**
- * 
+ * Representation of a query, expressed in FQL, which can generate and execute dialect-specific SQL in the
+ * current database session.
  */
 public class Query
 {
@@ -167,12 +168,13 @@
    /**
     * The expected structure of a query, as generated by the {@link FqlToSqlConverter}. The content
     * is not very explicit for the moment, only the DMO and the number of fields. If more info
-    * is needed ity can be added in subsequent developonet rounds.
+    * is needed it can be added in subsequent development rounds.
     */
    private ArrayList<RowStructure> rowStructure = null;
    
    /**
     * Creates a new {@code Query} object.
+    * 
     * @param   cache 
     *          cache query lambda
     * @param   fql
@@ -415,7 +417,17 @@
          
          QUERY_PARSE.timer(op);
       }
-      
+
+      // Attach the runtime field-usage profile for this call-site on every execution (the sqlQuery is
+      // cached and reused across calls, so this cannot be done only on first build). The profile is
+      // resolved per database by the final FQL; it is null when the feedback layer is disabled or the
+      // per-database profile cap has been reached, in which case hydration stays eager (the default).
+      // Skip it entirely when lazy hydration is disabled for the dialect: with no reachable lazy path
+      // there is nothing to learn, so probing would be pure overhead.
+      sqlQuery.withProfile(dialect.useLazyHydration()
+                           ? PersistenceFactory.getInstance(db).getOrCreateProfile(fql)
+                           : null);
+
       // [paramCount] includes optional [maxResults], [firstResult] positions
       params.ensureParameterSize(paramCount);
       int paging = paramCount;
@@ -478,10 +490,10 @@
    
    /**
     * Get the dialect of the database the session is connected to.
-    * 
+    *
     * @param   session
     *          The session to be investigated.
-    * 
+    *
     * @return  the dialect of the database the session is connected to.
     */
    static Dialect getDialect(Session session)

=== modified file 'src/com/goldencode/p2j/persist/orm/RecordMeta.java'
--- old/src/com/goldencode/p2j/persist/orm/RecordMeta.java	2025-04-07 08:20:02 +0000
+++ new/src/com/goldencode/p2j/persist/orm/RecordMeta.java	2026-06-10 06:43:53 +0000
@@ -157,7 +157,7 @@
    /** 
     * The index in {@link #props} array where each {@link #loadSql} statement has the properties 
     * (with the same extent) stored.  The value of <code>loadSqlIndexes[0]</code> is always zero, as the
-    * {@link #loadSql}'s first element is the scalar properties SELET. 
+    * {@link #loadSql}'s first element is the scalar properties SELECT. 
     */
    final int[] loadSqlIndexes;
    
@@ -1101,6 +1101,11 @@
       return temporary;
    }
    
+   public boolean isSingleResultSetLoad()
+   {
+      return loadSql.length == 1;
+   }
+   
    /**
     * Get the array of {@link PropertyMeta property metadata} objects which describe how the core
     * data of this record is organized.

=== modified file 'src/com/goldencode/p2j/persist/orm/RowStructure.java'
--- old/src/com/goldencode/p2j/persist/orm/RowStructure.java	2025-07-16 14:01:50 +0000
+++ new/src/com/goldencode/p2j/persist/orm/RowStructure.java	2026-06-10 06:43:53 +0000
@@ -10,6 +10,7 @@
 ** 003 AL2 20231123 Refactored this as an interface. Moved implementation in AbstractRowStructure.
 **     AL2 20231211 Added getDmoMeta.
 ** 004 LS  20250714 Added 'getNeededFields'.
+** 005 AL2 20231214 Added hydrateAt and hydrateExtents
 */
 
 /*
@@ -69,8 +70,7 @@
 
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.BitSet;
-import java.util.Iterator;
+import java.util.*;
 
 import com.goldencode.p2j.persist.DataModelObject;
 import com.goldencode.p2j.persist.PersistenceException;
@@ -184,6 +184,49 @@
    public int hydrate(Session session, ResultSet resultSet, int rsOffset, BaseRecord r)
    throws PersistenceException,
           SQLException;
+   
+   /**
+    * Hydrate only one property in the base-record. This is largely used by the lazy hydrator,
+    * allowing it to hydrate only the property that is accessed now. The other hydration method 
+    * ({@link #hydrate}) eagerly hydrates all properties, is a slow process and may do useless
+    * hydrations on properties that are not accessed.
+    * 
+    * @param   resultSet
+    *          The ResultSet from which the hydration should be done
+    * @param   rsOffset
+    *          The result set offset from which the reading should be done. This is relevant as
+    *          the result set may contain a joined row, but we are interested only in a specific
+    *          record that may start at a specific offset.
+    * @param   r
+    *          The record that should be hydrated. 
+    * @param   propOffset
+    *          The property that should be hydrated. The caller is responsible in assuring that
+    *          this property wasn't hydrated before.
+    * 
+    * @return  {@code true} if this worked due to the fact that it was a scalar value. {@code false}
+    *          in case the property was a non-expanded 
+    *          
+    * @throws  PersistenceException
+    *          This is thrown when reading the scalar property fails.
+    */
+   public boolean hydrateAt(ResultSet resultSet, int rsOffset, BaseRecord r, int propOffset)
+   throws PersistenceException;
+
+   /**
+    * Hydrate record's non-expanded extents.
+    * 
+    * @param   session
+    *          The session to be used to query the non-expanded extent tables.
+    * @param   r
+    *          The record to be hydrated.
+    * @param   recOffset
+    *          The offset to which the extent data should be added into the record. 
+    * 
+    * @throws  PersistenceException
+    *          thrown when hydrating extents fails (as it requires extra SQL queries to be run).
+    */
+   public void hydrateExtents(Session session, BaseRecord r, int recOffset)
+   throws PersistenceException;
 
    /** 
     * Retrieve the DMO meta used by the underlying DMO of this structure.
@@ -219,4 +262,12 @@
    {
       return true;
    }
+   
+   /**
+    * Retrieve the fields that were loaded from the database.
+    * 
+    * @return A BitSet object, where true means that the field associated with that position in the {@code
+    * data} array has been loaded from database, and false - otherwise.
+    */
+   public BitSet getLoadedFields();
 }

=== modified file 'src/com/goldencode/p2j/persist/orm/SQLQuery.java'
--- old/src/com/goldencode/p2j/persist/orm/SQLQuery.java	2025-09-10 11:27:41 +0000
+++ new/src/com/goldencode/p2j/persist/orm/SQLQuery.java	2026-06-10 09:11:13 +0000
@@ -2,7 +2,7 @@
 ** Module   : SQLQuery.java
 ** Abstract : Object which performs an SQL query and processes the results.
 **
-** Copyright (c) 2019-2025, Golden Code Development Corporation.
+** Copyright (c) 2019-2026, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
 ** 001 ECF 20191001 Created initial version with stubbed methods.
@@ -133,6 +133,7 @@
 import java.lang.reflect.Array;
 import java.sql.*;
 import java.util.*;
+import java.util.function.*;
 import java.util.concurrent.atomic.*;
 import java.util.logging.*;
 import com.goldencode.p2j.directory.*;
@@ -155,7 +156,31 @@
  * Note: internally, the object is NOT synchronized, so the usage intervals MUST NOT overlap.
  */
 public class SQLQuery
-{
+
+{   /** Instrumentation for cases where we lazily hydrate or rehydrate a records. */
+   private static final SimpleCounter LAZY_HYDRATE_RECORD = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydratorLazyRecord);
+   
+   /** Instrumentation for cases where we lazily hydrate a record (not from cache). */
+   private static final SimpleCounter NEW_LAZY_RECORD = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydratorNewRecord);
+   
+   /** Instrumentation for cases where we fully hydrate a record. */
+   private static final SimpleCounter NON_LAZY_HYDRATE_RECORD = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydratorNonLazyRecord);
+   
+   /** Instrumentation for cases where we lazily hydrate a record (not from cache). */
+   private static final SimpleCounter LAZY_UNIQUE = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydratorLazyUnique);
+   
+   /** Instrumentation for cases where we fully hydrate a record. */
+   private static final SimpleCounter NON_LAZY_UNIQUE = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydratorNonLazyUnique);
+   
+   /** Profiler done for queries: cache hits, cache misses, etc. */
+   private static final QueryProfilerJMX SIMPLE_QUERY_PROFILER =
+      QueryProfilerJMX.getInstance(FwdServerJMX.QueryProfiler.QueryProfiler);
+   
    /** Logger */
    private static final CentralLogger LOG = CentralLogger.get(SQLQuery.class);
    
@@ -180,10 +205,6 @@
       fetchSize = dir.getInt(Directory.ID_RELATIVE_SERVER, CFG_FETCH_SIZE, DEFAULT_FETCH_SIZE);
       LOG.fine("JDBC fetch size initialized to: " + fetchSize);
    }
-
-   /** Profiler done for queries: cache hits, cache misses, etc. */
-   private static final QueryProfilerJMX SIMPLE_QUERY_PROFILER =
-      QueryProfilerJMX.getInstance(FwdServerJMX.QueryProfiler.QueryProfiler);
    
    /** The SQL statement, possibly with placeholders. */
    private final String baseSql;
@@ -212,8 +233,16 @@
    /** Flag indicating that the query can raise SQLWarning. */
    private boolean canRaiseSqlWarning = false;
    
-   /** Flags that query bind parameters should be mapped to session variables */    
-   private boolean bindParam2SessionVar = false; 
+   /** Flags that query bind parameters should be mapped to session variables */
+   private boolean bindParam2SessionVar = false;
+
+   /**
+    * Runtime field-usage profile for this call-site, or {@code null} when the feedback layer is off
+    * or no profile is available. Drives the eager-vs-lazy hydration decision in {@link #uniqueResult}
+    * and (via {@link ScrollableResults}) the scroll path. Re-attached on every execution by
+    * {@code Query.createSqlQuery}, since the {@code SQLQuery} is cached and reused.
+    */
+   private FieldUsageProfile profile;
 
    /**
     * Constructs a new object.
@@ -255,7 +284,22 @@
       this.containsErrorHandling = value;
       return this;
    }
-   
+
+   /**
+    * Attach the runtime field-usage profile for this call-site. Set on each execution because the
+    * {@code SQLQuery} is cached and reused across calls.
+    *
+    * @param   profile
+    *          The profile to attach, or {@code null} if none is available.
+    *
+    * @return  {@code this}
+    */
+   public SQLQuery withProfile(FieldUsageProfile profile)
+   {
+      this.profile = profile;
+      return this;
+   }
+
    /**
     * Checks if the query can raise {@code SQLWarning}.
     * 
@@ -383,6 +427,9 @@
       try
       {
          activateErrorHandler(session);
+         // Make sure we close result-set still opened on this sql. Otherwise, the statement will be reparsed.
+         session.unownResultSet(sql);
+         
          stmt = conn.prepareStatement(sql, scrollMode, ResultSet.CONCUR_READ_ONLY);
          if (bindParam2SessionVar)
          {
@@ -405,7 +452,7 @@
          ResultSet resultSet = SQLExecutor.getInstance().execute(session.getDatabase(), sql, f, stmt);
          
          reportSQLWarnings(session);
-         return new ScrollableResults<T>(stmt, resultSet, rowStructure, session).
+         return new ScrollableResults<T>(stmt, resultSet, rowStructure, session, profile).
                     onClose(() -> {resetErrorHandler(session); return null;});
       }
       catch (SQLException exc)
@@ -451,6 +498,9 @@
       try
       {
          activateErrorHandler(session);
+         // Make sure we close result-set still opened on this sql. Otherwise, the statement will be reparsed.
+         session.unownResultSet(sql);
+         
          stmt = conn.prepareStatement(sql);
          if (bindParam2SessionVar)
          {
@@ -469,7 +519,7 @@
          QueryStatement f = PreparedStatement::executeQuery;
          ResultSet resultSet = SQLExecutor.getInstance().execute(session.getDatabase(), sql, f, stmt);
 
-         return new ScrollableResults<T>(stmt, resultSet, rowStructure, session).
+         return new ScrollableResults<T>(stmt, resultSet, rowStructure, session, profile).
                     onClose(() -> {resetErrorHandler(session); return null;});
       }
       catch (SQLException exc)
@@ -530,10 +580,30 @@
       Connection conn = session.getConnection();
       checkClosed(conn);
       
+      Dialect dialect = Query.getDialect(session);
+      // Lazy hydration only when the dialect allows it AND this call-site's profile has earned the
+      // LAZY verdict. Default (EAGER/UNKNOWN, or no profile) is eager: a cheap forward-only cursor,
+      // no LazyResultSet wrap, immediate close. This is what eliminates the +35.7% cold-pass
+      // regression for fully-consumed single-record FINDs.
+      boolean useLazy = dialect != null && dialect.useLazyHydration()
+                        && profile != null && profile.isLazy();
+
+      boolean sessionOwner = false;
+      ResultSet rs = null;
+      boolean shouldCloseSession = false;
       try
       {
          activateErrorHandler(session);
-         stmt = conn.prepareStatement(sql);
+         // Make sure we close result-set still opened on this sql. Otherwise, the statement will be
+         // reparsed. Required on BOTH paths: it closes a previously-owned lazy result-set on this sql.
+         session.evictResultSet(sql);
+
+         // lazy needs a scroll-insensitive cursor (it keeps the row live and checks uniqueness via
+         // isLast() without moving the cursor); eager uses a cheaper forward-only cursor and verifies
+         // uniqueness by attempting to advance after fully reading the single row.
+         stmt = useLazy
+              ? conn.prepareStatement(sql, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY)
+              : conn.prepareStatement(sql);
          if (bindParam2SessionVar)
          {
             setSessionVars(conn);
@@ -542,56 +612,86 @@
          {
             setParameters(stmt, false);
          }
-         
+
          if (fine)
          {
             LOG.log(Level.FINE, "FWD ORM: " + stmt);
          }
-         
+
          QueryStatement f = PreparedStatement::executeQuery;
-         ResultSet resultSet = SQLExecutor.getInstance().execute(session.getDatabase(), sql, f, stmt);
-         
+         rs = SQLExecutor.getInstance().execute(session.getDatabase(), sql, f, stmt);
+         if (useLazy)
+         {
+            rs = new LazyResultSet(stmt, rs, session);
+            sessionOwner = true;
+            LAZY_UNIQUE.update(1);
+         }
+         else
+         {
+            NON_LAZY_UNIQUE.update(1);
+         }
+
          reportSQLWarnings(session);
-         
-         if (!resultSet.next()) // same effect as first()
+
+         if (!rs.next()) // same effect as first()
          {
             return null; // no record matching predicate
          }
          T ret = null;
-         
+
          if (rowStructure == null || rowStructure.getCount() == 1)
          {
             // only the PK or other scalar value is returned by the query
-            ret = (T) resultSet.getObject(1);
+            ret = (T) rs.getObject(1);
          }
          else
          {
-            // when multiple values are returned in a row then a fully hydrated Record is expected
-            ret = (T) hydrateRecord(resultSet, rowStructure, 1, session);
-         }
-         
-         if (resultSet.next())
-         {
-            close(); // ???
-            throw new UniqueResultException("Result not unique for " + stmt);
+            // when multiple values are returned in a row then a full Record is expected
+            ret = (T) hydrateRecord(rs, rowStructure, 1, session, profile);
          }
 
-         SIMPLE_QUERY_PROFILER.updateRowsCount(resultSet, 1);
+         if (useLazy)
+         {
+            // don't call any further next as it will invalidate lazy hydration
+            if (!rs.isLast())
+            {
+               shouldCloseSession = true;
+               throw new UniqueResultException("Result not unique for " + stmt);
+            }
+         }
+         else
+         {
+            // the single row has been fully hydrated, so advancing the forward-only cursor is safe and
+            // detects a second (i.e. non-unique) row
+            if (rs.next())
+            {
+               throw new UniqueResultException("Result not unique for " + stmt);
+            }
+         }
+         SIMPLE_QUERY_PROFILER.updateRowsCount(rs, 1);
          return ret;
       }
       catch (SQLException exc)
       {
          close();
-         Dialect dialect = Query.getDialect(session);
          if (dialect != null && dialect.isUdfOriginatedError(exc))
          {
             throw new UdfException(dialect.errorEntry(exc.getMessage()), exc);
          }
          throw new PersistenceException("Error uniqueResult", exc);
       }
-      finally
+      finally 
       {
-         close();
+         if (sessionOwner && rs != null && !shouldCloseSession)
+         {
+            // don't eagerly close it now as it will instantly invalidate lazy hydration
+            // let the result-set live more being managed by the session
+            session.ownResultSet(sql, rs);
+         }
+         else
+         {
+            close();
+         }
          resetErrorHandler(session);
       }
    }
@@ -700,6 +800,9 @@
       {
          activateErrorHandler(session);
          int rowSize = rowStructures.size();
+         // Make sure we close result-set still opened on this sql. Otherwise, the statement will be reparsed.
+         session.unownResultSet(sql);
+         
          List<T> ret = new ArrayList<>();
          stmt = conn.prepareStatement(sql);
          if (bindParam2SessionVar)
@@ -768,7 +871,7 @@
             RowStructure rowStruct = rowStructures.get(0);
             while (resultSet.next())
             {
-               ret.add((T) hydrateRecord(resultSet, rowStruct, 1, session));
+               ret.add((T) hydrateRecord(resultSet, rowStruct, 1, session, profile));
             }
          }
          else
@@ -785,7 +888,7 @@
                {
                   RowStructure rowStruct = rowStructures.get(i);
                   // populate row[k]'s fields
-                  row[k++] = hydrateRecord(resultSet, rowStruct, resOffset, session);
+                  row[k++] = hydrateRecord(resultSet, rowStruct, resOffset, session, profile);
                   resOffset += rowStruct.getCount();
                }
                ret.add((T) row);
@@ -820,11 +923,12 @@
     * @throws  SQLException
     *          On error in the database layer.
     */
-   private void reportSQLWarnings(Session session) throws SQLException
+   private void reportSQLWarnings(Session session)
+   throws SQLException
    {
        if (!canRaiseSqlWarning)
        {
-         return;
+          return;
        }
        Dialect dialect = Query.getDialect(session);
        SQLWarning warning = stmt.getWarnings();
@@ -833,15 +937,14 @@
           if (dialect.isUdfOriginatedError(warning))
           {
              ErrorEntry ee = dialect.errorEntry(warning.getMessage());
-             ErrorManager.recordOrShowError(
-                      new int[] {ee.num}, 
-                      new String[] {ee.text}, 
-                      true, 
-                      ee.prefix, 
-                      false,
-                      false,
-                      false,
-                      true);
+             ErrorManager.recordOrShowError(new int[] {ee.num},
+                                            new String[] {ee.text},
+                                            true,
+                                            ee.prefix,
+                                            false,
+                                            false,
+                                            false,
+                                            true);
           }
           warning = warning.getNextWarning();
       }
@@ -865,8 +968,14 @@
     *          case when multiple objects are queried at once).
     * @param   session
     *          The {@code Session} to be used.
+    * @param   profile
+    *          The runtime field-usage profile for the call-site, or {@code null}. When non-null and
+    *          still learning, the returned record is armed for demand probing (no-op once the verdict
+    *          is locked). Arming here &mdash; the single exit of every materialization &mdash; covers
+    *          both fresh hydrations and session-cache-hit revisits.
     *
-    * @return  The re-hydrated object.
+    * @return  The re-hydrated object. Whether the record is fully or partially hydrated depends on whether
+    *          lazy hydration is allowed or a fields/except clause is active.
     *
     * @throws  PersistenceException
     *          When an issue was encountered in the process.
@@ -874,21 +983,19 @@
    static BaseRecord hydrateRecord(ResultSet resultSet,
                                    RowStructure rowStructure,
                                    int rsOffset,
-                                   Session session)
+                                   Session session,
+                                   FieldUsageProfile profile)
    throws PersistenceException
    {
-      BaseRecord res;
-      if (FwdServerJMX.JMX_DEBUG)
-      {
-         res = HYDRATE_RECORD.timerWithReturn(
-             () -> hydrateRecordImpl(resultSet, rowStructure, rsOffset, session)
-         );
-      }
-      else
-      {
-         res = hydrateRecordImpl(resultSet, rowStructure, rsOffset, session);
-      }
-      return res;
+      BaseRecord[] res = new BaseRecord[1];
+      HYDRATE_RECORD.timer(() -> res[0] = hydrateRecordImpl(resultSet, rowStructure, rsOffset, session));
+
+      if (res[0] != null && profile != null)
+      {
+         res[0].armUsageProbe(profile);
+      }
+
+      return res[0];
    }
    
    /**
@@ -945,16 +1052,16 @@
     *          The data source. Contain the result of a query with the "main" properties. The
     *          extent properties are queried and hydrated before the record object is returned.
     * @param   rowStructure
-    *          The holder for the DMO of the object to be returned and the expected number of properties of this object,
-    *          as the select query was constructed. This governs the order and the types of the fields and
-    *          the number of columns used for each one.
+    *          The holder for the DMO of the object to be returned and the expected number of properties of
+    *          this object, as the select query was constructed. This governs the order and the types of the
+    *          fields and the number of columns used for each one.
     * @param   rsOffset
     *          The offset in the {@code ResultSet}'s row where the hydration process begins (in
     *          case when multiple objects are queried at once).
     * @param   session
     *          The {@code Session} to be used.
     *
-    * @return  The re-hydrated object.
+    * @return  A partially or fully re-hydrated DMO representing the record represented by the parameters.
     *
     * @throws  PersistenceException
     *          When an issue was encountered in the process.
@@ -967,15 +1074,19 @@
    {
       Connection conn = session.getConnection();
       checkClosed(conn);
+
+      boolean lazy = resultSet instanceof LazyResultSet;
       DmoMeta dmoInfo = rowStructure.getDmoMeta();
       Class<? extends Record> recordClass = dmoInfo.getImplementationClass();
       int count = rowStructure.getCount();
       // Note: we should check whether
       //          Session.PK.equalsIgnoreCase(resultSet.getMetaData().getColumnName(rsOffset))
       //       but to avoid the possible extra load of processing the metadata we assume the
-      //       primary key RECID/ID is the FIRST COLUMN in the result set of the query 
+      //       primary key RECID/ID is the FIRST COLUMN in the result set of the query
       
-      long pk;
+      Long pk = null;
+      BaseRecord r = null;
+      BaseRecord cachedRec = null;
       try
       {
          pk = resultSet.getLong(rsOffset);
@@ -986,8 +1097,9 @@
          {
             return null;
          }
-         Record cachedRec = session.getCached(recordClass, pk);
-         if (cachedRec != null && !cachedRec.checkState(DmoState.STALE))
+         boolean newHydrator = true;
+         cachedRec = session.getCached(recordClass, pk);
+         if (cachedRec != null)
          {
             // when the record is incomplete, it must be refreshed from the database
             // but first, verify whether the required fields are already loaded to avoid unnecessary refresh
@@ -995,10 +1107,11 @@
             {
                BitSet readFields = cachedRec._getReadFields();
                BitSet neededFields = rowStructure.getNeededFields();
-
+               newHydrator = false;
                if (neededFields == null)
                {
-                  cachedRec = session.get(recordClass, pk);
+                  newHydrator = true;
+                  cachedRec.updateState(session, DmoState.INCOMPLETE, false);
                }
                else
                {
@@ -1008,36 +1121,107 @@
                   {
                      // this will update the cachedRec reference, so the returned record instance does not change,
                      // only its data is updated
-                     cachedRec = session.get(recordClass, pk, clone);
+                     newHydrator = true;
                   }
                }
             }
-            
-            if (cachedRec != null)
+            else if (rowStructure.isIncomplete() || cachedRec.areAllPropertiesLive())
             {
                SIMPLE_QUERY_PROFILER.updateCacheHits(resultSet, 1);
-               // we found an unSTALE CACHED record, do not bother reading from result set
+               
+               // we do not want to do lazy hydration if the cached record is already fully hydrated or
+               // if the rowStructure is incomplete - providing limited amount of data
                return cachedRec;
             }
-         }
-         
-         if (count == 1)
-         {
-            SIMPLE_QUERY_PROFILER.updateCacheHits(resultSet, 1);
-            cachedRec = session.get(recordClass, pk);
-            
-            return cachedRec;
-         }
-      }
-      catch (SQLException sql)
-      {
-         // just log it, but continue to let the generic hydration detect other issues
+
+            // we have a cached record which needs further hydration, either immediate or deferred/lazy
+            r = cachedRec;
+         }
+         
+         if (r == null)
+         {
+            SIMPLE_QUERY_PROFILER.updateCacheMisses(resultSet, 1);
+            
+            if (count == 1 && !lazy)
+            {
+               // special case: the result set only has the PK and we need immediate hydration
+               return session.get(recordClass, pk);
+            }
+            
+            // all other cases, we need to instantiate a new record
+            r = recordClass.getDeclaredConstructor().newInstance();
+
+            if (rowStructure.isIncomplete())
+            {
+               // this should be done before hydrating; otherwise the reading won't mark
+               // the read properties properly
+               r.markIncomplete(session, rowStructure.getLoadedFields());
+            }
+         }
+         
+         if (!newHydrator)
+         {
+            return r;
+         }
+         else if (lazy)
+         {
+            LAZY_HYDRATE_RECORD.update(1);
+            r.primaryKey(pk);
+            if (r instanceof TempRecord)
+            {
+               ((TempRecord) r)._multiplex(resultSet.getInt(rsOffset + 1));
+            }
+            
+            r.createHydrator(rowStructure, (LazyResultSet) resultSet, rsOffset);
+            
+            if (cachedRec == null)
+            {
+               NEW_LAZY_RECORD.update(1);
+               session.associate(r);
+            }
+            else
+            {
+               updateIncompleteState(r, rowStructure, session);
+            }
+            
+            return r;
+         }
+         else if (cachedRec != null)
+         {
+            // We are not in lazy mode, but in Session cache we have a DMO which was not hydrated yet,
+            // or we have an INCOMPLETE DMO that needs further hydration.
+            rowStructure.hydrate(session, resultSet, rsOffset, r);
+
+            updateIncompleteState(r, rowStructure, session);
+
+            return r;
+         }
+
+
+         NON_LAZY_HYDRATE_RECORD.update(1);
+
+         // Normal non-lazy, fresh-record path (cachedRec == null, newHydrator, count > 1): r was already
+         // constructed (and possibly marked incomplete) above. Hydrate and cache it here rather than
+         // falling through to the fallback try below, which would re-count the cache miss and reflectively
+         // re-construct the record (the double-build). The fallback remains reachable only when the PK
+         // lookup above threw an SQLException (r may still be null there).
+         rsOffset = rowStructure.hydrate(session, resultSet, rsOffset, r);
+         session.associate(r);
+
+         return r;
+      }
+      catch (ReflectiveOperationException exc)
+      {
+         throw new PersistenceException("Error creating record", exc);
+      }
+      catch (SQLException exc)
+      {
+         // just log it, but if we are not returning, continue to let the generic hydration detect other
+         // issues
          LOG.log(Level.WARNING,
-                 "Failed to locate the primary key of " + recordClass.getSimpleName() + ".",
-                 sql);
+                 "Failed to locate the primary key of " + rowStructure.getDmoClass().getSimpleName() + ".");
       }
       
-      BaseRecord r = null;
       try
       {
          SIMPLE_QUERY_PROFILER.updateCacheMisses(resultSet, 1);
@@ -1047,7 +1231,7 @@
          {
             // this should be done before hydrating; otherwise the reading won't mark
             // the read properties properly
-            r.markIncomplete(session);
+            r.markIncomplete(session, rowStructure.getLoadedFields());
          }
          
          // expanded fields take contiguous elements in current row of returned record set
@@ -1056,8 +1240,11 @@
          // cache the hydrated record
          session.associate(r);
       }
-      catch (SQLException |
-             ReflectiveOperationException e)
+      catch (ReflectiveOperationException exc)
+      {
+         throw new PersistenceException("Error creating record", exc);
+      }
+      catch (SQLException e)
       {
          // extract Interrupted Exception out of SQLException
          if (e instanceof SQLException && e.getCause() instanceof InterruptedException)
@@ -1072,7 +1259,34 @@
       
       return r;
    }
-   
+
+   /**
+    * Check the {@link BaseRecord} is it should still be marked as INCOMPLETE or not.
+    * If it still INCOMPLETE, update {@link BaseRecord#readFields} to match with the current
+    * {@link RowStructure}.
+    *
+    * @param   r
+    *          Record to check
+    * @param   rowStructure
+    *          Used if is INCOMPLETE and to get the loaded fields.
+    * @param   session
+    *          Current Session
+    */
+   private static void updateIncompleteState(BaseRecord r, RowStructure rowStructure, Session session)
+   {
+      if (r.checkState(DmoState.INCOMPLETE))
+      {
+         if (!rowStructure.isIncomplete())
+         {
+            r.updateState(session, DmoState.INCOMPLETE, false);
+         }
+         else if (r._getReadFields() != null)
+         {
+            r._getReadFields().or(rowStructure.getLoadedFields());
+         }
+      }
+   }
+
    /**
     * Closes this object by releasing all acquired resources.
     */
@@ -1155,7 +1369,7 @@
          setParameter(stmt, pos, parameters[pIndex], forUpdate);
       }
    }
-
+   
    /**
     * Copy the set of parameters temporarily stored in this object to the session variables.
     *
@@ -1180,7 +1394,7 @@
          setSessionVar(conn, pos, parameters[pIndex]);
       }
    }
-
+   
    /**
     * Activate error handler at the database side.
     * @param  session 
@@ -1377,7 +1591,21 @@
       pstmt.executeUpdate();
    }
    
-
+   /**
+    * Logs a message with {@code Level.FINE} tracing level to the logger associated with this
+    * class.
+    * 
+    * @param   msg
+    *          The message to be printed to log.
+    */
+   private void log(Supplier<String> msg)
+   {
+      if (LOG.isLoggable(Level.FINE))
+      {
+         LOG.log(Level.FINE, msg.get());
+      }
+   }
+   
    /**
     * Test whether the connection is alive (not closed). If the connection is usable the method
     * returns without any side-effects. If the session is not usable, {@code PersistenceException}

=== modified file 'src/com/goldencode/p2j/persist/orm/ScrollableResults.java'
--- old/src/com/goldencode/p2j/persist/orm/ScrollableResults.java	2025-08-01 16:24:09 +0000
+++ new/src/com/goldencode/p2j/persist/orm/ScrollableResults.java	2026-06-10 08:21:46 +0000
@@ -79,6 +79,7 @@
 
 package com.goldencode.p2j.persist.orm;
 
+import com.goldencode.p2j.jmx.*;
 import com.goldencode.p2j.persist.*;
 import com.goldencode.p2j.util.logging.*;
 
@@ -104,6 +105,14 @@
  */
 public class ScrollableResults<T>
 {
+   /** Instrumentation for {@link #LazyResultSet}. */
+   private static final SimpleCounter LAZY_SCROLLABLE = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydratorLazyScrollable);
+
+   /** Instrumentation for {@link #LazyResultSet}. */
+   private static final SimpleCounter NON_LAZY_SCROLLABLE = 
+            SimpleCounter.getInstance(FwdServerJMX.Counter.HydratorNonLazyScrollable);
+   
    /** Logger */
    private static final CentralLogger LOG = CentralLogger.get(ScrollableResults.class);
    
@@ -118,7 +127,10 @@
    
    /** The {@code Session} to be used.*/
    private final Session session;
-   
+
+   /** Runtime field-usage profile for this call-site, or {@code null} when none is available. */
+   private final FieldUsageProfile profile;
+
    /** On close hook*/
    private SandboxRun<Void> onClose = () -> null;
    
@@ -136,14 +148,29 @@
     *          The expected structure of a query, as generated by the {@link FqlToSqlConverter}.
     * @param   session
     *          The {@code Session} to be used.
+    * @param   profile
+    *          The runtime field-usage profile for the call-site, or {@code null} if none is
+    *          available. Lazy hydration is used only when the dialect allows it AND this profile has
+    *          earned the LAZY verdict; otherwise the rows hydrate eagerly (the default).
     */
-   public ScrollableResults(Statement stmt, ResultSet rs, ArrayList<RowStructure> rowStructure, Session session)
+   public ScrollableResults(Statement stmt, ResultSet rs, ArrayList<RowStructure> rowStructure,
+                            Session session, FieldUsageProfile profile)
    {
       this.stmt = stmt;
-      this.rs = rs;
+      if (Query.getDialect(session).useLazyHydration() && profile != null && profile.isLazy())
+      {
+         LAZY_SCROLLABLE.update(1);
+         this.rs = new LazyResultSet(stmt, rs, session);
+      }
+      else
+      {
+         NON_LAZY_SCROLLABLE.update(1);
+         this.rs = rs;
+      }
       this.rowStructure = rowStructure;
       this.session = session;
-      
+      this.profile = profile;
+
       /*
       //TODO: check where and whether to put this:
       ScrollableResults.execute(() -> {
@@ -367,14 +394,15 @@
             {
                if (rowStruct.getDmoClass().equals(dmoInfo.iface))
                {
-                  BaseRecord rec = SQLQuery.hydrateRecord(rs, rowStruct, rsOffset, session);
+                  BaseRecord rec = SQLQuery.hydrateRecord(rs, rowStruct, rsOffset, session, profile);
                   Object[] filteredRec = new Object[cols.length];
+                  int lng = rec.getLength();
                   for (int k = 0; k < cols.length; k++)
                   {
                      Integer index = colsSelection.get(cols[k]);
-                     if (index > 0 && index <= rec.data.length)
+                     if (index > 0 && index <= lng)
                      {
-                        filteredRec[k] = rec.data[index - 1];
+                        filteredRec[k] = rec.getDatum(index - 1);
                      }
                      else
                      {
@@ -477,7 +505,8 @@
    public void close()
    throws PersistenceException
    {
-      execute(() -> { stmt.close(); onClose.run(); return null; }, null);
+      // explicitly close the result-set. If is is lazy result-set, it should invalidate its hydrators.
+      execute(() -> { rs.close(); stmt.close(); onClose.run(); return null; }, null);
    }
    
    /**
@@ -518,7 +547,7 @@
       int rowSize = (rowStructure == null) ? 0 : rowStructure.size();
       int columnCount = rs.getMetaData().getColumnCount();
       Object[] ret;
-
+      
       if (rowSize == 0 || rowSize == columnCount)
       {
          // if the [columnCount] matches the [rowSize] then we have only one column for each DMO to be
@@ -541,11 +570,11 @@
          for (int i = 0; i < rowStructure.size(); i++)
          {
             RowStructure rowStruct = rowStructure.get(i);
-
+            
             // if the current row contains only the recId we do not need to hydrate the record
             boolean needOnlyPK = !forceOnlyId && rowStruct instanceof AdaptiveRowStructure
-                    && ((AdaptiveRowStructure) (rowStruct)).hasOnlyPK();
-
+               && ((AdaptiveRowStructure) (rowStruct)).hasOnlyPK();
+            
             int expColumnCount = rowStruct.getCount();
             try
             {
@@ -557,15 +586,15 @@
                else
                {
                   ret[recCnt] =
-                     SQLQuery.hydrateRecord(rs, rowStruct, rsOffset, session);
+                     SQLQuery.hydrateRecord(rs, rowStruct, rsOffset, session, profile);
                }
-
+               
             }
             catch (PersistenceException e)
             {
                throw new SQLException(e);
             }
-
+            
             // position on next record
             rsOffset += expColumnCount;
             ++recCnt;
@@ -573,7 +602,6 @@
       }
       return ret;
    }
-
    /**
     * Internal worker method. Execute a piece of code that potentially throws a
     * {@code SQLException} and returns the result. If the exception is throws, a default value is

=== modified file 'src/com/goldencode/p2j/persist/orm/Session.java'
--- old/src/com/goldencode/p2j/persist/orm/Session.java	2026-04-22 07:07:12 +0000
+++ new/src/com/goldencode/p2j/persist/orm/Session.java	2026-06-10 08:10:18 +0000
@@ -138,11 +138,14 @@
 
 package com.goldencode.p2j.persist.orm;
 
+import java.lang.ref.WeakReference;
 import java.lang.reflect.*;
 import java.sql.*;
 import java.util.*;
 import java.util.function.*;
 import java.util.logging.*;
+import java.util.logging.Level;
+
 import javax.sql.DataSource;
 import com.goldencode.asm.*;
 import com.goldencode.cache.*;
@@ -169,6 +172,9 @@
            CacheExpiryListener<RecordIdentifier<String>, BaseRecord>,
            AutoCloseable
 {
+   /** The size of the number of result-sets to be owned by this session. */
+   private static final int RESULT_SET_CACHE_SIZE = 64; 
+   
    /**
     * The default name of the primary key. The value is the same as the default value of
     * {@code com.goldencode.p2j.schema.SchemaConfig#primaryKeyName}, used at conversion and
@@ -201,6 +207,13 @@
     */
    public static String PK = DEFAULT_PK;
    
+   /** 
+    * Some result-sets that are managed by the session. These result-sets are scoped 
+    * to the lifecycle of the session, because their generating queries were not able to properly
+    * determine the timing of their closing. 
+    */
+   private Cache<String, ResultSet> resultSetCache;
+   
    /** Cache of database records associated with this session */
    private ExpiryCache<RecordIdentifier<String>, BaseRecord> cache;
    
@@ -349,6 +362,7 @@
       this.cache = useCache ? createCache() : null;
       this.cacheExtension = null;
       this.versioning = versioning;
+      this.resultSetCache = createResultSetCache();
       this.tenantName = tenantName;
       
       DatabaseConfig config = cfg;
@@ -373,6 +387,7 @@
       {
          this.hook.onSessionClaim(this.conn);
       }
+      this.resultSetCache = createResultSetCache();
    }
    
    /**
@@ -529,6 +544,16 @@
             "Error accessing DMO implementation class metadata for " + dmoClass.getName());
       }
    }
+
+   /**
+    * Retrieve the loader as a weak reference to avoid leaking.
+    * 
+    * @return   Retrieve the loader used by this session.
+    */
+   public WeakReference<Loader> getLoader()
+   {
+      return new WeakReference<>(loader);
+   }
    
    /**
     * Get the cached DMO of the given type with the given primary key.
@@ -823,18 +848,19 @@
             for (int p = dirtyProps.nextSetBit(0); p >= 0; p = dirtyProps.nextSetBit(p + 1))
             {
                // direct access
-               loaded.data[p] = dmo.data[p];
+               loaded.setDatum(p, dmo.getDatum(p));
             }
 
             if (partialFields == null)
             {
                // copy the full record (preserving the dirty properties) to the cached record, to preserve the
                // instance
-               System.arraycopy(loaded.data, 0, dmo.data, 0, dmo.data.length);
+               dmo.copyArray(loaded.getData());
 
                // set the incomplete record as complete
                dmo.updateState(this, DmoState.INCOMPLETE, false);
                dmo.readFields = null;
+               dmo.setAllLiveProps();
             }
             else
             {
@@ -844,7 +870,7 @@
                // only overwrite the new fields that were set
                for (int p = partialFields.nextSetBit(0); p >= 0; p = partialFields.nextSetBit(p + 1))
                {
-                  dmo.data[p] = loaded.data[p];
+                  dmo.setDatum(p, loaded.getDatum(p));
                   dmo.readFields.set(p);
 
                   boolean extent = props[p].dmoProperty.expanded;
@@ -853,17 +879,18 @@
                      int size = props[p].dmoProperty.extent;
                      for (int offset = p + 1; offset < p + size; offset++)
                      {
-                        dmo.data[offset] = loaded.data[offset];
+                        dmo.setDatum(offset, loaded.getDatum(offset));
                         dmo.readFields.set(offset);
                      }
                   }
                }
                // we have progressively loaded all fields and the DMO is no longer incomplete
-               if (dmo.readFields.cardinality() == dmo.data.length)
+               if (dmo.readFields.cardinality() == dmo.getData().length)
                {
                   // set the incomplete record as complete
                   dmo.updateState(this, DmoState.INCOMPLETE, false);
                   dmo.readFields = null;
+                  dmo.setAllLiveProps();
                }
             }
          }
@@ -1408,7 +1435,7 @@
          }
       }
       
-      System.arraycopy(dmo.data, 0, persisted.data, 0, dmo.data.length);
+      persisted.copyArray(dmo.getData());
       
       return persisted;
    }
@@ -1845,6 +1872,8 @@
       deregisterVersioned();
       
       cache = createCache();
+      clearResultSetCache();
+      resultSetCache = createResultSetCache();
       cacheExtension = null;
       
       // TODO: cancel all pending saves, updates and deletions
@@ -1931,6 +1960,7 @@
       try
       {
          deregisterVersioned();
+         clearResultSetCache();
          
          if (!conn.isClosed())
          {
@@ -1954,6 +1984,7 @@
          conn = null;
          cache = null;
          cacheExtension = null;
+         resultSetCache = null;
       }
    }
    
@@ -2389,4 +2420,138 @@
       return copy.equals(subset);
    }
 
+   /**
+    * Mark the session as the owner o the provided result-set. In other words, the session will be
+    * responsible for the destroying of the result-set when suitable. To avoid large memory consumption,
+    * the session will limit the number of owner result-sets. 
+    * 
+    * @param   rs
+    *          The result-set that should be managed by the session.
+    */
+   public void ownResultSet(String sql, ResultSet rs)
+   {
+      if (resultSetCache != null)
+      {
+         resultSetCache.put(sql, rs);
+         return;
+      }
+      
+      try
+      {
+         Statement stmt = rs.getStatement();
+         
+         rs.close();
+         
+         if (stmt != null)
+         {
+            stmt.close();
+         }
+      }
+      catch (SQLException e)
+      {
+         log.log(Level.SEVERE, "Failed closing the result-set and statement owned by the session: " + sql);
+      }      
+   }
+   
+   /**
+    * Evict the result-set from the session ownership. This will release the result-set, so it
+    * needs to be closed. Mind closing the statement at the same time.
+    * 
+    * @param   sql
+    *          The sql key identifying the result-set to be released.
+    */
+   public void unownResultSet(String sql)
+   {
+      if (resultSetCache == null)
+      {
+         return;
+      }
+
+      unownResultSet(sql, resultSetCache.get(sql));
+   }
+
+   /**
+    * Evict the given result-set from the session ownership, closing it and its statement. Unlike
+    * {@link #unownResultSet(String)}, this does not re-fetch the value from the cache, so it is safe
+    * to call from the cache-expiry listener: the {@code ExpiryCache} removes the entry <i>before</i>
+    * firing listeners, so a re-fetch there would return {@code null} and leak the open cursor and
+    * statement.
+    *
+    * @param   sql
+    *          The sql key identifying the result-set, used only for logging.
+    * @param   old
+    *          The result-set to release. May be {@code null}, in which case nothing is done.
+    */
+   public void unownResultSet(String sql, ResultSet old)
+   {
+      if (old != null)
+      {
+         try
+         {
+            Statement stmt = old.getStatement();
+
+            old.close();
+
+            if (stmt != null)
+            {
+               stmt.close();
+            }
+         }
+         catch (SQLException e)
+         {
+            if (log.isLoggable(Level.SEVERE))
+            {
+               log.log(Level.SEVERE,
+                       "Failed closing the result-set and statement owned by the session: " + sql);
+            }
+         }
+      }
+   }
+   
+   public void evictResultSet(String sql)
+   {
+      if (resultSetCache == null)
+      {
+         return;
+      }
+      
+      unownResultSet(sql);
+      resultSetCache.remove(sql);
+   }
+   
+   /**
+    * Clear the result-set cache by un-owning all result-sets. This means only closing the stored result-sets
+    * and will not actually evict all its records. Make sure the caller is resetting the cache.
+    */
+   private void clearResultSetCache()
+   {
+      if (resultSetCache != null)
+      {
+         resultSetCache.entries().forEach((entry) ->
+         {
+            unownResultSet(entry.getKey(), entry.getValue());
+         });
+      }
+   }
+   
+   /**
+    * This generates a new cache for storing result-sets owned by the session. This will also attempt to attach
+    * a listener that un-owns their result-sets when evicted.
+    * 
+    * @return   A new cache suitable for storing result-sets owned by the session.
+    */
+   private Cache<String, ResultSet> createResultSetCache()
+   {
+      ExpiryCache<String, ResultSet> rsCache = new LRUCache<>(RESULT_SET_CACHE_SIZE);
+      rsCache.addCacheExpiryListener((event) ->
+      {
+         // close the value carried in the event: the entry has already been removed from the cache
+         // before this listener fires, so re-fetching by key would return null and leak the cursor
+         for (Map.Entry<String, ResultSet> entry : event.getExpiredEntries().entrySet())
+         {
+            unownResultSet(entry.getKey(), entry.getValue());
+         }
+      });
+      return rsCache;
+   }
 }

=== modified file 'src/com/goldencode/p2j/persist/orm/SessionFactory.java'
--- old/src/com/goldencode/p2j/persist/orm/SessionFactory.java	2025-04-17 12:49:52 +0000
+++ new/src/com/goldencode/p2j/persist/orm/SessionFactory.java	2026-06-10 06:43:53 +0000
@@ -26,6 +26,7 @@
 **                  based on [sharedDb] parameter.
 ** 005 OM  20250211 New parameter for TenantManager.getTenant().
 ** 006 AP  20250417 Call the hook when a Session is reclaimed.
+** 007 ECF 20231002 Lazy hydration prototype.
 */
 
 /*
@@ -85,10 +86,10 @@
 
 import java.util.*;
 import java.util.concurrent.ConcurrentLinkedQueue;
-
 import com.goldencode.p2j.directory.DirectoryService;
 import com.goldencode.p2j.persist.*;
 import com.goldencode.p2j.persist.orm.TenantManager.Tenant;
+import com.goldencode.p2j.persist.dialect.*;
 import com.goldencode.p2j.util.Utils;
 import com.goldencode.p2j.util.logging.CentralLogger;
 

=== modified file 'src/com/goldencode/p2j/persist/orm/UniqueTracker.java'
--- old/src/com/goldencode/p2j/persist/orm/UniqueTracker.java	2025-06-25 11:07:55 +0000
+++ new/src/com/goldencode/p2j/persist/orm/UniqueTracker.java	2026-06-10 06:43:53 +0000
@@ -1101,7 +1101,7 @@
        */
       Key createKey(BaseRecord dmo)
       {
-         return new Key(this, dmo.data);
+         return new Key(this, dmo.getData());
       }
       
       /**

=== modified file 'src/com/goldencode/p2j/persist/orm/UnorderedRowStructure.java'
--- old/src/com/goldencode/p2j/persist/orm/UnorderedRowStructure.java	2025-08-14 09:57:08 +0000
+++ new/src/com/goldencode/p2j/persist/orm/UnorderedRowStructure.java	2026-06-10 08:09:38 +0000
@@ -10,6 +10,7 @@
 ** 002 SP  20250319 Added support for hydrating expanded extent fields.
 ** 003 LS  20250714 Added 'getNeededFields' implementation.
 ** 004 AS  20250814 Return null if hasExtents in getNeededFields.
+** 005 AL2 20231214 Moved from List to Map representation to allow faster hydrateAt.
 */
 
 /*
@@ -87,7 +88,7 @@
     * The fields that should be hydrated. This stores only their ids in the data array.
     * For recid and multiplex, we use special markers.
     */
-   public final List<Integer> fields;
+   public final Map<Integer, Integer> fields;
    
    /** A cached value of the property meta array that can be found in the record meta */
    private final PropertyMeta[] pm;
@@ -104,7 +105,7 @@
    public UnorderedRowStructure(DmoMeta meta)
    {
       super(meta);
-      this.fields = new ArrayList<>();
+      this.fields = new LinkedHashMap<>();
       this.pm = dmoMeta.recordMeta.getPropertyMeta(false);
       this.count = 0;
    }
@@ -121,7 +122,7 @@
    @Override
    public boolean addRecid(Property recid)
    {
-      fields.add(ReservedProperty.ID_PRIMARY_KEY);
+      fields.put(ReservedProperty.ID_PRIMARY_KEY, count);
       count++;
       return true;
    }
@@ -138,7 +139,7 @@
    @Override
    public boolean addMultiplex(Property multiplex)
    {
-      fields.add(ReservedProperty.ID_MULTIPLEX);
+      fields.put(ReservedProperty.ID_MULTIPLEX, count);
       count++;
       return true;
    }
@@ -163,7 +164,7 @@
       }
       
       int idx = dmoMeta.recordMeta.getIndexOfProperty(prop.name);
-      fields.add(idx);
+      fields.put(idx, count);
       count += pm[idx].columnCount;
       return true;
    }
@@ -188,27 +189,36 @@
    @Override
    public Iterator<Property> getProps()
    {
-      return new Iterator<Property>() 
+      // iterate the field ids in append order (LinkedHashMap preserves insertion order), mirroring
+      // the order hydrate() uses; the map is keyed by property id, not by position, so the reserved
+      // (negative) ids must be resolved to their reserved Property rather than indexed into pm[]
+      return new Iterator<Property>()
       {
-         private int idx = 0;
+         private final Iterator<Integer> ids = fields.keySet().iterator();
 
          @Override
          public boolean hasNext()
          {
-            return idx < fields.size();
+            return ids.hasNext();
          }
 
          @Override
          public Property next()
          {
-            if (!hasNext())
-            {
-               throw new NoSuchElementException();
-            }
-            
-            return pm[fields.get(idx++)].dmoProperty;
+            int id = ids.next();
+
+            if (id == ReservedProperty.ID_PRIMARY_KEY)
+            {
+               return ReservedProperty.ID;
+            }
+            if (id == ReservedProperty.ID_MULTIPLEX)
+            {
+               return ReservedProperty._MULTIPLEX;
+            }
+
+            return pm[id].dmoProperty;
          }
-         
+
       };
    }
 
@@ -246,9 +256,9 @@
       int fieldCnt = fields.size();
       int lastId = 0;
       int recOffset = 0;
-      for (int i = 0; i < fieldCnt; i++)
+      for (Map.Entry<Integer, Integer> field : fields.entrySet())
       {
-         int idx = fields.get(i);
+         int idx = field.getKey();
          if (lastId != idx)
          {
             recOffset = 0;
@@ -277,9 +287,55 @@
       {
          hydrateExtents(session, r, -1);
       }
+
+      r.clearHydrator();
       
       return rsOffset;
    }
+   
+   /**
+    * Hydrate only one property in the base-record. This is largely used by the lazy hydrator,
+    * allowing it to hydrate only the property that is accessed now. The other hydration method 
+    * ({@link #hydrate}) eagerly hydrates all properties, is a slow process and may do useless
+    * hydrations on properties that are not accessed.
+    *
+    * @param   resultSet
+    *          The ResultSet from which the hydration should be done
+    * @param   rsOffset
+    *          The result set offset from which the reading should be done. This is relevant as
+    *          the result set may contain a joined row, but we are interested only in a specific
+    *          record that may start at a specific offset.
+    * @param   r
+    *          The record that should be hydrated. 
+    * @param   propOffset
+    *          The property that should be hydrated. The caller is responsible in assuring that
+    *          this property wasn't hydrated before.
+    *
+    * @return  {@code true} if this worked due to the fact that it was a scalar value. {@code false}
+    *          in case the property was a non-expanded 
+    *
+    * @throws  PersistenceException
+    *          This is thrown when reading the scalar property fails.
+    */
+   @Override
+   public boolean hydrateAt(ResultSet resultSet, int rsOffset, BaseRecord r, int propOffset)
+   throws PersistenceException
+   {
+      PropertyMeta pm = getPropertyMeta()[propOffset];
+      if (pm.getExtent() == 0 || pm.dmoProperty.expanded)
+      {
+         Integer pos;
+         if ((pos = fields.get(propOffset)) == null)
+         {
+            throw new PersistenceException("Can't hydrate property that was not selected into the result-set.");
+         }
+         boolean minus = fields.get(ReservedProperty.ID_PRIMARY_KEY) < pos;
+         r.readProperty(propOffset, resultSet, rsOffset + pos + (minus ? -1 : 0), false);
+         return true;
+      }
+      
+      return false;
+   }
 
    /**
     * Returns a BitSet representing the fields required by this unordered row structure.
@@ -297,7 +353,7 @@
       }
 
       BitSet bitSet = new BitSet();
-      for (Integer id : fields)
+      for (Integer id : fields.keySet())
       {
          if (id != null && id != ReservedProperty.ID_PRIMARY_KEY && id != ReservedProperty.ID_MULTIPLEX)
          {
@@ -306,4 +362,24 @@
       }
       return bitSet;
    }
+   
+   /**
+    * Retrieve the fields that were loaded from the database.
+    *
+    * @return A BitSet object, where true means that the field associated with that position in the {@code
+    * data} array has been loaded from database, and false - otherwise.
+    */
+   @Override
+   public BitSet getLoadedFields()
+   {
+      BitSet loadedFields = new BitSet(pm.length);
+      for (Integer index : fields.keySet())
+      {
+         if (index != ReservedProperty.ID_PRIMARY_KEY && index != ReservedProperty.ID_MULTIPLEX)
+         {
+            loadedFields.set(index);
+         }
+      }
+      return loadedFields;
+   }
 }

=== modified file 'src/com/goldencode/p2j/persist/orm/Validation.java'
--- old/src/com/goldencode/p2j/persist/orm/Validation.java	2025-07-29 12:32:17 +0000
+++ new/src/com/goldencode/p2j/persist/orm/Validation.java	2026-06-10 06:43:53 +0000
@@ -278,7 +278,7 @@
       
       for (int i = uIndex.nextSetBit(0); i >= 0; i = uIndex.nextSetBit(i + 1))
       {
-         Object datum = dmo.data[i];
+         Object datum = dmo.getDatum(i);
          if (props[i].getType() == recid.class)
          {
             // RECID values are not displayed here
@@ -828,7 +828,7 @@
       {
          // compute the size for an index property 
          PropertyMeta crtProp = properties[i];
-         int size = crtProp.getDataHandler().getFieldSizeInIndex(dmo.data[i]);
+         int size = crtProp.getDataHandler().getFieldSizeInIndex(dmo.getDatum(i));
          
          // search the unique indexes where it is used and add the size there. If an index is
          // breaking one of the constraints, stop with error and skip additional computations 
@@ -837,7 +837,7 @@
             if (uIndexes[uComp].get(i))
             {
                uiSizes[uComp] += size;
-               duiSizes[uComp] += dialect.computeIndexKeySize(crtProp.getType(), dmo.data[i]);
+               duiSizes[uComp] += dialect.computeIndexKeySize(crtProp.getType(), dmo.getDatum(i));
                checkIndexSize(uiSizes[uComp], duiSizes[uComp], dialectMaxIndexSize, uComp, true);
             }
          }
@@ -848,7 +848,7 @@
             if (nIndexes[nComp].get(i))
             {
                niSizes[nComp] += size;
-               dniSizes[nComp] += dialect.computeIndexKeySize(crtProp.getType(), dmo.data[i]);
+               dniSizes[nComp] += dialect.computeIndexKeySize(crtProp.getType(), dmo.getDatum(i));
                checkIndexSize(niSizes[nComp], dniSizes[nComp], dialectMaxIndexSize, nComp, false);
             }
          }
@@ -933,7 +933,7 @@
       BitSet nonNullable = dmo._recordMeta().nonNullableProps;
       for (int i = offsets.nextSetBit(0); i != -1; i = offsets.nextSetBit(i + 1))
       {
-         if (nonNullable.get(i) && dmo.data[i] == null)
+         if (dmo._recordMeta().nonNullableProps.get(i) && dmo.getDatum(i) == null)
          {
             failNotNull(i);  // err: 110
          }
@@ -1295,7 +1295,7 @@
       
       for (int i = uIndex.nextSetBit(0); i >= 0; i = uIndex.nextSetBit(i + 1))
       {
-         Object datum = dmo.data[i];
+         Object datum = dmo.getDatum(i);
          if (datum != null)
          {
             if (props[i].getType() == character.class)
@@ -1393,8 +1393,8 @@
             // the violation is cleared; note that the changed record may still fail its own validation later
             for (int i = index.nextSetBit(0); i >= 0; i = index.nextSetBit(i + 1))
             {
-               Object uniqueDatum = dmo.data[i];
-               Object cachedDatum = cached.data[i];
+               Object uniqueDatum = dmo.getDatum(i);
+               Object cachedDatum = cached.getDatum(i);
                if (!Objects.equals(uniqueDatum, cachedDatum))
                {
                   return false;

=== modified file 'src/com/goldencode/p2j/persist/orm/types/DataHandler.java'
--- old/src/com/goldencode/p2j/persist/orm/types/DataHandler.java	2024-04-17 10:14:07 +0000
+++ new/src/com/goldencode/p2j/persist/orm/types/DataHandler.java	2026-06-10 06:43:53 +0000
@@ -131,6 +131,9 @@
     * Sets a value of a property in a {@code Record}. The specified value is converted from
     * {@code BaseDataType} FWD standard to plain Java, as the {@code data} array expects before
     * assigning the value to right offset.
+    * NOTE: this method DOESN'T affect the {@link com.goldencode.p2j.persist.Record#liveProps}, so it is the 
+    * programmer's responsibility to change the value for the {@code offset} if in need.
+    * 
     *
     * @param   data
     *          The {@code data} array of the record to be altered.

