=== modified file 'build.gradle'
--- old/build.gradle	2023-01-28 04:45:51 +0000
+++ new/build.gradle	2023-02-09 11:42:31 +0000
@@ -237,6 +237,10 @@
 repositories {
     // https://repo1.maven.org/maven2/
     mavenCentral()
+
+    flatDir {
+       dirs("custom")
+    }
     
     maven { 
        name "maven2"
@@ -441,7 +445,8 @@
     fwdCCS group: 'org.apache.commons', name: 'commons-collections4', version: '4.4'
     fwdConvertServer group: 'org.jboss.logging', name: 'jboss-logging', version: '3.1.0.GA'
     fwdConvertServer group: 'antlr', name: 'antlr', version: '2.7.7'
-    fwdConvertServer group: 'com.goldencode', name: 'fwd-h2', version: '1.4.200-6'
+    // fwdConvertServer group: 'com.goldencode', name: 'fwd-h2', version: '1.4.200-6'
+    fwdConvertServer files('custom/myh2.jar')
     fwdConvertServer group: 'javax.transaction', name: 'jta', version: '1.1'
     fwdConvertServer group: 'org.janusgraph', name: 'janusgraph-core', version: '0.1.0'
 

=== modified file 'src/com/goldencode/p2j/jmx/FwdJMX.java'
--- old/src/com/goldencode/p2j/jmx/FwdJMX.java	2023-01-13 19:02:33 +0000
+++ new/src/com/goldencode/p2j/jmx/FwdJMX.java	2023-02-10 13:59:34 +0000
@@ -14,7 +14,7 @@
 **     IAS 20220626 Added SSL counters.
 **     AL2 20220828 Added H2 Query Profiler.
 **     CA  20221006 Added instrumentation for some APIs in the ORM and persistence layer.  Refs #6814
-*     CA  20221031 Added more instrumentation for:
+*      CA  20221031 Added more instrumentation for:
 **                  - the 'execute' time at H2 statements (OrmTempTableQuery).
 **                  - ContextLocal.get (ContextLocalGet).
 **                  - TableMapper.mapTemporaryTable (OrmTableMapperMap).
@@ -25,6 +25,7 @@
 **     AL2 20230104 Added OrmRedo.
 **                  Added OrmNoUndoTrace.
 **     OM  20230112 Added finer-granulation instrumentation for processing of dynamic queries. 
+**     AL2 20230210 Added instrumentation for RandomAccessQuery resolutions: fast-find, direct access, SQL.
 */ 
 
 /*
@@ -291,7 +292,6 @@
       /** Instrumentation for <code>TableMapper.mapTemporaryTable</code>. */
       OrmTableMapperMap,
       
-      
       /** Instrumentation for <code>TemporaryBuffer.associate</code>. */
       OrmTempTableParam,
       /** Instrumentation for <code>TemporaryBuffer.createDynamicTable</code>. */
@@ -301,6 +301,17 @@
       /** Instrumentation for <code>DataSet.createDynamicDataSet</code>. */
       OrmDynamicDataSetParam,
 
+      /** Instrumentation for <code>RandomAccessQuery</code> fast-find cache hits. */
+      OrmFFCacheHit,
+      /** Instrumentation for <code>RandomAccessQuery</code> fast-find cache hits with no record. */
+      OrmFFCacheHitNoRecord,
+      /** Instrumentation for <code>RandomAccessQuery</code> direct access technique over unique index. */
+      OrmFindDirectAccess,
+      /** Instrumentation for <code>RandomAccessQuery</code> direct access technique over recid */
+      OrmFindDirectAccessPk,
+      /** Instrumentation for <code>RandomAccessQuery.executeImpl</code> */
+      OrmFindExecute,
+
       /** Instrumentation for <code>OutputTableCopier.finished</code> */
       OutputTableCopier,
       /** Instrumentation for <code>OutputTableHandleCopier.finished</code> */

=== modified file 'src/com/goldencode/p2j/persist/FQLPreprocessor.java'
--- old/src/com/goldencode/p2j/persist/FQLPreprocessor.java	2022-11-30 14:33:29 +0000
+++ new/src/com/goldencode/p2j/persist/FQLPreprocessor.java	2023-02-10 14:06:39 +0000
@@ -282,6 +282,7 @@
 **                           are not the same), to be aware of the external buffers (i.e. added for CAN-FIND
 **                           sub-select clauses).  The translate will be performed before the query is being
 **                           executed.
+**     AL2 20230210          Use prop matches to identify if the where clause is a look-up on unique index.
 */
 
 /*
@@ -630,6 +631,9 @@
    /** Flag the query that it should performed on recid/rowid lookup table. */
    private boolean findByRowid = false;
    
+   /** Helper that helps indentifying if this where clause is in fact a unique index look-up. */
+   private UniqueIndexLookup uniqueIndexLookup = null;
+   
    /** 
     * The actual {@code rowid} value that is to be loaded if it is a constant detected in 
     * {@code fql} preprocessing. If it is {@code null} but {@code findByRowid = true} then
@@ -1280,7 +1284,15 @@
          // character datatype special case
          return new int64(((BaseDataType) params)).longValue();
       }
-      
+      else if (params instanceof Integer)
+      {
+         return ((Integer) params).longValue();
+      }
+      else if (params instanceof Long)
+      {
+         return ((Long) params).longValue();
+      }
+         
       if (LOG.isLoggable(Level.WARNING))
       {
          LOG.log(Level.WARNING, "Incompatible query parameters");
@@ -1289,6 +1301,34 @@
    }
    
    /**
+    * Check if the where clause is a conjunction of equality clauses that strictly cover an 
+    * unique index. This way, it is guaranteed that there is zero or one record fetched at 
+    * the end.
+    * 
+    * @param    includeRowid
+    *           {@code true} if the function should consider rowid as a field of an unique index.
+    *           
+    * @return   {@code true} if the target where clause if for zero or one record due to 
+    *           an unique index look-up.
+    */
+   public boolean isUniqueFind(boolean includeRowid)
+   {
+      return uniqueIndexLookup != null || (includeRowid ? isFindByRowid() : false);
+   }
+   
+   /**
+    * Retrieve the unique index loop-up object, responsible for storing information on the
+    * unique index that is used. Note that the properties form a unique index, but not necessarily
+    * in the provided order. Also, some values may be null due to the fact that they are substitution.
+    * 
+    * @return   an object which may assist faster look-up based on unique indexes
+    */
+   public UniqueIndexLookup getUniqueIndexLookup()
+   {
+      return uniqueIndexLookup;
+   }
+   
+   /**
     * Get the <code>ParameterIndices</code> object associated with this query.
     * This object maintains an array of zero-based indices into the array of
     * query substitution arguments for the where clause, as it exists
@@ -1491,13 +1531,12 @@
             annotateParameters(root, paramTypes);
          }
          
-         if (informational && parameters != null)
-         {
-            // at the moment, this method is only interesting for server-side join optimization
-            // analysis, which presumes at least one field reference parameter; if this becomes
-            // more useful generally, we can remove the conditional test above
-            collectPropertyMatches(root);
-         }
+         //if (informational && parameters != null)
+         // at the moment, this method is only interesting for server-side join optimization
+         // analysis, which presumes at least one field reference parameter; if this becomes
+         // more useful generally, we can remove the conditional test above
+         // it is useful for H2 direct access to identify queries on unique indexes
+         collectPropertyMatches(root);
          
          // simplify the predicate in raport to boolean values
          root = processCompareToUnknowns(root, parameters, indexedFields);
@@ -1554,6 +1593,8 @@
          
          this.findByRowid = checkFindByRowid(root);
          
+         this.uniqueIndexLookup = checkUniqueFind();
+         
          // Emit the preprocessed FQL expression text.
          this.fql = emit(root, false);
          
@@ -2086,6 +2127,130 @@
       return false;
    }
    
+
+   
+   /**
+    * Check if the where clause is a look-up over a unique index.
+    * <p>
+    * The goal here is to identify a common business logic pattern: find by an unique index, usually
+    * primary. The implementation doesn't detect if the where clauses combination suggest that 
+    * zero or one record is expected. It just checks if the property matches match an unique 
+    * index. Callers rely on this. Extending the method with heuristics for uniqueness beyond
+    * unique indexes may cause problems.
+    * 
+    * @return   {@code true} if the where clause is a conjunction of equalities covering all
+    *           the fields of an unique index.
+    */
+   private UniqueIndexLookup checkUniqueFind()
+   {      
+      List<PropertyMatch> matches = getPropertyMatches();
+      if (matches == null)
+      {
+         return null;
+      }
+      
+      RecordBuffer targetBuffer = null;
+      List<String> properties = new ArrayList<>();
+      for (int i = 0; i < matches.size(); i++)
+      {
+         PropertyMatch match = matches.get(i);
+         
+         if (match.operator != EQUALS)
+         {
+            return null;
+         }
+         
+         RecordBuffer buffer = DataTypeHelper.lookupBuffer(match.alias, bufferMap, fql);
+         if (buffer == null)
+         {
+            // this is unexpected; return false for fault tolerance
+            return null;
+         }
+         
+         if (targetBuffer == null)
+         {
+            targetBuffer = buffer;
+         }
+         else if (targetBuffer != buffer)
+         {
+            // mixed where clauses
+            return null;
+         }
+
+         properties.add(match.property);
+      }
+      
+      final DmoMeta meta = targetBuffer.getDmoInfo();
+      final Dialect dialect = targetBuffer.getDialect();
+      Function<String, String> honorComputed = (originalProp) -> 
+      {
+         Property propMeta = meta.getFieldInfo(originalProp);
+         if (propMeta._isCharacter && dialect.needsComputedColumns())
+         {
+            Boolean ccIgnoreCase = meta.isIndexedIgnoreCase(originalProp);
+            if (ccIgnoreCase != null)
+            {
+               return dialect.getComputedColumnPrefix(!ccIgnoreCase) + originalProp;
+            }
+         }
+         
+         return originalProp;
+      };
+      
+      Iterator<Set<String>> uniques = meta.getUniqueConstraints().iterator();
+      while (uniques.hasNext())
+      {
+         Set<String> compSet = uniques.next();
+         // match exactly an unique index; 
+         // having less fields doesn't make the where unique
+         // having more fields allows the where clause to return zero records even if a record is 
+         // found according to the unqiue index
+         if (compSet.size() == properties.size() && properties.containsAll(compSet))
+         {
+            UniqueIndexLookup lookup = new UniqueIndexLookup();
+            for (int i = 0; i < matches.size(); i++)
+            {
+               PropertyMatch match = matches.get(i);
+               Object arg = null;
+               switch (match.rvalType)
+               {
+                  case SUBST:
+                     arg = null;
+                     break;
+                  case BOOL_TRUE:
+                  case BOOL_FALSE:
+                     arg = match.rvalType == BOOL_TRUE;
+                     break;
+                  case NUM_LITERAL:
+                     arg = Long.parseLong(match.rval);
+                     break;
+                  case STRING:
+                     if (match.rval.startsWith("'") && match.rval.endsWith("'"))
+                     {
+                        arg = match.rval.substring(1,  match.rval.length() - 1);
+                     }
+                     else
+                     {
+                        // panic
+                        return null;
+                     }
+                     break;
+                  case DEC_LITERAL:
+                     arg = Double.parseDouble(match.rval);
+                     break;
+                  default:
+                     return null; // safety
+               }
+               String sqlProp = honorComputed.apply(match.property);
+               lookup.addMapping(sqlProp, arg);
+            }
+            return lookup;
+         }
+      }
+      
+      return null;
+   }
+   
    /**
     * Scan and parse the input where clause and return it in the form of an
     * abstract syntax tree.  Rewriting is not performed at this stage.

=== modified file 'src/com/goldencode/p2j/persist/RandomAccessQuery.java'
--- old/src/com/goldencode/p2j/persist/RandomAccessQuery.java	2022-11-30 14:33:29 +0000
+++ new/src/com/goldencode/p2j/persist/RandomAccessQuery.java	2023-02-10 14:18:23 +0000
@@ -347,6 +347,7 @@
 **                           are not the same), to be aware of the external buffers (i.e. added for CAN-FIND
 **                           sub-select clauses).  The translate will be performed before the query is being
 **                           executed.
+**     AL2 20230210          Allow direct access for recid/unique index look-ups.
 */
 
 /*
@@ -409,6 +410,9 @@
 import java.util.*;
 import java.util.function.*;
 import java.util.logging.*;
+
+import com.goldencode.p2j.jmx.FwdJMX;
+import com.goldencode.p2j.jmx.NanoTimer;
 import com.goldencode.p2j.persist.dirty.*;
 import com.goldencode.p2j.persist.event.*;
 import com.goldencode.p2j.persist.lock.*;
@@ -489,6 +493,26 @@
 {
    /** Logger */
    private static final Logger LOG = LogHelper.getLogger(RandomAccessQuery.class.getName());
+
+   /** Instrumentation for <code>RandomAccessQuery</code> fast-find cache hits. */
+   private static final NanoTimer FF_CACHE_HIT = 
+            NanoTimer.getInstance(FwdJMX.TimeStat.OrmFFCacheHit);
+
+   /** Instrumentation for <code>RandomAccessQuery</code> direct access technique over unique index. */
+   private static final NanoTimer FF_CACHE_HIT_NO_RECORD = 
+            NanoTimer.getInstance(FwdJMX.TimeStat.OrmFFCacheHitNoRecord);
+   
+   /** Instrumentation for <code>RandomAccessQuery</code> direct access technique over recid. */
+   private static final NanoTimer FIND_DIRECT_ACCESS_PK = 
+            NanoTimer.getInstance(FwdJMX.TimeStat.OrmFindDirectAccessPk);
+
+   /** Instrumentation for <code>RandomAccessQuery</code> direct access technique over unique index. */
+   private static final NanoTimer FIND_DIRECT_ACCESS = 
+            NanoTimer.getInstance(FwdJMX.TimeStat.OrmFindDirectAccess);
+
+   /** Instrumentation for <code>RandomAccessQuery.executeImpl</code>. */
+   private static final NanoTimer FIND_EXECUTE = 
+            NanoTimer.getInstance(FwdJMX.TimeStat.OrmFindExecute);   
    
    /** Record buffer which backs this query */
    private RecordBuffer buffer;
@@ -3444,6 +3468,7 @@
          RecordIdentifier<String> cacheResult = null;
          FastFindCache.Key key = null;
          FQLPreprocessor fqlPreproc = helper.getFQLPreprocessor();
+         DmoMeta meta = buffer.getDmoInfo();
          
          // attempt to get the result from fast-find cache, avoiding the expensive database trip.
          // This is not possible and the cache lookup is skipped when:
@@ -3451,9 +3476,7 @@
          //  - the query has a join
          //  - the query has an embedded CAN-FIND
          //  - the query has a client-side where expression
-         if (ffCache != null &&
-             (activeBundleKey == FIRST || activeBundleKey == LAST || activeBundleKey == UNIQUE) &&
-             join == null && getExternalBuffers() == null)
+         if (ffCache != null && isSimpleQuery())
          {
             BitSet properties = fqlPreproc.getQueryProperties();
             key = FastFindCache.createKey(buffer, index, where, properties, activeBundleKey, values);
@@ -3464,6 +3487,7 @@
                if (cacheResult == FastFindCache.NO_RECORD)
                {
                   // record will not be found in database
+                  FF_CACHE_HIT_NO_RECORD.timer(() -> { });
                   return null;
                }
                else
@@ -3483,15 +3507,25 @@
                         // were acquiring the lock; it is safe to use:
                         try
                         {
-                           return session.get(cacheResult);
+                           Record[] r = new Record[1];
+                           RecordIdentifier<String> internalCacheResult = cacheResult;
+                           FF_CACHE_HIT.timer(() -> r[0] = session.get(internalCacheResult));
+                           return r[0];
                         }
-                        catch (PersistenceException e)
+                        catch (RuntimeException e)
                         {
-                           if (LOG.isLoggable(Level.WARNING))
-                           {
-                              LOG.log(Level.WARNING, 
-                                      "Failed to get fast-find cached value for " + cacheResult,
-                                      e);
+                           if (e.getCause() instanceof PersistenceException)
+                           {
+                              if (LOG.isLoggable(Level.WARNING))
+                              {
+                                 LOG.log(Level.WARNING, 
+                                         "Failed to get fast-find cached value for " + cacheResult,
+                                         e.getCause());
+                              }
+                           }
+                           else
+                           {
+                              throw e;
                            }
                         }
                      }
@@ -3508,8 +3542,20 @@
             }
          }
          
