=== modified file 'src/com/goldencode/p2j/persist/RecordBuffer.java'
--- src/com/goldencode/p2j/persist/RecordBuffer.java	2021-01-29 00:53:41 +0000
+++ src/com/goldencode/p2j/persist/RecordBuffer.java	2021-02-18 17:51:32 +0000
@@ -1119,6 +1119,8 @@
 **     OM  20201218          Fixed implementation for error and rejected attributes/hidden fields.
 **     OM  20210127          Replaced TableMapper.getLegacyName() triple map lookup with direct access to
 **                           local dmoMeta.legacyTable.
+**     ECF 20210117          Do not update DMO state when explicitly validating. Attempt to flush a transient
+**                           record if still in the buffer when its scope ends.
 */
 
 /*
@@ -3191,7 +3193,7 @@
             {
                // in batch assign mode, standard validation happens before assign triggers are fired
                Record dmo = buffer.getCurrentRecord();
-               buffer.validateMaybeFlush(dmo, false);
+               buffer.validateMaybeFlush(dmo, false, false);
                
                // fire assign triggers, if any
                TriggerTracker tracker = buffer.getTriggerTracker();
@@ -6717,7 +6719,7 @@
          // validate and persist the record
          if (currentRecord.checkState(DmoState.NEW) || currentRecord.checkState(DmoState.CHANGED))
          {
-            validateMaybeFlush(currentRecord, true);
+            validateMaybeFlush(currentRecord, true, false);
          }
          else if (currentRecord.checkState(DmoState.INVALID))
          {
@@ -10039,24 +10041,15 @@
             if (currentRecord.checkState(DmoState.INVALID))
             {
                // release the broken record as if undone
+               // TODO: is this still correct, now that we no longer update the DMO state to INVALID on a
+               // failed, explicit validation?
                setCurrentRecord(null, true, false, false, false);
             }
             else
             {
-               try
-               {
-                  flush();
-               }
-               catch (ValidationException exc)
-               {
-                  // if the current referenced record does not pass validation,
-                  // it means it was never flushed when it was created in the 
-                  // first place. so, before allowing the validation exception
-                  // to be processed, we must release this not-valid record
-                  setCurrentRecord(null, true, false, false, false);
-                  
-                  throw exc;
-               }
+               // flush the transient record currently in the buffer, which will attempt validation;
+               // the create will be aborted if this record is invalid
+               flush(true);
             }
          }
          
@@ -10294,7 +10287,7 @@
    throws ValidationException,
           PersistenceException
    {
-      validateMaybeFlush(dmo, false);
+      validateMaybeFlush(dmo, false, true);
    }
    
    /**
@@ -10821,6 +10814,9 @@
     *          Record whose data is to be validated and optionally flushed to the database.
     * @param   flush
     *          {@code true} to validate and persist DMO's data; {@code false} to validate only.
+    * @param   explicit
+    *          {@code true} if this validation was explicitly requested by business logic; {@code false} if
+    *          it is implicit/organic (e.g., in response to a record being updated).
     *
     * @return  The changed property, or {@code null} if there is no current record or if we are in
     *          batch mode.
@@ -10832,7 +10828,7 @@
     *          if there was a validation error. This represents an application level data
     *          validation failure and is recoverable.
     */
