=== modified file 'src/com/goldencode/p2j/persist/AbstractQuery.java'
--- old/src/com/goldencode/p2j/persist/AbstractQuery.java	2022-04-12 17:05:25 +0000
+++ new/src/com/goldencode/p2j/persist/AbstractQuery.java	2022-11-09 18:36:33 +0000
@@ -220,6 +220,8 @@
 **     OM  20210917          Code maintenance.
 **     OM  20220112          Added implementation of SKIP-DELETED-RECORD attribute.
 **     AL2 20220412          Do proxy checks for values used as query parameters.
+**     CA  20221109          Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded 
+**                           as 'partial/incomplete'.
 */
 
 /*
@@ -282,6 +284,7 @@
 import java.util.logging.*;
 import com.goldencode.p2j.persist.event.*;
 import com.goldencode.p2j.persist.lock.*;
+import com.goldencode.p2j.persist.orm.*;
 import com.goldencode.p2j.util.*;
 import com.goldencode.p2j.util.ErrorManager;
 
@@ -323,6 +326,12 @@
    /** Flag to indicate if this query is optimized using indexed reposition */
    protected boolean indexedReposition = false;
    
+   /** The included properties for each DMO (FIELDS option). */
+   protected Map<DataModelObject, Property[]> included = null;
+
+   /** The excluded properties for each DMO (EXCEPT option). */
+   protected Map<DataModelObject, Property[]> excluded = null;
+   
    /** Flag indicating fatal error was encountered processing query substitution parameters */
    private boolean fatalError = false;
    
@@ -3173,8 +3182,18 @@
     */
    public void include(DataModelObject dmo, String ... fields)
    {
-      // UnimplementedFeature.missing("RECORD-PHRASE: FIELDS (...) option");
-      // TODO: implement
+      RecordBuffer buffer = (RecordBuffer) ((BufferReference) dmo).buffer();
+      if (buffer.isTemporary())
+      {
+         return;
+      }
+      
+      if (included == null)
+      {
+         included = new IdentityHashMap<>();
+      }
+      
+      collectFields(included, dmo, fields);
    }
    
    /**
@@ -3188,8 +3207,18 @@
     */
    public void exclude(DataModelObject dmo, String ... fields)
    {
-      // UnimplementedFeature.missing("RECORD-PHRASE: EXCLUDE (...) option");
-      // TODO: implement
+      RecordBuffer buffer = (RecordBuffer) ((BufferReference) dmo).buffer();
+      if (buffer.isTemporary())
+      {
+         return;
+      }
+      
+      if (excluded == null)
+      {
+         excluded = new IdentityHashMap<>();
+      }
+      
+      collectFields(excluded, dmo, fields);
    }
    
    /**
@@ -3429,4 +3458,32 @@
          return new logical(false);
       }
    }
+   
+   /**
+    * Populate the map of with the ORM properties specified in the given fields.
+    * 
+    * @param    target
+    *           The target map.
+    * @param    dmo
+    *           The DMO instance.
+    * @param    fields
+    *           The fields to resolve.
+    */
+   private void collectFields(Map<DataModelObject, Property[]> target, DataModelObject dmo, String[] fields)
+   {
+      RecordBuffer buffer = (RecordBuffer) ((BufferReference) dmo).buffer();
+      DmoMeta meta = buffer.getDmoInfo();
+
+      Property[] props = target.computeIfAbsent(dmo, (d) ->
+      {
+         return new Property[meta.getExistingFields().size()];
+      });
+      
+      for (int i = 0; i < fields.length; i++)
+      {
+         Property p = meta.getFieldInfo(fields[i]);
+         
+         props[p.id - 1] = p;
+      }
+   }
 }

=== modified file 'src/com/goldencode/p2j/persist/FastFindCache.java'
--- old/src/com/goldencode/p2j/persist/FastFindCache.java	2022-01-04 19:29:50 +0000
+++ new/src/com/goldencode/p2j/persist/FastFindCache.java	2022-11-09 18:35:42 +0000
@@ -14,6 +14,7 @@
 **                  negative result (i.e., no record is found) do not have the negative result cached.
 **     CA  20220104 Node.l3Cache must be a weak reference, otherwise once the direct cache invalidates a key,
 **                  the reverse lookup has no chance of being cleaned up, as this is not done explicitly.
