TemporaryBuffer.java

/*
** Module   : TemporaryBuffer.java
** Abstract : Specialized record buffer which supports temp table semantics
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060125   @24356 Created initial version. Extends RecordBuffer
**                           to provide temp table semantics.
** 002 ECF 20060414   @25545 Added clear() method. Removes all records
**                           from a virtual temp table.
** 003 ECF 20060504   @26008 Added runtime support for the creation and
**                           dropping of secondary temp tables. These are
**                           tables created to support the extent fields
**                           of a primary temp table.
** 004 GES 20060517   @26201 Match new Finalizable interface requirement.
** 005 ECF 20060608   @27002 Use updated signature for Persistence.list().
** 006 ECF 20060621   @27508 Fixed temp table cleanup. Ensure that
**                           dropping a table or removing records as part
**                           of multiplex scope closure is performed
**                           within a committed database transaction. Also
**                           added a per-context reference count facility
**                           for temp tables.
** 007 ECF 20060627   @27594 Fixed temp table creation and buffer scope
**                           opening. Temp table creation must occur
**                           within a transaction. The superclass'
**                           implementation of openScope() is invoked from
**                           the overriding method.
** 008 ECF 20060713   @28047 Use new DBUtils class.
** 009 ECF 20060719   @28103 Use updated signature for Persistence.list().
** 010 ECF 20060914   @29596 Implemented master/slave buffers. These are
**                           linked to one another by a common multiplex
**                           ID.
** 011 ECF 20060919   @29699 Override the parent's implementations of
**                           processChange(), incrementDMOUseCount(),
**                           decrementDMOUseCount(), commit(), and
**                           rollback(). The first three disable the
**                           parent class' aggressive record eviction
**                           policy for temp tables, and the latter two
**                           disable subtransaction-level undo support for
**                           temp tables. NOTE: some level of undo support
**                           may need to be implemented eventually.
** 012 ECF 20061010   @30312 Added support for undoable temp tables. By
**                           default, temp tables are not undoable,
**                           however, this setting can be overridden by
**                           using a new variant of the static define()
**                           method.
** 013 ECF 20061019   @30545 Fixed NPE in copyAllRows(). Must not iterate
**                           if no source records are returned by the list
**                           query.
** 014 ECF 20061116   @31238 Fixed shared buffers and slave buffers.
**                           Tables were not being cleaned up properly in
**                           certain situations.
** 015 ECF 20061121   @31346 Fixed commit problem. Force ChangeBroker to
**                           report a pending flush after persisting or
**                           deleting records.
** 016 ECF 20061211   @31657 Make temp-tables undoable by default. The
**                           no-undo variant requires a separate option to
**                           be declared in Progress and will convert to
**                           special variant of the define() method, so
**                           undoability is the more appropriate default.
** 017 ECF 20070130   @32043 Various cleanup. Introduced methods for table
**                           counting by context. Fixed invocations of
**                           superclass methods to use new signatures.
** 018 ECF 20070202   @32072 Fixed regression in copyAllRows(). Recent
**                           changes to bulk copy implementation had
**                           broken this implementation.
** 019 ECF 20070326   @32612 Fixed associate() method and improved
**                           removeRecords() implementation. The former
**                           required a conditional flush before copying
**                           input records. The latter now uses a bulk
**                           delete instead of a looping delete.
** 020 ECF 20070329   @32649 Fixed shared temp-table support. Added static
**                           method useShared() to access a previously
**                           established, shared temp-table definition.
** 021 ECF 20070412   @32922 Override parent's setPinnedLockType() method.
**                           Does nothing, since locking is a no-op for
**                           temp tables.
** 022 ECF 20070415   @33012 Fixed define() variant which creates a slave
**                           buffer. In some cases, master buffer was not
**                           being determined correctly, resulting in an
**                           NPE.
** 023 ECF 20070416   @33030 Integrated user interrupt handling.
** 024 ECF 20070508   @33463 Fixed associate() to properly handle a
**                           validation exception. New requirement based
**                           on a change to RecordBuffer.flush().
** 025 ECF 20070601   @33919 Fixed memory leak. Buffers were not being
**                           removed from context's domains map when the
**                           buffer was no longer needed, which pinned the
**                           instances in context local memory. Changed
**                           implementation of domains map to be a weak
**                           hash map.
** 026 ECF 20070602   @33927 Implemented deferred table drop optimization.
**                           Temp tables created for the current database
**                           session are not dropped until the session is
**                           being closed. This prevents a condition where
**                           the database repeatedly creates and drops the
**                           same tables as a secondary program which
**                           creates TemporaryBuffers and allows them to
**                           go out of scope is invoked within a loop of
**                           another program.
** 027 EVL 20070609   @34005 Adding explicit import of the class
**                           org.hibernate.type.Type to eliminate conflict
**                           with the same class from java.lang.reflect
**                           package to be able to compile for Java 6.
** 028 ECF 20070706   @34405 Added deregisteredSessionListener() method.
**                           Required by change to SessionListener
**                           interface. Also replaced StringBuffer with
**                           StringBuilder.
** 029 ECF 20070715   @34856 Fixed removeRecords(). Release current buffer
**                           record after bulk delete.
** 030 ECF 20070827   @34994 Added bulk delete and optimized multiplexing.
**                           Override parent's implementation of delete()
**                           variant which accepts a where clause and
**                           query substitution parms to perform a bulk
**                           delete. Omit multiplex ID from where clauses
**                           when possible.
** 031 ECF 20070914   @35071 Fixed regression introduced by H030 (@34994).
**                           When removing records during the closure of a
**                           multiplex scope, we have to ensure only those
**                           records associated with the multiplex ID of
**                           the closing scope are removed from the temp
**                           table.
** 032 ECF 20070914   @35294 Fixed ConnectionManager notifications. We
**                           override the parent's implementation of two
**                           protected methods which notify the connection
**                           manager of buffer open and close events to do
**                           nothing, because we don't want to trace these
**                           for temp tables. Replaced database name
**                           string with database information object.
** 033 ECF 20071002   @35332 Exposed certain methods as public. Expanded
**                           use of generics.
** 034 ECF 20071018   @35592 Integrated PersistenceFactory.
** 035 ECF 20071108   @35909 Made nextMPID access threadsafe.
** 036 ECF 20071130   @36126 Integrated generics.
** 037 ECF 20071220   @36548 Modified Context.sessionClosing(). We now do
**                           more state cleanup when the session is
**                           closing due to an error.
** 038 SVL 20071231   @36628 Next primary key generation is carried out
**                           to the separate function which overrides the
**                           one in RecordBuffer.
** 039 ECF 20080102   @36638 Replaced use of deprecated nextPrimaryKey()
**                           method in Persistence. Primary key generation
**                           is now managed within Context inner class.
** 040 ECF 20080103   @36696 Fixed regression caused by #039 (@36638).
**                           Used wrong buffer to call nextPrimaryKey()
**                           from copyAllRows().
** 041 ECF 20080108   @36732 Fixed bulk delete. Changed call to delete()
**                           method in Persistence.
** 042 ECF 20080310   @37487 Fixed potential memory leak. copyAllRows()
**                           could leave DMOs in the Hibernate session if
**                           they were never loaded into a buffer. Also
**                           integrated DDL to drop indexes and to sweep
**                           index names in create and drop statements.
** 043 CA  20080430   @38161 Changes due to the TempTableHelper.sql*Drop
**                           /Create method signature change: instead of
**                           a Persistence object, the first parameter is
**                           a Database object.
** 044 SVL 20080516   @38292 Some superclass methods were overridden to
**                           no-ops to reduce memory consumption.
** 045 CA  20080530   @38458 Temporary tables passed as parameters to
**                           external procedures must match using each
**                           field's position and type, not its name;
**                           PROGRESS will not care if field names are
**                           different. Ref: #38457
** 046 ECF 20080602   @38615 Override getIndexedProperties(boolean) to do
**                           nothing. This method supports dirty checking,
**                           which is not needed for temp tables.
** 047 ECF 20080609   @38654 Backed out #046 (@38615) which regressed
**                           unique constraint validation. Removed
**                           getIndexProperties(boolean) method and added
**                           postValidateDirtyShare(), which overrides the
**                           parent's method to do nothing.
** 048 CA  20080605   @38570 The multiplex ID must start from 1.
**                           Implemented reversible bulk delete action
**                           for temporary buffers.
** 049 ECF 20080708   @39075 Fixed copyAllRows(). When copying from one
**                           table type to another, properties which are
**                           not available via a DMO's API must be
**                           skipped (i.e., computed columns).
** 050 ECF 20080723   @39172 Fixed flushing behavior for temp-tables with
**                           no indexes. Updates to such tables must be
**                           flushed to the database immediately.
** 051 ECF 20080806   @39329 Added rowid support. New primary keys are
**                           64-bit integers.
** 052 CA  20080808   @39360 For no-undo tables, the bulk delete will 
**                           always force removal of the records. Also,
**                           a reversible is registered with so the delete
**                           will be re-executed if db tx is rolled back.
** 053 ECF 20080815   @39558 Support global DMO change events.
** 054 CA  20080815   @39468 Support API change in DBUtils.
** 055 SIY 20080904   @39720 Inferred generics in buffer creation methods.
** 056 ECF 20080925   @39950 Refactored Reversible concrete classes.
** 057 ECF 20080926   @39954 Fixed ReversibleBulkDelete.setDMO(). Should
**                           not throw UnsupportedOperationException.
** 058 ECF 20081009   @40083 Integrated HQLPreprocessor API change.
** 059 ECF 20081022   @40212 Modified to adjust for Reversible API change.
** 060 ECF 20081127   @40710 Implement reclamation of temp table primary
**                           keys. On delete, a record's primary key is
**                           collected for possible recycling. On full
**                           transaction commit/rollback, pending keys are
**                           made available for re-use, or discarded,
**                           respectively. Currently, keys surrendered by
**                           bulk deletes and by rolled back record
**                           creations are not reclaimed.
** 061 ECF 20081128   @40718 Fixed regression introduced by #060 (@40710).
**                           Data structures for recycling primary keys
**                           were not being managed by table properly.
** 062 ECF 20081201   @40734 Modified primary key reclamation strategy.
**                           We now piggyback off the implementation in
**                           the superclass, so that surrendered keys are
**                           collected during the buffer's commit() and
**                           rollback() operations. This fixes yet another
**                           regression caused by the previous two mods.
** 063 ECF 20081202   @40744 Fixed regression in key reclamation. Must
**                           clear key reclamation data structures when
**                           the session is closed to avoid unique
**                           constraint violations in following session.
** 064 ECF 20081211   @40871 Improved logging in temp table creation code.
** 065 ECF 20090107   @41025 Fixed record release during bulk delete. A
**                           bulk delete event is now broadcast, which
**                           allows listening buffers to release their
**                           current records if they were deleted.
** 066 ECF 20090112   @41164 Fixed undo-related locking behavior. Locks
**                           were being downgraded rather than released in
**                           certain transaction-boundary cases. The
**                           change necessary in this class was to
**                           override certain lock-related methods from
**                           the superclass to provide no-op behavior.
** 067 ECF 20090218   @41334 Added safety code for dealing with maps of
**                           Reversibles. BufferManager methods which
**                           return these maps can now return null.
** 068 ECF 20090227   @41446 Attempted to address memory leak. Remove all
**                           Reversibles from BufferManager when the
**                           associated virtual table is no longer in use.
** 069 GES 20090424   @41943 Import change.
** 070 ECF 20090508   @42128 Added inner class TempValidationHelper. Extends
**                           RecordBuffer's implementation to override certain
**                           methods.
** 071 ECF 20090610   @42634 Optimized copyAllRows(). Call BufferManager's
**                           evictDMOIfUnused() method instead of incrementing
**                           then decrementing DMO use counts.
** 072 ECF 20090701   @43223 Various fixes and optimizations. Changed
**                           copyAllRows() method to use scrollable results
**                           instead of a results list. Added table empty
**                           tracking. Removed immediate flush concept.
**                           Refactored initialization to be lazy, upon open
**                           scope event.
** 073 ECF 20090717   @43230 Fixed regressions in #072 (@43223). Opening a
**                           multiplex scope was unconditionally setting the
**                           emptyTempTables flag for that multiplex ID, but
**                           it must only do so if the reference count is 1.
**                           When closing the last scope using a physical temp
**                           table, we must remove all the rows before the
**                           table is dropped, otherwise we can get into
**                           trouble flushing deferred rollbacks.
** 074 ECF 20090724   @43423 Added getMasterBuffer() method. This is needed to
**                           support proper rollback handling of shared temp
**                           tables.
** 075 ECF 20090729   @43450 Fixed copyAllRows(). Call to ChangeBroker to
**                           force a pending flush must come before the micro
**                           transaction commit, otherwise the copied records
**                           are lost. Also implemented no-op validate() call
**                           for Context, which implements Commitable.
** 076 ECF 20090810   @43591 Changed to accommodate SessionListener API
**                           change. sessionClosing() method was replaced with
**                           sessionEvent().
** 077 CA  20090916   @43916 Fixed bulk delete for no-undo temp tables. On
**                           commit, do not flush the bulk deletes for no-undo 
**                           temp tables, as this are relevant only for
**                           undoable tables.
** 078 CA  20091001   @44061 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).
** 079 CA  20091202   @44469 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.
** 080 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).
** 081 SVL 20130223          Added createDynamicTable and associate(TableParameter ...).
** 082 VMN 20130407          Method copyAllRows() changes related with runtime support for
**                           BUFFER-COPY (KW_BUF_COPY)
** 083 CA  20130610          Added APIs to populate or access temp table data using a result set.
** 084 SVL 20130822          Added openFreeScope, dropTable and DYNAMIC attribute.
** 085 CA  20131015          Added registration/deregistration of legacy names for a temp-table.
** 086 ECF 20131029          Import change and some methods made public for lock metadata support.
** 087 SVL 20131028          Added openScopeAt(), added legacyName and blockDepth parameter to
**                           constructors and define(), added tableHandle(), refactored multiplex
**                           scope closing.
** 088 CA  20131209          Added no-op deleted() method, required by the changes in Finalizable
**                           interface.
** 089 SVL 20140106          Changes related to copyAllRows.
** 090 SVL 20140123          Added support for extents in copyAllRows.
** 091 SVL 20140124          getResource is used for unwrapping handles.
** 092 SVL 20140205          copyAllRows supports looseCopy and skipUniqueConflicts modes.
** 093 SVL 20140210          Use HQLExpression instead of HQL string.
** 094 CA  20140214          Fixed readAllRows - use the actual DMO, not the DMO proxy.
** 095 SVL 20140221          OutputTableHandleCopier was made external; changes in
**                           createDynamicTable.
** 096 ECF 20140305          Removed explicit transaction processing from dropAllTables. Replaced
**                           Apache commons logging with J2SE logging. Adapted for API changes in
**                           RecordBuffer for copy operations.
** 097 OM  20140417          Dynamic temp-table buffers are directly available at the moment they 
**                           are declared instead of waiting for next block to open.
** 098 SVL 20140414          Use outputParameter instead of outputDMO + outputAppend in order to
**                           support remote parameters.
** 099 CA  20140513          Added a weight for the context-local var, to ensure predetermined 
**                           order during context reset.
** 100 ECF 20140613          Fixed tableHandle to force the assignment regardless of pending error
**                           status; simplified tempTableResource.
** 101 SVL 20140709          insertAllRows was properly implemented. Other changes related to
**                           table parameters.
** 102 SVL 20140725          Fixed depth at which a buffer was opened in
**                           createDynamicBufferForTempTable. Added an overloaded version of
**                           TemporaryBuffer.define function.
** 103 ECF 20140814          Replaced RecordBuffer.flushAll with flush in copyAllRows method.
**                           Added extra-transaction validation for no-undo temp-tables.
** 104 ECF 20140920          Changed to accommodate memory fixes in DynamicTablesHelper. Adjusted
**                           openScope, openScopeAt, createTable to properly map table information
**                           for DMOs shared across dynamic temp-tables.
** 105 OM  20141003          COPY and COMPARE buffer operations use legacy names for pairing
**                           fields to be copied.
** 106 ECF 20141118          Fixed NPE in openMultiplexScope.
** 107 OM  20150107          Avoid double call of super.stateChanged(event) for INSERT events in
**                           stateChanged() method. Added stack trace when detected database synch
**                           is lost and documented event.
** 108 ECF 20150302          Registration of NoUndoValidator objects was too aggressive; only
**                           register them when buffer scopes explicitly are opened. Prevent commit
**                           or rollback when StopConditionException is thrown during copyAllRows.
** 109 OM  20150303          Added Dynamic Temp Buffers to HandleChain.
** 110 ECF 20150324          API change in TableMapper.
** 111 ECF 20150326          Added optional logging when dropping tables.
** 112 OM  20150504          Make sure the buffer is released when calling removeRecords().
**                           Removed warning about incorrect caching.
** 113 ECF 20150516          Changed readAllRows to sort by primary index (defaulting to primary
**                           key if no primary index is defined). Fixed dynamic resource cleanup.
** 114 ECF 20150520          Fixed a memory leak with static temp tables.
** 115 ECF 20150810          Minimize use of context-local lookups to improve performance.
** 116 ECF 20150910          Added getDynamicName to return name assigned with TEMP-TABLE-PREPARE.
** 117 ECF 20150929          Adjusted calls to modified BufferManager.evictDMOIfUnused method.
** 118 OM  20151026          Added support for writable UNDO attribute through UndoStateProvider.
** 119 GES 20151203          Javadoc fix.
** 120 CA  20151219          Fixed conversion of shared buffers.  Fixed runtime of global
**                           temp-tables.  Added getAllSlaveBuffers().
** 121 ECF 20160225          Changes required by new PropertyHelper implementation.
** 122 ECF 20160320          Fixed regression in copyAllRows and streamlined the copy to use a
**                           simple Undoable.assign in the event the source and target buffers
**                           have the same backing DMO implementation class. Fixed memory leak
**                           in readAllRows and copyAllRows.
** 123 OM  20160316          Changed signature of listDirtyIndexes() to match super method.
**     ECF 20160430          Performance improvement: avoid database access when we know table is
**                           empty in record copy and removal methods. Bypass Hibernate and use
**                           native SQL where possible, to avoid unnecessary overhead.
** 124 ECF 20160512          Optimized removeRecords to use SQL on update when no HQL where
**                           is provided. Modified getPrimaryOrderByClause to deal with null
**                           primary index. Migrated more HQL to SQL.
** 125 ECF 20160603          Minor performance fixes.
** 126 ECF 20160613          Fix removeRecords regression from #125. Fixed ReversibleBulkDelete
**                           for no-undo temp-tables.
** 127 OM  20160629          HQLPreprocessor needs the list of fields of the current index.
** 128 ECF 20160720          Fixed buffer lookup during HQL preprocessing.
** 129 CA  20160728          Expose the temp-table global state, to be available for undoable
**                           support.
** 130 ECF 20160822          Avoid deregistering multiplexer in global scope.
** 131 ECF 20160829          Performance enhancements. Use local BufferManager to check
**                           transaction status instead of TransactionManager. Session flushing
**                           improvements to allow better batching of database updates.
** 132 ECF 20161004          Use implicit transactions where possible to reduce flushing.
** 133 ECF 20171028          Refactored readAllRows to make row handling flexible.
**     CA  20171030          Added support for BY-REFERENCE table parameter mode.
**                           Added support for REFERENCE-ONLY temp-table option.
** 134 GES 20171207          Removed forced version of assign().
**     ECF 20171213          Added query substitution parameter error handling to bulk delete.
** 135 OM  20171212          HQLPreprocessor.get() signature change. Added delete() variant with
**                           support for external buffers for nested selects.
**     ECF 20171217          Enable DMO alias qualifier to be forced in certain case in
**                           removedRecords method.
** 136 ECF 20180512          Fixed empty table check in removeRecords.
** 137 ECF 20180609          Reduce excessive transaction commits.
** 138 CA  20180525          Fixed removeRecords - alias was being set twice.
** 139 ECF 20181221          Adjust for inlined substitution parameters in removeRecords.
** 140 OM  20190129          Added ref-counting for oo fields.
**     CA  20190311          A buffer may be active as the source for multiple output table 
**                           parameters.
**     ECF 20190313          Minor optimizations.
**     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.
** 141 SVL 20190614          Support for CLOB code pages.
** 142 CA  20190628          blob and clob fields must be read, before sending them to the remote
**                           side (for appserver calls).
** 143 OM  20190510          Added error management in getDefaultBuffer().
**     OM  20190523          Unified parameter options handling for TempTables and DataSet params.
**     OM  20190611          Added support for BEFORE/AFTER buffers.
** 144 ECF 20190714          Added support for aggressive buffer flushing mode.
**     SBI 20190807          Changed removeRecords to fix generated sql code.
**     ECF 20190813          Change to RecordBuffer.release API.
** 145 CA  20190722          Fixed a case of BY-REFERENCE table argument.
**     CA  20190723          OUTPUT TABLE-HANDLE argument must get a copy of a TABLE parameter.
**     CA  20190726          tableHandle() must look at the master buf, if is set. other created 
**                           buffers will not have the tempTableRef set. 
**     CA  20190728          For BY-REFERENCE/REFERENCE-ONLY transfer the buffer's record, too.
**     EVL 20190729          Fix from CA to skip same reference processing in associate() method.
**     CA  20190812          Wrap the DMO proxy in a special proxy which allows runtime change
**                           of the DMO; this allows to make the instance mutable, and provides
**                           support for BY-REFERENCE and REFERENCE-ONLY options.
**     CA  20190816          Static temp-tables are now created when their default buffer is 
**                           defined. Fixed some issues with the mutable buffers - not all need 
**                           to be mutable (especially the dynamic ones created for a dynamic 
**                           temp-table).
** 146 SVL 20190801          Fixes for COLUMN-CODEPAGE, especially for shared tables.
** 147 OM  20190909          Added support for ERROR-FLAG and ERROR-STRING attributes.
**     CA  20191009          Added where and sort clause translation in case of bound buffers.
** 148 CA  20191119          Track the executing legacy class, to be able to register any buffer
**                           to its source definition class.
**                           Added support for before-table meta information, when creating a 
**                           temp-table from a remote (appserver) parameter.
**     CA  20191230          Fixed reversible bulk delete - needs the preprocessed WHERE clause, 
**                           and proper tracking of the hasWhere flag.
** 149 AIL 20200108          Prevented direct access to record buffer members.
** 150 AIL 20200113          Changed delete signature to match the override.
**     CA  20200110          More fixes for TABLE parameters and resource delete.
** 151 CA  20200323          A fix for binding a source buffer's DMO alias to the destination: this
**                           must be permanent only in case of BIND arguments.  Otherwise, the
**                           DMO alias binding is limited only for the duration of that top-level
**                           block call.
** 152 IAS 20200413          Fixed DMO instantiation on SESSION:DATE-FORMAT.
**     CA  20200427          Allow a TABLE to be read from JSON argument, in case of a remote REST call.
**     CA  20200514          Added support for SOAP web services (field are sent as BDT already, but the 
**                           'fromJson' flags remains).
** 153 ECF 20200419          Removed Hibernate dependencies.
**     CA  20200622          Small performance improvement - use IdentityHashMap if the key is java.lang.Class.
**                           Fixed removeRecords - FQL requires 'AS' in the FROM clause for DELETE.
** 154 SVL 20200913          isTemporary became public.
**     ECF 20200917          Backed out bracketing of DMO instantiation with SESSION:DATE-FORMAT use,
**                           as it no longer had an effect with recent ORM changes. Implemented forced
**                           NO-UNDO mode.
**     OM  20200919          Improved access to dmo metadata.
**     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          Index components carry multiple information to avoid map lookups for them.
**     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.
**     OM  20201001          Improved DMO manipulation performance by caching slow Property annotation access.
**     CA  20201003          Replaced Guava identity HashSet with Collections.newSetFromMap(IdentityHashMap).
**                           Use an identity HashSet where possible.
**     CA  20201005          Fixed bulkCopyAllRows and copyAllRows - H2 will now match the WHERE and ORDER BY
**                           indexes, avoiding re-sorting the fetched rows.
**     AIL 20201031          Added sorted and direct keywords for insert into select from.
**     AIL 20201118          Made extent fields bulk copy use insert into select from.
**         20201125          Added fast copy validation using DMO signature.
**         20201201          Moved fast copy implementation to FastCopyHelper. Added helper methods.
**     CA  20201202          Record change notifications are processed only if the multiplex ID matches with
**                           the event's source.
**     AIL 20201215          Added before table prop mapping to the fast-copy helper call.
**         20201216          Added support for fast loose-copy.
**         20201228          Invalidated fast-find cache after fast-copy.
**     CA  20201118          Fixed a leak related to cleaning up the multiplex scope when deleting a static
**                           temp-table part of a persistent external program.
**     OM  20201120          Fixed buffer copy methods. Fixed rejectChanges() method. Fixed management of
**                           BEFORE-BUFFER attributes. Fixed buffer/table copy.
**     OM  20201218          Fixed implementation for error and rejected attributes/hidden fields.
**     OM  20210223          Invalidate index-based cache in the event of low-level SQL operations.
**     CA  20210304          Fixed date, datetime and datetimetz literal parsing.
**     OM  20210328          Registered buffer with parent Temp-Table.
**     OM  20210404          The getCodePage() method resolve CP to CPINTERNAL instead of returning null.
**     CA  20210412          Fixed BUFFER:COPY-TEMP-TABLE in REPLACE mode (if the target has no record matching
**                           a source record, then this must not be shown as an error message - instead, use
**                           'nestedSilent' for this query).
**     OM  20210503          The components of ORDER BY clause used by readAllRows() must be property names
**                           not SQL column names. They will be converted to SQL later, in FqlToSqlConverter.
**     RFB 20210504          Temporarily undo OM changes until the regression can be determined.
**     ECF 20210504          BufferManager API name change.
**     ECF 20210506          Use RecordBuffer.getDmoInfo() getter instead of direct field access to
**                           prevent NPE in proxy case.
**     CA  20210508          Child shared buffers or temp-tables act like REFERENCE-ONLY, where they are bound
**                           to the master buffer/temp-table.
**     ECF 20210511          Replaced DynamicTablesHelper.normalizeName with TextOps.rightTrimLower.
**     CA  20210516          During BROWSE population, if the query uses other buffers than the original ones,
**                           the original buffers (only temporary and for static BROWSE only) will be bound to
**                           the query's buffers.
**     SVL 20210517          In createDynamicTable check if the source table is valid.
**     SVL 20210517          Update to the previous change in createDynamicTable.
**     OM  20210521          DELETE and CREATE return the proper logical value.
**     ECF 20210606          Use the master buffer's global setting in both useShared method variants, when
**                           creating a child shared buffer. This was previously set to true always, causing
**                           all these buffers to be stored by BufferManager in the global scope. This created
**                           a large memory leak over time, as they would never be cleaned up.
**     ECF 20210612          Deregister a buffer from the tempTableRef AbstractTempTable when it goes out of
**                           scope to prevent a memory leak.
**     CA  20210609          Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly 
**                           initialized at the method's execution, and not at the caller's arguments.
**     SVL 20210614          Remote OUTPUT TABLE/DATASET-HANDLE parameters are treated like INPUT-OUTPUT
**                           parameters with no input data rows, which allows to access table structure if
**                           non-unknown table/dataset handle has been passed from the calling side.
**     CA  20210625          If the remote table has no name, do not create it in case of OUTPUT parameters.
**     CA  20210626          Do not delete the master shared temp-table until the last of its associated
**                           buffers (i.e. child shared temp-tables) is deleted.
**     ECF 20210820          Retrofit for query substitution parameter preprocessing fixes.
**     CA  20210831          Fixed insertAllRows, the before-table must track if there records in the source
**                           table, so will not be marked as 'definitely empty'.
**     OM  20210831          Avoid NPE caused by transient peer records when before record attributes are set.
**     OM  20210904          Force saving records whose before-table attributes are set and records were NOT
**                           initially marked as CHANGED.
**     OM  20211005          Registered the de-association of table parameters with ProcedureManager instead
**                           of a finalizable in TransactionManager.
**     CA  20210929          Fixed cases when rowid function is used in the pairs for BUFFER-COPY or
**                           SAVE-ROW-CHANGES.
**     OM  20211102          Optimized DELETE does not work correctly for changes-tracked dataset tables.
**     CA  20211112          Allow the DATASET and TABLE parameters for remote SOAP calls to be transferred
**                           via XML (so that relations, schema, etc is done on the server-side).
**     CA  20211116          Fixed a NPE related to CA/2021112.
**     OM  20211209          The dynamic buffers for TempTables are opened at global level (0), same as for
**                           permanent tables.
**     OM  20211020          Added _DATASOURCE_ROWID property.
**     OM  20211217          Remove temp-table from TableMapper if its multiplex is not in use anymore.
**     CA  20211222          Fixed memory leak: remove the temp-table from the domains map, on delete;
**                           invalidate the FastFindCache for the temp-table, when its multiplex scope is
**                           closed.
**     CA  20211227          writeToXml() must return null for invalid handle.
**     CA  20220104          Fixed createRecords() to be aware of the added _DATASOURCE_ROWID property.
**     CA  20220105          readFromXml must force UTF-8 as codepage at the longchar created to hold the
**                           XML string.
**     CA  20220110          Refactored the temp-table's row-meta state to use constants for the indexes in 
**                           the array used for serialization.
**     OM  20220115          Added reminder comment for possible flaw in useShared() method.
**     CA  20220214          The slave buffers are cleaned up only when the associated temp-table gets deleted.
**     CA  20220210          Removed Commitable.rollbackPending and its notifications, as there is no actual 
**                           implementation of this method.
**     CA  20220309          The PKs are reclaimed using a (DMO, multiplexID) pair as key.
**     CA  20220318          Fixed insertAllRows, when the before-table has ROW-DELETED records (this have no
**                           correspondent in the peer table).
**     CA  20220309          If the temp-table is currently being queried, force a loop delete when emptying
**                           the table, and also force a query loop when creating the records (bypass 
**                           FastCopyHelper).
**     OM  20220327          Use the implicit primary index if none expressly defined for table iterations.
**     CA  20220329          Switched the condition in delete(DataModelObject[], String, Object...), a small
**                           performance improvement.
**     CA  20220330          Fixed a regression in CA/20220309 - nextPrimaryKey() will consume pending 
**                           reclaimed PKs until they are exhausted.
**     CA  20220627          A CREATE BUFFER statement will use the bound buffer instead of the default buffer
**                           for the temp-table.
**     OM  20220707          Shared meta-knowledge about fields and properties of a record was collected in
**                           DmoMeta to avoid duplicating their collection and storage.
**     CA  20220830          Temp-tables sent via a web-service (like REST) may not contain all the fields,
**                           and the missing fields will default to unknown.
**     VVT 20221003          CommonHandle.getResourceType() method renamed to resourceType() to prevent
**                           conflicts with namesakes in DMO. See #6694.
**     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 definitions (dynamic and static). 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
**                           Refactored TableMapper for temp-tables to keep a cache of LegacyTableInfo and
**                           the mutable state in MutableFieldInfo, for the actual TempTable instance. 
**                           Refs #6825
**                           Do not access tableHandle() in the FWD runtime, get the TempTable instance 
**                           directly.  Refs #6826
**     CA  20221007          Added a counter for the number of bindings of this buffer, when passed as a 
**                           parameter and bound to another buffer.
**     OM  20221012          The TriggerTracker are bound to DMO, not to the buffer they are contained into.
**     OM  20221019          Do not assign new PK for replaced records in copyAllRows().
**     CA  20221031          Added JMX instrumentation for TABLE[-HANDLE] parameter processing.
**     TJD 20220504          Java 11 compatibility minor changes.
**     OM  20221104          Logged mapping error in createRecords(). Avoiding possible NPE.
**     OM  20221125          Handled BINDing of REFERENCE-ONLY OUTPUT buffers.
**     AL2 20221205          Replaced AdaptiveQuery with PreselectQuery for loopDelete.
**                           Use null sort instead of empty sort for PreselectQuery/
**     OM  20221220          Simlpified TempTableBuilder.getOrderedPropertyNames() signature/implementation.
**     CA  20220524          An explicit buffer created for a temp-table must save the tempTableRef from the
**                           master.
**     GES 20210430          Reworked associate() and createDynamicTable() to remove unnecessary arguments.
**     CA  20220603          The before-table record on a DELETE event must be created regardless if the 
**                           buffer is attached to a dataset or not.  Commented the _originRowid and 
**                           _datasourceRowid setters for the before-record associated with a DELETE (needs
**                           confirmation).
**     CA  20220609          A fix for 'OUTPUT TABLE tt1 BIND' argument, where the table arg is REFERENCE-ONLY.
**     CA  20220613          registerBindingBuffer must use as procedure the SOURCE-PROCEDURE in case of
**                           OUTPUT and THIS-PROCEDURE in case of INPUT, for BIND parameters.
**     CA  20220727          Fixed OO dataset/table parameters (at the method definition and method call) when 
**                           there is an APPEND option.
**     CA  20220909          The 'doNotProxy' method array must be a constant, otherwise it will leak at the
**                           ProxyFactory$CacheKey.DO_NOT_PROXY_HASH_CODES.
**                           If a buffer is created with a dynamic master and the buffer is not dynamic, then
**                           the 'global' flag must be flase.  This fixed a Multiplexer leak in 
**                           TransactionManager$WorkArea.globalBlock.finalizables.
**     CA  20220918          Replaced 'emptyTableFlags' with a bitmap 'nonEmptyTableFlags', where only 
**                           temp-tables which have records are tracked (by their mutiplex IDs).
**                           Delete the dynamic query at copyParentUnchangedRecords.
**                           Only global or dynamic tables are registering a global dynamic multiplexer.
**                           When a temp-table is deleted or emptied, clear the ORM session of its cached 
**                           records and the reclaimed keys.
**     CA  20220919          Auto-delete a bound buffer when the definition gets deleted, if the bound buffer
**                           was created internally by FWD.
**                           When a BIND parameter association is being performed, check if the definition is
**                           REFERENCE-ONLY.
**     CA  20220920          DMO interface must be used on ReclaimTable constructor, and not buffer DMO iface.
**     VVT 20220913          CommonHandle.getResourceType() method renamed to resourceType() to prevent
**                           conflicts with namesakes in DMO. See #6694.
**     CA  20221007          Added a counter for the number of bindings of this buffer, when passed as a 
**                           parameter and bound to another buffer.
**     CA  20221014          In 'doCloseMultiplexScope', 'pruneSessionCache' must be called only if the table
**                           truly empty; otherwise, buffers may remain with records marked as 'cached' when
**                           they are not.
**     OM  20221027          Use Dialect API to create and drop sequences.
**     IAS 20221102          Added error reporting for SCHEMA-MARSHAL == NONE.
**     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
**     IAS 20221103          Fixed support for a binded table parameter.
**     CA  20221206          If a REFERENCE-ONLY table is already bound to the same target table, do not 
**                           re-bind. 
**     RAA 20230109          Changed inline statement(s) to prepared statement(s).
**     CA  20230110          'getAllSlaveBuffers' returns null if no buffers are found.
**     SVL 20230110          P2JIndex.components() provides direct access to the components array in order to
**                           improve performance.
**     AL2 20221205          Replaced AdaptiveQuery with PreselectQuery for loopDelete.
**     SVL 20230113          Improved performance by replacing some "for-each" loops with indexed "for" loops.
**     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.
**     HC  20230118          Eliminated some of the uses of String.toUpperCase and/or
**                           String.toLowerCase for performance.
**     CA  20230208          Backed out changes in 'OM  20221125          Handled BINDing of REFERENCE-ONLY 
**                           OUTPUT buffers.' as this causes regressions.
** 155 RFB 20230303          Condition under which NO-SCHEMA-MARSHAL is thrown needed to be more stringent.
**                           If a result set was received, then it must have the schema. Ref. #7145.
** 156 CA  20230215          Improved other cases to reduce BDT usage from within FWD runtime.
** 157 RAA 20230314          Re-factored some fields in removeRecords so they are now part of prepared
**                           statements rather than being hard-coded. 
** 158 RAA 20230316          Added an initial capacity to the list of additional arguments in removeRecords.
** 159 DDF 20230316          Added definitelyHasRecords().
**     DDF 20230303          Only allow computed column prefix if dialect allows so (refs: #7108).
** 160 OM  20230215          Handled collision of temporary buffer names by temporarily overriding them.
** 161 GBB 20230410          Using ErrorManager.displayError() overloaded version with less params.
** 162 SR  20230510          Matched persistent.list() new signature.
** 163 IAS 20230422          'readAllRows' returns number of processed records. 
** 164 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 165 DDF 20230523          Added looseCopy parameter to executeCopy call.
** 166 CA  20230523          In ReferenceProxy, cache the DMO property names and the relation between the
**                           definition DMO and the bound DMO property names. 
** 166 CA  20230530          Unbind an explicit buffer if the temp-table is not REFERENCE-ONLY.
**                           definition DMO and the bound DMO property names.
** 167 OM  20230529          Using loop delete when removing records in a after buffer with tracking on.
** 168 AL2 20230608          Use timerWithReturn on define() to avoid creating extra results array.
**     AL2 20230613          Use the existent buffer manager and procedure helper where possible.
** 170 DDF 20230620          Made FORCE_NO_UNDO_TEMP_TABLES non-final, renamed it.
**     DDF 20230627          Made forceNoUndoTempTables accessible through a method.
** 171 SR  20230614          Updated copyAllRows to allow fast-copy in replace mode if destination is empty.
**     SR  20230621          In copyAllRows, if simpleCopy is true looseCopy should be false.
**         20230704          Let simpleCopy be true even when the sqlSignature match.
**     RAA 20230807          Compressed FastCopyHelper usage into one function call. 
**     RAA 20230823          Made fastCopyCache contextual, meaning that it is now stored in Context.
**     RAA 20230823          When replacing a record in the destination table, its primary key is no longer
**                           changed with the one from the source record.
**     RAA 20230828          Handled potential flag changes before using the fastCopyCache.
**     RAA 20230828          Extracted local context from the source buffer instead of function call.
**     RAA 20230830          Matched the behavior of master copy for before copy when using FastCopyHelper.
**     RAA 20230904          Removed FastCopyHelper.finishCopy usage.
**     AL2 20230905          Fixed noMapping flag to be part of the helper, not the context.
**                           Removed replaceMode from fast-copy as it is not supported.
** 172 CA  20230907          Reworked the binding rules: if the buffer is not already bound (implicitly or
**                           explicitly), then the actual target will be bound to the source, while the rest
**                           of the buffers for this table (explicitly defined or the default buffer) will
**                           have a new buffer created.
** 173 OM  20230915          Added support for generic session triggers.
** 174 GBB 20231011          SecurityManager session methods calls updated.
** 175 OM  20231011          Fixed scope of attached buffers.
** 176 AL2 20231013          Removed in-memory key reclaiming. Allowed FWD-H2 to do the reclaim in-house.
** 177 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.
** 178 AL2 20231101          Set-up the fast-copy mapping tables only once, not at each copy attempt.
** 179 AL2 20231103          Made the dropMappings use the session instead of the persistence.
** 180 DDF 20230818          Generate a temporary sequence for temporary tables.
** 181 AL2 20231116          Notify the direct-access driver that a multiplex scope was closed.
** 182 AL2 20231127          loopDelete should not invalidate records while using a PreselectQuery.
** 183 CA  20231129          Moved the LegacyTableInfo instance from TableMapper to AbstractTempTable (only
**                           for temp-tables).
** 184 ES  20231203          Removed query.next() from loopDelete method forEach inner block.
** 185 OM  20240117          The unbound associated buffers are open at parent scope (-1).
** 186 AD  20240125          Optimized delete queries to remove where condition when possible.
** 187 HC  20240222          Enabled JMX on FWD Client.
** 188 CA  20240302          A BIND at the param or argummet 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.
**     CA  20240303          Mark as REFERENCE-ONLY the explicit buffers, too, for a temp-table.
**                           DATASET's buffers are set as the definition buffer, and not the outer 
**                           ReferenceProxy instance - resolve the actual bound buffer when associating a 
**                           temp-table.
** 189 CA  20240321          Avoid BDTs within internal FWD runtime.  Replaced iterators with Java 'for',  
**                           where it applies.
** 190 AL2 20240315          Improved transaction handling when flushing the buffers before copyAllRows.
** 191 ES  20240405          In createDynamicTableImpl table handle should be assigned to handle in case 
**                           of BIND option too.
** 192 ES  20240408          In case of a bind temp-table in a procedure, the table should remain bind after
**                           the execution of the procedure. 
** 193 CA  20240410          Flush the buffers in 'copyChanges' (called by BUFFER:GET-CHANGES).
** 194 CA  20240422          Savepoints are used to rollback undoable temp-tables, and no-undo support is
**                           builtin into H2 itself; so, removeRecords will always perform an actual DELETE.
** 195 CA  20240502          Fixed 'removeRecords' where the FQL was prepared with a faulty alias.
** 196 AL2 20240512          Removed bulk option from nextPrimaryKey.
** 197 AL2 20240514          Removed any usage of sequences as records have their id provided by the database.
** 198 CA  20240514          Avoid flushing both the before and after buffers in 'copyChanges', as 
**                           'flushBuffers' now will take care of flushing both tables.
** 199 SP  20240527          Avoid updating after buffer ERROR-FLAG and ERROR-STRING attributes when
**                           before buffer row state is ROW_DELETED.
**     SP  20240527          When deleting a record from the after buffer, also delete it from the before buffer
**                           if before.rowState = ROW_CREATED, regardless of TRACKING-CHANGES value.
** 200 AL2 20240618          Changed initialize signature to include fromDefine parameter.
** 201 SP  20240611          When deleting a record from the after buffer, also delete it from the before buffer
**                           if TRACKING-CHANGES is false.
** 202 ES  20240626          Avoid logging DirectedAccessExceptions in expected cases.
** 203 CA  20240627          Explicitly delete all records loaded in the buffers before emptying the temp-table,
**                           as a NEW record loaded in a buffer will have just the PK row in the temp-table.
**     CA  20240701          In 'clearBuffers', mark the before-buffer in 'setup', so the delete can happen
**                           regardless of TRACKING-CHANGES state.
**     AL2 20240808          Overridden cleanup to remove buffer from slave collection when out-of-scope.
** 204 RAA 20240808          Adjusted FastFindCache to be retrieved from the persistence context.  
**     DDF 20240808          The database should be retrieved using the existent persistence context.
** 205 SP  20240806          Moved code from define() to defineImpl() method.
**                           Skip using JMX timers when JMX_DEBUG flag is not set.
** 206 LS  20240904          Changed useShared() to set autoDeleteBinding flag to false for DEF SHARED BUFFER.
** 207 SP  20240910          Unreserve PK of newly created record in copyAllRows when validation fails.
** 208 OM  20240909          Improved Database API. Concurrent multitenancy functionality implementation.
** 209 CA  20240924          Do not destroy the L2/L3 FastFindCache(s), until the DMO gets out of scope.
**     CA  20230924          Further reduce context-local lookup.
** 210 AL2 20240726          If the associate uses the same tables, skip the associating process.
**     TJD 20240729          Removed AL2 20240726 changes due to regression 
**     TJD 20240902          forceLoopingDelete when there is any DMO in RecordNursery related to this Buffer
** 211 ES  20240919          Remove multiplexID in loopDelete.
** 212 ICP 20241126          Fixed conditions for throwing 11885 legacy error in copyAllRows.
** 213 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
**                           based on [sharedDb] parameter.
** 214 ICP 20241202          In copyAllRows, refined conditions for throwing 11885 legacy error and added
**                           error throw when the copy fails. Fixed createPropsMap to also match INT and
**                           INT64 types to match legacy behaviour.
** 215 SP  20241106          Changed 'removeRecords' to always use an alias in the prepared FQL.
** 216 AS  20250131          Added the deleted flag, indicating if a buffer was deleted as a resource.
**                           Improved slave buffers cleanup. 
** 217 ES  20250130          Override the dmoAlias of the source buffer in copyAllRows and restore it at the 
**                           end of the buffer.
** 218 DDF 20250317          Make sure all fields that are not part of an index are flushed when copying rows
**                           from the source to the destination buffer.
**     DDF 20250318          Check the RecordNursery after the buffer is flushed.
** 219 CA  20250429          'removeRecords', if there is a WHERE clause, must evict all cached records which 
**                           are being deleted.
**     LS  20250502          Removed 'doesReclaimKeys'.
** 220 ES  20250509          Added mutableBufferProxy method to create a proxy which has as super class the proxy
**                           class from previous level and no plugin.
** 221 RNC 20250520          Append the property name instead of the column name when generating the ORDER BY
**                           clause based on the primary index.
** 222 SP  20250606          Modified readAllRows and getPrimaryOrderByClause to append a table alias when
**                           generating the ORDER BY clause.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
** 
** Additional terms under GNU Affero GPL version 3 section 7:
** 
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
** 
**   0. Attribution Requirement.
** 
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
** 
**   1. No License To Use Trademarks.
** 
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
** 
**   2. No Misrepresentation of Affiliation.
** 
**     You may not represent yourself as Golden Code Development Corporation or FWD.
** 
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
** 
**   3. No Misrepresentation of Source or Origin.
** 
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist;

import org.roaringbitmap.*;
import java.lang.InstantiationException;
import java.lang.reflect.*;
import java.sql.*;
import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.function.*;
import java.util.logging.*;

import com.goldencode.cache.Cache;
import com.goldencode.p2j.*;
import com.goldencode.p2j.convert.*;
import com.goldencode.p2j.directory.Base64;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.persist.FastCopyHelper.FastCopyContext;
import com.goldencode.p2j.persist.FastCopyHelper.FastCopyKey;
import com.goldencode.p2j.persist.TableMapper.LegacyFieldInfo;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.orm.types.*;
import com.goldencode.p2j.persist.trigger.DatabaseEventType;
import com.goldencode.p2j.security.*;
import com.goldencode.p2j.security.SecurityManager;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.goldencode.proxy.ProxyFactory;
import com.goldencode.util.*;

/**
 * A specialized record buffer implementation which uses a temporary table
 * as backing storage and supports Progress' temp/work-table semantics.  A
 * new instance is created by invoking the static {@link #define} method,
 * which, like the superclass' method of the same name, creates a proxy
 * object which client code uses to interact with the database.
 * <p>
 * <b>Implementation Notes</b><br>
 * <b>Multiplexing.</b>  Because of limitations imposed by Hibernate's lack
 * of direct support for temp tables, a single temp table instance in the
 * backing database is multiplexed if multiple buffers of the same type of
 * DMO class are opened simultaneously in the same context.  This can happen
 * either when calling code explicitly defines multiple temp table buffers
 * using the same DMO type, or when method calls pass a temp table DMO proxy
 * as a parameter.  Multiplexing is used to enable Progress' semantic of
 * copying data between temp tables when they are passed as parameters to
 * nested procedure calls.
 * <p>
 * Multiplexing is achieved by adding a multiplex ID column to every temp
 * table schema.  Each buffer instance is assigned a unique multiplex ID the
 * first time a scope is opened on it.  Every access to the backing temp
 * table (i.e., queries, updates, inserts, deletes) take the multiplex ID
 * into account, such that the backing table is managed as multiple, virtual
 * table instances, one per distinct multiplex ID.
 * <p>
 * A temp table is created (along with its indexes, if any) the first time a
 * particular DMO type is accessed.  It is only dropped when no buffers
 * reference it.  As buffers go in and out of scope, a context local "in-use"
 * map is updated to reflect which buffers reference which physical temp
 * tables.  When a buffer goes out of scope, cleanup processing checks
 * whether it is the last buffer to reference its backing table.  If so, the
 * table is dropped.  If not, the table is not dropped, but all records with
 * that buffer's multiplex ID are removed from the table.
 * <p>
 * Temporary buffers may be scoped globally, or to the top-level (i.e.,
 * method) scope.  If global, cleanup is performed only when the context
 * itself is about to go out of scope.  If not global, cleanup is performed
 * when the method scope is popped by the transaction manager.  The latter
 * coincides with the external procedure scope in Progress.
 * <p>
 * There is a notion of a <i>master</i> buffer and <i>slave</i> buffers.  The
 * former is created as the result of converting a DEFINE TEMP-TABLE Progress
 * statement.  The latter is created as the result of converting a DEFINE
 * BUFFER FOR <i>temp-table</i> Progress statement.  A master buffer and all
 * of the associated slave buffers share the same multiplex ID, since these
 * buffers must all point to the same backing, virtual temp table for their
 * backing data.  The first time any buffer in such a group has a multiplex
 * ID assigned, all buffers in the group have their multiplex IDs assigned to
 * the same value.
 */