-   private Object[] validateMaybeFlush(Record dmo, boolean flush)
+   private Object[] validateMaybeFlush(Record dmo, boolean flush, boolean explicit)
    throws PersistenceException,
           ValidationException
    {
@@ -10851,7 +10847,8 @@
                                              persistenceContext.getNursery(),
                                              uniqueTracker,
                                              uniqueTrackerCtx,
-                                             flush);
+                                             flush,
+                                             !explicit);
       try
       {
          diffs = validation.validateMaybeFlush();
@@ -11175,10 +11172,19 @@
          {
             activeScopeDepth = -1;
             
-            // Mark current record unavailable; however, a no-undo buffer appears to retain its
-            // current record and continue reporting errors with an invalid record beyond the
-            // buffer's scope
-            setCurrentRecord(null, false, false, false, false);
+            // attempt to flush a transient record in the buffer at the time its block scope ends; this will
+            // trigger validation and may report an error
+            if (isTransient())
+            {
+               try
+               {
+                  flush(true);
+               }
+               catch (ValidationException exc)
+               {
+                  reportValidationException(exc, this, false);
+               }
+            }
             
             // The snapshot is no longer available
             snapshot = null;
@@ -12297,7 +12303,7 @@
                {
                   try
                   {
-                     diffs = validateMaybeFlush(currentRecord, false);
+                     diffs = validateMaybeFlush(currentRecord, false, false);
                   }
                   catch (ValidationException exc)
                   {
@@ -13445,15 +13451,16 @@
    }
    
    /**
-    * Set table statistics record
-    * @param tableStats
-    *        table statistics record
+    * Set table statistics record.
+    * 
+    * @param   tableStats
+    *          Table statistics record
     */
    private void setTableStats(TableStats tableStats)
    {
       this.tableStats = tableStats;
    }
-
+   
    /**
     * Getter for the {@link #propsBySetter} map.
     * 
@@ -13463,7 +13470,7 @@
    {
       return propsBySetter;
    }
-
+   
    /**
     * Getter for the {@link #propsByGetter} map.
     * 
@@ -13473,7 +13480,7 @@
    {
       return propsByGetter;
    }
-
+   
    /**
     * Getter for the {@link #setterDatums} map.
     * 
@@ -13483,7 +13490,7 @@
    {
       return setterDatums;
    }
-
+   
    /**
     * Getter for the {@link #getterDatums} map.
     * 
@@ -13493,7 +13500,7 @@
    {
       return getterDatums;
    }
-
+   
    /**
     * Information needed by a getter or setter method to affect the datum of one property (or one
     * element within an extent property), namely, the property's name and optional extent index,

=== modified file 'src/com/goldencode/p2j/persist/orm/RecordMeta.java'
--- src/com/goldencode/p2j/persist/orm/RecordMeta.java	2021-02-04 18:09:36 +0000
+++ src/com/goldencode/p2j/persist/orm/RecordMeta.java	2021-02-04 20:37:19 +0000
@@ -2,7 +2,7 @@
 ** Module   : RecordMeta.java
 ** Abstract : Information required to perform CRUD operations on a specific DMO type.
 **
-** Copyright (c) 2019-2020, Golden Code Development Corporation.
+** Copyright (c) 2019-2021, Golden Code Development Corporation.
 **
 ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
 ** 001 ECF 20191015 First revision. Information required to perform basic CRUD operations on a
@@ -13,6 +13,7 @@
 **     OM  20201001 Improved DMO manipulation performance by caching slow Property annotation access.
 **     OM  20201002 Use DmoMeta cached information instead of map lookups.
 **     OM  20201007 Replaced inlined column name with TemporaryBuffer.MULTIPLEX_FIELD_NAME.
+**     ECF 20210204 Removed word index unsupported log message.
 */
 
 /*
@@ -710,17 +711,7 @@
       {
          if (next.word())
          {
-            // TODO: handle word index
-            
-//            if (log.isLoggable(Level.WARNING))
-//            {
-//               String msg = "Word indices are not supported in this version of FWD: " +
-//                            dmoIface.getCanonicalName() +
-//                            ": " +
-//                            next.name();
-//               log.log(Level.WARNING, msg);
-//            }
-            
+            // TODO: handle word index; how do word indices affect validation?
             continue;
          }
          

=== modified file 'src/com/goldencode/p2j/persist/orm/Validation.java'
--- src/com/goldencode/p2j/persist/orm/Validation.java	2021-01-26 21:32:14 +0000
+++ src/com/goldencode/p2j/persist/orm/Validation.java	2021-02-18 17:46:06 +0000
@@ -16,6 +16,7 @@
 **     AIL 20201020 Closed unclosed prepared statement.
 **     ECF 20201201 Fixed FFC invalidation for flush update case when record was validated previously.
 **         20210126 Ensure all unvalidated unique indices are validated at flush time.
+**     ECF 20210117 Do not update DMO state when explicitly validating.
 */
 
 /*
@@ -118,6 +119,9 @@
    /** Whether to flush the record; {@code false} to validate only */
    private final boolean flush;
    
+   /** Whether to update the DMO state as a side effect of validation */
+   private final boolean updateState;
+   
    /** Whether we are validating the most recently updated DMO property only, or the entire DMO */
    private final boolean singleProp;
    
@@ -165,6 +169,9 @@
     *          Context-local API helper for unique constraint tracking.
     * @param   flush
     *          {@code true} to validate and persist DMO's data; {@code false} to validate only.
+    * @param   updateState
+    *          Update the DMO's index and validation state as a side effect of validation. This should be set
+    *          to {@code true} for implicit/organic validation; {@code false} for explicit validation.
     */
    public Validation(RecordBuffer buffer,
                      BaseRecord dmo,
@@ -173,7 +180,8 @@
                      RecordNursery nursery,
                      UniqueTracker uniqueTracker,
                      UniqueTracker.Context uniqueTrackerCtx,
-                     boolean flush)
+                     boolean flush,
+                     boolean updateState)
    {
       this.buffer = buffer;
       this.dmo = dmo;
@@ -183,6 +191,7 @@
       this.uniqueTracker = uniqueTracker;
       this.uniqueTrackerCtx = uniqueTrackerCtx;
       this.flush = flush;
+      this.updateState = updateState;
       this.singleProp = dmo.getActiveOffset() >= 0;
    }
    
@@ -300,8 +309,9 @@
          // before the unique constraint checks, which will clear the changed property state
          Object[] diff = singleProp ? dmo.getActiveUpdateDiffs() : null;
          
-         // validate fully updated unique indices, if any
-         BitSet valUnique = dmo.getUnvalidatedIndices(offset, flush);
+         // validate fully updated unique indices, if any, or all indices if flushing or not updating index
+         // state
+         BitSet valUnique = dmo.getUnvalidatedIndices(offset, flush || !updateState);
          if (!valUnique.isEmpty())
          {
             // validate unique indices (and flush, if flush was requested)
@@ -318,7 +328,7 @@
          // update the nursery if we did not already flush and:
          // * the table has no remaining index needing update; or
          // * any indices for a new record were updated
-         if (isNew && !flushed)
+         if (isNew && (!flushed && updateState))
          {
             if (dmo.getIndexState().isEmpty())
             {
@@ -340,7 +350,7 @@
             }
          }
          
-         if (!flushed)
+         if (!flushed && updateState)
          {
             if (singleProp)
             {
@@ -430,8 +440,11 @@
             uniqueTrackerCtx.rollbackChange(uniqueTracker, token);
          }
          
-         // mark record as having failed validation; it will stay this way until it passes
-         dmo.updateState(session, INVALID, true);
+         if (updateState)
+         {
+            // mark record as having failed validation; it will stay this way until it passes
+            dmo.updateState(session, INVALID, true);
+         }
          
          throw exc;
       }
@@ -785,7 +798,10 @@
       
       String msg = bufferName + "." + fi.legacy + " is mandatory, but has unknown (?) value";
       
-      dmo.updateState(buffer.getSession(false), INVALID, true);
+      if (updateState)
+      {
+         dmo.updateState(buffer.getSession(false), INVALID, true);
+      }
       
       throw new ValidationException(msg, 110);
    }

