BufferImpl.java

/*
** Module   : BufferImpl.java
** Abstract : Implementation of Buffer interface.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --------------------------------------Description----------------------------------------
** 001 ECF 20130118 Created initial version. Implementation of the Buffer
**                  interface; provides services for buffer-related, Progress
**                  methods, attributes, and some language statements.
** 002 VMN 20130207 Added support for handle-based methods
**                  BUFFER-COPY (KW_BUF_COPY)
**                  BUFFER-COMPARE (KW_BUF_COMP)
**                  BUFFER-FIELD (KW_BUF_FLD)
**                  BUFFER-RELEASE (KW_BUF_REL)
**                  FIND-BY-ROWID (KW_FIND_BR)
**                  FIND-UNIQUE (KW_GET_UNI)
**                  FIND-FIRST (KW_FIND_1ST)
**                  FIND-LAST (KW_FIND_LST)
**                  and attributes
**                  NUM-FIELDS (KW_NUM_FLD)
**                  COLUMN-LABEL (KW_COL_LAB)
**                  INITIAL (KW_INIT)
**                  AMBIGUOUS (KW_AMBIG)
** 003 ECF 20130214 Added bufferCreate and variants of bufferCopy.
** 004 SVL 20130213 Added disable*Triggers functions.
** 005 VMN 20130217 Added support for handle-based attributes:
**                  CURRENT-ITERATION (KW_CUR_ITER)
**                  ERROR (KW_ERROR)
**                  DBNAME (KW_DBNAME)
**                  TABLE (KW_TABLE)
** 006 VMN 20130219 Added support for attributes
**                  TABLE-HANDLE (KW_TAB_HAND)
** 007 CA  20130221 Added support for ADM-DATA, UNIQUE-ID, NAMESPACE-URI. Implemented Deletable.
**                  Added missing BUFFER-FIELD stubs.
** 008 GES 20130224 Added support for a DELETE statement with a validation expression.
** 009 ECF 20130224 Added limited support for embedded SQL.
** 010 OM  20130222 Added bufferField variants and dereference from Dereferenceable.
** 011 OM  20130304 Refactored isValid and isUnknown of WrappedResource to valid and unknown.
** 012 VMN 20130306 Corrected support for handle-based methods:
**                  FIND-UNIQUE (KW_GET_UNI)
**                  FIND-FIRST (KW_FIND_1ST)
**                  FIND-LAST (KW_FIND_LST)
** 013 VMN 20130311 Corrected support and runtime for handle-based methods:
**                  FIND-UNIQUE (KW_GET_UNI)
**                  FIND-FIRST (KW_FIND_1ST)
**                  FIND-LAST (KW_FIND_LST)
**                  FIND-BY-ROWID (KW_FIND_BR)
**                  BUFFER-RELEASE (KW_BUF_REL)
**                  BUFFER-COMPARE (KW_BUF_COMP)
** 014 ECF 20130309 Changed API for bufferCopy and bufferCompare; only wrapped data types are
**                  allowed for literal parameters, to prevent the explosion of the API. 
** 015 VMN 20130314 Implemented runtime for handle-based methods:
**                  BUFFER-COMPARE (KW_BUF_COMP) also noLobs parameter type changed
**                  BUFFER-COPY (KW_BUF_COPY) (simple signatures)
**                  First parameter type changed from String to character for
**                  FIND-UNIQUE (KW_GET_UNI)
**                  FIND-FIRST (KW_FIND_1ST)
**                  FIND-LAST (KW_FIND_LST)
** 016 CA  20130323 Changed the APIs for KW_BUF_DEL and KW_EMPTY_TT to return logical.
** 017 ECF 20130417 Moved initial() method to FieldReference.
** 018 VMN 20130426 Implemented runtime for handle-based methods:
**                  BUFFER-COMPARE (KW_BUF_COMP)
**                  BUFFER-COPY (KW_BUF_COPY)
** 019 CA  20130603 resourceDelete must return a boolean value.
** 020 VMN 20130711 Implemented verifyTransaction() and added to handle-based methods:
**                  BUFFER-CREATE (KW_BUF_CREA)
**                  BUFFER-DELETE (KW_BUF_DELETE)
**                  FIND-BY-ROWID (KW_FIND_BR)
** 021 VMN 20130717 Added aggregated functions support for embedded SQL.
** 022 SVL 20130822 Added DYNAMIC attribute, implemented bufferField.
** 023 CA  20130927 Removed unknown() API, as by default the resource is known. 
** 024 CA  20131021 Do not delete static buffers.
** 025 ECF 20131028 Import change.
** 026 SVL 20131028 Handled bufferField() for static tables, added doSetName.
** 027 CA  20131013 Static buffers are deleted only when the instantiating procedure gets deleted.
** 028 OM  20131128 Stub implementation for getUniqueID.
** 029 HC  20131222 Implemented ADM-DATA and UNIQUE-ID attribute.
** 030 VMN 20140110 Implemented bufferField(character).
** 031 OM  20140107 Implemented disableDumpTriggers() and disableLoadTriggers() methods and 
**                  and removed unneeded openReadOnlyScope().
** 032 OM  20140116 Implemented attributes and methods (NUM-FIELDS, ERROR, DBNAME, TABLE,
**                  NAMESPACE-URI, dereference(::), BUFFER-FIELD(int), intermediary step for
**                  findFirst, findLast, findUnique).
** 033 OM  20140122 Added primary index selection for find-by-rowid dynamic finds. Added 
**                  implementation for dynamic finds methods using DynamicQueryHelper.
** 034 ECF 20140305 Added defaultBufferHandle implementation, reworked bufferField(String) to
**                  handle invalid buffer name.
** 035 VMN 20140330 Added LockType errors processing.
** 036 VIG 20140314 Changed currentIteration() method signature.
** 037 OM  20141103 Added doGetName() method.
** 038 ECF 20141117 Added stub methods for all raw-transfer variants.
** 039 ECF 20150114 Reimplemented valid() to always return true. A buffer whose scope is not open
**                  is still valid for use.
** 040 OM  20150205 Queries do not need to be unregistered from DynamicQueryHelper.
** 041 OM  20150302 Fixed bufferField() behavior on invalid field name.
** 042 ECF 20150315 Replaced FieldReference with BufferFieldImpl for BUFFER-FIELD use cases.
** 043 OM  20150303 Fixed hasNext/PrevSibling(). Implemented special resource chaining for
**                  BUFFERs (TEMP-TABLEs in front of permanent TABLE buffers).
** 044 OM  20150506 Updated javadoc for deleteAll() and delete() methods.
** 045 CA  20151219 Implicit buffers for a defined global shared var can not be deleted.  Also,
**                  deleted buffers must be tracked explicitly.
** 046 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 047 ECF 20160219 Added checkExtentRange and outOfBoundsError, methods which are called from
**                  DMO proxies. Reimplemented _numFields.
** 048 EVL 20160325 Javadoc fixes.
** 049 ECF 20160404 Javadoc fixes.
** 050 OM  20160302 Dropped errors generated inside _find() methods.
**         20160507 Partially reverted dropping of _find() errors to fix regressions.
** 051 SVL 20160510 Support for table-level delete validation.
** 052 ECF 20160517 Modified findByRowID to deal with null primary index more efficiently.
** 053 GES 20160530 Validation processing now uses lambdas instead of BufferValidator instances.
** 054 OM  20160623 Small change in error message.
** 055 SVL 20160705 Added indexInformation method.
** 056 IAS 20160714 Use legacy name instead of DMO alias.
** 057 ECF 20160829 Performance enhancement: use local BufferManager to check transaction status
**                  instead of TransactionManager.
** 058 IAS 20161907 Resolve P2JQuery.Parameter in delete(String, Object...)
** 059 ECF 20171016 Implemented readXml and writeXml methods.
** 060 GES 20171206 Switch to ErrorManager.silent().
**     ECF 20171212 Remove redundant query substitution parameter preprocessing for bulk delete.
** 061 OM  20171216 Added delete() variant with support for external buffers for nested selects.
**     CA  20171220 Cleanup the SharedVariableManager.bufferCreator on buffer delete.
** 062 GES 20180419 Added the RAW-TRANSFER() legacy method.
** 063 OM  20181114 Added NamedSerializable interface implementation.
** 064 ECF 20181208 Added stubs for recordLength, findCurrent, readJson, writeJson methods.
** 065 OM  20181206 Added QUERY readonly attribute for BUFFERs.
** 066 OM  20190117 Added CURRENT-CHANGED, FIND-CURRENT. Fixed locking in _find.
**     ECF 20190118 Added basic runtime implementation of readJson, writeJson.
**     OM  20190206 Added implementation of recordLength().
**     OM  20190213 Added error messages in dereference().
**     ECF 20190212 Added no-lobs implementation to bufferCopy, bufferCompare methods.
**     OM  20190228 Removed deleted buffers from dynamic queries.
** 067 OM  20190327 Renamed DataSource to avoid conflicts with DataSet source.
** 068 CA  20190423 Added dereference(Class, String), used for chained :: operators.
**     OM  20190423 Implemented copyTempTable() methods.
** 069 CA  20190511 Added API for BUFFER-FIELD(POLY).
**                  Added BUFFER-VALIDATE stub.
** 070 SVL 20190614 Reflected changes in TableMapper.
** 071 OM  20190426 Extracted hasField() and getHibernateField() methods to avoid code duplication.
**     OM  20190510 Clarified generation of methods and attributes for variables with BUFFER /
**                  TEMP-TABLE ABL namespaces specified. Implemented several attributes.
**     HC  20190707 Implemented read(dataSet) and read(tempTable) methods of JsonObject legacy
**                  class.
** 072 ECF 20190813 Change to RecordBuffer.release API.
** 073 OM  20190720 Added BATCH FILL support. FILL returns FALSE and stops if a child FILL fails.
**                  Fixed CamelCase Sensitivity. Fixed FILL with DATA-SOURCE with provided query.
**     CA  20190728 If a callback raises an ERROR condition, invokeCallback must return false.
**                  Also, any FILL operation must be aborted.
**     CA  20190812 Allow this class to be mutable, to allow switching between resources, in cases
**                  of BY-REFERENCE or REFERENCE-ONLY.  WARNING: it is not allowed to access
**                  instance fields, if they are in another instance than 'this' reference!
**                  Use the 'ref()' API to get the runtime instance or add the field to an 
**                  accessor method.
**     OM  20190906 Added support for MERGE and REPLACE fill modes. Added ERROR-STRING and
**                  ERROR-FLAG attributes support.
** 075 CA  20191119 Fixed some issue with SAVE-ROW-CHANGES: any ERROR message prior to invocation
**                  must not affect the result; fixed the 'source was not changed in the database'
**                  check, where the source and before buffer must be checked. 
** 076 AIL 20191128 Moved the validation phase in RecordBuffer.
** 077 AIL 20200107 Added name override and restore for temporarily changing name with proxy.
**     AIL 20200108 Prevented direct access to record buffer members
**     CA  20200110 Fixes for static buffer resource delete.
** 078 IAS 20200221 Reworked RAW-TRANSFER method support.
** 079 IAS 20200414 bufferValidate() method implementation.
**     CA  20200427 Allow bulk assign support for BUFFER-VALUE and ::, in case of extent fields.
**     CA  20200428 Added LAST-BATCH runtime stub.
**     CA  20200503 Added BUFFER:IS-MULTI-TENANT runtime stub.
** 080 ECF 20200419 Removed Hibernate dependencies.
**     CA  20200714 The rowState is available even for temp-tables not part of a dataset.
** 081 GES 20200807 Reduced ErrorManager context-local usage.
**     CA  20200910 Added BUFFER:SERIALIZE-ROW() runtime stub.
**         20200911 attachDataSource allows empty include/excludeFields values.
**         20200914 attachDataSource allows ROWID function for source field.
**     OM  20201016 Added implementation for LAST-BATCH, DATA-SOURCE-MODIFIED.
**     OM  20201030 Invalid attribute API support for getters/setters.
**     OM  20201202 Fixed persistence support for ACCEPT-CHANGES and REJECT-CHANGES methods.
**     OM  20201205 Added AUTO-DELETE attribute implementation.
**     OM  20201218 Fixed implementation for error and rejected attributes/hidden fields.
**     OM  20210127 Replaced TableMapper.getLegacyName() triple map lookup with direct access to local
**                  dmoMeta.legacyTable. Fixed implementation of validate() method/statement.
**     OM  20210223 Invalidate index-based cache in the event of low-level SQL operations.
**     OM  20210309 Do not use DmoMeta as key in TableMapper because temp-tables might share the same
**                  DmoMeta instance.
**     ECF 20210506 Use RecordBuffer.getDmoInfo() getter instead of direct field access to prevent
**                  NPE in proxy case.
**     CA  20210509 Fixed 'defaultTypeMapping' implicit value for readXml - it must be the unknown value, not
**                  the "?" string.
**     AIL 20210511 Ensure field string names are normalized before searching.
**     OM  20210521 DELETE and CREATE return the proper logical value.
**     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.
**     CA  20210629 Added dereference operator for indexed extent fields (rvalue and lvalue).
**     OM  20210809 Completed implementation of SERIALIZE-ROW|WRITE-JSON}(JsonObject|JsonArray).
**     ECF 20210820 Retrofit for query substitution parameter preprocessing fixes.
**     OM  20210921 Added missing implementations for NAMESPACE-PREFIX and XML-NODE-NAME attribute.
**     ECF 20210924 Minor refactoring.
**     CA  20210929 Fixed ATTACH-DATA-SOURCE when rowid function is used in the pairs.
**     OM  20211023 Fixed FILL for EXTENT fields.
**     AL2 20211105 Character values provided by dereference are case insensitive.
**     ECF 20211203 Fixed error checking in _find method.
**     OM  20211206 Improve implementation of DATA-SOURCE-COMPLETE-MAP attribute. Fixed FILL assignments.
**     OM  20211020 Added _DATASOURCE_ROWID property.
**     CA  20211228 BUFFER:IS-MULTI-TENANT attribute must return false, even if is not implemented - unknown 
**                  is not an acceptable returned value for this attribute.
**                  Replaced the UnimplementedFeature.missing from tenant-related APIs with a TODO comment.
**     CA  20211204 Fixed BUFFER-FIELD behavior, they need to behave like real resources, with their lifetime 
**                  linked with the parent BUFFER resource.
**     OM  20220218 When saving DS changes, allow Session.PK to be resolved instead of dereferencing it.
**                  Javadoc update.
**     OM  20220224 Small fixed of the error message for invalid WHERE clause.
**     OM  20220422 Allow Xml/JsonImport to load records into buffer without exposing package private methods.
**     OM  20220620 Added missing features in ATTACH-DATA-SOURCE method.
**     OM  20220707 Shared meta-knowledge about fields and properties of a record was collected in
**                  DmoMeta to avoid duplicating their collection and storage.
**     CA  20220707 Create the BUFFER-FIELD resources only when they are accessed.
**     OM  20220801 Allow unqualified field names in attachDataSource().
**     OM  20220810 Fixed check for changes in saveRowChangesImpl2() to include extent fields.
**     OM  20221003 Added missing STREAM targets for JSON/XML serializations.
**     CA  20220930 Refactored the callback invocation to be performed via a call-site and InvokeConfig, to
**                  allow caching of the resolved target.
**     CA  20221006 Added JMX instrumentation for 'resourceDelete'. Refs #6814
**                  UNIQUE-ID and ADM-DATA are kept as Java types instead of BDT.  Refs #6827
**                  BUFFER-FIELD's instantiating procedure must be the BUFFER's, and not when it gets lazily
**                  created.
**     CA  20221010 Reduced 'toLowerCase' usage in 'attachDataSource'.  Refs #6820
**     TJD 20220504 Upgrade do Java 11 minor changes
**     CA  20220426 Added PRIMARY attribute stubs.
**     OM  20220526 Added KEYS and SYNCHRONIZE implementations.
**     CA  20220531 If the BUFFER is in 'unknown mode' (due to an outer join not finding a record), do not 
**                  raise 'no record is available' ERROR condition.
**     CA  20220601 Added 'derefPoly', emitted when '::' is used as r-value in a dynamic extent assignment.
**     OM  20220603 Fixed the order of validation for DATA-SOURCE-ROWID getter.
**     OM  20220604 Improved dereferenciation support.
**     OM  20220607 Added EXTENT support for SAVE-ROW-CHANGES.
**     CA  20220613 Fixed RAW-TRANSFER via 'rawCopyFrom(Buffer)' - the data was being read twice from the 
**                  stream.  Also, the dest buffer creates a new record only if there isn't one already loaded.
**     OM  20220617 Improve DATA-SOURCE-ROWID implementation.
**     OM  20220620 Added missing features in ATTACH-DATA-SOURCE method and DATA-SOURCE-ROWID attribute.
**     OM  20220609 Fixes for several 4GL attributes.
**     CA  20220909 When a query gets deleted and is set at this buffer's 'lastQuery', remove the reference.
**     CA  20220918 Always delete the REPLACE dynamic query at the FILL operation.
**     CA  20221010 Reduced 'toLowerCase' usage in 'attachDataSource'.  Refs #6820
**     CA  20221121 For a dynamic TEMP-TABLE, if the NAME attribute is changed, then the default-buffer's NAME
**                  must be changed, too.
**     SVL 20230108 Improved performance by replacing some "for-each" loops with indexed "for" loops.
**     RAA 20230109 Changed inline statement(s) to prepared statement(s).
**     CA  20230110 Do not create a 'callbacks' map until the application actually sets a callback.
**     CA  20230110 UNIQUE-ID is generated only when is read by the application.
**                  Cache the helper for ProcedureManager, ObjectOps and others (as needed), to reduce  
**                  context-local lookup.
**     SVL 20230110 P2JIndex.components() provides direct access to the components array in order to improve
**                  performance.
**     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.
** 082 CA  20230303 When deleting a temp-table, the delete can be vetoed if any buffer for the temp-table is
**                  associated with an OUTPUT dataset.  Refactored to move the resource-specific code in a 
**                  'allowDelete' method.
** 083 CA  20230215 Improved other cases to reduce BDT usage from within FWD runtime.
**     CA  20230303 CA/20190720 change in HandleOps.delete related to postponing a temp-table delete if is an 
**                  OUTPUT argument is currently flawed, is it compares handle instances instead of the
**                  handle's resource.  For this reason, this code was commented out.
** 084 OM  20230320 bufferFieldNative() use the original property name, not a random one from denormalized set.
** 085 IAS 20230405 Fixed FILL support for recursive DATA-RELATION
**                  Added support for the RESTART-ROW attribute.
**                  Added support for the SYNCHRONIZE method.
** 086 IAS 20230325 Added source validation to readJson().
**     IAS 20230501 Added 'emergency exit' for WRITE-XML
** 087 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 088 RAA 20230525 Creating a new BaseDataType instance is now done using BaseDataTypeFactory.
** 089 CA  20230523 Performance improvement of the buffer fill operation - when possible (data type match),
**                  both source and destination buffers), use direct access to read and write the value. 
**                  Extent fields are not supported yet.
** 090 CA  20230607 TRANSACTION-MODE AUTO must preserve the tx in the root appserver block - this is required
**                  because in modes other than State-free, requests can be executed on the same agent who had
**                  TRANSACTION-MODE AUTO, and this must see as active the tx initially set.
** 091 CA  20230701 In FILL, if the source buffer has no record, do nothing, and allow the record to use the
**                  default values.
** 092 AL2 20230704 Avoid .buffer() calls in attachDataSource, so precomupte the dmo meta for each buffer.
** 093 OM  20230712 Reworked the DATA-SOURCE-COMPLETE-MAP and the related methods and attributes.
** 094 CA  20230724 Further reduce context-local usage.
** 095 CA  20230815 Ensure a dynamic query created by the FWD runtime during a FILL operation gets deleted
**                  (this is the case when the data-source does not specify a query handle).
** 096 CA  20230817 A dynamic BUFFER part of a dataset can't be explicitly deleted.
** 097 EVL 20230817 Adding GET-ITERATION implementation stub.
** 098 CA  20230828 If 'parentDataSource' is invalid, allow attaching another datasource.
** 099 CA  20231114 ERROR conditions via ErrorManager API calls must be followed by a return statement.
** 100 AD  20231127 Optimized bufferFieldnative(int) to directly use DmoMeta to get name of property.
** 101 OM  20231031 Added error handling in MERGE-CHANGES and MERGE-ROW-CHANGES methods.
** 102 CA  20231129 Directly set the INSTANTIATING-PROCEDURE referent, from the parent BUFFER's, instead of
**                  passing it as a handle. 
**     CA  20231221 Lazily create fieldsByName map.
** 103 RAA 20231220 Rewrote extent value parsing in assign. Modified two loops to be index-type ones.
** 104 OM  20240110 Fixed management of implicit/explicit FILL queries.
** 105 OM  20240109 Before writing a JSON/XML all buffers of the temp-tables must be flushed. After the
**                  operation ends the used buffer is released.
** 106 CA  20240206 Error 11874 must not set ERROR-STATUS:ERROR flag.
** 107 HC  20240222 Enabled JMX on FWD Client.
** 108 AL2 20240307 Mark the query to retrieve the entire result set (useful for FILL operations).
** 109 CA  20240320 Avoid BDTs within internal FWD runtime.  Replaced iterators with Java 'for', where it 
**                  applies.
** 110 CA  20240323 Performance optimization to cache the FieldStructure for each legacy field, during FILL
**                  operations.
**     CA  20240324 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime.
**     CA  20240331 Use a plain HashMap with strings for the FieldStructure cache, to avoid recomputing the 
**                  hashCode.
** 111 OM  20240401 When attaching a data-source with explicit query, the latter is configured with the
**                  substitution buffers from this buffer's dataset.
** 112 CA  20240404 Errors 12360, 12780, 12781, 14275, 11935, 11906, 11909, 11912, 11913 do not set the 
**                  ERROR-STATUS:ERROR flag.
** 113 CA  20240411 Flush is done for all buffers in 'copyChanges' so validation is no longer needed for 
**                  'getChanges'.
** 114 CA  20240410 SAVE-ROW-CHANGES must use only the data-source fields which are belong to the source 
**                  buffer.
** 115 AL2 20240412 Enforce released record flushing, overruling the fact that is may be referenced by other
**                  buffers.
** 116 CA  20240424 The buffers on which the DATA-SOURCE is attached must be available to any FIND- queries.
** 117 CA  20240422 Allow the query to be reset to its initialization state, so the same instance can be used 
**                  to execute child buffer FILL operations.
** 118 CA  20240525 Bulk delete can't be executed if TRACKING-CHANGES is on, FWD runtime needs to emulate an 
**                  actual FOR EACH to delete all records.
** 119 SB  20240528 Fixed a regression where honorMultipleReferences was honored when releasing a dynamic
**                  buffer preventing, its flush. Refs #8760. 
** 120 DDF 20240523 Added an additional parameter to displayError() call in dereference().
**     DDF 20240524 Replaced displayError() with recordOrShowError() in dereference().
**     DDF 20240527 Replaced recordOrShowError() with recordOrShowDatabaseError() where possible.
**     DDF 20240428 Reverted changes and added only the ones tested for database behavior.
** 121 TJD 20240124 Java 17 - checkExtentRange and setQuery need to be public to enable access from generated
**                  code
** 122 SP  20240603 Fixed error handling when FILL fails with unique validation error in APPEND mode.
** 123 SP  20240606 Error 4065 must not set ERROR-STATUS:ERROR flag.
** 124 CA  20240627 Always register the buffer as dirty for the batch fill operation, as direct access 
**                  bypasses the proxy call in RecordBuffer.
** 125 CA  20240723 ACCEPT-CHANGES must not prune the session cache, but update the before-fields for the 
**                  cached records directly.
** 126 SP  20240805 Skip using RESOURCE_DEL TimeStat if JMX_DEBUG flag is not set.
** 127 CA  20240918 Removed default methods from interfaces and moved them as concrete implementations, as
**                  default methods cause the Java metaspace to spike one order of magnitude.
** 128 OM  20241001 Implemented tenant-related attributes.
** 129 SP  20241029 Added NPE failsafe for copyTempTable and deleteAll. Javadoc fixes.
** 130 CA  20241030 Cache the BUFFER-FIELD handles.
** 131 AI  20241107 Fixed a regression in H130 - 'bufferField' must validate the index.
** 132 ES  20241030 Changed default onPhrase in the doBlock in saveRowChangesImpl2 method.
** 133 OM  20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
**                  based on [sharedDb] parameter.
** 134 AS  20241203 Introduced areBDTsEqual, used to detect changes in saveRowChanges.
** 135 ICP 20250123 Used integer and logical constants to leverage caches instances.
** 136 ES  20250312 Added label methods.
** 137 ES  20250312 Added methods autoSynchronize and canRead methods.
** 138 CA  20250321 Added 'setCallback' overloads.
** 139 ES  20250508 Added runtime support for AUTO-SYNCHRONIZE buffer attribute.
** 140 ES  20250313 Moved NAMESPACEURI, NAMESPACEPREFIX, SERIALIZENAME, XMLNODENAME attributes 
**                  to buffer instance.
*/

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

package com.goldencode.p2j.persist;

import java.io.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.Level;
import java.lang.reflect.*;
import com.goldencode.p2j.*;
import com.goldencode.p2j.jmx.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.persist.trigger.*;
import com.goldencode.p2j.persist.serial.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.BlockManager.TransactionType;
import com.goldencode.p2j.util.ErrorManager.*;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.util.UniqueIdGenerator.*;
import com.goldencode.p2j.persist.TempTable.BeforeType;
import com.goldencode.util.*;
import org.apache.commons.lang3.tuple.*;

import static com.goldencode.p2j.util.BlockManager.*;

/**
 * This class implements services representing Progress builtin functions,
 * methods, attributes, and language statements, which operate on record
 * buffers.
 * <p>
 * Converted business logic invokes these methods on DMO proxies via a
 * DMO-specific interface in the form <code>&lt;DMO interface&gt;.Buf</code>.
 * Each such interface extends both a custom interface of DMO-specific getter
 * and setter methods, as well as the {@link Buffer} interface, which defines
 * the API for these standard services.
 * <p>
 * Dynamic proxy classes for each DMO type extend this class, so that business
 * logic can invoke these standard services directly as method calls on DMO
 * proxy instances.
 * <p>
 * This class is intentionally package private, since it is not meant to be
 * extended outside of this package (and within this package only by dynamic
 * proxy class), nor instantiated directly.
 * <p>
 * This class implements {@link BufferReference} in order to grant its
 * instance methods access to the {@link RecordBuffer} object associated with
 * each instance of the DMO proxy class which extends this class. This access
 * is necessary for these methods to do their work. Even though {@link
 * #buffer()} is a public method, external packages will not be able to invoke
 * this method (except through reflection). This is by design, as business
 * logic is not meant to have access to <code>RecordBuffer</code> instances.
 * The default implementation of this method here throws an exception, as it
 * is never intended to be called, but it needs to be present in order for
 * this class to compile, since it is referenced by other methods. Each DMO
 * proxy class overrides this method to provide a working implementation.
 */
public abstract class BufferImpl
extends HandleChain
implements Buffer
{
   /** Instrumentation for {@link #resourceDelete()}. */
   private static final NanoTimer RESOURCE_DEL = NanoTimer.getInstance(FwdServerJMX.TimeStat.OrmBufferDelete);

   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(BufferImpl.class.getName());
   
   /** The call-site for the {@link Buffer#BEFORE_FILL_EVENT} event callback. */
   private static final InvokeConfig BEFORE_FILL_CALL_SITE = new InvokeConfig();

   /** The call-site for the {@link Buffer#AFTER_FILL_EVENT} event callback. */
   private static final InvokeConfig AFTER_FILL_CALL_SITE = new InvokeConfig();
   
   /** The call-site for the {@link Buffer#BEFORE_ROW_FILL_EVENT} event callback. */
   private static final InvokeConfig BEFORE_ROW_FILL_CALL_SITE = new InvokeConfig();

   /** The call-site for the {@link Buffer#AFTER_ROW_FILL_EVENT} event callback. */
   private static final InvokeConfig AFTER_ROW_FILL_CALL_SITE = new InvokeConfig();

   /** The call-site for the {@link Buffer#ROW_CREATE_EVENT} event callback. */
   private static final InvokeConfig ROW_CREATE_CALL_SITE = new InvokeConfig();

   /** The call-site for the {@link Buffer#ROW_DELETE_EVENT} event callback. */
   private static final InvokeConfig ROW_DELETE_CALL_SITE = new InvokeConfig();

   /** The call-site for the {@link Buffer#ROW_UPDATE_EVENT} event callback. */
   private static final InvokeConfig ROW_UPDATE_CALL_SITE = new InvokeConfig();

   /** The call-site for the {@link Buffer#FIND_FAILED_EVENT} event callback. */
   private static final InvokeConfig FIND_FAILED_CALL_SITE = new InvokeConfig();

   /** The call-site for the {@link Buffer#SYNCHRONIZE_EVENT} event callback. */
   private static final InvokeConfig SYNCHRONIZE_CALL_SITE = new InvokeConfig();

   /**
    * Special 'pointer' used in management of BUFFER resources. This helps delimit the TEMP-tables
    * from PERMANENT ones in the list of BUFFERs. The TEMP-TABLE buffers are kept always in front
    * of the list.
    * To be sure there are no conflicts with legacy resource types declared in 
    * {@link LegacyResource}, this type starts with an underscore.
    */
   private static final String PERMANENT_TABLE = "_PERMANENT-TABLE";

   /** Corresponds to ADM-DATA attribute. */
   private String admData = null;
   
   /** Corresponds to UNIQUE-ID attribute. */
   private Long uniqueID = null;
   
   /** Flag indicating this buffer was deleted. */
   private boolean deleted;
   
   /** Flag indicating that the deletion of this buffer is in progress. */
   private boolean deleting;
   
   /** The last query this buffer was added/set to. */
   private QueryWrapper lastQuery = null;
   
   /** The current dataset. May be {@code null}. */
   private DataSet dataSet = null;
   
   /** The attached {@code DataSource}, if any. */
   private DataSource dataSource = null;
   
   /** The current value of BATCH-SIZE attribute. Valid only when {@code dataSet} is valid. */
   private int batchSize = 0;
   
   /** Current value of the {@code DATA-SOURCE-MODIFIED} attribute. */
   private boolean dataSourceModified = false;
   
   /** The current value of LAST-BATCH attribute. Valid only when {@code dataSet} is valid. */
   private boolean lastBatch = false;
   
   /**
    * The current value of the {@code FILL-MODE} attribute. The default value is {@code MERGE}.  For buffers
    * not part of a dataset, {@link #fillMode()} will return the unknown value. 
    */
   private String fillMode = FM_MERGE;
   
   /** The current value of AUTO-DELETE attribute. */
   private boolean autoDelete = true;
   
   /**
    * Flag used for bracketing internal management of BEFORE-BUFFER. ABL programmer's direct
    * actions (CREATE/DELETE/ALTER buffer) are outside this bracket and will lead to errors. 
    */
   private boolean setUpBeforeBuffer = false;
   
   /** Is this an AFTER-BUFFER or BEFORE-BUFFER? Cached after value is computed. */
   private BeforeType beforeBufferType = BeforeType.UNKNOWN;
   
   /** The current value of the {@code ATTACHED-PAIRLIST} attribute. */
   private String attachedPairlist = null;
   
   /** The current value of the {@code DATA-SOURCE-COMPLETE-MAP} attribute. */
   private String dataSrcMappingStr = null;
   
   /** The mapping of fields in data source. */
   private List<Pair<String, DataSource.FieldReference>> dataSrcMapping = null;
   
   /**
    * The property meta for the destination buffer. The key is the legacy name.  This is set when the 
    * data-source is assigned. 
    */
   private Map<String, PropertyMeta> dstFieldMeta = null;
   
   /** A map with all known callbacks. The keys MUST be in UPPERCASE. */
   private Map<String, CallbackData> callbacks = null;
   
   /** This is not null, when a Buffer proxy overrides the name and a backup is required */
   private String oldName;
   
   /** The list of buffer fields. */
   private BufferFieldImpl[] fields = null;
   
   /** Cache of handle references to return.  The assumption is they are read-only. */
   private handle[] fieldHandles = null;
   
   /** A mapping of fields by their property name. */
   private HashMap<String, BufferFieldImpl> fieldsByName = null;
   
   /**
    * The cached peer buffer. For AFTER-BUFFER it is the BEFORE-BUFFER and vice-versa. The SIMPLE
    * buffers do not have a peer buffer.
    */
   protected BufferImpl peerBuffer = null;
   
   /** The parent {@code DataSource}, if any. This is differnet from the attached datasource! */
   private DataSource parentDataSource = null;
   
   /** The list of keys, if the buffer is a component of a {@code DataSource}. */
   private List<String> keys = null;
   
   /** The NAMESPACE-URI of the XML node for serialized data of this {@code TEMP-TABLE}. */
   private String namespaceURI = null;
   
   /** The NAMESPACE-PREFIX of the XML node for serialized data of this {@code TEMP-TABLE}. */
   private String namespacePrefix = null;
   
   /** The current value of the {@code SERIALIZE-NAME} attribute. */
   private String serializeName = null;
   
   /** The name of the XML node for serialized data of this {@code TEMP-TABLE}. */
   private String xmlNodeName = null;
   
   /** 
    * The buffer's current FILL query, which is reset on each sub-fill executed for this buffer.  Will be 
    * closed only when the complete FILL operation completes.
    */
   private QueryWrapper reusableQuery;
   
   /** The current value of AUTO-SYNCHRONIZE attribute. Valid only when {@code dataSet} is valid. */
   private boolean autoSynchronize = false;
   
   /** The mode for finding (first, last, unique) records in a dynamic call to a buffer handle. */
   enum FindMode
   {
      FIRST,   /* apply first() to a query */
      LAST,    /* apply last() to a query */
      UNIQUE   /* apply unique() to a query */
   }
   
   /**
    * The default constructor. Calls the super with appropriate parameter values.
    */
   protected BufferImpl()
   {
      super(false, false);
   }
   
   /**
    * Worker static method for wrapping ErrorConditionException and reusing
    * in all P2JQuery.findFirst() implementations
    *
    * @param   findQuery
    *          instance of FindQuery
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
    */
   private static logical wrapFirst(FindQuery findQuery, LockType lockType)
   {
      boolean success = true;
      try
      {
         findQuery.first(lockType);
      }
      catch (ErrorConditionException exc)
      {
         success = false;
      }
      return logical.of(success);
   }
   
   /**
    * Worker static method for wrapping ErrorConditionException and reusing
    * in all P2JQuery.findLast() implementations
    *
    * @param   findQuery
    *          instance of FindQuery
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    */
   private static logical wrapLast(FindQuery findQuery, LockType lockType)
   {
      boolean success = true;
      try
      {
         findQuery.last(lockType);
      }
      catch (ErrorConditionException exc)
      {
         success = false;
      }
      return logical.of(success);
   }
   
   /**
    * Worker static method for wrapping ErrorConditionException and reusing
    * in all P2JQuery.findUnique() implementations
    *
    * @param   findQuery
    *          instance of FindQuery
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
    */
   private static logical wrapUnique(FindQuery findQuery, LockType lockType)
   {
      boolean success = true;
      try
      {
         findQuery.unique(lockType);
      }
      catch (ErrorConditionException exc)
      {
         success = false;
      }
      return logical.of(success);
   }
   
   /**
    * Checks if transaction is active otherwise records or shows error message.
    * 
    * @param   buffer
    *          Record buffer.
    *
    * @return  <code>true</code> if a transaction is active else <code>false</code>.
    */
   private static boolean verifyTransaction(RecordBuffer buffer)
   {
      if (buffer.getBufferManager().isTransaction())
      {
         return true;
      }
      else
      {
         String msg = "Unable to CREATE/DELETE unless a TRANSACTION is running";
         ErrorManager.recordOrShowError(7345, msg, false);
         
         return false;
      }
   }
   
   /**
   * Get the {@link RecordBuffer} instance associated with this object.
   * <p>
   * This implementation exists only to satisfy the Java compiler. This
   * method is only meaningful when a proxy DMO class is created by {@link
   * RecordBuffer}, which will implement this method to return an instance
   * of <code>RecordBuffer</code> associated with the proxy DMO instance.
   *
   * @return  Record buffer associated with this object.
   */
   public abstract RecordBuffer buffer();
   
   /**
    * Resolve the actual resource used by the runtime.
    * 
    * @return   The instance.
    */
   public BufferImpl ref()
   {
      return this;
   }
   
   /**
    * Get the PRIMARY attribute.
    * 
    * @return   The primary index.
    */
   @Override
   public character primaryIndex()
   {
      P2JIndex primaryIndex = buffer().getDmoInfo().getPrimaryIndex(true);
      if (primaryIndex == null)
      {
         return new character("default");
      }
      
      return new character(primaryIndex.getLegacyName());
   }
   
   /**
    * Set the PRIMARY attribute.
    * 
    * @param    primary
    *           The primary index.
    */
   @Override
   public void primaryIndex(String primary)
   {
      if (primary == null)
      {
         ErrorManager.recordOrShowError(4083, "PRIMARY", LegacyResource.BUFFER + " widget");
         // **Unable to assign UNKNOWN value to attribute <attribute> on <widget id>. (4083)
         return;
      }
      
      ErrorManager.recordOrShowError(4052, "PRIMARY", "setable", LegacyResource.BUFFER + " widget");
      // **<attribute> is not a <settable/queryable> attribute for <widget id>. (4052)
   }
   
   /**
    * Set the PRIMARY attribute.
    * 
    * @param    primary
    *           The primary index.
    */
   @Override
   public void primaryIndex(Text primary)
   {
      primaryIndex(primary.toJavaType());
   }
   
   /**
    * Get the BUFFER:LAST-BATCH attribute.
    * 
    * @return  the value of BUFFER:LAST-BATCH attribute.
    */
   @Override
   public logical lastBatch()
   {
      return (dataSet == null) ? logical.UNKNOWN : logical.of(lastBatch && batchSize > 0);
   }
   
   /**
    * Set the BUFFER:LAST-BATCH attribute.
    * 
    * @param    lastBatch
    *           The flag state.
    */
   @Override
   public void lastBatch(boolean lastBatch)
   {
      if (dataSet == null)
      {
         ErrorManager.recordOrShowError(12360, false);
         return;
      }
      
      // all checked, can set value now:
      this.lastBatch = lastBatch; 
   }
   
   /**
    * Set the BUFFER:LAST-BATCH attribute.
    * 
    * @param    lastBatch
    *           The flag state.
    */
   @Override
   public void lastBatch(logical lastBatch)
   {
      if (lastBatch == null || lastBatch.isUnknown())
      {
         ErrorManager.recordOrShowError(4083, "LAST-BATCH", "BUFFER widget");
         return;
      }
      
      lastBatch(lastBatch.booleanValue()); 
   }
   
   /**
    * Get the BUFFER:AUTO-DELETE attribute.
    *
    * @return  the value of BUFFER:AUTO-DELETE attribute.
    */
   @Override
   public logical autoDelete()
   {
      if (!_dynamic() || dataSet == null)
      {
         return logical.UNKNOWN;
      }
      
      return logical.of(this.autoDelete);
   }
   
   /**
    * Set the BUFFER:AUTO-DELETE attribute.
    *
    * @param    autoDelete
    *           The flag state.
    */
   @Override
   public void autoDelete(boolean autoDelete)
   {
      this.autoDelete = autoDelete;
   }
   
   /**
    * Set the BUFFER:AUTO-DELETE attribute.
    *
    * @param    autoDelete
    *           The flag state.
    */
   @Override
   public void autoDelete(logical autoDelete)
   {
      if (autoDelete == null || autoDelete.isUnknown())
      {
         ErrorManager.recordOrShowError(4083, "AUTO-DELETE", LegacyResource.BUFFER + " widget");
         // **Unable to assign UNKNOWN value to attribute <attribute> on <widget id>.
         return;
      }
      
      autoDelete(autoDelete.booleanValue());
   }
   
   /**
    * Checks whether the data in the data source has been modified.
    *
    * @return  {@code true} when the data has been modified.
    */
   @Override
   public logical isDataSourceModified()
   {
      return logical.of(dataSourceModified);
   }
   
   /**
    * Manually mark the data in the target structure as modified.
    *
    * @param   mod
    *          {@code true} when the data is marked as modified and {@code false} otherwise.
    */
   @Override
   public void setDataSourceModified(logical mod)
   {
      if (mod == null || mod.isUnknown())
      {
         unableToAssignUnknown("DATA-SOURCE-MODIFIED", "DATASET widget");
         return;
      }
      
      setDataSourceModified(mod.getValue());
   }
   
   /**
    * Manually mark the data in the target structure as modified.
    *
    * @param   mod
    *          {@code true} when the data is marked as modified and {@code false} otherwise.
    */
   @Override
   public void setDataSourceModified(boolean mod)
   {
      this.dataSourceModified = mod;
   }
   
   /**
    * Reports if this object is valid for use.
    *
    * @return  the state of the {@link #deleted} flag.  If a record buffer has been defined, it
    *          is valid for use until is deleted.
    */
   public boolean valid()
   {
      return !deleted;
   }
   
   /**
    * Check whether the delete operation for this buffer is in progress. 
    * 
    * @return  {@code true} when the delete operation for this buffer is under way. 
    */
   public boolean isDeleting()
   {
      return deleting;
   }
   
   /**
    * Obtain the handle to the buffer that was created by default for the temporary table backing
    * this buffer. Note that this implementation presumes that this is a temp-table buffer. It
    * should not be invoked on a permanent table buffer.
    * 
    * @return  Default buffer for the temporary table backing this buffer, or an unknown handle if
    *          that resource is not valid.
    */
   public handle defaultBufferHandle()
   {
      TempTable tempTable = buffer().getParentTable();
      if (tempTable == null || !tempTable.valid())
      {
         return new handle();
      }
      
      return tempTable.defaultBufferHandle();
   }
   
   /**
    * Obtain the handle to the buffer that was created by default for the temporary table backing
    * this buffer. Note that this implementation presumes that this is a temp-table buffer. It
    * should not be invoked on a permanent table buffer.
    * 
    * @return  Default buffer for the temporary table backing this buffer, or an unknown handle if
    *          that resource is not valid.
    */
   @Override
   public Buffer defaultBufferHandleNative()
   {
      TempTable tempTable = buffer().getParentTable();
      if (tempTable == null || !tempTable.valid())
      {
         return null;
      }
      
      return tempTable.defaultBufferHandleNative();
   }
   
   /**
    * Get the record ID of the current buffer's current record, if any.  P2J
    * implementation of the RECID function.
    * <p>
    * This method will return a 32-bit recid which represents the internal
    * row ID of the given record.  If the actual row ID (which is implemented
    * as a wider integer) cannot fit due to overflow, an error condition will
    * be raised.
    *
    * @return  Record ID of the underlying data record. If no record currently
    *          backs the buffer associated with <code>dmo</code>, a recid
    *          initialized as unknown value is returned.
    * 
    * @throws  ErrorConditionException
    *          if the actual record ID is too wide to fit within a 32-bit
    *          integer.
    */
   public recid recordID()
   {
      return buffer().recordID();
   }
   
   /**
    * Get the row ID of the current buffer's current record, if any.  P2J
    * implementation of the ROWID function.
    *
    * @return  Row ID of the underlying data record.  If no record currently
    *          backs the buffer associated with <code>dmo</code>, a
    *          <code>rowid</code> initialized as unknown value is returned.
    */
   public rowid rowID()
   {
      return buffer().rowID();
   }

   /**
    * Get the row ID of the current buffer's current record, if any.
    *
    * @return  Row ID of the underlying data record.  If no record currently backs the buffer associated with 
    *          <code>dmo</code>, a <code>null</code> value is returned.
    */
   public Long _rowID()
   {
      return buffer()._rowID();
   }
   
   /**
    * Indicate whether a backing data record currently is available.
    *
    * @return  <code>true</code> if data is available, <code>false</code> if
    *          no record backs this buffer.
    */
   public logical available()
   {
      return (logical.of(_available()));
   }
   
   /**
    * Indicate whether a backing data record currently is available.
    *
    * @return  <code>true</code> if data is available, <code>false</code> if
    *          no record backs this buffer.
    */
   public boolean _available()
   {
      return buffer().isAvailable();
   }
   
   /**
    * Checks if the record in the buffer has changed in database since the 
    * last <code>FIND CURRENT</code> or <code>GET CURRENT</code>.
    * P2J implementation of 4GL function <code>CURRENT-CHANGED</code>.
    *
    * @return  <code>true</code> if the database record differs from the
    *          value in memory buffer that was queried with the last
    *          <code>FIND</code> and <code>false</code> otherwise
    */
   @LegacyAttribute(name = "CURRENT-CHANGED")
   public logical currentChanged()
   {
      return logical.of(_currentChanged());
   }
   
   /**
    * Checks if the record in the buffer has changed in database since the 
    * last <code>FIND CURRENT</code> or <code>GET CURRENT</code>.
    * P2J implementation of 4GL function <code>CURRENT-CHANGED</code>.
    *
    * @return  <code>true</code> if the database record differs from the
    *          value in memory buffer that was queried with the last
    *          <code>FIND</code> and <code>false</code> otherwise
    */
   public boolean _currentChanged()
   {
      return buffer().isRecordChanged();
   }
   
   /**
    * Indicate whether the buffer associated currently contains a record, and
    * whether that record is transient.  A record is transient if it has been
    * newly created and has not yet been saved to the database.
    *
    * @return  <code>true</code> if a record exists and is new, else
    *          <code>false</code>.
    */
   public logical newlyCreated()
   {
      return (logical.of(_newlyCreated()));
   }
   
   /**
    * Indicate whether the buffer associated currently contains a record, and
    * whether that record is transient.  A record is transient if it has been
    * newly created and has not yet been saved to the database.
    *
    * @return  <code>true</code> if a record exists and is new, else
    *          <code>false</code>.
    */
   public boolean _newlyCreated()
   {
      return buffer().isNewlyCreated();
   }
   
   /**
    * Indicate whether the last record retrieval attempt was denied because
    * the target record was locked.
    *
    * @return  <code>true</code> if the record was locked;  <code>false</code>
    *          if the attempt succeeded, or if this query has not yet
    *          attempted to retrieve a record.
    */
   public logical locked()
   {
      return (logical.of(_locked()));
   }
   
   /**
    * Indicate whether the last record retrieval attempt was denied because
    * the target record was locked.
    *
    * @return  <code>true</code> if the record was locked;  <code>false</code>
    *          if the attempt succeeded, or if this query has not yet
    *          attempted to retrieve a record.
    */
   public boolean _locked()
   {
      return buffer().wasLocked();
   }
   
   /**
    * Indicate whether the last record retrieval attempt failed because the
    * query criteria was ambiguous.
    *
    * @return  <code>true</code> if the query was ambiguous;
    *          <code>false</code> if the attempt succeeded, or if no attempt
    *          has yet been made.
    */
   public logical ambiguous()
   {
      return (logical.of(_ambiguous()));
   }
   
   /**
    * Indicate whether the last record retrieval attempt failed because the
    * query criteria was ambiguous.
    *
    * @return  <code>true</code> if the query was ambiguous;
    *          <code>false</code> if the attempt succeeded, or if no attempt
    *          has yet been made.
    */
   public boolean _ambiguous()
   {
      return buffer().wasAmbiguous();
   }
   
   /**
    * Getter for the IS-MULTI-TENANT attribute.
    * 
    * @return   See above.
    */
   @Override
   public logical isMultiTenant()
   {
      return logical.of(buffer().getDmoInfo().multiTenant);
   }
   
   /**
    * Setter for the IS-MULTI-TENANT attribute.
    * 
    * @param    l
    *           The flag value.
    */
   @Override
   public void setMultiTenant(logical l)
   {
      ErrorManager.recordOrShowError(4052, "IS-MULTI-TENANT", "setable" /*sic!*/, LegacyResource.BUFFER); 
   }
   
   /**
    * Setter for the IS-MULTI-TENANT attribute.
    * 
    * @param    l
    *           The flag value.
    */
   @Override
   public void setMultiTenant(boolean l)
   {
      ErrorManager.recordOrShowError(4052, "IS-MULTI-TENANT", "setable" /*sic!*/, LegacyResource.BUFFER);
   }
   
   /**
    * Perform a RAW-TRANSFER operation from this buffer to the given raw variable. Serializes the
    * record currently in the buffer into the specified field.
    * 
    * @param   r
    *          A <code>raw</code> field to which to copy the record in this buffer.
    */
   public void rawCopyTo(raw r)
   {
      // TODO: this is just a basic implementation, it needs improvements to handle boundary
      //       conditions, add proper error handling is missing and the serialization is 
      //       not binary compatible with the 4GL version
      
      if (r == null)
      {
         return;
      }
      
      boolean error = false;
      
      // obtain the signature for the buffer
      ArrayList<TableMapper.LegacyFieldInfo> fields = TableMapper.getLegacyOrderedList(this, true);
      TableSignature sig = new TableSignature(fields);
      
      // collect all serialized output in an in-memory byte[]
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      ObjectOutputStream    oos = null;
      
      try
      {
         // collect all serialized output in an in-memory byte[]
         oos = new ObjectOutputStream(baos);
         
         // serialize the signature
         sig.writeExternal(oos);
         
         RecordBuffer         buffer  = buffer();
         BufferReference      proxy   = buffer.getDMOProxy();
         Map<String, Method>  getters = buffer.getDmoInfo().getLegacyGetterMap();
         Map<String, Integer> extents = buffer.getDmoInfo().getExtentMap();
         
         // iterate through the field descriptors (in POSITION sequence)
         for (int j = 0; j < fields.size(); j++)
         {
            TableMapper.LegacyFieldInfo lfi = fields.get(j);
            String prop = lfi.getJavaName();
            
            // extent or not?
            Integer extent = extents.get(prop);
            
            // obtain the getter methods for the current property
            Method getter = getters.get(prop);
            
            Externalizable result;
            
            // read the value
            if (extent == null)
            {
               // scalar case
               result = (Externalizable) getter.invoke(proxy);
               
               // serialize
               result.writeExternal(oos);
            }
            else
            {
               // array case
               for (int i = 0; i < extent; i++)
               {
                  result = (Externalizable) getter.invoke(proxy, i);
               
                  // serialize
                  result.writeExternal(oos);
               }
            }
         }
      }
      
      catch (IOException | SecurityException | IllegalAccessException | InvocationTargetException ex)
      {
         error = true;
         ErrorManager.recordOrThrowError(new NumberedException("RAW-TRANSFER failed!", -1, ex));
      }
      
      finally
      {
         // the baos does not need to be closed and can be accessed even after you call close()
         if (oos != null)
         {
            try
            {
               // this one should be closed
               oos.close();
            }
            
            catch (IOException ex)
            {
               // don't care
            }
         }
      }
      
      // save the result in the raw variable
      if (!error)
      {
         r.assign(baos.toByteArray());
      }
   }
   
   /**
    * Perform a RAW-TRANSFER operation from this buffer to the given field, which should be of
    * type <code>raw</code>. Serializes the record currently in the buffer into the specified
    * field.
    * 
    * @param   field
    *          A <code>raw</code> field to which to copy the record in this buffer.
    */
   public void rawCopyTo(FieldReference field)
   {
      if (field == null)
      {
         return;
      }
      
      raw r = new raw();
      
      rawCopyTo(r);
      field.set(r);
   }
   
   /**
    * Perform a RAW-TRANSFER operation from this buffer to the given buffer.
    * 
    * @param   buffer
    *          A buffer into which to copy the record in this buffer.
    * @return <code>true</code> if successful
    */
   public boolean rawCopyTo(Buffer buffer)
   {
      if (buffer == null)
      {
         return false;
      }
      
      raw r = new raw();
      
      rawCopyTo(r);
      return buffer.rawCopyFrom(r);
   }
   
   /**
    * Perform a RAW-TRANSFER operation from the given raw field into this buffer. Deserializes the
    * data in the specified field into this buffer.
    * 
    * @param   field
    *          A <code>raw</code> field from which to copy the record into this buffer.
    */
   public void rawCopyFrom(FieldReference field)
   {
      if (field == null)
      {
         return;
      }
      
      rawCopyFrom((raw) field.get());
   }
   
   /**
    * Perform a RAW-TRANSFER operation from the given raw field into this buffer. Deserializes the
    * data in the specified field into this buffer.
    * 
    * @param   field
    *          A <code>raw</code> field from which to copy the record into this buffer.
    * @return <code>true</code> if successful
    */
   public boolean rawCopyFrom(raw field)
   {
      // TODO: this is just a basic implementation, it needs improvements to handle boundary
      //       conditions, add proper error handling is missing and the deserialization is 
      //       not binary compatible with the 4GL version
      
      if (field == null || field.isUnknown())
      {
         ErrorManager.recordOrShowError(
               4952,
               "Couldn't extract raw value from source field of RAW-TRANSFER",
               false, false, true
         );
         return true;
      }
      
      byte[] data = field.getByteArray();
      
      if (data == null || data.length == 0)
      {
         ErrorManager.recordOrShowError(
               4952,
               "Couldn't extract raw value from source field of RAW-TRANSFER",
               false, false, true
         );
         return true;
      }
      
      // this should create a new record (causing any existing record release and firing triggers as needed
      if (!_available())
      {
         create();
      }
      
      // obtain the signature for the buffer
      ArrayList<TableMapper.LegacyFieldInfo> fields = TableMapper.getLegacyOrderedList(this, true);
      TableSignature tarSig = new TableSignature(fields);
      
      // collect all serialized output in an in-memory byte[]
      ByteArrayInputStream bais = new ByteArrayInputStream(data);
      try (ObjectInputStream ois = new ObjectInputStream(bais))
      {
         // read all serialized input from an in-memory byte[]
         
         TableSignature srcSig = new TableSignature();
         
         // deserialize the signature
         srcSig.readExternal(ois);
         
         if (!srcSig.equals(tarSig))
         {
            ErrorManager.recordOrShowError(
                  4955,
                  "Table signatures do not match in RAW-TRANSFER operation",
                  false, false, true
            );
            return true;
         }
         
         RecordBuffer         buffer  = buffer();
         Map<String, Integer> extents = buffer.getDmoInfo().getExtentMap();
         Map<String, BaseDataType[]> values = readValues(fields, extents, ois);
         if (values == null)
         {
            return true;
         }
         
         BufferReference      proxy   = buffer.getDMOProxy();
         Map<String, Method>  setters = buffer.getDmoInfo().getLegacySetterMap();
         
         boolean batchError = true;
         RecordBuffer.startBatch(buffer.bufferManager, true);
         
         try
         {
            // iterate through the field descriptors (in POSITION sequence)
            for (int j = 0; j < fields.size(); j++)
            {
               TableMapper.LegacyFieldInfo lfi = fields.get(j);
               String prop = lfi.getJavaName();
               BaseDataType[] pvals = values.get(prop);
               
               // extent or not?
               Integer extent = extents.get(prop);
               
               // obtain the setter methods for the current property
               Method setter = setters.get(prop);
               
               BaseDataType value = null;
               
               // process the value
               if (extent == null)
               {
                  // call the default constructor
                  value = pvals[0];
                  
                  // scalar case
                  setter.invoke(proxy, value);
               }
               else
               {
                  // array case
                  for (int i = 0; i < extent; i++)
                  {
                     // call the default constructor
                     value = pvals[i];
                     
                     // scalar case
                     setter.invoke(proxy, i, value);
                  }
               }
            }
            
            batchError = false;
         }
         finally
         {
            RecordBuffer.endBatch(buffer.bufferManager, batchError);
         }
         return true;
      }
      catch (StreamCorruptedException sce)
      {
         ErrorManager.recordOrShowError(
               4952,
               "Couldn't extract raw value from source field of RAW-TRANSFER",
               false, false, true
         );
         return true;
      }
      catch (IOException                  |
             ReflectiveOperationException |
             SecurityException            |
             IllegalArgumentException e)
      
      {
         // TODO: if something happened during the copy but after the create, do we need to put
         //       the original record back in?
         
         ErrorManager.recordOrThrowError(new NumberedException("RAW-TRANSFER failed!", -1, e));
      }
      return false;
   }

   /**
    * RAW-TRANSFER() method implementation that maps its processing to the same methods used
    * for implementation of the RAW-TRANSFER statement.  This will serialize data from this
    * buffer into a RAW field or buffer OR it will deserialize data from a RAW field or buffer
    * into this buffer.
    * 
    * @param   toMode
    *          {@code true} if the copy direction is into the handle's resource and {@code false}
    *          if the data is being copied into the buffer.
    * @param   h
    *          A buffer or raw buffer field.
    * 
    * @return  <code>true</code> if RAW-TRANSFER() succeeds, else <code>false</code>.
    */
   @LegacyMethod(name = "RAW-TRANSFER")
   public logical rawTransfer(boolean toMode, handle h)
   {
      if (h == null || h.isUnknown())
      {
         ErrorManager.recordOrShowError(
               4065,
               "**The RAW-TRANSFER attribute on the BUFFER widget has invalid arguments",
               false, false, false
         );
         return logical.FALSE;
      }

      WrappedResource wr = (WrappedResource) h.get();
      
      if (wr instanceof Buffer)
      {
         Buffer buf = (Buffer) wr;
         
         if (toMode)
         {
            if (!available().booleanValue())
            {
               ErrorManager.recordOrShowError(
                     4951,
                     "Source element of a RAW-TRANSFER statement has no record",
                     false, false, true
               );
               return logical.FALSE;
            }
            return logical.of(rawCopyTo(buf));
         }
         else
         {
            if (!buf.available().booleanValue())
            {
               ErrorManager.recordOrShowError(
                     4951,
                     "Source element of a RAW-TRANSFER statement has no record",
                     false, false, true
               );
               return logical.FALSE;
            }
            return logical.of(buf.rawCopyTo(this));
         }
      }
      else if (wr instanceof BufferField)
      {
         BufferField fld = (BufferField) wr;
         String ftype = ((CommonField) h.getResource()).getDataType().getValue();
         if (!"raw".equals(ftype))
         {
            String fn = h.unwrap().name().getValue();
            ErrorManager.recordOrShowDatabaseError(
                  4824,
                  String.format("Field '%s' referenced in RAW-TRANSFER must be of type RAW", fn),
                  false, false
            );
            return logical.FALSE;
         }
         if (toMode)
         {
            if (!available().booleanValue())
            {
               ErrorManager.recordOrShowError(
                     4951,
                     "Source element of a RAW-TRANSFER statement has no record",
                     false, false, true
               );
               return logical.FALSE;
            }
            raw r = new raw();
            rawCopyTo(r);
            
            fld.changeValue(r);
         }
         else
         {
            if (!h.unwrapBufferField().available().booleanValue())
            {
               ErrorManager.recordOrShowError(
                     4951,
                     "Source element of a RAW-TRANSFER statement has no record",
                     false, false, true
               );
               return logical.FALSE;
            }
            BaseDataType bdt = fld.value();
            
//            if (!(bdt instanceof raw))
//            {
//               // TODO: error?
//               return logical.FALSE;
//            }
            
            return logical.of(rawCopyFrom((raw) bdt));
         }
      }
      else
      {
         // untested at this time for 'database behavior' related to throwing structured exceptions 
         // - see recordOrShowDatabaseError
         ErrorManager.recordOrShowError(
               4067,
               "The second argument for RAW-TRANSFER on the BUFFER widget should" +
               " not be of type WIDGET-HANDLE",
               false, true, false
         );
         return logical.FALSE;
      }
      
      // TODO: we probably need to know if there was a failure in the backing worker methods
      //       but the current signatures don't return a value
      return logical.TRUE;
   }
   
   /**
    * RAW-TRANSFER() method implementation that maps its processing to the same methods used
    * for implementation of the RAW-TRANSFER statement.  This will serialize data from this
    * buffer into a RAW field or buffer OR it will deserialize data from a RAW field or buffer
    * into this buffer.
    * 
    * @param   toMode
    *          {@code true} if the copy direction is into the handle's resource and {@code false}
    *          if the data is being copied into the buffer.
    * @param   h
    *          A buffer or raw buffer field.
    * 
    * @return  <code>true</code> if RAW-TRANSFER() succeeds, else <code>false</code>.
    */
   @LegacyMethod(name = "RAW-TRANSFER")
   public logical rawTransfer(logical toMode, handle h)
   {
      if (toMode == null || toMode.isUnknown())
      {
         // TODO: check this behavior
         return logical.FALSE;
      }
      
      return rawTransfer(toMode.booleanValue(), h);
   }
   
   /**
    * Create a new record in the buffer.
    *
    * @throws  ErrorConditionException
    *          if there is an error retrieving the next available primary key
    *          from the database or locking the record.
    */
   public void create()
   {
      buffer().create();
   }
   
   /**
    * Delete the record currently in the buffer, if any.
    *
    * @return  {@code true} if the delete was possible.
    *
    * @throws  ErrorConditionException
    *          if no record currently is loaded in the buffer, or if there is
    *          an error deleting the current record from the database.
    */
   public logical deleteRecord()
   throws ErrorConditionException
   {
      if (isBeforeBuffer() && !setUpBeforeBuffer)
      {
         // the BEFORE-BUFFERS can only be queried, not altered in any way (REMOVE/CHANGE)
         ErrorManager.recordOrThrowError(12374, doGetName(), "");
         // You may not delete BEFORE-TABLE <name> record.  Use ACCEPT-ROW-CHANGES instead.
         return logical.FALSE;
      }
      
      // no validation is taken in consideration
      return deleteRecord(() -> logical.TRUE, () -> new character("")); 
   }
   
   /**
    * Delete the record currently in the buffer, IF and ONLY IF the given validation expression
    * evaluates <code>true</code>.
    *
    * @param    valexp
    *           The validation expression to evaluate.
    * @param    valmsg
    *           The error message to display when validation fails.
    *
    * @return   <code>true</code> if validation succeeds, <code>false</code> if it fails and no
    *           exception is raised.
    *
    * @throws   ErrorConditionException
    *           If no record currently is loaded in the buffer, or if there is an error deleting
    *           the current record from the database or if the validation expression fails.
    */
   public logical deleteRecord(Supplier<logical> valexp, String valmsg)
   throws ErrorConditionException
   {
      return deleteRecord(valexp, () -> new character(valmsg));
   }
   
   /**
    * Delete the record currently in the buffer, IF and ONLY IF the given validation expression
    * evaluates <code>true</code>.
    *
    * @param    valexp
    *           The validation expression to evaluate.
    * @param    valmsg
    *           The error message to display when validation fails.
    *
    * @return   <code>true</code> if validation succeeds, <code>false</code> if it fails and no
    *           exception is raised.
    *
    * @throws   ErrorConditionException
    *           If no record currently is loaded in the buffer, or if there is an error deleting
    *           the current record from the database or if the validation expression fails.
    */
   public logical deleteRecord(Supplier<logical> valexp, Supplier<character> valmsg)
   throws ErrorConditionException
   {
      RecordBuffer buffer = buffer();
      
      if (!buffer.isTemporary())
      {
         if (!verifyTransaction(buffer))
         {
            return logical.FALSE;
         }
         
         // the validation is done at compile type, thus dynamic buffers with validation
         // can't be deleted - also no delete trigger is fired
         if (buffer.isDynamic())
         {
            Class<? extends DataModelObject> iface = buffer.getDMOInterface();
            if (TableMapper.hasValidation(iface))
            {
               String msg = "Table " + buffer.getDmoInfo().legacyTable + " is defined with schema delete " +
                            "validation and may only be deleted by the DELETE statement";
               ErrorManager.recordOrShowError(7347, msg, false, false);
               
               return logical.FALSE;
            }
         }
      }
      
      if (dataSet != null) // implies buffer.isTemporary()
      {
         // checks whether tracking is active
         if (buffer.getParentTable()._isTrackingChanges())
         {
            if (!invokeCallback(ROW_DELETE_EVENT))
            {
               returnError(); // veto on record delete
            }
         }
      }
      
      try
      {
         return logical.of(buffer.delete(valexp, valmsg));
      }
      catch (PersistenceException e)
      {
         return logical.FALSE;
      }
   }
   
   /**
    * Delete all records associated with this buffer, which match the criteria specified by the
    * given where clause.  Note that this where clause must not contain DMO alias qualifiers.
    * <p>
    * <b>NOTE:</b>  the current implementation must not be invoked for a DMO which is backed by a
    * permanent table;  only temp-table bulk delete is supported at this time.
    * <p>
    * When this method returns:
    * <ul>
    *    <li><strong>temp-table</strong>buffers became empty (Ie. {@code available} returns 
    *          {@code false}). This is because these buffers are used in a loop and the last
    *          record is also deleted. P2J will detect these loops and convert it to the much
    *          faster bulk delete. P4GL does not allow bulk delete using SQL 
    *          {@code DELETE FROM ... WHERE} syntax.
    *    <li>P2J does not support yet bulk deletion of <strong>permanent table</strong> buffers.
    *          In P4GL they will be empty if the predicate matches the currently stored record and
    *          remains unaffected otherwise.
    * </ul>
    *
    * @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 HQL 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  ErrorConditionException
    *          if there is an error deleting the records.
    * @throws  UnsupportedOperationException
    *          if invoked on a buffer associated with a permanent table.
    */
   public void delete(DataModelObject[] suppDMOs, String where, Object... args)
   {
      RecordBuffer buffer = buffer();
      AbstractQuery.preprocessSubstitutionArguments(buffer.bufferManager, () -> false, false, args);
      try
      {
         buffer.delete(suppDMOs, where, args);
      }
      catch (PersistenceException exc)
      {
         ErrorManager.recordOrThrowError(exc);
      }
   }
   
   /**
    * Delete all records associated with this buffer, which match the criteria specified by the
    * given where clause.  Note that this where clause must not contain DMO alias qualifiers.
    * <p>
    * <b>NOTE:</b>  the current implementation must not be invoked for a DMO which is backed by a
    * permanent table;  only temp-table bulk delete is supported at this time.
    * <p>
    * When this method returns:
    * <ul>
    *    <li><strong>temp-table</strong>buffers became empty (Ie. {@code available} returns 
    *          {@code false}). This is because these buffers are used in a loop and the last
    *          record is also deleted. P2J will detect these loops and convert it to the much
    *          faster bulk delete. P4GL does not allow bulk delete using SQL 
    *          {@code DELETE FROM ... WHERE} syntax.
    *    <li>P2J does not support yet bulk deletion of <strong>permanent table</strong> buffers.
    *          In P4GL they will be empty if the predicate matches the currently stored record and
    *          remains unaffected otherwise.
    * </ul>
    * 
    * @param   where
    *          An HQL 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  ErrorConditionException
    *          if there is an error deleting the records.
    * @throws  UnsupportedOperationException
    *          if invoked on a buffer associated with a permanent table.
    */
   public void delete(String where, Object ...args)
   {
      delete(null, where, args);
   }
   
   /**
    * Bulk delete all records associated with the {@code Buffer} or {@code TempTable}.  This allows bulk
    * delete when TRACKING-CHANGES is on, as this emulates a {@code FOR EACH tt1. DELETE tt1. end.} block.
    *
    * @throws  ErrorConditionException
    *          If an error occurs performing the bulk delete operation.
    */
   @Override
   public void deleteForAll()
   {
      if (!(this instanceof Temporary))
      {
         ErrorManager.recordOrShowError(7352);
         // You may only invoke EMPTY-TEMP-TABLE on valid temp-tables.
         return;
      }

      // need to update the ORIGIN-HANDLE attribute. Will defer execution through default buffer
      TempTableBuilder.asTempTable((Temporary) this).deleteForAll();
   }
   
   /**
    * Bulk delete all records associated with the buffer.
    * <p>
    * When this method returns, the buffer is empty (Ie. {@link #available} returns {@code false})
    * for both temp and permanent tables.
    * TODO: when is this invoked for permanent tables ?????
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the bulk delete operation.
    */
   @Override
   public logical deleteAll()
   {
      if (!(this instanceof Temporary))
      {
         ErrorManager.recordOrShowError(7352);
         // You may only invoke EMPTY-TEMP-TABLE on valid temp-tables.
         return logical.FALSE;
      }
      
      TempTable tt = TempTableBuilder.asTempTable((Temporary) this);
      if (tt == null)
      {
         LOG.log(Level.SEVERE,
                 "Nothing to delete, this dynamic TEMP-TABLE was not prepared yet.",
                 new Throwable());
         return logical.FALSE;
      }
      
      // need to update the ORIGIN-HANDLE attribute. Will defer execution through default buffer
      return tt.deleteAll();
   }
   
   /**
    * Aggregate method which averages the values of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void average(String column, NumberType num)
   {
      average(column, num, null);
   }
   
   /**
    * Aggregate method which averages the values of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void average(String column, NumberType num, String where)
   {
      average(column, num, where, (Object[]) null);
   }
   
   /**
    * Aggregate method which averages the values of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void average(String column, NumberType num, String where, Object...args)
   {
      buffer().average(column, num, where, args);
   }
   
   /**
    * Aggregate method which counts the rows in the table backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate. May be the wildcard
    *          character ("*").
    * @param   num
    *          Variable into which to store the aggregate result.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void count(String column, NumberType num)
   {
      count(column, num, null);
   }
   
   /**
    * Aggregate method which counts the rows in the table backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate. May be the wildcard
    *          character ("*").
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void count(String column, NumberType num, String where)
   {
      count(column, num, where, (Object[]) null);
   }
   
   /**
    * Aggregate method which counts the rows in the table backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate. May be the wildcard
    *          character ("*").
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void count(String column, NumberType num, String where, Object...args)
   {
      buffer().count(column, num, where, args);
   }
   
   /**
    * Aggregate method which finds the minimum value of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void minimum(String column, NumberType num)
   {
      minimum(column, num, null);
   }
   
   /**
    * Aggregate method which finds the minimum value of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void minimum(String column, NumberType num, String where)
   {
      minimum(column, num, where, (Object[]) null);
   }
   
   /**
    * Aggregate method which finds the minimum value of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void minimum(String column, NumberType num, String where, Object...args)
   {
      buffer().minimum(column, num, where, args);
   }
   
   /**
    * Aggregate method which finds the maximum value of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void maximum(String column, NumberType num)
   {
      maximum(column, num, null);
   }
   
   /**
    * Aggregate method which finds the maximum value of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void maximum(String column, NumberType num, String where)
   {
      maximum(column, num, where, (Object[]) null);
   }
   
   /**
    * Aggregate method which finds the maximum value of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void maximum(String column, NumberType num, String where, Object...args)
   {
      buffer().maximum(column, num, where, args);
   }
   
   /**
    * Aggregate method which sums the values of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void sum(String column, NumberType num)
   {
      sum(column, num, null);
   }
   
   /**
    * Aggregate method which sums the values of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void sum(String column, NumberType num, String where)
   {
      sum(column, num, where, (Object[]) null);
   }
   
   /**
    * Aggregate method which sums the values of a single column across rows in the table
    * backing this buffer.
    * 
    * @param   column
    *          An expression indicating the name of the column to aggregate.
    * @param   num
    *          Variable into which to store the aggregate result.
    * @param   where
    *          HQL where clause used to filter the records.
    * @param   args
    *          Query substitution arguments.
    * 
    * @throws  ErrorConditionException
    *          if an error occurs performing the aggregate operation.
    */
   public void sum(String column, NumberType num, String where, Object...args)
   {
      buffer().sum(column, num, where, args);
   }
   
   /**
    * Release the record currently in the buffer, if any. No validation, no write trigger is fired. This
    * method is invoked as result of an internal error condition when the content of the buffer failed
    * internal validation and the buffer must be released.
    */
   public void drop()
   {
      buffer().release(true, false);
   }
   
   /**
    * Validate and release the record currently in the buffer, if any. May cause a write trigger to fire.
    *
    * @throws  ErrorConditionException
    *          if the buffer fails validation.
    */
   @Override
   public void release()
   {
      // the converted RELEASE statement is forcefully flushing the record, so the fact that the underlying 
      // record is loaded in multiple buffers in the same time shouldn't be honored.
      release(false);
   }
   
   /**
    * Validate and release the record currently in the buffer, if any. May cause a write trigger to fire.
    * 
    * @param   honorMultipleReferences
    *          {@code true} if the flushing of the existing record should be suppressed if the record is also
    *          loaded in other buffers.
    *
    * @throws  ErrorConditionException
    *          if the buffer fails validation.
    */
   public void release(boolean honorMultipleReferences)
   {
      buffer().release(false, true, honorMultipleReferences);
   }
   
   /**
    * Validate the record currently in the buffer, if any.
    *
    * @throws  ErrorConditionException
    *          if the buffer fails validation.
    */
   @Override
   public void validate()
   {
      try
      {
         buffer().validate(true);
      }
      catch (ValidationException e)
      {
         // rethrow as 4GL error condition
         ErrorManager.recordOrThrowError(e.getNumber(), e.getMessage(), false, false);
      }
   }
   
   /**
    * Validate the record currently in the buffer, if any.
    *
    * @throws  ErrorConditionException
    *          if the buffer fails validation.
    */
   @Override
   public logical bufferValidate()
   {
      RecordBuffer recBuf = buffer();
      
      try
      {
         recBuf.validate(true);
         return logical.TRUE;
      }
      catch (ValidationException e)
      {
         if (recBuf.isNewlyCreated())
         {
            // untested at this time for 'database behavior' related to throwing structured exceptions 
            // - see recordOrShowDatabaseError
            ErrorManager.recordOrShowError(e.getNumber(), e.getMessage(), false, false, false);
         }
      }
      
      return logical.FALSE;
   }
   
   /**
    * Conversion of the RECORD-LENGTH built-in function.
    * 
    * @return  Length of the record currently in the buffer, in bytes.
    */
   @Override
   public integer recordLength()
   {
      if (!_available())
      {
         return new integer(); // quick out
      }
      
      RecordBuffer         buffer  = buffer();
      BufferReference      proxy   = buffer.getDMOProxy();
      Map<String, Method>  getters = buffer.getDmoInfo().getLegacyGetterMap();
      Map<String, Integer> extents = buffer.getDmoInfo().getExtentMap();
      
      int counter = 15; // hidden 2 fields and any header
      int fieldCounter = getters.size() + 2;
      if (fieldCounter > 15)
      {
         counter += 3;
      }
      counter += 2 * ((fieldCounter + 1) / 16);
         
      for (Map.Entry<String, Method> entry : getters.entrySet())
      {
         String prop = entry.getKey();
         Method method = entry.getValue();
         Integer ext = extents.get(prop);
         if (ext == null)
         {
            try
            {
               BaseDataType bdt = (BaseDataType) method.invoke(proxy);
               counter += bdt.getSize();
            }
            catch (IllegalAccessException | InvocationTargetException e)
            {
               // log it?
            }
         }
         else 
         {
            counter += 4;  // probably one 32bit int = the size of the extent 
            if (ext > 15)
            {
               counter += 3;
            }
            counter += 2 * ((ext + 1) / 16);
            
            for (int k = 0; k < ext; k++) 
            {
               try
               {
                  BaseDataType bdt = (BaseDataType) method.invoke(proxy, k);
                  counter += bdt.getSize();
               }
               catch (IllegalAccessException | InvocationTargetException e)
               {
                  // log it ?
               }
            }
         }
      }
      
      return new integer(counter);
   }
   
   /**
    * Resolve the BUFFER-FIELD with the given converted DMO property name.
    * 
    * @param     propertyName
    *            The property name.
    *            
    * @return    See above.
    */
   public BufferFieldImpl bufferFieldForProperty(String propertyName)
   {
      if (fieldsByName == null)
      {
         fieldsByName = new HashMap<>();
      }
      
      BufferFieldImpl field = fieldsByName.get(propertyName);
      
      if (field == null)
      {
         RecordBuffer recBuf = buffer();
         int fieldId = TableMapper.getLegacyFieldInfo(recBuf, propertyName).getFieldId();

         field = new BufferFieldImpl(_instantiatingProcedure(), (DataModelObject) this, propertyName);
         
         if (fields == null)
         {
            int numFields = _numFields();
            fields = new BufferFieldImpl[numFields + 1];
         }

         fields[fieldId] = field;
         fieldsByName.put(propertyName, field);
      }
      
      return field;
   }
   
   /**
    * Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
    * <p>
    * Gets the handle to a particular buffer of a query or DataSet object.
    *
    * @param   fieldNumber
    *          field number. An INTEGER expression representing the sequence
    *          number of the field in the buffer.
    *
    * @return  handle to a particular field in the buffer.
    */
   @Override
   public handle bufferField(int fieldNumber)
   {
      if (fieldHandles == null)
      {
         fieldHandles = new handle[_numFields() + 1];
      }
      
      if (fieldNumber <= 0 || fieldNumber > _numFields())
      {
         String msg = "BUFFER-FIELD sequence number must be from 1 to " +
            _numFields() + " for buffer " + buffer().getDmoInfo().legacyTable;
         ErrorManager.recordOrShowDatabaseError(7350, msg, false, false);
         
         return new handle();
      }
      
      handle hf = fieldHandles[fieldNumber];
      if (hf == null)
      {
         BufferField field = bufferFieldNative(fieldNumber);
         if (field != null)
         {
            hf = new handle(field);
            fieldHandles[fieldNumber] = hf;
         }
         else
         {
            hf = new handle();
         }
      }
      
      return hf;
   }
   
   /**
    * Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
    * <p>
    * Gets the handle to a particular buffer of a query or DataSet object.
    *
    * @param   fieldNumber
    *          field number. An INTEGER expression representing the sequence
    *          number of the field in the buffer.
    *
    * @return  a particular field in the buffer.
    */
   public BufferFieldImpl bufferFieldNative(int fieldNumber)
   {
      int fieldCount = _numFields();
      RecordBuffer recBuf = buffer();
      String legacyTableName = recBuf.getDmoInfo().legacyTable;
      if (fieldNumber <= 0 || fieldNumber > fieldCount)
      {
         String msg = "BUFFER-FIELD sequence number must be from 1 to " +
                      fieldCount + " for buffer " + legacyTableName;
         ErrorManager.recordOrShowDatabaseError(7350, msg, false, false);
         
         return null;
      }
      
      BufferFieldImpl field = fields == null ? null : fields[fieldNumber];
      if (field == null)
      {
         String hibernateName;
         Property fieldProperty = recBuf.getDmoInfo().getExistingFieldProperty(fieldNumber);
         if (fieldProperty.original == null || fieldProperty.original.isEmpty())
         {
            hibernateName = fieldProperty.name;
         }
         else
         {
            hibernateName = fieldProperty.original;
         }
         
         field = new BufferFieldImpl(_instantiatingProcedure(), (DataModelObject) this, hibernateName);
         
         if (fields == null)
         {
            int numFields = _numFields();
            fields = new BufferFieldImpl[numFields + 1];
         }
         fields[fieldNumber] = field;
         if (fieldsByName == null)
         {
            fieldsByName = new HashMap<>();
         }
         fieldsByName.put(field.getOriginalPropertyName(), field);
      }
      
      return field;
   }
   
   /**
    * Implementation of BUFFER-FIELD() method (KW_BUF_FLD).
    * <p>
    * Gets the handle to a particular field within a buffer.
    *
    * @param   fieldName
    *          The legacy field name. A CHARACTER string expression representing the name
    *          of the field in the buffer.
    *
    * @return  handle to a particular field in the buffer.
    */
   @Override
   public handle bufferField(String fieldName)
   {
      return new handle(bufferFieldNative(fieldName));
   }

   /**
    * Implementation of BUFFER-FIELD() method (KW_BUF_FLD).
    * <p>
    * Gets a particular field within a buffer.
    *
    * @param   fieldName
    *          The legacy field name. A CHARACTER string expression representing the name
    *          of the field in the buffer.
    *
    * @return  a particular field in the buffer.
    */
   public BufferField bufferFieldNative(String fieldName)
   {
      Property property = buffer().getDmoInfo().byLegacyName(fieldName);
      if (property != null)
      {
         BufferFieldImpl field = bufferFieldForProperty(property.name);
         if (field != null)
         {
            return field;
         }
      }

      String msg = "BUFFER-FIELD " + fieldName + " was not found in buffer " + doGetName();
      ErrorManager.recordOrShowDatabaseError(7351, msg, false, false);

      // return unknown handle
      return null;
   }

   /**
    * Conversion of BUFFER-COPY() method (KW_BUF_COPY).
    * <p>
    * This method copies any common fields, determined by name, data type,
    * and extent-matching, from the source buffer to the receiving buffer.
    * If there are fields in one buffer that do not exist in the other, they
    * are ignored. This method is used to accommodate temp-tables of joins.
    * 
    * @param   buffer
    *          The source buffer to copy from.
    *
    * @return  <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
    */
   public logical bufferCopy(Buffer buffer)
   {
      return bufferCopy(buffer, null, null);
   }
   
   /**
    * Conversion of BUFFER-COPY() method (KW_BUF_COPY).
    * <p>
    * This method copies any common fields, determined by name, data type,
    * and extent-matching, from the source buffer to the receiving buffer.
    * If there are fields in one buffer that do not exist in the other, they
    * are ignored. This method is used to accommodate temp-tables of joins.
    *
    * @param   buffer
    *          The source buffer to copy from.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the copy process.
    *
    * @return  <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
    */
   public logical bufferCopy(Buffer buffer, character except)
   {
      return bufferCopy(buffer, except, null);
   }
   
   /**
    * Conversion of BUFFER-COPY() method (KW_BUF_COPY).
    * <p>
    * This method copies any common fields, determined by name, data type,
    * and extent-matching, from the source buffer to the receiving buffer.
    * If there are fields in one buffer that do not exist in the other, they
    * are ignored. This method is used to accommodate temp-tables of joins.
    * 
    * @param   buffer
    *          The source buffer to copy from.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the copy process.
    * @param   pairs
    *          A comma-separated list of field-name pairs to be copied.  Each pair must contain 
    *          one field name from the source and one field name from the target.
    * 
    * @return  <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
    */
   public logical bufferCopy(Buffer buffer, character except, character pairs)
   {
      return bufferCopy(buffer, except, pairs, logical.FALSE);
   }
   
   /**
    * Conversion of BUFFER-COPY() method (KW_BUF_COPY).
    * <p>
    * This method copies any common fields, determined by name, data type,
    * and extent-matching, from the source buffer to the receiving buffer.
    * If there are fields in one buffer that do not exist in the other, they
    * are ignored. This method is used to accommodate temp-tables of joins.
    * 
    * @param   buffer
    *          The source buffer to copy from.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the copy process.
    * @param   pairs
    *          A comma-separated list of field-name pairs to be copied.  Each pair must contain 
    *          one field name from the source and one field name from the target.
    * @param   noLobs
    *          If <code>TRUE</code>, BLOB and CLOB fields are ignored during the copy.
    * 
    * @return  <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
    */
   public logical bufferCopy(Buffer buffer, character except, character pairs, logical noLobs)
   {
      buffer().setBufferCopyDestination(true);
      return RecordBuffer.copy(buffer(), ((BufferImpl) buffer).buffer(), except, pairs, noLobs);
   }
   
   /**
    * Conversion of BUFFER-COPY() method (KW_BUF_COPY).
    * <p>
    * This method copies any common fields, determined by name, data type,
    * and extent-matching, from the source buffer to the receiving buffer.
    * If there are fields in one buffer that do not exist in the other, they
    * are ignored. This method is used to accommodate temp-tables of joins.
    * 
    * @param   bufHandle
    *          The handle of the source buffer to copy from.
    * 
    * @return  <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
    */
   public logical bufferCopy(handle bufHandle)
   {
      return bufferCopy((Buffer) bufHandle.get());
   }
   
   /**
    * Conversion of BUFFER-COPY() method (KW_BUF_COPY).
    * <p>
    * This method copies any common fields, determined by name, data type,
    * and extent-matching, from the source buffer to the receiving buffer.
    * If there are fields in one buffer that do not exist in the other, they
    * are ignored. This method is used to accommodate temp-tables of joins.
    * 
    * @param   bufHandle
    *          The handle of the source buffer to copy from.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the copy process.
    * 
    * @return  <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
    */
   public logical bufferCopy(handle bufHandle, character except)
   {
      return bufferCopy((Buffer) bufHandle.get(), except);
   }
   
   /**
    * Conversion of BUFFER-COPY() method (KW_BUF_COPY).
    * <p>
    * This method copies any common fields, determined by name, data type,
    * and extent-matching, from the source buffer to the receiving buffer.
    * If there are fields in one buffer that do not exist in the other, they
    * are ignored. This method is used to accommodate temp-tables of joins.
    * 
    * @param   bufHandle
    *          The handle of the source buffer to copy from.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the copy process.
    * @param   pairs
    *          A comma-separated list of field-name pairs to be copied.  Each pair must contain 
    *          one field name from the source and one field name from the target.
    * 
    * @return  <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
    */
   public logical bufferCopy(handle bufHandle, character except, character pairs)
   {
      return bufferCopy((Buffer) bufHandle.get(), except, pairs);
   }
   
   /**
    * Conversion of BUFFER-COPY() method (KW_BUF_COPY).
    * <p>
    * This method copies any common fields, determined by name, data type,
    * and extent-matching, from the source buffer to the receiving buffer.
    * If there are fields in one buffer that do not exist in the other, they
    * are ignored. This method is used to accommodate temp-tables of joins.
    * 
    * @param   bufHandle
    *          The handle of the source buffer to copy from.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the copy process.
    * @param   pairs
    *          A comma-separated list of field-name pairs to be copied.  Each pair must contain 
    *          one field name from the source and one field name from the target.
    * @param   noLobs
    *          If <code>TRUE</code>, BLOB and CLOB fields are ignored during the copy.
    * 
    * @return  <code>true</code> if BUFFER-COPY() succeeds, else <code>false</code>.
    */
   public logical bufferCopy(handle bufHandle,
                             character except,
                             character pairs,
                             logical noLobs)
   {
      return bufferCopy((Buffer) bufHandle.get(), except, pairs, noLobs);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   buffer
    *          Buffer with which to compare.
    *
    * @return  <code>true</code> if BUFFER-COMPARE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferCompare(Buffer buffer)
   {
      return bufferCompare(buffer, null, null, null);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   buffer
    *          Buffer with which to compare.
    * @param   mode
    *          The text "binary" or "case-sensitive", to provide that type of comparison.
    *
    * @return  <code>true</code> if BUFFER-COMPARE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferCompare(Buffer buffer, character mode)
   {
      return bufferCompare(buffer, mode, null, null);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   buffer
    *          Buffer with which to compare.
    * @param   mode
    *          The text "binary" or "case-sensitive", to provide that type of comparison.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the compare process.
    *
    * @return  <code>true</code> if BUFFER-COMPARE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferCompare(Buffer buffer, character mode, character except)
   {
      return bufferCompare(buffer, mode, except, null);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   srcBuffer
    *          Buffer with which to compare.
    * @param   mode
    *          The text "binary" or "case-sensitive", to provide that type of comparison.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the compare process.
    * @param   pairs
    *          A comma-separated list of field-name pairs to be compared.  Each pair must
    *          contain one field name from the source and one field name from the target.
    *
    * @return  <code>true</code> if BUFFER-COMPARE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferCompare(Buffer srcBuffer,
                                character mode,
                                character except,
                                character pairs)
   {
      return bufferCompare(srcBuffer, mode, except, pairs, null);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   srcBuffer
    *          Buffer with which to compare.
    * @param   mode
    *          The text "binary" or "case-sensitive", to provide that type of comparison.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the compare process.
    * @param   pairs
    *          A comma-separated list of field-name pairs to be compared.  Each pair must
    *          contain one field name from the source and one field name from the target.
    * @param   noLobs
    *          If {@code true}, BLOB and CLOB fields are ignored during the comparison. May be
    *          {@code false} or {@code null} to include such fields.
    *
    * @return  {@code true} if BUFFER-COMPARE() succeeds, else {@code false}.
    */
   public logical bufferCompare(Buffer    srcBuffer,
                                character mode,
                                character except,
                                character pairs,
                                logical   noLobs)
   {
      return RecordBuffer.compare(((BufferImpl) srcBuffer).buffer(),
                                  buffer(),
                                  BufferCompare.Mode.fromString(mode),
                                  except,
                                  pairs,
                                  noLobs);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   bufHandle
    *          Handle of buffer with which to compare.
    *
    * @return  <code>true</code> if BUFFER-COMPARE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferCompare(handle bufHandle)
   {
      return bufferCompare((Buffer) bufHandle.get());
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   bufHandle
    *          Handle of buffer with which to compare.
    * @param   mode
    *          The text "binary" or "case-sensitive", to provide that type of comparison.
    *
    * @return  <code>true</code> if BUFFER-COMPARE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferCompare(handle bufHandle, character mode)
   {
      return bufferCompare((Buffer) bufHandle.get(), mode);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   bufHandle
    *          Handle of buffer with which to compare.
    * @param   mode
    *          The text "binary" or "case-sensitive", to provide that type of comparison.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the compare process.
    *
    * @return  <code>true</code> if BUFFER-COMPARE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferCompare(handle bufHandle, character mode, character except)
   {
      return bufferCompare((Buffer) bufHandle.get(), mode, except);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   bufHandle
    *          Handle of buffer with which to compare.
    * @param   mode
    *          The text "binary" or "case-sensitive", to provide that type of comparison.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the compare process.
    * @param   pairs
    *          A comma-separated list of field-name pairs to be compared.  Each pair must
    *          contain one field name from the source and one field name from the target.
    *
    * @return  <code>true</code> if BUFFER-COMPARE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferCompare(handle bufHandle, character mode, character except, character pairs)
   {
      return bufferCompare((Buffer) bufHandle.get(), mode, except, pairs);
   }
   
   /**
    * Conversion of BUFFER-COMPARE() method (KW_BUF_COMP).
    * <p>
    * This method does a rough compare of any common fields, determined by
    * name, data type, and extent-matching, between the source buffer and the
    * target buffer. The resulting logical value is either TRUE or FALSE as
    * a whole. A single field that does not compare causes the entire buffer
    * to return FALSE. If there are fields in one buffer that do not exist in
    * the other, they are ignored.
    *
    * @param   bufHandle
    *          Handle of buffer with which to compare.
    * @param   mode
    *          The text "binary" or "case-sensitive", to provide that type of comparison.
    * @param   except
    *          A comma-separated list of fields that will be ignored in the compare process.
    * @param   pairs
    *          A comma-separated list of field-name pairs to be compared.  Each pair must
    *          contain one field name from the source and one field name from the target.
    * @param   noLobs
    *          If <code>TRUE</code>, BLOB and CLOB fields are ignored during the compare.
    *
    * @return  <code>true</code> if BUFFER-COMPARE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferCompare(handle    bufHandle,
                                character mode,
                                character except,
                                character pairs,
                                logical   noLobs)
   {
      return bufferCompare((Buffer) bufHandle.get(), mode, except, pairs, noLobs);
   }
   
   /**
    * Conversion of BUFFER-CREATE() method (KW_BUF_CREA).
    * <p>
    * Creates a record, sets fields to their default values, and moves a copy of the record into the buffer.
    *
    * @return  {@code true} if successful
    */
   public logical bufferCreate()
   {
      if (isBeforeBuffer() && !setUpBeforeBuffer)
      {
         // the BEFORE-BUFFERS can only be queried, not altered in any way (REMOVE/CHANGE)
         ErrorManager.recordOrThrowError(12373, doGetName(), "");
         // You may not delete BEFORE-TABLE <name> record.  Use ACCEPT-ROW-CHANGES instead.
         return logical.FALSE;
      }
      
      RecordBuffer buffer = buffer();
      if (!buffer.isTemporary() && !verifyTransaction(buffer))
      {
         return logical.FALSE;
      }
      
      return logical.of(buffer.create());
   }
   
   /**
    * Conversion of BUFFER-RELEASE() method (KW_BUF_REL).
    * <p>
    * Releases a record from a buffer object. The BUFFER-RELEASE method
    * corresponds to the RELEASE statement.
    *
    * @return  <code>true</code> if BUFFER-RELEASE() succeeds,
    *          else <code>false</code>.
    */
   public logical bufferRelease()
   {
      boolean success = true;
      try
      {
         release();
      }
      catch (ErrorConditionException exc)
      {
         success = false;
      }
      
      return logical.of(success);
   }
   
   /**
    * Emitted for <code>::</code> operator used as r-value in a dynamic extent assignment, as the conversion
    * rules doesn't know if the target field is extent or scalar.
    * 
    * @param    memberName
    *           The name of the member to be extracted.
    *
    * @return   The value of the member (scalar or extent) or unknown (?) if there is no such member.
    */
   @Override
   public Object derefPoly(String memberName)
   {
      Property property = buffer().getDmoInfo().byLegacyName(memberName);
      
      if (property == null)
      {
         ErrorManager.recordOrShowError(7351, memberName, doGetName());
         // BUFFER-FIELD <field-name> was not found in buffer <buffer-name>.
         return new unknown();
      }
      
      if (property.extent == 0)
      {
         return dereference(memberName);
      }
      
      if (!buffer().isAvailable() && !buffer().isUnknownMode())
      {
         String msg1 = "No " + doGetName() + " record is available";
         ErrorManager.displayError(91, msg1, true);
         
         String msg2 = "Unable to extract BUFFER-VALUE for field " + memberName;
         ErrorManager.displayError(7366, msg2, false);
         
         return new unknown();
      }
      
      BaseDataType[] ret = new BaseDataType[property.extent];
      for (int idx = 0; idx < ret.length; idx++)
      {
         ret[idx] = bufferFieldForProperty(property.name).value(idx + 1);
      }
      return ret;
   }
   
   /**
    * Dereference a named member from this collection.
    *
    * @param   memberName
    *          The legacy name of the member to be extracted.
    *
    * @return  The value of the member or unknown (?) if there is no such member.
    */
   @Override
   public BaseDataType dereference(String memberName)
   {
      Property property = buffer().getDmoInfo().byLegacyName(memberName);
      
      if (property == null)
      {
         ErrorManager.recordOrShowError(7351, memberName, doGetName());
         // BUFFER-FIELD <field-name> was not found in buffer <buffer-name>.
         return new unknown();
      }
      
      if (property.extent != 0)
      {
         ErrorManager.recordOrThrowError(14905);
         // Whole-array assignment target and source must have the same extent unless the target is indeterminate. (14905)
         return new unknown();
      }
      
      if (!buffer().isAvailable() && !buffer().isUnknownMode())
      {
         String msg1 = "No " + doGetName() + " record is available";
         ErrorManager.recordOrShowDatabaseError(91, msg1, false, true);
         
         String msg2 = "Unable to extract BUFFER-VALUE for field " + memberName;
         ErrorManager.recordOrShowDatabaseError(7366, msg2, false, false);
         
         return new unknown();
      }
      
      BaseDataType dbt = bufferFieldForProperty(property.name).value();
      if (dbt instanceof character)
      {
         // quirk in 4GL: the character fields retrieved using dereference operator are always
         // case-insensitive (no matter the initial field property case sensitivity)
         ((character) dbt).setCaseSensitive(false);
      }
      return dbt;
   }
   
   /**
    * Dereference a named member from this collection. If this collection is a ProDataSet then 
    * the returned value is a {@link handle} to a named buffer, if the collection is a buffer
    * the returned value is the field value with the respective name. In the case that this 
    * collection does not have a named member with the name specified <code>memberName</code>
    * the unknown value is returned.
    *  
    * @param   index
    *          The subscript to be applied to an extent field.
    * @param   memberName
    *          The name of the member to be extracted.
    *
    * @return  The value of the member or unknown (?) if there is no such member.
    */
   @Override
   public BaseDataType dereference(NumberType index, String memberName)
   {
      Property property = buffer().getDmoInfo().byLegacyName(memberName);
      
      if (property == null)
      {
         ErrorManager.recordOrShowError(7351, memberName, doGetName());
         // BUFFER-FIELD <field-name> was not found in buffer <buffer-name>.
         return new unknown();
      }
      
      if (!buffer().isAvailable())
      {
         String msg1 = "No " + doGetName() + " record is available";
         ErrorManager.recordOrShowDatabaseError(91, msg1, false, true);
         
         String msg2 = "Unable to extract BUFFER-VALUE for field " + memberName;
         ErrorManager.recordOrShowDatabaseError(7366, msg2, false, false);
         
         return new unknown();
      }
      
      if (property.extent == 0)
      {
         return dereference(memberName);
      }

      if (index.isUnknown())
      {
         return new unknown();
      }

      long idx = index.longValue();
      if (idx == 0)
      {
         // no error is shown/raised
         return new unknown();
      }
      
      if (idx < 1 || idx > property.extent)
      {
         String msg1 = "Array subscript " + index + " is out of range";
         ErrorManager.displayError(26, msg1, true);
         
         String msg2 = "Unable to extract BUFFER-VALUE for field " + memberName;
         ErrorManager.displayError(7366, msg2, false);
         
         return new unknown();
      }
      
      return bufferFieldForProperty(property.name).value((int) idx);
   }
   
   /**
    * Dereference a named member from this collection. If this collection is a ProDataSet then 
    * the returned value is a {@link handle} to a named buffer, if the collection is a buffer
    * the returned value is the field value with the respective name. In the case that this 
    * collection does not have a named member with the name specified <code>memberName</code>
    * the unknown value is returned.
    *  
    * @param   index
    *          The subscript to be applied to an extent field.
    * @param   memberName
    *          The name of the member to be extracted.
    *
    * @return  The value of the member or unknown (?) if there is no such member.
    */
   @Override
   public BaseDataType dereference(long index, String memberName)
   {
      return dereference(new int64(index), memberName);
   }
   
   /**
    * Dereference a named member from this collection. If this collection is a DataSet then
    * the returned value is a {@link handle} to a named buffer, if the collection is a buffer
    * the returned value is the field value with the respective name. In the case that this
    * collection does not have a named member with the name specified {@code memberName}
    * the unknown value is returned.
    * 
    * @param   type
    *          The expected type of the returned value.
    * @param   memberName
    *          The name of the member to be extracted.
    *
    * @return  The value of the member or unknown (?) if there is no such member.
    */
   @Override
   public <T extends BaseDataType> T dereference(Class<T> type, String memberName)
   {
      BaseDataType val = dereference(memberName);
      if (type.isAssignableFrom(val.getClass()))
      {
         return (T) val;
      }
      
      String errMsg2 =
          "Lead attributes in a chained-attribute expression (a:b:c) must be type " +
          "HANDLE or a user-defined type and valid (not UNKNOWN)";
      ErrorManager.recordOrThrowError(10068, errMsg2, false, false);
      
      return (T) BaseDataType.generateUnknown(type);
   }
   
   /**
    * This overloaded method is used when the dereference is the left-side of an assignment.
    * The implementation will extract the member from the collection and assign it the new value.
    *
    * @param   memberName
    *          The name of the member to be extracted.
    * @param   value
    *          The new value that will be assigned to the extracted field.
    */
   @Override
   public void dereference(String memberName, Object value)
   {
      if (isBeforeBuffer() && !setUpBeforeBuffer)
      {
         ErrorManager.recordOrShowError(12375, doGetName());
         // You may not update BEFORE-TABLE <name> record.
         ErrorManager.recordOrShowError(142, doGetName());
         // ** Unable to update <filename> Field.
         ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
         ErrorManager.recordOrThrowError(142, doGetName(), "");
         // ** Unable to update <filename> Field.
         return;
      }
      
      Property property = buffer().getDmoInfo().byLegacyName(memberName);
      if (property == null)
      {
         String errMsg  = "BUFFER-FIELD " + memberName + " was not found in buffer " + doGetName();
         ErrorManager.recordOrThrowError(7351, errMsg);
         
         return;
      }
      
      boolean bulk = value.getClass().isArray();
      if (bulk)
      {
         BufferFieldImpl field = new BufferFieldImpl(_instantiatingProcedure(), 
                                                     (DataModelObject) this, 
                                                     property.name, 
                                                     bulk);
         try
         {
            field.changeValue(value);
         }
         finally
         {
            // force the delete, this instance is required for bulk assign of an extent field, and is not a 
            // real BUFFER-FIELD resource
            field.forceDelete();
         }
      }
      else
      {
         BufferFieldImpl field = bufferFieldForProperty(property.name);
         field.changeValue(value);
      }
   }
   
   /**
    * This overloaded method is used when the dereference is the left-side of an assignment.
    * The implementation will extract the member from the collection and assign it the new value.
    *
    * @param   index
    *          The subscript to be applied to an extent field.
    * @param   memberName
    *          The name of the member to be extracted.
    * @param   value
    *          The new value that will be assigned to the extracted field.
    */
   @Override
   public void dereference(NumberType index, String memberName, Object value)
   {
      if (isBeforeBuffer() && !setUpBeforeBuffer)
      {
         ErrorManager.recordOrShowError(12375, doGetName());
         // You may not update BEFORE-TABLE <name> record.
         ErrorManager.recordOrShowError(142, doGetName());
         // ** Unable to update <filename> Field.
         ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
         ErrorManager.recordOrThrowError(142, doGetName(), "");
         // ** Unable to update <filename> Field.
         return;
      }
      
      Property property = buffer().getDmoInfo().byLegacyName(memberName);
      if (property == null)
      {
         String errMsg  = "BUFFER-FIELD " + memberName + " was not found in buffer " + doGetName();
         ErrorManager.recordOrThrowError(7351, errMsg);
         
         return;
      }
      
      if (index.isUnknown())
      {
         ErrorManager.recordOrShowError(7365, memberName, String.valueOf(property.extent));
         // <field> BUFFER-VALUE argument must be valid array index, less than or equal to field extent <extent>
         ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
         ErrorManager.recordOrThrowError(142, doGetName(), "");
         // ** Unable to update <filename> Field.
         return;
      }
      
      long idx = index.longValue();
      if (property.extent == 0 && idx == 0)
      {
         dereference(memberName, value);
         return;
      }
      
      if (idx == 0)
      {
         ErrorManager.recordOrShowError(26, String.valueOf(idx));
         // ** Array subscript <index> is out of range. (26)
         ErrorManager.recordOrShowError(142, doGetName(), "");
         // ** Unable to update <filename> Field.
         ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
         ErrorManager.recordOrThrowError(142, doGetName(), "");
         // ** Unable to update <filename> Field.
         return;
      }
      
      if (idx < 1 || idx > property.extent)
      {
         ErrorManager.recordOrShowError(7365,
                                        memberName,
                                        String.valueOf(property.extent),
                                        LegacyResource.BUFFER_FIELD);
         // <field> BUFFER-VALUE argument must be valid array index, less than or equal to field extent <extent>
         ErrorManager.recordOrShowError(3131, "?", "", LegacyResource.BUFFER_FIELD);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>
         ErrorManager.recordOrThrowError(142, doGetName(), "");
         // ** Unable to update <filename> Field.
         return;
      }
      
      bufferFieldForProperty(property.name).changeValue((int) idx, value);
   }
   
   /**
    * This overloaded method is used when the dereference is the left-side of an assignment.
    * The implementation will extract the member from the collection and assign it the new value.
    *
    * @param   index
    *          The subscript to be applied to an extent field.
    * @param   memberName
    *          The name of the member to be extracted.
    * @param   value
    *          The new value that will be assigned to the extracted field.
    */
   @Override
   public void dereference(long index, String memberName, Object value)
   {
      dereference(new int64(index), memberName, value);
   }
   
   /**
    * Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
    * <p>
    * Locates the record with the rowid you specify, then moves the record
    * into the buffer.
    *
    * @param   id
    *          An expression of type ROWID that represents the rowid of the
    *          desired record.
    *
    * @return  <code>true</code> if it finds the record, else
    *          <code>false</code>.
    */
   public logical findByRowID(rowid id)
   {
      return findByRowID(id, LockType.LT_SHARE);
   }
   
   /**
    * Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
    * <p>
    * Locates the record with the rowid you specify, then moves the record into the buffer.
    *
    * @param   id
    *          An expression of type ROWID that represents the rowid of the desired record.
    * @param   lockType
    *          Lock type
    *
    * @return  {@code true} if it finds the record, else {@code false}.
    */
   public logical findByRowID(rowid id, LockType lockType)
   {
      final RecordBuffer buffer = buffer();
      final String dmoAlias = buffer.getDMOAlias();
      
      if (id.isUnknown())
      {
         String msg = "Invalid rowid for FIND-BY-ROWID method of BUFFER object " + buffer.getLegacyName();
         ErrorManager.recordOrShowError(7341, msg, false);
         
         return logical.FALSE;
      }
      
      try
      {
         boolean err = ErrorManager.silent(() ->
         {
            buffer.initialize();
            
            // manufacture a primary key sort clause; this will be discarded from the final FQL, but it is
            // needed by RAQ.initialize
            String sort = dmoAlias + "." + Session.PK + " asc";
            
            FindQuery findQuery = new FindQuery(buffer,
                                                dmoAlias + "." + Session.PK + " = ?",
                                                null,
                                                sort,
                                                new Object[] { id },
                                                lockType);
            findQuery.unique();
         });
         
         return logical.of(!err);
      }
      catch (IllegalArgumentException | ErrorConditionException | QueryOffEndException exc)
      {
         // all ignored
      }
      
      return logical.FALSE;
   }
   
   /**
    * Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
    * <p>
    * Locates the record with the rowid you specify, then moves the record into the buffer.
    *
    * @param   id
    *          An expression of type ROWID that represents the rowid of the desired record.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    *
    * @return  {@code true} if it finds the record, else {@code false}.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findByRowID(rowid id, long lock)
   {
      LockType lockType = fromLockNowait(lock, 0);
      
      return lockType == null ? logical.FALSE : findByRowID(id, lockType);
   }
   
   /**
    * Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
    * <p>
    * Locates the record with the rowid you specify, then moves the record into the buffer.
    *
    * @param   id
    *          An expression of type ROWID that represents the rowid of the desired record.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    *
    * @return  {@code true} if it finds the record, else {@code false}.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findByRowID(rowid id, NumberType lock)
   {
      LockType lockType = fromLockNowait(lock.longValue(), 0);
      
      return lockType == null ? logical.FALSE : findByRowID(id, lockType);
   }
   
   /**
    * Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
    * <p>
    * Locates the record with the rowid you specify, then moves the record into the buffer.
    *
    * @param   id
    *          An expression of type ROWID that represents the rowid of the desired record.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          NO-WAIT
    *
    * @return  {@code true} if it finds the record, else {@code false}.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findByRowID(rowid id, long lock, long nowait)
   {
      LockType lockType = fromLockNowait(lock, nowait);
      
      return lockType == null ? logical.FALSE : findByRowID(id, lockType);
   }
   
   /**
    * Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
    * <p>
    * Locates the record with the rowid you specify, then moves the record into the buffer.
    *
    * @param   id
    *          An expression of type ROWID that represents the rowid of the desired record.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          NO-WAIT
    *
    * @return  {@code true} if it finds the record, else {@code false}.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findByRowID(rowid id, NumberType lock, long nowait)
   {
      LockType lockType = fromLockNowait(lock.longValue(), nowait);
      
      return lockType == null ? logical.FALSE : findByRowID(id, lockType);
   }
   
   /**
    * Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
    * <p>
    * Locates the record with the rowid you specify, then moves the record into the buffer.
    *
    * @param   id
    *          An expression of type ROWID that represents the rowid of the desired record.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          NO-WAIT
    *
    * @return  {@code true} if it finds the record, else {@code false}.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findByRowID(rowid id, long lock, NumberType nowait)
   {
      LockType lockType = fromLockNowait(lock, nowait.longValue());
      
      return lockType == null ? logical.FALSE : findByRowID(id, lockType);
   }
   
   /**
    * Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
    * <p>
    * Locates the record with the rowid you specify, then moves the record into the buffer.
    *
    * @param   id
    *          An expression of type ROWID that represents the rowid of the desired record.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          NO-WAIT
    *
    * @return  {@code true} if it finds the record, else {@code false}.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findByRowID(rowid id, NumberType lock, NumberType nowait)
   {
      LockType lockType = fromLockNowait(lock.longValue(), nowait.longValue());
      
      return lockType == null ? logical.FALSE : findByRowID(id, lockType);
   }
   
   /**
    * Conversion of FIND-BY-ROWID() method (KW_FIND_BR).
    * <p>
    * Locates the record with the rowid you specify, then moves the record into the buffer.
    *
    * @param   id
    *          An expression of type ROWID that represents the rowid of the desired record.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          NO-WAIT
    *
    * @return  {@code true} if it finds the record, else {@code false}.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findByRowID(rowid id, integer lock, int nowait)
   {
      LockType lockType = fromLockNowait(lock.longValue(), nowait);
      
      return lockType == null ? logical.FALSE : findByRowID(id, lockType);
   }
   
   /**
    * Conversion of FIND-FIRST() method (KW_FIND_1ST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds,
    *          else <code>false</code>.
    */
   public logical findFirst()
   {
      return _find(null, FindMode.FIRST, LockType.SHARE);
   }
   
   /**
    * Conversion of FIND-FIRST() method (KW_FIND_1ST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
    */
   public logical findFirst(character predicate)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.FIRST, LockType.SHARE);
   }
   
   /**
    * Conversion of FIND-FIRST() method (KW_FIND_1ST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lockType
    *          Lock type
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds,
    *          else <code>false</code>.
    */
   public logical findFirst(character predicate, LockType lockType)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.FIRST, lockType);
   }
   
   /**
    * Conversion of FIND-FIRST() method (KW_FIND_1ST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
    */
   public logical findFirst(character predicate, long lock)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.FIRST, LockType.fromInt((int) lock));
   }
   
   /**
    * Conversion of FIND-FIRST() method (KW_FIND_1ST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
    */
   public logical findFirst(character predicate, NumberType lock)
   {
      LockType lockType = lock.isUnknown() ? LockType.SHARE : LockType.fromInt(lock.intValue());
      
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.FIRST, lockType);
   }
   
   /**
    * Conversion of FIND-FIRST() method (KW_FIND_1ST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findFirst(character predicate, long lock, long nowait)
   {
      LockType lockType = fromLockNowait(lock, nowait);
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
         ? logical.FALSE
         : _find(predicate.toJavaType(), FindMode.FIRST, lockType);
   }
   
   /**
    * Conversion of FIND-FIRST() method (KW_FIND_1ST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findFirst(character predicate, NumberType lock, long nowait)
   {
      long javaLock = lock.isUnknown() ? LockType.LT_SHARE : lock.longValue();
      
      LockType lockType = fromLockNowait(javaLock, nowait);
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
         ? logical.FALSE
         : _find(predicate.toJavaType(), FindMode.FIRST, lockType);
   }
   
   /**
    * Conversion of FIND-FIRST() method (KW_FIND_1ST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findFirst(character predicate, long lock, NumberType nowait)
   {
      LockType lockType = fromLockNowait(lock, nowait.longValue());
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
        ? logical.FALSE
        : _find(predicate.toJavaType(), FindMode.FIRST, lockType);
   }
   
   /**
    * Conversion of FIND-FIRST() method (KW_FIND_1ST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-FIRST() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findFirst(character predicate, NumberType lock, NumberType nowait)
   {
      long javaLock = lock.isUnknown() ? LockType.LT_SHARE : lock.longValue();
      
      LockType lockType = fromLockNowait(javaLock, nowait.longValue());
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
         ? logical.FALSE
         : _find(predicate.toJavaType(), FindMode.FIRST, lockType);
   }
   
   /**
    * Conversion of FIND-LAST() method (KW_FIND_LST).
    * <p>
    * Gets the last record.
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    */
   public logical findLast()
   {
      return _find(null, FindMode.LAST, LockType.SHARE);
   }
   
   /**
    * Conversion of FIND-LAST() method (KW_FIND_LST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    */
   public logical findLast(character predicate)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.LAST, LockType.SHARE);
   }
   
   /**
    * Conversion of FIND-LAST() method (KW_FIND_LST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lockType
    *          Lock type
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    */
   public logical findLast(character predicate, LockType lockType)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.LAST, lockType);
   }
   
   /**
    * Conversion of FIND-LAST() method (KW_FIND_LST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    */
   public logical findLast(character predicate, long lock)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.LAST, LockType.fromInt((int) lock));
   }
   
   /**
    * Conversion of FIND-LAST() method (KW_FIND_LST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    */
   public logical findLast(character predicate, NumberType lock)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.LAST, LockType.fromInt(lock.intValue()));
   }
   
   /**
    * Conversion of FIND-LAST() method (KW_FIND_LST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findLast(character predicate, long lock, long nowait)
   {
      LockType lockType = fromLockNowait(lock, nowait);
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
         ? logical.FALSE
         : _find(predicate.toJavaType(), FindMode.LAST, lockType);
   }
   
   /**
    * Conversion of FIND-LAST() method (KW_FIND_LST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findLast(character predicate, NumberType lock, long nowait)
   {
      LockType lockType = fromLockNowait(lock.longValue(), nowait);
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
         ? logical.FALSE
         : _find(predicate.toJavaType(), FindMode.LAST, lockType);
   }
   
   /**
    * Conversion of FIND-LAST() method (KW_FIND_LST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findLast(character predicate, long lock, NumberType nowait)
   {
      // TODO: add support for ? nowait mode
      LockType lockType = fromLockNowait(lock, nowait.longValue());
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
         ? logical.FALSE
         : _find(predicate.toJavaType(), FindMode.LAST, lockType);
   }
   
   /**
    * Conversion of FIND-LAST() method (KW_FIND_LST).
    * <p>
    * Gets a single record. This method lets a user get the last record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-LAST() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findLast(character predicate, NumberType lock, NumberType nowait)
   {
      LockType lockType = fromLockNowait(lock.longValue(), nowait.longValue());
      // toJavaType() returns null if predicate is unknown
      return lockType == null ?
            logical.FALSE :
            _find(predicate.toJavaType(), FindMode.LAST, lockType);
   }
   
   /**
    * Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
    * <p>
    * Gets a single record. This make sense only when there is only one record in the table.
    * Otherwise, the error 3166 "More than one &lt;tablename&gt; records found by a unique FIND."
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
    */
   public logical findUnique()
   {
      return _find(null, FindMode.UNIQUE, LockType.SHARE);
   }
   
   /**
    * Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
    * <p>
    * Gets a single record. This method lets a user get a unique record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
    */
   public logical findUnique(character predicate)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.UNIQUE, LockType.SHARE);
   }
   
   /**
    * Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
    * <p>
    * Gets a single record. This method lets a user get a unique record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lockType
    *          Lock type
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
    */
   public logical findUnique(character predicate, LockType lockType)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
   }
   
   /**
    * Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
    * <p>
    * Gets a single record. This method lets a user get a unique record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
    */
   public logical findUnique(character predicate, long lock)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.UNIQUE, LockType.fromInt((int) lock));
   }
   
   /**
    * Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
    * <p>
    * Gets a single record. This method lets a user get a unique record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
    */
   public logical findUnique(character predicate, NumberType lock)
   {
      // toJavaType() returns null if predicate is unknown
      return _find(predicate.toJavaType(), FindMode.UNIQUE, LockType.fromInt(lock.intValue()));
   }
   
   /**
    * Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
    * <p>
    * Gets a single record. This method lets a user get a unique record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findUnique(character predicate, long lock, long nowait)
   {
      LockType lockType = fromLockNowait(lock, nowait);
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
         ? logical.FALSE
         : _find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
   }
   
   /**
    * Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
    * <p>
    * Gets a single record. This method lets a user get a unique record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds, else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findUnique(character predicate, NumberType lock, long nowait)
   {
      LockType lockType = fromLockNowait(lock.longValue(), nowait);
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
         ? logical.FALSE
         : _find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
   }
   
   /**
    * Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
    * <p>
    * Gets a single record. This method lets a user get a unique record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds,
    *          else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findUnique(character predicate, long lock, NumberType nowait)
   {
      LockType lockType = fromLockNowait(lock, nowait.longValue());
      
      // toJavaType() returns null if predicate is unknown
      return lockType == null
         ? logical.FALSE
         : _find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
   }
   
   /**
    * Conversion of FIND-UNIQUE() method (KW_FIND_UNI).
    * <p>
    * Gets a single record. This method lets a user get a unique record that
    * satisfies the predicate expression.
    *
    * @param   predicate
    *          Predicate expression for find.
    * @param   lock
    *          SHARE-LOCK|EXCLUSIVE-LOCK|NO-LOCK
    * @param   nowait
    *          Integer value of one of the following: NO-WAIT, 0, or the Unknown value (?).
    *
    * @return  <code>true</code> if FIND-UNIQUE() succeeds,
    *          else <code>false</code>.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   public logical findUnique(character predicate, NumberType lock, NumberType nowait)
   {
      LockType lockType = fromLockNowait(lock.longValue(), nowait.longValue());
      // toJavaType() returns null if predicate is unknown
      return lockType == null ?
            logical.FALSE :
            _find(predicate.toJavaType(), FindMode.UNIQUE, lockType);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED attribute.
    * 
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent()
   {
      return _current(LockType.SHARE);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
    * attribute.
    * 
    * @param   lockType
    *          Lock type.
    * 
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
    *          false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent(LockType lockType)
   {
      if (lockType == null)
      {
         return logical.FALSE;
      }
      
      return _current(lockType);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
    * attribute.
    * 
    * @param   lock
    *          Lock type.
    *          
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
    *          false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent(NumberType lock)
   {
      LockType lockType = fromLockNowait(lock.longValue(), 0);
      
      return (lockType == null) ? logical.FALSE : _current(lockType);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
    * attribute.
    * 
    * @param   lock
    *          Lock type.
    *          
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
    *          false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent(long lock)
   {
      LockType lockType = fromLockNowait(lock, 0);
      
      return (lockType == null) ? logical.FALSE : _current(lockType);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
    * attribute.
    * 
    * @param   lockType
    *          Lock type.
    * @param   noWait
    *          Zero to lock without waiting; non-zero to wait to acquire the lock.
    * 
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
    *          false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent(LockType lockType, long noWait)
   {
      if (lockType == null)
      {
         return logical.FALSE;
      }
      lockType = fromLockNowait(LockType.toInt(lockType), noWait);
      
      return (lockType == null) ? logical.FALSE : _current(lockType);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
    * attribute.
    * 
    * @param   lock
    *          Lock type.
    * @param   noWait
    *          Zero to lock without waiting; non-zero to wait to acquire the lock.
    *          
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
    *          false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent(NumberType lock, long noWait)
   {
      if (lock == null)
      {
         return logical.FALSE;
      }
      LockType lockType = fromLockNowait(lock.longValue(), noWait);
      
      return (lockType == null) ? logical.FALSE : _current(lockType);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
    * attribute.
    * 
    * @param   lock
    *          Lock type.
    * @param   noWait
    *          Zero to lock without waiting; non-zero to wait to acquire the lock.
    *          
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
    *          false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent(long lock, long noWait)
   {
      LockType lockType = fromLockNowait(lock, noWait);
      
      return (lockType == null) ? logical.FALSE : _current(lockType);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
    * attribute.
    * 
    * @param   lockType
    *          Lock type.
    * @param   noWait
    *          Zero or unknown value to lock without waiting; non-zero to wait to acquire the
    *          lock.
    * 
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
    *          false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent(LockType lockType, NumberType noWait)
   {
      if (lockType == null || noWait == null)
      {
         return logical.FALSE;
      }
      lockType = fromLockNowait(LockType.toInt(lockType), noWait.longValue());
      
      return (lockType == null) ? logical.FALSE : _current(lockType);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
    * attribute.
    * 
    * @param   lock
    *          Lock type.
    * @param   noWait
    *          Zero or unknown value to lock without waiting; non-zero to wait to acquire the
    *          lock.
    *          
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
    *          false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent(NumberType lock, NumberType noWait)
   {
      if (lock == null || noWait == null)
      {
         return logical.FALSE;
      }
      LockType lockType = fromLockNowait(lock.longValue(), noWait.longValue());
      
      return (lockType == null) ? logical.FALSE : _current(lockType);
   }
   
   /**
    * Conversion of the FIND-CURRENT() method.
    * <p> 
    * Changes the lock type for the record currently in the buffer. Resets the CURRENT-CHANGED
    * attribute.
    * 
    * @param   lock
    *          Lock type.
    * @param   noWait
    *          Zero or unknown value to lock without waiting; non-zero to wait to acquire the
    *          lock.
    *          
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code
    *          false}.
    */
   @LegacyMethod(name = "FIND-CURRENT")
   public logical findCurrent(long lock, NumberType noWait)
   {
      if (noWait == null)
      {
         return logical.FALSE;
      }
      LockType lockType = fromLockNowait(lock, noWait.longValue());
      
      return (lockType == null) ? logical.FALSE : _current(lockType);
   }
   
   /**
    * Conversion of NUM-FIELDS attribute (KW_NUM_FLD).
    * <p>
    * Getter of NUM-FIELDS attribute.
    *
    * @return  current value of NUM-FIELDS attribute.
    */
   public integer numFields()
   {
      return integer.of(_numFields());
   }
   
   /**
    * Disable the triggers associated with the FIND event for the buffer's table.
    */
   public void disableDumpTriggers()
   {
      final Class<?> dmoBuf = buffer().getDMOBufInterface();
      if (Buffer.class.isAssignableFrom(dmoBuf))
      {
         DatabaseTriggerManager.disableDumpTriggers((Class<? extends Buffer>) dmoBuf);
      }
   }
   
   /**
    * Disable the triggers associated with the CREATE, WRITE, DELETE and ASSIGN events for the
    * buffer's table.
    *
    * @param   allowReplication
    *          If <code>true</code> then only CREATE, DELETE, ASSIGN and WRITE triggers are
    *          disabled, and not REPLICATION-CREATE, REPLICATION-DELETE and REPLICATION-WRITE
    *          triggers.
    */
   public void disableLoadTriggers(boolean allowReplication)
   {
      final Class<?> dmoBuf = buffer().getDMOBufInterface();
      if (Buffer.class.isAssignableFrom(dmoBuf))
      {
         DatabaseTriggerManager.disableLoadTriggers((Class<? extends Buffer>) dmoBuf, allowReplication);
      }
   }
   
   /**
    * Conversion of CURRENT-ITERATION attribute (KW_CUR_ITER).
    * <p>
    * Getter of CURRENT-ITERATION attribute.
    *
    * @return  Current value of CURRENT-ITERATION attribute.
    */
   public handle currentIteration()
   {
      // TODO: implement, returning unknown values for the moment
      return new handle();
      
      // TODO: this method should return an integer instead 
      // if (dataSet == null)
      // {
      //    return new integer();
      // }
      // else compute iteration
   }

   /**
    * Implements the GET-ITERATION() widget or buffer method.
    *
    * @param    level
    *           The level of the iteration to get.
    *
    * @return   The handle of the iteration, field-group or buffer.
    */
   @Override
   public handle getIteration(NumberType level)
   {
      return level == null || level.isUnknown() ? new handle() : getIteration(level.intValue());
   }
   
   /**
    * Implements the GET-ITERATION() buffer method.
    *
    * @param    level
    *           The level of the iteration to get.  Record number.
    *
    * @return   The handle of the iteration, field-group or buffer.
    */
   @Override
   public handle getIteration(int level)
   {
      // TODO: Implement me
      return new handle();
   }
   
   /**
    * Sets the ERROR-STRING attribute of 4GL temp-table.
    *
    * @param errStr The new value for the ERROR-STRING attribute.
    */
   @Override
   public void changeErrorString(character errStr)
   {
      RecordBuffer buf = buffer();
      if (!buf.isTemporary())
      {
         // permanent table case
         ErrorManager.recordOrShowError(11917, doGetName());
         // Cannot set ERROR if there is no BEFORE-TABLE for <name>.
         ErrorManager.recordOrShowError(3131, "ERROR-STRING", "", LegacyResource.BUFFER);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
         return;
      }
      
      if (!_available())
      {
         // temp-tables case
         ErrorManager.recordOrShowError(11897, doGetName());
         // <name> record is not available to set ERROR in.
         ErrorManager.recordOrShowError(3131, "ERROR-STRING", "", LegacyResource.BUFFER);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
         return;
      }
      
      if (!isBeforeBuffer())
      {
         if (!isAfterBuffer() || // SIMPLE BUFFER
             (buf.peerRowid() == null || buf.peerRowid().isUnknown())) // no AFTER-IMAGE
         {
            ErrorManager.recordOrShowError(11918, doGetName(), "");
            // Cannot set ERROR if there is no BEFORE-TABLE row for <name>.
            return;
         }
      }
      
      buffer().errorString(errStr);
   }
   
   /**
    * Obtain the current ERROR-STRING attribute of 4GL temp-table.
    *
    * @return the current ERROR-STRING of the temp-table
    */
   @Override
   public character errorString()
   {
      RecordBuffer recBuf = buffer();
      if (!recBuf.isAvailable() || !recBuf.isTemporary())
      {
         return new character();
      }
      
      DataSet ds = _dataSet();
      if (ds == null)
      {
         return new character();
      }
      
      return recBuf.errorString();
   }
   
   /**
    * Returns the state of the ERROR attribute
    *
    * @return  Current state of the ERROR attribute.
    */
   @Override
   public logical error()
   {
      RecordBuffer recBuf = buffer();
      if (!recBuf.isAvailable() || !recBuf.isTemporary())
      {
         return logical.FALSE;
      }
      
      DataSet ds = _dataSet();
      if (ds == null)
      {
         return logical.FALSE;
      }
      
      // NOTE: internally, the P4GL database the __error-flag__ is an integer storing multiple flags.
      //       We need to extract the right bit (ERROR_ERROR) and convert it to a logical value.
      Integer flags = recBuf.errorFlags();
      return logical.of(flags != null && (flags & Buffer.ERROR_ERROR) != 0);
   }
   
   /**
    * Setter for {@code ERROR} attribute.<br>
    * The {@code ERROR} attribute is writable for a {@code Buffer} object. The attribute can be set
    * programmatically in order to manage the application response to errors.<br>
    * If set to {@code FALSE}, the application will ignore all errors that might be returned for a
    * statement using the {@code NO-ERROR} option.<br>
    * However, manually setting this attribute won't raise the {@code ERROR} condition.
    *
    * @param    value
    *           The new value of {@code ERROR} attribute.
    */
   @Override
   public void error(boolean value)
   {
      RecordBuffer recBuf = buffer();
      if (!_available())
      {
         // temp-tables case
         ErrorManager.recordOrShowError(11916, doGetName());
         // <name> record is not available to set ERROR in.
         ErrorManager.recordOrShowError(3131, "ERROR", "", LegacyResource.BUFFER);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
         return;
      }
      
      if (!recBuf.isTemporary())
      {
         // permanent table case
         ErrorManager.recordOrShowError(11917, doGetName());
         // Cannot set ERROR if there is no BEFORE-TABLE for <name>.
         ErrorManager.recordOrShowError(3131, "ERROR", "", LegacyResource.BUFFER);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
         return;
      }
      
      if (!isBeforeBuffer())
      {
         if (!isAfterBuffer() || // SIMPLE BUFFER
             (recBuf.peerRowid() == null || recBuf.peerRowid().isUnknown())) // no AFTER-IMAGE
         {
            ErrorManager.recordOrShowError(11918, doGetName(), "");
            // Cannot set ERROR if there is no BEFORE-TABLE row for <name>.
            return;
         }
      }
      
      // NOTE: internally, the P4GL database the __error-flag__ is an integer storing multiple flags.
      //       We need to update the right bit (ERROR_ERROR) to new state (value).
      recBuf.updateErrorFlags(Buffer.ERROR_ERROR, value);
   }
   
   /**
    * Setter for ERROR attribute.
    *
    * @param   value
    *          The new value of ERROR attribute.
    */
   @Override
   public void error(logical value)
   {
      error(value.booleanValue());
   }
   
   /**
    * Returns the state of the DBNAME attribute
    *
    * @return  Current value of the DBNAME attribute.
    */
   public character getDbName()
   {
      final RecordBuffer buf = buffer();
      if (buf.getParentTable() != null)
      {
         // in the case of temp-tables (defined statically or dynamically)
         return new character("PROGRESST");
      }
      
      // permanent tables, it works as expected (the logical name of the database of the buffer)
      return new character(buf.getLogicalDatabase());
   }
   
   /**
    * Returns the state of the TABLE attribute
    *
    * @return    Current value of the TABLE attribute.
    */
   @Override
   public character getTable()
   {
      if (buffer().isTemporary())
      {
         AbstractTempTable table = (AbstractTempTable) tableHandleNative();
         return table.name();
      }
      else
      {
         return new character(buffer().getDmoInfo().legacyTable);
      }
   }
   
   /**
    * Conversion of TABLE-HANDLE attribute (KW_TAB_HAND).
    * <p>
    * Gets the handle of a temp-table object, if any, associated with the buffer object.
    *
    * @return  the handle of a temp-table object.
    */
   public handle tableHandle()
   {
      return buffer().tableHandle();
   }
   
   /**
    * Get the TEMP-TABLE resource associated with this buffer.
    * 
    * @return   See above.
    */
   public TempTable tableHandleNative()
   {
      return buffer().tableHandleResource();
   }
   
   /**
    * Conversion of INDEX-INFORMATION method. Returns information about the first (according to
    * the definition order) table index, as a string.
    *
    * @return Information about the first (according to definition order) index, as a string. The
    *         returned comma-separated list consists of the following in the specified order:
    *         <ul>
    *         <li>the index name;
    *         <li>three integers of value 0 (false) or 1 (true) depending on whether the index is
    *             unique, primary or a word index;
    *         <li>the names of the index fields, each followed by a 0 (ascending) or 1
    *             (descending).
    *         </ul>
    *         An example: "index-one,1,1,0,field-one,0".
    */
   public character indexInformation()
   {
      return indexInformation(new integer(1));
   }
   
   /**
    * Conversion of INDEX-INFORMATION method. Returns information about the specified table index,
    * as a string.
    *
    * @param  indexNum
    *         1-based number of the index in the definition order.
    *
    * @return Information about the specified index, as a string. The returned comma-separated
    *         list consists of the following in the specified order:
    *         <ul>
    *         <li>the index name;
    *         <li>three integers of value 0 (false) or 1 (true) depending on whether the index is
    *             unique, primary or a word index;
    *         <li>the names of the index fields, each followed by a 0 (ascending) or 1
    *             (descending).
    *         </ul>
    *         An example: "index-one,1,1,0,field-one,0".
    */
   public character indexInformation(int indexNum)
   {
      return indexInformation(new integer(indexNum));
   }
   
   /**
    * Conversion of INDEX-INFORMATION method. Returns information about the specified table index,
    * as a string.
    *
    * @param  indexNum
    *         1-based number of the index in the definition order.
    *
    * @return Information about the specified index, as a string. The returned comma-separated
    *         list consists of the following in the specified order:
    *         <ul>
    *         <li>the index name;
    *         <li>three integers of value 0 (false) or 1 (true) depending on whether the index is
    *             unique, primary or a word index;
    *         <li>the names of the index fields, each followed by a 0 (ascending) or 1
    *             (descending).
    *         </ul>
    *         An example: "index-one,1,1,0,field-one,0".
    */
   public character indexInformation(NumberType indexNum)
   {
      if (indexNum.isUnknown())
      {
         return new character();
      }
      
      return new character(TableMapper.getLegacyIndexInfoString(this.buffer(), indexNum.intValue() - 1));
   }
   
   /**
    * Conversion of DYNAMIC attribute. Returns <code>true</code> if the buffer is dynamic.
    *
    * @return <code>true</code> if the buffer is dynamic.
    */
   @Override
   public boolean _dynamic()
   {
      return buffer().isDynamic();
   }
   
   /**
    * Conversion of DYNAMIC attribute. Returns <code>true</code> if the buffer is dynamic.
    *
    * @return <code>true</code> if the buffer is dynamic.
    */
   @Override
   public logical dynamic()
   {
      return logical.of(_dynamic());
   }
   
   /**
    * Get the value of the ADM-DATA attribute.
    * 
    * @return   See above.
    */
   @Override
   public character getADMData()
   {
      return new character(this.admData);
   }
   
   /**
    * Set the value of the ADM-DATA attribute.
    * 
    * @param    value
    *           The new value.
    */
   @Override
   public void setADMData(String value)
   {
      this.admData = value;
   }
   
   /**
    * Set the value of the ADM-DATA attribute.
    * 
    * @param   value
    *          The new value.
    */
   @Override
   public void setADMData(character value)
   {
      this.admData = value.toJavaType();
   }
   
   /**
    * Gets the unique ID number associated to this object by the underlying system. Not the
    * same as the handle value itself.
    * 
    * @return  An integer representing an unique id between all buffers ever created in the 
    *          current process.
    */
   @Override
   public integer getUniqueID()
   {
      if (uniqueID == null)
      {
         uniqueID = UniqueIdGenerator.getUniqueId(IdKind.BUFFER);
      }
      
      return new integer(this.uniqueID);
   }
   
   /**
    * Returns the value of the NAMESPACE-URI attribute. Only for ELEMENT or ATTRIBUTE,
    * for other attributes return unknown value.
    *
    * @return  See above.
    */
   @Override
   public character namespaceURI()
   {
      return character.of(namespaceURI);
   }
   
   /**
    * Setter for NAMESPACE-URI attribute.
    *
    * @param   uri
    *          The new value of the NAMESPACE-URI attribute.
    */
   @Override
   public void namespaceURI(String uri)
   {
      if (uri == null)
      {
         ErrorManager.recordOrShowError(4083, "NAMESPACE-URI", LegacyResource.BUFFER + " widget");
         return;
      }
      
      namespaceURI = uri;
   }
   
   /**
    * Setter for NAMESPACE-URI attribute.
    *
    * @param   uri
    *          The new value of the NAMESPACE-URI attribute.
    */
   @Override
   public void namespaceURI(character uri)
   {
      namespaceURI(uri.toJavaType());
   }
   
   /**
    * Returns the value of the NAMESPACE-PREFIX attribute. Only for ELEMENT or ATTRIBUTE,
    * for other attributes return unknown value.
    *
    * @return  See above.
    */
   @Override
   public character namespacePrefix()
   {
      return character.of(namespacePrefix);
   }
   
   /**
    * Setter for NAMESPACE-PREFIX attribute.
    *
    * @param   prefix
    *          The new value of the NAMESPACE-PREFIX attribute.
    */
   @Override
   public void namespacePrefix(String prefix)
   {
      if (prefix == null)
      {
         ErrorManager.recordOrShowError(4083, "NAMESPACE-PREFIX", LegacyResource.BUFFER + " widget");
         return;
      }
      
      namespacePrefix = prefix;
   }
   
   /**
    * Setter for NAMESPACE-PREFIX attribute.
    *
    * @param   prefix
    *          The new value of the NAMESPACE-PREFIX attribute.
    */
   @Override
   public void namespacePrefix(character prefix)
   {
      namespacePrefix(prefix.toJavaType());
   }
   
   /**
    * Implementation of the read access of the {@code LABEL}.
    *
    * @return  The current value of the {@code LABEL}.
    */
   public character label()
   {
      ErrorManager.recordOrShowError(4052, "LABEL", "queryable", LegacyResource.BUFFER + " widget");
      return character.UNKNOWN;
   }
   
   /**
    * Implementation of the write access of the {@code LABEL}.
    *
    * @param   sName
    *          the new value for the {@code LABEL}.
    */
   public void label(Text sName)
   {
      ErrorManager.recordOrShowError(4052, "LABEL", "setable" /*sic*/, LegacyResource.BUFFER + " widget");
   }
   
   /**
    * Implementation of the write access of the {@code LABEL}.
    *
    * @param  sName
    *         The new value for the {@code LABEL}.
    */
   public void label(String sName)
   {
      ErrorManager.recordOrShowError(4052, "LABEL", "setable" /*sic*/, LegacyResource.BUFFER + " widget");
   }
   
   /**
    * Implementation of the read access of the {@code SERIALIZE-NAME} attribute.
    *
    * @return  the current value of the {@code SERIALIZE-NAME} attribute.
    */
   public character getSerializeName()
   {
      DmoMeta dmoInfo = buffer().getDmoInfo();
      return serializeName == null || serializeName.trim().isEmpty() ? new character(dmoInfo.legacyTable) : 
                                                                       new character(serializeName);
   }
   
   /**
    * Implementation of the write access of the {@code SERIALIZE-NAME} attribute.
    *
    * @param   sName
    *          the new value for the {@code SERIALIZE-NAME} attribute. 
    */
   public void setSerializeName(Text sName)
   {
      setSerializeName(sName.toJavaType());
   }
   
   /**
    * Implementation of the write access of the {@code SERIALIZE-NAME} attribute.
    *
    * @param   sName
    *          the new value for the {@code SERIALIZE-NAME} attribute. 
    */
   public void setSerializeName(String sName)
   {
      serializeName = sName;
   }
   
   /**
    * Implementation of the read access of the {@code XML-NODE-NAME} attribute.
    *
    * @return  the current value of the {@code XML-NODE-NAME} attribute.
    */
   public character getXmlNodeName()
   {
      return xmlNodeName == null || xmlNodeName.trim().isEmpty() ? getSerializeName() : 
                                                                   new character(xmlNodeName);
   }
   
   /**
    * Implementation of the write access of the {@code XML-NODE-NAME} attribute.
    *
    * @param   sName
    *          the new value for the {@code XML-NODE-NAME} attribute. 
    */
   public void setXmlNodeName(Text sName)
   {
      setXmlNodeName(sName.toJavaType());
   }
   
   /**
    * Implementation of the write access of the {@code XML-NODE-NAME} attribute.
    *
    * @param   sName
    *          the new value for the {@code XML-NODE-NAME} attribute. 
    */
   public void setXmlNodeName(String sName)
   {
      xmlNodeName = sName;
   }
   
   /**
    * Obtain the XML Schema data type for the buffer-field object.
    *
    * @return the XML Schema data type for the buffer-field object.
    */
   @Override
   public character getXmlDataType()
   {
      handle.invalidAttribute("XML-DATA-TYPE", false, LegacyResource.BUFFER);
      return new character();
   }
   
   /**
    * Get the BUFFER:AUTO-SYNCHRONIZE attribute.
    *
    * @return  the value of BUFFER:AUTO-SYNCHRONIZE attribute.
    */
   @Override
   public logical autoSynchronize()
   {
      if (dataSet == null)
      {
         return logical.UNKNOWN;
      }
      
      return logical.of(autoSynchronize);
   }
   
   /**
    * Set the BUFFER:AUTO-SYNCHRONIZE attribute.
    */
   @Override
   public void autoSynchronize(boolean autoSyncronize)
   { 
      if (dataSet == null)
      {
         return;
      }
      
      this.autoSynchronize = autoSyncronize;
   }
   
   /**
    * Set the BUFFER:AUTO-SYNCHRONIZE attribute.
    */
   @Override
   public void autoSynchronize(logical autoSyncronize)
   {
      if (autoSyncronize == null || autoSyncronize.isUnknown())
      {
         ErrorManager.recordOrShowError(4083, "AUTO-SYNCHRONIZE", "BUFFER widget");
         return;
      }
      
      autoSynchronize(autoSyncronize.booleanValue());
   }
   
   /**
    * Obtain the CAN-READ data type for the buffer-field object.
    *
    * @return the CAN-READ data type for the buffer-field object.
    */
   @Override
   public logical canRead()
   {
      UnimplementedFeature.missing("BUFFER:CAN-READ");
      return logical.UNKNOWN;
   }
   
   /**
    * Implementation of the NUM-ITERATIONS getter.
    *
    * @return  The current value of the NUM-ITERATIONS attribute.
    */
   @Override
   public integer numIterations()
   {
      UnimplementedFeature.missing("BUFFER:NUM-ITERATIONS");
      return new integer();
   }
   
   /**
    * Implementation of the NUM-REFERENCES getter.
    *
    * @return  The current value of the NUM-REFERENCES attribute.
    */
   @Override
   public integer numReferences()
   {
      UnimplementedFeature.missing("BUFFER:NUM-REFERENCES");
      return new integer();
   }
   
   /**
    * Configures the XML Schema data type for the buffer-field object.
    *
    * @param newType the new XML Schema data type for the buffer-field object.
    */
   @Override
   public void setXmlDataType(String newType)
   {
      handle.invalidAttribute("XML-DATA-TYPE", true, LegacyResource.BUFFER);
   }
   
   /**
    * Configures the XML Schema data type for the buffer-field object.
    *
    * @param newType the new XML Schema data type for the buffer-field object.
    */
   @Override
   public void setXmlDataType(Text newType)
   {
      handle.invalidAttribute("XML-DATA-TYPE", true, LegacyResource.BUFFER);
   }
   
   /**
    * Obtain the XML node type of this object. This represents the getter for {@code XML-NODE-TYPE} ABL
    * attribute.
    *
    * @return  the XML node type of this object.
    */
   @Override
   public character getXmlNodeType()
   {
      handle.invalidAttribute("XML-NODE-TYPE", false, LegacyResource.BUFFER);
      return new character();
   }
   
   /**
    * Allows specifying how this object will be represented in XML and XML Schema. Valid XML node types are
    * "ATTRIBUTE", "ELEMENT", "HIDDEN", and "TEXT". This represents the setter for {@code XML-NODE-TYPE} ABL
    * attribute.
    *
    * @param   newType
    *          The new type.
    */
   @Override
   public void setXmlNodeType(String newType)
   {
      handle.invalidAttribute("XML-NODE-TYPE", true, LegacyResource.BUFFER);
   }
   
   /**
    * Allows specifying how this object will be represented in XML and XML Schema. Valid XML node types are
    * "ATTRIBUTE", "ELEMENT", "HIDDEN", and "TEXT". This represents the setter for {@code XML-NODE-TYPE} ABL
    * attribute.
    *
    * @param   newType
    *          The new type.
    */
   @Override
   public void setXmlNodeType(Text newType)
   {
      handle.invalidAttribute("XML-NODE-TYPE", true, LegacyResource.BUFFER);
   }
   
   /**
    * Obtain the list of key fields of the buffer as a comma-separated list.
    *
    * @return  the list of key fields of the buffer. If the buffer is not part of a datasource, {@code rowid}
    *          is returned.
    */
   @Override
   public character keys()
   {
      if (parentDataSource == null || !parentDataSource.valid() || keys == null)
      {
         P2JIndex primaryIndex = buffer().getDmoInfo().getPrimaryIndex(true);
         if (primaryIndex == null || !primaryIndex.isUnique())
         {
            return new character(DataSource.ROWID_KEY);
         }
         
         StringBuilder sb = new StringBuilder();

         ArrayList<P2JIndexComponent> comps = primaryIndex.components();
         for (int i = 0; i < comps.size(); i++)
         {
            P2JIndexComponent comp = comps.get(i);
            if (sb.length() != 0)
            {
               sb.append(",");
            }
            sb.append(comp.getLegacyName());
         }
         
         return new character(sb.toString());
      }
      
      StringBuilder sb = new StringBuilder();
      for (String key : keys)
      {
         if (Session.PK.equals(key))
         {
            continue;
         }
         
         if (sb.length() != 0)
         {
            sb.append(",");
         }
         sb.append(key);
      }
      
      return new character(sb.toString());
   }
   
   /**
    * Obtain the list of key fields of the buffer as a comma-separated list.
    *
    * @param   seqNum
    *          Ignored.
    *
    * @return  the list of key fields of the buffer.
    */
   @Override
   public character keys(long seqNum)
   {
      return keys();
   }
   
   /**
    * Obtain the list of key fields of the buffer as a comma-separated list.
    *
    * @param   seqNum
    *          Ignored.
    *
    * @return  the list of key fields of the buffer.
    */
   @Override
   public character keys(NumberType seqNum)
   {
      return keys();
   }
   
   /**
    * Set the CURRENT-ITERATION widget handle for this frame. This attribute is writable only for frames.
    *
    * @param   newIteration
    *          Ignored.
    */
   @Override
   public void setCurrentIteration(handle newIteration)
   {
      if (newIteration == null || newIteration.isUnknown())
      {
         ErrorManager.recordOrShowError(4083, "CURRENT-ITERATION", "BUFFER widget");
      }
      else
      {
         handle.invalidAttribute("CURRENT-ITERATION", true, LegacyResource.BUFFER);
      }
   }
   
   /**
    * Perform actual delete of an resource. At the time of this call, it is assumed the resource
    * is valid for deletion (the handle and the resource are both valid).
    * The method is overridden only to allow this object to maintain the structure of the chain of
    * BUFFERs (to update the PERMANENT-TABLE pointer if it was pointing to this buffer AND if the
    * {@link HandleChain#delete()} really disconnected it from the chain)
    */
   @Override
   public void delete()
   {
      // save next sibling because super.delete() will wipe it out 
      HandleChain nextResource = this.nextSibling;
      
      super.delete();
      if (this.nextSibling != null)
      {
         // the super.delete() did not removed this object from chain
         return;
      }
      
      // special care for BUFFER resources: update the PERMANENT-TABLE pointer
      if (this == HandleChain.getHead(PERMANENT_TABLE)) 
      {
         HandleChain.setHead(PERMANENT_TABLE, nextResource);
      }
   }
   
   /**
    * Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
    * <p>
    * Gets the handle to a particular buffer of a query or DataSet object.
    *
    * @param   fieldNumber
    *          field number. An INTEGER expression representing the sequence
    *          number of the field in the buffer.
    *
    * @return  handle to a particular field in the buffer.
    */
   @Override
   public handle bufferField(NumberType fieldNumber)
   {
      if (fieldNumber == null || fieldNumber.isUnknown())
      {
         String msg = "Argument for BUFFER-FIELD method for buffer " +
                      buffer().getDmoInfo().legacyTable + " could not be evaluated";
         // untested at this time for 'database behavior' related to throwing structured exceptions
         // - see recordOrShowDatabaseError
         ErrorManager.recordOrShowError(7349, msg, false, false, false);
         return new handle();
      }
      
      return bufferField(fieldNumber.intValue());
   }
   
   /**
    * Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
    * <p>
    * Gets the handle to a particular buffer of a query or DataSet object.
    *
    * @param   fieldName
    *          A CHARACTER string expression representing the name of the field in the buffer.
    *
    * @return  handle to a particular field in the buffer.
    */
   @Override
   public handle bufferField(character fieldName)
   {
      if (fieldName.isUnknown())
      {
         String msg = "Argument for BUFFER-FIELD method for buffer " +
                      buffer().getDmoInfo().legacyTable + " could not be evaluated";
         // untested at this time for 'database behavior' related to throwing structured exceptions
         // - see recordOrShowDatabaseError
         ErrorManager.recordOrShowError(7349, msg, false, false, false);
         
         // TODO: if this message needs to be displayed, the code must be moved in other place
         // String errMsg2 =
         //    "Lead attributes in a chained-attribute expression (a:b:c) must be type " +
         //    "HANDLE or a user-defined type and valid (not UNKNOWN)";
         // ErrorManager.recordOrShowError(10068, errMsg2, false, false, false);
         return new handle();
      }
      else
      {
         return bufferField(fieldName.getValue());
      }
   }
   
   /**
    * Conversion of BUFFER-FIELD() method (KW_BUF_FLD).
    * <p>
    * Gets the handle to a particular buffer of a query or DataSet object.
    *
    * @param   fieldName
    *          A CHARACTER string expression representing the name of the field in the buffer.
    *
    * @return  handle to a particular field in the buffer.
    */
   @Override
   public handle bufferField(BaseDataType fieldName)
   {
      if (Text.class.isAssignableFrom(fieldName.getClass()))
      {
         return bufferField(fieldName.toStringMessage());
      }
      
      if (NumberType.class.isAssignableFrom(fieldName.getClass()))
      {
         return bufferField((NumberType) fieldName);
      }
      
      if (BinaryData.class.isAssignableFrom(fieldName.getClass()))
      {
         BinaryData bd = (BinaryData) fieldName;
         character nm = bd.getString(0);
         
         return bufferField(nm);
      }
      
      // otherwise, it uses the 'hex representation' of the value to compute a read a string.
      // we don't emulate this at this time.
      UnimplementedFeature.missing("BUFFER-POLY(incompatible-BDT) not implemented!");
      
      String msg = "BUFFER-FIELD " + fieldName + " was not found in buffer " +
                   buffer().getDmoInfo().legacyTable;
      ErrorManager.recordOrShowDatabaseError(7351, msg, false, false);
      
      // return unknown handle
      return new handle();
   }
   
   /**
    * Implementation of the READ-XML method. Reads XML data from the specified data source into
    * this object.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>Default field type mappings</li>
    * <li>LOOSE schema verification mode</li>
    * </ul>
    * 
    * @param   source
    *          Data source.
    * @param   readMode
    *          Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
    *          EMPTY}, {@code MERGE}, or {@code REPLACE}.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to use the
    *          {@code xsi:noNamespaceSchemaLocation} value set in the XML data, if any.
    * @param   overrideDefaultMapping
    *          {@code True} to override the default string-to-binary data type mappings when
    *          creating a temp-table schema from an XML schema.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical readXml(SourceData source,
                          character readMode,
                          character schemaLocation,
                          logical overrideDefaultMapping)
   {
      return readXml(source, readMode, schemaLocation, overrideDefaultMapping, new character());
   }
   
   /**
    * Implementation of the READ-XML method. Reads XML data from the specified data source into
    * this object.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>LOOSE schema verification mode</li>
    * </ul>
    * 
    * @param   source
    *          Data source.
    * @param   readMode
    *          Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
    *          EMPTY}, {@code MERGE}, or {@code REPLACE}.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to use the
    *          {@code xsi:noNamespaceSchemaLocation} value set in the XML data, if any.
    * @param   overrideDefaultMapping
    *          {@code True} to override the default string-to-binary data type mappings when
    *          creating a temp-table schema from an XML schema.
    * @param   fieldTypeMapping
    *          Comma-delimited list of field name and data type name pairs to use to override
    *          data type mappings of specific fields. Specify unknown value to use default
    *          mappings.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical readXml(SourceData source,
                          character readMode,
                          character schemaLocation,
                          logical overrideDefaultMapping,
                          character fieldTypeMapping)
   {
      return readXml(source,
                     readMode,
                     schemaLocation,
                     overrideDefaultMapping,
                     fieldTypeMapping,
                     new character("LOOSE"));
   }
   
   /**
    * Implementation of the READ-XML method. Reads XML data from the specified data source into
    * this object.
    * 
    * @param   source
    *          Data source.
    * @param   readMode
    *          Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
    *          EMPTY}, {@code MERGE}, or {@code REPLACE}.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to use the
    *          {@code xsi:noNamespaceSchemaLocation} value set in the XML data, if any.
    * @param   overrideDefaultMapping
    *          {@code True} to override the default string-to-binary data type mappings when
    *          creating a temp-table schema from an XML schema.
    * @param   fieldTypeMapping
    *          Comma-delimited list of field name and data type name pairs to use to override
    *          data type mappings of specific fields. Specify unknown value to use default
    *          mappings.
    * @param   verifySchemaMode
    *          Schema verification mode. Must evaluate to {@code IGNORE}, {@code LOOSE}, or
    *          {@code STRICT}. Default is {@code LOOSE}.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical readXml(SourceData source,
                          character readMode,
                          character schemaLocation,
                          logical overrideDefaultMapping,
                          character fieldTypeMapping,
                          character verifySchemaMode)
   {
      RecordBuffer buffer = buffer();
      
      // sanity
      if (!buffer.isTemporary())
      {
         ErrorManager.recordOrShowError(13158);
         // XML Methods only allowed on TEMP-TABLE buffers.
         return logical.FALSE;
      }

      Map<String, String> mapping = null;
      if (fieldTypeMapping != null)
      {
         String javaStrMapping = fieldTypeMapping.toJavaType();
         if (javaStrMapping != null)
         {
            mapping = new HashMap<>();
            String[] splits = javaStrMapping.split(",");
            int len = splits.length;
            if (len == 0 || len % 2 == 1)
            {
               ErrorManager.recordOrShowError(13200);
               // Odd number of values in field-type-mapping argument.
               return logical.of(false);
            }
            // no other validation at this time

            for (int k = 0; k < len; k += 2)
            { 
               mapping.putIfAbsent(splits[k], splits[k + 1]); // do not overwrite if field is duplicated
            }
         }
      }

      // normalize parameters
      XmlImport xmlImport = new XmlImport(source,
                                          readMode.toJavaType(),
                                          schemaLocation.toJavaType(),
                                          !overrideDefaultMapping.isUnknown() && 
                                           overrideDefaultMapping.booleanValue(),
                                          mapping,
                                          verifySchemaMode.toJavaType());
      
      // perform import
      return logical.of(xmlImport.importTable(this, RecordBuffer::loadRecord));
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>Unformatted text</li>
    * <li>UTF-8 encoding</li>
    * <li>xsi:noNamespaceSchemaLocation=?</li>
    * <li>No XML schema written</li>
    * <li>No before-image data written</li>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical writeXml(TargetData target)
   {
      return writeXml(target, null, null, null, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>UTF-8 encoding</li>
    * <li>xsi:noNamespaceSchemaLocation=?</li>
    * <li>No XML schema written</li>
    * <li>No before-image data written</li>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical writeXml(TargetData target, logical formatted)
   {
      return writeXml(target, formatted, null, null, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>xsi:noNamespaceSchemaLocation=?</li>
    * <li>No XML schema written</li>
    * <li>No before-image data written</li>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical writeXml(TargetData target, logical formatted, character encoding)
   {
      return writeXml(target, formatted, encoding, null, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>No XML schema written</li>
    * <li>No before-image data written</li>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical writeXml(TargetData target, logical formatted, character encoding, character schemaLocation)
   {
      return writeXml(target, formatted, encoding, schemaLocation, null, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>No before-image data written</li>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    * @param   writeXmlSchema
    *          {@code True} to write XML schema information, else {@code false}.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical writeXml(TargetData target,
                           logical formatted,
                           character encoding,
                           character schemaLocation,
                           logical writeXmlSchema)
   {
      return writeXml(target, formatted, encoding, schemaLocation, writeXmlSchema, null, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>No before-image data written</li>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    * @param   writeXmlSchema
    *          {@code True} to write XML schema information, else {@code false}.
    * @param   minXmlSchema
    *          {@code True} to write minimal XML schema information, else {@code false}. Must be
    *          {@code false} if {@code writeXmlSchema} is {@code false}.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical writeXml(TargetData target,
                           logical formatted,
                           character encoding,
                           character schemaLocation,
                           logical writeXmlSchema,
                           logical minXmlSchema)
   {
      return writeXml(target, formatted, encoding, schemaLocation, writeXmlSchema, minXmlSchema, null, null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    * @param   writeXmlSchema
    *          {@code True} to write XML schema information, else {@code false}.
    * @param   minXmlSchema
    *          {@code True} to write minimal XML schema information, else {@code false}. Must be
    *          {@code false} if {@code writeXmlSchema} is {@code false}.
    * @param   writeBeforeImage
    *          {@code True} to write before-image data and error information, else {@code false}.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical writeXml(TargetData target,
                           logical formatted,
                           character encoding,
                           character schemaLocation,
                           logical writeXmlSchema,
                           logical minXmlSchema,
                           logical writeBeforeImage)
   {
      return writeXml(target,
                      formatted,
                      encoding,
                      schemaLocation,
                      writeXmlSchema,
                      minXmlSchema,
                      writeBeforeImage,
                      null);
   }
   
   /**
    * Implementation of the WRITE-XML method. Writes XML data from this object into the specified
    * data target.
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the XML to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   schemaLocation
    *          Name of an external XML schema file, or empty string or unknown value to set the
    *          {@code xsi:noNamespaceSchemaLocation} to empty string or unknown value.
    * @param   writeXmlSchema
    *          {@code True} to write XML schema information, else {@code false}.
    * @param   minXmlSchema
    *          {@code True} to write minimal XML schema information, else {@code false}. Must be
    *          {@code false} if {@code writeXmlSchema} is {@code false}.
    * @param   beforeImage
    *          {@code True} to write before-image data and error information, else {@code false}.
    * @param   noInitialVals
    *          {@code True} to omit data for fields whose values match their initial values.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   public logical writeXml(TargetData target,
                           logical formatted,
                           character encoding,
                           character schemaLocation,
                           logical writeXmlSchema,
                           logical minXmlSchema,
                           logical beforeImage,
                           logical noInitialVals)
   {
      RecordBuffer buffer = buffer();
      boolean ret;
      
      try
      {
         // sanity
         if (!buffer.isTemporary())
         {
            ErrorManager.recordOrShowError(13158);
            // XML Methods only allowed on TEMP-TABLE buffers.
            return logical.FALSE;
         }
         
         if (!buffer.getParentTable().flushBuffers())
         {
            return logical.FALSE;
         }
         
         XmlExport export = new XmlExport(
               target,
               formatted != null && !formatted.isUnknown() && formatted.getValue(),
               encoding == null ? null : encoding.toJavaType());
         
         ret = export.writeTempTable(
               (TemporaryBuffer) buffer,
               schemaLocation == null ? null : schemaLocation.toJavaType(),
               writeXmlSchema != null && !writeXmlSchema.isUnknown() && writeXmlSchema.getValue(),
               minXmlSchema != null && !minXmlSchema.isUnknown() && minXmlSchema.getValue(),
               beforeImage != null && !beforeImage.isUnknown() && beforeImage.getValue(),
               noInitialVals != null && !noInitialVals.isUnknown() && noInitialVals.getValue());
         
         buffer.release(false); 
      }
      catch (XmlExport.Fatal e) // Emergency exit - for the 4GL compatibility
      {
         // Error was already reported, just stop;
         ret = false;
      }
      finally
      {
         try
         {
            target.close();
         }
         catch (IOException e)
         {
            // TODO: log
            ret = false;
         }
      }
      return logical.of(ret);
   }
   
   /**
    * Writes an XML representation of the definition of this {@code TempTable}.
    *
    * @param   target
    *          Data target.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target)
   {
      return writeXmlSchema(target, null, null, null, null);
   }
   
   /**
    * Writes an XML representation of the definition of this {@code TempTable}.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          Use {@code true} for a more human-readable document.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target, logical formatted)
   {
      return writeXmlSchema(target, formatted, null, null, null);
   }
   
   /**
    * Writes an XML representation of the definition of this {@code TempTable}.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          Use {@code true} for a more human-readable document.
    * @param   encoding
    *          The character encoding. The default is UTF-8.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target, logical formatted, Text encoding)
   {
      return writeXmlSchema(target, formatted, encoding, null, null);
   }
   
   /**
    * Writes an XML representation of the definition of this {@code TempTable}.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          Use {@code true} for a more human-readable document.
    * @param   encoding
    *          The character encoding. The default is UTF-8.
    * @param   minXmlSchema
    *          If {@code true}, the minimum amount of schema for the object will be written.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target, logical formatted, Text encoding, logical minXmlSchema)
   {
      return writeXmlSchema(target, formatted, encoding, minXmlSchema, null); 
   }
   
   /**
    * Writes an XML representation of the definition of this {@code TempTable}.
    *
    * @param   target
    *          Data target.
    * @param   formatted
    *          Use {@code true} for a more human-readable document.
    * @param   encoding
    *          The character encoding. The default is UTF-8.
    * @param   minXmlSchema
    *          If {@code true}, the minimum amount of schema for the object will be written.
    * @param   omitInitVal
    *          Use {@code true} to omit the initial values.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical writeXmlSchema(TargetData target,
                                 logical formatted,
                                 Text encoding,
                                 logical minXmlSchema,
                                 logical omitInitVal)
   {
      RecordBuffer buffer = buffer();
      boolean ret;
      
      try
      {
         // sanity
         if (!buffer.isTemporary())
         {
            ErrorManager.recordOrShowError(13158);
            // XML Methods only allowed on TEMP-TABLE buffers.
            return logical.FALSE;
         }
         
         XmlExport export = new XmlExport(
               target,
               formatted != null && !formatted.isUnknown() && formatted.getValue(),
               encoding == null ? null : encoding.toJavaType());
            
            ret = export.writeTableSchema(
                     (TemporaryBuffer) buffer,
                     minXmlSchema != null && !minXmlSchema.isUnknown() && minXmlSchema.getValue(),
                     omitInitVal != null && !omitInitVal.isUnknown() && omitInitVal.getValue());
      }
      finally
      {
         try
         {
            target.close();
         }
         catch (IOException e)
         {
            // TODO: log
            ret = false;
         }
      }
      
      return logical.of(ret);
   }
   
   /**
    * Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data 
    * target, using the specified format.
    *
    * @param   targetFormat
    *          The format ("xml" or "json").
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the data for making it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   omitInitialValues
    *          {@code True} to omit data for fields whose values match their initial values.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical serializeRow(character  targetFormat,
                               TargetData target,
                               logical    formatted,
                               character  encoding,
                               logical    omitInitialValues)
   {
      return logical.of(serializeRowImpl(targetFormat.toJavaType(),
                                          target,
                                          formatted.toJavaType(),
                                          encoding.toJavaType(),
                                          omitInitialValues.toJavaType(),
                                          null));
   }
   
   /**
    * Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data 
    * target, using the specified format.
    *
    * @param   format
    *          The format ("xml" or "json").
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the data for making it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical serializeRow(character format, TargetData target, logical formatted, character encoding)
   {
      return logical.of(serializeRowImpl(
            format.toJavaType(), target, formatted.toJavaType(), encoding.toJavaType(), null, null));
   }
   
   /**
    * Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data 
    * target, using the specified format.
    *
    * @param   format
    *          The format ("xml" or "json").
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the data for making it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical serializeRow(character format, TargetData target, logical formatted)
   {
      return logical.of(serializeRowImpl(
            format.toJavaType(), target, formatted.toJavaType(), null, null, null));
   }
   
   /**
    * Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data 
    * target, using the specified format.
    *
    * @param   format
    *          The format ("xml" or "json").
    * @param   target
    *          Data target.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical serializeRow(character format, TargetData target)
   {
      return logical.of(serializeRowImpl(format.toJavaType(), target, null, null, null, null));
   }
   
   /**
    * Definition of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data 
    * target, using the specified format.
    *
    * @param   targetFormat
    *          The format ("xml" or "json").
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the data for making it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   omitInitialValues
    *          {@code True} to omit data for fields whose values match their initial values.
    * @param   omitOuterObject
    *          {@code True} to omit data for the outermost object from the output.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical serializeRow(character  targetFormat,
                               TargetData target,
                               logical    formatted,
                               character  encoding,
                               logical    omitInitialValues,
                               logical    omitOuterObject)
   {
      return logical.of(serializeRowImpl(targetFormat.toJavaType(),
                                          target,
                                          formatted.toJavaType(),
                                          encoding.toJavaType(),
                                          omitInitialValues.toJavaType(),
                                          omitOuterObject.toJavaType()));
   }
   
   /**
    * Reads XML Schema from an XML document and uses that schema to either create a schema for a
    * {@code TempTable} or {@code TempTable} or verify existing schema in a {@code TempTable},
    * {@code TempTable} or temp-table {@code Buffer} object.
    *
    * @param   source
    *          Data source.
    * @param   override
    *          If {@code true} the default mapping between XML Schema string and binary data types
    *          and ABL data types when creating an ABL temp-table schema from an XML Schema will
    *          be overwritten.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical readXmlSchema(SourceData source, logical override)
   {
      return readXmlSchema(source, override, null, null);
   }
   
   /**
    * Reads XML Schema from an XML document and uses that schema to either create a schema for a
    * {@code TempTable} or {@code TempTable} or verify existing schema in a {@code TempTable},
    * {@code TempTable} or temp-table {@code Buffer} object.
    *
    * @param   source
    *          Data source.
    * @param   override
    *          If {@code true} the default mapping between XML Schema string and binary data types
    *          and ABL data types when creating an ABL temp-table schema from an XML Schema will
    *          be overwritten.
    * @param   mapping
    *          A comma-delimited list of field name, data type pairs. This allows to specify the
    *          data type for a specific field.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical readXmlSchema(SourceData source, logical override, Text mapping)
   {
      return readXmlSchema(source, override, mapping, null);
   }
   
   /**
    * Reads XML Schema from an XML document and uses that schema to either create a schema for a
    * {@code TempTable} or {@code TempTable} or verify existing schema in a {@code TempTable},
    * {@code TempTable} or temp-table {@code Buffer} object.
    *
    * @param   source
    *          Data source.
    * @param   override
    *          If {@code true} the default mapping between XML Schema string and binary data types
    *          and ABL data types when creating an ABL temp-table schema from an XML Schema will
    *          be overwritten.
    * @param   mapping
    *          A comma-delimited list of field name, data type pairs. This allows to specify the
    *          data type for a specific field.
    * @param   verifyMode
    *          Specifies how the XML will be verified against the schema. Valid values: "LOOSE" or
    *          "STRICT".
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical readXmlSchema(SourceData source, logical override, Text mapping, Text verifyMode)
   {
      return TempTableBuilder.asTempTable((Temporary) this).readXmlSchema(source,
                                                                          override,
                                                                          mapping,
                                                                          verifyMode);
   }
   
   /**
    * Implementation of the READ-JSON method. Reads JSON data from the specified data source into
    * this object.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>{@code MERGE} read mode</li>
    * </ul>
    * 
    * @param   source
    *          Data source.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical readJson(SourceData source)
   {
      return readJson(source, new character("MERGE"));
   }
   
   /**
    * Implementation of the READ-JSON method. Reads JSON data from the specified data source into
    * this object.
    * 
    * @param   source
    *          Data source.
    * @param   readMode
    *          Mode in which data is read into buffer. Must evaluate to {@code APPEND}, {@code
    *          EMPTY}, {@code MERGE}, or {@code REPLACE}.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical readJson(SourceData source, character readMode)
   {
      if (!source.configureSupportedSources(SourceData.SD_READ_JSON, 
               LegacyResource.TEMP_TABLE + " widget"))
      {
         return logical.FALSE;
      }
      RecordBuffer buffer = buffer();
      
      // sanity
      if (!buffer.isTemporary())
      {
         ErrorManager.recordOrShowError(15378);
         // JSON methods only allowed on TEMP-TABLE buffers.
         return logical.FALSE;
      }
      
      // normalize parameters
      JsonImport jsonImport = new JsonImport(source, readMode.toJavaType());
      boolean success;
      
      try
      {
         // perform import
         success = jsonImport.readTable((TemporaryBuffer) buffer, RecordBuffer::loadRecord);
      }
      catch (PersistenceException exc)
      {
         // TODO: log
         success = false;
      }
      
      return logical.of(success);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>Unformatted text</li>
    * <li>UTF-8 encoding</li>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * <li>Do not omit data for the outermost object, unless {@code target} represents a {@code
    *     JsonArray} object.
    * <li>No before-image data written</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target)
   {
      return writeJson(target,
                       logical.FALSE,
                       new character(),
                       logical.FALSE,
                       logical.FALSE,
                       logical.FALSE);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>UTF-8 encoding</li>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * <li>Do not omit data for the outermost object, unless {@code target} represents a {@code
    *     JsonArray} object.
    * <li>No before-image data written</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target, logical formatted)
   {
      return writeJson(target, 
                       formatted,
                       new character(),
                       logical.FALSE,
                       logical.FALSE,
                       logical.FALSE);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>Do not omit temp-table fields which contain their initial values</li>
    * <li>Do not omit data for the outermost object, unless {@code target} represents a {@code
    *     JsonArray} object.
    * <li>No before-image data written</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target, logical formatted, character encoding)
   {
      return writeJson(target,
                       formatted,
                       encoding,
                       logical.FALSE,
                       logical.FALSE,
                       logical.FALSE);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>Do not omit data for the outermost object, unless {@code target} represents a {@code
    *     JsonArray} object.
    * <li>No before-image data written</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   omitInitialValues
    *          {@code True} to omit data for fields whose values match their initial values.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target,
                            logical formatted,
                            character encoding,
                            logical omitInitialValues)
   {
      return writeJson(target,
                       formatted,
                       encoding,
                       omitInitialValues,
                       logical.FALSE,
                       logical.FALSE);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    * <p>
    * Defaults applied:
    * <ul>
    * <li>No before-image data written</li>
    * </ul>
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   omitInitialValues
    *          {@code True} to omit data for fields whose values match their initial values.
    * @param   omitOuterObject
    *          {@code True} to omit data for the outermost object from the output.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target,
                            logical formatted,
                            character encoding,
                            logical omitInitialValues,
                            logical omitOuterObject)
   {
      return writeJson(target,
                       formatted,
                       encoding,
                       omitInitialValues,
                       omitOuterObject,
                       logical.FALSE);
   }
   
   /**
    * Implementation of the WRITE-JSON method. Writes JSON data from this object into the
    * specified data target.
    * 
    * @param   target
    *          Data target.
    * @param   formatted
    *          {@code True} to format the JSON to make it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   omitInitialValues
    *          {@code True} to omit data for fields whose values match their initial values.
    * @param   omitOuterObject
    *          {@code True} to omit data for the outermost object from the output.
    * @param   writeBeforeImage
    *          {@code True} to write before-image data and error information, else {@code false}.
    * 
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   @Override
   public logical writeJson(TargetData target,
                            logical formatted,
                            character encoding,
                            logical omitInitialValues,
                            logical omitOuterObject,
                            logical writeBeforeImage)
   {
      RecordBuffer buffer = buffer();
      boolean success;
      
      try
      {
         // sanity
         if (!buffer.isTemporary())
         {
            ErrorManager.recordOrShowError(15378);
            // JSON methods only allowed on TEMP-TABLE buffers.
            return logical.FALSE;
         }
         
         if (writeBeforeImage != null      && 
             !writeBeforeImage.isUnknown() &&
             writeBeforeImage.getValue())
         {
            ErrorManager.recordOrShowError(17366);
            // write-before-image not allowed for TEMP-TABLE or BUFFER object.
            ErrorManager.recordOrShowError(4065, false, "WRITE-JSON", "BUFFER widget");
            // **The <attribute> attribute on the <widget id> has invalid arguments.
            return logical.FALSE;
         }
         
         if (!buffer.getParentTable().flushBuffers())
         {
            return logical.FALSE;
         }
         
         com.fasterxml.jackson.core.JsonEncoding jsonEncoding = JsonExport.parseEncoding(encoding);
         if (jsonEncoding == null)
         {
            ErrorManager.recordOrShowError(15342, encoding.toStringMessage(), "WRITE-JSON");
            // Invalid encoding '<encoding>' argument for <method>.
            return logical.FALSE;
         }
         
         JsonExport export = new JsonExport();
         success = export.exportTempBuffer((TemporaryBuffer) buffer,
                                           target,
                                           formatted,
                                           jsonEncoding,
                                           omitInitialValues,
                                           omitOuterObject);
         
         buffer.release(false);
      }
      catch (PersistenceException exc)
      {
         // TODO: log
         success = false;
      }
      finally
      {
         try
         {
            target.close();
         }
         catch (IOException e)
         {
            // TODO: log
            success = false;
         }
      }
      
      return logical.of(success);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    *
    * @param   other
    *          Some other temp-table to copy from.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other)
   {
      return copyTempTable(other, false, false, false, null);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    *
    * @return  {@code true}  if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, logical append)
   {
      return copyTempTable(other, append, logical.FALSE, logical.FALSE, new character());
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    *
    * @return  {@code true} if operation is successful
    */
   @Override
   public logical copyTempTable(handle other, logical append, logical replace)
   {
      return copyTempTable(other, append, replace, logical.FALSE, new character());
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    * @param   looseCopy
    *          Loose copy mode.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, logical append, logical replace, logical looseCopy)
   {
      return copyTempTable(other, append, replace, looseCopy, new character());
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    *
    * @return  {@code true}  if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, boolean append)
   {
      return copyTempTable(other, append, false, false, null);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, boolean append, boolean replace)
   {
      return copyTempTable(other, append, replace, false, null);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current
    * records are dropped before copy operation.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    * @param   looseCopy
    *          Loose copy mode.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other, boolean append, boolean replace, boolean looseCopy)
   {
      return copyTempTable(other, append, replace, looseCopy, null);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current 
    * records are dropped before copy operation. 
    * Only the first parameter is mandatory so this method will probably be heavy overloaded.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    * @param   looseCopy
    *          Loose copy mode.
    * @param   prefix
    *          Prefix for naming the copied temp-table.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other,
                                logical append,
                                logical replace,
                                logical looseCopy,
                                character prefix)
   {
      TempTable tt = TempTableBuilder.asTempTable((Temporary) this);
      if (tt == null)
      {
         LOG.log(Level.SEVERE,
                 "Nothing to copy into, this dynamic TEMP-TABLE was not prepared yet.",
                 new Throwable());
         return logical.of(false);
      }
      return tt.copyTempTable(other, append, replace, looseCopy, prefix);
   }
   
   /**
    * Copies the other TEMP-TABLE. Other must be a TEMP-TABLE handle. By default, all current 
    * records are dropped before copy operation. 
    * Only the first parameter is mandatory so this method will probably be heavy overloaded.
    *
    * @param   other
    *          Some other temp-table to copy from.
    * @param   append
    *          Append mode.
    * @param   replace
    *          Replace mode.
    * @param   looseCopy
    *          Loose copy mode.
    * @param   prefix
    *          Prefix for naming the copied temp-table.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical copyTempTable(handle other,
                                boolean append,
                                boolean replace,
                                boolean looseCopy,
                                String prefix)
   {
      TempTable tt = TempTableBuilder.asTempTable((Temporary) this);
      if (tt == null)
      {
         LOG.log(Level.SEVERE,
                 "Nothing to copy into, this dynamic TEMP-TABLE was not prepared yet.",
                 new Throwable());
         return logical.of(false);
      }
      return tt.copyTempTable(other,
                              logical.of(append),
                              logical.of(replace),
                              logical.of(looseCopy),
                              new character(prefix));
   }
   
   /**
    * Obtain the last associated query (to which this buffer was added or set).
    *
    * @return   The associated query or {@code unknown} if none exists.
    */
   @Override
   public handle getQueryAsHandle()
   {
      // a bit of maintenance
      if (lastQuery != null && !lastQuery.valid())
      {
         lastQuery = null;
      }
      
      return lastQuery == null ? new handle() : new handle(lastQuery);
   }
   
   /**
    * Set the query associated with the given resource. This is illegal when called directly by
    * P4GL application and will raise 4052 condition.
    *
    * @param   qry
    *          The query to associate.
    */
   @Override
   public void setQueryAsHandle(handle qry)
   {
      handle.invalidAttribute("QUERY", true, LegacyResource.BUFFER);
   }
   
   /**
    * Set the query associated with the given resource. This is illegal when called directly by
    * P4GL application and will raise 4052 condition.
    *
    * @param   qry
    *          The query to associate.
    */
   @Override
   public void setQueryAsHandle(QueryWrapper qry)
   {
      handle.invalidAttribute("QUERY", true, LegacyResource.BUFFER);
   }
   
   /**
    * Gets the maximum number of {@code DataSet} temp-table rows to retrieve in each {@code FILL}
    * operation. The default value is zero (which retrieves all rows that satisfy the associated
    * query).
    *
    * @return  the maximum number of DataSet temp-table rows to retrieve in each {@code FILL}
    *          operation.
    */
   @Override
   public integer batchSize()
   {
      return new integer(batchSize); 
   }
   
   /**
    * Sets the maximum number of {@code DataSet} temp-table rows to retrieve in each {@code FILL}
    * operation.
    *
    * @param   size
    *          The new value for the {@code BATCH-SIZE} attribute.
    */
   @Override
   public void batchSize(long size)
   {
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(12361);
         // BATCH-SIZE may only be set for dataset temp-tables.
         return;
      }
      batchSize = (int) size;
   }
   
   /**
    * Sets the maximum number of {@code DataSet} temp-table rows to retrieve in each {@code FILL}
    * operation.
    *
    * @param   size
    *          The new value for the {@code BATCH-SIZE} attribute.
    */
   @Override
   public void batchSize(NumberType size)
   {
      if (size == null || size.isUnknown())
      {
         unableToAssignUnknown("BATCH-SIZE", "BUFFER widget");
         return;
      }
      
      batchSize(size.longValue());
   }
   
   /**
    * Gets the {@code ROWID} of the row in the after-image table that is the current version of
    * the row in the before-image table currently associated with this buffer. This row can
    * be a new or modified row.
    *
    * @return  the {@code ROWID} of the row in the after-image table that is the current version
    *          of the row in the before-image table
    */
   @Override
   public rowid afterRowid()
   {
      RecordBuffer recBuf = buffer();
      if (isAfterBuffer() || !recBuf.isAvailable())
      {
         // AFTER-ROWID is only available for BEFORE-BUFFERS of DATASETs
         return new rowid();
      }
      
      return recBuf.peerRowid();
   }
   
   /**
    * Gets the {@code ROWID} of the row in the before-image table that is the current version of
    * the row in the before-image table currently associated with this buffer. This row can
    * be a new or modified row.
    *
    * @return  the {@code ROWID} of the row in the before-image table that is the current version
    *          of the row in the before-image table
    */
   @Override
   public rowid beforeRowid()
   {
      RecordBuffer recBuf = buffer();
      if (isBeforeBuffer() || !recBuf.isAvailable())
      {
         // BEFORE-ROWID is only available for AFTER-BUFFERS of DATASETs
         return new rowid();
      }
      
      return recBuf.peerRowid();
   }
   
   /**
    * Obtain the current change state of the {@code DataSet} temp-table row associated with this
    * buffer object.
    *
    * @return  the current change state of the {@code DataSet} temp-table row associated with this
    *          buffer object.
    */
   @Override
   public integer rowState()
   {
      RecordBuffer recBuf = buffer();
      if (!recBuf.isAvailable() || !recBuf.isTemporary() || beforeBufferType == BeforeType.SIMPLE)
      {
         return new integer();
      }
      
      // even if there is no dataset for this table, the rowState can be read.
      return recBuf.rowState();
   }
   
   /**
    * Accepts changes to the data in one row of this {@code DataSet} temp-table.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical acceptRowChanges()
   {
      if (!isBeforeBuffer())
      {
         ErrorManager.recordOrShowError(11933, doGetName());
         // ACCEPT-ROW-CHANGES must be run on the BEFORE table of buffer <name>.
         return logical.FALSE;
      }
      
      if (!_available())
      {
         ErrorManager.recordOrShowError(11934, doGetName());
         // ACCEPT-ROW-CHANGES buffer <name> not available.
         return logical.FALSE;
      }
      
      int crtState = buffer().rowState().intValue();
      if (crtState == Buffer.ROW_MODIFIED || crtState == Buffer.ROW_CREATED)
      {
         // find and update the after-image/record
         BufferImpl after = afterBufferNative();
         after.findByRowID(this.afterRowid());
         RecordBuffer afterRecBuf = after.buffer();
         afterRecBuf.peerRowid(new rowid());
         afterRecBuf.rowState(Buffer.ROW_UNMODIFIED);
      }
      // otherwise (DELETE - after image is already deleted)
      
      // then drop the before-image data:
      setUpBeforeBuffer(true);
      deleteRecord();
      setUpBeforeBuffer(false);
      
      return logical.TRUE; 
   }
   
   /**
    * Rejects changes to the data in one row of this {@code DataSet} temp-table.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical rejectRowChanges()
   {
      if (!isBeforeBuffer())
      {
         ErrorManager.recordOrShowError(11939, doGetName());
         // REJECT-ROW-CHANGES must be run on the BEFORE table of buffer <name>.
         return logical.FALSE;
      }
      
      if (!_available())
      {
         ErrorManager.recordOrShowError(11936, doGetName());
         // REJECT-ROW-CHANGES buffer <name> not available.
         return logical.FALSE;
      }
      
      // find and update the after-image/record
      BufferImpl after = afterBufferNative();
      TemporaryBuffer afterImage = (TemporaryBuffer) after.buffer();
      RecordBuffer recBuf = buffer();
      switch (recBuf.rowState().toJavaIntegerType())
      {
         case Buffer.ROW_CREATED:
            after.findByRowID(this.afterRowid());
            after.deleteRecord();
            break;
         case Buffer.ROW_DELETED:
            after.create();
            BaseRecord.copy(afterImage.getCurrentRecord(), recBuf.getCurrentRecord(), false);
            after.release();
            break;
         case Buffer.ROW_MODIFIED:
            after.findByRowID(this.afterRowid());
            BaseRecord.copy(afterImage.getCurrentRecord(), recBuf.getCurrentRecord(), false);
            afterImage.peerRowid(new rowid());
            afterImage.rowState(Buffer.ROW_UNMODIFIED);
            after.release();
            break;
      }
      
      // then drop the before-image data:
      setUpBeforeBuffer(true);
      deleteRecord();
      setUpBeforeBuffer(false);
   
      return logical.TRUE;
   }
   
   /**
    * Saves changes from one row of the {@code DataSet} temp-table to the associated data
    * source.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical saveRowChanges()
   {
      return saveRowChangesImpl(0, null, false);
   }
   
   /**
    * Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
    *
    * @param   buffIndex
    *          The index of the buffer in the data source.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical saveRowChanges(NumberType buffIndex)
   {
      int idx = buffIndex.isUnknown() ? 1 : buffIndex.intValue();
      return saveRowChangesImpl(idx - 1, null, false);
   }
   
   /**
    * Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
    *
    * @param   buffName
    *          The name of the buffer in the data source.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical saveRowChanges(Text buffName)
   {
      return saveRowChangesImpl(buffName.toJavaType(), null, false);
   }
   
   /**
    * Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
    *
    * @param   buffIndex
    *          The index of the buffer in the data source.
    * @param   skipList
    *          A list of field names that should not be assigned after a new row is created
    *          (comma-separated).
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical saveRowChanges(NumberType buffIndex, Text skipList)
   {
      int idx = buffIndex.isUnknown() ? 1 : buffIndex.intValue();
      return saveRowChangesImpl(idx - 1, skipList.toJavaType(), false);
   }
   
   /**
    * Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
    *
    * @param   buffName
    *          The name of the buffer in the data source.
    * @param   skipList
    *          A list of field names that should not be assigned after a new row is created
    *          (comma-separated).
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical saveRowChanges(Text buffName, Text skipList)
   {
      return saveRowChangesImpl(buffName.toJavaType(), skipList.toJavaType(), false);
   }
   
   /**
    * Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
    * <p>
    * <strong>Note:</strong> this form is used when unknown literal {@code ?} is used to specify
    *         the buffer. In either case, numeric or character, the meaning is the same.
    *
    * @param   buffInfo
    *          Unknown value. 
    * @param   skipList
    *          A list of field names that should not be assigned after a new row is created
    *          (comma-separated).
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical saveRowChanges(unknown buffInfo, Text skipList)
   {
      return saveRowChangesImpl(0, skipList.toJavaType(), false);
   }
   
   /**
    * Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
    *
    * @param   buffIndex
    *          The index of the buffer in the data source.
    * @param   skipList
    *          A list of field names that should not be assigned after a new row is created
    *          (comma-separated).
    * @param   noLobs
    *          Use {@code true} to specify the BLOB and CLOB fields should be ignored in the
    *          save operation.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical saveRowChanges(NumberType buffIndex, Text skipList, logical noLobs)
   {
      boolean skipLobs = !noLobs.isUnknown() && noLobs.getValue();
      int idx = buffIndex.isUnknown() ? 1 : buffIndex.intValue();
      return saveRowChangesImpl(idx - 1, skipList.toJavaType(), skipLobs);
   }
   
   /**
    * Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
    * <p>
    * <strong>Note:</strong> this form is used when unknown literal {@code ?} is used to specify
    *         the buffer. In either case, numeric or character, the meaning is the same.
    *
    * @param   buffInfo
    *          Unknown value. 
    * @param   skipList
    *          A list of field names that should not be assigned after a new row is created
    *          (comma-separated).
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical saveRowChanges(unknown buffInfo, Text skipList, logical noLobs)
   {
      boolean skipLobs = !noLobs.isUnknown() && noLobs.getValue();
      return saveRowChangesImpl(0, skipList.toJavaType(), skipLobs);
   }
   
   /**
    * Saves changes from one row of the {@code DataSet} temp-table to the associated data source.
    *
    * @param   buffName
    *          The name of the buffer in the data source.
    * @param   skipList
    *          A list of field names that should not be assigned after a new row is created
    *          (comma-separated).
    * @param   noLobs
    *          Use {@code true} to specify the BLOB and CLOB fields should be ignored in the
    *          save operation.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical saveRowChanges(Text buffName, Text skipList, logical noLobs)
   {
      boolean skipLobs = !noLobs.isUnknown() && noLobs.getValue();
      return saveRowChangesImpl(buffName.toJavaType(), skipList.toJavaType(), skipLobs);
   }
   
   /**
    * Obtain the mode in which the {@link DataSet#fill()} method fills a {@code DataSet} member
    * buffer. The default mode is {@code MERGE}.
    *
    * @return  the fill mode for this buffer.
    */
   @Override
   public character fillMode()
   {
      if (dataSet == null)
      {
         // FILL-MODE is unknown for 'free' buffers
         return new character();
      }
      
      return new character(fillMode); 
   }
   
   /**
    * Configures the mode in which the {@link DataSet#fill()} method fills a {@code DataSet}
    * member buffer. The allowed modes are: {@code APPEND}, {@code EMPTY}, {@code MERGE},
    * {@code NO-FILL}, and {@code REPLACE}.
    *
    * @param   mode
    *          The new fill mode for this buffer.
    */
   @Override
   public void fillMode(String mode)
   {
      if (mode == null)
      {
         unableToAssignUnknown("FILL-MODE", "BUFFER widget");
         return;
      }
      
      if (dataSet == null)
      {
         ErrorManager.recordOrShowError(11959);
         // FILL-MODE is only valid for a dataset buffer.
         ErrorManager.recordOrShowError(4058, "FILL-MODE", "BUFFER widget", mode);
         // **Attribute <attribute> for the <widget id> has an invalid value of <string>.
         return;
      }
      
      String upperMode = mode.toUpperCase();
      switch (upperMode)
      {
         case FM_APPEND:
         case FM_EMPTY:
         case FM_MERGE:
         case FM_NOFILL:
         case FM_REPLACE:
            // one of these is expected
            break;
         default:
            // but this is not. Complain about it and use default, MERGE
            ErrorManager.recordOrShowError(11960);
            // FILL-MODE must be one of NO-FILL, MERGE, EMPTY, etc. 
            ErrorManager.recordOrShowError(4058, "FILL-MODE", "BUFFER widget", mode);
            // **Attribute <attribute> for the <widget id> has an invalid value of <string>.
            upperMode = FM_MERGE;
      }
      
      this.fillMode = upperMode;
   }
   
   /**
    * Configures the mode in which the {@link DataSet#fill()} method fills a {@code DataSet}
    * member buffer. The allowed modes are: {@code APPEND}, {@code EMPTY}, {@code MERGE},
    * {@code NO-FILL}, and {@code REPLACE}.
    *
    * @param   mode
    *          the new fill mode for this buffer.
    */
   @Override
   public void fillMode(Text mode)
   {
      fillMode(mode == null ? null : mode.toJavaType());
   }
   
   /**
    * Attaches a {@code DataSource} object to this temp-table {@code Buffer} in a {@code DataSet}
    * object.
    *
    * @param   hds
    *          The handle to the {@code DataSource} to be attached.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical attachDataSource(handle hds)
   {
      return attachDataSource(hds, null, null, null);
   }
   
   /**
    * Attaches a {@code DataSource} object to this temp-table {@code Buffer} in a {@code DataSet}
    * object.
    *
    * @param   hds
    *          The handle to the {@code DataSource} to be attached.
    * @param   pairList
    *          A comma-delimited list of field name pairs that specify a mapping between different
    *          field names in the {@code DataSource} buffer and the {@code DataSet} temp-table
    *          buffer.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical attachDataSource(handle hds, character pairList)
   {
      return attachDataSource(hds, pairList, null, null);
   }
   
   /**
    * Attaches a {@code DataSource} object to this temp-table {@code Buffer} in a {@code DataSet}
    * object.
    *
    * @param   hds
    *          The handle to the {@code DataSource} to be attached.
    * @param   pairList
    *          A comma-delimited list of field name pairs that specify a mapping between different
    *          field names in the {@code DataSource} buffer and the {@code DataSset} temp-table
    *          buffer.
    * @param   exceptFields
    *          A comma-separated list of fields in the {@code DataSet} object buffer that will not
    *          be populated with data from the data source.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical attachDataSource(handle hds, character pairList, character exceptFields)
   {
      return attachDataSource(hds, pairList, exceptFields, null);
   }
   
   /**
    * Attaches a {@code DataSource} object to this temp-table {@code Buffer} in a {@code DataSet}
    * object. This method provides much of the internal definition for the {@code FILL}. Among
    * other things, this method creates the {@code FILL} query.
    * <p>
    * This Java method corresponds to ABL {@code ATTACH-DATA-SOURCE} method with all 4 parameters.
    *
    * @param   hds
    *          The handle to the {@code DataSource} to be attached.
    * @param   pairList
    *          A comma-delimited list of field name pairs that specify a mapping between different
    *          field names in the {@code DataSource} buffer and the {@code DataSet} temp-table buffer.
    * @param   exceptFields
    *          A comma-separated list of fields in the {@code DataSet} object buffer that will not
    *          be populated with data from the data source.
    * @param   includeFields
    *          A comma-separated list of fields to include in the {@code DataSet} object buffer,
    *          as an ALTERNATIVE to specifying fields to exclude in except-fields.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical attachDataSource(handle hds,
                                   character pairList,
                                   character exceptFields,
                                   character includeFields)
   {
      // validate parameters
      if (hds == null || !hds._isValid() || !hds.isType(LegacyResource.DATA_SOURCE))
      {
         ErrorManager.recordOrShowError(11957);
         // ATTACH-DATA-SOURCE must have a valid data-source handle as the first argument.
         return logical.FALSE;
      }
      
      DataSource newDSrc = (DataSource) hds.getResource();
      if (newDSrc._getNumSourceBuffers() == 0)
      {
         ErrorManager.recordOrShowError(11958);
         // ATTACH-DATA-SOURCE may not be called until source buffers have been assigned to the DATA-SOURCE.
         return logical.FALSE;
      }
      
      if (exceptFields != null && !exceptFields.isUnknown() && !exceptFields.getValue().isEmpty() &&
          includeFields != null && !includeFields.isUnknown() && !includeFields.getValue().isEmpty())
      {
         ErrorManager.recordOrShowError(12779);
         // Cannot have both exclude list and include list.
         return logical.FALSE;
      }
      
      Set<String> incFields = null;
      Set<String> excFields = null;
      if (exceptFields != null && !exceptFields.isUnknown() && !exceptFields.getValue().isEmpty())
      {
         excFields = new CaseInsensitiveHashSet<>();
         // note: TAB, CR, LF are treated like normal character, only SPACE and , are separators
         StringTokenizer st = new StringTokenizer(exceptFields.toJavaType(), " ,", false);
         while (st.hasMoreTokens())
         {
            String tok = st.nextToken();
            int i = tok.indexOf('.');
            if (i != -1)
            {
               // .f1 is OK, <not-a-table>.f1 is also OK, f1 is not
               excFields.add(tok.substring(i + 1));
            }
            else
            {
               excFields.add(tok);
            }
         }
      }
      else if (includeFields != null && !includeFields.isUnknown() && !includeFields.getValue().isEmpty())
      {
         incFields = new CaseInsensitiveHashSet<>();
         // note: TAB, CR, LF are treated like normal character, only SPACE and , are separators
         StringTokenizer st = new StringTokenizer(includeFields.toJavaType(), " ,", false);
         while (st.hasMoreTokens())
         {
            String tok = st.nextToken();
            int i = tok.indexOf('.');
            if (i != -1)
            {
               // .f1 is OK, <not-a-table>.f1 is also OK, f1 is not
               incFields.add(tok.substring(i + 1));
            }
            else
            {
               incFields.add(tok);
            }
         }
      }
      
      // silently return true if same data source is assigned
      if (newDSrc == dataSource)
      {
         return logical.TRUE;
      }
      
      // this must be a DataSet buffer
      if (dataSet == null)
      {
         ErrorManager.recordOrShowError(11952);
         // ATTACH-DATA-SOURCE is only valid for buffers that are members of a dataset.
         return logical.FALSE;
      }
      
      dstFieldMeta = new CaseInsensitiveHashMap<>();
      // pre-compute field indexes into the record data array
      DmoMeta dstMeta = buffer().getDmoInfo();
      RecordMeta recordMeta = dstMeta.getRecordMeta();
      PropertyMeta[] props = recordMeta.getPropertyMeta(false);
      for (int i = 0, propsLength = props.length; i < propsLength; i++)
      {
         PropertyMeta pmeta = props[i];
         dstFieldMeta.put(pmeta.getDmoProperty().legacyLower, pmeta);
      }
      
      // analyze field mapping between this buffer and those from new DataSource
      String pairListStr = null;
      ArrayList<TableMapper.LegacyFieldInfo> myFields = TableMapper.getLegacyOrderedList(this, true);
      
      // The algorithm for creating the full list of pairs of fields takes into consideration the method
      // parameters (the explicit pair list, except and include list of fields) but also analyses the list
      // of fields in both current and data-source buffers for matching names. It looks something like this:
      //    * analyse/validate the provided list and mark those fields;
      //          - if a field name is found in multiple source buffers mark all of them;
      //    * for each source buffer
      //          - iterate all destination fields in the order of definition
      //                # if the pair is in attach list, use the pair
      //                # if fields have different name, skip the pair
      //                # if one of the fields is marked, skip the pair
      //                # otherwise (same name, not marked), use the pair.
      //
      // This algorithm will cause some interesting results. For example:
      //    * if attach-list is empty/unknown and each of N source and destination buffers have a commonly
      //          named field (f1), there will be N pairs (the destination field f1 is overwritten N time);
      //    * in the above scenario, if one of the source fields with same name is paired with a different
      //          field from destination (f2), all of the above pairs will be dropped. The field f1 remains
      //          unaffected, even if there are N-1 fields to pair from source buffers.
      int numSourceBuffers = newDSrc._getNumSourceBuffers();
      BufferImpl[] buffers = new BufferImpl[numSourceBuffers];
      DmoMeta[] metaData = new DmoMeta[numSourceBuffers];
      List<Pair<String, String>>[] orderedPairlist = new ArrayList[numSourceBuffers];
      List<Pair<String, DataSource.FieldReference>>[] mappedNamesMap = new ArrayList[numSourceBuffers];
      for (int k = 1; k <= numSourceBuffers; k++)
      {
         // fewer calls to buffer / getSourceBufferNative / getDmoInfo are better, that is why we precompute
         BufferImpl buffer = newDSrc.getSourceBufferNative(k);
         buffers[k - 1] = buffer;
         metaData[k - 1] = buffer.buffer().getDmoInfo();
         orderedPairlist[k - 1] = new ArrayList<>();
      }
      
      Set<String> marked = new HashSet<>(); // the marked fields are not matched automatically
      if (pairList != null && !pairList.isUnknown())
      {
         // validate the list of field pairs
         pairListStr = pairList.getValue();
         String[] splits = pairListStr.isEmpty() ? new String[0] : pairListStr.split(",");
         if (splits.length % 2 == 1)
         {
            ErrorManager.recordOrShowError(9035);
            // Pairs list had too many pairs or odd number of entries.
            return logical.FALSE;
         }
         
         String key = null;
         for (String field : splits)
         {
            final String origField = field;
            if (StringHelper.startsWithIgnoreCase(field, "rowid("))
            {
               field = field.substring(field.indexOf('(') + 1, field.indexOf(')')) + "." + Session.PK;
            }
            else
            {
               int k = field.indexOf('.');
               if (k != -1)
               {
                  marked.add(field.substring(k + 1).toLowerCase());
               }
               else
               {
                  marked.add(field.toLowerCase());
               }
            }
            
            if (key == null)
            {
               key = field;
            }
            else
            {
               String keyNoEx = key.indexOf('[') == -1 ? key : key.substring(0, key.indexOf('['));
               String valNoEx = field.indexOf('[') == -1 ? field : field.substring(0, field.indexOf('['));
               
               if (!hasFieldEx(keyNoEx))
               {
                  if (!hasFieldEx(valNoEx))
                  {
                     ErrorManager.recordOrShowError(9034, key);
                     // Pair with <member> failed to have a member in each table.
                     return logical.FALSE;
                  }
                  
                  // swap [key] and [field] so that [key] is in destination (current) buffer
                  String tmp = key;
                  key = field;
                  field = tmp;
                  tmp = keyNoEx;
                  keyNoEx = valNoEx;
                  valNoEx = tmp;
               }
               
               // lookup the buffer for [field] / [valNoEx]
               int srcBufferIndex = 0;
               for (int k = 1; k <= numSourceBuffers; k++)
               {
                  BufferImpl buffer = newDSrc.getSourceBufferNative(k);
                  if (buffer.hasFieldEx(valNoEx))
                  {
                     if (field.indexOf('.') == -1)
                     {
                        field = buffer.doGetName() + "." + field;
                     }
                     srcBufferIndex = k;
                     break;
                  }
               }
               if (srcBufferIndex == 0)
               {
                  // the [fld2] could not be found in any of the buffers of DataSource
                  ErrorManager.recordOrShowError(9034, origField);
                  // Pair with <member> failed to have a member in each table.
                  return logical.FALSE;
               }
               
               if (key.indexOf('.') == -1)
               {
                  key = this.doGetName() + "." + key;
               }
               orderedPairlist[srcBufferIndex - 1].add(Pair.of(key, field));
               key = null;
            }
         }
      }
      
      for (int k = 0; k < numSourceBuffers; k++)
      {
         DmoMeta bufMeta = metaData[k];
         mappedNamesMap[k] = new ArrayList<>();
         for (TableMapper.LegacyFieldInfo myField : myFields)
         {
            String fieldName = myField.getLegacyName();
            if (marked.contains(myField.getLcLegacyName()))
            {
               continue;
            }
            
            if (bufMeta.byLegacyName(fieldName) != null)
            {
               if (incFields != null && !incFields.contains(fieldName) ||
                   excFields != null && excFields.contains(fieldName))
               {
                  continue; // drop it!
               }
               
               Property prop2 = bufMeta.byLegacyName(fieldName);
               Class<? extends BaseDataType> ftype2 = (prop2 == null) ? null : (Class) prop2._fwdType;
               Integer offset2 = null;
               if (prop2 != null)
               {
                  offset2 = bufMeta.getRecordMeta().getIndexOfProperty(prop2.name);
               }
               // TODO: pass the Property to FieldReference ctor? 
               mappedNamesMap[k].add(
                     Pair.of(fieldName,
                             new DataSource.FieldReference(buffers[k], fieldName, ftype2, offset2)));
            }
         }
      }
      
      for (int k = 0; k < mappedNamesMap.length; k++)
      {
         BufferImpl buffer = buffers[k];
         DmoMeta bufMeta = metaData[k];
         //List<Pair<String, DataSource.FieldReference>> pairs = mappedNamesMap[k];
         for (Pair<String, String> pair : orderedPairlist[k])
         {
            // normalize [fld1] a nd [fld2]
            String fld1 = pair.getKey();
            int j = fld1.indexOf('.');
            if (j > 0)
            {
               fld1 = fld1.substring(j + 1);
            }
            
            String fld2 = pair.getValue();
            j = fld2.indexOf('.');
            if (j > 0)
            {
               fld2 = fld2.substring(j + 1);
            }
            Property prop2 = bufMeta.byLegacyName(fld2);
            Class<? extends BaseDataType> ftype2 = (prop2 == null) ? null : (Class) prop2._fwdType;
            Integer offset2 = (prop2 == null) ? null : bufMeta.getRecordMeta().getIndexOfProperty(prop2.name);
            // TODO: pass the Property to FieldReference ctor? 
            mappedNamesMap[k].add(Pair.of(fld1.toLowerCase(),
                                          new DataSource.FieldReference(buffer, fld2, ftype2, offset2)));
         }
      }
      
      // cache the DATA-SOURCE-COMPLETE-MAP value
      dataSrcMapping = new ArrayList<>();
      StringBuilder sb = new StringBuilder();
      String myName = (name == null) ? "?" : name;
      
      for (List<Pair<String, DataSource.FieldReference>> pairs : mappedNamesMap)
      {
         for (Pair<String, DataSource.FieldReference> pair : pairs)
         {
            String ciName = pair.getKey();
            DataSource.FieldReference dataSrcField = pair.getValue();
            dataSrcMapping.add(Pair.of(ciName, dataSrcField));
            if (sb.length() != 0)
            {
               sb.append(',');
            }
            
            sb.append(myName).append('.').append(ciName).append(',');
            if (Session.PK.equals(dataSrcField.getName()))
            {
               sb.append("rowid(").append(dataSrcField.getBuffer()._name()).append(")");
            }
            else
            {
               sb.append(dataSrcField.getBuffer()._name()).append('.').append(dataSrcField.getName());
               
               if (dataSrcField.getIndex() > 0)
               {
                  sb.append("[").append(dataSrcField.getIndex()).append("]");
               }
            }
         }
      }
      dataSrcMappingStr = sb.toString()/*.toLowerCase()*/;
      
      // all fine, create the links between objects:
      this.dataSource = newDSrc;
      if (isAfterBuffer())
      {
         this.peerBuffer.dataSource = newDSrc;
      }
      this.attachedPairlist = pairListStr;
      dataSource.attach(this, dataSrcMapping);
      
      if (dataSource.hasExplicitQuery())
      {
         QueryWrapper query = (QueryWrapper) dataSource.getQueryAsHandle().getResource();
         query.setSubstitutionBuffers(this);
      }
      
      return logical.TRUE;
   }
   
   /**
    * Detaches the {@code DataSource} from the temp-table buffer in a {@code DataSet} object.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical detachDataSource()
   {
      detachDataSourceImpl();
      
      return logical.TRUE;
   }
   
   /**
    * Get the handle to the {@code DataSource} object currently attached to the {@code DataSet}
    * object buffer.
    *
    * @return  The value of {@code DATA-SOURCE} as described above.
    */
   @Override
   public handle dataSource()
   {
      return new handle(dataSource);
   }
   
   /**
    * Get the handle to the {@code DataSource} object currently attached to the {@code DataSet}
    * object buffer.
    *
    * @return  The value of {@code DATA-SOURCE} as described above.
    */
   public DataSource dataSourceNative()
   {
      return dataSource;
   }
   
   /**
    * Obtain the handle to the default buffer of the after-image table that corresponds to
    * the buffer of the before-image table currently associated with this buffer handle.
    *
    * @return  The value of {@code AFTER-BUFFER} attribute as described above.
    */
   @Override
   public handle afterBuffer()
   {
      return new handle(afterBufferNative());
   }

   /**
    * Obtain the default buffer of the after-image table that corresponds to
    * the buffer of the before-image table currently associated with this buffer.
    *
    * @return  The value of {@code AFTER-BUFFER} attribute as described above.
    */
   public BufferImpl afterBufferNative()
   {
      if (!isBeforeBuffer())
      {
         // permanent and simple temp-tables do not have BEFORE/AFTER buffers
         return null;
      }

      return peerBuffer;
   }

   /**
    * Obtain the {@code rowid} of the row in the original before-image table that corresponds to
    * the row in the change table currently associated with this buffer handle.
    * <p>
    * Implementation note: 
    *    FWD uses as a storage backing for both {@code ORIGIN-ROWID} and {@code DATA-SOURCE-ROWID}
    *    the same field ({@link TempRecord#_originRowid}) exclusively.
    *    <ul>
    *       <li>when the record is a normal dataset temp-table record the meaning is 
    *            {@code DATA-SOURCE-ROWID}, for both BEFORE and AFTER image. In the case the
    *            {@code DataSource} must be also attached or an ABL error is raised. </li> 
    *       <li>when we use it as a CHANGE dataset temp-table, the meaning of the field is
    *             {@code ORIGIN-ROWID}.</li>
    *   </ul>
    *
    * @return  The value of {@code ORIGIN-ROWID} attribute as described above.
    * 
    * @see     #dataSourceRowid() 
    */
   @Override
   public rowid originRowid()
   {
      RecordBuffer recBuf = buffer();
      
      if (!recBuf.isTemporary() || !recBuf.isAvailable())
      {
         return new rowid();
      }
      
      if (!tableHandleNative().getOriginHandle()._isValid())
      {
         return new rowid();
      }
      
      if (!recBuf.isTemporary() || !recBuf.isAvailable())
      {
         return new rowid();
      }
      
      TempRecord record = (TempRecord) recBuf.getCurrentRecord();
      return new rowid(record._originRowid());
   }
   
   /**
    * Obtain the handle to the default buffer of the before-image table that corresponds to
    * the buffer of the after-image table currently associated with this buffer handle.
    *
    * @return  The value of {@code BEFORE-BUFFER} attribute as described above.
    */
   @Override
   public handle beforeBuffer()
   {
      return new handle(beforeBufferNative());
   }

   /**
    * Obtain the default buffer of the before-image table that corresponds to
    * the buffer of the after-image table currently associated with this buffer.
    *
    * @return  The value of {@code BEFORE-BUFFER} attribute as described above.
    */
   public BufferImpl beforeBufferNative()
   {
      if (!isAfterBuffer())
      {
         // permanent and simple temp-tables do not have BEFORE/AFTER buffers
         return null;
      }

      return peerBuffer;
   }

   /**
    * The list of field name pairs for all fields  (in comm-delimited format) in a {@code DataSet}
    * temp-table buffer that are mapped to corresponding fields in an attached data-source object.
    *
    * @return  The value of {@code DATA-SOURCE-COMPLETE-MAP} attribute.
    */
   @Override
   public character dataSourceCompleteMap()
   {
      if (dataSet == null)
      {
         ErrorManager.recordOrShowError(12780, false);
         // DATA-SOURCE-COMPLETE-MAP only legal for dataset tables.
         return new character();
      }
      
      if (dataSource == null)
      {
         ErrorManager.recordOrShowError(12781, false);
         // DATA-SOURCE-COMPLETE-MAP requires prior ATTACH-DATA-SOURCE method to be run.
         return new character();
      }
      
      return new character(dataSrcMappingStr);
   }
   
   /**
    * Obtains the value of {@code DATA-SOURCE-ROWID} attribute (the {@code DataSource} row corresponding to
    * this buffer object).
    *
    * @return  The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
    */
   @Override
   public rowid dataSourceRowid()
   {
      if (dataSource == null || !dataSource.valid())
      {
         ErrorManager.recordOrShowError(14276, doGetName());
         // Dataset record for <name> required for DATA-SOURCE-ROWID
         return new rowid();
      }
      
      if (dataSet == null || !dataSet.valid())
      {
         if (!isBeforeBuffer() || peerBuffer.dataSet == null || !peerBuffer.dataSet.valid())
         {
            ErrorManager.recordOrShowError(14275, false, doGetName());
            // Dataset record for <name> required for DATA-SOURCE-ROWID
            return new rowid();
         }
      }
      
      RecordBuffer buffer = buffer();
      if (!buffer.isTemporary() || !buffer.isAvailable())
      {
         return new rowid();
      }
      
      TempRecord record = (TempRecord) buffer.getCurrentRecord();
      Long bruteRow = record._datasourceRowid();
      if (bruteRow == null)
      {
         return new rowid();
      }
      
      // validate the [bruteRow]: it must belong to an existing record of only buffer of the current datasource
      BufferImpl srcBuf = dataSource.getSourceBufferNative();
      try
      {
         Record srcRec = buffer.getPersistence().getSession(Persistence.TEMP_CTX)
                               .get(srcBuf.buffer().getDMOImplementationClass(), bruteRow);
         if (srcRec == null)
         {
            return new rowid();
         }
      }
      catch (PersistenceException e)
      {
         return new rowid();
      }
      
      return new rowid(bruteRow);
   }
   
   /**
    * Obtains the {@code DataSource} row corresponding to this buffer object.
    *
    * @param   joinLevel
    *          The level of join, if the data-source is a join.
    *
    * @return  The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
    */
   @Override
   public rowid dataSourceRowid(long joinLevel)
   {
      return dataSourceRowid(new int64(joinLevel));
   }
   
   /**
    * Obtains the {@code DataSource} row corresponding to this buffer object.
    *
    * @param   buffName
    *          The name of one of the data-source buffers.
    *
    * @return  The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
    */
   @Override
   public rowid dataSourceRowid(String buffName)
   {
      return dataSourceRowid(new character(buffName));
   }
   
   /**
    * Obtains the {@code DataSource} row corresponding to this buffer object.
    *
    * @param   joinLevel
    *          The level of join, if the data-source is a join.
    *
    * @return  The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
    */
   @Override
   public rowid dataSourceRowid(NumberType joinLevel)
   {
      if (dataSource == null || !dataSource.valid())
      {
         ErrorManager.recordOrShowError(14276, doGetName());
         // Dataset record for <name> required for DATA-SOURCE-ROWID
         return new rowid();
      }
      
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(14275, false, doGetName());
         // Dataset record for <name> required for DATA-SOURCE-ROWID
         return new rowid();
      }
      
      RecordBuffer buffer = buffer();
      if (!buffer.isTemporary() || !buffer.isAvailable())
      {
         return new rowid();
      }
      
      if (joinLevel.isUnknown() ||
          joinLevel.intValue() == 1 && dataSource._getNumSourceBuffers() == 1)
      {
         return dataSourceRowid();
      }
      
      int jjl = joinLevel.intValue();
      if (jjl < 1 || jjl > dataSource._getNumSourceBuffers())
      {
         ErrorManager.recordOrShowError(14277, "", "");
         // Unable to identify DATA-SOURCE buffer <temp-table name> in DATA-SOURCE-ROWID. (14277)
         return new rowid();
      }
      
      BufferImpl buf = dataSource.getSourceBufferNative(jjl);
      String findDataSource = dataSource.getSaveWhereStringImpl(buf, false);
      if (findDataSource == null)
      {
         ErrorManager.recordOrShowError(14278, buf.doGetName(), "");
         // Unable to generate FIND for <source-name> in DATA-SOURCE-ROWID. (14278)
         return new rowid();
      }
      
      logical ok = new logical();
      ErrorManager.silent(() -> ok.assign(
            buf._find(findDataSource, FindMode.UNIQUE, LockType.EXCLUSIVE, BufferImpl.this)));
      
      if (ok.isUnknown() || !ok.getValue() || !buf._available())
      {
         return new rowid();
      }
      
      return buf.rowID();
   }
   
   /**
    * Obtains the {@code DataSource} row corresponding to this buffer object.
    *
    * @param   buffName
    *          The name of one of the data-source buffers.
    *
    * @return  The current value of {@code DATA-SOURCE-ROWID} ABL attribute.
    */
   @Override
   public rowid dataSourceRowid(character buffName)
   {
      if (dataSource == null || !dataSource.valid())
      {
         ErrorManager.recordOrShowError(14276, doGetName());
         // Dataset record for <name> required for DATA-SOURCE-ROWID
         return new rowid();
      }
      
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(14275, false, doGetName());
         // Dataset record for <name> required for DATA-SOURCE-ROWID
         return new rowid();
      }
      
      RecordBuffer buffer = buffer();
      if (!buffer.isTemporary() || !buffer.isAvailable())
      {
         return new rowid();
      }
      
      BufferImpl buf = null;
      if (!buffName.isUnknown())
      {
         String javaType = buffName.toJavaType();
         int n = dataSource._getNumSourceBuffers();
         for (int k = 1; k <= n; k++)
         {
            BufferImpl buff = dataSource.getSourceBufferNative(k);
            if (buff.doGetName().equalsIgnoreCase(javaType))
            {
               buf = buff;
               break;
            }
         }
      }
      
      if (buf == null)
      {
         ErrorManager.recordOrShowError(14277, "", "");
         // Unable to identify DATA-SOURCE buffer <temp-table name> in DATA-SOURCE-ROWID. (14277)
         return new rowid();
      }
      
      if (dataSource._getNumSourceBuffers() == 1)
      {
         return dataSourceRowid();
      }
      
      String findDataSource = dataSource.getSaveWhereStringImpl(buf, false);
      if (findDataSource == null)
      {
         ErrorManager.recordOrShowError(14278, buf.doGetName(), "");
         // Unable to generate FIND for <source-name> in DATA-SOURCE-ROWID. (14278)
         return new rowid();
      }
      
      logical ok = new logical();
      BufferImpl finalBuf = buf;
      ErrorManager.silent(() -> ok.assign(
            finalBuf._find(findDataSource, FindMode.UNIQUE, LockType.EXCLUSIVE, BufferImpl.this)));
      
      if (ok.isUnknown() || !ok.getValue() || !finalBuf._available())
      {
         return new rowid();
      }
      
      return finalBuf.rowID();
   }
   
   /**
    * Configures the {@code DataSource} row corresponding to this buffer object.
    *
    * @param   val
    *          The new value of {@code DATA-SOURCE-ROWID} ABL attribute.
    */
   @Override
   public void dataSourceRowid(rowid val)
   {
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(14275, false, doGetName());
         // Dataset record for <name> required for DATA-SOURCE-ROWID
         return;
      }
      
      RecordBuffer buffer = buffer();
      if (!buffer.isTemporary() || !buffer.isAvailable())
      {
         return;
      }
      
      if (isBeforeBuffer())
      {
         peerBuffer.dataSourceRowid(val);
         return;
      }
      
      setDatasourceRowidImpl(val.getValue());
      
      // TODO: set the _datasourceRowid on the peer record (AFTER / BEFORE)
   }
   
   /**
    * Configures the {@code DataSource} row corresponding to this buffer object.
    *
    * @param   joinLevel
    *          The level of join, if the data-source is a join.
    * @param   val
    *          The new value of {@code DATA-SOURCE-ROWID} ABL attribute.
    */
   @Override
   public void dataSourceRowid(int64 joinLevel, rowid val)
   {
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(14275, false, doGetName());
         // Dataset record for <name> required for DATA-SOURCE-ROWID
         return;
      }
      
      RecordBuffer buffer = buffer();
      if (!buffer.isTemporary() || !buffer.isAvailable())
      {
         return;
      }
      
      if (isBeforeBuffer())
      {
         peerBuffer.dataSourceRowid(joinLevel, val);
         return;
      }
      
      UnimplementedFeature.missing("BUFFER:DATA-SOURCE-ROWID(n)= not implemented");
   }
   
   /**
    * Configures the {@code DataSource} row corresponding to this buffer object.
    *
    * @param   buffName
    *          The name of one of the data-source buffers.
    * @param   val
    *          The new value of {@code DATA-SOURCE-ROWID} ABL attribute.
    */
   @Override
   public void dataSourceRowid(character buffName, rowid val)
   {
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(14275, false, doGetName());
         // Dataset record for <name> required for DATA-SOURCE-ROWID
         return;
      }
      
      RecordBuffer buffer = buffer();
      if (!buffer.isTemporary() || !buffer.isAvailable())
      {
         return;
      }
      
      if (isBeforeBuffer())
      {
         peerBuffer.dataSourceRowid(buffName, val);
         return;
      }
      
      UnimplementedFeature.missing("BUFFER:DATA-SOURCE-ROWID(name)= not implemented");
   }
   
   /**
    * Get the {@code DataSet} object of which the buffer is a member or {@code null} otherwise.
    *
    * @return  the {@code DataSet} object of which the buffer is a member.
    */
   public DataSet _dataSet()
   {
      return isBeforeBuffer() ? peerBuffer.ref().dataSet : dataSet;
   }
   
   /**
    * Get the handle for the {@code DataSet} object of which the buffer is a member.
    *
    * @return  handle for the {@code DataSet} object of which the buffer is a member.
    */
   @Override
   public handle dataSet()
   {
      return new handle(_dataSet());
   }
   
   /**
    * Counts the ACTIVE relations for which the buffer is the parent. A buffer may be a parent in
    * multiple relations, but a child in only one.
    *
    * @return  The value of {@code NUM-CHILD-RELATIONS} attribute as described above.
    */
   @Override
   public integer numChildRelations()
   {
      if (dataSet == null)
      {
         // quick out, this is no dataset buffer
         return new integer(0);
      }
      
      List<DataRelation> rels = dataSet.getRelations(this, true, false, true);
      return new integer(rels.size());
   }
   
   /**
    * Obtain the handle to the ACTIVE parent data-relation object for the parent of this buffer.
    * Since a buffer may be a child in only one ACTIVE relation, there can be maximum only one
    * parent for any buffer at any time. 
    *
    * @return  The value of {@code PARENT-RELATION} attribute as described above.
    */
   @Override
   public handle parentRelation()
   {
      return new handle(parentRelationNative());
   }
   
   /**
    * Obtain the handle to the ACTIVE parent data-relation object for the parent of this buffer.
    * Since a buffer may be a child in only one ACTIVE relation, there can be maximum only one
    * parent for any buffer at any time. 
    *
    * @return  The value of {@code PARENT-RELATION} attribute as described above.
    */
   public DataRelation parentRelationNative()
   {
      if (dataSet == null)
      {
         // quick out, this is no dataset buffer
         return null;
      }
      
      List<DataRelation> rels = dataSet.getRelations(this, false, true, true);
      if (rels.isEmpty())
      {
         return null;
      }
      
      for (DataRelation rel : rels)
      {
         if (rel._isActive())
         {
            return rel;
         }
      }
      
      return null;
   }
   
   /**
    * Creates a blank before-table record for each buffer record, with a {@code ROW-STATE} of
    * {@code ROW-CREATED} for a table with only new data.
    *
    * @return  {@code true} on success.
    */
   @Override
   public logical markNew()
   {
      if (!isAfterBuffer())
      {
         ErrorManager.recordOrShowError(14274);
         // MARK-ROW-STATE and MARK-NEW may only be used on a dataset member buffer with an existing BEFORE-TABLE.
         return logical.FALSE;
      }
      
      return logical.of(((TemporaryBuffer) buffer()).markNew(beforeBufferNative()));
   }
   
   /**
    * Manually set the {@code ROW-STATE} attribute for the current record/row.
    *
    * @param   newState
    *          The new state the record is marked with. Legal values: {@code ROW_CREATED},
    *          {@code ROW_MODIFIED}, {@code ROW_DELETED}, and {@code ROW_UNMODIFIED}.
    *
    * @return {@code true} on success.
    */
   @Override
   public logical markRowState(int64 newState)
   {
      int jState = (newState == null || newState.isUnknown()) ?
            Buffer.ROW_UNMODIFIED :
            newState.intValue();
      return markRowState(jState, null);
   }
   
   /**
    * Manually set the {@code ROW-STATE} attribute for the current record/row.
    *
    * @param   newState
    *          The new state the record is marked with. Legal values: {@code ROW_CREATED},
    *          {@code ROW_MODIFIED}, {@code ROW_DELETED}, and {@code ROW_UNMODIFIED}.
    *
    * @return {@code true} on success.
    */
   @Override
   public logical markRowState(long newState)
   {
      return markRowState(newState, null);
   }
   
   /**
    * Manually set the {@code ROW-STATE} attribute for the current record/row.
    *
    * @param   newState
    *          The new state the record is marked with. Legal values: {@code ROW_CREATED},
    *          {@code ROW_MODIFIED}, {@code ROW_DELETED}, and {@code ROW_UNMODIFIED}.
    * @param   before
    *          A handle to the before-image source buffer.
    *
    * @return {@code true} on success.
    */
   @Override
   public logical markRowState(int64 newState, handle before)
   {
      int jState = (newState == null || newState.isUnknown()) ?
            Buffer.ROW_UNMODIFIED :
            newState.intValue();
      return markRowState(jState, before);
   }
   
   /**
    * Manually set the {@code ROW-STATE} attribute for the current record/row.
    *
    * @param   newState
    *          The new state the record is marked with. Legal values: {@code ROW_CREATED},
    *          {@code ROW_MODIFIED}, {@code ROW_DELETED}, and {@code ROW_UNMODIFIED}.
    * @param   compareTo
    *          A handle to the before-image source buffer - used to compare to current data.
    *
    * @return {@code true} on success.
    */
   @Override
   public logical markRowState(long newState, handle compareTo)
   {
      if (!isAfterBuffer())
      {
         ErrorManager.recordOrShowError(14274);
         // MARK-ROW-STATE and MARK-NEW may only be used on a dataset member buffer with an existing BEFORE-TABLE.
         return logical.FALSE;
      }
      
      if (newState == Buffer.ROW_UNMODIFIED)
      {
         return logical.TRUE; // nothing to process
      }
      else if (newState != Buffer.ROW_CREATED  &&
               newState != Buffer.ROW_MODIFIED &&
               newState != Buffer.ROW_DELETED)
      {
         ErrorManager.recordOrShowError(14288);
         // Invalid ROW-STATE given to MARK-ROW-STATE.
         return logical.FALSE;
      }
      
      if (!_available())
      {
         ErrorManager.recordOrShowError(14275, false, doGetName());
         // Dataset record for <name> required for DATA-SOURCE-ROWID.
         return logical.FALSE;
      }
      
      RecordBuffer compRB = null;
      BufferImpl compBuffer = null;
      if (compareTo != null && !compareTo.isUnknown())
      {
         if (!compareTo.isType(LegacyResource.BUFFER))
         {
            ErrorManager.recordOrShowError(14289);
            // Invalid handle or handle type for MARK-ROW-STATE
            return logical.FALSE;
         }
         
         compBuffer = (BufferImpl) compareTo.getResource();
         if (!compBuffer._available())
         {
            ErrorManager.recordOrShowError(0, "MARK-ROW-STATE called with an empty BEFORE-BUFFER");
            // Progress OpenEdge crashes.
            return logical.FALSE;
         }
         if (compBuffer.buffer().isTemporary())
         {
            compRB = compBuffer.buffer();
         }
      }
      
      integer rowState = rowState();
      if (!rowState.isUnknown() && rowState.intValue() != Buffer.ROW_UNMODIFIED)
      {
         return logical.TRUE; // already has before image/record
      }
      
      BufferImpl b4Buffer = beforeBufferNative();
      RecordBuffer b4RecBuf = b4Buffer.buffer();
      RecordBuffer recBuf = buffer();
      b4Buffer.setUpBeforeBuffer(true);
      switch ((int) newState)
      {
         case ROW_CREATED:
            b4Buffer.create();
            recBuf.rowState((int) newState);
            recBuf.peerRowid(b4RecBuf.rowID());
            b4RecBuf.peerRowid(recBuf.rowID());
            b4RecBuf.rowState((int) newState);
            b4Buffer.release();
            break;
         case ROW_MODIFIED:
            b4Buffer.create();
            if (compRB != null)
            {
               RecordBuffer.copyCommonFields(b4RecBuf.getCurrentRecord(), compRB.getCurrentRecord());
            }
            else
            {
               RecordBuffer.copyCommonFields(b4RecBuf.getCurrentRecord(), recBuf.getCurrentRecord());
            }
            recBuf.rowState((int) newState);
            recBuf.peerRowid(b4RecBuf.rowID());
            b4RecBuf.peerRowid(recBuf.rowID());
            b4RecBuf.rowState((int) newState);
            b4Buffer.release();
            break;
         case ROW_DELETED:
            // this buffer is not updated
            b4Buffer.create();
            if (compRB != null)
            {
               RecordBuffer.copyCommonFields(b4RecBuf.getCurrentRecord(), compRB.getCurrentRecord());
            }
            else 
            {
               RecordBuffer.copyCommonFields(b4RecBuf.getCurrentRecord(), recBuf.getCurrentRecord());
            }
            b4RecBuf.peerRowid(new rowid());
            b4RecBuf.rowState((int) newState);
            b4Buffer.release();
            break;
      }
      b4Buffer.setUpBeforeBuffer(false);
      
      return logical.TRUE;
   }
   
   /**
    * Obtain the handle to a {@code DataRelation} object for which the buffer is the parent.
    *
    * @param   index
    *          The index of the data-relation object (1-based).
    *
    * @return  the handle to a {@code DataRelation} object for which the buffer is the parent.
    */
   @Override
   public handle childRelation(long index)
   {
      if (dataSet == null || index < 1)
      {
         // quick out, this is no dataset buffer
         return new handle();
      }
      
      List<DataRelation> rels = dataSet.getRelations(this, true, false, true);
      if (index > rels.size())
      {
         return new handle();
      }
      
      return new handle(rels.get((int) (index - 1)));
   }
   
   /**
    * Obtain the handle to a {@code DataRelation} object for which the buffer is the parent.
    *
    * @param   index
    *          The index of the data-relation object (1-based).
    *
    * @return  the handle to a {@code DataRelation} object for which the buffer is the parent.
    */
   @Override
   public handle childRelation(int64 index)
   {
      if (index == null || index.isUnknown())
      {
         return new handle();
      }
      
      return childRelation(index.longValue());
   }
   
   /**
    * Definition of the SYNCHRONIZE method.
    * 
    * @return   <code>true</code> if the synchronization was performed.
    */
   @Override
   public logical querySynchronize()
   {
      if (dataSet == null)
      {
         return logical.FALSE;
      }
      List<DataRelation> childRelations = dataSet.getRelations(this, true, false, true);
      boolean ok = true;
      for (DataRelation rel : childRelations)
      {
         BufferImpl childBuffer = rel.getChildBufferNative();
         if (!rel.isRecursive().booleanValue() || childBuffer != this)
         {
            synchronize(rel);
            ok &= childBuffer.querySynchronize().booleanValue();
         }
      }
      return logical.of(ok);
   }

   /**
    * Obtain a comma-separated list of field name pairs for fields in a {@code DataSet}
    * temp-table buffer that are mapped to corresponding fields in an attached {@code DataSource}
    * object. This list includes only the field name pairs you specified with the most recently
    * attached data-source object, in the order they were specified.
    * <p>
    * @return  the list of attach fields, as described above.
    */
   @Override
   public character attachedPairlist()
   {
      return new character(attachedPairlist);
   }
   
   /**
    * Returns the state of the REJECTED attribute
    *
    * @return Current state of the REJECTED attribute.
    */
   @Override
   public logical rejected()
   {
      RecordBuffer recBuf = buffer();
      if (!recBuf.isAvailable() || !recBuf.isTemporary())
      {
         return logical.FALSE;
      }
      
      DataSet ds = _dataSet();
      if (ds == null)
      {
         return logical.FALSE;
      }
      
      // NOTE: internally, the P4GL database the __error-flag__ is an integer storing multiple flags.
      //       We need to extract the right bit (ERROR_REJECTED) and convert it to a logical value.
      Integer flags = recBuf.errorFlags();
      return logical.of(flags != null && (flags & Buffer.ERROR_REJECTED) != 0);
   }
   
   /**
    * Setter for REJECTED attribute.
    *
    * @param value The new value of REJECTED attribute.
    */
   @Override
   public void rejected(boolean value)
   {
      RecordBuffer recBuf = buffer();
      
      if (!_available())
      {
         // temp-tables case
         ErrorManager.recordOrShowError(11916, doGetName());
         // <name> record is not available to set ERROR in.
         ErrorManager.recordOrShowError(3131, "REJECTED", "", LegacyResource.BUFFER);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
         return;
      }
      
      if (!recBuf.isTemporary())
      {
         // permanent table case
         ErrorManager.recordOrShowError(11917, doGetName());
         // Cannot set ERROR if there is no BEFORE-TABLE for <name>.
         ErrorManager.recordOrShowError(3131, "REJECTED", "", LegacyResource.BUFFER);
         // Unable to set attribute <attribute name> in widget <widget name> of type <widget-type>.
         return;
      }
      
      if (!isBeforeBuffer())
      {
         if (!isAfterBuffer() || // SIMPLE BUFFER
             (recBuf.peerRowid() == null || recBuf.peerRowid().isUnknown())) // no AFTER-IMAGE
         {
            ErrorManager.recordOrShowError(11918, doGetName(), "");
            // Cannot set ERROR if there is no BEFORE-TABLE row for <name>.
            return;
         }
      }
      
      // NOTE: internally, the P4GL database the __error-flag__ is an integer storing multiple flags.
      //       We need to update the right bit (ERROR_ERROR) to [value].
      recBuf.updateErrorFlags(Buffer.ERROR_REJECTED, value);
   }
   
   /**
    * Setter for REJECTED attribute.
    *
    * @param value The new value of REJECTED attribute.
    */
   @Override
   public void rejected(logical value)
   {
      if (value == null || value.isUnknown())
      {
         unableToAssignUnknown("REJECTED", "BUFFER widget");
         return;
      }
      
      rejected(value.getValue());
   }
   
   /**
    * Fills a {@code Buffer} object, recursively, based on its defined data sources.
    *
    * @return {@code true} if operation is successful.
    */
   @Override
   public logical fill()
   {
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(11945);
         // BUFFER:FILL is only valid for buffers that are members of a dataset.
         return logical.FALSE;
      }
      
      lastBatch = false;
      
      if (FM_NOFILL.equalsIgnoreCase(fillMode))
      {
         // nothing to do
         return logical.TRUE;
      }
      
      return new FillWorker().fill();
   }
   
   /**
    * Synchronize child query of the DATA-RELATION.
    *  
    * @param rel
    *        The DATA-RELATION in question
    */
   private boolean synchronize(DataRelation rel)
   {
      Buffer childBuffer = rel.getChildBufferNative();
      String[] relFields = rel.getRelationFields(false);
      String[] parentRelFields = rel.getRelationFields(true);
      String cname = childBuffer.name().toJavaType();
      String mname = this.name;
      StringBuilder sb = new StringBuilder("FOR EACH ").append(cname).append(" WHERE ");
      for (int nf = 0; nf < relFields.length; ++nf) {
         if (nf > 0)
         {
            sb.append(" and ");         
         }
         sb.append(cname).append(".").append(relFields[nf]).append(" = ").
            append(mname).append(".").append(parentRelFields[nf]);
      }
      handle hQuery = new handle(false);
      QueryWrapper.createQuery(hQuery);
      P2JQuery query = hQuery.unwrapQuery();
      query.addBuffer(childBuffer);
      query.prepare(sb.toString());
      query.hintFullResults();
      query.queryOpen();
      boolean rc = query.getFirst().booleanValue();
      query.queryClose();
      HandleOps.delete(hQuery);
      return rc;
   }

   /**
    * Assigns the value of {@code srcBuffer.df} to field {@code f} of this buffer. Takes care of the extents.
    * 
    * @param   myField
    *          The destination field of this buffer.
    * @param   df
    *          The source field.
    * @param   srcBuffer
    *          The source buffer.
    * @param   dstRecBuff
    *          The destination record buffer.
    * @param   fields
    *          A map of resolved {@link FieldStructure} for each destination field.
    */
   private void assign(String                      myField, 
                       DataSource.FieldReference   df, 
                       BufferImpl                  srcBuffer, 
                       RecordBuffer                dstRecBuff, 
                       Map<String, FieldStructure> fields)
   {
      if (!srcBuffer._available())
      {
         return;
      }
      
      FieldStructure field = fields.get(myField);
      
      if (field == null)
      {
         int dstIndex = 0;
         String crtField;
         int i = myField.indexOf('[');
         if (i != -1)
         {
            int myFieldSize = myField.length();
            char[] crtFieldChars = new char[i];
            boolean openParenthesis = false;
            for (int j = 0; j < myFieldSize; j++)
            {
               char current = myField.charAt(j);
               if (current == '[')
               {
                  openParenthesis = true;
               }
               else if (current == ']')
               {
                  break;
               }
               else if (openParenthesis == false)
               {
                  crtFieldChars[j] = current;
               }
               else if (openParenthesis == true)
               {
                  dstIndex = dstIndex * 10 + ((int) current - '0');
               }
            }
            
            crtField = new String(crtFieldChars);
         }
         else
         {
            crtField = myField;
         }
         
         DmoMeta dmoInfo = dstRecBuff.getDmoInfo();
         Property dstProp = dmoInfo.byLegacyName(crtField);
         
         if (dstProp == null)
         {
            return; // something is wrong... invalid [crtField] name. Raise OE condition?
         }
         
         field = new FieldStructure();
         field.dstProp = dstProp;
         field.crtField = crtField;
         field.dstIndex = dstIndex;
         fields.put(myField, field);
      }
      
      // TRACKING-CHANGES can't be active during FILL, so BEFORE-TABLE is not needed

      // only TEMP-TABLES can perform FILL, so no triggers can be invoked
      
      // this assign is wrapped in a start/endBatch bracket, so no validation/flush is done on individual field
      // assignment

      // TODO: extent
      
      PropertyMeta dstPropMeta = field.dstProp.getMeta();
      Integer dstFieldOffset = dstPropMeta == null ? null : dstPropMeta.getOffset();
      Integer srcFieldOffset = df.getOffset();
      RecordBuffer srcRecBuff = srcBuffer.buffer();
      boolean directAssign = dstPropMeta != null && 
                             df.getType() != null && 
                             df.getType() == dstPropMeta.getType();
      
      if (field.dstProp.extent == 0)
      {
         if (df.getIndex() == 0)
         {
            Object src = null;
            
            if (directAssign && srcFieldOffset != null)
            {
               src = srcRecBuff.getDirectField(srcFieldOffset);
            }
            else
            {
               src = srcBuffer.dereference(df.getName());
            }
            
            if (directAssign && dstFieldOffset != null)
            {
               dstRecBuff.setDirectField(src, dstFieldOffset);
            }
            else
            {
               dereference(field.crtField, src);
            }
         }
         else
         {
            dereference(field.crtField, srcBuffer.dereference(df.getIndex() /* -1? */ , df.getName()));
         }
      }
      else
      {
         if (field.dstIndex != 0)
         {
            if (df.getIndex() == 0)
            {
               dereference(field.dstIndex, field.crtField, srcBuffer.dereference(df.getName()));
            }
            else
            {
               dereference(field.dstIndex /* -1? */, 
                           field.crtField, 
                           srcBuffer.dereference(df.getIndex() /* -1? */ , df.getName()));
            }
         }
         else
         {
            // bulk copy: (assume) both source and destination are extent fields
            for (int k = 0; k < field.dstProp.extent; k++)
            {
               BufferField srcField = srcBuffer.bufferFieldNative(df.getName());
               BufferField destField = bufferFieldNative(field.crtField);
               
               destField.changeValue(k + 1, srcField.value(k + 1));
            }
         }
      }
   }
   
   /**
    * Applies a callback procedure, which allows execution of a defined event without duplicating
    * the event procedure definition.
    *
    * @param   eventName
    *          The event whose callback will be called.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical applyCallback(String eventName)
   {
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(11949);
         // APPLY-CALLBACK is only for buffers that are members of datasets.
         return logical.FALSE;
      }
      
      if (!isCallbackName(eventName))
      {
         return logical.FALSE;
      }
      
      return logical.of(invokeCallback(eventName.toUpperCase()));
   }
   
   /**
    * Retrieves the name of the internal procedure associated with the ABL callback for the
    * specified event.
    *
    * @param   eventName
    *          The name of the event.
    *
    * @return the name of the internal procedure associated with specified event.
    */
   @Override
   public character getCallbackProcName(String eventName)
   {
      if (!isCallbackName(eventName))
      {
         return new character();
      }
      
      CallbackData cbd = callbacks == null ? null : callbacks.get(eventName.toUpperCase());
      if (cbd == null)
      {
         return new character();
      }
      
      return new character(cbd.getTarget());
   }
   
   /**
    * Retrieves the handle of the procedure that contains the internal procedure associated
    * with the ABL callback for the specified event
    *
    * @param   eventName
    *          The name of the event.
    *
    * @return  a handle to the procedure that contains the callback procedure.
    */
   @Override
   public handle getCallbackProcContext(String eventName)
   {
      if (!isCallbackName(eventName))
      {
         return new handle();
      }
      
      CallbackData cbd = callbacks == null ? null : callbacks.get(eventName.toUpperCase());
      if (cbd == null || cbd.getProcedure() == null)
      {
         return new handle();
      }
      
      return new handle(cbd.getProcedure());
   }
   
   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid.
    * 
    * @param    event
    *           The event name, must evaluate to one of the events supported by implementing object. 
    * @param    proc
    *           The internal procedure to be executed for this event, which will be executed using
    *           the current THIS-PROCEDURE reference. To remove a callback mapping, set this
    *           parameter to the empty string.
    *           
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(String event, String proc)
   {
      return setCallbackProcedure(event, proc, null);
   }

   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using
    *          the current THIS-PROCEDURE reference. To remove a callback mapping, set this
    *          parameter to the empty string.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(String event, character proc)
   {
      return setCallbackProcedure(event, proc.toJavaType(), null);
   }

   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using
    *          the current THIS-PROCEDURE reference. To remove a callback mapping, set this
    *          parameter to the empty string.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(character event, character proc)
   {
      return setCallbackProcedure(event.toJavaType(), proc.toJavaType(), null);
   }
   
   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid or if the procedure's context is
    * not a valid handle.
    * 
    * @param    event
    *           The event name, must evaluate to one of the events supported by implementing object. 
    * @param    proc
    *           The internal procedure to be executed for this event. To remove a callback 
    *           mapping, set this parameter to the empty string.
    * @param    context
    *           The procedure handle where the internal procedure will be searched and executed.
    *           
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(character event, String proc, handle context)
   {
      return setCallbackProcedure(event.toJavaType(), proc, context);
   }

   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid or if the procedure's context is
    * not a valid handle.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using
    *          the current THIS-PROCEDURE reference. To remove a callback mapping, set this
    *          parameter to the empty string.
    * @param   context
    *          The procedure handle where the internal procedure will be searched and executed.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(String event, character proc, handle context)
   {
      return setCallbackProcedure(event, proc.toJavaType(), context);
   }
   
   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid or if the procedure's context is not a valid
    * handle.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using the current
    *          THIS-PROCEDURE reference. To remove a callback mapping, set this parameter to the empty
    *          string.
    * @param   context
    *          The procedure handle where the internal procedure will be searched and executed.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(character event, character proc, handle context)
   {
      return setCallbackProcedure(event.toJavaType(), proc.toJavaType(), context);
   }
   
   /**
    * Applies a callback procedure, which allows execution of a defined event without duplicating
    * the event procedure definition.
    *
    * @param   eventName
    *          The event whose callback will be called.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical applyCallback(Text eventName)
   {
      return applyCallback(eventName.toJavaType());
   }

   /**
    * Retrieves the name of the internal procedure associated with the ABL callback for the
    * specified event.
    *
    * @param   eventName
    *          The name of the event.
    *
    * @return  the name of the internal procedure associated with specified event.
    */
   @Override
   public character getCallbackProcName(Text eventName)
   {
      return getCallbackProcName(eventName.toJavaType());
   }
   
   /**
    * Retrieves the handle of the procedure that contains the internal procedure associated
    * with the ABL callback for the specified event
    *
    * @param   eventName
    *          The name of the event.
    *
    * @return  a handle to the procedure that contains the callback procedure.
    */
   @Override
   public handle getCallbackProcContext(Text eventName)
   {
      return getCallbackProcContext(eventName.toJavaType());
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, Text routineName, object<?> context)
   {
      return setCallback(callbackName.toJavaType(), routineName.toJavaType(), context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, String routineName, handle context)
   {
      return setCallbackProcedure(callbackName, routineName, context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, Text routineName, handle context)
   {
      return setCallbackProcedure(callbackName.toJavaType(), routineName.toJavaType(), context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, Text routineName, handle context)
   {
      return setCallbackProcedure(callbackName, routineName.toJavaType(), context);
   }

   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, String routineName)
   {
      return setCallback(callbackName, routineName, (object<?>) null);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, Text routineName)
   {
      return setCallback(callbackName.toJavaType(), routineName.toJavaType(), (object<?>) null);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, String routineName)
   {
      return setCallback(callbackName.toJavaType(), routineName);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, Text routineName)
   {
      return setCallback(callbackName, routineName.toJavaType());
   }

   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(String callbackName, Text routineName, object<?> context)
   {
      return setCallback(callbackName, routineName.toJavaType(), context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, String routineName, object<?> context)
   {
      return setCallback(callbackName.toJavaType(), routineName, context);
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are
    *          invalid.
    */
   @Override
   public logical setCallback(Text callbackName, String routineName, handle context)
   {
      return setCallback(callbackName.toJavaType(), routineName, context);
   }

   /**
    * Associates an internal procedure with an ABL callback event.
    * <p>
    * This method returns false if the event name is not valid.
    *
    * @param   event
    *          The event name.
    * @param   proc
    *          The internal procedure to be executed for this event, which will be executed using
    *          the current THIS-PROCEDURE reference. To remove a callback mapping, set this
    *          parameter to the empty string.
    *
    * @return   {@code true} if the event was registered.
    */
   @Override
   public logical setCallbackProcedure(character event, String proc)
   {
      return setCallbackProcedure(event.toJavaType(), proc, null);
   }

   /**
    * Associates an internal procedure with an ABL callback event.
    *
    * @param   eventName
    *          The eventName of the event.
    * @param   callback
    *          The eventName of the internal procedure associated with the callback event.
    * @param   context
    *          A handle to a procedure that contains the internal procedure specified by
    *          {@code callback}. If not specified, {@code THIS-PROCEDURE} is used as the procedure
    *          context.
    *
    * @return  {@code true} when the method ends with success.
    */
   @Override
   public logical setCallbackProcedure(String eventName, String callback, handle context)
   {
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(11951);
         // SET-CALLBACK-PROCEDURE is only valid for buffers that are members of a dataset.
         return logical.FALSE;
      }
      
      if (!isCallbackName(eventName))
      {
         return logical.FALSE;
      }
      
      if (callback == null)
      {
         return logical.TRUE;
      }
      
      if (callback.isEmpty())
      {
         if (callbacks != null)
         {
            callbacks.remove(eventName.toUpperCase());
         }
         
         return logical.TRUE;
      }
      
      if (context == null || context.isUnknown())
      {
         // defaulting to THIS-PROCEDURE
         context = pm.thisProcedure();
      }
      else
      {
         if (!context._isValid() || !(context.getResource() instanceof PersistentProcedure))
         {
            ErrorManager.recordOrShowError(13273);
            // Third argument to SET-CALLBACK must be a valid procedure handle or object reference.
            return logical.FALSE;
         }
      }
      
      InvokeConfig callSite = getCallSite(eventName);
      if (callbacks == null)
      {
         callbacks = new HashMap<>();
      }
      callbacks.put(eventName.toUpperCase(), new CallbackData(callback, context, null, callSite));
      
      return logical.TRUE;
   }
   
   /**
    * Configures a callback. Creates an association between a method within a class instance or
    * an internal procedure within a persistent procedure, with an ABL callback event.
    *
    * @param   callbackName
    *          The name of a callback.
    * @param   routineName
    *          The name of a method or an internal procedure to be associated.
    * @param   context
    *          The context in which the callback will be executed.
    *
    * @return  {@code true} if operation is successful and {@code false} if the parameters are invalid.
    */
   @Override
   public logical setCallback(String callbackName, String routineName, object<?> context)
   {
      if (dataSet == null || !dataSet.valid())
      {
         ErrorManager.recordOrShowError(11951);
         // SET-CALLBACK-PROCEDURE is only valid for buffers that are members of a dataset.
         return logical.FALSE;
      }
      
      if (!isCallbackName(callbackName))
      {
         return logical.FALSE;
      }
      
      if (routineName == null)
      {
         return logical.TRUE;
      }
      
      if (routineName.isEmpty())
      {
         if (callbacks != null)
         {
            callbacks.remove(callbackName.toUpperCase());
         }
         return logical.TRUE;
      }
      
      CallbackData cbd;
      if (context == null || context.isUnknown())
      {
         // defaulting to THIS-OBJECT or to THIS-PROCEDURE, depending on the context
         if (ObjectOps.thisObject().isUnknown())
         {
            InvokeConfig callSite = getCallSite(callbackName);
            cbd = new CallbackData(routineName, pm.thisProcedure(), null, callSite);
         }
         else
         {
            cbd = new CallbackData(routineName, null, ObjectOps.thisObject(), null);
         }
      }
      else
      {
         cbd = new CallbackData(routineName, null, context, null);
      }
      
      if (callbacks == null)
      {
         callbacks = new HashMap<>();
      }
      callbacks.put(callbackName.toUpperCase(), cbd);
      
      return logical.TRUE;
   }
   
   /**
    * Loads the empty target {@code TempTable}) object with changed rows from original (source)
    * object. This can be either the BEFORE or the AFTER buffer. If the BEFORE/AFTER temp-tables
    * are already populated, they are emptied before copying the changes.
    *
    * @param   original
    *          Handle to original object {@code DataSet} buffer (BEFORE or AFTER buffer) that
    *          contains the changed rows to load into the target object.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical getChanges(handle original)
   {
      if (original == null || original.isUnknown() || !original.isType(LegacyResource.BUFFER))
      {
         ErrorManager.recordOrShowError(11947);
         // bufferhdl:GET-CHANGES must have a valid buffer handle to the original table, from which changes are coming, as the first argument.
         return logical.FALSE;
      }
      
      // check the argument first: it must be an AFTER or BEFORE buffer:
      BufferImpl srcBefore = (BufferImpl) original.getResource();
      BufferImpl srcAfter = null;
      BufferImpl buf = srcBefore.beforeBufferNative();
      if (buf == null)
      {
         buf = srcBefore.afterBufferNative();
         if (buf == null)
         {
            ErrorManager.recordOrShowError(11925, srcBefore.doGetName());
            // GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>.
            return logical.FALSE;
         }
         else
         {
            srcAfter = buf;
         }
      }
      else
      {
         // oops, [xHandle] is the BEFORE buffer and [srcBefore] is the AFTER buffer, fixing:
         srcAfter = srcBefore;
         srcBefore = buf;
      }
      
      // now check the current buffer: it also must be an AFTER or BEFORE buffer:
      BufferImpl dstBefore = null;
      BufferImpl dstAfter = null;
      buf = beforeBufferNative();
      if (buf == null)
      {
         buf = afterBufferNative();
         if (buf == null)
         {
            ErrorManager.recordOrShowError(11925, doGetName());
            // GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>.
            return logical.FALSE;
         }
         else 
         {
            dstBefore = this;
            dstAfter = buf;
         }
      }
      else
      {
         dstBefore = buf;
         dstAfter = this;
      }
      
      // @this moment we have the dst/src-Before/After initialized. Be sure dsts are empty first:
      dstBefore.setUpBeforeBuffer(true);
      AbstractTempTable dstTT = (AbstractTempTable) dstAfter.tableHandleNative();
      boolean trackingChanges = dstTT._isTrackingChanges();
      dstTT.setTrackingChanges(false);
      try
      {
         if (!dstBefore.deleteAll().getValue() || !dstAfter.deleteAll().getValue())
         {
            return logical.FALSE;
         }
      }
      finally
      {
         // restore back
         if (trackingChanges)
         {
            dstTT.setTrackingChanges(true);
         }
         dstBefore.setUpBeforeBuffer(false);
      }
      
      boolean ok = TemporaryBuffer.copyChanges((TemporaryBuffer) srcBefore.buffer(),
                                               (TemporaryBuffer) srcAfter.buffer(),
                                               (TemporaryBuffer) dstBefore.buffer(),
                                               (TemporaryBuffer) dstAfter.buffer());
      
      // in 4GL, there is no guarantee as to the contents of the buffers involved, when the method returns
      // to add predictability, FWD makes sure they are empty
      srcBefore.release();
      srcAfter.release();
      
      if (ok)
      {
         TempTable origin = TempTableBuilder.asTempTable((Temporary) srcAfter);
         ((AbstractTempTable) TempTableBuilder.asTempTable((Temporary) dstBefore)).setOriginTable(origin);
         ((AbstractTempTable) TempTableBuilder.asTempTable((Temporary) dstAfter)).setOriginTable(origin);
      }
      return logical.of(ok);
   }
   
   /**
    * Loads the empty target ({@code DataSet} or {@code TempTable}) object with changed rows from
    * original (source) object.
    *
    * @param   original
    *          Handle to original object ({@code DataSet} or {@code TempTable}) that contains the
    *          changed rows to load into the target object.
    * @param   parentMode
    *          When {@code TRUE} the target gets the changed parent rows of each changed child row.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical getChanges(handle original, logical parentMode)
   {
      return getChanges(original, parentMode.getValue());
   }
   
   /**
    * Loads the empty target ({@code DataSet} or {@code TempTable}) object with changed rows from
    * original (source) object.
    *
    * @param   original
    *          Handle to original object ({@code DataSet} or {@code TempTable}) that contains the
    *          changed rows to load into the target object.
    * @param   parentMode
    *          When {@code TRUE} the target gets the changed parent rows of each changed child row.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical getChanges(handle original, boolean parentMode)
   {
      ErrorManager.recordOrThrowError(12778);
      // GET-PARENT mode not supported for buffer GET-CHANGES.
      return logical.FALSE;
   }
   
   /**
    * Merges single changed row of a single temp-table in a source {@code DataSet} object loaded
    * with the {@code GET-CHANGES()} method into the corresponding rows of a single temp-table
    * into the original (target) {@code DataSet} buffer.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical mergeRowChanges()
   {
      return mergeRowChanges(null, false);
   }
   
   /**
    * Merges single changed row of a single temp-table in a source {@code DataSet} object loaded
    * with the {@code GET-CHANGES()} method into the corresponding rows of a single temp-table
    * into the original (target) {@code DataSet} buffer.
    *
    * @param   original
    *          A handle to the original (target) buffer object of a single target {@code DataSet}
    *          temp-table to merge with the changed rows from the source object.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical mergeRowChanges(handle original)
   {
      return mergeRowChanges(original, false);
   }
   
   /**
    * Merges single changed row of a single temp-table in a source {@code DataSet} object loaded
    * with the {@code GET-CHANGES()} method into the corresponding rows of a single temp-table
    * into the original (target) {@code DataSet} buffer.
    *
    * @param   original
    *          A handle to the original (target) buffer object of a single target {@code DataSet}
    *          temp-table to merge with the changed rows from the source object.
    * @param   allCopyMode
    *          Use {@code TRUE} to indicates that all rows of the source after-image table must be
    *          merged.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical mergeRowChanges(handle original, boolean allCopyMode)
   {
      if (!_available())
      {
         ErrorManager.recordOrShowError(11937, doGetName());
         // MERGE-ROW-CHANGES buffer <name> not available.
         return logical.FALSE;
      }
      
      BufferImpl afterBufferChanges = null;
      if (!isBeforeBuffer())
      {
         if (!isAfterBuffer())
         {
            ErrorManager.recordOrShowError(11925, doGetName());
            // GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>.
            return logical.FALSE;
         }
         else
         {
            afterBufferChanges = this;
         }
      }
      else
      {
         afterBufferChanges = peerBuffer;
      }
      
      BufferImpl origBuffer;
      AbstractTempTable originTable;
      if (original == null)
      {
         TempTable parentTable = this.buffer().getParentTable();
         originTable = (AbstractTempTable) parentTable.getOriginTable();
         if (originTable == null || !originTable.valid())
         {
            ErrorManager.recordOrShowError(11944, afterBufferChanges.doGetName());
            // MERGE-ROW-CHANGES finds ORIGIN-HANDLE in table <name> is no longer valid.
            return logical.FALSE;
         }
         origBuffer = (BufferImpl) originTable.defaultBufferHandleNative();
      }
      else
      {
         if (!original._isValid() || !original.isType(LegacyResource.BUFFER))
         {
            ErrorManager.recordOrShowError(11943);
            // First argument to MERGE-ROW-CHANGES must be a valid BUFFER handle.
            return logical.FALSE;
         }
         origBuffer = (BufferImpl) original.getResource();
         originTable = (AbstractTempTable) origBuffer.tableHandleNative();
      }
      
      if (originTable._isTrackingChanges())
      {
         ErrorManager.recordOrShowError(11922, originTable.name().toStringMessage());
         // MERGE-CHANGES may not be run on table <name> with TRACKING-CHANGES on.
         return logical.FALSE;
      }
      
      if (origBuffer.beforeBufferType == BeforeType.BEFORE)
      {
         // the mergeRowChangesImpl() requires the AFTER-BUFFER as second parameter
         origBuffer = origBuffer.peerBuffer;
      }
      
      return logical.of(mergeRowChangesImpl(this.ref(), origBuffer.ref()));
   }
   
   /**
    * Merges single changed row of a single temp-table in a source {@code DataSet} object loaded
    * with the {@code GET-CHANGES()} method into the corresponding rows of a single temp-table
    * into the original (target) {@code DataSet} buffer.
    *
    * @param   original
    *          A handle to the original (target) buffer object of a single target {@code DataSet}
    *          temp-table to merge with the changed rows from the source object.
    * @param   allCopyMode
    *          Use {@code TRUE} to indicates that all rows of the source after-image table must be
    *          merged.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical mergeRowChanges(handle original, logical allCopyMode)
   {
      return mergeRowChanges(original, !allCopyMode.isUnknown() && allCopyMode.getValue());
   }
   
   /**
    * Merges the changed rows of all temp-tables or a single temp-table in a source
    * {@code DataSet} object loaded with the {@code GET-CHANGES()} method into the corresponding
    * rows of all temp-tables or a single temp-table (respectively) in the original (target)
    * {@code DataSet} object.
    *
    * @param   original
    *          A handle to the original (target) {@code DataSet} object or the buffer object of a
    *          single target {@code DataSet} temp-table to merge with the changed rows from the
    *          source object.
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical mergeChanges(handle original)
   {
      return mergeChanges(original, false);
   }
   
   /**
    * Merges the changed rows of all temp-tables or a single temp-table in a source
    * {@code DataSet} object loaded with the {@code GET-CHANGES()} method into the corresponding
    * rows of all temp-tables or a single temp-table (respectively) in the original (target)
    * {@code DataSet} object.
    *
    * @param   original
    *          A handle to the original (target) {@code DataSet} object or the buffer object of a
    *          single target {@code DataSet} temp-table to merge with the changed rows from the
    *          source object.
    * @param   allCopyMode
    *          Use {@code TRUE} to indicates that all rows of the source after-image table must be
    *          merged.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical mergeChanges(handle original, logical allCopyMode)
   {
      return mergeChanges(original, !allCopyMode.isUnknown() && allCopyMode.getValue());
   }
   
   /**
    * Merges the changed rows of all temp-tables or a single temp-table in a source {@code DataSet} object
    * loaded with the {@code GET-CHANGES()} method into the corresponding rows of all temp-tables or a single
    * temp-table (respectively) in the original (target) {@code DataSet} object.
    *
    * @param   original
    *          A handle to the original (target) {@code DataSet} object or the buffer object of a single
    *          target {@code DataSet} temp-table to merge with the changed rows from the source object.
    * @param   allCopyMode
    *          Use {@code TRUE} to indicates that all rows of the source after-image table must be merged.
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical mergeChanges(handle original, boolean allCopyMode)
   {
      // the parameter must be a valid dataset handle:
      if (original == null || original.isUnknown() || !original.isType(LegacyResource.BUFFER))
      {
         ErrorManager.recordOrShowError(11953);
         // First argument to MERGE-CHANGES must be a valid BUFFER handle.
         return logical.FALSE;
      }
      
      BufferImpl origBuf = (BufferImpl) original.getResource();
      origBuf = origBuf.ref();
      AbstractTempTable origTT = (AbstractTempTable) tableHandleNative().getOriginTable();
      if (origTT != origBuf.tableHandleNative())
      {
         ErrorManager.recordOrShowError(11923, origBuf.doGetName(), doGetName());
         // MERGE-CHANGES run on original table <name> that does not match the ORIGIN-HANDLE in the GET-CHANGES table <name>.
         return logical.FALSE;
      }
      
      if (origTT._isTrackingChanges())
      {
         ErrorManager.recordOrShowError(11922, origTT.name().toStringMessage());
         // MERGE-CHANGES may not be run on table <name> with TRACKING-CHANGES on.
         return logical.FALSE;
      }
      
      // make sure we use the AFTER-TABLE buffer
      if (origBuf.isBeforeBuffer())
      {
         origBuf = origBuf.peerBuffer;
      }
      
      if (isAfterBuffer())
      {
         return logical.of(mergeChangesImpl(origBuf, allCopyMode));
      }
      else if (isBeforeBuffer())
      {
         return logical.of(peerBuffer.mergeChangesImpl(origBuf, allCopyMode));
      }
      
      ErrorManager.recordOrShowError(11925, doGetName());
      // GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>. 
      return logical.TRUE;
   }
   
   /**
    * Accepts changes to the data in target object (a temp-table or all temp-tables in a {@code DataSet}).
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical acceptChanges()
   {
      if (!isBeforeBuffer())
      {
         ErrorManager.recordOrShowError(11887, doGetName());
         // ACCEPT-CHANGES run on table <name> that has no BEFORE-TABLE.
         return logical.FALSE;
      }
      
      RecordBuffer beforeBuf = buffer();
      Persistence persistence = beforeBuf.getPersistence();
      BufferImpl after = afterBufferNative();
      RecordBuffer afterBuf = after.buffer();
      
      // invalidate index-based caches because of low-level operations:
      afterBuf.invalidateFFCache(0);
      beforeBuf.invalidateFFCache(0);
      
      // release all buffers
      ((TemporaryBuffer) afterBuf).getAllSlaveBuffers().forEach((b) -> b.release(false));
      
      // execute a simple query to update the [_rowState], [_originRowid] and [_peerRowid] fields to default
      // values in the AFTER-BUFFER. The [_dataSourceRowid] remains unmodified. 
      StringBuilder buf = new StringBuilder();
      buf.append("update ").append(afterBuf.getTable()).append(" set ")
         .append(Buffer.ROW_STATE_FIELD).append("=").append(Buffer.ROW_UNMODIFIED).append(", ")
         .append(Buffer.ORIGIN_ROWID_FIELD).append("=NULL, ")
         .append(Buffer.AFTER_ROWID_FIELD).append("=NULL")
         .append(" where ")
         .append(TemporaryBuffer.MULTIPLEX_FIELD_NAME).append(" = ?");
      
      Object[] args = new Object[] {afterBuf.getMultiplexID()};
      try
      {
         boolean noUndo = !beforeBuf.isUndoable();
         int cnt = persistence.executeSQL(buf.toString(), Persistence.TEMP_CTX, args, noUndo);
         afterBuf.updateCachedRecords((BaseRecord r) ->
         {
            // direct access, so the row's state doesn't get changed
            // at this moment, all buffers are flushed, nothing is new/deleted/dirty
            OrmUtils.setField(r, TempRecord._ROW_STATE_DATA_INDEX, Buffer.ROW_UNMODIFIED);
            OrmUtils.setField(r, TempRecord._ORIGIN_ROWID_DATA_INDEX, null);
            OrmUtils.setField(r, TempRecord._PEER_ROWID_DATA_INDEX, null);
         });
         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, "ACCEPT-CHANGES accepted " + cnt + " changes from " + beforeBuf.getDMOName());
         }
      }
      catch (PersistenceException e)
      {
         return logical.FALSE;
      }
      
      // then drop the before-image data for this table:
      try
      {
         beforeBuf.delete(TemporaryBuffer.MULTIPLEX_FIELD_NAME + "=?", beforeBuf.getMultiplexID());
      }
      catch (PersistenceException e)
      {
         if (LOG.isLoggable(Level.WARNING))
         {
            LOG.log(Level.WARNING,
                    "ACCEPT-CHANGES: Failed to drop before images of " + afterBuf.getDMOName());
         }
      }
      
      beforeBuf.pruneSessionCache();
      
      afterBuf.getParentTable().setOriginTable(null);
      beforeBuf.getParentTable().setOriginTable(null);
      
      return logical.TRUE;
   }
   
   /**
    * Rejects changes to the data in target object (a temp-table or all temp-tables in a
    * {@code DataSet}).
    *
    * @return  {@code true} if operation is successful.
    */
   @Override
   public logical rejectChanges()
   {
      if (!isBeforeBuffer())
      {
         ErrorManager.recordOrShowError(11919, doGetName());
         // REJECT-CHANGES run on table <name> that has no BEFORE-TABLE.
         return logical.FALSE;
      }
      
      TemporaryBuffer buffer = (TemporaryBuffer) buffer();
      try
      {
         buffer.flush();
      }
      catch (ValidationException e)
      {
         // normally this should not happen because before tables do not have unique indexes
         // TODO: but if it still happens? which error we we throw?
         return logical.FALSE;
      }
      return logical.of(buffer.rejectChanges(afterBufferNative()));
   }
   
   /**
    * Directly set the {@link #name()} attribute.
    * 
    * @param    name
    *           The name to be set.
    */
   protected void nameInternal(character name)
   {
      this.name = name.getValue();
   }
   
   /**
    * Tests whether the table from this buffer has a legacy field named {@code legFieldName}.
    * 
    * @param   legFieldName
    *          The legacy field name to be tested.
    *
    * @return  {@code true} if the buffer has a legacy field as specified.
    */
   protected boolean hasField(String legFieldName)
   {
      return buffer().getDmoInfo().byLegacyName(legFieldName) != null;
   }
   
   /**
    * Tests whether the table from this buffer has a legacy field named {@code legFieldName}.
    * Unlike the {@code #hasField} method, this method will validate the field names that also
    * contain the buffer name as qualification.
    * 
    * @param   legFieldName
    *          The field name to be tested.
    *
    * @return  {@code true} if the buffer has a legacy field as specified.
    */
   protected boolean hasFieldEx(String legFieldName)
   {
      // normalize name (redundant with DmoMeta.byLegacyName below, but needed for pre-processing)
      legFieldName = TextOps.rightTrimLower(legFieldName);
      
      // check whether this is a scoped name
      if (legFieldName.contains("."))
      {
         String prefix = (name == null ? "?" : name) + ".";
         if (StringHelper.startsWithIgnoreCase(legFieldName, prefix))
         {
            legFieldName = legFieldName.substring(prefix.length());
         }
         else
         {
            return false; // not this buffer
         }
      }
      
      if (legFieldName.equals(Session.PK))
      {
         return true;
      }
      
      return buffer().getDmoInfo().byLegacyName(legFieldName) != null;
   }
   
   /**
    * Actual setting of the query. This is called indirectly, by FWD from the query when this
    * buffer is set / added.
    * 
    * @param   qry
    *          The query to associate.
    */
   public void setQuery(QueryWrapper qry)
   {
      if (qry == null || !qry.valid())
      {
         this.lastQuery = null;
         return;
      }
      
      this.lastQuery = qry;
   }
   
   /**
    * Reset the {@link #lastQuery} if is the same as the given one, as the query has been deleted.
    * 
    * @param   qry
    *          The qry to check against.
    */
   protected void resetLastQuery(QueryWrapper qry)
   {
      if (lastQuery == qry)
      {
         this.lastQuery = null;
      }
   }
   
   /**
    * Sets the current 'parent' dataset. A buffer can be in at most one dataset at a specific
    * time. Trying to add it to another one will generate error 11860. This is checked in
    * dataset add... procedures.
    * 
    * @param   ds
    *          The new parent {@code DataSet}.
    */
   protected void setDataSet(DataSet ds)
   {
      if (ds != null && this.dataSet != null && this.dataSet.valid())
      {
         LOG.warning("New dataset configured but one is already active.");
         // return internal error ?
      }
      
      detachDataSourceImpl();
      dataSet = (ds == null) ? null : ds.ref();
      lastBatch = false;
   }
   
   /**
    * Flags the bracketing internal management of BEFORE-BUFFER. ABL programmer's direct
    * actions (CREATE/DELETE/ALTER buffer) are outside this bracket and will lead to errors.
    * The flag is forwarded to {@code RecordBuffer}.
    * 
    * @param   on
    *          {@code true} to activate bracket, {@code false} to end it and activate errors.
    */
   public void setUpBeforeBuffer(boolean on)
   {
      this.setUpBeforeBuffer = on;
   }
   
   /**
    * Checks whether we are out of BEFORE-TABLE management mode. In this mode, changes to the
    * record in buffer, including CREATE/DELETE <strong>WILL</strong> throw errors.
    * 
    * @return  {@code false} when {@link #setUpBeforeBuffer} is set.
    */
   public boolean hasBeforeBufferConstraints()
   {
      return !setUpBeforeBuffer;
   }
   
   /**
    * Tests whether this is a {@code BEFORE-BUFFER} of a dataset.
    * 
    * @return  {@code true} if this is a {@code BEFORE-BUFFER} of a dataset.
    */
   public boolean isBeforeBuffer()
   {
      if (beforeBufferType == BeforeType.UNKNOWN)
      {
         computeBufferType();
      }
      
      return beforeBufferType == BeforeType.BEFORE;
   }
   
   /**
    * Tests whether this is a {@code AFTER-BUFFER} of a dataset.
    *
    * @return  {@code true} if this is a {@code AFTER-BUFFER} of a dataset.
    */
   public boolean isAfterBuffer()
   {
      if (beforeBufferType == BeforeType.UNKNOWN)
      {
         computeBufferType();
      }
      
      return beforeBufferType == BeforeType.AFTER;
   }
   
   /**
    * Constructs and returns a string containing the P4GL schema definition of the table. The result is not
    * cached so this method is to be used only for debug.
    *
    * @return  a string containing the P4GL legacy schema definition of the table.
    */
   public String tableDefinition()
   {
      return buffer().getDmoInfo().getTableDefinition();
   }
   
   /**
    * Computes the buffer type and if it is an {@code AFTER} or a {@code BEFORE} the
    * peer (complementary) buffer is also stored in {@code peerBuffer}.
    */
   void computeBufferType()
   {
      if (!(this instanceof Temporary))
      {
         peerBuffer = null;
         beforeBufferType = BeforeType.SIMPLE;
         return;
      }
      
      TempTable tempTable = TempTableBuilder.asTempTable((Temporary) this);

      TempTable ttBefore = tempTable.getBeforeTableNative();
      if (ttBefore != null)
      {
         peerBuffer = (BufferImpl) ttBefore.defaultBufferHandleNative();
         peerBuffer = peerBuffer.ref();
         beforeBufferType = BeforeType.AFTER;
         return;
      }
      
      TempTable ttAfter = tempTable.getAfterTableNative();
      if (ttAfter != null)
      {
         peerBuffer = (BufferImpl) ttAfter.defaultBufferHandleNative();
         peerBuffer = peerBuffer.ref();
         beforeBufferType = BeforeType.BEFORE;
         return;
      }
      
      peerBuffer = null;
      beforeBufferType = BeforeType.SIMPLE;
   }
   
   /**
    * Invokes a defined callback if one exist.
    *
    * @param   event
    *          Event name.
    *
    * @return  {@code false} if the callback raised an error. If the callback does not exist or ended with
    *          success {@code true} is returned.
    */
   boolean invokeCallback(String event)
   {
      if (dataSet == null)
      {
         return true;
      }
      
      CallbackData cbd = callbacks == null ? null : callbacks.get(event);
      if (cbd == null)
      {
         return true;
      }
      
      return dataSet.invokeCallback(new handle(this), cbd);
   }
   
   /**
    * Set NAME attribute without any checks.
    *
    * @param name
    *        Name to set.
    */
   protected void doSetName(String name)
   {
      this.name = name;
   }
   
   /**
    * Get the {@code NAME} attribute as java string.
    *
    * @return  the {@code NAME} attribute as a java string. {@code null} if {@code NAME} is {@code unknown}.
    */
   public String doGetName()
   {
      return this.name;
   }
   
   /**
    * Makes a backup of the current name and it replaces it with another one.
    * This is usually used by an argument proxy to temporarily replace the name.
    * The override is successful only if the name wasn't overridden beforehand.
    * 
    * @param     newName
    *            The new name which should be temporarily used by this Buffer.
    *            
    * @return    true if the overriding was successful, false otherwise.
    */
   protected boolean overrideName(String newName)
   {
      if (oldName != null)
      {
         return false;
      }
      
      oldName = name;
      name = newName;
      return true;
   }
   
   /**
    * Restores the name by replacing the current name with the one stored in the backup.
    */
   protected void restoreName()
   {
      name = oldName;
      oldName = null;
   }
   
   /**
    * Check if this resource supports the NEXT-SIBLING attribute. Since the session Buffer list
    * is a simple linked list, the returned value is {@code true}. The chain is one-directional,
    * starting at SESSION:FIRST-BUFFER.
    * <p> 
    * Only the {@link #dynamic} buffers are stored in the chain. The method will return false for
    * static buffers. 
    * 
    * @return   {@code true} only for dynamic buffers.
    */
   @Override
   protected boolean hasNextSibling()
   {
      return buffer() != null && _dynamic();
   }
   
   /**
    * Check if this resource supports the PREV-SIBLING attribute. Since the session Buffer list
    * is only a simple linked list, the returned value is {@code false}. The buffers chain is
    * one-directional, starting at SESSION:FIRST-BUFFER. There is no LAST-BUFFER attribute
    * associated with the SESSION handle.
    * 
    * @return   Always {@code false}.
    */
   @Override
   protected boolean hasPrevSibling()
   {
      return false;
   }
   
   /**
    * Interlinks the newly created HandleChain in the both chains. The back and forth links are
    * created with respective elements.
    * The heads of the chains are updated: if this is the first of its kind, it becomes the first
    * element of the chain. It is always stored as the last resource in his chain.
    * Even if this kind of resource does not support NEXT or PREV sibling, both heads of the
    * chain are kept for a fast access in future maintenance.
    * <p>
    * BUFFERs use special order: TEMP-TABLES (static and dynamic in the order they are added)
    * first, then PERMANENT buffers at the end of the list.
    */
   @Override
   protected void interlink()
   {
      if (!hasNextSibling() && !hasPrevSibling())
      {
         // quick out this resource is not chain-able
         return;
      }
      
      String type = type();
      boolean permanentTable = this.tableHandleNative() == null;
      
      BufferImpl firstPermanent = (BufferImpl) HandleChain.getHead(PERMANENT_TABLE);
      if (permanentTable)
      {
         // add buffer at the end, like other resources, but don't forget to update the 
         // first-permanent-buffer pointer:
         if (firstPermanent == null)
         {
            // there are no others, this is the first permanent buffer
            HandleChain.setHead(PERMANENT_TABLE, this);
         }
         // then we continue processing like any other resource, 
         // appending this resource at the end of the list 
      }
      else
      {
         // we must insert this TEMP buffer just before the FIRST-PERMANENT pointer
         if (firstPermanent != null)
         {
            if (firstPermanent.prevSibling == null)
            {
               // there are no TEMP-TABLE buffers? then install in front
               HandleChain.setHead(type, this);
               firstPermanent.prevSibling = this;
               this.nextSibling = firstPermanent;
               this.prevSibling = null;
            }
            else
            {
               // there are both TEMP and PERMANENT TABLE buffers, insert in between
               this.nextSibling = firstPermanent;
               this.prevSibling = firstPermanent.prevSibling;
               ((BufferImpl)this.nextSibling).prevSibling = this;
               ((BufferImpl)this.prevSibling).nextSibling = this;
            }
            return; // all processing done here
         }
         // else there are no other permanent buffers, continue processing like any other
         // resource, appending this resource at the end of the list 
      }
      
      super.interlink();
   }

   /**
    * Check if the resource can veto the delete or not, before attempting the {@link #delete()} call.
    * 
    * @param    ref
    *           The exact handle holding the reference, on which the delete is attempted.
    * @param    explicit
    *           Flag indicating this is called via <code>DELETE OBJECT</code> from the application.
    * 
    * @return   <code>false</code> if the buffer's {@link #_dataSet() DATASET} is registered as a OUTPUT 
    *           DATASET-HANDLE parameter.
    */
   @Override
   public boolean allowDelete(handle ref, boolean explicit)
   {
      DataSet bufds = _dataSet();
      if (bufds != null && bufds.valid())
      {
         DataSetManager dmgr = DataSetManager.instance();
         
         if (dmgr.isOutputDataSetHandle(bufds))
         {
            // TODO: when (and if) will we delete the buffer?
            return false;
         }
         else if (explicit && _dynamic())
         {
            ErrorManager.recordOrThrowError(12326,
                                            "Cannot delete temp-table member " + _name() +
                                            " of dataset  without deleting the dataset first. (12326)",
                                            false,
                                            true);
            return false;
         }
      }
      /*
      CA: this code is disabled because 'isOutputTableHandle' checks for reference equality, which
          is always false, as 'tableHandle()' always returns a new handle  
      handle htt = tableHandle();
      if (htt._isValid()                                && 
          htt.getResource() instanceof TempTableBuilder && 
          BufferManager.get().isOutputTableHandle(htt))
      {
         // TODO: when (and if) will we delete the buffer?
         return false;
      }
      */
      
      return true;
   }
   
   /**
    * Delete the resource.
    * 
    * @return   <code>true</code> if the resource was deleted.
    */
   @Override
   protected boolean resourceDelete()
   {
      if (FwdServerJMX.JMX_DEBUG)
      {
         return RESOURCE_DEL.timerWithReturn(this::resourceDeleteImpl);
      }
      else
      {
         return resourceDeleteImpl();
      }
   }
   
   /**
    * Delete the resource.
    * 
    * @return   <code>true</code> if the resource was deleted.
    */
   private boolean resourceDeleteImpl()
   {
      if (!_dynamic() && !implicitDeletion())
      {
         // static buffers can't be deleted, but no error message is shown
         return false;
      }
      
      RecordBuffer recBuf = buffer();
      boolean staticTT = recBuf.isTemporary() && !recBuf.isDynamic();
      if (staticTT)
      {
         TempTable tt = tableHandleNative();
         if (tt != null && tt.valid())
         {
            // if this is the default buffer for a GLOBAL SHARED TEMP-TABLE, then do not delete
            if (recBuf.isGlobal() && tt.defaultBufferHandleNative() == this)
            {
               return false;
            }
            
            // if there are still other buffers bounded to it, then do not delete.
            TemporaryBuffer tbuf = (TemporaryBuffer) recBuf;
            if (tbuf.getMasterBuffer() == tbuf && tbuf.hasExplicitBuffers())
            {
               return false;
            }
         }
      }
      
      deleting = true;
      // the order must be preserved: there are checks  in resourceDeleted for the buffer's open scope count,  
      // which gets decremented in 'finished'.
      recBuf.resourceDeleted();
      recBuf.finished();
      
      SharedVariableManager.cleanupBuffer(recBuf.getDMOProxy());
      
      deleted = true;
      deleting = false;
      
      if (staticTT)
      {
         TemporaryBuffer tbuf = (TemporaryBuffer) recBuf;
         TemporaryBuffer mbuf = (TemporaryBuffer) tbuf.getMasterBuffer();
         if (!mbuf.isGlobal() && mbuf != tbuf && !mbuf.hasExplicitBuffers())
         {
            TempTable tt = mbuf.getParentTable();
            if (tt != null && tt.valid() && !tt.instantiatingProcedure()._isValid())
            {
               // if the master table is referenced only from other programs and its program has been deleted,
               // and this is the last child buffer referencing it, then delete it, too.
               StaticTempTable stt = (StaticTempTable) tt;
               stt.setForceDelete(true);
               
               pm.deleteResource(stt);
            }
         }
      }
      
      if (fields != null)
      {
         for (int i = 0; i < fields.length; i++)
         {
            if (fields[i] != null)
            {
               fields[i].forceDelete();
            }
         }
      }
      
      fieldHandles = null;
      
      return true;
   }
   
   /**
    * Check whether an extent field index is within the valid range {@code 0 &le; index &lt; max}.
    * Raise an error if not, unless errors are silenced.
    * <p>
    * This method is meant to be called by DMO proxy objects which extend this class, from
    * within their indexed setter and getter methods which accept an {@code int} value for the
    * element index.
    * 
    * @param   index
    *          Zero-based extent field index to check.
    * @param   max
    *          Maximum number of elements in the extent field.
    * 
    * @return  {@code true} if {@code index} falls within the valid range,
    *          {@code false} if outside the range and errors are silenced.
    * 
    * @throws  ErrorConditionException
    *          if {@code index} is outside the valid range and errors are not silenced.
    */
   public boolean checkExtentRange(int index, int max)
   {
      if (index < 0 || index >= max)
      {
         outOfBoundsError(index);
         
         return false;
      }
      
      return true;
   }
   
   /**
    * Record or throw an "array subscript out of range" error condition for the given subscript.
    * 
    * @param   index
    *          Zero-based subscript value.
    */
   protected void outOfBoundsError(int index)
   {
      String msg = "Array subscript " + (index + 1) + " is out of range";
      ErrorManager.recordOrThrowError(26, msg);
   }
   
   /**
    * Delete the record currently in the buffer, if any.
    *
    * @param   validated
    *          <code>true</code> if there was validation and it passed. <code>false</code> if
    *          there was no validation.
    *
    * @return  {@code true} if the delete was possible.
    *
    * @throws  ErrorConditionException
    *          if no record currently is loaded in the buffer, or if there is
    *          an error deleting the current record from the database.
    */
   protected logical deleteRecord(boolean validated)
   {
      RecordBuffer recBuf = buffer();
      
      if (!recBuf.isTemporary())
      {
         if (!verifyTransaction(recBuf))
         {
            return logical.FALSE;
         }
         
         if (!validated)
         {
            Class<? extends DataModelObject> iface = recBuf.getDMOInterface();
            if (TableMapper.hasValidation(iface))
            {
               String msg = "Table " + recBuf.getDmoInfo().legacyTable + " is defined with schema delete " +
                            "validation and may only be deleted by the DELETE statement";
               ErrorManager.recordOrShowError(7347, msg, false, false);
               
               return logical.FALSE;
            }
         }
      }
      
      if (dataSet != null) // implies buffer.isTemporary()
      {
         // checks whether tracking is active
         if (recBuf.getParentTable()._isTrackingChanges())
         {
            if (!invokeCallback(ROW_DELETE_EVENT))
            {
               returnError(); // veto on record delete
            }
         }
      }
      
      try
      {
         recBuf.delete();
      }
      catch (PersistenceException exc)
      {
         return logical.FALSE;
      }
      
      return logical.TRUE;
   }
   
   /**
    * Test whether a string is a valid CALLBACK name for {@code DataSet}s. This method is
    * responsible for raising the 11950 error if the name specified is unknown or nor not a name
    * of a known callback for {@code DataSet}.
    *
    * @param   someName
    *          The name to test.
    *
    * @return  {@code true} only if {@code someName} is equals to "BEFORE-FILL", "AFTER-FILL",
    *          "BEFORE-ROW-FILL", "AFTER-ROW-FILL", "ROW-CREATE", "ROW-DELETE", "ROW-UPDATE",
    *          "FIND-FAILED", or "SYNCHRONIZE", in ignore-case mode.
    */
   private static boolean isCallbackName(String someName)
   {
      if (someName == null ||
         !BEFORE_FILL_EVENT.equalsIgnoreCase(someName)     &&
         !AFTER_FILL_EVENT.equalsIgnoreCase(someName)      &&
         !BEFORE_ROW_FILL_EVENT.equalsIgnoreCase(someName) &&
         !AFTER_ROW_FILL_EVENT.equalsIgnoreCase(someName)  &&
         !ROW_CREATE_EVENT.equalsIgnoreCase(someName)      &&
         !ROW_DELETE_EVENT.equalsIgnoreCase(someName)      &&
         !ROW_UPDATE_EVENT.equalsIgnoreCase(someName)      &&
         !FIND_FAILED_EVENT.equalsIgnoreCase(someName)     &&
         !SYNCHRONIZE_EVENT.equalsIgnoreCase(someName))
      {
         ErrorManager.recordOrShowError(11950, someName != null ? someName : "");
         // Argument  to CALLBACK method must be a valid buffer or dataset event name.
         return false;
      }
      
      return true;
   }
   
   /**
    * Get the {@link InvokeConfig callsite} for the specified event.
    * 
    * @param    eventName
    *           The event name.
    */
   private static InvokeConfig getCallSite(String eventName)
   {
      switch (eventName.toUpperCase())
      {
         case BEFORE_FILL_EVENT:
            return BEFORE_FILL_CALL_SITE;
         case AFTER_FILL_EVENT:
            return AFTER_FILL_CALL_SITE;
         case BEFORE_ROW_FILL_EVENT:
            return BEFORE_ROW_FILL_CALL_SITE;
         case AFTER_ROW_FILL_EVENT:
            return AFTER_ROW_FILL_CALL_SITE;
         case ROW_CREATE_EVENT:
            return ROW_CREATE_CALL_SITE;
         case ROW_DELETE_EVENT:
            return ROW_DELETE_CALL_SITE;
         case ROW_UPDATE_EVENT:
            return ROW_UPDATE_CALL_SITE;
         case FIND_FAILED_EVENT:
            return FIND_FAILED_CALL_SITE;
         case SYNCHRONIZE_EVENT:
            return SYNCHRONIZE_CALL_SITE;
      }

      return null;
   }
   
   /**
    * Return {@code LockType} instance for the given internal lock and nowait with corresponding
    * error processing.
    *
    * @param   lock
    *          Integer value.
    * @param   nowait
    *          Integer value.
    *
    * @return  {@code LockType} instance.
    *
    * @throws  ErrorConditionException
    *          If locking operation fails.
    */
   private static LockType fromLockNowait(long lock, long nowait)
   throws ErrorConditionException
   {
      LockType lockType = null;
      try
      {
         lockType = LockType.fromLockNowait(lock, nowait);
      }
      catch (LockTypeException e)
      {
         String err;
         int num;
         if (e.isNowaitCorrect())
         {
            err = "FIND-BY-ROWID, FIND-FIRST etc., second argument must be " +
                  "NO-LOCK, SHARE-LOCK or EXCLUSIVE-LOCK";
            num = 7344;
         }
         else
         {
            err = "FIND method argument must be NO-WAIT, or 0 or ?";
            num = 7343;
         }
         ErrorManager.recordOrThrowError(num, err);
      }
      return lockType;
   }

   /**
    * Implementation of the SERIALIZE-ROW method. Writes current row from this buffer into the specified data 
    * target, using the specified format.
    *
    * @param   format
    *          The format ("xml" or "json").
    * @param   targetData
    *          Data target.
    * @param   formatted
    *          {@code True} to format the data for making it more readable; {@code false} to write it
    *          as unformatted text. Unknown value is treated as {@code false}.
    * @param   encoding
    *          Name of the character encoding to use.
    * @param   omitInitial
    *          {@code True} to omit data for fields whose values match their initial values.
    * @param   omitOuter
    *          {@code True} to omit data for the outermost object from the output.
    *
    * @return  {@code True} if the operation completed successfully, else {@code false}.
    */
   private boolean serializeRowImpl(String format,
                                    TargetData targetData,
                                    Boolean formatted,
                                    String encoding,
                                    Boolean omitInitial,
                                    Boolean omitOuter)
   {
      try
      {
         if (!buffer().isTemporary())
         {
            ErrorManager.recordOrShowError(13158);
            // XML Methods only allowed on TEMP-TABLE buffers.
            return false;
         }
         
         // the buffer must not be empty
         if (!_available())
         {
            ErrorManager.recordOrShowError(16017);
            // No record in buffer for SERIALIZE-ROW.
            return false;
         }
         
         // check the specified target format
         if (format == null)
         {
            ErrorManager.recordOrShowError(16019, "unknown", "");
            // Invalid target-format '<format>' for SERIALIZE-ROW.
            return false;
         }
         boolean isXML = "XML".equalsIgnoreCase(format);
         boolean isJSON = "JSON".equalsIgnoreCase(format);
         if (!isJSON && !isXML)
         {
            ErrorManager.recordOrShowError(16019, format, "");
            // Invalid target-format '<format>' for SERIALIZE-ROW.
            return false;
         }
         
         if (formatted == null)
         {
            formatted = false;
         }
         if (omitInitial == null)
         {
            omitInitial = false;
         }
         if (omitOuter == null)
         {
            omitOuter = false;
         }
      
         if (isXML)
         {
            if (omitOuter)
            {
               ErrorManager.recordOrShowError(16020);
               // omit-outer-object argument not allowed in SERIALIZE-ROW for XML
               ErrorManager.recordOrShowError(4065, false, "SERIALIZE-ROW", "BUFFER widget");
               // **The <attribute> attribute on the <widget id> has invalid arguments.
               return false;
            }
            XmlExport export = new XmlExport(targetData, formatted, encoding);
            return export.writeRecord(this, omitInitial);
         }
         else
         {
            // is JSON
            com.fasterxml.jackson.core.JsonEncoding jsonEnc = 
                  JsonExport.parseEncoding(new character(encoding));
            if (jsonEnc == null)
            {
               ErrorManager.recordOrShowError(15342, encoding, "WRITE-JSON");
               // Invalid encoding '<encoding>' argument for <method>.
               return false;
            }
            
            JsonExport export = new JsonExport();
            return export.exportRecord(this, targetData, formatted, jsonEnc, omitInitial, omitOuter);
         }
      }
      finally
      {
         try
         {
            targetData.close();
         }
         catch (IOException e)
         {
            // TODO: log
         }
      }
   }
   
   /**
    * Obtain the list of data-source keys of this buffer. If this buffer is not in a DATA-SOURCE, {@code null}
    * is returned.
    * 
    * @return  the list of data-source keys of this buffer.
    */
   List<String> keysList()
   {
      return keys;
   }
   
   /**
    * Sets the {@code keys} for this source buffer, related to the specified {@code DataSource}. If a
    * DATA-SOURCE was already configured using the buffer, an error message will be thrown back.
    * 
    * @param   parent
    *          The parent {@code DataSource}.
    * @param   keys
    *          The array of fields used as keys.
    *
    * @return  {@code true} only if operation is successful.
    */
   boolean keys(DataSource parent, String[] keys)
   {
      if (parentDataSource != null && parentDataSource.valid())
      {
         ErrorManager.recordOrThrowError(12298, doGetName(), "");
         // Buffer <name> may be in only one data-source at a time.  Use different buffers for the same table. (12298)
         return false;
      }
      
      this.keys = Arrays.asList(keys);
      this.parentDataSource = parent;
      
      return true;
   }
   
   /**
    * Clear the {@link #parentDataSource} and {@link #keys}, as the {@link #parentDataSource} has been 
    * deleted.
    */
   void clearParentDataSource()
   {
      this.parentDataSource = null;
      this.keys = null;
   }
   
   /**
    * Getter of NUM-FIELDS attribute.  Only normal fields are counted, the hidden fields are not
    * taken into consideration.
    *
    * @return  current value of NUM-FIELDS attribute.
    */
   int _numFields()
   {
      return buffer().getNumFields();
   }
   
   /**
    * Execute a dynamic FIRST, LAST or UNIQUE find query. All the public overloaded methods
    * (findFirst, findLast and findUnique) will call this private implementation using the 
    * appropriate parameters. Internally this method uses the DynamicQueryHelper.parseFindQuery
    * static parser to return a FindQuery object using the passed predicate and locking mode.
    * After the FindQuery is obtained, the appropriate query is executed (FIRST, LAST or UNIQUE)
    * and the result is returned to caller method.
    * <p> 
    * If the method fails, it does not raise an error but displays a message. To suppress it,
    * the method should be called with NO-ERROR clause.
    *
    * @param   predicate
    *          Predicate expression for find that evaluates to the following syntax:
    *             [ WHERE [ logical-expression ] ] [ USE-INDEX index-name ]
    *          <ul>
    *             <li>Once evaluated, predicate-expression can contain only constants and
    *                   unabbreviated references to fields from the buffer.
    *             <li>The predicate-expression itself can be built using a concatenation of
    *                   character expressions.
    *          </ul>
    * @param   mode
    *          Search mode, one of FIRST, LAST or UNIQUE.
    * @param   lock
    *          Lock type. May additionally have no-wait flag set.
    * @param   substBuffers
    *          A list of substitution buffers.
    *
    * @return  {@code true} if operation succeeds, else {@code false}.
    */
   logical _find(String predicate, FindMode mode, LockType lock, Buffer... substBuffers)
   {
      final String qName = "find_" + mode + "_" + lock;
      List<Buffer> substs = new ArrayList<>();
      if (substBuffers.length > 0)
      {
         substs.addAll(Arrays.asList(substBuffers));
      }
      if (parentDataSource != null)
      {
         handle hbuf = parentDataSource.getDatasetBuffer();
         if (hbuf._isValid())
         {
            BufferImpl buf = (BufferImpl) hbuf.getResource();
            if (!substs.contains(buf))
            {
               substs.add(buf);
            }
            buf = buf.beforeBufferNative();
            if (buf != null && !substs.contains(buf))
            {
               substs.add(buf);
            }
         }
      }
      P2JQuery query = DynamicQueryHelper.parseFindQuery(this, predicate, lock, qName, substs);
      if (query == null)
      {
         String msg = "Invalid WHERE clause in FIND method for buffer " + buffer().getLegacyName() + " (10041)";
         ErrorManager.recordOrShowError(
               new int[] { 10041 },
               new String[] { msg },
               false, false, false, true, false);  // no DOT at the end of error message!
         return logical.FALSE;
      }
      
      boolean success = false;
      try
      {
         switch (mode)
         {
            case FIRST:
               query.first(lock);
               break;
            
            case LAST:
               query.last(lock);
               break;
            
            case UNIQUE:
               query.unique(lock);
               break;
         }
         if (!ErrorManager.isPendingError())
         {
            success = _available();
         }
      }
      catch (ErrorConditionException e)
      {
         // the error condition already has been handled by the ErrorManager, so we just return failure;
         // unexpected, unchecked exceptions are allowed to unwind the stack
         success = false;
      }
      
      return logical.of(success);
   }
   
   /**
    * Detaches the {@code DataSource} from the temp-table buffer in a {@code DataSet} object.
    */
   void detachDataSourceImpl()
   {
      // TODO: check if something is vetoing the detach process
      if (dataSource != null)
      {
         dataSource.attach(null, null);
         dataSource = null;
         if (isAfterBuffer())
         {
            peerBuffer.dataSource = null;
         }
      }
      
      attachedPairlist = null;
      dataSrcMappingStr = null;
      dataSrcMapping = null;
      dstFieldMeta = null;
   }
   
   /**
    * Clear the {@link #reusableQuery query} used for FILL operations.
    */
   void clearQuery()
   {
      if (reusableQuery != null)
      {
         reusableQuery.setDataRelation(null);
         reusableQuery.queryClose();
         reusableQuery = null;
      }
   }
   
   /**
    * Internal implementation of FIND-CURRENT() method.
    * <p>
    * Changes the lock type for the record currently in the buffer. Refreshes the record if it was
    * changed by other client. Resets the CURRENT-CHANGED attribute accordingly.
    *
    * @param   lockType
    *          Extended lock type (includes the NO-WAIT status).
    *
    * @return  {@code true} if FIND-CURRENT() succeeds (the lock type is changed), else {@code false}.
    */
   private logical _current(LockType lockType)
   {
      RecordBuffer recBuf = buffer();
      
      // make sure to ignore errors caused by LockUnavailableException 
      try
      {
         recBuf.txHelper.errHlp.ignore(() ->
         {
            recBuf.armCurrentChanged();
            recBuf.reload(lockType, false);
         });
      }
      
      finally
      {
         recBuf.updateCurrentChanged();
      }
      
      // NOTE: the reference manual states that if lock operation fails, a message is displayed.
      //       However, this is not how the behaviour was observed. Actually there is no error
      //       whatsoever. Instead, the buffer is discarded after a failure to acquire the lock.
      //       The error was not observable, in fact at all, in all my testcases.
      return logical.of(recBuf.isAvailable());
   }
   
   /**
    * Implementation of the merge changes. Always to be called for the AFTER-BUFFER. It will also
    * process the changes for the BEFORE BUFFER (the DELETE changes only), too.
    *
    * @param   porigBuf
    *          The original AFTER buffer.
    * @param   allCopyMode
    *          Use {@code true} to indicates that all rows of the source after-image table must be
    *          merged.
    *
    * @return  {@code true} on success. When {@code false} is returned the error code was already
    *          configured in {@code ErrorManager}.
    */
   private boolean mergeChangesImpl(BufferImpl porigBuf, boolean allCopyMode)
   {
      if (!isAfterBuffer() || !porigBuf.isAfterBuffer())
      {
         ErrorManager.recordOrShowError(11925, doGetName());
         // GET/MERGE-CHANGES require a BEFORE-TABLE for table <name>.
         return false;
      }
      
      logical ok = new logical(true);
      TemporaryBuffer afterBuffer = (TemporaryBuffer) buffer();
      TemporaryBuffer beforeBuffer = (TemporaryBuffer) peerBuffer.buffer();
      Class<? extends Record> origAfterImplClass = porigBuf.buffer().getDmoInfo().getImplementationClass();
      Class<? extends Record> origBeforeImplClass = porigBuf.beforeBufferNative()
            .buffer().getDmoInfo().getImplementationClass();
      Class<? extends Record> afterImplClass = afterBuffer.getDmoInfo().getImplementationClass();
      Session session = afterBuffer.getPersistence().getSession(Persistence.TEMP_CTX);
      beforeBuffer.readAllRows(
         (dmo) -> {
            if (!ok.booleanValue())
            {
               return;
            }
            
            // prepare the original before image
            TempRecord origBefRecord = null;
            try
            {
               origBefRecord = (TempRecord) session.get(origBeforeImplClass, dmo._originRowid());
            }
            catch (PersistenceException | ConditionException e)
            {
               // origBefRecord stays null
            }
            if (origBefRecord == null)
            {
               // this should not happen. The origin-rowid of the dmo should not be unknown! If it was not 
               // found then the changes in the original table were already accepted (?)
               
               ErrorManager.recordOrShowError(11928, doGetName(), porigBuf.doGetName());
               // INVALID origin-rowid of the change table <name> of <name>.
               ErrorManager.recordOrShowError(11926);
               // Found unexpected error while merging changes.
               
               ok.assign(false);
               return;
            }
            
            Integer errFlags = dmo._errorFlags();
            if (errFlags != null && errFlags != 0)
            {
               BufferImpl porigBefBuf = porigBuf.peerBuffer;
               porigBefBuf.buffer().loadRecord(origBefRecord);
               
               // if the record's any error/rejected markers were set, drop it from merge operation
               ok.assign(porigBefBuf.rejectRowChanges());
            }
            else
            {
               // prepare the original after image and put it in the [porigBuf] buffer
               TempRecord origRecord = null;
               try
               {
                  origRecord = (TempRecord) session.get(origAfterImplClass, origBefRecord._peerRowid());
               }
               catch (PersistenceException | ConditionException e)
               {
                  // origRecord stays null
               }
               porigBuf.buffer().loadRecord(origRecord);
               
               // prepare the after image/record in current buffer 
               TempRecord afterRecord;
               try
               {
                  afterRecord = (TempRecord) session.get(afterImplClass, dmo._peerRowid());
               }
               catch (PersistenceException | ConditionException e)
               {
                  afterRecord = null;
               }
               afterBuffer.loadRecord(afterRecord);
               
               // run the merge-row-changes code for prepared records 
               ok.assign(mergeRowChangesImpl(this.ref(), porigBuf.ref()));
            }
         });
      
      return ok.booleanValue();
   }
   
   /**
    * Implementation of the row merge changes. Always to be called for the AFTER-BUFFER.
    *
    * @param   changesBuf
    *          The changes AFTER buffer.
    * @param   origAfterBuf
    *          The original AFTER buffer.
    *
    * @return  {@code true} on success. When {@code false} is returned the error code was already
    *          configured in {@code ErrorManager}.
    */
   private static boolean mergeRowChangesImpl(BufferImpl changesBuf, BufferImpl origAfterBuf)
   {
      if (changesBuf.rowState().isUnknown() || changesBuf.rowState().intValue() == Buffer.ROW_UNMODIFIED)
      {
         return true; // unmodified record, nothing to merge
      }
      
      int recState = changesBuf.rowState().intValue();
      boolean isError = changesBuf.error().booleanValue();
      
      if (recState != Buffer.ROW_DELETED)
      {
         if (changesBuf.beforeBufferType == BeforeType.AFTER && isError)
         {
            changesBuf.peerBuffer.findByRowID(changesBuf.beforeRowid());
            changesBuf = changesBuf.peerBuffer;
         }
         else if (changesBuf.beforeBufferType == BeforeType.BEFORE && !isError)
         {
            // on error keep the original (before), on non error, keep the change (after)
            changesBuf.peerBuffer.findByRowID(changesBuf.afterRowid());
            changesBuf = changesBuf.peerBuffer;
         }
      }
      
      TempRecord dmo = (TempRecord) changesBuf.buffer().getCurrentRecord();
      if (dmo == null)
      {
         LOG.log(Level.SEVERE, "mergeRowChangesImpl: null TempRecord in changesBuf");
         return true;
      }
      
      Long originB4Rowid = dmo._originRowid();
      BufferImpl origB4Buffer = origAfterBuf.peerBuffer;
      
      if (recState == ROW_DELETED)
      {
         if (dmo._rowState() == Buffer.ROW_DELETED &&
             origB4Buffer.findByRowID(new rowid(originB4Rowid)).getValue())
         {
            if (origB4Buffer.rowState().intValue() != Buffer.ROW_DELETED)
            {
               ErrorManager.recordOrShowError(11928, changesBuf.doGetName(), origAfterBuf.doGetName());
               // INVALID origin-rowid of the change table <name> of <name>.
               ErrorManager.recordOrShowError(11926);
               // Found unexpected error while merging changes.
               return false;
            }
            // just remove the BEFORE-IMAGE, the AFTER-IMAGE is long gone 
            origB4Buffer.setUpBeforeBuffer(true);
            origB4Buffer.deleteRecord();
            origB4Buffer.setUpBeforeBuffer(false);
         }
         return true;
      }
      // else CREATE / MODIFIED
      
      if (originB4Rowid == null)
      {
         ErrorManager.recordOrShowError(11923, origAfterBuf.doGetName(), changesBuf.doGetName());
         // MERGE-CHANGES run on original table <name> that does not match the ORIGIN-HANDLE in the GET-CHANGES table <name>.
         return false;
      }
      else if (!origB4Buffer.findByRowID(new rowid(originB4Rowid)).getValue())
      {
         ErrorManager.recordOrShowError(11928, changesBuf.doGetName(), origAfterBuf.doGetName());
         // INVALID origin-rowid of the change table <name> of <name>.
         ErrorManager.recordOrShowError(11926);
         // Found unexpected error while merging changes.
         return false;
      }
      
      Long afterRowid = origB4Buffer.afterRowid().getValue();
      if (afterRowid == null)
      {
         ErrorManager.recordOrShowError(11926);
         // Found unexpected error while merging changes.
         return false;
         // exits lambda, continue with next record.
         // The error message will be displayed multiple times, one for each record.
      }
      else if (!origAfterBuf.findByRowID(new rowid(afterRowid)).getValue())
      {
         ErrorManager.recordOrShowError(11923, origAfterBuf.doGetName(), changesBuf.doGetName());
         // MERGE-CHANGES run on original table <name> that does not match the ORIGIN-HANDLE in the GET-CHANGES table <name>.
         return false;
         // exits lambda, continue with next record.
         // The error message will be displayed multiple times, one for each record.
      }
      
      RecordBuffer origAfterRecBuf = origAfterBuf.buffer();
      TempRecord dstRec = (TempRecord) origAfterRecBuf.getCurrentRecord();
      Record snapshot = null;
      
      if (recState == ROW_CREATED && isError)
      {
         origAfterBuf.deleteRecord(); // on error do a rollback = delete
      }
      else
      {
         if (dstRec._rowState() == null || dstRec._rowState() == Buffer.ROW_UNMODIFIED)
         {
            ErrorManager.recordOrShowError(11928, changesBuf.doGetName(), origAfterBuf.doGetName());
            // INVALID origin-rowid of the change table <name> of <name>.
            ErrorManager.recordOrShowError(11926);
            // Found unexpected error while merging changes.
            return false;
         }
         snapshot = dstRec.snapshot();
         
         // accepts the changes by copying the after-image table rows to the corresponding
         // after-image table rows in the original DataSet temp-table.
         BaseRecord.copy(dstRec, dmo, false);
      }
      
      Long oldBefore = dstRec._peerRowid();
      if (origAfterBuf.isAfterBuffer() && oldBefore != null)
      {
         // removes the before-image table row (if it has one).
         if (origB4Buffer.findByRowID(new rowid(oldBefore)).getValue())
         {
            origB4Buffer.setUpBeforeBuffer(true);
            origB4Buffer.deleteRecord();
            origB4Buffer.setUpBeforeBuffer(false);
         }
      }
      
      // also:
      //    sets the BEFORE-ROWID attribute of the row in the after-image table to the Unknown value (?)
      //    sets the ROW-STATE of the row in the after-image table to ROW-UNMODIFIED (0)
      //    resets ERROR/REJECTED attributes and ERROR-STRING to unknown value.
      dstRec._peerRowid(null);
      dstRec._errorFlags(null);
      dstRec._errorString(null);
      dstRec._peerRowid(null);
      dstRec._rowState(Buffer.ROW_UNMODIFIED);
      
      try
      {
         Map<String, List<Integer>> props = new HashMap<>();
         props.put(Buffer.ROW_STATE_FIELD, null);
         props.put(Buffer.AFTER_ROWID_FIELD, null);
         props.put(Buffer.ERROR_FLAG_FIELD, null);
         props.put(Buffer.ERROR_STRING_FIELD, null);
         origAfterRecBuf.getChangeBroker().stateChanged(
               RecordChangeEvent.Type.UPDATE, origAfterRecBuf, dstRec, snapshot, props);
      }
      catch (PersistenceException e)
      {
         ErrorManager.recordOrShowError(11926);
         // Found unexpected error while merging changes.
         return false;
      }
      
      // great, return with success
      return true;
   }
   
   /**
    * Sets the {@code _originRowid} internal field and register the change with the {@code ChangeBroker} to
    * make sure the change is persisted.
    * 
    * @param   val
    *          The new value for {@code _originRowid} field. May be {@code null} for an unknown.
    */
   private void setOriginRowidImpl(Long val)
   {
      RecordBuffer recBuf = buffer();
      TempRecord dmo = (TempRecord) recBuf.getCurrentRecord();
      TempRecord snapshot = (TempRecord) dmo.deepCopy();
      dmo._originRowid(val);
      try
      {
         Map<String, List<Integer>> props = new HashMap<>();
         props.put(Buffer.ORIGIN_ROWID_FIELD, null);
         recBuf.getChangeBroker().stateChanged(RecordChangeEvent.Type.UPDATE, recBuf, dmo, snapshot, props);
      }
      catch (PersistenceException e)
      {
         LOG.warning("Caught exception:", e);
      }
   }
      
   /**
    * Sets the {@code _dataSourceRowid} internal field and register the change with the {@code ChangeBroker}
    * to make sure the change is persisted.
    * 
    * @param   val
    *          The new value for {@code _dataSourceRowid} field. May be {@code null} for an unknown.
    */
   private void setDatasourceRowidImpl(Long val)
   {
      RecordBuffer recBuf = buffer();
      TempRecord dmo = (TempRecord) recBuf.getCurrentRecord();
      TempRecord snapshot = (TempRecord) dmo.deepCopy();
      dmo._datasourceRowid(val);
      try
      {
         Map<String, List<Integer>> props = new HashMap<>();
         props.put(Buffer.DATA_SOURCE_ROWID_FIELD, null);
         recBuf.getChangeBroker().stateChanged(RecordChangeEvent.Type.UPDATE, recBuf, dmo, snapshot, props);
      }
      catch (PersistenceException e)
      {
         LOG.warning("Caught exception:", e);
      }
   }
   
   /**
    * Partial implementation of {@code SAVE-ROW-CHANGES} method. Validates some parameters and
    * calls the final worker.
    *
    * @param   bufNo
    *          The 0-base buffer index in the attached {@code DataSource}. 
    * @param   skip
    *          A list of field names that should not be assigned after a new row is created
    *          (comma-separated).
    * @param   noLobs
    *          Use {@code true} to specify the BLOB and CLOB fields should be ignored in the save
    *          operation.
    *
    * @return  {@code true} if operation is successful.
    */
   private logical saveRowChangesImpl(int bufNo, String skip, boolean noLobs)
   {
      if (!isBeforeBuffer())
      {
         ErrorManager.recordOrShowError(11938, doGetName());
         // SAVE-ROW-CHANGES must be run on the BEFORE table of buffer <name>.
         return logical.FALSE;
      }
      
      BufferImpl after = afterBufferNative();
      DataSource dsrc = after.ref().dataSource;
      if (dsrc == null || !dsrc.valid())
      {
         ErrorManager.recordOrShowError(11942, doGetName(), after.doGetName());
         // SAVE-ROW-CHANGES for BEFORE buffer <name>, requires that the corresponding AFTER dataset member buffer <name> be attached to a DATA-SOURCE.
         return logical.FALSE;
      }
      
      return saveRowChangesImpl2(dsrc, after, bufNo, skip, noLobs);
   }
   
   /**
    * Implementation of {@code SAVE-ROW-CHANGES} method. Validates some parameters and
    * calls the final worker.
    *
    * @param   bufName
    *          The buffer name in the attached {@code DataSource}. 
    * @param   skip
    *          A list of field names that should not be assigned after a new row is created
    *          (comma-separated).
    * @param   noLobs
    *          Use {@code true} to specify the BLOB and CLOB fields should be ignored in the save
    *          operation.
    *
    * @return  {@code true} if operation is successful.
    */
   private logical saveRowChangesImpl(String bufName, String skip, boolean noLobs)
   {
      if (!isBeforeBuffer())
      {
         ErrorManager.recordOrShowError(11938, doGetName());
         // SAVE-ROW-CHANGES must be run on the BEFORE table of buffer <name>.
         return logical.FALSE;
      }
      
      BufferImpl after = afterBufferNative();
      DataSource dsrc = after.ref().dataSource;
      if (dsrc == null || !dsrc.valid())
      {
         ErrorManager.recordOrShowError(11942, doGetName(), after.doGetName());
         // SAVE-ROW-CHANGES for BEFORE buffer <name>, requires that the corresponding AFTER dataset member buffer <name> be attached to a DATA-SOURCE.
         return logical.FALSE;
      }
      
      return saveRowChangesImpl2(dsrc, after, dsrc.getSourceBufferIndex(bufName), skip, noLobs);
   }
   
   /**
    * Implementation of {@code SAVE-ROW-CHANGES} method.
    *
    * @param   dSrc
    *          The {@code DataSource} that will be updated.
    * @param   after
    *          The AFTER-BUFFER to which the {@code DataSource} is attached.
    * @param   bufNo
    *          The 0-base buffer index in the attached {@code DataSource}.
    * @param   skip
    *          A list of field names that should not be assigned after a new row is created
    *          (comma-separated).
    * @param   noLobs
    *          Use {@code true} to specify the BLOB and CLOB fields should be ignored in the save
    *          operation.
    *
    * @return  {@code true} if operation is successful.
    */
   private logical saveRowChangesImpl2(DataSource dSrc,
                                       BufferImpl after,
                                       int bufNo,
                                       String skip,
                                       boolean noLobs)
   {
      if (!_available())
      {
         ErrorManager.recordOrShowError(11935, false, doGetName());
         // SAVE-ROW-CHANGES buffer <name> not available.
         return logical.FALSE;
      }
      
      if (bufNo < 0 || bufNo >= dSrc._getNumSourceBuffers())
      {
         ErrorManager.recordOrShowError(11906, false);
         // Unable to identify data-source buffer for SAVE-ROW-CHANGES.
         return logical.FALSE;
      }
      
      BufferImpl srcBuff = dSrc.getSourceBufferNative(bufNo + 1);
      Set<String> skipFields = new HashSet<>();
      if (skip != null && !skip.isEmpty())
      {
         String[] skipList = skip.split(",");
         for (String tok : skipList)
         {
            if (tok.contains(".") || !srcBuff.hasField(tok))
            {
               ErrorManager.recordOrShowError(11909, false, tok, srcBuff.doGetName());
               // Unable to find sequence field  <name> in table <name> during SAVE-ROW-CHANGES.
               return logical.FALSE;
            }
            skipFields.add(tok);
         }
      }
      
      String saveWhere = dSrc.getSaveWhereString(bufNo + 1).toJavaType();
      int rowState = rowState().intValue();
      character allPairs = new character();
      character allSrcFields = new character();
      
      if (rowState != ROW_DELETED)
      {
         StringBuilder sb = new StringBuilder();
         StringBuilder sbAllSrc = new StringBuilder();
         
         for (Pair<String, DataSource.FieldReference> entry : after.dataSrcMapping)
         {
            String srcFld = entry.getValue().getName();
            String dsFld = entry.getKey();
            
            if (sbAllSrc.length() != 0)
            {
               sbAllSrc.append(",");
            }
            sbAllSrc.append(srcFld);
            
            if (entry.getValue().getBuffer() == srcBuff && !skipFields.contains(srcFld))
            {
               if (sb.length() != 0)
               {
                  sb.append(",");
               }
               sb.append(dsFld).append(",").append(srcFld);
            }
         }
         allPairs.assign(sb.toString());
         allSrcFields.assign(sbAllSrc.toString());
      }
      
      logical retval = new logical();
      OnPhrase[] onPhrase0 = new OnPhrase[]{new OnPhrase(Condition.ERROR, Action.THROW, "save-row-changes-label")};
      doBlock(TransactionType.FULL, "save-row-changes-label", onPhrase0, new Block() {
         @Override
         public void body()
         {
            if (rowState == ROW_CREATED)
            {
               srcBuff.create();
               
               after.findByRowID(afterRowid());
               logical ok = TemporaryBuffer.copy(
                     srcBuff.buffer(), after.buffer(), allSrcFields, allPairs, logical.of(noLobs));
               
               if (ok.getValue())
               {
                  after.setDatasourceRowidImpl(srcBuff.rowID().getValue());
               }
               
               if (!ok.booleanValue())
               {
                  setErrorOnSave();
               }
               retval.assign(ok);
               return;
            }
            
            // first, detect whether the record loaded in buffer has been changed. If so (changed or modified), 
            // update the DATA-SOURCE-MODIFIED attribute for me, my temp-table, the temp-table of the
            // after-buffer, and dataset. The newly created record will not affect this attribute.
            // NOTE: the attribute/function name suggests that the data source buffer was altered but, from my
            //       tests, changing the source buffers does not affect this attribute. Having a modified or
            //       deleted before-image loaded in before-buffer(this) will set the attribute, instead.
            if (rowState == ROW_MODIFIED || rowState == ROW_DELETED)
            {
               BufferImpl.this.setDataSourceModified(true);
               tableHandleNative().setDataSourceModified(true);
               after.tableHandleNative().setDataSourceModified(true);
               peerBuffer.dataSet.setDataSourceModified(true);
            }
            
            logical ok = new logical();
            ErrorManager.silent(() -> ok.assign(
                  srcBuff._find(saveWhere, FindMode.UNIQUE, LockType.EXCLUSIVE, BufferImpl.this)));
            
            if (ok.isUnknown() || !ok.getValue() || !srcBuff._available())
            {
               if (rowState == ROW_DELETED)
               {
                  // this is a silent acceptance of the fact that the record was deleted from both
                  // locations (source and dataset table)
                  
                  // in any case, update peer: the deleted row is now gone for good
                  setOriginRowidImpl(null);
                  retval.assign(true);
                  return;
               }
               
               ErrorManager.recordOrShowError(11912, false, srcBuff.doGetName());
               // Unable to find record for db buffer <name> during SAVE-ROW-CHANGES
               setErrorOnSave();
               retval.assign(false);
               return;
            }
            
            if (rowState == ROW_DELETED)
            {
               srcBuff.deleteRecord();
               // update peer: the deleted row is now gone for good
               setOriginRowidImpl(null);
               retval.assign(true);
               return;
            }
            
            if (rowState == ROW_MODIFIED)
            {
               after.findByRowID(afterRowid());
               
               boolean preferDataset = dSrc.getPreferDataset().getValue();
               boolean mergeByField = dSrc.getMergeByField().getValue();
               ArrayList<TableMapper.LegacyFieldInfo> myFields =
                     TableMapper.getLegacyOrderedList(BufferImpl.this, true);
               logical res = new logical();
               
               try
               {
                  if (preferDataset)
                  {
                     if (!mergeByField)
                     {
                        // overwrite and lose any DB changes to entire row 
                        res.assign(TemporaryBuffer.copy(srcBuff.buffer(),
                                                        after.buffer(),
                                                        allSrcFields,
                                                        allPairs,
                                                        logical.of(noLobs)));
                     }
                     else
                     {
                        // save DS changes. Non overlapping DB field changes are kept.
                        StringBuilder sb = new StringBuilder();
                        for (int i = 0; i < myFields.size(); i++)
                        {
                           TableMapper.LegacyFieldInfo fieldInfo = myFields.get(i);
                           String fldName = fieldInfo.getLegacyName();
                           boolean modified;
                           
                           if (fieldInfo.getExtent() == 0)  // scalar mode
                           {
                              BaseDataType fldVal = Session.PK.equals(fldName) ? after.rowID()
                                                                               : after.dereference(fldName);
                              modified = !areBDTsEqual(fldVal, dereference(fldName));
                           }
                           else //extent mode
                           {
                              modified = false;
                              BaseDataType[] srcVals = (BaseDataType[]) srcBuff.derefPoly(fldName);
                              BaseDataType[] originalVals = (BaseDataType[]) derefPoly(fldName);
                              for (int j = 0; j < srcVals.length; j++)
                              {
                                 if (!areBDTsEqual(srcVals[j], originalVals[j]))
                                 {
                                    modified = true;
                                    break;
                                 }
                              }
                           }
                           
                           if (modified)
                           {
                              DataSource.FieldReference srcField = getMappedField(after, fldName);
                              if (srcField == null || srcField.getBuffer() != srcBuff)
                              {
                                 continue;
                              }
                              
                              String srcName = srcField.getName();
                              if (sb.length() != 0)
                              {
                                 sb.append(",");
                              }
                              sb.append(fldName).append(",").append(srcName);
                           }
                        }
                        character dsChangedPairs = new character(sb.toString());
                        
                        res.assign(TemporaryBuffer.copy(srcBuff.buffer(),
                                                        after.buffer(),
                                                        allSrcFields,
                                                        dsChangedPairs,
                                                        logical.of(noLobs)));
                     }
                  }
                  else
                  {
                     if (!mergeByField)
                     {
                        // if any field was modified in database raise error, otherwise update all fields
                        for (int j = 0; j < myFields.size(); j++)
                        {
                           TableMapper.LegacyFieldInfo fieldInfo = myFields.get(j);
                           String fldName = fieldInfo.getLegacyName();
                           DataSource.FieldReference srcField = getMappedField(after, fldName);
                           if (srcField == null || srcField.getBuffer() != srcBuff)
                           {
                              continue;
                           }

                           String srcName = srcField.getName();
                           boolean changed;

                           if (fieldInfo.getExtent() == 0) // scalar mode
                           {
                              BaseDataType srcVal = Session.PK.equals(srcName) ? srcBuff.rowID()
                                    : srcBuff.dereference(srcName);

                              changed = !areBDTsEqual(srcVal, (dereference(fldName)));
                           }
                           else // extent mode
                           {
                              changed = false;
                              BaseDataType[] srcVals = (BaseDataType[]) srcBuff.derefPoly(srcName);
                              BaseDataType[] originalVals = (BaseDataType[]) derefPoly(fldName);
                              for (int i = 0; i < srcVals.length; i++)
                              {
                                 if (!areBDTsEqual(srcVals[j], originalVals[j]))
                                 {
                                    changed = true;
                                    break;
                                 }
                              }
                           }

                           // before-buffer and source must be the same (otherwise, there was a change
                           // in the database).
                           if (changed)
                           {
                              // synchronize before & after buffers with the source
                              TemporaryBuffer.copy(after.buffer(),
                                                   srcBuff.buffer(),
                                                   allSrcFields,
                                                   allPairs,
                                                   logical.of(noLobs));
                              setUpBeforeBuffer(true);
                              TemporaryBuffer.copy(buffer(),
                                                   srcBuff.buffer(),
                                                   allSrcFields,
                                                   allPairs,
                                                   logical.of(noLobs));
                              setUpBeforeBuffer(false);

                              // set ERROR & DATA-SOURCE-MODIFIED attributes
                              ErrorManager.recordOrShowError(11913, false, srcBuff.doGetName());
                              // SAVE-ROW-CHANGES found <name> record with conflicting change by another user.
                              setErrorOnSave();
                              res.assign(false);
                              break;
                           }
                        }
                        
                        if (res.isUnknown()) // ie. no 11913 was encountered
                        {
                           res.assign(TemporaryBuffer.copy(srcBuff.buffer(),
                                                           after.buffer(),
                                                           allSrcFields,
                                                           allPairs,
                                                           logical.of(noLobs)));
                        }
                     }
                     else
                     {
                        // attempt to save DS changes for each field. If the field was altered in source
                        // buffer raise error and return.
                        StringBuilder sb = new StringBuilder();
                        nextField: for (int i = 0; i < myFields.size(); i++)
                        {
                           TableMapper.LegacyFieldInfo fieldInfo = myFields.get(i);
                           String fldName = fieldInfo.getLegacyName();
                           boolean isExtent = fieldInfo.getExtent() != 0;
                           boolean sourceModified = false;
                           String srcName = null;

                           if (isExtent)
                           {
                              BaseDataType[] fldVals = (BaseDataType[]) after.derefPoly(fldName);
                              BaseDataType[] originalVals = (BaseDataType[]) derefPoly(fldName);

                              boolean modified = false;
                              for (int j = 0; j < fldVals.length; j++)
                              {
                                 if (!areBDTsEqual(fldVals[j], originalVals[j]))
                                 {
                                    modified = true;

                                    DataSource.FieldReference srcField = getMappedField(after, fldName);
                                    if (srcField == null || srcField.getBuffer() != srcBuff)
                                    {
                                       continue nextField;
                                    }

                                    srcName = srcField.getName();
                                    sourceModified = !areBDTsEqual(srcBuff.dereference(j + 1, srcName), originalVals[j]);
                                    break;
                                 }
                              }

                              if (!modified)
                              {
                                 continue;
                              }
                           }
                           else
                           {
                              BaseDataType fldVal;
                              BaseDataType originalVal;

                              if (Session.PK.equals(fldName))
                              {
                                 fldVal = after.rowID();
                                 originalVal = rowID();
                              }
                              else
                              {
                                 fldVal = after.dereference(fldName);
                                 originalVal = dereference(fldName);
                              }
                              
                              if (!areBDTsEqual(fldVal, originalVal))
                              {
                                 DataSource.FieldReference srcField = getMappedField(after, fldName);
                                 if (srcField == null || srcField.getBuffer() != srcBuff)
                                 {
                                    continue;
                                 }

                                 srcName = srcField.getName();
                                 BaseDataType srcVal = Session.PK.equals(srcName) ? srcBuff.rowID()
                                       : srcBuff.dereference(srcName);
                                 sourceModified = !areBDTsEqual(srcVal, originalVal);
                              }
                              else
                              {
                                 continue;
                              }
                           }
                           
                           if (sourceModified)
                           {
                              // synchronize before & after buffers with the source
                              TemporaryBuffer.copy(after.buffer(),
                                                   srcBuff.buffer(),
                                                   allSrcFields,
                                                   allPairs,
                                                   logical.of(noLobs));
                              setUpBeforeBuffer(true);
                              TemporaryBuffer.copy(buffer(),
                                                   srcBuff.buffer(),
                                                   allSrcFields,
                                                   allPairs,
                                                   logical.of(noLobs));
                              setUpBeforeBuffer(false);

                              // set ERROR & DATA-SOURCE-MODIFIED attributes
                              ErrorManager.recordOrShowError(11913, false, srcBuff.doGetName());
                              // SAVE-ROW-CHANGES found <name> record with conflicting change by another user.
                              res.assign(false);
                              break;
                           }

                           if (sb.length() != 0)
                           {
                              sb.append(",");
                           }
                           sb.append(fldName).append(",").append(srcName);
                        }
                        
                        if (res.isUnknown()) // ie. no 11913 was encountered
                        {
                           res.assign(TemporaryBuffer.copy(srcBuff.buffer(),
                                                           after.buffer(),
                                                           allSrcFields,
                                                           new character(sb.toString()),
                                                           logical.of(noLobs)));
                        }
                     }
                  }
               }
               catch (ErrorConditionException ex)
               {
                  ErrorManager.recordOrShowError(11910, srcBuff.doGetName());
                  // SAVE-ROW-CHANGES was unable to update the database table <table>.
                  setErrorOnSave();
                  retval.assign(false);
                  return;
               }
               
               srcBuff.release();
               if (!res.booleanValue())
               {
                  setErrorOnSave();
               }
               retval.assign(res);
               return;
            }
         }
      });
      
      if (retval.isUnknown())
      {
         // something has gone wrong, there is no other valid value for [rowState]
         setErrorOnSave();
         retval.assign(false);
      }
      
      return retval;
   }

   /**
    * A method for comparing BDTs, used to detect modified fields in saveRowChangesImpl2.
    * character fields are compared using their String representation, this matches
    * SAVE-ROW-CHANGES behavior in 4GL.
    *
    * @param   op1
    *          The first operand of the method.
    * @param   op2
    *          The second operand of the method.
    *
    * @return  true if the operands' values are equal.
    */
   private boolean areBDTsEqual(BaseDataType op1, BaseDataType op2)
   {
      if (op1 instanceof Text && op2 instanceof Text)
      {
         return  Text.valueEquals((Text) op1, (Text) op2);
      }
      else
      {
         return op1.equals(op2);
      }
   }
   
   /**
    * Returns the mapped {@code FieldReference} of a specific field of a buffer. To obtain the result to be
    * returned, the buffer's {@code dataSrcMapping} is iterated for a matching key. If the buffer hasn't a
    * datasource attached (hence the {@code dataSrcMapping}) is {@code null} or the field has no mapping, 
    * {@code null} is returned.
    * 
    * @param   buffer
    *          The buffer to be investigated.
    * @param   fldName
    *          The field name.
    *
    * @return  The {@code FieldReference} mapped to the field in the attached data source or {@code null}
    *          otherwise.
    */
   static DataSource.FieldReference getMappedField(BufferImpl buffer, String fldName)
   {
      if (buffer.dataSrcMapping == null)
      {
         return null;
      }
      
      for (Pair<String, DataSource.FieldReference> entry : buffer.dataSrcMapping)
      {
         if (entry.getKey().equalsIgnoreCase(fldName))
         {
            return entry.getValue();
         }
      }
      return null;
   }
   
   /**
    * Sets the {@code ERROR} attribute to {@code true} after a failed SAVE-ROW operation.
    * Affected entities are:
    * <ul>
    *    <li>current IMAGE (it is a BEFORE IMAGE)</li>
    *    <li>the AFTER-IMAGE of this BEFORE-IMAGE</li>
    *    <li>the TEMP-TABLE of current BUFFER</li>
    *    <li>the TEMP-TABLE of the AFTER-BUFFER</li>
    *    <li>the DATASET of the BUFFER</li>
    * </ul>
    * 
    * The  is not affected.
    * <p>
    * The attribute is not set to FALSE on a successful SAVE operation, the programmer must do
    * this by hand.
    */
   private void setErrorOnSave()
   {
      if (!isBeforeBuffer())
      {
         return;
      }
      
      error(true);               // current IMAGE (it is a BEFORE IMAGE)
      peerBuffer.error(true);    // the AFTER-IMAGE of this BEFORE-IMAGE
      
      TempTable beforeTable = TempTableBuilder.asTempTable((Temporary) this);
      TempTable afterTable = beforeTable.getAfterTableNative();
      
      beforeTable.error(true);   // the TEMP-TABLE of current BUFFER
      afterTable.error(true);    // the TEMP-TABLE of the AFTER-BUFFER
      
      DataSet ds = this.dataSet != null ? this.dataSet : peerBuffer.dataSet;
      if (ds != null && ds.valid())
      {
         ds.error(true);       // the DATASET of the BUFFER
      }
   }
   
   /**
    * Read record data from the ObjectInputStream 
    * 
    * @param   fields
    *          Record fields
    * @param   extents
    *          Record extents
    * @param   ois
    *          Source stream
    * 
    * @return  Values of the record fields or <code>null</code> if failed.
    */
   private Map<String, BaseDataType[]> readValues(ArrayList<TableMapper.LegacyFieldInfo> fields,
                                                  Map<String, Integer> extents,
                                                  ObjectInputStream ois)
   {
      Map<String, BaseDataType[]> values = new HashMap<>();
      try
      {
         for (int i = 0; i < fields.size(); i++)
         {
            TableMapper.LegacyFieldInfo lfi = fields.get(i);
            String prop = lfi.getJavaName();
            Integer extent = extents.get(prop);
            
            Class<? extends BaseDataType> cls = lfi.getDataType();
            BaseDataType[] val;
            
            if (extent == null)
            {
               // scalar case
               val = new BaseDataType[1];
               // call the default constructor
               val[0] = BaseDataTypeFactory.instantiate(cls);
               
               // deserialize
               val[0].readExternal(ois);
            }
            else
            {
               // array case
               val = new BaseDataType[extent];
               for (int j = 0; j < extent; j++)
               {
                  // call the default constructor
                  val[j] = BaseDataTypeFactory.instantiate(cls);
                  
                  // deserialize
                  val[j].readExternal(ois);
               }
            }
            values.put(prop, val);
         }
         
         return values;
      }
      catch (IOException | SecurityException | ReflectiveOperationException | IllegalArgumentException ex)
      {
         // TODO: if something happened during the copy but after the create, do we need to put
         //       the original record back in?
         ErrorManager.recordOrShowError(
               4952,
               "Couldn't extract raw value from source field of RAW-TRANSFER",
               false, false, true
         );
         return null;
      }
   }
   
   /**
    * FILL operation worker. 
    */
   private class FillWorker
   {
      /** Buffer reference */
      private final BufferImpl selfBuf = BufferImpl.this;

      /** Buffer handle */
      private final handle self = new handle(selfBuf);
      
      /** FILL result */
      private final logical fillOk = new logical(true);
      
      /** FILL in append mode flag */
      private final boolean isAppend = FM_APPEND.equals(fillMode);

      /** FILL in merge mode flag */
      private final boolean isMerge = FM_MERGE.equals(fillMode);

      /** FILL in replace mode flag */
      private final boolean isReplace= FM_REPLACE.equals(fillMode);
      
      /** Number of source buffers */
      private int srcBufCount;
      
      /** Source buffers */
      private BufferImpl[] srcBuffs;
      
      /** Target record buffer */
      private RecordBuffer recBuf;

      /** 
       * Fill the target DATA-SET.
       * 
       * @return 'true' on success
       */
      public logical fill()
      {
         CallbackData bed = (callbacks == null) ? null : callbacks.get(BEFORE_FILL_EVENT);
         boolean ok;
         if (bed != null)
         {
            ok = dataSet.invokeCallback(self, bed);
            if (!ok)
            {
               dataSet.error(true);
               return logical.FALSE;
            }
         }
         else if (dataSource == null || !dataSource.valid())
         {
            ErrorManager.recordOrShowError(11874, false, doGetName());
            // FILL requires an attached data-source for buffer %1, or an active before-fill callback procedure.
            dataSet.error(true);
            return logical.FALSE;
         }
         
         if (TempTableBuilder.asTempTable((Temporary) selfBuf).isTrackingChanges().getValue())
         {
            ErrorManager.recordOrShowError(11863, doGetName());
            // FILL is not valid for table <name> when TRACKING-CHANGES is on.
            dataSet.error(true);
            return logical.FALSE;
         }
         
         if (dataSource != null && dataSource.valid())
         {
            // check DATA-SOURCE sanity and decide whether we need to create a new query
            srcBufCount = dataSource._getNumSourceBuffers();
            if (srcBufCount > 1 && !dataSource.getQueryAsHandle()._isValid())
            {
               ErrorManager.recordOrShowError(11980);
               // Must assign a user query to a data-source that is a join before filling.
               ErrorManager.recordOrShowError(11875, doGetName());
               // FILL of buffer <name> requires a DATA-SOURCE QUERY to be prepared and assigned for this type of buffer.
               dataSet.error(true);
               return logical.FALSE;
            }
            
            // we can start FILLing the table now 
            if (FM_EMPTY.equalsIgnoreCase(fillMode))
            {
               deleteAll();
            }
            
            initBuffers();
            if (isReplace)
            {
               P2JIndex primaryIndex = recBuf.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
                  
                  dataSet.error(true);
                  return logical.FALSE;
               }
            }
            
            boolean queryOpen = true;
            boolean queryClose = true;
            QueryWrapper fillQuery = null;
            if (reusableQuery != null && dataSet.isReuseFillQuery())
            {
               queryClose = false;
               queryOpen = false;
               fillQuery = reusableQuery;
            }
            
            if (fillQuery == null)
            {
               handle hQuery = dataSource.getQueryAsHandle();
               if (!hQuery._isValid())
               {
                  dataSet.error(true);
                  return new logical(false);
               }
               fillQuery = (QueryWrapper) hQuery.getResource();
               if (fillQuery.prepareString().isUnknown())
               {
                  ErrorManager.recordOrShowError(11876, doGetName());
                  // FILL of buffer <name> with user-supplied query requires that the query be QUERY-PREPARED. 
                  dataSet.error(true);
                  return new logical(false);
               }
               
               if (dataSet.isReuseFillQuery())
               {
                  queryClose = false;
                  queryOpen = true;
                  reusableQuery = fillQuery;
               }
            }
            
            Boolean secondPassRequired = null;
            Optional<DataRelation> relation = dataSet.getRelation(selfBuf);
            List<DataRelation> relations = dataSet.getRelations(selfBuf, true, false, true);
            boolean recursive = relation.map(DataRelation::isRecursive)
                                        .map(logical::booleanValue).orElse(false);
            if (recursive)
            {
               secondPassRequired = secondPassRequired(relation.get(), fillQuery);
               if (secondPassRequired == null)
               {
                  ErrorManager.recordOrShowError(14282);
                  // Unable to recurse during FILL. (14282)
                  relations = Collections.emptyList();
               }
            }
            
            processDataSource(fillQuery, relations, queryOpen, queryClose);
            
            if (Boolean.TRUE.equals(secondPassRequired))
            {
               if (reusableQuery != null)
               {
                  clearQuery();
               }

               fillQuery.setDataRelation(relation.get());
               processDataSource(fillQuery, Collections.emptyList(), true, true);
            }
         }
         
         ok = dataSet.invokeCallback(self, (callbacks == null) ? null : callbacks.get(AFTER_FILL_EVENT));
         ok = ok && fillOk.booleanValue();
         dataSet.error(!ok);
         return logical.of(ok);
      }
      
      /**
       * Check if second pass is required (for recursive relation)
       * 
       * @param   relation
       *          The relation to be processed.
       * @param   fillQuery
       *          The source query for the FILL operation.
       *
       * @return  <code>Boolean.TRUE</code> if second pass is required.
       */
      private Boolean secondPassRequired(DataRelation relation, P2JQuery fillQuery)
      {
         if (!(fillQuery instanceof QueryWrapper))
         {
            return null;
         }
         
         QueryWrapper wrapper = (QueryWrapper) fillQuery;
         RecordBuffer[] buffers = wrapper.getRecordBuffers();
         String[] relFields = relation.getRelationFields(false);
         DmoMeta dmoMeta = buffers[0].getDmoInfo();

         for (String relField: relFields)
         {
            if (dmoMeta.byLegacyName(relField) == null)
            {
               BufferReference bufferRef = (BufferReference) relation.getChildBuffer().getResource();
               String legacyTable = bufferRef.buffer().getDmoInfo().legacyTable;
               ErrorManager.recordOrShowError(11883, dmoMeta.legacyTable, relField, legacyTable);
               return null;
            }
         }
         
         for (String relField: relation.getRelationFields(true))
         {
            if (dmoMeta.byLegacyName(relField) == null)
            {
               return Boolean.FALSE;
            }
         }

         return Boolean.TRUE;
      }

      /**
       * Process the DATA-SOURCE.
       * 
       * @param   fillQuery
       *          The source query for the FILL operation.
       * @param   childRelations
       *          The list of child relations.
       * @param   open
       *          Flag indicating that the fill query needs to be opened.
       * @param   close
       *          Flag indicating that the fill query needs to be closed.
       */
      private void processDataSource(P2JQuery           fillQuery, 
                                     List<DataRelation> childRelations, 
                                     boolean            open, 
                                     boolean            close)
      {
         ErrorHelper errHlp = recBuf.bufferManager.getTxHelper().errHlp;
         boolean wmOn = errHlp.isWarningMode();
         if (!wmOn)
         {
            errHlp.setWarningMode(true);
         }
         
         try
         {
            RecordBuffer buffer = buffer();
            
            if (open)
            {
               fillQuery.hintFullResults();
               fillQuery.queryOpen();
            }
            else
            {
               fillQuery.clearResults();
            }

            fillQuery.first();
            Long[] rowids = dataSource.getRestartRowids();
            if (rowids[0] != null)
            {
               fillQuery.repositionByID(rowids);
               fillQuery._getNext();
            }
            else
            {
               int value = dataSource.getRestartRow().intValue();
               if (value > 0)
               {
                  fillQuery.reposition(value);
                  fillQuery._getNext();
               }
            }

            // iterate the result and populate this buffer
            integer batchRemaining = new integer(batchSize > 0 ? batchSize : -1);
            logical testRes = new logical(); // safe to use, as is just assigned on each evaluation
            LogicalOp test = () -> 
            {
               testRes.assign(!fillQuery._isOffEnd() && batchRemaining.intValue() != 0); 
               return testRes;
            };
            String lbl = "fill-buffer-" + name;
            logical hasMore = new logical();
            Map<String, FieldStructure> fields = new HashMap<>();
            Runnable batchFill = () ->
            {
               recBuf.bufferManager.addDirtyBatchBuffer(buffer, null);
               if (!fillRow(fillQuery, hasMore, buffer, fields))
               {
                  BlockManager.next(lbl);
               }
            };
            Runnable update = () -> RecordBuffer.batch(batchFill);
            
            BlockManager.repeatToWhile(TransactionType.SUB, lbl, null, test,
                     new Block((Body) () -> 
                     {
                        if (!fillOk.getValue())
                        {
                           BlockManager.leave();
                        }
                        
                        if (callbacks != null && 
                            !dataSet.invokeCallback(self, callbacks.get(BEFORE_ROW_FILL_EVENT)))
                        {
                           fillOk.assign(false);
                           BlockManager.leave();
                        }

                        selfBuf.create();
                        if (!isMerge && !isReplace)
                        {
                           try
                           {
                              update.run();
                           }
                           catch (ErrorConditionException var10)
                           {
                              fillOk.assign(false);
                              ErrorManager.recordOrShowError(11878, false, "-1", doGetName());
                              // FILL got error -1 creating record <buffer-name> (11878)
                              BlockManager.leave();
                           }
                        }
                        else
                        {
                           recBuf.txHelper.errHlp.silent(update);
                           recBuf.txHelper.errHlp.setPending(false);
                        }

                        if (batchRemaining.intValue() > 0)
                        {
                           batchRemaining.decrement();
                        }

                        if (callbacks != null && 
                            !dataSet.invokeCallback(self, callbacks.get(AFTER_ROW_FILL_EVENT)))
                        {
                           selfBuf.release();
                           fillOk.assign(false);
                           BlockManager.leave();
                        }

                        int childRelationsSize = childRelations.size();
                        for (int i = 0; i < childRelationsSize; i++)
                        {
                           DataRelation rel = childRelations.get(i);
                           Buffer childBuffer = rel.getChildBufferNative();
                           if (rel._isReposition())
                           {
                              childBuffer.lastBatch(true);
                           }
                           else if (!rel._isRecursive() || childBuffer != selfBuf)
                           {
                              fillOk.assign(childBuffer.fill().getValue());
                              if (!fillOk.getValue())
                              {
                                 break;
                              }
                           }
                        }

                        selfBuf.release();
                        if (!fillOk.getValue())
                        {
                           BlockManager.leave();
                        }

                        hasMore.assign(fillQuery._getNext());
                     }));
            if (batchSize > 0 && batchRemaining.intValue() != 0 && childRelations.isEmpty())
            {
               // the batch ended before [batchSize] records were processed
               lastBatch = true;
            }
            else if (!hasMore.booleanValue())
            {
               // the last invocation to fill-query:get-next() returned false so there are no more batches
               lastBatch = true;
            }
         }
         finally
         {
            dataSource.updateFillRowidInfo(fillOk.getValue());
            if (close)
            {
               fillQuery.setDataRelation(null);
               fillQuery.queryClose();
            }
            
            selfBuf.release();
            if (!wmOn)
            {
               errHlp.setWarningMode(false);
            }
         }
      }
      
      /**
       * Initialize buffers
       */
      private void initBuffers()
      {
         srcBuffs = new BufferImpl[srcBufCount];

         for (int k = 0; k < srcBufCount; ++k)
         {
            srcBuffs[k] = (BufferImpl) dataSource.getSourceBuffer(k + 1).unwrapBuffer();
            srcBuffs[k].buffer().initialize();
         }

         recBuf = buffer();
         recBuf.initialize();
         flushAll();
      }

      /**
       * Populate the target record by iterating the data-source mapping and copying values between paired
       * fields.
       *
       * @param   fillQuery
       *          The source query.
       * @param   hasMore
       *          Flag indicating that more records are available.
       * @param   dstBuffer
       *          The destination record buffer.
       * @param   fields
       *          A map of resolved {@link FieldStructure} for each destination field.
       *
       * @return  {@code true} on success.
       */
      private boolean fillRow(P2JQuery                    fillQuery, 
                              logical                     hasMore, 
                              RecordBuffer                dstBuffer, 
                              Map<String, FieldStructure> fields)
      {
         int dataSrcMappingSize = dataSrcMapping.size();
         for (int i = 0; i < dataSrcMappingSize; i++)
         {
            Pair<String, DataSource.FieldReference> entry = dataSrcMapping.get(i);
            String myField = entry.getKey();
            DataSource.FieldReference srcField = entry.getValue();

            BufferImpl srcBuffer = srcField.getBuffer();
            if (srcField.getName().equals(Session.PK))
            {
               dereference(myField, srcBuffer.buffer().rowID());
            }
            else
            {
               assign(myField, srcField, srcBuffer, dstBuffer, fields);
            }
         }

         if (srcBufCount == 1)
         {
            TempRecord newRec = (TempRecord) recBuf.getCurrentRecord();
            newRec._datasourceRowid(srcBuffs[0].buffer()._rowID());
         }

         if (isMerge || isReplace || isAppend)
         {
            boolean validated = false;
            try
            {
               validated = recBuf.validate(isAppend);
            }
            catch (ValidationException | ErrorConditionException e)
            {
               if (isAppend)
               {
                  // append mode: abort the fill operation
                  recBuf.release(true, false);
                  throw new ErrorConditionException(
                           "FILL: Failed with unique validation error in APPEND mode.", e);
               }
            }

            // was there a unique conflict with this record?
            if (!validated)
            {
               if (isMerge)
               {
                  // merge mode: ignore it, just skip to next record
                  TransactionManager.disableLoopProtection(null);
                  hasMore.assign(fillQuery._getNext());
                  // we need to release the newly created record as if undone. We cannot call
                  // BlockManager.undoNext(lbl) here as it will affect the buffer(s) from the query
                  // driving the fill operation.
                  recBuf.release(true, false); // I.e. drop() - this form avoids a buffer() lookup
                  return false;
               }
               else if (isReplace)
               {
                  // replace mode: eliminate competition, then try again on clean ground
                  try
                  {
                     OrmUtils.dropUniqueIndexConflicts(recBuf,
                                                       recBuf.getPersistence(),
                                                       recBuf.getMultiplexID());
                  }
                  catch (PersistenceException e)
                  {
                     throw new ErrorConditionException(
                              "FILL: Failed to make room for new record in REPLACE mode", e);
                  }
                  // now there should not be any collisions and the new record should be flushed
               }
            }
         }

         return true;
      }

      /**
       * Flush buffers
       */
      private void flushAll()
      {
         boolean inTx = false;

         try
         {
            inTx = recBuf.persistenceContext.beginTransaction(null);
            recBuf.flush();

            for (BufferImpl srcBuff : srcBuffs)
            {
               srcBuff.buffer().flush();
            }
         }
         catch (PersistenceException | ValidationException exc)
         {
            ErrorManager.recordOrThrowError(exc);
         }
         finally
         {
            if (inTx)
            {
               try
               {
                  recBuf.persistenceContext.commit();
               }
               catch (PersistenceException exc)
               {
                  ErrorManager.recordOrThrowError(exc);
               }
            }
         }
      }
   }
   
   /**
    * A structure to cache the resolved information for the destination field, used during FILL operations. 
    */
   private static class FieldStructure
   {
      /** The resolved extent index. */
      public int dstIndex;
      
      /** The resolved field name. */
      public String crtField;
      
      /** The associated property for this field. */
      public Property dstProp;
      
   }
}