-         assert (cacheResult == null);
-         Record record = executeImpl(values, lockType, unique, updateLock);
+         Record record = null;
+         
+         // use direct-access after ffCache failed
+         Optional<Record> directAccessResult = executeDirectAccess(values);
+         if (directAccessResult.isPresent())
+         {
+            record = directAccessResult.get();
+         }
+         else
+         {
+            Record[] r = new Record[1];
+            FIND_EXECUTE.timer(() -> r[0] = executeImpl(values, lockType, unique, updateLock));
+            record = r[0];
+         }
          
          if (ffCache != null &&
              cacheResult == null &&
@@ -4735,6 +4781,90 @@
    }
    
    /**
+    * Core method of attempting to retrieve the record for this query using direct access.
+    * This does all the prerequisites checks and is safely implemented such that a failed direct
+    * access allows easy recovery. 
+    * <p>
+    * A simple query (no join, external buffers and single result) over the temporary database 
+    * can be resolved using a direct access driver (currently supported by H2). This avoids generating
+    * an FQL/SQL and simply uses the internal H2 structures (tables, indexes, etc.) to find the
+    * searched record.
+    * <p>
+    * Currently, this can be used only if the H2 row structure is very similar to the FWD record 
+    * structure (to allow hydration) and the searched row can be uniquely identified: using recid
+    * or an unique index.
+    * 
+    * @param    values
+    *           The resolved substitution parameters to be used by the query.
+    *           
+    * @return   An optional depending on the success of this attempt. If a direct access exception 
+    *           occurred, allow fallback by returning an empty optional. If a record could be retrieved,
+    *           get it through the optional.
+    */
+   private Optional<Record> executeDirectAccess(Object[] values)
+   {
+      try
+      {
+         // reduce expenses; don't create a session just to check direct access
+         Session currentSession = buffer.getSession(false);
+         DmoMeta meta = buffer.getDmoInfo();
+         FQLPreprocessor fqlPreproc = helper.getFQLPreprocessor();
+         if (currentSession != null && 
+             isSimpleQuery() && 
+             buffer.isTemporary() && // prerequisites
+             fqlPreproc.isUniqueFind(true) && // this is the core condition right now
+             DirectAccessHelper.hasDirectAccess(currentSession) && // only if we have direct access to H2 
+             !meta.hasComputedColumns(false)) // only if all H2 columns represent the DMO fields
+         {            
+            Record[] r = new Record[1];
+            if (fqlPreproc.isFindByRowid())
+            {
+               long recid = fqlPreproc.getFindByRowid(values);
+               FIND_DIRECT_ACCESS_PK.timer(() ->
+                  r[0] = DirectAccessHelper.findByRowid(currentSession, meta, recid)
+               );
+            }
+            else if (fqlPreproc.isUniqueFind(false))
+            {
+               // make a copy to add the custom multiplex
+               UniqueIndexLookup lookup = new UniqueIndexLookup(fqlPreproc.getUniqueIndexLookup()); 
+               if (!lookup.fill(values))
+               {
+                  throw new DirectAccessException("Invalid fill operation");
+               }
+               lookup.addMapping(TemporaryBuffer.MULTIPLEX_FIELD_NAME, buffer.getMultiplexID());
+               FIND_DIRECT_ACCESS.timer(() -> 
+                  r[0] = DirectAccessHelper.findByUniqueIndex(currentSession, meta, lookup)
+               );
+            }
+            else
+            {
+               throw new DirectAccessException("Invalid state for direct access");
+               // not a valid state
+            }
+            
+            // the record can be null; make sure we let the caller know that we succeeded
+            return Optional.of(r[0]);
+         }
+      }
+      catch (Exception e)
+      {
+         if (e instanceof DirectAccessException || e.getCause() instanceof DirectAccessException)
+         {
+            return Optional.empty();
+         }
+         else
+         {
+            throw new RuntimeException(e);
+         }
+      }
+
+      // don't use exceptions if we fail here; if we do, there will be a lot of exception being thrown 
+      // it is faster to just let the caller know that we failed or not using DirectAccessResult
+      return Optional.empty();
+   }
+   
+   /**
     * Process the results of a dirty index check and return the most
     * appropriate result for the query.  This method may acquire a record lock
     * for the selected result.
@@ -4841,6 +4971,20 @@
    }
    
    /**
+    * Check if this is a "simple" query. The term simple is vague. It refers to queries which can be
+    * easier resolved through fast-find cache or direct access. "simple" means no join, no external
+    * buffer and no dependent bundle key (FIRST, LAST and UNIQUE do not depend on a previous
+    * sorted query).
+    * 
+    * @return   {@code true} if this query can be used by fast-find or direct access.
+    */
+   private boolean isSimpleQuery()
+   {
+      return (activeBundleKey == FIRST || activeBundleKey == LAST || activeBundleKey == UNIQUE) &&
+              join == null && getExternalBuffers() == null;
+   }
+   
+   /**
     * Log debug details about the inputs and results of a query.
     * 
     * @param   hql

=== modified file 'src/com/goldencode/p2j/persist/TempTableHelper.java'
--- old/src/com/goldencode/p2j/persist/TempTableHelper.java	2023-01-10 16:58:00 +0000
+++ new/src/com/goldencode/p2j/persist/TempTableHelper.java	2023-02-10 14:18:57 +0000
@@ -54,6 +54,7 @@
 **     OM  20221027          H2 dialect requires that the index names to be truly unique.
 **     SVL 20230110          P2JIndex.components() provides direct access to the components array in order to
 **                           improve performance.
+**     AL2 20230210          Mark the computed columns as INVISIBLE. This works as a hint for the H2 engine.
 */
 
 /*
@@ -591,9 +592,12 @@
          {
             String sqlType = dialect.getSqlMappedType(fieldMeta._fwdType, normalizedScale);
             String ccFormula = dialect.getComputedColumnFormula(fieldMeta.column, !fieldMeta.caseSensitive);
+            String invisible = dialect.getInvisibleSpecificator();
             sb.append(tab)
               .append(dialect.getComputedColumnPrefix(fieldMeta.caseSensitive)).append(fieldMeta.column)
-              .append(" ").append(sqlType).append(" as ").append(ccFormula)
+              .append(" ").append(sqlType)
+              .append(invisible != null ? " " + invisible : "")
+              .append(" as ").append(ccFormula)
               .append(",\n");
          }
          
@@ -601,9 +605,11 @@
          // in case it is lost 
          if (fieldMeta._isDatetimeTz)
          {
+            String invisible = dialect.getInvisibleSpecificator();
             sb.append(tab)
               .append(fieldMeta.column).append(DDLGeneratorWorker.DTZ_OFFSET).append(" ")
               .append(dialect.getSqlMappedType("integer", 0))
+              .append(invisible != null ? " " + invisible : "")
               .append(mandatoryColumn ? " not null,\n" : ",\n");
          }
       }

=== added file 'src/com/goldencode/p2j/persist/UniqueIndexLookup.java'
--- old/src/com/goldencode/p2j/persist/UniqueIndexLookup.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/persist/UniqueIndexLookup.java	2023-02-10 14:32:31 +0000
@@ -0,0 +1,171 @@
+/*
+** Module   : UniqueIndexLookup.java
+** Abstract : Container for properties and values used when doing a unique index look-up.
+**
+** Copyright (c) 2023, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- -----------------------------------Description-----------------------------------
+** 001 AL2 20230210 Created initial version.
+*/
+
+/*
+** 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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Utility class which stores the properties and values of a look-up which is done 
+ * over an unique index. 
+ */
+public class UniqueIndexLookup
+{
+   /** The properties of the unique index, not necessarily in order */
+   private List<String> properties;
+   
+   /** 
+    * The values used for look-up; the order matched the order of the properties
+    * A null value means a value that is provided latter (by substitution parameters).
+    */
+   private List<Object> values;
+
+   /**
+    * Basic constructor
+    */
+   public UniqueIndexLookup()
+   {
+      properties = new ArrayList<>();
+      values = new ArrayList<>();
+   }
+   
+   /**
+    * Copy constructor
+    * 
+    * @param  prototype
+    *         The prototype to be used when building this object.
+    */
+   public UniqueIndexLookup(UniqueIndexLookup prototype)
+   {
+      this.properties = new ArrayList<>(prototype.properties);
+      // deep copy is not needed; values are immutable
+      this.values = new ArrayList<>(prototype.values);
+   }
+   
+   /**
+    * Fill the {@code null} values from the values array with the provided fill values.
+    * This is used to lazily add the substitution parameters when needed. 
+    * 
+    * @param   fillValues
+    *          The values to be used when filling the parameters array.
+    *          
+    * @return  {@code true} if the filling succeeded. If this returns {@code false}, the 
+    *          object reaches a stale state, so avoid using it.
+    */
+   public boolean fill(Object[] fillValues)
+   {
+      int from = 0;
+      for (int i = 0; i < this.values.size(); i++)
+      {
+         if (this.values.get(i) == null)
+         {
+            if (fillValues == null || fillValues.length <= from)
+            {
+               return false;
+            }
+            this.values.set(i, fillValues[from++]);
+         }
+      }
+      if (fillValues == null)
+      {
+         return from == 0;
+      }
+      return from == fillValues.length;
+   }
+
+   /** 
+    * Add a new property-value mapping to this look-up.
+    * 
+    * @param   property
+    *          The property from an unique index.
+    * @param   value
+    *          The value to be used when doing look-up.
+    */
+   public void addMapping(String property, Object value)
+   {
+      properties.add(property);
+      values.add(value);
+   }
+
+   /**
+    * Retrieve the properties as an array.
+    * 
+    * @return   The array of properties.
+    */
+   public String[] getProperties()
+   {
+      return properties.toArray(new String[0]);
+   }
+   
+   /**
+    * Retrieve the values as an array.
+    * 
+    * @return   The array of values.
+    */
+   public Object[] getValues()
+   {
+      return values.toArray();
+   }
+   
+}