public final class TemporaryBuffer
extends RecordBuffer
{
   /** Name of the multiplex ID field in temp table DMO classes */
   public static final String MULTIPLEX_FIELD_NAME = "_multiplex";
   
   /** Error text for error 12324. */
   public static final String ERROR_MISMATCHED_FIELDS = "Remote mismatched fields or " +
      "mismatched BEFORE-TABLE attribute for temp-table parameter or dataset member parameter";
   
   /** Error text template for error 8030. */
   public static final String ERROR_TABLE_MISMATCH_TEMPLATE = "Parameter number %d (table %s) " +
      "mismatch. Has %d fields - client schema has %d fields";
   
   /** Error text template for error 8029. */
   public static final String ERROR_EXTENT_MISMATCH_TEMPLATE = "Table %s mismatch: The extent " +
      "number of field %d does not match the local definition";
   
   /** Error text template for error 8028. */
   public static final String ERROR_FIELD_TYPE_MISMATCH_TEMPLATE = "Table %s  mismatch: The " +
      "client data type of field %d does not match the definition on the AppServer";
   
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(TemporaryBuffer.class.getName());

   /** Instrumentation for {@link #define}. */
   private static final NanoTimer DEF_BUFFER = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmBufferDefineTemp);

   /** Instrumentation for {@link #createDynamicBufferForTempTable}. */
   private static final NanoTimer CREATE_BUFFER = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmCreateDynamicBuffer);
   
   /** Instrumentation for {@link #associate}. */
   private static final NanoTimer STATIC_PARAM = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmTempTableParam);
   
   /** Instrumentation for {@link #createDynamicTable}. */
   private static final NanoTimer DYN_PARAM = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmDynamicTableParam);

   /** Multiplex ID sets currently in use for this context by DMO interface */
   private static final ContextLocal<Context> context = new ContextLocal<Context>()
   {
      @Override
      public WeightFactor getWeight() { return WeightFactor.LEVEL_2; }
      protected Context initialValue() { return (new Context()); }
   };
   
   /** Names of tables which are not NO-UNDO when forcing NO-UNDO temp-table behavior globally */
   private static final Set<String> forceNoUndoConflicts = new HashSet<>();
   
   /** Does this buffer represent a global temp table? */
   private boolean global = false;
   
   /** Can property changes on this buffer be undone? */
   private UndoStateProvider undoStateProvider;
   
   /** Context associated with this buffer instance */
   private final Context local;
   
   /** Constructor used to create new instances of the backing DMO */
   private Constructor<? extends Record> dmoCtor = null;
   
   /** SQL used to check whether a record was deleted by a bulk delete */
   private String bulkDeleteEventSQL = null;
   
   /** Destination table parameters for output copy if necessary before scope closes */
   private Set<TableParameter> outputParameters = null;
   
   /** Multiplex ID assigned to this buffer instance */
   private Integer multiplexID = null;
   
   /** Globally-scoped multiplexer for dynamic temp-tables ({@code null} for others) */
   private Multiplexer dynamicMultiplexer = null;
   
   /** Master buffer for this buffer;  null indicates this is the master */
   private TemporaryBuffer master = null;
   
   /** Reference to parent temp table; if master buffer exists this field is <code>null</code> */
   private TempTable tempTableRef = null;
   
   /** Order by clause associated with primary index of backing temp-table, if any */
   private String primaryOrderBy = null;
   
   /** Flag indicating if REFERENCE-ONLY option was set for this TEMP-TABLE. */
   private boolean referenceOnly = false;
   
   /**
    * Map of code pages of the CLOB fields, which were declared with COLUMN-CODEPAGE option.
    * Code pages will be evaluated and applied in {@link #openScope()}. Keyed by hibernate
    * property names of the CLOB fields.
    */
   private Map<String, Supplier> registeredCodePages = null;

   /**
    * The handler used to bound to another instance at runtime.  This is saved only for the 
    * instance created when the buffer was made mutable, at the external program/legacy OO class 
    * instantiation.
    */
   private ReferenceProxy mutableHandler;
   
   /** The actual proxy instance wrapping the DMO proxy. */
   private Temporary mutableProxy;
   
   /**
    * For a master buffer, all explicit buffers created for it.
    * TODO: I think there is a memory leak hear, needs to be reviewed.
    */
   private final Set<Temporary> explicitBuffers = Collections.newSetFromMap(new IdentityHashMap<>());
   
   /** 
    * Flag indicating that the {@link #loopDelete} is being executed, so the PKs can be reclaimed for the 
    * record delete. 
    */
   private boolean loopingDelete = false;

   /** Flag indicating is the buffer was deleted as a resource. */
   private boolean deleted = false;
   
   /**
    * Constructor. This class is only instantiated via the {@link #define}
    * factory method.  The implementation class associated with the given
    * <code>dmoIface</code> must have a constructor which accepts a multiplex
    * ID and must contain a <code>_multiplex</code> instance variable of type
    * <code>Integer</code>.
    *
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's API.  This interface MUST extend
    *          {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer is associated
    *          in the transaction manager whenever it is registered.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          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.
    */
   private <T extends TempTableBuffer> TemporaryBuffer(Class<?> def,
                                                       Class<T> dmoBufIface,
                                                       String variable,
                                                       boolean global,
                                                       UndoStateProvider undoable,
                                                       int blockDepth)
   {
      super(def, dmoBufIface, DatabaseManager.TEMP_TABLE_SCHEMA, variable, blockDepth);
      
      this.global = global;
      this.undoStateProvider = undoable;
      this.local = context.get();
   }
   
   /**
    * Defer error that has occurred during {@link #copyAllRows} processing.
    */
   public static void deferCopyError()
   {
      TransactionManager.deferError("copying", new ErrorConditionException("Copy all rows error"), true);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Type of constructed instance.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<T> dmoBufIface,
                                                      String variable,
                                                      boolean global)
   {
      return define(dmoBufIface, variable, null, global);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Type of constructed instance.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<T> dmoBufIface,
                                                      String variable,
                                                      String legacyName,
                                                      boolean global)
   {
      return define(null, dmoBufIface, variable, legacyName, global);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<?> def,
                                                      Class<T> dmoBufIface,
                                                      String variable,
                                                      String legacyName,
                                                      boolean global)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         return DEF_BUFFER.timerWithReturn(() -> defineImpl(def, dmoBufIface, variable, legacyName, global));
      }
      else
      {
         return defineImpl(def, dmoBufIface, variable, legacyName, global);
      }
   }

   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from
    *          <code>Temporary</code>.
    */
   private static <T extends TempTableBuffer> T defineImpl(Class<?> def,
                                                           Class<T> dmoBufIface,
                                                           String variable,
                                                           String legacyName,
                                                           boolean global)
   {
      TemporaryBuffer buffer = makeBuffer(def, dmoBufIface, variable, global, new DefaultUndoState(true));

      Map<TemporaryBuffer, Object> domain = new WeakHashMap<>();
      domain.put(buffer, null);
      Map<TemporaryBuffer, Map<TemporaryBuffer, Object>> domains = buffer.local.domains;
      domains.put(buffer, domain);

      return makeMutable(dmoBufIface, createProxy(dmoBufIface, buffer, legacyName));
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Type of constructed instance.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          buffer.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<T> dmoBufIface,
                                                      String variable,
                                                      boolean global,
                                                      UndoStateProvider undoable)
   {
      return define(dmoBufIface, variable, null, global, undoable);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Type of constructed instance.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          <code>true</code> if changes made to records managed by this
    *          buffer can be undone;  <code>false</code> if such changes are
    *          committed immediately, such that they cannot be undone.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<T> dmoBufIface,
                                                      String variable,
                                                      boolean global,
                                                      boolean undoable)
   {
      return define(dmoBufIface, variable, null, global, new DefaultUndoState(undoable));
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Type of constructed instance.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          buffer.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<T> dmoBufIface,
                                                      String variable,
                                                      String legacyName,
                                                      boolean global,
                                                      UndoStateProvider undoable)
   {
      return define(dmoBufIface, variable, legacyName, global, undoable, -1);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Type of constructed instance.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          <code>true</code> if changes made to records managed by this
    *          buffer can be undone;  <code>false</code> if such changes are
    *          committed immediately, such that they cannot be undone.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<T> dmoBufIface,
                                                      String variable,
                                                      String legacyName,
                                                      boolean global,
                                                      boolean undoable)
   {
      return define(dmoBufIface, variable, legacyName, global, new DefaultUndoState(undoable), -1);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    * 
    * @param   <T> 
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          <code>true</code> if changes made to records managed by this
    *          buffer can be undone;  <code>false</code> if such changes are
    *          committed immediately, such that they cannot be undone.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<?> def,
                                                      Class<T> dmoBufIface,
                                                      String variable,
                                                      String legacyName,
                                                      boolean global,
                                                      boolean undoable)
   {
      return define(def, dmoBufIface, variable, legacyName, global, new DefaultUndoState(undoable), -1);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The buffer is permanently
    * associated with the temp table database. It will be registered with the transaction manager
    * under the specified variable name.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a sub-interface of
    *          {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be associated in the
    *          transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          {@code true} if the buffer survives until end of context life;  {@code false} if it
    *          expires upon leaving the top level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          buffer.
    * @param   blockDepth
    *          Zero-based depth of the block (starting from the outermost scope) at which the
    *          buffer is defined or {@code -1} for the current scope.
    *
    * @return  An object which implements the specified interface, but which initially has no
    *          backing data record.
    *
    * @throws  IllegalArgumentException
    *          if {@code dmoIface} is not assignable from {@code Temporary}.
    */
   public static <T extends TempTableBuffer> T define(Class<T> dmoBufIface,
                                                      String variable,
                                                      String legacyName,
                                                      boolean global,
                                                      UndoStateProvider undoable,
                                                      int blockDepth)
   {
      return define(null, dmoBufIface, variable, legacyName, global, undoable, blockDepth, true);
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          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.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<?> def,
                                                      Class<T> dmoBufIface,
                                                      String variable,
                                                      String legacyName,
                                                      boolean global,
                                                      UndoStateProvider undoable,
                                                      int blockDepth)
   {
      return define(def, dmoBufIface, variable, legacyName, global, undoable, blockDepth, true);
   }

   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          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.
    * @param   mutable
    *          Flag indicating if the buffer must be mutable.
    *          
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from
    *          <code>Temporary</code>.
    */
   static <T extends TempTableBuffer> T define(Class<?> def,
                                               Class<T> dmoBufIface,
                                               String variable,
                                               String legacyName,
                                               boolean global,
                                               UndoStateProvider undoable,
                                               int blockDepth,
                                               boolean mutable)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         return DEF_BUFFER.timerWithReturn(
            () -> defineImpl(def, dmoBufIface, variable, legacyName, global, undoable, blockDepth, mutable));
      }
      else
      {
         return defineImpl(def, dmoBufIface, variable, legacyName, global, undoable, blockDepth, mutable);
      }
   }

   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          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.
    * @param   mutable
    *          Flag indicating if the buffer must be mutable.
    *
    * @return  An object which implements the specified interface, but which
    *          initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from
    *          <code>Temporary</code>.
    */
   private static <T extends TempTableBuffer> T defineImpl(Class<?> def,
                                                           Class<T> dmoBufIface,
                                                           String variable,
                                                           String legacyName,
                                                           boolean global,
                                                           UndoStateProvider undoable,
                                                           int blockDepth,
                                                           boolean mutable)
   {
      TemporaryBuffer buffer = makeBuffer(def, dmoBufIface, variable, global, undoable, blockDepth);

      Map<TemporaryBuffer, Object> domain = new WeakHashMap<>();
      domain.put(buffer, null);
      Map<TemporaryBuffer, Map<TemporaryBuffer, Object>> domains = buffer.local.domains;
      domains.put(buffer, domain);

      T proxy = createProxy(dmoBufIface, buffer, legacyName);

      return mutable ? makeMutable(dmoBufIface, proxy) : proxy;
   }
   
   /**
    * Define a new buffer which is based upon the specified interface. The
    * buffer is permanently associated with the temp table database. It will
    * be registered with the transaction manager under the specified variable
    * name.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          sub-interface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          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.
    *
    * @throws  IllegalArgumentException
    *          if <code>dmoIface</code> is not assignable from <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<T> dmoBufIface,
                                                      String variable,
                                                      String legacyName,
                                                      boolean global,
                                                      boolean undoable,
                                                      int blockDepth)
   {
      return define(dmoBufIface, variable, legacyName, global, new DefaultUndoState(undoable), blockDepth);
   }
   
   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer.
    * This is used as an analog to the DEFINE BUFFER statement when defining
    * a buffer for a temp-table or work-table.  The DMO interface and global
    * setting of the master buffer are used for the new buffer.  This buffer
    * will have the same multiplex ID as the master buffer, once it is
    * assigned to either buffer.
    *
    * @param   <T> 
    *          Type of constructed instance.
    * @param   template
    *          An instance of <code>Temporary</code> which is associated with
    *          a temporary buffer that serves as a template for the new
    *          buffer.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    *
    * @return  An object which implements the DMO interface of the master
    *          buffer, but which initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>master</code>'s DMO interface is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Temporary template, String variable)
   {
      return define(template, variable, null);
   }
   
   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer.
    * This is used as an analog to the DEFINE BUFFER statement when defining
    * a buffer for a temp-table or work-table.  The DMO interface and global
    * setting of the master buffer are used for the new buffer.  This buffer
    * will have the same multiplex ID as the master buffer, once it is
    * assigned to either buffer.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   template
    *          An instance of <code>Temporary</code> which is associated with
    *          a temporary buffer that serves as a template for the new
    *          buffer.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    *
    * @return  An object which implements the DMO interface of the master
    *          buffer, but which initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>master</code>'s DMO interface is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Temporary template,
                                                      String variable,
                                                      String legacyName)
   {
      return define(null, template, variable, legacyName, -1, false);
   }

   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer.
    * This is used as an analog to the DEFINE BUFFER statement when defining
    * a buffer for a temp-table or work-table.  The DMO interface and global
    * setting of the master buffer are used for the new buffer.  This buffer
    * will have the same multiplex ID as the master buffer, once it is
    * assigned to either buffer.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   template
    *          An instance of <code>Temporary</code> which is associated with
    *          a temporary buffer that serves as a template for the new
    *          buffer.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    *
    * @return  An object which implements the DMO interface of the master
    *          buffer, but which initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>master</code>'s DMO interface is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<?>  def,
                                                      Temporary template,
                                                      String    variable,
                                                      String    legacyName)
   {
      return define(def, template, variable, legacyName, -1, false);
   }
   
   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer.  This is used as an 
    * analog to the DEFINE BUFFER statement when defining a buffer for a temp-table or work-table.
    * The DMO interface and global setting of the master buffer are used for the new buffer.  
    * This buffer will have the same multiplex ID as the master buffer, once it is assigned to 
    * either buffer.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   template
    *          An instance of <code>Temporary</code> which is associated with a temporary buffer 
    *          that serves as a template for the new
    *          buffer.
    * @param   variable
    *          Name of the variable with which the record buffer will be associated in the 
    *          transaction manager.
    * @param   legacyName
    *          Legacy name of the buffer.
    * @param   shared
    *          When <code>true</code>, this flag indicates we are defining a new shared buffer.
    *
    * @return  An object which implements the DMO interface of the master
    *          buffer, but which initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>master</code>'s DMO interface is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Temporary template,
                                                      String variable,
                                                      String legacyName,
                                                      boolean shared)
   {
      return define(null, template, variable, legacyName, -1, shared);
   }
   
   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer.
    * This is used as an analog to the DEFINE BUFFER statement when defining
    * a buffer for a temp-table or work-table.  The DMO interface and global
    * setting of the master buffer are used for the new buffer.  This buffer
    * will have the same multiplex ID as the master buffer, once it is
    * assigned to either buffer.
    *
    * @param   <T>
    *          Type of constructed instance.
    * @param   template
    *          An instance of <code>Temporary</code> which is associated with
    *          a temporary buffer that serves as a template for the new
    *          buffer.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @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 DMO interface of the master
    *          buffer, but which initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>master</code>'s DMO interface is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Temporary template,
                                                      String variable,
                                                      int blockDepth)
   {
      return define(null, template, variable, null, blockDepth, false);
   }
   
   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer.
    * This is used as an analog to the DEFINE BUFFER statement when defining
    * a buffer for a temp-table or work-table.  The DMO interface and global
    * setting of the master buffer are used for the new buffer.  This buffer
    * will have the same multiplex ID as the master buffer, once it is
    * assigned to either buffer.
    *
    * @param   <T> 
    *          Type of constructed instance.
    * @param   template
    *          An instance of <code>Temporary</code> which is associated with
    *          a temporary buffer that serves as a template for the new
    *          buffer.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @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 DMO interface of the master
    *          buffer, but which initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>master</code>'s DMO interface is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Temporary template,
                                                      String variable,
                                                      String legacyName,
                                                      int blockDepth)
   {
      return define(null, template, variable, legacyName, blockDepth, false);
   }
   
   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer.
    * This is used as an analog to the DEFINE BUFFER statement when defining
    * a buffer for a temp-table or work-table.  The DMO interface and global
    * setting of the master buffer are used for the new buffer.  This buffer
    * will have the same multiplex ID as the master buffer, once it is
    * assigned to either buffer.
    *
    * @param   <T> 
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   template
    *          An instance of <code>Temporary</code> which is associated with
    *          a temporary buffer that serves as a template for the new
    *          buffer.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @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.
    * @param   shared
    *          When <code>true</code>, this flag indicates we are defining a new shared buffer.
    *
    * @return  An object which implements the DMO interface of the master
    *          buffer, but which initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>master</code>'s DMO interface is not assignable from
    *          <code>Temporary</code>.
    */
   public static <T extends TempTableBuffer> T define(Class<?>  def,
                                                      Temporary template,
                                                      String variable,
                                                      String legacyName,
                                                      int blockDepth,
                                                      boolean shared)
   {
      return define(def, template, variable, legacyName, blockDepth, shared, true);
   }
   
   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer.
    * This is used as an analog to the DEFINE BUFFER statement when defining
    * a buffer for a temp-table or work-table.  The DMO interface and global
    * setting of the master buffer are used for the new buffer.  This buffer
    * will have the same multiplex ID as the master buffer, once it is
    * assigned to either buffer.
    *
    * @param   <T> 
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   template
    *          An instance of <code>Temporary</code> which is associated with
    *          a temporary buffer that serves as a template for the new
    *          buffer.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @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.
    * @param   shared
    *          When <code>true</code>, this flag indicates we are defining a new shared buffer.
    * @param   mutable
    *          Flag indicating if the buffer must be mutable.
    *
    * @return  An object which implements the DMO interface of the master
    *          buffer, but which initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>master</code>'s DMO interface is not assignable from
    *          <code>Temporary</code>.
    */
   @SuppressWarnings("unchecked")
   static <T extends TempTableBuffer> T define(Class<?>  def,
                                               Temporary template, 
                                               String    variable,
                                               String    legacyName,
                                               int       blockDepth,
                                               boolean   shared,
                                               boolean   mutable)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         return DEF_BUFFER.timerWithReturn(
            () -> defineImpl(def, template, variable, legacyName, blockDepth, shared, mutable));
      }
      else
      {
         return defineImpl(def, template, variable, legacyName, blockDepth, shared, mutable);
      }
   }
   
   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer.
    * This is used as an analog to the DEFINE BUFFER statement when defining
    * a buffer for a temp-table or work-table.  The DMO interface and global
    * setting of the master buffer are used for the new buffer.  This buffer
    * will have the same multiplex ID as the master buffer, once it is
    * assigned to either buffer.
    *
    * @param   <T> 
    *          Type of constructed instance.
    * @param   def
    *          The currently executing OE class or external program.
    * @param   template
    *          An instance of <code>Temporary</code> which is associated with
    *          a temporary buffer that serves as a template for the new
    *          buffer.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @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.
    * @param   shared
    *          When <code>true</code>, this flag indicates we are defining a new shared buffer.
    * @param   mutable
    *          Flag indicating if the buffer must be mutable.
    *
    * @return  An object which implements the DMO interface of the master
    *          buffer, but which initially has no backing data record.
    *
    * @throws  IllegalArgumentException
    *          if <code>master</code>'s DMO interface is not assignable from
    *          <code>Temporary</code>.
    */
   @SuppressWarnings("unchecked")
   private static <T extends TempTableBuffer> T defineImpl(Class<?>  def,
                                                           Temporary template, 
                                                           String    variable,
                                                           String    legacyName,
                                                           int       blockDepth,
                                                           boolean   shared,
                                                           boolean   mutable)
   {
      // an explicit buffer must always be created using as master the buffer as it exists on
      // conversion; after that, if the master is bound, this will be bound, too.
      TemporaryBuffer m = (TemporaryBuffer) ((BufferReference) template).definition();
      
      TemporaryBuffer masterBuf = (m.master == null) ? m : m.master;
      Class<T> dmoBufIface = (Class<T>) masterBuf.getDMOBufInterface();
      TemporaryBuffer buffer = makeBuffer(def,
                                          dmoBufIface,
                                          variable,
                                          masterBuf.global,
                                          masterBuf.undoStateProvider,
                                          blockDepth);
      buffer.master = masterBuf;
      buffer.referenceOnly = masterBuf.referenceOnly;
      buffer.tempTableRef = masterBuf.tempTableRef; // inherit this, as is directly accessed 
      buffer.multiplexID = masterBuf.multiplexID;
      
      ProcedureManager.ProcedureHelper pm = buffer.getProcedureManager();
      Map<TemporaryBuffer, Map<TemporaryBuffer, Object>> domains = buffer.local.domains;
      Map<TemporaryBuffer, Object> domain = domains.get(masterBuf);
      domain.put(buffer, null);
      
      T proxy = createProxy(dmoBufIface, buffer, legacyName);
      
      if (shared)
      {
         SharedVariableManager.addBuffer(ScopeLevel.NEXT, variable, proxy);
      }
      
      // register this in the master buffer's explicit buffer set
      
      if (!mutable)
      {
         return proxy;
      }
      
      proxy = makeMutable(dmoBufIface, proxy);
      buffer.mutableProxy = proxy;
      buffer.master.explicitBuffers.add(proxy);
      
      // bind it if the master is bound
      TemporaryBuffer boundMaster = (TemporaryBuffer) get(template);
      if (boundMaster != m)
      {
         Temporary newBuf;
         try
         {
            pm.disablePendingResources();
            newBuf = TemporaryBuffer.define(def,
                                            (Temporary) boundMaster.getDMOProxy(), 
                                            buffer.getDMOAlias(), 
                                            buffer.getLegacyName(), 
                                            -1,
                                            false,
                                            false);
            boundMaster.explicitBuffers.remove(newBuf);
         }
         finally
         {
            pm.enablePendingResources();
         }
         
         TemporaryBuffer tbuf = (TemporaryBuffer) ((BufferReference) buffer).buffer();
         
         buffer.mutableHandler.bind(newBuf, true);
         pm.executeOnReturn(() -> {
            buffer.mutableHandler.unbind();
            tbuf._setDynamic();
            
            ((BufferImpl) newBuf).delete();
         }, WeightFactor.LAST);
      }
      
      return proxy;
   }
   
   /**
    * Define a new buffer which is based upon a &quot;master&quot; buffer
    * stored in the {@link com.goldencode.p2j.util.SharedVariableManager
    * SharedVariableManager}.  This is used as an analog to the DEFINE SHARED
    * TEMP-TABLE statement when defining a buffer for a shared temp-table or
    * work-table which has been established elsewhere.  The DMO interface,
    * global, and undoability settings of the master buffer are used for the
    * new buffer.  This buffer will have the same multiplex ID as the master
    * buffer, which should have been assigned by an enclosing scope by the
    * time this method is invoked.
    *
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager and shared variable
    *          manager.
    * @param   expected
    *          The expected DMO type of the existing master temp-table.
    *
    * @return  An object which is bound to the master buffer.
    *
    * @throws  IllegalArgumentException
    *          if {@code master}'s DMO interface is not assignable from {@code Temporary}.
    */
   public static Temporary useShared(String variable, Class<? extends Temporary> expected)
   {
      // TODO: The error message should contain the legacy name of the TEMP-TABLE, not the java variable name!
      //       The chances are the lookup might also fail for same reason (the local name for a temp-table
      //       might different in different external procedures if local name collisions occur (?)).
      //       Maybe use the legacy name also for the procedure name, also present in error message?
      
      Temporary master = SharedVariableManager.lookupTempTable(variable, expected);
      
      // define a secondary buffer for the master
      String masterVar = ((BufferReference) master).buffer().getDMOAlias();
      Temporary masterBuffer = define(master, masterVar);
      
      boolean global = ((BufferReference) masterBuffer).definition().isGlobal();
      Temporary child = define((Class) expected, variable, global);
      TemporaryBuffer childBuffer = (TemporaryBuffer) ((BufferReference) child).buffer();
      ReferenceProxy childHandler = childBuffer.mutableHandler;
      childHandler.bind(masterBuffer, true);
      
      return child;
   }
   
   /**
    * Create a buffer which is bound to the master buffer, for use in a child shared buffer case.
    * 
    * @param   master
    *          The master buffer.
    * @param   expected
    *          The child's buffer DMO interface.
    * @param   name
    *          The child buffer's name.
    * 
    * @return  An object which is bound to the master buffer.
    */
   public static Temporary useShared(Temporary master, Class<?> expected, String name)
   {
      boolean global = ((BufferReference) master).definition().isGlobal();
      Temporary child = define((Class) expected, name, global);
      
      TemporaryBuffer childBuffer = (TemporaryBuffer) ((BufferReference) child).buffer();
      ReferenceProxy childHandler = childBuffer.mutableHandler;
      childHandler.bind(master, false);
      
      return child;
   }

   /**
    * Associate two temp tables with one another, such that all relevant
    * records can be copied between them at prescribed times.
    *
    * @param    src
    *           <code>TableParameter</code> containing reference to the source
    *           temporary buffer or result set.
    * @param    dstDMO
    *           DMO instance returned by a previous call to {@link #define} on
    *           the destination temporary buffer.
    *
    * @throws   ErrorConditionException
    *           if a recoverable error occurs flushing the source buffer in
    *           preparation for an input copy or while copying records.
    * @throws   RuntimeException
    *           if an unrecoverable error occurs invoking methods on the
    *           underlying DMOs to get or set data values.
    */
   public static void associate(TableParameter src, Temporary dstDMO)
   {
      associate(src, dstDMO, ParameterOption.NONE);
   }
   
   /**
    * Associate two temp tables with one another, such that all relevant
    * records can be copied between them at prescribed times.
    *
    * @param    src
    *           <code>TableParameter</code> containing reference to the source
    *           temporary buffer or result set.
    * @param    dstDMO
    *           DMO instance returned by a previous call to {@link #define} on
    *           the destination temporary buffer.
    * @param    input
    *           if <code>true</code>, records are copied from source table to
    *           destination table immediately upon invocation of this method,
    *           otherwise they are not.
    * @param    output
    *           if <code>true</code>, records are copied from destination
    *           table back to source table when the current transaction or
    *           subtransaction is committed;  otherwise they are not.
    *
    * @throws   ErrorConditionException
    *           if a recoverable error occurs flushing the source buffer in
    *           preparation for an input copy or while copying records.
    * @throws   RuntimeException
    *           if an unrecoverable error occurs invoking methods on the
    *           underlying DMOs to get or set data values.
    */
   public static void associate(TableParameter src,
                                Temporary      dstDMO,
                                boolean        input,
                                boolean        output)
   {
      associate(src, dstDMO, input, output, ParameterOption.NONE);
   }
   
   /**
    * Associate two temp tables with one another, such that all relevant records can be copied
    * between them at prescribed times.
    *
    * @param    src
    *           {@code TableParameter} containing reference to the source temporary buffer or
    *           result set.
    * @param    dstDMO
    *           DMO instance returned by a previous call to {@link #define} on the destination
    *           temporary buffer.
    * @param    input
    *           If {@code true}, records are copied from source table to destination table
    *           immediately upon invocation of this method, otherwise they are not.
    * @param    output
    *           If {@code true}, records are copied from destination table back to source table
    *           when the current transaction or subtransaction is committed;  otherwise they are
    *           not.
    * @param    option
    *           Parameter passing mode APPEND, BY-VALUE, BIND or BY-REFERENCE option.
    *
    * @throws   ErrorConditionException
    *           if a recoverable error occurs flushing the source buffer in preparation for an
    *           input copy or while copying records.
    * @throws   RuntimeException
    *           if an unrecoverable error occurs invoking methods on the underlying DMOs to get or
    *           set data values.
    */
   public static void associate(TableParameter  src,
                                Temporary       dstDMO,
                                boolean         input,
                                boolean         output,
                                ParameterOption option)
   {
      associate(src, dstDMO, input, output, EnumSet.of(option));
   }
   
   /**
    * Associate two temp tables with one another, such that all relevant records can be copied
    * between them at prescribed times.
    *
    * @param    src
    *           {@code TableParameter} containing reference to the source temporary buffer or
    *           result set.
    * @param    dstDMO
    *           DMO instance returned by a previous call to {@link #define} on the destination
    *           temporary buffer.
    * @param    input
    *           If {@code true}, records are copied from source table to destination table
    *           immediately upon invocation of this method, otherwise they are not.
    * @param    output
    *           If {@code true}, records are copied from destination table back to source table
    *           when the current transaction or subtransaction is committed;  otherwise they are
    *           not.
    * @param    options
    *           Parameter passing mode APPEND, BY-VALUE, BIND or BY-REFERENCE options.
    *
    * @throws   ErrorConditionException
    *           if a recoverable error occurs flushing the source buffer in preparation for an
    *           input copy or while copying records.
    * @throws   RuntimeException
    *           if an unrecoverable error occurs invoking methods on the underlying DMOs to get or
    *           set data values.
    */
   public static void associate(TableParameter  src,
                                Temporary       dstDMO,
                                boolean         input,
                                boolean         output,
                                EnumSet<ParameterOption> options)
   {
      if (src.isBind() && options.contains(ParameterOption.BIND) && output && !input)
      {
         // reverse the source and destination
         if (src.isTableHandle())
         {
            // TODO: what?
         }
         else
         {
            boolean refOnly = ((TemporaryBuffer) get(src.getTable())).referenceOnly;
            if (refOnly)
            {
               Temporary aux = src.getTable();
               src.setTable(dstDMO);
               dstDMO = aux;
            }
         }
      }
      src.setInputMode(input);
      src.setOutputMode(output);
      
      associate(src, dstDMO, options);
   }

   /**
    * Associate two temp tables with one another, such that all relevant records can be copied
    * between them at prescribed times.
    *
    * @param   src
    *          {@code TableParameter} containing reference to the source temporary buffer or
    *          result set.
    * @param   dstDMO
    *          DMO instance returned by a previous call to {@link #define} on the destination
    *          temporary buffer.
    * @param   option
    *          Table parameter option (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    *
    * @throws  ErrorConditionException
    *          if a recoverable error occurs flushing the source buffer in preparation for an
    *          input copy or while copying records.
    * @throws  RuntimeException
    *          if an unrecoverable error occurs invoking methods on the underlying DMOs to get or
    *          set data values.
    */
   public static void associate(final TableParameter src, Temporary dstDMO, ParameterOption option)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         STATIC_PARAM.timer(() -> associateImpl(src, dstDMO, EnumSet.of(option)));
      }
      else
      {
         associateImpl(src, dstDMO, EnumSet.of(option));
      }
   }
   
   /**
    * Associate two temp tables with one another, such that all relevant records can be copied
    * between them at prescribed times.
    *
    * @param   src
    *          {@code TableParameter} containing reference to the source temporary buffer or
    *          result set.
    * @param   dstDMO
    *          DMO instance returned by a previous call to {@link #define} on the destination
    *          temporary buffer.
    * @param   options
    *          Table parameter options (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    *
    * @throws  ErrorConditionException
    *          if a recoverable error occurs flushing the source buffer in preparation for an
    *          input copy or while copying records.
    * @throws  RuntimeException
    *          if an unrecoverable error occurs invoking methods on the underlying DMOs to get or
    *          set data values.
    */
   public static void associate(TableParameter src, Temporary dstDMO, EnumSet<ParameterOption> options)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         STATIC_PARAM.timer(() -> associateImpl(src, dstDMO, options));
      }
      else
      {
         associateImpl(src, dstDMO, options);
      }
   }
   
   /**
    * Associate two temp tables with one another, such that all relevant records can be copied
    * between them at prescribed times.
    *
    * @param   src
    *          {@code TableParameter} containing reference to the source temporary buffer or
    *          result set.
    * @param   dstDMO
    *          DMO instance returned by a previous call to {@link #define} on the destination
    *          temporary buffer.
    * @param   options
    *          Table parameter options (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    *
    * @throws  ErrorConditionException
    *          if a recoverable error occurs flushing the source buffer in preparation for an
    *          input copy or while copying records.
    * @throws  RuntimeException
    *          if an unrecoverable error occurs invoking methods on the underlying DMOs to get or
    *          set data values.
    */
   private static void associateImpl(TableParameter src, Temporary dstDMO, EnumSet<ParameterOption> options)
   {
      if (src.getTable() == (Temporary) ((BufferImpl) dstDMO).ref())
      {
         // same reference, don't do anything
         return;
      }
      
      boolean input  = src.isInputMode();
      boolean output = src.isOutputMode();
      boolean append = options.contains(ParameterOption.APPEND);
      
      if (!(input || output))
      {
         throw new IllegalStateException("Parameter mode (INPUT/OUTPUT) was not specified!");
      }

      TemporaryBuffer buffer = (TemporaryBuffer) get(dstDMO);

      BufferManager.registerScopeable(buffer.bufferManager);
      
      ProcedureManager.ProcedureHelper pm = buffer.getProcedureManager();

      if (buffer.referenceOnly && !(src.isByReference() || src.isBind()))
      {
         Object referent = pm._thisProcedure();
         String pname = pm.getRelativeName(referent);
         String bufName = ((Buffer) src.getTable()).name().toJavaType();
         
         String msg = "%s BIND or BY-REFERENCE modifier required on RUN, FUNCTION or METHOD " +
                      "invocation parameter when called TABLE or DATASET parameter %s is " +
                      "REFERENCE-ONLY and is still not bound";
         msg = String.format(msg, pname, bufName);
         
         if (buffer.getTxHelper().isSuppressError())
         {
            ErrorManager.addError(13012, msg, true, false);
            
            throw new ErrorConditionException(new NumberedException(msg + ". (13012)", 13012));
         }
         else
         {
            ErrorManager.recordOrThrowError(13012, msg, false);
            
            // TODO: this doesn't raise an ERROR at the caller ...
         }
         
         return;
      }
      
      if (src.isRemoteParameter()  && !src.getResultSet().isReceivedSchema())
      {
         RecordBuffer rb = ((BufferReference) dstDMO).buffer();
         DmoMeta meta = rb.getDmoInfo();
         Iterator<Property> props = meta.getFields(false);
         List<PropertyDefinition> pdl = new ArrayList<>();
         props.forEachRemaining(p -> pdl.add(new PropertyDefinition(p)));
         src.getResultSet().setProperties(pdl);
      }
      
      boolean bind = src.isBind() || options.contains(ParameterOption.BIND);
      
      // a BIND at the param or argument has effect only if either the argument has BY-REFERENCE or the target
      // is REFERENCE-ONLY
      if (!(src.isByReference()                                                                 ||
            (src.getTable() != null && 
             ((TemporaryBuffer) ((BufferReference) src.getTable()).definition()).referenceOnly) ||
            ((TemporaryBuffer) ((BufferReference) dstDMO).definition()).referenceOnly))
      {
         bind = false;
      }
      
      if (src.isByReference() || bind || (!src.isRemoteParameter() && buffer.referenceOnly))
      {
         Temporary srcTable = src.getTable();
         if (srcTable == null)
         {
            if (!src.isRemoteParameter())
            {
               // this is a TABLE-HANDLE!
               handle hsrc = src.getTableHandle();
               if (hsrc._isValid())
               {
                  src.setTable((Temporary) hsrc.getResource());
               }
               else
               {
                  
                  hsrc.assign(((BufferReference) dstDMO).buffer().tableHandle());
                  src.setTable(dstDMO);
               }
            }
            else
            {
               // TODO: remote table-handle param ??
            }
         }
         else
         {
            if (output && ((TemporaryBuffer) ((BufferReference) srcTable).buffer()).referenceOnly)
            {
               // switch source and dest
               Temporary old = srcTable;
               srcTable = dstDMO;
               dstDMO = old;
            }

            // save the reference, will be needed for bind later
            TemporaryBuffer dstBufferTarget = (TemporaryBuffer) ((BufferReference) dstDMO).definition();
            
            // find the default buffer and perform the binding of the before-buffer, if it exists
            TemporaryBuffer dstBuf = (TemporaryBuffer) ((BufferReference) dstDMO).definition();
            dstDMO = (Temporary) dstBuf.getParentTable().defaultBufferHandleNative();
            dstBuf = (TemporaryBuffer) ((BufferReference) dstDMO).buffer();
            boolean dstReferenceOnly = dstBuf.referenceOnly;
            
            // do not re-BIND if a REFERENCE-ONLY table is already bound to the same target table 
            TemporaryBuffer srcDefaultBuf = ((TemporaryBuffer) ((BufferReference) srcTable).buffer());
            if (srcDefaultBuf.mutableHandler != null && srcDefaultBuf.mutableHandler.bound != null)
            {
               // if the source is bound, then get the 'real source'.  This can happen because static DATASETs
               // work with the 'definition' and not the actual ReferenceProxy instance.
               srcTable = srcDefaultBuf.mutableHandler.bound;
               srcDefaultBuf = srcDefaultBuf.mutableHandler.boundBuffer;
            }
            srcDefaultBuf = (TemporaryBuffer) ((BufferReference) srcDefaultBuf.getParentTable()
                                                                              .defaultBufferHandleNative())
                                                                              .buffer();
            if (dstBuf.mutableHandler.boundBuffer == null)
            {
               // this is the very first bind
               handle hSrcBefore = ((Buffer) srcTable).beforeBuffer();
               if (!hSrcBefore.isUnknown())
               {
                  // associate the before buffer, too!
                  handle hDstBefore = ((Buffer) dstDMO).beforeBuffer();
                  if (hDstBefore.isUnknown())
                  {
                     // TODO: error?
                  }
                  else
                  {
                     TableParameter beforeParam = new TableParameter((Temporary) hSrcBefore.getResource(),
                                                                     src.getParameterOptions(),
                                                                     input,
                                                                     output);
                     associate(beforeParam, (Temporary) hDstBefore.getResource(), options);
                  }
               }
            }

            {
               // CA: this test is disabled, as it regresses a BIND case for OUTPUT BIND where the caller is 
               // REFERENCE-ONLY
               /*
               if (bind && (srcBuf.referenceOnly || dstBuf.referenceOnly))
               {
                  handle procHandle = ProcedureManager.thisProcedure();
                  String procName = ((PersistentProcedure) procHandle.getResource()).name().toJavaType();
                  ErrorManager.recordOrThrowError(13009, procName, dstBuf.getLegacyName());
                  // <program name> BIND modifier not allowed for cases where neither the caller nor the
                  // called TABLE or DATASET parameter <parameter-name> has been defined REFERENCE-ONLY and
                  // neither caller nor called parameter is a TABLE-HANDLE or DATASET-HANDLE. (13009)
               }
               else
               */
               {
                  // go through all explicit buffers in the dest table, including the default buffer 
                  Set<Temporary> copy = new HashSet<>(dstBuf.explicitBuffers);
                  copy.add(dstDMO);
                  for (Temporary dmo : copy)
                  {
                     TemporaryBuffer exBuf = (TemporaryBuffer) ((BufferReference) dmo).definition();
                     
                     // if the buffer is not already bound, then:
                     // 1. the DMO target: this and only this will be explicitly bound
                     // 2. all other explicit or default buffer will have a new buffer created 
                     
                     if (exBuf.mutableHandler.boundBuffer != null)
                     {
                        // skip the buffer which is bound from a previous call
                        continue;
                     }
                     
                     if (exBuf == dstBufferTarget)
                     {
                        Temporary oldBound = exBuf.mutableHandler.bind((Temporary) ((BufferImpl) srcTable).ref());
                        
                        // CA: added back the !bind test, as the binding needs to be rolledback on procedure exit, 
                        // in non-bind cases
                        if (!bind || !dstReferenceOnly)
                        {
                           // TODO: can a future BIND call veto any unbind from an existing call on the stack?
                           TemporaryBuffer finalDstBuf = exBuf;
                           pm.executeOnReturn(() -> {
                              finalDstBuf.mutableHandler.unbind();
                              
                              if (oldBound != null)
                              {
                                 finalDstBuf.mutableHandler.bind(oldBound);
                              }
                           }, WeightFactor.LAST);
                        }
                        
                     }
                     else
                     {
                        // create a new buffer
                        Temporary newBuf;
                        try
                        {
                           pm.disablePendingResources();
                           newBuf = TemporaryBuffer.define(null,
                                                           srcTable,
                                                           exBuf.getDMOAlias(),
                                                           exBuf.getLegacyName(),
                                                           -1,
                                                           false,
                                                           false);
                           exBuf.explicitBuffers.remove(newBuf);
                        }
                        finally
                        {
                           pm.enablePendingResources();
                        }
                        
                        Temporary oldExBound = exBuf.mutableHandler.bind(newBuf, true);
                        TemporaryBuffer tbuf = (TemporaryBuffer) ((BufferReference) newBuf).buffer();
                        // open scope for this new buffer. Bound buffers go to global scope, the other local
                        tbuf.openScopeAt(bind ? 0 : tbuf.bufferManager.getOpenBufferScopes() - 1);
                        
                        if (!bind || !dstReferenceOnly)
                        {
                           pm.executeOnReturn(() -> {
                              exBuf.mutableHandler.unbind();
                              
                              if (oldExBound != null)
                              {
                                 exBuf.mutableHandler.bind(oldExBound);
                              }
                              
                              tbuf._setDynamic();
                              
                              ((BufferImpl) newBuf).delete();
                           }, WeightFactor.LAST);
                        }
                     }
                  }
               }
            }
            
            return;
         }
      }
      
      // same reference, nothing to do
      if (src.getTableHandle() != null && src.getTableHandle().equals(buffer.tableHandle()))
      {
         // by-ref/ref-only still have work to do related to keeping the mutable buffers in sync.
         return;
      }
      
      if (input)
      {
         if (!src.isRemoteParameter())
         {
            Temporary srcDMO = src.getTable();

            if (srcDMO != null)
            {
               TemporaryBuffer srcBuf = (TemporaryBuffer) ((BufferReference) srcDMO).buffer();
               Persistence.Context pc = srcBuf.persistenceContext;
               boolean inTx = false;
               
               try
               {
                  if (srcBuf.isTransient() && !srcBuf.bufferManager.isTransaction())
                  {
                     inTx = pc.beginTransaction(null);
                  }
                  
                  srcBuf.flush();
                  
                  if (inTx)
                  {
                     pc.commit();
                  }
               }
               catch (ValidationException | PersistenceException exc)
               {
                  if (inTx)
                  {
                     try
                     {
                        pc.rollback(true);
                     }
                     catch (PersistenceException pe)
                     {
                        if (LOG.isLoggable(Level.WARNING))
                        {
                           LOG.log(Level.WARNING, "Error flushing source temp-table buffer", exc);
                        }
                     }
                  }
                  
                  ErrorManager.recordOrThrowError(exc);
               }
               
               rowid srcRec = (buffer.referenceOnly || src.isByReference()) && srcBuf.isAvailable() 
                                 ? srcBuf.rowID() 
                                 : null;
               
               boolean res = copyAllRows(srcRec, srcDMO, dstDMO, append, CopyTableMode.INPUT_PARAM_MODE);
               if (!res)
               {
                  BlockManager.returnError();
               }
            }
         }
         else if (src.getResultSet().getXmlTable() != null)
         {
            String xmlTable = src.getResultSet().getXmlTable();
            readFromXml(xmlTable, append, ((Buffer) dstDMO).tableHandle());
         }
         else
         {
            insertAllRows(src.getResultSet(),
                          dstDMO,
                          append,
                          CopyTableMode.INPUT_PARAM_MODE,
                          src.getParameterIndex());
         }
      }
      
      if (output)
      {
         // Set the destination DMO if output copy is required.  This is
         // actually the source DMO passed to this method, since the direction
         // of the copy is reversed on output.
         TemporaryBuffer dstBuf = (TemporaryBuffer) ((BufferReference) dstDMO).buffer();
         if (dstBuf.outputParameters == null)
         {
            dstBuf.outputParameters = Collections.newSetFromMap(new IdentityHashMap<>());
         }
         dstBuf.outputParameters.add(src);
         
         OutputTableCopier copier = new OutputTableCopier(dstBuf, src);
         pm.executeOnReturn(copier::finished, copier.weight());
      }
   }

   /**
    * Creates a dynamic temporary table for the given temp table handle. Intended to handle
    * conversion of DEFINE PARAMETER TABLE-HANDLE.
    *
    * @param    src
    *           <code>TableParameter</code> containing reference to the source table.
    * @param    target
    *           Handle for storing the target table.
    */
   public static void createDynamicTable(TableParameter src, handle target)
   {
      createDynamicTable(src, target, ParameterOption.NONE);
   }
   
   /**
    * Creates a dynamic temporary table for the given temp table handle. Intended to handle
    * conversion of DEFINE PARAMETER TABLE-HANDLE.
    *
    * @param    src
    *           <code>TableParameter</code> containing reference to the source table.
    * @param    target
    *           Handle for storing the target table.
    * @param    input
    *           if <code>true</code>, records are copied from source table to
    *           destination table immediately upon invocation of this method,
    *           otherwise they are not.
    * @param    output
    *           if <code>true</code>, records are copied from destination
    *           table back to source table when the current transaction or
    *           subtransaction is committed;  otherwise they are not.
    */
   public static void createDynamicTable(TableParameter src,
                                         handle         target,
                                         boolean        input,
                                         boolean        output)
   {
      createDynamicTable(src, target, input, output, ParameterOption.NONE);
   }
   
   /**
    * Creates a dynamic temporary table for the given temp table handle. Intended to handle
    * conversion of DEFINE PARAMETER TABLE-HANDLE.
    *
    * @param    src
    *           {@code TableParameter} containing reference to the source table.
    * @param    target
    *           Handle for storing the target table.
    * @param    input
    *           If {@code true}, records are copied from source table to destination table
    *           immediately upon invocation of this method, otherwise they are not.
    * @param    output
    *           If {@code true}, records are copied from destination table back to source table
    *           when the current transaction or subtransaction is committed;  otherwise they are
    *           not.
    * @param    option
    *           Parameter passing mode APPEND, BY-VALUE, BIND or BY-REFERENCE option.
    */
   public static void createDynamicTable(TableParameter  src,
                                         handle          target,
                                         boolean         input,
                                         boolean         output,
                                         ParameterOption option)
   {
      src.setInputMode(input);
      src.setOutputMode(output);
      
      createDynamicTable(src, target, option);
   }

   /**
    * Creates a dynamic temporary table for the given temp table handle. Intended to handle
    * conversion of DEFINE PARAMETER TABLE-HANDLE.
    *
    * @param    src
    *           {@code TableParameter} containing reference to the source table.
    * @param    target
    *           Handle for storing the target table.
    * @param    option
    *           Table parameter option (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    */
   public static void createDynamicTable(TableParameter src, handle target, ParameterOption option)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         DYN_PARAM.timer(() -> createDynamicTableImpl(src, target, option));
      }
      else
      {
         createDynamicTableImpl(src, target, option);
      }
   }
   
   /**
    * Creates a dynamic temporary table for the given temp table handle. Intended to handle
    * conversion of DEFINE PARAMETER TABLE-HANDLE.
    *
    * @param    src
    *           {@code TableParameter} containing reference to the source table.
    * @param    target
    *           Handle for storing the target table.
    * @param    option
    *           Table parameter option (APPEND, BY-VALUE, BIND or BY-REFERENCE).
    */
   private static void createDynamicTableImpl(TableParameter src, handle target, ParameterOption option)
   {
      boolean input  = src.isInputMode();
      boolean output = src.isOutputMode();
      boolean append = (option == ParameterOption.APPEND);
      
      if (!(input || output))
      {
         throw new IllegalStateException("Parameter mode (INPUT/OUTPUT) was not specified!");
      }
      
      BufferManager bMgr = BufferManager.get();
      BufferManager.registerScopeable(bMgr);
      ProcedureManager.ProcedureHelper pm = bMgr.getProcedureHelper();
      
      if (output)
      {
         TransactionManager.registerCurrent(target);
      }
      
      if (src.isByReference() || src.isBind())
      {
         target.assign(src.getTableHandle());
         
         if (target._isValid())
         {
            if (target.getResource() instanceof TempTableBuilder)
            {
               TempTableBuilder ttb = (TempTableBuilder) target.getResource();
               boolean old = ttb.isPostponedDelete();
               ttb.setPostponedDelete(true);
               pm.executeOnReturn(() -> ttb.setPostponedDelete(old), WeightFactor.LAST);
            }
         }
         else if (option == ParameterOption.BIND)
         {
            StaticTempTable table = (StaticTempTable) target.getResource();
            TemporaryBuffer targetBuffer = (TemporaryBuffer) ((BufferImpl) table.defaultBufferHandleNative()).buffer();
            if (targetBuffer.referenceOnly)
            {
               // BIND is assumed
               pm.executeOnReturn(() ->
               {
                  // if target is still valid, then bind it to the argument
                  if (target._isValid())
                  {
                     TempTable tt = (TempTable) target.getResource();
                     BufferImpl defBuf = (BufferImpl) tt.defaultBufferHandleNative();
                     targetBuffer.bind((TemporaryBuffer) defBuf.buffer());
                  }
        	   }, WeightFactor.LAST);
        	}
         }
         else if (output)
         {
            OutputTableHandleCopier copier = new OutputTableHandleCopier(src, target);
            pm.executeOnReturn(copier::finished, copier.weight());
            bMgr.registerOutputTableHandleCopier(copier);
         }
         
         return;
      }
      
      if (input || src.isRemoteParameter())
      {
         handle srcHandle = src.getTableHandle();
         if (srcHandle._isValid())
         {
            TempTableBuilder dstTable = new TempTableBuilder();
            logical res = dstTable.copyTempTable(srcHandle, append, false, false, "");
            if (!res.booleanValue())
            {
               BlockManager.returnError();
            }
            target.assign(dstTable);
         }
         else if (src.getResultSet() != null && src.getResultSet().getXmlTable() != null)
         {
            String xmlTable = src.getResultSet().getXmlTable();
            readFromXml(xmlTable, append, target);
         }
         else
         {
            TableWrapper resultSet = src.getResultSet();
            if (resultSet != null && resultSet.isValid() && resultSet.getTableName() != null)
            {
               if (!resultSet.isReceivedSchema())
               {
                  // A NO-SCHEMA-MARSHAL table cannot be used as a parameter where the receiving side
                  // does not have a pre-prepared schema
                  ErrorManager.recordOrThrowError(12323);
               }

               TempTableBuilder builder = new TempTableBuilder();
               builder.createFromTableWrapper(resultSet, src.getParameterIndex());
               target.assign(builder);
            }
         }
      }
      
      if (output)
      {
         OutputTableHandleCopier copier = new OutputTableHandleCopier(src, target);
         pm.executeOnReturn(copier::finished, copier.weight());
         bMgr.registerOutputTableHandleCopier(copier);
      }
   }
   
   /**
    * Get default buffer for a temporary table.
    *
    * @param  tempTableHandle
    *         Handle to a temporary table.
    *
    * @return see above.
    */
   public static Temporary getDefaultBuffer(handle tempTableHandle)
   {
      if (!tempTableHandle._isValid())
      {
         return null;
      }
      
      String msg = null;
      switch (((HandleResource) tempTableHandle.getResource()).type().toUpperCase())
      {
         case LegacyResource.TEMP_TABLE:
            // this is normally expected and will continue without raising errors
            break;
         case LegacyResource.BUFFER:
            msg = "BUFFER handle used where TEMP-TABLE handle parameter needed";
            ErrorManager.recordOrShowError(9067, msg, true, false);
            return null;
         default:
            msg = "TEMP-TABLE handle parameter requires valid and prepared handle";
            ErrorManager.recordOrShowError(9068, msg, true, false);
            return null;
      }
      
      TempTable tempTable = (TempTable) tempTableHandle.getResource();
      return (Temporary) tempTable.defaultBufferHandleNative();
   }
   
   /**
    * Get the DMO which represents the database record currently held in the buffer.
    *
    * @return DMO for the current record, or {@code null} if the buffer currently is empty.
    */
   @Override
   public TempRecord getCurrentRecord()
   {
      return (TempRecord) super.getCurrentRecord();
   }
   
   /**
    * Get an iterator on the names of all temp tables which currently exist
    * in the current context.
    * 
    * @return  Iterator on active table names.
    */
   public static Iterator<String> activeTables()
   {
      return context.get().activeTables.iterator();
   }
   
   /**
    * Report the number of temp tables which currently exist in the current context.
    *
    * @return  Number of temp tables which have been created for the current context and not yet dropped.
    */
   public static int getTableCount()
   {
      Context ctx = null;
      try
      {
         ctx = TemporaryBuffer.context.get(false);
      }
      catch (ContextLocalCleanupException e)
      {
         // do nothing, ctx stays null
      }
      
      return ctx == null ? 0 : ctx.getTableCount();
   }
   
   /**
    * Populate the temp table with records created using the specified result set.
    * 
    * @param    srcResultSet
    *           The source result set.
    * @param    dstDMO
    *           DMO instance refering the destination DMO buffer.
    * @param    append
    *           <code>true</code> to append the data, <code>false</code> to remove all records
    *           first.
    * @param    errorMode
    *           Table copy mode, affects error handling.
    * @param    tableParameterIndex
    *           1-based index of the corresponding table parameter (in declaration of the
    *           function). Affects error handling.
    *
    * @return   <code>true</code> if copy was successful. <code>false</code> if an error occurs
    *           during copying records and error was recorder rather than thrown.
    *
    * @throws   ErrorConditionException
    *           if an error occurs during copying records.
    */
   public static boolean insertAllRows(TableWrapper srcResultSet,
                                       Temporary dstDMO,
                                       boolean append,
                                       CopyTableMode errorMode,
                                       int tableParameterIndex)
   {
      TemporaryBuffer dstBuf = (TemporaryBuffer) ((BufferReference) dstDMO).buffer();
      dstBuf.initialize();
      TempTable dstTempTable = dstBuf.getParentTable();
      TemporaryBuffer dstB4Buf = null;
      boolean processB4 = false;
      TempTable beforeTT = dstTempTable.getBeforeTableNative();
      if (beforeTT != null && beforeTT.valid())
      {
         Temporary dstB4DMO = (Temporary) beforeTT.defaultBufferHandleNative();
         dstB4Buf = (TemporaryBuffer) ((BufferReference) dstB4DMO).buffer();
         processB4 = true;
      }
      
      // get map of destination setter and getter methods, and extents, keyed by property name
      Map<String, Method> dstSetters = dstBuf.getDmoInfo().getPojoSetterMap();
      Map<String, Method> dstGetters = dstBuf.getDmoInfo().getPojoGetterMap();
      Map<String, Integer> dstExtents = dstBuf.getDmoInfo().getExtentMap();
      Map<String, Method> dstB4Setters = processB4 ? dstB4Buf.getDmoInfo().getPojoSetterMap() : null;
      Map<String, Method> dstB4Getters = processB4 ? dstB4Buf.getDmoInfo().getPojoGetterMap() : null;
      Map<String, Integer> dstB4Extents = processB4 ? dstB4Buf.getDmoInfo().getExtentMap() : null;
      
      ArrayList<PropertyDefinition> srcPropList = srcResultSet.getProperties();
      int srcPropListSize                  = srcPropList.size();
      String[] srcProperties               = new String[srcPropListSize];
      for (int i = 0; i < srcPropListSize; i++)
      {
         srcProperties[i] = srcPropList.get(i).getName();
      }
      
      String[] dstProperties = TempTableBuilder.getOrderedPropertyNames(dstTempTable);
      
      if (!srcResultSet.isFromJson() && srcProperties.length != dstProperties.length)
      {
         // property validation is done only if this is not from a web service call
         if (errorMode == CopyTableMode.INPUT_PARAM_MODE)
         {
            String errorText = String.format(ERROR_TABLE_MISMATCH_TEMPLATE,
                                             tableParameterIndex,
                                             dstTempTable.name().toStringMessage(),
                                             dstProperties.length,
                                             srcProperties.length);
            throw new InvalidParameterConditionException(
               new NumberedException(errorText, 8030, false));
         }
         else if (errorMode == CopyTableMode.OUTPUT_TABLE_PARAM_MODE ||
                  errorMode == CopyTableMode.OUTPUT_TABLE_HANDLE_PARAM_MODE)
         {
            String errorText;
            if (srcResultSet.isTableHandle())
            {
               errorText = String.format(ERROR_TABLE_MISMATCH_TEMPLATE,
                                         tableParameterIndex,
                                         dstTempTable.name().toStringMessage(),
                                         dstProperties.length,
                                         srcProperties.length);
            }
            else
            {
               errorText = String.format(ERROR_TABLE_MISMATCH_TEMPLATE,
                                         tableParameterIndex,
                                         srcResultSet.getTableName(),
                                         srcProperties.length,
                                         dstProperties.length);
            }
            throw new InvalidParameterConditionException(
                                                new NumberedException(errorText, 8030, false));
         }
         else
         {
            throw new IllegalStateException("Wrong table copy error mode: " + errorMode);
         }
      }
      
      if (!srcResultSet.isFromJson())
      {
         for (int i = 0; i < srcProperties.length; i++)
         {
            PropertyDefinition property = srcPropList.get(i);
            String dstPropName = dstProperties[i];
            
            // compare data types
            Method dstGetter = dstGetters.get(dstPropName);
            ParmType srcType = ParmType.fromClass(property.getType());
            ParmType dstType = ParmType.fromClass(dstGetter.getReturnType());
            
            if (srcType == null || dstType == null || !srcType.equals(dstType))
            {
               if (errorMode == CopyTableMode.INPUT_PARAM_MODE)
               {
                  throw new InvalidParameterConditionException(
                                    new NumberedException(ERROR_MISMATCHED_FIELDS, 12324, false));
               }
               else if (errorMode == CopyTableMode.OUTPUT_TABLE_PARAM_MODE ||
                        errorMode == CopyTableMode.OUTPUT_TABLE_HANDLE_PARAM_MODE)
               {
                  if (!srcResultSet.isTableHandle())
                  {
                     throw new InvalidParameterConditionException(
                           new NumberedException(ERROR_MISMATCHED_FIELDS, 12324, false));
                  }
                  else
                  {
                     String errorText = String.format(ERROR_FIELD_TYPE_MISMATCH_TEMPLATE,
                                                      dstTempTable.name().toStringMessage(),
                                                      i + 1);
                     ErrorManager.displayError(8028, errorText, false);
                     throw new InvalidParameterConditionException(
                           new NumberedException(ERROR_MISMATCHED_FIELDS, 12324, false));
                  }
               }
               else
               {
                  throw new IllegalStateException("Wrong table copy error mode: " + errorMode);
               }
            }
            
            // compare extent sizes
            Integer dstExtent = dstExtents.get(dstPropName);
            long srcExtentSize = property.getExtent();
            if (srcExtentSize < 1)
            {
               srcExtentSize = 1;
            }
            
            long dstExtentSize = dstExtent == null ? 1 : dstExtent.intValue();
            
            if (srcExtentSize != dstExtentSize)
            {
               if (errorMode == CopyTableMode.INPUT_PARAM_MODE)
               {
                  throw new InvalidParameterConditionException(
                     new NumberedException(ERROR_MISMATCHED_FIELDS, 12324, false));
               }
               else if (errorMode == CopyTableMode.OUTPUT_TABLE_PARAM_MODE ||
                        errorMode == CopyTableMode.OUTPUT_TABLE_HANDLE_PARAM_MODE)
               {
                  if (!srcResultSet.isTableHandle())
                  {
                     throw new InvalidParameterConditionException(
                        new NumberedException(ERROR_MISMATCHED_FIELDS, 12324, false));
                  }
                  else
                  {
                     String errorText = String.format(ERROR_EXTENT_MISMATCH_TEMPLATE,
                                                      dstTempTable.name().toStringMessage(),
                                                      i + 1);
                     ErrorManager.displayError(8029, errorText, false);
                     throw new InvalidParameterConditionException(
                        new NumberedException(ERROR_MISMATCHED_FIELDS, 12324, false));
                  }
               }
               else
               {
                  throw new IllegalStateException("Wrong table copy error mode: " + errorMode);
               }
            }
         }
      }
      
      Map<Long, TempRecord> dstNewIds = processB4 ? new HashMap<>() : null;
      Map<Long, TempRecord> dstB4NewIds = processB4 ? new HashMap<>() : null;
      
      Persistence persistence = dstBuf.getPersistence();
      boolean savedAny = false;
      boolean savedB4Any = false;
      int commit = 0;   // 0 = do nothing, 1 = commit, -1 = rollback
      try
      {
         commit = (persistence.beginTransaction(Persistence.TEMP_CTX) ? 1 : 0);
         
         // if not in input-output mode, first remove all records
         if (!append)
         {
            if (!dstBuf.forceLoopingDelete())
            {
               dstBuf.removeRecords(null, null, null);
               if (processB4)
               {
                  dstB4Buf.removeRecords(null, null, null);
               }
            }
            else
            {
               dstBuf.loopDelete(null, null, null);
               if (processB4)
               {
                  dstB4Buf.loopDelete(null, null, null);
               }
            }
         }
         
         savedAny = createRecords(srcResultSet,
                                  dstBuf, 
                                  dstNewIds, 
                                  dstProperties,
                                  dstSetters, 
                                  dstExtents);
         
         if (processB4 && srcResultSet.getPeerDef() != null)
         {
            savedB4Any = createRecords(srcResultSet.getPeerDef(),
                                       dstB4Buf, 
                                       dstB4NewIds,
                                       dstProperties,
                                       dstB4Setters,
                                       dstB4Extents);
            
            for (TempRecord dstB4Ttr : dstB4NewIds.values())
            {
               TempRecord dstTtr = dstNewIds.get(dstB4Ttr._peerRowid());
               
               if (dstTtr != null)
               {
                  // remap the links; this can be null in case of deleted records (row-state = ROW_DELETED)
                  dstB4Ttr._peerRowid(dstTtr.primaryKey());
                  dstTtr._peerRowid(dstB4Ttr.primaryKey());
               }
            }
         }
         
         if (dstNewIds != null)
         {
            for (TempRecord ttr : dstNewIds.values())
            {
               persistence.save(ttr, ttr.primaryKey());
            }
         }
         
         if (dstB4NewIds != null)
         {
            for (TempRecord ttr : dstB4NewIds.values())
            {
               persistence.save(ttr, ttr.primaryKey());
            }
         }
      }
      catch (PersistenceException exc)
      {
         commit *= -1;
         DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
         ErrorManager.recordOrThrowError(exc);
      }
      catch (IllegalAccessException | InstantiationException exc)
      {
         commit *= -1;
         
         throw new RuntimeException("Error creating record", exc);
      }
      catch (InvocationTargetException exc)
      {
         commit *= -1;
         DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
         
         throw new RuntimeException("Error creating record", exc);
      }
      finally
      {
         if (savedAny)
         {
            dstBuf.local.nonEmptyTableFlags.add(dstBuf.multiplexID);
         }
         if (savedB4Any)
         {
            dstB4Buf.local.nonEmptyTableFlags.add(dstB4Buf.multiplexID);
         }
         
         try
         {
            switch (commit)
            {
               case 1:
                  persistence.commit(Persistence.TEMP_CTX);
                  break;
               case -1:
                  persistence.rollback(Persistence.TEMP_CTX);
                  break;
               case 0:
                  break;
            }
         }
         catch (PersistenceException exc)
         {
            DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
            ErrorManager.recordOrThrowError(exc);
         }
      }
      
      return true;
   }
   
   /**
    * Create records from the given result set.
    *  
    * @param    srcResultSet
    *           The source result set.
    * @param    dstBuf
    *           The destination DMO buffer.
    * @param    dstNewIds
    *           Map to save the new record IDs.  May be {@code null}, in which case the 
    *           record is saved immediately; otherwise, the save will be postponed, to be able to
    *           cross-link with the before-table meta information.
    * @param    dstProperties
    *           The properties to save.
    * @param    dstSetters
    *           The property setters.
    * @param    dstExtents
    *           The property extents.
    *            
    * @return   {@code true} if at least one record was added.
    */
   private static boolean createRecords(TableWrapper          srcResultSet,
                                        TemporaryBuffer       dstBuf,
                                        Map<Long, TempRecord> dstNewIds,
                                        String[]              dstProperties,
                                        Map<String, Method>   dstSetters,
                                        Map<String, Integer>  dstExtents) 
   throws InstantiationException, 
          IllegalAccessException,
          InvocationTargetException,
          PersistenceException
   {
      DatumAccess[] datums = new DatumAccess[dstProperties.length];
      for (int i = 0; i < dstProperties.length; i++)
      {
         String dstProp = dstProperties[i];
         
         DatumAccess da = dstBuf.getSetterDatums().get(dstProp);
         
         if (da == null)
         {
            Method dstSetter = dstSetters.get(dstProp);
            PropertyMeta dstMeta = dstBuf.getDmoInfo().getPropsBySetterMap().get(dstSetter);
            boolean dstAssignTrigger =
                     dstSetter.getReturnType() != Void.class &&
                     dstMeta.getOriginalExtent() == 0        &&
                     dstBuf.getTriggerTracker() != null      &&
                     dstBuf.getTriggerTracker().isTriggerEnabled(DatabaseEventType.ASSIGN, 
                                                                 dstProp, 
                                                                 dstBuf.getLogicalDatabase());
            da = new DatumAccess(dstProp, 
                                 null, 
                                 dstSetter, 
                                 dstExtents.get(dstProp), 
                                 dstMeta,
                                 dstBuf.isPropertyIndexed(dstMeta.getOffset()),
                                 dstAssignTrigger);
            dstBuf.getSetterDatums().put(dstProp, da);
         }
         
         datums[i] = da;
      }
      
      boolean savedAny = false;
      Persistence persistence = dstBuf.getPersistence();
      boolean fromJson = srcResultSet.isFromJson();
      
      Map<String, Class<? extends BaseDataType>> legacyToType = null;
      Map<String, Integer> legacyToIndex = null;
      if (fromJson)
      {
         legacyToType = new HashMap<>();
         legacyToIndex = new HashMap<>();
         
         List<LegacyFieldInfo> legacyFields = TableMapper.getAllLegacyFieldInfo(dstBuf.tempTableRef);
         for (LegacyFieldInfo lfi : legacyFields)
         {
            legacyToType.put(lfi.getLegacyName().toLowerCase(), lfi.getDataType());
         }
         
         for (int i = 0; i < dstProperties.length; i++)
         {
            String dstPropName = dstProperties[i];
            String legacyName = dstBuf.getDmoInfo().getFieldInfo(dstPropName).legacyLower;
            legacyToIndex.put(legacyName, i);
         }
      }
      
      // in next lines the TempRecord [dstRec] is processed at lower-level than usual in order to avoid the
      // extra cost of extra maintenance (locking, triggers, dirty checks) which is not expected to happen
      // as result of these record being inserted into the database. However, the index must be updated and
      // the [ffCache] must be in synch, so we will invalidate all its data:
      dstBuf.invalidateFFCache(0);
      
      ArrayList<PropertyDefinition> srcProperties = srcResultSet.getProperties();
      Iterator<Object[]> rowIter = srcResultSet.rowIterator();
      Iterator<Object[]> rowMetaIter = srcResultSet.rowMetaIterator();
      while (rowIter.hasNext())
      {
         Object[] row = rowIter.next();
         Object[] rowMeta = rowMetaIter.next();
         
         TempRecord dstRec = dstBuf.instantiateDMO();
         dstRec.initialize(dstBuf.getSession(), false);
         Long id = dstBuf.nextPrimaryKey();
         
         if (fromJson)
         {
            Object[] newRow = new Object[dstProperties.length];
            for (int i = 0; i < srcProperties.size(); i++)
            {
               PropertyDefinition srcProp = srcProperties.get(i);
               String legacyName = srcProp.getLegacyName().toLowerCase();
               
               Object data = row[i];
               Integer newIdx = legacyToIndex.get(legacyName);
               if (newIdx == null)
               {
                  LOG.log(Level.WARNING, "In createRecords(): unknown legacy name: " + legacyName);
                  continue;
               }
               
               if (data instanceof BaseDataType || (data != null && data.getClass().isArray()))
               {
                  // SOAP requests build the fields as BDT already; the marker 'fromJson' remains.
                  newRow[newIdx] = data;
               }
               else
               {
                  BaseDataType var = BaseDataType.generateUnknown(legacyToType.get(legacyName));
                  if (data != null)
                  {
                     if (srcProp.getType() == unknown.class)
                     {
                        // this is a marker for de-serializing explicitly for real JSON data
                        
                        switch (var.getClass().getSimpleName())
                        {
                           case "date":
                              var.assign(date.parseLiteral(data.toString()));
                              break;
                           case "datetime":
                              var.assign(datetime.parseLiteral(data.toString()));
                              break;
                           case "datetimetz":
                              var.assign(datetimetz.parseLiteral(data.toString()));
                              break;
                              
                           case "blob":
                           case "raw":
                           case "memptr":
                              ((BinaryData) var).assign(Base64.safeBase64ToByteArray(data.toString()));
                              break;
                              
                           default:
                              Stream.assignDatum(var, data.toString(), true);
                        }
                     }
                     else
                     {
                        Stream.assignDatum(var, data.toString(), true);
                     }
                  }
                  
                  newRow[newIdx] = var;
               }
            }
            
            row = newRow;
         }
         
         for (int i = 0; i < datums.length; i++)
         {
            DatumAccess da = datums[i];
            Method setter = da.getAccessor();
            Integer extent = da.getExtent();
            if (extent == null)
            {
               Object data = row[i];
               if (data instanceof unknown)
               {
                  data = BaseDataType.generateUnknown(da.getPropertyMeta().getType());
               }
               Utils.invoke(setter, dstRec, data);
            }
            else
            {
               // indexed setter invocation
               BaseDataType[] data = (BaseDataType[]) row[i];
               int size = extent;
               for (int idx = 0; idx < size; idx++)
               {
                  Object _data = data[idx];
                  if (_data instanceof unknown)
                  {
                     _data = BaseDataType.generateUnknown(da.getPropertyMeta().getType());
                  }
                  Utils.invoke(setter, dstRec, idx, _data);
               }
            }
         }
         
         // set the rowState, errorString and errorFlag
         dstRec._rowState((Integer) rowMeta[TempRecord._ROW_STATE_DATA_INDEX]);
         dstRec._peerRowid((Long) rowMeta[TempRecord._PEER_ROWID_DATA_INDEX]); // this will be processed later on
         // TODO: originRowId - does it have any meaning?
         // TODO: datasourceRowID - does it have any meaning?
         dstRec._errorFlags((Integer) rowMeta[TempRecord._ERROR_FLAG_DATA_INDEX]);
         dstRec._errorString((String) rowMeta[TempRecord._ERROR_STRING_DATA_INDEX]);
         
         savedAny = true;
         
         if (dstNewIds != null)
         {
            dstRec.primaryKey(id);
            dstNewIds.put((Long) rowMeta[TempRecord._PK_DATA_INDEX], dstRec);
         }
         else
         {
            persistence.save(dstRec, id);
         }
      }
      
      return savedAny;
   }
   
   /**
    * Mark the specified temp-tables as having the REFERENCE-ONLY option.
    * 
    * @param    dmos
    *           Variable length list of DMO instances returned by previous calls to {@link #define}.
    */
   public static void referenceOnly(Temporary... dmos)
   {
      for (Temporary dmo : dmos)
      {
         TemporaryBuffer buf = ((TemporaryBuffer) get(dmo));
         buf.referenceOnly = true;
         if (buf.master != null)
         {
            buf = buf.master;
         }
         
         Set<TemporaryBuffer> explicitBuffers = buf.getAllSlaveBuffers();
         if (explicitBuffers != null)
         {
            for (TemporaryBuffer buf2 : explicitBuffers)
            {
               buf2.referenceOnly = true;
            }
         }
      }
   }
   
   /**
    * Register code page expression for the CLOB field. Code page will be evaluated and applied in
    * {@link #openScope()}.
    *
    * @param dmo
    *        DMO instance returned by previous calls to {@link #define}.
    * @param property
    *        Hibernate property name of the CLOB field.
    * @param codePageExpr
    *        Code page expression.
    */
   public static void registerCodePage(Temporary dmo, String property, Supplier codePageExpr)
   {
      ((TemporaryBuffer) get(dmo)).registerCodePage(property, codePageExpr);
   }
   
   /**
    * Open scope for the dynamic buffers which back the specified DMO instances at the given block
    * depth.
    *
    * @param blockDepth
    *        Zero-based depth of the block (starting from the outermost scope) 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);
      }
   }
   
   /**
    * Read all rows from the table targeted by the specified buffer and return each row as an
    * array, each row's values in the ordered specified by the given property list. 
    * 
    * @param    srcDMO
    *           The source DMO used to read the data.
    * @param    props
    *           The properties needed for this DMO.
    *
    * @return   The table rows, in a 2-element array; on index 0, the record; on index 1, the 
    *           record's meta information.
    */
   protected static List<Object[][]> readAllRows(Temporary srcDMO, ArrayList<PropertyDefinition> props)
   {
      // get source buffer, which must be a TemporaryBuffer instance
      TemporaryBuffer srcBuf = (TemporaryBuffer) ((BufferReference) srcDMO).buffer();
      Map<String, Method> getters = srcBuf.getDmoInfo().getPojoGetterMap();
      Map<Method, PropertyMeta> metas = srcBuf.getDmoInfo().getPropsByGetterMap();
      
      List<Object[][]> data = new ArrayList<>();
      
      // row handler which stores an array of property values for each row found in the list to
      // be returned
      Consumer<TempRecord> worker = (dmo) ->
      {
         try
         {
            int idx = 0;
            Object[] row = new Object[props.size()];
            for (int j = 0; j < props.size(); j++)
            {
               PropertyDefinition property = props.get(j);
               Method getter = getters.get(property.getName());
               PropertyMeta meta = metas.get(getter);
               DataHandler handler = meta.getDataHandler();
               int offset = meta.getOffset();
               
               if (property.isExtent())
               {
                  int extentSize = property.getExtent();
                  BaseDataType[] extentArray = new BaseDataType[extentSize];
                  
                  for (int i = 0; i < extentSize; i++)
                  {
                     extentArray[i] = handler.getField(dmo, offset + i);
                  }
                  
                  row[idx++] = extentArray;
               }
               else
               {
                  Object bdt = handler.getField(dmo, offset);
                  if (bdt instanceof blob)
                  {
                     ((blob) bdt).readData();
                  }
                  else if (bdt instanceof clob)
                  {
                     ((clob) bdt).readData();
                  }
                  
                  row[idx++] = bdt;
               }
            }
            
            Object[] rowMeta = dmo.toArray();
            
            data.add(new Object[][] { row, rowMeta });
         }
         catch (SQLException exc)
         {
            throw new RuntimeException("Error reading DMO property", exc);
         }
      };
      
      srcBuf.readAllRows(worker);
      
      return data;
   }
   
   /**
    * Create a dynamic buffer for the temporary table. For creation of default buffers for dynamic
    * tables use {@link #createDefaultDynamicBufferForTempTable(TempTableBuilder)}.
    * <p>
    * Because the buffer is created dynamically it is bound to global scope, allowing it to survive the scope
    * where it was created. because of this, it must be manually finalized.
    *
    * @param  bufferName
    *         Target buffer name (4GL form).
    * @param  master
    *         Master buffer (default buffer of the target table).
    *
    * @return DMO proxy of the created buffer.
    */
   protected static Temporary createDynamicBufferForTempTable(String bufferName, TemporaryBuffer master)
   {
      if (master.mutableHandler != null && master.mutableHandler.bound != null)
      {
         // if the buffer is bound to another buffer, use that.
         master = (TemporaryBuffer) ((BufferImpl) master.mutableHandler.bound).buffer();
      }
      
      return createDynamicBufferForTempTable(bufferName, master, master.getParentTable());
   }
   
   /**
    * Create the default dynamic buffer for the dynamic temporary table.
    *
    * @param  tempTable
    *         The temp-table builder object associated with the target table.
    *
    * @return DMO proxy of the created buffer.
    */
   protected static Temporary createDefaultDynamicBufferForTempTable(TempTableBuilder tempTable)
   {
      return createDynamicBufferForTempTable(tempTable.name().getValue(), null, tempTable);
   }
   
   /**
    * Create a dynamic buffer for the temporary table.
    * <p>
    * Because the buffer is created dynamically it is bound to global scope, allowing it to survive the scope
    * where it was created. because of this, it must be manually finalized.
    *
    * @param  bufferName
    *         Target buffer name (4GL form).
    * @param  master
    *         Master buffer (default buffer of the target table). Can be {@code null} if the
    *         default buffer for the table is created.
    * @param  tempTable
    *         The temp-table object associated with the target table.
    *
    * @return DMO proxy of the created buffer.
    */
   private static Temporary createDynamicBufferForTempTable(String bufferName,
                                                            TemporaryBuffer master,
                                                            TempTable tempTable)
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         return CREATE_BUFFER.timerWithReturn(
            () -> createDynamicBufferForTempTableImpl(bufferName, master, tempTable));
      }
      else
      {
         return createDynamicBufferForTempTableImpl(bufferName, master, tempTable);
      }
   }

   /**
    * Create a dynamic buffer for the temporary table.
    * <p>
    * Because the buffer is created dynamically it is bound to global scope, allowing it to survive the scope
    * where it was created. because of this, it must be manually finalized.
    *
    * @param  bufferName
    *         Target buffer name (4GL form).
    * @param  master
    *         Master buffer (default buffer of the target table). Can be {@code null} if the
    *         default buffer for the table is created.
    * @param  tempTable
    *         The temp-table object associated with the target table.
    *
    * @return DMO proxy of the created buffer.
    */
   private static Temporary createDynamicBufferForTempTableImpl(String bufferName,
                                                                TemporaryBuffer master,
                                                                TempTable tempTable)
   {
      Temporary temporary;
      final BufferManager bufferManager = master != null ? master.bufferManager : BufferManager.get();
      String uniqueBufferName = bufferManager.generateBufferName(bufferName);
      if (master != null)
      {
         temporary = TemporaryBuffer.define(null,
                                            (Temporary) master.getDMOProxy(),
                                            uniqueBufferName,
                                            bufferName,
                                            0,
                                            false,
                                            false);
      }
      else
      {
         temporary = TemporaryBuffer.define(null,
                                            tempTable.getDMOBufInterface(),
                                            uniqueBufferName,
                                            bufferName,
                                            true,
                                            tempTable,
                                            0,
                                            false);
      }
      TemporaryBuffer temporaryBuffer = ((TemporaryBuffer) RecordBuffer.get(temporary));
      temporaryBuffer.setDynamic();
      temporaryBuffer.setTempTableRef(tempTable);
      ((BufferImpl) temporary).interlink(); // HandleChain implementation
      TemporaryBuffer.openScopeAt(0 /*blockDepth*/, temporary);

      return temporary;
   }

   /**
    * Factory method to construct a new temporary buffer.
    *
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API.  This must be a
    *          subinterface of {@link Temporary}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   global
    *          <code>true</code> if the buffer survives until end of context
    *          life;  <code>false</code> if it expires upon leaving the top
    *          level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          buffer.
    *
    * @return  A new instance of this class.
    */
   private static <T extends TempTableBuffer>
   TemporaryBuffer makeBuffer(Class<?> def,
                              Class<T> dmoBufIface,
                              String variable,
                              boolean global,
                              UndoStateProvider undoable)
   {
      return makeBuffer(def, dmoBufIface, variable, global, undoable, -1);
   }
   
   /**
    * Factory method to construct a new temporary buffer.
    *
    * @param   def
    *          The currently executing OE class or external program.
    * @param   dmoBufIface
    *          Interface which defines DMO's public API. This must be a subinterface of
    *          {@link TempTableBuffer}.
    * @param   variable
    *          Name of the variable with which the record buffer will be
    *          associated in the transaction manager.
    * @param   global
    *          {@code true} if the buffer survives until end of context life;  {@code false} if it
    *          expires upon leaving the top level procedure scope.
    * @param   undoable
    *          An {@code UndoStateProvider} that manages the UNDO status of the table of this
    *          buffer.
    * @param   blockDepth
    *          Zero-based depth of the block (starting from the outermost scope) at which the
    *          buffer is defined or {@code -1} for the current scope.
    *
    * @return  A new instance of this class.
    */
   private static <T extends TempTableBuffer>
   TemporaryBuffer makeBuffer(Class<?> def,
                              Class<T> dmoBufIface,
                              String variable,
                              boolean global,
                              UndoStateProvider undoable,
                              int blockDepth)
   {
      if (!DatabaseManager.isTempTable(dmoBufIface))
      {
         // will never happen due [dmoBufIface] extends [TempTableBuffer] 
         throw new IllegalArgumentException("DMO interface must extend Temporary");
      }
      
      return new TemporaryBuffer(def, dmoBufIface, variable, global, undoable, blockDepth);
   }
   
   /**
    * Copy all records from one temp table to another, optionally appending
    * to records already present in the destination table.
    * <p>
    * TODO:  it seems this method can be optimized to use a DMO's deepCopy()
    * method instead of reflection when the source and destination DMO types
    * are the same.
    *
    * @param   srcDMO
    *          DMO instance returned by a previous call to {@link #define} on
    *          the source temporary buffer.
    * @param   dstDMO
    *          DMO instance returned by a previous call to {@link #define} on
    *          the destination temporary buffer.
    * @param   append
    *          <code>true</code> to add new records without affecting
    *          existing records;  <code>false</code> to remove all existing
    *          records before the copy takes place.
    * @param   errorMode
    *          Table copy mode, affects error handling.
    *
    * @return  <code>true</code> if copy was successful. <code>false</code> if validation error
    *          has occurred or the table is appended to itself.
    *
    * @throws  ErrorConditionException
    *          if a recoverable error occurs copying records.
    * @throws  RuntimeException
    *          if an unrecoverable error occurs invoking methods on the
    *          underlying DMOs to get or set data values.
    */
   static boolean copyAllRows(Temporary srcDMO, Temporary dstDMO, boolean append, CopyTableMode errorMode)
   {
      return copyAllRows(null, srcDMO, dstDMO, append, errorMode, false, false, false);
   }
   
   /**
    * Copy all records from one temp table to another, optionally appending
    * to records already present in the destination table.
    * <p>
    * TODO:  it seems this method can be optimized to use a DMO's deepCopy()
    * method instead of reflection when the source and destination DMO types
    * are the same.
    *
    * @param   srcRec
    *          The {@link rowid} of the current record in the source buffer - if not null, it will
    *          be set in the destination buffer, too.
    * @param   srcDMO
    *          DMO instance returned by a previous call to {@link #define} on
    *          the source temporary buffer.
    * @param   dstDMO
    *          DMO instance returned by a previous call to {@link #define} on
    *          the destination temporary buffer.
    * @param   append
    *          <code>true</code> to add new records without affecting
    *          existing records;  <code>false</code> to remove all existing
    *          records before the copy takes place.
    * @param   errorMode
    *          Table copy mode, affects error handling.
    *
    * @return  <code>true</code> if copy was successful. <code>false</code> if validation error
    *          has occurred or the table is appended to itself.
    *
    * @throws  ErrorConditionException
    *          if a recoverable error occurs copying records.
    * @throws  RuntimeException
    *          if an unrecoverable error occurs invoking methods on the
    *          underlying DMOs to get or set data values.
    */
   static boolean copyAllRows(rowid     srcRec,
                              Temporary srcDMO,
                              Temporary dstDMO,
                              boolean append,
                              CopyTableMode errorMode)
   {
      return copyAllRows(srcRec, srcDMO, dstDMO, append, errorMode, false, false, false);
   }
   
   /**
    * Copy all records from one temp table to another, optionally appending to records already present in the
    * destination table.
    *
    * @param   srcRecId
    *          The {@link rowid} of the current record in the source buffer - if not null, it will be set in
    *          the destination buffer, too.
    * @param   srcDMO
    *          DMO instance returned by a previous call to {@link #define} on the source temporary buffer.
    * @param   dstDMO
    *          DMO instance returned by a previous call to {@link #define} on the destination temporary
    *          buffer.
    * @param   append
    *          {@code true} to add new records without affecting existing records;
    *          {@code false} to remove all existing records before the copy takes place.
    * @param   errorMode
    *          Table copy mode, affects error handling.
    * @param   skipUniqueConflicts
    *          Corresponds append-mode in COPY-TEMP-TABLE. If there is a unique index on the target temp-table
    *          and a row with a duplicate key is found, the row is not replaced. This parameter is ignored if
    *          {@code replaceMode} is {@code true}.
    * @param   replaceMode
    *          Corresponds replace-mode in COPY-TEMP-TABLE. When {@code true} records in the target temp-table
    *          are replaced with corresponding records from the source temp-table. The target temp-table must
    *          have a unique primary index that be can used to find the corresponding record. When the
    *          corresponding record is found in the target temp-table, the target record is replaced with the
    *          source record.
    * @param   looseCopy
    *          Corresponds loose-copy-mode in COPY-TEMP-TABLE. Only the fields with the same name that appear
    *          in both the source and target temp-table are copied. Other fields are ignored.
    *          <p><b>Note:</b><br>
    *          Because of an undocumented bug in OE, if the {@code replaceMode} is {@code true} the value will
    *          'leak' and overwrite this parameter. As result, a temp-table can be copied to another,
    *          incompatible temp-table even if {@code looseCopy} is explicitly set to {@code false}, just by
    *          passing {@code true} for the previous parameter. 
    *
    * @return  {@code true} if copy was successful. 
    *          {@code false} if validation error has occurred or the table is appended to itself.
    *
    * @throws  ErrorConditionException
    *          if a recoverable error occurs copying records.
    * @throws  RuntimeException
    *          if an unrecoverable error occurs invoking methods on the underlying DMOs to get or set data
    *          values.
    */
   static boolean copyAllRows(rowid srcRecId,
                              Temporary srcDMO,
                              Temporary dstDMO,
                              boolean append,
                              CopyTableMode errorMode,
                              boolean skipUniqueConflicts,
                              boolean replaceMode,
                              boolean looseCopy)
   {
      if (replaceMode)
      {
         // see the javadoc note above for [looseCopy] parameter 
         looseCopy = true;
      }
      
      // get source and destination buffers, both of which must be TemporaryBuffer instances
      TemporaryBuffer srcBuf = (TemporaryBuffer) ((BufferReference) srcDMO).buffer();
      TemporaryBuffer dstBuf = (TemporaryBuffer) ((BufferReference) dstDMO).buffer();
      
      // we need to check if this is the real bound buffer or not
      if (srcBuf.mutableHandler != null && srcBuf.mutableHandler.boundBuffer != null)
      {
         srcDMO = srcBuf.mutableHandler.bound;
         srcBuf = srcBuf.mutableHandler.boundBuffer;
      }
      if (dstBuf.mutableHandler != null && dstBuf.mutableHandler.boundBuffer != null)
      {
         dstDMO = dstBuf.mutableHandler.bound;
         dstBuf = dstBuf.mutableHandler.boundBuffer;
      }
      
      srcBuf.initialize();
      dstBuf.initialize();
                  
      BufferImpl srcB4DMO = null;
      BufferImpl dstB4DMO = null;
      TemporaryBuffer srcB4Buf = null;
      TemporaryBuffer dstB4Buf = null;
      boolean before = false;
      
      BufferImpl dstBufImpl = (BufferImpl) dstDMO;
      BufferImpl srcBufImpl = (BufferImpl) srcDMO;
      if (srcBufImpl.isAfterBuffer() && dstBufImpl.isAfterBuffer())
      {
         srcB4DMO = (BufferImpl) srcBufImpl.beforeBufferNative(); 
         dstB4DMO = (BufferImpl) dstBufImpl.beforeBufferNative();
         srcB4Buf = (TemporaryBuffer) ((BufferReference) srcB4DMO).buffer();
         dstB4Buf = (TemporaryBuffer) ((BufferReference) dstB4DMO).buffer();
         
         srcB4Buf.initialize();
         dstB4Buf.initialize();
         before = true;
      }
      
      boolean inTx = false;
      boolean shouldRollback = false;
      
      try
      {
         inTx = dstBuf.persistenceContext.beginTransaction(null);
         srcBuf.flush();
         // Flushing will not guarantee that non-indexed fields are also flushed.
         // This check will make sure that the any such scenario with an available
         // key in the nursery is made visible before we copy the records.
         RecordNursery srcNursery = srcBuf.getPersistenceContext().getNursery();
         if (srcNursery.containsRecordBufferDmo(srcBuf))
         {
            srcNursery.makeVisible(srcBuf.getDmoInfo().getId(), srcBuf.getMultiplexID(), 0);
         }
         
         dstBuf.flush();
         
         if (before)
         {
            srcB4Buf.flush();
            dstB4Buf.flush();
         }
         
         if (inTx)
         {
            try
            {
               dstBuf.persistenceContext.commit();
               shouldRollback = false;
            }
            catch (PersistenceException exc)
            {
               // TODO: what to do here?
               if (LOG.isLoggable(Level.WARNING))
               {
                  LOG.log(Level.WARNING, "Error flushing before buffers", exc);
               }
               shouldRollback = true;
            }
         }
      }
      catch (ValidationException | PersistenceException exc)
      {
         if (LOG.isLoggable(Level.WARNING))
         {
            LOG.log(Level.WARNING, "Failed flushing the after buffers or before buffers on copyAllRows", exc);
         }

         shouldRollback = true;
         ErrorManager.recordOrThrowError(exc);
         
      }
      finally 
      {
          try
          {
             // clear the transaction somehow
             if (inTx && shouldRollback)
             { 
                dstBuf.persistenceContext.rollback(true);
             }
          }
          catch (PersistenceException exc)
          {
             DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
             ErrorManager.recordOrThrowError(exc);
          }
      }
      
      AbstractTempTable srcTempTable = srcBuf.getParentTable();
      AbstractTempTable dstTempTable = dstBuf.getParentTable();
      
      String srcBufName = ((HandleChain) srcDMO)._name();
      String dstBufName = ((HandleChain) dstDMO)._name();
      
      if (srcTempTable.equals(dstTempTable))
      {
         if (errorMode == CopyTableMode.COPY_TEMP_TABLE_MODE)
         {
            String msg = "Source and target are the same table in temp-table copy operation for " + 
                         dstBufName;
            ErrorManager.displayError(12301, msg, false);
            
            return false;
         }
         
         if (append)
         {
            String msg = srcTempTable._name() + " cannot be appended to itself";
            ErrorManager.recordOrShowError(5362, msg, false, false);
            
            return false;
         }
         
         return true;
      }
      

      boolean simpleCopy = srcBuf.getDMOImplementationClass().equals(dstBuf.getDMOImplementationClass());
      
      // simpleCopy can be true even if the tables names are different, but the sqlSignatures match
      if(!simpleCopy)
      {
         String srcSignature = srcBuf.getDmoInfo().getSignature().getSqlSignature();
         String dstSignature = dstBuf.getDmoInfo().getSignature().getSqlSignature();
         simpleCopy = srcSignature.equals(dstSignature);
      }
      
      // PROGRESS does not care if the field names are different; what it cares about are only the field type
      // and position - if a source field has the same position and type as the destination field, then they
      // are a match. If the source and destination fields at a certain position do not match, then PROGRESS
      // will throw an error and abend the application.
      
      // if the backing tables are different, check if the fields match
      Map<DatumAccess, DatumAccess> propsMap = null;
      Map<DatumAccess, DatumAccess> propsB4Map = null;
      
      if (!simpleCopy)
      {
         // we need to modify the setter properties, so instantiate a new map
         propsMap = createPropsMap(looseCopy, srcBufImpl, dstBufImpl, errorMode);
         if (propsMap == null)
         {
            return false;
         }
         
         if (before)
         {
            propsB4Map = createPropsMap(looseCopy, srcB4DMO, dstB4DMO, errorMode);
            
            if (propsB4Map == null)
            {
               return false;
            }
         }
      }
      else 
      {
         looseCopy = false;
      }
      
      Persistence persistence = srcBuf.getPersistence();
      String srcBufRenamed = null;
      boolean savedAny = false;
      int commit = 0;   // 0 = do nothing, 1 = commit, -1 = rollback
      boolean validationError = false;
      boolean copyError = false;
      rowid destRecId = null;
      
      try
      {
         commit = (persistence.beginTransaction(Persistence.TEMP_CTX) ? 1 : 0);
         boolean dstQueried = dstBuf.isQueried();
         boolean dstB4Queried = before && dstB4Buf.isQueried();
         
         // if not in append mode, first remove records (if any) from destination table.
         if (!append)
         {
            if (!dstBuf.hasOoDestructors() && !dstQueried)
            {
               dstBuf.removeRecords(null, null, null);
            }
            else
            {
               dstBuf.loopDelete(null, null, null);
            }
            if (before)
            {
               if (!dstB4Buf.hasOoDestructors() && !dstB4Queried)
               {
                  dstB4Buf.removeRecords(null, null, null);
               }
               else
               {
                  dstB4Buf.loopDelete(null, null, null);
               }
            }
         }
         
         if (!srcBuf.isTableDefinitelyEmpty())
         {
            // if the destination is empty, replace works as append
            boolean removeAppend = ((replaceMode || append) && dstBuf.isTableDefinitelyEmpty());
            boolean removeAppendB4 = ((replaceMode || append) && (dstB4Buf == null || dstB4Buf.isTableDefinitelyEmpty()));
            if (!dstQueried && ((removeAppend && removeAppendB4) || !replaceMode))
            {
               Context ctx = srcBuf.local;
               boolean doBeforeCopy = srcB4Buf != null && dstB4Buf != null && !srcB4Buf.isTableDefinitelyEmpty();
               
               FastCopyContext masterCopyContext = new FastCopyContext();
               masterCopyContext.setSimpleCopy(simpleCopy)
                                .setSourceBuffer(srcBuf)
                                .setDestinationBuffer(dstBuf)
                                .setPropsMap(propsMap)
                                .setAppendMode(removeAppend ? false : append)
                                .setLooseCopyMode(looseCopy)
                                .setForceMapping(doBeforeCopy || srcRecId != null)
                                .setIsBefore(false);
               FastCopyHelper masterCopyHelper = ctx.getFastCopyHelper(masterCopyContext);

               FastCopyHelper beforeCopyHelper = null;
               FastCopyContext beforeCopyContext = null;
               if (doBeforeCopy)
               {
                  beforeCopyContext = new FastCopyContext();
                  beforeCopyContext.setSimpleCopy(simpleCopy)   
                                   .setSourceBuffer(srcB4Buf)
                                   .setDestinationBuffer(dstB4Buf)
                                   .setPropsMap(propsB4Map)
                                   .setAppendMode(removeAppendB4 ? false : append)
                                   .setLooseCopyMode(looseCopy)
                                   .setForceMapping(true)
                                   .setIsBefore(true);
                  beforeCopyHelper = ctx.getFastCopyHelper(beforeCopyContext);
               }
               
               if (masterCopyHelper.canUseFastCopy() && 
                  (beforeCopyHelper == null || beforeCopyHelper.canUseFastCopy()))
               {
                  destRecId = masterCopyHelper.executeCopy(masterCopyContext, srcRecId);
                  if (doBeforeCopy)
                  {
                     beforeCopyHelper.executeCopy(beforeCopyContext, null);
                     
                     // we need to remap the copied records from the master table with the ones in the 
                     // before table
                     FastCopyHelper.updatePeerRecords(masterCopyHelper, 
                                                      masterCopyContext,
                                                      beforeCopyHelper,
                                                      beforeCopyContext);
                     beforeCopyHelper.clear(beforeCopyContext);
                  }
                  
                  masterCopyHelper.clear(masterCopyContext);
                  savedAny = true;
                  dstBuf.invalidateFFCache(0);
                  if (before)
                  {
                     dstB4Buf.invalidateFFCache(0);
                  }
                  
                  return true;
               }
            }
            
            String replaceQuery = null;
            
            if (replaceMode)
            {
               P2JIndex primaryIndex = dstBuf.getDmoInfo().getPrimaryIndex(true);
               if (primaryIndex == null || !primaryIndex.isUnique())
               {
                  ErrorManager.recordOrShowError(11885);
                  // A unique primary index is required in the target table, each field of which is mapped to some source field, in order to do a replace-mode or parent-mode COPY/GET/MERGE/FILL type of operation on a dataset table.
                  return false;
               }

               DmoMeta dstDMOInfo = dstBuf.getDmoInfo();
               DmoMeta srcDMOInfo = srcBuf.getDmoInfo();

               for (P2JIndexComponent comp : primaryIndex.components())
               {
                  // Get destination property
                  Property dstProp = dstDMOInfo.byLegacyName(comp.getLegacyName());

                  // Get source property
                  Property srcProp = srcDMOInfo.byLegacyName(comp.getLegacyName());
                  if (srcProp == null)
                  {
                     ErrorManager.recordOrShowError(11885);
                     return false;
                  }
                  // Compare types
                  if (!((dstProp._fwdType == srcProp._fwdType)                                 ||
                        (dstProp._fwdType == integer.class && srcProp._fwdType == int64.class) ||
                        (dstProp._fwdType == int64.class && srcProp._fwdType == integer.class)))
                  {
                     // Types do not match
                     ErrorManager.recordOrShowError(11885);
                     return false;
                  }
               }

               if (dstBuf.getLegacyName().equals(srcBuf.getLegacyName()))
               {
                  BufferManager bm = BufferManager.get();
                  // we have a name collision
                  srcBufRenamed = srcBuf.getLegacyName() + "_db_" + srcBuf.buffer().getDmoInfo().sqlTable;
                  srcBuf.overrideLegacyName(srcBufRenamed);
                  srcBuf.overrideDMOAlias(bm.generateBufferName(srcBufRenamed));
               }
                  
               StringBuilder sb = new StringBuilder("WHERE ");
               ArrayList<P2JIndexComponent> comps = primaryIndex.components();
               for (int i = 0; i < comps.size(); i++)
               {
                  P2JIndexComponent comp = comps.get(i);
                  if (i > 0)
                  {
                     sb.append(" and ");
                  }
                  
                  sb.append(dstBuf.getLegacyName()).append(".").append(comp.getLegacyName()).append("=")
                    .append(srcBufRenamed == null ? srcBuf.getLegacyName() : srcBufRenamed)
                                          .append(".").append(comp.getLegacyName());
               }
               
               replaceQuery = sb.toString();
            }
            
            // get the list of source records to be copied.  These will be all records in
            // srcBuf's backing temp table which have srcDMO's multiplex ID.
            FQLExpression fqlBuilder = new FQLExpression("from ");
            fqlBuilder.append(srcBuf.getDMOImplementationName());
            
            // srcBuf is a TemporaryBuffer, so it isMultiplexed()
            fqlBuilder.append(" where ");
            fqlBuilder.append(MULTIPLEX_FIELD_NAME);
            fqlBuilder.append(" = ", true);
            
            // force the records to be ordered by their ID
            // TODO: in copy-temp-table sorting order affects which record remains in constraints
            //       violation case. The correct sorting is by primary index if it presents or by
            //       the index with the lexicographically first name.
            fqlBuilder.append(" order by ");
            fqlBuilder.append(MULTIPLEX_FIELD_NAME);
            fqlBuilder.append(",");
            fqlBuilder.append(Session.PK);
            
            String fql = fqlBuilder.toFinalExpression();
            Object[] parms = new Object[] { srcBuf.getMultiplexID() };
            ScrollableResults<TempRecord> results =
                  persistence.scroll(null, fql, parms, 0, 0, ResultSet.TYPE_FORWARD_ONLY);
            
            BufferManager bufMgr = srcBuf.getBufferManager();
            
            try
            {
               while (results.next())
               {
                  // copy each source record into a new destination DMO instance
                  Object[] row = results.get();
                  TempRecord srcRec = (TempRecord) row[0];
                  srcBuf.loadRecord(srcRec);
                  TempRecord dstRec = null;
                  boolean createNew = true;
                  
                  if (replaceMode)
                  {
                     logical found = new logical();
                     String finalReplaceQuery = replaceQuery;
                     ErrorManager.nestedSilent(() -> found.assign(dstBufImpl._find(finalReplaceQuery,
                                                                                   BufferImpl.FindMode.UNIQUE,
                                                                                   LockType.EXCLUSIVE,
                                                                                   srcBufImpl)));
                     if (found.booleanValue())
                     {
                        // do not create a new record. An existing record matching the index was found and is
                        // loaded into buffer 
                        createNew = false;
                        dstRec = ((TempRecord) dstBufImpl.buffer().getCurrentRecord());
                     }
                  }
                  
                  if (createNew)
                  {
                     dstRec = dstBuf.instantiateDMO();
                     dstRec.initialize(dstBuf.getSession(), true);
                  }
                  
                  if (simpleCopy)
                  {
                     Long dstId = dstRec.primaryKey();
                     dstRec.copy(srcRec);
                     dstRec._multiplex(dstBuf.multiplexID);
                     dstRec.primaryKey(dstId);
                  }
                  else
                  {
                     logical response = copy((DataModelObject) srcRec,
                                             (DataModelObject) dstRec,
                                             propsMap,
                                             null);
                     if (!response.booleanValue())
                     {
                        copyError = true;
                        continue;
                     }
                  }
                  
                  bufMgr.evictDMOIfUnused(persistence, srcBuf.getPersistenceContext(), srcRec);
                  if (LOG.isLoggable(Level.FINEST))
                  {
                     String msg = "Copy temp table record from MPID " +
                                  srcBuf.getMultiplexID() + " to MPID " +
                                  dstBuf.getMultiplexID() + "...";
                     LOG.log(Level.FINEST, msg);
                     LOG.log(Level.FINEST, srcBuf.describe() + srcBuf.toString(srcRec));
                     LOG.log(Level.FINEST, dstBuf.describe() + dstBuf.toString(dstRec));
                  }
                  
                  Long id;
                  if (createNew)
                  {
                     id = dstBuf.nextPrimaryKey();
                     dstRec.primaryKey(id);
                  }
                  else
                  {
                     id = dstRec.primaryKey();
                  }
                  
                  // validate a record against the target table
                  try
                  {
                     dstBuf.validate(dstRec);
                  }
                  catch (ValidationException e)
                  {
                     try
                     {
                        if (createNew)
                        {
                           // this is important to dealloc the reserved PK
                           DirectAccessHelper.clearPrimaryKey(dstBuf.getSession(), dstRec);
                        }
                     }
                     catch (DirectAccessException dae)
                     {
                        LOG.log(Level.SEVERE, 
                               "Failed deleting the transient record on validation exception", dae);
                     }
                     if (skipUniqueConflicts)
                     {
                        continue;
                     }
                     else
                     {
                        ErrorManager.recordOrShowError(e.getNumber(), e.getMessage(), false);
                        validationError = true;
                        break;
                     }
                  }
                  
                  if (srcRecId != null && srcRecId.equals(new rowid(srcRec.primaryKey())))
                  {
                     destRecId = new rowid(id);
                  }
                  
                  if (before && srcRec._rowState() != null && srcRec._rowState() > 0)
                  {
                     if (srcB4DMO.findByRowID(new rowid(srcRec._peerRowid())).getValue())
                     {
                        TempRecord srcB4Rec = ((TemporaryBuffer) srcB4DMO.buffer()).getCurrentRecord();
                        TempRecord dstB4Rec = dstB4Buf.instantiateDMO();
                        dstB4Rec.initialize(dstB4Buf.getSession(), false);
                        
                        if (simpleCopy)
                        {
                           dstB4Rec.copy(srcB4Rec);
                           dstB4Rec._multiplex(dstB4Buf.multiplexID);
                        }
                        else
                        {
                           copy((DataModelObject) srcB4Rec, (DataModelObject) dstB4Rec, propsB4Map, null);
                        }
                        Long b4id = dstB4Buf.nextPrimaryKey();
                        dstB4Rec.primaryKey(b4id);
                        
                        // now interlink:
                        dstRec._rowState(srcRec._rowState());
                        dstB4Rec._rowState(srcRec._rowState()); // same
                        dstRec._peerRowid(b4id);
                        dstB4Rec._peerRowid(id);
                        
                        persistence.save(dstB4Rec, b4id);
                     }
                  }
                  
                  persistence.save(dstRec, id, true);
                  savedAny = true;
                  
                  // In replace mode, after the element is inserted, the cache needs to be invalidated
                  if(replaceMode)
                  {
                     dstBuf.invalidateFFCache(0);  
                  }
               }
            }
            catch (Throwable t) // debug block
            {
               LOG.log(Level.FINER, "", t);
               throw t;
            }
            finally
            {
               if (results != null)
               {
                  results.close();
               }
               
               // invalidate ff cache (see previous occurrence / instantiateDMO() for details)
               dstBuf.invalidateFFCache(0);
               if (before)
               {
                  dstB4Buf.invalidateFFCache(0);
               }
            }
         }
         
         if (before && !srcB4Buf.isTableDefinitelyEmpty())
         {
            // copy the DELETED records from srcB4Buf
            FQLExpression buf = new FQLExpression("from ");
            buf.append(srcB4Buf.getDMOImplementationName());
            buf.append(" where ");
            buf.append(MULTIPLEX_FIELD_NAME);
            buf.append(" = ", true);
            buf.append(" and _rowState = ");
            buf.append(String.valueOf(Buffer.ROW_DELETED));
            buf.append(" order by ");
            buf.append(Session.PK);
            
            String fql = buf.toFinalExpression();
            Object[] parms = new Object[] { srcB4Buf.getMultiplexID() };
            ScrollableResults<Record> results =
                  persistence.scroll(null, fql, parms, 0, 0, ResultSet.TYPE_FORWARD_ONLY);
            
            BufferManager bufMgr = srcB4Buf.getBufferManager();
            
            try
            {
               while (results.next())
               {
                  // copy each source record into a new destination DMO instance.
                  TempRecord srcB4Rec = (TempRecord) results.get()[0];
                  TempRecord dstB4Rec = dstB4Buf.instantiateDMO();
                  dstB4Rec.initialize(dstB4Buf.getSession(), false);
                  
                  if (simpleCopy)
                  {
                     dstB4Rec.copy(srcB4Rec);
                     dstB4Rec._multiplex(dstB4Buf.multiplexID);
                  }
                  else
                  {
                     copy((DataModelObject) srcB4Rec, (DataModelObject) dstB4Rec, propsB4Map, null);
                  }
                  
                  bufMgr.evictDMOIfUnused(persistence, srcB4Buf.getPersistenceContext(), srcB4Rec);
                  if (LOG.isLoggable(Level.FINEST))
                  {
                     String msg = "Copy temp table record from MPID " +
                                  srcB4Buf.getMultiplexID() + " to MPID " +
                                  dstB4Buf.getMultiplexID() + "...";
                     LOG.log(Level.FINEST, msg);
                     LOG.log(Level.FINEST, srcB4Buf.describe() + srcB4Buf.toString(srcB4Rec));
                     LOG.log(Level.FINEST, dstB4Buf.describe() + dstB4Buf.toString(dstB4Rec));
                  }
                  
                  Long id = dstB4Buf.nextPrimaryKey();
                  dstB4Rec.primaryKey(id);
                  dstB4Rec._rowState(Buffer.ROW_DELETED); // this is what we searched for
                  // dstB4Rec._peerRowid(id); TODO: why was this set? There is no peer ID here
                  
                  persistence.save(dstB4Rec, id);
                  savedAny = true;
               }
            }
            finally
            {
               if (results != null)
               {
                  results.close();
               }
               
               // in case of only DELETE records the other invalidations will not be reached
               dstB4Buf.invalidateFFCache(0);
            }
         }
      }
      catch (PersistenceException exc)
      {
         commit *= -1;
         DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
         ErrorManager.recordOrThrowError(exc);
      }
      catch (IllegalAccessException | InstantiationException exc)
      {
         commit *= -1;
         
         throw new RuntimeException("Error creating record", exc);
      }
      catch (InvocationTargetException exc)
      {
         commit *= -1;
         DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
         
         throw new RuntimeException("Error creating record", exc);
      }
      catch (StopConditionException exc)
      {
         // thrown by Persistence when a JDBC-level error was encountered; Hibernate session will
         // have been closed, don't try to commit or rollback below
         commit = 0;
         
         if (LOG.isLoggable(Level.SEVERE))
         {
            LOG.log(Level.SEVERE, "Error copying all temp table records", exc);
         }
         
         throw exc;
      }
      finally
      {
         if (savedAny)
         {
            dstBuf.local.nonEmptyTableFlags.add(dstBuf.multiplexID);
            if (before)
            {
               dstB4Buf.local.nonEmptyTableFlags.add(dstB4Buf.multiplexID);
            }
         }
         
         try
         {
            switch (commit)
            {
               case 1:
                  persistence.commit(Persistence.TEMP_CTX);
                  break;
               case -1:
                  persistence.rollback(Persistence.TEMP_CTX);
                  break;
               case 0:
                  break;
            }
         }
         catch (PersistenceException exc)
         {
            DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
            ErrorManager.recordOrThrowError(exc);
         }
         
         if (srcBufRenamed != null)
         {
            srcBuf.restoreLegacyName();
            srcBuf.restoreDMOAlias();
         }
         
         if (destRecId != null)
         {
            ((Buffer) dstDMO).findByRowID(destRecId);
         }
      }
      if (validationError || copyError)
      {
         //Error found during COPY-TEMP-TABLE operation
         ErrorManager.recordOrShowError(12304,
                                        ((BufferImpl) srcDMO).doGetName(),
                                        ((BufferImpl) dstDMO).doGetName());
      }
      
      return !(validationError || copyError);
   }
   
   /**
    * Create a {@code DatumAccess} mapping for the source and destination DMOs.
    * 
    * @param    looseCopy
    *           Corresponds loose-copy-mode in COPY-TEMP-TABLE. Only the fields with the same name
    *           that appear in both the source and target temp-table are copied. Other fields are
    *           ignored.
    * @param    srcDMO
    *           The source buffer.
    * @param    dstDMO
    *           The destination buffer.
    * @param    errorMode
    *           Table copy mode, affects error handling.
    *           
    * @return   See above.
    */
   private static Map<DatumAccess, DatumAccess> createPropsMap(boolean looseCopy, 
                                                               BufferImpl srcDMO,
                                                               BufferImpl dstDMO,
                                                               CopyTableMode errorMode)
   {
      TemporaryBuffer srcBuf = (TemporaryBuffer) srcDMO.buffer();
      TemporaryBuffer dstBuf = (TemporaryBuffer) dstDMO.buffer();
      
      TempTable srcTempTable = srcBuf.getParentTable();
      TempTable dstTempTable = dstBuf.getParentTable();
      
      String srcBufName = srcDMO._name();
      String dstBufName = dstDMO._name();
      
      String[] srcProperties = TempTableBuilder.getOrderedPropertyNames(srcTempTable);
      String[] dstProperties = TempTableBuilder.getOrderedPropertyNames(dstTempTable);
      Map<String, Integer> srcExtents = srcBuf.getDmoInfo().getExtentMap();
      Map<String, Integer> dstExtents = dstBuf.getDmoInfo().getExtentMap();
      
      if (!looseCopy && (srcProperties.length != dstProperties.length))
      {
         handleTablesDoNotMatch(errorMode, srcBufName, dstBufName);
         return null;
      }
      
      String[] srcPropertiesLegacy = null;
      Map<String, Integer> dstPropertiesLegacy = null;
      
      if (looseCopy)
      {
         srcPropertiesLegacy = new String[srcProperties.length];
         for (int i = 0; i < srcProperties.length; i++)
         {
            srcPropertiesLegacy[i] =
               srcBuf.getDmoInfo().getFieldInfo(srcProperties[i]).legacyLower;
         }
         
         dstPropertiesLegacy = new CaseInsensitiveHashMap<>();
         for (int i = 0; i < dstProperties.length; i++)
         {
            Property fieldInfo = dstBuf.getDmoInfo().getFieldInfo(dstProperties[i]);
            dstPropertiesLegacy.put(fieldInfo.legacy, i);
         }
      }
      
      Map<String, Method> srcGetters = srcBuf.getDmoInfo().getPojoGetterMap();
      Map<String, Method> dstGetters = dstBuf.getDmoInfo().getPojoGetterMap();
      Map<String, Method> dstSetters = dstBuf.getDmoInfo().getPojoSetterMap();
      
      Map<DatumAccess, DatumAccess> propsMap = new HashMap<>(); // TODO: reserved properties
      for (int i = 0; i < srcProperties.length; i++)
      {
         String dstPropName;
         if (looseCopy)
         {
            Integer idx = dstPropertiesLegacy.get(srcPropertiesLegacy[i]);
            if (idx == null)
            {
               continue;
            }
            dstPropName = dstProperties[idx];
         }
         else if (i < dstProperties.length)
         {
            dstPropName = dstProperties[i];
         }
         else
         {
            handleTablesDoNotMatch(errorMode, srcBufName, dstBufName);
            return null;
         }
         
         String srcPropName = srcProperties[i];
         DatumAccess srcDatum = srcBuf.getGetterDatums().get(srcPropName);
         DatumAccess dstDatum = dstBuf.getSetterDatums().get(dstPropName);
         
         // compare data types
         Method srcGetter = srcDatum == null ? srcGetters.get(srcPropName) : srcDatum.getAccessor();
         Method dstGetter = dstGetters.get(dstPropName);
         Method dstSetter = dstDatum == null ? dstSetters.get(dstPropName) : dstDatum.getAccessor();
         Integer srcExtent = srcDatum == null ? srcExtents.get(srcPropName) : srcDatum.getExtent();
         Integer dstExtent = dstDatum == null ? dstExtents.get(dstPropName) : dstDatum.getExtent();
         ParmType srcType = ParmType.fromClass(srcGetter.getReturnType());
         ParmType dstType = ParmType.fromClass(dstGetter.getReturnType());

         boolean typesCompatible = (srcType == dstType) ||
                                   (srcType == ParmType.INT && dstType == ParmType.INT64) ||
                                   (srcType == ParmType.INT64 && dstType == ParmType.INT);

         if (srcType == null || dstType == null || !typesCompatible)
         {
            if (looseCopy)
            {
               continue;
            }
            else
            {
               handleTablesDoNotMatch(errorMode, srcBufName, dstBufName);
               return null;
            }
         }
         
         // compare extent sizes
         long srcExtentSize = srcExtent == null ? 1 : srcExtent;
         long dstExtentSize = dstExtent == null ? 1 : dstExtent;
         
         if (srcExtentSize != dstExtentSize)
         {
            if (looseCopy)
            {
               continue;
            }
            else
            {
               handleTablesDoNotMatch(errorMode, srcBufName, dstBufName);
               return null;
            }
         }
         
         if (srcDatum == null)
         {
            PropertyMeta srcPropMeta = srcBuf.getDmoInfo().getPropsByGetterMap().get(srcGetter);
            
            srcDatum = new DatumAccess(srcPropName,
                                       null,
                                       srcGetter,
                                       srcExtent,
                                       srcPropMeta,
                                       srcBuf.isPropertyIndexed(srcPropMeta.getOffset()),
                                       false);
            srcBuf.getGetterDatums().put(srcPropName, srcDatum);
         }
         
         if (dstDatum == null)
         {
            PropertyMeta dstPropMeta = dstBuf.getDmoInfo().getPropsBySetterMap().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 &&
                     dstPropMeta.getOriginalExtent() == 0    &&
                     dstBuf.getTriggerTracker() != null      &&
                     dstBuf.getTriggerTracker().isTriggerEnabled(DatabaseEventType.ASSIGN, 
                                                                 dstPropName, 
                                                                 dstBuf.getLogicalDatabase());
            dstDatum = new DatumAccess(dstPropName,
                                       null,
                                       dstSetter,
                                       dstExtent,
                                       dstPropMeta,
                                       dstBuf.isPropertyIndexed(dstPropMeta.getOffset()),
                                       dstAssignTrigger);
            dstBuf.getSetterDatums().put(dstPropName, dstDatum);
         }
         
         propsMap.put(srcDatum, dstDatum);
      }
      
      return propsMap;
   }
   
   /**
    * This is the actual implementation of TEMP-TABLE buffer's GET-CHANGES method. No validation
    * is performed, the method expects the parameters to be validated in calling method.
    * <p>
    * What this method actually does is copy all records from source buffers to destination,
    * skipping the UNMODIFIED records from AFTER source, into destination. During the process,
    * the BEFORE/AFTER ROWIDs relation between the new records is maintained.
    *
    * @param   srcBefore
    *          The buffer for BEFORE buffer source. All records from this table are copied.
    * @param   srcAfter
    *          The buffer for AFTER buffer source. The non-modified records of this table are
    *          dropped.
    * @param   dstBefore
    *          The destination buffer for BEFORE records. With the exception of DELETEd records
    *          from this table will have a link (AFTER-ROWID) to a record in {@code dstAfter}.
    * @param   dstAfter
    *          The destination buffer for AFTER records. All records from this table will have
    *          a link (BEFORE-ROWID) to a record in {@code dstBefore}.
    */
   static boolean copyChanges(TemporaryBuffer srcBefore,
                              TemporaryBuffer srcAfter,
                              TemporaryBuffer dstBefore,
                              TemporaryBuffer dstAfter)
   {
      // flush the records
      if (!srcAfter.getParentTable().flushBuffers())
      {
         return false;
      }
      
      if (srcBefore.isTableDefinitelyEmpty())
      {
         return true;
      }
      
      Persistence persistence = srcBefore.getPersistence();
      boolean savedAny = false;
      int commit = 0;   // 0 = do nothing, 1 = commit, -1 = rollback
      
      try
      {
         commit = persistence.beginTransaction(Persistence.TEMP_CTX) ? 1 : 0;
         
         // get the list of source records to be copied.  These will be all records in
         // srcBefore's backing temp table which have srcDMO's multiplex ID.
         FQLExpression beforeFql = new FQLExpression("from ");
         beforeFql.append(srcBefore.getDMOImplementationName());
         
         // srcBefore is a TemporaryBuffer, so it isMultiplexed()
         beforeFql.append(" where ").append(MULTIPLEX_FIELD_NAME).append(" = ", true);
         
         // force the records to be ordered by their ID
         // TODO in copy-temp-table sorting order affects which record remains in constraints
         // violation case. The correct sorting is by primary index if it presents or by
         // the index with the lexicographically first name.
         beforeFql.append(" order by ").append(Buffer.ROW_STATE_FIELD).append(",").append(Session.PK);
         
         ScrollableResults<Record> beforeRes =
            persistence.scroll(null,
                               beforeFql.toFinalExpression(),
                               new Object[] { srcBefore.getMultiplexID() },
                               0,
                               0,
                               ResultSet.TYPE_FORWARD_ONLY);
         
         try
         {
            while (beforeRes.next())
            {
               // Copy each source record into a new destination DMO instance.
               TempRecord srcBeforeRec = (TempRecord) beforeRes.get()[0];
               TempRecord dstBeforeRec = dstBefore.instantiateDMO();
               dstBeforeRec.initialize(dstBefore.getSession(), false);
               
               BaseRecord.copy(dstBeforeRec, srcBeforeRec, true);
               Integer rowState = srcBeforeRec._rowState();
               dstBeforeRec._rowState(rowState);
               
               Long idChBefore = dstBefore.nextPrimaryKey();
               dstBeforeRec.primaryKey(idChBefore);
               
               // usually we need to duplicate the after-buffer destination, too. The only
               // exception is, in fact, the DELETEd records which do not have a pair rowid record
               if (srcBeforeRec._peerRowid() != null)
               {
                  Record srcAfterRec = persistence.quickLoad(
                     new RecordIdentifier<>(srcAfter.getEntityName(), srcBeforeRec._peerRowid()),
                     Persistence.TEMP_CTX);
                  if (srcAfterRec != null)
                  {
                     TempRecord dstAfterRec = dstAfter.instantiateDMO();
                     dstAfterRec.initialize(dstAfter.getSession(), false);
                     BaseRecord.copy(dstAfterRec, srcAfterRec, true);
                     dstAfterRec._rowState(rowState);
                     
                     Long idChAfter = dstAfter.nextPrimaryKey();
                     dstAfterRec.primaryKey(idChAfter);
                     
                     // inter-link the newly created records:
                     dstAfterRec._peerRowid(idChBefore);
                     dstBeforeRec._peerRowid(idChAfter);
                     dstAfterRec._originRowid(srcBeforeRec.primaryKey());
                     dstBeforeRec._originRowid(srcBeforeRec.primaryKey());
                     
                     persistence.save(dstAfterRec, idChAfter);
                  }
                  else
                  {
                     LOG.warning("Copy error: AFTER-RECORD not found for (" + srcBefore.getTable() +
                           ":" + Long.toHexString(srcBeforeRec._peerRowid()) + ")");
                  }
               }
               else
               {
                  // normally these are DELETEd changes
                  dstBeforeRec._originRowid(srcBeforeRec.primaryKey());
               }
               
               persistence.save(dstBeforeRec, idChBefore);
               savedAny = true;
            }
         }
         catch (ReflectiveOperationException exc)
         {
            DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
            commit *= -1;
            ErrorManager.recordOrShowError(11924, srcAfter.getLegacyName(), dstAfter.getLegacyName());
            // MERGE-CHANGES run on original table <name> and change table <name> whose columns do not match exactly.
            return false;
         }
         finally
         {
            if (beforeRes != null)
            {
               beforeRes.close();
            }
            // invalidate ff cache (see previous occurrence / instantiateDMO() for details)
            dstAfter.invalidateFFCache(0);
            dstBefore.invalidateFFCache(0);
         }
      }
      catch (PersistenceException exc)
      {
         DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
         commit *= -1;
         ErrorManager.recordOrShowError(11924, srcAfter.getLegacyName(), dstAfter.getLegacyName());
         // MERGE-CHANGES run on original table <name> and change table <name> whose columns do not match exactly.
         return false;
      }
      finally
      {
         if (savedAny)
         {
            dstAfter.local.nonEmptyTableFlags.add(dstAfter.multiplexID);
            dstBefore.local.nonEmptyTableFlags.add(dstBefore.multiplexID);
         }
         
         try
         {
            switch (commit)
            {
               case 1:
                  persistence.commit(Persistence.TEMP_CTX);
                  break;
               case -1:
                  persistence.rollback(Persistence.TEMP_CTX);
                  break;
               case 0:
                  break;
            }
         }
         catch (PersistenceException exc)
         {
            DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
            ErrorManager.recordOrShowError(11924, srcAfter.getLegacyName(), dstAfter.getLegacyName());
            // MERGE-CHANGES run on original table <name> and change table <name> whose columns do not match exactly.
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Rejects changes to the data in the AFTER-BUFFER of this BEFORE-BUFFER.
    * 
    * @param   after
    *          The AFTER-BUFFER to be processed.
    *
    * @return  {@code true} if operation is successful.
    */
   protected boolean rejectChanges(BufferImpl after)
   {
      if (isTableDefinitelyEmpty())
      {
         return true; // nothing to process
      }
      
      Persistence persistence = getPersistence();
      TemporaryBuffer afterDMO = (TemporaryBuffer) after.buffer();
      boolean savedAny = false;
      int commit = 0;   // 0 = do nothing, 1 = commit, -1 = rollback
      
      try
      {
         commit = persistence.beginTransaction(Persistence.TEMP_CTX) ? 1 : 0;
         
         FQLExpression fql = new FQLExpression("from ");
         fql.append(getDMOImplementationName());
         fql.append(" where ").append(TemporaryBuffer.MULTIPLEX_FIELD_NAME).append(" = ", true);
         fql.append(" order by ");
         fql.append(Session.PK);
         
         ScrollableResults<TempRecord> beforeRes =
            persistence.scroll(null,
                               fql.toFinalExpression(),
                               new Object[] { getMultiplexID() },
                               0,
                               0,
                               ResultSet.TYPE_FORWARD_ONLY);
         
         try
         {
            while (beforeRes.next())
            {
               // Copy each source record into a new destination DMO instance.
               TempRecord srcBeforeRec = (TempRecord) beforeRes.get()[0];
               Long afterRowid = srcBeforeRec._peerRowid();
               afterDMO.setIgnoreBeforeTracking(true);
               switch (srcBeforeRec._rowState())
               {
                  case Buffer.ROW_DELETED:
                     // restore back the deleted record:
                     after.create();
                     Record deletedRec = afterDMO.getCurrentRecord();
                     BaseRecord.copy(deletedRec, srcBeforeRec, false);
                     Long idAfter = afterDMO.nextPrimaryKey();
                     afterDMO.rowState(Buffer.ROW_UNMODIFIED);
                     afterDMO.originRowid(new rowid());
                     after.release();
                     savedAny = true;
                     break;
                     
                  case Buffer.ROW_MODIFIED:
                     // restore old value of fields
                     after.findByRowID(new rowid(afterRowid));
                     if (after._available())
                     {
                        Record afterRec = afterDMO.getCurrentRecord();
                        BaseRecord.copy(afterRec, srcBeforeRec, false);
                        afterDMO.peerRowid(new rowid());
                        afterDMO.rowState(Buffer.ROW_UNMODIFIED);
                        afterDMO.originRowid(new rowid());
                        after.release();
                        savedAny = true;
                     }
                     break;
                     
                  case Buffer.ROW_CREATED:
                     // delete back this record
                     after.findByRowID(new rowid(afterRowid));
                     if (after._available())
                     {
                        after.deleteRecord();
                     }
                     break;
                  // otherwise ?
               }
               afterDMO.setIgnoreBeforeTracking(false);
            }
         }
         finally
         {
            if (beforeRes != null)
            {
               beforeRes.close();
            }
         }
         
         // if everything went fine, delete all data from this table
         deleteAll();
         
         // and remove the origin-table link
         ((AbstractTempTable) this.getParentTable()).setOriginTable(null);
         ((AbstractTempTable) after.tableHandleNative()).setOriginTable(null);
      }
      catch (PersistenceException exc)
      {
         DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
         commit *= -1;
         
         // ErrorManager.recordOrShowError(?);
         if (LOG.isLoggable(Level.WARNING))
         {
            LOG.log(Level.WARNING, "Error rejecting changed", exc);
         }
         return false;
      }
      finally
      {
         if (savedAny)
         {
            afterDMO.local.nonEmptyTableFlags.add(afterDMO.multiplexID);
         }
         
         try
         {
            switch (commit)
            {
               case 1:
                  persistence.commit(Persistence.TEMP_CTX);
                  break;
               case -1:
                  persistence.rollback(Persistence.TEMP_CTX);
                  break;
               case 0:
                  break;
            }
         }
         catch (PersistenceException exc)
         {
            DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
            
            // ErrorManager.recordOrShowError(?);
            if (LOG.isLoggable(Level.WARNING))
            {
               LOG.log(Level.WARNING, "Error rejecting changed", exc);
            }
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Actual implementation of {@code MARK-NEW} method. Called from {@link BufferImpl#markNew()}.
    *
    * @param   before
    *          The before buffer.
    * 
    * @return  {@code true} if operation is successful.
    */
   protected boolean markNew(BufferImpl before)
   {
      if (isTableDefinitelyEmpty())
      {
         return true; // nothing to process
      }
      
      Persistence persistence = getPersistence();
      boolean savedAny = false;
      int commit = 0;   // 0 = do nothing, 1 = commit, -1 = rollback
      
      TemporaryBuffer beforeBuffer = (TemporaryBuffer) before.buffer();
      try
      {
         commit = persistence.beginTransaction(Persistence.TEMP_CTX) ? 1 : 0;
         
         FQLExpression fql = new FQLExpression("from ");
         fql.append(getDMOImplementationName());
         fql.append(" where ").append(TemporaryBuffer.MULTIPLEX_FIELD_NAME).append(" = ", true);
         fql.append(" and (_rowState is null or _rowState = 0) order by ");
         fql.append(Session.PK);
         
         ScrollableResults<Record> beforeRes =
            persistence.scroll(null,
                               fql.toFinalExpression(),
                               new Object[] { getMultiplexID() },
                               0,
                               0,
                               ResultSet.TYPE_FORWARD_ONLY);
         
         try
         {
            while (beforeRes.next())
            {
               Record srcBeforeRec = (Record) beforeRes.get()[0];
               
               before.setUpBeforeBuffer(true);
               before.create();
               beforeBuffer.peerRowid(new rowid(srcBeforeRec.primaryKey()));
               beforeBuffer.rowState(Buffer.ROW_CREATED);
               before.release();
               before.setUpBeforeBuffer(false);
               savedAny = true;
            }
         }
         finally
         {
            if (beforeRes != null)
            {
               beforeRes.close();
            }
         }
         
         // then update the flag for the after buffer:
         fql = new FQLExpression("update ");
         fql.append(getDMOImplementationName());
         fql.append(" set _rowState = ?");
         fql.append(" where ").append(MULTIPLEX_FIELD_NAME).append(" = ?");
         fql.append(" and (_rowState is null or _rowState = 0)");
         Object[] args = new Object[] {Integer.toString(Buffer.ROW_CREATED), 
                                       Integer.toString(getMultiplexID())};
         persistence.deleteOrUpdate(fql.toFinalExpression(),
                                    Persistence.TEMP_CTX,
                                    args,
                                    getEntityName(),
                                    !isUndoable());
      }
      catch (PersistenceException exc)
      {
         DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
         commit *= -1;
         
         // ErrorManager.recordOrShowError(?);
         if (LOG.isLoggable(Level.WARNING))
         {
            LOG.log(Level.WARNING, "Error marking new", exc);
         }
         return false;
      }
      finally
      {
         if (savedAny)
         {
            beforeBuffer.local.nonEmptyTableFlags.add(beforeBuffer.multiplexID);
         }
         
         try
         {
            switch (commit)
            {
               case 1:
                  persistence.commit(Persistence.TEMP_CTX);
                  break;
               case -1:
                  persistence.rollback(Persistence.TEMP_CTX);
                  break;
               case 0:
                  break;
            }
         }
         catch (PersistenceException exc)
         {
            DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
            
            // ErrorManager.recordOrShowError(?);
            if (LOG.isLoggable(Level.WARNING))
            {
               LOG.log(Level.WARNING, "Error marking new", exc);
            }
            return false;
         }
      }
      
      return true;
   }
   
   /**
    * Helper method for {@code DataSet} GET-CHANGES. This method copies the difference of records
    * that results from {@code parent-mode} flag of this method.
    * <br>
    * Copies only unchanged records from {@code this} to {@code parentDst} for all changed records
    * from {@code dst}, using an UNIQUE index for iteration. These are normally skipped when
    * {@code parentDst} was populated from {@code parentSrc} but now are required because of the
    * {@code parentMode} flag. Only {@code AFTER} records are copied: since they were NOT modified
    * they do not have a BEFORE pair.
    *
    * @param   parentDst
    *          The buffer that will receive the copied records.
    * @param   parentSrc
    *          The buffer that will receive the copied records.
    * @param   childSrc
    *          The buffer/table that contains the changes in the child table.
    * @param   whereStr
    *          The where predicate that represents the relation between the original buffers, used
    *          for locating parent-records.
    *
    * @return  {@code true} on success and {@code false} if any error is detected.
    */
   protected static boolean copyParentUnchangedRecords(BufferImpl parentDst,
                                                       BufferImpl parentSrc,
                                                       BufferImpl childSrc,
                                                       String whereStr)
   {
      // What this method does:
      //    for each [childSrc.before]
      //        each parentSrc
      //        whereStr 
      //      if (parentSrc.state == U)
      //          create parentDst.
      //          copy parentSrc to parentDst
      
      BufferImpl childSrcBefore = childSrc.beforeBufferNative();
      QueryWrapper query = QueryWrapper.createQueryUnnamedPool();
      query.setBuffers(childSrcBefore, parentSrc);
      StringBuilder sb = new StringBuilder();
      sb.append("FOR EACH ");
      sb.append(childSrcBefore.doGetName());
      sb.append(", EACH ");
      sb.append(parentSrc._name());
      if (whereStr != null && !whereStr.isEmpty())
      {
         sb.append(" ");
         // TODO: this is not very smart! it will fail for particular buffer name cases
         sb.append(whereStr.replace(childSrc.doGetName(), childSrcBefore.doGetName()));
      }
      sb.append(" NO-LOCK");
      try
      {
         query.prepare(sb.toString());
         query.hintFullResults();
         query.queryOpen();
         query.getFirst();
         
         while (!query._isOffEnd())
         {
            integer rowState = parentSrc.rowState();
            if (rowState.isUnknown() || rowState.intValue() == Buffer.ROW_UNMODIFIED)
            {
               parentDst.create();
               Record dstDMO = parentDst.buffer().getCurrentRecord();
               Record srcDMO = parentSrc.buffer().getCurrentRecord();
               BaseRecord.copy(dstDMO, srcDMO, false);
               parentDst.release();
            }
            query._getNext();
         }
      }
      catch (Exception ex)
      {
         if (LOG.isLoggable(Level.WARNING))
         {
            LOG.log(Level.WARNING, "Failed to copy parent unchanged records", ex);
         }
         ErrorManager.recordOrShowError(
            0, "GET-CHANGES error. Raise log level to WARNING to see details");
         
         return false;
      }
      finally
      {
         query.queryClose();
         query.delete();
      }
      
      return true;
   }
   
   /**
    * If <code>copyTempTableErrorMode</code> is <code>true</code> then dispaly "The caller's
    * temp-table parameter {srcBufName} does not match to the target temp-table {dstBufName}"
    * error message and abend. Else display "COPY-TEMP-TABLE requires {srcBufName} and
    * {dstBufName} columns to match exactly unless the fourth loose-mode parameter is passed as
    * TRUE" error message.
    *
    * @param errorMode
    *        Table copy mode, affects error handling.
    * @param srcBufName
    *        Source buffer name.
    * @param dstBufName
    *        Destination buffer name.
    */
   protected static void handleTablesDoNotMatch(CopyTableMode errorMode,
                                                String srcBufName,
                                                String dstBufName)
   {
      NumberedException e;
      switch (errorMode)
      {
         case COPY_TEMP_TABLE_MODE:
            ErrorManager.recordOrShowError(12303, srcBufName, dstBufName);
            // COPY-TEMP-TABLE requires <source-name> and <target-name> columns to match exactly unless the fourth loose-mode parameter is passed as TRUE.
            break;
            
         case INPUT_PARAM_MODE:
            e = new NumberedException("The caller's temp-table parameter " + srcBufName +
                        " does not match to the target temp-table " + dstBufName, 5363, false);
            ErrorManager.displayError(e.getNumber(), e.getMessage(), e.isPrefix());
            throw new InvalidParameterConditionException(e);
            
         case OUTPUT_TABLE_PARAM_MODE:
            e = new NumberedException("The caller's temp-table parameter " + dstBufName +
                        " does not match to the target temp-table " + srcBufName, 5363, false);
            ErrorManager.displayError(e.getNumber(), e.getMessage(), e.isPrefix());
            throw new InvalidParameterConditionException(e);
            
         case OUTPUT_TABLE_HANDLE_PARAM_MODE:
            ErrorManager.displayError(5363,
                  "The caller's temp-table parameter " + dstBufName +
                  " does not match to the target temp-table " + srcBufName, false);
            e = new NumberedException(
                  "Try switching TEMP-TABLE method ADD-FIELDS-FROM for CREATE-LIKE", 9030, false);
            ErrorManager.displayError(e.getNumber(), e.getMessage(), e.isPrefix());
            throw new InvalidParameterConditionException(e);
      }
   }
   
   /**
    * Make the specified proxy buffer instance mutable, to allow runtime buffer binding.
    * 
    * @param    dmoBufIface
    *           The DMO buffer interface.
    * @param    proxy
    *           The DMO proxy.
    *           
    * @return   The mutable DMO proxy.
    */
   protected static <T> T makeMutable(Class<T> dmoBufIface, T proxy)
   {
      Class<?>[] interfaces =
      {
         dmoBufIface,
         BufferReference.class,
         TempTableRecord.class 
      };
      
      TemporaryBuffer buffer = (TemporaryBuffer) ((BufferReference) proxy).buffer();
      buffer.initialize(true);
      
      buffer.mutableHandler = new ReferenceProxy((Temporary) proxy);
      try
      {
         T dmoProxy = ProxyFactory.getProxy(BufferImpl.class,
                                            true,
                                            interfaces,
                                            false,
                                            doNotProxyRes,
                                            () -> new DmoProxyPlugin(dmoBufIface, buffer.getDMOInterface()),
                                            buffer.mutableHandler);
         
         return dmoProxy;
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }
   
   /**
    * 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. Proxy will have as a super class the proxy class from
    * previous level. 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   <T>
    *          Record type.
    * @param   buffer
    *          Record buffer instance associated with the created proxy.
    * @param   ref
    *          Interface which defines DMO's public API methods.
    *
    * @return  An object which implements the specified interface, and has as a super class the proxy class from
    *          previous level.
    */
   public static <T extends Buffer> T mutableBufferProxy(Buffer ref, Class<T> type)
   {
      Class<?>[] interfaces =
      {
         type,
         BufferReference.class,
         TempTableRecord.class 
      };
                        
      TemporaryBuffer buffer = (TemporaryBuffer) ((BufferReference) ref).buffer();
      buffer.initialize(true);
            
      buffer.mutableHandler = new ReferenceProxy((Temporary) ref);
                  
      try
      {
         T dmoProxy = ProxyFactory.getProxy(ref.getClass(),
                                            true,
                                            interfaces,
                                            false,
                                            doNotProxyRes,
                                            null,
                                            buffer.mutableHandler);
                     
         return dmoProxy;
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
   }
   
   /**
    * Update the object reference count.  It will decrement the old value reference and increment the new 
    * value reference.
    *
    * @param    dmoIface
    *           The DMO interface.
    * @param    oldVal
    *           The old value.
    * @param    newVal
    *           The new value.
    */
   static void updateObjectRefCount(Class<?> dmoIface, object<?> oldVal, object<?> newVal)
   {
      Context local = context.get();
      
      if (oldVal != null && !oldVal.isUnknown())
      {
         decrementObjectCountRef(local, dmoIface, oldVal);
      }
      
      if (newVal != null && !newVal.isUnknown())
      {
         incrementObjectCountRef(local, dmoIface, newVal);
      }
   }
   
   /**
    * Write the specified TABLE handle to a XML.
    * <p>
    * This is used when the remote call sent the TABLE as XML and it must receive it in the same format
    * (used by SOAP callers).
    * 
    * @param    tableHandle
    *           The handle referencing the table.
    * @param    isHandle
    *           Flag indicating that this is used for a DATASET-HANDLE parameter.
    *           
    * @return   The XML representation of this table.
    */
   static String writeToXml(handle tableHandle, boolean isHandle)
   {
      if (!tableHandle._isValid())
      {
         return null;
      }
      
      AbstractTempTable atb = (AbstractTempTable) tableHandle.getResource();
      longchar tableXml = new longchar("");
      atb.writeXml(new TargetData(TargetData.TYPE_LONGCHAR, tableXml), 
                  new logical(false), // this must be false 
                  null, 
                  null,
                  new logical(isHandle ? true : false), // schema is sent only for TABLE-HANDLE
                  null,
                  new logical(false),
                  null);
      
      return tableXml.getValue();
   }

   /**
    * Read the TABLE from XML.
    * <p>
    * This is used when the remote call sent the TABLE as XML and it must receive it in the same format
    * (used by SOAP callers).
    * 
    * @param    xmlTable
    *           The XML serialized table.
    * @param    append
    *           Flag indicating if the read mode is 'append'. Otherwise, is 'empty.
    * @param    table
    *           The handle where to read the table.
    */
   private static void readFromXml(String xmlTable, boolean append, handle table)
   {
      character readMode = new character(append ? "APPEND" : "EMPTY");
      
      if (!table._isValid())
      {
         TempTableBuilder.create(table);
      }
      
      AbstractTempTable atb = (AbstractTempTable) table.getResource();
      longchar lc = new longchar();
      longchar.fixCodePage(lc, "utf-8");
      lc.assign(xmlTable);
      atb.readXml(new SourceData("longchar", lc), 
                  readMode, 
                  new character(), 
                  new logical(false));
   }

   /**
    * Decrements the counter-reference for the specified object.
    *
    * @param    local
    *           The context-local instance.
    * @param    dmoIface
    *           The DMO interface.
    * @param    obj
    *           The object whose reference need to be decremented.
    */
   private static void decrementObjectCountRef(Context local, Class<?> dmoIface, object<?> obj)
   {
      if (ObjectOps.hasDestructors(obj.ref()))
      {
         RefCount count = local.ooObjectCounter.get(dmoIface);
         if (count == null || count.get() <= 0)
         {
            if (LOG.isLoggable(Level.WARNING))
            {
               LOG.log(Level.WARNING, "Unbalanced counter for stored Object with d'tors");
            }
         }
         else
         {
            count.decrement();
         }
      }
      
      ObjectOps.decrement(obj.ref());
   }
   
   /**
    * Increments the counter-reference for the specified object.
    *
    * @param    local
    *           The context-local instance.
    * @param    dmoIface
    *           The DMO interface.
    * @param    obj
    *           The object whose reference need to be decremented.
    */
   private static void incrementObjectCountRef(Context local, Class<?> dmoIface, object<?> obj)
   {
      ObjectOps.increment(obj.ref());
      
      if (ObjectOps.hasDestructors(obj.ref()))
      {
         RefCount count = local.ooObjectCounter.get(dmoIface);
         if (count == null) 
         {
            count = new RefCount();
            local.ooObjectCounter.put(dmoIface, count);
         } 
         else
         {
            count.increment();
         }
      }
   }
   
   /**
    * Bind this instance to the other buffer.  This will force the proxy to return the original's buffer handle
    * and temp-table, even if the buffer is bound.
    * 
    * @param    other
    *           The other buffer.
    *           
    * @return   The previously bound buffer.
    */
   public Temporary bind(TemporaryBuffer other)
   {
      Temporary old = mutableHandler.bind((Temporary) other.getDMOProxy());
      mutableHandler.forceDefinition = true;
      return old;
   }
   
   /**
    * Unbind this buffer and re-bind it to its old buffer.
    * 
    * @param    old
    *           The previous buffer to which this instance was bound.
    */
   public void unbind(Temporary old)
   {
      mutableHandler.unbind();
      mutableHandler.forceDefinition = false;
      if (old != null)
      {
         mutableHandler.bind(old);
      }
   }
   
   /**
    * Check if this buffer is associated with a global temp-table.
    * 
    * @return   The state of the {@link #global} flag.
    */
   @Override
   public boolean isGlobal()
   {
      return global;
   }
   
   /**
    * Restores this buffer's state to that which it had at the beginning of
    * the current block.  In particular, this rolls back all the reversible
    * items for the current scope and removes these entries from the rollback
    * dictionaries managed by the <code>BufferManager</code>.
    * <p>
    * Auto-commit buffers are never rolled back.
    *
    * @param   transaction
    *          <code>true</code> if this commit represents the master
    *          transaction rollback;  <code>false</code> if it represents a
    *          sub-transaction rollback.
    */
   @Override
   public void rollback(boolean transaction)
   {
      if (!isUndoable())
      {
         // in case of the no-undo tables, we need to re-apply all the changes
         // recorded during this transaction; so, the empty flag needs to be reset
         // TODO: maybe a restoration mechanism would be better?
         local.nonEmptyTableFlags.add(multiplexID);
      }
      
      super.rollback(transaction);
   }
   
   /**
    * Commit the changes made within a subtransaction.  This entails:
    * <ul>
    *   <li>copying all reversible, record-level actions associated with the
    *       current scope to the next higher scope, so rollback capability
    *       for these actions is not lost when the scope is popped;
    *   <li>copying all undoable, property-level changes associated with the
    *       current scope to the next higher scope, so rollback capability
    *       for these actions is not lost when the scope is popped;
    * </ul>
    * <p>
    * Nothing is done if this is a full transaction commit, because no
    * rollback will be possible at the parent scope once the database-level
    * transaction is committed.
    * <p>
    * If this is a full transaction and there are pending bulk actions, force
    * those actions to be executed (i.e. force the delete).
    *
    * @param   transaction
    *          <code>true</code> if this commit represents the master
    *          transaction commit;  <code>false</code> if it represents a
    *          sub-transaction commit.
    */
   /*
   @Override
   public void commit(boolean transaction)
   {
      boolean inChangeScope = inChangeScope();
      
      List<PersistenceException> errors = null;
      
      if (inChangeScope)
      {
         errors = new ArrayList<>();
         
         if (transaction && isUndoable())
         {
            // flush the bulk deleted records which had their multiplex ID
            // changed; this is performed only for undoable tables.
            Set<Reversible> r0 = bufferManager.getReversibles(this, 0, false);
            if (r0 != null && !r0.isEmpty())
            {
               ReversibleBulkDelete rev = null;
               try 
               {
                  List<Long> freeIds = new ArrayList<>();
                  
                  Iterator<Reversible> iter = r0.iterator();
                  while (iter.hasNext())
                  {
                     Reversible r = iter.next();
                     
                     if (r instanceof ReversibleBulkDelete)
                     {
                        rev = (ReversibleBulkDelete) r;
                        rev.forceDelete();
                        iter.remove();
                     }
                     else if (r instanceof RecordBuffer.ReversibleDelete)
                     {
                        // collect IDs of reversible deletes
                        freeIds.add(r.getId());
                     }
                  }
                  
                  if (!freeIds.isEmpty())
                  {
                     // reclaim keys from committed deletes
                     reclaimKeys(freeIds);
                  }
               }
               catch (PersistenceException exc)
               {
                  if (LOG.isLoggable(Level.SEVERE))
                  {
                     LOG.log(Level.SEVERE,
                             describe() + ":  error rolling back undoable bulk delete " + rev,
                             exc);
                  }
                  
                  errors.add(exc);
               }
            }
         }
      }
      
      super.commit(transaction);
      
      if (inChangeScope)
      {
         // Report or eat errors.
         for (PersistenceException exc : errors)
         {
            ErrorManager.recordOrThrowError(exc);
         }
      }
   }
   */
   
   /**
    * Respond to a record change event.  Override the parent's implementation
    * to handle bulk delete events.  If our current record no longer exists in
    * the database, we must release it.
    *
    * @param   event
    *          Event which describes the DMO state change.
    *
    * @throws  PersistenceException
    *          if an error occurs executing a query to detect whether current
    *          record still exists.
    */
   @Override
   public void stateChanged(RecordChangeEvent event)
   throws PersistenceException
   {
      Integer srcMPID = event.getSource().getMultiplexID();
      if (!srcMPID.equals(multiplexID))
      {
         return;
      }
      
      if (event.isInsert())
      {
         local.nonEmptyTableFlags.add(multiplexID);
         
         super.stateChanged(event);
      }
      else if (event.isBulkDelete())
      {
         if (!isActive())
         {
            return;
         }
         
         Record dmo = getCurrentRecord();
         if (dmo == null)
         {
            // No release necessary.
            return;
         }
         
         // We can avoid issuing a query if the event represents a full bulk
         // delete and the deleted multiplex ID matches our own, because we
         // know our record must have been deleted.
         if (event.isFullBulkDelete())
         {
            release(false);
            return;
         }
         
         // Query whether the buffer's current record still exists.
         Long id = dmo.primaryKey();
         Persistence persistence = getPersistence();
         if (bulkDeleteEventSQL == null)
         {
            bulkDeleteEventSQL = "select tt." + Session.PK + " from " + getTable() + " tt " +
                                 "where tt." + Session.PK + " = ?1";
         }
         
         Long result = persistence.getSingleSQLResult(bulkDeleteEventSQL,
                                                      Persistence.TEMP_CTX, 
                                                      new Object[] { id });
         if (result == null)
         {
            release(false);
         }
      }
      else
      {
         super.stateChanged(event);
      }
   }
   
   /**
    * Override the superclass method to always return LockType.NONE. Locking
    * is a no-op for temp table records, since each user has exclusive access
    * to their temp table instance.
    *
    * @param  id
    *         Not used.
    *
    * @return LockType.NONE
    */
   @Override
   public LockType getPinnedLockType(Long id)
   {
      return LockType.NONE;
   }
   
   /**
    * Override the superclass method to do nothing.  Locking is a no-op for
    * temp table records, since each user has exclusive access to their temp
    * table instance.
    *
    * @param   id
    *          Not used.
    * @param   pinnedLockType
    *          Not used.
    */
   @Override
   public void setPinnedLockType(Long id, LockType pinnedLockType)
   {
   }
   
   /**
    * Returns TABLE-HANDLE for the buffer.
    *
    * @return TABLE-HANDLE for the buffer.
    */
   @Override
   public handle tableHandle()
   {
      TempTable tt = tableHandleResource();
      
      return tt == null ? new handle() : new handle(tt);
   }
   
   /**
    * Get the associated temp-table (for tempporary buffers).
    * 
    * @return   The TABLE-HANDLE resource for the buffer.
    */
   @Override
   public TempTable tableHandleResource()
   {
      if (master != null)
      {
         return master.tableHandleResource();
      }
      
      return tempTableRef;
   }
   
   /**
    * 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.
    */
   @Override
   public boolean initialize()
   {
      return initialize(false);
   }
   
   /**
    * 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.
    * 
    * @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.
    * @throws  RuntimeException
    *          if the backing DMO implementation class does not implement a
    *          constructor which accepts a multiplex ID.
    */
   @Override
   public boolean initialize(boolean fromDefine)
   {
      if (global && master != null && master.isDynamic() && !isDynamic())
      {
         // if this is not a dynamic buffer and a master is set, then it means this is a bound buffer to some 
         // other buffer; the 'global' flag needs to be false, otherwise TransactionManager$WorkArea.globalBlock
         // will leak Multiplexer instances in the 'finalizables' list.
         global = false;
      }
      
      if (!super.initialize(fromDefine))
      {
         return false;
      }
      
      if (!DynamicTablesHelper.isDynamicTableBuffer(this))
      {
         BufferReference tempBuffer = getDMOProxy();
         if (master == null)
         {
            tempTableRef = new StaticTempTable((Temporary) tempBuffer);
            bufferManager.registerPendingStaticTempTable((StaticTempTable) tempTableRef, global);
         }
         else
         {
            tempTableRef = master.tempTableRef;
         }
         
         ((AbstractTempTable) tempTableRef).registerBuffer((Buffer) tempBuffer);
      }
      
      Class<? extends Record> dmoCls = getDMOImplementationClass();
      if (!TempRecord.class.isAssignableFrom(dmoCls))
      {
         String msg = "Invalid temporary table DMO:  " + dmoCls.getName() + ".";
         throw new RuntimeException(msg);
      }
      
      @SuppressWarnings("unchecked")
      Class<? extends TempRecord> tempCls = (Class<? extends TempRecord>) dmoCls; 
      
      try
      {
         dmoCtor = tempCls.getDeclaredConstructor(Integer.class);
      }
      catch (NoSuchMethodException exc)
      {
         String msg = "Invalid temporary table DMO:  " + dmoCls.getName() +
                      " does not implement a multiplex constructor";
            
         if (LOG.isLoggable(Level.SEVERE))
         {
            LOG.log(Level.SEVERE, describe() + ":  " + msg, exc);
         }
         
         throw new RuntimeException(msg, exc);
      }
      
      return true;
   }
   
   /**
    * Read all rows from the virtual temp-table backing this buffer (i.e., only those rows with
    * this buffer's multiplex ID) and allow the given row handler to process each found row. Rows
    * are queried according to the primary index on the table, or in ascending order of primary
    * key, in the absence of a primary index.
    * 
    * @param   rowHandler
    *          Consumer function which will handle each result row according to the needs of the
    *          caller.
    * @return  number of processed rows
    * @throws  ErrorConditionException
    *          if there is any error querying the rows in the table.
    */
   public int readAllRows(Consumer<TempRecord> rowHandler)
   {
      int nr = 0;
      if (isTableDefinitelyEmpty())
      {
         // nothing to do
         return nr;
      }
      
      initialize();
      
      Persistence persistence = getPersistence();
      
      try
      {
         FQLExpression buf = new FQLExpression("from ");
         buf.append(getDMOImplementationName());
         
         String tableAlias = getDMOAlias();
         buf.append(" as ").append(tableAlias);
         
         // srcBuf is a TemporaryBuffer, so it isMultiplexed()
         buf.append(" where ");
         buf.append(MULTIPLEX_FIELD_NAME);
         buf.append(" = ", true);
         
         // force the records to be ordered by their primary index (or by ID if none defined)
         String orderBy = getPrimaryOrderByClause(tableAlias);
         buf.append(orderBy);
         
         String fql = buf.toFinalExpression();
         Object[] parms = new Object[] { getMultiplexID() };
         ScrollableResults<TempRecord> results =
               persistence.scroll(null, fql, parms, 0, 0, ResultSet.TYPE_FORWARD_ONLY);
         
         BufferManager bufMgr = getBufferManager();
         
         // process results
         while (results.next())
         {
            nr++;
            Object[] row = results.get();
            TempRecord dmo = (TempRecord) row[0];
            rowHandler.accept(dmo);
            bufMgr.evictDMOIfUnused(persistence, getPersistenceContext(), dmo);
         }
      }
      catch (PersistenceException exc)
      {
         DBUtils.handleException(DatabaseManager.TEMP_TABLE_DB, exc);
         ErrorManager.recordOrThrowError(exc);
      }
      return nr;
   }
   
   /**
    * Get code page of the specified CLOB field.
    *
    * @param   property
    *          The ORM property name of the CLOB field.
    *
    * @return  The code page of the specified CLOB field. If not configured, the CPINTERNAL is returned.
    */
   @Override
   public String getCodePage(String property)
   {
      String cp = tempTableRef.getCodePage(property);
      return (cp == null) ? I18nOps._getCPInternal() : cp;
   }
   
   /**
    * Counts the number of records in the TEMP-TABLE behind this buffer. This implementation is
    * designed to be (much) faster than the aggregate equivalent methods but it does not support
    * any filtering predicates. 
    * <p>
    * This is a SILENCED method. If a persistence error occur, -1 is returned and NO exception is
    * thrown. In fact the method was created to be used internally, by FWD.
    * 
    * @return  the number of records in the TEMP-TABLE associated with this buffer. -1 is returned
    *          if any issue is encountered.
    */
   public int count()
   {
      if (isTableDefinitelyEmpty())
      {
         return 0; // evidently
      }
      
      try
      {
         Object[] args = new Object[] {getMultiplexID()};
         String sql = "select count(*) from " + getTable() + 
                      " where " + MULTIPLEX_FIELD_NAME + " = ?";
         Long res = getPersistence().getSingleSQLResult(sql, Persistence.TEMP_CTX, args);
         return res.intValue();
      
      }
      catch (Exception ex)
      {
         if (LOG.isLoggable(Level.WARNING))
         {
            LOG.log(Level.WARNING, "Failed to count records from " + getTable(), ex);
         }
         return -1;
      }
   }
   
   /**
    * Override the superclass method to do nothing.  Locking is a no-op for
    * temp table records, since each user has exclusive access to their temp
    * table instance.
    *
    * @throws  PersistenceException
    *          never.
    */
   @Override
   public final void upgradeLock()
   throws PersistenceException
   {
   }
   
   /**
    * Indicate whether this is a temp-table buffer and we are not currently within an existing
    * database transaction.
    *
    * @return  {@code true} if auto-commit, else {@code false}.
    */
   @Override
   public boolean isAutoCommit()
   {
      return !persistenceContext.isTransactionOpen();
   }
   
   /**
    * Convenience method to indicate whether this buffer is backed by a
    * temporary table.
    *
    * @return  <code>true</code>.
    */
   @Override
   public boolean isTemporary()
   {
      return true;
   }
   
   /**
    * Check whether this instance was defined as REFERENCE-ONLY.
    *
    * @return  {@code true} if the table was defined as REFERENCE-ONLY.
    */
   public boolean referenceOnly()
   {
      return referenceOnly;
   }
   
   /**
    * Get the implicit SQL index name.
    * 
    * @param   dmoInterface
    *          DMO interface which represents the temp table.
    *          
    * @return  See the local method implementation.
    */
   public String getImplicitSqlIndexName(Class<? extends DataModelObject> dmoInterface)
   {
      return local.getImplicitSqlIndexName(dmoInterface);
   }
   
   /**
    * Get the implicit index name.
    * 
    * @param   dmoInterface
    *          DMO interface which represents the temp table.
    *          
    * @return  See the local method implementation.
    */
   public String getImplicitIndexName(Class<? extends DataModelObject> dmoInterface)
   {
      return local.getImplicitIndexName(dmoInterface);
   }

   /**
    * Get the logical database name associated with this buffer.  This will
    * always be the standard name for the temp table database as defined in
    * {@link DatabaseManager#TEMP_TABLE_DB}, regardless of the value of the
    * <code>ldbOrAlias</code> parameter.
    *
    * @param   ldbOrAlias
    *          A logical database name or database alias.
    *
    * @return  Standard, temp table database name.
    */
   @Override
   protected String getLDBName(String ldbOrAlias)
   {
      return DatabaseManager.TEMP_TABLE_SCHEMA;
   }
   
   /**
    * Get the database information object associated with this buffer.  This
    * will always be the standard temp table database as defined in
    * {@link DatabaseManager#TEMP_TABLE_DB}, regardless of the value of the
    * <code>ldbName</code> parameter.
    *
    * @param   ldbName
    *          A logical database name.
    *
    * @return  Standard, temp table database.
    */
   @Override
   protected Database getPDB(String ldbName)
   {
      return DatabaseManager.TEMP_TABLE_DB;
   }
   
   /**
    * 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).
    * <p>
    * This implementation overrides the parent method to do nothing. The parent implementation notifies the
    * connection manager that this buffer is active, but accesses to the temp table database are outside of
    * the connection manager's responsibilities.
    */
   @Override
   protected void onOpenOutermostScope()
   {
   }
   
   /**
    * Perform any actions needed when this buffer's outermost scope closes.
    * <p>
    * This implementation overrides the parent method to not interact with the connection manager. The parent
    * implementation notifies the connection manager that this buffer is no longer active, but accesses to
    * the temp table database are outside of the connection manager's responsibilities.
    */
   @Override
   protected void onCloseOutermostScope()
   {
      Buffer buf;
      if (tempTableRef != null && (buf = (Buffer) getDMOProxy()) != null)
      {
         ((AbstractTempTable) tempTableRef).deregisterBuffer(buf);
      }
   }
   
   /**
    * Create a new instance of the DMO implementation class associated with
    * this buffer, and assign this buffer's multiplex ID to it.  This method
    * must not be invoked before a buffer scope has been opened on this
    * buffer.
    *
    * @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.
    */
   @Override
   protected TempRecord instantiateDMO()
   throws InstantiationException,
          IllegalAccessException,
          InvocationTargetException
   {
      if (multiplexID == null)
      {
         throw new IllegalStateException("Cannot create a new record before buffer scope is opened");
      }
      
      TempRecord dmo = (TempRecord) dmoCtor.newInstance(multiplexID);
      
      if (!isUndoable())
      {
         dmo.updateState(null, DmoState.NOUNDO, true);
      }
      
      return dmo;
   }
   
   /**
    * Look up the number of fields in the legacy table represented by this buffer.
    * 
    * @return  Number of fields.
    */
   @Override
   protected int lookupNumFields()
   {
      return TableMapper.getNumFields(tempTableRef);
   }
   
   /**
    * Indicate whether the table backing this buffer is known to either: (a) not exist; or (b) contain no
    * rows.
    * <p>
    * This implementation involves only the virtual table backing the current buffer (i.e., whose multiplex
    * ID matches that of this buffer), not the entire physical table.
    * 
    * @return  {@code false} to indicate the backing table does not exist or is known to be empty; {@code
    *          true} if the table exists and might have rows.
    */
   @Override
   protected boolean isTableDefinitelyEmpty()
   {
      boolean hasRecords = local.nonEmptyTableFlags.contains(multiplexID);
      
      return !hasRecords;
   }
   
   /**
    * 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.
    */
   @Override
   public Boolean fastHasRecords()
   {
      Session session = getSession();
      if (session == null)
      {
         return null;
      }
      
      try
      {
         return DirectAccessHelper.hasRecords(session, getTable(), getMultiplexID());
      }
      catch (DirectAccessException e)
      {
         if (LOG.isLoggable(Level.WARNING) && !e.isExpected())
         {
            LOG.log(Level.WARNING, "Failed fastHasRecords with direct-access due to " + e.getMessage());
         }
         return null;
      }
   }
   
   /**
    * Indicate whether changes made to records managed by this buffer can be undone during a
    * rollback.
    * <p>
    * We override this method for temp-tables, which can be marked NO-UNDO when defined. There
    * also is a directory configuration option to treat all temp-tables as NO-UNDO, regardless
    * of how they originally were defined.
    *
    * @return  {@code true} if record changes can be undone, else {@code false}.
    */
   @Override
   protected boolean isUndoable()
   {
      if (DatabaseManager.isForceNoUndoTempTables())
      {
         if (undoStateProvider != null && undoStateProvider.isUndoable())
         {
            // log a warning: we are running in force-no-undo mode, but we have encountered a temp-table
            // which was not marked NO-UNDO
            String table = getTable();
            if (LOG.isLoggable(Level.WARNING) && !forceNoUndoConflicts.contains(table))
            {
               boolean doLog;
               
               // only log once per table to avoid flooding the log
               synchronized (forceNoUndoConflicts)
               {
                  doLog = forceNoUndoConflicts.add(table);
               }
               
               if (doLog)
               {
                  String legacyName = this.getLegacyName();
                  String msg = "Temp-table '" + legacyName + "' (" + table + ")" +
                               " is defined undoable, but is being forced to NO-UNDO by global NO-UNDO mode";
                  LOG.log(Level.WARNING, msg);
                  if (LOG.isLoggable(Level.FINE))
                  {
                     // log stack trace as an aid to finding this temp-table in business logic
                     LOG.log(Level.FINE, "Location:", new Throwable());
                  }
               }
            }
         }
         
         return false;
      }
      else if (undoStateProvider != null)
      {
         return undoStateProvider.isUndoable();
      }
      
      // Normally, this exception will never be thrown.
      // * in the case of dynamic temp-table, the UndoStateProvider is configured in constructor
      //    by the TempTableBuilder and passed directly
      // * in the case of static temp-tables, the StaticTempTable is configured later,
      //    in openScope() of the buffer. This method cannot be called before the scope is open.
      throw new IllegalStateException("UndoStateProvider was not yet configured");
   }
   
   /**
    * Get the multiplex ID of this temporary buffer.  This will be
    * <code>null</code> if this method is invoked before a buffer scope is
    * opened on this buffer for the first time.  Otherwise, it will be a
    * unique identifier within the current context.
    *
    * @return  Multiplex ID for this temporary buffer.
    */
   @Override
   public Integer getMultiplexID()
   {
      return multiplexID;
   }
   
   /**
    * 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>
    * Temp tables always are multiplexed.
    *  
    * @return  <code>true</code>.
    */
   @Override
   protected boolean isMultiplexed()
   {
      return true;
   }
   
   /**
    * Indicate whether this buffer implementation allows bulk actions.
    *  
    * @return  <code>true</code>.
    */
   @Override
   protected boolean allowsBulkActions()
   {
      return true;
   }
   
   /**
    * Retrieve the master record buffer for this buffer.
    * <p>
    * This only has meaning for shared temp tables.  For a temp table defined
    * as NEW SHARED, this buffer is returned.  For a temp table defined as
    * SHARED, the domain master buffer is returned.
    * 
    * @return  The master buffer.
    */
   @Override
   protected RecordBuffer getMasterBuffer()
   {
      return (master != null ? master : this);
   }
   
   /**
    * Gets the legacy name assigned to a dynamically prepared temp-table.
    * 
    * @return  The name assigned by TEMP-TABLE-PREPARE, or <code>null</code> if this buffer
    *          represents a static temp-table.
    */
   @Override
   protected String getDynamicName()
   {
      if (!isDynamic())
      {
         return null;
      }
      
      character name = tempTableRef.name();
      if (name == null || name.isUnknown())
      {
         return null;
      }
      
      return name.toStringMessage();
   }
   
   /**
    * Open a new buffer scope for this record buffer.  Open a new multiplex
    * scope and assign a multiplex ID the first time we are invoked for a
    * buffer instance.
    */
   @Override
   protected void openScope()
   {
      BufferManager.registerScopeable(bufferManager);
      
      if (referenceOnly)
      {
         return;
      }
      
      super.openScope();
      
      // only need to process if we are opening the outermost scope for this buffer; nested
      // scopes need not apply
      if (getOpenScopeCount() == 1)
      {
         // a temporary buffer can be initialized as soon as its first scope is opened, because
         // the _temp database is permanently connected
         initialize();
         
         // force calculation of this buffer's type (before or after); this must not be delayed
         // because otherwise another temp-table with the same name may be added on the stack, 
         // hiding its definition - so we force this when its outermost scope is opened
         ((BufferImpl) getDMOProxy()).computeBufferType();
         
         // if this is the first time this table is being used, create it in the database
         if (openMultiplexScope())
         {
            local.createTable(getDMOInterface(), getTable());
         }
         
         // map legacy info (this is tolerant to being called more than once)
         TableMapper.mapTemporaryTable(tempTableResource());
         
         // register for temp table cleanup
         Multiplexer multiplexer = new Multiplexer(persistenceContext);
         if (global && (master == null || isDynamic()))
         {
            // a global temp table buffer is cleaned up when the context ends
            txHelper.registerFinalizable(multiplexer, true);
            
            if (isDynamic())
            {
               dynamicMultiplexer = multiplexer;
            }
         }
         else
         {
            // a non-global temp table buffer is cleaned up when the top level method scope ends
            // (this corresponds with the nearest enclosing top level procedure, function, or trigger)
            pm.executeOnReturn(multiplexer::finished, multiplexer.weight());
         }
         
         // register for no-undo validation if outside an application transaction
         if (!isUndoable() && !bufferManager.isTransaction())
         {
            NoUndoValidator val = new NoUndoValidator();
            pm.executeOnReturn(val::finished, val.weight());
         }
         
         processCodePages();
      }
   }
   
   /**
    * Open a new buffer scope for this record buffer at the given block depth. Open a new
    * multiplex scope and assign a multiplex ID the first time we are invoked for a buffer
    * instance.
    *
    * @param   blockDepth
    *          Zero-based depth of the block (starting from the outermost scope) at which the
    *          buffer scope is opened.
    */
   @Override
   protected void openScopeAt(int blockDepth)
   {
      BufferManager.registerScopeable(bufferManager);

      if (referenceOnly)
      {
         return;
      }
      
      super.openScopeAt(blockDepth);
      
      initialize();
      
      // if this is the first time this table is being used, create it in the database
      if (openMultiplexScope())
      {
         local.createTable(getDMOInterface(), getTable());
      }
      
      // map legacy info (this is tolerant to being called more than once)
      TableMapper.mapTemporaryTable(tempTableResource());
      
      // register for temp table cleanup
      Multiplexer multiplexer = new Multiplexer(persistenceContext);
      txHelper.registerFinalizableAt(blockDepth, multiplexer);
      if (isDynamic() && blockDepth == 0)
      {
         dynamicMultiplexer = multiplexer;
      }
      
      // register for no-undo validation if outside an application transaction
      // TODO: doing this global scope creates a memory leak; does it even make sense to register
      // these at a scope other than the current scope?
      if (blockDepth > 0 && !isUndoable() && !bufferManager.isTransaction())
      {
         txHelper.registerFinalizableAt(blockDepth, new NoUndoValidator());
      }
   }
   
   /**
    * Check if this buffer is referenced by other {@link #explicitBuffers buffers}.
    * 
    * @return   See above.
    */
   protected boolean hasExplicitBuffers()
   {
      return explicitBuffers != null && !explicitBuffers.isEmpty();
   }
   
   /**
    * Deregister a dynamic multiplexer object from the transaction manager's global finalizables
    * when this buffer goes out of scope and invoke the multiplexer's scope closure processing.
    */
   @Override
   protected void resourceDeleted()
   {
      super.resourceDeleted();
      
      if (master != null)
      {
         master.explicitBuffers.remove(mutableProxy);
      }
      
      if (mutableHandler != null             && 
          mutableHandler.autoDeleteBinding   && 
          mutableHandler.boundBuffer != null && 
          mutableHandler.boundBuffer != this)
      {
         // bound buffers created internally by FWD need to be automatically deleted.
         mutableHandler.boundBuffer.resourceDeleted();
      }
      
      if (master == null || dynamicMultiplexer != null)
      {
         closeMultiplexScope();
         
         if (dynamicMultiplexer != null)
         {
            // remove multiplexer from TransactionManager's global finalizables, unless we are being
            // invoked in the global scope, since it will be removed imminently anyway
            int currentScope = bufferManager.getOpenBufferScopes();
            if (currentScope > 0)
            {
               txHelper.deregisterGlobalFinalizable(dynamicMultiplexer);
            }
         }
      }
   }
   
   /**
    * Override the superclass method to do nothing.  Locking is a no-op for
    * temp table records, since each user has exclusive access to their temp
    * table instance.
    */
   @Override
   protected void initPinnedLockTypes()
   {
   }
   
   /**
    * Perform a bulk delete of all records in the table associated with this buffer.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the bulk delete operation.
    */
   @Override
   protected void deleteAll()
   {
      try
      {
         if (!forceLoopingDelete())
         {
            removeRecords(null, null, null);
         }
         else
         {
            loopDelete(null, null, null);
         }
      }
      catch (PersistenceException exc)
      {
         ErrorManager.recordOrThrowError(exc);
      }
   }
   
   /**
    * Get all slave buffers associated with this buffer.  If there are no slave buffers, <code>null</code> is
    * returned.
    * 
    * @return   See above.
    */
   protected Set<TemporaryBuffer> getAllSlaveBuffers()
   {
      Map<TemporaryBuffer, Object> slaves = local.domains.get(this);
      if (slaves == null)
      {
         return null;
      }
      
      Set<TemporaryBuffer> buffers = new HashSet<>();
      buffers.addAll(slaves.keySet());
      
      return buffers;
   }
   
   /**
    * Remove all the domains associated with this buffer.
    */
   protected void cleanupSlaveBuffers()
   {
      local.domains.remove(this);
      this.setDeleted(true);
   }
   
   /**
    * Decrements the counter-reference for the specified object.
    *
    * @param   obj
    *          The object whose reference need to be decremented.
    */
   @Override
   protected void decrementObjectCountRef(object<?> obj)
   {
      if (obj == null || obj.isUnknown())
      {
         return;
      }
      
      decrementObjectCountRef(local, getDMOInterface(), obj);
   }
   
   /**
    * Get the mappings of property names, from definition to bound buffer.
    * 
    * @return   The {@link ReferenceProxy#properties} mappings.
    */
   @Override
   protected Map<String, String> getBoundPropertyMappings()
   {
      return mutableHandler == null ? null : mutableHandler.properties;
   }
   
   /**
    * 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  The current value of the {@code ROW-STATE} property for this record as explained above.
    */
   @Override
   protected integer rowState()
   {
      if (!isAvailable() /*|| ((BufferImpl) getDMOProxy())._dataSet() == null*/)
      {
         return new integer();
      }
      
      TempRecord record = getCurrentRecord();
      Integer state = record._rowState();
      return new integer(state == null ? Buffer.ROW_UNMODIFIED : state);
   }
   
   /**
    * Sets the value of the {@code ROW-STATE} 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.
    */
   @Override
   public void rowState(Integer state)
   {
      if (!isAvailable())
      {
         return;
      }
      
      if (state != null &&
          state != Buffer.ROW_UNMODIFIED &&
          state != Buffer.ROW_DELETED &&
          state != Buffer.ROW_MODIFIED &&
          state != Buffer.ROW_CREATED)
      {
         LOG.log(Level.SEVERE, "Internal: invalid ROW-STATE constant: " + state);
         return;
      }
      
      setupBeforeFlagImpl((r) -> r._rowState(state),
                          Buffer.ROW_STATE_FIELD,
                          Buffer.__ROW_STATE__,
                          false);
   }
   
   /**
    * 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  The rowid value of the peer record or {@code unknown} if there is none.
    */
   @Override
   protected rowid peerRowid()
   {
      if (!isAvailable())
      {
         return new rowid();
      }
      
      TempRecord record = getCurrentRecord();
      
      return new rowid(record._peerRowid());
   }
   
   /**
    * 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 long rowid value of the peer record or {@code null}.
    */
   @Override
   public void peerRowid(rowid peer)
   {
      setupBeforeFlagImpl((r) -> r._peerRowid(peer.getValue()),
                          Buffer.AFTER_ROWID_FIELD,
                          Buffer.__AFTER_ROWID__,
                          false);
   }
   
   /**
    * Sets the {@code origin-rowid} for AFTER-TABLE record.
    *
    * @param   peer
    *          The new long rowid value of the record or {@code null}.
    */
   @Override
   public void originRowid(rowid peer)
   {
      setupBeforeFlagImpl((r) -> r._peerRowid(peer.getValue()),
                          Buffer.ORIGIN_ROWID_FIELD,
                          Buffer.__ORIGIN_ROWID__,
                          false);
   }
   
   /**
    * Sets the {@code datasource-rowid} for AFTER-TABLE record.
    *
    * @param   sourceRowid
    *          The new long rowid value of the record or {@code null}.
    */
   @Override
   public void datasourceRowid(rowid sourceRowid)
   {
      setupBeforeFlagImpl((r) -> r._peerRowid(sourceRowid.getValue()),
                          Buffer.DATA_SOURCE_ROWID_FIELD,
                          Buffer.__DATA_SOURCE_ROWID__,
                          false);
   }
   
   /**
    * 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.
    * 
    * @see     Buffer#__ERROR_FLAG__
    * @see     Buffer#ERROR_ERROR
    * @see     Buffer#ERROR_REJECTED
    */
   @Override
   protected Integer errorFlags()
   {
      if (!isAvailable())
      {
         return null;
      }
      
      return getCurrentRecord()._errorFlags();
   }
   
   /**
    * 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.
    *
    * @see     Buffer#__ERROR_FLAG__
    * @see     Buffer#ERROR_ERROR
    * @see     Buffer#ERROR_REJECTED
    */
   @Override
   protected void updateErrorFlags(int errFlag, boolean set)
   {
      integer rowState = rowState();
      boolean updatePeer = !rowState.isUnknown() && rowState.intValue() != Buffer.ROW_DELETED;
      setupBeforeFlagImpl(
         r -> {
            Integer errFlags = r._errorFlags();
            if (errFlags == null)
            {
               if (set)
               {
                  r._errorFlags(errFlag);
               }
               // else let it unchanged (null)
            }
            else
            {
               r._errorFlags(set ? (errFlags | errFlag) : (errFlags & ~errFlag));
            }
         },
         Buffer.ERROR_FLAG_FIELD,
         Buffer.__ERROR_FLAG__,
         updatePeer);
   }
   
   /**
    * Gets {@code errorString} of this record.
    *
    * @return  the {@code errorFlag} of this record or {@code unknown} if it was not configured.
    * 
    * @see Buffer#__ERROR_STRING__
    */
   @Override
   protected character errorString()
   {
      if (!isAvailable())
      {
         return new character();
      }
   
      return new character(getCurrentRecord()._errorString());
   }
   
   /**
    * Sets the {@code error string} of this record.
    *
    * @param   string
    *          The new {@code error string} of this record.
    *
    * @see Buffer#__ERROR_STRING__
    */
   @Override
   protected void errorString(Text string)
   {
      integer rowState = rowState();
      boolean updatePeer = !rowState.isUnknown() && rowState.intValue() != Buffer.ROW_DELETED;
      setupBeforeFlagImpl((r) -> r._errorString(string.toJavaType()),
                          Buffer.ERROR_STRING_FIELD,
                          Buffer.__ERROR_STRING__,
                          updatePeer);
   }
   
   /**
    * Set reference to the parent temp table.
    *
    * @param   tempTableRef
    *          Parent temp table.
    */
   protected void setTempTableRef(TempTable tempTableRef)
   {
      this.tempTableRef = tempTableRef;
      if (undoStateProvider instanceof DefaultUndoState)
      {
         // if undoStateProvider is a default one, replace it with proper one
         undoStateProvider = tempTableRef;
      }
      
      ((AbstractTempTable) tempTableRef).registerBuffer((Buffer) getDMOProxy());
   }
   
   /**
    * Close multiplex scope and mark the underlying table to be dropped when the session closes.
    */
   protected void closeMultiplexScope()
   {
      if (referenceOnly)
      {
         return;
      }
      
      if (!isMultiplexScopeClosed())
      {
         try
         {
            doCloseMultiplexScope();
         }
         catch (PersistenceException exc)
         {
            ErrorManager.recordOrThrowError(exc);
         }
      }
   }
   
   /**
    * Copy output data for the linked table parameter, if any.
    * 
    * @param    outputParameter
    *           The table parameter where to copy the data.
    */
   protected void performOutputCopy(TableParameter outputParameter)
   {
      if (!outputParameters.contains(outputParameter))
      {
         // was already processed (by i.e. scope close)
         return;
      }
      
      Temporary tempDMO = (Temporary) getDMOProxy();
      if (!outputParameter.isRemoteParameter())
      {
         rowid srcRec = (referenceOnly || outputParameter.isByReference()) && isAvailable() 
                           ? rowID() 
                           : null;

         if (outputParameter.getTable() != null)
         {
            boolean res = copyAllRows(srcRec,
                                      tempDMO,
                                      outputParameter.getTable(),
                                      outputParameter.isAppend(),
                                      CopyTableMode.OUTPUT_TABLE_PARAM_MODE);
            if (!res)
               deferCopyError();
         }
         else
         {
            // create a copy of this temp-table and assign it to this handle
            TempTableBuilder.create(outputParameter.getTableHandle());
            TempTableBuilder ttDst = (TempTableBuilder) outputParameter.getTableHandle().getResource();
            TempTable tSrc = (((BufferReference) tempDMO).buffer()).tableHandleResource();
            handle htSrc = new handle(tSrc);
            ttDst.createLike(htSrc);
            ttDst.tempTablePrepare(tSrc.name());
            logical res = ttDst.copyTempTable(htSrc);
            if (!res.booleanValue())
            {
               deferCopyError();
            }
         }
      }
      else
      {
         if (outputParameter.getResultSet().isAsXml())
         {
            TableWrapper wrapper = outputParameter.getResultSet();
            handle tth = ((Buffer) tempDMO).tableHandle();
            String tableXml = writeToXml(tth, wrapper.isTableHandle());
            wrapper.setXmlTable(tableXml);
            return;
         }

//         try
//         {
            TempTableResultSet resultSet = new TempTableResultSet(tempDMO,
                                                                  false,
                                                                  true,
                                                                  outputParameter.isAppend());
            TableWrapper tableWrapper = new TableWrapper(resultSet);
            tableWrapper.init(getParentTable());
            outputParameter.setResultSet(tableWrapper);
//         }
//         catch (ClassNotFoundException e)
//         {
//            deferCopyError();
//         }
      }
      
      outputParameters.remove(outputParameter);
   }
   
   /**
    * Do clean-up in case the buffer runs out of (outermost) scope. This should be called from places responsible of 
    * the buffer lifecycle (e.g. {@link #finishedImpl()}).
    */
   protected void cleanup()
   {
      super.cleanup();
      
      if (this.master == null)
      {
         // this is the default buffer of the table. The domains will get cleared anyway as
         // the table will be out-of-scope soon.
         return;
      }
      
      Map<TemporaryBuffer, Object> slaves = local.domains.get(this.master);
      
      if (slaves == null)
      {
          // If the master buffer was already deleted,
          // then it is expected not to find any slave buffers in the domains for this.master.
          // In this case just return normally.
         if (LOG.isLoggable(Level.WARNING) && !this.master.isDeleted())
         {
            LOG.log(Level.WARNING, "Non-consistent domains collection. "
                     + "Can't clean-up a slave buffer properly as there is no slaves collection");
         }
         
         return;
      }
      
      slaves.remove(this);
   }
   
   /**
    * Activate or deactivate the CHANGE-TRACKING for this buffer. When set to {@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.  
    * 
    * @param   ignore
    *          {@code true} to deactivate the changes-tracking for this buffer. 
    */
   private void setIgnoreBeforeTracking(boolean ignore)
   {
      ignoreBeforeTracking = ignore;
   }
   
   /**
    * Check whether in this temp-table there are any objects that have pending destructors to be
    * called in case they are deleted.
    * 
    * @return  {@code true} when there is at least one object store in this temp-table that has at
    *          least one active destructor.
    */
   private boolean hasOoDestructors()
   {
      RefCount count = local.ooObjectCounter.get(getDMOInterface());
      
      return count != null && count.get() > 0;
   }
   
   /**
    * Register code page expression for the CLOB field. Code page will be evaluated and applied in
    * {@link #openScope()}.
    *
    * @param property
    *        Hibernate property name of the CLOB field.
    * @param codePageExpr
    *        Code page expression.
    */
   private void registerCodePage(String property, Supplier codePageExpr)
   {
      if (registeredCodePages == null)
      {
         registeredCodePages = new HashMap<>();
      }
      
      registeredCodePages.put(property, codePageExpr);
   }
   
   /**
    * Set code pages for the CLOB fields of the parent {@link TempTable}. A code page can be
    * inherited through LIKE option (either table- or field-level) or specified in
    * COLUMN-CODEPAGE option.
    */
   private void processCodePages()
   {
      if (master == null)
      {
         // Apply code pages inherited through LIKE option.
         //List<TableMapper.LegacyFieldInfo> fields = TableMapper.getAllLegacyFieldInfo(tempTableRef);
         
         DmoMeta dmoInfo = getDmoInfo();
         Iterator<Property> fields = dmoInfo.getFields(false);
         fields.forEachRemaining(field ->
         {
            if (field._fwdType == clob.class)
            {
               String like = field.like;
               if (!like.isEmpty())
               {
                  int pos = like.lastIndexOf('.');
                  if (pos < 0)
                  {
                     String msg = "LIKE option " + like + " of the field " + getLegacyName() + "." + 
                                  field.legacy + " must have a table qualifier";
                     ErrorManager.displayError(msg);
                     return;
                  }
                  
                  String srcFieldName = like.substring(pos + 1);
                  String fullTableName = like.substring(0, pos);
                  Persistence persistence = DynamicTablesHelper.findExistingTable(fullTableName);
                  if (persistence == null)
                  {
                     String msg = "Could not find persistence for LIKE option " + like + " of the field " + 
                                  getLegacyName() + "." + field.legacy;
                     ErrorManager.displayError(msg);
                     return;
                  }
                  
                  if (!persistence.isTemporary())
                  {
                     // It's an open question if temp-table inherits CODEPAGE from a permanent
                     // table. 4GL behavior is buggy. I'm inclined to think that it actually
                     // inherits and this should be fixed. SVL
                     return;
                  }
                  
                  String srcTableName = DynamicTablesHelper.parseTableName(fullTableName)[0];
                  P2JField likeField = TempTableBuilder.getExistingField(persistence,
                                                                         srcTableName,
                                                                         srcFieldName);
                  if (likeField == null)
                  {
                     String msg = "Could not find like-field for LIKE option " + like + " of the field " + 
                                  getLegacyName() + "." + field.legacy;
                     ErrorManager.displayError(msg);
                     return;
                  }
                  
                  String property = field.name;
                  String normalizedTableName4GL = TextOps.rightTrimLower(srcTableName);
                  TempTable likeTable = TempTableBuilder.getExistingTempTable(normalizedTableName4GL);
                  String likeProperty = TableMapper.getPropertyName(likeTable, likeField.getName());
                  Supplier supplier = likeTable.getCodePageSupplier(likeProperty);
                  if (supplier != null)
                  {
                     tempTableRef.setCodePageSupplier(property, supplier);
                     tempTableRef.setCodePage(property, resolveCodePage(supplier));
                  }
                  else
                  {
                     tempTableRef.setCodePage(property, likeField.getCodePage());
                  }
               }
            }
         });
      }
      
      // Apply code pages specified in COLUMN-CODEPAGE option.
      if (registeredCodePages != null)
      {
         registeredCodePages.forEach((property, supplier) ->
         {
            // Shared tables use code page defined in NEW SHARED statement. But suppliers are
            // updated because they are resolved if the shared table is specified as a source in
            // a LIKE statement.
            tempTableRef.setCodePageSupplier(property, supplier);
            
            if (master == null)
            {
               tempTableRef.setCodePage(property, resolveCodePage(supplier));
            }
         });
         
         registeredCodePages = null;
      }
   }

   /**
    * Resolves the supplier and returns the code page.
    *
    * @param  supplier
    *         Supplier to be resolved.
    *
    * @return resolved code page.
    */
   private String resolveCodePage(Supplier supplier)
   {
      Object codePage = supplier.get();
      if (codePage != null)
      {
         Class cls = codePage.getClass();
         if (String.class.isAssignableFrom(cls))
         {
            return (String) codePage;
         }
         else if (character.class.isAssignableFrom(cls))
         {
            return ((character) codePage).getValue();
         }
         else
         {
            ErrorManager.recordOrThrowError(
                  223, "Incompatible data types in expression or assignment");
         }
      }

      return null;
   }
   
   /**
    * Call {@link RecordBuffer#delete()} on all buffers for this temp-table, before emptying the temp-table
    * (explicitly or doing a looping delete), as any NEW record in a buffer will not be in the temporary 
    * database and its reserved PK row needs to be removed explicitly, by deleting it.
    */
   private void clearBuffers()
   {
      Set<TemporaryBuffer> bufs = getAllSlaveBuffers();
      if (bufs == null)
      {
         return;
      }
      
      for (TemporaryBuffer buf : bufs)
      {
         if (buf.isAvailable())
         {
            BufferImpl dmo = (BufferImpl) buf.getDMOProxy();
            boolean setupBefore = dmo.isBeforeBuffer() && dmo.hasBeforeBufferConstraints();
            if (setupBefore)
            {
               dmo.setUpBeforeBuffer(true);
            }
            try
            {
               buf.delete();
            }
            catch (PersistenceException exc)
            {
               ErrorManager.recordOrThrowError(exc);
            }
            finally
            {
               if (setupBefore)
               {
                  dmo.setUpBeforeBuffer(false);
               }
            }
         }
      }
   }
   
   /**
    * Emulate a loop for deleting a set of records from this table.
    * <p>
    * This is only used when the table contains {@code Progress.Lang.Object} fields and there is
    * at least one record that holds an objects that has defined any destructors to be called when
    * it is deleted.
    * <p>
    * Because the method fetches every affected record instead of executing a bulk operation,
    * this method has a performance penalty associated.
    *
    * @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. {@code null} to
    *          delete all records whose multiplex IDs match that of this buffer.
    * @param   args
    *          Query substitution parameters required by the where clause. {@code null} if there
    *          are no substitutions or no where clause.
    */
   private void loopDelete(DataModelObject[] suppDMOs, String where, Object[] args)
   {
      if (where == null)
      {
         clearBuffers();
      }
      
      PreselectQuery query0 = new PreselectQuery()
      {
         /**
          * Check if this type of query should register record change listeners. Use this 
          * to avoid computing any scope for registration and end up with a no registration
          * behavior. 
          * 
          * @return   {@code false} as this doesn't require any change listener.
          */
         @Override
         public boolean shouldRegisterRecordChangeListeners()
         {
            return false;
         }
      };
      
      boolean success = true;
      try
      {
         loopingDelete = true;
         
         BlockManager.forEach(query0, "innerDeleteLoop", new Block(
            (Init) () -> {
               query0.initialize(this, where, null, null, args);
               query0.addExternalBuffers(suppDMOs);
            },
            (Body) () -> {
               try
               {
                  delete();
               }
               catch (PersistenceException exc)
               {
                  ErrorManager.recordOrThrowError(exc);
               }
            }));
      }
      catch (Exception e)
      {
         success = false;
         LOG.severe("", e);
      }
      finally
      {
         loopingDelete = false;
         
         if (where == null)
         {
            if (success)
            {               
               local.nonEmptyTableFlags.remove(multiplexID);
            }
         }

         // if the delete was partial, there is no need to prune the session cache,
         // the delete() method handles it
         if (where == null)
         {
            pruneSessionCache();
         }
      }
   }
   
   /**
    * Get the order by clause determined by this buffer's backing table's primary index, if any. If no
    * explicit primary index is defined for the backing table, the effective primary index is used (the first
    * in the definition order, if any). If no index is defined, the order by clause will specify an ascending
    * sort by primary key.
    *
    * @param   tableAlias
    *          Alias used to qualify column names in the ORDER BY clause.
    * 
    * @return  Order by clause for temp-table's primary index.
    */
   private String getPrimaryOrderByClause(String tableAlias)
   {
      if (primaryOrderBy != null)
      {
         return primaryOrderBy;
      }
      
      P2JIndex index = buffer().getDmoInfo().getPrimaryIndex(true);
      
      // not found? use PK as last resort
      if (index == null)
      {
         // no primary index found, use default
         if (isTemporary() && ((BufferImpl) this.getDMOProxy()).isBeforeBuffer())
         {
            primaryOrderBy = " order by _rowState asc, " + Session.PK + " asc";
         }
         else
         {
            primaryOrderBy = " order by " + Session.PK + " asc";
         }
         
         if (LOG.isLoggable(Level.FINER))
         {
            LOG.log(Level.FINER, "Setting default order by clause '" + primaryOrderBy + "'");
         }
         
         return primaryOrderBy;
      }
      
      // [primaryOrderBy] is null. Let's compute it based on [index] components.
      StringBuilder buf = new StringBuilder(" order by ");
      String prefix = (tableAlias != null && !tableAlias.isEmpty()) ? tableAlias + "." : "";
      ArrayList<P2JIndexComponent> comps = index.components();
      for (int i = 0; i < comps.size(); i++)
      {
         P2JIndexComponent comp = comps.get(i);
         if (i > 0)
         {
            buf.append(", ");
         }

         String name = comp.getPropertyName();
         if (comp.isCharType() && getDialect().needsComputedColumns())
         {
            name = getDialect().getComputedColumnPrefix(!comp.isIgnoreCase()) + name;
         }
         String direction = comp.isDescending() ? "desc" : "asc";
         
         buf.append(prefix).append(name).append(' ').append(direction);
      }
      primaryOrderBy = buf.toString();
      
      return primaryOrderBy;
   }
   
   /**
    * Open a new multiplex scope for this buffer.  The return value indicates
    * whether it is the first time a multiplex scope has been opened for this
    * buffer's DMO interface in this context.  If so, the caller will need to
    * create the backing temp table in the database.  This method permanently
    * assigns the multiplex ID to this buffer instance.
    *
    * @return  {@code true} if this is the first multiplex scope opened for the underlying DMO
    *          type; {@code false} if multiplex scopes are already open in this context for this
    *          DMO type.
    */
   private boolean openMultiplexScope()
   {
      if (!DynamicTablesHelper.isDynamicTableBuffer(this))
      {
         // buffers can be opened in an arbitrary order, so the first buffer opened for a table
         // registers static temp-table object and sets tempTableRef in the table default buffer
         if (master != null)
         {
            setTempTableRef(master.tempTableRef);
         }
      }
      
      Class<? extends DataModelObject> dmoIface = getDMOInterface();
      Map<Class<?>, Map<Integer, Integer>> map = local.inUseMPIDs;
      
      // get the map of multiplex IDs which are in use for this DMO type within our context.  The
      // value for each multiplex ID key is the reference count of buffers using that ID.
      Map<Integer, Integer> inUse = map.computeIfAbsent(dmoIface, k -> new HashMap<>());
      int refCount = 0;
      
      if (multiplexID == null)
      {
         // Generate a unique multiplex ID.
         Integer id = local.getNextMPID(inUse);
         
         // Assign the unique ID as the permanent multiplex ID of every
         // buffer in this buffer's domain.
         RecordBuffer key = getMasterBuffer();
         Map<TemporaryBuffer, Object> domain = local.domains.get(key);
         for (TemporaryBuffer next : domain.keySet())
         {
            next.multiplexID = id;
         }
         
         // Start off the reference count.
         refCount = 1;
      }
      else
      {
         // Increment the existing reference count for this multiplex ID.
         Integer rcObj = inUse.get(multiplexID);
         refCount = (rcObj == null ? 1 : rcObj + 1);
      }
      
      boolean createTable = inUse.isEmpty();
      
      inUse.put(multiplexID, refCount);
      
      // If this is the first use of this multiplex ID, the virtual table must be empty.
      if (refCount == 1)
      {
         local.nonEmptyTableFlags.remove(multiplexID);
      }
      
      return createTable;
   }
   
   /**
    * Perform a bulk delete, restricted by the given where clause and query
    * substitution parameters.
    * <p>
    * The targeted records are not actually removed from the buffer; instead,
    * they are assigned another multiplex ID, which marks them as deleted.
    * <p>
    * Note:  primary keys surrendered via bulk deletes are not currently recycled.
    * 
    * @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
   {
      if (((BufferImpl) getDMOProxy()).isAfterBuffer() || forceLoopingDelete())
      {
         loopDelete(null, where, args);
      }
      else
      {
         invalidateFFCache(0);
         removeRecords(null, where, args);
      }
   }
   
   /**
    * Perform a bulk delete, restricted by the given where clause and query
    * substitution parameters.
    * <p>
    * The targeted records are not actually removed from the buffer; instead,
    * they are assigned another multiplex ID, which marks them as deleted.
    * <p>
    * Note:  primary keys surrendered via bulk deletes are not currently recycled.
    * 
    *
    * @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
    *          if an error occurs performing the bulk delete operation.
    */
   protected void delete(DataModelObject[] suppDMOs, String where, Object... args)
   throws PersistenceException
   {
      if (((BufferImpl) getDMOProxy()).isAfterBuffer() || forceLoopingDelete())
      {
         loopDelete(suppDMOs, where, args);
      }
      else
      {
         removeRecords(suppDMOs, where, args);
      }
   }
   
   /**
    * 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.
    */
   @Override
   protected boolean delete(Supplier<logical> valexp, Supplier<character> valmsg)
   throws PersistenceException
   {
      if (createRowCallback)
      {
         // deleting a transient record, see [createRowCallback] javadoc
         release(true, false);
         return true;
      }
      
      // check the BEFORE-BUFFER constraints
      BufferImpl buf = (BufferImpl) getDMOProxy();
      if (buf.isBeforeBuffer() && buf.hasBeforeBufferConstraints())
      {
         ErrorManager.recordOrThrowError(12374, buf.doGetName(), "");
         // You may not delete BEFORE-TABLE <name> record.  Use ACCEPT-ROW-CHANGES instead.
         return false;
      }
      
      if (getCurrentRecord() == null)
      {
         TempTable tempTable = TempTableBuilder.asTempTable((Temporary) getDMOProxy());
         ErrorManager.recordOrShowError(341, tempTable.name().toStringMessage());
         // ** There is no current <file-name> record. Delete failed.
         return false;
      }
      
      boolean autoCommit = !bufferManager.isTransaction();
      Long id = null;
      autoCommit &= persistenceContext.beginTransaction(null);
      
      try
      {
         if (autoCommit || loopingDelete)
         {
            id = getCurrentRecord().primaryKey();
         }
         
         Record b4Copy = null;
         rowid b4Rowid = null;
         Long sourceRowid = null;
         BufferImpl before = null;
         BufferImpl after = (BufferImpl) getDMOProxy();
         if (!ignoreBeforeTracking    &&
             buffer().isAvailable()   &&
             after.isAfterBuffer())
         {
            b4Copy = buffer().getCurrentRecord().snapshot();
            b4Rowid = after.beforeRowid();
            before = after.beforeBufferNative();
            sourceRowid = ((TempRecord) buffer().getCurrentRecord())._datasourceRowid();
         }
         
         super.delete(valexp, valmsg);
         
         if (before != null)
         {
            boolean isTrackingChanges = 
                  ((AbstractTempTable) TempTableBuilder.asTempTable((Temporary) after))._isTrackingChanges();
            before.setUpBeforeBuffer(true);
            boolean updateB4RecordBuffer = true;
            if (b4Rowid != null && !b4Rowid.isUnknown())
            {
               before.findByRowID(b4Rowid);
               if (before._available())
               {
                  integer rowState = before.rowState();
                  if (!rowState.isUnknown() && 
                      (!isTrackingChanges || rowState.intValue() == Buffer.ROW_CREATED))
                  {
                     before.deleteRecord();
                  }
                  updateB4RecordBuffer = false;
               }
            }
            
            // if this is the buffer of an active AFTER-BUFFER, update the BEFORE-TABLE, too:
            if (b4Copy != null && isTrackingChanges)
            {
               RecordBuffer b4Buffer = before.buffer();
               if (updateB4RecordBuffer)  // ROW-STATE(before) == Buffer.ROW_UNMODIFIED
               {
                  // create new before record image, then update my before-rowid and row-state
                  before.create();
                  BaseRecord.copy(b4Buffer.getCurrentRecord(), b4Copy, false);
               }
               
               if (b4Buffer.isAvailable())
               {
                  b4Buffer.rowState(Buffer.ROW_DELETED);
                  b4Buffer.peerRowid(new rowid());
                  if (sourceRowid != null)
                  {
                     ((TempRecord) b4Buffer.getCurrentRecord())._datasourceRowid(sourceRowid);
                  }
               }
               
               // do not release the before buffer, let it hold the newly created row, but flush it!
               if (b4Buffer.isAvailable())
               {
                  try
                  {
                     b4Buffer.flush();
                  }
                  catch (ValidationException e)
                  {
                     // this should not happen, there are no constraints to validate on before records 
                     throw new PersistenceException(e);
                  }
               }
            }
            before.setUpBeforeBuffer(false);
         }
         
         if (autoCommit || loopingDelete)
         {
            local.reclaimKey(getDMOInterface(), getMultiplexID(), id);
         }
      }
      finally
      {
         if (autoCommit)
         {
            try
            {
               persistenceContext.commit();
            }
            catch (PersistenceException exc)
            {
               // TODO: what to do here?
               if (LOG.isLoggable(Level.WARNING))
               {
                  LOG.log(Level.WARNING, "Error committing delete", exc);
               }
            }
         }
      }
      
      // everything went fine
      return true;
   }
   
   /**
    * 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>.
    */
   @Deprecated
   @Override
   protected void reclaimKeys(List<Long> keys)
   {
      Class<? extends DataModelObject> dmoIface = getDMOInterface();
      Integer multiplexID = getMultiplexID();
      for (Long key : keys)
      {
         local.reclaimKey(dmoIface, multiplexID, key);
      }
   }
   
   /**
    * Returns the primary key for a new DMO associated with this buffer.
    * 
    * @return  The primary key for a new DMO associated with this buffer.
    */
   @Override
   protected Long nextPrimaryKey()
   {
      return local.nextPrimaryKey(getDMOInterface(), getMultiplexID());
   }
   
   /**
    * Check if this temp-table is currently being queried.
    * 
    * @return   See above.
    * 
    * @see  ChangeBroker#isQueried
    */
   protected boolean isQueried()
   {
      return changeBroker.isQueried(getDMOInterface(), getMultiplexID());
   }
   
   /**
    * Check if the delete operation must be forced using a loop, and not a batch sql.
    * 
    * @return  {@code true} if the buffer is currently being queried or any object fields have a destructor.
    */
   protected boolean forceLoopingDelete()
   {
      return isQueried() || hasOoDestructors() || getPersistenceContext().getNursery().containsRecordBufferDmo(this);
   }
   
   /**
    * Close the current multiplex scope for the underlying DMO type in the current context. Remove all records
    * associated with this buffer's multiplex ID and adjust the multiplex ID in use map. If the multiplex is
    * not in use anymore, the legacy information is removed from {@code TableMapper}. If this is the last
    * scope to be closed for this DMO type, also request the context to drop the backing temp table from the
    * database when the database session is closed.
    * 
    * @throws  PersistenceException
    *          if any error occurs within a listener while processing a change event.
    */
   private void doCloseMultiplexScope()
   throws PersistenceException
   {
      // do we need to copy our current records before cleaning up?
      if (outputParameters != null)
      {
         // use a copy, as outputParameters gets cleaned up by performOutputCopy
         List<TableParameter> copy = new LinkedList<>(outputParameters);
         for (TableParameter outputParam : copy)
         {
            performOutputCopy(outputParam);
         }
      }
      
      // invalidate the cache for this DMO and its multiplex ID, as it is going out of scope/deleted.
      persistenceContext.getFastFindCache().invalidate(getDmoInfo().getId(), multiplexID, true);
      
      Class<? extends DataModelObject> dmoIface = getDMOInterface();
      Map<Class<?>, Map<Integer, Integer>> map = local.inUseMPIDs;
      
      // get the map of multiplex IDs which are in use for this DMO type within our context
      Map<Integer, Integer> inUse = map.get(dmoIface);
      
      // access our reference count from this map
      int refCount = inUse.get(multiplexID);
      
      // decrement the reference count.  If 0, remove the key, else store the new value back in the map
      if (--refCount == 0)
      {
         inUse.remove(multiplexID);
      }
      else
      {
         inUse.put(multiplexID, refCount);
      }
      
      // if the in-use map is now empty, the backing table will need to be dropped.  Also remove
      // the inUse map from the master map and remove this buffer's domain if it is the master buffer
      if (inUse.isEmpty())
      {
         map.remove(dmoIface);
         
         try
         {
            removeRecords(null, null, null);
         }
         catch (PersistenceException exc)
         {
            ErrorManager.recordOrThrowError(exc);
         }
         finally
         {
            local.nonEmptyTableFlags.remove(multiplexID);
            
            // prepare the table to be dropped when the database session closes
            local.dropTable(tempTableResource(), getTable(), dmoIface);

            pruneSessionCache();
            
            DirectAccessHelper.killMultiplex(local.pctx.getSession(), getDmoInfo().getSqlTableName(), multiplexID);
         }
      }
      // otherwise, if the current multiplexID is no longer in use, remove all records for this multiplex ID
      // from the table, but leave the table intact for the other buffers still using it.
      else if (refCount == 0)
      {
         try
         {
            removeRecords(null, null, null);
         }
         catch (PersistenceException exc)
         {
            ErrorManager.recordOrThrowError(exc);
         }
         finally
         {
            local.nonEmptyTableFlags.remove(multiplexID);

            pruneSessionCache();
            
            DirectAccessHelper.killMultiplex(local.pctx.getSession(), getDmoInfo().getSqlTableName(), multiplexID);
         }
      }
   }
   
   /**
    * Determine if the multiplex scope for the underlying DMO type in the current context is
    * closed. See {@link #closeMultiplexScope()} for more information.
    *
    * @return <code>true</code> if multiplex scope is closed.
    */
   private boolean isMultiplexScopeClosed()
   {
      Map<Class<?>, Map<Integer, Integer>> map = local.inUseMPIDs;
      
      // Get the map of multiplex IDs which are in use for this DMO type within our context.
      Map<Integer, Integer> inUse = map.get(getDMOInterface());
      
      if (inUse == null)
      {
         return true;
      }
      
      // Access our reference count from this map.
      Integer rc = inUse.get(multiplexID);
      
      return rc == null;
   }
   
   /*
   private void count()
   {
      Persistence persistence = getPersistence();
      StringBuilder buf = new StringBuilder("select count(*) from ");
      buf.append(getDMOImplementationName());
      String fql = buf.toString();
      try
      {
         List list = persistence.list(fql, null, null, 0, 0, true);
         Number count = (Number) list.get(0);
         LOG.info(getTable() + " contains " + count + " records.");
      }
      catch (PersistenceException exc)
      {
         ErrorManager.recordOrThrowError(-1,
                                         "Error counting temp table records",
                                         exc);
      }
   }
   */
   
   /**
    * Remove or change the multiplex ID for all record rows from the backing table which meet the
    * specified criteria. Because temp table instances are multiplexed across nested buffer scopes
    * and across multiple buffers which use the same backing DMO type, the backing table cannot be
    * dropped until after the last buffer scope referencing the backing table in the current
    * context has been closed.  This method is invoked for all buffer scope closing events.
    * This is also the worker method for the {@link #deleteAll()} and 
    * {@link RecordBuffer#delete(DataModelObject[], String, Object[])} methods.
    *
    * @param   extDMOs
    *          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.  <code>null</code> to delete all
    *          records whose multiplex IDs match that of this buffer.
    * @param   args
    *          Query substitution parameters required by the where clause.
    *          <code>null</code> if there are no substitutions or no where clause.
    * 
    * @throws  PersistenceException
    *          if an error occurs performing the bulk delete operation.
    */
   private void removeRecords(DataModelObject[] extDMOs, String where, Object[] args)
   throws PersistenceException
   {
      if (where == null)
      {
         clearBuffers();
      }
      
      Record currentRec = getCurrentRecord();
      
      // after executing this method, the buffer is empty regardless the where predicate matches
      // the currently stored record
      // TODO: analyze whether it is appropriate to allow write trigger to fire here; this
      //       method can be invoked in various situations
      release(true);
      
      if (isTableDefinitelyEmpty())
      {
         // nothing to do
         return;
      }
      
      // enforce the savepoint, in case of rollback, but only if we are not closing the multiplex
      if (!isMultiplexScopeClosed() && undoStateProvider != null && undoStateProvider.isUndoable())
      {
         getSession(true).lazilySetSavepoints();
      }
      
      FastFindCache ffCache = persistenceContext.getFastFindCache();
      if (extDMOs == null || extDMOs.length == 0)
      {
         ffCache.invalidate(getDmoInfo().getId(), multiplexID);
      }
      else
      {
         int len = extDMOs.length;
         List<Long> tableIds = new ArrayList<>(len + 1);
         tableIds.add(FastFindCache.combine(getDmoInfo().getId(), multiplexID));
         for (int i = 0; i < len; i++)
         {
            RecordBuffer buffer = ((BufferReference) extDMOs[i]).buffer();
            int id = buffer.getDmoInfo().getId();
            tableIds.add(FastFindCache.combine(id, buffer.getMultiplexID()));
         }
         ffCache.invalidate(tableIds);
      }
      
      if (args != null &&
          !AbstractQuery.preprocessSubstitutionArguments(bufferManager, () -> false, false, args))
      {
         // there was an error processing where clause arguments; abort
         return;
      }
      
      boolean hasWhere = (where != null);
      Persistence persistence = getPersistence();
      
      // native SQL is more efficient, so we use it in the simple case of deleting/updating
      // everything
      boolean useSql = !hasWhere && (args == null || args.length == 0);
      
      String dmoImplName = getDMOImplementationName();
      String dmoAlias = getDMOAlias();
      String aliasDot = dmoAlias + ".";
      
      StringBuilder buf = new StringBuilder();
      
      boolean hasOO = !getDmoInfo().getOoFields().isEmpty();
      StringBuilder fql = new StringBuilder();
      if (hasOO)
      {
         fql.append("from ").append(dmoImplName).append(" as ").append(dmoAlias);
         fql.append(" where ");
      }
      
      List<Object> additionalArgs = new ArrayList<>(2);
      List<Object> ooArgs = new ArrayList<>(2);
      // create portion of where clause which restricts by multiplex ID
      buf.append(aliasDot);
      buf.append(MULTIPLEX_FIELD_NAME);
      buf.append(" = ?");
      String mpidWhere = buf.toString();
      
      FQLExpression query = new FQLExpression();
      
      query.append("delete from ");
      
      String table = getTable();
      if (useSql)
      {
         query.append(table);
      }
      else
      {
         query.append(dmoImplName);
      }
      query.append(" as ").append(dmoAlias);
      query.append(" where ");
      
      // apply multiplex filter and any additional conditions
      if (hasWhere)
      {
         query.append("(");
         if (hasOO) { fql.append("("); }
      }
      query.append(mpidWhere);
      additionalArgs.add(multiplexID);
      if (hasOO)
      {
         fql.append(dmoAlias).append(".").append(MULTIPLEX_FIELD_NAME).append("=?");
         ooArgs.add(multiplexID);
      }
      if (hasWhere)
      {
         query.append(") and (");
         if (hasOO) { fql.append(") and ("); }
         
         // add the external buffers to the list
         RecordBuffer[] buffs = new RecordBuffer[extDMOs == null ? 1 : extDMOs.length + 1];
         buffs[0] = this;
         if (extDMOs != null)
         {
            for (int i = 0; i < extDMOs.length; i++)
            {
               buffs[i + 1] = ((BufferReference) extDMOs[i]).buffer();
            }
         }
         FQLPreprocessor preproc = FQLPreprocessor.get(where,
                                                       getDatabase(),
                                                       getDialect(),
                                                       buffs,
                                                       args,
                                                       null,
                                                       false,
                                                       null,
                                                       null,
                                                       false,
                                                       null);
         // we need to use the preprocessed where clause
         where = preproc.getFQL().toFinalExpression();
         query.append(preproc.getFQL());
         query.append(")");
         if (hasOO)
         {
            fql.append(preproc.getFQL());
            fql.append(")");
         }
         
         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();
         }
      }
      String delStmt = query.toFinalExpression();
      
      boolean full = !hasWhere;
      
      // before performing the actual operation, retrieve all records about to be deleted and
      // decrement the ref-counter for Object fields.
      if (hasOO)
      {
         if (args != null)
         {
            Collections.addAll(ooArgs, args);
         }
         
         List<Record> recs = persistence.list(null, fql.toString(), ooArgs.toArray(), 0, 0, true);
         if (recs != null)
         {
            Map<String, Integer> extentMap = getDmoInfo().getExtentMap();
            Map<String, Method> getterMap = getDmoInfo().getPojoGetterMap();
            for (Record dmo : recs)
            {
               for (String field : getDmoInfo().getOoFields())
               {
                  Method method = getterMap.get(field);
                  PropertyMeta meta = getDmoInfo().getPropsByGetterMap().get(method);
                  DataHandler handler = meta.getDataHandler();
                  int offset = meta.getOffset();
                  Integer extent = extentMap.get(field);
                  if (extent == null)
                  {
                     object val = (object) handler.getField(dmo, offset);
                     decrementObjectCountRef(val);
                  }
                  else 
                  {
                     for (int k = 0; k < extent; k++)
                     {
                        object val = (object) handler.getField(dmo, offset + k);
                        decrementObjectCountRef(val);
                     }
                  }
               }
            }
         }
      }
      
      // the index-based cache was already invalidated at the top of this method
      int rowCount = 0;
      if (useSql)
      {
         // sql is used only in full mode
         rowCount = persistence.executeSQL(delStmt,
                                           Persistence.TEMP_CTX,
                                           additionalArgs.toArray(),
                                           !isUndoable());
      }
      else
      {
         if (args != null)
         {
            Collections.addAll(additionalArgs, args);
         }
         
         Session session = persistenceContext.getSession();
         if (!full)
         {
            // evict from cache all records which will be deleted
            String pkSelect = "select " + dmoAlias + "." + Session.PK + " " + 
                              delStmt.substring("delete ".length());
            ScrollableResults<Long> res = persistence.scroll(pkSelect, additionalArgs.toArray());
            while (res.next())
            {
               Object[] o = res.get();
               Long pk = (Long) o[0];
               BaseRecord rec = session.getCached(getDMOImplementationClass(), pk);
               if (rec != null)
               {
                  session.evict(rec);
                  rec.updateState(session, DmoState.DELETED, true);
               }
            }
            res.close();
         }
         
         rowCount = persistence.deleteOrUpdate(delStmt,
                                               Persistence.TEMP_CTX,
                                               additionalArgs.toArray(), 
                                               getEntityName(), 
                                               !isUndoable());
      }
      
      if (rowCount > 0)
      {
         if (!full)
         {
            Boolean hasRecords = this.fastHasRecords();
            if (hasRecords != null)
            {
               full = !hasRecords;
            }
            else
            {
               Object[] multiplexArgs = new Object[] {multiplexID};
               // execute a simple query to determine if there is any record in this virtual table
               String checkStmt = 
                     "select tt." + Session.PK + " from " + table + " tt " +
                     "where tt." + MULTIPLEX_FIELD_NAME + " = ?" + " limit 1";
               Long id = persistence.getSingleSQLResult(checkStmt, Persistence.TEMP_CTX, multiplexArgs);
               
               full = (id == null);
            }
         }
         
         // Notify all interested parties that a bulk delete occurred.
         // Listening buffers may need to release deleted records.
         RecordChangeEvent.Type eventType =
            (full
             ? RecordChangeEvent.Type.FULL_BULK_DELETE
             : RecordChangeEvent.Type.RESTRICTED_BULK_DELETE);
         changeBroker.stateChanged(eventType, this, full ? currentRec : null, null, null);
      }
      
      // if the virtual table is now empty, mark it as such
      if (full)
      {
         local.nonEmptyTableFlags.remove(multiplexID);
      }

      // WARNING: session cache can't be pruned unless all records are being deleted from the table
      if (full)
      {
         pruneSessionCache();
      }
   }
   
   /**
    * Get the {@link TempTable} resource associated with this temporary buffer.
    * 
    * @return   See above.
    */
   private TempTable tempTableResource()
   {
      return tempTableRef;
   }

   /**
    * Functional interface for setting a BEFORE-BUFFER field.
    */
   @FunctionalInterface
   private interface BeforeFieldUpdater
   {
      /**
       * Actual update of the field.
       *
       * @param   record
       *          The {@code TempRecord} to be updated.
       */
      void update(TempRecord record);
   }
   
   /**
    * Common implementation for all setters of BEFORE-BUFFER special (hidden) fields.
    * 
    * @param   updater
    *          The (lambda) code that does the actual field update.
    * @param   changedField
    *          The java name of the field to be updated. used to notify the {@code ChangeBroker}. 
    * @param   legacy
    *          The legacy name of the field. Only used for logging when operation fails.
    * @param   updatePeer
    *          Update the peer too: make the same change to the peer record.
    */
   private void setupBeforeFlagImpl(BeforeFieldUpdater updater,
                                    String changedField,
                                    String legacy,
                                    boolean updatePeer)
   {
      if (!isAvailable())
      {
         return;
      }
      
      TempRecord record = getCurrentRecord();
      TempRecord snapshot = (TempRecord) record.deepCopy();
      updater.update(record);
      record.updateState(null, DmoState.CHANGED, true);
      
      try
      {
         Map<String, List<Integer>> props = new HashMap<>();
         props.put(changedField, null);
         changeBroker.stateChanged(RecordChangeEvent.Type.UPDATE, this, record, snapshot, props);
         Long peerRowid = record._peerRowid();
         
         if (updatePeer && peerRowid != null)
         {
            RecordBuffer peerBuffer = ((BufferImpl) getDMOProxy()).peerBuffer.buffer();
            Class<? extends Record> peerClass = peerBuffer.getDMOImplementationClass();
            Session session = getPersistence().getSession(Persistence.TEMP_CTX);
            TempRecord peerRecord = (TempRecord) session.get(peerClass, peerRowid);
            if (peerRecord == null)
            {
               // the peer record was created but not reached the session's cache yet, check the peer buffer
               if (peerRowid.equals(peerBuffer.rowID().getValue()))
               {
                  peerRecord = (TempRecord) peerBuffer.getCurrentRecord();
               }
               else
               {
                  // where is the peer record?
                  throw new RuntimeException("Failed to locate record #" + Long.toHexString(peerRowid));
               }
            }
            
            // check if the peer record was changed since it was saved or fetched from database
            boolean doSave = !peerRecord.checkState(DmoState.NEW) &&
                             !peerRecord.checkState(DmoState.CHANGED);
            
            TempRecord peerSnapshot = (TempRecord) peerRecord.deepCopy();
            updater.update(peerRecord);
            peerRecord.updateState(null, DmoState.CHANGED, true);
            
            // reusing props
            changeBroker.stateChanged(RecordChangeEvent.Type.UPDATE,
                                      peerBuffer,
                                      peerRecord,
                                      peerSnapshot,
                                      props);
            
            if (doSave)
            {
               // evidently, the record was changed locally, must flush it to bring it back to same state
               Savepoint sp = session.setSavepoint();
               boolean fullTx = false;
               
               if (sp == null)
               {
                  // not currently within a transaction? Create a micro-transaction to persist the change
                  fullTx = session.beginTransaction(null);
               }
               
               try
               {
                  session.save(peerRecord, true);
               }
               catch (PersistenceException exc)
               {
                  // did something go wrong?
                  if (sp != null)
                  {
                     session.rollbackSavepoint(sp);
                  }
                  else if (fullTx)
                  {
                     session.rollback();
                  }
               }
               
               // release the temporary savepoint or commit the micro-transaction
               if (sp != null)
               {
                  session.releaseSavepoint(sp);
               }
               else if (fullTx)
               {
                  session.commit();
               }
            }
         }
      }
      catch (PersistenceException pe)
      {
         if (LOG.isLoggable(Level.WARNING))
         {
            LOG.log(Level.WARNING, "Failed to set " + legacy, pe);
         }
      }
   }
   
   /**
    * Helper which tracks the multiplex IDs currently in use by DMO interface
    * in the current client context, as well as the reference count of temp
    * tables created in the database using the context's current connection,
    * and domains of master/slave buffers.
    * <p>
    * This object also tracks tables which have been created during the
    * current database session, and drops them when the session is being
    * closed.  This is done as an optimization to prevent potential thrashing
    *in the event a class which creates a temporary buffer and then allows it
    * to go out of scope is invoked repeatedly in a loop.
    */
   private static class Context
   implements SessionListener
   {
      /** Map of DMO interfaces to counts of multiplex IDs currently in use */
      private final Map<Class<?>, Map<Integer, Integer>> inUseMPIDs = new IdentityHashMap<>();
      
      /** Map of open tables to most recent primary key ID generated */
      private final Map<Class<? extends DataModelObject>, AtomicLong> openTables = new IdentityHashMap<>();
      
      /** Set of DMO interfaces whose corresponding tables will be dropped at the next commit */
      private final Set<Class<? extends DataModelObject>> pendingDrop = 
                                                           Collections.newSetFromMap(new IdentityHashMap<>());
      
      /** Names of active temp tables in the order they were created */
      private final Set<String> activeTables = new LinkedHashSet<>();
      
      /** Map of flags indicating whether each virtual table is empty */
      private final RoaringBitmap nonEmptyTableFlags = new RoaringBitmap();
      
      /** Singleton map of a unique key to session ID string */
      private final Map<String, String> sweepMap;
      
      /** Persistence service object for temp tables */
      private Persistence persistence;
      
      /** Persistence context for transaction management */
      private Persistence.Context pctx;
      
      /** Map of master buffers to weak hash maps with slave buffer keys */
      private Map<TemporaryBuffer, Map<TemporaryBuffer, Object>> domains = new WeakHashMap<>();
      
      /** 
       * The counter of stored {@code object}s for each DMO interface.
       * <p>
       * Only the {@code object}s that have active destructors are counted.
       */
      private final Map<Class<?>, RefCount> ooObjectCounter = new IdentityHashMap<>();
      
      /** Next value to try when assigning a multiplex ID */
      private int nextMPID = 1;
      
      /** Cache responsible with storing instances of FastCopyHelper. */
      private final Cache<FastCopyKey, 
                          FastCopyHelper> fastCopyCache = CacheManager.createLRUCache(TemporaryBuffer.class,
                                                                                      256);
      
      /** Flag to indicate that this context database session has the mapping tables for fast-copy created */
      private boolean hasMappingTables = false;
      
      /**
       * Default constructor.
       */
      Context()
      {
         SecurityManager secMgr = SecurityManager.getInstance();
         String sid = String.valueOf(secMgr.sessionSm.getSessionId());
         sweepMap = Collections.singletonMap(TempTableHelper.UNIQUE_SUFFIX_KEY, sid);
      }
      
      /**
       * This method is invoked by the persistence context when a Hibernate session has just
       * ended a transaction or is about to close. Used as a hook to cleanup temp-table and
       * other resources.
       *
       * @param   event
       *          Session event type.
       *
       * @return  {@code false}, indicating that this session listener should remain registered
       *          after this method returns.
       *
       * @throws  PersistenceException
       *          if any error occurs dropping temp tables from the closing session.
       */
      public boolean sessionEvent(SessionListener.Event event)
      throws PersistenceException
      {
         switch (event)
         {
            case SESSION_CLEANUP:
               dropUnusedTables(false);
               
               break;
               
            case SESSION_CLOSING_NORMALLY:
               dropUnusedTables(true);
               cleanup();
               
               break;
               
            case SESSION_CLOSING_WITH_ERROR:
               // On error, we want TempTableConnectionProvider to release the
               // broken connection, so make sure we are not pinning it open.
               activeTables.clear();
               pendingDrop.clear();
               
               // Clear remainder of state.
               Iterator<Class<? extends DataModelObject>> iter = openTables.keySet().iterator();
               while (iter.hasNext())
               {
                  Class<?> next = iter.next();
                  inUseMPIDs.remove(next);
                  iter.remove();
               }
               
               cleanup();
               
               break;
               
            default:
               
               break;
         }
         
         return false;
      }
      
      /**
       * Invoked when this session listener is removed from the list of
       * registered session listeners.
       * <p>
       * This implementation does nothing.
       */
      public void deregisteredSessionListener()
      {
      }
      
      /**
       * Initialize this object by storing the persistence service object and
       * registering as a session listener.  This method is tolerant of being
       * invoked multiple times, but only does its initialization work the
       * first time it is invoked for the current <code>Context</code>
       * instance.
       */
      void init()
      {
         if (persistence != null)
         {
            return;
         }
         
         persistence = PersistenceFactory.getInstance(DatabaseManager.TEMP_TABLE_DB);
         persistence.registerSessionListener(this, SessionListener.Scope.NONE, Persistence.TEMP_CTX);
         pctx = persistence.getContext(Persistence.TEMP_CTX);
      }
      
      /**
       * Destroy the user's in-DB PK mapping tables.
       */
      void cleanup()
      {
         if (hasMappingTables)
         {
            try (Session session = new Session(persistence.getDatabase(Persistence.TEMP_CTX)))
            {
               FastCopyHelper.dropMappings(session);
               hasMappingTables = false;
            }
            catch (PersistenceException e)
            {
               if (LOG.isLoggable(Level.SEVERE))
               {
                  LOG.log(Level.SEVERE, "Problem dropping user's mapping tables.", e);
               }
            }
         }
      }
      
      /**
       * Report the number of temp tables which currently are in use in the
       * current context for the database associated with this buffer.
       *
       * @return  Number of temp tables associated with
       *          <code>persistence</code>.
       */
      int getTableCount()
      {
         return activeTables.size();
      }
      
      /**
       * Returns a context-local multiplex ID value.
       * 
       * @param   inUse
       *          Map of multiplex IDs currently in use in this context, to
       *          reference count of buffers using them.
       * 
       * @return  The next available multiplex ID.
       */
      Integer getNextMPID(Map<Integer, Integer> inUse)
      {
         Integer id = null;
         do
         {
            id = nextMPID++;
         }
         while (inUse.containsKey(id));
         
         return id;
      }
      
      /**
       * Create a temp table in the database for the given DMO interface, if it does not already
       * exist. This will also create all indexes defined for the temp table.
       * 
       * @param   dmoIface
       *          DMO interface for which a backing temp table is needed.
       * @param   table
       *          Name of temp table to be created.
       */
      void createTable(Class<? extends DataModelObject> dmoIface, String table)
      {
         init();
         
         activeTables.add(table);
         
         // remove from pending drop tables, if present
         boolean notPendingDrop = !pendingDrop.remove(dmoIface);
         
         // add to open tables, if not already present
         boolean notOpen = (openTables.putIfAbsent(dmoIface, new AtomicLong()) == null);
         
         // create table only if it doesn't already exist (don't want overhead of using IF NOT
         // EXISTS at the database)
         if (notPendingDrop && notOpen)
         {
            doCreateTable(dmoIface);
         }
      }
      
      /**
       * Remove the given table from the active temp tables in use within the current context,
       * and add it to the pending drop tables. This method does not actually drop a table,
       * since all table drops are deferred until the database session notifies it needs cleanup.
       *  This prevents potential thrashing in the event a class which creates a temporary buffer
       * and then allows it to go out of scope is invoked repeatedly in a loop.
       * 
       * @param   tt
       *          The temp-table resource.
       * @param   table
       *          Name of temp table to be dropped.
       * @param   dmoIface
       *          DMO interface.
       */
      void dropTable(TempTable tt, String table, Class<? extends DataModelObject> dmoIface)
      {
         activeTables.remove(table);

         pendingDrop.add(dmoIface);
      }
      
      /**
       * Returns the next available primary key for the temp table associated with the given DMO interface.
       *
       * @param   dmoIface
       *          Data model object interface.
       * @param   multiplexID
       *          The multiplex ID for the temp-table reclaiming this key.
       *          
       * @return  The primary key for a new DMO associated with this buffer.
       */
      Long nextPrimaryKey(Class<? extends DataModelObject> dmoIface, Integer multiplexID)
      {
          try 
          {
             DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
             {
                //  let FWD-H2 do the primary key providing
                return DirectAccessHelper.nextPrimaryKey(persistence.getSession(!dmoInfo.multiTenant), 
                                                         dmoInfo.getSqlTableName(), 
                                                         multiplexID);
             } 
          }
          catch (DirectAccessException e) 
          {
             // fall-back to the old fashioned in memory sequence
             // this is not optimal as it doesn't reclaim unused keys
             if (LOG.isLoggable(Level.SEVERE) && !e.isExpected())
             {
                LOG.log(Level.SEVERE, "Direct access failure when obtaining the next key: " + e.getMessage(), e);
             }
             
             AtomicLong id = openTables.get(dmoIface);
             return id.incrementAndGet();
          }
      }
      
      /**
       * Prepare a single, primary key for reclamation.  On the next, full
       * transaction commit, this key will be made available for re-use.
       * Until then, it will be stored in a set of keys which are pending
       * reclamation.  This set will be cleared if the current transaction is
       * rolled back.
       * 
       * @param   dmoIface
       *          DMO interface for temporary table records.
       * @param   multiplexID
       *          The multiplex ID for the temp-table reclaiming this key.
       * @param   id
       *          Primary key to be recycled.
       */
      @Deprecated
      void reclaimKey(Class<?> dmoIface, Integer multiplexID, Long id)
      {
         
      }
      
      /**
       * Drop all temporary tables which have been created during the current database session,
       * but which are no longer in use. This occurs when the session notifies that it needs
       * cleanup.
       * 
       * @param   sessionClosing
       *          {@code true} to mark all open tables as unused, so they are dropped;
       *          {@code false} to only drop tables previously marked as ready to be dropped.
       * 
       * @throws  PersistenceException
       *          if a database error occurs dropping any table or creating
       *          the transaction which contains these actions.
       */
      private void dropUnusedTables(boolean sessionClosing)
      throws PersistenceException
      {
         if (sessionClosing)
         {
            pendingDrop.addAll(openTables.keySet());
         }
         
         if (pendingDrop.isEmpty())
         {
            return;
         }
         
         boolean commit = pctx.beginTransaction(null);
         
         try
         {
            Iterator<Class<? extends DataModelObject>> iter = pendingDrop.iterator();
            while (iter.hasNext())
            {
               Class<? extends DataModelObject> dmoIface = iter.next();
               iter.remove();
               openTables.remove(dmoIface);
               doDropTable(dmoIface);
            }
            
            if (commit)
            {
               pctx.commit();
            }
         }
         finally
         {        
            if (sessionClosing)
            {
               openTables.clear();
               pendingDrop.clear();
            }
         }
      }
      
      /**
       * Get the implicit SQL index name.
       * 
       * @param   dmoIface
       *          DMO interface which represents the temp table.
       *          
       * @return  See above.
       */
      private String getImplicitSqlIndexName(Class<? extends DataModelObject> dmoIface)
      {
         String idxName = TempTableHelper.getImplicitSqlIndexName(
               persistence.getDatabase(Persistence.TEMP_CTX), dmoIface);
         return StringHelper.sweep(idxName, sweepMap);
      }
      
      /**
       * Get the implicit index name.
       * 
       * @param   dmoIface
       *          DMO interface which represents the temp table.
       *          
       * @return  See above.
       */
      private String getImplicitIndexName(Class<? extends DataModelObject> dmoIface)
      {
         String idxName = TempTableHelper.getImplicitIndexName(
               persistence.getDatabase(Persistence.TEMP_CTX), dmoIface);
         return StringHelper.sweep(idxName, sweepMap);
      }
      
      /**
       * Issue the SQL statement(s) against the backing database necessary to
       * create the backing temp table needed for the given DMO interface, as
       * well as all indexes defined for that table.  Because temp table
       * instances are multiplexed across nested buffer scopes and across
       * multiple buffers which use the same backing DMO type, this method is
       * invoked only the first time a backing table is actually needed for a
       * particular DMO interface type in the current context.  Thereafter,
       * this method is not invoked again, unless the backing table has first
       * been dropped.
       *
       * @param   dmoIface
       *          DMO interface for which a backing temp table is needed.
       *
       * @throws  ErrorConditionException
       *          if an error occurs creating the table and silent error mode
       *          is disabled.
       *
       * @see     #doDropTable
       */
      private void doCreateTable(Class<? extends DataModelObject> dmoIface)
      {
         String sql = null;
         
         try
         {
            if (LOG.isLoggable(Level.FINER))
            {
               String msg = Utils.describeContext() + "Creating temp-table for " + dmoIface;
               LOG.log(Level.FINER, msg, new Throwable());
            }
            
            Database database = persistence.getDatabase(Persistence.TEMP_CTX);
            List<String> sqlList = new ArrayList<>();
            
            // Create table(s).
            Iterator<String> tableIter = TempTableHelper.sqlTempTableCreate(database, dmoIface);
            while (tableIter.hasNext())
            {
               sql = tableIter.next();
               sqlList.add(sql);
            }
            
            // Create indexes (if any).
            Iterator<String> indexIter = TempTableHelper.sqlTempTableIndexCreate(database, dmoIface);
            while (indexIter.hasNext())
            {
               sql = indexIter.next();
               sql = StringHelper.sweep(sql, sweepMap);
               sqlList.add(sql);
            }
            
            persistence.executeSQLBatch(sqlList, true, Persistence.TEMP_CTX);
            
            pctx.useSession();
         }
         catch (PersistenceException exc)
         {
            openTables.remove(dmoIface);
            
            ErrorManager.recordOrThrowError(
               -1,
               "Error creating temp table:  " + getTableName(dmoIface) + " [" + sql + "]", 
               exc);
         }
      }
      
      /**
       * Issue the SQL statement(s) against the backing database necessary to
       * drop the backing temp table for the given DMO interface.  Because
       * temp table instances are multiplexed across nested buffer scopes and
       * across multiple buffers which use the same backing DMO type, this
       * method is invoked only when the current database session is being
       * closed, after the last buffer scope referencing the backing table in
       * the current context has been closed.
       *
       * @param   dmoIface
       *          DMO interface for which the backing temp table should be
       *          dropped.
       *
       * @throws  PersistenceException
       *          if an error occurs dropping the table and silent error mode
       *          is disabled.
       *
       * @see     #doCreateTable
       */
      private void doDropTable(Class<? extends DataModelObject> dmoIface)
      throws PersistenceException
      {
         try
         {
            if (LOG.isLoggable(Level.FINER))
            {
               String msg = Utils.describeContext() + "Dropping temp-table for " + dmoIface;
               LOG.log(Level.FINER, msg, new Throwable());
            }
            
            pctx.releaseSession();
            
            List<String> sqlList = new ArrayList<>();
            
            Database database = persistence.getDatabase(Persistence.TEMP_CTX);
            
            // drop indexes (if any)
            Iterator<String> indexIter = TempTableHelper.sqlTempTableIndexDrop(database, dmoIface);
            while (indexIter.hasNext())
            {
               String sql = indexIter.next();
               sql = StringHelper.sweep(sql, sweepMap);
               sqlList.add(sql);
            }
            
            // drop table
            Iterator<String> tableIter = TempTableHelper.sqlTempTableDrop(database, dmoIface);
            while (tableIter.hasNext())
            {
               String sql = tableIter.next();
               sqlList.add(sql);
            }
            
            persistence.executeSQLBatch(pctx, sqlList, true);
         }
         catch (PersistenceException exc)
         {
            throw new PersistenceException( "Error dropping temp table:  " + getTableName(dmoIface), exc);
         }
      }
      
      /**
       * Get the backing temp table name associated with the given DMO
       * interface, primarily for debugging and error reporting purposes.
       *
       * @param   dmoIface
       *          DMO interface for which a table name is needed.
       *
       * @return  Name of the temp table associated with the given interface.
       */
      private String getTableName(Class<?> dmoIface)
      {
         DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(dmoIface);
         return dmoInfo.getSqlTableName();
      }
      
      /**
       * Get an instance of a {@code FastCopyHelper} from the cache if it exists, or create a new one
       * if there is no suitable instance in the cache.
       * 
       * @param   fastCopyContext
       *          The context for the {@code FastCopyHelper}.
       *          
       * @return  FastCopyHelper
       *          Instance of {@code FastCopyHelper}, either retrieved from cache, or just created.
       */
      private FastCopyHelper getFastCopyHelper(FastCopyContext fastCopyContext)
      {
         if (!hasMappingTables)
         {
            hasMappingTables = true;
            FastCopyHelper.createMappings(fastCopyContext.srcBuf.getPersistence());
         }
         
         FastCopyKey key = new FastCopyKey(fastCopyContext);
         FastCopyHelper fastCopyHelper = fastCopyCache.get(key);
         if (fastCopyHelper == null)
         {
            fastCopyHelper = fastCopyContext.build();
            fastCopyCache.put(key, fastCopyHelper);
         }
         
         return fastCopyHelper;
      }
   }
   
   /**
    * An object registered with the <code>TransactionManager</code> to be
    * notified when its enclosing buffer's scope is closing.  This drives
    * cleanup of the backing temp table, which can take one of two forms:
    * <ul>
    *   <li>if no other buffers are using the backing table any longer, the
    *       table is dropped (along with all records);
    *   <li>otherwise, only those records associated with this buffer
    *       instance (as defined by this buffer's multiplex ID) are removed
    *       from the table, but the table itself is left intact.
    * </ul>
    */
   private class Multiplexer
   implements Finalizable
   {
      /**
       * Constructor.
       * 
       * @param   ctx
       *          Persistence context for transaction management.
       */
      Multiplexer(Persistence.Context ctx)
      {
      }
      
      /**
       * No-op.
       */
      @Override
      public void iterate()
      {
      }
      
      /**
       * No-op.
       */
      @Override
      public void retry()
      {
      }
      
      /**
       * Cleans up the backing temp table by either dropping it (if no other
       * buffers are using it any longer), or by removing this buffer's
       * records from it.  If we are not already within a database-level
       * transaction, begin a transaction before making the changes, and
       * commit it afterward.
       */
      @Override
      public void finished()
      {
         // only allow cleanup if the outermost scope opened on this buffer has been closed
         if (getOpenScopeCount() > 0)
         {
            return;
         }
         
         closeMultiplexScope();
      }
      
      /**
       * Provides a notification that the external program in which the object is registered is
       * being deleted and the object's reference may be lost after this method is called.
       * <p>
       * This implementation does nothing.
       */
      @Override
      public void deleted()
      {
         // no-op
      }
   }
   
   /**
    * An object of this class is registered at all blocks within a no-undo buffer's scope, except
    * internal procedures (TODO: further testing is needed to verify that this is correct). Upon
    * each iteration, retry, and exit (finished) event of one of those blocks, it performs a
    * non-destructive validation (i.e., validation which reports any error, but does not alter
    * control flow).
    */
   private class NoUndoValidator
   implements Finalizable
   {
      /**
       * The block is exiting; perform a non-destructive validation of the record in the buffer.
       */
      @Override
      public void finished()
      {
         nonDestructiveValidate();
      }
      
      /**
       * The block is iterating; perform a non-destructive validation of the record in the buffer.
       */
      @Override
      public void iterate()
      {
         nonDestructiveValidate();
      }
      
      /**
       * The block is retrying; perform a non-destructive validation of the record in the buffer.
       */
      @Override
      public void retry()
      {
         nonDestructiveValidate();
      }
      
      /**
       * No-op.
       */
      @Override
      public void deleted()
      {
         // no-op
      }
      
      /**
       * Validate the current record in the buffer (if any), but do not change the control flow
       * of the block logic in the event of an error; just report the error if not in silent
       * error mode.
       */
      private void nonDestructiveValidate()
      {
         try
         {
            validate(false, false);
         }
         catch (ErrorConditionException exc)
         {
            // TODO: we eat this because we can't allow the exception to unravel control flow,
            // but we need to report the error if not in silent error mode; requires some changes
            // to the TransactionManager or BlockManager infrastructure
            if (LOG.isLoggable(Level.FINE))
            {
               LOG.log(Level.FINE, "Error validating NO-UNDO temp-table record", exc);
               if (LOG.isLoggable(Level.FINER))
               {
                  LOG.log(Level.FINER, TemporaryBuffer.this.toString());
               }
            }
         }
      }
   }
   
   /**
    * A class that holds the default UNDO attibute for a TEMP-TABLE. Even for static TEMP-TABLEs,
    * the attribute can be changed at runtime. The lifespan of this object is short, it is created
    * at the moment of the definition of the default buffer, and just until the buffer is open,
    * at which moment this {@link UndoStateProvider} is replaced by the {@link TempTable} that
    * will properly manage the UNDO attribute.
    * This class is immutable and is discarded when not needed any more.
    */
   private static class DefaultUndoState
   implements UndoStateProvider
   {
      /** The default value of the UNDO attribute. */
      final boolean undo;
      
      /**
       * The constructor. Initializes the internal state.
       * @param   undo
       *          The default value of the default UNDO attribute.
       */
      private DefaultUndoState(boolean undo)
      {
         this.undo = undo;
      }
      
      /**
       * Accessor for the internal state.
       *
       * @return   The default value of the default UNDO attribute.
       */
      @Override
      public boolean isUndoable()
      {
         return undo;
      }
   }
   
   /**
    * The handler to switch between references, when binding a definition table to another one.
    */
   private static class ReferenceProxy
   implements InvocationHandler
   {
      /** The default properties - PK and any other properties in {@link TempRecord}. */
      private static final Map<String, String> defaultProps;
      
      static
      {
         Map<String, String> props = new HashMap<>();
         for (Method m : TempTableRecord.class.getMethods())
         {
            props.put(m.getName(), m.getName());
         }
         props.put(Session.PK, Session.PK);
         
         defaultProps = Collections.unmodifiableMap(props);
      }
      
      /** Cache of property names, for each DMO. */
      private static final Map<Class<? extends DataModelObject>, String[]> BOUND_PROPERTIES = 
         Collections.synchronizedMap(new IdentityHashMap<>());
      
      /** Cache of property indexes, for each DMO. */
      private static final Map<Class<? extends DataModelObject>, Map<String, Integer>> PROPERTY_INDEXES = 
         Collections.synchronizedMap(new IdentityHashMap<>());
      
      /** 
       * Cache of mappings of definition DMO property names to bound DMO property names.  First key is composed
       * from the definition DMO name, chr(1) and the bound DMO name, as a string. 
       */
      private static final Map<String, Map<String, String>> PROPERTIES = 
         Collections.synchronizedMap(new HashMap<>());
      
      /** The definition-time DMO proxy. */
      private final Temporary def;
      
      /** The buffer instance created at the definition time. This is what {@link #def} proxies. */
      private final TemporaryBuffer defBuffer;
      
      /** The DMO property setters to their property name. */
      private Map<Method, String> defPropsBySetter = null;
      
      /** The DMO property getters to their property name. */
      private Map<Method, String> defPropsByGetter = null;
      
      /** A map between the definition-time DMO properties and their indexes. */
      private Map<String, Integer> propertyIndexes = null;
      
      /** The bound DMO proxy. */
      private Temporary bound = null;
      
      /** The bound DMO property names. */
      private String[] boundProperties = null;
      
      /** The bound DMO property getters. */
      private Map<String, Method> boundGetterByProp = null;
      
      /** The bound DMO property setters. */
      private Map<String, Method> boundSetterByProp = null;
      
      /** The bound DMO buffer.  This is what {@link #bound} proxies. */
      private TemporaryBuffer boundBuffer = null;
      
      /**
       * A map of conversion-time (original) DMO property names to their runtime correspondents. 
       */
      private Map<String, String> properties = null;
      
      /** Force the BUFFER and TEMP-TABLE to be returned from the definition, and not the bound buffer. */
      private boolean forceDefinition = false;

      /** 
       * Flag indicating if the current binding must be auto-deleted when the definition buffer gets deleted,
       * as this was created internally by FWD.
       */
      private boolean autoDeleteBinding = false;
      
      /**
       * Create a new mutable proxy for the given definition-time DMO proxy.
       * 
       * @param    def
       *           The definition-type DMO proxy.
       */
      public ReferenceProxy(Temporary def)
      {
         this.def = def;
         this.defBuffer = (TemporaryBuffer) ((BufferReference) def).buffer();
      }
      
      /**
       * Bind to the specified DMO proxy.
       * 
       * @param    table
       *           The new DMO proxy to bound to.
       *           
       * @return   The old {@link #bound} DMO proxy.
       */
      public Temporary bind(Temporary table)
      {
         return bind(table, false);
      }
      
      /**
       * Bind to the specified DMO proxy.
       * 
       * @param    table
       *           The new DMO proxy to bound to.
       * @param    autoDeleteBinding
       *           Flag indicating if the bound buffer must be auto-deleted when the definition buffer gets 
       *           deleted.
       *           
       * @return   The old {@link #bound} DMO proxy.
       */
      public Temporary bind(Temporary table, boolean autoDeleteBinding)
      {
         this.autoDeleteBinding = autoDeleteBinding;
         
         Class<? extends DataModelObject> defDmoInterface = defBuffer.getDMOInterface();
         
         defPropsBySetter = PropertyHelper.pojoPropertiesBySetter(defDmoInterface);
         defPropsByGetter = PropertyHelper.pojoPropertiesByGetter(defDmoInterface);
         
         Temporary old = bound;
         
         boundBuffer = (TemporaryBuffer) ((BufferReference) table).buffer();
         if (boundBuffer.mutableHandler != null)
         {
            bound = boundBuffer.mutableHandler.bound == null
                       ? boundBuffer.mutableHandler.def 
                       : boundBuffer.mutableHandler.bound;
         }
         else
         {
            bound = (Temporary) boundBuffer.getDMOProxy();
         }
         
         Class<? extends DataModelObject> boundDmoInterface = boundBuffer.getDMOInterface();
         
         // compute the method association between the definition and the runtime binding
         
         List<TableMapper.LegacyFieldInfo> boundFields = null;
         boundProperties = BOUND_PROPERTIES.get(boundDmoInterface);
         if (boundProperties == null)
         {
            boundFields = TableMapper.getAllLegacyFieldInfo(boundBuffer.getParentTable());
            
            boundProperties = new String[boundFields.size()];
            for (int i = 0; i < boundFields.size(); i++)
            {
               boundProperties[i] = boundFields.get(i).getJavaName();
            }
            
            BOUND_PROPERTIES.put(boundDmoInterface, boundProperties);
         }
         
         List<TableMapper.LegacyFieldInfo> cvtFields = null;
         String key = defDmoInterface.toString() + ((char) 1) + boundDmoInterface.toString();
         properties = PROPERTIES.get(key);
         if (properties == null)
         {
            if (boundFields == null)
            {
               boundFields = TableMapper.getAllLegacyFieldInfo(boundBuffer.getParentTable());
            }

            cvtFields = TableMapper.getAllLegacyFieldInfo(defBuffer.getParentTable());

            properties = new HashMap<>();
            properties.putAll(defaultProps);
            for (int i = 0; i < boundFields.size(); i++)
            {
               properties.put(cvtFields.get(i).getJavaName(), boundProperties[i]);
            }
            properties = Collections.unmodifiableMap(properties);
            
            PROPERTIES.put(key, properties);
         }

         propertyIndexes = PROPERTY_INDEXES.get(defDmoInterface);
         if (propertyIndexes == null)
         {
            if (cvtFields == null)
            {
               cvtFields = TableMapper.getAllLegacyFieldInfo(defBuffer.getParentTable());
            }
            
            propertyIndexes = new HashMap<>();
            for (int i = 0; i < cvtFields.size(); i++)
            {
               propertyIndexes.put(cvtFields.get(i).getJavaName(), i);
            }
            
            PROPERTY_INDEXES.put(defDmoInterface, propertyIndexes);
         }
         
         boundSetterByProp = PropertyHelper.pojoSettersByProperty(boundDmoInterface);
         boundGetterByProp = PropertyHelper.pojoGettersByProperty(boundDmoInterface);
         
         return old;
      }

      /**
       * Unbind this mutable proxy.
       */
      public void unbind()
      {
         autoDeleteBinding = false;
         bound = null;
         boundBuffer = null;
         boundProperties = null;
         boundSetterByProp = null;
         boundGetterByProp = null;
         properties = null;
      }

      /**
       * Proxy handler.   It delegates the call to the {@link #bound} buffer and takes care of
       * translating bound buffer's properties to the definition properties (which exist in the
       * converted code).
       * <p>
       * Some method has special routing:
       * <ul>
       *    <li> {@link BufferReference#definition} will always return the {@link #def} buffer.
       *    </li>
       *    <li> {@link BufferImpl#ref()} will always return the runtime instance for this
       *         mutable proxy ({@link #bound} if set, otherwise {@link #def}).</li>
       * </ul>
       */
      @SuppressWarnings("deprecation")
      @Override
      public Object invoke(Object proxy, Method method, Object[] args)
      throws Throwable
      {
         Object instance = (bound == null ? def : bound);
         
         Class<?> declClass = method.getDeclaringClass();
         if (declClass.equals(BufferReference.class) && method.getName().equals("definition"))
         {
            return defBuffer;
         }
         if (BufferImpl.class.isAssignableFrom(declClass) && method.getName().equals("ref"))
         {
            return forceDefinition ? def : instance;
         }
         if (forceDefinition && BufferImpl.class.isAssignableFrom(declClass) && method.getName().equals("tableHandle"))
         {
            instance = def;
         }
         
         if (declClass.equals(BufferReference.class) ||
             declClass.equals(Object.class)          ||
             HandleResource.class.isAssignableFrom(declClass))
         {
            return Utils.invoke(method, instance, args);
         }
         
         if (bound != null && boundBuffer.getDMOInterface() != defBuffer.getDMOInterface())
         {
            if (Temporary.class.isAssignableFrom(declClass))
            {
               boolean setter = true;
               String defProperty = defPropsBySetter.get(method);
               if (defProperty == null)
               {
                  setter = false;
                  defProperty = defPropsByGetter.get(method);
               }
               
               int defIndex = propertyIndexes.get(defProperty);
               String boundProperty = boundProperties[defIndex];
               
               Method mbound = (setter ? boundSetterByProp.get(boundProperty) 
                                       : boundGetterByProp.get(boundProperty));
               
               method = mbound;
            }
         }
         // tried to migrate to related Method.canAccess(this), but it does not work as expected
         // added @SuppressWarnings("deprecation") to invoke method to ignore
         // warning until a correct migration is implemented
         boolean accessible = method.isAccessible();
         try
         {
            if (!accessible)
            {
               method.setAccessible(true);
            }
            
            return Utils.invoke(method, instance, args);
         }
         catch (Throwable t)
         {
            Throwable cause = t;
            while (cause != null && !(cause instanceof ConditionException))
            {
               cause = cause.getCause();
            }
            
            if (cause instanceof ConditionException)
            {
               throw cause;
            }
            else
            {
               throw t;
            }
         }
         finally
         {
            if (!accessible)
            {
               method.setAccessible(false);
            }
         }
      }
   }
   
   /**
    * Table copy mode.
    */
   public enum CopyTableMode
   {
      /** Table is copied using COPY-TEMP-TABLE. */
      COPY_TEMP_TABLE_MODE,
      
      /** Table is passed as an INPUT TABLE[-HANDLE] parameter. */
      INPUT_PARAM_MODE,
      
      /** Table is passed as an OUTPUT TABLE parameter. */
      OUTPUT_TABLE_PARAM_MODE,
      
      /** Table is passed as an OUTPUT TABLE-HANDLE parameter. */
      OUTPUT_TABLE_HANDLE_PARAM_MODE
   }

   /**
   * A getter for the deleted flag.
   * @return True is the buffer was deleted as a resource.
   */
   public boolean isDeleted()
   {
       return deleted;
   }

    /**
     * A setter for the deleted flag.
     *
     * @param deleted
     *        Boolean value indicating that the buffer was deleted.
     */
   public void setDeleted(boolean deleted)
   {
       this.deleted = deleted;
   }
}