RecordBuffer.java

/*
** Module   : RecordBuffer.java
** Abstract : Encapsulates a data record and provides methods to operate on it
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20050816   @22479 Created initial version. Abstract base class
**                           of all DMO classes which provides integration
**                           with stateful, navigable queries.
** 002 ECF 20050916   @22934 Complete rewrite to use dynamic proxies. The
**                           buffer is now a container for a DMO reference
**                           which may change during the course of query
**                           execution. The buffer reference is registered
**                           with the transaction manager, possibly more
**                           than once, by client code.
** 003 ECF 20051028   @23244 First cut at full transaction support. This
**                           includes undo processing, transaction
**                           commit/rollback, automatic lock upgrade,
**                           downgrade, release, and record release. Added
**                           logging as well.
** 004 ECF 20051106   @23375 Added real-time update validation. Introduced
**                           use of DMOValidator class. Also made various
**                           improvements to transaction support and
**                           adjustments to accommodate use with compound
**                           and preselect queries.
** 005 ECF 20051129   @23566 Added support for batch assign mode. DMO
**                           updates must be bracketed by static method
**                           calls to startBatch and endBatch.
** 006 ECF 20051205   @23669 Integrated with ConnectionManager. Uses CM
**                           services at construction to determine logical
**                           and physical database names. Notifies CM of
**                           buffer scope open/close events.
** 007 ECF 20051216   @23748 Implemented various Progress language
**                           statement and built-in function replacements:
**                           copy, compare, ambiguous, new, and locked.
** 008 ECF 20060104   @23830 Added support for temporary records. These
**                           are used for client-side (i.e., non-database)
**                           record sorting.
** 009 ECF 20060117   @23951 Honor pending error status when invoking DMO
**                           setter methods. If in silent error mode and
**                           and error is pending, assignment of DMO
**                           properties is not permitted.
** 010 ECF 20060120   @24042 Replaced ThreadLocal with ContextLocal. Minor
**                           changes to logging.
** 011 ECF 20060201   @24350 Added temp table support. This required
**                           removing the final modifier from the class,
**                           making numerous private methods protected,
**                           and abstracting/adding several new virtual
**                           methods to make this class extensible.
** 012 ECF 20060310   @25031 Added support for natural joins. Added new
**                           inner class ForeignSyncher to perform reverse
**                           foreign record resolution as legacy foreign
**                           key properties are changed by application
**                           code.
** 013 ECF 20060503   @25961 Fixed incorrect handling of latestRecord.
**                           This variable should not be updated when a
**                           record is newly created.
** 014 ECF 20060504   @25981 Improved error reporting. Better handling of
**                           DMO getter/setter index out of bounds errors.
** 015 ECF 20060504   @26007 Fixed defects in buffer copy. Indexed fields
**                           were not being managed properly, leading to
**                           exceptions.
** 016 GES 20060517   @26200 Match new Finalizable interface requirement.
** 017 ECF 20060522   @26588 Fixed delete processing. In addition to
**                           deleting the current record, it is removed
**                           from the buffer.
** 018 ECF 20060531   @26852 Replaced EndConditionException with
**                           QueryOffEndException. The latter is a more
**                           specific version of the former, used to
**                           indicate a query has run off the end of its
**                           result set.
** 019 ECF 20060608   @27025 Fixed defect in record create. After creating
**                           a DMO instance, all records joined to the DMO
**                           by legacy keys must be set to their defaults.
** 020 ECF 20060610   @27103 Fixed defect with wasLocked() reporting.
**                           Underlying locked flag was being cleared too
**                           aggressively.
** 021 ECF 20060621   @27507 Flush newly created records to database more
**                           aggressively. These are now flushed upon any
**                           validation of a new record to ensure that a
**                           query made subsequent to a property being set
**                           on a newly created record finds the new
**                           record.
** 022 ECF 20060627   @27592 Refined validation and flushing. We now track
**                           dirty properties and only validate and flush
**                           if changes to the DMO's state actually have
**                           been made. As a result, validation is more
**                           targeted.
** 023 ECF 20060628   @27669 Track nested open scopes. Added method to
**                           report a reference count of open scopes:
**                           getOpenScopeCount().
** 024 ECF 20060630   @27790 More fixes to flushing/validating. Separated
**                           flushing from validation logic. Added a
**                           rollback capability for invalid buffers.
** 025 ECF 20060720   @28119 Major enhancements. Implemented the change
**                           broker architecture and a more aggressive
**                           eviction policy for DMOs. The former enables
**                           numerous performance optimizations. The
**                           latter reduces the working set and improves
**                           the execution time of most session flushes,
**                           though eviction itself is somewhat costly.
** 026 ECF 20060728   @28273 Moved DMO reference counting mechanism to
**                           BufferManager. incrementDMOUseCount() and
**                           decrementDMOUseCount() are now implemented
**                           there.
** 027 ECF 20060831   @29088 Changed implementation of getSchema().
** 028 ECF 20060906   @29302 Fixed validation defect. Validation and
**                           flushing of a newly created record must not
**                           be triggered by a proxy setter method call
**                           until all columns of a unique constraint have
**                           been updated. The first unique constraint to
**                           have all its columns updated triggers the
**                           flush.
** 029 ECF 20060911   @29462 Fixed memory leak. Setting the same current
**                           record more than once was incrementing the
**                           DMO use count with the BufferManager too
**                           often.
** 030 ECF 20060916   @29667 Major changes to undo architecture. We no
**                           longer rely on the TransactionManager undo
**                           processing for record changes. Rather, we
**                           track property changes and record-level
**                           actions as they occur, saving instructions on
**                           how to rollback changes at each block scope.
**                           Also made some minor fixes to lock handling
**                           to prevent locks from being released too
**                           early in some cases.
** 031 ECF 20060918   @29676 Fix to setCurrentRecord(). Don't flush brand
**                           new records; instead, give business logic a
**                           chance to modify them first.
** 032 ECF 20060919   @29698 Introduced in/decrementDMOUseCount() methods
**                           which delegate to worker methods in
**                           BufferManager. Previously, the BufferManager
**                           methods were called directly, but the new
**                           methods allow subclasses to override the
**                           implementations. Also made processChange()
**                           protected, so that subclasses could override
**                           its implementation.
** 033 ECF 20060926   @29959 Back out #031 (@29676). This was an improper
**                           fix, in that we are not flushing the newly
**                           set record, but rather the record it is
**                           replacing. The latter record MUST be flushed,
**                           or it is possible to lose new records.
** 034 ECF 20061002   @30072 Fixed invocation handler. A getter method
**                           call on an empty buffer must return unknown
**                           value when the error manager is in silent
**                           error mode. Previously it returned null.
** 035 ECF 20061006   @30198 Fixed snapshot logic. A snapshot is never
**                           taken for newly created records, only for
**                           records read from the database. Modified
**                           implementation of isNew() to retain the
**                           knowledge of a record's newly created status,
**                           even after it is made persistent.
** 036 ECF 20061010   @30311 Made various fixes and added foundational
**                           support for auto-commit temp-tables. Added
**                           safety code to prevent an error from being
**                           raised if batch assign mode is interrupted
**                           prematurely, then restarted at a later time.
** 037 ECF 20061012   @30379 Fixed iterate() method. Must release current
**                           record in preparation for next pass of loop.
** 038 ECF 20061018   @30485 Moved processing to clean up batch assign
**                           mode to BufferManager. This was interfering
**                           with end-of-scope processing in some cases.
** 039 ECF 20061018   @30531 Integrated SessionListener architecture.
**                           The invocation handler now receives
**                           sessionClosing notifications. This allows the
**                           handler to force the Persistence object to
**                           bind to a Hibernate session before attempting
**                           a method call on a DMO. This is required now
**                           because a DMO may be proxied by Hibernate,
**                           and we no longer pin sessions open
**                           artificially. Thus, it is necessary to force
**                           a session bind in some cases, to avoid a
**                           LazyInitializationException.
** 040 ECF 20061023   @30596 Fixed defect in invocation handler. Newly
**                           created DMOs were being validated too early,
**                           before all properties in a unique constraint
**                           had been updated.
** 041 ECF 20061027   @30756 Replaced Object with DataModelObject for DMO
**                           arguments to methods/constructors. Required
**                           for compile-time type safety.
** 042 ECF 20061103   @30947 Fixed reportChange(). Bind the persistence
**                           service to a backing session if necessary,
**                           before invoking DMO's deepCopy() method. This
**                           allows the DMO's internal associations to be
**                           lazily populated by Hibernate if this was not
**                           previously done.
** 043 ECF 20061105   @31095 Fixed delete(). This method was not properly
**                           handling inverse foreign key relations to the
**                           deleted record. These are now nulled out when
**                           a record is deleted. The ForeignNuller class
**                           is used to track down dependent records and
**                           break their foreign key references to the
**                           deleted record. Also fixed a defect whereby a
**                           deleted record was not being cleared properly
**                           from other buffers which held the record.
** 044 ECF 20061114   @31141 Removed ForeignSyncher inner class. This code
**                           was refactored and expanded to create the
**                           external AssociationSyncher class and its
**                           subclasses.
** 045 ECF 20061114   @31144 Disabled prohibitively expensive foreign
**                           association synchronization operations. This
**                           effectively removes all such synchronization
**                           except setting the foreign DMO in a local DMO
**                           when a legacy key property in that local DMO
**                           is changed.
** 046 ECF 20061114   @31160 Moved batch assign mode state variables to
**                           BufferManager. This change enables us to
**                           employ a more discriminating batch mode
**                           cleanup mechanism because the batch mode
**                           can be tied to a specific block scope.
** 047 ECF 20061115   @31188 Fixed off-end behavior in setCurrentRecord().
**                           Clear the snapshot record only if setting a
**                           null current record AND this would be
**                           considered an error condition by the calling
**                           query.
** 048 ECF 20061115   @31211 Fixed regression caused by #047 (@31188).
**                           Added parameter to setCurrentRecord() method
**                           to allow caller to determine under what
**                           circumstance to reset snapshot.
** 049 ECF 20061116   @31237 Fixed nested buffer scope handling. Upon the
**                           closure of a nested buffer scope, cleanup
**                           processing was releasing the current record
**                           unconditionally.  This must happen only upon
**                           closure of the outermost buffer scope.
** 050 ECF 20061116   @31242 Fixed error saving new records. Needed to
**                           register the need for a pending session flush
**                           with the ChangeBroker for the saved DMO type.
**                           Otherwise, Hibernate complains about possible
**                           nonthreadsafe access to the session.
** 051 ECF 20061120   @31327 Optimization. Do not create reversibles for
**                           create and delete events if buffer is not
**                           undoable.
** 052 ECF 20061207   @31620 Report record insert as a state change to the
**                           ChangeBroker. Previously, an insert caused a
**                           pending session flush but did not trigger a
**                           state change event to be sent to ChangeBroker
**                           listeners.
** 053 ECF 20061212   @31668 Fixed undoable processing for newly created
**                           records. In cases where the record's creation
**                           and its persistence to the database occurred
**                           in different scopes, undo was not working
**                           properly.
** 054 ECF 20070130   @32040 Fixed handling of no-undo temp-tables. Change
**                           undone with a database-level transaction
**                           rollback is re-rolled forward and committed,
**                           as if the rollback had not occurred. Moved
**                           Reversible inner class into a top level
**                           class. Also fixed copy implementation to
**                           flush newly created destination record at the
**                           appropriate time.
** 055 ECF 20070202   @32071 Changed signature for copy() worker method.
**                           It is necessary for this method to accept
**                           either a proxy or backing DMO object as the
**                           destination record. The former supports the
**                           buffer-copy implementation; the latter is
**                           used internally by TemporaryBuffer.
** 056 ECF 20070212   @32160 Rollback and flush fixes. Force session flush
**                           when DMOs are rolled back in memory. Fixed
**                           NPE when flushing a newly created record to
**                           database in some circumstances. Fixed
**                           database error when a newly created record
**                           was improperly flushed upon a buffer scope
**                           ending after a rollback.
** 057 ECF 20070221   @32229 Track reference count for DMOs managed by
**                           no-undo temp-tables. These had been excluded
**                           from the aggressive eviction policy.
** 058 ECF 20070306   @32288 Fixed change notification. Changes to
**                           multiple indexes in an extent field made in
**                           batch mode were not being processed and
**                           reported properly. Only the last change was
**                           being remembered.
** 059 ECF 20070307   @32336 No longer throw 'error not on file' when a
**                           null current record is set. This is now the
**                           responsibility of the caller, since different
**                           errors can be raised for the same condition,
**                           depending upon the calling context.
** 060 ECF 20070309   @32367 Fixed ClassCastException. Regression
**                           introduced with #058 (@32288).
** 061 ECF 20070318   @32444 Fixed defect persisting unmodified, new DMO
**                           instances. An unmodified, newly created DMO
**                           must not be flushed to the database.
** 062 ECF 20070322   @32546 Reworked flushing and validation. A record is
**                           flushed at the earlier of:  all columns in
**                           any index are updated, or another record (or
**                           null) is stored as the current buffer record,
**                           or a query is about to be executed, or the
**                           outermost open buffer scope ends.
** 063 ECF 20070323   @32563 More flushing cleanup. This version of the
**                           class implements an interim solution for
**                           dirty checks before flushing a transient
**                           record. When checking whether all columns of
**                           any index are dirty, we use all indexes for
**                           temp tables, but only unique indexes for
**                           permanent tables. This is because index info
**                           for temp tables is available via DMOIndex,
**                           which is not the case for permanent tables.
**                           Ultimately, we will want to use JDBC database
**                           metadata to query this index information for
**                           both types.
** 064 ECF 20070412   @32986 Rearchitected record locking implementation.
**                           The same lock manager is used, but record
**                           locks within a session are now coordinated by
**                           one RecordLockContext instance per database,
**                           per session. Removed the LockState inner
**                           class, as this logic is now managed by the
**                           RecordLockContext class for all buffers in a
**                           session (by database), rather than by each
**                           buffer independently.
** 065 ECF 20070415   @32994 Fixed record delete notification regression.
**                           Notification of a record delete was not
**                           being broadcast via the ChangeBroker. Refined
**                           rollback-based deletes as well. Added static
**                           helper method to compare DMOs by primary key.
** 066 ECF 20070416   @33029 Integrated user interrupt handling.
** 067 ECF 20070503   @33391 Added registration of new instance with the
**                           BufferManager.
** 068 ECF 20070509   @33461 Fixed validation processing during bulk copy.
**                           Fixed isIndexDirty() implementation to be
**                           more complete in its index checks.
** 069 ECF 20070515   @33672 Fixed DMO proxy invocation handler. When a
**                           setter method is invoked, the underlying
**                           property must be marked dirty regardless of
**                           whether the value of the property actually
**                           changed. This is necessary for the proper
**                           function of the isIndexDirty() method, so
**                           that newly created records are flushed to the
**                           database at the appropriate time.
** 070 ECF 20070517   @33676 More changes to fine tune flush timing for a
**                           newly created record. Records in tables with
**                           no indices are flushed immediately upon
**                           creation (effectively just temp tables, since
**                           permanent tables always have indices). When
**                           in batch assign mode, any setter invocation
**                           causes the buffer to be marked dirty, even if
**                           the invocation did not change the value.
** 071 ECF 20070523   @33728 Fixed defect related to deletions and saves.
**                           Deletion of a record was not properly
**                           cleaning up the validation context and the
**                           dirty read manager. Likewise, saves related
**                           to undo processing were not updating these
**                           mechanisms.
** 072 ECF 20070525   @33769 Fixed LightweightUndoable processing. Need to
**                           increment/decrement DMO use counts properly
**                           when TransactionManager creates Undoable
**                           backup sets and when scopes end. Also moved
**                           registration of LightweightUndoable from
**                           constructor to when first buffer scope is
**                           opened. These changes ensure that undo
**                           processing does not store a detached DMO into
**                           the buffer.
** 073 ECF 20070531   @33909 Corrected handling of bulk setter methods.
**                           DMOs with indexed properties have bulk setter
**                           methods which accept a scalar value or an
**                           array of values to set multiple indices of
**                           property in one call. The invocation handler
**                           had to be reworked to properly detect, report
**                           and process change in these cases.
** 074 ECF 20070601   @33917 Moved registration with ChangeBroker. Instead
**                           of registering upon construction, we now wait
**                           until the outermost scope is opened. This
**                           prevents a memory leak from registering at
**                           the wrong scope.
** 075 ECF 20070403   @34361 Retrofit for ForeignNuller change. Changed
**                           invocation of ForeignNuller.breakLinkages()
**                           to use simpler signature.
** 076 ECF 20070706   @34403 Added deregisteredSessionListener() to
**                           invocation handler. Required by change to
**                           SessionListener interface.
** 077 ECF 20070706   @34415 Minor optimizations. Cache ForeignNuller
**                           objects lazily instead of letting the static
**                           method ForeignNuller.breakLinkages() gather
**                           these instances repeatedly for each deleted
**                           record. Replaced StringBuffer with
**                           StringBuilder. Cache ChangeBroker instance.
** 078 ECF 20070709   @34423 Minor cleanup. Optimized commit() and
**                           rollback() slightly.
** 079 ECF 20070726   @34719 Refined release on iterate behavior. Allow
**                           release on iterate to be overridden. This is
**                           necessary for compound queries, so that the
**                           outer buffers in a client-side join do not
**                           lose their state while records are found for
**                           inner buffers.
** 080 ECF 20070815   @34853 Improved temporary record handling for nested
**                           client-side where expressions. Necessary to
**                           support client-side, converted CAN-FIND
**                           statements nested within a where clause.
** 081 ECF 20070821   @34902 Added prepare() and disableSilentError()
**                           methods. These additions enable errors during
**                           query substitution parameter processing to be
**                           suppressed using silent error mode, just like
**                           in Progress during where clause processing.
** 082 ECF 20070827   @34992 Added support for restricted bulk deletes.
**                           A new static delete() method variant accepts
**                           a where clause and query substitution parms
**                           and delegates the bulk delete service to the
**                           appropriate RecordBuffer instance. Currently,
**                           this service is not supported for permanent
**                           tables (future optimization), but the
**                           addition of this infrastructure allows
**                           subclasses to implement support for this
**                           service.
** 083 ECF 20070926   @35248 Fixed undo processing defect. Snapshot was
**                           incorrectly being nulled out in cases where
**                           a TM undo cycle tried to reset the current
**                           record to itself. We now backup the latest
**                           snapshot and restore it upon undo.
** 084 ECF 20070907   @35293 Fixed ConnectionManager notifications. Calls
**                           to open and close buffer scopes notified the
**                           connection manager unconditionally, but this
**                           is only appropriate for permanent tables.
**                           This functionality was abstracted into
**                           protected methods which can be overridden by
**                           subclasses. Replaced database name string
**                           database information object.
** 085 ECF 20071018   @35591 Integrated PersistenceFactory.
** 086 ECF 20071029   @35633 Changed in[de]crementDMOUseCount() signature.
**                           Matches change in BufferManager.
** 087 ECF 20071130   @36124 Fixed setRecord(). Removed resetSnapshot
**                           parameter. This method no longer clears the
**                           snapshot when a null current record is set.
**                           In this circumstance, the placeholder
**                           snapshot must not be lost. Integrated
**                           generics.
** 088 ECF 20071205   @36265 Track off-end status and sort index of last
**                           query updating the buffer. Added sortIndex
**                           and offEnd parameters to setRecord(); added
**                           getOffEnd() method. This information is used
**                           by a find query to ensure it starts its
**                           search at the proper placeholder record.
** 089 ECF 20071221   @36546 Refined rollback of auto-commit temp records.
**                           Do not try to re-delete a record already not
**                           present in the database.
** 090 SVL 20071231   @36627 Next primary key generation is carried out
**                           to the separate function.
** 091 ECF 20080129   @36935 Added debug/trace level logging. Log all
**                           create and flush events, as well as the
**                           details of any rollbacks.
** 092 ECF 20080310   @37483 Added getDialect() convenience method. Also
**                           integrated generics.
** 093 SVL 20080312   @37428 The inverse synchronization is enabled and
**                           performed for newly created DMOs.
** 094 SVL 20080418   @38038 Added support for the inverse sorting.
** 095 ECF 20080424   @38150 Integrated generics.
** 096 SVL 20080421   @38098 Association Synchers are disabled if foreign
**                           keys are disabled.
** 097 SVL 20080430   @38175 Reversible actions and updates are removed
**                           from the current scope into the commit()
**                           if we are at the master transaction level.
** 098 ECF 20080506   @38220 Fixed automatic lock upgrade logic. No
**                           upgrade is necessary if the datum being set
**                           in the DMO is the same as the existing value.
** 099 SVL 20080512   @38260 Added pinnedLockTypes map which maps primary
**                           keys of past and present currentRecords to
**                           their pinned LockTypes.
** 100 SVL 20080516   @38291 Memory consumption optimization.
** 101 SVL 20080526   @38397 pinnedLockTypes are cleared into finished(). 
** 102 CA  20080604   @38539 Fixed the rollback of nested CREATE/DELETE 
**                           actions, when they are applied for the same
**                           DMO. Fixed the rollback of nested UPDATE
**                           actions for the same DMO - the changes must
**                           be reported to the ChangeBroker. Ref: #38537
** 103 ECF 20080506   @38612 Integrated DirtyShareContext. Used to share
**                           uncommitted index changes across user
**                           sessions.
** 104 ECF 20080606   @38643 Removed ValidationContext references.
**                           Obsoleted by DirtyShareContext.
** 105 ECF 20080609   @38653 Fixed several regressions. LightweightUnoable
**                           was improperly flushing a transient record
**                           during undo processing. Refactored dirty
**                           index update sharing to permit temp tables to
**                           bypass this processing.
** 106 ECF 20080610   @38691 Fixed rollback of DMO property updates. Dirty
**                           share manager was not being notified of
**                           changes being rolled back.
** 107 CA  20080612   @38713 Fixed FIND statement record retrieval and a
**                           dirty share issue:the snapshot must be set to
**                           null when current block is finished and also
**                           the snapshot must save the new record on 
**                           create.
** 108 ECF 20080612   @38729 Fixed rollback() in ReversibleCreate and
**                           ReversibleDelete. Removed optimization to let
**                           database handle rollback on full transaction
**                           boundaries. Our internal state (and that of
**                           Hibernate) was not correct in this case.
** 109 CA  20080616   @38787 Fixed rollback() in ReversibleCreate: 
**                           reportChange(...) must be called on a record 
**                           insert rollback, so all the registered
**                           listeners for this buffer will be notified.
** 110 ECF 20080626   @38961 Fixed ReversibleCreate.rollback(). Current
**                           record must be nulled out unconditionally,
**                           whether or not the record was persisted. Also
**                           removed immediate rollback of DMO updates
**                           during failed validation. Instead, we rely on
**                           the normal, TM-driven rollback mechanism.
** 111 CA  20080605   @38568 Implemented rollback of bulk delete actions.
**                           Fixed rollback of reversible update actions.
** 112 ECF 20080628   @39070 More undo/rollback fixes. We no longer pin
**                           DMOs to the current Hibernate session when
**                           they are stored in LightweightUndoable.
**                           Added special handling for sharing dirty
**                           changes when newly created records have not
**                           yet been stored in the primary database.
** 113 CA  20080702   @39018 Fixed ReversibleUpdate.rollback() - the map
**                           unreportedChanges was not populated correct
**                           when there are changes for extent fields.
** 114 CA  20080716   @39124 Modifications due to DirtyShareContext API 
**                           changes. Fixed rollback - if there are dirty
**                           CREATE or DELETE actions, all the indexes for
**                           the entity will be locked before the chain of
**                           reversibles is processed.
** 115 CA  20080718   @39170 Fix ValidationException in not-transacted
**                           blocks.
** 116 ECF 20080723   @39171 Fixed flushing behavior for temp-tables with
**                           no indexes. Updates to such tables must be
**                           flushed to the database immediately.
** 117 CA  20080725   @39211 Fix rollback for when the reversibles 
**                           reference "stale" records.
** 118 ECF 20080806   @39328 Added rowid support. Reimplemented recordID()
**                           and added rowID().
** 119 CA  20080808   @39358 Implemented no-undo temp-table support: these
**                           tables require no rollback; on each CREATE,
**                           DELETE or UPDATE event, the BufferManager is
**                           added a Reversible containing the current
**                           record state. Fixed subtle flow in
**                           detectChange() related to diff processing.
** 120 ECF 20080815   @39557 Support global DMO change events.
** 121 CA  20080826   @39567 Added rollbackPending which is called by TM
**                           when UNDO targets a parent block. 
** 122 CA  20080815   @39466 Support API change in DBUtils. Register with
**                           the BufferManager at the end of the c'tor.
** 123 SIY 20080904   @39719 Inferred generics in buffer creation methods.
** 124 SVL 20080901   @39653 Key reclamation is performed into commit()
**                           and rollback().
** 125 ECF 20080925   @39948 Refactored Reversible concrete classes.
** 126 GES 20080926   @39974 Minor signature change.
** 127 ECF 20081001   @39998 Changed openScope() to use varargs. This
**                           allows the conversion to compact multiple
**                           openScope() calls in business logic to a
**                           single call. Also fixed NPE in
**                           ReversibleDelete.getId(). Added versions of
**                           certain static helpers which return primitive
**                           boolean instead of logical.
** 128 ECF 20081007   @40049 Removed deprecated available() method.
** 129 ECF 20081020   @40209 Fixed rollback at full transaction. Cannot
**                           create/update/delete DMO at full transaction
**                           boundary just before database rollback.
** 130 ECF 20081031   @40286 Fixed delete of DMOs with extent fields. In
**                           some cases, DMOs with uninitialized, lazy,
**                           persistent collections were being stored in
**                           ReversibleDelete objects. Upon rollback, the
**                           missing collection data was unavailable.
** 131 ECF 20081106   @40326 Fixed errorNotOnFile(). Use DMO alias instead
**                           of DMO interface name.
** 132 ECF 20081114   @40453 Fixed record lock leak. Record locks must be
**                           relinquished in iterate() and retry() if we
**                           are at the outermost open scope.
** 133 ECF 20081118   @40550 Fixed ReversibleDelete. RecordBuffer's
**                           current record DMO must be used for rollback
**                           if it represents the same record as the
**                           ReversibleDelete's internal DMO. Removed
**                           processing in RecordBuffer.rollback() which
**                           was intended to handle this case, but was
**                           incorrect.
** 134 ECF 20081201   @40733 Added support for key reclamation for temp
**                           tables. Uses the same hooks as we had for key
**                           reclamation in permanent tables (commit() and
**                           rollback() methods). A new reclaimKeys(List)
**                           method allows subclasses to override the
**                           original implementation.
** 135 SVL 20081204   @40794 Added undoables map cleanup into rollback().
** 136 ECF 20081211   @40870 Better debug information in toString().
** 137 ECF 20081215   @40918 Modified transient record flushing. Removed
**                           flush(Method, Object[]) variant. Simplified
**                           flush() variant.
** 138 ECF 20090107   @41015 Minor update to stateChanged().
** 139 ECF 20090112   @41163 Fixed undo-related locking behavior. Locks
**                           were being downgraded rather than released in
**                           certain transaction-boundary cases.
** 140 ECF 20090128   @41240 Added stale flag and isStale(), setStale().
**                           A buffer is stale when the contained DMO's
**                           state is known to be out of sync with the
**                           corresponding record in the database. This
**                           can occur after a database-level rollback.
** 141 ECF 20090202   @41249 Fixed setCurrentRecord(). Release/downgrade
**                           of lock on outgoing record was not properly
**                           handled in transaction cases.
** 142 ECF 20090206   @41264 Major changes to invocation handler and auto-
**                           commit processing. Batch mode was modified in
**                           order to enable an auto-commit transaction to
**                           begin at the first update and end in the
**                           endBatch() method. The copy() method was
**                           modified to not begin a new transaction if
**                           already in an auto-commit batch. The proxy
**                           invocation handler now attempts to associate
**                           a detached record with the current Hibernate
**                           session before invoking an indexed setter or
**                           getter method, to avoid a lazy initialization
**                           exception.
** 143 ECF 20090218   @41332 Memory improvements. Avoid creating
**                           unnecessary Reversible maps. Set reduced
**                           default sizes on certain collections.
**                           Leverage cached collections of getter methods
**                           in PropertyHelper.
** 144 ECF 20090306   @41437 Fixed load(). Added code to prevent releasing
**                           a newly created (still transient) record when
**                           trying to re-load the same record.
** 145 ECF 20090312   @41585 Fixed regression caused by #142 (@41264).
**                           Detached record association logic inside
**                           invocation handler could try to reassociate a
**                           null record.
** 146 ECF 20090315   @41607 Fixed ReversibleDelete.getDMO(). In the event
**                           target DMO is currentRecord, ensure lazily
**                           initialized extent field collections have
**                           been realized before returning DMO. Minor
**                           optimization to load() to avoid reloading the
**                           same temp table record unnecessarily during
**                           undo processing.
** 147 GES 20090424   @41942 Import change.
** 148 ECF 20090508   @42127 Rewrote validation processing. Validation is now
**                           much more incremental: only those indexes and
**                           properties which have been modified are validated
**                           immediately for new records. Remaining validation
**                           occurs when the buffer is flushed.
** 149 ECF 20090512   @42155 Fixed regressions caused by #148 (@42127).
**                           Integrated BufferFlushQueue.
** 150 ECF 20090513   @42167 Added isPropertyIndexed(). Needed for validation.
** 151 SVL 20090225   @41364 API change. recordID() returns recid instead of
**                           integer.
** 152 ECF 20090610   @42633 Added evictDMOIfUnused(). Evicts DMO from
**                           Hibernate session if no resource references it.
** 153 ECF 20090611   @42660 Optimized load() method and ValidationHelper.
**                           We now avoid expensive database operations and
**                           validation, respectively, when possible.
** 154 ECF 20090612   @42666 Fixed regressions in #153 (@42660). Partially
**                           rolled back changes to load() method.
** 155 ECF 20090623   @42963 Made changes to use updated BufferManager API.
**                           Some methods to retrieve undoable/reversible
**                           actions were changed.
** 156 SVL 20090706   @43054 Into copy() catch and rethrow all
**                           ConditionExceptions without rolling back
**                           transaction.
** 157 ECF 20090707   @43104 Fixed validation corner case. A newly created,
**                           permanent record with dirty properties but no
**                           dirty indexes was being validated and flushed,
**                           but not shared with other sessions. Later, an
**                           attempt to rollback dirty changes failed.
** 158 SVL 20090706   @43062 Into endBatch() rollback transaction if
**                           validation has failed.
** 159 CA  20090709   @43126 When batch mode ends, if validation fails, the
**                           reversible info must be collected (to allow
**                           rollback for the changed values). On rollback,
**                           a ReversibleUpdate must actually rollback the
**                           changed property, regardless of the mode (re-roll
**                           forward or not full transaction block).
** 160 ECF 20090716   @43222 Refactored initialization logic. Since it is
**                           possible to define a buffer and even open a scope
**                           on it without the backing database being
**                           connected, initialization of many critical
**                           instance variables cannot be set up during
**                           construction. This logic was moved to the new
**                           initialize() method, which is invoked lazily, as
**                           late as possible, within and external to this
**                           class.
** 161 ECF 20090717   @43229 Fixed regressions in #159 and #160. Backed out
**                           changes to ReversibleUpdate.rollbackWorker, which
**                           can cause unique constraint violations. Buffer
**                           must be initialized when invocation handler is
**                           called for a DMO method.
** 162 ECF 20090720   @43306 Improved ValidationHelper. Changed prevalidate
**                           method to prevent unnecessary, future validation
**                           in some cases.
** 163 CA  20090724   @43382 Rolledback H140 (#41240), to fix the rollback 
**                           mechanism. Also, on rollback, the DMO's which
**                           have reversible actions will be processed in 
**                           reverse order instead the order they were 
**                           registered.
** 164 CA  20090724   @43395 On query off end, the undoables registered with
**                           this record buffer are not undone - the buffer 
**                           must not reference any records after a query
**                           which uses the buffer goes off-end.
** 165 ECF 20090725   @43422 Fixed validation and flushing of temp tables in
**                           invocation handler. Added getMasterBuffer()
**                           method to support proper rollback handling of
**                           shared temp tables. Prevent prevalidation in some
**                           cases.
** 166 ECF 20090729   @43449 Implemented Commitable.validate() and fixed
**                           flush(). The former is needed to validate and
**                           flush transient records when field assignments do
**                           not trigger flushing. The persistence and change
**                           reporting within flush() is now bracketed with an
**                           explicit transaction to prevent auto-commit temp
**                           table records from leaving behind a pending flush
**                           entry in the ChangeBroker. This was causing flush
**                           problems and downstream data loss.
** 167 SVL 20090803   @43477 Raise stop condition into initialize() if the
**                           database isn't connected.
** 168 CA  20090804   @43508 Added implementation for reset() method in
**                           QueryOffEndListener - this will set the buffer to
**                           reference no record.
** 169 CA  20090807   @43557 Reversibles must be kept by database and table
**                           name, not by RecordBuffer instance - to ensure
**                           proper reversible order when rolling back records
**                           in a table with multiple buffers defined for it.
**                           This fixes @43542.
** 170 SVL 20090810   @43578 Into Handler.invoke() log
**                           IndexOutOfBoundsException stacktrace only as 
**                           debug output.
** 171 ECF 20090810   @43581 Fixed getDatabaseAndTable(). This method must not
**                           throw IllegalStateException if the buffer is not
**                           yet initialized. Instead, it must return null.
** 172 ECF 20090810   @43590 Changed to accommodate SessionListener API
**                           change. sessionClosing() method was replaced with
**                           sessionEvent().
** 173 ECF 20090812   @43603 Replaced getDatabaseAndTable() with
**                           getBufferType(). More information was required by
**                           callers than was provided by the database and
**                           table string, so the BufferType class is used
**                           instead.
** 174 ECF 20090813   @43615 Fixed validation problem which caused a flushing
**                           error downstream. When validating dirty props, we
**                           now undo them immediately upon failure, so they
**                           are never flushed to the database nor recorded
**                           in Reversibles. Failure to do this was causing
**                           unique constraint failures in some cases.
** 175 ECF 20090815   @43634 Fixed invocation handler range checking for
**                           extent field access. Element index is now checked
**                           for unknown value and out of bounds conditions
**                           before backing method is invoked.
** 176 ECF 20090819   @43691 Fixed ReversibleUpdate.rollbackWorker(). This
**                           method was making an assumption that the buffer
**                           still contained a snapshot of the record being
**                           rolled back. It now uses a different API into the
**                           dirty share context.
** 177 CA  20091001   @44059 Fixed leak caused by bulk delete duplication: do 
**                           not duplicate it if the record was deleted 
**                           explicitly (via a DELETE operation) or implicitly
**                           (via a bulk delete).
** 178 CA  20091001   @44081 Avoid NPE in rollback(). In certain bulk delete
**                           cases, the Reversible will not return a DMO, so
**                           we cannot make a deep copy for the snapshot.
** 179 SVL 20091019   @44144 Added support of assign and delete database
**                           triggers.
** 180 SVL 20091019   @44159 Pass a duplicate of the old value into assign
**                           triggers in order to ensure that changes into
**                           this value will not affect undo processing.
** 181 ECF 20091026   @44192 Fixed compare() method. Need to use getter method
**                           specific to each DMO's buffer, in case the types
**                           are different. We were using the getter method
**                           for only the first DMO, which made an implicit
**                           (and incorrect) assumption that both DMOs were of
**                           the same type.
** 182 CA  20091102   @44300 Fixed retry processing - on retry, the openScope 
**                           call must not be executed.
** 183 SVL 20091117   @44402 Notify the BufferManager when a new record was 
**                           loaded (or the old record was reloaded) into the
**                           buffer.
** 184 CA  20091202   @44467 Reworked the rollback mechanism, to maintain 
**                           proper reversible order and thus to solve some
**                           unique-constraint violations during rollback of
**                           some bulk delete cases and mixed record operations.
**                           The ReversibleChain is no longer used, instead a 
**                           linked set with all operations which affect a 
**                           certain table is used.
** 185 ECF 20100128   @44553 Fixed NPE in rollback(). Added safety code to
**                           handle certain bulk delete case.
** 186 SIY 20100429   @44837 Fixed double rollback in some conditions.
** 187 CA  20100527   @44880 Added support for CREATE and "no record is 
**                           available triggers". The CREATE, DELETE and 
**                           ASSIGN triggers will execute too, but the passed
**                           DMO will not be the buffer proxy; instead, it
**                           will be the actual record instance.
** 188 CS  20121031          Removed client specific references
** 189 CA  20130117          Added possibility to retrieve the DMO interface
**                           from the proxy's invocation handler.
** 190 ECF 20130119          Changed the public API to this class; a number of
**                           static methods have been removed and replaced
**                           by the Buffer interface API. This enables more
**                           natural, direct method calls on DMO proxy objects
**                           for standard services. This entailed a change to
**                           the DMO proxy implementation from the dynamic
**                           proxy support in java.lang.reflect to that in
**                           com.goldencode.proxy (we needed to inherit our
**                           proxies from a functional base class).
** 191 CA  20130126          Renamed getDMOInterfaceForProxy to 
**                           getDMOInterfaceForObject and fixed possible 
**                           ClassCastException and made it to receive any 
**                           kind of object. 
** 192 OM  20121106          Added CURRENT-CHANGED built-in function implementation.
** 193 OM  20130212          Added CREATE-BUFFER conversion support.
** 194 ECF 20130224          Added limited support for embedded SQL.
** 195 OM  20130304          Added missing create() (CREATE-BUFFER) signatures and ordered the
**                           existing ones.
** 196 VMN 20130428          Implemented runtime support for handle-based methods BUFFER-COMPARE
**                           and BUFFER-COPY, reusing code with corresponding statements. Stub
**                           added for compare() method containing FieldReference as last
**                           parameter. Added CASE-SENSITIVE option conversion and runtime
**                           support for the statement version of BUFFER-COMPARE
** 197 SVL 20130331          Upgraded to Hibernate 4.
** 198 OM  20130416          Added reminder for integer (maybe other) validation fails.
**                           Scanned InvocationTargetException in invoke(Object, Method, Object[])
**                           for ErrorConditionException and act as P4GL.
**                           Added support for int64 expression indexes.
** 199 VMN 20130711          Fixed properties comparing with extent: isAssignableTypes().
** 200 OM  20130702          Fixed 'unmappable character for encoding ASCII' error
** 201 VMN 20130722          Added aggregated functions support for embedded SQL.
** 202 VMN 20130903          Added table lock to deleteAll().
** 203 OM  20130829          Finished/fixed current-changed implementation.
**                           Method areDMOsSame() is now package visible.
** 204 CA  20130918          Added widget-pool support. The pool name must be validated before 
**                           anything else, and a null name means the default pool is used.
** 205 SVL 20130822          Added isDynamic, changed some access modifiers.
** 206 ECF 20130509          Commented out validation error message about failure to update a
**                           field from invocation handler. We may need to put this back under
**                           certain conditions at some point, but for now I can't recreate the
**                           circumstances which must have prompted me to first put this in.
**                           Added base metadata support.
** 207 ECF 20131028          Added lock metadata support. Made aggregate function-related methods
**                           package private instead of public.
** 208 SVL 20131028          Added legacyName and blockDepth parameter to constructors and
**                           define(), Added openScopeAt. Implemented create() functions.
** 209 CA  20131209          Added support for INSTANTIATING-PROCEDURE. Fixed implicit deletion
**                           of static buffers.
** 210 VMN 20131212          Fixes in deleteAll(): using java annotations for defining primary
**                           index and NPE (initialize() moved)
** 211 OM  20131128          Implementation of database trigger firing.
** 212 SVL 20140106          Fixed copy(), added validate(Persistable, boolean).
** 213 OM  20140607          Fixed double-call of finishedImpl() call when processing Finalizables
**                           from TransactionManager. Added read/only getter.
** 214 SVL 20140124          getResource is used for unwrapping handles.
** 215 ECF 20140303          Fixed copy/compare methods called from converted handle-based methods
**                           to use legacy field names and added error handling for many error
**                           conditions. Replaced Apache commons logging with J2SE logging.
** 216 SVL 20140311          Allow existence of NO-UNDO records which violate constraints.
** 217 VMN 20140325          Added support for custom denormalization of fields with extent.
** 218 CA  20140406          Fixed lock downgrade for buffers surviving a persistent procedure.
** 219 VMN 20140409          Fixes in BUFFER-COMPARE support, including using legacy field names.
** 220 OM  20140417          Fixed typo in method name (addToAllBuffersArray).
** 221 ECF 20140416          Fixed bug in getPropsMap.
** 222 OM  20140508          Added package access getter for the value of a requested field
**                           (needed by DMOValidator when computing the index-key sizes).
**                           Added index-key validation in validateRemaining().
** 223 ECF 20140613          Renamed getDMOInterfaceForObject to getDMOBufInterfaceForObject and
**                           changed it to return the combined DMO and Buffer interface, rather
**                           than just the DMO interface.
** 224 ECF 20140703          Use current block depth in createDynamicBufferForPermTable instead of
**                           -1. This ensures buffer is registered properly with buffer manager.
** 225 VMN 20140723          Fixed BUFFER-COPY method when pairs-list maps concrete index of field
**                           with extent to the field without extent.
** 226 SVL 20140725          Fixed depth at which a buffer was opened in
**                           createDynamicBufferForPermTable.
** 227 VMN 20140806          Fixed BUFFER-COPY and BUFFER-COMPARE methods when pairs-list maps
**                           fields with equal and different extents or concrete index of field
**                           with extent to the field without extent. Method getExtent(...)
**                           corrected.
** 228 ECF 20140801          Major rework around trigger support and the validation and flushing
**                           algorithms. Changed the timing of implicit validation to occur at the
**                           earlier of the end of the current, full transaction or the current
**                           buffer's outermost scope. This had major repercussions on the dirty
**                           sharing algorithms as well. Eliminated use of BufferFlushQueue, which
**                           was flushing records too aggressively.
** 229 ECF 20140930          Log a warning if there is an error during rollback in flush method.
** 230 OM  20141002          Fixed getter field proxy to return correct type of UNKNOWN value.
** 231 OM  20141003          COPY and COMPARE buffer operations use legacy names for pairing
**                           fields to be copied.
** 232 OM  20141015          compare returns single element for EXTENT fields.
**                           Removed locking on temporary readonly buffers in Handler.invoke().
** 233 VMN 20141022          Corrected equals() and hashCode() methods for DatumAccess.
** 234 OM  20141029          Added defineAlias() and support for defining buffer parameters and
**                           implemented stacking of buffer names (property & legacy).
** 235 ECF 20150101          Fixed createDynamicBufferForPermTable and callers to use the logical
**                           database name instead of physical database name when defining a new
**                           buffer.
** 236 OM  20150112          Added special care of buffers when it is open inside a persistent
**                           procedure. They will be stored in global scope of the local
**                           ChangeBroker and will be removed when the procedure is deleted.
** 237 ECF 20150127          Fixed aggregate method, which was not preprocessing the HQL where
**                           clause, and thus was not dealing with query substitution parameters
**                           properly.
** 238 VMN 20150228          Fixed array setter reference for denormalized field for simple value.
** 239 VMN 20150314          Fixed new warning when compiling.
** 240 VMN 20150317          Added check isJavaBeanMethod() in method getDenormalizedMethod().
** 241 OM  20150303          Chained dynamic buffers into HandleChain.
**                           getLDBName() returns null for aliases to unconnected databases.
** 242 ECF 20150323          Deregister dynamic buffers with the buffer manager when they are no
**                           longer in use; other performance improvements.
** 243 VMN 20150403          Fixed array setter reference for denormalized field for array value.
** 244 OM  20150504          Fixed registration of buffers belonging to procedures running in
**                           PERSISTENT mode in global scope of the ChangeBroker.
** 245 ECF 20150517          Fixed global scope cleanup, rearranged methods by access modifier.
** 246 ECF 20150801          Added support for compound query optimization. Initial fixes for
**                           runtime denormalized schema support.
** 247 ECF 20150906          Reimplemented temporary silent error handling of query substitution
**                           parameters. Changed DMO in-use tracking to match changes in
**                           BufferManager API. Added isUnknownMode for outer join support. Fixed
**                           buffer-copy transaction leak and field pairs flaws.
** 248 GES 20150923          Javadoc fix.
** 249 ECF 20150929          Optimized certain frequently invoked Persistence methods.
**                           Reimplemented invocation handler to no longer be a SessionListener,
**                           eliminating most of the SessionListener notification traffic.
** 250 ECF 20151013          Clarified defineAlias javadoc.
** 251 ECF 20151031          Modified getLogicalDatabase to initialize buffer.
** 252 OM  20151026          Fixed flush() when UNDO attribute has changed (temp-tables only).
**                           Marked record as persisted on [create] rollback merge.
** 253 SVL 20151119          Close related queries when buffer's scope ends.
** 254 ECF 20151208          Temporary fix to prevent NPE in toString(Persistable). NPE is gone,
**                           but output is still not quite right for temp-table buffers; needs
**                           further attention, but is not high priority.
** 255 ECF 20151214          Created package private throwOffEnd method.
** 256 ECF 20160105          Allow getDatabase to be called before a buffer is active.
** 257 ECF 20160113          Fixed case of processing undo at the end of a block when the record
**                           in the buffer hasn't changed. We should not retrieve the record from
**                           database in this case.
** 258 ECF 20160216          Fixed denormalized schema support. Modified and significantly
**                           streamlined invocation handler to work with improved DMO proxies.
**                           Now, the only getter/setter methods which are passed to the
**                           invocation handler are simple bean methods, plus one variant each of
**                           an indexed getter and setter, which take a simple int value as the
**                           subscript parameter. Mapping of a legacy-style, indexed call to a
**                           denormalized extent field is now handled by the DMO proxies directly,
**                           even those method variants which accept or return arrays, or assign a
**                           scalar value to all elements in an extent field. See DmoProxyPlugin
**                           for additional details.
** 259 ECF 20160321          Cache hash code at construction and override hashCode and equals
**                           methods to be final. Ensure reversibles are not merged to parent
**                           scope if that would be outside of the buffer's scope.
** 260 OM  20160302          Adjusted the listDirtyIndexes() and getDirtyProperties();
**                           cumulativeDirtyProps also keeps the changes.
**     ECF 20160430          Fixed ChangeBroker registration in openScopeAt. Do not include dirty
**                           DMOs in DMO reference counting.
** 261 ECF 20160512          Avoid TM.deregisterGlobalFinalizable warning when global scope is
**                           finished. Modified deleteAll to deal with null primary index.
** 262 CA  20160518          Change ParameterIndices usage to collect all substitution parameters,
**                           as their number might change (up or down) after ternary is normalized.
** 263 ECF 20160605          Reduce unnecessary flushing and other minor performance fix.
** 264 ECF 20160615          Performance tweaks.
** 265 OM  20160629          HQLPreprocessor needs the list of fields of the current index.
** 266 IAS 20160701          Added getLegacyName() method, fixed temp table lookup.
** 267 IAS 20160714          Use legacy name instead of DMO alias.
** 268 OM  20160715          Added support for template records.
** 269 ECF 20160720          Fixed buffer lookup during HQL preprocessing.
** 270 CA  20160627          Reworked undoable support - the undoables register themselves with
**                           all blocks up the stack, until either 1. the tx block which created
**                           it is reached or 2. the last tx block where it was saved is reached.
** 271 ECF 20160823          Changed locking for improved concurrency, disabled primary key
**                           reclamation; improved detection of records deleted in other
**                           contexts.
** 272 ECF 20160827          Fixed failure of buffer compare for some temp-table cases. Avoid
**                           calling RecordLockContext.relinquishBufferLocks inside a
**                           transaction.
** 273 ECF 20160829          Performance enhancements. Use local BufferManager to check
**                           transaction status instead of TransactionManager. Changed session
**                           session flushing and auto-commit behavior to allow better batching
**                           of database changes. Modified DMO eviction implementation.
** 274 GES 20160804          Minor javadoc update.
**         20160919          Changes to match query class constructor/initialization changes.
** 275 ECF 20161004          Use implicit transactions where possible to reduce flushing.
** 276 ECF 20161014          Fixed implementation of reload() method in the event there is no
**                           current record in the buffer to reload, and this is not considered
**                           an error in the context of the query (e.g., query represents an
**                           outer join and record can be null).
** 277 OM  20170202          Moved initialization of createScope before calling CREATE trigger.
**                           Implemented bulk delete for permanent tables.
** 278 ECF 20170325          Fixed lookupNumFields, which must use the legacy table name.
** 279 SVL 20171025          Added buffer-specific field attributes.
** 280 ECF 20171025          Made getDMOProxy and getParentTable public to enabled access from
**                           serial package.
** 281 ECF 20171128          If no lock type is provided for load method, use the currently
**                           pinned lock type for the record being loaded.
** 282 GES 20171207          Removed pending error checking to match new silent mode approach.
**     ECF 20171212          Added a TODO for query substitution parameter error handling for
**                           aggregate queries (embedded SQL).
** 283 ECF 20171202          Omit DMO alias from HQL for a bulk delete.
**     OM  20171212          HQLPreprocessor.get() signature change. Added delete() variant with
**                           support for external buffers for nested selects.
**     ECF 20171217          Allow new bulk delete variant to be invoked iff supplementary DMO
**                           array is null.
**         20171219          Made isTransient public.
**     CA  20171220          Added getUndoable.
** 284 EVL 20180207          Fix for invalid char in comment issue.
** 285 ECF 20180224          Fix for load: must register buffer with record lock context.
** 286 ECF 20180305          Buffer is read-only if underlying table is read-only.
** 287 OM  20180307          Added context local trigger awareness.
** 288 ECF 20180319          Fixed BDT format errors when detailed logging is enabled.
** 289 ECF 20180412          Fixed lookupNumFields to use legacy table name, not buffer name.
**         20180418          Fixed write trigger and flushing logic.
** 290 ECF 20180522          Fixed validation failure during record delete.
** 291 ECF 20180527          Force a pending flush for a non-transient, modified, validated
**                           record when a batch assignment is closed.
** 292 CA  20180520          Fixed dynamic buffer creation when the name is qualified with the 
**                           schema.
**     ECF 20180527          Force a pending flush for a non-transient, modified, validated
**                           record when a batch assignment is closed.
** 293 CA  20180621          Added recordNotAvailable[Type] - an API emitted for VALEXP where 
**                           there are buffer references with invalid scopes.
** 294 ECF 20180710          Minor performance tweak to popPinnedLockScope.
** 295 CA  20180723          Batch assign mode emits as lambda instead of start/endBatch brackets.
**     ECF 20180731          Ensure a temporary record placed in the buffer to satisfy client-side
**                           sorting or filtering is associated with the session (needed to avoid
**                           LazyInitializationException for collections used by extent fields).
** 296 ECF 20180809          Ensure template record is not attached to session by a lock request.
** 297 ECF 20180920          Ignore NonUniqueObjectException when ensuring DMO is associated with
**                           Hibernate session.
** 298 OM  20181022          Fixed CURRENT-CHANGED implementation (to ignore changes in WRITE
**                           triggers).
** 299 OM  20181031          Added BufferManager-level unknown mode support.
** 300 ECF 20181220          Fixed armCurrentChanged: ensure Hibernate session is bound before
**                           deep-copying current record.
** 301 OM  20190111          Added support for VST R/O checking. Using annotations to manipulate
**                           tables.
** 302 OM  20190202          Fixed VST R/O checking (including create and delete).
** 303 OM  20190117          Integrated _Trans metadata support.
**     OM  20190129          Added ref-counter for objects store in fields.
**     ECF 20190212          Refactored copy and compare methods. Added no-lobs implementation to
**                           same.
**     ECF 20190311          Ensure DMO is bound to a Hibernate session before attempting deep
**                           copy of a record with lazy extent fields.
**     ECF 20190314          Ensure we are deleting the correct DMO instance when rolling back a
**                           ReversibleCreate.
**     ECF 20190317          Provisionally modified validate(boolean) to flush unconditionally for
**                           undoable buffers. Ensure new current record instance is stored, even
**                           if existing current record has the same primary key.
**     ECF 20190322          Minor change to rollbackPending due to optimization in BufferManager.
**     ECF 20190323          Avoid managing undoable resources if no changes have been made by a
**                           buffer instance.
**     CA  20190325          Implemented ProcedureHelper instead of direct usage of the static
**                           API.  This allows the elimination of context local usage in 
**                           ProcedureManager.
**     ECF 20190325          Replaced TransactionManager static calls with TransactionHelper
**                           instance calls to reduce context-local lookups.
**     ECF 20190326          Avoid assign trigger lookup for DMO getter method invocations.
** 304 ECF 20190414          More lazy initialization fixes. Prevent recursive deletions of the
**                           same record by cascading/nested delete triggers.
** 305 SVL 20190614          Added functions for obtaining code page of a field.
** 306 ECF 20190706          Treat any call to a BLOB/CLOB setter as a property change,
**                           regardless of whether the value has changed.
** 307 OM  20190510          Error 5365 is generated only from statements, not methods. Added
**                           support for before/after buffers. Implementes ROW-STATE, BEFORE-ROWID
**                           and AFTER-ROWID.
** 308 ECF 20190714          Added support for aggressive buffer flushing mode.
** 309 ECF 20190628          When undoing dirty changes, apply directly to current DMO, not to
**                           proxy.
**     SBI 20190728          Added [get|set]FieldHelp.
**     ECF 20190725          Distinguish "fake" buffers (i.e., those created as the read-only
**                           "old" buffer when a write trigger is fired) from other types of
**                           read-only buffers.
**     ECF 20190813          Prevent write trigger from firing at inappropriate times.
** 310 AIL 20190821          Added property to tracker when a trigger is fired at the end of a
**                           batch.
** 311 CA  20190724          BUFFER-COPY must not process BEFORE-TABLE if the TRACKING-CHANGES 
**                           flag is not set. 
**     ECF 20190802          Minor fix to error message. Fixed a legacy name regression caused by
**                           a change to BufferManager.addToAllBuffersArray. Fixed a leak of
**                           RecordChangeListeners registered with ChangeBroker.
**     CA  20190812          Changes to allow for mutable buffers; any API which receives a Buffer 
**                           instance and is invoked from converted code must resolve the runtime
**                           instance before saving the instance.
**     ECF 20190824          Do not initialize the extent fields if this is a transient record.
**     ECF 20190827          Added getSchemaDictionary to properly configure a context-local
**                           SchemaDictionary instance which can resolve database references for
**                           a specific list of buffers.
**     OM  20190831          Added hidden fields and index specific to BEFORE TEMP-TABLES.
**     ECF 20190915          Another fix to flush and write trigger invocation during validation.
** 312 OM  20190906          Added support for MERGE and REPLACE fill modes. Improved ERROR-STRING
**                           and ERROR-FLAG support.
**     CA  20191009          Added where and sort clause translation in case of bound buffers.
** 313 CA  20191119          Track the executing legacy class, to be able to register any buffer  
**                           to its source definition class.
** 314 AIL 20191128          Added validation between the delete trigger and effective delete.
**     CA  20200110          Static buffers defined in an internal entry must be deleted upon exit.
** 315 AIL 20191213          Added ArgumentBuffer as a proxy used in case a buffer is referenced 
**                           through a parameter buffer. Fixed proxy suppressing enclosed proxies.
**     AIL 20200108          Prevented direct access to proxy members (without using the handler)
** 316 AIL 20200110          Fixed trigger calling to be done before updating the dirty context.
** 317 OM  20200120          Small typo fixes in javadoc.
** 318 AIL 20200410          Added methods for handling eviction avoiding for temp records.
**         20200415          Reverted eviction handling.
** 319 IAS 20200413          Fixed DMO instantiation on SESSION:DATE-FORMAT, minor change
**                           to validate() (conditional ValidationException throw)
** 320 AIL 20200611          Moved buffer manager's buffer loaded notify after record loading.
** 321 OM  20191101          Used new persistence API.
** 322 OM  20200623          Added persistentProcedure member which stores the procedure who
**                           opened this buffer for the first time.
**     IAS 20200819          Added table CRUD ops statistics support
**     GES 20200731          Eliminated context-local lookups by using ErrorHelper.
**     ECF 20200909          Added variant of the release method which is invoked as part of an undo
**                           operation. Added handling for stale records. Reworked the logic in
**                           validate(boolean, boolean) logic to work better with the new ORM
**                           implementation.
**     SVL 20200913          isTemporary became public.
**     ECF 20200917          Backed out bracketing of DMO instantiation/copy with SESSION:DATE-FORMAT
**                           use, as it no longer had an effect with recent ORM changes.
**         20200919          We no longer force a refresh from the database when [re]loading a DMO.
**     CA  20200922          Optimized performance of record copy (be it from parameter access or buffer-copy),
**                           by avoiding the RecordBuffer$Handler overhead, for non-indexed, non-mandatory,
**                           non-trigger properties - for these, the access (getter or setter) is done 
**                           directly via the record's datum.
**     OM  20200924          Avoid validation and flush of read-only DMOs.
**     CA  20200924          Fixed a problem with CA/20200922 - when the DMO is a proxy, access to fields must
**                           be done via getter methods, to allow the proxy to de-reference the proper bound
**                           buffer instance.
**                           Replaced Method.invoke with ReflectASM.
**                           Replaced DMO property access from reflection to property's data handler.
**                           Added caches for the buffer's schema and data model ASTs.
**     ECF 20200925          Added variant of getSession which creates session if needed.
**     CA  20200927          Avoid context-local lookups by relying on the helper instance.
**     OM  20201001          Improved DMO manipulation performance by caching slow Property annotation access.
**     OM  20201002          Use DmoMeta cached information instead of map lookups.
**     CA  20201007          Fixed buffer definitions from internal procedures/functions - the 'defining 
**                           procedure' was incorrectly assumed as the caller, and not the currently executed
**                           external program.
**     AIL 20201210          Added faster buffer-copy only for buffers which compatible explicit signatures.
**     ECF 20201210          Reworked toString() to use legacy field names.
**     AIL 20201223          Extended support for buffer-copy in fast mode.
**         20210104          Fixed assign trigger checking when doing fast buffer copy.
**         20210129          Setting active buffer before doing fast buffer copy.
**     OM  20201120          Added DATA-SOURCE-MODIFIED and DECIMALS implementation. Made methods accessible
**                           to [serial] package. Fixed dataset events firing.
**     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.
**     OM  20210223          Invalidate index-based cache in the event of low-level SQL operations.
**     AIL 20210228          Allow markBatchDirty to mark this buffer as dirty in a batch context.
**     ECF 20210301          Fixed NPE in toString(Record).
**     OM  20210309          Do not use DmoMeta as key in TableMapper because temp-tables may share the same
**                           DmoMeta instance.
**     EVL 20210319          Added method to get field value that can be updated at runtime.  Will be used
**                           with higher priority over the legacy value.
**     AIL 20210322          Fixed write trigger firing on validate statement.
**     OM  20210323          Dropped the local field attribute map in favour of delegation to delegation to
**                           delegation to TableMapper.LFI.
**     OM  20210404          CLOB fields are created and returned with their CP set.
**     OM  20210419          After a record is successfully inserted in database table, the buffer storing it
**                           is removed from early inserts list of dirty context.
**     IAS 20210419          _UserTableStat counters' update
**     OM  20210429          Release the current record at the end of the scope and decrement the DMO use
**                           count to allow the persistence to evict it if not in use.
**     ECF 20210504          BufferManager API name change.
**     ECF 20210428          Fixed buffer-copy() pairs processing for individual extent field elements. Avoid
**                           unnecessary work in fastCopy for the quick-exit cases.
**     ECF 20210506          Made dmoInfo private. All external access must be through its getter method to
**                           prevent NPE when accessing this information via a proxy. Removed unused
**                           tableStats variable and setter method.
**     IAS 20210506          Added _UserTableStat-delete counter update, 
**                           TODO for _UserTableStat support, fixed Javadoc
**     ECF 20210511          Replaced DynamicTablesHelper.normalizeName with TextOps.rightTrimLower.
**     ECF 20210519          Use new Record APIs to track DMO "in-use" status, instead of BufferManager.
**     OM  20210521          Implemented trigger vetoing mechanism: if trigger return error CREATE and
**                           DELETE operations are aborted.
**     OM  20210603          Implemented vetoing mechanism for ASSIGN triggers, too.
**     ECF 20210612          Renamed hooks which are called upon opening/closing of outermost buffer scope
**                           from {open|close}ConnectionManagerScope to on{Open|Close}OutermostScope.
**     OM  20210628          Default the legacy name to dmoInfo.legacyTable if name is null or unknown.
**     ECF 20210805          Fixed nested calls to set and restore the current record's activeBuffer state.
**     ECF 20210820          Retrofit for query substitution parameter preprocessing fixes.
**     CA  20210920          When a buffer is deleted, all related queries must be reset (their predicate and
**                           buffer list).
**     ECF 20210924          Prevent validation, flushing of a new/changed record while it is being deleted.
**                           Other minor refactoring.
**     AL2 20211015          Use real referents instead of class defs when registering buffer.
**     CA  20210929          Fixed cases when rowid function is used in the pairs for BUFFER-COPY or 
**                           SAVE-ROW-CHANGES.
**     AL2 20211022          Look-up def only if this should be registered as pending.
**     ECF 20211122          Fix open buffer registration to register persistent procedure buffers at the
**                           global scope of the BufferManager.openBuffers scoped list.
**     ECF 20211124          Rolled back previous fix. Let BufferManager determine the proper scope at which
**                           to track an uninitialized, open buffer.
**     OM  20211209          Fixed regression when registering a buffer as a Finalizable with the TM.
**                           The dynamic buffers of TempTable are created at GLOBAL level.
**     OM  20211015          Added datasourceRowid() method.
**     CA  20211222          Fixed memory leak: remove the persistent procedure reference from the buffer,
**                           when it gets deleted.
**     ECF 20211230          Remove a newly created record from the record nursery if it is deleted before it
**                           is flushed.
**     ECF 20211231          Improved efficiency of last fix (only attempt to remove the deleted record from
**                           the nursery once).
**     CA  20220104          Fixed memory leak: when deleting a buffer, clear it from BufferManager regardless
**                           if this is a dynamic buffer or not.
**                           An attempt to fix a leak from ConnectionManager.pdbReferences - in finishedImpl,
**                           if the buffer's scope is still 0 and is active, then call onCloseOutermostScope.
**     AL2 20220119          Avoid the buffer-copy if the source and target have the same record instance.
**     CA  20220201          Any instance field used from a static method must be accessed via a getter or
**                           setter, as the reference may be a proxy buffer (fixed currentRecord, dmoProxy and
**                           bulkCopy usage).
**     IAS 20220218          Change visibility of the 'isUnknownMode' method to public.
**     CA  20220206          Each buffer knows the scope(s) at which it was registered with the allBuffers,
**                           openBuffers, loadedBuffers and byLegacyName registries, so that the processing
**                           during delete (at deregisterDynamicBuffer) will target explicit scopes, and will
**                           avoid iterating all scopes.
**                           Added 'dirtyBuffers' scoped dictionary, to track any buffer which had a Create,
**                           Update or Delete operation in the current scope. The scope is cleared only when
**                           the transaction block is processed, otherwise buffers are merged to previous 
**                           scope, but only if their record is actual 'dirty'.
**     CA  20220210          Removed Commitable.rollbackPending and its notifications, as there is no actual
**                           implementation of this method.
**     OM  20220228          Added byOuterQuery flag for marking situations when the buffer is empty because
**                           it is set by an OUTER-JOIN table.
**     OM  20220301          Merged [unknownMode] and [byOuterQuery] flags.
**     IAS 20220324          Re-worked LockTableUpdater.
**     OM  20220328          Count and return errors in [endBatch] method.
**     OM  20220506          Fixed snapshot used for OLD buffer in a WRITE trigger. Fixed buffer preparation
**                           for buffer copy operation.
**     IAS 20220506          Fixed 'formatProperty' for primitive arrays.
**     SVL 20220518          Added resetSnapshot().
**     OM  20220511          Fixed database trigger (WRITE and ASSIGN) regressions.
**         20220608          Allowed the record to be validated even if transient. The initial values of the
**                           fields might collide with existing records breaking the unique indexes.
**         20220629          In case of Buffer-copy, make sure the destination buffer is aware of the fact
**                           that its fields were altered and prepare it for the write trigger.
**     OM  20220707          Shared meta-knowledge about fields and properties of a record was collected in
**                           DmoMeta to avoid duplicating their collection and storage.
**     OM  20220718          Added associate(DataModelObject) method. Invoked WRITE trigger when a transient
**                           is flushed to make room for a newly created record.
**     CA  20220720          Instance fields can not be accessed directly from static methods - their getter
**                           must be used, as the buffer instance may be proxied.  Fixed OM/20220707.
**     OM  20220809          Use a temporary virtual scope while invoking the db triggers to block the 
**                           release() to be recursively invoked at the end of a trigger().
**     CA  20220901          Refactored scope notification support: ScopeableFactory was removed, and the 
**                           registration is now specific to each type of scopeable.  For each case, the block
**                           will be registered for scope support (for that particular scopeable) only when
**                           the scopeable is 'active' (i.e. unnamed streams or accumulators are used).  This
**                           allows a lazy registration of scopeables, to avoid the unnecessary overhead of
**                           processing all the scopeables for each and every block.
**     CA  20220906          BufferManager's undoable support is no longer being used, so it was removed.
**                           Extracted all BufferManager state which requires notification for 
**                           'isImportantBlock' to an external TxWrapper class (including the private class 
**                           here), to allow the scopeable BufferManager state related only to buffers (and  
**                           not transaction related) to be registered for scope notifications in a lazy 
**                           manner, when buffers are accessed/used/opened.
**     CA  20221006          Added JMX instrumentation for buffer define. Refs #6814
**                           Use a Supplier for the proxy plugin instance, as when the proxy is already 
**                           created, there is no need to use this instance.  Refs #6822
**                           Do not access tableHandle() in the FWD runtime, get the TempTable instance 
**                           directly.  Refs #6826
**     CA  20221010          Performance improvements - avoid the ProcedureData lookup, by keeping a parallel 
**                           stack of this data for THIS-PROCEDURE.  Refs #6826
**     OM  20221012          The TriggerTracker are bound to DMO, not to the buffer they are contained into.
**     OM  20221017          Moved {OM  20220809} changes from finishedImpl() to maybeFireWriteTrigger().
**     OM  20221019          Limited the number of "Unimplemented fast copy" warnings to 10.
**     OM  20221021          Quick out in create() if tableName is empty.
**     CA  20221101          Accessing a setter while not in a TRANSACTION will raise error 7369.
**     TJD 20220504          Upgrade do Java 11 minor changes
**     OM  20221125          Added referenceOnly method.
**     OM  20220603          All buffers are tracked if the table's TRACKING-CHANGES attribute is set.
**     CA  20220613          registerBindingBuffer must use as procedure the SOURCE-PROCEDURE in case of
**                           OUTPUT and THIS-PROCEDURE in case of INPUT, for BIND parameters.
**     OM  20220616          Fixed population of before-table in case of BUFFER-COPY operations.
**     OM  20220909          Allow unchanged transient records to be flushed even if no trigger is fired.
**     CA  20220909          The 'doNotProxy' method array must be a constant, otherwise it will leak at the
**                           ProxyFactory$CacheKey.DO_NOT_PROXY_HASH_CODES.
**     ECF 20221009          Fixed a NO-WAIT lock case.
**     CA  20221010          Performance improvements - avoid the ProcedureData lookup, by keeping a parallel 
**                           stack of this data for THIS-PROCEDURE.  Refs #6826
**     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
**     CA  20230110          'setterDatums' and 'getterDatums' are created only when they are first accessed 
**                           (via their getter methods).
**                           Removed 'cumulativeDirtyProps', 'scopeNameStack' and 'uniqueProperties' as these 
**                           are never used.
**                           'indexedProperties' is created when is actually accessed.
**     HC  20230116          Replaced some handle usages with the actual wrapped resources for performance.
**     CA  20230116          Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD
**                           runtime.
**     CA  20230123          isPropertyIndexed must have protected access, because is accessed from static
**                           methods.
** 323 ECF 20230301          Added isMutable to indicate that buffer represents a DMO generated from a scan of
**                           a mutable database.
** 324 CA  20230322          Do not modify the maps returned by 'LegacyFieldNameMap' in 'getPropsMap'.
**                           Fixed a case where BUFFER-COPY or BUFFER-COMPARE were using a 'tt1.r1,rowid(tt2)' 
**                           pair, which was not resolving properly.
** 325 DDF 20230327          Added definitelyHasRecords().
** 326 OM  20230215          Handled collision of temporary buffer names by temporarily overridding them.
**                           Avoid NPE in createSimplePropsMap() when property's accessors are not found.
** 327 IAS 20230407          Fixed 'getPropsMap'.
** 328 SR  20230510          Changed persistent.list() to match the new signature.
** 329 CA  202300505         The H327 change in 'getPropsMap' was incorrect - root cause was resolution of
**                           'recid' column when it was not qualified with a buffer name.
** 330 IAS 20230414          Make 'loadRecord' method public.
** 331 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 332 RAA 20230525          Disabled TriggerTracker for TemporaryBuffer.
** 333 CA  20230523          Performance improvement of the buffer fill operation - when possible (data type 
**                           match), both source and destination buffers), use direct access to read and write
**                           the value. Extent fields are not supported yet.
** 334 CA  20230530          ArgumentBuffer must get the definition buffer from the proxy buffer instance, and
**                           not the RecordBuffer bound instance.
** 335 GBB 20230616          Log msgs performance improvements & syntax cleanup.
** 336 CA  20230724          Further reduce context-local usage.
** 337 ES  20230911          Add call to markChangeScope for destination buffer at buffer-copy
** 338 OM  20230915          Added support for generic session triggers.
** 339 AL2 20230926          Reworked definatelyHasRecords into fastHasRecords. This returns only if we can
**                           make a sure statement on the fact that the underlying table has records or not.
** 340 DDF 20231128          Modified reportValidationException() method to handle the SQLException
**                           based on the dialect used.
** 341 ES  20231203          Added lenientOffEnd parameter to setRecord method. To know when QQE exception
**                           should be thrown
** 342 RAA 20231220          Made isTemporary a field of this class. Replaced instances of isTemporary()
**                           with isTemporary.
**     RAA 20231220          Fixed a case in which ReflectiveOperationException could have been thrown when
**                           a RecordBuffer was created through a proxy.
** 343 OM  20240117          Removed expired comment.
** 344 HC  20240222          Enabled JMX on FWD Client.
** 345 OM  20240201          Make sure the Write trigger is armed in case of fast-access copy operations.
** 346 CA  20240302          A BIND at the param or argument has effect only if either the argument has 
**                           BY-REFERENCE or the target is REFERENCE-ONLY. Removed the 'bindBuffer' support 
**                           which sets and unsets the DMO alias as this was determined to be obsolete and/or
**                           incorrect.
** 347 SB  20240311          Added the useOpenScope flag for armWriteTrigger. Refs #8404.
** 348 OM  20240319          Fixed a "merging (?)" error (duplicate if conditional).
** 349 CA  20240320          Avoid BDTs within internal FWD runtime.  Replaced iterators with Java 'for',  
**                           where it applies.
** 350 CA  20240404          Static temp-table or dataset resolution by name must be done using the currently  
**                           executing type and the static temp-table/dataset's defining type.
** 351 CA  20240409          Fixed 'getPropsMap' used by BUFFER-COPY() method - it must compute the property 
**                           name from the value of the corresponding src/dst field maps.
** 352 AI  20240402          Added check of index out of bounds for denormalized fields for getters or
**                           setters invoked on DMOs.
**     AI  20240409          Added return value after ErrorManager.recordOrThrowError.
** 353 AI  20240416          Fixed NPE when getter without arguments is used. 
** 354 AL2 20240409          Honor multiply referenced records (i.e. it is stored in more than one buffer)
**     Al2 20240412          Notify the buffer if the referenced record was changed, 
**                           so this can be reported as dirty. Also made converted RELEASE more aggresive.
** 355 EAB 20240417          Checked if batchAssignMode is true when the field for which we are trying to assign 
**                           a value is mandatory and that value is null.
** 356 OM  20240412          Mutable attribute is inherited by meta tables.
** 357 CA  20240423          Reduce the context-local lookup for TriggerTracker APIs.
** 358 AL2 20240517          Make validateMaybeFlush protected to honor possible argument proxy over the buffer.
** 359 EAB 20240423          Added sqlTableContentJson method.
** 360 AL2 20240617          Register buffer as finalizable if initialized, but its scope was not opened.
**     AL2 20240618          Changed initialize signature to include fromDefine parameter.
** 361 DDF 20240613          Do not release a record when closing the scope of the buffer if there is a DELETE
**                           trigger being executed, as the change of the record needs to be reported and the
**                           event released from the TriggerTracker.
**     DDF 20240620          Simplify condition that checks if a DELETE trigger is executing.
** 362 AL2 20240701          Added isBufferScopeLeaking to detect exceptional cases of buffer scope leaks.
** 363 AL2 20240517          Eagerly delete the DMO if transient and released in UNDO mode.
**     AL2 20240523          Use DirectAccessHelper instead of persistence delete.
**     AL2 20240708          Refactored finishedImpl to extract logic in a clean-up procedure.
** 364 AL2 20240708          Lower logging level for initialization without scope being opened.
** 365 RAA 20240613          Added tenant and database id parameters when calling FastFindCache.getInstance.
**     RAA 20240617          Rearranged ffCache parameters.
**     RAA 20240808          Adjusted FastFindCache to be retrieved from the persistence context.
** 366 AD  20240321          Ordered the methods in doNotProxy by their name.
** 367 CA  20240723          Added 'updateCachedRecords', to apply some code to cached temp-table records.
** 368 CA  20240809          Skip using JMX timers when JMX_DEBUG flag is not set.
** 369 CA  20240813          Assume OffEnd.NONE for a buffer just opening its scope, as no query is assumed
**                           of being active on the buffer, when its outermost scope is opened.
** 370 DDF 20240305          Made validateMaybeFlush() method protected to be invoked over the proxy,
**                           avoiding a NPE as proxies do not have a persistenceContext.
** 371 CA  20240827          Notify that a record has been flushed - for non-temp records, this will allow
**                           invalidation of the FastFindCache for that DMO. 
** 372 OM  20240901          Improved Database API. Concurrent multitenancy functionality implementation.
** 373 CA  20230924          Further reduce context-local lookup.
** 374 TJD 20221130          Prevent recursive Invoke calls of creatingBuffer handler.
**     TJD 20230329          Reset offEnd on the end of FOR/FOR EACH so other queries could continue iterating
**     TJD 20230417          Allow buffer locks rollback on full rollback
**     TJD 20230508          Updates needed to support dirty records leaks to other sessions on FIND
**     TJD 20230824          Dont use TransactionManager, use txHelper instead
**     TJD 20240110          Fixes for IndexOoB in toString
**     TJD 20240110          Fixes for DirtyShare cross session.
**     TJD 20240308          Call to shareDirty is not needed anymore
**     TJD 20240315          Moved extractDMOInterface to RecordBuffer
**     TJD 20240411          Updated 20230329 change to initialize offEnd at beginning of each iteration
**     TJD 20240619          Fixed check for array length in formatProperty
**     TJD 20240626          QueryOffEndListener needs also to reset RecordBuffer.snapshot
**     AL2 20240710          On delete, remove the record from record nursery no matter its state.
**     TJD 20240730          Allow offEnd.FRONT to be treated like offEnd.BACK for prev/last queries
**     TJD 20240821          Get FastFindCache from the persistence context fix.
** 375 AS  20241009          Added support for validating a subset of unique indices.
**     AS  20241009          Reworked setting an active buffer for the batch assign to be performed
**                           only once for every buffer inb the batch.
**     AS  20241009          Moved the restoring of the active state for the dirty batch
**                           asign buffers to cleanupBatchMode().
** 376 AL2 20241023          Manage the scoped registration of this buffer to buffersByDmoClass collection.
** 377 AL2 20240829          Removed dirty context work from setter handler.
**     AL2 20240903          Reworked recordInserted to no longer honor dirty share manager. The upcoming
**                           nursery remove will do the job.
**     AL2 20241112          Adapt to new nursery remove signature.
** 378 AP  20241009          Added new method to reset the sortIndex.
** 379 AOG 20241121          Updated condition of currentRecord.setActiveBuffer only when invoking setter.
** 380 AS  20241127          Set the active buffer for CLOB getters.
** 381 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
**                           based on [sharedDb] parameter.
**                           Implementation of [setMultiTenantAlternative] method.
** 382 ES  20241125          Added additional check for buffer flushing in case a buffer is not undoable 
**                           and not in the context of a agressiveFlushing.
** 383 ICP 20241202          In copy, added new error throw if invoke fails.
** 384 OM  20250114          Small optimization.
** 385 AP  20250129          Removed sqlTableContentJson method as its logic was moved to a specialized class.
** 386 ES  20250130          Changed the method call from generateUniqueBufferName to generateBufferName.
**     ES  20250205          Added token variable to uniquely identify the buffers.
** 387 DDF 20250131          Added createProxy() used to create a proxy in case of a passed buffer parameter
**                           that matches the expected type (two dmos, but the same temp-table), while the 
**                           existent proxy could not be "casted".
** 388 AS  20250220          Do not restore the active state of the buffers in copy/fastCopy. 
** 389 ICP 20250129          Used logical.TRUE and character.EMPTY_STRING to leverage cached values.
** 390 AS  20250220          Refactored validateMaybeFlush to report only if the current property is changed.
** 391 AP  20250226          Optimized record equality checking in updateCurrentChanged.
** 392 CA  20250303          Fixed a NPE in 'FIND CURRENT EXCLUSIVE-LOCK', when the current record was deleted.
**                           Added state-changed 'refresh buffers' event, to force reload of all buffers in
**                           current context, for the specified DMO.
** 393 OM  20250130          Added createMeta() for internal usage (backstage meta table management).
** 394 OM  20250130          Added support for readonly meta tables.
** 395 OM  20250410          Disable R/O flag for admin meta buffers.
** 396 LS  20250403          Updated getSession() to handle the PersistenceException using DBUtils.
** 397 ES  20250407          Use timeout parameter in the call to Persistence.load.
** 398 CA  20250423          A buffer can be passed to a method call, which has a different DMO iface, 
**                           because of resource-level annotations like XML-NODE-NAME.  Emit a 
**                           'RecordBuffer.bufferProxy' for this case, which will create a proxy.
**                           TODO: implement 'bufferProxy.
** 399 ES  20250509          Make ArgumentBuffer and bufferProxy proxies classes to have as a super class the proxy class
**                           from previous level and no plugin, as it would infer the methods for extent 
**                           fields from super class.
** 400 ES  20250508          Synchronize the curent buffer in case AUTO-SYNCHRONIZE attribute is set on true for
**                           CREATE, DELETE, RELEASE, BUFFER-COPY operations. 
*/

/*
** 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.lang.reflect.*;
import java.lang.reflect.Array;
import java.sql.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import org.apache.commons.lang3.tuple.*;
import org.roaringbitmap.*;
import com.goldencode.p2j.*;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.orm.types.*;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.meta.*;
import com.goldencode.p2j.persist.trigger.*;
import com.goldencode.p2j.schema.*;
import com.goldencode.p2j.uast.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.goldencode.proxy.*;
import com.goldencode.util.*;

/**
 * A container for a Data Model Object (DMO) instance which represents a
 * single database record (or the lack thereof). A record buffer provides an
 * interface for the persistence framework to deal with many different DMO
 * types in a consistent manner. It exposes several static methods which
 * permit client code to define a new buffer and to manipulate the data
 * record which backs that buffer.
 * <p>
 * Client code does not manipulate DMO implementation class instances
 * directly, nor does it manage instances of <code>RecordBuffer</code>
 * directly. Rather, DMOs are exposed to client code by proxy. Each DMO
 * defines a business-domain interface, which is the means by which client
 * code manages data records. A new buffer is defined using the {@link
 * #define} factory method. This method returns a dynamic proxy to a
 * particular DMO interface; however, the proxy initially is not backed by
 * any DMO implementation instance. A backing instance is only created by
 * either invoking the {@link #create} method, or by passing the proxy to
 * a query and successfully retrieving a data record. Prior to the creation
 * of a backing DMO implementation instance in a buffer by one of these
 * means, any invocation of the DMO interface methods against the proxy will
 * result in an exception. Likewise, if any attributes of the buffer which
 * require a backing record are manipulated before such a record is available,
 * an exception is thrown.
 * <p>
 * The dynamic proxy created at buffer definition actually implements more
 * than just the DMO-specific interface. It also implements the {@link
 * BufferReference} interface. This package private interface allows other
 * persistence framework classes to access the record buffer associated with
 * a DMO proxy, when the proxy is passed in as a parameter from client code.
 * The proxy {@link RecordBuffer.Handler invocation handler} will detect
 * method calls destined for this interface and will retrieve the record
 * buffer object reference.
 * <p>
 * The primary responsibility of the invocation handler, though, is to
 * dispatch DMO interface getter and setter method calls invoked against the
 * proxy, to the correct backing DMO. This decoupled architecture is
 * leveraged to provide support for the Progress early constraint checking
 * semantic, whereby not-null and uniqueness constraints are checked at
 * data value assignment time, rather than at record commit time.  We also
 * take advantage of this hook to perform reverse foreign record resolution.
 * <p>
 * Reverse foreign record resolution is the process whereby a foreign record
 * associated with the current record DMO must be updated at runtime, as
 * application code modifies the properties which represent the database
 * fields upon which Progress relied as a foreign key.  During conversion,
 * natural joins between tables are detected and any legacy foreign keys are
 * replaced with a foreign key which refers to the foreign table's surrogate
 * primary key.  Since this explicit foreign key reference did not exist in
 * the pre-conversion Progress code, we must maintain relational integrity by
 * manually updating this foreign key as the legacy key values change.  This
 * is the job of {@link AssociationSyncher} and its subclasses.  The DMO
 * proxy invocation handler is used to detect when the value of such a legacy
 * key is modified, and the foreign key is reset immediately, unless the
 * current context is running within an assignment batch (see below where
 * batch processing is described with respect to validation).  In the case of
 * an active batch, the foreign record resolution is deferred until the batch
 * ends.
 * <p>
 * In some cases, client code must make multiple updates to a DMO instance,
 * and validation must be deferred until the last update is made, where it
 * would not be meaningful to validate while the DMO is in an intermediate
 * state.  Batch assign mode is designed to handle this requirement.  Once
 * batch assign mode is {@link #startBatch started}, validation for all
 * buffers in the current context is deferred until batch mode is {@link
 * #endBatch ended}.  At this point, all buffers which were updated during
 * batch assign mode are validated, in the order in which they were updated.
 * <p>
 * Each new instance registers a lightweight implementation of the
 * <code>Undoable</code> interface to enable the {@link
 * com.goldencode.p2j.util.TransactionManager TransactionManager} to preserve
 * the state of the buffer, such that it can be restored in an undo situation.
 * <p>
 * This class implements the <code>Finalizable</code> interface, so that it
 * can release locks and records when a buffer scope closes.  The actual time
 * of release also depends upon the current state of the transaction manager.
 * <p>
 * This class implements the <code>Commitable</code> interface to enable the
 * {@link com.goldencode.p2j.util.TransactionManager TransactionManager} to
 * commit database transactions and subtransactions.
 * <p>
 * Upon instantiation, a buffer is associated with a logical database. From
 * that point on, it can only be used with that database instance.
 * <p>
 * In certain circumstances (for outer join queries, particularly), a record
 * buffer may not have a database record loaded, but must still be able to
 * service proxied, DMO method calls without failing.  <i>Unknown mode</i> is
 * intended to support this need.  A record buffer can temporarily be placed
 * into this mode, such that getter methods invoked on the associated proxy
 * return unknown value, instead of raising an error condition.  This mode
 * only is used internally by this package.
 * <p>
 * A similar feature is <i>temporary record mode</i>.  Not to be confused
 * with temp-table support (see {@link TemporaryBuffer}), this mode is used
 * in certain cases where the buffer must temporarily hold some record data,
 * in order to allow evaluation of an expression involving the associated
 * DMO.  This is done, for instance, when presorting preselected query
 * results, and when filtering query results against a client-side where
 * clause filter.  This mode only is used internally by this package.  The
 * query invokes this mode by invoking the {@link #setTempRecord} method first
 * with a non-<code>null</code> value, then executing the target expression
 * possibly repeating this sequence multiple times, and finally clears the
 * mode by invoking the <code>setTempRecord</code> method again, this time
 * with a <code>null</code> argument.  Setting the buffer's temp record does
 * not trigger all of the locking and other baggage normally associated with
 * setting the buffer's current record.
 * <p>
 * TODO:  combine many boolean flags into a bit field.
 * <p>
 * TODO:  currently, when there are multiple buffers defined for the same 
 *        table, the BufferManager keeps together all reversibles related 
 *        to the same table belonging to the same database. This ensures 
 *        proper reversible processing on rollback.
 *        <p>
 *        But, the rollback related part in {@link Commitable} can be reworked 
 *        to allow multiple buffers for a certain table in a database. This 
 *        will eliminate the small overhead of rolling back a buffer which
 *        has nothing to do (rollback was performed by one of its siblings).
 *        <p>
 *        Note: a case which needs to be taken into consideration and checked 
 *        is when the foreign-keys are enabled. In this case, if the child is 
 *        rolled back first in a Child.Parent relation, there might be some
 *        problems.
 * 
 * @see  CompoundQuery
 * @see  PreselectQuery
 * @see  PresortQuery
 * @see  RandomAccessQuery
 */
public class RecordBuffer
implements BufferReference,
           RecordChangeListener,
           Commitable,
           Finalizable
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(RecordBuffer.class.getName());

   /** Log message. Stored as constant to avoid regular concatenation. */
   private static final String LOG_MSG_SIGN_BROKEN = "Explicit signature checking is broken; unsuccessful " +
      "fast buffer copy. Fallback to classic implementation.";
   
   /** Log message. Stored as constant to avoid regular concatenation. */
   private static final String LOG_MSG_SIGN_UNIMPL = "Unimplemented fast copy for explicit signature " +
      "match, but different field order. Fallback to classic implementation.";

   /** Log message. Stored as constant to avoid regular concatenation. */
   private static final String LOG_MSG_DB_UNEXP_REF = "Buffer '%s' used in database expression references " +
      "unexpected database schema '%s'. Please check 'default-databases' configuration";
   
   /** Instrumentation for {@link #defineReadOnly} and {@link #define}. */
   private static final NanoTimer DEF_BUFFER = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmBufferDefinePerm);

   /** Array of methods not to be proxied */
   private static final Method[] doNotProxy;
   
   /** Array of methods not to be proxied from the {@link HandleResource}. */
   protected static final Method[] doNotProxyRes;

   static
   {
      List<Method> doNotProxyList = Arrays.asList(Buffer.class.getMethods());
      Collections.sort(doNotProxyList, (m1, m2) -> m1.toString().compareTo(m2.toString()));
      doNotProxy = new Method[doNotProxyList.size()];
      for (int i = 0; i < doNotProxyList.size(); i++) 
      {
         doNotProxy[i] = doNotProxyList.get(i);
      }
      
      try
      {
         doNotProxyRes = new Method[] { HandleResource.class.getDeclaredMethod("processResource") };
      }
      catch (NoSuchMethodException | 
             SecurityException e)
      {
         throw new RuntimeException(e);
      }
   }
   
   /** Possible buffer operation types */
   protected enum OperationType
   {
      /** Compare operation */
      COMPARE,
      
      /** Copy operation */
      COPY
   }
   
   /** Possible method types */
   protected enum MethodType
   {
      /** Statement */
      STATEMENT,
      
      /** Method */
      METHOD
   }
   
   /** Context-local ChangeBroker */
   protected final ChangeBroker changeBroker = ChangeBroker.get();
   
   /** Object which manages this and all buffers in the current context */
   protected final BufferManager bufferManager = BufferManager.get();
   
   /** The database trigger manager for this context. */
   protected final DatabaseTriggerManager dbTrigManager = DatabaseTriggerManager.get();
   
   /** Transaction helper object */
   protected final TransactionManager.TransactionHelper txHelper = bufferManager.getTxHelper();
   
   /** Block depth at which outermost scope was opened */
   protected int activeScopeDepth = -1;
   
   /** Number of open scopes for this buffer (open scopes can be nested) */
   protected int openScopeCount = 0;
   
   /**
    * If this buffer was ever registered with a persistent procedure, remember the scope it was
    * open at (-1 in fact because the assignment is performed (and later compared) before
    * incrementing the {@code openScopeCount}). If never registered with global scope it stays -1.
    * It is used to unregister from the global scope of ChangeBroker when {@code openScopeCount}
    * reaches back to this value.
    * This flag only applies to static buffers from procedures run in PERSISTENT mode.
    */
   protected int registeredWithGlobalScope = -1;
   
   /**
    * Flag indicating the finalizable processing for this buffer will be delayed for when the
    * external procedure gets deleted
    */
   protected boolean delayed = false;
   
   /** List of queries which should be closed when the scope of this buffer is closing */
   protected Set<P2JQuery> relatedQueries = null;
   
   /**
    * If {@code true} this flag makes the change-tracking of this dataset AFTER-BUFFER ignore
    * changes and skip storing changes to associated BEFORE-BUFFER. This is needed by 
    * REJECT-CHANGES when CHANGE-TRACKING is still on. Although the flag is visible for permanent
    * tables, it is {@code false} for them, and only changed for {@code TemporaryBuffer}s.
    */
   protected boolean ignoreBeforeTracking = false;
   
   /** Current persistence context. It is updated in multi-tenant environment, when the tenant changes. */
   protected Persistence.Context persistenceContext = null;
   
   /** Cached hash code */
   private final int hashCode;
   
   /** Logical database or database alias associated with this buffer */
   private final String ldbOrAlias;
   
   /** Invocation handler for DMO proxy associated with this buffer */
   private final Handler handler = new Handler();
   
   /**
    * Grouping DMO and Buffer interface associated with this buffer.
    * Ex: {@code dmo-path.Foo_1$Buf} or {@code dmo-path.Bar_1_1$Buf} for temp-tables.
    */
   private final Class<? extends Buffer> dmoBufInterface;
   
   /** 
    * Interface which defines DMO's API and {@code Table} metadata annotation.
    * (Ex: {@code dmo-path.Foo} or {@code dmo-path.Bar_1} for temp-tables).
    */
   private final Class<? extends DataModelObject> dmoInterface;
   
   /** The DMO meta information for this buffer. */
   private final DmoMeta dmoInfo;
   
   /** Map of {@link DatumAccess} setter instances, for each property. */
   private Map<String, DatumAccess> setterDatums = null;
   
   /** Map of {@link DatumAccess} getter instances, for each property. */
   private Map<String, DatumAccess> getterDatums = null;
   
   /** Set of getter methods of the current DMO interface */
   private final Set<Method> getters;
   
   /** Property changes pending submission to the ChangeBroker */
   private Object[][] unreportedChanges = null;
   
   /**
    * Helper object to manage database trigger information. DO NOT instantiate it, yet, wait for the buffer
    * to be populated and then use the factory to get the associated instance with respective DMO.
    */
   private TriggerTracker triggerTracker;
   
   /** Helper object to track unique constraint information for uncommitted transactions */
   private final UniqueTracker uniqueTracker;
   
   /** Context-local API for unique constraint tracker */
   private final UniqueTracker.Context uniqueTrackerCtx;
   
   /** Helper to use the ProcedureManager without any context local lookups. */
   protected final ProcedureManager.ProcedureHelper pm = ProcedureManager.getProcedureHelper();
   
   /**
    * Flag indicating if a buffer is exposed to the world until its creator procedure gets
    * deleted, in cases when its associated procedure is ran persistent
    */
   private boolean worldScope = false;
   
   /** Temporary override of legacy name. */
   private String overrideLegacyName = null;
   
   /** The old name of the buffer, after it was overridden by a proxy. */
   private String oldVariable = null;
   
   /** Legacy name of this buffer */
   private String variable;
   
   /** Legacy name of this buffer and transaction manager's lookup key for this buffer */
   private String token;
   
   /** Object which manages dirty read sharing across sessions */
   private DirtyShareContext dirtyContext = null;
   
   /** Buffer which created current, unflushed record, if different from this one */
   private RecordBuffer creatingBuffer = null;
   
   /** 
    * The persistent procedure which defined this buffer. This is null if the buffer
    * was not defined inside a persistent procedure.
    */
   private Object persistentProc = null;
   
   /** Name of (post-conversion) database table associated with this buffer */
   private String table = null;
   
   /** Buffer type (key consisting of database, table, and undoable flag) */
   private BufferType bufferType = null;
   
   /** Class which defines DMO's implementation */
   private final Class<? extends Record> dmoClass;
   
   /** Persistence service object */
   private Persistence persistence = null;
   
   /** Logical name of database associated with this buffer */
   private String logicalDatabase = null;
   
   /** Foreign association synchronization helper objects */
   private ArrayList<AssociationSyncher> assocSynchers = null;
   
   /** Names of properties which represent legacy foreign keys */
   private Set<String> legacyForeignKeys = null;
   
   /** Object which identifies modified indexes */
   private IndexHelper indexHelper = null;
   
   /** Properties which participate in any indexes of the backing table */
   private BitSet indexedProperties = null;
   
   /** Object which tracks locks pinned by this buffer */
   private PinnedLocks pinnedLocks = null;
   
   /** Convenience reference to associated DMO proxy */
   private BufferReference dmoProxy = null;
   
   /** DMO data record which currently backs this buffer */
   private Record currentRecord = null;
   
   /** Latest DMO data record to back this buffer */
   private Record snapshot = null;
   
   /** Temporary record stack used for purposes such as sorting */
   private List<Record> tempRecords = null;
   
   /** Helpers which null out foreign key references to deleted DMOs */
   private List<ForeignNuller> foreignNullers = null;
   
   /** Scope depth at which transient record was created */
   private int createScope = -1;
   
   /** Is current record newly created (not read from the database)? */
   private boolean newlyCreated = false;
   
   /**
    * Marks the {@code currentRecord} as being set from an OUTER query. In this case, the error 91,
    * {@code No <buffer> record is available.} is NOT emitted when a field is accessed in read-only mode but
    * the buffer is empty. This is somewhat logical because the buffer is expected not to be loaded with a
    * record.
    */
   private boolean unknownMode = false;
   
   /** Indicates if last find failed because record could not be locked */
   private boolean locked = false;
   
   /** Indicates if last find failed because criteria was ambiguous */
   private boolean ambiguous = false;
   
   /** Is a transaction commit necessary upon ending a batch assign? */
   private boolean commitPending = false;
   
   /** Is buffer currently the destination of a bulk copy operation? */
   private boolean bulkCopy = false;
   
   /** Scope at which iterate should not release the current record */
   private int iterateReleaseOverrideScope = -1;
   
   /** Sort index last used by a query updating the contents of this buffer */
   private SortIndex sortIndex = null;
   
   /** Off-end status of query which last updated this buffer */
   private OffEnd offEnd = OffEnd.NONE;
   
   /** Is current record actually a dirty copy from the dirty database? */
   private boolean dirtyCopy = false;
   
   /** Flag indicating the rollback is caused by a off-end query */
   private boolean offEndRollback = false;
   
   /** Flag indicating that the block to which the buffer is scoped is retrying */
   private boolean retrying = false;
   
   /** Flags this record buffer as readonly. Readonly buffers cannot be assigned. */
   private boolean readonly = false;
   
   /** Flags this record buffer as a "fake" buffer (e.g., the old buffer in a write trigger) */
   private boolean fake = false;
   
   /** This flags that the record held is a VST. */
   private final boolean vst;
   
   /** Flag that indicates whether this buffer is backed by a temporary table. */
   private final boolean isTemporary;
   
   /**
    * Flags whether the record stored has been write-touched. That is, whether a setter was invoked on the
    * DMO, regardless whether the value was not actually changed since the last load of the record. Unlike
    * {@code recordChanged} that tracks external changes (from other users), this boolean value tracks
    * write-touches only from current user.
    */
   private boolean isTouched = false;
   
   /**
    * Flag used to avoid recursive call while invoking ROW-UPDATE event procedure while another block is
    * finished.
    */
   private boolean firingRowUpdate = false;
   
   /** Determines if this buffer is dynamic. */
   private boolean dynamic = false;
   
   /**
    * Marker for a CREATE-ROW callback in process. When this flag is on, the buffer contains a transient
    * record but the before image was not yet created for it. In the event of a DELETE operation in a
    * subsequent callback, the record is simply discarded instead of creating a superfluous delete
    * before-image for it.
    */
   protected boolean createRowCallback = false;
   
   /** The query off-end listener associated with this record buffer. */
   private final QueryOffEndListener offEndListener = new QueryOffEndListener()
   {
      public void initialize() { offEndRollback  = true;  persistenceContext.useSession(); }
      public void finish() { offEndRollback = false; offEnd = OffEnd.NONE; persistenceContext.releaseSession(); }
      public void reset() { offEnd = OffEnd.FRONT;  setCurrentRecord(null, false, false, false, false, false); resetSnapshot(); }
   };
   
   /**
    * Flag to mark this buffer as changed by another user on a multi-user 
    * environment. Its value is will be returned by currentChanged() method.
    */
   private boolean recordChanged = false;
   
   /**
    * Temporary copy of the <code>currentRecord</code> used to detect if its value has changed
    * as a result of refreshing from db (FIND CURRENT / GET CURRENT).
    * This field is normally null except for armCurrentChanged/updateCurrentChanged bracketing.
    */
   private Record shadowCopyRecord = null;
   
   /** Number of fields in the legacy table associated with this buffer (initialized lazily) */
   private int numFields = -1;
   
   /** The undoable instance for this buffer. */
   private LightweightUndoable undoable = null;
   
   /** The metadata of the record to be stored in this buffer. */
   private RecordMeta metadata = null;
   
   /** The data model AST for this buffer.  Cached when a dynamic query is used with this buffer. */
   private DataModelAst dataModelAst;
   
   /** The table schema AST for this buffer.  Cached when a dynamic query is used with this buffer. */
   private ProgressAst schemaTableAst;
   
   /** The set of scopes where the buffer was registered with {@link BufferManager byLegacyName}. */
   private final RoaringBitmap byLegacyNameScopes = new RoaringBitmap();
   
   /** The set of scopes where the buffer was registered with {@link BufferManager allBuffers}. */
   private final RoaringBitmap allBufferScopes = new RoaringBitmap();

   /** The set of scopes where the buffer was registered with {@link BufferManager buffersByDMOBufClass}. */
   private final RoaringBitmap buffersByDmoClassScopes = new RoaringBitmap();
   
   /** The set of scopes where the buffer was registered with {@link BufferManager loadedBuffers}. */
   private final RoaringBitmap loadedBuffersScopes = new RoaringBitmap();
   
   /** The set of scopes where the buffer was registered with {@link BufferManager openBuffers}. */
   private final RoaringBitmap openBuffersScopes = new RoaringBitmap();
   
   /** Bi-directional map of legacy field names (lower case) to DMO property names */
   private LegacyFieldNameMap legacyFieldNameMap = null;
   
   /** Count the warnings with the intent of reducing the number of log messages. */
   private static int unimplementedFastCopyWarningCounter = 0;
   
   /** Cache of the list returned by {@link #recordBuffers()}. */
   private List<RecordBuffer> recordBuffers = null;
   
   /** The defining type where this buffer is constructed. */
   private final Class<?> definingType;
   
   /** Cache the trigger map from {@link TriggerTracker}. */
   private final Map<Object, TriggerTracker> triggerMap;

   /** Flag indicating OffEnd.FRONT is reversible to OffEnd.BACK in case of last/prev query*/
   private boolean reversibleOffend = false;
   
   /** Flag indicating that buffer is used as a destination in BUFFER-COPY operation for AUTO-SYNCHRONIZE. */
   private boolean bufferCopyDestination = false;
   
   /**
    * Implicit constructor. Should be used ONLY in the context of a proxy. An invocation handler
    * should manage the methods inside this instance.
    */
   protected RecordBuffer()
   {
      dmoBufInterface = null;
      hashCode = 0;
      getters = null;
      dmoInterface = null;
      triggerTracker = null;
      vst = false;
      dmoClass = null;
      ldbOrAlias = null;
      uniqueTracker = null;
      uniqueTrackerCtx = null;
      dmoInfo = null;
      isTemporary = false;
      definingType = null;
      triggerMap = TriggerTracker.getTriggerMap();
   }
   
   /**
    * Constructor. This class is only instantiated via the {@link #define} factory method.
    *
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API plus all {@link
    *          Buffer} methods.
    * @param   ldbOrAlias
    *          Logical name of database with which this buffer will be
    *          associated permanently, or an alias previously created for the
    *          target database.
    * @param   variable
    *          Name of the variable with which the record buffer is associated
    *          for the purposes of differentiating multiple buffers backed by
    *          the same DMO type.
    */
   protected <T extends Buffer> RecordBuffer(Class<?> def,
                                             Class<T> dmoBufIface,
                                             String ldbOrAlias,
                                             String variable)
   {
      this(def, dmoBufIface, ldbOrAlias, variable, -1);
   }
   
   /**
    * Constructor. This class is only instantiated via the {@link #define} factory method.
    *
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API plus all {@link Buffer} methods.
    * @param   ldbOrAlias
    *          Logical name of database with which this buffer will be associated permanently, or
    *          an alias previously created for the target database.
    * @param   variable
    *          Name of the variable with which the record buffer is associated for the purposes of
    *          differentiating multiple buffers backed by the same DMO type.
    * @param   blockDepth
    *          Zero-based depth of the block (from the outermost scope) at which the buffer is
    *          created or <code>-1</code> for the current scope.
    */
   protected <T extends Buffer> RecordBuffer(Class<?> def,
                                             Class<T> dmoBufIface,
                                             String ldbOrAlias,
                                             String variable,
                                             int blockDepth)
   {
      triggerMap = TriggerTracker.getTriggerMap();

      this.hashCode = super.hashCode();
      
      this.dmoBufInterface = dmoBufIface;
      
      // extract only the DMO schema-specific interface (i.e., exclude the Buffer interface)
      Class<? extends DataModelObject> dmoIface = null;
      Class<?>[] superIfaces = dmoBufIface.getInterfaces();
      int len = superIfaces.length;
      
      for (int i = 0; i < len; i++)
      {
         Class<?> next = superIfaces[i];
         if (DataModelObject.class.isAssignableFrom(next))
         {
            dmoIface = (Class<? extends DataModelObject>) next;
            break;
         }
      }
      
      if (dmoIface == null)
      {
         throw new IllegalArgumentException("Invalid DMO interface used to create RecordBuffer");
      }
      
      this.ldbOrAlias = ldbOrAlias;
      this.variable = variable;
      if (variable != null)
      {
         this.token = BufferManager.get().generateUniqueToken(variable);
      }
      this.dmoInfo = DmoMetadataManager.registerDmo(dmoIface);
      this.dmoInterface = dmoInfo.getAnnotatedInterface();
      this.dmoClass = dmoInfo.getImplementationClass();
      this.isTemporary = isTemporary();
      
      if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE, describe() + ":  constructing record buffer");
      }
      
      // set buffer as read-only if underlying table is read-only
      if (DmoMetadataManager.isReadOnly(dmoClass.getName()))
      {
         this.readonly = true;
      }
      
      // store the set of getter methods
      getters = PropertyHelper.pojoPropertiesByGetter(this.dmoInterface).keySet();
      
      initPinnedLockTypes();
      
      if (blockDepth == -1)
      {
         if (def == null)
         {
            // this can be null only for external programs.
            def = pm.getInstantiatingExternalProgramClass();
            if (def == null)
            {
               // we must be executing business logic, and not initiating a program/OE class
               Iterator<Class<?>> iter = pm.executingIterator();
               if (iter.hasNext())
               {
                  def = iter.next();
               }
               else
               {
                  throw new IllegalStateException("Could not determine the defining class!");
               }
            }
         }
         
         // the c'tor will be invoked in most cases from the program's default c'tor.
         // so, there will be no opened scope in P2J - and the registration with
         // the BufferManager will be leaked to the block from the parent program
         // which executed this program.
         // so, the registration is delayed until the next scope is entered.
         // if any exceptions are thrown during initialization, the registration
         // must not happen - so this was moved at the end of the c'tor.
         bufferManager.registerPending(def, this);
         
         this.definingType = def;
      }
      else
      {
         this.definingType = null;
         bufferManager.addToAllBuffersArray(blockDepth, this);
      }
      
      // get unique tracker for this buffer and unique tracker context for this user session
      uniqueTracker = UniqueTracker.getInstance(dmoInfo);
      uniqueTrackerCtx = UniqueTracker.getContext();
      
      // check whether this is a VST table
      vst = dmoInfo.isMeta() && dmoInfo.metaTable.category == MetadataManager.Category.VST;
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The buffer is created
    * in read-only mode, temporarily for the scope of a trigger. It is in associated with the
    * specified database.
    *
    * @param   <T>
    *          Record type.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API plus all {@link Buffer} methods.
    * @param   database
    *          Name of the database which will be associated with the new record buffer.
    * @param   roValue
    *          The read-only value.
    * @param   fake
    *          {@code true} to mark this buffer as a "fake" buffer.
    *
    * @return  An object which implements the specified interface, which has roValue as
    *          immutable data record.
    * 
    * @see     #isFake()
    */
   public static <T extends Buffer> T defineReadOnly(Class<?> def,
                                                     Class<T> dmoBufIface,
                                                     String database,
                                                     Record roValue,
                                                     boolean fake)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         return DEF_BUFFER.timerWithReturn(() -> defineReadOnlyImpl(def, dmoBufIface, database, roValue, fake));
      }
      else
      {
         return defineReadOnlyImpl(def, dmoBufIface, database, roValue, fake);
      }
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the specified database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Record type.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API plus all {@link
    *          Buffer} methods.
    * @param   database
    *          Name of the database which will be associated with the new
    *          record buffer.
    * @param   variable
    *          Name of the variable with which the record buffer is associated
    *          for the purposes of differentiating multiple buffers backed by
    *          the same DMO type.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    */
   public static <T extends Buffer> T define(Class<T> dmoBufIface, String database, String variable)
   {
      return define(dmoBufIface, database, variable, null);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the specified database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    *
    * @param   <T>
    *          Record type.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API plus all {@link Buffer} methods.
    * @param   database
    *          Name of the database which will be associated with the new
    *          record buffer.
    * @param   variable
    *          Name of the variable with which the record buffer is associated
    *          for the purposes of differentiating multiple buffers backed by
    *          the same DMO type.
    * @param   legacyName
    *          Legacy name of the buffer.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    */
   public static <T extends Buffer> T define(Class<T> dmoBufIface,
                                             String database,
                                             String variable,
                                             String legacyName)
   {
      return define(null, dmoBufIface, database, variable, legacyName, -1);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the specified database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    *
    * @param   <T>
    *          Record type.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API plus all {@link
    *          Buffer} methods.
    * @param   database
    *          Name of the database which will be associated with the new
    *          record buffer.
    * @param   variable
    *          Name of the variable with which the record buffer is associated
    *          for the purposes of differentiating multiple buffers backed by
    *          the same DMO type.
    * @param   legacyName
    *          Legacy name of the buffer.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    */
   public static <T extends Buffer> T define(Class<?> def,
                                             Class<T> dmoBufIface,
                                             String database,
                                             String variable,
                                             String legacyName)
   {
      return define(def, dmoBufIface, database, variable, legacyName, -1);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the specified database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Record type.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API plus all {@link
    *          Buffer} methods.
    * @param   database
    *          Name of the database which will be associated with the new
    *          record buffer.
    * @param   variable
    *          Name of the variable with which the record buffer is associated
    *          for the purposes of differentiating multiple buffers backed by
    *          the same DMO type.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   blockDepth
    *          Zero-based depth of the block (starting from the outermost scope) at which the
    *          buffer is defined or <code>-1</code> for the current scope.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    */
   public static <T extends Buffer> T define(Class<?> def,
                                             Class<T> dmoBufIface,
                                             String database,
                                             String variable,
                                             String legacyName,
                                             int blockDepth)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         Buffer[] res = new Buffer[1];
         
         DEF_BUFFER.timer(() ->
         {
            RecordBuffer buffer = new RecordBuffer(def, dmoBufIface, database, variable, blockDepth);
            
            res[0] = createProxy(dmoBufIface, buffer, legacyName);
         });
         
         return (T) res[0];
      }
      else
      {
         RecordBuffer buffer = new RecordBuffer(def, dmoBufIface, database, variable, blockDepth);
         
         return createProxy(dmoBufIface, buffer, legacyName);
      }
   }
   
   /**
    * Create a proxy for the given buffer instance, to act as an instance of the specified type, so it can be
    * passed as an argument to a direct Java method call.
    *
    * @param    ref
    *           The buffer instance which needs to be proxied.
    * @param    type
    *           The DMO type to which the reference will be proxied.
    * 
    * @return   A proxy to access the given reference as the given type.
    */
    public static <T extends Buffer> T bufferProxy(Buffer ref, Class<T> type)
    {      
       return (T) TemporaryBuffer.mutableBufferProxy(ref, type);
    }
   
   /**
    * When a buffer is passed as a parameter, a new instance should be created in order to
    * differentiate the references. Even if the buffer parameter is INPUT-OUTPUT, the DMO alias
    * and buffer name differ based on the reference used in the conversion. Thus, this will 
    * return a proxy to the buffer in order to deliberately override the DMO alias or buffer name
    * depending on the case. The proxy returned is a dummy {@link RecordBuffer}; any access to this
    * buffer should be done using the handler, otherwise inconsistent data will be returned.
    *
    * @param   <T> 
    *          Record type.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API plus all {@link Buffer} methods.
    * @param   aliasOf
    *          The buffer to be aliased.
    * @param   varName
    *          The converted variable name.
    * @param   legacyName
    *          The legacy variable name.
    *
    * @return  A proxy for the {@code aliasOf} buffer.
    */
   public static <T extends Buffer> T defineAlias(Class<T> dmoBufIface,
                                                  Buffer aliasOf,
                                                  String varName,
                                                  String legacyName)
   {      
      return makeArgumentBuffer(dmoBufIface, aliasOf, varName, legacyName);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    * The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer. <code>null</code> if
    *          the unnamed pool must be used.
    */
   public static void create(handle h, String table, String bufName, String widgetPool)
   {
      create(h,
             new character(table),
             new character(bufName),
             (widgetPool == null ? null : new character(widgetPool)));
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    * The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer. <code>null</code> if
    *          the unnamed pool must be used.
    */
   public static void create(handle h, String table, String bufName, character widgetPool)
   {
      create(h, new character(table), new character(bufName), widgetPool);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    * The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer. <code>null</code> if
    *          the unnamed pool must be used.
    */
   public static void create(handle h, String table, character bufName, String widgetPool)
   {
      create(h,
             new character(table),
             bufName,
             (widgetPool == null ? null : new character(widgetPool)));
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    * The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer. <code>null</code> if
    *          the unnamed pool must be used.
    */
   public static void create(handle h, String table, character bufName, character widgetPool)
   {
      create(h, new character(table), bufName, widgetPool);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    * The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer. <code>null</code> if
    *          the unnamed pool must be used.
    */
   public static void create(handle h, character table, String bufName, String widgetPool)
   {
      create(h,
             table,
             new character(bufName),
             (widgetPool == null ? null : new character(widgetPool)));
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    * The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer.
    */
   public static void create(handle h, character table, String bufName, character widgetPool)
   {
      RecordBuffer.create(h, table, new character(bufName), widgetPool);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    * The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer. <code>null</code> if
    *          the unnamed pool must be used.
    */
   public static void create(handle h, character table, character bufName, String widgetPool)
   {
      create(h, table, bufName, (widgetPool == null ? null : new character(widgetPool)));
   }
   
   /**
    * Dynamically creates a new buffer for a meta table  and assign it to handle h.
    * Note: this must only be used for _meta buffers.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   p
    *          The {@code Persistence} object.
    * @param   tableName
    *          The meta name for the .
    */
   public static void createMeta(handle h, Persistence p, String tableName, String ldbName)
   {
      if (p == null || !p.getDatabase().isMeta())
      {
         return; // no-op
      }
      
      Class<? extends Record> meta = TableMapper.getDMOClass("_meta", tableName);
      if (meta == null)
      {
         // some tables are store in the primary permanent database (_user / _sec-authentication-*)
         meta = TableMapper.getDMOClass(ldbName, tableName);
         if (meta == null)
         {
            // sorry, could not get the table from TableMapper
            return;
         }
         else
         {
            // switch to this new persistence object
            p = PersistenceFactory.getInstance(ldbName);
         }
      }
      DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(meta);
      
      DataModelObject dmo = createDynamicBufferForPermTable(
            dmoInfo.getAnnotatedInterface(), tableName, ldbName);
      
      RecordBuffer rb = ((BufferImpl) dmo).buffer();
      rb.initialize(false, p, p.getDatabase());
      rb.logicalDatabase = ldbName;
      rb.readonly = false;    // allow to write when metadata is processed by admin tools
      h.assign(dmo);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of {@code CREATE-BUFFER} statement that uses the table-name.
    * The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer. {@code null} if it is not explicitly specified.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer. {@code null} if
    *          the unnamed pool must be used.
    */
   public static void create(handle h, character table, character bufName, character widgetPool)
   {
      if (!WidgetPool.validWidgetPool(widgetPool))
      {
         return;
      }
      
      if (table.isUnknown() || (bufName != null && bufName.isUnknown()))
      {
         ErrorManager.recordOrThrowError(
            7333,
            "FOR TABLE phrase or CONNECT phrase in CREATE widget could not be evaluated",
            false);
         
         return;
      }
      
      String tableName = table.getValue();
      if (tableName.isEmpty())
      {
         ErrorManager.recordOrThrowError(
               7334,
               String.format("Could not create buffer object for table %s", tableName),
               false);
         return;
      }
      
      Persistence persistence = DynamicTablesHelper.findExistingTable(tableName);
      if (persistence == null)
      {
         ErrorManager.recordOrThrowError(
            7334,
            String.format("Could not create buffer object for table %s", tableName),
            false);
         
         return;
      }
      
      String[] parts = DynamicTablesHelper.parseTableName(tableName);
      tableName = parts[0];
      String actualBufName = (bufName != null) ? bufName.getValue() : tableName;
      
      DataModelObject dmo;
      if (!persistence.isTemporary())
      {
         Database database = persistence.getDatabase(Persistence.PRIVATE_CTX);
         String schema = DatabaseManager.getSchema(database);
         Class<? extends Record> dmoClass = TableMapper.getDMOClass(schema, tableName);
         DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoClass);
         String ldbName = ConnectionManager.get().getLDBName(database);
         dmo = createDynamicBufferForPermTable(dmoInfo.getAnnotatedInterface(), actualBufName, ldbName);
      }
      else
      {
         tableName = TextOps.rightTrimLower(tableName);
         BufferManager bufferManager = BufferManager.get();
         int blockDepth = bufferManager.getStaticTempTableDepth(tableName);
         if (blockDepth == -1)
         {
            throw new IllegalStateException("Cannot locate static temp table " + tableName);
         }
         
         StaticTempTable staticTempTable = bufferManager.getStaticTempTable(null, tableName);
         TemporaryBuffer master = staticTempTable.defaultBuffer();
         
         dmo = TemporaryBuffer.createDynamicBufferForTempTable(actualBufName, master);
      }
      
      RecordBuffer rb = get(dmo);
      if (!rb.isDynamic())
      {
         // most likely this will never happen as in both cases, dmo is already configured as
         // dynamic in the createDynamicBuffer..()
         rb.setDynamic();
         ((BufferImpl) dmo).interlink(); // HandleChain implementation
      }
      h.assign(dmo);
      ((BufferImpl) dmo).addToPool(widgetPool);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    */
   public static void create(handle h, String table, String bufName)
   {
      create(h, new character(table), new character(bufName), (character) null);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    */
   public static void create(handle h, String table, character bufName)
   {
      create(h, new character(table), bufName, (character) null);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    */
   public static void create(handle h, character table, String bufName)
   {
      create(h, table, new character(bufName), (character) null);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    */
   public static void create(handle h, character table, character bufName)
   {
      create(h, table, bufName, (character) null);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    */
   public static void create(handle h, String table)
   {
      create(h, new character(table), (character) null, (character) null);
   }
   
   /**
    * Creates a dynamic buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses the table-name.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   table
    *          The name of the table to create the buffer for.
    */
   public static void create(handle h, character table)
   {
      create(h, table, (character) null, (character) null);
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
    * a temporary table reference.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   tt
    *          The temporary table to create the buffer for.
    */
   public static void create(handle h, Temporary tt)
   {
      create(h, tt, (character) null);
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
    * a temporary table reference.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   tt
    *          The temporary table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    */
   public static void create(handle h, Temporary tt, String bufName)
   {
      create(h, tt, bufName, (character) null);
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
    * a temporary table reference.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   tt
    *          The temporary table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    */
   public static void create(handle h, Temporary tt, character bufName)
   {
      create(h, tt, bufName, "");
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
    * a temporary table reference. The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   tt
    *          The temporary table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer.
    */
   public static void create(handle h, Temporary tt, String bufName, String widgetPool)
   {
      create(h,
             tt,
             new character(bufName),
             (widgetPool == null ? null : new character(widgetPool)));
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
    * a temporary table reference. The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   tt
    *          The temporary table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer.
    */
   public static void create(handle h, Temporary tt, String bufName, character widgetPool)
   {
      create(h, tt, new character(bufName), widgetPool);
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
    * a temporary table reference. The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   tt
    *          The temporary table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer.
    */
   public static void create(handle h, Temporary tt, character bufName, String widgetPool)
   {
      create(h, tt, bufName, (widgetPool == null ? null : new character(widgetPool)));
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses
    * a temporary table reference. The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   tt
    *          The temporary table to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer.
    */
   public static void create(handle h, Temporary tt, character bufName, character widgetPool)
   {
      handle hsrc = new handle();
      // this needs the resource, not the RecordBuffer instance
      hsrc.assign((Buffer) RecordBuffer.get(tt).getDMOProxy());
      
      create(h, hsrc, bufName, widgetPool);
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
    * buffer already assigned to a handle.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   hsrc
    *          The handle to table or other table buffer to create the buffer for.
    */
   public static void create(handle h, handle hsrc)
   {
      create(h, hsrc, (character) null, (character) null);
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
    * buffer already assigned to a handle.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   hsrc
    *          The handle to table or other table buffer to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    */
   public static void create(handle h, handle hsrc, String bufName)
   {
      create(h, hsrc, bufName, (character) null);
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
    * buffer already assigned to a handle.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   hsrc
    *          The handle to table or other table buffer to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    */
   public static void create(handle h, handle hsrc, character bufName)
   {
      create(h, hsrc, bufName, (character) null);
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
    * buffer already assigned to a handle. The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   hsrc
    *          The handle to table or other table buffer to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer.
    */
   public static void create(handle h, handle hsrc, String bufName, String widgetPool)
   {
      create(h,
             hsrc,
             new character(bufName),
             (widgetPool == null ? null : new character(widgetPool)));
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
    * buffer already assigned to a handle. The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   hsrc
    *          The handle to table or other table buffer to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer.
    */
   public static void create(handle h, handle hsrc, String bufName, character widgetPool)
   {
      create(h, hsrc, new character(bufName), widgetPool);
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
    * buffer already assigned to a handle. The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   hsrc
    *          The handle to table or other table buffer to create the buffer for.
    * @param   bufName
    *          The name for the buffer.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer.
    */
   public static void create(handle h, handle hsrc, character bufName, String widgetPool)
   {
      create(h, hsrc, bufName, (widgetPool == null ? null : new character(widgetPool)));
   }
   
   /**
    * Dynamically creates a new buffer and assign it to handle h.
    * This is the correspondent of <code>CREATE-BUFFER</code> statement that uses a another
    * buffer already assigned to a handle. The new buffer will be created in the specified widget pool.
    *
    * @param   h
    *          The handle to which the created buffer is assigned to.
    * @param   hsrc
    *          The handle to table or other table buffer to create the buffer for.
    * @param   bufName
    *          The name for the buffer or <code>null</code> if it is not explicitly specified.
    * @param   widgetPool
    *          The name of the widget pool that contains the dynamic buffer.
    */
   public static void create(handle h, handle hsrc, character bufName, character widgetPool)
   {
      if (!WidgetPool.validWidgetPool(widgetPool))
      {
         return;
      }
      
      if (hsrc.isUnknown() || (bufName != null && bufName.isUnknown()))
      {
         ErrorManager.recordOrThrowError(
            7333,
            "FOR TABLE phrase or CONNECT phrase in CREATE widget could not be evaluated",
            false);
         
         return;
      }
      
      AbstractTempTable srcTable = null;
      RecordBuffer srcBuffer = null;
      DataModelObject dmo;
      
      Object src = hsrc.get();
      if (TempTable.class.isAssignableFrom(src.getClass()))
      {
         srcTable = (AbstractTempTable) src;
      }
      else if (DataModelObject.class.isAssignableFrom(src.getClass()))
      {
         srcBuffer = RecordBuffer.get((DataModelObject) src);
         srcTable = srcBuffer.getParentTable();
      }
      else
      {
         // unknown handle type
         String name = bufName != null ? bufName.getValue() : "";
         ErrorManager.recordOrThrowError(7334, "Could not create buffer object for table " + name, false);
         
         return;
      }
      
      if (srcTable != null)
      {
         // temporary buffer
         int blockDepth;
         TemporaryBuffer master;
         
         if (srcTable._dynamic())
         {
            TempTableBuilder ttb = (TempTableBuilder) srcTable;
            blockDepth = 0;
            master = (TemporaryBuffer) RecordBuffer.get((Temporary) ttb.defaultBufferHandleNative());
         }
         else
         {
            BufferManager bufMan = srcBuffer != null ? srcBuffer.bufferManager : BufferManager.get();
            StaticTempTable staticTempTable = (StaticTempTable) srcTable;
            blockDepth = bufMan.getStaticTempTableDepth(staticTempTable);
            master = staticTempTable.defaultBuffer();
         }
         
         if (master == null || blockDepth == -1)
         {
            throw new IllegalStateException();
         }
         
         String actualBufName = bufName != null ? bufName.getValue() : srcTable._name();
         dmo = TemporaryBuffer.createDynamicBufferForTempTable(actualBufName, master);
      }
      else
      {
         // permanent buffer
         String actualBufName = bufName != null ? bufName.getValue() : srcBuffer.getDmoInfo().legacyTable;
         
         dmo = createDynamicBufferForPermTable(srcBuffer.getDMOInterface(),
                                               actualBufName,
                                               srcBuffer.getLogicalDatabase());
      }
      
      RecordBuffer rb = RecordBuffer.get(dmo);
      if (!rb.isDynamic())
      {
         // most likely this will never happen as in both cases, dmo is already configured as
         // dynamic in the createDynamicBuffer..()
         rb.setDynamic();
         ((BufferImpl) dmo).interlink(); // HandleChain implementation
      }
      h.assign(dmo);
      ((BufferImpl) dmo).addToPool(widgetPool);
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>character</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static character recordNotAvailableCharacter(String buffer)
   {
      errorNotAvailable(buffer);
      return new character();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>comhandle</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static comhandle recordNotAvailableComhandle(String buffer)
   {
      errorNotAvailable(buffer);
      return new comhandle();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>date</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static date recordNotAvailableDate(String buffer)
   {
      errorNotAvailable(buffer);
      return new date();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>datetime</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static datetime recordNotAvailableDatetime(String buffer)
   {
      errorNotAvailable(buffer);
      return new datetime();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>datetimetz</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static datetimetz recordNotAvailableDatetimetz(String buffer)
   {
      errorNotAvailable(buffer);
      return new datetimetz();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>decimal</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static decimal recordNotAvailableDecimal(String buffer)
   {
      errorNotAvailable(buffer);
      return new decimal();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>handle</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static handle recordNotAvailableHandle(String buffer)
   {
      errorNotAvailable(buffer);
      return new handle();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>integer</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static integer recordNotAvailableInteger(String buffer)
   {
      errorNotAvailable(buffer);
      return new integer();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>int64</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static int64 recordNotAvailableInt64(String buffer)
   {
      errorNotAvailable(buffer);
      return new int64();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>logical</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static logical recordNotAvailableLogical(String buffer)
   {
      errorNotAvailable(buffer);
      return new logical();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>raw</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static raw recordNotAvailableRaw(String buffer)
   {
      errorNotAvailable(buffer);
      return new raw();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>recid</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static recid recordNotAvailableRecid(String buffer)
   {
      errorNotAvailable(buffer);
      return new recid();
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    * 
    * @return   An unknown value of the <code>rowid</code> type.
    * 
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   public static rowid recordNotAvailableRowid(String buffer)
   {
      errorNotAvailable(buffer);
      return new rowid();
   }
   
   /**
    * Open a new buffer scope for the record buffer which backs the specified
    * DMO.  This ensures that the {@link BufferManager} is tracking this
    * buffer for block scope entries and exits within transactions.
    *
    * @param   dmos
    *          Variable length list of DMO instances returned by previous
    *          calls to {@link #define}.
    */
   public static void openScope(DataModelObject... dmos)
   {
      for (DataModelObject dmo : dmos)
      {
         get(dmo).openScope();
      }
   }
   
   /**
    * Open a new buffer scope at the specified block depth for the record buffer which backs
    * the specified DMO. This ensures that the {@link BufferManager} is tracking this buffer for
    * block scope entries and exits within transactions.
    *
    * @param   blockDepth
    *          Zero-based depth of the block (starting from the outermost block) at which the
    *          buffer scopes are opened.
    * @param   dmos
    *          Variable length list of DMO instances returned by previous
    *          calls to {@link #define}.
    */
   public static void openScopeAt(int blockDepth, DataModelObject... dmos)
   {
      for (DataModelObject dmo : dmos)
      {
         get(dmo).openScopeAt(blockDepth);
      }
   }
   
   /**
    * Worker method that executes a given block of code in batch assign mode, by bracketing the call in 
    * {@link #startBatch} and {@link #endBatch} calls.
    *
    * @param    code
    *           The block of code to execute in batch assign mode.
    * 
    * @see      #startBatch
    * @see      #endBatch
    */
   public static void batch(Runnable code)
   {
      BufferManager bufMan = BufferManager.get();
      
      startBatch(bufMan, false);
      boolean batchError = true;
      
      try
      {
         code.run();
         batchError = false;
      }
      finally
      {
         endBatch(bufMan, batchError);
      }
   }
   
   /**
    * Worker method that executes a given block of code in batch assign mode, having the silent
    * error mode enabled.
    *
    * @param    code
    *           The block of code to execute in batch assign mode.
    *
    * @return   The value of the ERROR-STATUS:ERROR flag when the execution is complete. 
    * 
    * @see      #batch(Runnable)
    * @see      ErrorManager#silent(Runnable)
    */
   public static boolean silentBatch(Runnable code)
   {
      return ErrorManager.silent(() -> batch(code));
   }
   
   /**
    * Enter batch assign mode in the current context.  Batch assign mode allows multiple DMO property
    * assignments without incurring validation. Validation for all modified buffers in the current context is
    * performed when batch mode is ended for the current context.
    * 
    * @param   bufMan
    *          The {@link BufferManager} instance.
    * @param   internal
    *          Use {@code false} for a programmer required batch assign mode. These can be started at most one
    *          at each scope depth.<br>
    *          Use {@code true} for a FWD internal maintenance batch block when some data must be processed
    *          "in batch". In this case the depth, is not checked. Only one level of this can be added as the
    *          top-level. This mode must be be ended before starting any new batch. 
    *
    * @see     #endBatch
    *
    * @throws  IllegalStateException
    *          if batch mode already is active in the current block or was started but never
    *          ended in a previous block.
    */
   public static void startBatch(BufferManager bufMan, boolean internal)
   {
      if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE, Utils.describeContext() + ":  entering batch assign mode");
      }
      
      bufMan.startBatchAssignMode(internal);
   }
   
   /**
    * Exit batch assign mode in the current context, triggering pending
    * validation on all buffers which were modified while in this mode.
    * <p>
    * Implementation Note:  as a simplification, dirty buffers are validated
    * in their entirety, not only the fields that were modified during the
    * most recent batch.  However, this should be safe (though possibly less
    * efficient), since the buffers must already have been valid upon
    * entering batch mode.
    *
    * @param   bufMan
    *          The {@link BufferManager} instance.
    * @param   error
    *          {@code false} all assignments end without issues. Validate them.
    *          <p>
    *          {@code true} if some error occurred during the batch. In this case all assignments performed
    *          in the batch block MUST be reverted!<br>
    *          <strong>TODO: implement this case!</strong>
    *
    * @return  The number of errors (usually buffers which failed validation).
    *          If there is none, 0 is returned.
    *
    * @see     #startBatch
    *
    * @throws  IllegalStateException
    *          if the current context is not in batch mode when this method is invoked.
    * @throws  ErrorConditionException
    *          if validation fails on any buffer which was modified in batch mode;
    *          if there is any error accessing the database.
    */
   public static int endBatch(BufferManager bufMan, boolean error)
   {
      Map<RecordBuffer, Runnable> bufs = bufMan.endBatchAssignMode();
      int fails = 0;
      
      if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE,
                 "%s: exiting batch assign mode;  dirty buffers:  %s",
                 Utils.describeContext(),
                 bufs == null ? "none" : bufs.toString());
      }
      
      if (bufs == null)
      {
         throw new IllegalStateException("Cannot end batch assign mode; not currently in batch mode");
      }
      
      if (error)
      {
         // TODO: implement the rollback of all fields, attributes and variables altered in this batch
         cleanupBatchMode(bufs);
         return 0; // no errors occurred during this method
      }
      
      try
      {
         Iterator<RecordBuffer> iter = bufs.keySet().iterator();
         while (iter.hasNext())
         {
            RecordBuffer buffer = iter.next();
            boolean commitPending = buffer.isCommitPending();
            Persistence persistence = buffer.getPersistence();
            boolean exception = false;
            
            try
            {
               // in batch assign mode, standard validation happens before assign triggers are fired
               Record dmo = buffer.getCurrentRecord();
               buffer.validateMaybeFlush(dmo, false, false);
               
               // fire assign triggers, if any
               TriggerTracker tracker = buffer.getTriggerTracker();
               LinkedList<Pair<String, BaseDataType>> triggerFields = 
                     tracker == null ? null : tracker.getPendingBatchFields();
               if (triggerFields != null)
               {
                  Class<? extends Buffer> bufIface = buffer.getDMOBufInterface();
                  Buffer dmoProxy = (Buffer) buffer.getDMOProxy();
                  String ldb = ((BufferImpl) dmoProxy).buffer().logicalDatabase;
                  
                  try
                  {
                     for (Pair<String, BaseDataType> pair : triggerFields)
                     {
                        String prop = pair.getLeft();
                        if (tracker.isFieldInTrigger(prop))
                        {
                           // the field is allowed to be changed inside its trigger, but indirect
                           // recursion is an error
                           if (!tracker.isFieldCurrentTrigger(prop))
                           {
                              DatabaseTriggerManager.handleError(
                                 1003,
                                 DatabaseEventType.ASSIGN,
                                 buffer.getDmoInfo().legacyTable,
                                 TableMapper.getLegacyFieldName(buffer, prop));
                           }
                           
                           continue;
                        }
                        
                        // fire the trigger
                        tracker.pushField(prop);
                        DatabaseTriggerManager.triggerAssign(bufIface, dmoProxy, prop, pair.getRight(), ldb);
                        String top = tracker.popField();
                        assert prop.equals(top);  // the property must be the last one pushed
                     }
                  }
                  finally
                  {
                     // do the cleanup. The affected properties and their old values can be forgotten now
                     tracker.clearPendingBatchFields();
                  }
               }
               
//               buffer.syncForeignRelations(false);
               
               buffer.processChange();
            }
            catch (ValidationException exc)
            {
               buffer.clearUnreportedChanges();
               exception = true;
               reportValidationException(exc, buffer, false);
            }
            catch (PersistenceException exc)
            {
               exception = true;
               throw new RuntimeException(exc);
            }
            catch (ConditionException exc)
            {
               // ConditionException may occur into database triggers
               buffer.clearUnreportedChanges();
               exception = true;
               throw exc;
            }
            finally
            {
               if (exception)
               {
                  fails++;
                  buffer.setBufferCopyDestination(false);
               }
               
               if (!exception && buffer.bufferCopyDestination)
               {
                  Buffer buf = (Buffer) buffer.getDMOProxy();
                  if (buf.autoSynchronize().booleanValue())
                  {
                     buf.querySynchronize();
                  }
                  
                  buffer.setBufferCopyDestination(false);
               }
               
               if (commitPending)
               {
                  buffer.setCommitPending(false);
                  
                  try
                  {
                     if (!exception)
                     {
                        persistence.commit(!buffer.dmoInfo.multiTenant);
                     }
                     else
                     {
                        persistence.rollback(!buffer.dmoInfo.multiTenant);
                     }
                  }
                  catch (PersistenceException exc)
                  {
                     throw new RuntimeException(exc);
                  }
               }
            }
         }
      }
      finally
      {
         cleanupBatchMode(bufs);
      }
      return fails;
   }
   
   /**
    * Get code page of the specified CLOB field.
    *
    * @param  field
    *         field reference.
    *
    * @return code page of the specified CLOB field.
    */
   public static character getCodePage(FieldReference field)
   {
      RecordBuffer buf = field.getParentBuffer();
      return new character(buf.getCodePage(field.getProperty()));
   }
   
   /**
    * Get code page of the specified CLOB field.
    *
    * @param  field
    *         field reference.
    *
    * @return code page of the specified CLOB field.
    */
   public static character getCodePage(BufferField field)
   {
      BufferFieldImpl impl = (BufferFieldImpl) field;
      RecordBuffer buf = RecordBuffer.get((DataModelObject) impl.getDMOProxy());
      return new character(buf.getCodePage(impl.getProperty()));
   }
   
   /**
    * Get the backing, DMO and {@link Buffer} grouping interface for the given object. If the
    * object is not a {@link BufferReference} or is null, return null.
    * 
    * @param   obj
    *          The assumed DMO for which the interface is needed.
    *          
    * @return  The DMO's interface.
    */
   public static Class<?> getDMOBufInterfaceForObject(Object obj)
   {
      if (!(obj instanceof BufferReference))
      {
         return null;
      }
      
      return ((BufferReference) obj).buffer().getDMOBufInterface();
   }
   
   /**
    * Getter for doNotProxy array;
    * @return  The doNotProxy array.
    */
   public static Method[] getDoNotProxy()
   {
      return doNotProxy;
   }
   
   /**
    * Create a proxy object which client code will use to interact with the backing DMO. Create an
    * invocation handler for the proxy which intercepts method requests made on the proxy and
    * redirects them as necessary.
    *
    * @param   expectedType
    *          Interface which defines DMO's public API methods.
    * @param   argument
    *          Used to create a proxy based on the expected type.
    *
    * @return  An object which represents the proxy created from the expectedType to the argument's instance.
    */
   public static Object createProxy(Class<?> expectedType, Object argument)
   {
      Class<?>[] ifaces = Temporary.class.isAssignableFrom(expectedType) 
                             ? new Class<?>[] { expectedType, BufferReference.class, TempTableRecord.class } 
                             : new Class<?>[] { expectedType, BufferReference.class };
      return ProxyFactory.getProxy(BufferImpl.class,
                                   true,
                                   ifaces,
                                   false,
                                   doNotProxyRes,
                                   () -> 
                                   {
                                      Class<?> dmoBufIface = expectedType;
                                      Class<? extends DataModelObject> dmoIface = null;
                                      for (int i = 0; i < dmoBufIface.getInterfaces().length; i++)
                                      {
                                         Class<?> iface = dmoBufIface.getInterfaces()[i];
                                         if (DataModelObject.class.isAssignableFrom(iface) && 
                                             !Buffer.class.isAssignableFrom(iface))
                                         {
                                            dmoIface = (Class<? extends DataModelObject>) iface;
                                            break;
                                         }
                                      }
                                      
                                      return new DmoProxyPlugin(dmoBufIface, dmoIface);
                                   },
                                   new InvocationHandler() 
                                   {
                                      @Override
                                      public Object invoke(Object proxy, Method method, Object[] args) 
                                      throws Throwable
                                      {
                                         Method marg = argument.getClass()
                                                               .getMethod(method.getName(),
                                                                          method.getParameterTypes());
                                         return marg.invoke(argument, args);
                                      }
                                   });
   }
   
   /**
    * Create a proxy object which client code will use to interact with the backing DMO. Create an
    * invocation handler for the proxy which intercepts method requests made on the proxy and
    * redirects them as necessary.
    *
    * @param   <T>
    *          Record type.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API methods.
    * @param   buffer
    *          Record buffer instance associated with the created proxy.
    * @param   legacyName
    *          Legacy name of the buffer.
    *
    * @return  An object which implements the specified interface, but which initially has no
    *          backing data record.
    */
   protected static <T> T createProxy(Class<?> dmoBufIface, RecordBuffer buffer, String legacyName)
   {
      Class<?> dmoClass = buffer.getDMOClass();
      Class<?>[] interfaces = TempRecord.class.isAssignableFrom(dmoClass)
            ? new Class[] { dmoBufIface, BufferReference.class, TempTableRecord.class }
            : new Class[] { dmoBufIface, BufferReference.class };
      
      T dmoProxy = ProxyFactory.getProxy(BufferImpl.class,
                                         interfaces,
                                         false,
                                         doNotProxy,
                                         () -> new DmoProxyPlugin(dmoBufIface, buffer.getDMOInterface()),
                                         buffer.getHandler());
      
      buffer.setDMOProxy((BufferReference) dmoProxy);
      ((BufferImpl) dmoProxy).doSetName(legacyName == null ? buffer.getDmoInfo().legacyTable : legacyName);
      
      buffer.pm.processResource((WrappedResource) dmoProxy, false);
      
      return dmoProxy;
   }
   
   /**
    * Given two sets of legacy field names, an additional list of legacy field names (optional),
    * and a flag indicating whether the additional list is inclusive or exclusive, find the
    * appropriate intersection of field names.
    * <p>
    * The resulting set will contain at most the intersection of {@code set1} and {@code set2}.
    * However, this set may be further restricted if a non-{@code null} {@code fieldList} is
    * provided, as follows:
    * <ul>
    *   <li>if {@code exclusive} is {@code true}, all elements of {@code fieldList} are excluded
    *       from the intersection of {@code set1} and {@code set2}.
    *   <li>if {@code exclusive} is {@code false}, only those elements present in {@code set1}
    *       and in {@code set2} and in {@code fieldList} are included in the final result.
    * </ul>
    *
    * @param   set1
    *          First set of legacy field names.
    * @param   set2
    *          Second set of legacy field names.
    * @param   fieldList
    *          An optional array of legacy field names; may be {@code null}.
    * @param   exclusive
    *          if {@code true}, {@code fieldList} is an exclusive list of field names;
    *          if {@code false}, {@code propList} is an inclusive list. Ignored if {@code
    *          fieldList} is {@code null}.
    *
    * @return  The set of property names resulting from following the rules
    *          described above.
    */
   protected static Set<String> getCommonFields(Set<String> set1,
                                                Set<String> set2,
                                                String[] fieldList,
                                                boolean exclusive)
   {
      Set<String> props = null;
      
      // Do we have an explicit property list?
      if (fieldList != null)
      {
         Set<String> explicit = new HashSet<>(Arrays.asList(fieldList));
         if (exclusive)
         {
            // Find intersection of property sets.
            props = new HashSet<>(set1);
            props.retainAll(set2);
            
            // Exclude properties explicitly listed.
            props.removeAll(explicit);
         }
         else
         {
            // Include only properties explicitly listed.
            props = explicit;
            
            // Find intersection with property sets.
            props.retainAll(set1);
            props.retainAll(set2);
         }
      }
      else
      {
         // Find simple intersection of property sets.
         props = new HashSet<>(set1);
         props.retainAll(set2);
      }
      
      return props;
   }
   
   /**
    * Perform a bulk copy of data from a source record to a destination
    * record.  Data is copied for the properties listed in <code>propsMap</code>.
    * Other properties of the source and destination DMOs are ignored.
    *
    * @param   srcDMO
    *          Source DMO record (an actual record, not a proxy)
    * @param   dstDMO
    *          Destination DMO, which may be a dynamic proxy or the backing
    *          data model object.  A proxy should be used in cases where it
    *          is necessary to take advantage of flushing/validating logic
    *          implemented in the proxy invocation handler.  The backing DMO
    *          instance should be used if a raw copy is all that is needed.
    *          In the latter case, calling code is responsible for creating
    *          the record, optionally validating it, and persisting it to the
    *          database at the appropriate time.
    * @param   propsMap
    *          Map of property access info objects in source record buffer to those in destination
    *          record buffer
    * @param   pkProps
    *          When not null, a list of destination properties which need to be set to the source 
    *          DMO PK.
    *          
    * @return  <code>true</code> if copy is successful, otherwise <code>false</code>.
    *
    * @throws  IllegalAccessException
    *          if a getter or setter method cannot be invoked because it is not accessible.
    * @throws  InvocationTargetException
    *          if an invoked getter or setter method throws an exception.
    */
   protected static logical copy(DataModelObject srcDMO,
                                 DataModelObject dstDMO,
                                 Map<DatumAccess, DatumAccess> propsMap,
                                 List<DatumAccess> pkProps)
   throws IllegalAccessException,
          InvocationTargetException
   {
      /* TODO: update _UserTableStat counter(s) */
      Record srcRec = srcDMO instanceof BufferReference
                         ? ((BufferReference) srcDMO).buffer().getCurrentRecord() 
                         : (Record) srcDMO;
      Record dstRec;
      RecordBuffer dstRB = null;
      boolean hasBefore = false;
      if (dstDMO instanceof BufferReference)
      {
         dstRB = ((BufferReference) dstDMO).buffer();
         dstRec = dstRB.getCurrentRecord();
         if (dstRec == null)
         {
            // create it, if it does not exist
            dstRB.create();
            dstRec = dstRB.getCurrentRecord();
         }
         
         hasBefore = ((BufferImpl) dstDMO).isAfterBuffer() && dstRB.getParentTable()._isTrackingChanges();
      }
      else
      {
         // Note: if the destination is just a simple [Record] object, there is no buffer accessible to arm
         //       the [Write] trigger for it ([dstRB] remains null)
         dstRec = (Record) dstDMO;
         if (dstRec == null)
         {
            // no destination ?
            return new logical(false);
         }
      }

      for (Map.Entry<DatumAccess, DatumAccess> access : propsMap.entrySet())
      {
         DatumAccess srcAccess = access.getKey();
         DatumAccess dstAccess = access.getValue();
         Method getter = srcAccess.getAccessor();
         Method setter = dstAccess.getAccessor();
         Integer extent = srcAccess.getExtent();
         PropertyMeta srcProp = srcAccess.getPropertyMeta();
         PropertyMeta dstProp = dstAccess.getPropertyMeta();

         // if the destination has before table, the before-image must be updated
         boolean directAccess = !hasBefore && dstAccess.isDirectAccess(srcAccess);

         Object result = null;
         try
         {
            if (extent == null)
            {
               // invoke usual getter
               result = directAccess ? OrmUtils.getField(srcRec, srcProp.getOffset())
                  : Utils.invoke(getter, srcDMO);
            }
            else
            {
               Integer srcExtentIndex = srcAccess.getExtentIndex();
               if (srcExtentIndex != null)
               {
                  // invoke indexed getter
                  result = directAccess ? OrmUtils.getField(srcRec, srcProp.getOffset() + srcExtentIndex)
                     : Utils.invoke(getter, srcDMO, srcExtentIndex);
               }
            }

            if (extent == null || result != null)
            {
               Integer dstExtentIndex = dstAccess.getExtentIndex();
               if (dstExtentIndex == null)
               {
                  // invoke usual setter
                  if (directAccess)
                  {
                     armWriteTrigger(dstRB, false);
                     OrmUtils.setField(dstRec, dstProp.getOffset(), result);
                  }
                  else
                  {
                     Utils.invoke(setter, dstDMO, result);
                  }
               }
               else
               {
                  // invoke indexed setter
                  if (directAccess)
                  {
                     armWriteTrigger(dstRB, false);
                     OrmUtils.setField(dstRec, dstProp.getOffset() + dstExtentIndex, result);
                  }
                  else
                  {
                     Utils.invoke(setter, dstDMO, dstExtentIndex, result);
                  }
               }
            }
            else
            {
               // invoke all pairs of indexed getters and setters; assume extent sizes match
               for (int i = 0; i < extent; i++)
               {
                  result = directAccess ? OrmUtils.getField(srcRec, srcProp.getOffset() + i)
                     : Utils.invoke(getter, srcDMO, i);
                  if (directAccess)
                  {
                     armWriteTrigger(dstRB, false);
                     OrmUtils.setField(dstRec, dstProp.getOffset() + i, result);
                  }
                  else
                  {
                     Utils.invoke(setter, dstDMO, i, result);
                  }
               }
            }
         }
         catch (ErrorConditionException e)
         {
            if (e.getProgressErrorCode() == 15747)
            {
               //Couldn't update field of target in a BUFFER-COPY statement
               ErrorManager.recordOrShowError(5368, dstAccess.propertyMeta.getLegacyName());
               return logical.FALSE;
            }
            else
            {
               throw e;
            }
         }
      }

      if (pkProps != null)
      {
         rowid srcPk = new rowid(srcRec.primaryKey());
         for (DatumAccess da : pkProps)
         {
            try
            {
               Method setter = da.getAccessor();
               Utils.invoke(setter, dstDMO, srcPk);
            }
            catch (ErrorConditionException e)
            {
               if (e.getProgressErrorCode() == 15747)
               {
                  //Couldn't update field of target in a BUFFER-COPY statement
                  ErrorManager.recordOrShowError(5368, da.propertyMeta.getLegacyName());
                  return logical.FALSE;
               }
               else
               {
                  throw e;
               }
            }
         }
      }
      
      return logical.TRUE;
   }
   
   /**
    * Creates simple properties map with mapping properties with the same legacy names. The
    * {@code srcLegacyFieldsMap} and {@code dstLegacyFieldsMap} are used to obtain the properties
    * names and {@code srcGetters} and {@code dstSetters} are used to check if the types of
    * properties have the same type (are directly assignable).
    *
    * @param   legacyFields
    *          The list of fields to be mapped (legacy fields names).
    * @param   srcBuf
    *          The source buffer.
    * @param   dstBuf
    *          The destination buffer.
    * @param   srcGetters
    *          Map of source DMO methods.
    * @param   dstSetters
    *          Map of destination DMO methods.
    * @param   srcExtents
    *          Map of source extent properties.
    * @param   dstExtents
    *          Map of destination extent properties.
    * @param   srcLegacyFieldsMap
    *          Map of legacy field names to properties for source buffer.
    * @param   dstLegacyFieldsMap
    *          Map of legacy field names to properties for destination buffer.
    * @param   srcProps
    *          Map of source accessor methods to the property meta.
    * @param   dstProps
    *          Map of destination accessor methods to the property meta.
    * @param   srcDatums
    *          Map of source properties to their {@link DatumAccess} instance.
    * @param   dstDatums
    *          Map of destination properties to their {@link DatumAccess} instance.
    * @param   noLobs
    *          If {@code true}, exclude any BLOB/CLOB fields from the operation, else include
    *          these fields.
    * @param   operationType
    *          Operation type (COMPARE/COPY).
    *
    * @return  properties map with mapping property to the property with the same name of the 
    *          legacy fields.
    */
   protected static Map<DatumAccess, DatumAccess> createSimplePropsMap(Collection<String> legacyFields,
                                                                       RecordBuffer srcBuf,
                                                                       RecordBuffer dstBuf,
                                                                       Map<String, Method> srcGetters,
                                                                       Map<String, Method> dstSetters,
                                                                       Map<String, Integer> srcExtents,
                                                                       Map<String, Integer> dstExtents,
                                                                       Map<String, String> srcLegacyFieldsMap,
                                                                       Map<String, String> dstLegacyFieldsMap,
                                                                       Map<Method, PropertyMeta> srcProps,
                                                                       Map<Method, PropertyMeta> dstProps,
                                                                       Map<String, DatumAccess> srcDatums,
                                                                       Map<String, DatumAccess> dstDatums,
                                                                       boolean noLobs,
                                                                       OperationType operationType)
   {
      Map<DatumAccess, DatumAccess> copyMap = new HashMap<>();
      for (String lField : legacyFields)
      {
         String srcProp = srcLegacyFieldsMap.get(lField);
         String dstProp = dstLegacyFieldsMap.get(lField);
         
         DatumAccess srcDatum = srcDatums.get(srcProp);
         DatumAccess dstDatum = dstDatums.get(dstProp);
         
         Method srcGetter = srcDatum == null ? srcGetters.get(srcProp) : srcDatum.getAccessor();
         Method dstSetter = dstDatum == null ? dstSetters.get(dstProp) : dstDatum.getAccessor();
         
         if (srcGetter == null || dstSetter == null)
         {
            LOG.log(Level.SEVERE,
                    "Cannot copy '" + srcBuf.getDMOName() + "." + srcProp + "' to '" + 
                                      dstBuf.getDMOName() + "." + dstProp + "'.\n" +
                    "Getter is " + srcGetter + "\n" +
                    "Setter is " + dstSetter);
            continue;
         }
         
         if (isAssignableTypes(srcGetter, dstSetter, noLobs, operationType))
         {
            if (srcDatum == null)
            {
               PropertyMeta srcMeta = srcProps.get(srcGetter);
               
               srcDatum = new DatumAccess(srcProp, 
                                          null, 
                                          srcGetter, 
                                          srcExtents.get(srcProp), 
                                          srcMeta, 
                                          srcBuf.isPropertyIndexed(srcMeta.getOffset()),
                                          false);
               srcDatums.put(srcProp, srcDatum);
            }
            
            if (dstDatum == null)
            {
               PropertyMeta dstMeta = dstProps.get(dstSetter);
               
               // we are on a setter dst method, with a non-extent property which has an ASSIGN trigger
               boolean dstAssignTrigger = dstSetter.getReturnType() != Void.class &&
                        dstMeta.getOriginalExtent() == 0                          &&
                        dstBuf.getTriggerTracker() != null                        &&
                        dstBuf.getTriggerTracker().isTriggerEnabled(DatabaseEventType.ASSIGN, 
                                                                    dstProp, 
                                                                    dstBuf.logicalDatabase);
               
               dstDatum = new DatumAccess(dstProp, 
                                          null, 
                                          dstSetter, 
                                          dstExtents.get(dstProp), 
                                          dstMeta, 
                                          dstBuf.isPropertyIndexed(dstMeta.getOffset()),
                                          dstAssignTrigger);
               dstDatums.put(dstProp, dstDatum);
            }
            
            copyMap.put(srcDatum, dstDatum);
         }
      }
      return copyMap;
   }
   
   /**
    * Helper method to report and possibly log a validation exception. Since validation exceptions generally
    * are recoverable errors, they are logged only at debug level and finer.
    * 
    * @param   exc
    *          The validation exception.
    * @param   buffer
    *          The affected buffer.
    * @param   fieldUpdate
    *          if {@code true} the error message 142 is also displayed.
    */
   protected static void reportValidationException(ValidationException exc,
                                                   RecordBuffer buffer,
                                                   boolean fieldUpdate)
   {
      if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE, exc, "{%s} validation error", Utils.describeContext());
      }
      
      String bufName = buffer.getLegacyName();
      int[] errCodes = new int[fieldUpdate ? 2 : 1];
      String[] errMessages = new String[fieldUpdate ? 2 : 1];
      if (fieldUpdate)
      {
         errCodes[1] = 142;
         errMessages[1] = "Unable to update " + bufName + " Field";
      }
      
      // go up the exception chain and look for a SQL exception:
      Throwable cause = exc.getCause();
      while (cause != null                   &&
            !(cause instanceof SQLException) &&
            cause != cause.getCause())
      {
         cause = cause.getCause();
      }
      
      if (cause instanceof SQLException)
      {
         // 23001 - RESTRICT VIOLATION - restrict_violation
         // 23503 - FOREIGN KEY VIOLATION - foreign_key_violation
         // 23514 - CHECK VIOLATION - check_violation

         SQLException sqle = (SQLException) cause;
         if ("23502".equals(sqle.getSQLState()))
         {
            // 23502 - NOT NULL VIOLATION - not_null_violation
            // heuristically detect the sql name of the column (seems to be the first word double quoted)
            String msg = sqle.getMessage();
            String fieldName = "?FIELD?";
            int k1 = msg.indexOf('"') + 1;
            if (k1 > 0)
            {
               int k2 = msg.indexOf('"', k1);
               if (k2 > k1)
               {
                  fieldName = msg.substring(k1, k2);
               }
            }
            
            // heuristically convert from SQL name to legacy name
            Iterator<Property> fields = buffer.getDmoInfo().getFields(false);
            while (fields.hasNext())
            {
               Property prop = fields.next();
               if (prop.column.equalsIgnoreCase(fieldName)) // ignore-case not necessary 
               {
                  fieldName = prop.legacy;
                  break;
               }
            }
            
            // print the error message, defaulting to SQL column name if any heuristics failed
            errCodes[0] = 275;
            errMessages[0] = fieldName + " is mandatory, but has a value of ?";
         }
         else if (buffer.getDialect().isUniqueConstraintViolation(sqle))
         {
            // Depending on the dialect:
            // 23000 - INTEGRITY CONSTRAINT VIOLATION - integrity_constraint_violation
            // 23505 - UNIQUE VIOLATION - unique_violation
            errCodes[0] = 132;
            BitSet uIndex = null;
            RecordMeta meta = buffer.getCurrentRecord()._getRecordMeta();
            String[] uniqueIndexNames = meta.getUniqueIndexNames();
            String message = cause.getMessage().toUpperCase();
            for (int i = 0; i < uniqueIndexNames.length; i++)
            {
               String uniqueIndexName = uniqueIndexNames[i];
               if (message.contains(uniqueIndexName.toUpperCase()))
               {
                  uIndex = meta.getUniqueIndices()[i];
                  break;
               }
            }
            errMessages[0] = (uIndex != null)
                     ? Validation.getFailUniqueIndexMessage(uIndex, bufName, buffer.getCurrentRecord())
                     : bufName + " already exists with ? ?"; // failed to detect breached index
         }
      }
      
      if (errMessages[0] == null)
      {
         errCodes[0] = exc.getNumber();
         errMessages[0] = exc.getMessage();
      }
      ErrorManager.recordOrThrowError(errCodes, errMessages, true);
   }
   
   /**
    * Get a local instance of the schema dictionary which is configured to resolve references
    * related to the given list of buffers, when parsing a dynamic query or validation expression.
    * 
    * @param   buffers
    *          List of buffers referenced in the dynamic expression.
    * 
    * @return  Configured, context-local schema dictionary.
    */
   static SchemaDictionary getSchemaDictionary(List<Buffer> buffers)
   {
      // the set of schema names referenced by the given list of buffers, alphabetically sorted
      // to produce a consistent key to look up the cached SchemaDictionary
      SortedSet<String> inUseSchemata = null;
      
      // load program-specific databases (actually schema names); may be null if project is not
      // configured to specify default databases by package
      Set<String> defDbsByPkg = ProcedureManager.getDefaultDatabases();
      
      // collect the subset of schema names needed by the schema dictionary to resolve database
      // symbols for the tables in the given list of buffers
      if (defDbsByPkg != null && !buffers.isEmpty())
      {
         inUseSchemata = new TreeSet<>();
         
         // collect the names of schemas referenced by the buffers
         for (Buffer buf : buffers)
         {
            RecordBuffer recBuf = ((BufferImpl) buf).buffer();
            Database db = recBuf.getDatabase();
            if (db.isMeta())
            {
               db = db.toType(Database.Type.PRIMARY);
            }
            String schema = DatabaseManager.getSchema(db);
            
            // this is really just a sanity check, because if we hit a schema that wasn't in the
            // default-databases configuration, there is something wrong with the caller's
            // composition of the buffer list, or with the default-databases configuration
            if (defDbsByPkg.contains(schema) || ConnectionManager.connected(db.getName()).booleanValue())
            {
               inUseSchemata.add(schema);
            }
            else if (!db.isTemporary() && LOG.isLoggable(Level.WARNING))
            {
               LOG.log(Level.WARNING, LOG_MSG_DB_UNEXP_REF, recBuf.getDMOAlias(), schema);
            }
         }
      }
      
      return SchemaDictionary.localInstance(inUseSchemata);
   }
   
   /**
    * BUFFER-COPY statement runtime.
    *
    * Perform a bulk copy of data from the current source record (if any) to
    * the current destination record.  Data is copied between properties of
    * like name.  Other properties of the source and destination DMOs are
    * ignored.  If the destination buffer does not contain a current record,
    * one is automatically created.
    * <p>
    * If a list of source properties is provided, the copy behaves
    * differently, depending upon the value of <code>exclusive</code>:
    * <ul>
    *   <li>if <code>true</code>, the listed properties are excluded from the copy;
    *   <li>if <code>false</code>, the listed properties are the only properties copied.
    * </ul>.
    * <p>
    * The destination buffer is optionally validated after the copy.
    *
    * @param   srcDmo
    *          Source DMO instance returned by a previous call to {@link #define}.
    * @param   dstDmo
    *          Destination DMO instance returned by a previous call to {@link #define}.
    * @param   srcFields
    *          Names of those source properties to be included or excluded from the copy,
    *          depending upon <code>exclusive</code>.  May be <code>null</code>.
    * @param   exclusive
    *          <code>true</code> if <code>srcProps</code> represents an exclusive list;
    *          <code>false</code> if it represents an inclusive list.
    * @param   noLobs
    *          If {@code true}, exclude any BLOB/CLOB fields from the copy, else include these
    *          fields.
    * @param   validate
    *          <code>true</code> to validate the destination buffer after the copy.
    *
    * @throws  ErrorConditionException
    *          if destination buffer is validated and fails;  if there is no
    *          record in the source buffer.
    */
   static void copy(DataModelObject srcDmo,
                    DataModelObject dstDmo,
                    String[] srcFields,
                    boolean exclusive,
                    boolean noLobs,
                    boolean validate)
   {
      /* TODO: update _UserTableStat counter(s) */
      BufferReference dstProxy = (BufferReference) dstDmo;
      BufferReference srcProxy = (BufferReference) srcDmo;
      RecordBuffer srcBuf = srcProxy.buffer();
      RecordBuffer dstBuf = dstProxy.buffer();
      dstBuf.bufferManager.registerDirtyBuffer(dstBuf);
      
      srcBuf.initialize();
      dstBuf.initialize();
      
      dstBuf.setBufferCopyDestination(true);
      
      // Get source record.  It is an error if there is none.
      Record srcRec = srcBuf.getCurrentRecord();
      if (srcRec == null)
      {
         String msg = "Source element of a BUFFER-COPY statement has no record";
         ErrorManager.recordOrThrowError(5365, msg, false);
         
         return;
      }
      
      // get destination record.  Create one if necessary.
      Record dstRec = dstBuf.getCurrentRecord();
      if (dstRec == null)
      {
         dstBuf.create();
         dstRec = dstBuf.getCurrentRecord();
      }
      
      if (srcRec == dstRec)
      {
         // already the same instance of the record
         return;
      }
      
      boolean hasBefore = ((BufferImpl) dstProxy).isAfterBuffer() && 
                          dstBuf.getParentTable()._isTrackingChanges();
      
      // make sure the destination buffer is aware of the fact that its fields were altered and prepare it for
      // the write trigger
      armWriteTrigger(dstBuf, true);
      
      // try a fast copy first. If it worked, then we are done with the copy all together
      if (fastCopy(srcBuf, dstBuf, srcFields != null && srcFields.length > 0, noLobs, validate, hasBefore))
      {
         Buffer bufferImpl = (Buffer) dstBuf.getDMOProxy();
         
         if (validate)
         {
            if (bufferImpl.autoSynchronize().booleanValue())
            {
               bufferImpl.querySynchronize();               
            }
            
            dstBuf.setBufferCopyDestination(false);
         }
         
         return;
      }
      
      Map<String, String> srcLegacyFields = srcBuf.getLegacyFieldNameMap().getLegacyField2Name();
      Map<String, String> dstLegacyFields = dstBuf.getLegacyFieldNameMap().getLegacyField2Name();
      
      // establish the set of fields to be copied
      Set<String> lFields = getCommonFields(srcLegacyFields.keySet(),
                                            dstLegacyFields.keySet(),
                                            srcFields,
                                            exclusive);
      
      Map<DatumAccess, DatumAccess> copyMap = createSimplePropsMap(lFields,
                                                                   srcBuf,
                                                                   dstBuf,
                                                                   srcBuf.getDmoInfo().getLegacyGetterMap(),
                                                                   dstBuf.getDmoInfo().getLegacySetterMap(),
                                                                   srcBuf.getDmoInfo().getExtentMap(),
                                                                   dstBuf.getDmoInfo().getExtentMap(),
                                                                   srcLegacyFields,
                                                                   dstLegacyFields,
                                                                   srcBuf.getDmoInfo().getPropsByGetterMap(),
                                                                   dstBuf.getDmoInfo().getPropsBySetterMap(),
                                                                   srcBuf.getGetterDatums(),
                                                                   dstBuf.getSetterDatums(),
                                                                   noLobs,
                                                                   OperationType.COPY);
      
      copy(srcBuf, dstBuf, copyMap, null, null, validate);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    *
    * This method does a rough compare of any common fields, determined by name, data type, and
    * extent-matching, between the source buffer and the target buffer. The resulting logical
    * value is either TRUE or FALSE as a whole. A single field that does not compare causes the
    * entire buffer to return FALSE. If there are fields in one buffer that do not exist in the
    * other, they are ignored.
    *
    * @param   srcBuf
    *          Source record buffer.
    * @param   dstBuf
    *          Destination record buffer.
    * @param   mode
    *          If mode-exp is given, it must evaluate to either "binary" or "case-sensitive"
    *          to provide that type of comparison. BUFFER-COMPARE( ) method supports binary
    *          and case-sensitive comparisons between CLOB as well as CHARACTER fields.
    * @param   except
    *          A character expression that evaluates to a comma-separated list of field names
    *          to be excluded from the compare.
    * @param   pairs
    *          A character expression that evaluates to a comma-delimited list of field-name
    *          pairs to be compared.
    * @param   noLobs
    *          If {@code true}, BLOB and CLOB fields are ignored during the comparison. May be
    *          {@code false} or {@code null} to include such fields.
    *
    * @return  <code>true</code> if all fields values are equal, otherwise <code>false</code>.
    */
   static logical compare(RecordBuffer srcBuf,
                          RecordBuffer dstBuf,
                          BufferCompare.Mode mode,
                          character except,
                          character pairs,
                          logical noLobs)
   {
      if (!doBuffersExist(srcBuf, dstBuf, MethodType.METHOD))
      {
         return new logical(false);
      }
      
      // Get maps of source getter methods and destination setter methods,
      // each keyed by property name.
      Map<String, Method> dstGetters = dstBuf.getDmoInfo().getLegacyGetterMap();
      try
      {
         Map<DatumAccess, DatumAccess> propsMap = getPropsMap(srcBuf,
                                                              dstBuf,
                                                              dstGetters,
                                                              dstBuf.getDmoInfo().getPropsByGetterMap(),
                                                              dstBuf.getGetterDatums(),
                                                              except,
                                                              pairs,
                                                              noLobs,
                                                              OperationType.COMPARE,
                                                              null,
                                                              null);
         if (propsMap == null)
         {
            return new logical(false);
         }
         else
         {
            List<String> diffs = compare(srcBuf, dstBuf, propsMap, mode, false);
            
            return new logical(diffs != null && diffs.size() == 0);
         }
      }
      catch (ErrorConditionException e)
      {
         return new logical(false);
      }
   }
   
   /**
    * Create a dynamic buffer for the permanent table.
    *
    * @param  dmoIface
    *         DMO interface associated with the table.
    * @param  bufferName
    *         Target buffer name (4GL form).
    * @param  ldbName
    *         Logical name of permanent database associated with table.
    *
    * @return DMO proxy of the created buffer.
    */
   static <T extends Buffer> DataModelObject createDynamicBufferForPermTable(
         Class<?> dmoIface, String bufferName, String ldbName)
   {
      // TODO: not sure this is right...
      Class<T> bufClass = (Class<T>) dmoIface.getDeclaredClasses()[0];
      String uniqueBufferName = BufferManager.get().generateBufferName(bufferName);
      
      DataModelObject buffer = (DataModelObject) RecordBuffer.define(
            null, bufClass, ldbName, uniqueBufferName, bufferName, 0);
      RecordBuffer recordBuffer = RecordBuffer.get(buffer);
      recordBuffer.setDynamic();
      ((BufferImpl) buffer).interlink(); // HandleChain implementation
      
      recordBuffer.openScopeAt(0);
      
      return buffer;
   }
   
   /**
    * Copy the content of a DMO to another. Only matching fields are processed. Only the fields
    * are copied, the special and surrogate data is not copied. No errors are generated if fields
    * are not in pairs and copy fails for them. The multiplexer, peer rowid and row state are
    * not copied.
    *
    * @param   target
    *          The target DMO.
    * @param   source
    *          The source DMO.
    */
   static void copyCommonFields(Record target, Record source)
   {
      Method[] tMethods = target.getClass().getMethods();
      for (Method tMethod : tMethods)
      {
         String tmName = tMethod.getName();
         if (tmName.length() > 3 && !tmName.equals("setId") && tmName.startsWith("set"))
         {
            Method sMethod = null;
            String smName = (tMethod.getReturnType() == logical.class ? "is" : "get") +
                  tmName.substring(3);
            try
            {
               sMethod = source.getClass().getDeclaredMethod(smName);
            }
            catch (NoSuchMethodException e)
            {
               // silently skip this field
               continue;
            }
            
            try
            {
               Utils.invoke(tMethod, target, Utils.invoke(sMethod, source));
            }
            catch (IllegalAccessException | InvocationTargetException e)
            {
               // silently skip this field
            }
         }
      }
   }
   
   /**
    * BUFFER-COPY() handle-based method runtime.
    *
    * This method copies any common fields, determined by name, data type, and extent-matching,
    * from the source buffer to the receiving buffer. If there are fields in one buffer that do
    * not exist in the other, they are ignored. This method is used to accommodate temp-tables
    * of joins.
    *
    * @param   dstBuf
    *          Destination record buffer.
    * @param   srcBuf
    *          Source record buffer.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the copy process.
    * @param   pairs
    *          A character expression that evaluates to a comma-separated list of field-name
    *          pairs to be copied.
    * @param   noLobs
    *          If {@code true}, BLOB and CLOB fields are ignored during the comparison. May be
    *          {@code false} or {@code null} to include such fields.
    *
    * @return  <code>true</code> if copy is successful, otherwise <code>false</code>.
    */
   static logical copy(RecordBuffer dstBuf,
                       RecordBuffer srcBuf,
                       character except,
                       character pairs,
                       logical noLobs)
   {
      /* TODO: update _UserTableStat counter(s) */
      Record srcRec = srcBuf.getCurrentRecord();
      if (srcRec == null)
      {
         return new logical(false);
      }
      
      dstBuf.bufferManager.registerDirtyBuffer(dstBuf);
      
      boolean created = false;
      Record dstRec = dstBuf.getCurrentRecord();
      if (dstRec == null)
      {
         // create destination record if destination buffer is empty
         dstBuf.create();
         created = true;
         dstRec = dstBuf.getCurrentRecord();
      }
      
      BufferReference dstProxy = dstBuf.getDMOProxy();
      Buffer srcProxy = (Buffer) srcBuf.getDMOProxy();
      boolean hasBefore = ((BufferImpl) dstProxy).isAfterBuffer() && 
                          dstBuf.getParentTable()._isTrackingChanges();
      boolean hasPairs = !(pairs == null || pairs.isUnknown() || pairs.getValue().length() == 0);
      boolean hasExcept = !(except == null || except.isUnknown() || except.getValue().length() == 0);
      List<DatumAccess> pkDstProps = new ArrayList<>();
      List<DatumAccess> pkSrcProps = new ArrayList<>();
      
      try
      {
         // make sure the destination buffer is aware of the fact that its fields were altered and prepare it
         // for the write trigger
         armWriteTrigger(dstBuf, false);
         
         // try a fast copy first. If it worked, then we are done with the copy all together
         if (fastCopy(srcBuf, dstBuf, hasPairs || hasExcept, noLobs.getValue(), true, hasBefore))
         {
            if (dstBuf.bufferCopyDestination)
            {
               if (srcProxy.autoSynchronize().booleanValue())
               {
                  srcProxy.querySynchronize();                  
               }
               
               dstBuf.setBufferCopyDestination(false);
            }
            return new logical(true);
         }
         
         Map<String, Method> dstSetters = dstBuf.getDmoInfo().getLegacySetterMap();
         Map<DatumAccess, DatumAccess> propsMap = getPropsMap(srcBuf,
                                                              dstBuf,
                                                              dstSetters,
                                                              dstBuf.getDmoInfo().getPropsBySetterMap(),
                                                              dstBuf.getSetterDatums(),
                                                              except,
                                                              pairs,
                                                              noLobs,
                                                              OperationType.COPY,
                                                              pkDstProps,
                                                              pkSrcProps);
         if (propsMap != null)
         {
            return copy(srcBuf, dstBuf, propsMap, pkDstProps, pkSrcProps, true);
         }
      }
      catch (ConditionException exc)
      {
         if (created && dstRec == dstBuf.getCurrentRecord())
         {
            try
            {
               dstBuf.delete();
            }
            catch (PersistenceException pe)
            {
               // already in exception handler
            }
         }
         
         throw exc;
      }
      
      return new logical(false);
   }
   
   /**
    * Get the buffer instance which corresponds with the given DMO proxy.
    *
    * @param   dmo
    *          DMO instance returned by a previous call to {@link #define}.
    *
    * @return  Buffer associated with <code>dmo</code>.
    */
   static RecordBuffer get(DataModelObject dmo)
   {
      return ((BufferReference) dmo).buffer();
   }
   
   /**
    * Reset to default values the state variables related to batch assign mode.  If any dirty buffers have
    * commits pending, these are rolled back now.
    * 
    * @param   bufferManager
    *          Context-local {@code BufferManager} instance.
    */
   static void cleanupBatchMode(BufferManager bufferManager)
   {
      cleanupBatchMode(bufferManager.endBatchAssignMode());
   }
      
   /**
    * Reset to default values the state variables related to batch assign mode.  If any dirty buffers have
    * commits pending, these are rolled back now.
    * 
    * @param   bufs
    *          Map of dirty batch assign mode buffers with
    *          corresponding code that restores their active state.
    */
   static void cleanupBatchMode(Map<RecordBuffer, Runnable> bufs)
   {
      if (bufs == null)
      {
         return;
      }
      
      try
      {
         Iterator<RecordBuffer> iter = bufs.keySet().iterator();
         while (iter.hasNext())
         {
            RecordBuffer buffer = iter.next();
            if (buffer.isCommitPending())
            {
               buffer.setCommitPending(false);
               try
               {
                  buffer.getPersistence().rollback(!buffer.dmoInfo.multiTenant);
               }
               catch (PersistenceException exc)
               {
                  ErrorManager.recordOrThrowError(exc);
               }
            }
         }
      }
      finally
      {
          // Reset the active state for each dirty buffer in the batch assign
          Iterator<Runnable> iter = bufs.values().iterator();
          Runnable resetActiveState;
          while (iter.hasNext())
          {
             resetActiveState = iter.next();
             if (resetActiveState != null)
             {
                resetActiveState.run();
             }
          }
          bufs.clear();
      }
   }
   
   /**
    * Indicate whether two DMO instances represent the same record, as
    * determined by a comparison of their primary key identifiers.  To be
    * considered the same, neither record may be <code>null</code>, nor have
    * a <code>null</code> primary key identifier.
    *
    * @param   dmo1
    *          First record to compare.
    * @param   dmo2
    *          Second record to compare.
    *
    * @return  <code>true</code> if both records are non-<code>null</code>
    *          and have the same non-<code>null</code> primary keys.
    */
   static boolean areDMOsSame(Record dmo1, Record dmo2)
   {
      if (dmo1 == null || dmo2 == null)
      {
         return false;
      }
      
      Long id1 = dmo1.primaryKey();
      Long id2 = dmo2.primaryKey();
      
      if (id1 == null || id2 == null)
      {
         return false;
      }
      
      return id1.equals(id2);
   }
   
   /**
    * BUFFER-COMPARE statement runtime, returning the list of legacy fields that are different.
    *
    * Compare the contents of the properties which two DMO instances have in common. Common
    * properties are defined as having the same legacy name and data type. Other properties are
    * ignored for comparison purposes.
    * <p>
    * If a list of additional legacy field names is provided, the comparison behaves differently,
    * depending upon the value of {@code exclusive}:
    * <ul>
    *   <li>if {@code true}, the listed properties are excluded from the compare;</li>
    *   <li>if {@code false}, the listed properties are the only properties compared.</li>
    * </ul>.
    * <p>
    * <b>Implementation Note:</b>  this is not a full replacement for the Progress BUFFER-COMPARE
    * statement. In particular, it does not handle the EXPLICIT COMPARES option.
    *
    * @param   dmo1
    *          DMO instance returned by a previous call to {@link #define}.
    * @param   dmo2
    *          DMO instance returned by a previous call to {@link #define}.
    * @param   fieldList
    *          Legacy names of those fields to be included or excluded from the comparison,
    *          depending upon {@code exclusive}.  May be {@code null}.
    * @param   noLobs
    *          If {@code true}, exclude any BLOB/CLOB fields from the comparison, else include
    *          these fields.
    * @param   mode
    *          Comparison mode. May be {@code null}.
    * @param   exclusive
    *          {@code true} if {@code fieldList} represents an exclusive list; {@code false} if
    *          it represents an inclusive list.
    * @param   exhaustive
    *          If {@code true}, all common properties are compared; if {@code false}, the
    *          comparison ends immediately after the first time a pair of properties fails to
    *          compare as equal.
    *
    * @return  A list of names of those properties which failed to compare as equal. If empty, the
    *          two DMOs compared as equal overall; if not empty, the comparison determined one or
    *          more differences between them. Will return {@code null} only if one or both
    *          buffers are empty, indicating a comparison could not be performed.
    *
    * @throws  ErrorConditionException
    *          if one or both buffers are empty, such that a comparison could not be performed;
    *          if there is a problem extracting data from a buffer.
    */
   static List<String> compare(DataModelObject dmo1,
                               DataModelObject dmo2,
                               String[] fieldList,
                               boolean noLobs,
                               BufferCompare.Mode mode,
                               boolean exclusive,
                               boolean exhaustive)
   {
      RecordBuffer buf1 = ((BufferReference) dmo1).buffer();
      RecordBuffer buf2 = ((BufferReference) dmo2).buffer();
      if (!doBuffersExist(buf1, buf2, MethodType.STATEMENT))
      {
         return null;
      }
      
      Map<String, String> legacyFields1 = buf1.getLegacyFieldNameMap().getLegacyField2Name();
      Map<String, String> legacyFields2 = buf2.getLegacyFieldNameMap().getLegacyField2Name();
      
      // Establish the set of properties being compared.
      Set<String> fields = getCommonFields(legacyFields1.keySet(),
                                           legacyFields2.keySet(),
                                           fieldList,
                                           exclusive);
      
      Map<DatumAccess, DatumAccess> propMap = createSimplePropsMap(fields,
                                                                   buf1,
                                                                   buf2,
                                                                   buf1.getDmoInfo().getLegacyGetterMap(),
                                                                   buf2.getDmoInfo().getLegacyGetterMap(),
                                                                   buf1.getDmoInfo().getExtentMap(),
                                                                   buf2.getDmoInfo().getExtentMap(),
                                                                   legacyFields1,
                                                                   legacyFields2,
                                                                   buf1.getDmoInfo().getPropsByGetterMap(),
                                                                   buf2.getDmoInfo().getPropsByGetterMap(),
                                                                   buf1.getGetterDatums(),
                                                                   buf2.getGetterDatums(),
                                                                   noLobs,
                                                                   OperationType.COMPARE);
      
      return compare(buf1, buf2, propMap, mode, exhaustive);
   }
   
   
   /**
    * Creates a proxy with an argument buffer as handler. This should be used when 
    * a buffer is referenced through an argument inside a procedure (by default buffers are
    * input-output parameters). Although they are the same record buffer, the references
    * should be distinct as they have different DMO aliases. This is why an argument buffer is 
    * actually a proxy to the original record buffer, not the buffer itself. Super class of the
    * Proxy is the the class of the proxy from previous level. THe new proxy class generated will
    * have the following format:
    * 
    *  class Proxy extends proxy.getClass()
    *  implements dmoBufIface, BufferReference, TempTableRecord?
    *  {
    *     ...
    *  }
    * 
    * @param   dmoBufIface
    *          The DMO buffer interface.
    * @param   proxy
    *          The buffer proxy.
    * @param   varName
    *          The new DMO alias which should be adopted by the record buffer when referenced
    *          through this argument buffer.
    * @param   legacyName
    *          The name which should be adopted by the buffer impl when referenced through this 
    *          argument buffer.
    * @return  The argument buffer proxy.
    */
   static <T> T makeArgumentBuffer(Class<T> dmoBufIface, 
                                   Buffer proxy, 
                                   String varName, 
                                   String legacyName)
   {
      BufferImpl buffer = (BufferImpl) proxy;
      RecordBuffer rbuf = buffer.buffer();
      BufferManager.registerPendingScopeable(rbuf.bufferManager);

      Class<? extends DataModelObject> dmoIface = rbuf.getDMOInterface();
      Class<?> dmoClass = buffer.buffer().getDMOImplementationClass();
      
      Class<?>[] interfaces = TempRecord.class.isAssignableFrom(dmoClass)
            ? new Class[] { dmoBufIface, BufferReference.class, TempTableRecord.class }
            : new Class[] { dmoBufIface, BufferReference.class };
      
      try
      {
         InvocationHandler handler = new ArgumentBuffer(buffer, varName, legacyName);
         
         @SuppressWarnings("unchecked")
         T dmoProxy = ProxyFactory.getProxy(proxy.getClass(),
                                            true,
                                            interfaces,
                                            false,
                                            doNotProxyRes,
                                            null,
                                            handler);
         return dmoProxy;
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The buffer is created
    * in read-only mode, temporarily for the scope of a trigger. It is in associated with the
    * specified database.
    *
    * @param   <T>
    *          Record type.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API plus all {@link Buffer} methods.
    * @param   database
    *          Name of the database which will be associated with the new record buffer.
    * @param   roValue
    *          The read-only value.
    * @param   fake
    *          {@code true} to mark this buffer as a "fake" buffer.
    *
    * @return  An object which implements the specified interface, which has roValue as
    *          immutable data record.
    * 
    * @see     #isFake()
    */
   private static <T extends Buffer> T defineReadOnlyImpl(Class<?> def,
                                                          Class<T> dmoBufIface,
                                                          String database,
                                                          Record roValue,
                                                          boolean fake)
   {
      RecordBuffer buffer = new RecordBuffer(def, dmoBufIface, database, null);
      buffer.initialize(true);
      buffer.setCurrentRecord(roValue, false, false, false, false);
      buffer.setReadOnly(true);
      buffer.setFake(fake);
      
      return createProxy(dmoBufIface, buffer, null);
   }
   
   /**
    * Makes sure the specified record buffer has the trigger tracked armed (ready to fire at next flush).
    * If the buffer is null or does not have triggers, or it was already armed, nothing happens.
    * 
    * @param   rb
    *          The buffer to be armed.
    * @param   useOpenScope
    *          If {@code true} uses the number of open scopes to set DMO, else uses the
    *          scope depth at which transient record was created.
    */
   private static void armWriteTrigger(RecordBuffer rb, boolean useOpenScope)
   {
      if (rb == null)
      {
         return;
      }
      
      TriggerTracker tt = rb.triggerTracker;
      if (tt == null             || 
          tt.getOldDMO() != null || 
          !tt.isTriggerEnabled(DatabaseEventType.WRITE, null, rb.logicalDatabase))
      {
         return; // no tracker, or already armed
      }
      
      if (rb.snapshot == null)
      {
         rb.snapshot = rb.currentRecord.snapshot();
      }
      
      if (useOpenScope)
      {
         tt.setOldDMO(rb.snapshot, rb.bufferManager.getOpenBufferScopes());
      }
      else
      {
         tt.setOldDMO(rb.snapshot, rb.createScope);
      }
      
   }
   
   /**
    * This method establishes the map of properties being compared or copied.
    *
    * @param   srcBuf
    *          Source buffer for copy or compare operation.
    * @param   dstBuf
    *          Destination buffer for copy or compare operation.
    * @param   dstMethods
    *          Map of legacy field names to DMO getter or setter methods for the destination
    *          buffer.
    * @param   dstPropMeta
    *          Map of destination accessor methods to the property meta.
    * @param   dstDatums
    *          Map of destination properties to their {@link DatumAccess} instance.
    * @param   except
    *          A character expression that evaluates to a comma-separated list of field names
    *          to be excluded from the copy or compare operation.
    * @param   pairs
    *          A character expression that evaluates to a comma-delimited list of field-name
    *          pairs to be copied or compared.
    * @param   noLobs
    *          If {@code true}, exclude any BLOB/CLOB fields from the operation, else include
    *          these fields.
    * @param   opType
    *          Operation type (COMPARE/COPY).
    * @param   pkDstProps
    *          A list of <code>DatumAccess</code> objects where to copy the source PK (for pairs which are
    *          specified like <code>rowid(tt-src),tt-dst.f-rowid</code>).  If <code>null</code>, these 
    *          will not be reported back (BUFFER-COMPARE ignores these kind of mappings).
    * @param   pkSrcProps
    *          A list of <code>DatumAccess</code> objects where to copy the destination PK (for pairs which 
    *          are specified like <code>rowid(tt-src),tt-dst.f-rowid</code>).  If <code>null</code>, these 
    *          will not be reported back (BUFFER-COMPARE ignores these kind of mappings).
    *          
    * @return  A map of <code>DatumAccess</code> objects for the source buffer to objects of the
    *          same type for the destination buffer, which is used to perform the copy or compare
    *          operation, or <code>null</code> if an error occurs during the mapping analysis.
    * 
    * @throws  ErrorConditionException
    *          if any Progress-defined error occurs during the mapping analysis and not in silent
    *          error mode.
    */
   private static Map<DatumAccess, DatumAccess> getPropsMap(RecordBuffer              srcBuf,
                                                            RecordBuffer              dstBuf,
                                                            Map<String, Method>       dstMethods,
                                                            Map<Method, PropertyMeta> dstPropMeta,
                                                            Map<String, DatumAccess>  dstDatums,
                                                            character                 except,
                                                            character                 pairs,
                                                            logical                   noLobs,
                                                            OperationType             opType,
                                                            List<DatumAccess>         pkDstProps,
                                                            List<DatumAccess>         pkSrcProps)
   {
      Map<String, String> srcFieldMap = srcBuf.getLegacyFieldNameMap().getLegacyField2Name();
      Map<String, String> dstFieldMap = dstBuf.getLegacyFieldNameMap().getLegacyField2Name();
      
      // collect the intersection of matching field names between the buffers; they will be
      // checked for data type compatibility later
      Map<String, String> sharedFieldMap = new HashMap<>(srcFieldMap);
      sharedFieldMap.keySet().retainAll(dstFieldMap.keySet());
      // add the PK, too
      sharedFieldMap.put(Session.PK, Session.PK);
      
      // process fields to be excluded, if any
      if (except != null && !except.isUnknown() && except.getValue().length() > 0)
      {
         String excStr = except.toStringMessage();
         List<String> exceptFields = StringHelper.parseTokens(excStr, ",", true, true, false);
         
         for (String next : exceptFields)
         {
            FieldInfo info = null;
            try
            {
               info = new FieldInfo(next);
            }
            catch (IllegalArgumentException e)
            {
               // P4GL silently discards bad field names
               // spaces are NOT allowed within list
               continue;
            }
            if (info.getExtentIndex() >= 0)
            {
               // this appears to be an error which reports a message and aborts the operation,
               // but does not raise an error condition
               String msg = "Exclude or Include field list may not have elements with subscripts";
               ErrorManager.displayError(12414, msg, false);
               
               return null;
            }
            
            sharedFieldMap.remove(info.getFieldName());
         }
      }
      
      boolean skipLobs = noLobs != null && noLobs.booleanValue();
      
      Map<String, Method> srcMethods = srcBuf.getDmoInfo().getLegacyGetterMap();
      Map<String, Integer> srcExtents = srcBuf.getDmoInfo().getExtentMap();
      Map<String, Integer> dstExtents = dstBuf.getDmoInfo().getExtentMap();
      Map<Method, PropertyMeta> srcPropMeta = srcBuf.getDmoInfo().getPropsByGetterMap();
      
      // create props map for shared fields, which must match in name, type, and extent, else they
      // are discarded
      Map<DatumAccess, DatumAccess> propsMap = new HashMap<>();
      for (Map.Entry<String, String> sharedEntry : sharedFieldMap.entrySet())
      {
         String field = sharedEntry.getKey();
         String srcProp = srcFieldMap.get(field);
         String dstProp = dstFieldMap.get(field);
         
         DatumAccess srcDatum = srcBuf.getGetterDatums().get(srcProp);
         DatumAccess dstDatum = dstDatums.get(dstProp);
         
         Method srcMeth = srcDatum == null ? srcMethods.get(srcProp) : srcDatum.getAccessor();
         Method dstMeth = dstDatum == null ? dstMethods.get(dstProp) : dstDatum.getAccessor();
         if (dstMeth == null)
         {
            continue;
         }
         
         if (isAssignableTypes(srcMeth, dstMeth, skipLobs, opType))
         {
            Integer srcExtent = srcDatum == null ? srcExtents.get(srcProp) : srcDatum.getExtent();
            Integer dstExtent = dstDatum == null ? dstExtents.get(dstProp) : dstDatum.getExtent();
            
            if ((srcExtent == null && dstExtent == null) ||
                (srcExtent != null && dstExtent != null && srcExtent.equals(dstExtent)))
            {
               if (srcDatum == null)
               {
                  PropertyMeta srcMeta = srcPropMeta.get(srcMeth);
                  
                  srcDatum = new DatumAccess(srcProp,
                                             null,
                                             srcMeth,
                                             srcExtent,
                                             srcMeta,
                                             srcBuf.isPropertyIndexed(srcMeta.getOffset()),
                                             false);
                  srcBuf.getGetterDatums().put(srcProp, srcDatum);
               }
               
               if (dstDatum == null)
               {
                  PropertyMeta dstMeta = dstPropMeta.get(dstMeth);
                  
                  // we are on a setter dst method, with a non-extent property which has an ASSIGN trigger
                  boolean dstAssignTrigger =
                        dstMeth.getReturnType() != Void.class &&
                        dstMeta.getOriginalExtent() == 0      &&
                        dstBuf.getTriggerTracker() != null    &&
                        dstBuf.getTriggerTracker().isTriggerEnabled(DatabaseEventType.ASSIGN, 
                                                                    dstProp, 
                                                                    dstBuf.logicalDatabase);
                  
                  dstDatum = new DatumAccess(dstProp,
                                             null,
                                             dstMeth,
                                             dstExtent,
                                             dstMeta,
                                             dstBuf.isPropertyIndexed(dstMeta.getOffset()),
                                             dstAssignTrigger);
                  dstDatums.put(dstProp, dstDatum);
               }
               
               propsMap.put(srcDatum, dstDatum);
            }
         }
      }
      
      // if there is no explicit list of field pairs to be processed, we are done
      if (pairs == null || pairs.isUnknown() || pairs.getValue().length() == 0)
      {
         return propsMap;
      }
      
      // process pair string, which can be in any order, can have buffer name qualifiers and
      // extent index values
      String pairsValue = pairs.getValue();
      if (pairsValue != null && pairsValue.length() > 0)
      {
         List<String> pairsFields = StringHelper.parseTokens(pairsValue, ",", true, true, false);
         int size = pairsFields.size();
         
         // process each pair together
         // note: the order of each pair (destination vs. source) does not matter, as long as the
         // names are unambiguous, EXCEPT if a pair indicates 2 different elements of an extent
         // field whose name is common to both buffers, the first of the pair is the destination
         // and the second of the pair is the source; this works even if the fields are of
         // different extents, as long as each index specified is legal for that buffer
         for (int i = 0; i < size; i += 2)
         {
            // optimally would be to check the list size before processing it, but Progress 
            // processes it in groups of 2, only realizing very late that the list has an odd 
            // number of fields
            if (i + 1 >= size)
            {
               String msg = "Pairs list had too many pairs or odd number of entries";
               ErrorManager.recordOrThrowError(9035, msg, false);
               
               return null;
            }
            
            String fieldSpec1 = pairsFields.get(i);
            String fieldSpec2 = pairsFields.get(i + 1);
            
            // another feature of Progress: if the first item in a pair starts with @ character,
            // the entire pair is simply ignored.
            // TODO: * are there other characters with this property ?
            //       * what is the reason for this 'feature' 
            if (fieldSpec1.startsWith("@"))
            {
               continue;
            }
            
            if (fieldSpec1.toLowerCase().startsWith("rowid("))
            {
               fieldSpec1 = fieldSpec1.substring(fieldSpec1.indexOf('(') + 1, fieldSpec1.indexOf(')'));
               fieldSpec1 = fieldSpec1 + "." + Session.PK;
            }
            if (fieldSpec2.toLowerCase().startsWith("rowid("))
            {
               fieldSpec2 = fieldSpec2.substring(fieldSpec2.indexOf('(') + 1, fieldSpec2.indexOf(')'));
               fieldSpec2 = fieldSpec2 + "." + Session.PK; 
            }
            
            String srcPropF1 = null;
            String srcPropF2 = null;
            String dstPropF1 = null;
            String dstPropF2 = null;
            String srcProp = null;
            String dstProp = null;
            String srcKey = null;
            String dstKey = null;
            boolean ext1Valid = false;
            boolean ext2Valid = false;
            Integer srcExtent = null;
            Integer dstExtent = null;
            
            try
            {
               FieldInfo info1 = null;
               FieldInfo info2 = null;
               
               try
               {
                  // if the second field is bad, message is still about fieldSpec1
                  info1 = new FieldInfo(fieldSpec1);
                  info2 = new FieldInfo(fieldSpec2);
               }
               catch (IllegalArgumentException e)
               {
                  String msg = String.format("Pair with %s failed to have a member in each table",
                                             fieldSpec1);
                  if (opType == OperationType.COPY)
                  {
                     ErrorManager.recordOrThrowError(9034, msg, false);
                  }
                  else
                  {
                     ErrorManager.recordOrShowError(9034, msg, false, false);
                  }
                  
                  return null;
               }
               
               String field1 = info1.getFieldName();
               String field2 = info2.getFieldName();
               
               srcPropF1 = (srcBuf.checkBufferName(info1) ? srcFieldMap.get(field1) : null);
               srcPropF2 = (srcBuf.checkBufferName(info2) ? srcFieldMap.get(field2) : null);
               dstPropF1 = (dstBuf.checkBufferName(info1) ? dstFieldMap.get(field1) : null);
               dstPropF2 = (dstBuf.checkBufferName(info2) ? dstFieldMap.get(field2) : null);
               
               if (Session.PK.equals(field1))
               {
                  if (srcBuf.getLegacyName().equalsIgnoreCase(info1.getBufferName()))
                  {
                     srcPropF1 = Session.PK;
                  }
                  else if (dstBuf.getLegacyName().equalsIgnoreCase(info1.getBufferName()))
                  {
                     dstPropF1 = Session.PK;
                  }
                  else if (dstPropF2 != null)
                  {
                     // dst was alredy resolved, we can assume PK is for src
                     srcPropF1 = Session.PK;
                  }
               }
               if (Session.PK.equals(field2))
               {
                  if (srcBuf.getLegacyName().equalsIgnoreCase(info2.getBufferName()))
                  {
                     srcPropF2 = Session.PK;
                  }
                  else if (dstBuf.getLegacyName().equalsIgnoreCase(info2.getBufferName()))
                  {
                     dstPropF2 = Session.PK;
                  }
                  else if (srcPropF1 != null)
                  {
                     // src was alredy resolved, we can assume PK is for dst
                     dstPropF2 = Session.PK;
                  }
               }
               
               // last ditch effort: for any null results, try using the entire field spec as a
               // key, because even though the dot is used to delimit a buffer name from a field
               // name, it also is a legal character within a field name
               String full1 = info1.getFullSpec();
               String full2 = info2.getFullSpec();
               if (dstPropF1 == null)
               {
                  dstPropF1 = dstFieldMap.get(full1);
               }
               if (dstPropF2 == null)
               {
                  dstPropF2 = dstFieldMap.get(full2);
               }
               if (srcPropF1 == null)
               {
                  srcPropF1 = srcFieldMap.get(full1);
               }
               if (srcPropF2 == null)
               {
                  srcPropF2 = srcFieldMap.get(full2);
               }
               
               int e1 = info1.getExtentIndex();
               int e2 = info2.getExtentIndex();
               
               boolean isExtent = (e1 >= 0 || e2 >= 0);
               
               Integer extent1 = (e1 < 0 ? null : e1);
               Integer extent2 = (e2 < 0 ? null : e2);
               
               // try to match first of pair with destination and second of pair with end and if
               // that is not possible, try it the other way around; note that in the event the
               // field name exists in both the source and destination buffer, Progress always
               // seems to treat the first item as the destination and the second as the source
               if (srcPropF2 != null && dstPropF1 != null)
               {
                  ext2Valid = (e2 < 0 || srcBuf.isValidExtent(e2, srcPropF2));
                  ext1Valid = (e1 < 0 || dstBuf.isValidExtent(e1, dstPropF1));
                  
                  if (ext2Valid && ext1Valid)
                  {
                     srcProp = srcPropF2;
                     srcExtent = extent2;
                     dstProp = dstPropF1;
                     dstExtent = extent1;
                     
                     // lookup keys for DatumAccess objects must include the specific extent field element
                     srcKey = extent2 == null ? srcProp : srcProp + '[' + extent2 + ']';
                     dstKey = extent1 == null ? dstProp : dstProp + '[' + extent1 + ']';
                  }
               }
               else if (srcPropF1 != null && dstPropF2 != null)
               {
                  ext1Valid = (e1 < 0 || srcBuf.isValidExtent(e1, srcPropF1));
                  ext2Valid = (e2 < 0 || dstBuf.isValidExtent(e2, dstPropF2));
                  
                  if (ext1Valid && ext2Valid)
                  {
                     srcProp = srcPropF1;
                     srcExtent = extent1;
                     dstProp = dstPropF2;
                     dstExtent = extent2;
                     
                     // lookup keys for DatumAccess objects must include the specific extent field element
                     srcKey = extent1 == null ? srcProp : srcProp + '[' + extent1 + ']';
                     dstKey = extent2 == null ? dstProp : dstProp + '[' + extent2 + ']';
                  }
               }
               else
               {
                  // if we matched a destination buffer, but not a source buffer, check whether
                  // the error was an improper buffer qualifier on the source and report the
                  // error accordingly
                  if (dstPropF1 != null && !srcBuf.reportBadBufferName(info2, fieldSpec2))
                  {
                     return null;
                  }
                  if (dstPropF2 != null && !srcBuf.reportBadBufferName(info1, fieldSpec1))
                  {
                     return null;
                  }
                  
                  // report error if we didn't get hits in both tables
                  String msg = String.format("Pair with %s failed to have a member in each table",
                                             trimFieldSpec(fieldSpec1, info1));
                  if (opType == OperationType.COPY)
                  {
                     ErrorManager.recordOrThrowError(9034, msg, false);
                  }
                  else
                  {
                     ErrorManager.recordOrShowError(9034, msg, false, false);
                  }
                  
                  return null;
               }
               
               // report error if either specified extent is out of bounds
               if (isExtent && (!ext1Valid || !ext2Valid))
               {
                  String failedSpec;
                  FieldInfo failedInfo;
                  if (ext1Valid)
                  {
                     failedSpec = fieldSpec2;
                     failedInfo = info2;
                  }
                  else
                  {
                     failedSpec = fieldSpec1;
                     failedInfo = info1;
                  }
                  String msg = String.format("Array index %d for %s is out of range",
                                             failedInfo.getExtentIndex() + 1,
                                             trimFieldSpec(failedSpec, failedInfo));
                  ErrorManager.recordOrThrowError(11856, msg, false);
                  
                  return null;
               }
               
               // report error if extent types are mismatched in pair (i.e., one spec refers to a
               // scalar field (including a single element of an extent field, the other refers to
               // an extent field)
               boolean srcScalar = !srcExtents.containsKey(srcProp) || srcExtent != null;
               boolean dstScalar = !dstExtents.containsKey(dstProp) || dstExtent != null;
               if (srcScalar != dstScalar)
               {
                  String msg = String.format("Pair extents do not match for %s and %s",
                                             trimFieldSpec(fieldSpec1, info1),
                                             trimFieldSpec(fieldSpec2, info2));
                  ErrorManager.recordOrThrowError(11857, msg, false);
                  
                  return null;
               }
               
               // Progress also reports 11857 error both for BUFFER-COPY and BUFFER-COMPARE if
               // mapped fields have different extent values
               if (srcExtent == null &&
                   dstExtent == null &&
                   srcExtents.containsKey(srcProp) &&
                   dstExtents.containsKey(dstProp))
               {
                  // compare extent sizes
                  Integer srcExtentSize = srcExtents.get(srcProp);
                  if (srcExtentSize != null)
                  {
                     Integer dstExtentSize = dstExtents.get(dstProp);
                     if (!srcExtentSize.equals(dstExtentSize))
                     {
                        String msg = String.format("Pair extents do not match for %s and %s",
                                                   trimFieldSpec(fieldSpec1, info1),
                                                   trimFieldSpec(fieldSpec2, info2));
                        ErrorManager.recordOrThrowError(11857, msg, false);
                        
                        return null;
                     }
                  }
               }
            }
            catch (IllegalArgumentException exc)
            {
               // an invalid field spec does not cause an error, but the pair containing the
               // invalid entry is ignored
               if (LOG.isLoggable(Level.FINE))
               {
                  LOG.log(Level.FINE, "Invalid field specifier: ", exc);
               }
               continue;
            }
            
            // if we made it through all of the above checks, do one final test to determine that
            // the paired fields are data-type-compatible, then add them to the props map
            DatumAccess srcDatum = srcBuf.getGetterDatums().get(srcKey);
            DatumAccess dstDatum = dstDatums.get(dstKey);
            Method srcMeth = srcDatum == null ? srcMethods.get(srcProp) : srcDatum.getAccessor();
            Method dstMeth = dstDatum == null ? dstMethods.get(dstProp) : dstDatum.getAccessor();
            if (srcMeth == null && Session.PK.equals(srcProp))
            {
               srcMeth = BaseRecord.PK_GETTER;
            }
            if (dstMeth == null && Session.PK.equals(dstProp))
            {
               dstMeth = BaseRecord.PK_GETTER;
            }
            if (isAssignableTypes(srcMeth, dstMeth, skipLobs, opType))
            {
               if (srcDatum == null && srcMeth != BaseRecord.PK_GETTER)
               {
                  PropertyMeta srcMeta = srcPropMeta.get(srcMeth);
                  
                  srcDatum = new DatumAccess(srcProp,
                                             srcExtent,
                                             srcMeth,
                                             srcExtents.get(srcProp),
                                             srcMeta,
                                             srcBuf.isPropertyIndexed(srcMeta.getOffset()),
                                             false);
                  srcBuf.getGetterDatums().put(srcKey, srcDatum);
               }
               
               if (dstDatum == null && dstMeth != BaseRecord.PK_GETTER)
               {
                  PropertyMeta dstMeta = dstPropMeta.get(dstMeth);
                  
                  // we are on a setter dst method, with a non-extent property which has an ASSIGN trigger
                  boolean dstAssignTrigger =
                        dstMeth.getReturnType() != Void.class &&
                        dstMeta.getOriginalExtent() == 0      &&
                        dstBuf.getTriggerTracker() != null    &&
                        dstBuf.getTriggerTracker().isTriggerEnabled(DatabaseEventType.ASSIGN, 
                                                                    dstProp, 
                                                                    dstBuf.logicalDatabase);
                  
                  dstDatum = new DatumAccess(dstProp,
                                             dstExtent,
                                             dstMeth, 
                                             dstExtents.get(dstProp),
                                             dstMeta,
                                             dstBuf.isPropertyIndexed(dstMeta.getOffset()),
                                             dstAssignTrigger);
                  dstDatums.put(dstKey, dstDatum);
               }
               
               if (srcMeth == BaseRecord.PK_GETTER)
               {
                  if (pkDstProps != null)
                  {
                     pkDstProps.add(dstDatum);
                  }
               }
               else if(dstMeth == BaseRecord.PK_GETTER)
               {
                  if (pkSrcProps != null)
                  {
                     pkSrcProps.add(srcDatum);
                  }
               }
               else
               {
                  propsMap.put(srcDatum, dstDatum);
               }
            }
            else
            {
               String msg = "Datatypes in pairlist argument for ATTACH-DATA-SOURCE, BUFFER-COPY" +
                            " or BUFFER-COMPARE methods must match for fields " + fieldSpec1 + 
                            " and " + fieldSpec2;
               ErrorManager.recordOrThrowError(12860, msg, false);
               
               return null;
            }
         }
      }
      
      return propsMap;
   }
   
   /**
    * Trim the extent dereference portion (if any) from a field specifier and return the root
    * specifier, including buffer name qualifier (if any).
    * 
    * @param   fieldSpec
    *          Full field specifier.
    * @param   fieldInfo
    *          <code>FieldInfo</code> object which represents the given specifier.
    * 
    * @return  The trimmed field specifier.
    */
   private static String trimFieldSpec(String fieldSpec, FieldInfo fieldInfo)
   {
      int index = fieldInfo.getExtentIndex();
      if (index < 0)
      {
         return fieldSpec;
      }
      
      int trim = String.valueOf(index).length() + 2;
      
      return (fieldSpec.substring(0, fieldSpec.length() - trim));
   }
   
   /**
    * Helper method to format a property value. To be used for debug only.
    * 
    * @param   value
    *          {@code BaseDataType} or {@code Object} property value.
    * 
    * @return  Formatted string representation of the value.
    */
   private static String formatProperty(Object value)
   {
      if (value == null)
      {
         return "?";
      }
      else if (value instanceof BaseDataType)
      {
         return ((BaseDataType) value).toStringMessage();
      }
      else if (value.getClass().isArray())
      {
         if (value.getClass().getComponentType().isPrimitive())
         {
            int arrlength = Array.getLength(value);
            if (arrlength == 0)
            {
               return "[]";
            }
            
            StringBuilder b = new StringBuilder();
            b.append('[');
            for (int i = 0; ; i++)
            {
               if (i == arrlength)
               {
                  return b.append(']').toString();
               }
               b.append(Array.get(value, i));
               b.append(", ");
            }
         }
         return Arrays.toString((Object[]) value);
      }
      
      return String.valueOf(value);
   }
   
   /**
    * This method checks that two getter or setter methods have compatible corresponding return
    * or parameter types for given operation type.
    *
    * @param   srcMethod
    *          Source method for given operation.
    * @param   dstMethod
    *          Destination method for given operation.
    * @param   noLobs
    *          If {@code true}, return {@code false} if the source method has a BLOB/CLOB return
    *          type.
    * @param   operationType
    *          Operation type (COMPARE/COPY).
    *
    * @return  <code>true</code> if methods are compatible, otherwise <code>false</code>.
    */
   private static boolean isAssignableTypes(Method srcMethod,
                                            Method dstMethod,
                                            boolean noLobs,
                                            OperationType operationType)
   {
      if (operationType == OperationType.COMPARE)
      {
         // values for srcParameterTypes.length and dstParameterTypes.length (both getters)
         // are 0, 1:
         // getF1() and getF2() or
         // getF1(int index) and getF2(int index)
         Class<?>[] srcParameterTypes = srcMethod.getParameterTypes();
         Class<?>[] dstParameterTypes = dstMethod.getParameterTypes();
         if (srcParameterTypes.length != dstParameterTypes.length)
         {
            return false;
         }
      }
      
      Class<?> srcMethodType = DBUtils.getGetterSetterType(srcMethod);
      
      // if noLobs == true, return false for BLOB/CLOB field
      if (noLobs && (srcMethodType.equals(blob.class) || srcMethodType.equals(clob.class)))
      {
         return false;
      }
      
      Class<?> dstMethodType = DBUtils.getGetterSetterType(dstMethod);
      if (operationType == OperationType.COMPARE)
      {
         return dstMethodType.equals(srcMethodType);
      }
      else
      {
         return dstMethodType.isAssignableFrom(srcMethodType);
      }
   }
   
   /**
    * This method checks that two buffer correspond not null records and displayOrRecordError
    * otherwise.
    *
    * @param   srcBuf
    *          Source record buffer.
    * @param   dstBuf
    *          Destination record buffer.
    *
    * @return  <code>true</code> if both records are not nulls, otherwise <code>false</code>.
    */
   private static boolean doBuffersExist(RecordBuffer srcBuf, RecordBuffer dstBuf, MethodType methodType)
   {
      srcBuf.initialize();
      dstBuf.initialize();
      
      if (srcBuf.getCurrentRecord() != null && dstBuf.getCurrentRecord() != null)
      {
         return true;
      }
      
      if (methodType == MethodType.METHOD)
      {
         String msg = "BUFFER-COMPARE must have records in both target and source buffers";
         ErrorManager.recordOrShowError(9039, msg, false);
      }
      else
      {
         if (srcBuf.getCurrentRecord() == null)
         {
            String msg = "Source element of a BUFFER-COMPARE statement has no record";
            ErrorManager.recordOrThrowError(5369, msg, false);
         }
         else
         {
            String msg = "Target element of a BUFFER-COMPARE statement has no record";
            ErrorManager.recordOrThrowError(5370, msg, false);
         }
      }
      
      return false;
   }
   
   /**
    * Sets compare mode to data type instance for BUFFER-COPY and BUFFER-COMPARE methods.
    * <p>
    * #2080, note 93: ... when you get the value for a DMO property (via getter.invoke), you
    * get a copy, not the actual reference which is saved at the DMO. Thus, is enough to use
    * setCaseSensitive() for that Text instance, without having to worry to restore the
    * previous state (as we work with a copy which will get discarded once its job is done).
    * About the threading issue: Text.caseSens is a field which is set per each
    * character/longchar var instance. Currently, all communication is done sync (with some few
    * CTRL-C exceptions), so no two threads should be able to change the Text.caseSens field
    * for the same instance.
    *
    * @param   datum
    *          Data type instance.
    * @param   mode
    *          A buffer compare mode constant.
    * 
    * @return  The buffer compare mode needed for the datum's type; if no special mode is
    *          required based on the datum's type, return DEFAULT, even if a different mode was
    *          passed in.
    */
   private static BufferCompare.Mode setCompareMode(BaseDataType datum, BufferCompare.Mode mode)
   {
      switch (mode)
      {
         case CASE_SENSITIVE:
            if (datum instanceof Text)
            {
               ((Text) datum).setCaseSensitive(true);
               
               return mode;
            }
            break;
            
         case BINARY:
            if (datum instanceof clob || datum instanceof blob)
            {
               return mode;
            }
            break;
            
         case DEFAULT:
            break;
      }
      
      return BufferCompare.Mode.DEFAULT;
   }
   
   /**
    * This method compares given fields between the source buffer and the target buffer.
    *
    * @param   srcBuf
    *          Source record buffer.
    * @param   dstBuf
    *          Destination record buffer.
    * @param   propsMap
    *          Map of properties in source record buffer to properties in destination
    *          record buffer
    * @param   mode
    *          If mode-exp is given, it must evaluate to either "binary" or "case-sensitive"
    *          to provide that type of comparison.BUFFER-COMPARE( ) method supports binary
    *          and case-sensitive comparisons between CLOB as well as CHARACTER fields.
    * @param   exhaustive
    *          if <code>true</code>, all common properties are compared; if <code>false</code>,
    *          the comparison ends immediately after the first time a pair of properties fails
    *          to compare as equal.
    *
    * @return  A list of fields which failed to compare as equal.  If empty, the two buffers
    *          compared as equal overall; if not empty, the comparison determined one or more
    *          differences between them.  Will return <code>null</code> to indicate a comparison
    *          could not be performed.
    */
   private static List<String> compare(RecordBuffer srcBuf,
                                       RecordBuffer dstBuf,
                                       Map<DatumAccess, DatumAccess> propsMap,
                                       BufferCompare.Mode mode,
                                       boolean exhaustive)
   {
      if (propsMap == null)
      {
         return null;
      }
      
      Record srcRec = srcBuf.getCurrentRecord();
      Record dstRec = dstBuf.getCurrentRecord();
      
      try
      {
         Map<String, String> srcName2LegacyField = srcBuf.getLegacyFieldNameMap().getName2LegacyField();
         
         List<String> diffs = null;
         
         for (Map.Entry<DatumAccess, DatumAccess> entry : propsMap.entrySet())
         {
            DatumAccess daSrc = entry.getKey();
            DatumAccess daDst = entry.getValue();
            String srcProp = daSrc.getPropertyName();
            String dstProp = daDst.getPropertyName();
            PropertyMeta srcPropMeta = daSrc.getPropertyMeta();
            PropertyMeta dstPropMeta = daDst.getPropertyMeta();
            DataHandler srcHandler = srcPropMeta.getDataHandler();
            DataHandler dstHandler = dstPropMeta.getDataHandler();
            Integer srcExtent = daSrc.getExtent();
            Integer dstExtent = daDst.getExtent();
            
            if (srcExtent == null)
            {
               srcExtent = 0;
            }
            if (dstExtent == null)
            {
               dstExtent = 0;
            }
            
            if (srcExtent.intValue() != dstExtent.intValue())
            {
               // TODO: is this an error?
               continue;
            }
            
            boolean test;
            
            if (srcExtent == 0)
            {
               // Simple getter invocations.
               BaseDataType d1 = srcHandler.getField(srcRec, srcPropMeta.getOffset());
               BaseDataType d2 = dstHandler.getField(dstRec, dstPropMeta.getOffset());
               
               if (setCompareMode(d1, mode) == BufferCompare.Mode.BINARY)
               {
                  test = LargeObject.binaryEquals((LargeObject) d1, (LargeObject) d2);
               }
               else
               {
                  setCompareMode(d2, mode);
                  test = d1.equals(d2);
               }
               
               if (!test)
               {
                  // Populate the list with legacy field names instead of post-conversion DMO
                  // property names.
                  if (diffs == null)
                  {
                     diffs = new ArrayList<>();
                  }
                  diffs.add(srcName2LegacyField.get(srcProp));
                  if (!exhaustive)
                  {
                     return diffs;
                  }
               }
            }
            else
            {
               // indexed getter and setter invocations.
               for (int i = 0; i < srcExtent; i++)
               {
                  BaseDataType d1 = srcHandler.getField(srcRec, srcPropMeta.getOffset() + i);
                  BaseDataType d2 = dstHandler.getField(dstRec, dstPropMeta.getOffset() + i);
                  
                  if (setCompareMode(d1, mode) == BufferCompare.Mode.BINARY)
                  {
                     test = LargeObject.binaryEquals((LargeObject) d1, (LargeObject) d2);
                  }
                  else
                  {
                     setCompareMode(d2, mode);
                     test = d1.equals(d2);
                  }
                  
                  if (!test)
                  {
                     // populate the list with legacy field names instead of post-conversion
                     // DMO property names.
                     if (diffs == null)
                     {
                        diffs = new ArrayList<>();
                     }
                     diffs.add(srcName2LegacyField.get(srcProp));
                     if (!exhaustive)
                     {
                        return diffs;
                     }
                     // if at least one element from the extent is different, the whole field
                     // is marked as different, we don't need to check the others
                     break;
                  }
               }
            }
         }
         
         if (diffs == null)
         {
            diffs = Collections.emptyList();
         }
         
         return diffs;
      }
      catch (ConditionException exc)
      {
         DBUtils.handleException(srcBuf.getDatabase(), exc);
         DBUtils.handleException(dstBuf.getDatabase(), exc);
         
         if (!srcBuf.txHelper.errHlp.isSilent())
         {
            throw exc;
         }
      }
      catch (Exception exc)
      {
         DBUtils.handleException(srcBuf.getDatabase(), exc);
         DBUtils.handleException(dstBuf.getDatabase(), exc);
         String msg = "Error performing buffer compare";
         LOG.log(Level.SEVERE, msg, exc);
         // TODO:  use proper error.
         ErrorManager.recordOrThrowError(-1, msg);
      }
      
      return null;
   }
   
   /**
    * This method copies given fields from the source buffer to the receiving buffer.
    *
    * @param   srcBuf
    *          Source record buffer.
    * @param   dstBuf
    *          Destination record buffer.
    * @param   propsMap
    *          Map of properties in source record buffer to properties in destination record buffer
    * @param   validate
    *          {@code true} to validate the destination buffer after the copy.
    * @param   pkDstProps
    *          List of destination table <code>DatumAccess</code> instances where to copy the source PK.
    * @param   pkSrcProps
    *          List of source table <code>DatumAccess</code> instances where to copy the destination PK.
    * @return  {@code true} if copy is successful, otherwise {@code false}.
    */
   private static logical copy(RecordBuffer srcBuf,
                               RecordBuffer dstBuf,
                               Map<DatumAccess, DatumAccess> propsMap,
                               List<DatumAccess> pkDstProps,
                               List<DatumAccess> pkSrcProps,
                               boolean validate)
   {
      /* TODO: update _UserTableStat counter(s) */
      BufferReference srcProxy = srcBuf.getDMOProxy();
      BufferReference dstProxy = dstBuf.getDMOProxy();
      
      // Perform the copy.  The record, if newly created, will automatically be flushed to the
      // database once all properties in any index have been updated, or once the copy is complete
      // if validation is requested.
      boolean inBatch = dstBuf.getBufferManager().isBatchAssignMode();
      boolean error = false;
      boolean batchError = true;
      logical result;
      
      try
      {
         if (validate)
         {
            startBatch(srcBuf.bufferManager, true);
         }
         
         Record dstRecord = dstProxy.buffer().getCurrentRecord();
         Runnable restoreActiveStateDst = null;
         Runnable restoreActiveStateSrc = null;
         
         try
         {
            // perform the copy
            dstBuf.setBulkCopy(true);
            restoreActiveStateDst = dstRecord.setActiveBuffer(dstBuf);
            result = copy(srcProxy, dstProxy, propsMap, pkDstProps);
            
            if (result.getValue() && pkSrcProps != null && !pkSrcProps.isEmpty())
            {
               restoreActiveStateSrc = srcBuf.getCurrentRecord().setActiveBuffer(srcBuf);
               
               // copy the dst PK to pkSrcProps in srcBuf
               rowid dstPk = new rowid(dstRecord.primaryKey());
               for (DatumAccess da : pkSrcProps)
               {
                  BufferImpl deferBuff = (BufferImpl) srcBuf.buffer().getDMOProxy();
                  deferBuff.dereference(da.propertyMeta.getLegacyName(), dstPk);
               }
               if (srcBuf.getCurrentRecord().checkState(DmoState.CHANGED))
               {
                  srcBuf.getBufferManager().addDirtyBatchBuffer(srcBuf, restoreActiveStateSrc);
               }
            }
            
            if (dstRecord.checkState(DmoState.CHANGED))
            {
               dstBuf.getBufferManager().addDirtyBatchBuffer(dstBuf, restoreActiveStateDst);
            }
            
            batchError = false;
         }
         finally
         {
            
            if (validate)
            {
               endBatch(srcBuf.bufferManager, batchError);
            }
         }
         
         if (result.getValue())
         {
            dstBuf.markChangeScope();
         }
         return result;
      }
      catch (ConditionException exc)
      {
         error = true;
         throw exc;
      }
      catch (Exception exc)
      {
         error = true;
         
         DBUtils.handleException(srcBuf.getDatabase(), exc);
         DBUtils.handleException(dstBuf.getDatabase(), exc);
         
         String msg = "Error performing buffer copy";
         LOG.log(Level.SEVERE, msg, exc);
         
         // TODO:  use proper error.
         ErrorManager.recordOrThrowError(-1, msg);
      }
      finally
      {
         dstBuf.setBulkCopy(false);
         
         if (dstBuf.isCommitPending() && !inBatch)
         {
            try
            {
               if (!error)
               {
                  dstBuf.getPersistence().commit(dstBuf.getPersistenceContext());
               }
               else
               {
                  dstBuf.getPersistence().rollback(dstBuf.getPersistenceContext());
               }
            }
            catch (Exception exc)
            {
               DBUtils.handleException(srcBuf.getDatabase(), exc);
               DBUtils.handleException(dstBuf.getDatabase(), exc);
               
               String msg = "Error " + (error ? "rolling back" : "committing") + " during buffer copy";
               LOG.log(Level.SEVERE, msg, exc);
            }
            finally
            {
               dstBuf.setCommitPending(false);
            }
         }
      }
      
      return new logical(false);
   }
   
   /**
    * Processes validation rules, error checking and any other user-defined constraint processing
    * that needs to be applied. This is used to check if a proposed update to an object will
    * pass validation. If the method silently returns then all tests have passed, otherwise
    * an exception will be thrown on any validation failure.
    *
    * @param    valexp
    *           The validation expression to evaluate.
    * @param    valmsg
    *           The error message to display when validation fails.
    *
    * @return   <code>true</code> if the validation succeeded and the record can be deleted.
    * 
    * @throws  ErrorConditionException
    *          if the database trigger or the validation will fail
    */
   private static boolean validate(Supplier<logical> valexp, Supplier<character> valmsg)
   {
      boolean ok = false;
      
      try
      {
         ok = valexp.get().booleanValue();
      }
      catch (ErrorConditionException e)
      {
         // TODO: if we don't have to raise an END condition, this can be removed and we can
         //       use the common code below
         ErrorManager.recordOrThrowError(0, valmsg.get().toStringMessage(), false, true);
         
         // TODO: why are we raising the END condition here?  It seems wrong.
         throw new EndConditionException(e);
      }
      
      if (!ok)
      {
         // check IF this even generates an error condition AND what the err num should
         // be if it does AND IF it honors NO-ERROR (which definitely can be applied to
         // the delete statement)
         ErrorManager.recordOrThrowError(0, valmsg.get().toStringMessage(), false, true);
      }
      
      return ok;
   }
   
   /**
    * Fast way to do the buffer-copy. This doesn't work in all scenarios, but can greatly fasten
    * some cases (which are often in practice). This will check if the current scenario is trivial:
    * no assign triggers, no before buffers, no explicit pair copy and explicit dmo signature match.
    * For this case, the fields are set in bulk through direct access.
    * 
    * @param   srcBuf
    *          The source buffer from which the copy is done.
    * @param   dstBuf
    *          The destination buffer to which the copy is done.
    * @param   forcePair
    *          Flag to indicate if an explicit pair of fields should be copied / not copied.
    * @param   noLobs
    *          Flag to indicate if lobs should be omitted from the copy.
    * @param   validate
    *          {@code true} to validate the destination buffer after the copy.
    * @param   hasBefore
    *          Flag to indicate if the buffer have before buffer which should be also copied.
    * 
    * @return  {@code true} if the fast copy is eligible and could be done.
    *          {@code false} if the fast copy couldn't be applied for these buffers.
    */
   private static boolean fastCopy(RecordBuffer srcBuf,
                                   RecordBuffer dstBuf,
                                   boolean forcePair,
                                   boolean noLobs,
                                   boolean validate,
                                   boolean hasBefore)
   {
      if (noLobs || forcePair || hasBefore)
      {
         return false;
      }
      
      DmoMeta srcMeta = srcBuf.getDmoInfo();
      DmoMeta dstMeta = dstBuf.getDmoInfo();
      DmoSignature srcSignature = srcMeta.getSignature();
      DmoSignature dstSignature = dstMeta.getSignature();
      
      if (!DMOSignatureHelper.validBufferCopy(srcSignature, dstSignature) ||
          dstBuf.triggerTracker != null && 
                dstBuf.triggerTracker.hasAnyAssignTrigger(dstMeta, dstBuf.logicalDatabase))
      {
         return false;
      }
      
      // all the field names are both in the source and the destination
      // at this point the DMOs are similar enough to fasten the copy
      boolean success = true;
      
      Record srcRec = srcBuf.getCurrentRecord();
      Record dstRec = dstBuf.getCurrentRecord();
      
      if (DMOSignatureHelper.exactPropertyOrder(srcSignature, dstSignature))
      {
         // the fields are also in the same order, which means we can assign directly the values
         // from the source to the destination
         boolean batchError = true;
         boolean error = false;
         boolean inBatch = dstBuf.getBufferManager().isBatchAssignMode();
         
         try
         {
            if (validate)
            {
               startBatch(srcBuf.bufferManager, true);
            }
            
            Runnable restoreActiveState = dstRec.setActiveBuffer(dstBuf);
            try
            {
               dstBuf.setBulkCopy(true);
               success &= OrmUtils.setAllFields(srcRec, dstRec);
               if (dstRec.checkState(DmoState.CHANGED))
               {
                  dstBuf.getBufferManager().addDirtyBatchBuffer(dstBuf, restoreActiveState);
               }
               batchError = false;
            }
            finally
            {
               if (validate)
               {
                  endBatch(srcBuf.bufferManager, batchError);
               }
            }
            
            if (success)
            {
               dstBuf.markChangeScope();
               return true;
            }
            else
            {
               // otherwise, fallback to the classic implementation
               if (LOG.isLoggable(Level.WARNING))
               {
                  LOG.log(Level.WARNING, LOG_MSG_SIGN_BROKEN);
               }
               return false;
            }
         }
         catch (ConditionException exc)
         {
            error = true;
            throw exc;
         }
         catch (Exception exc)
         {
            error = true;
            
            DBUtils.handleException(srcBuf.getDatabase(), exc);
            DBUtils.handleException(dstBuf.getDatabase(), exc);
            
            String msg = "Error performing buffer copy";
            if (LOG.isLoggable(Level.SEVERE))
            {
               LOG.log(Level.SEVERE, msg, exc);
            }
            
            // TODO:  use proper error.
            ErrorManager.recordOrThrowError(-1, msg);
            return false;
         }
         finally
         {
            dstBuf.setBulkCopy(false);
            
            if (dstBuf.isCommitPending() && !inBatch)
            {
               try
               {
                  if (!error)
                  {
                     dstBuf.getPersistence().commit(dstBuf.getPersistenceContext());
                  }
                  else
                  {
                     dstBuf.getPersistence().rollback(dstBuf.getPersistenceContext());
                  }
               }
               catch (Exception exc)
               {
                  DBUtils.handleException(srcBuf.getDatabase(), exc);
                  DBUtils.handleException(dstBuf.getDatabase(), exc);
                  if (LOG.isLoggable(Level.SEVERE))
                  {
                     LOG.log(Level.SEVERE,
                             exc,
                             "Error %s during buffer copy",
                             error ? "rolling back" : "committing");
                  }
               }
               finally
               {
                  dstBuf.setCommitPending(false);
               }
            }
         }
      }
      else
      {
         // TODO: implement a faster copy in case the signatures match but the field order doesn't match
         if (unimplementedFastCopyWarningCounter < 10 && LOG.isLoggable(Level.WARNING))
         {
            LOG.log(Level.WARNING, LOG_MSG_SIGN_UNIMPL);
            
            if (++unimplementedFastCopyWarningCounter == 10)
            {
               LOG.log(Level.WARNING,
                       "10 warnings were printed. No further similar messages will be printed.");
            }
         }
         return false;
      }
   }
   
   /**
    * Get the {@link FastFindCache} instance for the database associated with this buffer.
    * 
    * @return   See above.
    */
   public FastFindCache getFastFindCache()
   {
      return persistenceContext.getFastFindCache();
   }
   
   /**
    * Get the {@link #undoable} instance.
    * 
    * @return   See above.
    */
   public LazyUndoable getUndoable()
   {
      return undoable;
   }
   
   /**
    * Report the record buffers for whose changes this object is interested
    * in listening.  This listener will be registered with the {@link
    * ChangeBroker} to receive notifications of any change to DMOs whose
    * interface types match those returned by the {@link
    * RecordBuffer#getDMOInterface} methods of the returned buffers.
    *
    * @return  An iterator on the record buffers of interest.  In this
    *          implementation, the iterator will only return this buffer
    *          instance.
    */
   @Override
   public Iterator<RecordBuffer> recordBuffers()
   {
      if (recordBuffers == null)
      {
         recordBuffers = Arrays.asList(new RecordBuffer[] { this });
      }
      
      return recordBuffers.iterator();
   }
   
   /**
    * Respond to a record change event.  This implementation stores the
    * event's DMO snapshot as its own DMO snapshot if the following
    * conditions are met:
    * <ol>
    *   <li>this buffer's snapshot has not already been stored previously;
    *   <li>the event's <i>post-modification</i> DMO instance is the same
    *       object instance as this buffer's current record;
    *   <li>the current record was not newly created.
    * </ol>
    * <p>
    * If the event indicates a record delete operation, we check if the
    * deleted record is the same as our current record.  If so, we clear it
    * from the buffer, since that record should no longer be available in any
    * buffer, once it has been deleted.
    * <p>
    * In the special case where a different, local buffer created the record we have loaded,
    * and the record is not yet flushed, we will be notified of the flush event here.
    * Stop tracking the creating buffer.
    *
    * @param   event
    *          Event which describes the DMO state change.
    *
    * @throws  PersistenceException
    *          not thrown;  required by the interface.
    */
   @Override
   public void stateChanged(RecordChangeEvent event)
   throws PersistenceException
   {
      if (!isActive())
      {
         return;
      }
      
      Record dmo = event.getDMO();
      
      if (snapshot == null && dmo == currentRecord && !newlyCreated)
      {
         if (event.getSnapshot() != null && undoable != null)
         {
            undoable.checkUndoable(true);
         }
         snapshot = event.getSnapshot();
      }
      
      if (dmo == currentRecord && currentRecord != null)
      {
         if (event.isDelete())
         {
            release(false);
         }
         else if (creatingBuffer != null && event.isInsert() && event.getSource() == creatingBuffer)
         {
            // the creating buffer has flushed the record to the database; we no longer need to
            // delegate setter calls to it
            creatingBuffer = null;
         }
         else if (event.getSource() != this &&
                  currentRecord.isMultiplyReferenced(false) && 
                  event.isUpdate())
         {
            // this is also a dirty buffer if the record was updated using another buffer
            bufferManager.registerDirtyBuffer(this);
         }
      }
      
      if (currentRecord != null && event.isBufferRefresh())
      {
         Long id = currentRecord.primaryKey();
         reload(getPinnedLockType(id), false);
      }
   }
   
   /**
    * Get the entity name associated with this buffer, which is the fully
    * qualified name of the DMO implementation class.
    * 
    * @return  DMO implementation class name.
    */
   public String getEntityName()
   {
      checkActive();
      
      return dmoClass.getName();
   }

   /**
    * Resets the <code>sortIndex</code> used by this buffer to <code>null</code>.
    */
   public void resetSortIndex()
   {
      this.sortIndex = null;
   }
   
   /**
    * Get <code>IndexHelper</code> instance used by this buffer.
    * 
    * @return  <code>IndexHelper</code> instance.
    */
   public IndexHelper getIndexHelper()
   {
      checkActive();
      
      return indexHelper;
   }
   
   /**
    * Get a representation of the latest DMO to be held in the buffer.  This
    * may be the same instance as current record;  it may be a copy.  In
    * cases where the current record has been deleted or released, this
    * represents the most recent record to back the buffer <i>before</i> such
    * deletion or release.
    * <p>
    * This is used to enable a query to keep its place when navigating to a
    * record, based upon the data in the last record held in the buffer,
    * even if that record is no longer available.
    *
    * @return  Snapshot of the latest DMO record, or <code>null</code> if the
    *          buffer never held a current record.
    */
   public Record getSnapshot()
   {
      return (snapshot != null) ? snapshot : currentRecord;
   }
   
   /**
    * Notifies that a block is about to be normally exited or normally iterated and that any
    * deferred validation processing may need to be executed.
    * <p>
    * A normal exit or iteration is one that occurs naturally by hitting the end of the block (or
    * loop). A <code>LEAVE</code>, <code>NEXT</code> and <code>RETURN</code> are also considered
    * normal exits or iterations so long as they were not preceded by an <code>UNDO</code>.
    * Likewise, a <code>QUIT</code> is a normal exit so long as it is not inside a block that has
    * an <code>ON QUIT UNDO, &lt;action&gt;</code>.
    * <p>
    * If the buffer is active and we are at a full transaction boundary or the boundary of the
    * buffer scope, we fire any write triggers that are pending. Also, if the current record is
    * transient, i.e., nothing has triggered a flush by the time the creating block is about to
    * exit/iterate, we validate and flush the record.
    *
    * @param   transaction
    *          <code>true</code> if this is a full transaction and <code>false</code> if this is
    *          only a sub-transaction (a nested scope with transaction support). This method will
    *          only be invoked outside a transaction for no-undo temp-table buffers.
    * @param   aggressiveFlush
    *          <code>true</code> if transaction manager is in aggressive subtransaction
    *          flush mode, indicating that any transient buffers should be validated and
    *          flushed, regardless of other state.
    *
    * @throws  ErrorConditionException
    *          If validation fails.
    */
   @Override
   public void validate(boolean transaction, boolean aggressiveFlush)
   {
      if (!isActive()                        ||
          currentRecord == null              ||
          !inChangeScope() && !isTransient() ||
          currentRecord.checkState(DmoState.DELETING))
      {
         // nothing to do
         return;
      }
      
      // if a separate buffer created the current record, then it is tracking all change to that
      // record and will handle validation and triggers
      if (creatingBuffer != null)
      {
         return;
      }
      
      // at a full transaction boundary, flush/validate the record (flush() will determine if it is
      // actually needed)
      boolean flush = transaction;
      
      if (!flush && (currentRecord.checkState(DmoState.NEW) || currentRecord.checkState(DmoState.CHANGED)))
      {
         // override aggressive flush if we are processing a CREATE trigger
         if (aggressiveFlush        &&
             isTransient()          &&
             triggerTracker != null &&
             triggerTracker.isExecuting(DatabaseEventType.CREATE, currentRecord.primaryKey()))
         {
            aggressiveFlush = false;
         }
         
         // at a sub-transaction boundary, validate and flush a new or changed record
         
         // Note: for a new record, this means default values will be saved in any fields which have not
         //       otherwise been explicitly updated
         
         boolean global = registeredWithGlobalScope == 0;
         int currentScope = bufferManager.getOpenBufferScopes();
         boolean exitingActiveScope = currentScope == activeScopeDepth && (!global || !worldScope);
         
         // if the record is also loaded in another buffer, 
         // don't force the flushing even if this buffer is running out of scope
         if (!isUndoable() || 
             (exitingActiveScope && !(currentRecord.isMultiplyReferenced(false))) || 
             aggressiveFlush)
         {
            flush = !isUndoable() && !aggressiveFlush ?  transaction                          ||
                                                         !currentRecord.hasIndices()          ||
                                                         currentRecord.isAnyIndexFullyDirty() : 
                                                         true;
         }
         else
         {
            return;
         }
         
         // TODO: need better test for flush/write trigger, to ensure it reflects the LAST buffer
         // holding the modified record, rather than ANY buffer holding the modified record
         
      }
      
      if (flush)
      {
         try
         {
            flush(true);
         }
         catch (ValidationException exc)
         {
            ErrorManager.recordOrThrowError(exc);
         }
      }
   }
   
   /**
    * Process a commit event for the current buffer.
    *
    * @param   transaction
    *          {@code true} if this commit represents the master transaction commit;
    *          {@code false} if it represents a sub-transaction commit.
    */
   @Override
   public void commit(boolean transaction)
   {
      if (!isActive())
      {
         return;
      }
      
      if (transaction && isTouched && !firingRowUpdate)
      {
         firingRowUpdate = true;
         maybeFireRowUpdateEvent();
         isTouched = false; // prevent firing ROW-UPDATE a second time when record is released
         firingRowUpdate = false;
      }
      int currentScope = bufferManager.getOpenBufferScopes();
      if (triggerTracker != null)
      {
         triggerTracker.commit(currentScope);
      }
   }
   
   /**
    * Process a rollback event for the current buffer.
    * <p>
    * Auto-commit buffers are never rolled back.
    *
    * @param   transaction
    *          {@code true} if this commit represents the master transaction rollback;
    *          {@code false} if it represents a sub-transaction rollback.
    */
   @Override
   public void rollback(boolean transaction)
   {
      if (!isActive())
      {
         return;
      }
      
      boolean inChangeScope = inChangeScope();
      
      if (isUndoable())
      {
         int currentScope = bufferManager.getOpenBufferScopes();
         if (triggerTracker != null)
         {
            triggerTracker.rollback(currentScope);
         }
      }
      else if (inChangeScope && transaction)
      {
         resetChangeScope();
      }
   }
   
   /**
    * Called by the transaction manager just before a buffer scope closes.
    * This is our hook to update the state of locks and record availability.
    * The current record is made unavailable and locks acquired during this
    * scope are either:
    * <ul>
    *   <li>released immediately, if this buffer scope never intersected a
    *       transaction's scope;
    *   <li>downgraded, if a transaction occurred within this buffer scope,
    *       but ended previous to this call;  or
    *   <li>marked for deferred release, if we are still within the scope of
    *       an active transaction;  the locks will be released at the end of
    *       the transaction.
    * </ul>
    */
   public void finished()
   {
      // need to be sure delayed is not polluted by a previous call
      delayed = !isDynamic() && txHelper.isExternalBlock();
      
      if (!delayed)
      {
         finishedImpl();
      }
      else
      {
         // we are a static buffer scoped to the external program (or OE class) and must be world scope
         worldScope = true;
      }
   }
   
   /**
    * Provides a notification that the external program scope in which the object is registered is
    * being deleted and the object's reference will be lost after this method is called.
    */
   @Override
   public void deleted()
   {
      if (delayed)
      {
         finishedImpl();
      }
   }
   
   /**
    * Called by the transaction manager after each iteration of a repeating
    * block.  Releases the current record.  Relinquishes locks pinned by this
    * buffer if we are at the outermost scope boundary.
    */
   public final void iterate()
   {
      boolean active = isActive();
      
      if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE,
                 describe() + ":  iterating " + (active ? "" : "in") + "active buffer scope");
      }
      
      if (!active)
      {
         return;
      }
      
      // Release the current record if this behavior has not been overridden
      // in the current scope.
      // JPRM @34715
      int currentScope = bufferManager.getOpenBufferScopes();
      if (currentScope != iterateReleaseOverrideScope && isUndoable())
      {
         // JPRM @30382.
         // do not allow write trigger to fire here; we are not at a point in the block
         // processing cycle where we can handle arbitrary business logic
         release(false);
      }
      
      // We are about to make another pass through an iterating block.
      // Relinquish any record locks this buffer is pinning, if we are at the
      // outermost open scope boundary.
      if (currentScope == activeScopeDepth && !bufferManager.isTransaction())
      {
         Set<Long> released = persistenceContext.getRecordLockContext().relinquishBufferLocks(this);
         if (released != null)
         {
            bufferManager.reclaimPendingKeys(persistence, released);
         }
      }
   }
   
   /**
    * Called by the transaction manager before a block is retried after an
    * error.  Relinquishes locks pinned by this buffer if we are at the
    * outermost scope boundary.
    */
   public final void retry()
   {
      // on retry, set a flag so the next openScope call will act accordigly
      retrying = true;
      
      boolean active = isActive();
      
      if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE,
                 describe() + ":  retrying " + (active ? "" : "in") + "active buffer scope");
      }
      
      if (!active)
      {
         return;
      }
      
      // We are about to retry a block.  Relinquish any record locks this
      // buffer is pinning, if we are at the outermost open scope boundary.
      int currentScope = bufferManager.getOpenBufferScopes();
      if (currentScope == activeScopeDepth && !bufferManager.isTransaction())
      {
         Set<Long> released = persistenceContext.getRecordLockContext().relinquishBufferLocks(this);
         if (released != null)
         {
            bufferManager.reclaimPendingKeys(persistence, released);
         }
      }
   }
   
   /**
    * Checks whether the record contained is a virtual system table (VST).
    * 
    * @return  {@code true} when the record contained is a VST.
    */
   public boolean isVst()
   {
      return vst;
   }
   
   /**
    * Return a reference to this object instance.  This is implemented to
    * satisfy the <code>BufferReference</code> interface, and provides a
    * means for the persistence layer to retrieve a reference to the backing
    * <code>RecordBuffer</code> instance for a DMO proxy object provided by
    * client code.  It is invoked by the enclosed invocation handler
    * implementation, which services proxied method invocations on DMO proxy
    * objects.
    *
    * @return  Current record buffer (i.e., <code>this</code>).
    */
   public final RecordBuffer buffer()
   {
      return this;
   }
   
   /**
    * Resolve the instance as it exists when the external program or 4GL class was 
    * initially instantiated. 
    * 
    * @return   The definition-time instance.
    */
   public final RecordBuffer definition()
   {
      return this;
   }
   
   /**
    * Get the DMO which represents the database record currently held in the buffer.
    *
    * @return  DMO for the current record, or <code>null</code> if the buffer currently is empty.
    */
   public Record getCurrentRecord()
   {
      return currentRecord;
   }
   
   /**
    * Get the lock type which potentially "pins" the communal record lock on
    * the record with the given identifier (the record was ever or is
    * currently loaded into this buffer)
    *
    * @param   id
    *          Record identifier.
    *
    * @return  Pinned lock type of the specified record.
    *
    * @see     RecordLockContext
    */
   public LockType getPinnedLockType(Long id)
   {
      LockType lockType = pinnedLocks.getPinnedLockType(id);
      
      return (lockType == null ? LockType.NONE : lockType);
   }
   
   /**
    * Set the lock type which potentially "pins" the communal record lock on
    * the record with the given id (the record was ever or is currently loaded
    * into this buffer).
    *
    * @param   id
    *          Record identifier.
    * @param   pinnedLockType
    *          New pinned lock type.
    *
    * @see     RecordLockContext
    */
   public void setPinnedLockType(Long id, LockType pinnedLockType)
   {
      pinnedLocks.setPinnedLockType(id, pinnedLockType);
   }
   
   /**
    * Get the state of the {@link #worldScope} flag.
    * 
    * @return   See above.
    */
   public boolean isWorldScope()
   {
      return worldScope;
   }
   
   /**
    * Get the depth of the scope at which this buffer was first opened.
    * Nested scopes are ignored;  only the depth of the outermost open scope
    * is reported.
    *
    * @return  Depth at which outermost scope was opened for this buffer.
    */
   public int getScopeOpenDepth()
   {
      return activeScopeDepth;
   }
   
   /**
    * Get the record ID of the current buffer's current record, if any.  P2J
    * implementation of the RECID function.
    * <p>
    * This method will return a 32-bit recid which represents the internal
    * row ID of the given record.  If the actual row ID (which is implemented
    * as a wider integer) cannot fit due to overflow, an error condition will
    * be raised.
    *
    * @return  Record ID of the underlying data record. If no record currently
    *          backs the buffer associated with <code>dmo</code>, a recid
    *          initialized as unknown value is returned.
    * 
    * @throws  ErrorConditionException
    *          if the actual record ID is too wide to fit within a 32-bit
    *          integer.
    */
   public recid recordID()
   {
      if (isAvailable())
      {
         Record record = getCurrentRecord();
         long id = record.primaryKey();
         if (id < Integer.MIN_VALUE || id > Integer.MAX_VALUE)
         {
            // TODO: don't know what 4GL's overflow behavior is or whether the corresponding
            // error is more serious than an ERROR condition; internally, we support a 64-bit
            // value, but the recid object follows the 32-bit constraint in terms of not
            // formatting this
            txHelper.errHlp.noRecordOrThrowError(
               "Row id of " + id + " is too wide to fit within a 32-bit record ID");
         }
         
         return new recid(id);
      }
      else
      {
         return new recid();
      }
   }
   
   /**
    * Get the row ID of the current buffer's current record, if any.  P2J
    * implementation of the ROWID function.
    *
    * @return  Row ID of the underlying data record.  If no record currently
    *          backs the buffer associated with <code>dmo</code>, a
    *          <code>rowid</code> initialized as unknown value is returned.
    */
   public rowid rowID()
   {
      return new rowid(_rowID());
   }
   
   /**
    * Get the row ID of the current buffer's current record, if any.
    *
    * @return  Row ID of the underlying data record.  If no record currently backs the buffer associated with 
    *          <code>dmo</code>, a <code>null</code> value is returned.
    */
   public Long _rowID()
   {
      if (isAvailable())
      {
         Record record = getCurrentRecord();
         Long id = record.primaryKey();
         
         if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && id == null)
         {
            ErrorManager.recordOrShowError(15991, getLegacyName());
            // Attempt to create in multi-tenant table '<tablename>' where the partition (usually default) has not been allocated. (15991)
            return null;
         }
         
         return id;
      }
      
      return null;
   }
   
   /**
    * Returns TABLE-HANDLE for the buffer.
    *
    * @return TABLE-HANDLE for the buffer. Is an unknown handle for permanent tables.
    */
   public handle tableHandle()
   {
      return new handle();
   }
   
   /**
    * Get the associated temp-table (for tempporary buffers).
    * 
    * @return   Always <code>null</code>.
    */
   public TempTable tableHandleResource()
   {
      return null;
   }
   
   /**
    * Get the DMO interface associated with this buffer.
    *
    * @return  DMO interface.
    */
   public Class<? extends DataModelObject> getDMOInterface()
   {
      return dmoInterface;
   }
   
   /**
    * Get the meta information of the DMO associated with the buffer.
    * 
    * @return  the meta information collection of the DMO associated with the buffer.
    */
   public DmoMeta getDmoInfo()
   {
      return dmoInfo;
   }
   
   /**
    * Validate the current record and flush it to the database, if validation passes. However, abort
    * if the record already is marked invalid or is stale.
    *
    * @throws  ValidationException
    *          if validation on the buffer fails.
    * @throws  ErrorConditionException
    *          if there is an error accessing the database while persisting the record.
    */
   public void flush()
   throws ValidationException
   {
      flush(false);
   }
   
   /**
    * Validate the current record and flush it to the database, if validation passes. Abort if the record
    * is stale.
    * 
    * @param   retryInvalid
    *          if {@code true}, retry the validation and flush even if the record previously was marked as
    *          invalid; if {@code false}, abort if the record is already marked as invalid.
    *
    * @throws  ValidationException
    *          if validation on the buffer fails.
    * @throws  ErrorConditionException
    *          if there is an error accessing the database while persisting the record.
    */
   private void flush(boolean retryInvalid)
   throws ValidationException
   {
      if (readonly)
      {
         // the read-only buffers (those used as old values in WRITE triggers) must not be altered so 
         // no need to be flushed to database
         return;
      }
      
      // let the transaction Id to be generated if it was not yet set:
      getTxHelper().checkTransaction(persistence.getDatabase(!dmoInfo.multiTenant).getId());
      
      if (vst)
      {
         checkReadOnlyVstBuffer();
         
         return;
      }
      
      // if no current record, OR the record has been marked STALE, OR the record is in the process of being
      // deleted, OR the record already has been determined to be invalid AND we are not ignoring that fact,
      // abort the flush
      if (currentRecord == null ||
          currentRecord.checkState(DmoState.STALE) ||
          currentRecord.checkState(DmoState.DELETING) ||
          (currentRecord.checkState(DmoState.INVALID) && !retryInvalid))
      {
         return;
      }
      
      try
      {
         // validate and persist the record
         boolean changed = currentRecord.checkState(DmoState.CHANGED);
         if (changed || currentRecord.checkState(DmoState.NEW))
         {
            validateMaybeFlush(currentRecord, true, false);
            if (changed)
            {
               UserTableStatUpdater.update(getDatabase(), this);
            }
         }
         else if (currentRecord.checkState(DmoState.INVALID))
         {
            throw new ValidationException("Invalid record.");
         }
         else
         {
            // record was neither in need of validation nor invalid, so there is nothing more to do
            return;
         }
         
         /*
         // share uncommitted change to the record, if not previously done
         if (dirtyContext != null && dirtyShared)
         {
            dirtyContext.insert(this, currentRecord, null, true);
         }
         */
         
         if (LOG.isLoggable(Level.FINE))
         {
            String desc = describe();
            LOG.log(Level.FINE, desc + ":  flushed buffer");
            if (LOG.isLoggable(Level.FINEST))
            {
               LOG.log(Level.FINEST, desc + ":  " + toString());
               LOG.log(Level.FINEST, desc + ":  location:", new Throwable());
            }
         }
      }
      catch (PersistenceException exc)
      {
         ErrorManager.recordOrThrowError(exc);
      }
   }
   
   /**
    * Get code page of the specified CLOB field.
    *
    * @param  property
    *         DMO property name of the CLOB field.
    *
    * @return code page of the specified CLOB field.
    */
   public String getCodePage(String property)
   {
      // for permanent buffers only, overridden for temporary buffers
      return TableMapper.getLegacyFieldInfo(dmoInfo, property).getCodePage();
   }
   
   /**
    * Checks whether the table is read-only. If the current DMO belongs to a (VST) table that does
    * not allow the modified data to be persisted then the flush event should be discarded. Note
    * that some VST tables will silently discard the flush event while some of them will also
    * display the 14378 error message.
    */
   private void checkReadOnlyVstBuffer()
   {
      String legacyName = dmoInfo.legacyTable;
      
      if (isTouched)
      {
         String msg = "Updating " + legacyName + " Virtual System Table is not supported";
         ErrorManager.recordOrShowError(14378, msg, true);
      }
      
      if (legacyName.equalsIgnoreCase("_Trans"))
      {
         // flush the transaction id to persistence if required
         TransactionManager.flushTransMetaData(getPersistence());
      }
   }
   
   /**
    * Return the hash code for this object. Overrides the parent's behavior to return a value
    * which is cached at construction and does not change. Implemented as a performance
    * enhancement.
    * 
    * @return  Hash code.
    */
   @Override
   public final int hashCode()
   {
      return hashCode;
   }
   
   /**
    * Determine whether this object is equivalent to the specified object. Forces the parent's
    * implementation.
    * 
    * @param   o
    *          Object to test for equality.
    * 
    * @return  {@code true} if equivalent, else {@code false}.
    */
   @Override
   public final boolean equals(Object o)
   {
      return super.equals(o);
   }
   
   /**
    * Get a string representation of the internal state of this object,
    * primarily for debugging purposes.
    *
    * @return  String representing this object's state.
    */
   public String toString()
   {
      Record tempRecord = getTempRecord();
      if (tempRecord != null)
      {
         return "[TEMP RECORD ACTIVE]\n" + toString(tempRecord);
      }
      
      return toString(currentRecord);
   }
   
   /**
    * Constructs and returns a string containing the P4GL schema definition of the table. The result is not
    * cached so this method is to be used only for debug.
    *
    * @return  a string containing the P4GL legacy schema definition of the table.
    */
   public String tableDefinition()
   {
      return dmoInfo.getTableDefinition();
   }
   
   /**
    * Get the DMO proxy which references this buffer instance.
    *
    * @return  DMO proxy object. This is {@code null} only if this object is incompletely constructed.
    */
   public BufferReference getDMOProxy()
   {
      return dmoProxy;
   }
   
   /**
    * Get parent table.
    *
    * @return  Parent <code>TempTable</code> object for temporary buffers or <code>null</code>
    *          for permanent buffers.
    */
   public AbstractTempTable getParentTable()
   {
      TempTable tt = tableHandleResource();
      return tt != null && tt.valid() ? (AbstractTempTable) tt : null;
   }
   
   /**
    * Get the zero-based transaction block nesting level, where 0 indicates the current block is a full
    * transaction block, 1 indicates the current block is the first level of nested subtransaction, and so
    * on. Note that this includes only blocks with transaction properties; no-transaction blocks which may
    * be nested between blocks with transaction properties or ignored for purposes of this count.
    * 
    * @return  The current block's transaction nesting level, or -1 if not currently in an active
    *          transaction.
    */
   public Long getTxNestingId()
   {
      Session session = persistenceContext.getSessionNoCreate();
      
      return session != null ? session.getTxNestingId() : null;
   }
   
   /**
    * Get the currently active ORM database session, if any.
    * 
    * @return  Current database session, or {@code null} if none is active.
    */
   public Session getSession()
   {
      return getSession(false);
   }
   
   /**
    * Get the currently active ORM database session, optionally creating one if it does not exist.
    * 
    * @param   create
    *          If {@code true} and there is no current database session, create one. Otherwise, return the
    *          current session, even if it is {@code null}.
    * 
    * @return  Current database session or {@code null}.
    */
   public Session getSession(boolean create)
   {
      if (!create)
      {
         return persistenceContext.getSessionNoCreate();
      }
      
      try
      {
         return persistenceContext.getSession();
      }
      catch (PersistenceException exc)
      {
         DBUtils.handleException(getDatabase(), exc);
         return null;
      }
   }
   
   /**
    * Initialize the core data on which this buffer relies.  This should be
    * called only once per buffer instance, when the buffer's scope is first
    * opened.
    * 
    * @return  {@code true} if the buffer was initialized with this call;
    *          {@code false} if the buffer had been initialized with a
    *          previous call to this method.
    *
    * @throws  IllegalArgumentException
    *          if no implementation class is found for the given database and
    *          interface.
    * @throws  RuntimeException
    *          if the backing DMO implementation class does not implement a
    *          constructor which accepts a multiplex ID.
    */
   public boolean initialize()
   {
      return initialize(false);
   }
   
   /**
    * Get the id of the tenant (if any) from the persistence context.
    * 
    * @return  The id of the tenant from the persistence context.
    */
   public String getTenantName()
   {
      return persistenceContext.getTenantName();
   }
   
   /**
    * Tenant changes forces update of (cached) tenant-related internal data.
    * 
    * @param   context
    *          The context containing the updated tenant info. 
    */
   void updateTenant(Persistence.Context context)
   {
      persistenceContext = context;
   }
   
   /**
    * Initialize the core data on which this buffer relies.  This should be called after the
    * buffer's scope is first opened, but before the buffer is used for a query, or interrogated
    * for any information besides its DMO interface or variable name.
    * 
    * @param   fromDefine
    *          {@code true} to acknowledge that this is an early initialization from a define statement
    *          and the scope shouldn't have been opened before-hand.
    * 
    * @return  {@code true} if the buffer was initialized with this call;
    *          {@code false} if the buffer had been initialized with a
    *          previous call to this method.
    *
    * @throws  IllegalArgumentException
    *          if no implementation class is found for the given database and interface;  if setup
    *          for an invalid foreign relation fails.
    * @throws  StopConditionException
    *          if index information cannot be obtained for backing table or if database isn't
    *          connected.
    */
   public boolean initialize(boolean fromDefine)
   {
      if (persistence != null)
      {
         if (bufferManager.isTransaction())
         {
            bufferManager.maybeActivateTxWrapper(persistence.getDatabase(!dmoInfo.multiTenant));
         }
         
         return false;
      }
      
      Database database = getDatabase();
      if (database == null)
      {
         // error will have been processed by getDatabase method
         return false;
      }
      
      return initialize(fromDefine, PersistenceFactory.getInstance(database), database);
   }
   
   /**
    * Initialize the core data on which this buffer relies.  This should be called after the
    * buffer's scope is first opened, but before the buffer is used for a query, or interrogated
    * for any information besides its DMO interface or variable name.
    * 
    * @param   fromDefine
    *          {@code true} to acknowledge that this is an early initialization from a define statement
    *          and the scope shouldn't have been opened before-hand.
    * @param   p
    *          The Persistence instance used by this buffer.
    * @param   database
    *          The Database instance used by this buffer.
    * 
    * @return  {@code true} if the buffer was initialized with this call;
    *          {@code false} if the buffer had been initialized with a
    *          previous call to this method.
    *
    * @throws  IllegalArgumentException
    *          if no implementation class is found for the given database and interface;  if setup
    *          for an invalid foreign relation fails.
    * @throws  StopConditionException
    *          if index information cannot be obtained for backing table or if database isn't
    *          connected.
    */
   private boolean initialize(boolean fromDefine, Persistence p, Database database)
   {
      BufferManager.registerScopeable(bufferManager);
      
      // Get IndexHelper for this database.
      indexHelper = IndexHelper.get(database);
      
      // Get Persistence instance used by this buffer.
      persistence = p;
      persistenceContext = persistence.getContext(!dmoInfo.multiTenant);
      
      // get dirty share context used by this buffer. If the table is permanent and not marked with read-dirty
      // annotation, it is categorized to PRIMARY_NON_DIRTY database from POV of dirty context.
      Database shareCtxDb = dmoInfo.isTempTable() || dmoInfo.isDirtyRead()
                            ? database
                            : new Database(database.getName(), Database.Type.PRIMARY_NON_DIRTY);
      dirtyContext = DirtyShareFactory.getContextInstance(shareCtxDb);
      
      // Generate BufferType, which will be used for a hash key with Reversibles.
      bufferType = new BufferType(database, getTable(), isUndoable());
      
      metadata = dmoInfo.getRecordMeta();
      
      // Notify the connection manager that this buffer is using the backing, physical database.
      onOpenOutermostScope();
      
      // Store synchers for all foreign key associations from this buffer's
      // table to other tables, and the inverse associations from other
      // tables to this buffer's table.
      if (Persistence.isForeignKeysEnabled())
      {
         Iterator<RelationInfo> iter1 = dmoInfo.inverseRelations(); 
         Iterator<RelationInfo> iter2 = dmoInfo.relations();
         assocSynchers = (iter1.hasNext() || iter2.hasNext() ? new ArrayList<>() : null);
         
         if (assocSynchers != null)
         {
            // Create inverse foreign synchers.
            while (iter1.hasNext())
            {
               RelationInfo next = iter1.next();
               assocSynchers.add(new InverseSyncher(this, next));
            }
            
            // Create primary foreign synchers.
            while (iter2.hasNext())
            {
               RelationInfo next = iter2.next();
               assocSynchers.add(new LocalSyncher(this, next));
            }
            
            assocSynchers.trimToSize();
         }
         
         legacyForeignKeys = new HashSet<>();
//         dirtyLegacyKeyProps = new HashSet<>();
         
         /*
         // Store synchers for all foreign key associations from this buffer's
         // table to other tables.
         String schema = getSchema();
         Iterator<RelationInfo> iter = DmoMetadataManager.relations(schema, dmoInterface);
         assocSynchers = (iter.hasNext()
                          ? new ArrayList<AssociationSyncher>()
                          : null);
         
         if (assocSynchers != null)
         {
            // Create primary association synchers.
            while (iter.hasNext())
            {
               RelationInfo next = iter.next();
               assocSynchers.add(new LocalSyncher(this, next));
            }
            
            assocSynchers.trimToSize();
         }
         */
      }
      else
      {
         assocSynchers = null;
         legacyForeignKeys = null;
      }
      
      if (!bufferManager.bufferInitialized(this) && !fromDefine)
      {
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, "FAIL-SAFE: The buffer is initialized without having its scope opened. "
                     + "Registering it as finalizable now! Tracing info: { "
                     + "DMOBufIface: " + this.getDMOBufInterface().getName() + ", "
                     + "definingType: " + this.getDefiningType() + ", "
                     + "variable: " + this.variable + " }");
         }
         
         // this is a fallback plan required to ensure closeBufferScope is called to avoid memory leaks
         txHelper.registerFinalizable(this, false);
      }
      
      if (bufferManager.isTransaction())
      {
         bufferManager.maybeActivateTxWrapper(database);
      }
      this.reversibleOffend = false;
      return true;
   }
   
   /**
    * Indicate whether the buffer currently contains a record, and whether
    * that record is transient.  A record is transient if it has been newly
    * created and has not yet been saved to the database.
    *
    * @return  <code>true</code> if record is new, else <code>false</code>.
    */
   public boolean isTransient()
   {
      return (currentRecord != null && createScope >= 0);
   }
   
   /**
    * Indicate whether this is a temp-table buffer and we are not currently within an existing
    * database transaction.
    *
    * @return  {@code false} for permanent table buffers; subclasses should override for
    *          different behavior.
    */
   public boolean isAutoCommit()
   {
      return false;
   }
   
   /**
    * Returns the persistent procedure which is responsible for this record buffer.
    * This means that this persistent procedure was the first one to open meaning
    * that this is a global buffer for the procedure.
    * 
    * @return  The persistent procedure which defined this record buffer for the first time.
    */
   public Object getPersistentProc()
   {
      return persistentProc;
   }
   
   /**
    * Convenience method to indicate whether this buffer is backed by a
    * temporary table.
    *
    * @return  <code>false</code>.
    */
   public boolean isTemporary()
   {
      return false;
   }
   
   /**
    * Check whether this buffer is associated with a persistent table from a mutable database (i.e., that
    * its DMO is generated at runtime by a {@link SchemaCheck database scan}).
    * <p>
    * TODO: this only checks whether the backing database is configured as mutable; the current assumption
    * is that all tables in a mutable database are mutable, but in the future, we may need to support mutable
    * databases with a mix of static and dynamic tables.
    */
   public boolean isMutable()
   {
      try
      {
         Database database = getDatabase();
         if (database.isMeta())
         {
            database = database.toType(Database.Type.PRIMARY);
         }
         return DatabaseManager.isMutable(database);
      }
      catch (PersistenceException exc)
      {
         return false;
      }
   }
   
   /**
    * Check whether this instance was defined as REFERENCE-ONLY. This applies only to temp-tables so this
    * method returns {@code false} all the time.
    * 
    * @return  always {@code false}.
    */
   public boolean referenceOnly()
   {
      return false;
   }
   
   /**
    * Convenience method to get the {@code Dialect} in use for this record buffer.
    * 
    * @return  Database dialect.
    */
   public Dialect getDialect()
   {
      checkActive();
      
      return persistence.getDialect();
   }
   
   /**
    * Invalidates all or a specific index for this buffer's table.
    *
    * @param   index
    *          If not null, it represents the index to be invalidated in ff cache. Otherwise all indices
    *          are invalidated.
    */
   public void invalidateFFCache(int index)
   {
      FastFindCache ffCache = persistenceContext.getFastFindCache();
      if (index == 0)
      {
         // invalidate all indices
         ffCache.invalidate(dmoInfo.getId(), getMultiplexID());
      }
      else
      {
         // invalidate all only the specified one
         ffCache.invalidate(dmoInfo.getId(), getMultiplexID(), index);
      }
   }
   
   /**
    * Notification that the record in this buffer has been flushed.
    */
   public void recordFlushed()
   {
      bufferManager.getTxWrapperHelper().recordFlushed(this, metadata.getDmoMeta().getId());
   }
   
   /**
    * Use this method to notify this object that the record it holds was successfully inserted into the
    * database. As result, the record is removed from {@code DirtyShareContext.earlyInserts}.
    *
    */
   public void recordInserted()
   {
      if (dirtyContext != null && currentRecord != null)
      {
         dirtyContext.recordInserted(new RecordIdentifier<>(getEntityName(),
                                                            currentRecord.primaryKey()));
      }
   }
   
   /**
    * Get the buffer's defining type.
    * 
    * @return   The {@link #definingType}.
    */
   protected Class<?> getDefiningType()
   {
      return definingType;
   }
   
   /**
    * Prune the session cache of all records associated with this buffer's backing table.
    */
   protected void pruneSessionCache()
   {
      Session session = getSession(false);
      if (session != null)
      {
         session.invalidateRecords(getDMOImplementationClass().getName(), getMultiplexID());
      }
   }
   
   /**
    * Apply the given update to all the cached records.
    * <p>
    * WARNING: this must be called only for temp-tables, and it must be used with a direct SQL on the same 
    * temp-table records, so that the cached and the sql table records have the same state.
    * 
    * @param    work
    *           The code to apply on the cached records.
    */
   protected void updateCachedRecords(Consumer<BaseRecord> work)
   {
      Session session = getSession(false);
      if (session != null)
      {
         session.updateCachedRecords(getDMOImplementationClass().getName(), getMultiplexID(), work);
      }
   }
   
   /**
    * Gets the current value of the {@code ROW-STATE} property for this record. Valid values
    * are defined as constants in {@link Buffer} interface. Additionally, {@code unknown} value is
    * returned if property is not available. 
    *
    * @return  Since this method is invoked for permanent records this will always return
    *          {@code unknown} value.
    */
   protected integer rowState()
   {
      // not a temp-table record
      return new integer();
   }
   
   /**
    * Sets the value of the {@code ROW-STATUS} property for this record. Valid values are defined
    * as constants in {@link Buffer} interface. Additionally {@code null} value is allowed if 
    * property is not available. 
    *
    * @param  state
    *         The new value of the {@code ROW-STATE} property for this record as explained above.
    */
   protected void rowState(Integer state)
   {
      // not a temp-table record
   }
   
   /**
    * Gets {@code rowid} of the peer record. Exclusively, for a BEFORE-TABLE record this is the 
    * {@code after-rowid} and {@code before-rowid} for AFTER-TABLE record.
    *
    * @return  Since this method is invoked for permanent records this will always return
    *          {@code unknown} value.
    */
   protected rowid peerRowid()
   {
      // not a temp-table record
      return new rowid();
   }
   
   /**
    * Sets the {@code rowid} value of the peer record. Exclusively, for a BEFORE-TABLE record this
    * is the {@code after-rowid} and {@code before-rowid} for AFTER-TABLE record.
    *
    * @param   peer
    *          The new rowid value of the peer record or {@code null}.
    */
   protected void peerRowid(rowid peer)
   {
      // not a temp-table record
   }
   
   /**
    * Sets the {@code origin-rowid} for AFTER-TABLE record.
    *
    * @param   peer
    *          The new rowid value of the record or {@code null}.
    */
   public void originRowid(rowid peer)
   {
      // not a temp-table record
   }
   
   /**
    * Sets the {@code datasource-rowid} for AFTER-TABLE record.
    *
    * @param   sourceRowid
    *          The new long rowid value of the record or {@code null}.
    */
   public void datasourceRowid(rowid sourceRowid)
   {
      // not a temp-table record
   }
   
   /**
    * Gets {@code errorFlag} of this record. 
    *
    * @return  the {@code errorFlag} of this record or {@code null} if it was not configured. If not null,
    *          the value is a bitwise combination of ERROR and REJECTED attribute. It's up to the called to
    *          extract the bit(s) it needs.<br>
    *          Since this method is invoked for permanent records this will always return {@code unknown}
    *          value.
    */
   protected Integer errorFlags()
   {
      // not a temp-table record
      return null;
   }
   
   /**
    * Sets or removes the {@code errorFlag} value this peer record. The {@code error} attribute of the record
    * is composed of multiple bit flags. This method manages its value based on the parameters it receives.
    *
    * @param   errFlag
    *          The error bit to be set or removed.
    * @param   set
    *          Use {@code true} to set the flag and {@code false} to remove it.
    */
   protected void updateErrorFlags(int errFlag, boolean set)
   {
      // not a temp-table record
   }
   
   /**
    * Gets {@code error string} of this record.
    *
    * @return  Since this method is invoked for permanent records this will always return
    *          {@code unknown} value.
    */
   protected character errorString()
   {
      // not a temp-table record
      return new character();
   }
   
   /**
    * Sets the {@code error string} of this record.
    *
    * @param   string
    *          The new {@code error string} of this record.
    */
   protected void errorString(Text string)
   {
      // not a temp-table record
   }
   
   /**
    * Potentially set the current high-water block depth at which an undoable change has been
    * made.
    */
   protected final void markChangeScope()
   {
      MutableInteger changeScope = bufferManager.changeScopes.get(bufferType);
      
      int currentScope = bufferManager.getOpenBufferScopes();
      if (changeScope != null)
      {
         changeScope.set(Math.max(changeScope.get(), currentScope));
      }
      else
      {
         bufferManager.changeScopes.put(bufferType, new MutableInteger(currentScope));
      }
   }
   
   /**
    * Reset to its default state the current high-water mark block depth at which an undoable
    * change has been made.
    */
   protected final void resetChangeScope()
   {
      bufferManager.changeScopes.remove(bufferType);
   }
   
   /**
    * Determine whether the current open buffer scope is at or lower than the current high-water mark
    * open buffer scope depth at which an undoable change has been made. If it is not, we can be confident
    * that there are not reversible actions stored, thereby bypassing the effort to manage any
    * such resources at full and sub-transaction boundaries.
    * 
    * @return  {@code true} if the current open scope depth is within the scope of any reversible
    *          changes that need to be managed; else {@code false}.
    */
   protected final boolean inChangeScope()
   {
      MutableInteger changeScope = bufferManager.changeScopes.get(bufferType);
      int currentScope = bufferManager.getOpenBufferScopes();
      
      return changeScope != null && currentScope <= changeScope.get();
   }
   
   /**
    * Get default buffer (for temporary tables only).
    *
    * @return for temporary tables: default buffer for the table to which this buffer belongs. For
    *         permanent tables <code>null</code> is returned.
    */
   protected RecordBuffer getDefaultBuffer()
   {
      if (!isTemporary)
      {
         return null;
      }
      
      TempTable table = getParentTable();
      Buffer buf = table.defaultBufferHandleNative();
      return get((DataModelObject) buf);
   }
   
   /**
    * Close related queries.
    */
   protected void closeRelatedQueries()
   {
      if (relatedQueries != null && !relatedQueries.isEmpty())
      {
         P2JQuery[] queries = relatedQueries.toArray(new P2JQuery[relatedQueries.size()]);
         for (P2JQuery query : queries)
         {
            if (query.isOpen().booleanValue())
            {
               query.close();
            }
            query.resetQuery();
         }
      }
   }
   
   /**
    * A no-op implementation called during scope cleanup ({@link #finishedImpl()}), which should
    * be overridden by a subclass needing to perform some action at this point.
    */
   protected void resourceDeleted()
   {
      // remove buffer from BufferManager's tracking
      bufferManager.deregisterDynamicBuffer(this, persistentProc);
      
      // remove buffer from TransactionManager's global finalizables, unless we are being
      // invoked in the global scope, since we will be removed imminently anyway
      int currentScope = bufferManager.getOpenBufferScopes();
      if (currentScope > 0)
      {
         txHelper.deregisterGlobalFinalizable(this);
      }
      
      // release the persistent procedure reference.
      this.persistentProc = null;
   }
   
   /**
    * Get the state of the {@link #isTouched} flag.
    * 
    * @return   See above.
    */
   protected boolean isTouched()
   {
      return isTouched;
   }
   
   /**
    * Get the {@link #bulkCopy} flag.
    * 
    * @return   See {@link #bulkCopy}.
    */
   protected boolean isBulkCopy()
   {
      return bulkCopy;
   }
   
   /**
    * Set the {@link #bulkCopy} flag.
    * 
    * @param    bulk
    *           <code>true</code> if buffer currently the destination of a bulk copy operation.
    */
   protected void setBulkCopy(boolean bulk)
   {
      this.bulkCopy = bulk;
   }
   
   /**
    * Indicate whether changes made to records managed by this buffer can be
    * undone during a rollback.  This implementation always returns
    * <code>true</code>.
    *
    * @return  <code>true</code>.
    */
   protected boolean isUndoable()
   {
      return true;
   }
   
   /**
    * Indicate whether the table backing this buffer is known to either: (a) not exist; or (b) contain no
    * rows.
    * 
    * @return  {@code false} to indicate it is not definitively known whether the backing table exists or
    *          might have rows.
    */
   protected boolean isTableDefinitelyEmpty()
   {
      return false;
   }
   
   /**
    * Indicate whether the table backing this buffer contains records. If this can't be inferred easily,
    * this returns null.
    * 
    * @return  {@code true} if the table surely has records, {@code false} if the table surely doesn't have
    *          records, {@code null} if this can't be computed wihout actually querying the database.
    */
   public Boolean fastHasRecords()
   {
      return null;
   }
   
   /**
    * Get the logical database name associated with the given parameter. If the parameter already
    * represents a logical database name, this name is returned. If it represents a valid alias,
    * the underlying, logical name is returned.
    *
    * @param   ldbOrAlias
    *          A logical database name or database alias.
    *
    * @return  Logical database name or {@code null} if {@code ldbOrAlias} is not a valid and
    *          connected logical database name or alias. 
    */
   protected String getLDBName(String ldbOrAlias)
   {
      ConnectionManager cm = bufferManager.getConnMgr();
      
      // check if there is such a database connected
      Database db = cm.getDatabase(ldbOrAlias);
      if (db == null)
      {
         return null; 
      }
      
      // retrieve logical database name in case this is an alias.
      return cm.getLDBName(ldbOrAlias);
   }
   
   /**
    * Get the physical database name associated with the given logical name.
    *
    * @param   ldbName
    *          A logical database name.
    *
    * @return  Physical database name.
    */
   protected String getPDBName(String ldbName)
   {
      ConnectionManager cm = bufferManager.getConnMgr();
      
      // Convert logical database name to a physical database name.
      return cm.getPDBName(ldbName, true);
   }
   
   /**
    * Get the database information object associated with the given logical database name.
    *
    * @param   ldbName
    *          A logical database name.
    *
    * @return  Database information object associated with <code>ldbName</code>.
    */
   protected Database getPDB(String ldbName)
   {
      // Look up physical database by logical database name.
      Database db = bufferManager.getConnMgr().getDatabase(ldbName);
      return dmoInfo.multiTenant ? db : db.getDefault();
   }
   
   /**
    * Perform any actions needed when this buffer's outermost scope opens (or if the buffer was not active
    * when that scope was opened, when the buffer is first activated thereafter).
    */
   protected void onOpenOutermostScope()
   {
      bufferManager.getConnMgr().openBufferScope(this);
   }
   
   /**
    * Very specific method to check if a buffer is still dangling in {@link ConnectionManager}. 
    * This should be called only in extra-ordinary execution paths that may involve
    * native Java integrations outside the persist framework. This should be called in case of
    * fail-safes to avoid memory leaks.
    * <p>
    * DON'T use this method for trivial checks on whether the buffer has its scope opened or not.
    * This should be used only to detect unexpected leaks inside {@link ConnectionManager} that can't
    * be solved by the normal lifecycle of {@link TransactionManager} or {@link BufferManager}.
    * <p>
    * For example, invoking generic triggers that do not properly manage the buffer lifecycle
    * may result in memory leaks. Therefore, attempt to detect this case and improve the fault
    * tolerance on persistence layer side.
    *          
    * @return  {@code true} if this buffer still resides in {@link ConnectionManager} due to a 
    *          bad handling of the buffer from native Java integrations like generic triggers.
    */
   public boolean isBufferScopeLeaking()
   {
      return bufferManager.getConnMgr().isBufferScopeLeaking(this);
   }
   
   /**
    * Perform any actions needed when this buffer's outermost scope closes.
    */
   protected void onCloseOutermostScope()
   {
      bufferManager.getConnMgr().closeBufferScope(this);
   }
   
   /**
    * Create a new instance of the DMO implementation class associated with
    * this buffer.  All default data values are assigned, but the new instance
    * is not yet associated with the current database session.
    *
    * @return  A transient DMO instance.
    *
    * @throws  InstantiationException
    *          if the class cannot be instantiated.
    * @throws  IllegalAccessException
    *          if the DMO class' target constructor cannot be accessed.
    * @throws  InvocationTargetException
    *          if the underlying constructor throws an exception.
    */
   protected Record instantiateDMO()
   throws ReflectiveOperationException
   {
      return getDMOImplementationClass().getDeclaredConstructor().newInstance();
   }
   
   /**
    * Open a new buffer scope for this record buffer.  This ensures that the
    * {@link BufferManager} is tracking this buffer for block scope entries
    * and exits within transactions, such that this buffer can be registered
    * with the <code>TransactionManager</code> for commit/rollback
    * notifications.  The buffer is also registered for finish processing so
    * that the appropriate record and lock transition processing can occur
    * when the buffer scope closes.
    */
   protected void openScope()
   {
      BufferManager.registerScopeable(bufferManager);
      
      if (retrying)
      {
         // reset the flag
         retrying = false;
         
         // this is a retry, nothing to do here
         return;
      }
      this.reversibleOffend = false;
      boolean outermost = (openScopeCount == 0);
      
      if (outermost)
      {
         offEnd = OffEnd.NONE;
         
         // If this is the outermost scope open for this buffer, remember the
         // scope depth at which it was opened.  This is tracked so that we
         // know when it is safe to release resources.
         activeScopeDepth = bufferManager.getOpenBufferScopes();
         
         worldScope = false;
         
         // listen for changes driven by this or other buffers for the same DMO type
         boolean persistent = pm.isThisProcedurePersistent();
         
         if (persistent && txHelper.getNearestTopLevelType() == BlockType.EXTERNAL_PROC)
         {
            // the listener is pushed to global scope for persistent procedures; it will be
            // removed together with the persistent procedure
            changeBroker.addListener(this, 0);
            registeredWithGlobalScope = openScopeCount; // save the scope it was globally added
         }
         else
         {
            // The listener is added at current scope
            changeBroker.addListener(this);
         }
         
         if (persistent && persistentProc == null)
         {
            persistentProc = pm._thisProcedure();
         }
         
         // register lightweight undoable to reset current record for undo and retry; this is only for
         // undoable tables.
         if (isUndoable())
         {
            txHelper.registerCurrent(this.undoable = new LightweightUndoable(isGlobal()));
         }
      }
      
      // register this open buffer with the buffer manager; this must be done after the persistent procedure
      // logic above, so the buffer manager can determine the right scope at which to track the buffer
      bufferManager.openScope(this);
      
      boolean active = isActive();
      
      if (active)
      {
         onOpenOutermostScope();
      }
      
      // Register this buffer as a Finalizable with the TM, so that its
      // finished method is called when this buffer scope is about to close.
      txHelper.registerFinalizable(this, false);
      
      if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE,
                 describe() +
                 ":  opened " +
                 (active ? "" : "in") +
                 "active buffer scope " +
                 bufferManager.getOpenBufferScopes());
      }
      
      // Increment reference count of open scopes for this buffer.
      openScopeCount++;
   }
   
   /**
    * Open a new buffer scope for this record buffer at the specified block depth. This ensures
    * that the {@link BufferManager} is tracking this buffer for block scope entries and exits
    * within transactions, such that this buffer can be registered with the
    * <code>TransactionManager</code> for commit/rollback notifications. The buffer is also
    * registered for finish processing so that the appropriate record and lock transition
    * processing can occur when the buffer scope closes.
    *
    * @param blockDepth
    *        Zero-based depth of the block (starting from the outermost scope) at which the buffer
    *        scope is opened.
    */
   protected void openScopeAt(int blockDepth)
   {
      BufferManager.registerScopeable(bufferManager);
      
      // Register this scope with the buffer manager.
      bufferManager.openScopeAt(blockDepth, this);
      
      activeScopeDepth = blockDepth;
      
      worldScope = false;
      
      // Listen for changes driven by this or other buffers for the same DMO type.
      changeBroker.addListener(this, blockDepth);
      
      if (blockDepth == 0 && isUndoable())
      {
         txHelper.registerCurrent(this.undoable = new LightweightUndoable(isGlobal()));
      }
      
      boolean active = isActive();
      
      if (active)
      {
         onOpenOutermostScope();
      }
      
      // Register this buffer as a Finalizable with the TM, so that its
      // finished method is called when this buffer scope is about to close.
      txHelper.registerFinalizableAt(blockDepth, this);
      
      if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE,
                 describe() + ":  opened " + (active ? "" : "in") + "active buffer scope " + blockDepth);
      }
      
      // Increment reference count of open scopes for this buffer.
      openScopeCount++;
   }
   
   /**
    * Get the multiplex ID of this buffer.  This default implementation
    * always returns <code>null</code>, but subclasses may return a
    * non-<code>null</code> value.
    *
    * @return  <code>null</code>.
    */
   public Integer getMultiplexID()
   {
      return null;
   }
   
   /**
    * Indicate whether the backing table for this buffer is multiplexed;  that
    * is, whether the backing table represents multiple, virtual tables by
    * using a multiplex ID to differentiate table segments.
    * <p>
    * This default implementation always returns <code>false</code> and should
    * be overridden by subclasses which require a different return value.
    *  
    * @return  <code>false</code>.
    */
   protected boolean isMultiplexed()
   {
      return false;
   }
   
   /**
    * Indicate whether this buffer implementation allows bulk actions.
    *  
    * @return  <code>false</code>.
    */
   protected boolean allowsBulkActions()
   {
      return false;
   }
   
   /**
    * Retrieve the master record buffer for this buffer.
    * <p>
    * This only is meaningful for shared temp tables and should be overridden
    * by subclasses.
    * 
    * @return  This buffer.
    */
   protected RecordBuffer getMasterBuffer()
   {
      return this;
   }
   
   /**
    * Fire write triggers, if any are pending, using the record currently in the buffer as the
    * current value, and a snapshot of that record taken when it was first changed as the old
    * value.
    * 
    * @return  {@code true} if the associated trigger might have been executed.
    */
   public boolean maybeFireWriteTrigger()
   {
      if (currentRecord == null || triggerTracker == null) // i.e. not available
      {
         return false;
      }
      
      DatabaseEventType det = DatabaseEventType.WRITE;
      Record oldTriggerDMO = triggerTracker.getOldDMO();
      Long recId = currentRecord.primaryKey();
      
      // determine whether there's anything to do
      if (oldTriggerDMO == null                                        ||
          !triggerTracker.isTriggerEnabled(det, null, logicalDatabase) ||
          triggerTracker.isExecuting(det, recId))
      {
         return false;
      }
      
      try
      {
         triggerTracker.pushEvent(det, recId);
         openScopeCount++;
         DatabaseTriggerManager.trigger(det,
                                        dmoBufInterface,
                                        (Buffer) dmoProxy,
                                        oldTriggerDMO,
                                        logicalDatabase);
      }
      finally
      {
         openScopeCount--;
         triggerTracker.popEvent(recId);
         triggerTracker.resetOldDMO();
      }
      
      return true;
   }
   
   /**
    * Delete the current record from the database.  Set the current record to
    * null if the deletion is successful.
    *
    * @throws  PersistenceException
    *          if no record currently is loaded in the buffer, or if there is
    *          an error deleting the current record from the database.
    * @throws  ErrorConditionException
    *          if the database trigger or the validation will fail
    */
   protected void delete()
   throws PersistenceException
   {
      // by default there is no validation
      delete(() -> logical.TRUE, () -> character.EMPTY_STRING);
   }
   
   /**
    * Delete the current record from the database.  Set the current record to {@code null} if the deletion is
    * successful.  Reclaim the record's primary key for later re-use.
    *
    * @param   valexp
    *          The validation expression to evaluate.
    * @param   valmsg
    *          The error message to display when validation fails.
    *
    * @return  {@code true} if operation is successful and {@code false} otherwise.
    *
    * @throws  PersistenceException
    *          if no record currently is loaded in the buffer, or if there is an error deleting the current
    *          record from the database.
    */
   protected boolean delete(Supplier<logical> valexp, Supplier<character> valmsg)
   throws PersistenceException
   {
      if (vst || readonly)
      {
         // TODO: find the proper message to throw when attempting to delete a record from a readonly table.
         
         // this is quite fatal, NO-ERROR will not prevent it from stopping the procedure
         ErrorManager.throwError(6311, "Deleting Virtual System Tables is not allowed", null, false);
         
         return false;
      }
      
      if (currentRecord == null)
      {
         // Nothing to delete!
         throw new PersistenceException(
            "There is no current " + getLegacyName() + " record. Delete failed",
            341);
      }
      
      if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && currentRecord.primaryKey() == null)
      {
         setCurrentRecord(null, true, false, false, false, false);
         resetSnapshot();
         return true; // the records created by the default tenant are volatile, non persistable
      }
      
      Record deletedRecord = currentRecord;
      
      // prevent nested deletes (can be caused by cascading deletes in triggers, due to lack of
      // referential integrity in legacy environment)
      if (currentRecord.checkState(DmoState.DELETING))
      {
         return false;
      }
      
      bufferManager.registerDirtyBuffer(this);
      
      Session session = persistenceContext.getSessionNoCreate();
      
      try
      {
         currentRecord.updateState(session, DmoState.DELETING, true);
         
         DatabaseEventType det = DatabaseEventType.DELETE;
         if (triggerTracker != null && triggerTracker.isTriggerEnabled(det, null, logicalDatabase))
         {
            Long recId = currentRecord.primaryKey();
            
            DatabaseEventType topEvent = triggerTracker.peekEvent(recId);
            
            if (topEvent != null)
            {
               // cannot fire DELETE trigger while any other trigger is executing
               DatabaseTriggerManager.handleError(3169, topEvent, dmoInfo.legacyTable, null);
            }
            else
            {
               // invoke database triggers
               ErrorManager.ErrorHelper err = ErrorManager.getErrorHelper();
               boolean wasSilent = err.isSilent();
               if (!wasSilent)
               {
                  err.setSilent(true);
               }
               try
               {
                  triggerTracker.pushEvent(det, recId);
                  if (!DatabaseTriggerManager.trigger(det,
                                                      dmoBufInterface,
                                                      (Buffer) dmoProxy,
                                                      null,
                                                      logicalDatabase))
                  {
                     return false; // trigger vetoed, return, no apply
                  }
               }
               finally
               {
                  triggerTracker.popEvent(recId);
                  if (!wasSilent)
                  {
                     err.setSilent(false);
                  }
               }
            }
         }
         
         // only if the trigger succeeds, the validation is tried out
         validate(valexp, valmsg);
         
         // the buffer is deleted only if the trigger and the validation succeed
         
         upgradeLock();
         
         // Null out any foreign key references to the deleted DMO.
         if (foreignNullers == null)
         {
            List<ForeignNuller> l = ForeignNuller.instances(this);
            foreignNullers = Collections.unmodifiableList(l);
         }
         if (!foreignNullers.isEmpty())
         {
            ForeignNuller.breakLinkages(foreignNullers, currentRecord);
         }
         
         // before the actual deletion check all [object] fields and decrement the ref-counter
         Set<String> ooFields = dmoInfo.getOoFields();
         if (isTemporary && !ooFields.isEmpty())
         {
            Map<String, Method> pojoGetterMap = dmoInfo.getPojoGetterMap(); // caching it locally
            for (String field : ooFields)
            {
               Method getter = pojoGetterMap.get(field);
               PropertyMeta meta = dmoInfo.getPropsByGetterMap().get(getter);
               int offset = meta.getOffset();
               DataHandler handler = meta.getDataHandler();
               //if (getter.getReturnType() == object.class)
               {
                  Map<String, Integer> extents = dmoInfo.getExtentMap();
                  Integer extent = extents.get(field);
                  if (extent != null)
                  {
                     for (int k = 0; k < extent; k++)
                     {
                        object<?> someObject = (object<?>) handler.getField(currentRecord, offset + k);
                        decrementObjectCountRef(someObject);
                     }
                  }
                  else
                  {
                     decrementObjectCountRef((object<?>) handler.getField(currentRecord, offset));
                  }
               }
            }
         }
         
         // TODO: this probably belongs in the ORM layer (in Session?)
         UniqueTracker.Token token = null;
         
         try
         {
            // the whole table becomes invalid on delete
            persistenceContext.getFastFindCache().invalidate(dmoInfo.getId(), getMultiplexID());
            
            token = uniqueTrackerCtx.lockAndDelete(uniqueTracker, currentRecord);
            persistence.delete(currentRecord);
         }
         finally
         {
            // if the deleted record was never flushed, remove it from the record nursery,
            // so it is not flushed later
            persistenceContext.getNursery().remove(this, currentRecord);
            
            if (token != null)
            {
               uniqueTrackerCtx.unlock(uniqueTracker, token);
            }
         }
         
         // notify dirty share context that this record is deleted
         if (dirtyContext != null)
         {
            dirtyContext.delete(getEntityName(), currentRecord.primaryKey(), false, true);
         }
         
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, describe() + ":  record " + currentRecord.primaryKey() + " deleted");
            if (LOG.isLoggable(Level.FINEST))
            {
               LOG.log(Level.FINEST, toString());
            }
         }
         
         if (snapshot != currentRecord && undoable != null)
         {
            undoable.checkUndoable(true);
         }
         snapshot = currentRecord;
         
         // notify listeners of the change, which will release the record in stateChanged() for this buffer
         // and any other buffer listeners
         reportChange(currentRecord, false, true);
         
         Buffer buf = (Buffer) getDMOProxy();
         if (buf.autoSynchronize().booleanValue())
         {
            buf.querySynchronize();
         }
      }
      finally
      {
         deletedRecord.updateState(session, DmoState.DELETING, false);
      }
      
      // update the counter only if exception was not thrown
      UserTableStatUpdater.delete(getDatabase(), this);
      
      // everything went fine
      return true;
   }
   
   /**
    * Perform a bulk delete, restricted by the given where clause and query substitution
    * parameters.
    * <p>
    * This method is invalid in base class so it will always throw an exception. It must be
    * overwritten by {@link TemporaryBuffer#delete} with correct logic.
    *
    * @param   suppDMOs
    *          The DMOs for the external (additional) buffers that are accessed in inner
    *          subselect, or {@code null} in case of a simple {@code where} predicate.
    * @param   where
    *          An FQL WHERE clause snippet which defines the restriction criteria to apply to the
    *          DELETE.  All references to properties in a DMO must be unqualified.
    * @param   args
    *          Query substitution parameters required by the where clause.
    *
    * @throws  PersistenceException
    *          always.
    */
   protected void delete(DataModelObject[] suppDMOs, String where, Object... args)
   throws PersistenceException
   {
      if (suppDMOs != null)
      {
         throw new IllegalArgumentException(
            "This form of the method must be called only with temp-tables.");
      }
      
      delete(where, args);
   }
   
   /**
    * Perform a bulk delete, restricted by the given where clause and query substitution
    * parameters.
    * <p>
    * This default implementation uses a looping, record-by-record delete, which is substantially
    * slower than an SQL-driven bulk delete. This is necessary for persistent tables which may be
    * accessed concurrently.
    * 
    * @param   where
    *          An FQL where clause snippet which defines the restriction
    *          criteria to apply to the DELETE.  All references to properties
    *          in a DMO must be unqualified.
    * @param   args
    *          Query substitution parameters required by the where clause.
    * 
    * @throws  PersistenceException
    *          if an error occurs performing the bulk delete operation.
    */
   protected void delete(String where, Object... args)
   throws PersistenceException
   {
      release(true);
      
      PreselectQuery q = new PreselectQuery();
      BlockManager.forEach(BlockManager.TransactionType.FULL, q, "loopAndDel",
         new Block(
            (Init) () ->
            {
               q.initialize(this, where, null, null, args);
               q.setOmitAlias(true);
            },
            (Body) () ->
            {
               q.next();
               try
               {
                  delete();
               }
               catch (PersistenceException exc)
               {
                  LOG.log(Level.SEVERE, "Error deleting record", exc);
               }
            }
         )
      );
   }
   
   /**
    * Perform a bulk delete of all records in the table associated with this buffer.
    * <p>
    * DELETE FOR was also tested with Progress 4GL with WHERE clause. It is not needed for this
    * method, but behaviour is the same. As result it is noticed that even such statement also
    * waits for whole table even if only one record is locked independently whether this record
    * satisfies WHERE clause or not . So, it seems idea of table-level locking is correct.
    * <p>
    *
    * @throws  IllegalArgumentException
    *          if no implementation class is found for the given database and interface;
    *          if setup for an invalid foreign relation fails.
    * @throws  StopConditionException
    *          if index information cannot be obtained for backing table or if database isn't
    *          connected.
    */
   protected void deleteAll()
   {
      initialize();
      
      // get EXCLUSIVE table lock, remembering current lock level
      LockType previousTableLock;
      try
      {
         previousTableLock = persistence.lockTable(table, LockType.EXCLUSIVE, true, !dmoInfo.multiTenant);
      }
      catch (LockUnavailableException e)
      {
         ErrorManager.recordOrThrowError(e);
         return;
      }
      DataModelObject dmo = getDMOProxy();
      try
      {
         P2JIndex primaryIndex = indexHelper.getPrimaryIndex(getEntityName());
         String sort;
         if (primaryIndex != null)
         {
            sort = primaryIndex.getOrderByClause(getDMOAlias(), P2JIndexComponent.PROPERTY);
         }
         else
         {
            sort = getDMOAlias() + "." + Session.PK + " asc";
         }
         final PreselectQuery query = new PreselectQuery().initialize(dmo, null, null, sort);
         
         BlockManager.forEach(BlockManager.TransactionType.FULL, query, null, new Block()
         {
            public void body()
            {
               query.next();
               try
               {
                  delete();
               }
               catch (PersistenceException e)
               {
                  ErrorManager.recordOrThrowError(e);
               }
            }
         });
      }
      catch (PersistenceException e)
      {
         ErrorManager.recordOrThrowError(e);
      }
      catch (ConditionException | IllegalStateException | IllegalArgumentException exc)
      {
         ErrorManager.recordOrThrowError(new NumberedException(exc.getMessage(), exc));
      }
      finally
      {
         // set table lock back to previous level
         try
         {
            persistence.lockTable(table, previousTableLock, true, !dmoInfo.multiTenant);
         }
         catch (LockUnavailableException e)
         {
            ErrorManager.recordOrThrowError(e);
         }
      }
   }
   
   /* *
    * Perform a bulk delete of all records in the table associated with this
    * buffer.
    * <p>
    * DELETE FOR was also tested with Progress 4GL with WHERE clause. It is not needed for this
    * method, but behaviour is the same. As result it is noticed that even such statement also
    * waits for whole table even if only one record is locked independently whether this record
    * satisfies WHERE clause or not . So, it seems idea of table-level locking is correct.
    * <p>
      Below is Savepoint based approach for deleteAll.
      Since we don't currently have simple answers for the Hibernate second-level cache or
      for dealing with any records currently held in buffers, we'd like to first use a simple,
      "converted-style" FOR-EACH loop as the basis for the RecordBuffer.deleteAll
      implementation. This approach leverages the code we already have to deal with these
      issues. If we find this causes memory problems, due to the bulk delete of very large
      tables, we can revisit the savepoint implementation and figure out these remaining issues.
    */
   /*
      protected void deleteAll()
      {
         // TODO: current table lock level is queried/stored;

         initialize();
         try
         {
            flushAll();
         }
         catch (ValidationException exc)
         {
            reportValidationException(exc);
         }

         StringBuilder buf = new StringBuilder("delete from ");
         String dmoImplName = getDMOImplementationName();
         buf.append(dmoImplName);
         String fql = buf.toString();

         try {
            int rowCount = persistence.deleteOrUpdate(fql, null, null);
            if (rowCount > 0)
            {
               changeBroker.forcePendingFlush(this);

               if (txHelper.isTransaction())
               {
                  // force the records to be deleted, on db tx rollback
                  SavepointReversible rev = new SavepointReversible(getDMOName(),
                                                                    "BULK DELETE",
                                                                    persistence.bind());
                  bufferManager.addReversible(this, null, rev, false);
               }

               // Notify all interested parties that a bulk delete occurred.
               // Listening buffers may need to release deleted records.
               RecordChangeEvent.Type eventType = RecordChangeEvent.Type.FULL_BULK_DELETE;
               changeBroker.stateChanged(eventType, this, null, null, null);
            }

         }
         catch (PersistenceException exc)
         {
            ErrorManager.recordOrThrowError(exc);
         }

         // TODO: exclusive table lock is acquired;
         // TODO: original table lock level is restored.
      }
   */
   
   /**
    * Reclaim the given list of primary keys for re-use.
    * 
    * @param   keys
    *          A list of primary keys which are available for re-use.  Must
    *          not be <code>null</code>.
    */
   protected void reclaimKeys(List<Long> keys)
   {
      /*
       * 20160823 ECF: disabling primary key reclamation. This has proven to be unsafe in
       * certain circumstances. Our record sorting algorithms rely on the natural order of
       * primary key values to disambiguate the order of records which would otherwise
       * sort equally (and thus ambiguously) using non-unique sort phrases. This can lead
       * to deadlocks across sessions, due to inconsistent sorting. This only applies to
       * permanent tables.
       */
      /*
      checkActive();
      
      bufferManager.addPendingReclaimedKeys(persistence, keys);
      */
   }
   
   /**
    * Get the cached {@link #dataModelAst} tree.
    * 
    * @return   See above.
    */
   protected DataModelAst getDataModelAst()
   {
      return dataModelAst;
   }
   
   /**
    * Cache the {@link #dataModelAst}.
    * 
    * @param    dataModelAst
    *           The data model AST.
    */
   protected void setDataModelAst(DataModelAst dataModelAst)
   {
      this.dataModelAst = dataModelAst;
   }
   
   /**
    * Get the cached {@link #schemaTableAst} tree.
    * 
    * @return   See above.
    */
   protected ProgressAst getSchemaTableAst()
   {
      return schemaTableAst;
   }
   
   /**
    * Cache the {@link #schemaTableAst}.
    * 
    * @param    schemaTableAst
    *           The schema table AST.
    */
   protected void setSchemaTableAst(ProgressAst schemaTableAst)
   {
      this.schemaTableAst = schemaTableAst;
   }
   
   /**
    * Get the number of fields in the legacy table represented by this buffer. Only normal fields
    * are counted, the hidden fields are not taken into consideration.
    * 
    * @return  Number of fields.
    */
   protected final int getNumFields()
   {
      if (numFields < 0)
      {
         numFields = lookupNumFields();
      }
      
      return numFields;
   }
   
   /**
    * Look up the number of fields in the legacy table represented by this buffer.
    * 
    * @return  Number of fields.
    */
   protected int lookupNumFields()
   {
      return TableMapper.getNumFields(getSchema(), dmoInfo.legacyTable);
   }
   
   /**
    * Get bi-directional runtime mapping of DMO properties to their associated (lower case)
    * legacy table names.
    *
    * @return  Bi-directional runtime map of DMO properties to their associated (lower case)
    *          legacy table names.
    */
   protected LegacyFieldNameMap getLegacyFieldNameMap()
   {
      if (legacyFieldNameMap == null)
      {
         legacyFieldNameMap = TableMapper.getLegacyFieldNameMap(this);
      }
      
      return legacyFieldNameMap;
   }
   
   /**
    * Gets the legacy name assigned to a dynamically prepared temp-table. This implementation
    * returns <code>null</code> and should be overridden by subclasses which require a
    * non-<code>null</code> name to be returned.
    * 
    * @return  <code>null</code>.
    */
   protected String getDynamicName()
   {
      return null;
   }
   
   /**
    * Get the buffer manager associated with this buffer.
    */
   protected BufferManager getBufferManager()
   {
      return bufferManager;
   }
   
   /**
    * Increment the context local use count for the specific instance of the
    * given record.  Used in conjunction with {@link #decrementDMOUseCount} to
    * track a DMO's in-use state.
    * <p>
    * Use counts must be maintained because multiple buffers may hold the
    * same instance of a record.  A record must not be evicted while any
    * buffer still needs it.
    *
    * @param   dmo
    *          DMO instance whose reference count is to be incremented.
    * @param   transient_
    *          {@code true} if this is a transient reference (usually from a presort query)
    * 
    * @return  Use count for <code>dmo</code> after incrementing.
    */
   protected long incrementDMOUseCount(Record dmo, boolean transient_)
   {
      checkActive();
      
      if (this.dirtyCopy)
      {
         return -1;
      }
      
      return dmo.incrementReferences(transient_);
   }
   
   /**
    * Decrement the context local use count for the specific instance of the
    * given record.  Used in conjunction with {@link #incrementDMOUseCount} to
    * track a DMO's in-use state.
    * <p>
    * Use counts must be maintained because multiple buffers may hold the
    * same instance of a record.  A record must not be evicted while any
    * buffer still needs it.
    *
    * @param   dmo
    *          DMO instance whose reference count is to be decremented.
    * @param   transient_
    *          {@code true} if this is a transient reference (usually from a presort query)
    * 
    * @return  Use count for <code>dmo</code> after decrementing.
    */
   protected long decrementDMOUseCount(Record dmo, boolean transient_)
   {
      checkActive();
      
      if (this.dirtyCopy)
      {
         return -1;
      }
      
      return dmo.decrementReferences(transient_);
   }
   
   /**
    * Evict the given DMO from the current Hibernate session if it is not
    * referenced by any buffer or other resource.  This method does not
    * modify the DMO's use count, if any.  It is intended to quickly evict
    * DMOs loaded into the session by side effect.
    * 
    * @param   dmo
    *          DMO instance whose reference count is to be decremented.
    * 
    * @return  <code>true</code> if the DMO was evicted;  <code>false</code>
    *          if it was referenced by other resources.
    *
    * @throws  PersistenceException
    *          if an error occurs detaching <code>dmo</code> from the
    *          persistence session.
    */
   protected boolean evictDMOIfUnused(Record dmo)
   throws PersistenceException
   {
      checkActive();
      
      return bufferManager.evictDMOIfUnused(persistence, persistenceContext, dmo);
   }
   
   /**
    * Report any pending changes in the current record's state to the {@link
    * ChangeBroker} for broadcast to interested listeners.  A change includes
    * any modification to a property or the deletion of the record.  If there
    * is no change to report, this method returns immediately.
    * <p>
    * If this buffer has not previously taken a snapshot of its current
    * record, a deep copy is made of <code>dmo</code> (which represents the
    * post-modification state of this buffer's current record).  Any
    * unreported changes are then rolled back from this copy, so that the
    * copy represents a snapshot of the record at the time it was first
    * stored in the buffer.
    * <p>
    * This seemingly tortuous method of lazy initialization of the snapshot
    * is used because it allows us in certain cases to avoid ever making a
    * deep copy of the buffer's current record.  If that record represents a
    * Hibernate proxy which was never initialized, we avoid the overhead of
    * ever hydrating the proxy, which in tight loops can result in a HUGE
    * time savings.
    * <p>
    * The snapshot is then passed along with <code>dmo</code>, this buffer,
    * and a map of property names to getter method indexes (for extent/array
    * properties), to the change broker for dispatching to listeners.
    *
    * @param   dmo
    *          DMO which represents the post-modification state of the
    *          current record.  In the case of record deletion, this object
    *          represents the state of the record just before deletion.
    * @param   insert
    *          <code>true</code> if change represents a record insertion,
    *          otherwise <code>false</code>.
    * @param   delete
    *          <code>true</code> if change represents a record deletion,
    *          otherwise <code>false</code>.
    *
    * @throws  PersistenceException
    *          if any reflection error occurs taking the snapshot, or if the
    *          change broker forwards an exception from one of its listeners.
    */
   protected void reportChange(Record dmo, boolean insert, boolean delete)
   throws PersistenceException
   {
      // sanity check
      if (!delete                      &&    // is not a delete
          !insert                      &&    // is not an insert
          unreportedChanges == null)         // is not an update
      {
         return;
      }
      
      try
      {
         Record snapshot = insert ? null : this.snapshot;
         boolean takeSnapshot = (snapshot == null) && !insert;
         
         if (takeSnapshot)
         {
            // DMO parameter represents the current record post-change (except
            // in the delete case), so we deep copy it, then apply the old
            // values selectively below, to reconstruct the original state.
            // In the delete case, there are no diffs to apply, but we still
            // need the snapshot.
            snapshot = dmo.snapshot();
         }
         
         // Property/extent map for change event.
         Map<String, List<Integer>> properties = null;
         
         if (unreportedChanges != null)
         {
            try
            {
               PropertyMeta[] props = metadata.getPropertyMeta(false);
               List<Integer> extentIndices = null;
               int lastExtent = 0;
               
               for (int k = 0; k < props.length; k++)
               {
                  Object[] values = unreportedChanges[k];
                  if (values == null)
                  {
                     continue;
                  }
                  PropertyMeta prop = props[k];
                  int crtExtent = prop.getExtent();
                  
                  if (crtExtent != 0) // is it an extent?
                  {
                     if (extentIndices == null || crtExtent != lastExtent)
                     {
                        extentIndices = new ArrayList<>();
                        if (properties == null)
                        {
                           properties = new HashMap<>();
                        }
                        properties.put(prop.getName(), extentIndices);
                        lastExtent = crtExtent;
                     }
                     
                     extentIndices.add(k - prop.getOffset());
                     if (takeSnapshot)
                     {
                        OrmUtils.setField(snapshot, k, values[0]);
                     }
                  }
                  else // simple property
                  {
                     if (properties == null)
                     {
                        properties = new HashMap<>();
                     }
                     properties.put(prop.getName(), null);
                     if (takeSnapshot)
                     {
                        // apply old value of property to snapshot
                        OrmUtils.setField(snapshot, k, values[0]);
                     }
                  }
               }
            }
            catch (Exception rflExc)
            {
               DBUtils.handleException(getDatabase(), rflExc);
               
               throw new PersistenceException(rflExc);
            }
         }
         
         if (properties == null)
         {
            properties = Collections.emptyMap();
         }
         
         if (LOG.isLoggable(Level.FINEST))
         {
            String desc = describe();
            LOG.log(Level.FINEST, desc + ":  DMO state changed...");
            LOG.log(Level.FINEST, desc + ":  Old state:  " + toString(snapshot));
            LOG.log(Level.FINEST, desc + ":  New state:  " + toString());
            LOG.log(Level.FINEST, desc + ":  Properties/Extents:  " + properties);
         }
         
         // Notify listeners.
         RecordChangeEvent.Type type = null;
         if (insert)
         {
            type = RecordChangeEvent.Type.INSERT;
         }
         else if (delete)
         {
            type = RecordChangeEvent.Type.DELETE;
         }
         else
         {
            type = RecordChangeEvent.Type.UPDATE;
         }
         changeBroker.stateChanged(type, this, dmo, snapshot, properties);
      }
      finally
      {
         unreportedChanges = null;
      }
   }
   
   /**
    * Provide a short descriptor of this buffer instance for debugging and
    * logging purposes.  The descriptor includes the buffer's database name,
    * short DMO interface name, its variable name, and the identifier for the
    * current context.
    *
    * @return  Short text descriptor.
    */
   protected String describe()
   {
      StringBuilder buf = new StringBuilder("[");
      buf.append(Utils.describeContext());
      buf.append("-->");
      if (isActive())
      {
         buf.append(getDatabase());
      }
      else
      {
         buf.append("'");
         buf.append(ldbOrAlias);
         buf.append("'");
      }
      buf.append(".");
      buf.append(getDMOName());
      Integer mpid = getMultiplexID();
      if (mpid != null)
      {
         buf.append(":");
         buf.append(mpid);
      }
      buf.append("@");
      buf.append(bufferManager.getOpenBufferScopes());
      buf.append(" (");
      buf.append(variable);
      buf.append(") ");
      buf.append(txHelper.currentTransactionLevel());
      buf.append("]");
      
      return buf.toString();
   }
   
   /**
    * Attempt to upgrade to an exclusive lock on the current record, if we do
    * not already have one.  The current context must already possess a share
    * lock (or greater) for this method to complete successfully.  The current
    * thread will block inside the lock manager if the lock is not immediately
    * available.  When this method returns normally, the caller can assume the
    * lock has been acquired.
    *
    * @throws  PersistenceException
    *          if the current context does not have a share lock (or greater)
    *          on the current record.
    */
   public void upgradeLock()
   throws PersistenceException
   {
      if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && currentRecord.primaryKey() == null)
      {
         return; // the records created by the default tenant are volatile. No locking in this case.
      }
      
      RecordIdentifier<String> ident = currentRecord.getRecordLockIdentifier();
      LockType currentType = persistence.queryLock(ident, !dmoInfo.multiTenant);
      
      if (currentType == LockType.NONE)
      {
         throw new PersistenceException(
            getLegacyName() + " record has NO-LOCK status, update to field not allowed", 396);
      }
      
      persistenceContext.getRecordLockContext().lock(ident, LockType.EXCLUSIVE, 0L, this);
   }
   
   /**
    * Load the {@code DataModelObject} sent as parameter in this buffer with associate it with the persistence
    * session of the buffer. It will automatically receive a primary key and all properties will be marked as
    * 'dirty' to allow the object to be saved to database. If there is a record already in the buffer, that
    * record is validated, its WRITE trigger fired and then flushed to database before setting the new record.
    * 
    * @param   dmo
    *          The object to be associated.
    *
    * @throws  PersistenceException
    *          if there was an error while retrieving the next primary key.
    */
   public void associate(DataModelObject dmo)
   throws PersistenceException
   {
      if (!(dmo instanceof Record) || ((Record) dmo).primaryKey() != null)
      {
         throw new IllegalArgumentException("Invalid DMO: not a Record or DMO is already associated.");
      }
      
      Record asRecord = (Record) dmo;
      BitSet dirtyProps = asRecord.getDirtyProps(); // this assumes the original field is returned, not a copy
      RecordMeta rm = asRecord._getRecordMeta();
      boolean sharedDb = !rm.getDmoMeta().multiTenant;
      
      dirtyProps.set(0, rm.getPropertyMeta(false).length);
      asRecord.updateState(persistence.getSession(sharedDb), DmoState.NEW | DmoState.CHANGED, true);
      asRecord.primaryKey(nextPrimaryKey());
      setCurrentRecord(asRecord, false, true, false, true);
   }
   
   /**
    * Retrieve the set of all properties for this buffer's DMO entity, which represent database columns that
    * participate in an index on the entity's backing table, or only the subset which participate in unique
    * indexes.
    * 
    * @param   unique
    *          {@code false} to get all indexed properties; {@code true} to get only those that participate
    *          in unique indexes.
    * 
    * @return  BitSet of indexed properties.  The set may be empty but it will never be {@code null}.
    */
   private BitSet getIndexedProperties(boolean unique)
   {
      if (unique)
      {
         return metadata.getAllIndexedProperties();
      }
      
      BitSet[] indices = metadata.getUniqueIndices();
      BitSet ret = new BitSet();
      
      for (BitSet idx : indices)
      {
         ret.or(idx);
      }
      
      return ret;
   }
   
   /**
    * Generate a formatted description of the contents of {@code record}.
    * The report includes the name and current value of every data property within the record.
    *
    * @param   record
    *          Record whose contents are to be written to the report.
    *
    * @return  A formatted report of {@code record}'s contents.
    */
   protected String toString(Record record)
   {
      boolean active = isActive();
      String sep = System.getProperty("line.separator");
      StringBuilder buf = new StringBuilder();
      
      buf.append(active ? getDatabase() : ldbOrAlias);
      buf.append(" ");
      buf.append(getDMOName());
      buf.append(" (");
      buf.append(getDMOAlias());
      buf.append("@");
      buf.append(bufferManager.getOpenBufferScopes());
      
      if (!active)
      {
         buf.append(") - INACTIVE");
         
         return buf.toString();
      }
      
      buf.append(":");
      buf.append(activeScopeDepth);
      buf.append("#");
      buf.append(openScopeCount);
      buf.append(") [");
      buf.append(getTable());
      buf.append("]").append(sep);
      buf.append("---------------------------------------------").append(sep);
      
      if (record == null)
      {
         buf.append("   No record");
         
         return buf.toString();
      }
      
      buf.append(record.toString(false)).append(sep);
      buf.append("---------------------------------------------").append(sep);
      
      if (metadata != null)
      {
         String name = null;
         PropertyMeta[] propMeta = metadata.getPropertyMeta(false);
         int len = propMeta.length;
         String[] names = new String[len];
         int maxWidth = Session.PK.length();
         
         for (int i = 0; i < len; i++)
         {
            PropertyMeta next = propMeta[i];
            name = next.getLegacyName();
            int extent = next.getExtent();
            if (extent > 0)
            {
               name = name + '[' + extent + ']';
            }
            names[i] = name;
            maxWidth = Math.max(maxWidth,  name.length());
         }
         
         buf.append("   ");
         StringHelper.leftAlignText(Session.PK, maxWidth, buf);
         buf.append(" :  ");
         buf.append(record.primaryKey());
         
         Object[] data = record.getData(this);
         
         for (int i = 0; i < len; i++)
         {
            name = names[i];
            buf.append(sep);
            buf.append("   ");
            StringHelper.leftAlignText(name, maxWidth, buf);
            buf.append(" :  ");
            buf.append(formatProperty(data[i]));
         }
      }
      
      buf.append(sep);
      
      return buf.toString();
   }
   
   /**
    * Returns the primary key for a new DMO associated with this buffer.
    *
    * @return   The primary key for a new DMO associated with this buffer.
    *
    * @throws   PersistenceException
    *           if there was an error while retrieving the next primary key.
    */
   protected Long nextPrimaryKey()
   throws PersistenceException
   {
      checkActive();
      
      return persistence.nextPrimaryKey(table, !dmoInfo.multiTenant);
   }
   
   /**
    * Initializes the map that contains primary keys of past and present records held by this buffer,
    * mapped to their pinned LockTypes.
    * <p>
    * Initialize the set which will hold the primary keys of records affected by a rollback in the current
    * scope.
    */
   protected void initPinnedLockTypes()
   {
      pinnedLocks = new PinnedLocks();
   }
   
   /**
    * Decrements the counter-reference for the specified object.
    * <p>
    * This is only active for TEMP-TABLES, for permanent tables this has no effect as they cannot
    * store {@code object} fields.
    *
    * @param   obj
    *          The object whose reference need to be decremented.
    */
   protected void decrementObjectCountRef(object<?> obj)
   {
   }
   
   /**
    * Check if this buffer is associated with a global buffer.
    * 
    * @return  Always <code>false</code>.
    */
   protected boolean isGlobal()
   {
      return false;
   }
   
   /**
    * Get the persistence context for this buffer.
    * 
    * @return  Persistence context, which will be <code>null</code> if this buffer has not yet
    *          been initialized.
    */
   protected Persistence.Context getPersistenceContext()
   {
      return persistenceContext;
   }
   
   /**
    * Register a query that should be closed when the scope of this buffer is closed.
    *
    * @param   query
    *          Query to register.
    */
   protected void registerRelatedQuery(P2JQuery query)
   {
      if (relatedQueries == null)
      {
         relatedQueries = new HashSet<>();
      }
      
      relatedQueries.add(query);
   }
   
   /**
    * Unregister a query from the list of the queries that should be closed when the scope of
    * this buffer is closed.
    *
    * @param   query
    *          Query to unregister.
    */
   protected void unregisterRelatedQuery(P2JQuery query)
   {
      if (relatedQueries != null)
      {
         relatedQueries.remove(query);
      }
   }
   
   /**
    * Get the table/buffer legacy name. If the legacy name was temporarily overridden, then that name is
    * returned instead until the name is restored back to old name.
    * 
    * @return  The legacy name explicitly set by business logic, or if none, the default legacy table name.
    */
   protected String getLegacyName()
   {
      if (overrideLegacyName != null)
      {
         return overrideLegacyName;
      }
      
      String name = dmoProxy != null ? ((BufferImpl) dmoProxy)._name() : null;
      
      return name == null ? dmoInfo.legacyTable : name;
   }
   
   /**
    * Aggregate method which averages the values of a single column across rows in the table
    * backing this buffer.
    *
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          FQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    *
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   protected void average(String column, NumberType num, String where, Object... args)
   {
      aggregate("avg", column, num, where, args);
   }
   
   /**
    * Aggregate method which counts the rows in the table backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate. May be the wildcard
    *          character ("*").
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          FQL where clause used to filter the records.
    * @param   args
    *          Optional query substitution arguments.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   protected void count(String column, NumberType num, String where, Object... args)
   {
      aggregate("count", column, num, where, args);
   }
   
   /**
    * Aggregate method which finds the minimum value of a single column across rows in the table
    * backing this buffer.
    *
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          FQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    *
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   protected void minimum(String column, NumberType num, String where, Object... args)
   {
      aggregate("min", column, num, where, args);
   }
   
   /**
    * Aggregate method which finds the maximum value of a single column across rows in the table
    * backing this buffer.
    *
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          FQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    *
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   protected void maximum(String column, NumberType num, String where, Object... args)
   {
      aggregate("max", column, num, where, args);
   }
   
   /**
    * Aggregate method which sums the values of a single column across rows in the table
    * backing this buffer.
    *
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          FQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    *
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   protected void sum(String column, NumberType num, String where, Object... args)
   {
      aggregate("sum", column, num, where, args);
   }
   
   /**
    * Indicate whether this buffer is active.  A buffer is active if it has
    * been used by a query or otherwise initialized.  Opening a buffer scope
    * alone is not enough to activate a buffer, but it is a prerequisite to
    * activation;  the buffer must actually be used to be considered active.
    * 
    * @return  <code>true</code> if the buffer has been initialized.
    */
   protected boolean isActive()
   {
      return (persistence != null);
   }
   
   /**
    * Record or throw a &quot;record not on file&quot; error condition for
    * the current DMO type.
    *
    * @throws  ErrorConditionException
    *          if silent error mode is suppressed.
    */
   protected void errorNotOnFile()
   {
      final String msg = getLegacyName() + " record not on file";
      
      if (LOG.isLoggable(Level.FINE))
      {
         PersistenceException exc = new PersistenceException(msg, 138);
         LOG.log(Level.FINE, msg, exc);
      }
      
      ErrorManager.recordOrThrowError(138, msg);
   }
   
   /**
    * Get the number of scopes currently open for this buffer.  This is
    * necessary because buffer scopes can be nested.
    *
    * @return  Number of scopes currently open for this buffer.
    */
   protected int getOpenScopeCount()
   {
      return openScopeCount;
   }
   
   /**
    * Validate and release the current record in this buffer and release its
    * lock, if any, according to the following rules:
    * <ul>
    *   <li>the lock is released at the later of:
    *   <ul>
    *     <li>end of the current transaction (if any);
    *     <li>end of the current buffer scope.
    *   </ul>
    *   <li>the lock is downgraded at the end of the current transaction,
    *       if the current buffer scope is larger than the transaction scope.
    *   <li>if the lock never passed through a transaction, it is released
    *       immediately.
    * </ul>
    * 
    * @param   allowWriteTrigger
    *          {@code true} to allow a write trigger to be fired on this release. This generally
    *          should be {@code true} for non-iterating queries and releases invoked explicitly
    *          by business logic. It should be {@code false} for iterating queries. Other implicit
    *          releases depend on the situation.
    *
    * @throws  ErrorConditionException
    *          if validation of the current buffer record (if any) fails.
    */
   protected void release(boolean allowWriteTrigger)
   {
      release(false, allowWriteTrigger);
   }
   
   /**
    * Possibly validate and release the current record in this buffer and release its
    * lock, if any, according to the following rules:
    * <ul>
    *   <li>the lock is released at the later of:
    *   <ul>
    *     <li>end of the current transaction (if any);
    *     <li>end of the current buffer scope.
    *   </ul>
    *   <li>the lock is downgraded at the end of the current transaction,
    *       if the current buffer scope is larger than the transaction scope.
    *   <li>if the lock never passed through a transaction, it is released
    *       immediately.
    * </ul>
    * 
    * @param   undo
    *          {@code true} if currently processing an undo, else {@code false}. The outgoing
    *          record, if any, is not validated and flushed if this parameter is {@code true}.
    * @param   allowWriteTrigger
    *          {@code true} to allow a write trigger to be fired on this release. This generally
    *          should be {@code true} for non-iterating queries and releases invoked explicitly
    *          by business logic. It should be {@code false} for iterating queries. Other implicit
    *          releases depend on the situation.
    *
    * @throws  ErrorConditionException
    *          if validation of the current buffer record (if any) fails.
    */
   protected void release(boolean undo, boolean allowWriteTrigger)
   {
      release(undo, allowWriteTrigger, true);
   }
   
   /**
    * Possibly validate and release the current record in this buffer and release its
    * lock, if any, according to the following rules:
    * <ul>
    *   <li>the lock is released at the later of:
    *   <ul>
    *     <li>end of the current transaction (if any);
    *     <li>end of the current buffer scope.
    *   </ul>
    *   <li>the lock is downgraded at the end of the current transaction,
    *       if the current buffer scope is larger than the transaction scope.
    *   <li>if the lock never passed through a transaction, it is released
    *       immediately.
    * </ul>
    * 
    * @param   undo
    *          {@code true} if currently processing an undo, else {@code false}. The outgoing
    *          record, if any, is not validated and flushed if this parameter is {@code true}.
    * @param   allowWriteTrigger
    *          {@code true} to allow a write trigger to be fired on this release. This generally
    *          should be {@code true} for non-iterating queries and releases invoked explicitly
    *          by business logic. It should be {@code false} for iterating queries. Other implicit
    *          releases depend on the situation.
    * @param   honorMultipleReferences
    *          {@code true} if the released record should be flushed only if this buffer was the
    *          only one referencing it. If this is {@code false}, even if the release buffer is
    *          loaded in other buffers, it will be flushed.
    *
    * @throws  ErrorConditionException
    *          if validation of the current buffer record (if any) fails.
    */
   protected void release(boolean undo, boolean allowWriteTrigger, boolean honorMultipleReferences)
   {
      if (currentRecord != null)
      {
         if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && currentRecord.primaryKey() == null)
         {
            ErrorManager.throwError(15991, getLegacyName(), ""); // non-silentiable
            // Attempt to create in multi-tenant table '<tablename>' where the partition (usually default) has not been allocated. (15991)
            return;
         }
         
         setCurrentRecord(null, undo, false, false, allowWriteTrigger, honorMultipleReferences);
      }
            
      Buffer bufferImpl = (Buffer) dmoProxy;
      if (bufferImpl.autoSynchronize().booleanValue())
      {
         bufferImpl.querySynchronize();
      }
      // be sure the flag is reset even if the buffer remains empty
      if (!undo)
      {
         unknownMode = false;
      }
   }
   
   /**
    * Release the current record and discard the reference record snapshot.
    */
   protected void reset()
   {
      this.reversibleOffend = false;
      // do not allow WRITE trigger to fire; this is only called in the context of an iterating query
      release(false);
      
      if (snapshot != null && undoable != null)
      {
         undoable.checkUndoable(true);
      }
      snapshot = null;
   }

   /**
    * Discard the reference record snapshot.
    */
   protected void resetSnapshot()
   {
      snapshot = null;
   }
   
   /**
    * Reload the current record, if any, from the database. If {@code lockType} is higher than
    * the current lock type, it is upgraded immediately. If lower, it is downgraded immediately
    * only if outside of a transaction. If inside a transaction, the lock is marked for downgrade
    * to the specified type upon transaction end.
    *
    * @param   lockType
    *          Requested lock type.
    * @param   errorIfNull
    *          {@code true} if the current record being null should raise an error; else
    *          {@code false}.
    *
    * @throws  ErrorConditionException
    *          if there is no record loaded into the buffer, the error manager is not in silent
    *          mode, and the caller considers this an error.
    */
   protected void reload(LockType lockType, boolean errorIfNull)
   {
      if (currentRecord == null && !errorIfNull)
      {
         // we have no current record from which to get a primary key to perform a reload, and
         // this is not considered an error by the caller, so nothing to do
         return;
      }
      
      load(currentRecord, lockType, false, true);
   }
   
   /**
    * Loads the template record for this buffer. The template cannot be altered (at least without
    * acquiring the database schema lock) and cannot be saved back to database. It must not reach
    * the ORM session.
    * <p>
    * By convention, the template records have negative rowid/recid.
    * 
    * @param   templateId
    *          The id of the template record.
    *
    * @throws  IllegalArgumentException
    *          if the {@code templateId} is not the rowid of a template record (it is positive).
    */
   protected void loadTemplateRecord(long templateId)
   {
      Long actualTemplateId = MetadataManager.getTemplateRowid(getDMOImplementationClass());
      if (actualTemplateId == null || actualTemplateId != templateId)
      {
         // P2J metadata is not active or the template is incorrect
         throw new IllegalArgumentException("Error loading template record");
      }
      
      Record dmo = null;
      try
      {
         dmo = instantiateDMO();
      }
      catch (ReflectiveOperationException exc)
      {
         throw new RuntimeException("Error creating record", exc);
      }
      
      dmo.primaryKey(templateId);
      setCurrentRecord(dmo, false, false, false, false);
      
      sortIndex = null;
      offEnd = OffEnd.NONE;
   }
   
   /**
    * Set the DMO implementation instance which backs this buffer.  It is expected that this
    * variant is invoked only by queries storing their retrieved record.
    * <p>
    * Setting the value to {@code null} may result in an error or query off-end condition,
    * depending upon the value of {@code errorIfNull} and the current silent error mode.
    * <p>
    * It is expected that the calling query specify the sort index which was in use and whether
    * the query ran off-end (and in which direction). This information is useful to {@link
    * FindQuery find queries} which must determine an appropriate starting point for their search.
    * <p>
    * Note: the access level of this method must be at least {@code protected}. Although this is not required
    *       at compile time, some applications may stop with a {@code NullPointerException} because the
    *       not be proxied otherwise.
    *
    * @param   currentRecord
    *          DMO instance which represents the record currently backing this buffer.
    * @param   lockType
    *          Lock type (already applied to a non-{@code null} {@code currentRecord}), which will
    *          be pinned by this record buffer.
    * @param   errorIfNull
    *          {@code false} to raise an end condition if the record being set is {@code null};
    *          else {@code true} (caller is responsible for handling appropriately in the latter case).
    * @param   lenientOffEnd
    *          {@code false} to raise an end condition if the record being set is {@code null};
    *          else {@code true} (caller is responsible for handling appropriately in the latter case).
    * @param   sortIndex
    *          Sort index which was in use at the time the given record was fetched.
    * @param   offEnd
    *          Enum indicating whether query is off-end, and if so, in which direction.
    * @param   inverseSorting
    *          Indicates whether {@code offEnd} is specified against the inverse sorting direction.
    * @param   dirtyCopy
    *          Indicates whether {@code currentRecord} represents a copy of a DMO found in the
    *          dirty database.
    * @param   allowWriteTrigger
    *          {@code true} to allow a WRITE trigger to be fired on this event, else {@code false}.
    * 
    * @throws  QueryOffEndException
    *          if {@code currentRecord} is {@code null} and {@code errorIfNull} is {@code false}.
    * 
    * @see     #getOffEnd(SortIndex,boolean)
    */
   protected void setRecord(Record currentRecord,
                            LockType lockType,
                            boolean errorIfNull,
                            boolean lenientOffEnd, 
                            SortIndex sortIndex,
                            OffEnd offEnd,
                            boolean inverseSorting,
                            boolean dirtyCopy,
                            boolean allowWriteTrigger)
   throws QueryOffEndException
   {
      setCurrentRecord(currentRecord, false, false, dirtyCopy, allowWriteTrigger);
      this.sortIndex = sortIndex;
      this.offEnd    = inverseSorting ? offEnd.inverse() : offEnd;
      
      if (currentRecord == null)
      {
         if (!errorIfNull && !lenientOffEnd)
         {
            throwOffEnd();
         }
      }
      else
      {
         Long recId = currentRecord.primaryKey();
         if (lockType != LockType.NONE)
         {
            RecordIdentifier<String> ident = currentRecord.getRecordLockIdentifier();
            persistenceContext.getRecordLockContext().registerBuffer(ident, this);
         }
         setPinnedLockType(recId, lockType);
         
         DatabaseEventType det = DatabaseEventType.FIND;
         if (triggerTracker != null && triggerTracker.isTriggerEnabled(det, null, logicalDatabase))
         {
            if (triggerTracker.isExecuting(det, recId))
            {
               DatabaseTriggerManager.handleError(2873, det, dmoInfo.legacyTable, null);
            }
            else
            {
               // when everything is done, execute find triggers
               ErrorManager.ErrorHelper err = ErrorManager.getErrorHelper();
               boolean wasSilent = err.isSilent();
               if (!wasSilent)
               {
                  err.setSilent(true);
               }
               try
               {
                  triggerTracker.pushEvent(det, recId);
                  if (!DatabaseTriggerManager.trigger(det,
                                                      dmoBufInterface,
                                                      (Buffer) dmoProxy,
                                                      null,
                                                      logicalDatabase))
                  {
                     setCurrentRecord(null, false, false, false, false);
                  }
               }
               finally
               {
                  triggerTracker.popEvent(recId);
                  if (!wasSilent)
                  {
                     err.setSilent(false);
                  }
               }
            }
         }
         UserTableStatUpdater.retrieve(getDatabase(), this);
      }
   }
   
   /**
    * Throw a <code>QueryOffEndException</code>.
    *
    * @throws  QueryOffEndException
    *          always.
    */
   protected void throwOffEnd()
   {
      throw new QueryOffEndException("", new QueryOffEndListener[] { offEndListener });
   }
   
   /**
    * Get an indication whether the last current record set by a query:
    * <ol>
    *   <li>was <code>null</code> (meaning the query was off-end), and if so
    *   <li>whether it used the same sort index as the one specified, and if
    *       so
    *   <li>in which direction the query ran off-end.
    * </ol>
    * <p>
    * In the event no query has yet set a record in this buffer, a value of
    * <code>OffEnd.FRONT</code> will be returned.  In the event the last query
    * neither ran off-end, nor used the given sort index, a value of
    * <code>OffEnd.NONE</code> will be returned.
    * 
    * @param   sortIndex
    *          Sort index to compare with the last sort index used to
    *          populate (or clear) the buffer.
    * @param   inverseSorting
    *          Indicates whether the off-end is requested by a query having
    *          the inverse sorting direction.
    * 
    * @return  See above.
    * 
    * @see     #setRecord
    */
   protected OffEnd getOffEnd(SortIndex sortIndex, boolean inverseSorting)
   {
      if (this.sortIndex == null || this.sortIndex.equals(sortIndex))
      {
         return inverseSorting ? offEnd.inverse() : offEnd;
      }
      
      return OffEnd.NONE;
   }
   
   /**
    * Set the off-end status for the query most recently involving this buffer.
    *
    * @param   offEnd
    *          Off-end status.
    */
   protected void setOffEnd(OffEnd offEnd)
   {
      this.offEnd = offEnd;
   }
   
   /** 
    * Get the {@link QueryOffEndListener} instance for this record buffer.
    * 
    * @return the {@link QueryOffEndListener} instance for this record buffer.
    */
   protected QueryOffEndListener getQueryOffEndListener()
   {
      return offEndListener;
   }
   
   /**
    * Push a temporary record context onto the buffer for purposes such as
    * client-side sorting.  Such a context must be pushed before {@link
    * #setTempRecord(Record) setting a temporary record} is possible.
    * A context allows one or more temporary records to be set for a specific
    * operation, such as the execution of client-side where clause
    * expressions by a query, and for such operations to be nested.  For
    * example, a client-side where expression may eventually execute a query
    * which then itself may execute a client-side where expression.  Each
    * level of client-side where clause expression must execute within its
    * own temporary record context, such that the state of the outer where
    * clause expression is not lost when the inner expression is finished.
    * When client code is finished with a temporary record context, it should
    * {@link #popTempContext() pop} the current context.
    * <p>
    * Pushes and pops of the temporary record context must be balanced.  This
    * is the responsibility of the caller.
    */
   protected void pushTempContext()
   {
      if (tempRecords == null)
      {
         // It is rare that we would need a depth greater than 1, so make the
         // default size 1.
         tempRecords = new ArrayList<>(1);
      }
      
      tempRecords.add(null);
   }
   
   /**
    * Pop the current, temporary record context from this buffer.
    * <p>
    * As a convenience, the currently active temporary record for this buffer
    * is set to <code>null</code> first.
    * 
    * @throws  PersistenceException
    *          if setting the current, temporary record to <code>null</code>
    *          triggers a failure to evict the temporary record from the
    *          active Hibernate session.
    * 
    * @see     #pushTempContext()
    */
   protected void popTempContext()
   throws PersistenceException
   {
      setTempRecord(null);
      tempRecords.remove(tempRecords.size() - 1);
   }
   
   /**
    * Set a temporary record into the buffer for purposes such as client-side
    * sorting.  Setting a temporary record has no impact on locking or
    * validation.  It is essentially a read-only record which temporarily
    * backs the buffer proxy, so that non-updating client code can run using
    * the buffer.
    * <p>
    * The default value of the temporary record is <code>null</code>.  It is
    * only set to a non-<code>null</code> reference in order to do some work
    * with the buffer.  When the work is complete, it is critical that the
    * temporary record be set back to <code>null</code>.
    * <p>
    * Temporary records increment the context-wide DMO use count while they
    * are active and decrement it when they are released.  This may result
    * in a temporary record being evicted from the persistence session when
    * it is replaced.
    *
    * @param   tempRecord
    *          Record which will temporarily back the buffer.
    */
   protected void setTempRecord(Record tempRecord)
   {
      Record old = getTempRecord();
      
      if (old != null)
      {
         decrementDMOUseCount(old, true);
      }
      
      tempRecords.set(tempRecords.size() - 1, tempRecord);
      
      if (tempRecord != null)
      {
         incrementDMOUseCount(tempRecord, true);
      }
   }
   
   /**
    * Get the combined DMO and Buffer grouping interface associated with this buffer.
    * 
    * @return  DMO and Buffer grouping interface.
    */
   protected Class<? extends Buffer> getDMOBufInterface()
   {
      return dmoBufInterface;
   }
   
   /**
    * Report the DMO implementation class associated with this buffer.
    *
    * @return  DMO implementation class.
    *
    * @throws  IllegalStateException
    *          if the buffer fails the initialization verification.
    */
   protected Class<? extends Record> getDMOImplementationClass()
   {
      initialize();
      
      return dmoClass;
   }
   
   /**
    * Get the unqualified name of the DMO's interface.
    *
    * @return  DMO interface short name.
    */
   protected String getDMOName()
   {
      return Utils.getUnqualifiedName(dmoInterface);
   }
   
   /**
    * Get the unqualified name of the DMO implementation class.
    *
    * @return  DMO short class name.
    */
   protected String getDMOImplementationName()
   {
      return Utils.getUnqualifiedName(getDMOImplementationClass());
   }
   
   /**
    * Get the variable name associated with this buffer instance.
    *
    * @return  Variable name associated with the buffer.
    */
   public String getDMOAlias()
   {
      return variable;
   }
   
   /**
    * Get the token associated with this buffer instance.
    *
    * @return  Token name associated with the buffer.
    */
   public String getDMOToken()
   {
      return token;
   }
   
   /**
    * Report whether this buffer is in unknown mode, due to an outer join not finding a record.
    * 
    * @return  <code>true</code> if in unknown mode, else <code>false</code>.
    */
   public boolean isUnknownMode()
   {
      return unknownMode;
   }
   
   /**
    * Makes a backup of the current variable and it replaces it with another one.
    * This is usually used by an argument proxy to temporarily replace the DMO alias.
    * The override is successful only if the DMO alias wasn't overridden beforehand.
    * 
    * @param   newVariable
    *          The new DMO alias which should be temporarily used by this RecordBuffer.
    * 
    * @return  {@code true} if the overriding was successful, {@code false} otherwise.
    */
   protected boolean overrideDMOAlias(String newVariable)
   {
      if (oldVariable != null)
      {
         return false;
      }
      
      oldVariable = variable;
      variable = newVariable;
      return true;
   }
   
   /**
    * Restores the DMO alias by replacing the current variable with the one stored in the backup.
    */
   protected void restoreDMOAlias()
   {
      variable = oldVariable;
      oldVariable = null;
   }
   
   /**
    * Makes a backup of the current legacy name and temporarily it replaces it with another one. This is used
    * to avoid buffer name collisions when working with dynamic buffers/queries.
    * The override is successful only if the DMO name wasn't overridden beforehand. Be sure to call the 
    * {@code restoreLegacyName} to restore the real name.
    * 
    * @param   newName
    *          The new legacy name which should be temporarily used by this RecordBuffer.
    * 
    * @return  {@code true} if the overriding was successful, {@code false} otherwise.
    */
   protected boolean overrideLegacyName(String newName)
   {
      if (overrideLegacyName != null)
      {
         return false;
      }
      
      overrideLegacyName = newName;
      return true;
   }
   
   /**
    * Restores the DMO alias by replacing the current variable with the one stored in the backup.
    */
   protected void restoreLegacyName()
   {
      overrideLegacyName = null;
   }
   
   /**
    * Get the persistence service object associated with this buffer.  This
    * object is the buffer's interface to the physical database which backs
    * it.
    *
    * @return  Persistence service object for this buffer's database.
    */
   public Persistence getPersistence()
   {
      checkActive();
      
      return persistence;
   }
   
   /**
    * Get the physical database name associated with this buffer.
    *
    * @return  Physical database name.
    */
   public Database getDatabase()
   {
      if (persistence != null)
      {
         return persistence.getDatabase(!dmoInfo.multiTenant);
      }
      
      if (logicalDatabase == null)
      {
         // retrieve logical database name
         logicalDatabase = getLDBName(ldbOrAlias);
         
         // database hasn't been connected
         if (logicalDatabase == null)
         {
            int[] num = new int[] {855, 725};
            String[] text = new String[2];
            text[0] = "Unknown database name " + ldbOrAlias;
            text[1] = "Unknown or ambiguous table " + getDMOName();
            
            ErrorManager.showErrorAndAbend(num, text);
            
            return null;
         }
      }
      
      // look up physical database associated with logical name
      Database database = getPDB(logicalDatabase);
      
      if (MetadataManager.isMetaDMO(dmoInterface))
      {
         // a metadata buffer will be querying its data from an embedded, metadata database
         database = database.toType(Database.Type.META);
      }
      
      return database;
   }
   
   /**
    * Get the schema name of the database associated with this buffer.
    *
    * @return  Database schema name.
    */
   protected String getSchema()
   {
      return DatabaseManager.getSchema(getDatabase());
   }
   
   /**
    * Get the logical database name associated with this buffer.
    *
    * @return  Logical database name.
    */
   protected String getLogicalDatabase()
   {
      initialize();
      
      return logicalDatabase;
   }
   
   /**
    * Get the name of the table associated with this buffer.
    *
    * @return  Name of SQL table backing this record buffer.
    */
   protected String getTable()
   {
      if (table == null)
      {
         table = dmoInfo.getSqlTableName(); 
      }
      
      return table;
   }
   
   /**
    * Get the dirty share context used by this buffer.
    * 
    * @return  Dirty share context for this buffer's database.
    */
   protected DirtyShareContext getDirtyContext()
   {
      checkActive();
      
      return dirtyContext;
   }
   
   /**
    * Get the record lock context helper.
    * 
    * @return  Record lock context helper.
    */
   protected RecordLockContext getRecordLockContext()
   {
      checkActive();
      
      return persistenceContext.getRecordLockContext();
   }
   
   /**
    * Enter into a safe mode where current record is <code>null</code>, but
    * calls to DMO getter methods return unknown value instead of raising an
    * error condition.  This is intended to support outer join.
    */
   protected void setUnknownMode()
   {
      unknownMode = true;
   }
   
   /**
    * Instantiate a data record to back the specified DMO and to be added to the database upon transaction
    * commit.  The record's data members are initialized to their default values.  The created record becomes
    * the buffer's current record.  A share lock is automatically applied to the new record.  Foreign keys, if
    * any, are synchronized using the default values of legacy key properties.
    *
    * @return  {@code true} if operation is successful and a fresh new record can be found in the buffer.
    *
    * @throws  ErrorConditionException
    *          if there is an error retrieving the next available primary key
    *          from the database or locking the record.
    */
   protected boolean create()
   {
      if (vst || readonly)
      {
         // TODO: find the proper message to throw when attempting to create a record in a readonly table.
         
         // this is quite fatal, NO-ERROR will not prevent it from stopping the procedure
         ErrorManager.throwError(6248,
                                 "Creating records in a Virtual System Table is not allowed",
                                 null,
                                 false);
         return false;
      }
      
      // check the before buffer constraints
      BufferImpl buf = (BufferImpl) dmoProxy;
      if (isTemporary && buf.isBeforeBuffer() && buf.hasBeforeBufferConstraints())
      {
         ErrorManager.recordOrThrowError(12373, buf.doGetName(), "");
         // You may not create BEFORE-TABLE <name> record.
         return false;
      }
      
      initialize();
      
      bufferManager.registerDirtyBuffer(this);
      
      Long recId = null;
      RecordIdentifier<String> ident = null;
      
      try
      {
         if (isTransient())
         {
            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
            {
               // flush the transient record currently in the buffer, which will attempt validation;
               // the CREATE method will be aborted if this record is invalid
               flush(true);
            }
         }
         
         // create new record with default properties and starting state
         Record record = instantiateDMO();
         record.initialize(getSession(), true);
         
         // in case of multi-tenant without default tables, transient records CAN BE CREATED without PK,
         // but they are NEVER flushed
         boolean noDefaultTenant = !dmoInfo.multiTenant           ||
                                   dmoInfo.multiTenantWithDefault ||
                                   persistenceContext.getTenantId() != TenantManager.DEFAULT_TENANT_ID;
         if (noDefaultTenant)
         {
            // generate and assign a unique key (the fact that we are doing this test here means that probably
            // in 4GL this does not happen at this moment, but rather at first access of PK)
            recId = nextPrimaryKey();
            record.primaryKey(recId);
            
            // acquire a default share lock on the new record
            ident = record.getRecordLockIdentifier();
            RecordLockContext rlc = persistenceContext.getRecordLockContext();
            rlc.lock(ident, LockType.SHARE, 0L, this);
            rlc.registerBuffer(ident, this);
         }
         
         // set the new record into the buffer
         setCurrentRecord(record, false, true, false, true);
         
         if (noDefaultTenant)
         {
            // store the record temporarily in the record nursery, until it gets flushed to the database;
            // it is stored only under the "special" recid index for now
            persistenceContext.getNursery().indexUpdated(this, null, true, record);
         }
         
         // remember that this record has not been persisted yet; an eventual trigger must know
         // the record is still transient.
         createScope = bufferManager.getOpenBufferScopes();
         
         // invoke callback procedures first. Any trigger procedure is executed after this event is handled
         if (isTemporary)
         {
            if (buf._dataSet() != null)
            {
               // checks whether tracking is active
               if (getParentTable()._isTrackingChanges())
               {
                  try
                  {
                     createRowCallback = true;
                     if (!buf.invokeCallback(Buffer.ROW_CREATE_EVENT))
                     {
                        BlockManager.returnError();
                     }
                  }
                  finally
                  {
                     createRowCallback = false;
                  }
                  
                  if (currentRecord == null)
                  {
                     // the callback procedure/method deleted the newly created transient record
                     // nothing more to do, just return
                     return false;
                  }
               }
            }
         }
         
         DatabaseEventType det = DatabaseEventType.CREATE;
         if (triggerTracker != null)
         {
            if (triggerTracker.isTriggerEnabled(det, null, logicalDatabase))
            {
               if (triggerTracker.isExecuting(det, recId))
               {
                  DatabaseTriggerManager.handleError(3168, det, dmoInfo.legacyTable, null);
               }
               else
               {
                  // when everything is done, execute create triggers
                  try
                  {
                     triggerTracker.pushEvent(det, recId);
                     if (!DatabaseTriggerManager.trigger(det,
                                                         dmoBufInterface,
                                                         (Buffer) dmoProxy,
                                                         null,
                                                         logicalDatabase))
                     {
                        // this will happen only in no-error mode if the trigger vetoed the create operation
                        setCurrentRecord(null, false, false, false, false);
                        return false;
                     }
                  }
                  finally
                  {
                     triggerTracker.popEvent(recId);
                  }
               }
            }
         }
         
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, describe() + ":  new record created");
            
            if (LOG.isLoggable(Level.FINEST))
            {
               LOG.log(Level.FINEST, toString());
            }
         }
         
         // the whole table becomes invalid on creation of new records, even if not yet committed
         persistenceContext.getFastFindCache().invalidate(dmoInfo.getId(), getMultiplexID());
         
         // save the current record into the snapshot
         if (undoable != null)
         {
            undoable.checkUndoable(true);
         }
         
         snapshot = currentRecord.snapshot();
         armWriteTrigger(this, false); // sets new record's snapshot as the "old" DMO for write trigger
         
         // null out [ident] to indicate we completed successfully
         ident = null;
         
         // if this is buffer of an active AFTER-BUFFER, update the BEFORE-TABLE, too:
         BufferImpl after = (BufferImpl) dmoProxy;
         if (!ignoreBeforeTracking    &&
             after.isAfterBuffer()    &&
             ((AbstractTempTable) TempTableBuilder.asTempTable((Temporary) after))._isTrackingChanges())
         {
            BufferImpl before = after.beforeBufferNative();
            if (before != null)
            {
               // create new before record image, then update my before-rowid and row-state
               before.setUpBeforeBuffer(true);
               before.create();
               RecordBuffer beforeBuf = before.buffer();
               RecordBuffer afterBuf = after.buffer();
               beforeBuf.rowState(Buffer.ROW_CREATED);
               afterBuf.rowState(Buffer.ROW_CREATED);
               beforeBuf.peerRowid(after.rowID());
               afterBuf.peerRowid(before.rowID());
               before.setUpBeforeBuffer(false);
               // do not release the before buffer, let it hold the newly created row
            }
            // else there's already a record in before-table related to this modified. Do not add!
         }
         
         if (buf.autoSynchronize().booleanValue())
         {
            buf.querySynchronize();
         }
      }
      catch (ValidationException exc)
      {
         reportValidationException(exc, this, false);
      }
      catch (PersistenceException exc)
      {
         ErrorManager.recordOrThrowError(exc);
      }
      catch (IllegalAccessException | InstantiationException exc)
      {
         throw new RuntimeException("Error creating record", exc);
      }
      catch (InvocationTargetException exc)
      {
         DBUtils.handleException(getDatabase(), exc);
         
         throw new RuntimeException("Error creating record", exc);
      }
      catch (ReflectiveOperationException exc)
      {
         throw new RuntimeException("Error creating record", exc);
      }
      catch (Throwable t)
      {
         // for debug only:
         LOG.info("Rethrowing:", t);
         // then rethrow:
         throw t;
      }
      finally
      {
         // if ident is not null, we are not exiting normally; have to unlock record to avoid leak
         if (ident != null)
         {
            persistenceContext.getRecordLockContext().forceRelease(ident, this);
         }
      }
      
      // everything went fine ?
      return currentRecord != null;
   }
   
   /**
    * Validate the record, if any, currently in the buffer. If no record is available, return immediately.
    * 
    * @param   throwValidationException
    *          What to do in case the validation fails. If {@code true}, throw a {@code ValidationException}
    *          otherwise return {@code true} if validation is successful and {@code false} otherwise. 
    * 
    * @return  <code>true</code> if validation passed OK.
    * 
    * @throws  ValidationException 
    *          if the buffer fails validation.
    * @throws  ErrorConditionException
    *          if the buffer fails validation.
    */
   public boolean validate(boolean throwValidationException)
   throws ValidationException
   {
      if (dmoInfo.multiTenant && !dmoInfo.multiTenantWithDefault && currentRecord.primaryKey() == null)
      {
         ErrorManager.recordOrThrowError(15991, getLegacyName());
         // Attempt to create in multi-tenant table '<tablename>' where the partition (usually default) has not been allocated. (15991)
         
         return false; // the records created by the default tenant are volatile, non persistable
      }
      
      if (!isAvailable())
      {
         return false;
      }
      
      Record dmo = getCurrentRecord();
      
      if (dmo != null && dmo.isDirty() && !isTemporary())
      {
         maybeFireWriteTrigger();
      }
      
      try
      {
         validate(dmo);
         return true;
      }
      catch (ValidationException exc)
      {
         if (throwValidationException)
         {
            reportValidationException(exc, this, false);
            throw exc;
         }
         
         return false;
      }
      catch (PersistenceException exc)
      {
         if (throwValidationException)
         {
            ErrorManager.recordOrThrowError(exc);
            throw new ValidationException(exc);
         }
         
         return false;
      }
   }
   
   /**
    * Perform a full validation (all unique constraints and non-nullable properties) of the given record.
    *
    * @param   dmo
    *          DMO to be validated.
    *
    * @throws  ValidationException
    *          if the DMO state fails any unique or non-nullable constraint check.
    * @throws  PersistenceException
    *          if there is any error gathering index metadata or a database error performing validation.
    */
   void validate(Record dmo)
   throws ValidationException,
          PersistenceException
   {
      validateMaybeFlush(dmo, false, true);
   }
   
   /**
    * Indicate whether a backing data record currently is available. This
    * will only be the case if one has been created with {@link #create} or
    * retrieved in a query.
    *
    * @return  <code>true</code> if data is available, <code>false</code> if
    *          no record backs this buffer.
    */
   protected boolean isAvailable()
   {
      return (currentRecord != null);
   }
   
   /**
    * Checks if the record in the buffer has changed in database since the
    * last <code>FIND CURRENT</code> or <code>GET CURRENT</code>.
    * P2J implementation of 4GL function <code>CURRENT-CHANGED</code>.
    *
    * @return  <code>true</code> if the database record differs from the
    *          value in memory buffer that was queried with the last
    *          <code>FIND</code> and <code>false</code> otherwise
    */
   protected boolean isRecordChanged()
   {
      return recordChanged;
   }
   
   /**
    * Checks if this buffer is a read-only buffer. A read-only buffer is created by the {@code
    * DatabaseTriggerManager} when processing a WRITE event trigger with OLD buffer. It is also
    * used as an optimization for tables which contain read-only data.
    *
    * @return  {@code true} if the buffer can only contain read-only data.
    */
   protected boolean isReadonly()
   {
      return readonly;
   }
   
   /**
    * Indicate whether this buffer is a "fake" buffer. A "fake" buffer is one that is used as
    * the OLD buffer when processing a write trigger. Fake buffers are always read-only. This
    * method can be used to distinguish a read-only buffer which is used for a write trigger from
    * a buffer that is read-only as a performance optimization.
    * 
    * @return  {@code true} if the buffer is a fake buffer, as described above.
    */
   protected boolean isFake()
   {
      return fake;
   }
   
   /**
    * Indicate whether the last record retrieval attempt was denied because
    * the target record was locked.
    *
    * @return  <code>true</code> if the record was locked;  <code>false</code>
    *          if the attempt succeeded, or if this query has not yet
    *          attempted to retrieve a record.
    */
   protected boolean wasLocked()
   {
      return locked;
   }
   
   /**
    * Set the flag indicating whether the last record retrieval attempt
    * failed because another user had the target record locked.
    *
    * @param   locked
    *          New value of the flag.
    */
   protected void setLocked(boolean locked)
   {
      this.locked = locked;
   }
   
   /**
    * Indicate whether the buffer currently contains a record, and whether
    * that record was newly created (as opposed to having been read from the
    * database).
    * 
    * @return  <code>true</code> if record is newly created, else
    *          <code>false</code>.
    */
   protected boolean isNewlyCreated()
   {
      return (currentRecord != null && newlyCreated);
   }
   
   /**
    * Indicate whether the last record retrieval attempt failed because the
    * query criteria was ambiguous.
    *
    * @return  <code>true</code> if the query was ambiguous;
    *          <code>false</code> if the attempt succeeded, or if no attempt
    *          has yet been made.
    */
   protected boolean wasAmbiguous()
   {
      return ambiguous;
   }
   
   /**
    * Determines if the buffer is dynamic.
    *
    * @return <code>true</code> if it is a dynamic buffer.
    */
   protected boolean isDynamic()
   {
      return dynamic;
   }
   
   /**
    * Mark the buffer as dynamic.
    */
   protected void setDynamic()
   {
      this.dynamic = true;
      
      pm.processResource((WrappedResource) dmoProxy, true);
   }
   
   /**
    * Mark the buffer as dynamic.
    */
   protected void _setDynamic()
   {
      this.dynamic = true;
   }
   
   /**
    * Set the flag indicating whether the last record retrieval attempt
    * failed because the query criteria was ambiguous.
    *
    * @param   ambiguous
    *          New value of the flag.
    */
   protected void setAmbiguous(boolean ambiguous)
   {
      this.ambiguous = ambiguous;
   }
   
   /**
    * Disable the buffer's default behavior of releasing its current record
    * when the transaction manager invokes its {@link #iterate()} method.
    * This is necessary for the outer buffers in a compound query.
    */
   protected void disableReleaseOnIterate()
   {
      iterateReleaseOverrideScope = bufferManager.getOpenBufferScopes();
   }
   
   /**
    * Indicate whether the record currently in the buffer represents a copy of
    * a DMO from the dirty database.  This happens in the case where another
    * context inserts a new record, but has not committed it yet, and this
    * context finds that record via dirty checking.
    * 
    * @return  <code>true</code> if current record is a dirty copy, else
    *          <code>false</code>.
    */
   protected boolean isDirtyCopy()
   {
      return dirtyCopy;
   }
   
   /**
    * Mark the buffer's current record as no longer transient by resetting it
    * create scope to its default value.
    */
   protected void markPersisted()
   {
      createScope = -1;
   }
   
   /**
    * Add all property names in the given collection to the set of legacy key
    * properties being monitored for foreign association synchronization
    * purposes.  Any change made to one or more of these properties should
    * trigger a foreign key synchronization attempt.
    *
    * @param   properties
    *          A collection of DMO property names.
    *
    * @see     AssociationSyncher
    * @see     LocalSyncher
    * @see     InverseSyncher
    */
   protected void addLegacyForeignKeys(Collection<String> properties)
   {
      if (legacyForeignKeys != null)
      {
         legacyForeignKeys.addAll(properties);
      }
   }
   
   /**
    * Get a set of dirty properties. Optionally the list can be filter to those properties in both
    * dirty properties and those properties which participate in any indexes in the backing table.
    * 
    * @param   indexesOnly
    *          Filter only dirty properties that are part of index in backing table.
    * 
    * @return  A set with dirty property names. The set may be empty, but will not be {@code null}.
    */
   BitSet getDirtyProperties(boolean indexesOnly)
   {
      if (currentRecord == null)
      {
         return new BitSet();
      }
      
      BitSet dps = currentRecord.getDirtyProps();
      if (dps.isEmpty())
      {
         return new BitSet(); // regardless of [indexesOnly]
      }
      
      // be on safe side: make a copy:
      dps = (BitSet) dps.clone();
      if (indexesOnly)
      {
         BitSet allIndexedProperties = currentRecord._getRecordMeta().getAllIndexedProperties();
         dps.and(allIndexedProperties);
      }

      return dps;
   }
   
   /**
    * Indicate whether the given property participates in any index.
    * 
    * @param   propertyIndex
    *          Property name.
    * 
    * @return  {@code true} if {@code property} participates in an index, else {@code false}.
    */
   protected boolean isPropertyIndexed(int propertyIndex)
   {
      checkActive();
    
      if (indexedProperties == null)
      {
         indexedProperties = new BitSet();
         indexedProperties.or(getIndexedProperties(false));
      }
      
      return indexedProperties.get(propertyIndex);
   }
   
   /**
    * Get the buffer type hash key associated with this buffer.
    * 
    * @return  <code>BufferType</code> key associated with this buffer, or
    *          <code>null</code> if the buffer has not yet been initialized. 
    */
   protected BufferType getBufferType()
   {
      if (!isActive())
      {
         return null;
      }
      
      return bufferType;
   }
   
   /**
    * Returns the value of a field in the record of this buffer.
    * 
    * @param   field
    *          The property name of the field.
    * 
    * @return  the value of the field 
    */
   protected BaseDataType getValue(String field)
   {
      if (currentRecord == null)
      {
         // maybe throw a runtime exception
         return null;
      }
      
      Method method = dmoInfo.getPojoGetterMap().get(field);
      if (method == null)
      {
         // maybe throw a runtime exception
         return null;
      }
      
      PropertyMeta meta = dmoInfo.getPropsByGetterMap().get(method);
      return meta.getDataHandler().getField(currentRecord, meta.getOffset());
   }
   
   /**
    * Prepares for an immediate reload(). The current value of the record is saved temporarily
    * in order to be available to compare to the new value that will be obtained in reload().
    * This method is only called from within P2JQuery.current() implementation in order to
    * prepare the buffer to recognise if the field has changed externally. After a call to 
    * this method and a reload() of the buffer, updateCurrentChanged() must be called to ensure
    * the proper <code>recordChanged</code> settings and free of the temporary compare buffer.
    */
   protected void armCurrentChanged()
   {
      if (currentRecord == null)
      {
         shadowCopyRecord = null;
         
         return;
      }
      
      shadowCopyRecord = currentRecord.snapshot();
   }
   
   /**
    * Direct access to the field value on the specified offset.
    * 
    * @param    offset
    *           The field offset in the record's datum.
    *           
    * @return   The field's value.  This is not a BDT type, this will be a Java type.
    */
   protected Object getDirectField(int offset)
   {
      return OrmUtils.getField(currentRecord, offset);
   }
   
   /**
    * Direct assignment of a field, accessing the record's datum.
    * 
    * @param    value
    *           The field's value.  This is not a BDT type, this will be a Java type.
    * @param    offset
    *           The field offset in the record's datum.
    */
   protected void setDirectField(Object value, int offset)
   {
      Runnable restoreActiveState = null;
      try
      {
         restoreActiveState = currentRecord.setActiveBuffer(RecordBuffer.this);
         OrmUtils.setField(currentRecord, offset, value);
      }
      finally
      {
         if (restoreActiveState != null)
         {
            restoreActiveState.run();
         }
      }
   }
   
   /**
    * Updates the <code>recordChanged</code> flag.
    * This must be called right after an <code>armCurrentChanged()</code>followed by a
    * <code>reload()</code>.
    * This checks the current value stored in <code>currentRecord</code> against the value
    * temporarily saved and updates the <code>recordChanged</code> accordingly. After that, the
    * temp data is freed.
    */
   protected void updateCurrentChanged()
   {
      if (this.shadowCopyRecord == null)
      {
         // something is wrong
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE,
                    Utils.describeContext() +
                    ":  armCurrentChanged() should be called before updateCurrentChanged()");
         }
         this.recordChanged = false;
         return;
      }
      
      if (this.currentRecord == null)
      {
         // the record must have been deleted
         this.recordChanged = false;
         this.shadowCopyRecord = null;
         return;
      }
      
      this.recordChanged = !this.currentRecord.equalData(shadowCopyRecord);
      this.shadowCopyRecord = null;
   }
   
   /**
    * Get the mappings of property names, from definition to bound buffer.
    * 
    * @return   Always <code>null</code>.
    */
   protected Map<String, String> getBoundPropertyMappings()
   {
      return null;
   }
   
   /**
    * Return the transaction helper associated with this buffer
    * 
    * @return   The transaction helper of this buffer
    */
   protected TransactionManager.TransactionHelper getTxHelper()
   {
      return this.txHelper;
   }
   
   /**
    * Return the change broker associated with this buffer
    * 
    * @return   The change broker of this buffer
    */
   protected ChangeBroker getChangeBroker()
   {
      return this.changeBroker;
   }
   
   /**
    * Return the procedure manager associated with this buffer
    * 
    * @return   The procedure manager of this buffer
    */
   protected ProcedureManager.ProcedureHelper getProcedureManager()
   {
      return this.pm;
   }
   
   /**
    * Get the record nursery of the current persistence context. If the context is not created yet,
    * {@code null} is returned.
    *
    * @return  Record nursery.
    */
   protected RecordNursery getNursery()
   {
      return persistenceContext == null ? null : persistenceContext.getNursery();
   }
   
   /**
    * Set the DMO implementation instance which backs this buffer with a another record, loaded from database.
    * The buffer will help updating the new record, usually for internal, more complex processing.
    * <p>
    * This is a dedicated method accessible only from a very specific places within the persistence package.
    * When the new record is set, the one eventually existing is released.
    * <p>
    * Note that the triggers are NOT fired when using this method.
    *
    * @param   newCrtRecord
    *          DMO instance which represents the record currently backing this buffer. If {@code null}, the
    *          record (and possibly its corresponding lock) is released.
    *
    * @throws  ErrorConditionException
    *          if there is an error transitioning a lock when releasing the current record; if there is an
    *          error validating or saving a transient record during flush.
    *
    * @see     #setCurrentRecord(Record, boolean, boolean, boolean, boolean)
    */
   public void loadRecord(Record newCrtRecord)
   {
      setCurrentRecord(newCrtRecord, false, false, false, false);
   }
   
   /**
    * Sets whether buffer is part of buffer copy operation.
    * 
    * @return  <code>true</code> if it is part of BufferCopy operation, else <code>false</code>.
    */
   void setBufferCopyDestination(boolean bufferCopyDestination)
   {
      this.bufferCopyDestination = bufferCopyDestination;
   }
   
   /**
    * Add a scope where this buffer was registered with {@link BufferManager allBuffers}.
    *  
    * @param    scope
    *           The scope, as distance from the global scope.
    */
   final void addAllBufferScope(int scope)
   {
      allBufferScopes.add(scope);
   }
   
   /**
    * Add a scope where this buffer was registered with {@link BufferManager byLegacyName}.
    *  
    * @param    scope
    *           The scope, as distance from the global scope.
    */
   final void addByLegacyNameScope(int scope)
   {
      byLegacyNameScopes.add(scope);
   }
   
   /**
    * Add a scope where this buffer was registered with {@link BufferManager buffersByDMOBufClass}.
    *  
    * @param    scope
    *           The scope, as distance from the global scope.
    */
   final void addBufferByClassScope(int scope)
   {
      buffersByDmoClassScopes.add(scope);
   }
   
   /**
    * Add a scope where this buffer was registered with {@link BufferManager loadedBuffers}.
    *  
    * @param    scope
    *           The scope, as distance from the global scope.
    */
   final void addLoadedBuffersScope(int scope)
   {
      loadedBuffersScopes.add(scope);
   }
   
   /**
    * Add a scope where this buffer was registered with {@link BufferManager openBuffers}.
    *  
    * @param    scope
    *           The scope, as distance from the global scope.
    */
   final void addOpenBuffersScope(int scope)
   {
      openBuffersScopes.add(scope);
   }
   
   /**
    * Get the list of scopes in {@link #allBufferScopes}.
    * 
    * @return   See above.
    */
   final int[] getAllBuferScopes()
   {
      return allBufferScopes.toArray();
   }
   
   /**
    * Get the list of scopes in {@link #byLegacyNameScopes}.
    * 
    * @return   See above.
    */
   final int[] getByLegacyNameScopes()
   {
      return byLegacyNameScopes.toArray();
   }
   
   /**
    * Get the list of scopes in {@link #buffersByDmoClassScopes}.
    * 
    * @return   See above.
    */
   final int[] getBuffersByDmoClassScopes()
   {
      return buffersByDmoClassScopes.toArray();
   }
   
   /**
    * Get the list of scopes in {@link #loadedBuffersScopes}.
    * 
    * @return   See above.
    */
   final int[] getLoadedBuffersScope()
   {
      return loadedBuffersScopes.toArray();
   }
   
   /**
    * Get the list of scopes in {@link #openBuffersScopes}.
    * 
    * @return   See above.
    */
   final int[] getOpenBuffersScope()
   {
      return openBuffersScopes.toArray();
   }
   
   /**
    * Creates a new {@link Validation} instance using the passed arguments, and calls 
    * {@link Validation#validateMaybeFlush()}. Depending on the {@link Validation#wasFlushed()}
    * state, it will {@link #reportChange report} the change so that listeners will be notified.
    * 
    * @param   dmo
    *          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  True if the validation passed.
    *
    * @throws  PersistenceException
    *          if there was a database error during validation or data persistence. This
    *          generally will not be recoverable.
    * @throws  ValidationException
    *          if there was a validation error. This represents an application level data
    *          validation failure and is recoverable.
    */
   protected boolean validateMaybeFlush(Record dmo, boolean flush, boolean explicit)
   throws PersistenceException,
          ValidationException
   {
      if (dmo == null)
      {
         return false;
      }
      
      // in case of multi-tenant without default tables, transient records CAN BE CREATED without PK,
      // but they are NEVER validated/flushed, so neither they go to dirtyContext
      if (dmoInfo.multiTenant &&
          !dmoInfo.multiTenantWithDefault &&
          persistenceContext.getTenantId() == TenantManager.DEFAULT_TENANT_ID)
      {
         return false;
      }
      
      boolean insert = dmo.checkState(DmoState.NEW);
      
      Validation validation = new Validation(this,
                                             dmo,
                                             getMultiplexID(),
                                             getLegacyName(),
                                             persistenceContext.getNursery(),
                                             uniqueTracker,
                                             uniqueTrackerCtx,
                                             flush,
                                             !explicit);
      try
      {
         validation.validateMaybeFlush();
      }
      catch (StaleRecordException exc)
      {
         // not a fatal error, but DMO is now marked STALE and cannot be saved or associated with the
         // session cache
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE,
                    "stale record cannot be flushed:  " + getEntityName() + "#" + dmo.primaryKey());
         }
         return false;
      }
      
      if (dmo == currentRecord && validation.wasFlushed())
      {
         if (insert)
         {
            markPersisted();
         }
         
         reportChange(dmo, insert, false);
      }
      return true;
   }
   
   /**
    * Get the record metadata associated with this buffer's DMO class.
    * 
    * @return  Record metadata for the DMO.
    */
   private RecordMeta getMetadata()
   {
      return currentRecord != null
             ? currentRecord._getRecordMeta()  // quicker
             : Session.getMetadata(dmoClass);  // slightly slower
   }
   
   /**
    * Check whether the given <code>fieldInfo</code> object has a buffer name qualifier, and if
    * so, whether it matches the buffer's variable name or the dynamically assigned temp-table
    * name (if this is the buffer for a dynamically prepared temp-table).
    * 
    * @param   fieldInfo
    *          Object which contains parsed field specifier information.
    * 
    * @return  <code>true</code> if there is no buffer qualifier, or if there is one and it has
    *          the expected value; <code>false</code> if there is a mismatched qualifier.
    */
   private boolean checkBufferName(FieldInfo fieldInfo)
   {
      String bufName = fieldInfo.getBufferName();
      
      return bufName == null ||
             bufName.equalsIgnoreCase(variable) ||
             bufName.equalsIgnoreCase(getDynamicName());
   }
   
   /**
    * Check whether the given <code>fieldInfo</code> object has a buffer name qualifier, and if
    * so, whether it matches the buffer's variable name. Report an error if there is a qualifier
    * and it does not match the buffer's variable name.
    * 
    * @param   fieldInfo
    *          Object which contains parsed field specifier information.
    * @param   fieldSpec
    *          Field specifier string (used only to generate an error message).
    * 
    * @return  <code>true</code> if there is no buffer qualifier, or if there is one and it has
    *          the expected value; <code>false</code> if there is a mismatched qualifier, and we
    *          are in silent error mode.
    * 
    * @throws  ErrorConditionException
    *          if <code>fieldInfo</code> contains a mismatched buffer name qualifier and we are
    *          not in silent error mode.
    */
   private boolean reportBadBufferName(FieldInfo fieldInfo, String fieldSpec)
   {
      if (checkBufferName(fieldInfo))
      {
         return true;
      }
      
      String spec = trimFieldSpec(fieldSpec, fieldInfo);
      String msg = String.format("%s must be in a buffer in the data-source", spec);
      ErrorManager.recordOrThrowError(11855, msg, false);
      
      return false;
   }
   
   /**
    * Report whether the given extent index is valid for the given property. Note that this
    * requires that the buffer contains a record, since a method call is made to the backing DMO
    * to determine the extent of <code>property</code>, if it represents an extent field.
    * 
    * @param   extentIndex
    *          A non-negative, zero-based array index to be tested for validity.
    * @param   property
    *          Name of the property to be checked.
    * 
    * @return  <code>true</code> if <code>property</code> represents an extent field and
    *          <code>extentIndex</code> is non-negative and is not out of bounds;
    *          <code>false</code> if <code>extentIndex</code> is negative or <code>property</code>
    *          does not represent an extent field, or <code>extentIndex</code> is out of bounds.
    */
   private boolean isValidExtent(int extentIndex, String property)
   {
      if (extentIndex < 0)
      {
         return false;
      }
      
      Integer extent = dmoInfo.getExtentMap().get(property);
      
      return (extent != null && extentIndex < extent);
   }
   
   /**
    * Worker method which aggregates the values in the table backing this buffer.
    * 
    * @param   aggregate
    *          Name of FQL aggregate function (e.g., "count").
    * @param   column
    *          An expression indicating the name of the column to aggregate. May be the wildcard
    *          character ("*") for COUNT.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          FQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    *
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   private void aggregate(String aggregate, String column, NumberType num, String where, Object... args)
   {
      // TODO: handle parameter processing failure
      AbstractQuery.preprocessSubstitutionArguments(bufferManager, () -> false, false, args);
      
      initialize();
      
      Persistence persistence = getPersistence();
      String dmoImplName = getDMOImplementationName();
      
      StringBuilder buf = new StringBuilder();
      buf.append("select ");
      buf.append(aggregate);
      buf.append("(");
      buf.append(column);
      buf.append(") from ");
      buf.append(dmoImplName);
      
      try
      {
         if (where != null && !where.isEmpty())
         {
            FQLPreprocessor preproc = FQLPreprocessor.get(where,
                                                          getDatabase(),
                                                          getDialect(),
                                                          new RecordBuffer[] { this },
                                                          args,
                                                          null,
                                                          true,
                                                          null,
                                                          null,
                                                          false,
                                                          null);
            buf.append(" where ");
            buf.append(preproc.getFQL());
            
            if (args != null && args.length > 0)
            {
               ParameterIndices pi = preproc.getParameterIndices();
               int len = pi.getCount();
               List<Object> argList = new ArrayList<>(len);
               for (int i = 0, count = 0; count < len; i++)
               {
                  Integer nextIndex = pi.getIndex(i);
                  if (nextIndex == null)
                  {
                     // original substitution parameter was inlined into FQL by preprocessor; skip it.
                     continue;
                  }
                  
                  // substitution placeholders may have been reordered if this statement was
                  // rewritten by the FQL preprocessor; make sure we access the correct
                  // substitution parameter
                  Object next = args[nextIndex];
                  argList.add(next);
                  
                  count++;
               }
               args = argList.toArray();
            }
         }
         
         List<Object> list = persistence.list(new Class[] { getDMOImplementationClass() },
                                              buf.toString(),
                                              args,
                                              1,
                                              0,
                                              null,
                                              true);
         NumberType result = null;
         if (list != null)
         {
            Object value = list.get(0);
            if (value instanceof Object[])
            {
               value = ((Object[]) value)[0];
            }
            
            if (value instanceof Number)
            {
               result = new decimal((Number) value);
            }
            else if (value instanceof NumberType)
            {
               result = (NumberType) value;
            }
            
            if (result == null)
            {
               // this only should happen if the embedded SQL aggregate expression in business
               // logic is written incorrectly, so it is a sanity check but should not happen
               // normally at runtime with correctly written/converted business logic
               ErrorManager.throwError(new NumberedException("Wrong result type", 0));
               
               // won't reach here with current implementation of EM.throwError(NE), but needed
               // to satisfy compiler
               return;
            }
         }
         
         num.assign(result);
      }
      catch (PersistenceException e)
      {
         ErrorManager.recordOrThrowError(e);
      }
   }
   
   /**
    * Verify that the record buffer has been initialized.  If not, raise an
    * exception.
    * 
    * @throws  IllegalStateException
    *          if the buffer has not been initialized.
    */
   private void checkActive()
   {
      if (!isActive())
      {
         throw new IllegalStateException("Buffer " + variable + " is not active");
      }
   }
   
   /**
    * Called by the transaction manager just before a buffer scope closes.
    * This is our hook to update the state of locks and record availability.
    * The current record is made unavailable and locks acquired during this
    * scope are either:
    * <ul>
    *   <li>released immediately, if this buffer scope never intersected a
    *       transaction's scope;
    *   <li>downgraded, if a transaction occurred within this buffer scope,
    *       but ended previous to this call;  or
    *   <li>marked for deferred release, if we are still within the scope of
    *       an active transaction;  the locks will be released at the end of
    *       the transaction.
    * </ul>
    */
   protected void finishedImpl()
   {
      if (openScopeCount == 0)
      {
         if (isActive())
         {
            // Notify connection manager that this buffer is done with the connection.
            onCloseOutermostScope();
         }
         
         return;
      }
      
      // Decrement reference count of open scopes for this buffer.
      openScopeCount--;
      
      boolean debug = LOG.isLoggable(Level.FINE);
      if (debug)
      {
         LOG.log(Level.FINE, describe() + ":  finished block scope " + bufferManager.getOpenBufferScopes());
      }
      
      boolean globalScope = activeScopeDepth == 0;
      
      // buffers associated with persistent procedures survive the closing of the outermost buffer scope;
      // mark them as having "world" scope
      if (globalScope)
      {
         worldScope = true;
      }
      
      boolean active = isActive();
      if (active)
      {
         // Reset iterate release override scope.
         iterateReleaseOverrideScope = -1;
         
         // Perform cleanup if this is the outermost scope for this buffer
         // (buffers can be shared and open scopes can be nested).
         if (openScopeCount == 0)
         {
            cleanup();
         }
      }
      
      // if the buffer belongs to a persistent ran procedure that is deleting, or if the buffer
      // was opened in the global scope, deregister it as a record change listener
      if (registeredWithGlobalScope == openScopeCount || (globalScope && openScopeCount == 0))
      {
         changeBroker.removeFromGlobal(this);
         
         if (registeredWithGlobalScope == openScopeCount)
         {
            registeredWithGlobalScope = -1; // reset the flag
         }
      }
      
      if (debug)
      {
         LOG.log(Level.FINE,
                 describe() +
                 ":  closed " +
                 (active ? "" : "in") +
                 "active buffer scope " +
                 bufferManager.getOpenBufferScopes());
      }
   }
   
   /**
    * Do clean-up in case the buffer runs out of (outermost) scope. This should be called from places
    * responsible for the buffer lifecycle (e.g. {@link #finishedImpl()}).
    * Its goal is to release any unwanted references that may leak or remain otherwise open.
    */
   protected void cleanup()
   {
      activeScopeDepth = -1;
      
      // 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;
      
      undoable = null;
      
      // allow all locks pinned by this buffer to be released
      if (pinnedLocks != null)
      {
         pinnedLocks.closeScope();
      }
      
      // relinquish any locks pinned by this buffer if outside a transaction
      if (!bufferManager.isTransaction())
      {
         Set<Long> released = persistenceContext.getRecordLockContext().relinquishBufferLocks(this);
         if (released != null)
         {
            bufferManager.reclaimPendingKeys(persistence, released);
         }
      }
      // Notify connection manager that this buffer is done with the connection.
      onCloseOutermostScope();
      
      closeRelatedQueries();
      
      // Make sure that the record we are trying to release is not actually a record that is being
      // deleted. Because the converted 4GL trigger opens the scope of the buffer, it will
      // release the record that is currently being deleted when the scope is closed.
      if (currentRecord != null)
      {
         TriggerTracker tt = this.getTriggerTracker();
         if (tt == null || !tt.isExecuting(DatabaseEventType.DELETE, currentRecord.primaryKey()))
         {
            // release the current record and allow the persistence to evict the record since it 
            // is not in use by this buffer
            release(false, true);
         }
      }
   }
   
   /**
    * Process one or more DMO property changes by creating or updating a
    * <code>ReversibleUpdate</code> object and reporting the change to the
    * {@link ChangeBroker} for further distribution to registered listeners.
    *
    * @throws  PersistenceException
    *          if a problem occurs reporting the change to registered
    *          listeners.
    */
   private void processChange()
   throws PersistenceException
   {
      if (currentRecord != null && unreportedChanges == null)
      {
         return;
      }
      
      // if we are in a transaction, remember the scope depth at which this property change occurred
      if (bufferManager.isTransaction())
      {
         markChangeScope();
      }
      
      reportChange(currentRecord, false, false);
   }
   
   /**
    * Get the currently active temporary record.
    * 
    * @return  Current temporary record set into this buffer.
    * 
    * @see     #setTempRecord(Record)
    */
   private Record getTempRecord()
   {
      if (tempRecords == null || tempRecords.isEmpty())
      {
         return null;
      }
      
      return tempRecords.get(tempRecords.size() - 1);
   }
   
   /**
    * Return the handler associated with this buffer
    * 
    * @return   The handler of this buffer
    */
   private Handler getHandler() 
   {
      return this.handler;
   }
   
   /**
    * Return the DMO class associated with this buffer
    * 
    * @return   The DMO class of this buffer
    */
   private Class<?> getDMOClass() 
   {
      return this.dmoClass;
   }
   
   /** Clears the unreported changes associated with this buffer. */
   private void clearUnreportedChanges()
   {
      unreportedChanges = null;
   }
   
   /**
    * Adds the information about new change to {@code unreportedChanges2} structure.
    * @param   propIndex
    *          The index of the property in the {@code Record.data} table.
    * @param   extent
    *          If this is an extent, the index of the property. Otherwise 0.
    * @param   diff
    *          The difference. Contains the values before and after.
    */
   private void addUnreportedChanges(int propIndex, int extent, Object[] diff)
   {
      if (unreportedChanges == null)
      {
         unreportedChanges = new Object[metadata.getPropertyMeta(false).length][];
      }
      unreportedChanges[propIndex + extent] = diff;
   }
   
   /**
    * Return the trigger tracker associated with this buffer
    * 
    * @return   The trigger tracker of this buffer
    */
   protected TriggerTracker getTriggerTracker()
   {
      return this.triggerTracker;
   }
   
   /**
    * Set the read only flag associated with this buffer
    */
   private void setReadOnly(boolean readonly)
   {
      this.readonly = readonly;
   }
   
   /**
    * Set the fake flag associated with this buffer
    */
   private void setFake(boolean fake)
   {
      this.fake = fake;
   }
   
   /**
    * Load the specified record, if any, from the database.  If
    * <code>lockType</code> is higher than the current lock type, it is
    * upgraded immediately.  If lower, it is downgraded immediately only if
    * outside of a transaction.  If inside a transaction, the lock is marked
    * for downgrade to the specified type upon transaction end.
    * 
    * @param   target
    *          DMO to be reloaded.  If <code>null</code>, an exception will be
    *          raised.
    * @param   lockType
    *          Requested lock type.
    * @param   undo
    *          <code>true</code> if currently processing an undo or
    *          <code>false</code> if currently processing a reload from database
    * @param   allowWriteTrigger
    *          {@code true} to allow a write trigger to be fired on this event, else {@code
    *          false}.
    *
    * @throws  ErrorConditionException
    *          if there is no record loaded into the buffer and the error
    *          manager is not in silent mode.
    */
   private void load(Record target, LockType lockType, boolean undo, boolean allowWriteTrigger)
   {
      if (target == currentRecord && undo && isTemporary)
      {
         // A temporary record cannot have been changed by another session,
         // so there is no need to reload it in an undo situation if it is
         // already loaded as the current record.
         return;
      }
      
      LockType pinned = null;
      Long id = null;
      PersistenceException error = null;
      
      try
      {
         if (target == null)
         {
            errorNotOnFile();
            
            return;
         }
         
         if (!undo)
         {
            release(allowWriteTrigger);
         }
         
         id = target.primaryKey();
         
         pinned = getPinnedLockType(id);
         
         if (lockType == null)
         {
            lockType = pinned;
         }
         else if (!lockType.equals(pinned))
         {
            setPinnedLockType(id, lockType);
         }
         
         Record dmo = null;
         boolean dirtyCopy = false;
         
         if (dirtyContext != null && LockType.NONE.equals(lockType))
         {
            dmo = dirtyContext.getDirtyDMO(getEntityName(), id);
         }
         
         if (dmo != null)
         {
            dirtyCopy = true;
         }
         else if (target == currentRecord && (isTransient() || undo))
         {
            // if the target for reload is the same as the current record and the record either
            // was never saved to the database or this is an undo operation (i.e., we are
            // basically in a no-op reload, since the record has not changed (which we know
            // because no dirty DMO was found)), simply reload the same record
            dmo = target;
         }
         else
         {
            long timeout = persistence.getTimeOut();
            dmo = persistence.load(this.dmoClass, id, lockType, timeout, true);
         }
         
         setCurrentRecord(dmo, undo, newlyCreated, dirtyCopy, false);
         
         if (dmo != null && !LockType.NONE.equals(lockType))
         {
            // setCurrentRecord bypasses the registration of this buffer with the lock state
            // managed by the record lock context, which can cause premature lock release under
            // certain conditions, so we register the buffer here
            RecordIdentifier<String> ident = dmo.getRecordLockIdentifier();
            persistenceContext.getRecordLockContext().registerBuffer(ident, this);
         }
      }
      catch (MissingRecordException exc)
      {
         // record must have been deleted in another context
         setCurrentRecord(null, undo, false, false, false);
      }
      catch (LockUnavailableException exc)
      {
         locked = true;
         error = exc;
      }
      catch (PersistenceException exc)
      {
         error = exc;
      }
      finally
      {
         if (error != null)
         {
            if (id != null && pinned != null)
            {
               setPinnedLockType(id, pinned);
            }
            
            ErrorManager.recordOrThrowError(error);
         }
      }
   }
   
   /**
    * Set the DMO proxy which references this buffer instance.
    *
    * @param   dmoProxy
    *          DMO proxy object.
    */
   private void setDMOProxy(BufferReference dmoProxy)
   {
      this.dmoProxy = dmoProxy;
   }
   
   /**
    * Set the DMO implementation instance which backs this buffer, or release the record currently
    * stored in the buffer.
    * <p>
    * If a new DMO is set to this buffer, the {@code WRITE} trigger is fired and the existing one
    * (if any) is flushed to backing database, its count usage decremented and, if its use count
    * reaches zero, the old record is detached from the current Hibernate session.
    * <p>
    * If not {@code null}, the new DMO is registered with the current Hibernate session and its
    * use count is incremented.
    *
    * @param   newCrtRecord
    *          DMO instance which represents the record currently backing this buffer.  If
    *          {@code null}, the record (and possibly its corresponding lock) is released.
    * @param   undo
    *          {@code true} if currently processing an undo, else {@code false}. The outgoing
    *          record, if any, is not validated and flushed if this parameter is {@code true}.
    * @param   newlyCreated
    *          {@code true} if the record being stored in the buffer is newly created.
    * @param   dirtyCopy
    *          Indicates whether {@code newCrtRecord} represents a copy of a DMO found in the
    *          dirty database.
    * @param   allowWriteTrigger
    *          {@code true} to allow a write trigger to be fired on this event, else {@code
    *          false}.
    *
    * @throws  ErrorConditionException
    *          if there is an error transitioning a lock when releasing the current record;  if
    *          there is an error validating or saving a transient record during flush.
    * 
    * @see     #release(boolean)
    */
   private void setCurrentRecord(Record newCrtRecord,
                                 boolean undo,
                                 boolean newlyCreated,
                                 boolean dirtyCopy,
                                 boolean allowWriteTrigger)
   {
      setCurrentRecord(newCrtRecord, undo, newlyCreated, dirtyCopy, allowWriteTrigger, true);
   }
   
   /**
    * Set the DMO implementation instance which backs this buffer, or release the record currently
    * stored in the buffer.
    * <p>
    * If a new DMO is set to this buffer, the {@code WRITE} trigger is fired and the existing one
    * (if any) is flushed to backing database, its count usage decremented and, if its use count
    * reaches zero, the old record is detached from the current Hibernate session.
    * <p>
    * If not {@code null}, the new DMO is registered with the current Hibernate session and its
    * use count is incremented.
    *
    * @param   newCrtRecord
    *          DMO instance which represents the record currently backing this buffer.  If
    *          {@code null}, the record (and possibly its corresponding lock) is released.
    * @param   undo
    *          {@code true} if currently processing an undo, else {@code false}. The outgoing
    *          record, if any, is not validated and flushed if this parameter is {@code true}.
    * @param   newlyCreated
    *          {@code true} if the record being stored in the buffer is newly created.
    * @param   dirtyCopy
    *          Indicates whether {@code newCrtRecord} represents a copy of a DMO found in the
    *          dirty database.
    * @param   allowWriteTrigger
    *          {@code true} to allow a write trigger to be fired on this event, else {@code
    *          false}.
    * @param   honorMultipleReferences
    *          Honor the flushing suppressing of the existing record if is also loaded in another buffer. 
    *
    * @throws  ErrorConditionException
    *          if there is an error transitioning a lock when releasing the current record;  if
    *          there is an error validating or saving a transient record during flush.
    * 
    * @see     #release(boolean)
    */
   private void setCurrentRecord(Record newCrtRecord,
                                 boolean undo,
                                 boolean newlyCreated,
                                 boolean dirtyCopy,
                                 boolean allowWriteTrigger,
                                 boolean honorMultipleReferences)
   {
      if (dmoProxy != null)
      {
         // reset the DATA-SOURCE-MODIFIED
         ((Buffer) dmoProxy).setDataSourceModified(false);
      }
      
      if (newCrtRecord != null)
      {
         getTxHelper().checkTransaction(persistence.getDatabase(!dmoInfo.multiTenant).getId());
      }
      
      boolean sameRecord = areDMOsSame(this.currentRecord, newCrtRecord);

      if (newCrtRecord != null)
      {
         sameRecord = sameRecord && this.dirtyCopy == dirtyCopy;
         
         if (sameRecord)
         {
            if (!undo)
            {
               this.newlyCreated = newlyCreated;
               if (snapshot != null && undoable != null)
               {
                  undoable.checkUndoable(true);
               }
               snapshot = null;
            }
            
            bufferManager.notifyRecordWasLoaded(this);
            
            // the old and new DMO can represent the same backing record, but still be different
            // DMO instances; if they're the same instance, we're done here
            if (currentRecord == newCrtRecord)
            {
               return;
            }
         }
      }
      
      // the ROW-UPDATE event is fired "immediately before the record is updated in the temp-tables"
      // NOTE: because the way FWD handles transactions that moment is difficult to intercept. When a change
      //       operation occurred on the record, our validation has a secondary effect of flushing the
      //       record to database so at this moment the record is already updated and its flags set to CACHED
      //       and CHANGED flag is off. To detect if the record was indeed changed we test [isTouched] flag
      maybeFireRowUpdateEvent();
      
      // TODO: this is reported as unused
      boolean clearValidationState = true;
      try
      {
         // certain processing should not take place if we are resetting the current record due to an undo
         // also, write trigger and flushing shouldn't happen if the record is loaded in another buffer as well
         boolean singleLoadedBuffer = (currentRecord == null || !currentRecord.isMultiplyReferenced(false));
         if (!undo && (!honorMultipleReferences || singleLoadedBuffer))
         {
            // if a different buffer created this record, use it to handle write trigger and flushing duties
            RecordBuffer targetBuffer = (creatingBuffer != null ? creatingBuffer : this);
            
            // DO NOT flush or fire WRITE trigger for empty or readonly buffers
            // DO NOT flush or fire WRITE trigger if the current record is not changed
            boolean doFlush = false;
            
            // if the WRITE trigger is already executing (down the stack), do not re-fire it again;
            //       the record will be flushed when that trigger ends.
            // DO NOT fire WRITE trigger for readonly buffers
            // DO NOT fire WRITE trigger if the current record is not changed, even if transient
            if (isAvailable() && !isReadonly())
            {
               boolean currentRecordChanged = currentRecord.checkState(DmoState.CHANGED) &&
                        (targetBuffer.getTriggerTracker() == null   ||
                        !targetBuffer.getTriggerTracker().isExecuting(DatabaseEventType.WRITE,
                                                                      currentRecord.primaryKey()));
               if (currentRecordChanged)
               {
                  boolean maybeChanged = allowWriteTrigger && targetBuffer.maybeFireWriteTrigger();
                  if (maybeChanged && shadowCopyRecord != null)
                  {
                     // this is a bit odd, but the shadow copy needs to be refreshed if the trigger
                     // was executed because it might have been changed. [shadowCopyRecord] is used
                     // to check an external change. Trigger changes are NOT considered external.
                     armCurrentChanged();
                  }
                  
                  doFlush = true;
               }
               // if transient, JUST flush (otherwise the record is lost)
               else if (currentRecord.checkState(DmoState.NEW))
               {
                  doFlush = true;
               }
            }
            
            if (doFlush)
            {
               // flush any previously created record to the database before we release/replace it, unless
               // it is already marked invalid
               targetBuffer.flush(false);
               
               // if a newly created record is being replaced via a query, do not carry the 
               // createScope setting forward.  This only matters if the flush above was not clean
               if (!newlyCreated && !sameRecord)
               {
                  targetBuffer.markPersisted();
               }
            }
         }
      }
      catch (ValidationException exc)
      {
         if (!isUndoable())
         {
            // if not in silent error mode, we will exit abnormally with an error condition
            // exception from reportValidationException below, and we want to leave the validation
            // state intact for the next validation cycle; otherwise, we will continue and change
            // or clear the record in the buffer, so we want to clear the validation state to
            // prevent it from being out of sync with the buffer's record
            clearValidationState = txHelper.errHlp.isSilent();
         }
         
         reportValidationException(exc, this, false);
      }
      finally
      {
         if (dirtyCopy && newCrtRecord != null && dirtyContext != null)
         {
            RecordIdentifier<String> ident = new RecordIdentifier<>(getEntityName(),
                                                                    newCrtRecord.primaryKey());
            creatingBuffer = dirtyContext.getCreatingBuffer(ident);
         }
         else
         {
            creatingBuffer = null;
         }
      }
      
      if (this.currentRecord != null)
      {
         // store placeholder record if releasing current record and we haven't done so previously
         if (snapshot == null && (newCrtRecord == null || newlyCreated))
         {
            if (undoable != null)
            {
               undoable.checkUndoable(true);
            }
            
            snapshot = this.currentRecord;
         }
         
         // transition lock when releasing current record
         // the code below corresponds to case:  newCrtRecord == null || !sameRecord
         try
         {
            if (!sameRecord)
            {
               // Try to release or downgrade the lock, if any.
               if (bufferManager.isTransaction())
               {
                  Long id = this.currentRecord.primaryKey();
                  LockType pinned = getPinnedLockType(id);
                  
                  if (pinned.isExclusive())
                  {
                     // Downgrade pinned lock type.  This won't actually downgrade the lock, it
                     // just lets the lock context know that this buffer is done with its
                     // exclusive lock on this record.
                     setPinnedLockType(id, LockType.SHARE);
                  }
               }
               else
               {
                  // try to release the lock
                  RecordIdentifier<String> ident = this.currentRecord.getRecordLockIdentifier();
                  persistenceContext.getRecordLockContext().lock(ident, LockType.NONE, 0L, this);
               }
            }
         }
         catch (LockUnavailableException exc)
         {
            // safe to ignore, will not be thrown by lock release
         }
         finally
         {
            if (this.currentRecord != newCrtRecord && undoable != null)
            {
               this.undoable.checkUndoable(true);
            }
            
            this.isTouched = false;
            
            // invalidate CURRENT-CHANGED flag. it will be re-set at the end of
            // armCurrentChanged/updateCurrentChanged bracketing if the case
            this.recordChanged = false;
            
            // make sure the current record is updated, if an exception was thrown above
            Record oldRecord = this.currentRecord;
            this.currentRecord = newCrtRecord;
            
            if (!isTemporary)
            {
               TriggerTracker.releaseTracker(triggerMap, oldRecord);
               this.triggerTracker = TriggerTracker.getTracker(triggerMap, 
                                                               this.currentRecord, 
                                                               dmoBufInterface, 
                                                               bufferManager, 
                                                               dbTrigManager);
            }
            else if (oldRecord.checkState(DmoState.NEW) && undo)
            {
               try
               {
                  // this is important to dealloc the reserved PK
                  DirectAccessHelper.clearPrimaryKey(persistence.getSession(Persistence.TEMP_CTX),
                                                     (TempRecord) oldRecord);
               }
               catch (DirectAccessException e)
               {
                  LOG.log(Level.SEVERE, "Failed deleting the transient record on undo", e);
               }
            }
            
            updateNoUndoState();
            
            // make sure to notify bufferManager after the record was loaded
            bufferManager.notifyRecordWasLoaded(this);
            
            // don't count usages of template records and don't register with persistence
            if (oldRecord.primaryKey() != null && oldRecord.primaryKey() > 0)
            {
               decrementDMOUseCount(oldRecord, false);
            }
         }
      }
      
      // update current record
      if (this.currentRecord != newCrtRecord && undoable != null)
      {
         this.undoable.checkUndoable(true);
      }
      
      if (!isTemporary)
      {
         TriggerTracker.releaseTracker(triggerMap, currentRecord);
         this.triggerTracker = TriggerTracker.getTracker(triggerMap, 
                                                         newCrtRecord, 
                                                         dmoBufInterface, 
                                                         bufferManager, 
                                                         dbTrigManager);
      }
      
      this.currentRecord = newCrtRecord;
      this.recordChanged = false; // invalidate CURRENT-CHANGED flag (see above)
      this.isTouched = false;
      this.newlyCreated = newlyCreated;
      this.dirtyCopy = dirtyCopy;
      updateNoUndoState();
      
      bufferManager.notifyRecordWasLoaded(this);
      
      // don't count usages of template records and don't register with persistence
      if (newCrtRecord != null && newCrtRecord.primaryKey() != null && newCrtRecord.primaryKey() > 0)
      {
         incrementDMOUseCount(newCrtRecord, false);
      }
      
      // if not null and not currently processing an undo, reset certain state
      if (newCrtRecord != null && !undo)
      {
         locked = false;
         unknownMode = false;
         
         // creating a new record does not change our placeholder snapshot
         if (!newlyCreated)
         {
            if (snapshot != null && undoable != null)
            {
               undoable.checkUndoable(true);
            }
            snapshot = null;
         }
         
         if (LOG.isLoggable(Level.FINEST))
         {
            String desc = describe();
            LOG.log(Level.FINEST, desc + ":  currentRecord = " + this);
            LOG.log(Level.FINEST, desc + ":  snapshot = " + toString(snapshot));
         }
      }
      if (newlyCreated)
      {
         UserTableStatUpdater.create(getDatabase(), this);
      }
   }
   
   /**
    * Checks whether the conditions for firing ROW-UPDATE event are met. If affirmative the event is triggered
    * and the dataset's callback is invoked with a handle to this buffer as parameter.
    */
   private void maybeFireRowUpdateEvent()
   {
      if (isTouched && isTemporary && this.currentRecord != null)
      {
         BufferImpl buf = (BufferImpl) dmoProxy;
         if (buf._dataSet() != null)
         {
            // checks whether tracking is active
            if (getParentTable()._isTrackingChanges())
            {
               buf.invokeCallback(Buffer.ROW_UPDATE_EVENT);
               ErrorManager.clearPending(); // if an error is raised in this callback it is simply ignored 
            }
         }
      }
   }
   
   /**
    * Store the undoability of this buffer in the record's state. Even though the same record
    * may be stored in multiple buffers at the same time, this is safe because (1) a record
    * instance is unique to a user session, and (2) buffers backed by the same table must have
    * the same undoability state.
    */
   private void updateNoUndoState()
   {
      if (currentRecord != null)
      {
         currentRecord.updateState(null, DmoState.NOUNDO, !isUndoable());
      }
   }
   
   /**
    * Indicate whether a commit is pending for the most recent changes made
    * to the current record.
    *
    * @return  <code>true</code> if a commit is pending, else
    *          <code>false</code>.
    */
   private boolean isCommitPending()
   {
      return commitPending;
   }
   
   /**
    * Set the commit pending state.
    *
    * @param   commitPending
    *          <code>true</code> if we are currently in batch assign mode and
    *          the changes made while in this mode should be committed to the
    *          database immediately upon completing this mode;
    *          <code>false</code> if the commit pending state should be reset
    *          such that no commit should occur.
    */
   private void setCommitPending(boolean commitPending)
   {
      this.commitPending = commitPending;
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @param    buffer
    *           The legacy buffer name.
    *
    * @throws   ErrorConditionException
    *           if silent error mode is suppressed.
    */
   private static void errorNotAvailable(String buffer)
   {
      String msg = "No " + buffer + " record is available";
      
      if (LOG.isLoggable(Level.FINE))
      {
         PersistenceException exc = new PersistenceException(msg, 91);
         LOG.log(Level.FINE, msg, exc);
      }
      
      ErrorManager.recordOrThrowError(91, msg);
   }
   
   /**
    * Record or throw a &quot;record not available&quot; error condition for
    * the current DMO type.
    *
    * @throws  ErrorConditionException
    *          if silent error mode is suppressed.
    */
   private void errorNotAvailable()
   {
      // TODO execute "no record is available" triggers ?
      
      errorNotAvailable(getLegacyName());
   }
   
   /**
    * This class manages the record lock types pinned by this buffer. This information is used by
    * {@link RecordLockContext} to manage the communal locks held by the current user session.
    * <p>
    * When outside a transaction, only the lock type for the current record held by the buffer, if
    * any, is tracked. It only can be a SHARE lock or none at all.
    * <p>
    * When inside a transaction, the most restrictive lock type for every record which passes through
    * the buffer is tracked. Once a lock is acquired, it is not downgraded or released until the
    * transaction ends.
    */
   private class PinnedLocks
   {
      /** The pinned lock type for the current record, if any, when not in a transaction */
      private LockType noTrans = null;
      
      /** The pinned lock types for all records used by this buffer in the current transaction */
      private HashMap<Long, LockType> inTrans = new HashMap<>();
      
      /** Transaction count at last check, used for comparison to see if we are in a different transaction */
      private long lastXactCount = -1L;
      
      /**
       * Get the pinned lock type for the record with the given primary key.
       * 
       * @param   id
       *          Primary key.
       * 
       * @return  Lock type associated by this buffer with the given primary key.
       */
      LockType getPinnedLockType(Long id)
      {
         boolean transaction = manageTransactionState();
         
         if (!transaction)
         {
            return noTrans;
         }
         
         return inTrans.get(id);
      }
      
      /**
       * Set the pinned lock type for the record with the given primary key.
       * 
       * @param   id
       *          Primary key.
       * @param   lockType
       *          Lock type to set.
       */
      void setPinnedLockType(Long id, LockType lockType)
      {
         boolean transaction = manageTransactionState();
         
         LockType waitLockType = lockType == LockType.NONE ? null : lockType.toWaitVariant();
         
         if (!transaction)
         {
            noTrans = currentRecord != null && id == currentRecord.primaryKey() ? waitLockType : null;
            
            return;
         }
         
         // remember the most restrictive lock type for the given primary key
         inTrans.compute(id, (k, v) ->
         {
            if (v == null || waitLockType == null)
            {
               // add missing entry (v == null) or remove entry (waitLockType == null)
               return waitLockType;
            }
            
            if (waitLockType.compareTo(v) > 0)
            {
               // replace existing entry with more restrictive lock type
               v = waitLockType;
            }
            
            return v;
         });
      }
      
      /**
       * Clean up when the buffer closes its outermost scope.
       */
      void closeScope()
      {
         noTrans = null;
         inTrans.clear();
      }
      
      /**
       * Manage the internal state of this object, based on the current application transaction state.
       * 
       * @return  {@code true} if an application transaction is active, else {@code false}.
       */
      private boolean manageTransactionState()
      {
         boolean blockInTransaction = bufferManager.isTransaction();
         boolean rollingBack = blockInTransaction ? bufferManager.getTxHelper().hadRollback() : false;
         // treat rollback in progress as out of transaction
         boolean transaction = blockInTransaction && !rollingBack;
         long xactCount = bufferManager.getTransactionCount();
         
         if ((!inTrans.isEmpty()) == transaction && xactCount == lastXactCount)
         {
            return transaction;
         }
         
         lastXactCount = xactCount;
         
         Long id = currentRecord != null ? currentRecord.primaryKey() : null;
         
         if (transaction)
         {
            // add entry to (otherwise empty) inTrans map for current record and lock type, if any
            if (noTrans != null && id != null)
            {
               inTrans.put(id, noTrans);
            }
            // dont clean noTrans lock in order to be able to restore it
            //noTrans = null;
         }
         else
         {
            // remember lock type for current record, if any, downgrading from exclusive to share if needed
            // (can't hold exclusive lock outside a transaction)
            if (id != null)
            {
               LockType lt = inTrans.get(id);
               // downgrade noTrans lock only if not in rollback, otherwise restore it
               if (lt != null && !rollingBack)
               {
                  noTrans = lt.isExclusive() ? LockType.SHARE : lt;
               }
            }
            
            inTrans.clear();
         }
         
         return transaction;
      }
   }
   
   /**
    * Handler for methods invoked on the DMO proxy provided to client code.
    * Methods handled here include all methods of the DMO interface used to
    * create the proxy, as well as the <code>buffer()</code> method which
    * permits classes in this package to retrieve the record buffer instance
    * itself from the proxy. The former methods are meant for use by client
    * code, while the latter allows persistence support classes, such as
    * query objects, to access the buffer for implementation and housekeeping
    * purposes.
    */
   private final class Handler
   implements InvocationHandler
   {
      /** Exception thrown during validation of no-undo temp-tables, which must be deferred */
      private ValidationException deferredValidationError = null;
      
      /**
       * Default constructor.
       */
      Handler()
      {
      }
      
      /**
       * This method intercepts proxied method calls invoked on DMOs. This includes all property getter and
       * setter methods, as well as methods declared by {@code Object} and {@code BufferReference}.
       *
       * @param   proxy 
       *          The target proxy object.
       * @param   method 
       *          The method to be executed.
       * @param   args 
       *          The method arguments.
       *
       * @return  The method invocation result.
       *
       * @throws  Throwable
       *          For various reasons, like illegal access to fields, illegal arguments or target.
       */
      public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable
      {
         Object result = null;
         boolean isSetter = false;
         boolean isDirtyRead = false;
         Runnable restoreActiveState = null;
         String property = null; // TODO: remove this, use the property index
         PropertyMeta pm = null;
         boolean isCLOBGetter = false;
         
         try
         {
            // handle reserved methods
            Class<?> declClass = method.getDeclaringClass();
            if (declClass.equals(Object.class))
            {
               return Utils.invoke(method, RecordBuffer.this, args);
            }
            else if (declClass.equals(BufferReference.class))
            {
               switch (method.getName())
               {
                  case "buffer": return RecordBuffer.this;
                  case "definition": return RecordBuffer.this;
               }
               
               return Utils.invoke(method, RecordBuffer.this, args);
            }
            
            // TODO: use something more reliable (but efficient) here to determine whether this is a setter
            isSetter = (method.getReturnType() == Void.TYPE);
            if (isSetter)
            {
               if (!isTemporary && !bufferManager.isTransaction())
               {
                  ErrorManager.recordOrThrowError(7369, 
                                                  "Update of buffer field object requires a transaction", 
                                                  false);
                  return null;
               }
               
               // in case of multi-tenant without default tables, transient records CAN BE CREATED without PK,
               // but they are NEVER validated/flushed, so neither they go to dirtyContext
               if (!dmoInfo.multiTenant           ||
                   dmoInfo.multiTenantWithDefault ||
                   persistenceContext.getTenantId() != TenantManager.DEFAULT_TENANT_ID)
               {
                  // start tracking current record in DirtyContext
                  if (dirtyContext != null && DirtyShareSupport.isEnabledCrossSession())
                  {
                     dirtyContext.insert(RecordBuffer.this, getCurrentRecord(), true, null);
                  }
                  
                  bufferManager.registerDirtyBuffer(RecordBuffer.this);
               }
            }
            
            // if another buffer created the current record, delegate setter method calls to it,
            // to ensure all validation and flushing is tracked properly
            if (creatingBuffer != null && currentRecord != null && isSetter)
            {
               // prevent recursive call
               if (!creatingBuffer.handler.equals(this))
               {
                  return creatingBuffer.handler.invoke(proxy, method, args);
               }
            }
            
            initialize();
            
            // is property an extent field?
            boolean extent = false;
            if (args != null)
            {
               extent = (args.length == (isSetter ? 2 : 1));
            }
            
            // we may be in temporary record mode to allow client-side where clause processing
            Record tempRecord = getTempRecord();
            
            // if a temporary record is set, use it to back the invocation
            if (tempRecord != null)
            {
               return Utils.invoke(method, tempRecord, args);
            }
            
            if (isSetter)
            {
               // check whether we are allowed to make changes to this buffer
               BufferImpl buf = (BufferImpl) proxy;
               if (isTemporary && buf.isBeforeBuffer() && buf.hasBeforeBufferConstraints())
               {
                  ErrorManager.recordOrThrowError(12375, buf.name().toStringMessage());
                  // Cannot update fields in a BEFORE-TABLE.
                  return null;
               }
               
               // if a setter is called, ensure the transactionId is set for its database
               getTxHelper().checkTransaction(persistence.getDatabase(!dmoInfo.multiTenant).getId());
            }
            
            // ensure backing DMO is available
            if (currentRecord == null)
            {
               // TODO: getters map is going away...
               boolean validGetter = getters.contains(method);
               
               // outer join processing will set unknown mode, because in an outer join situation,
               // it can be valid to have no record in the buffer and still request a property
               // value. The same stands when query parameters are evaluated.
               // Return unknown value in these cases.
               if ((unknownMode || bufferManager.isUnknownMode()) && validGetter)
               {
                  return FieldReference.unknownValue(method);
               }
               else
               {
                  // throw an exception if not in silent mode
                  if (isSetter)
                  {
                     // attempt to set field on empty buffer results in "unable to update" error
                     ErrorManager.recordOrThrowError(142, "Unable to update " + getLegacyName() + " Field");
                  }
                  else
                  {
                     // attempt to get field from empty buffer results in "record not available" error
                     errorNotAvailable();
                  }
                  
                  // silent error mode;  return unknown value if possible
                  return (validGetter ? FieldReference.unknownValue(method) : null);
               }
            }
            
            boolean denormalized = false;
            boolean activeBufferSet = bufferManager.isBufferDirtyBatch(RecordBuffer.this);
            if (isSetter)
            {
               // for any setter method and some getters (e.g., _getClob), the DMO needs access to this buffer
               // the active buffer is set for each buffer is a single assign and just once for a buffer
               // used in a batch assign.

               if (!bufferManager.isBatchAssignMode() || !activeBufferSet)
               {
                  restoreActiveState = currentRecord.setActiveBuffer(RecordBuffer.this);
               }

               pm = dmoInfo.getPropsBySetterMap().get(method);
               if (pm == null)
               {
                  LOG.log(Level.SEVERE, "Failed to obtain property for " + method);
                  return null;  // TODO: better idea?
               }
               
               // in case of dynamic index of denormalized extent field, the [pm] is just the 'generic'
               // PropertyMeta (the first element, see DmoMeta.getPropsBySetterMap()) we need to find the true
               // property now
               if (!pm.getOriginalName().isEmpty() && args.length > 1)
               {
                  int extIdx = (int) args[0]; // already computed as 0-base
                  pm = metadata.getPropertyMeta(false)[pm.getOffset() + extIdx];
                  denormalized = true;
                  
                  if (extIdx < 0 || extIdx >= pm.getDmoProperty().extent)
                  {
                     String msg = "Array subscript " + (extIdx + 1) + " is out of range";
                     ErrorManager.recordOrThrowError(26, msg);
                     return null;
                  }
               }
               
               property = pm.getName();
               isDirtyRead = dmoInfo.isDirtyRead();
               
               // if this is a read-only mode buffer, do not allow this to happen
               if (readonly)
               {
                  int[] errCodes = { 2665, 142 };
                  String[] errMessages =
                  {
                     "Attempt to update a Read-Only buffer for table " + variable,
                     "Unable to update " + getLegacyName() + " Field"
                  };
                  
                  // TODO: this seems like an always fatal error, cannot be put on hold with no-error
                  ErrorManager.recordOrThrowError(errCodes, errMessages, true);
               }
               
               // if a template record is loaded we are not allowed to alter it that easy
               Long pk = currentRecord.primaryKey();
               if (pk != null && pk < 0)
               {
                  final String msg = "Cannot update a template record without a schema lock";
                  ErrorManager.recordOrThrowError(12376, msg, false, false);
                  
                  return null;
               }
               
               // setting an unknown value to a mandatory field will raise errors 275 and 142
               // regardless we are in a batch or not
               Object val = extent ? args[1] : args[0];
               if (!(val instanceof BaseDataType))
               {
                  if (LOG.isLoggable(Level.WARNING))
                  {
                     // is this legal?
                     LOG.log(Level.WARNING, "Attempt to assign a non - BDT value to a record field");
                  }
               }
               else
               {
                  boolean batchAssign = bufferManager.isBatchAssignMode();
                  BaseDataType newBdtValue = (BaseDataType) val; 
                  if (!(batchAssign || bulkCopy) && newBdtValue.isUnknown() && pm.isMandatory())
                  {
                     int[] errCodes = { 275, 142 };
                     String[] errMessages =
                     {
                        pm.getLegacyName() + " is mandatory, but has a value of ?",
                        "Unable to update " + getLegacyName() + " Field"
                     };
                     ErrorManager.recordOrThrowError(errCodes, errMessages, true);
                  }
               }
               
               // TODO: we need to copy the buffer to the before-table as it exists, but what if validation 
               //       fails bellow?
               // update the before table only when current (after) buffer is a dataset buffer
               // has a BEFORE-TABLE to keep the changes in and tracking is activated (not in FILL mode)
               BufferImpl after = (BufferImpl) RecordBuffer.this.dmoProxy;
               if (!ignoreBeforeTracking    &&
                   after.isAfterBuffer()    &&
                   ((AbstractTempTable) TempTableBuilder.asTempTable((Temporary) after))._isTrackingChanges())
               {
                  BufferImpl before = after.beforeBufferNative();
                  if (before != null) // should not be
                  {
                     rowid beforeRowid = peerRowid();
                     if (beforeRowid.isUnknown())
                     {
                        // create new before record image,
                        before.setUpBeforeBuffer(true);
                        before.create();
                        before.bufferCopy(after);
                        //  then update my before-rowid and row-state,
                        before.buffer().rowState(Buffer.ROW_MODIFIED);
                        after.buffer().rowState(Buffer.ROW_MODIFIED);
                        before.buffer().peerRowid(after.rowID());
                        after.buffer().peerRowid(before.rowID());
                        // keep the reference to the datasource-rowid / origin-rowid,
                        ((TemporaryBuffer) before.buffer()).getCurrentRecord()._originRowid(
                            ((TemporaryBuffer) after.buffer()).getCurrentRecord()._originRowid());
                        ((TemporaryBuffer) before.buffer()).getCurrentRecord()._datasourceRowid(
                            ((TemporaryBuffer) after.buffer()).getCurrentRecord()._datasourceRowid());
                        before.buffer().flush();
                        before.setUpBeforeBuffer(false);
                        // do not release the before buffer, let it hold the newly created row
                     }
                     // else actually do nothing, the before image is already saved
                  }
               }
            }
            else
            {
               pm = dmoInfo.getPropsByGetterMap().get(method);
               if (pm == null)
               {
                  LOG.log(Level.SEVERE, "Failed to obtain property for " + method);
                  return null;
               }

                if (pm.getDmoProperty()._fwdType.equals(clob.class))
                {
                   restoreActiveState = currentRecord.setActiveBuffer(RecordBuffer.this);
                   isCLOBGetter = true;
                }
               
               if (extent && !pm.getOriginalName().isEmpty())
               {
                  int extIdx = (int) args[0];
                  pm = metadata.getPropertyMeta(false)[pm.getOffset() + extIdx];
                  denormalized = true;
                  
                  if (extIdx < 0 || extIdx >= pm.getDmoProperty().extent)
                  {
                     String msg = "Array subscript " + (extIdx + 1) + " is out of range";
                     ErrorManager.recordOrThrowError(26, msg);
                     return FieldReference.unknownValue(method);
                  }
               }
            }
            
            // get the current value. It will become the [old] value in the trigger, if fired
            BaseDataType oldVal = null;
            if (isSetter               &&
                !extent                &&
                triggerTracker != null &&
                triggerTracker.isTriggerEnabled(DatabaseEventType.ASSIGN, property, logicalDatabase))
            {
               // get the existing value and save it for calling the triggers
               Method getter = dmoInfo.getPojoGetterMap().get(property);
               PropertyMeta meta = dmoInfo.getPropsByGetterMap().get(getter);
               oldVal = meta.getDataHandler().getField(currentRecord, meta.getOffset());
            }
            
            // we are about to change a field's value: take a snapshot the current values of the fields of the
            // record to be use later, as the old DMO, if we decide later that a write-trigger will be fired
            Record writeSnapshot = null;
            if (isSetter                                   &&
                !extent                                    &&
                triggerTracker != null                     &&
                triggerTracker.getOldDMO() == null         &&
                triggerTracker.isTriggerEnabled(DatabaseEventType.WRITE, null, logicalDatabase))
            {
               writeSnapshot = currentRecord.snapshot();
            }
            
            // invoke the method on the concrete DMO implementation
            result = Utils.invoke(method, currentRecord, args);
            
            if (writeSnapshot != null && !currentRecord.checkState(DmoState.CHANGED))
            {
               writeSnapshot = null; // actually no, the setter has not made any change to the property
            }
            
            boolean changed = false;
            boolean processChange = false;
            boolean batchAssign = false;
            
            if (isSetter)
            {
               isTouched = true;
               
               Object[] diff = null;
               batchAssign = bufferManager.isBatchAssignMode();
               int ormIndex = pm.getOffset();
               // NOTE: if the property was denormalized, the property index [ormIndex] already contains
               //       the extent index 
               int extentIndex = extent && !denormalized ? (Integer) args[0] : 0;
               diff = currentRecord.getActiveUpdateDiff(ormIndex, extentIndex);
               if (batchAssign || bulkCopy)
               {
                  // batch mode: defer validation until assignment batch is closed
                  // bulkCopy mode: skip validation (unless also in batch mode)
                  if (batchAssign && !activeBufferSet)
                  {
                     bufferManager.addDirtyBatchBuffer(RecordBuffer.this, restoreActiveState);
                  }
               }
               else
               {
                  try
                  {
                     if (!validateMaybeFlush(currentRecord, false, false))
                     {
                        diff = null;
                     }
                  }
                  catch (ValidationException exc)
                  {
                     if (isUndoable())
                     {
                        currentRecord.undoActive();
                        
                        throw exc;
                     }
                     else
                     {
                        // for no-undo buffers, we defer the exception, to allow the DMO to
                        // be updated with the incorrect value, to ensure future validation
                        // prevents this record from being flushed to the database; it is
                        // thrown near the end of invoke()
                        deferredValidationError = exc;
                     }
                  }
                  
                  processChange = true;
               }
               
               changed = diff != null;
               if (changed)
               {
                  addUnreportedChanges(ormIndex, extentIndex, diff);
               }
            }
            
            // decide whether the ASSIGN trigger will be invoked:
            //    * don't invoke the trigger if value hasn't changed
            //    * triggers do not operate on extent fields
            boolean invokeTriggers = triggerTracker != null && changed && !extent;
            boolean hasAssignTrigger = invokeTriggers &&
                                       triggerTracker.isTriggerEnabled(DatabaseEventType.ASSIGN, 
                                                                       property, 
                                                                       logicalDatabase);
            if (hasAssignTrigger && triggerTracker.isFieldInTrigger(property))
            {
               // we avoid recursive double triggering
               if (triggerTracker.isFieldCurrentTrigger(property))
               {
                  // block direct recursion; field is allowed to be changed inside its trigger
                  invokeTriggers = false;
               }
               else
               {
                  // this is an indirect recursive call
                  Class<? extends DataModelObject> dmoIface = getDMOInterface();
                  DatabaseTriggerManager.handleError(
                     1003,
                     DatabaseEventType.ASSIGN,
                     dmoInfo.legacyTable,
                     TableMapper.getLegacyFieldName(dmoIface, property));
               }
            }
            
            // we changed a field's value, archive the snapshot of the record to use later as the old DMO, if
            // there is a WRITE trigger registered for this table
            if (invokeTriggers && writeSnapshot != null)
            {
               triggerTracker.setOldDMO(writeSnapshot, bufferManager.getOpenBufferScopes());
            }
            
            // invoke assign triggers
            if (invokeTriggers && hasAssignTrigger)
            {
               if (batchAssign) // always false ?
               {
                  // defer firing of assign triggers in batch mode until batch is finished
                  triggerTracker.addPendingBatchField(property, oldVal);
               }
               else
               {
                  boolean ok;
                  try
                  {
                     triggerTracker.pushField(property);
                     ok = DatabaseTriggerManager.triggerAssign(getDMOBufInterface(),
                                                               (Buffer) getDMOProxy(),
                                                               property,
                                                               oldVal,
                                                               logicalDatabase);
                  }
                  finally
                  {
                     String top = triggerTracker.popField();
                     assert property.equals(top);  // the property must be the last one pushed
                  }
                  if (!ok)
                  {
                     // the assign trigger vetoed: rolling back the change
                     Utils.invoke(method, currentRecord, oldVal);
                     processChange = false;
                  }
               }
            }
            
            // broadcast notification if state changed
            if (processChange)
            {
               processChange();
            }
            
            if (deferredValidationError != null)
            {
               throw deferredValidationError;
            }
         }
         catch (ValidationException exc)
         {
            // this is a 3rd time validation, final, on SQL server side
            reportValidationException(exc, RecordBuffer.this, true);
            
            // when a field validation error occurs (e.g., 32-bit integer overflow) the exception 
            // thrown is an ErrorConditionException, which is the target for an 
            // InvocationTargetException, so it is caught below
         }
         catch (PersistenceException exc)
         {
            ErrorManager.recordOrThrowError(exc);
         }
         catch (InvocationTargetException exc)
         {
            // look for ErrorConditionException
            Throwable ex2 = exc;
            while (ex2 != null && !(ex2 instanceof ConditionException))
            {
               ex2 = ex2.getCause();
            }
            
            if (ex2 != null)
            {
               ErrorManager.recordOrThrowError(142, "Unable to update " + getLegacyName() + " Field");
            }
            else
            {
               DBUtils.handleException(getDatabase(), exc);
               
               Throwable cause = exc.getTargetException();
               
               if (LOG.isLoggable(Level.SEVERE))
               {
                  String errorText = describe() + ":  error invoking proxied DMO method";
                  LOG.log(Level.SEVERE, errorText, exc);
               }
               
               // must be the result of a programming error; don't wrap in a legacy-style
               // exception type
               throw cause;
            }
         }
         finally
         {
            deferredValidationError = null;
            // do not restore the active state for the buffers that were used in a batch assign,
            // it is done in cleanupBatchMode().
            if (restoreActiveState != null && (!bufferManager.isBatchAssignMode() || isCLOBGetter))
            {
               restoreActiveState.run();
            }
         }
         
         return result;
      }
   }
   
   /**
    * Cross reference dirty DMO properties with indexes containing them, creating a list of indexes which have
    * been updated.
    *
    * @param   unique
    *          If {@code true}, only unique indexes are checked. If {@code false}, only non-unique indexes are
    *          checked.
    * @param   fullMatch
    *          If {@code true} every property in an index must be dirty for that index to be considered dirty.
    *          If {@code false}, any dirty property in an index is enough to consider that index dirty.
    * @param   cumulative
    *          Only in the case of non-unique indexes. If {@code true}, collect all indexes that were marked
    *          dirty since the record was loaded. If {@code false}, only indexes marked dirty since the last
    *          validation/share.
    *
    * @return  A list of names of indexes considered dirty, in ascending alphabetical order (?).
    */
   List<String> listDirtyIndexes(boolean unique, boolean fullMatch, boolean cumulative)
   {
      RecordMeta recordMeta = currentRecord._getRecordMeta();
      BitSet[] indexSet = unique ? recordMeta.getUniqueIndices() : recordMeta.getNonuniqueIndices();
      String[] indexNames = unique ? recordMeta.getUniqueIndexNames() : recordMeta.getNonuniqueIndexNames();
      BitSet crtDirty = currentRecord.getDirtyProps();
      List<String> ret = new ArrayList<>();
      
      for (int k = 0; k < indexSet.length; k++)
      {
         if (fullMatch)
         {
            // all properties of an index are required to be dirty
            BitSet indexCopy = (BitSet) indexSet[k].clone(); // make a copy as the [and] op is destructive
            
            indexCopy.and(crtDirty);
            if (cumulative)
            {
               indexCopy.clear();
            }
            
            if (indexCopy.equals(indexSet[k]))
            {
               ret.add(indexNames[k]);
            }
         }
         else
         {
            // check if at least one property is dirty
            if (crtDirty.intersects(indexSet[k]))
            {
               ret.add(indexNames[k]);
            }
         }
      }
      return ret;
   }
   
   /**
    * A lightweight implementation of the <code>Undoable</code> interface
    * which restores to the record buffer the current record and snapshot
    * which were active at the time of backup.
    * <p>
    * An instance of this class is registered for undo support with the
    * <code>TransactionManager</code> during record buffer construction.
    */
   protected final class LightweightUndoable
   extends LazyUndoable
   {
      /** DMO which is active in record buffer at the time of backup */
      private Record activeRecord = null;
      
      /** Snapshot which is active in record buffer at the time of backup */
      private Record activeSnapshot = null;
      
      /** Lock type pinned by the active record at the time of backup */
      private LockType pinnedLockType = null;
      
      /**
       * Default c'tor - is a no-op.
       */
      protected LightweightUndoable()
      {
         // nothing to do
      }
      
      /**
       * Create a new instance and mark it as global depending on the associated flag.
       * 
       * @param    global
       *           Flag identifying if this buffer is associated with a global temp-table.
       */
      protected LightweightUndoable(boolean global)
      {
         setGlobal(global);
      }
      
      /**
       * Store a reference to the record currently active in the record
       * buffer in a new instance of this class and return it.
       *
       * @return  An instance of this class which contains the undoable state
       *          of the record buffer at the moment it is called.
       */
      public Undoable deepCopy()
      {
         LightweightUndoable copy = new LightweightUndoable();
         
         copy.activeRecord = currentRecord;
         copy.activeSnapshot = snapshot;
         
         copy.pinnedLockType = (currentRecord != null
                                ? getPinnedLockType(currentRecord.primaryKey())
                                : LockType.NONE);
         
         return copy;
      }
      
      /**
       * Restore to the record buffer the current record and current
       * reversible which were set at the time the record buffer was backed
       * up for undo support.
       *
       * @param   from
       *          An instance of this class containing the backup state to be
       *          restored.
       */
      public void assign(Undoable from)
      {
         if (offEndRollback)
         {
            return;
         }
         
         if (!isActive())
         {
            // If the buffer is not yet active, then the state will not have
            // changed, and there is nothing to do.
            return;
         }
         
         LightweightUndoable copy = (LightweightUndoable) from;
         
         // reset current record, snapshot, and pinned lock type to what they were before backup
         Record active = copy.activeRecord;
         setCurrentRecord(active, true, false, false, false);
         snapshot = copy.activeSnapshot;
         LockType lockType = copy.pinnedLockType;
         
         if (active != null)
         {
            Long id = active.primaryKey();
            
            LockType pinned = getPinnedLockType(id);
            if (lockType == null)
            {
               lockType = pinned;
            }
            if (!lockType.equals(pinned))
            {
               setPinnedLockType(id, lockType);
            }
         }
      }
      
      /**
       * Check if this undoable has changed; if so, and we are in a transaction block (and not
       * {@code rollingBack}), then register this instance with all blocks up the stack, which
       * were started after the object's last save transaction level.
       * 
       * @param    changed
       *           Flag indicating if this undoable has changed or not.
       */
      @Override
      public void checkUndoable(boolean changed)
      {
         super.checkUndoable(changed);
      }
      
      /**
       * Check if this instance is undoable.
       * 
       * @return   The same state as returned by {@link RecordBuffer#isUndoable()}.
       */
      @Override
      protected boolean isUndoable()
      {
         return RecordBuffer.this.isUndoable();
      }
   }
   
   /**
    * Getter for the {@link #setterDatums} map.
    * 
    * @return   See above.
    */
   protected Map<String, DatumAccess> getSetterDatums()
   {
      if (setterDatums == null)
      {
         setterDatums = new HashMap<>();
      }
      return setterDatums;
   }
   
   /**
    * Getter for the {@link #getterDatums} map.
    * 
    * @return   See above.
    */
   protected Map<String, DatumAccess> getGetterDatums()
   {
      if (getterDatums == null)
      {
         getterDatums = new HashMap<>();
      }
      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,
    * if accessing a single element in an extent property.
    */
   protected static class DatumAccess
   {
      /** Property name */
      private final String propertyName;
      
      /** Index of an element in an extent property or null */
      private final Integer extentIndex;
      
      /** The cached accessor for this instance. */
      private final Method accessor;
      
      /** The extent value of this property or null. */
      private final Integer extent;
      
      /** The cached property meta. */
      private final PropertyMeta propertyMeta;
      
      /** Flag indicating if there is an index using this property. */
      private final boolean indexed;
      
      /** Flag indicating if there is an ASSIGN trigger for this property. */
      private final boolean trigger;
      
      /**
       * 
       * Constructor.
       * 
       * @param    propertyName
       *           Property name.
       * @param    extentIndex
       *           Index of an element in an extent property or {@code null}. If present, the index should 
       *           be zero-based.
       * @param    accessor
       *           The cached accessor for this instance.
       * @param    extent
       *           The extent value of this property or {@code null}.
       * @param    propertyMeta
       *           The cached property meta.
       * @param    indexed
       *           Flag indicating if there is an index using this property.
       * @param    trigger
       *           Flag indicating if there is an ASSIGN trigger for this property.
       */
      protected DatumAccess(String propertyName,
                            Integer extentIndex, 
                            Method accessor, 
                            Integer extent, 
                            PropertyMeta propertyMeta, 
                            boolean indexed,
                            boolean trigger)
      {
         this.propertyName = propertyName;
         this.extentIndex = extentIndex;
         this.accessor = accessor;
         this.extent = extent;
         this.propertyMeta = propertyMeta;
         
         this.indexed = indexed;
         this.trigger = trigger;
      }
      
      /**
       * Check if this access can be done directly via the field's datum.
       * 
       * @param   other
       *          In case a getter and a setter are used in pair, the 'other' and this property must be of
       *          the same type.
       * 
       * @return  {@code true} if the access can be done directly.
       */
      public boolean isDirectAccess(DatumAccess other)
      {
         // direct access can be used only if the two property have the same type and the dst property is not:
         // - mandatory
         // - part of an index
         // - ASSIGN trigger
         
         return (other == null || propertyMeta.getDataHandler() == other.propertyMeta.getDataHandler()) && 
                !(indexed || propertyMeta.isMandatory() || trigger);
      }
      
      /**
       * Override hash code algorithm
       * 
       * @return  Hash code.
       */
      public int hashCode()
      {
         int result = propertyName.hashCode();
         result = 31 * result + (extentIndex != null ? extentIndex.hashCode() : 0);
         
         return result;
      }
      
      /**
       * Override equality algorithm to be consistent with hash code algorithm.
       * 
       * @param   o
       *          Object to be compared.
       * 
       * @return  <code>true</code> if this instance is equivalent with the given instance.
       */
      public boolean equals(Object o)
      {
         if (this == o)
         {
            return true;
         }
         
         if (o == null || getClass() != o.getClass())
         {
            return false;
         }
         
         DatumAccess that = (DatumAccess) o;
         
         if ((this.extentIndex == null) != (that.extentIndex == null))
         {
            // both must have or both must not have extent
            return false;
         }
         
         if (this.extentIndex != null && this.extentIndex != that.extentIndex)
         {
            // if both have but it is different  
            return false;
         }
         
         return propertyName.equals(that.propertyName);
      }
      
      /**
       * Get the {@link #propertyMeta}.
       * 
       * @return   See above.
       */
      public PropertyMeta getPropertyMeta()
      {
         return propertyMeta;
      }
      
      /**
       * Get the {@link #extent}.
       * 
       * @return   See above.
       */
      public Integer getExtent()
      {
         return extent;
      }
      
      /**
       * Get the {@link #accessor}.
       * 
       * @return   See above.
       */
      protected Method getAccessor()
      {
         return accessor;
      }
      
      /**
       * Get field name.
       * 
       * @return  Property name
       */
      protected String getPropertyName()
      {
         return propertyName;
      }
      
      /**
       * Get extent index (zero-based).
       * 
       * @return  Extent index or <code>null</code> if all elements are needed or not an extent
       *          field.
       */
      protected Integer getExtentIndex()
      {
         return extentIndex;
      }
      
      @Override
      public String toString()
      {
         StringBuilder sb = new StringBuilder("DatumAccess{");
         sb.append(propertyName).append("/").append(propertyMeta.getLegacyName());
         if (extent != null && extent > 0)
         {
            sb.append("[").append(extent).append("]");
         }
         sb.append("}");
         return sb.toString();
      }
   }
   
   /** 
    * Used as a handler for buffers which are referenced through a buffer parameter. This should
    * handle specific DMO alias operations, while any other method should be delegated to the 
    * back of the proxy - the buffer. The motivation behind this is related to the fact that 
    * buffer parameters should have the same backing buffer, but still differ through their 
    * DMO alias and name. This hander will manage both {@link Buffer} proxies and 
    * {@link RecordBuffer} proxies.
    */
   private static class ArgumentBuffer
   implements InvocationHandler
   {
      /** The backing buffer of this proxy, which should handle the majority of the methods */
      private BufferImpl buffer;
      
      /** The definition buffer of the buffer instance. */
      private RecordBuffer defBuffer;
      
      /** The bound buffer of the buffer instance. Can be the same as {@code defBuffer}. */
      private RecordBuffer boundBuffer;
      
      /** The proxy for the definition buffer */
      private RecordBuffer defProxy;
      
      /** 
       * The proxy for the bound buffer. It is the same as {@code defProxy}, if the definition
       * buffer is the same as the bound buffer.
       */
      private RecordBuffer boundProxy;
      
      /** The DMO alias which should be adopted by the backing record buffer */
      private String dmoAlias;
      
      /** The buffer name which should be adopted by the buffer */
      private String bufferName;

      /**
       * The basic initializing constructor.
       * 
       * @param   buffer
       *          The buffer which is going to handle most of the methods
       * @param   dmoAlias
       *          The DMO alias which should be adopted by the RecordBuffer
       * @param   bufferName
       *          The name which should be adopted by the BufferImpl
       */
      ArgumentBuffer(BufferImpl buffer, String dmoAlias, String bufferName)
      {
         this.buffer = buffer;
         this.dmoAlias = dmoAlias;
         this.bufferName = bufferName;
         
         this.boundBuffer = buffer.buffer();
         this.defBuffer = ((BufferReference) buffer).definition();
         
         this.boundProxy = createProxy();
         if (defBuffer == boundBuffer)
         {
            this.defProxy = this.boundProxy;
         }
         else
         {
            this.defProxy = createProxy();
         }
      }
      
      /**
       * Delivers a new proxy backed by this handler. The instance created is made with the
       * implicit constructor, so invoking methods on this instance without an invocation handler
       * in between, can trigger inconsistent data or exceptions.

       * @return   A proxy for the specified buffer.
       */
      private RecordBuffer createProxy()
      {
         try
         {
            @SuppressWarnings("unchecked")
            RecordBuffer dmoProxy = 
               (RecordBuffer) ProxyFactory.getProxy(RecordBuffer.class,
                                                    true,
                                                    new Class[0],
                                                    false,
                                                    null,
                                                    null,
                                                    this);
            
            return dmoProxy;
         }
         catch (Exception e)
         {
            throw new RuntimeException(e);
         }
      }

      /**
       * Proxy handler which should let the methods to the buffer, beside the ones which
       * imply the DMO alias or buffer name, which will be handled here.
       */
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
      {
         try 
         {
            if (proxy instanceof BufferImpl)
            {           
               if (method.getName().equals("buffer"))
               {
                  return boundProxy;
               }        
               
               if (method.getName().equals("definition"))
               {
                  return defProxy;
               }

               // avoid altering methods which should be handled directly by the buffer
               // also this is used to suppress the behavior of enclosed argument proxies
               if (method.getName().equals("overrideName") ||
                   method.getName().equals("restoreName"))
               {
                  return Utils.invoke(method, buffer, args);
               }
               
               boolean overridden = buffer.overrideName(bufferName);
               Object invokeReturn;
                             
               try
               {
                  invokeReturn = Utils.invoke(method, buffer, args);
               }
               finally
               {
                  if (overridden)
                  {
                     buffer.restoreName();
                  }
               }
               
               return invokeReturn;
            }
            
            if (proxy instanceof RecordBuffer)
            {
               RecordBuffer backBuffer;
               if (proxy == boundProxy)
               {
                  backBuffer = boundBuffer;
               }
               else
               {
                  backBuffer = defBuffer;
               }
               
               // avoid altering methods which should be handled directly by the buffer
               // also this is used to suppress the behavior of enclosed argument proxies
               if (method.getName().equals("overrideDMOAlias") ||
                   method.getName().equals("restoreDMOAlias"))
               {
                  return Utils.invoke(method, backBuffer, args);
               }
               
               boolean overridden = backBuffer.overrideDMOAlias(dmoAlias);
               Object invokeReturn;
               
               try
               {
                  invokeReturn = Utils.invoke(method, backBuffer, args);
               }
               finally
               {
                  if (overridden)
                  {
                     backBuffer.restoreDMOAlias();
                  }
               }
               
               return invokeReturn;
            }
         }
         catch (Throwable t)
         {
            Throwable cause = t;
            while (cause != null && !(cause instanceof ConditionException))
            {
               cause = cause.getCause();
            }
            
            if (cause instanceof ConditionException)
            {
               throw cause;
            }
            else
            {
               throw t;
            }
         }
         
         // should be unreachable
         return null;
      }
   }
   
   /**
    * Obtain a string representation of the table content stored on SQL. This is a SQL-wise view of the
    * primary table of the DMO, not an actual ABL data. Only to be used in debug purposes.<br>
    * Notes:
    * <ul>
    *    <li>the EXTENT properties are not shown unless the table is denormalized (in which case the these
    *          properties are stored in the primary table);
    *    <li>the name of the columns in the returned string are the SQL column names, not the legacy names;
    *    <li>the hidden, reserved before/after-table specific attributes are visible for temp-tables;
    *    <li>the larger string/character values are cut to max 32 chars;
    * </ul>
    *
    * @param   limit
    *          The maximum number of rows to be retrieved. Use 0 or negative to get them all.
    *
    * @return  A string with the table content in a tabular format. 
    *
    * @throws  PersistenceException
    * @throws  SQLException
    *          If any issue was encountered. Since this method is supposed to be called only in debug mode,
    *          it's up to programmer to decide what went wrong.
    */
   public String sqlTableContent(int limit)
   throws PersistenceException, SQLException
   {
      String sql = "select * from " + dmoInfo.sqlTable;
      if (limit > 0)
      {
         sql += " limit " + limit;
      }
      
      Connection conn = persistence.getSession(!dmoInfo.multiTenant).getConnection();
      try (PreparedStatement ps = conn.prepareStatement(sql))
      {
         ResultSet res = ps.executeQuery();
         ResultSetMetaData md = res.getMetaData();
         int cc = md.getColumnCount();
         int[] lens = new int[cc];
         
         List<String[]> rows = new ArrayList<>(10);
         while (res.next())
         {
            String[] row = new String[cc];
            for (int k = 1; k <= cc; k++)
            {
               row[k - 1] = res.getString(k);
               int length = row[k - 1] == null ? 4 : row[k - 1].length();
               if (lens[k - 1] < length)
               {
                  lens[k - 1] = length;
               }
            }
            rows.add(row);
         }
         
         StringBuilder sb = new StringBuilder();
         sb.append("ABL: ").append(dmoInfo.legacyTable).append("/ SQL:").append(dmoInfo.sqlTable).append("\n");
         for (int k = 1; k <= cc; k++)
         {
            String label = md.getColumnLabel(k);
            if (lens[k - 1] < label.length())
            {
               lens[k - 1] = label.length();
            }
            
            if (lens[k - 1] > 32)
            {
               lens[k - 1] = 32;
            }
            else if (lens[k - 1] < 4)
            {
               lens[k - 1] = 4;
            }
            
            if (k != 1)
            {
               sb.append(" | ");
            }
            fit(sb, label, lens[k - 1], md.getColumnType(k));
         }
         sb.append("\n");
         
         for (int k = 1; k <= cc; k++)
         {
            if (k != 1)
            {
               sb.append("-+-");
            }
            int len = lens[k - 1];
            while (len-- > 0)
            {
               sb.append("-");
            }
         }
         sb.append("\n");
         
         for (String[] row : rows)
         {
            for (int k = 1; k <= cc; k++)
            {
               if (k != 1)
               {
                  sb.append(" | ");
               }
               fit(sb, row[k - 1], lens[k - 1], md.getColumnType(k));
            }
            
            sb.append("\n");
         }
         
         res.close();
         return sb.toString();
      }
   }
   
   /**
    * Helper method for {@link #sqlTableContent(int)}. Used for generating the tabular pretty format. 
    *
    * @param   sb {@code StringBuilder} to put the result into.
    *          The 
    * @param   text
    *          The value to be formatted.
    * @param   size
    *          The available space.
    * @param   type
    *          The column type. Used for alignment.
    */
   private void fit(StringBuilder sb, String text, int size, int type)
   {
      boolean leftAlign = false;
      switch (type)
      {
         case Types.NCHAR:
         case Types.NVARCHAR:
         case Types.CHAR:
         case Types.VARCHAR:
         case Types.LONGVARCHAR:
            leftAlign = true;
            break;
      }
      
      String ret;
      if (text == null)
      {
         ret = "null";
      }
      else if (text.length() <= size)
      {
         ret = text;
      }
      else
      {
         ret = text.substring(0, size);
      }
      
      int k = size - ret.length();
      if (leftAlign)
      {
         sb.append(ret);
      }
      while (k-- > 0)
      {
         sb.append(" ");
      }
      if (!leftAlign) 
      {
         sb.append(ret);
      }
   }

   /**
    * Helper class to extract DMO super Interface from Buffer interface.
    *
    * @param   iface
    *          The buffer interface.
    *
    * @return  DMO type interface
    *
    */
   public static Class<?> extractDMOInterface(Class<?> iface)
   {
      if (Buffer.class.isAssignableFrom(iface))
      {
         // this should have been DMO interface and not Buffer interface
         for(Class <?> superIface:iface.getInterfaces())
         {
            if (DataModelObject.class.isAssignableFrom(superIface))
            {
               return (Class<?>) superIface;
            }
         }
         
      }
      return iface;
   }
   
   /**
    * Check if OffEnd is reversible.
    * Reversible OffEnd treats OffEnd.FRONT as OffEnd.BACK for PREV/LAST queries
    * @return true if OffEnd is reversible
    */
   boolean isOffEndReversible()
   {
      return reversibleOffend;
   }
   
   /**
    * Set offEnd to OffEnd.FRONT and allow it to be treated as OffEnd.BACK for PREV/LAST queries
    */
   void allowReversibleOffEnd()
   {
      this.offEnd = OffEnd.FRONT;
      this.reversibleOffend = true;
   }
   
   /**
    * Disable OffEnd from being reversible for for PREV/LAST queries
    */
   void disableReversibleOffEnd()
   {
      this.reversibleOffend = false;
   }
}