=== modified file 'src/com/goldencode/p2j/persist/dialect/Dialect.java'
--- old/src/com/goldencode/p2j/persist/dialect/Dialect.java	2023-01-28 04:45:51 +0000
+++ new/src/com/goldencode/p2j/persist/dialect/Dialect.java	2023-02-10 14:22:34 +0000
@@ -95,6 +95,7 @@
 **     SVL 20230110          P2JIndex.components() provides direct access to the components array in order to
 **                           improve performance.
 **     ECF 20230127          Added resolveBooleanConstant method.
+**     AL2 20230210          Added getInvisibleSpecificator.
 */
 
 /*
@@ -1061,6 +1062,18 @@
    }
    
    /**
+    * Return a string which marks that a column is computed, so it should be marked as invisible
+    * to allow implicit skipping of it when using wildcard selection (*) or direct access.
+    * 
+    * @return   An SQL string specificator for invisible columns. {@code null} means that no such 
+    *           specificator is supported. 
+    */
+   public String getInvisibleSpecificator()
+   {
+      return null;
+   }
+   
+   /**
     * Prepare the given metadata query parameter.
     * 
     * @param   param

=== modified file 'src/com/goldencode/p2j/persist/dialect/P2JH2Dialect.java'
--- old/src/com/goldencode/p2j/persist/dialect/P2JH2Dialect.java	2023-01-10 09:20:39 +0000
+++ new/src/com/goldencode/p2j/persist/dialect/P2JH2Dialect.java	2023-02-10 14:22:21 +0000
@@ -106,6 +106,7 @@
 **     OM  20221026          Excluded table name from index name by default. It will be added as needed by
 **                           dialects.
 **     RAA 20230109          Overrided methods getSequenceSetValString() and getSequenceCurrValString().
+**     AL2 20230210          Added getInvisibleSpecificator.
 */
 
 /*
@@ -1017,6 +1018,18 @@
    }
 
    /**
+    * Return a string which marks that a column is computed, so it should be marked as invisible
+    * to allow implicit skipping of it when using wildcard selection (*) or direct access.
+    * 
+    * @return   An SQL string specificator for invisible columns. {@code null} means that no such 
+    *           specificator is supported. 
+    */
+   public String getInvisibleSpecificator()
+   {
+      return "invisible";
+   }
+
+   /**
     * Prepare the given metadata query parameter by making it uppercase.
     * 
     * @param   param

=== modified file 'src/com/goldencode/p2j/persist/hql/DataTypeHelper.java'
--- old/src/com/goldencode/p2j/persist/hql/DataTypeHelper.java	2022-11-03 00:24:27 +0000
+++ new/src/com/goldencode/p2j/persist/hql/DataTypeHelper.java	2023-02-10 14:22:01 +0000
@@ -37,6 +37,7 @@
 ** 022 IAS 20201123          Added ARRAY type
 **     IAS 20220926          Process extra parentheses in the expressionType() method.
 **     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
+**     AL2 20230210          Allow permissive look-up buffer (without exception of fail).
 */
 
 /*
@@ -433,18 +434,48 @@
                                            FQLExpression fql)
    {
       String aliasName = alias != null ? alias.getText() : null;
-      RecordBuffer buffer = bufferMap.get(aliasName);
-      
-      if (buffer == null)
+      RecordBuffer rb = lookupBuffer(aliasName, bufferMap, fql);
+      if (rb == null)
       {
-         throw new IllegalArgumentException(
-            "Unrecognized buffer at column " +
+         throw new IllegalArgumentException("Unrecognized buffer at column " +
             (alias != null ? alias.getColumn() : "<unknown>") +
             " in where clause:  '" +
             aliasName +
             "' [" +
             (fql != null ? fql.toString() : "<unavailable>") +
-            "]");
+            "]"
+         );
+      };
+      return rb;
+   }
+   
+   /**
+    * Attempt to look up a record buffer by alias from the given map. The DMO alias is extracted
+    * from the {@code alias} AST. If there is no matching entry in the buffer map, an exception
+    * is thrown.
+    * <p>
+    * The returned buffer is initialized by side effect.
+    * 
+    * @param   aliasName
+    *          Alias for DMO. If {@code null}, a {@code null} lookup key is used.
+    * @param   bufferMap
+    *          A map of DMO aliases to record buffers. If no alias is provided (e.g., for an
+    *          unqualified property reference, the appropriate buffer is expected to be mapped
+    *          to the {@code null} key.
+    * @param   fql
+    *          Optional FQL expression from which the alias is parsed.
+    * 
+    * @return  A record buffer matching the given alias.
+    */
+   public static RecordBuffer lookupBuffer(String aliasName,
+                                            Map<String, RecordBuffer> bufferMap,
+                                            FQLExpression fql)
+   {
+      RecordBuffer buffer = bufferMap.get(aliasName);
+      
+      if (buffer == null)
+      {
+         return null;
       }
       
       buffer.initialize();

=== added file 'src/com/goldencode/p2j/persist/orm/DirectAccessException.java'
--- old/src/com/goldencode/p2j/persist/orm/DirectAccessException.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/persist/orm/DirectAccessException.java	2023-02-10 14:34:13 +0000
@@ -0,0 +1,100 @@
+/*
+** Module   : DirectAccessException.java
+** Abstract : Exception related to the direct access of internal database structures technique.
+**
+** Copyright (c) 2023, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- -----------------------------------Description-----------------------------------
+** 001 AL2 20230210 Created initial version.
+*/
+
+/*
+** 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;
+
+/**
+ * Exception used when anything goes wrong in the direct access attempts. This is used to
+ * do recovery in case direct access doesn't work. As direct access attempts are read-only,
+ * throwing such exception doesn't imply stale states. Therefore, ensure that a fallback 
+ * plan is used if such exception occurs.
+ */
+public class DirectAccessException
+extends Exception
+{
+   /**
+    * Basic constructor
+    * 
+    * @param   msg
+    *          Exception message.
+    */
+   public DirectAccessException(String msg)
+   {
+      super(msg);
+   }
+   
+   /**
+    * Basic constructor
+    * 
+    * @param   msg
+    *          Exception message.
+    * @param   cause 
+    *          Exception cause.
+    */
+   public DirectAccessException(String msg, Exception cause)
+   {
+      super(msg, cause);
+   }
+
+}

=== added file 'src/com/goldencode/p2j/persist/orm/DirectAccessHelper.java'
--- old/src/com/goldencode/p2j/persist/orm/DirectAccessHelper.java	1970-01-01 00:00:00 +0000
+++ new/src/com/goldencode/p2j/persist/orm/DirectAccessHelper.java	2023-02-10 14:42:16 +0000
@@ -0,0 +1,286 @@
+/*
+** Module   : DirectAccessHelper.java
+** Abstract : Middleware between FWD persistence layer and underlying database with direct access interface.
+**
+** Copyright (c) 2023, Golden Code Development Corporation.
+**
+** -#- -I- --Date-- -----------------------------------Description-----------------------------------
+** 001 AL2 20230210 Created initial version.
+*/
+
+/*
+** 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.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.h2.embedded.DirectAccessDriver;
+import org.h2.jdbc.JdbcConnection;
+
+import com.goldencode.p2j.persist.PersistenceException;
+import com.goldencode.p2j.persist.Record;
+import com.goldencode.p2j.persist.UniqueIndexLookup;
+import com.goldencode.p2j.util.*;
+
+/**
+ * Common point where all the direct access interface is used inside FWD. Any attempt
+ * to use a database driver internal structures should be done through this gateway.
+ */
+public class DirectAccessHelper
+{
+   /**
+    * Direct access attempt on database structures to retrieve a single record based on recid.
+    * 
+    * @param   session 
+    *          The session with a database with direct access interface.
+    * @param   meta
+    *          The meta of the table to be queried.
+    * @param   recid
+    *          The value of the recid for the record to be retrieved.
+    *          
+    * @return  A hydrated record directly from the underlying direct access interface.
+    */
+   public static Record findByRowid(Session session, DmoMeta meta, long recid)
+   throws DirectAccessException
+   {
+      try
+      {
+         JdbcConnection conn = getH2Connection(session);
+         DirectAccessDriver driver = conn.getDirectAccessDriver();
+         String tableName = meta.getSqlTableName();
+         ResultSet rs = driver.getRow(tableName, recid);
+         return hydrateFromResultSet(session, meta, rs);
+      }
+      catch (SQLException | PersistenceException e)
+      {
+         throw new DirectAccessException("Error direct accessing for finding record by rowid", e);
+      }
+   }
+
+   /**
+    * Direct access attempt on database structures to retrieve a single record based on a 
+    * unique index.
+    * 
+    * @param   session 
+    *          The session with a database with direct access interface.
+    * @param   meta
+    *          The meta of the table to be queried.
+    * @param   lookup
+    *          Specific object which contains low-level information about the properties 
+    *          and values needed for unique index lookup. 
+    *          
+    * @return  A hydrated record directly from the underlying direct access interface.
+    */
+   public static Record findByUniqueIndex(Session session, DmoMeta meta, UniqueIndexLookup lookup)
+   throws DirectAccessException
+   {
+      try
+      {
+         JdbcConnection conn = getH2Connection(session);
+         DirectAccessDriver driver = conn.getDirectAccessDriver();
+         String tableName = meta.getSqlTableName();
+         Object[] values = lookup.getValues();
+         for (int i = 0; i < values.length; i++)
+         {
+            values[i] = normalizeValue(values[i]);
+         }
+         ResultSet rs = driver.getUniqueRow(tableName, lookup.getProperties(), values);
+         return hydrateFromResultSet(session, meta, rs);
+      }
+      catch (SQLException | PersistenceException e)
+      {
+         throw new DirectAccessException("Error direct accessing for finding record by unique index", e);
+      }
+   }
+
+   /**
+    * Check if the provided session allows a direct access interface.
+    * 
+    * @param   session 
+    *          The session with a database with direct access interface.
+    *          
+    * @return  {@code true} if this session allows direct access.
+    */
+   public static boolean hasDirectAccess(Session session)
+   {
+      return getH2Connection(session) != null;
+   }
+
+   /**
+    * Normalize the FWD specific data types into Java native types. This is needed
+    * in order to interface with the underlying database through direct access.
+    * 
+    * @param   arg
+    *          The object to be "normalized" (converted to Java native type)
+    *          
+    * @return  A Java native type that can be used by the direct access interface.
+    */
+   private static Object normalizeValue(Object arg)
+   throws DirectAccessException
+   {
+      if (arg == null)
+      {
+         return null;
+      }
+      if (arg instanceof Integer || 
+          arg instanceof Long    || 
+          arg instanceof Boolean || 
+          arg instanceof Double  ||
+          arg instanceof String)
+      {
+         // already normalized
+         return arg;
+      }
+      if (arg instanceof integer)
+      {
+         return ((integer) arg).toJavaIntegerType();
+      }
+      if (arg instanceof int64)
+      {
+         return ((int64) arg).toJavaLongType();
+      }
+      if (arg instanceof logical)
+      {
+         return ((logical) arg).toJavaType();
+      }
+      if (arg instanceof handle)
+      {
+         return ((handle) arg).getResourceId();
+      }
+      if (arg instanceof character)
+      {
+         return ((character) arg).toJavaType();
+      }
+      if (arg instanceof decimal)
+      {
+         return ((decimal) arg).toJavaType();
+      }
+      
+      throw new DirectAccessException("Can't normalize FWD value to use it with direct access.");
+   }
+   
+   /**
+    * Core method of hydrating a record from the retrieved result set. Usually, the result-set
+    * is a dummy container of row structures from the underlying database. Make sure these
+    * are converted to FWD structures by hydrating.
+    * 
+    * @param   session 
+    *          The session with a database with direct access interface.
+    * @param   meta
+    *          The meta of the table to be queried.
+    * @param   rs
+    *          The result-set returned by the direct access interface.
+    *          
+    * @return  A hydrated record directly from the underlying direct access interface.
+    */
+   private static Record hydrateFromResultSet(Session session, DmoMeta meta, ResultSet rs)
+   throws PersistenceException, 
+          SQLException
+   {
+      RowStructure rowStructure = new RowStructure(meta.dmoImplInterface, meta.propsByName.size());
+      if (rs.next())
+      {
+         return (Record) SQLQuery.hydrateRecord(rs, rowStructure, 1, session);
+      }
+      else
+      {
+         return null;
+      }
+   }
+   
+   /**
+    * Retrieve the H2 connection straight from the provided session. This returns {@code null}
+    * if the H2 connection doesn't allows direct access interface of if this is not a H2 connection.
+    * This should be refactored maybe to be more generic (not necessarily that close related to H2)
+    * 
+    * @param   session 
+    *          The session with a database with direct access interface.
+    *          
+    * @return  A H2 specific connection.
+    */
+   private static JdbcConnection getH2Connection(Session session)
+   {
+      if (session == null)
+      {
+         return null;
+      }
+      
+      Connection conn;
+      try
+      {
+         conn = session.getConnection();
+      }
+      catch (PersistenceException e)
+      {
+         return null;
+      }
+      
+      if (TempTableDataSourceProvider.isProxy(conn))
+      {
+         conn = TempTableDataSourceProvider.unwrapProxy(conn);
+      }
+      
+      if (conn != null && 
+          conn instanceof JdbcConnection && 
+          ((JdbcConnection) conn).getDirectAccessDriver() != null)
+      {
+         return (JdbcConnection) conn;
+      }
+      
+      return null;
+   }
+
+}

=== modified file 'src/com/goldencode/p2j/persist/orm/DmoMeta.java'
--- old/src/com/goldencode/p2j/persist/orm/DmoMeta.java	2023-01-26 11:18:48 +0000
+++ new/src/com/goldencode/p2j/persist/orm/DmoMeta.java	2023-02-10 14:23:03 +0000
@@ -49,6 +49,7 @@
 **     SVL 20230113 Improved performance by replacing some "for-each" loops with indexed "for" loops.
 **     HC  20230118 Eliminated some of the uses of String.toUpperCase and/or String.toLowerCase
 **                  for performance.
+**     AL2 20230120 Compute if the underlying DMO requires extra computed columns.
 **     CA  20221114 Added API to get the denormalized properties for a DMO extent property name.
 **     ECF 20221207 Changed heuristic to extract schema name from full DMO interface name.
 */
@@ -282,6 +283,9 @@
    /** Map of property names to extents for DMO array properties */
    private Map<String, Integer> extentMap = null;
    
+   /** Check if this DMO is going to have computed columns in the underlying database representation */
+   private Boolean[] hasComputedColumns = null;
+   
    /** The map used in type-widening operations. */
    private static final Map<Class<? extends BaseDataType>, Class<? extends BaseDataType>> toWider = 
          new IdentityHashMap<>();
@@ -1595,6 +1599,75 @@
       return Category.VST.name().equals(category) && systemTable.fileName.equalsIgnoreCase(legacyTable);
    }
    