+**     CA  20221109 Partial/Incomplete records (loaded with EXCEPT/FIELDS options) can't be cached.
 */
 
 /*
@@ -262,8 +263,10 @@
     */
    public void put(Key k, Record value)
    {
-      // if no key or record represents a housekeeping (e.g., dirty database) copy, don't store it
-      if (k == null || (value != null && value.checkState(DmoState.COPY)))
+      // if no key or record represents a housekeeping (e.g., dirty database) copy or an incomplete record 
+      // (partial fields are populated due to EXCEPT/FIELDS options), don't store it
+      if (k == null || 
+          (value != null && (value.checkState(DmoState.COPY) || value.checkState(DmoState.INCOMPLETE))))
       {
          return; // throw some error?
       }

=== modified file 'src/com/goldencode/p2j/persist/Persistence.java'
--- old/src/com/goldencode/p2j/persist/Persistence.java	2022-10-31 09:45:09 +0000
+++ new/src/com/goldencode/p2j/persist/Persistence.java	2022-11-09 18:36:45 +0000
@@ -577,6 +577,8 @@
 **                           allows a lazy registration of scopeables, to avoid the unnecessary overhead of
 **                           processing all the scopeables for each and every block.
 **     TJD 20220504          Upgrade do Java 11 minor changes
+**     CA  20221109          Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded 
+**                           as 'partial/incomplete'.
 */
 
 /*
@@ -634,9 +636,9 @@
 
 package com.goldencode.p2j.persist;
 
-import java.lang.*;
 import java.sql.*;
 import java.util.*;
+import java.util.BitSet;
 import java.util.Date;
 import java.util.function.*;
 import java.util.logging.*;
@@ -1756,7 +1758,7 @@
             // have to take another hit retrieving the data almost immediately after this method
             // returns, which is the common case for dynamic record retrieval.
             Class<? extends Record> dmoClass = buffer.getDMOImplementationClass();
-            initialResult = result = session.get(dmoClass, id);
+            initialResult = result = session.get(dmoClass, id, null);
             
             if (result == null)
             {
@@ -1978,6 +1980,53 @@
    throws PersistenceException,
           LockTimeoutException
    {
+      return load(implClass, id, lockType, timeout, updateLock, null);
+   }
+   
+   /**
+    * Load a single record from the session-level cache or, optionally, refresh it from the
+    * database. The record will be locked with the specified lock type upon return from this
+    * method. Note that the lock refers to a runtime-level lock, not a database-level lock.
+    *
+    * @param   implClass
+    *          The DMO implementation class of record being read.
+    * @param   id
+    *          Primary key of the record to be loaded into the buffer.  May not be {@code null}.
+    * @param   lockType
+    *          Type of lock the lock manager should register on the resulting record for the
+    *          current user context.
+    * @param   timeout
+    *          Number of milliseconds to wait to acquire the requested lock type. Throws
+    *          {@link LockTimeoutException} if this time elapses before the lock is acquired.
+    *          Specify 0 to wait indefinitely for the lock. Ignored if {@code lockType} is
+    *          {@code NONE}.
+    * @param   updateLock
+    *          {@code true} to update the current lock state;
+    *          {@code false} to leave the current lock state unchanged.
+    * @param   partialFields
+    *          For an incomplete record, read only these fields.
+    *
+    * @return  A single DMO instance loaded from the database, or {@code null} if no result was
+    *          found.
+    *
+    * @throws  LockTimeoutException
+    *          if a record was found, but its lock could not be acquired within the positive
+    *          {@code timeout} period specified.
+    * @throws  LockUnavailableException
+    *          if a record was found and a no-wait lock type was specified, but the lock could
+    *          not be acquired immediately.
+    * @throws  PersistenceException
+    *          if there was an error loading the DMO from the ORM session and/or database.
+    */
+   public Record load(Class<? extends Record> implClass,
+                      Long id,
+                      LockType lockType,
+                      long timeout,
+                      boolean updateLock,
+                      BitSet partialFields)
+   throws PersistenceException,
+          LockTimeoutException
+   {
       boolean trace = LOG.isLoggable(Level.FINEST);
       boolean warn = (trace || LOG.isLoggable(Level.WARNING));
       boolean trackSlow = warn && loadThreshold > 0L;
@@ -2025,7 +2074,7 @@
          }
          
          Session session = local.getSession();
-         result = session.get(implClass, id);
+         result = session.get(implClass, id, partialFields);
          
          if (result == null)
          {

=== modified file 'src/com/goldencode/p2j/persist/PreselectQuery.java'
--- old/src/com/goldencode/p2j/persist/PreselectQuery.java	2022-10-13 18:29:04 +0000
+++ new/src/com/goldencode/p2j/persist/PreselectQuery.java	2022-11-09 18:37:12 +0000
@@ -587,6 +587,8 @@
 **     CA  20221006          Added JMX instrumentation for query assemble. Refs #6814
 **     CA  20221013          In 'repositionByID', if the row number is after the last row in the ResultSet,
 **                           assume an error (query off-end) and do not try to fetch the record.
+**     CA  20221109          Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded 
+**                           as 'partial/incomplete'.   At this time, extent fields are always loaded.
 */
 
 /*
@@ -4439,9 +4441,10 @@
          
          boolean qualified = !isOmitAlias();
          
+         RecordBuffer buffer = comp.getBuffer();
+
          if (qualified)
          {
-            RecordBuffer buffer = comp.getBuffer();
             buf.append(buffer.getDMOAlias());
          }
          
@@ -4454,6 +4457,66 @@
             
             buf.append(DatabaseManager.PRIMARY_KEY);
          }
+         else 
+         {
+            Property[] iprops = included == null ? null : included.get(buffer.getDMOProxy());
+            Property[] xprops = excluded == null ? null : excluded.get(buffer.getDMOProxy());
+            if (iprops != null || xprops != null)
+            {
+               Property[] props = null;
+               
+               // only permanent buffers can use include/exclude
+               if (iprops == null)
+               {
+                  DmoMeta meta = buffer.getDmoInfo();
+                  
+                  props = new Property[meta.getExistingFields().size()];
+                  
+                  // include all fields
+                  Iterator<Property> piter = meta.getFields(false);
+                  while (piter.hasNext())
+                  {
+                     Property p = piter.next();
+                     
+                     if (p.propId > 0)
+                     {
+                        props[p.id - 1] = p;
+                     }
+                  }
+               }
+               else
+               {
+                  props = new Property[iprops.length];
+                  System.arraycopy(iprops, 0, props, 0, props.length);
+               }
+               
+               if (xprops != null)
+               {
+                  // exclude properties
+                  for (int j = 0; j < xprops.length; j++)
+                  {
+                     Property xprop = xprops[j];
+                     if (xprop != null)
+                     {
+                        props[j] = null;
+                     }
+                  }
+               }
+               
+               String alias = buffer.getDMOAlias();
+               buf.append('.').append(DatabaseManager.PRIMARY_KEY);
+               for (int j = 0; j < props.length; j++)
+               {
+                  Property p = props[j];
+                  if (p == null || p.extent != 0) // TODO: denormalized extents
+                  {
+                     continue;
+                  }
+                  
+                  buf.append(", ").append(alias).append(".").append(p.name);
+               }
+            }
+         }
       }
       
       buf.append(" ");
@@ -5802,7 +5865,8 @@
                   {
                      try
                      {
-                        dmo = p.load(buffer.getDMOImplementationClass(), id, lt, timeout, updateLock);
+                        // force the load of the entire record
+                        dmo = p.load(buffer.getDMOImplementationClass(), id, lt, timeout, updateLock, null);
                        
                         break;
                      }
@@ -5853,7 +5917,13 @@
                }
                else
                {
-                  dmo = p.load(buffer.getDMOImplementationClass(), id, lt, 0L, updateLock);
+                  dmo = p.load(buffer.getDMOImplementationClass(), 
+                               id, 
+                               lt, 
+                               0L, 
+                               updateLock, 
+                               // the entire record is loaded if the lock is not NONE
+                               updateLock || dmo == null ? null : dmo._getReadFields());
                }
             }
             

=== modified file 'src/com/goldencode/p2j/persist/Record.java'
--- old/src/com/goldencode/p2j/persist/Record.java	2022-10-31 09:45:09 +0000
+++ new/src/com/goldencode/p2j/persist/Record.java	2022-11-09 18:37:40 +0000
@@ -22,6 +22,8 @@
 **     OM  20220713 Added support for converting to/from POJOs.
 **     OM  20220718 Fixed looping in getPropertyValues(). Added to the returned list the denormalized props.
 **     TJD 20220504 Upgrade do Java 11 minor changes
+**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
+**                  'partial/incomplete'.
 */
 
 /*
@@ -143,6 +145,8 @@
     */
    public final integer _getInteger(int offset)
    {
+      checkIncomplete(offset);
+
       Integer datum = (Integer) data[offset];
       
       return datum != null ? new integer(datum) : new integer();
@@ -161,6 +165,8 @@
     */
    protected final void _setInteger(int offset, NumberType w)
    {
+      checkIncomplete(offset);
+
       Integer datum = null;
       if (w != null)
       {
@@ -250,6 +256,8 @@
     */
    public final datetimetz _getDatetimetz(int offset)
    {
+      checkIncomplete(offset);
+
       OffsetDateTime datum = (OffsetDateTime) data[offset];
       
       return datum != null ? new datetimetz(datum) : new datetimetz();
@@ -354,6 +362,8 @@
     */
    public final datetime _getDatetime(int offset)
    {
+      checkIncomplete(offset);
+
       Timestamp datum = (Timestamp) data[offset];
       
       return datum != null ? new datetime(datum) : new datetime();
@@ -457,6 +467,8 @@
     */
    public final date _getDate(int offset)
    {
+      checkIncomplete(offset);
+
       Date datum = (Date) data[offset];
       
       return datum != null ? new date(datum) : new date();
@@ -560,6 +572,8 @@
     */
    public final logical _isLogical(int offset)
    {
+      checkIncomplete(offset);
+
       Boolean datum = (Boolean) data[offset];
       
       return datum != null ? new logical(datum) : new logical();
@@ -770,6 +784,8 @@
     */
    public final decimal _getDecimal(int offset)
    {
+      checkIncomplete(offset);
+
       BigDecimal datum = (BigDecimal) data[offset];
       
       return datum != null ? new decimal(datum) : new decimal();
@@ -886,6 +902,8 @@
     */
    public final character _getCharacter(int offset)
    {
+      checkIncomplete(offset);
+
       String datum = (String) data[offset];
       
       if (datum != null)
@@ -996,6 +1014,8 @@
     */
    public final rowid _getRowid(int offset)
    {
+      checkIncomplete(offset);
+
       Long datum = (Long) data[offset];
       
       return datum != null ? new rowid(datum) : new rowid();
@@ -1101,6 +1121,8 @@
     */
    public final handle _getHandle(int offset)
    {
+      checkIncomplete(offset);
+
       Long datum = (Long) data[offset];
       
       return datum != null ? handle.fromResourceId(datum, false) : new handle();
@@ -1206,6 +1228,8 @@
     */
    public final recid _getRecid(int offset)
    {
+      checkIncomplete(offset);
+
       Integer datum = (Integer) data[offset];
       
       return datum != null ? new recid(datum) : new recid();
@@ -1310,6 +1334,8 @@
     */
    public final blob _getBlob(int offset)
    {
+      checkIncomplete(offset);
+
       byte[] datum = (byte[]) data[offset];
       
       return datum != null ? new blob(datum) : new blob();
@@ -1415,6 +1441,8 @@
     */
    public final clob _getClob(int offset)
    {
+      checkIncomplete(offset);
+
       return new clob((String) data[offset], getCodePage(offset));
    }
    
@@ -1516,6 +1544,8 @@
     */
    public final comhandle _getComhandle(int offset)
    {
+      checkIncomplete(offset);
+
       String datum = (String) data[offset];
       
       return datum != null ? comhandle.fromString(datum) : new comhandle();
@@ -1619,6 +1649,8 @@
     */
    public final object<?> _getObject(int offset)
    {
+      checkIncomplete(offset);
+
       Long datum = (Long) data[offset];
       
       return datum != null ? object.fromResourceId(datum) : new object<>();
@@ -1726,6 +1758,8 @@
     */
    public final raw _getRaw(int offset)
    {
+      checkIncomplete(offset);
+
       byte[] datum = (byte[]) data[offset];
       
       return datum != null ? new raw(datum) : raw.instantiateUnknownRaw();
@@ -1893,7 +1927,10 @@
             {
                sb.append("\"");
             }
-            sb.append(data[i] == null ? "?" : data[i]);
+            
+            boolean incomplete = readFields != null && !readFields.get(i);
+            
+            sb.append(data[i] == null ? incomplete ? "n/a" : "?" : data[i]);
             if (isChar)
             {
                sb.append("\"");

=== modified file 'src/com/goldencode/p2j/persist/orm/BaseRecord.java'
--- old/src/com/goldencode/p2j/persist/orm/BaseRecord.java	2022-10-31 09:45:09 +0000
+++ new/src/com/goldencode/p2j/persist/orm/BaseRecord.java	2022-11-09 18:38:23 +0000
@@ -28,6 +28,8 @@
 **                  used in a Java REST service, which must not be associated with a session.
 **     ECF 20221005 Added support for dynamic initial values.
 **     TJD 20220504 Upgrade do Java 11 minor changes
+**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
+**                  'partial/incomplete'.
 */
 
 /*
@@ -128,6 +130,9 @@
    /** 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;
+   
    /** Current state of the record */
    protected final RecordState state = new RecordState();
    
@@ -785,6 +790,16 @@
    }
    
    /**
+    * Get the {@link #readFields}.
+    * 
+    * @return   The {@link #readFields}.
+    */
+   public BitSet _getReadFields()
+   {
+      return readFields;
+   }
+
+   /**
     * Set a single datum into the data array, and update the record and property state accordingly.
     * 
     * @param   offset
@@ -794,6 +809,11 @@
     */
    protected void setDatum(int offset, Object datum)
    {
+      if (readFields != null)
+      {
+         readFields.set(offset);
+      }
+
       if (activeBuffer != null)
       {
          activeOffset = offset;
@@ -1015,6 +1035,38 @@
    }
    
    /**
+    * Check if for an incomplete record, the specified field was read or assigned.
+    * 
+    * @param    index
+    *           The field index.
+    */
+   protected void checkIncomplete(int index)
+   {
+      if (checkState(DmoState.INCOMPLETE) && !readFields.get(index))
+      {
+         RecordMeta meta = _recordMeta();
+         String tname = meta.legacyName;
+         String fname = meta.getPropertyMeta(false)[index].getLegacyName();
+         String msg = "Field " + fname + " from " + tname + " record (recid " + primaryKey() + 
+                       ") was missing from FIELDS phrase";
+         
+         ErrorManager.recordOrThrowError(8826, msg, false);
+      }
+   }
+
+   /**
+    * Mark this record as incomplete.
+    * 
+    * @param    session
+    *           Database session (used for UNDO purposes).
+    */
+   void markIncomplete(Session session)
+   {
+      updateState(session, DmoState.INCOMPLETE, true);
+      readFields = new BitSet(data.length);
+   }
+
+   /**
     * An index was updated.
     * 
     * @param   idxUid
@@ -1225,11 +1277,41 @@
    final int readProperty(int propOffset, ResultSet rs, int rsOffset)
    throws PersistenceException
    {
+      return readProperty(propOffset, rs, rsOffset, false);
+   }
+   
+   /**
+    * Read a property directly from a {@code ResultSet}.
+    *
+    * @param   propOffset
+    *          The index of the property to be read.
+    * @param   rs
+    *          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}.
+    * @param   skip
+    *          Flag indicating if this property will not be read from the result-set.
+    *          
+    * @return  The number of columns actually read from the {@code ResultSet}.
+    */
+   final int readProperty(int propOffset, ResultSet rs, int rsOffset, boolean skip)
+   throws PersistenceException
+   {
       PropertyMeta pm = _recordMeta().getPropertyMeta(false)[propOffset];
       PropertyReader propertyReader = pm.getDataHandler();
+      if (skip)
+      {
+         return propertyReader.propertySize();
+      }
       
       try
       {
+         if (readFields != null)
+         {
+            readFields.set(propOffset);
+         }
+         
          return propertyReader.readProperty(rs, rsOffset, data, propOffset);
       }
       catch (java.sql.SQLException sqle)

=== modified file 'src/com/goldencode/p2j/persist/orm/DmoState.java'
--- old/src/com/goldencode/p2j/persist/orm/DmoState.java	2021-09-25 06:43:09 +0000
+++ new/src/com/goldencode/p2j/persist/orm/DmoState.java	2022-11-09 18:38:44 +0000
@@ -2,12 +2,14 @@
 ** Module   : DmoState.java
 ** Abstract : DMO states which govern persistence behavior.
 **
-** Copyright (c) 2020-2021, Golden Code Development Corporation.
+** Copyright (c) 2020-2022, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
 ** 001 ECF 20200315 First revision.
 ** 002 ECF 20210519 Added TRACKED state.
 **     ECF 20210923 Added DELETING state.
+**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
+**                  'partial/incomplete'.
 */
 
 /*
@@ -110,4 +112,7 @@
    
    /** DMO currently is being tracked for possible UNDO processing */
    public static final int TRACKED = 0x0400;
+   
+   /** DMO is read incomplete (due to EXCEPT/FIELDS options) and can't be cached. */
+   public static final int INCOMPLETE = 0x0800;
 }

=== modified file 'src/com/goldencode/p2j/persist/orm/Loader.java'
--- old/src/com/goldencode/p2j/persist/orm/Loader.java	2022-10-28 10:54:33 +0000
+++ new/src/com/goldencode/p2j/persist/orm/Loader.java	2022-11-09 18:39:09 +0000
@@ -11,6 +11,8 @@
 **                  (Temporary?) set the properties to be accessed with old-fashioned accessors.
 **     CA  20201005 Fixed the sort order when loading a child extent table.
 **     TJD 20220504 Upgrade do Java 11 minor changes
+**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
+**                  'partial/incomplete'.  Extent fields are always loaded at this time.
 */
 
 /*
@@ -323,7 +325,7 @@
     *          DMO implementation class.
     * @param   id
     *          Surrogate primary key.
-    * 
+    *          
     * @return  An instance of {@code dmoClass}, loaded with data from the database, or {@code
     *          null} if the record was not found. The DMO is instantiated either way, but it will
     *          be discarded if the record is not found.
@@ -334,12 +336,40 @@
    <T extends BaseRecord> T load(Class<T> dmoClass, Long id)
    throws PersistenceException
    {
+      return load(dmoClass, id, null);
+   }
+   
+   /**
+    * Load a single record from the database, given its DMO class and primary key. The primary
+    * key is assumed to represent a valid identifier; if not, {@code null} will be returned.
+    * <p>
+    * TODO: a SQL statement is prepared and closed for each load statement executed by this
+    * method. We rely on statement caching at the connection pool level to make this performant,
+    * but perhaps there is some predictive analysis we can do to make this more efficient.
+    * 
+    * @param   dmoClass
+    *          DMO implementation class.
+    * @param   id
+    *          Surrogate primary key.
+    * @param   partialFields
+    *          For an incomplete record, read only these fields.
+    *          
+    * @return  An instance of {@code dmoClass}, loaded with data from the database, or {@code
+    *          null} if the record was not found. The DMO is instantiated either way, but it will
+    *          be discarded if the record is not found.
+    * 
+    * @throws  PersistenceException
+    *          if an error occurred accessing the database.
+    */
+   <T extends BaseRecord> T load(Class<T> dmoClass, Long id, BitSet partialFields)
+   throws PersistenceException
+   {
       try
       {
          T dmo = dmoClass.getDeclaredConstructor().newInstance();
          dmo.primaryKey(id);
          
-         return load(dmo, id);
+         return load(dmo, id, partialFields);
       }
       catch (ReflectiveOperationException  exc)
       {
@@ -360,6 +390,8 @@
     *          DMO implementation class.
     * @param   id
     *          Surrogate primary key.
+    * @param   partialFields
+    *          For an incomplete record, read only these fields.
     * 
     * @return  An instance of {@code dmoClass}, loaded with data from the database, or {@code
     *          null} if the record was not found. The DMO is instantiated either way, but it will
@@ -368,7 +400,7 @@
     * @throws  PersistenceException
     *          if an error occurred accessing the database.
     */
-   <T extends BaseRecord> T load(T dmo, Long id)
+   <T extends BaseRecord> T load(T dmo, Long id, BitSet partialFields)
    throws PersistenceException
    {
       try
@@ -380,6 +412,10 @@
          boolean hasScalarData = props.length > 0 && props[0].getExtent() == 0;
          
          int start = 0;
+         if (partialFields != null)
+         {
+            dmo.markIncomplete(session);
+         }
          
          for (String sql : loadSql)
          {
@@ -404,7 +440,7 @@
                   }
                   
                   // the first query is always on the primary table, if the DMO has scalar data
-                  start = readScalarData(dmo, rs, 0, props);
+                  start = readScalarData(dmo, rs, 0, props, partialFields);
                }
                else
                {
@@ -448,6 +484,8 @@
     *          use 0. If the record data is prepended by a single column (Ex: primaryKey), pass 1.
     * @param   props
     *          Array of property metadata objects.
+    * @param   partialFields
+    *          For an incomplete record, read only these fields.
     * 
     * @return  Index of next slot in the data array to be filled (if any) after this series of
     *          scalar data has been read, or -1 to indicate the result set was empty (record
@@ -459,7 +497,8 @@
    private static int readScalarData(BaseRecord r,
                                      ResultSet rs,
                                      int rowOffset,
-                                     PropertyMeta[] props)
+                                     PropertyMeta[] props,
+                                     BitSet partialFields)
    throws SQLException, PersistenceException
    {
       // the current row is expected to be already selected, since the filter is the primary
@@ -480,7 +519,8 @@
             break;
          }
          
-         int colCnt = r.readProperty(pm.offset, rs, pm.columnIndex + rowOffset);
+         boolean skip = partialFields != null && !partialFields.get(pm.offset);
+         int colCnt = r.readProperty(pm.offset, rs, pm.columnIndex + rowOffset, skip);
          
          if (colCnt != pm.columnCount)
          {

=== modified file 'src/com/goldencode/p2j/persist/orm/PropertyReader.java'
--- old/src/com/goldencode/p2j/persist/orm/PropertyReader.java	2020-02-12 20:17:12 +0000
+++ new/src/com/goldencode/p2j/persist/orm/PropertyReader.java	2022-11-09 18:39:36 +0000
@@ -3,10 +3,12 @@
 ** Abstract : Functional interface for reading a (possibly multiple column) property from a SQL
 **            result set.
 **
-** Copyright (c) 2020, Golden Code Development Corporation.
+** Copyright (c) 2020-2022, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------Description---------------------------------
 ** 001 OM  20200212 Created initial version.
+** 002 CA  20221109 Added 'propertySize' method which returns the number of columns in the result-set for this
+**                  property type.
 */
 
 /*
@@ -93,4 +95,14 @@
     */
    public int readProperty(ResultSet rs, int rsOffset, Object[] data, int propIndex)
    throws SQLException;
+   
+   /**
+    * Get the size (number of columns in the result-set) required by this property.
+    * 
+    * @return   Defaults to 1.
+    */
+   public default int propertySize()
+   {
+      return 1;
+   }
 }

=== modified file 'src/com/goldencode/p2j/persist/orm/RecordState.java'
--- old/src/com/goldencode/p2j/persist/orm/RecordState.java	2021-09-25 06:43:09 +0000
+++ new/src/com/goldencode/p2j/persist/orm/RecordState.java	2022-11-09 18:39:43 +0000
@@ -2,12 +2,14 @@
 ** Module   : RecordState.java
 ** Abstract : Current state of a database record in an application.
 **
-** Copyright (c) 2019-2021, Golden Code Development Corporation.
+** Copyright (c) 2019-2022, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
 ** 001 ECF 20191001 First revision. Object which tracks DMO state as changes are applied to it.
 ** 002 ECF 20210519 Added TRACKED state to toString.
 **     ECF 20210924 Added DELETING state to toString.
+**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
+**                  'partial/incomplete'.
 */
 
 /*
@@ -144,7 +146,11 @@
          }
          if ((state & DmoState.TRACKED) == DmoState.TRACKED)
          {
-            sb.append("TRACKED");
+            sb.append("TRACKED ");
+         }
+         if ((state & DmoState.INCOMPLETE) == DmoState.INCOMPLETE)
+         {
+            sb.append("INCOMPLETE");
          }
       }
       sb.append("}");

=== modified file 'src/com/goldencode/p2j/persist/orm/SQLQuery.java'
--- old/src/com/goldencode/p2j/persist/orm/SQLQuery.java	2022-10-28 10:54:33 +0000
+++ new/src/com/goldencode/p2j/persist/orm/SQLQuery.java	2022-11-09 18:40:44 +0000
@@ -33,6 +33,8 @@
 **                  a wrapper for two separate parameters, changed hydrateRecordImpl logic to skip fields that are not
 **                  queried.
 **     TJD 20220504 Upgrade do Java 11 minor changes
+**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
+**                  'partial/incomplete'.  Extent fields are always loaded at this time.
 */
 
 /*
@@ -755,7 +757,7 @@
       try
       {
          SIMPLE_QUERY_PROFILER.updateCacheMisses(resultSet, 1);
-         r = recordClass.getDeclaredConstructor().newInstance();
+         r = recordClass.newInstance();
          
          // see FqlToSqlConverter.expandAlias()
          Set<Map.Entry<String, Property>> allFieldEntries = dmoInfo.propsByName.entrySet();
@@ -771,7 +773,13 @@
          }
          int k = 0;
          int recOffset = 0;
-         boolean hasExtents = false;
+         // TODO: EXCEPT/FIELDS for extent fields; for now, load all extents
+         boolean incomplete = fieldEntries != allFieldEntries;
+         boolean hasExtents = incomplete; 
+         if (incomplete)
+         {
+            r.markIncomplete(session);
+         }
          
          for (Map.Entry<String, Property> prop : fieldEntries)
          {

=== modified file 'src/com/goldencode/p2j/persist/orm/Session.java'
--- old/src/com/goldencode/p2j/persist/orm/Session.java	2022-06-28 22:25:22 +0000
+++ new/src/com/goldencode/p2j/persist/orm/Session.java	2022-11-09 18:40:30 +0000
@@ -25,6 +25,10 @@
 **     ECF 20211022 A cache expiration is not tracked as an undoable event.
 **     ECF 20211230 Do not attempt to delete a newly created record that was never flushed to the database.
 **     OM  20220628 Signature change for createSQLQuery() method.
+**     CA  20221109 Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded as 
+**                  'partial/incomplete'.  Partial/Incomplete records are being cached, but when they try to
+**                  be loaded with no 'partial field information', the cached record is fully refreshed from
+**                  the database, and not just returned.
 */
 
 /*
@@ -450,7 +454,33 @@
    public <T extends BaseRecord> T get(Class<T> dmoImplClass, Long id)
    throws PersistenceException
    {
-      return getImpl(dmoImplClass, id, new RecordIdentifier<>(dmoImplClass.getName(), id));
+      return getImpl(dmoImplClass, id, new RecordIdentifier<>(dmoImplClass.getName(), id), null);
+   }
+
+   /**
+    * Gets a specified record of a table. To lookup the record local cache is analyzed first. If
+    * the record is not found in cache, the backing database is queried. If the specified record
+    * is not found in database, {@code null} is returned. If the record was fetched from the
+    * database at this moment, it is kept in cache for further access.
+    *
+    * @param   <T>
+    *          The type of the record.
+    * @param   dmoImplClass
+    *          The class of the entity (class that implements the table's DMO interface).
+    * @param   id
+    *          The id/rowid of the record to be returned.
+    * @param   partialFields
+    *          For an incomplete record, read only these fields.
+    *
+    * @return  The requested record, if found in cache or database and {@code null} otherwise.
+    *
+    * @throws  PersistenceException
+    *          In case some error occurs in the process.
+    */
+   public <T extends BaseRecord> T get(Class<T> dmoImplClass, Long id, BitSet partialFields)
+   throws PersistenceException
+   {
+      return getImpl(dmoImplClass, id, new RecordIdentifier<>(dmoImplClass.getName(), id), partialFields);
    }
    
    /**
@@ -473,7 +503,7 @@
    public <T extends BaseRecord> T get(RecordIdentifier<String> key)
    throws PersistenceException
    {
-      return getImpl(null, key.getRecordID(), key);
+      return getImpl(null, key.getRecordID(), key, null);
    }
    
    /**
@@ -497,7 +527,7 @@
    public <T extends BaseRecord> T get(String entity, Long id)
    throws PersistenceException
    {
-      return getImpl(null, id, new RecordIdentifier<>(entity, id));
+      return getImpl(null, id, new RecordIdentifier<>(entity, id), null);
    }
    
    /**
@@ -513,6 +543,8 @@
     * @param   key
     *          The cache key of the entity. If {@code null} the returned value is {@code null};
     *          provided.
+    * @param   partialFields
+    *          For an incomplete record, read only these fields.
     * @param   <T>
     *          The compile type of the record to be returned. 
     *
@@ -522,7 +554,10 @@
     * @throws  PersistenceException
     *          if there is an error getting a JDBC connection from the data source.
     */
-   private <T extends BaseRecord> T getImpl(Class<T> dmoImplClass, Long id, RecordIdentifier<String> key)
+   private <T extends BaseRecord> T getImpl(Class<T> dmoImplClass, 
+                                            Long id, 
+                                            RecordIdentifier<String> key, 
+                                            BitSet partialFields)
    throws PersistenceException
    {
       // quick out
@@ -544,6 +579,7 @@
       
       T dmo = getCached(key);
       
+      boolean refreshIncomplete = false;
       if (dmo != null)
       {
          // assert that we have the right DMO type for this primary key
@@ -557,7 +593,16 @@
             throw new PersistenceException(msg);
          }
          
-         return dmo;
+         if (dmo.checkState(DmoState.INCOMPLETE) && partialFields == null)
+         {
+            // we have a cached record which is incomplete, and this requires to refresh the incomplete record
+            // from the database
+            refreshIncomplete = true;
+         }
+         else
+         {
+            return dmo;
+         }
       }
       
       if (dmoImplClass == null)
@@ -579,7 +624,29 @@
          throw new PersistenceException("Invalid entity name: " + dmoImplClass.getName());
       }
       
-      dmo = loader.load(dmoImplClass, id);
+      T loaded = loader.load(dmoImplClass, id, partialFields);
+      if (refreshIncomplete)
+      {
+         // evict the incomplete record from the cache, will be recached if one was found
+         evict(dmo);
+         
+         if (loaded == null)
+         {
+            dmo = null;
+         }
+         else
+         {
+            System.arraycopy(loaded.data, 0, dmo.data, 0, dmo.data.length);
+
+            // set the incomplete record as complete
+            dmo.updateState(this, DmoState.INCOMPLETE, false);
+            dmo.readFields = null;
+         }
+      }
+      else
+      {
+         dmo = loaded;
+      }
       
       if (dmo != null)
       {
@@ -962,7 +1029,7 @@
       // ignore the cache and go directly to underlying database
       // use the variant of load which overwrites the DMOs current field values with data
       // fetched from the database
-      T loaded = loader.load(dmo, dmo.id);
+      T loaded = loader.load(dmo, dmo.id, null);
       
       RecordIdentifier<String> key = new RecordIdentifier<>(dmo.getClass().getName(), dmo.id);
       

=== modified file 'src/com/goldencode/p2j/persist/orm/types/DatetimetzType.java'
--- old/src/com/goldencode/p2j/persist/orm/types/DatetimetzType.java	2022-10-08 21:58:19 +0000
+++ new/src/com/goldencode/p2j/persist/orm/types/DatetimetzType.java	2022-11-09 18:41:02 +0000
@@ -13,6 +13,8 @@
 **     CA  20210304 Fixed datetimetz literal parsing.
 **     OM  20220218 Fixed timezone when the dtz is rehydrated.
 **     ECF 20221005 Added support for dynamic initial values.
+**     CA  20221109 Added 'propertySize' method which returns the number of columns in the result-set for this
+**                  property type.
 */
 
 /*
@@ -180,6 +182,17 @@
    }
    
    /**
+    * Get the number of columns read from the result-set.
+    * 
+    * @return   always 2.
+    */
+   @Override
+   public int propertySize()
+   {
+      return 2;
+   }
+   
+   /**
     * Instantiate a low-level initial value suitable for storage in a {@link BaseRecord}'s data
     * array, parsed from a string representation.
     * 