+   
+   /**
+    * Check if this DMO has computed columns in the underlying database representation. This is 
+    * used mostly for temporary tables and may not be completely compatible with persistent tables.
+    * 
+    * @param    includeInvisible 
+    *           {@code true} to include the columns which are defined as invisible in the underlying 
+    *           database. Invisible columns are the ones which are not implicitly retrieved using 
+    *           SELECT * or direct access.
+    * 
+    * @return   {@code true} if we also have other columns that the legacy ones in the database.
+    */
+   public boolean hasComputedColumns(boolean includeInvisible)
+   {
+      if (hasComputedColumns == null)
+      {
+         hasComputedColumns = new Boolean[2];
+      }
+
+      int position = includeInvisible ? 0 : 1;
+      if (hasComputedColumns[position] == null)
+      {
+         hasComputedColumns[position] = false;
+         
+         Set<Map.Entry<String, Property>> fieldEntries = this.propsByName.entrySet();
+ 
+         Set<Property> indexedProperties = new HashSet<>();
+         Iterator<P2JIndex> databaseIndexes = this.getDatabaseIndexes();
+       
+         while (databaseIndexes.hasNext())
+         {
+            P2JIndex index = databaseIndexes.next();
+            ArrayList<P2JIndexComponent> components = index.components();
+            for (int i = 0; i < components.size(); i++)
+            {
+               P2JIndexComponent comp = components.get(i);            
+               Property property = this.getFieldInfo(comp.getPropertyName());
+               indexedProperties.add(property);
+            }
+         }
+
+         // temp-tables have invisible columns
+         boolean relaxed = tempTable && !includeInvisible;
+         for (Map.Entry<String, Property> field : fieldEntries)
+         {
+            Property property = field.getValue();
+            
+            if (!relaxed && indexedProperties.contains(property) && property._isCharacter)
+            {
+               hasComputedColumns[position] = true;
+               break;
+            }
+
+            if (property.extent > 0)
+            {
+               hasComputedColumns[position] = true;
+               break;
+            }
+            
+            if (!relaxed && property._isDatetimeTz)
+            {
+               hasComputedColumns[position] = true;
+               break;
+            }
+         }
+      }
+      return hasComputedColumns[position];
+   }
+   
    /**
     * Get the map of DMO property names to {@code P2JField} objects describing the corresponding fields,
     * creating it first, if necessary.

=== modified file 'src/com/goldencode/p2j/persist/orm/TempTableDataSourceProvider.java'
--- old/src/com/goldencode/p2j/persist/orm/TempTableDataSourceProvider.java	2022-10-11 15:08:04 +0000
+++ new/src/com/goldencode/p2j/persist/orm/TempTableDataSourceProvider.java	2023-02-10 14:26:23 +0000
@@ -15,6 +15,8 @@
 **     CA  20221011 Decreased prepared statement cache to 2048.  The PreparedStatements are not cleaned when
 **                  a temp-table is dropped, and this will pin in memory the entire temp-table (with rows,
 **                  indexes, etc) at H2 level.
+** 003 AL2 20230210 Major refactor. Proxied connection is no longer an annonymous class. Make it named
+**                  to identify if the connection is proxied or not.
 */
 
 /*
@@ -96,6 +98,41 @@
 class TempTableDataSourceProvider
 implements DataSourceProvider
 {
+   
+   /**
+    * Check if the connection is a proxy delivered by this provider.
+    * 
+    * @param   conn
+    *          The connection to be checked.
+    *          
+    * @return  {@code true} if the provided connection is proxied by this provider.
+    */
+   public static boolean isProxy(Connection conn)
+   {
+      return conn instanceof DataSourceImpl.TempProxyConnection;
+   }
+   
+   /**
+    * Retrieve the original connection without proxy. Use this with care as it bypassed
+    * some critical business logic. Avoid executing SQL directly on the physical connection,
+    * unless you are sure that the wrapper is not affected.
+    * 
+    * @param   conn
+    *          The connection to unwrap.
+    *          
+    * @return  The physical connection. The unwrapping works only if this is a proxy of this
+    *          provider.
+    */
+   public static Connection unwrapProxy(Connection conn)
+   {
+      if (conn == null || !isProxy(conn))
+      {
+         return null;
+      }
+      DataSourceImpl.TempProxyConnection proxy = (DataSourceImpl.TempProxyConnection) conn;
+      return proxy.ctx.physicalConn;
+   }
+   
    /**
     * Obtain the {@code DataSource} for a specific {@code Database}, within the current context.
     *
@@ -110,6 +147,7 @@
       return new DataSourceImpl();
    }
    
+   
    /**
     * Implementation of the {@code DataSource} interface, which maintains context-local data for each
     * private temp-table connection.
@@ -177,7 +215,7 @@
             String url = settings.getString(Settings.URL, null);
             if (username == null)
             {
-               username =settings.getString(Settings.USER, null);
+               username = settings.getString(Settings.USER, null);
             }
             if (password == null)
             {
@@ -196,124 +234,7 @@
          
          if (ctx.proxyConn == null)
          {
-            ctx.proxyConn = new Connection()
-            {
-               public void close() throws SQLException
-               {
-                  DataSourceImpl.this.close();
-               }
-               
-               public PreparedStatement prepareStatement(String sql)
-               throws SQLException
-               {
-                  // prepare using default result set type and concurrency
-                  return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
-               }
-               
-               public PreparedStatement prepareStatement(String sql,
-                                                         int resultSetType,
-                                                         int resultSetConcurrency)
-               throws SQLException
-               {
-                  PSKey key = new PSKey(sql, resultSetType, resultSetConcurrency);
-                  UnclosablePreparedStatement ps = ctx.psCache.get(key);
-                  
-                  if (ps == null || ps.isClosed())
-                  {
-                     if (ps != null)
-                     {
-                        if (LOG.isLoggable(Level.SEVERE))
-                        {
-                           LOG.log(Level.SEVERE, "Closed statements shouldn't be cached.");
-                        }
-                     }
-                     
-                     PreparedStatement raw = ctx.physicalConn.prepareStatement(sql, resultSetType, resultSetConcurrency);
-                     ps = new UnclosablePreparedStatement(raw, false);
-                     ctx.psCache.put(key, ps);
-                  }
-                  else if (ps.isCheckedOut())
-                  {
-                     // the prepared statement is already in use
-                     // create another one which will not be cached
-                     PreparedStatement raw = ctx.physicalConn.prepareStatement(sql, resultSetType, resultSetConcurrency);
-                     ps = new UnclosablePreparedStatement(raw, true); 
-                     
-                     if (LOG.isLoggable(Level.FINE))
-                     {
-                        LOG.log(Level.FINE, "Multiply-cached PreparedStatement: " + sql);
-                     }
-                  }
-                  
-                  ps.checkOut();
-                  return ps;
-               }
-               
-               public <T> T unwrap(Class<T> iface) throws SQLException { return ctx.physicalConn.unwrap(iface); }
-               public boolean isWrapperFor(Class<?> iface) throws SQLException { return ctx.physicalConn.isWrapperFor(iface); }
-               public Statement createStatement() throws SQLException { return ctx.physicalConn.createStatement(); }
-               public CallableStatement prepareCall(String sql) throws SQLException { return ctx.physicalConn.prepareCall(sql); }
-               public String nativeSQL(String sql) throws SQLException { return ctx.physicalConn.nativeSQL(sql); }
-               public void setAutoCommit(boolean autoCommit) throws SQLException { ctx.physicalConn.setAutoCommit(autoCommit); }
-               public boolean getAutoCommit() throws SQLException { return ctx.physicalConn.getAutoCommit(); }
-               public void commit() throws SQLException { ctx.physicalConn.commit(); }
-               public void rollback() throws SQLException { ctx.physicalConn.rollback(); }
-               public boolean isClosed() throws SQLException { return ctx.physicalConn == null || ctx.physicalConn.isClosed(); }
-               public DatabaseMetaData getMetaData() throws SQLException { return ctx.physicalConn.getMetaData(); }
-               public void setReadOnly(boolean readOnly) throws SQLException { ctx.physicalConn.setReadOnly(readOnly); }
-               public boolean isReadOnly() throws SQLException { return ctx.physicalConn.isReadOnly(); }
-               public void setCatalog(String catalog) throws SQLException { ctx.physicalConn.setCatalog(catalog); }
-               public String getCatalog() throws SQLException { return ctx.physicalConn.getCatalog(); }
-               public void setTransactionIsolation(int level) throws SQLException { ctx.physicalConn.setTransactionIsolation(level); }
-               public int getTransactionIsolation() throws SQLException { return ctx.physicalConn.getTransactionIsolation(); }
-               public SQLWarning getWarnings() throws SQLException { return ctx.physicalConn.getWarnings(); }
-               public void clearWarnings() throws SQLException { ctx.physicalConn.clearWarnings(); }
-               public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
-                  return ctx.physicalConn.createStatement(resultSetType, resultSetConcurrency); }
-               public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
-                  return ctx.physicalConn.prepareCall(sql, resultSetType, resultSetConcurrency); }
-               public Map<String, Class<?>> getTypeMap() throws SQLException {
-                  return ctx.physicalConn.getTypeMap(); }
-               public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
-                  ctx.physicalConn.setTypeMap(map); }
-               public void setHoldability(int holdability) throws SQLException { ctx.physicalConn.setHoldability(holdability); }
-               public int getHoldability() throws SQLException { return ctx.physicalConn.getHoldability(); }
-               public Savepoint setSavepoint() throws SQLException { return ctx.physicalConn.setSavepoint(); }
-               public Savepoint setSavepoint(String name) throws SQLException { return ctx.physicalConn.setSavepoint(name); }
-               public void rollback(Savepoint savepoint) throws SQLException { ctx.physicalConn.rollback(savepoint); }
-               public void releaseSavepoint(Savepoint savepoint) throws SQLException { ctx.physicalConn.releaseSavepoint(savepoint); }
-               public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
-                  return ctx.physicalConn.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); }
-               public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
-                  return ctx.physicalConn.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); }
-               public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
-                  return ctx.physicalConn.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); }
-               public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { 
-                  return ctx.physicalConn.prepareStatement(sql, autoGeneratedKeys); }
-               public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { 
-                  return ctx.physicalConn.prepareStatement(sql, columnIndexes); }
-               public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { 
-                  return ctx.physicalConn.prepareStatement(sql, columnNames); }
-               public Clob createClob() throws SQLException { return ctx.physicalConn.createClob(); }
-               public Blob createBlob() throws SQLException { return ctx.physicalConn.createBlob(); }
-               public NClob createNClob() throws SQLException { return ctx.physicalConn.createNClob(); }
-               public SQLXML createSQLXML() throws SQLException { return ctx.physicalConn.createSQLXML(); }
-               public boolean isValid(int timeout) throws SQLException { return ctx.physicalConn.isValid(timeout); }
-               public void setClientInfo(String name, String value) throws SQLClientInfoException { ctx.physicalConn.setClientInfo(name, value); }
-               public void setClientInfo(Properties properties) throws SQLClientInfoException { ctx.physicalConn.setClientInfo(properties); }
-               public String getClientInfo(String name) throws SQLException { return ctx.physicalConn.getClientInfo(name); }
-               public Properties getClientInfo() throws SQLException { return ctx.physicalConn.getClientInfo(); }
-               public Array createArrayOf(String typeName, Object[] elements) throws SQLException { 
-                  return ctx.physicalConn.createArrayOf(typeName, elements); }
-               public Struct createStruct(String typeName, Object[] attributes) throws SQLException { 
-                  return ctx.physicalConn.createStruct(typeName, attributes); }
-               public void setSchema(String schema) throws SQLException { ctx.physicalConn.setSchema(schema); }
-               public String getSchema() throws SQLException { return ctx.physicalConn.getSchema(); }
-               public void abort(Executor executor) throws SQLException { ctx.physicalConn.abort(executor); }
-               public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { 
-                  ctx.physicalConn.setNetworkTimeout(executor, milliseconds); }
-               public int getNetworkTimeout() throws SQLException { return ctx.physicalConn.getNetworkTimeout(); }
-            };
+            ctx.proxyConn = new TempProxyConnection(ctx);
          }
          
          return ctx.proxyConn;
@@ -473,6 +394,137 @@
             }
          }
       }
+      
+      public class TempProxyConnection
+      implements Connection
+      {
+         /** */
+         private Context ctx;
+         
+         /**
+          * @param ctx
+          */
+         TempProxyConnection(Context ctx)
+         {
+            this.ctx = ctx;
+         }
+         
+         public void close() throws SQLException
+         {
+            DataSourceImpl.this.close();
+         }
+         
+         public PreparedStatement prepareStatement(String sql)
+         throws SQLException
+         {
+            // prepare using default result set type and concurrency
+            return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
+         }
+         
+         public PreparedStatement prepareStatement(String sql,
+                                                   int resultSetType,
+                                                   int resultSetConcurrency)
+         throws SQLException
+         {
+            PSKey key = new PSKey(sql, resultSetType, resultSetConcurrency);
+            UnclosablePreparedStatement ps = ctx.psCache.get(key);
+            
+            if (ps == null || ps.isClosed())
+            {
+               if (ps != null)
+               {
+                  if (LOG.isLoggable(Level.SEVERE))
+                  {
+                     LOG.log(Level.SEVERE, "Closed statements shouldn't be cached.");
+                  }
+               }
+               
+               PreparedStatement raw = ctx.physicalConn.prepareStatement(sql, resultSetType, resultSetConcurrency);
+               ps = new UnclosablePreparedStatement(raw, false);
+               ctx.psCache.put(key, ps);
+            }
+            else if (ps.isCheckedOut())
+            {
+               // the prepared statement is already in use
+               // create another one which will not be cached
+               PreparedStatement raw = ctx.physicalConn.prepareStatement(sql, resultSetType, resultSetConcurrency);
+               ps = new UnclosablePreparedStatement(raw, true); 
+               
+               if (LOG.isLoggable(Level.FINE))
+               {
+                  LOG.log(Level.FINE, "Multiply-cached PreparedStatement: " + sql);
+               }
+            }
+            
+            ps.checkOut();
+            return ps;
+         }
+         
+         public <T> T unwrap(Class<T> iface) throws SQLException { return ctx.physicalConn.unwrap(iface); }
+         public boolean isWrapperFor(Class<?> iface) throws SQLException { return ctx.physicalConn.isWrapperFor(iface); }
+         public Statement createStatement() throws SQLException { return ctx.physicalConn.createStatement(); }
+         public CallableStatement prepareCall(String sql) throws SQLException { return ctx.physicalConn.prepareCall(sql); }
+         public String nativeSQL(String sql) throws SQLException { return ctx.physicalConn.nativeSQL(sql); }
+         public void setAutoCommit(boolean autoCommit) throws SQLException { ctx.physicalConn.setAutoCommit(autoCommit); }
+         public boolean getAutoCommit() throws SQLException { return ctx.physicalConn.getAutoCommit(); }
+         public void commit() throws SQLException { ctx.physicalConn.commit(); }
+         public void rollback() throws SQLException { ctx.physicalConn.rollback(); }
+         public boolean isClosed() throws SQLException { return ctx.physicalConn == null || ctx.physicalConn.isClosed(); }
+         public DatabaseMetaData getMetaData() throws SQLException { return ctx.physicalConn.getMetaData(); }
+         public void setReadOnly(boolean readOnly) throws SQLException { ctx.physicalConn.setReadOnly(readOnly); }
+         public boolean isReadOnly() throws SQLException { return ctx.physicalConn.isReadOnly(); }
+         public void setCatalog(String catalog) throws SQLException { ctx.physicalConn.setCatalog(catalog); }
+         public String getCatalog() throws SQLException { return ctx.physicalConn.getCatalog(); }
+         public void setTransactionIsolation(int level) throws SQLException { ctx.physicalConn.setTransactionIsolation(level); }
+         public int getTransactionIsolation() throws SQLException { return ctx.physicalConn.getTransactionIsolation(); }
+         public SQLWarning getWarnings() throws SQLException { return ctx.physicalConn.getWarnings(); }
+         public void clearWarnings() throws SQLException { ctx.physicalConn.clearWarnings(); }
+         public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
+            return ctx.physicalConn.createStatement(resultSetType, resultSetConcurrency); }
+         public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
+            return ctx.physicalConn.prepareCall(sql, resultSetType, resultSetConcurrency); }
+         public Map<String, Class<?>> getTypeMap() throws SQLException {
+            return ctx.physicalConn.getTypeMap(); }
+         public void setTypeMap(Map<String, Class<?>> map) throws SQLException {
+            ctx.physicalConn.setTypeMap(map); }
+         public void setHoldability(int holdability) throws SQLException { ctx.physicalConn.setHoldability(holdability); }
+         public int getHoldability() throws SQLException { return ctx.physicalConn.getHoldability(); }
+         public Savepoint setSavepoint() throws SQLException { return ctx.physicalConn.setSavepoint(); }
+         public Savepoint setSavepoint(String name) throws SQLException { return ctx.physicalConn.setSavepoint(name); }
+         public void rollback(Savepoint savepoint) throws SQLException { ctx.physicalConn.rollback(savepoint); }
+         public void releaseSavepoint(Savepoint savepoint) throws SQLException { ctx.physicalConn.releaseSavepoint(savepoint); }
+         public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
+            return ctx.physicalConn.createStatement(resultSetType, resultSetConcurrency, resultSetHoldability); }
+         public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
+            return ctx.physicalConn.prepareStatement(sql, resultSetType, resultSetConcurrency, resultSetHoldability); }
+         public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
+            return ctx.physicalConn.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability); }
+         public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { 
+            return ctx.physicalConn.prepareStatement(sql, autoGeneratedKeys); }
+         public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { 
+            return ctx.physicalConn.prepareStatement(sql, columnIndexes); }
+         public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { 
+            return ctx.physicalConn.prepareStatement(sql, columnNames); }
+         public Clob createClob() throws SQLException { return ctx.physicalConn.createClob(); }
+         public Blob createBlob() throws SQLException { return ctx.physicalConn.createBlob(); }
+         public NClob createNClob() throws SQLException { return ctx.physicalConn.createNClob(); }
+         public SQLXML createSQLXML() throws SQLException { return ctx.physicalConn.createSQLXML(); }
+         public boolean isValid(int timeout) throws SQLException { return ctx.physicalConn.isValid(timeout); }
+         public void setClientInfo(String name, String value) throws SQLClientInfoException { ctx.physicalConn.setClientInfo(name, value); }
+         public void setClientInfo(Properties properties) throws SQLClientInfoException { ctx.physicalConn.setClientInfo(properties); }
+         public String getClientInfo(String name) throws SQLException { return ctx.physicalConn.getClientInfo(name); }
+         public Properties getClientInfo() throws SQLException { return ctx.physicalConn.getClientInfo(); }
+         public Array createArrayOf(String typeName, Object[] elements) throws SQLException { 
+            return ctx.physicalConn.createArrayOf(typeName, elements); }
+         public Struct createStruct(String typeName, Object[] attributes) throws SQLException { 
+            return ctx.physicalConn.createStruct(typeName, attributes); }
+         public void setSchema(String schema) throws SQLException { ctx.physicalConn.setSchema(schema); }
+         public String getSchema() throws SQLException { return ctx.physicalConn.getSchema(); }
+         public void abort(Executor executor) throws SQLException { ctx.physicalConn.abort(executor); }
+         public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { 
+            ctx.physicalConn.setNetworkTimeout(executor, milliseconds); }
+         public int getNetworkTimeout() throws SQLException { return ctx.physicalConn.getNetworkTimeout(); }
+      }
    }
    
    /**

