# Persistence.java

    /*
    ** Module   : Persistence.java
    ** Abstract : Provides persistence services using FWD ORM
    **
    ** Copyright (c) 2004-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description----------------------------------
    ** 001 ECF 20050830   @22419 Created initial version. Provides bootstrap
    **                           services to configure a Hibernate session
    **                           factory per database.
    ** 002 ECF 20050902   @22480 Added essential query services; refactored.
    **                           This includes scrollable cursor queries,
    **                           individual record loads into a re-used data
    **                           model object, and enforced, unique queries.
    **                           Refactored all bootstrap and database
    **                           initialization logic into external class
    **                           (DatabaseManager).
    ** 003 ECF 20051104   @23254 Changes to support locking and transactions.
    **                           Added methods to lock records and query lock
    **                           status, and to begin, commit, and roll back
    **                           transactions.
    ** 004 ECF 20051109   @23382 Changed use paradigm from all static methods
    **                           to getting an instance by factory. Previously
    **                           all methods were static and required a
    **                           physical database name to be specified. Now
    **                           one instance is created and cached per
    **                           physical database. Client code retrieves an
    **                           instance via a factory method and operates on
    **                           that instance. Instances are immutable and
    **                           shared across multiple contexts. Added method
    **                           to retrieve the next unique primary key using
    **                           a database sequence.
    ** 005 ECF 20051215   @23751 Added support for ambiguous check. Also made
    **                           primary key (phase 1) searches read-only.
    ** 006 ECF 20060120   @24044 Integrated with P2J server. Replaced
    **                           ThreadLocal with ContextLocal.
    ** 007 ECF 20060130   @24354 Added temp table support. Required a new
    **                           list() method, disabling the lock manager for
    **                           temp table cases, and enabling arbitrary SQL
    **                           statements to be executed (for temp table
    **                           creation and dropping).
    ** 008 ECF 20060218   @24720 Created uniqueResultViolation method. This
    **                           processing was needed from multiple places,
    **                           including outside this class.
    ** 009 ECF 20060306   @25016 Added loadReadOnly method. Supports internal
    **                           queries used for persistence framework
    **                           housekeeping, including resolving foreign
    **                           records using legacy keys (for natural join
    **                           reverse resolution).
    ** 010 ECF 20060329   @25255 Fixed executeSQL method. Was passing the SQL
    **                           string to PreparedStatement's execute method.
    ** 011 ECF 20060602   @26943 Optimization. HQL queries now will always
    **                           attempt to load the full record, rather than
    **                           just the primary key ID. This avoids the
    **                           overhead of a separate load step.
    ** 012 ECF 20060608   @27001 Changed list() method signature and updated
    **                           implementation. Number of results can now be
    **                           limited and results can be set read-only.
    ** 013 ECF 20060610   @27098 Consolidated lock processing. Internal lock
    **                           processing now uses public lock() method.
    ** 014 ECF 20060621   @27505 Changed session management and transaction
    **                           implementation. Sessions are now always
    **                           created upon demand, and detached records
    **                           from a previous session are immediately
    **                           associated with the new session. The begin
    **                           transaction API is more lenient, reporting if
    **                           a new transaction could not be created,
    **                           rather than throwing an exception.
    ** 015 ECF 20060627   @27590 Improved temp table support. Always flush
    **                           Hibernate session before executing JDBC
    **                           statements directly. Ensure save and delete
    **                           operations are executed within transactions.
    ** 016 ECF 20060712   @28050 Added trimTextArgs. Trims trailing whitespace
    **                           from any query substitution parameter which
    **                           is of type com.goldencode.p2j.util.character.
    ** 017 ECF 20060719   @28100 Fixed memory leak. Implemented cleanup()
    **                           method for Hibernate Session's ContextLocal
    **                           variable to ensure Session is explicitly
    **                           closed when context ends. Although the P2J
    **                           server would not pin the Session's memory
    **                           directly, there was still a reference within
    **                           Hibernate, such that the Session data could
    **                           not be garbage collected after the client
    **                           context ended.
    ** 018 ECF 20060719   @28101 Enable result set 'paging'. The list() method
    **                           now accepts a startOffset parameter.
    ** 019 ECF 20060720   @28115 Integrated ChangeBroker. Enables optimization
    **                           of avoiding unnecessary auto-flushing of the
    **                           active Hibernate session. This dramatically
    **                           improves the performance of RandomAccessQuery
    **                           when the session contains a large number of
    **                           entities.
    ** 020 ECF 20060725   @28187 Consolidated context local variables within
    **                           the Context inner class. Also added a
    **                           mechanism to prevent sessions from being
    **                           closed while external entities are dependent
    **                           upon them.
    ** 021 ECF 20060728   @28269 Made queries cacheable; improved reliability
    **                           of locking in dynamic retrieval mode.
    ** 022 ECF 20060828   @28930 Better error handling. In some cases, session
    **                           was not being closed after catching an
    **                           unrecoverable exception.
    ** 023 ECF 20060830   @29005 Fixed getQuery(). In case where we were not
    **                           explicitly paging results, the query would
    **                           return an empty result set, due to incorrect
    **                           maxResults setting.
    ** 024 ECF 20060901   @29136 Fixed memory leak. decrementSessionsUsers()
    **                           was actually incrementing the session user
    **                           count, due to a typo.
    ** 025 ECF 20060903   @29203 Fixed Hibernate assertion failure. Ensure
    **                           session is flushed before an object is
    **                           evicted.
    ** 026 ECF 20060904   @29219 Better error reporting. scroll() reports HQL
    **                           statement and context info on query error.
    ** 027 GES 20060905   @29230 Updated trimTextArgs() to truncate any
    **                           character vars that have embedded null bytes.
    **                           This ensures that null bytes can never be
    **                           passed into queries as substitution
    **                           arguments.
    ** 028 ECF 20060911   @29460 Fixed query defect. Restore default query
    **                           settings when checking out a cached query.
    ** 029 ECF 20060919   @29697 Made mods required by changes to ChangeBroker
    **                           calls. Also added trace debug output during
    **                           record eviction.
    ** 030 ECF 20060929   @30035 Fixed out of range query substitution date
    **                           parameters. We now 'clip' those dates to the
    **                           minimum and maximum date values supported by
    **                           the backing database and log a debug message.
    **                           Renamed trimTextArgs() method to
    **                           preprocessQueryParameters() to reflect
    **                           broader purpose.
    ** 031 ECF 20061010   @30309 Made changes to accommodate auto-commit temp
    **                           tables. These were primarily side effects of
    **                           changes made to TemporaryBuffer and
    **                           RecordBuffer. A new variant of the static
    **                           factory method to retrieve instances of this
    **                           class was added. The new version of get()
    **                           will create a secondary instance of this
    **                           class, backed by the same database as the
    **                           primary instance. This allows auto-commit and
    **                           non-auto-commit temp tables to share the same
    **                           physical backend, but maintain separate
    **                           connections and Hibernate sessions.
    ** 032 ECF 20061012   @30390 Fixed nonthreadsafe access error. Flush
    **                           session to ensure consistency, when a request
    **                           to close the session cannot be honored
    **                           immediately (because a query is pinning it
    **                           open).
    ** 033 ECF 20061014   @30527 Integrated SessionListener architecture.
    **                           Objects which register as SessionListeners
    **                           are notified when a Hibernate session is
    **                           about to close. Deprecated session user count
    **                           mechanism which would pin open a session as
    **                           long as a reference count of dependents was
    **                           non-zero.
    ** 034 ECF 20061110   @31094 Improved logging. Errors reassociating
    **                           detached records are logged in detail to
    **                           prevent these errors from being masked in
    **                           silent error mode.
    ** 035 ECF 20061113   @31140 Added new variant of load(). Needed for cases
    **                           where a RecordBuffer instance is not readily
    **                           available.
    ** 036 ECF 20061116   @31236 Minor adjustments to exception handling.
    **                           Ensure session gets closed when a database
    **                           error occurs.
    ** 037 ECF 20061120   @31325 Changed session closing behavior. Do not
    **                           close temp table sessions if the temp table
    **                           count exceeds zero (with no database errors).
    ** 038 ECF 20061212   @31667 Fixed context cleanup. Inner class Context's
    **                           cleanup was not properly connected to end of
    **                           context cleanup, so it was never being
    **                           invoked.
    ** 039 ECF 20070111   @31779 Added flushBatch method. Flushes a batch of
    **                           records to the database and clears the active
    **                           session.
    ** 040 ECF 20070130   @32039 Various cleanup. Ensure session is flushed if
    **                           necessary after attaching a record. Ensure
    **                           session is closed for all Hibernate exception
    **                           processing. Ensure session is closed if a
    **                           SessionListener throws an exception during
    **                           close notification. Deprecated method which
    **                           creates secondary Persistence instance for a
    **                           single database.
    ** 041 ECF 20070212   @32155 Integrated DirtyReadManager. This allows DMO
    **                           validation to check uncommitted updates
    **                           across client contexts. Also added the
    **                           ability to temporarily disable flushing,
    **                           which was necessary to correct a flaw in
    **                           validation processing.
    ** 042 ECF 20070228   @32246 Modified HQL-based load() method. It can now
    **                           handle either an ID-only projection query or
    **                           a query which fetches and returns a DMO
    **                           directly. In the latter case, the additional
    **                           lock() and get() steps are omitted.
    ** 043 ECF 20070326   @32611 Added bulk delete method. Deletes all records
    **                           which meet the specified criteria for a given
    **                           DMO type.
    ** 044 ECF 20070329   @32688 Fixed attach/detach. Flush check was not
    **                           correct.
    ** 045 ECF 20070412   @32983 Retrofitted locking to use RecordLockContext.
    **                           An instance of this class is created for each
    **                           context, one per database. Simplified context
    **                           cleanup.
    ** 046 ECF 20070416   @33025 Integrated user interrupt handling.
    ** 047 ECF 20070508   @33458 Added queryIndexData(). Uses database
    **                           metadata to inspect indices on a given table.
    ** 048 ECF 20070514   @33485 Fixed queryIndexData(). In some cases, root
    **                           column name must be extracted from string
    **                           queried from index metadata in a dialect
    **                           specific way.
    ** 049 ECF 20070516   @33669 Fixed abend caused by an orphaned transaction
    **                           at session close. When a session is closed
    **                           due to an error, roll back any open
    **                           transaction before closing the session. Also
    **                           made error logging less noisy at this layer,
    **                           since a severe error usually will be logged
    **                           in error handling provided by users of the
    **                           persistence framework. Details are still
    **                           logged at FINE logging level.
    ** 050 ECF 20070519   @33682 Fixed load() method (variant which loads by
    **                           primary key). Ensure session is flushed if
    **                           necessary and use Session.get() instead of
    **                           Session.load(), in case record was deleted.
    ** 051 ECF 20070602   @33926 Integrated new scope options of
    **                           SessionListener. GLOBAL_SCOPE registers a
    **                           session listener cleanup routine with the
    **                           transaction manager global scope. NO_SCOPE
    **                           does not register a cleanup method at all.
    ** 052 EVL 20070609   @34005 Adding explicit import of the class
    **                           org.hibernate.type.Type to eliminate conflict
    **                           with the same class from java.lang.reflect
    **                           package to be able to compile for Java 6.
    ** 053 ECF 20070703   @34360 Removed IllegalStateException from flush().
    **                           Now throws recoverable PersistenceException
    **                           instead when no session is available.
    ** 054 ECF 20070706   @34401 Implemented SessionListener deregistration
    **                           notification.
    ** 055 ECF 20070708   @34414 Optimization. Cache ChangeBroker in context
    **                           local state, rather than retrieving it every
    **                           time flushing is checked.
    ** 056 ECF 20070709   @34419 Optimization. Consolidated access to Context
    **                           instance to prevent multiple ContextLocal
    **                           lookups within the same methods. Changed some
    **                           signatures and added/removed several private
    **                           methods to accomodate this.
    ** 057 ECF 20070720   @34620 Modified implementation of attach(). It now
    **                           associates a detached DMO with the current
    **                           Hibernate session.
    ** 058 ECF 20070812   @34857 Changed error message on non-unique result
    **                           exception. Report buffer name rather than
    **                           DMO name.
    ** 059 ECF 20070817   @34876 Fixed all variants of load() to prevent stale
    **                           data retrieval. There were cases where a DMO
    **                           instance in the Hibernate session cache would
    **                           become stale based on an update in another
    **                           user context. We now force a refresh from the
    **                           database when reading a record with a lock.
    ** 060 ECF 20070901   @35302 Changed executeSQL() method. Connection is
    **                           now set to autocommit=true before the SQL
    **                           statement is executed. Added new method
    **                           executeSQLQuery() to allow execution of
    **                           arbitrary SQL queries with no change to
    **                           connection's autocommit setting. Replaced use
    **                           of database name string with database info
    **                           object. Added trace logging of execution time
    **                           of queries and other database accesses.
    ** 061 ECF 20071002   @35331 Implemented dialect-specific callback hooks.
    **                           Registered P2JDialect is given a chance to
    **                           perform post-transaction processing with
    **                           dialect-specific data stored in the current
    **                           client context.
    ** 062 ECF 20071008   @35399 Added back convenience method get(String).
    ** 063 ECF 20071008   @35445 Fixed regression introduced by #060 (@35302).
    **                           Incorrect LockManager and DirtyReadManager
    **                           instances were being created for the temp
    **                           table database.
    ** 064 ECF 20071018   @35575 Refactored to enable creation by factory.
    **                           Moved static factory methods to new class
    **                           PersistenceFactory. Also, lock manager and
    **                           dirty read manager classes are now read from
    **                           the directory instead of hard-coded.
    ** 065 ECF 20071221   @36545 Fixed load() method implementations. We now
    **                           catch UnresolvableObjectException, which is a
    **                           recoverable condition which can occur when
    **                           invoking Session.refresh(). This will be
    **                           thrown by Hibernate when we have a DMO in the
    **                           first level cache, which has been deleted
    **                           from the database.
    ** 066 SVL 20071231   @36626 Identity manager is initialized to support
    **                           primary key generation on a per-table basis.
    ** 067 ECF 20080102   @36644 Removed deprecated nextPrimaryKey() method.
    **                           Replacement is nextPrimaryKey(String) added
    **                           in #066 (@36626).
    ** 068 ECF 20080108   @36733 Fixed bulk delete. Removed optimization of
    **                           query flush, which was causing a
    **                           StaleStateException in cases of deleted DMOs
    **                           with collection associations.
    ** 069 SVL 20080111   @36772 getIdentityManager() function has been added.
    ** 070 ECF 20080129   @37004 Added debug logging. Log DMO persist event.
    ** 071 ECF 20080312   @37480 Integrated support for embedded H2 database.
    **                           Also changed return type of list() method to
    **                           be less specific.
    ** 072 ECF 20080403   @38136 Use entity name instead of DMO class to test
    **                           if flush is required. Added load() variant
    **                           which accepts entity name instead of DMO
    **                           class.
    ** 073 SVL 20080421   @38097 Added foreignKeysEnabled property.
    ** 074 CA  20080430   @38160 Added a static method retrieveIndexData which
    **                           contains the queryIndexData's code related to
    **                           building the index list from a result set.
    ** 075 ECF 20080606   @38642 Removed ValidationContext references.
    ** 076 ECF 20080611   @38697 Major refactoring. Index metadata query
    **                           methods were rationalized to be accessible in
    **                           more situations. Added methods to temporarily
    **                           "borrow" a connection from a current session.
    **                           Removed all references to the obsolete
    **                           DirtyReadManager class hierarchy.
    ** 077 ECF 20080624   @38918 Optimized column type metadata methods used
    **                           primarily at startup. This approach avoids
    **                           many round trips to the database to query
    **                           column metadata, but may not work with all
    **                           JDBC drivers. In case not, the original,
    **                           unoptimized approach is used.
    ** 078 CA  20080605   @38566 Renamed method delete to deleteOrUpdate.
    ** 079 ECF 20080806   @39324 Added rowid support. Changed default identity
    **                           manager to SequenceIdentityManager, which
    **                           generates database-wide unique IDs. Primary
    **                           keys are now 64-bit integers.
    ** 080 CA  20080815   @39462 Support API change in DBUtils. Fixed NPE in
    **                           getColumnDataType.
    ** 081 SVL 20080901   @39652 Identity pool is initialized together with
    **                           identity manager.
    ** 082 ECF 20080923   @39936 Memory tuning. Do not cache queries used for
    **                           deletes/updates.
    ** 083 ECF 20080924   @39946 Added load(RecordIdentifier). Loads a single
    **                           record, given its unique identifier.
    ** 084 ECF 20081015   @40091 Refactored query substitution parameter
    **                           preprocessing. Abstracted out a static method
    **                           to process a single parameter, so it is
    **                           accessible to other classes in the package.
    ** 085 ECF 20081021   @40208 Force refresh of DMOs in active buffers when
    **                           getting a new Session after a rollback. This
    **                           ensures active buffers have current versions
    **                           of these records.
    ** 086 ECF 20081024   @40224 Fixed regression caused by #085 (@40208). If
    **                           a record in a buffer was deleted during a
    **                           rollback, we must handle this stale state
    **                           when creating the new session and refreshing
    **                           records left in active buffers.
    ** 087 ECF 20081106   @40354 Modified directory lookups for lock manager
    **                           and identity manager. Changed path to allow
    **                           for other, related properties to be set in
    **                           the directory.
    ** 088 ECF 20081118   @40549 Minor change to error message text.
    ** 089 ECF 20081204   @40803 Added performance warning logging. Queries
    **                           which take longer than certain thresholds
    **                           (configured in the directory) trigger a
    **                           warning log entry.
    ** 090 ECF 20090114   @41162 Fixed load() (HQL variant). Was not correctly
    **                           unlocking when passed a lock type of NONE.
    ** 091 ECF 20090128   @41239 Improved Context.getSession(). When
    **                           associating the current records of active
    **                           buffers with a new session after a rollback
    **                           of the previous session, we only refresh
    **                           those we know to be stale, rather than all
    **                           of them. This reduces the number of trips
    **                           to the database. The rolledBack parameter to
    **                           Context.closeSession() was removed as a
    **                           result of this change.
    ** 092 ECF 20090204   @41257 Improved getQuery() performance. We now cache
    **                           Query objects more aggressively, which avoids
    **                           the overhead of Hibernate's initialization of
    **                           these objects when possible.
    ** 093 ECF 20090212   @41295 Added bulk eviction method. Exposed Hibernate
    **                           session via bind() method.
    ** 094 ECF 20090311   @41502 Added support for limit/max value inlining in
    **                           a SELECT statement. We do this only in very
    **                           limited cases: currently only for simple load
    **                           queries.
    ** 095 ECF 20090601   @42576 API change. Renamed load(RecordIdentifier) to
    **                           quickLoad(RecordIdentifier). Also made method
    **                           public, as it is needed from dirty package.
    ** 096 ECF 20090619   @42771 Added scroll() variant which accepts ScrollMode.
    **                           Existing variant now calls this variant with a
    **                           default ScrollMode of SCROLL_INSENSITIVE.
    ** 097 CA  20090625   @42945 Transaction roll back is executed at database
    **                           level, so session needs to be flushed before the
    **                           actual roll back.
    ** 098 ECF 20090702   @43036 Fixed session closing/cleanup logic. Removed
    **                           deprecated code.
    ** 099 ECF 20090711   @43188 Implemented implicit transaction support. To
    **                           prevent Hibernate from beginning open-ended
    **                           transactions for query operations, we now open an
    **                           implicit transaction for each such operation.
    **                           These are closed either when an explicit
    **                           transaction is opened, or after an idle timeout.
    ** 100 ECF 20090716   @43219 Fixed regression in #099 (@43188). In the event a
    **                           session was marked as broken in the implicit
    **                           transaction timer thread, this state was never
    **                           being reset after a new session has been opened.
    ** 101 ECF 20090719   @43305 Added safety code to implicit commit task. It is
    **                           more resilient to unexpected errors (such as
    **                           OOME), to avoid taking down the timer thread.
    ** 102 ECF 20090722   @43325 Disabled implicit transaction commit thread. It
    **                           is the prime suspect in a "possible nonthreadsafe
    **                           access to session" error. May be re-enabled at a
    **                           later time.
    ** 103 ECF 20090723   @43339 Made preprocessQueryParameter() public. This
    **                           static method is needed by the dirty subpackage.
    ** 104 CA  20090724   @43381 Rolled back H091 (#41239) - removed the
    **                           buffer.isStale() dependency in
    **                           Context.getSession().
    ** 105 ECF 20090723   @43418 Rewrote implicit transaction support. Removed
    **                           secondary thread and push/pop calls from low
    **                           level query services. These must be implemented
    **                           by callers instead to enable coarser, implicit
    **                           transaction granularity (i.e., fewer transactions
    **                           when possible). If a session is being used only
    **                           for an implicit transaction, it is made read-only
    **                           to prevent unnecessary session flushing.
    ** 106 ECF 20090810   @43587 Fixed SessionListener notifications. Modified
    **                           notification mechanism to allow more event types.
    **                           Notify listeners when a transaction is about to
    **                           be committed, in addition to session closing
    **                           events.
    ** 107 ECF 20090811   @43599 Fixed regressions in #105 (@43418). Implicit
    **                           transaction use count was not being properly set
    **                           and used in some places. Fixed endTransaction(),
    **                           which was nulling out active transaction too
    **                           early.
    ** 108 CA  20090901   @43818 In initializeInstance, catch any StopCondition's
    **                           and re-throw them (do not pack it in a generic
    **                           runtime exception).
    ** 109 ECF 20090925   @44029 Temporarily disabled identity pool. There is a
    **                           defect in the implementation of IdentityPool that
    **                           can prevent primary keys from being distributed.
    **                           When that is fixed, this should be re-enabled.
    **                           Affected code was commented out from
    **                           createIdentityManager().
    ** 110 CA  20091001   @44062 Modified executeSQLQuery() to leave the statement
    **                           open; else, it will not be able to iterate
    **                           through the returned result set.
    ** 111 ECF 20091002   @44082 Re-enabled identity pool. Also made additional
    **                           change to executeSQLQuery() to require the caller
    **                           to push/pop the implicit transaction counter. We
    **                           cannot do this in the method, as this could cause
    **                           the transaction to be closed before the caller
    **                           could process results.
    ** 112 SVL 20091019   @44145 Added support for the trigger manager.
    ** 113 SVL 20091126   @44442 Added quickLoad(RecordIdentifier, boolean)
    **                           function.
    ** 114 CA  20100115   @44527 Added executeSQL and executeSQLQuery versions
    **                           which accept query parameters.
    ** 115 CA  20101028          All metadata string query parameters and results
    **                           will be processed using dialect-specific case.
    **                           Do not prepare permanent H2 database.
    ** 116 CA  20101202          The permanent H2 database needs to be prepared
    **                           in a special way - the custom functions need to
    **                           be registered with the HQL preprocessor.
    ** 117 AIL 20121206          Updated from character to TextOps.
    ** 118 SVL 20120331          Upgraded to Hibernate 4.
    ** 119 CA  20130620          Added APIs to check if a given class is one of P2J's Hibernate
    **                           compatible types and to disambiguate a BDT or Java native type to a
    **                           Hibernate compatible type.
    ** 120 OM  20130531          Added preprocessing of datetime/-tz parameters.
    **                           Added small patch for binding composed-type parameters.
    **                           Maybe this should be used for all parameters when switching
    **                           from positional to named parameters
    **                           Do not close sql connection when 4GL error occurs in CAN-FINDs.
    ** 121 VMN 20130830          Added lockTable method.
    ** 122 OM  20130829          Enriched the parameter list of load() methods with a boolean for
    **                           specifying if the record should be forced to be read from database.
    **                           Added support for -rereadnolock P4GL startup parameter emulation.
    ** 123 ECF 20130804          Added base metadata support.
    ** 124 ECF 20131028          Added lock metadata support.
    ** 125 CA  20131013          Added no-op deleted() method, required by the changes in Finalizable
    **                           interface.
    ** 126 OM  20131206          Added initialization call for DatabaseTriggerManager.
    ** 127 SVL 20140210          Set JPA-style HQL parameters instead of positional.
    ** 128 GES 20140206          Added isActive() to allow callers to determine if the persistence
    **                           layer is or is not disabled.
    ** 129 ECF 20140312          Replaced Apache commons logging with J2SE standard logging.
    ** 130 ECF 20140318          Minor memory optimization when gathering JDBC index metadata.
    ** 131 CA  20140513          Added a weight for the context-local var, to ensure predetermined
    **                           order during context reset.
    ** 132 ECF 20140624          Rolled back 119. See persist.type.TypeHelper.
    ** 133 OM  20140711          Changed initialization location for database triggers to pkgroot/dmo/.
    ** 134 ECF 20140930          Wrap IllegalStateException in PersistenceException in rollback, to
    **                           be consistent with the declared API and to be less catastrophic for
    **                           callers.
    ** 135 ECF 20141204          Fixes to transaction and context cleanup.
    ** 136 OM  20150112          Replaced calls to deprecated method. Additional code cleanup.
    ** 137 ECF 20150221          Raise STOP condition instead of ERROR for JDBC-level errors, to avoid
    **                           getting caught in RETRY loops for unrecoverable errors.
    ** 138 ECF 20150326          Optionally log diagnostic information on begin transaction conflicts.
    ** 139 ECF 20150504          Enable explicit deregistration of session listeners.
    ** 140 ECF 20150811          Fixed database connection leak during context cleanup. Added some
    **                           debug logging and adjusted level of existing log statements.
    ** 141 ECF 20150821          Minor format fix and import change.
    ** 142 ECF 20150906          Change code to match BufferManager API changes; override hashCode
    **                           and equals implementations.
    ** 143 ECF 20150928          Reimplemented query cache to reduce memory use. Refactored certain
    **                           methods used frequently within the persist package to reduce
    **                           unnecessary context local lookups.
    ** 144 OM  20151028          Added comment/javadoc.
    ** 145 OM  20140721          Called preparePermanentDatabase() for MS SQL database server in order
    **                           to prepare HQL for support UDFs from loaded assemblies.
    ** 146 ECF 20160305          Collect index metadata at initialization in multiple threads to
    **                           reduce server initialization time.
    ** 147 ECF 20160429          Do not make queries against _temp database cacheable. Reimplemented
    **                           executeSQL variants to work directly with JDBC, rather than using
    **                           Hibernate's SQLQuery, which was too expensive. Fixed load method
    **                           variants to not rely upon DMO reference counting in their refresh
    **                           logic. Refined rereadnolock implementation, at least when used by
    **                           RandomAccessQuery.
    ** 148 ECF 20160512          Fixed deregisterSessionListener.
    ** 149 ECF 20160610          Optimized getSingleSQLResult.
    ** 150 OM  20160603          Fixed rtrimming to space character only.
    ** 151 IAS 20160621          Added support for read-only tables
    ** 152 ECF 20160614          Reduce unnecessary flushing. Expanded use of read-only metadata.
    **                           Further refined rereadnolock implementation.
    ** 153 IAS 20160701          Fixed [3166] error message.
    ** 154 IAS 20160714          Use legacy name instead of DMO alias.
    ** 155 OM  20160715          Double guarded saving of template records to database. Prevented
    **                           ContextLocalCleanupException to be thrown when client disconnects.
    ** 156 ECF 20160820          Fixes to support better concurrency and better detect records
    **                           deleted in other sessions. Includes new lock timeout feature and
    **                           better integration with record lock context.
    ** 157 OM  20160905          Small optimizations.
    ** 158 ECF 20160831          Performance enhancements. Improved use of read-only table
    **                           information. Reworked session flushing to allow better batching of
    **                           database statements. Initialize P2OLookup instances at server
    **                           startup. Logging changes.
    ** 159 ECF 20160929          Removed obsolete enableFlush feature. Use implicit transactions
    **                           where possible to reduce flushing.
    ** 160 HC  20171003          Moved the "rereadnolock" check in the introduced class LegacyCLI.
    ** 161 ECF 20171116          Treat NonUniqueObjectException during new Session setup as non-fatal.
    ** 162 OM  20180108          Fixed FWD Version printed for database.
    ** 163 ECF 20180219          Reduce unnecessary session flushing.
    ** 164 ECF 20180418          Added logging of unrecoverable exception to handleException.
    ** 165 SBI 20180518          Renamed file-system/pkgroot into legacy-system/pkgroot.
    ** 166 ECF 20180609          Further reduce unnecessary session flushing.
    ** 167 ECF 20180809          Ensure template record is not attached to session by a lock request.
    ** 168 OM  20190117          Basic support for VST metadata.
    **     ECF 20190325          Minor code de-duplication.
    **     HC  20190401          Fixed a H2 DB performance issue during initialization, caching of column data
    **                           types didn't work correctly for H2.
    ** 169 OM  20190624          Allowed initialization of DataSetManager.
    ** 170 AIL 20190722          Removed initialization of DatabaseTriggerManager.
    ** 171 CA  20190902          Fixed problems when connecting to a FWD server running a database
    **                           schema which is not actively managed by the requester, too.
    ** 172 ECF 20200419          New ORM implementation. Removed Hibernate dependencies.
    ** 173 GES 20200804          Removed ErrorManager context-local usage.
    **     ECF 20200909          Added handling for stale records when creating a database session.
    **     OM  20200919          Improved access to dmo metadata.
    **     ECF 20200919          Eliminate use of Session.refresh where no longer necessary.
    **     AIL 20200919          Initialize TemporaryDatabaseManager.
    **     ECF 20201026          Increase size of query LRU cache.
    **     OM  20201120          Avoid UniqueResultException reaching client UI.
    **     IAS 20201125          Do not re-use re-written queries
    **     OM  20210223          Invalidate index-based cache in the event of low-level SQL operations.
    **     ECF 20210911          Minor change to session use counter.
    **     ECF 20210924          Ensure a call to save(Record, Long) updates the internal DMO state.
    **     ECF 20210927          Minor cleanup.
    **     IAS 20211223          Special processing of errors raised by UDFs.
    **     IAS 20220221          Call a correct version of the getFWDVersion (Java or SQL).
    **     IAS 20220324          Re-worked LockTableUpdater.
    **     OM  20220516          Added collation support for _meta databases (use the H2 dialect).
    **     IAS 20220608          Provide additional information in reportUDFVersion().
    **     IAS 20220712          Create missing UDFs
    **     IAS 20220816          Do not use 'udf' schema for non-PostgreSQL UDFs
    **     SBI 20220921          Added save(Record) to update the existing record.
    **     CA  20220901          Refactored scope notification support: ScopeableFactory was removed, and the
    **                           registration is now specific to each type of scopeable.  For each case, the block
    **                           will be registered for scope support (for that particular scopeable) only when
    **                           the scopeable is 'active' (i.e. unnamed streams or accumulators are used).  This
    **                           allows a lazy registration of scopeables, to avoid the unnecessary overhead of
    **                           processing all the scopeables for each and every block.
    **     TJD 20220504          Upgrade do Java 11 minor changes
    **     CA  20221109          Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded
    **                           as 'partial/incomplete'.
    **     RAA 20221221          Modified the execution of sql's. They now pass through the SQLStatementLogger
    **                           in case logging is intended.
    **     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
    **     CA  20221109          Implemented runtime for EXCEPT/FIELDS options - only NO-LOCK records are loaded
    **                           as 'partial/incomplete'.
    ** 174 RAA 20230213          Added the possibility to create a query defined with lazy.
    **     RAA 20230208          Removed the checks for no-undo as they are no longer necessary.
    **     AL2 20230224          Check at persistence level is lazy is supported.
    ** 175 IAS 20230405          Added getQuery(fql) method.
    **                           Fixed second pass query generation for queries with CONTAINS operator
    **                           Added DATE-RELATION to the 'scroll', 'list', an 'getQuery' parameters
    ** 176 IAS 20230410          Added 'old' methods w/o DataRelation argument.
    ** 177 SR  20230510          Removed readOnly parameter and fixed getQuery new signature.
    ** 178 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
    ** 179 RAA 20230518          Replaced lambdas with method referencing when using SQLStatementLogger.
    ** 180 RAA 20230607          SQLs are now executed through SQLExecutor instead of SQLStatementLogger.
    ** 181 RAA 20230615          Added SQL as a parameter when using SQLExecutor.
    ** 182 DDF 20230627          Made the size of the Context.staticQueryCache configurable.
    **     DDF 20230627          Removed the static block that reads the configuration sizes of the cache.
    **                           CacheManager will handle the finding of the configuration size and the
    **                           cache creation.
    **     DDF 20230627          Initialize server bootstrap configurations.
    **     DDF 20230627          Made the Context cache final.
    **     DDF 20230628          Initialize CacheManager before DatabaseManager.
    **     DDF 20230705          Initialize P2JQueryExecutor and Validation configurations.
    **     DDF 20230706          Replaced createLRUCache call with another one that will use a null
    **                           discriminator by default.
    ** 183 DDF 20230703          Manage reclaimable sessions using SessionFactory.
    **     DDF 20230703          Initialize the SessionCloseThread which is tasked with closing unused sessions
    **                           across all SessionFactory instances.
    **     DDF 20230703          Use a custom Consumer to manage association of records with new sessions.
    **     DDF 20230703          Add missing assignment of session in lambda.
    ** 184 CA  20230728          Ensure the current JDBC connection is not leaked at context cleanup.
    **     ECF 20230807          Do not let getSingleSQLResult create a long-lived session.
    **     DDF 20230807          Directly close the session when reclaiming is disabled.
    ** 185 ECF 20230808          Fixed regression in getSingleSQLResult method.
    ** 186 TT  20230621          Replaced LegacyCLI.getInstance() with SessionUtils._startupParameters()
    ** 187 AL2 20230822          Removed checkReadOnly method.
    ** 188 RAA 20230724          Replaced FunctionWithException with QueryStatement.
    **     RAA 20231006          Removed scrollThreshold, listThreshold and other fields used for logging.
    ** 189 OM  20231218          Added implementation for DBCODEPAGE and DBCOLLATION 4GL functions.
    ** 190 OM  20231214          Converted list() method to optionally return a list of values instead of
    **                           attempting to hydrate and return them as record(s).
    ** 191 OM  20240131          Fixed flaw in DBCODEPAGE / DBCOLLATION implementation.
    ** 192 DDF 20240207          Notify TRANSACTION_COMMITTING event when committing a database transaction.
    ** 193 RAA 20230802          Added supportsValidateMode() function.
    **     RAA 20230802          Changed name of validateMode to allowDBUniqueCheck.
    **     RAA 20231103          Added allowDBUniqueCheck() function.
    **     RAA 20231127          Swapped condition places in allowDBUniqueCheck.
    **     RAA 20240316          Rebase fix: added allowDBUniqueCheck flag.
    ** 194 DDF 20240322          Only close the open temporary database when cleaning up the context.
    ** 195 AL2 20240328          Added lazyMode to the query key (used for caching).
    ** 196 DDF 20240404          Removed change for closing the temporary database when cleaning up the
    **                           context.
    ** 197 OM  20240520          Fixed connection management in executeSQL().
    ** 198 RAA 20240423          Added multi-tenancy support.
    **     RAA 20240426          The Persistence.Context is now registered as a TenantChangeListener.
    **     RAA 20240513          When registering the context as a listener, provide the name of the database.
    **                           Added Context.getSession() function with a parameter intended for multi-tenancy.
    **     RAA 20240613          Added tenantId field in the persistence context.
    **     BS  20230512          Shared cache instance per persistent database.
    **     RAA 20240627          Made the RecordNursery tenant-aware. Enabled back the session reclaimable
    **                           feature when in multi-tenancy mode.
    **     RAA 20240702          When a tenant changes inside the context, expire its reclaimable session.
    **     RAA 20240710          Allow the database and the dialect to change depending on the tenant
    **                           holding the context.
    **     RAA 20240715          Clarified the type of database name occurrences.
    **     RAA 20240717          Removed landlord database field and adjusted getSession.
    **     RAA 20240726          Moved TenantConfig.initialize() call. Adjusted parameters for setTenantDatabase.
    **     RAA 20240730          If the database used in the Context constructor does not have a logical name,
    **                           compute it.
    **     RAA 20240807          If the database name is null when retrieving a session, make no tenant changes.
    **     RAA 20240808          Added FastFindCache and Database fields in the context.
    **     DDF 20240808          Added getDatabase() method for Context.
    ** 199 CA  20240723          Fixed a possible NPE when a the cause of a PersistenceException is checked
    **                           in 'load'.
    ** 200 SB  20240828          Added the unsafe flag which prevents an immediately rollback on a failed SQL
    **                           query. Refactored exception handling depending. Refs #8968.
    ** 201 OM  20240813          Invalidate all buffers to avoid leaking from one tenant database to another.
    **         20240909          Concurrent multitenancy functionality implementation.
    ** 202 OM  20240913          Made RecordLockContext context private instead of shared.
    ** 203 TJD 20240918          Defer tenant initialization for remote databases
    ** 204 DDF 20241002          Only close the open temporary database when cleaning up the context (removed
    **                           previously).
    ** 205 OM  20241001          Incremental support for tenants.
    ** 206 OM  20241128          Multi-tenant runtime support: selected the proper persistence context, eventually
    **                           based on [sharedDb] parameter.
    **                           Implementation of [setMultiTenantAlternative] method.
    ** 207 OM  20241211          RecordLockContext knowns its context from the creation time.
    ** 208 OM  20250110          Added sequence support for multi-tenant databases.
    **         20250114          Fixed tenant re-authentication within the same context.
    ** 209 CA  20250122          Avoid creating new character instances in 'preprocessQueryParameter'.
    ** 210 AS  20250203          Reduced the logging level for unique find violations.
    ** 211 ICP 20250213          Removed the initialization of the CacheManager, it was moved into a server hook.
    ** 212 RNC 20250224          If the SQLException is recoverable, do not throw StopConditionException.
    ** 213 CA  20250303          Added 'invalidateCache', an API to invalidate the caches and potentially lock
    **                           exclusively all records specified by an SQL.
    **                           Added 'refreshBuffers', an API to force reload of all buffers in current context,
    **                           for the specified DMO.
    ** 214 OM  20250211          More verbose error in initializeTenant(). New TenantManager.getTenant() param.
    ** 215 OM  20250315          Added support for readonly meta tables.
    ** 216 ES  20250409          Added timeout parameter.
    ** 217 OM  20250521          Fixed UDF type detection.
    */

    /*
    ** 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.sql.*;
    import java.util.*;
    import java.util.BitSet;
    import java.util.Date;
    import java.util.concurrent.*;
    import java.util.function.*;
    import java.util.logging.*;
    import com.goldencode.cache.*;
    import com.goldencode.p2j.persist.deploy.*;
    import com.goldencode.p2j.persist.dialect.*;
    import com.goldencode.p2j.persist.dirty.DirtyShareSupport;
    import com.goldencode.p2j.persist.event.*;
    import com.goldencode.p2j.persist.hql.*;
    import com.goldencode.p2j.persist.id.*;
    import com.goldencode.p2j.persist.lock.*;
    import com.goldencode.p2j.persist.meta.*;
    import com.goldencode.p2j.persist.orm.*;
    import com.goldencode.p2j.persist.orm.SQLExecutor.QueryStatement;
    import com.goldencode.p2j.schema.*;
    import com.goldencode.p2j.security.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.ErrorManager;
    import com.goldencode.p2j.util.ErrorManager.*;
    import com.goldencode.p2j.util.logging.*;

    /**
     * The central point for fundamental persistence services in the P2J environment.  This implementation is
     * backed by the FWD ORM framework. This class provides basic CRUD (create, read, update, delete) services,
     * as well as methods to begin/commit/rollback database-level transactions.
     * <p>
     * An instance of this class is retrieved from {@link PersistenceFactory#getInstance(Database)} method
     * One instance of this class is associated with a single, physical database. Multiple user sessions can
     * use this shared instance; each will have its own context.
     * <p>
     * An instance of a concrete {@link com.goldencode.p2j.persist.lock.LockManager} implementation is created
     * for each instance of this class. The implementation to use is queried from the directory service,
     * and defaults to the {@link InMemoryLockManager} if no other implementation is specified in the
     * directory.
     * <p>
     * Transactions are explicitly managed with {@link #beginTransaction}, {@link #commit} and {@link #rollback}.
     * Only one transaction per client context may be active at a time.  These are database-level transactions,
     * which may or may not coincide with application-level transactions.
     * <p>
     * <strong>External code which directly accesses query methods within the {@code Persistence} class should
     * always properly bracket its use of these methods with transactions</strong>.
     * <p>
     * ORM sessions are managed implicitly, transparently to application code and even to other classes within
     * the persistence framework. No more than one session per user context may be active at a time. Sessions are
     * opened lazily as they are needed to fulfill database operations:  executing a query, persisting a record,
     * creating a temporary table, etc. They are closed when the runtime environment detects that they are no
     * longer needed by any active resource. Upon opening a new session, the current context's {@link
     * BufferManager} instance is interrogated for all active records. These are the current records stored in
     * all open record buffers for the database matching the current {@code Persistence} instance. They represent
     * detached records previously associated with the last open session (if any).
     */
    public class Persistence
    implements UnsafePersistence,
               TenantChangeListener
    {
       /** Well-known name of ID generator sequence */
       public static final String ID_GEN_SEQUENCE = "p2j_id_generator_sequence";

       /** Logger */
       private static final CentralLogger LOG = CentralLogger.get(Persistence.class.getName());

       /** Default lock manager implementation */
       private static final String DEFAULT_LOCK_MANAGER =
          "com.goldencode.p2j.persist.lock.InMemoryLockManager";

       /** Default identity manager implementation */
       private static final String DEFAULT_IDENTITY_MANAGER =
          "com.goldencode.p2j.persist.id.SequenceIdentityManager";

       /** Special values for a {@code codepage} or {@code collation} which has not been detected yet. */
       private static final String NOT_DETECTED_YET = "NOT_DETECTED_YET";

       /** Flag indicating whether records are joined using foreign keys */
       private static final boolean foreignKeysEnabled =
          Utils.getDirectoryNodeBoolean(null, "persistence/foreign-keys", false, false);

       /** <code>true</code> if the persistence layer can be used. */
       private static volatile boolean active = false;

       /** The offset in the {@code contexts} array of the shared/common/default instance. */
       public static final int SHARED_IDX = 0;

       /**
        * The offset in the {@code contexts} array of the private instance. The object at this position may be
        * {@code null} for non-tenant enabled applications or if no tenant was authenticated.
        */
       public static final int PRIVATE_IDX = 1;

       /**
        * A constant used for clearer meaning that the shared/common/default context is preferred. This will be
        * the cases for temporary and meta databases since for these tables, the {@code DmoMeta.multiTenant} is
        * always {@code false} (ie shared, not tenant specific).
        * <p>
        * When this is passed to a method, the {@code SHARED_IDX} context is used.
        */
       public static final boolean SHARED_CTX = true;
       public static final boolean META_CTX = SHARED_CTX;
       public static final boolean TEMP_CTX = SHARED_CTX;

       /**
        * A constant used for clearer meaning that the private context is preferred. This value is used when
        * {@code DmoMeta.multiTenant}.
        * <p>
        * In case the tenant was not authenticated and the private context was not created, the shared context is
        * used as default.
        * <p>
        * When this is passed to a method, the {@code PRIVATE_IDX} context is used.
        */
       public static final boolean PRIVATE_CTX = false;

       /** Context local object to manage persistence variables. */
       private final ContextLocal<Context[]> contexts = new ContextLocal<Context[]>()
       {
          @Override
          public WeightFactor getWeight() { return WeightFactor.LEVEL_1; }

          /**
           * Creates the default array of contexts. The default/shared/common one is automatically created. The
           * shared context will be created later, when and if a tenant authenticate for this logical database.
           *
           * @return  The initial value for the array of contexts.
           */
          @Override
          protected Context[] initialValue()
          {
             return new Context[] { new Context(defaultTenant), null };
          }

          /**
           * Cleans up the contexts. The shared context is cleaned by default, the private one only if a tenant
           * vas ever authenticated.
           *
           * @param   value
           *          The last value this context local variable contained.
           */
          @Override
          protected void cleanup(Context[] value)
          {
             value[SHARED_IDX].cleanup();
             if (value[PRIVATE_IDX] != null)
             {
                value[PRIVATE_IDX].cleanup();
             }
          }
       };

       /** Flag that determines if multi-tenancy is GLOBALLY enabled for this database or not. */
       private final boolean isMultiTenancyEnabled;

       /** Database dialect helper. */
       private final Dialect dialect;

       /** The CP for this database. */
       private String codepage = NOT_DETECTED_YET;

       /** The collation for this database. */
       private String collation = NOT_DETECTED_YET;

       /** Does this object represent the temporary database? */
       private final boolean temporary;

       /** Flag that marks if server side index validation can be skipped or not. */
       private final boolean allowDBUniqueCheck;

       /** Cached hash code */
       private final int hashCode;

       /** Lock Timeout in seconds */
       private final long timeout;

       /** The default tenant data. */
       protected final Tenant defaultTenant;

       /**
        * The map with all tenants which share the database of this {@code Peristence} object, mapped by their
        * name.
        */
       private final ConcurrentHashMap<String, Tenant> tenantData = new ConcurrentHashMap<>();

       /**
        * A container class which stores tenant-specific objects. Although the persistence tracks all these
        * {@code Tenant}, at any given moment, only one of is active in each user context.
        */
       protected static class Tenant
       {
          /** The name of the tenant that was assigned to this context, if any. */
          private final String tenantName;

          /** The id of the tenant that was assigned to this context */
          private final int tenantId;

          /** Database served by this object. */
          private final Database database;

          /** DMO version tracker (persistent databases only). */
          private final DmoVersioning dmoVersion;

          /** Pessimistic lock manager */
          private final AssignOnceWrapper<LockManager<String>> lockManager = new AssignOnceWrapper<>();

          /** Object which keeps lock metadata in sync with record lock activity. */
          private LockTableUpdater lockTableUpdater = null;

          /** Identity manager. */
          private final AssignOnceWrapper<IdentityManager> identityManager = new AssignOnceWrapper<>();

          /** Context-local record lock context */
          private final ContextLocal<RecordLockContext> lockContext = new ContextLocal<>();

          /**
           * Constructs a {@code Tenant} starting with its name and the database it serves. Not all 'final' fields
           * will be initialized now, some of them use {@code AssignOnceWrapper} for a lazy initialization.
           *
           * @param   database
           *          The physical database for which the {@code Tenant} is constructed.
           * @param   tenantId
           *          The tenant's id.
           * @param   tenantName
           *          The tenant name.
           */
          public Tenant(Database database, int tenantId, String tenantName)
          {
             this.tenantName = tenantName;
             this.tenantId = tenantId;
             this.database = database;
             this.dmoVersion = database.isTemporary() ? null : new DmoVersioning();
          }

          /**
           * Returns a short description of this object, usually needed while debugging.
           *
           * @return  a {@code String} which identifies the physical database around which this object is built.
           */
          @Override
          public String toString()
          {
             return "Tenant{" + database + '}';
          }
       }

       /**
        * Construct an instance for a particular, physical database, storing the dialect which is used to create
        * a portability layer.
        *
        * @param   database
        *          Database with which this instance is permanently associated.
        * @param   dialect
        *          Portability object used to issue dialect-specific commands to the database.
        */
       protected Persistence(Database database, Dialect dialect)
       {
          this.hashCode = super.hashCode();
          this.temporary = database.isTemporary();
          this.dialect = dialect;
          this.allowDBUniqueCheck = temporary && dialect.allowDBUniqueCheck();

          this.isMultiTenancyEnabled = !temporary && TenantConfig.isEnabled() && !database.isMeta();
          this.defaultTenant = new Tenant(database,
                                          TenantManager.DEFAULT_TENANT_ID,
                                          TenantManager.DEFAULT_TENANT_NAME); // default tenant always available
          this.tenantData.put(TenantManager.DEFAULT_TENANT_NAME, defaultTenant);

          // for remote databases tenant initialization is deferred for after session is created
          if (database.isLocal())
          {
             initializeTenant(defaultTenant);
          }

          this.timeout = ConnectionManager.getTimeOut(getDatabase().getName());
       }

       /**
        * Get the instance of this class associated with the given, physical
        * database name, creating it first if necessary.  Newly created instances
        * are stored in a static cache.
        *
        * @param   name
        *          Name of physical database with which the instance is
        *          permanently associated.
        *
        * @return  Persistence object associated with the given database.
        *
        * @deprecated Use {@link PersistenceFactory#getInstance(String)} instead.
        */
       @Deprecated
       public static Persistence get(String name)
       {
          return PersistenceFactory.getInstance(name);
       }

       /**
        * Initialize various infrastructure components of the persistence framework:
        * <ul>
        *   <li>{@code DataTypeHelper}, which is used when preprocessing FQL where clauses.
        *   <li>{@code DatabaseManager}, which configures the ORM environment for the databases
        *       to which the framework connects.
        *   <li>{@code P2OLookup}, instances of which are used for runtime, on-the-fly query and
        *       temp-table conversion.
        *   <li>{@code DatabaseTriggerManager}, which tracks and fires database triggers at the
        *       appropriate times.
        * </ul>
        *
        * @throws  IllegalStateException
        *          if invoked more than once.
        * @throws  PersistenceException
        *          if any error is encountered initializing the persistence framework.
        *
        * @see     DataTypeHelper#initialize()
        * @see     DatabaseManager#initialize()
        * @see     TemporaryDatabaseManager#initialize()
        * @see     P2OLookup#initialize()
        */
       public static void initialize()
       throws PersistenceException
       {
          Session.bootstrap();
          DataTypeHelper.initialize();
          TemporaryDatabaseManager.initialize();
          TenantConfig.enableMultiTenancy();
          DatabaseManager.initialize();
          Validation.bootstrap();
          DirtyShareSupport.bootstrap();
          SessionFactory.SessionCloseThread.initialize();

          // if runtime conversion services are required, initialize schema lookup resources
          if (ConversionPool.isInitialized())
          {
             try
             {
                P2OLookup.initialize();
             }
             catch (SchemaException exc)
             {
                throw new PersistenceException(exc);
             }
          }

          TenantConfig.initialize();

          // this is volatile and only assigned here, so it should be safe without synchronization
          active = true;

          if (LOG.isLoggable(Level.INFO))
          {
             LOG.info("Persistence services ready");
          }
       }

       /**
        * Reports if the persistence layer can be used or not.
        *
        * @return   <code>true</code> if the persistence layer is initialized and can be used.
        */
       public static boolean isActive()
       {
          return active;
       }

       /**
        * Perform special handling on a query substitution parameter to avoid
        * errors during query execution.  This entails:
        * <ul>
        *   <li>Trimming all trailing spaces, tabs, line feeds, and carriage
        *       returns from any parameter of type <code>character</code>.  Any
        *       parameter affected is replaced with a new <code>character</code>
        *       instance at the same index position within <code>args</code>.
        *   <li>Clipping out of bounds date values to the range supported by the
        *       backing database's implementation of a date data type.  Any
        *       parameter affected is replaced with a new <code>date</code>
        *       instance at the same index position within <code>args</code>.
        * </ul>
        * <p>
        * Note:  this method has special processing for <code>null</code> bytes
        * embedded in <code>character</code> variables.  The text of the variable
        * is truncated starting at the first embedded <code>null</code> byte.
        * This means that the resulting substitution parameters are guaranteed
        * to never include <code>null</code> bytes.
        *
        * @param   arg
        *          A single substitution parameter.
        * @param   dialect
        *          Database dialect.
        *
        * @return  The given parameter, processed as described above.
        */
       public static Object preprocessQueryParameter(Object arg, Dialect dialect)
       {
          Object processed = arg;

          // Trim text arguments.
          if (arg instanceof character)
          {
             character c = (character) arg;

             if (!c.isUnknown())
             {
                // this obtains a clone of the character variable, truncating it's text at any null byte that may
                // be embedded
                c = c.isJavaSpacifyNull() ? c.safeDuplicate() : c;

                // this is safe because it doesn't alter the original reference's state but rather replaces that
                // reference
                processed = c.getValue().endsWith(" ") ? TextOps.rightTrim(c, " ") : c;
             }
          }

          // "Clip" dates which are out of range.  This may result in
          // incorrect query behavior (hence the logged warning), but will
          // avoid catastrophic failure.
          else if (arg instanceof date)
          {
             date d = (date) arg;
             Date when = d.dateValue(); // TODO: what if [d] is unknown (so [when] is null)?

             Date low = dialect.getLowDate();
             if (low != null && when != null && when.before(low))
             {
                if (d instanceof datetimetz)
                {
                   datetimetz dtz = (datetimetz)d;
                   processed = new datetimetz(new datetime(low), dtz.getOffset());
                }
                else if (d instanceof datetime)
                {
                   processed = new datetime(low);
                }
                else
                {
                   processed = new date(low);
                }
                if (LOG.isLoggable(Level.FINE))
                {
                   LOG.log(Level.FINE,
                           "Query substitution date parameter " + d.toStringMessage() +
                           " was reset to the earliest date (" + low +
                           ") supported by the backing database");
                }
             }

             Date high = dialect.getHighDate();
             if (high != null && when != null && when.after(high))
             {
                if (d instanceof datetimetz)
                {
                   datetimetz dtz = (datetimetz)d;
                   processed = new datetimetz(new datetime(high), dtz.getOffset());
                }
                else if (d instanceof datetime)
                {
                   processed = new datetime(high);
                }
                else
                {
                   processed = new date(high);
                }
                if (LOG.isLoggable(Level.FINE))
                {
                   LOG.log(Level.FINE,
                           "Query substitution date parameter " + d.toStringMessage() +
                           " was reset to the latest date (" + high +
                           ") supported by the backing database");
                }
             }
          }

          return processed;
       }

       /**
        * Determine whether records are joined using foreign keys.
        *
        * @return  <code>true</code> if records are joined using foreign keys;
        *          <code>false</code> if records are joined using legacy key
        *          fields.
        */
       static boolean isForeignKeysEnabled()
       {
          return foreignKeysEnabled;
       }

       /**
        * Logs the FWD version returned by the getFWDVersion user-defined function (UDF/p2jpl) for
        * this database. When this database is a temporary or not primary this method is a no-op.
        * If the method fails to retrieve the version string, "undefined" version is reported in
        * the log output.
        *
        * @param   database
        *          The database in use.
        * @param   cfg
        *          The database configuration in use.
        *
        * @throws  PersistenceException
        *          if there is a problem creating a database session or statement.
        */
       void reportUDFVersion(Database database, DatabaseConfig cfg)
       throws PersistenceException
       {
          String udfVersion = "undefined";
          boolean useSQLUdfs;
          String searchPath = "unknown";

          try (Session session = new Session(database, cfg);
               Connection conn = session.getConnection())
          {
             String sqlFunc = FQLPreprocessor.getRegisteredFunction(database, "getFWDVersion", null);
             if (sqlFunc == null)
             {
                sqlFunc = "getFWDVersion"; // the function is not overloaded for all dialects
             }
             if (cfg == null)
             {
                cfg = DatabaseManager.getConfiguration(database);
             }
             useSQLUdfs = dialect.isNativeUDFsSupported() && !cfg.isUseJavaUDFs();
             if (useSQLUdfs)
             {
                ScriptRunner.createMissingUdfs(database);
                if ((DatabaseManager.getDialect(database) instanceof P2JPostgreSQLDialect) &&
                    !sqlFunc.startsWith("udf."))
                {
                   sqlFunc = "udf." + sqlFunc;
                }
             }
             try (Statement st = conn.createStatement();
                  ResultSet rs = st.executeQuery("SELECT " + sqlFunc + "()"))
             {
                if (rs.next())
                {
                   String val = rs.getString(1);
                   if (val != null)
                   {
                      udfVersion = val;
                   }
                }
             }
             catch (SQLException exc)
             {
                // do not propagate the exception, in case the UDF is not defined we want to
                // report "undefined" version
                LOG.log(Level.FINEST, "Error querying UDF version for " + database, exc);
             }
             if (dialect instanceof P2JPostgreSQLDialect)
             {
                try (Statement st = conn.createStatement();
                     ResultSet rs = st.executeQuery(
                                  "SELECT setting FROM pg_settings WHERE name='search_path'"))
                {
                   if (rs.next())
                   {
                      String val = rs.getString(1);
                      if (val != null)
                      {
                         searchPath = val;
                      }
                   }
                }
                catch (SQLException exc)
                {
                   LOG.log(Level.FINEST, "Error querying search_path for " + database, exc);
                }
             }
          }
          catch (SQLException ex)
          {
             DBUtils.handleException(database, ex);

             throw new PersistenceException("Error querying UDF version for " + database, ex);
          }

          LOG.info(String.format("UDF version for db %s is %s; UDFs style: %s; search_path: [%s]",
                                  database,
                                  udfVersion,
                                  useSQLUdfs ? "SQL" : "Java",
                                  searchPath
                               )
                   );
       }

       /**
        * Override the parent's implementation to return a hash code cached at construction.
        */
       @Override
       public final int hashCode()
       {
          return hashCode;
       }

       /**
        * Override the parent's implementation to ensure it is consistent with {@link #hashCode}.
        */
       @Override
       public final boolean equals(Object o)
       {
          return super.equals(o);
       }

       /**
        * Invalidate all caches related to this DMO.
        *
        * @param    dmoIface
        *           The DMO which requires cache invalidation.
        *
        * @throws   PersistenceException
        *           if any error occurs executing the statement.
        */
       @Override
       public void invalidateCache(Class<? extends DataModelObject> dmoIface)
       throws PersistenceException
       {
          invalidateCache(dmoIface, null, null);
       }

       /**
        * Invalidate all caches related to this DMO.
        * <p>
        * If a SQL like <code>select recid from ... where ...</code> is provided, then an
        * <code>EXCLUSIVE-LOCK</code> will be attempted on all records.
        *
        * @param    dmoIface
        *           The DMO which requires cache invalidation.
        * @param    lockSql
        *           The SQL to get the PKs for the records to be locked exclusively.
        * @param    args
        *           Any arguments for the SQL, or <code>null</code> otherwise.
        *
        * @throws   PersistenceException
        *           if any error occurs executing the statement.
        */
       @Override
       public void invalidateCache(Class<? extends DataModelObject> dmoIface, String lockSql, Object[] args)
       throws PersistenceException
       {
          Session session = getSession();
          DmoMeta dmoInfo = DmoMetadataManager.registerDmo(dmoIface);
          String dmoSqlTable = dmoInfo.getSqlTableName();
          String dmoImplName = dmoInfo.getImplementationClass().getName();
          session.invalidateRecords(dmoImplName, null);
          FastFindCache ffCache = FastFindCache.getInstance(dmoInfo.isTempTable(),
                                                            getDatabase(!dmoInfo.multiTenant));
          ffCache.invalidate(dmoInfo.getId(), null);

          DmoVersioning versioning = defaultTenant.dmoVersion;

          if (lockSql != null)
          {
             LockManager<String> lm = getLockManager();
             ScrollableResults<Long> res = executeSQLQuery(lockSql, args);
             while (res.next())
             {
                Long pk = (Long) res.get(true)[0];
                lm.lock(LockType.EXCLUSIVE, new RecordIdentifier<>(dmoSqlTable, pk), true);

                versioning.increment(new RecordIdentifier<>(dmoImplName, pk));
             }
          }
          else
          {
             versioning.increment(dmoImplName);
          }
       }

       /**
        * Notify all buffers in the current FWD context, that they need to do a 'force refresh', as there have
        * been changes via direct JDBC access.
        *
        * @param    dmoIface
        *           The DMO which requires notifications.
        *
        * @throws   PersistenceException
        *           if any error occurs refreshing the buffers.
        */
       @Override
       public void refreshBuffers(Class<? extends DataModelObject> dmoIface)
       throws PersistenceException
       {
          handle hbuff = new handle();
          try
          {
             DmoMeta dmoInfo = DmoMetadataManager.registerDmo(dmoIface);

             // we need a buffer to use as 'reference' here.
             // Create a temporary one and delete it immediately after
             RecordBuffer.create(hbuff, dmoInfo.legacyTable);
             ChangeBroker changeBroker = ChangeBroker.get();

             BufferImpl buff = (BufferImpl) hbuff.get();
             RecordBuffer rb = buff.buffer();

             changeBroker.stateChanged(RecordChangeEvent.Type.BUFFER_REFRESH, rb, null, null, null);
          }
          finally
          {
             if (hbuff._isValid())
             {
                HandleOps.delete(hbuff);
             }
          }
       }

       /**
        * Get the database dialect helper object associated with the database
        * backing this object.
        *
        * @return  Database dialect helper.
        */
       public Dialect getDialect()
       {
          return dialect;
       }

       /**
        * Get the database lock time-out.
        *
        * @return  See above.
        */
       public long getTimeOut()
       {
          return timeout;
       }

       /**
        * Get the database information object associated with this persistence
        * service object. If the database cannot be retrieved from the context, take the main class object.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #rollback(boolean)} instead.
        *
        * @return  Database information object.
        */
       public Database getDatabase()
       {
          return defaultTenant.database;
       }

       /**
        * Get the database information object associated with this persistence
        * service object. If the database cannot be retrieved from the context, take the main class object.
        *
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @return  Database information object.
        */
       public Database getDatabase(boolean sharedDb)
       {
          if (tenantData.size() == 1)
          {
             return defaultTenant.database;
          }

          return getContext(sharedDb).tenant.database;
       }

       /**
        * Get the lock manager associated with this <code>Persistence</code>
        * instance.  For temp tables, this will be <code>null</code>, since temp
        * table data is naturally compartmentalized and record locking is
        * meaningless in this case.  For permanent databases, this method will
        * return the lock manager created when this class was instantiated.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #rollback(boolean)} instead.
        *
        * @return  Lock manager instance associated with this persistence service object.
        */
       public LockManager<String> getLockManager()
       {
          if (tenantData.size() == 1)
          {
             return defaultTenant.lockManager.get();
          }
          return getContext(PRIVATE_CTX).tenant.lockManager.get();
       }

       /**
        * Get the lock manager associated with this <code>Persistence</code>
        * instance.  For temp tables, this will be <code>null</code>, since temp
        * table data is naturally compartmentalized and record locking is
        * meaningless in this case.  For permanent databases, this method will
        * return the lock manager created when this class was instantiated.
        *
        * @param   shared
        *          Only used if a regular tenant is active.<br>
        *          If {@code true} the lock manager for default (shared/common) database is returned.
        *          Otherwise, the one of the tenant set on current context is returned.
        *
        * @return  Lock manager instance associated with this persistence service object.
        */
       public LockManager<String> getLockManager(boolean shared)
       {
          if (tenantData.size() == 1)
          {
             return defaultTenant.lockManager.get();
          }
          return getContext(shared).tenant.lockManager.get();
       }

       /**
        * Get the identity manager associated with this <code>Persistence</code>
        * instance.  For temp tables, this will be <code>null</code>, since for
        * temp tables unique primary keys generated using {@link
        * TemporaryBuffer#nextPrimaryKey()}. For permanent databases, this
        * method will return the identity manager created when this class was
        * instantiated.
        *
        * @param   shared
        *          Only used if a regular tenant is active.<br>
        *          If {@code true} the identity manager for default (shared/common) database is returned.
        *          Otherwise, the one of the tenant set on current context is returned.
        *
        * @return  Identity manager instance associated with this persistence service object.
        */
       public IdentityManager getIdentityManager(boolean shared)
       {
          if (tenantData.size() == 1)
          {
             return defaultTenant.identityManager.get();
          }
          return getContext(shared).tenant.identityManager.get();
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set. Records represented by
        * the result set are not locked by this method. This method creates a new, context local
        * ORM session if necessary.
        * <p>
        * <strong>Note:</strong><br>
        * While this method attempts to use a server-side database cursor to fetch results, some JDBC
        * driver implementations (e.g., PostgreSQL) may store an intermediate form of the
        * <em>entire</em> result set in memory instead, effectively doubling the amount of memory
        * required.  To avoid this, consider using {@link #scroll(String, Object[], int, int, DataRelation, int)}
        * instead.  Using that method with a {@code ScrollMode} of {@code FORWARD_ONLY} and setting a
        * JDBC fetch size to a reasonable value may allow a JDBC driver to use a server-side cursor to
        * fetch smaller batches of results at a time.  Consult your JDBC driver documentation and/or
        * source code to determine whether this is an issue for your JDBC implementation.
        *
        * @param   fql
        *          FQL query statement.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        *
        * @return  An object which provides access to a scrollable cursor on the result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(String fql, Object[] values)
       throws PersistenceException
       {
          return scroll(null, fql, values, 0, 0, null, ResultSet.TYPE_SCROLL_INSENSITIVE, false);
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set. Records represented by
        * the result set are not locked by this method. This method creates a new, context local
        * ORM session if necessary.
        * <p>
        * <strong>Note:</strong><br>
        * While this method attempts to use a server-side database cursor to fetch results, some JDBC
        * driver implementations (e.g., PostgreSQL) may store an intermediate form of the
        * <em>entire</em> result set in memory instead, effectively doubling the amount of memory
        * required.  To avoid this, consider using {@link #scroll(String, Object[], int, int, DataRelation, int)}
        * instead.  Using that method with a {@code ScrollMode} of {@code FORWARD_ONLY} and setting a
        * JDBC fetch size to a reasonable value may allow a JDBC driver to use a server-side cursor to
        * fetch smaller batches of results at a time.  Consult your JDBC driver documentation and/or
        * source code to determine whether this is an issue for your JDBC implementation.
        *
        * @param   fql
        *          FQL query statement.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   relation
        *          The recursive DATA-RELATION the query is to FILL for.
        *
        * @return  An object which provides access to a scrollable cursor on the result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(String fql, Object[] values, DataRelation relation)
       throws PersistenceException
       {
          return scroll(null, fql, values, 0, 0, relation, ResultSet.TYPE_SCROLL_INSENSITIVE, false);
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set. Records represented by
        * the result set are not locked by this method. This method creates a new, context local
        * ORM session if necessary.
        * <p>
        * <strong>Note:</strong><br>
        * While this method attempts to use a server-side database cursor to fetch results, some JDBC
        * driver implementations (e.g., PostgreSQL) may store an intermediate form of the
        * <em>entire</em> result set in memory instead, effectively doubling the amount of memory
        * required.  To avoid this, consider using {@link #scroll(String, Object[], int, int, DataRelation, int)}
        * instead.  Using that method with a {@code ScrollMode} of {@code FORWARD_ONLY} and setting a
        * JDBC fetch size to a reasonable value may allow a JDBC driver to use a server-side cursor to
        * fetch smaller batches of results at a time.  Consult your JDBC driver documentation and/or
        * source code to determine whether this is an issue for your JDBC implementation.
        *
        * @param   fql
        *          FQL query statement.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list.
        *          If this value is non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this
        *          value is non-positive, an offset of 0 is used by default.
        *
        * @return  An object which provides access to a scrollable cursor on the result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(String fql, Object[] values, int maxResults, int startOffset)
       throws PersistenceException
       {
          return scroll(null, fql, values, maxResults, startOffset, null, ResultSet.TYPE_SCROLL_INSENSITIVE, false);
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set. Records represented by
        * the result set are not locked by this method. This method creates a new, context local
        * ORM session if necessary.
        * <p>
        * <strong>Note:</strong><br>
        * While this method attempts to use a server-side database cursor to fetch results, some JDBC
        * driver implementations (e.g., PostgreSQL) may store an intermediate form of the
        * <em>entire</em> result set in memory instead, effectively doubling the amount of memory
        * required.  To avoid this, consider using {@link #scroll(String, Object[], int, int, DataRelation, int)}
        * instead.  Using that method with a {@code ScrollMode} of {@code FORWARD_ONLY} and setting a
        * JDBC fetch size to a reasonable value may allow a JDBC driver to use a server-side cursor to
        * fetch smaller batches of results at a time.  Consult your JDBC driver documentation and/or
        * source code to determine whether this is an issue for your JDBC implementation.
        *
        * @param   fql
        *          FQL query statement.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list.
        *          If this value is non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this
        *          value is non-positive, an offset of 0 is used by default.
        * @param   relation
        *          The recursive DATA-RELATION the query is to FILL for.
        *
        * @return  An object which provides access to a scrollable cursor on the result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(String fql,
                                              Object[] values,
                                              int maxResults,
                                              int startOffset,
                                              DataRelation relation)
       throws PersistenceException
       {
          return scroll(null, fql, values, maxResults, startOffset, relation, ResultSet.TYPE_SCROLL_INSENSITIVE, false);
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set.
        * Records represented by the result set are not locked by this method.
        * This method creates a new, context local ORM session if necessary.
        *
        * @param   fql
        *          FQL query statement.
        * @param   values
        *          Substitution values for the query.  If none, this should be an
        *          empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list.
        *          If this value is non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this
        *          value is non-positive, an offset of 0 is used by default.
        * @param   scrollMode
        *          Scroll mode which should apply to the result set.
        * @return  An object which provides access to a scrollable cursor on the
        *          result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(String fql,
                                              Object[] values,
                                              int maxResults,
                                              int startOffset,
                                              int scrollMode)
       throws PersistenceException
       {
          return scroll(null, fql, values, maxResults, startOffset, null, scrollMode, false);
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set.
        * Records represented by the result set are not locked by this method.
        * This method creates a new, context local ORM session if necessary.
        *
        * @param   fql
        *          FQL query statement.
        * @param   values
        *          Substitution values for the query.  If none, this should be an
        *          empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list.
        *          If this value is non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this
        *          value is non-positive, an offset of 0 is used by default.
        * @param   relation
        *          The recursive DATA-RELATION the query is to FILL for.
        * @param   scrollMode
        *          Scroll mode which should apply to the result set.
        * @return  An object which provides access to a scrollable cursor on the
        *          result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(String fql,
                                              Object[] values,
                                              int maxResults,
                                              int startOffset,
                                              DataRelation relation,
                                              int scrollMode)
       throws PersistenceException
       {
          return scroll(null, fql, values, maxResults, startOffset, relation, scrollMode, false);
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set. Records represented by the result
        * set are not locked by this method.
        *
        * @param   entities
        *          Names of the DMO entities associated with this query statement (Unused parameter).
        * @param   fql
        *          FQL query statement to be parsed and executed by ORM framework.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list. If this value is non-positive, no
        *          upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this value is non-positive, an offset
        *          of 0 is used by default.
        * @param   scrollMode
        *          Scroll mode which should apply to the result set.
        * @return  An object which provides access to a scrollable cursor on the result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(Class<? extends Record>[] entities,
                                              String fql,
                                              Object[] values,
                                              int maxResults,
                                              int startOffset,
                                              int scrollMode)
       throws PersistenceException
       {
          return scroll(entities, fql, values, maxResults, startOffset, null, scrollMode, false);
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set. Records represented by the result
        * set are not locked by this method.
        *
        * @param   entities
        *          Names of the DMO entities associated with this query statement (Unused parameter).
        * @param   fql
        *          FQL query statement to be parsed and executed by ORM framework.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list. If this value is non-positive, no
        *          upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this value is non-positive, an offset
        *          of 0 is used by default.
        * @param   relation
        *          The recursive DATA-RELATION the query is to FILL for.
        * @param   scrollMode
        *          Scroll mode which should apply to the result set.
        * @return  An object which provides access to a scrollable cursor on the result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(Class<? extends Record>[] entities,
                                              String fql,
                                              Object[] values,
                                              int maxResults,
                                              int startOffset,
                                              DataRelation relation,
                                              int scrollMode)
       throws PersistenceException
       {
           return scroll(entities, fql, values, maxResults, startOffset, relation, scrollMode, false);
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set. Records represented by the result
        * set are not locked by this method.
        *
        * @param   entities
        *          Names of the DMO entities associated with this query statement (Unused parameter).
        * @param   fql
        *          FQL query statement to be parsed and executed by ORM framework.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list. If this value is non-positive, no
        *          upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this value is non-positive, an offset
        *          of 0 is used by default.
        * @param   scrollMode
        *          Scroll mode which should apply to the result set.
        * @param   lazyMode
        *          Flag that indicates whether the query should be executed in a lazy manner.
        *
        * @return  An object which provides access to a scrollable cursor on the result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(Class<? extends Record>[] entities,
                                              String fql,
                                              Object[] values,
                                              int maxResults,
                                              int startOffset,
                                              int scrollMode,
                                              boolean lazyMode)
       throws PersistenceException
       {
          return scroll(entities, fql, values, maxResults, startOffset, null, scrollMode, lazyMode);
       }

       /**
        * Execute an FQL query and get a scrollable cursor on the result set. Records represented by the result
        * set are not locked by this method.
        *
        * @param   entities
        *          Names of the DMO entities associated with this query statement (Unused parameter).
        * @param   fql
        *          FQL query statement to be parsed and executed by ORM framework.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list. If this value is non-positive, no
        *          upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this value is non-positive, an offset
        *          of 0 is used by default.
        * @param   relation
        *          The recursive DATA-RELATION the query is to FILL for.
        * @param   scrollMode
        *          Scroll mode which should apply to the result set.
        * @param   lazyMode
        *          Flag that indicates whether the query should be executed in a lazy manner.
        *
        * @return  An object which provides access to a scrollable cursor on the result set.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> ScrollableResults<T> scroll(Class<? extends Record>[] entities,
                                              String fql,
                                              Object[] values,
                                              int maxResults,
                                              int startOffset,
                                              DataRelation relation,
                                              int scrollMode,
                                              boolean lazyMode)
       throws PersistenceException
       {
          Context local = getContext(entities);
          ScrollableResults<T> results = null;
          try
          {
             Query query = getQuery(local, fql, maxResults, startOffset, relation, values, lazyMode);
             results = query.scroll(local.getSession(), scrollMode);
          }
          catch (UdfException exc)
          {
             local.closeSession(true);
             ErrorEntry ee = exc.getErrorEntry();
             ErrorManager.recordOrShowError(
                      new int[] {ee.num},
                      new String[] {ee.text},
                      true,
                      ee.prefix,
                      false,
                      false,
                      false,
                      true);
             handleException(ErrorManager.buildErrorText(ee.num, ee.text, ee.prefix, false), exc);
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "error executing query [" + fql + "]");
          }

          return results;
       }

       /**
        * Execute an FQL query and return the results as a list. If the query returns multiple results
        * per row, each element in the list will be an array of {@code Object}s.
        * <p>
        * No locking is attempted.
        * <p>
        * <strong>Note:</strong><br>
        * This method should be used only for queries that are expected to return a relatively small
        * result set, since the entire result set is stored in the returned list.  Additionally, some
        * JDBC driver implementations (e.g., PostgreSQL) may store an intermediate form of the result
        * set in memory, effectively doubling the amount of memory required.  To avoid this, consider
        * using {@link #scroll(String, Object[], int, int, DataRelation, int)} instead.
        * Using that method with a
        * {@code ScrollMode} of {@code FORWARD_ONLY} and setting a JDBC fetch size to a reasonable
        * value may allow a JDBC driver to use a server-side cursor to fetch smaller batches of
        * results at a time.  Consult your JDBC driver documentation and/or source code to determine
        * whether this is an issue for your JDBC implementation.
        * <p>
        * This overloaded variant always attempts to hydrate and return the result as {@code Record}s.
        *
        * @param   fql
        *          FQL query statement.
        * @param   values
        *          Substitution values for the query. If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list.
        *          If this value is non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this
        *          value is non-positive, an offset of 0 is used by default.
        *
        * @return  A list of results, or {@code null} if no result was found.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> List<T> list(String fql, Object[] values, int maxResults, int startOffset)
       throws PersistenceException
       {
          return list(null, fql, values, maxResults, startOffset, null, true);
       }

       /**
        * Execute an FQL query and return the results as a list. If the query returns multiple results
        * per row, each element in the list will be an array of {@code Object}s.
        * <p>
        * No locking is attempted.
        * <p>
        * <strong>Note:</strong><br>
        * This method should be used only for queries that are expected to return a relatively small
        * result set, since the entire result set is stored in the returned list.  Additionally, some
        * JDBC driver implementations (e.g., PostgreSQL) may store an intermediate form of the result
        * set in memory, effectively doubling the amount of memory required.  To avoid this, consider
        * using {@link #scroll(String, Object[], int, int, DataRelation, int)} instead.
        * Using that method with a
        * {@code ScrollMode} of {@code FORWARD_ONLY} and setting a JDBC fetch size to a reasonable
        * value may allow a JDBC driver to use a server-side cursor to fetch smaller batches of
        * results at a time.  Consult your JDBC driver documentation and/or source code to determine
        * whether this is an issue for your JDBC implementation.
        * <p>
        * This overloaded variant always attempts to hydrate and return the result as {@code Record}s.
        *
        * @param   fql
        *          FQL query statement.
        * @param   values
        *          Substitution values for the query. If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list.
        *          If this value is non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this
        *          value is non-positive, an offset of 0 is used by default.
        * @param   relation
        *          The recursive DATA-RELATION the query is to FILL for.
        *
        * @return  A list of results, or {@code null} if no result was found.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> List<T> list(String fql,
                               Object[] values,
                               int maxResults,
                               int startOffset,
                               DataRelation relation)
       throws PersistenceException
       {
          return list(null, fql, values, maxResults, startOffset, relation, true);
       }

       /**
        * Execute an FQL query and return the results as a list. If the query returns multiple results
        * per row, each element in the list will be an array of {@code Object}s.
        * <p>
        * No locking is attempted.
        * <p>
        * <strong>Note:</strong><br>
        * This method should be used only for queries that are expected to return a relatively small
        * result set, since the entire result set is stored in the returned list.  Additionally, some
        * JDBC driver implementations (e.g., PostgreSQL) may store an intermediate form of the result
        * set in memory, effectively doubling the amount of memory required.  To avoid this, consider
        * using {@link #scroll(String, Object[], int, int, DataRelation, int)} instead.
        * Using that method with a
        * {@code ScrollMode} of {@code FORWARD_ONLY} and setting a JDBC fetch size to a reasonable
        * value may allow a JDBC driver to use a server-side cursor to fetch smaller batches of
        * results at a time.  Consult your JDBC driver documentation and/or source code to determine
        * whether this is an issue for your JDBC implementation.
        *
        * @param   <T>
        *          Type of object being listed.
        *
        * @param   entities
        *          Names of the DMO entities associated with this query statement (Unused parameter).
        * @param   fql
        *          FQL query statement to be processed. This method makes no assumptions as to the
        *          types of object(s) returned.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list. If this value is
        *          non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this value is non-positive,
        *          an offset of 0 is used by default.
        * @param   hydrateRecords
        *          If {@code true} the result is interpreted as legacy records, and they will be hydrated.
        *          Otherwise, the returned {@code List} contains an array of Java objects for each row.
        *
        * @return  A list of results, or {@code null} if no result was found.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> List<T> list(Class<? extends Record>[] entities,
                               String fql,
                               Object[] values,
                               int maxResults,
                               int startOffset,
                               boolean hydrateRecords)
       throws PersistenceException
       {
          return list(entities, fql, values, maxResults, startOffset, null, hydrateRecords);
       }

       /**
        * Execute an FQL query and return the results as a list. If the query returns multiple results
        * per row, each element in the list will be an array of {@code Object}s.
        * <p>
        * No locking is attempted.
        * <p>
        * <strong>Note:</strong><br>
        * This method should be used only for queries that are expected to return a relatively small
        * result set, since the entire result set is stored in the returned list.  Additionally, some
        * JDBC driver implementations (e.g., PostgreSQL) may store an intermediate form of the result
        * set in memory, effectively doubling the amount of memory required.  To avoid this, consider
        * using {@link #scroll(String, Object[], int, int, DataRelation, int)} instead.
        * Using that method with a
        * {@code ScrollMode} of {@code FORWARD_ONLY} and setting a JDBC fetch size to a reasonable
        * value may allow a JDBC driver to use a server-side cursor to fetch smaller batches of
        * results at a time.  Consult your JDBC driver documentation and/or source code to determine
        * whether this is an issue for your JDBC implementation.
        *
        * @param   <T>
        *          Type of object being listed.
        *
        * @param   entities
        *          Names of the DMO entities associated with this query statement (Unused parameter).
        * @param   fql
        *          FQL query statement to be processed. This method makes no assumptions as to the
        *          types of object(s) returned.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   maxResults
        *          The maximum number of elements to be returned in the list. If this value is
        *          non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this value is non-positive,
        *          an offset of 0 is used by default.
        * @param   relation
        *          The recursive DATA-RELATION the query is to FILL for.
        * @param   hydrateRecords
        *          If {@code true} the result is interpreted as legacy records, and they will be hydrated.
        *          Otherwise, the returned {@code List} contains an array of Java objects for each row.
        *
        * @return  A list of results, or {@code null} if no result was found.
        *
        * @throws  PersistenceException
        *          if there was an error executing the query.
        */
       public <T> List<T> list(Class<? extends Record>[] entities,
                               String fql,
                               Object[] values,
                               int maxResults,
                               int startOffset,
                               DataRelation relation,
                               boolean hydrateRecords)
       throws PersistenceException
       {
          Context local = getContext(entities);
          List<T> results = null;

          try
          {
             Query query = getQuery(local, fql, maxResults, startOffset, relation, values);
             results = query.list(local.getSession(), hydrateRecords);
          }
          catch (UdfException exc)
          {
             if (!local.isUnsafe())
             {
                cleanup();
                ErrorEntry ee = exc.getErrorEntry();
                ErrorManager.recordOrShowError(
                         new int[] {ee.num},
                         new String[] {ee.text},
                         true,
                         ee.prefix,
                         false,
                         false,
                         false,
                         true);
                handleException(ErrorManager.buildErrorText(ee.num, ee.text, ee.prefix, false), exc);
             }
             else
             {
                throw exc;
             }
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "error executing query [" + fql + "]");
          }

          return (results == null || results.isEmpty() ? null : results);
       }

       /**
        * Execute an FQL query and load a single record from the session-level cache or, optionally,
        * refresh it from the database. The record will be locked with the specified lock type upon
        * return from this method. Note that the lock refers to a runtime-level lock, not a
        * database-level lock. The record is <i>not</i> automatically loaded into the record buffer
        * provided. The <code>buffer</code> argument is used only to extract information needed
        * to perform the query.
        *
        * @param   buffer
        *          Record buffer which holds information about the DMO class type
        *          and name of the table being queried.
        * @param   fql
        *          FQL query statement. This may be either an ID-only projection query or a full query
        *          (see above).
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   lockType
        *          Type of lock the lock manager should register on the resulting
        *          record for the current user context.
        * @param   timeout
        *          Number of milliseconds to wait to acquire the requested lock type.
        *          Throws {@link LockTimeoutException} if this time elapses before the
        *          lock is acquired. Specify 0 to wait indefinitely for the lock. Ignored
        *          if {@code lockType} is {@code NONE}.
        * @param   unique
        *          {@code true} if no more than one result is expected from this query;
        *          {@code false} if the query could result in more than one record.
        * @param   updateLock
        *          {@code true} to update the current lock state;
        *          {@code false} to leave the current lock state unchanged.
        * @param   forceRefresh
        *          If {@code true} forces the record to be refreshed from database. Otherwise
        *          it will be refreshed only if the lockType has been upgraded from NONE.
        * @param   findByKey
        *          If {@code true}, this overrides the current {@code rereadnolock} setting;
        *          if {@code false}, honor {@code rereadnolock}.
        *
        * @return  A single DMO instance representing the query result, or {@code null} if no result
        *          was found.
        *
        * @throws  LockTimeoutException
        *          if a record was found, but its lock could not be acquired within the positive
        *          {@code timeout} period specified.
        * @throws  LockUnavailableException
        *          if a record was found and a no-wait lock type was specified, but the lock could
        *          not be acquired immediately.
        * @throws  PersistenceException
        *          if there was an error executing the query or if a unique query
        *          (i.e., {@code unique == true}) returned multiple results,
        */
       public Record load(RecordBuffer buffer,
                          String fql,
                          Object[] values,
                          LockType lockType,
                          long timeout,
                          boolean unique,
                          boolean updateLock,
                          boolean forceRefresh,
                          boolean findByKey)
       throws PersistenceException
       {
          boolean trace = LOG.isLoggable(Level.FINEST);
          boolean warn = (trace || LOG.isLoggable(Level.WARNING));
          boolean sharedDb = !buffer.getDmoInfo().multiTenant;
          Context local = getContext(sharedDb);
          boolean inlineLimit = (values == null || values.length == 0);
          Long id = null;
          Record initialResult = null;
          Record result = null;
          String table = buffer.getTable();
          LockType oldLock = null;
          RecordLockContext lockContext = local.getRecordLockContext();
          Session session = local.getSession();

          if (temporary)
          {
             updateLock = false;
          }

          try
          {
             // The inlineLimit flag is a crude test to detect a "simple" query:
             // no substitution parameters.  We want to avoid inlining the limit
             // on more complex statements, as the intended performance benefit
             // may backfire in these cases.
             dialect.inlineLimit(inlineLimit);

             // We have to give queries which are supposed to return a unique
             // result enough rope to hang themselves, so set max results to 2
             // for these.  All other queries are limited to 1 result.
             String entity = buffer.getEntityName();
             boolean readOnly = !temporary && DmoMetadataManager.isReadOnly(entity);
             Query query = getQuery(local, fql, unique ? 2 : 1, 0, null, values);

             // Reset ambiguous flag in buffer.
             buffer.setAmbiguous(false);

             // Find the record.
             Object o = query.uniqueResult(session);

             // Reset limit inlining if necessary.
             if (inlineLimit)
             {
                dialect.inlineLimit(false);
                inlineLimit = false;
             }

             if (o == null)
             {
                // No hit.
                return null;
             }

             if (o instanceof Record)
             {
                // This is the fast path where we assume we retrieved the DMO
                // directly instead of the ID because we don't have to lock
                // (we might need to unlock, however).
                // This is the case for temp-tables and no-lock queries.
                initialResult = result = (Record) o;

                // Lock record if required, remembering old lock type in case we
                // need to roll back on error.
                if (updateLock)
                {
                   id = result.primaryKey();
                   RecordIdentifier<String> ident = new RecordIdentifier<>(table, id);
                   oldLock = queryLock(ident, sharedDb);

                   if (oldLock.compareTo(lockType) != 0)
                   {
                      lockContext.lock(ident, lockType, timeout, null);

                      // ensure that if we have acquired a new lock, we have the latest version of
                      // the record from the database
                      if (!readOnly && !forceRefresh && oldLock == LockType.NONE)
                      {
    //                     LOG.info("LOAD refresh (lock acquisition 1) [" + fql + "]");
                         forceRefresh = true;
                      }
                   }
                }

                // If we found the record with a non-primary-key query and it already exists in any
                // buffer, it is possible it came from the session cache and needs to be refreshed
                if (!forceRefresh             &&
                    !temporary                &&
                    !readOnly                 &&
                    oldLock == LockType.NONE  &&
                    lockType == LockType.NONE &&
                    !findByKey                &&
                    local.isRereadNoLock())
                {
                   // if any other buffer references this DMO already, it means we got it from
                   // the session cache and it might be stale, so force a refresh
                   // TODO: goal is to make refresh unnecessary by knowing whether record came from
                   //       session cache or database
                   /*
                   int useCount = local.bufferManager.getDMOUseCount(result);
                   if (useCount > 0)
                   {
    //                  LOG.info("LOAD refresh (-rereadnolock 1) [" + fql + "]");
                      forceRefresh = true;
                   }
                   */
                }
             }
             else
             {
                // If we got here, we're dealing with a primary key query.
                id = (Long) o;
                RecordIdentifier<String> ident = new RecordIdentifier<>(table, id);

                // lock record if required, remembering old lock type in case we need to roll back on error
                if (updateLock)
                {
                   oldLock = queryLock(ident, sharedDb);
                   lockContext.lock(ident, lockType, timeout, null);
                }
                else
                {
                   lock(lockType, ident, false, sharedDb);
                }

                // We use get() instead of load() here because we don't want to return a proxy and
                // have to take another hit retrieving the data almost immediately after this method
                // returns, which is the common case for dynamic record retrieval.
                Class<? extends Record> dmoClass = buffer.getDMOImplementationClass();
                initialResult = result = session.get(dmoClass, id, null);

                if (result == null)
                {
                   return null;
                }

                // Ensure that if we are acquiring a lock, we have the latest version of the record
                // from the database. The Hibernate API advertises that Session.get() with a LockMode
                // of READ should bypass both levels of cache, but debugging indicates that the
                // record is always taken from the session cache if present.
                // So, we brute force a refresh instead.
                if (!forceRefresh && updateLock && oldLock == LockType.NONE && !readOnly)
                {
                   if (lockType != LockType.NONE)
                   {
    //                  LOG.info("LOAD refresh (lock acquisition 2) [" + fql + "]");

                      // forced refresh if lock is upgraded
                      forceRefresh = true;
                   }
                   // possibly force refresh if rereadnolock is enabled, and query is not by
                   // recid/rowid
                   // TODO: goal is to make refresh unnecessary
                   /*
                   else if (lockType == LockType.NONE && !findByKey && local.isRereadNoLock())
                   {
                      // if any other buffer references this DMO already, it means we got it from
                      // the session cache and it might be stale (since we have no lock on it), so
                      // force a refresh
                      int useCount = local.bufferManager.getDMOUseCount(result);
                      if (useCount > 0)
                      {
    //                     LOG.info("LOAD refresh (-rereadnolock 2) [" + fql + "]");
                         forceRefresh = true;
                      }
                   }
                   */
                }
             }

             // The Hibernate API advertises that Session.get() with a LockMode of READ should
             // bypass both levels of cache, but debugging indicates that the record is always
             // taken from the session cache if present. So, we brute force a refresh instead.
             // TODO: goal is to make refresh unnecessary
             /*
             if (!readOnly && forceRefresh)
             {
                Session session = local.getSession();
                session.refresh(result);
             }
             */
          }
          catch (UniqueResultException exc)
          {
             // This is recoverable, no need to close session.
             uniqueResultViolation(buffer, exc);
          }
          catch(UdfException exc)
          {
             result = null;
             ErrorEntry ee = exc.getErrorEntry();
             ErrorManager.recordOrShowError(
                      new int[] {ee.num},
                      new String[] {ee.text},
                      true,
                      ee.prefix,
                      false,
                      false,
                      false,
                      true);
             handleException(ErrorManager.buildErrorText(ee.num, ee.text, ee.prefix, false), exc);
          }
          catch (MissingRecordException | LockUnavailableException exc)
          {
             // the initial result we found must be evicted from the session cache
             if (initialResult != null) // NOTE: condition is always false
             {
                if (session != null)
                {
                   session.evict(initialResult);
                }
             }

             // Catching this exception indicates we were holding a stale copy of
             // a record that was deleted in the meantime.  This can happen in
             // bulk delete cases, or when a preselect query is holding an old
             // reference.  This is not a fatal error, it just means that the
             // refresh failed and the result is null.
             result = null;

             throw exc;
          }
          catch (PersistenceException exc)
          {
             if (!local.isUnsafe())
             {
                DBUtils.handleException(local.tenant.database, exc);

                // if exc was caused by ConditionException and
                //    ErrorManager is in CAN-FIND mode, let the session alive
                Throwable cause = exc.getCause();
                if (cause != null && !(cause instanceof ConditionException))
                {
                   cause = cause.getCause();
                }
                if (cause == null && !buffer.txHelper.errHlp.isInCanFindBracket())
                {
                   local.closeSession(true);
                }

                String msg = message("error loading record");

                if (warn)
                {
                   LOG.log(Level.WARNING, msg + System.lineSeparator() + fql);
                   if (LOG.isLoggable(Level.FINE))
                   {
                      LOG.log(Level.FINE, "Details:", exc);
                   }
                }

                handleException(msg, exc);
             }
             else
             {
                throw exc;
             }
          }
          finally
          {
             // Reset limit inlining if (still) necessary.
             if (inlineLimit)
             {
                dialect.inlineLimit(false);
             }

             if (updateLock      &&
                 id != null      &&
                 result == null  &&
                 oldLock != null &&
                 oldLock.compareTo(lockType) != 0)
             {
                // Reset lock to its previous state.  This is required if another
                // thread deleted the record after we got its ID but before we
                // retrieved its data.
                try
                {
                   RecordIdentifier<String> ident = new RecordIdentifier<>(table, id);
                   if (oldLock == LockType.NONE)
                   {
                      lockContext.forceRelease(ident, null);
                   }
                   else
                   {
                      lockContext.lock(ident, oldLock.toNoWaitVariant());
                   }
                }
                catch (LockUnavailableException exc)
                {
                   // OK to ignore.
                }
             }
          }

          return result;
       }

       /**
        * Load a single record from the session-level cache or, optionally, refresh it from the
        * database. The record will be locked with the specified lock type upon return from this
        * method. Note that the lock refers to a runtime-level lock, not a database-level lock.
        *
        * @param   implClass
        *          The DMO implementation class of record being read.
        * @param   id
        *          Primary key of the record to be loaded into the buffer.  May not be {@code null}.
        * @param   lockType
        *          Type of lock the lock manager should register on the resulting record for the
        *          current user context.
        * @param   timeout
        *          Number of milliseconds to wait to acquire the requested lock type. Throws
        *          {@link LockTimeoutException} if this time elapses before the lock is acquired.
        *          Specify 0 to wait indefinitely for the lock. Ignored if {@code lockType} is
        *          {@code NONE}.
        * @param   updateLock
        *          {@code true} to update the current lock state;
        *          {@code false} to leave the current lock state unchanged.
        *
        * @return  A single DMO instance loaded from the database, or {@code null} if no result was
        *          found.
        *
        * @throws  LockTimeoutException
        *          if a record was found, but its lock could not be acquired within the positive
        *          {@code timeout} period specified.
        * @throws  LockUnavailableException
        *          if a record was found and a no-wait lock type was specified, but the lock could
        *          not be acquired immediately.
        * @throws  PersistenceException
        *          if there was an error loading the DMO from the ORM session and/or database.
        */
       public Record load(Class<? extends Record> implClass,
                          Long id,
                          LockType lockType,
                          long timeout,
                          boolean updateLock)
       throws PersistenceException,
              LockTimeoutException
       {
          return load(implClass, id, lockType, timeout, updateLock, null);
       }

       /**
        * Load a single record from the session-level cache or, optionally, refresh it from the
        * database. The record will be locked with the specified lock type upon return from this
        * method. Note that the lock refers to a runtime-level lock, not a database-level lock.
        *
        * @param   implClass
        *          The DMO implementation class of record being read.
        * @param   id
        *          Primary key of the record to be loaded into the buffer.  May not be {@code null}.
        * @param   lockType
        *          Type of lock the lock manager should register on the resulting record for the
        *          current user context.
        * @param   timeout
        *          Number of milliseconds to wait to acquire the requested lock type. Throws
        *          {@link LockTimeoutException} if this time elapses before the lock is acquired.
        *          Specify 0 to wait indefinitely for the lock. Ignored if {@code lockType} is
        *          {@code NONE}.
        * @param   updateLock
        *          {@code true} to update the current lock state;
        *          {@code false} to leave the current lock state unchanged.
        * @param   partialFields
        *          For an incomplete record, read only these fields.
        *
        * @return  A single DMO instance loaded from the database, or {@code null} if no result was
        *          found.
        *
        * @throws  LockTimeoutException
        *          if a record was found, but its lock could not be acquired within the positive
        *          {@code timeout} period specified.
        * @throws  LockUnavailableException
        *          if a record was found and a no-wait lock type was specified, but the lock could
        *          not be acquired immediately.
        * @throws  PersistenceException
        *          if there was an error loading the DMO from the ORM session and/or database.
        */
       public Record load(Class<? extends Record> implClass,
                          Long id,
                          LockType lockType,
                          long timeout,
                          boolean updateLock,
                          BitSet partialFields)
       throws PersistenceException,
              LockTimeoutException
       {
          Record result = null;
          LockType oldLock = LockType.NONE;
          DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(implClass);
          Context local = getContext(!dmoInfo.multiTenant);
          String table = dmoInfo.getSqlTableName();
          RecordIdentifier<String> ident = new RecordIdentifier<>(table, id);
          RecordLockContext lockContext = local.getRecordLockContext();
          boolean resetLock = false;
          boolean vstTable = false;

          if (temporary)
          {
             updateLock = false;
          }

          if (local.tenant.database.isMeta())
          {
             // TODO: use a more efficient way to detect VST non-locking tables
             boolean isTrans = table.endsWith("_trans");
             boolean isLock = table.endsWith("_lock");

             // TODO: add all VST non-locking tables
             if (isTrans || isLock)
             {
                vstTable = true;
                updateLock = false;
             }
          }

          try
          {
             // lock record if required, remembering old lock type in case we need to roll back on error
             if (updateLock)
             {
                oldLock = queryLock(ident, !dmoInfo.multiTenant);
                lockContext.lock(ident, lockType, timeout, null);
             }
             else if (!vstTable)
             {
                lock(lockType, ident, false, !dmoInfo.multiTenant);
             }

             Session session = local.getSession();
             result = session.get(implClass, id, partialFields);

             if (result == null)
             {
                resetLock = true;

                return null;
             }
          }
          finally
          {
             if (resetLock && updateLock && oldLock.compareTo(lockType) != 0)
             {
                // reset lock to its previous state in case another thread deleted the record after
                // we got its ID but before we retrieved its data
                try
                {
                   if (oldLock == LockType.NONE)
                   {
                      lockContext.forceRelease(ident, null);
                   }
                   else
                   {
                      lockContext.lock(ident, oldLock.toNoWaitVariant());
                   }
                }
                catch (LockUnavailableException luExc)
                {
                   // OK to ignore.
                }
             }
          }

          return result;
       }

       /**
        * Load a single record from the database or from the session-level cache.
        * No locking is attempted, the assumption being that any required lock
        * previously was acquired.
        *
        * @param   ident
        *          Record identifier of the record to be loaded.  Consists of the
        *          primary key and the fully qualified DMO entity name.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        *
        * @return  A single DMO instance loaded from the database, or {@code null} if no result was found.
        *
        * @throws  PersistenceException
        *          if there was an error creating an ORM session or getting the requested record.
        */
       @Override
       public Record quickLoad(RecordIdentifier<String> ident, boolean sharedDb)
       throws PersistenceException
       {
          return quickLoad(ident, false, sharedDb);
       }

       /**
        * Load a single record from the database or from the session-level cache. No locking is attempted, the
        * assumption being that any required lock previously was acquired.
        *
        * @param   ident
        *          Record identifier of the record to be loaded.  Consists of the primary key and the fully
        *          qualified DMO entity name.
        * @param   refresh
        *          If {@code true} then re-read the state of the record from the underlying database. This is
        *          useful when record has been changed bypassing the FWD ORM.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        *
        * @return  A single DMO instance loaded from the database, or {@code null} if no result was found.
        *
        * @throws  PersistenceException
        *          if there was an error creating an ORM session or getting the requested record.
        */
       @Override
       public Record quickLoad(RecordIdentifier<String> ident, boolean refresh, boolean sharedDb)
       throws PersistenceException
       {
          Context local = getContext(sharedDb);
          String entity = ident.getTable();
          Long id = ident.getRecordID();
          Session session = local.getSession();
          Record result = null;
          boolean readOnly = !temporary && DmoMetadataManager.isReadOnly(entity);
          if (readOnly)
          {
             refresh = false;
          }

          try
          {
             result = session.get(entity, id);

             if (refresh && result != null)
             {
    //            LOG.info("QUICKLOAD refresh [" + ident + "]");
                session.refresh(result);
             }
          }
          catch (PersistenceException exc)
          {
             String msg = message("error executing quick load [" + entity + ":" + id + "]");
             handleException(local, exc, msg);
          }

          return result;
       }

       /**
        * Execute an arbitrary SQL statement which does not produce a result set.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #executeSQL(String, boolean)} instead.
        *
        * @param   sql
        *          SQL statement text.
        *
        * @return  The number of entities updated or deleted, or {@code -1} if the statement
        *          did not complete normally.
        *
        * @throws  PersistenceException
        *          if any error occurs executing the statement.
        */
       public int executeSQL(String sql)
       throws PersistenceException
       {
          return executeSQL(sql, Persistence.PRIVATE_CTX, null, false);
       }

       /**
        * Execute an arbitrary SQL statement which does not produce a result set.
        * <br><b>Note:</b>
        * Do not forget to invalidate the index-based cache ({@code FastFindCache}) of each buffer affected by
        * this statement. This cannot be done here because the method is not aware of the buffer list and if
        * multiple SQL statements are batch executed, then is better to issue a single invalidate command for all
        * of then.
        *
        * @param   sql
        *          SQL statement text.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        *
        * @return  The number of entities updated or deleted, or {@code -1} if the statement
        *          did not complete normally.
        *
        * @throws  PersistenceException
        *          if any error occurs executing the statement.
        */
       @Override
       public int executeSQL(String sql, boolean sharedDb)
       throws PersistenceException
       {
          return executeSQL(sql, sharedDb, null, false);
       }

       /**
        * Execute an arbitrary SQL statement which does not produce a result set.
        * <br><b>Note:</b>
        * Do not forget to invalidate the index-based cache ({@code FastFindCache}) of each buffer affected by
        * this statement. This cannot be done here because the method is not aware of the buffer list and if
        * multiple SQL statements are batch executed, then is better to issue a single invalidate command for all
        * of them.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #rollback(boolean)} instead.
        *
        * @param   sql
        *          SQL statement text.
        * @param   args
        *          The statement arguments.
        *
        * @return  The number of entities updated or deleted, or {@code -1} if the statement
        *          did not complete normally.
        *
        * @throws  PersistenceException
        *          if any error occurs executing the statement.
        */
       public int executeSQL(String sql, Object[] args)
       throws PersistenceException
       {
          return executeSQL(sql, SHARED_CTX, args, false);
       }

       /**
        * Execute an arbitrary SQL statement which does not produce a result set.
        * <br><b>Note:</b>
        * Do not forget to invalidate the index-based cache ({@code FastFindCache}) of each buffer affected by
        * this statement. This cannot be done here because the method is not aware of the buffer list and if
        * multiple SQL statements are batch executed, then is better to issue a single invalidate command for all
        * of them.
        *
        * @param   sql
        *          SQL statement text.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        * @param   args
        *          The statement arguments.
        *
        * @return  The number of entities updated or deleted, or {@code -1} if the statement
        *          did not complete normally.
        *
        * @throws  PersistenceException
        *          if any error occurs executing the statement.
        */
       @Override
       public int executeSQL(String sql, boolean sharedDb, Object[] args)
       throws PersistenceException
       {
          return executeSQL(sql, sharedDb, args, false);
       }

       /**
        * Execute an arbitrary SQL query.
        * <p>
        * It is the responsibility of the calling code to close the returned result set. In addition, if this
        * method is called outside a database transaction, the caller should optionally invoke
        * {@link Persistence#beginTransaction(boolean)} before calling this method and
        * {@link Persistence#commit(boolean)} or {@link Persistence#rollback(boolean)} after fully processing the
        * result set returned by this method. This will ensure a transaction is opened and properly closed if
        * necessary.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #executeSQLQuery(String, boolean, Object[])} instead.
        *
        * @param   sql
        *          SQL query text.
        * @param   args
        *          The query parameters.
        *
        * @return  Result set produced by query.
        *
        * @throws  PersistenceException
        *          if any error occurs executing the statement.
        */
       @Override
       public <T> ScrollableResults<T> executeSQLQuery(String sql, Object[] args)
       throws PersistenceException
       {
          return executeSQLQuery(sql, PRIVATE_CTX, args);
       }

       /**
        * Execute an arbitrary SQL query.
        * <p>
        * It is the responsibility of the calling code to close the returned result set. In addition, if this
        * method is called outside a database transaction, the caller should optionally invoke
        * {@link Persistence#beginTransaction(boolean)} before calling this method and
        * {@link Persistence#commit(boolean)} or {@link Persistence#rollback(boolean)} after fully processing the
        * result set returned by this method. This will ensure a transaction is opened and properly closed if
        * necessary.
        *
        * @param   sql
        *          SQL query text.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        * @param   args
        *          The query parameters.
        *
        * @return  Result set produced by query.
        *
        * @throws  PersistenceException
        *          if any error occurs executing the statement.
        */
       @Override
       public <T> ScrollableResults<T> executeSQLQuery(String sql, boolean sharedDb, Object[] args)
       throws PersistenceException
       {
          long start = 0L;
          boolean trace = LOG.isLoggable(Level.FINEST);
          if (trace)
          {
             start = System.currentTimeMillis();
          }

          Context local = getContext(sharedDb);
          Session session = local.getSession();
          ScrollableResults<T> results = null;

          try
          {
             SQLQuery query = Session.createSQLQuery(sql);

             if (args != null)
             {
                for (int i = 0; i < args.length; i++)
                {
                   query.setParameter(i, args[i]);
                }
             }

             results = query.scroll(session, null);
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "Error executing SQL statement:  " + sql);
          }
          finally
          {
             if (trace)
             {
                long elapsed = System.currentTimeMillis() - start;
                LOG.log(Level.FINEST, message("TIMING:SQLQ [" + elapsed + "] " + sql));
             }
          }

          return results;
       }

       /**
        * Convenience method to retrieve a single result from a SQL statement. This is intended to
        * be used for SQL statements that return only one result. Only the first result is checked;
        * if there is more than one in the result set, all but the first are ignored.
        * <p>
        * The result set and statement are closed immediately; no additional cleanup is required by the caller.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #getSingleSQLResult(String, boolean, Object[])} instead.
        *
        * @param   sql
        *          SQL statement to execute.
        * @param   args
        *          Query substitution parameters, if any. If present, these should match the number
        *          of placeholders in the SQL statement. If not present, this argument may either be
        *          {@code null} or an empty array.
        *
        * @return  The first (and presumably only) result returned by the SQL statement.
        *
        * @throws  PersistenceException
        *          if any error occurs, either during the statement's execution, or when pushing or
        *          popping an implicit transaction.
        */
       @Override
       public <T> T getSingleSQLResult(String sql, Object[] args)
       throws PersistenceException
       {
          return getSingleSQLResult(sql, PRIVATE_CTX, args);
       }

       /**
        * Convenience method to retrieve a single result from a SQL statement. This is intended to
        * be used for SQL statements that return only one result. Only the first result is checked;
        * if there is more than one in the result set, all but the first are ignored.
        * <p>
        * The result set and statement are closed immediately; no additional cleanup is required by the caller.
        *
        * @param   sql
        *          SQL statement to execute.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        * @param   args
        *          Query substitution parameters, if any. If present, these should match the number
        *          of placeholders in the SQL statement. If not present, this argument may either be
        *          {@code null} or an empty array.
        *
        * @return  The first (and presumably only) result returned by the SQL statement.
        *
        * @throws  PersistenceException
        *          if any error occurs, either during the statement's execution, or when pushing or
        *          popping an implicit transaction.
        */
       @Override
       public <T> T getSingleSQLResult(String sql, boolean sharedDb, Object[] args)
       throws PersistenceException
       {
          Context local = getContext(sharedDb);

          // if the context has an active session, use it; otherwise, create a short-lived session
          Session session = local.getSessionNoCreate();
          boolean shortLived = session == null;
          boolean commit;
          if (shortLived)
          {
             session = new Session(local.tenant.database, null, null, local.getTenantName());
             commit = session.beginTransaction();
          }
          else
          {
             commit = local.beginTransaction(null);
          }

          try
          {
             try (PreparedStatement stmt = session.getConnection().prepareStatement(sql))
             {
                if (args != null)
                {
                   int len = args.length;
                   for (int i = 0; i < len; i++)
                   {
                      stmt.setObject(i + 1, args[i]);
                   }
                }

                QueryStatement f = PreparedStatement::executeQuery;
                ResultSet rs = SQLExecutor.getInstance().execute(local.tenant.database, sql, f, stmt);

                return rs.next() ? (T) rs.getObject(1) : null;
             }
             catch (SQLException e)
             {
                throw new PersistenceException("Failed to execute [" + sql + "]", e);
             }
          }
          catch (PersistenceException exc)
          {
             if (!local.isUnsafe())
             {
                commit = false;

                DBUtils.handleException(local.tenant.database, exc);

                if (shortLived)
                {
                   session.rollback();
                }
                else
                {
                   local.closeSession(true);
                }

                String msg = "Error executing SQL statement:  " + sql;
                handleException(msg, exc);

                return null;
             }
             else
             {
                throw exc;
             }
          }
          finally
          {
             if (shortLived)
             {
                if (commit)
                {
                   session.commit();
                }

                try
                {
                   session.close();
                }
                catch (PersistenceException exc)
                {
                   if (LOG.isLoggable(Level.FINER))
                   {
                      LOG.log(Level.FINER, "Error closing session", exc);
                   }
                }
             }
             else if (commit)
             {
                local.commit();
             }
          }
       }

       /**
        * Execute an arbitrary list of SQL statements in batch.
        *
        * @param   sql
        *          List of SQL statements to execute in batch.
        * @param   noFlush
        *          If {@code true}, flushing is bypassed.
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @throws  PersistenceException
        *          if any error occurs executing the statements.
        */
       public void executeSQLBatch(List<String> sql, boolean noFlush, boolean sharedDb)
       throws PersistenceException
       {
          executeSQLBatch(getContext(sharedDb), sql, noFlush);
       }

       /**
        * Attempt to obtain the specified lock type on a particular record.  If the lock cannot be
        * obtained immediately, the current thread will block until the lock becomes available and
        * it is obtained, unless the requested lock type is a "no-wait" lock.
        * <p>
        * In the no-wait case, an exception is raised indicating that the lock is not available.
        * <p>
        * The current thread continues normally once the lock is obtained.
        * <p>
        * A lock currently held can be released by specifying {@code LockType.NONE} for the
        * {@code lockType} parameter.
        * <p>
        * The normal return of this method indicates that the lock request (or lack thereof)
        * completed successfully, and that the current context now holds a lock of the requested
        * type.
        * <p>
        * NOTE: legacy persistence framework code should access this method only via a {@link
        * RecordLockContext} method, unless simply testing whether a lock is available (i.e.,
        * {@code update == false}). This ensures that record locks shared across record buffers
        * within a client context are managed properly.  Non-legacy code may access this API
        * directly.
        *
        * @param   lockType
        *          Type of lock to be obtained. {@code LockType.NONE} is used to release an existing
        *          lock, and to continue with no lock.
        * @param   ident
        *          ID which uniquely identifies the record being locked.
        * @param   update
        *          {@code true} if the lock state for the current record should be updated;
        *          else {@code false}.
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @throws  LockUnavailableException
        *          if a "no-wait" lock cannot be acquired immediately.
        *
        * @see     #getRecordLockContext
        */
       public void lock(LockType lockType, RecordIdentifier<String> ident, boolean update, boolean sharedDb)
       throws LockUnavailableException
       {
          Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(sharedDb).tenant;
          if (tenant.lockManager.wasSet() && tenant.lockManager.get() != null)
          {
             tenant.lockManager.get().lock(lockType, ident, update);
          }
       }

       /**
        * Attempt to obtain the specified lock type on a particular record.  If the lock cannot be
        * obtained immediately, the current thread will block until the lock becomes available and
        * it is obtained, unless the requested lock type is a "no-wait" lock.
        * <p>
        * In the no-wait case, an exception is raised indicating that the lock is not available.
        * <p>
        * The current thread continues normally once the lock is obtained.
        * <p>
        * A lock currently held can be released by specifying {@code LockType.NONE} for the
        * {@code lockType} parameter.
        * <p>
        * The normal return of this method indicates that the lock request (or lack thereof)
        * completed successfully, and that the current context now holds a lock of the requested
        * type.
        * <p>
        * NOTE: legacy persistence framework code should access this method only via a {@link
        * RecordLockContext} method, unless simply testing whether a lock is available (i.e.,
        * {@code update == false}). This ensures that record locks shared across record buffers
        * within a client context are managed properly.  Non-legacy code may access this API
        * directly.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #lock(LockType, RecordIdentifier, boolean, long, boolean)} instead.
        *
        * @param   lockType
        *          Type of lock to be obtained. {@code LockType.NONE} is used to release an existing
        *          lock, and to continue with no lock.
        * @param   ident
        *          ID which uniquely identifies the record being locked.
        * @param   update
        *          {@code true} if the lock state for the current record should be updated; else {@code false}.
        *
        * @throws  LockUnavailableException
        *          if a "no-wait" lock cannot be acquired immediately.
        *
        * @see     #getRecordLockContext
        */
       public void lock(LockType lockType, RecordIdentifier<String> ident, boolean update)
       throws LockUnavailableException
       {
          Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(PRIVATE_CTX).tenant;
          if (tenant.lockManager.wasSet() && tenant.lockManager.get() != null)
          {
             tenant.lockManager.get().lock(lockType, ident, update);
          }
       }

       /**
        * Attempt to obtain the specified lock type on a particular record.  If the lock cannot be
        * obtained immediately, the current thread will block until the lock becomes available and
        * it is obtained, unless the requested lock type is a "no-wait" lock, or a positive timeout
        * value is specified and that time elapses before the lock is acquired.
        * <p>
        * In the no-wait case, an exception is raised indicating that the lock is not available.
        * In the timeout case, an exception is raised indicating that the timeout period elapsed.
        * <p>
        * The current thread continues normally once the lock is obtained.
        * <p>
        * A lock currently held can be released by specifying {@code LockType.NONE} for the
        * {@code lockType} parameter.
        * <p>
        * The normal return of this method indicates that the lock request (or lack thereof)
        * completed successfully, and that the current context now holds a lock of the requested
        * type.
        * <p>
        * NOTE: legacy persistence framework code should access this method only via a {@link
        * RecordLockContext} method, unless simply testing whether a lock is available (i.e.,
        * {@code update == false}). This ensures that record locks shared across record buffers
        * within a client context are managed properly.  Non-legacy code may access this API
        * directly.
        *
        * @param   lockType
        *          Type of lock to be obtained. {@code LockType.NONE} is used to release an existing
        *          lock, and to continue with no lock.
        * @param   ident
        *          ID which uniquely identifies the record being locked.
        * @param   update
        *          {@code true} if the lock state for the current record should be updated; else {@code false}.
        * @param   timeout
        *          Number of milliseconds to wait to acquire the requested lock type.
        *          Throws {@link LockTimeoutException} if this time elapses before the
        *          lock is acquired. Specify 0 to wait indefinitely for the lock. Ignored
        *          if {@code lockType} is {@code NONE}.
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @throws  LockTimeoutException
        *          if a lock cannot be acquired within the positive {@code timeout} period specified.
        * @throws  LockUnavailableException
        *          if a "no-wait" lock cannot be acquired immediately.
        *
        * @see     #getRecordLockContext
        */
       public void lock(LockType lockType,
                        RecordIdentifier<String> ident,
                        boolean update,
                        long timeout,
                        boolean sharedDb)
       throws LockUnavailableException
       {
          Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(sharedDb).tenant;
          if (tenant.lockManager.wasSet() && tenant.lockManager.get() != null)
          {
             tenant.lockManager.get().lock(lockType, ident, update, timeout);
          }
       }

       /**
        * Attempt to obtain the specified lock type on a table. If the lock cannot be obtained
        * immediately, the current thread will block until the lock becomes available and is
        * obtained, unless the requested lock type is a "no-wait" lock.  In the latter case,
        * an exception is raised, indicating that the lock is not available. The current thread
        * continues once the lock is obtained.
        * <p>
        * A lock currently held can be released by specifying
        * <code>LockType.NONE</code> for the <code>lockType</code> parameter.
        * <p>
        * Method returns previous lock type (so that calling code knows what to set it back to after
        * bulk delete).
        * The normal return of this method indicates that the lock request (or lack thereof)
        * completed successfully, and that the current context now holds a lock of the requested
        * type.
        *
        * @param   table
        *          Table name.
        * @param   lockType
        *          Type of lock to be obtained.  <code>LockType.NONE</code> is used to release an
        *          existing lock, and to continue with no lock.
        * @param   update
        *          {@code true} if the lock state for the current table should be updated; else {@code false}.
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @throws  LockUnavailableException
        *          if a "no-wait" lock cannot be acquired immediately.
        *
        * @return  previous lock type.
        */
       public LockType lockTable(String table, LockType lockType, boolean update, boolean sharedDb)
       throws LockUnavailableException
       {
          Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(sharedDb).tenant;
          if (tenant.lockManager.wasSet() && tenant.lockManager.get() != null)
          {
             return tenant.lockManager.get().lockTable(lockType, table, update);
          }

          return LockType.NONE;
       }

       /**
        * Query the lock type currently held by the current context for the
        * specified database record.
        * <p>
        * This method will always return the <b>non-</b><code>NO_WAIT</code>
        * variants of a lock type.  That is, even if the actual lock type is
        * <code>LockType.EXCLUSIVE_NO_WAIT</code> or
        * <code>LockType.SHARE_NO_WAIT</code>, the simpler types of
        * <code>LockType.EXCLUSIVE</code> and <code>LockType.SHARE</code>,
        * respectively, are returned.  The lack of a lock returns
        * <code>LockType.NONE</code> rather than <code>null</code>.
        *
        * @param   ident
        *          ID which uniquely identifies the record being queried.
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @return  The lock type currently held by the current context, never {@code null}.
        */
       public LockType queryLock(RecordIdentifier<String> ident, boolean sharedDb)
       {
          Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(sharedDb).tenant;
          if (tenant.lockManager.wasSet() && tenant.lockManager.get() == null)
          {
             return LockType.NONE;
          }

          return tenant.lockManager.get().queryLock(ident);
       }

       /**
        * Obtain the next available primary key ID on a per-table basis.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #nextPrimaryKey(String, boolean)} instead.
        *
        * @param   table
        *          Table for which next primary key will be returned.
        *
        * @return  The next available ID for the given table.
        *
        * @throws  PersistenceException
        *          if there is an error determining the next primary key ID.
        * @throws  IllegalStateException
        *          if identity manager was not initialized.
        */
       public Long nextPrimaryKey(String table)
       throws PersistenceException
       {
          Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(PRIVATE_CTX).tenant;
          if (tenant.identityManager.wasSet() && tenant.identityManager.get() != null)
          {
             return tenant.identityManager.get().nextPrimaryKey(table);
          }

          throw new IllegalStateException("identity manager should be initialized");
       }

       /**
        * Obtain the next available primary key ID on a per-table basis.
        *
        * @param   table
        *          Table for which next primary key will be returned.
        * @param   shared
        *          Only used if a regular tenant is active.<br>
        *          If {@code true} the lock manager for default (shared/common) database is returned.
        *          Otherwise, the one of the tenant set on current context is returned.
        *
        * @return  The next available ID for the given table.
        *
        * @throws  PersistenceException
        *          if there is an error determining the next primary key ID.
        * @throws  IllegalStateException
        *          if identity manager was not initialized.
        */
       public Long nextPrimaryKey(String table, boolean shared)
       throws PersistenceException
       {
          Tenant tenant = (tenantData.size() == 1) ? defaultTenant : getContext(shared).tenant;
          if (tenant.identityManager.wasSet() && tenant.identityManager.get() != null)
          {
             return tenant.identityManager.get().nextPrimaryKey(table);
          }

          throw new IllegalStateException("identity manager should be initialized");
       }

       /**
        * Updates the existing record in the backing database session. It is assumed this method will be invoked only
        * within a database transaction.
        *
        * @param   dmo
        *          DMO to be persisted.
        *
        * @throws  PersistenceException
        *          if a problem occurs saving the object in the current session.
        */
       public void save(Record dmo)
       throws PersistenceException
       {
          save(dmo, dmo.primaryKey(), true);
       }

       /**
        * Persist the specified object to the backing database session, using the given ID. It is assumed
        * this method will be invoked only within a database transaction.
        *
        * @param   dmo
        *          DMO to be persisted.
        * @param   id
        *          Primary key to associate with the persisted DMO.
        *
        * @throws  PersistenceException
        *          if a problem occurs saving the object in the current session.
        */
       public void save(Record dmo, Long id)
       throws PersistenceException
       {
          save(dmo, id, true);
       }

       /**
        * Persist the specified object to the backing database session, using the given ID. It is assumed
        * this method will be invoked only within a database transaction.
        *
        * @param   dmo
        *          DMO to be persisted.
        * @param   id
        *          Primary key to associate with the persisted DMO.
        * @param   updateDmoState
        *          {@code true} to update the record state of the DMO. Should be {@code false} for non-legacy
        *          use cases).
        *
        * @throws  PersistenceException
        *          if a problem occurs saving the object in the current session.
        */
       public void save(Record dmo, Long id, boolean updateDmoState)
       throws PersistenceException
       {
          Long dmoKey = dmo.primaryKey();

          if (dmoKey != null && dmoKey < 0)
          {
             // this should not happen: we have already tested this in RecordBuffer in the event of
             // a field change. Since the template record cannot be altered, it should also not need
             // to be flushed/saved to persistence.
             final String msg = "Cannot update a template record without a schema lock";
             ErrorManager.recordOrThrowError(12376, msg, false, false);

             // print the error as SEVERE
             if (LOG.isLoggable(Level.SEVERE))
             {
                LOG.log(Level.SEVERE,
                        "Template records MUST not be flushed/saved to database!",
                        new PersistenceException("Saving template records to database is forbidden"));
             }

             return;
          }

          Context local = getContext(!dmo._getRecordMeta().getDmoMeta().multiTenant);
          boolean inTx = local.beginTransaction(null);

          try
          {
             if (dmoKey == null)
             {
                dmo.primaryKey(id);
             }

             local.getSession().save(dmo, updateDmoState);

             if (inTx)
             {
                local.commit();
             }

             if (LOG.isLoggable(Level.FINER))
             {
                LOG.log(Level.FINER, message("Persisted DMO (" + dmo + ") with ID " + id));
             }
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "error persisting object");
          }
       }

       /**
        * Merges the state of the given object with the persistent object with the same identifier and
        * returns the merged object.
        *
        * @param   dmo
        *          DMO to merge.
        *
        * @return  Merged DMO.
        *
        * @throws  PersistenceException
        *          in error
        *
        * @deprecated
        */
       @Deprecated
       public Record merge(Record dmo)
       throws PersistenceException
       {
          Context local = getContext(!dmo._getRecordMeta().getDmoMeta().multiTenant);
          Record merged = null;

          try
          {
             merged = local.getSession().merge(dmo);
             // Hibernate's Session.merge() saves the record if not done already.
             // The caller must mark the dmo as persisted.

             if (LOG.isLoggable(Level.FINER))
             {
                LOG.log(Level.FINER, message("Merged DMO (" + dmo + ")"));
             }
          }
          catch (PersistenceException exc)
          {
             DBUtils.handleException(local.tenant.database, exc);

             local.closeSession(true);

             handleException("error merging object", exc);
          }

          return merged;
       }

       /**
        * Get the ORM session associated with the current context, creating one if needed.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #getSession(boolean)} instead.
        *
        * @return  ORM session for the current context.
        */
       public Session getSession()
       {
          try
          {
             return getContext(PRIVATE_CTX).getSession();
          }
          catch (PersistenceException e)
          {
             throw new IllegalStateException("Cannot get database session");
          }
       }

       /**
        * Get the ORM session associated with the current context, creating one if needed.
        *
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @return  ORM session for the current context.
        */
       public Session getSession(boolean sharedDb)
       {
          try
          {
             return getContext(sharedDb).getSession();
          }
          catch (PersistenceException e)
          {
             throw new IllegalStateException("Cannot get database session");
          }
       }

       /**
        * Get the context for the current user session.
        * <p>
        * NOTE: this method is package private and should remain so. It is meant to allow optimized
        *       calls to certain frequently used methods of this class, in order to avoid the overhead of
        *       an unnecessary context local lookup.
        *
        * @param   sharedDb
        *          In case of tenant databases, if this is {@code true}, the common (default/shared) context is
        *          returned. Otherwise, the private context of the current tenant is returned. If there is no
        *          active tenant, the context of the default database is returned.
        *
        * @return  User context.
        */
       public Context getContext(boolean sharedDb)
       {
          Context[] ctxs = contexts.get();
          // even if the private context is requested, the shared instance is returned if private was not created
          return ctxs[sharedDb || ctxs[PRIVATE_IDX] == null ? SHARED_IDX : PRIVATE_IDX];
       }

       /**
        * Clean up the current contexts.
        */
       public void cleanup()
       {
          Context[] ctxt = contexts.get(false);
          if (ctxt != null)
          {
             ctxt[SHARED_IDX].cleanup();
             if (ctxt[PRIVATE_IDX] != null)
             {
                ctxt[PRIVATE_IDX].cleanup();
             }
          }
       }

       /**
        * Delete the persisted object from the specified database.
        *
        * @param   dmo
        *          Persistent entity to be deleted.
        *
        * @throws  PersistenceException
        *          if a problem occurs deleting the object.
        */
       public void delete(Record dmo)
       throws PersistenceException
       {
          Context local = getContext(!dmo._getRecordMeta().getDmoMeta().multiTenant);

          try
          {
             Session session = local.getSession();
             session.delete(dmo);
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "error deleting persistent object");
          }
       }

       /**
        * Perform a bulk delete or update of all objects which meet the specified
        * criteria. The criteria must indicate a single DMO type only.
        * <p>
        * This action must take place within a database transaction, so this
        * method attempts to begin one and commit it, if one is not already
        * active.
        * <p>
        * This method should only be used for temp-tables (where an exclusive
        * lock is irrelevant) or in cases where the current context already holds
        * locks on all records which meet the specified criteria.
        *
        * @param   fql
        *          Query string which defines the criteria which determine which records are deleted
        *          or updated.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        * @param   values
        *          Query substitution parameters, if any.
        * @param   entity
        *          Entity associated with the FQL statement, if any. Used to check whether a session
        *          flush is needed before execution.
        * @param   noUndo
        *          {@code true} if this operation is being performed on a NO-UNDO temp-table, else
        *          {@code false}.
        *
        * @return  The number of entities deleted or updated.
        *
        * @throws  PersistenceException
        *          if a problem occurs performing the bulk delete or update.
        */
       @Override
       public int deleteOrUpdate(String fql, boolean sharedDb, Object[] values, String entity, boolean noUndo)
       throws PersistenceException
       {
          boolean trace = LOG.isLoggable(Level.FINEST);
          long start = System.currentTimeMillis();

          Context local = getContext(sharedDb);
          int count = 0;
          boolean inTx = local.beginTransaction(null);

          try
          {
             Session session = local.getSessionNoCreate();

             Query query = getQuery(local, fql, 0, 0, null, values, false);
             count = query.executeUpdate(session);

             if (inTx)
             {
                local.commit();
             }

             if (LOG.isLoggable(Level.FINER))
             {
                LOG.log(Level.FINER, "[" + fql + "] count = " + count);
             }
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "error performing bulk delete [" + fql + "]");
          }
          finally
          {
             if (trace)
             {
                long elapsed = System.currentTimeMillis() - start;
                LOG.log(Level.FINEST, message("TIMING:DELETE [" + elapsed + "] " + fql));
             }
          }

          return count;
       }

       /**
        * Make sure a (new) transaction instance is created and stored in the local context. A new
        * database session is created if one does not already exist in the current context.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #beginTransaction(boolean)} instead.
        *
        * @return  {@code true} if a new transaction was begun;
        *          {@code false} if there was already an active transaction.
        *
        * @throws  PersistenceException
        *          if there is an error beginning the new transaction.
        */
       public boolean beginTransaction()
       throws PersistenceException
       {
          return beginTransaction(null, PRIVATE_CTX);
       }

       /**
        * Make sure a (new) transaction instance is created and stored in the local context. A new
        * database session is created if one does not already exist in the current context.
        *
        * @return  {@code true} if a new transaction was begun;
        *          {@code false} if there was already an active transaction.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        *
        * @throws  PersistenceException
        *          if there is an error beginning the new transaction.
        */
       public boolean beginTransaction(boolean sharedDb)
       throws PersistenceException
       {
          return beginTransaction(null, sharedDb);
       }

       /**
        * Commit the current database transaction and close the current session. If the transaction cannot be
        * committed, roll it back and raise an error.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #commit(boolean)} instead.
        *
        * @throws  IllegalStateException
        *          if no transaction is active in the current session;  this
        *          represents a programming error.
        * @throws  PersistenceException
        *          if there is an error committing the transaction.
        */
       @Override
       public void commit()
       throws PersistenceException
       {
          commit(getContext(PRIVATE_CTX));
       }

       /**
        * Commit the current database transaction and close the current session.
        * If the transaction cannot be committed, roll it back and raise an error.
        *
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        *
        * @throws  IllegalStateException
        *          if no transaction is active in the current session;  this
        *          represents a programming error.
        * @throws  PersistenceException
        *          if there is an error committing the transaction.
        */
       @Override
       public void commit(boolean sharedDb)
       throws PersistenceException
       {
          commit(getContext(sharedDb));
       }

       /**
        * Roll back the current database transaction and close the current
        * session.  If the transaction cannot be rolled back, raise an error.
        *
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        *
        * @throws  IllegalStateException
        *          if no transaction is active in the current session;  this represents a programming error.
        * @throws  PersistenceException
        *          if there is an error rolling back the transaction.
        */
       @Override
       public void rollback(boolean sharedDb)
       throws PersistenceException
       {
          rollback(getContext(sharedDb));
       }

       /**
        * Roll back the current database transaction and close the current session.  If the transaction cannot be
        * rolled back, raise an error.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #rollback(boolean)} instead.
        *
        * @throws  IllegalStateException
        *          if no transaction is active in the current session;  this represents a programming error.
        * @throws  PersistenceException
        *          if there is an error rolling back the transaction.
        */
       @Override
       public void rollback()
       throws PersistenceException
       {
          rollback(getContext(PRIVATE_CTX));
       }

       /**
        * Flush the current session, which synchronizes the in-memory state of DMOs to the database.
        * <p>
        * This request bypasses the current client context's setting as to whether flushing is enabled.
        * <p>
        * It is expected that this method will only be called within an active
        * transaction.  If no session is active, this method will raise an exception.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #flush(boolean)} instead.
        *
        * @throws  IllegalStateException
        *          if no session currently is active.
        * @throws  PersistenceException
        *          if an error occurs during session flush.
        */
       @Override
       public void flush()
       throws PersistenceException
       {
          Context local = getContext(PRIVATE_CTX);
          Session session = local.getSessionNoCreate();

          // Sanity.
          if (session == null)
          {
             throw new PersistenceException("No active session to flush");
          }

          flush(local, session, false);
       }

       /**
        * Flush the current session, which synchronizes the in-memory state of DMOs to the database.
        * <p>
        * This request bypasses the current client context's setting as to whether flushing is enabled.
        * <p>
        * It is expected that this method will only be called within an active
        * transaction.  If no session is active, this method will raise an exception.
        *
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @throws  IllegalStateException
        *          if no session currently is active.
        * @throws  PersistenceException
        *          if an error occurs during session flush.
        */
       @Override
       public void flush(boolean sharedDb)
       throws PersistenceException
       {
          Context local = getContext(sharedDb);
          Session session = local.getSessionNoCreate();

          // Sanity.
          if (session == null)
          {
             throw new PersistenceException("No active session to flush");
          }

          flush(local, session, false);
       }

       /**
        * Indicate whether an explicit, database-level transaction currently is open.
        * <p>
        * <strong>NOTE:</strong><br>This method is not multi-tenant compatible. Although it will do the best
        *              effort to pick the right context, there are significant chances it WILL NOT work
        *              correctly in such environment. In single-tenant code it should work correctly, though.
        *              <p>To specify the user context for executing the query,
        *              use {@link #isTransactionOpen(boolean)} instead.
        *
        * @return  {@code true} if open, else {@code false}.
        */
       public boolean isTransactionOpen()
       {
          return getContext(PRIVATE_CTX).isTransactionOpen();
       }

       /**
        * Indicate whether an explicit, database-level transaction currently is open.
        *
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @return  {@code true} if open, else {@code false}.
        */
       public boolean isTransactionOpen(boolean sharedDb)
       {
          return getContext(sharedDb).isTransactionOpen();
       }

       /**
        * Format the given message text by prepending a descriptor including the
        * user's current context and the physical name of the database.
        *
        * @param   message
        *          Core message
        *
        * @return  Descriptor.
        */
       public String message(String message)
       {
          Context[] ctxs = contexts.get(false);
          Context ctx = (ctxs == null) ? null : ctxs[PRIVATE_IDX];
          return "[" + Utils.describeContext() + "-->" +
                 (ctx != null ? ctx.tenant.database : defaultTenant.database) +
                 "] " + message;
       }

       /**
        * Get the record lock context associated with the current client session. Although is method
        * is public to allow access from other internal packages, it is not meant for use outside the
        * <code>persist</code> package and its sub-packages.
        *
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        *
        * @return  Record lock context for the current session.
        */
       public RecordLockContext getRecordLockContext(boolean sharedDb)
       {
          return getContext(sharedDb).getRecordLockContext();
       }

       /**
        * Check if we can retrieve data lazily from the underlying database.
        *
        * @return   {@code true} if we can use database cursors to iterate through the records
        */
       public boolean supportsLazyQueryResultsMode()
       {
          return dialect.supportsLazyQueryResultsMode() && temporary;
       }

       /**
        * Use the dialect to obtain the code page (character encoding) of this connection.
        *
        * @return  the database's CP, if it can be retrieved in a dialect-specific manner.
        */
       public String getCodepage()
       {
          if (codepage == NOT_DETECTED_YET) // compare objects directly!
          {
             try
             {
                // all physical databases of same logical persistence MUST be configured with same codepage
                codepage = dialect.getCodepage(this.getSession(SHARED_CTX).getConnection());
             }
             catch (PersistenceException e)
             {
                codepage = null;
             }
          }

          return codepage;
       }

       /**
        * Use the dialect to obtain the collation of this connection.
        *
        * @return  the database's collation, if it can be retrieved in a dialect-specific manner.
        */
       public String getCollation()
       {
          if (collation == NOT_DETECTED_YET) // compare objects directly!
          {
             try
             {
                // all physical databases of same logical persistence MUST be configured with same collation
                collation = dialect.getCollation(this.getSession(SHARED_CTX).getConnection());
             }
             catch (PersistenceException e)
             {
                collation = null;
             }
          }

          return collation;
       }

       /**
        * Check if we can skip the index server-validation.
        *
        * @return   {@code true} If the dialect has support for unique index validation, meaning we can skip
        *           this process in the server.
        */
       public boolean allowDBUniqueCheck()
       {
          return allowDBUniqueCheck;
       }

       /**
        * Decide whether this persistence context is unsafe or not.
        *
        * @param   value
        *          If true the persistence will run in unsafe mode and in safe mode if false.
        * @param   sharedDb
        *          In case of multi-tenant environment: is the buffer shared or private?
        */
       public void setUnsafe(boolean value, boolean sharedDb)
       {
          getContext(sharedDb).setUnsafe(value);
       }

       /**
        * Checks whether any tenants (except for default) were set for this persistence, in any context until
        * this mment.
        *
        * @return  {@code true} if at least one regular tenant was ever authenticated for this persistence.
        */
       public boolean hasActiveTenants()
       {
          return isMultiTenancyEnabled && tenantData.size() > 1;
       }

       /**
        * Checks whether any tenants (except for default) were set for this persistence, in any context.
        *
        * @return  {@code true} if at least one regular tenant was authenticated for this persistence.
        */
       public boolean hasActiveTenantInContext()
       {
          if (!hasActiveTenants())
          {
             return false; //quick out
          }

          Context[] ctxs = contexts.get();
          return ctxs[PRIVATE_IDX] != null;
       }

       /**
        * Initialize the helper objects to which this object delegates certain service requests.
        */
       protected void initializeTenant(Tenant tenant)
       {
          try
          {
             tenant.lockManager.set(createLockManager(tenant.tenantId));

             // if using embedded H2 dialect and not a permanent database, prepare it
             if (temporary || tenant.database.isMeta() && !tenant.database.isTenant())
             {
                // tenant meta databases were prepared earlier, in MetadataManager.initMetaDb()
                H2Helper.prepareDatabase(tenant.database);
             }
             else if (!tenant.database.isTenant() || !tenant.database.isMeta())
             {
                dialect.preparePermanentDatabase(tenant.database);
             }

             tenant.identityManager.set(createIdentityManager(tenant.tenantId));
          }
          catch (StopConditionException sce)
          {
             throw sce;
          }
          catch (Exception exc)
          {
             throw new RuntimeException(
                   "Error initializing persistence services for database '" + tenant.database.getName() + "'.",
                   exc);
          }
       }

       /**
        * Create and initialize the lock manager to be used with this instance of
        * persistence services for the given database.  The lock manager
        * implementation to use is read from the directory.  The default if no
        * entry is found is the {@link InMemoryLockManager}.
        *
        * @param   tenantId
        *          The tenant id.
        *
        * @return  Lock manager instance.
        *
        * @throws  IllegalAccessException
        *          if the <code>LockManager</code> concrete implementation's default constructor
        *          cannot be accessed.
        * @throws  java.lang.InstantiationException
        *          if the <code>LockManager</code> concrete implementation cannot be instantiated.
        * @throws  ClassNotFoundException
        *          if the <code>LockManager</code> concrete implementation class cannot be located.
        * @throws  IllegalArgumentException
        *          if lock metadata is used by the current application, but no lock table updater is
        *          found for the current database.
        */
       protected LockManager<String> createLockManager(int tenantId)
       throws ReflectiveOperationException
       {
          Tenant tenant = (tenantId == TenantManager.DEFAULT_TENANT_ID) ? defaultTenant
                                                                        : getContext(PRIVATE_CTX).tenant;
          if (tenant.database.isTemporary())
          {
             return null;
          }

          // retrieve fully qualified name of LockManager implementation class and instantiate it
          String path = "database/" + tenant.database.getName() + "/p2j/lock_manager/class";
          String className = Utils.getDirectoryNodeString(null, path, DEFAULT_LOCK_MANAGER, false);
          LockManager<String> manager =
                (LockManager<String>) Class.forName(className).getDeclaredConstructor().newInstance();
          manager.setDatabase(tenant.database);

          // register lock table updater with lock manager if the former exists
          if (tenant.database.isPrimary())
          {
             // if a lock updater is found for the database, register it as a listener with the lock manager
             tenant.lockTableUpdater = DatabaseManager.getLockTableUpdater(tenant.database);
             if (tenant.lockTableUpdater != null)
             {
                manager.setLockListener(tenant.lockTableUpdater);
             }
          }

          if (LOG.isLoggable(Level.CONFIG))
          {
             LOG.config(message("lock manager initialized [" + className + "]"));
          }

          return manager;
       }

       /**
        * Create and initialize the identity manager to be used with this
        * instance of persistence services for the given database.  The identity
        * manager implementation to use is read from the directory. The default
        * if no entry is found is {@link #DEFAULT_IDENTITY_MANAGER}.
        *
        * @param   tenantId
        *          The tenant id.
        *
        * @return  Identity manager instance or <code>null</code> if given database is temporary.
        *
        * @throws  IllegalAccessException
        *          if the <code>IdentityManager</code> concrete implementation's
        *          default constructor cannot be accessed.
        * @throws  java.lang.InstantiationException
        *          if the <code>IdentityManager</code> concrete implementation
        *          cannot be instantiated.
        * @throws  ClassNotFoundException
        *          if the <code>IdentityManager</code> concrete implementation
        *          class cannot be located.
        */
       protected IdentityManager createIdentityManager(int tenantId)
       throws ReflectiveOperationException
       {
          Tenant tenant = (tenantId == TenantManager.DEFAULT_TENANT_ID) ? defaultTenant
                                                                        : getContext(PRIVATE_CTX).tenant;
          if (tenant.database.isTemporary())
          {
             return null;
          }

          String className = DEFAULT_IDENTITY_MANAGER;
          boolean usePool = false;

          if (tenant.database.isPrimary())
          {
             // retrieve fully qualified name of IdentityManager implementation class and instantiate
             String dbName = tenant.database.getName();
             String path = "database/" + dbName + "/p2j/identity_manager/class";
             className = Utils.getDirectoryNodeString(null, path, DEFAULT_IDENTITY_MANAGER, false);

             // check whether use of identity pool is forced by directory
             path = String.format("database/%s/p2j/identity_manager/pool", dbName);
             usePool = Utils.getDirectoryNodeBoolean(null, path, false, false);
          }

          IdentityPoolManager mgr = (IdentityPoolManager) Class.forName(className).getDeclaredConstructor().newInstance();

          mgr.setPersistence(this);

          IdentityPool identityPool = usePool ? new IdentityPool(tenant.database) : null;
          mgr.setIdentityPool(identityPool);

          if (LOG.isLoggable(Level.CONFIG))
          {
             LOG.config(message("identity manager initialized [" + className + "]"));
          }

          return mgr;
       }

       /**
        * Determine whether this persistence object is for a temporary table database.
        *
        * @return  {@code true} if temporary, else {@code false}.
        */
       public boolean isTemporary()
       {
          return temporary;
       }

       /**
        * Begin a new transaction and store the instance in the local context. A new database
        * session is created if one does not already exist in the current context.
        *
        * @return  {@code true} if a new transaction was begun;
        *          {@code false} if there was already an active transaction.
        *
        * @throws  PersistenceException
        *          if there is an error beginning the new transaction.
        */
       boolean beginTransaction(SavepointManager savepointManager, boolean sharedDb)
       throws PersistenceException
       {
          return beginTransaction(getContext(sharedDb), savepointManager);
       }

       /**
        * Begin a new transaction and store the instance in the local context. A new database
        * session is created if one does not already exist in the current context.
        *
        * @param   local
        *          User context.
        *
        * @return  {@code true} if a new transaction was begun;
        *          {@code false} if there was already an active transaction.
        *
        * @throws  PersistenceException
        *          if there is an error beginning the new transaction.
        */
       boolean beginTransaction(Context local, SavepointManager savepointManager)
       throws PersistenceException
       {
          if (!local.beginTransaction(savepointManager))
          {
             return false;
          }

          if (LOG.isLoggable(Level.FINER))
          {
             LOG.log(Level.FINER, message("beginning transaction"));
          }

          return true;
       }

       /**
        * Commit the current database transaction and close the current session.
        * If the transaction cannot be committed, roll it back and raise an error.
        *
        * @param   local
        *          User context.
        *
        * @throws  IllegalStateException
        *          if no transaction is active in the current session;  this
        *          represents a programming error.
        * @throws  PersistenceException
        *          if there is an error committing the transaction.
        */
       void commit(Context local)
       throws PersistenceException
       {
          local.commit();
       }

       /**
        * Roll back the current database transaction and close the current
        * session.  If the transaction cannot be rolled back, raise an error.
        *
        * @param   local
        *          User context.
        *
        * @throws  IllegalStateException
        *          if no transaction is active in the current session;  this
        *          represents a programming error.
        * @throws  PersistenceException
        *          if there is an error rolling back the transaction.
        */
       void rollback(Context local)
       throws PersistenceException
       {
          try
          {
             local.rollback(false);
          }
          catch (IllegalStateException exc)
          {
             throw new PersistenceException(exc);
          }
       }

       /***
        * Execute a batch SQL statement, while the argument supplier returns non-null values.
        *
        * @param   sql
        *          The SQL statement to execute.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        * @param   getArgs
        *          The argument supplier.
        */
       @Override
       public void executeSQLBatch(String sql, boolean sharedDb, Supplier<Object[]> getArgs)
       throws PersistenceException
       {
          Context local = getContext(sharedDb);

          long start = 0L;
          boolean trace = LOG.isLoggable(Level.FINEST);
          if (trace)
          {
             start = System.currentTimeMillis();
          }

          boolean inTx = local.beginTransaction(null);

          try
          {
             Session session = local.getSession();

             try (PreparedStatement stmt = session.getConnection().prepareStatement(sql))
             {
                int batchSize = 50;
                int i = 0;
                do
                {
                   Object[] args = getArgs.get();
                   if (args == null)
                   {
                      break;
                   }

                   for (int k = 0; k < args.length; k++)
                   {
                      stmt.setObject(k + 1, args[k]);
                   }

                   if (LOG.isLoggable(Level.FINE))
                   {
                      LOG.log(Level.FINE, "FWD ORM:\n" + stmt);
                   }

                   stmt.addBatch();
                   if (++i % batchSize == 0)
                   {
                      SQLExecutor.getInstance().execute(session.getDatabase(),
                                                        sql,
                                                        PreparedStatement::executeBatch,
                                                        stmt,
                                                        batchSize);
                   }
                }
                while (true);

                if (i % batchSize != 0)
                {
                   stmt.executeBatch();
                }
             }

             if (inTx)
             {
                local.commit();
             }
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "Error executing SQL statement: " + sql);
          }
          catch (SQLException e)
          {
             PersistenceException pe = new PersistenceException("Failed to execute SQL statement.", e);
             handleException(local, pe, "Error executing SQL statement:  " + sql);
          }
          finally
          {
             if (trace)
             {
                long elapsed = System.currentTimeMillis() - start;
                LOG.log(Level.FINEST, message("TIMING:SQLB [" + elapsed + "] " + sql));
             }
          }
       }

       /**
        * Execute an arbitrary list of standard JDBC statements in batch.
        *
        * @param   local
        *          Persistence context.
        * @param   sql
        *          List of SQL statements to execute in batch.
        * @param   noFlush
        *          If {@code true}, flushing is bypassed.
        *
        * @throws  PersistenceException
        *          if any error occurs executing the statements.
        */
       @Override
       public void executeSQLBatch(Context local, List<String> sql, boolean noFlush)
       throws PersistenceException
       {
          if (sql.isEmpty())
          {
             return;
          }

          long start = 0L;
          boolean trace = LOG.isLoggable(Level.FINEST);
          if (trace)
          {
             start = System.currentTimeMillis();
          }

          boolean inTx = local.beginTransaction(null);

          try
          {
             Session session = local.getSession();

             try (Statement stmt = session.getConnection().createStatement())
             {
                int batchSize = 50;
                int i = 0;
                for (String s : sql)
                {
                   if (LOG.isLoggable(Level.FINE))
                   {
                      LOG.log(Level.FINE, "FWD ORM:\n" + s);
                   }

                   stmt.addBatch(s);
                   if (++i % batchSize == 0)
                   {
                      SQLExecutor.getInstance().execute(session.getDatabase(),
                                                        s,
                                                        Statement::executeBatch,
                                                        stmt,
                                                        batchSize);
                   }
                }

                if (i % batchSize != 0)
                {
                   stmt.executeBatch();
                }
             }

             if (inTx)
             {
                local.commit();
             }
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "Error executing SQL statement:  " + sql);
          }
          catch (SQLException e)
          {
             PersistenceException pe = new PersistenceException("Failed to execute SQL statement.", e);
             handleException(local, pe, "Error executing SQL statement:  " + sql);
          }
          finally
          {
             if (trace)
             {
                long elapsed = System.currentTimeMillis() - start;
                LOG.log(Level.FINEST, message("TIMING:SQLB [" + elapsed + "] " + sql));
             }
          }
       }

       /**
        * Execute an arbitrary SQL statement which does not produce a result set.
        *
        * @param   sql
        *          SQL statement text.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        * @param   args
        *          The statement arguments.
        * @param   noUndo
        *          If {@code true}, the update is on a NO-UNDO temp-table.
        *
        * @return  The number of entities updated or deleted, or {@code -1} if the statement
        *          did not complete normally.
        *
        * @throws  PersistenceException
        *          if any error occurs executing the statement.
        */
       @Override
       public int executeSQL(String sql, boolean sharedDb, Object[] args, boolean noUndo)
       throws PersistenceException
       {
          long start = 0L;
          boolean trace = LOG.isLoggable(Level.FINEST);
          if (trace)
          {
             start = System.currentTimeMillis();
          }

          Context local = getContext(sharedDb);
          int rc = -1;
          boolean inTx = local.beginTransaction(null);

          try
          {
             Session session = local.getSession();
             SQLQuery query = Session.createSQLQuery(sql);
             if (args != null)
             {
                int len = args.length;
                for (int i = 0; i < len; i++)
                {
                   query.setParameter(i, args[i]);
                }
             }

             rc = query.executeUpdate(session);

             if (LOG.isLoggable(Level.FINER))
             {
                LOG.log(Level.FINER, "[" + sql + "] rc = " + rc);
             }

             if (inTx)
             {
                local.commit(); // commit  only the local transaction,
             }
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "Error executing SQL statement:  " + sql);
          }
          finally
          {
             if (trace)
             {
                long elapsed = System.currentTimeMillis() - start;
                LOG.log(Level.FINEST, message("TIMING:SQL [" + elapsed + "] " + sql));
             }
          }

          return rc;
       }

       /**
        * If this object is associated with a metadata database, set this object's lock table updater
        * to be the same as that of the <code>Persistence</code> instance associated with the given,
        * primary database.
        *
        * @param   database
        *          Primary database presumably associated with the metadata database represented by
        *          this object.
        *
        * @throws  PersistenceException
        *          if this object is not associated with a metadata database or the given database is
        *          not a primary database.
        */
       void shareLockTableUpdater(Database database)
       throws PersistenceException
       {
          Tenant tenant = (tenantData.size() == 1) ? defaultTenant
                                                   : getContext(!database.isTenant()).tenant;
          if (!tenant.database.isMeta() && database.isPrimary())
          {
             throw new PersistenceException("Invalid lock updater share request");
          }

          Persistence pPrim = PersistenceFactory.getInstance(database);
          tenant.lockTableUpdater = pPrim.getContext(!database.isTenant()).tenant.lockTableUpdater;
       }

       /**
        * Evict the given DMO from the current session immediately. If there is no existing session,
        * this method is a no-op.
        *
        * @param   local
        *          Persistence context.
        * @param   dmo
        *          Data model object to be evicted.
        */
       void evict(Persistence.Context local, Record dmo)
       {
          Session session = local.getSessionNoCreate();
          if (session != null)
          {
             session.evict(dmo);
          }
       }

       /**
        * Evict a collection of DMOs from the current ORM session. If there is no existing
        * session, this method is a no-op.
        *
        * @param   evictees
        *          Collection of DMOs to be evicted.
        */
       void evict(Collection<Record> evictees)
       {
          Context[] ctxs = contexts.get();
          Context def = ctxs[SHARED_IDX];
          Context ten = ctxs[PRIVATE_IDX];
          Session defSession = def.getSessionNoCreate();
          Session tenSession = (ten == null) ? defSession : ten.getSessionNoCreate();
          if (defSession != null || tenSession != null)
          {
             for (Record dmo : evictees)
             {
                if (dmo._getRecordMeta().getDmoMeta().multiTenant)
                {
                   if (tenSession != null)
                   {
                      tenSession.evict(dmo);
                   }
                }
                else
                {
                   if (defSession != null)
                   {
                      defSession.evict(dmo);
                   }
                }
             }
          }
       }

       /**
        * Register a <code>SessionListener</code> to receive session life
        * cycle event notifications.  The listener will be deregistered
        * according to the following rules at the earlier of:
        * <ul>
        *   <li>the point at which it is collected by the garbage collector
        *       (once business logic no longer strongly references it);  or
        *   <li>the end of the scope indicated by the <code>scope</code>
        *       parameter:
        *       <ul>
        *         <li><code>ENCLOSING_EXTERNAL_SCOPE</code>
        *         <li><code>CURRENT_SCOPE</code>
        *         <li><code>NEXT_EXTERNAL_SCOPE</code>
        *       </ul>
        * </ul>
        *
        * @param   listener
        *          Session listener to be registered.
        * @param   scope
        *          {@code SessionListener.Scope} enum indicating the scope to which this listener should be
        *          registered.  When the scope ends, the listener is deregistered as described above.
        * @param   sharedDb
        *          {@code true} if the buffers are shared (not tenant private).
        */
       void registerSessionListener(SessionListener listener, SessionListener.Scope scope, boolean sharedDb)
       {
          Context local = getContext(sharedDb);
          local.registerSessionListener(listener, scope);
       }

       /**
        * Deregister a session listener when it no longer needs to receive session life cycle events.
        *
        * @param   listener
        *          Listener to deregister.
        */
       void deregisterSessionListener(SessionListener listener)
       {
          Context[] ctxs = contexts.get();
          ctxs[SHARED_IDX].deregisterSessionListener(listener);
          if (ctxs[PRIVATE_IDX] != null)
          {
             ctxs[PRIVATE_IDX].deregisterSessionListener(listener);
          }
       }

       /**
        * Process an error condition in which a unique result should have been
        * found, but multiple results occurred.
        *
        * @param   buffer
        *          Record buffer associated with the failed query.
        * @param   exc
        *          Optional root cause exception.
        *
        * @throws  PersistenceException
        *          always;  indicates the nature of the error.
        */
       void uniqueResultViolation(RecordBuffer buffer, Exception exc)
       throws PersistenceException
       {
          int errnum = 3166;
          buffer.setAmbiguous(true);

          String msg = ErrorManager.buildErrorText(errnum,
                                                   "More than one " +
                                                   buffer.getLegacyName() +
                                                   " records found by a unique FIND",
                                                   false);

         if (LOG.isLoggable(Level.FINE))
         {
            LOG.log(Level.FINE, message(msg), exc);
         }

          // NOTE: do not add the UniqueResultException as cause for the new exception because it will leak
          //       into UI (see ErrorManager.compileErrorEntries())
          throw new PersistenceException(msg, errnum);
       }

       /**
        * Flush the current session, which synchronizes the in-memory state of
        * DMOs to the database.  Optionally, the session is cleared as well.
        * This request bypasses the current client context's setting as to
        * whether flushing is enabled.
        *
        * @param   local
        *          Context-local state.
        * @param   session
        *          ORM session
        * @param   clear
        *          <code>true</code> to clear the session (used during bulk
        *          inserts or updates to prevent a progressive memory leak) after
        *          the flush.
        *
        * @throws  PersistenceException
        *          if an error occurs during session flush.
        */
       /* private */ void flush(Context local, Session session, boolean clear)
       throws PersistenceException
       {
          try
          {
             if (clear)
             {
                session.clear();
             }
          }
          catch (PersistenceException exc)
          {
             handleException(local, exc, "error flushing and clearing session");
          }
       }

       /**
        * Perform special handling on query substitution parameters to avoid
        * errors during query execution.  This entails:
        * <ul>
        *   <li>Trimming all trailing spaces, tabs, line feeds, and carriage
        *       returns from any parameter of type <code>character</code>.  Any
        *       parameter affected is replaced with a new <code>character</code>
        *       instance at the same index position within <code>args</code>.
        *   <li>Clipping out of bounds date values to the range supported by the
        *       backing database's implementation of a date data type.  Any
        *       parameter affected is replaced with a new <code>date</code>
        *       instance at the same index position within <code>args</code>.
        * </ul>
        * <p>
        * Note:  this method has special processing for <code>null</code> bytes
        * embedded in <code>character</code> variables.  The text of the variable
        * is truncated starting at the first embedded <code>null</code> byte.
        * This means that the resulting substitution parameters are guaranteed
        * to never include <code>null</code> bytes.
        *
        * @param   args
        *          Array of query substitution parameters or <code>null</code> if
        *          none.
        */
       private void preprocessQueryParameters(Object[] args)
       {
          if (args != null)
          {
             int len = args.length;
             for (int i = 0; i < len; i++)
             {
                args[i] = preprocessQueryParameter(args[i], dialect);
             }
          }
       }

       /**
        * Get or create a query for the given FQL query string. A cached query is used if it exists.
        *
        * @param   local
        *          Context-local state.
        * @param   fql
        *          FQL query string.
        * @param   maxResults
        *          The maximum number of results to be returned by the query. If this value is
        *          non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this value is non-positive,
        *          an offset of 0 is used by default.
        * @param   relation
        *          The recursive DATA-RELATION the query is to FILL for.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        *
        * @return  FQL query object.
        *
        * @throws  PersistenceException
        *          if there was an error creating an ORM session.
        */
       private Query getQuery(Context local,
                              String fql,
                              int maxResults,
                              int startOffset,
                              DataRelation relation,
                              Object[] values)
       throws PersistenceException
       {
          return getQuery(local, fql, maxResults, startOffset, relation, values, false);
       }

       /**
        * Get or create a query for the given FQL query string. A cached query is used if it exists.
        *
        * @param   local
        *          Context-local state.
        * @param   fql
        *          FQL query string.
        * @param   maxResults
        *          The maximum number of results to be returned by the query. If this value is
        *          non-positive, no upper limit is applied.
        * @param   startOffset
        *          The 0-based offset of the first record to retrieve.  If this value is non-positive,
        *          an offset of 0 is used by default.
        * @param   relation
        *          The recursive DATA-RELATION the query is to FILL for.
        * @param   values
        *          Substitution values for the query.  If none, this should be an empty array.
        * @param   lazyMode
        *          Flag that indicates whether the query should be executed in a lazy manner.
        *
        * @return  FQL query object.
        *
        * @throws  PersistenceException
        *          if there was an error creating an ORM session.
        */
       private Query getQuery(Context local,
                              String fql,
                              int maxResults,
                              int startOffset,
                              DataRelation relation,
                              Object[] values,
                              boolean lazyMode)
       throws PersistenceException
       {
          Query query = local.getQuery(fql, maxResults, startOffset, relation, lazyMode);

          // Bind parameters, if any.  It is OK to do this with cached queries, because each use is
          // guaranteed to reset the parameters.
          if (values != null && values.length > 0)
          {
             preprocessQueryParameters(values);
             // query.setParameters(values, types);

             // This is an workaround for setting up named parameters. It was needed because
             // Hibernate cannot handle multi-column values with positional parameters.
             // Even this could work for all cases, the implementation was only tested for
             // simple cases CompositeCustomType that were needed for datetime-tz.
             // The code will be eliminated anyway when named parameters will replace
             // positional parameters globally.
             int posIndex = 0;
             for (int k = 0; k < values.length; k++)
             {
                /*
                if (types[k] instanceof org.hibernate.type.CompositeCustomType)
                {
                   query.setParameter("px" + (++posIndex), values[k], types[k]);
                }
                else
                */
                {
    //               query.setParameter(Integer.toString(k), values[k], types[k]);
                   query.setParameter(k, values[k]);
                }
             }
          }

          return query;
       }

       /**
        * Detects and return the tenant context based on the list of entities passed as argument. If
        * {@code entities} is {@code null} or does not contain any entity, or if entities from different
        * physical databases are identified, the tenant database context is returned if a tenant is active.
        * Otherwise, the permanent/no-tenant database context is returned.
        * <p>
        * In case of lack of information or a conflict (that is, both shared and private tables are detected), a
        * warning will be issued.
        *
        * @param   entities
        *          An array of {@code Class} objects which represent the tables involved in a query.
        *
        * @return  The multi-tenant context which should be used for executing the query.
        */
       private Context getContext(Class<? extends Record>[] entities)
       {
          if (!hasActiveTenants())
          {
             // quick out: not a multitenant database (_temp, meta) or no tenant set yet
             return getContext(SHARED_CTX);
          }

          int ctxType = -1;
          if (entities != null)
          {
             for (Class<? extends Record> entity : entities)
             {
                DmoMeta dmoInfo = DmoMetadataManager.getDmoInfo(entity);
                if (dmoInfo.multiTenant)
                {
                   if (ctxType == SHARED_IDX)
                   {
                      ctxType = -2;
                      break;
                   }
                   ctxType = PRIVATE_IDX;
                }
                else // !dmoInfo.multiTenant
                {
                   if (ctxType == PRIVATE_IDX)
                   {
                      ctxType = -2;
                      break;
                   }
                   ctxType = SHARED_IDX;
                }
             }
          }

          if (ctxType == -1)
          {
             LOG.log(Level.INFO, "MTCtx: No entities provided!");
          }
          else if (ctxType == -2)
          {
             LOG.log(Level.WARNING, "MTCtx: Failed TENANT context detection: collision detected!");
          }

          return getContext(ctxType == SHARED_IDX);
       }

       /**
        * Raise a STOP condition, if the given exception represents a catastrophic, JDBC failure;
        * else throw a <code>PersistenceException</code>.
        *
        * @param   message
        *          Error text for the exception.
        * @param   exc
        *          A persistence exception which may or may not represent an unrecoverable database error.
        *
        * @throws  StopConditionException
        *          if the error is unrecoverable.
        * @throws  PersistenceException
        *          if the error is likely recoverable.
        */
       private void handleException(String message, PersistenceException exc)
       throws PersistenceException
       {
          if (exc instanceof UdfException)  // TODO: temporary, need more robust inspection
          {
             if (LOG.isLoggable(Level.SEVERE))
             {
                LOG.log(Level.SEVERE, message);
             }

             throw new StopConditionException(message(message), exc);
          }
          else if (exc.getCause() instanceof SQLException)  // TODO: temporary, need more robust inspection
          {
             if (LOG.isLoggable(Level.SEVERE))
             {
                LOG.log(Level.SEVERE, message, exc);
             }

             if (!dialect.isRecoverableError(exc))
             {
                throw new StopConditionException(message(message), exc);
             }
          }

          throw new PersistenceException(message(message), exc);
       }

       /**
        * Provides a template for handling <link>PersistenceException</link> exceptions. Depending on context
        * a more suitable exception will be thrown. A rollback for the local context will also be initiated and
        * a log message will be written.
        *
        * @param   local
        *          The client context.
        * @param   exc
        *          A persistence exception which may or may not represent an unrecoverable database error.
        * @param   message
        *          The message which will be written as a log. If null no message will be displayed.
        *
        * @throws  StopConditionException
        *          if the error is unrecoverable.
        * @throws  PersistenceException
        *          if the error is likely recoverable.
        */
       private void handleException(Context local, PersistenceException exc, String message)
       throws PersistenceException
       {
          if (!local.isUnsafe())
          {
             DBUtils.handleException(local.tenant.database, exc);

             local.closeSession(true);

             String msg = "";
             if (message != null)
             {
                msg = message(message);

                if (LOG.isLoggable(Level.WARNING))
                {
                   LOG.log(Level.WARNING, msg);
                   if (LOG.isLoggable(Level.FINE))
                   {
                      LOG.log(Level.FINE, "Details:", exc);
                   }
                }
             }

             handleException(msg, exc);
          }
          else
          {
             throw exc;
          }
       }

       /**
        * Returns a short description of this object.
        *
        * @return  a short description of this object.
        */
       @Override
       public String toString()
       {
          return "Persistence{" + dialect.getJdbcDatabaseType() + ": " + getDatabase(Persistence.PRIVATE_CTX) + "}";
       }

       /**
        * When a tenant id is changed, we need to re-open the session, but taking into account
        * the new tenant's name.
        *
        * @param   tenantName
        *          The new tenant's name.
        * @param   ldbName
        *          The logical name of the database that the tenant will use in this context.
        */
       @Override
       public void tenantChanged(String tenantName, String ldbName)
       {
          Database defaultDb = defaultTenant.database;
          if (!isMultiTenancyEnabled || !ldbName.equalsIgnoreCase(defaultDb.getName()))
          {
             return; // quick out, this should not happen, anyway.
          }

          if (tenantName == null || TenantManager.DEFAULT_TENANT_NAME.equalsIgnoreCase(tenantName))
          {
             Context[] ctxs = contexts.get();
             Context tCtx = ctxs[PRIVATE_IDX];

             tCtx.cleanup();
             ctxs[PRIVATE_IDX] = null;
             return;
          }

          Database mtDb = TenantManager.getDatabase(tenantName, ldbName, false, defaultDb.getType());
          if (mtDb == null)
          {
             LOG.log(Level.WARNING,
                     "Failed to obtain the physical database for tenant '" + tenantName + "'.\n" +
                     "Tenant will not be changed!");
             return;
          }

          Tenant tenant = tenantData.computeIfAbsent(tenantName.toLowerCase(), s -> {
             int tenantId = TenantManager.getTenant(tenantName, null).id();
             Tenant t = new Tenant(mtDb, tenantId, tenantName);
             initializeTenant(t); // re-initialize database to fit the new tenant
             return t;
          });

          Context[] ctxs = contexts.get();
          Context tCtx;
          boolean isNew = false;
          if (ctxs[PRIVATE_IDX] == null)
          {
             // create the pure-tenant context, if it does not exist
             tCtx = ctxs[PRIVATE_IDX] = new Context(tenant);
             isNew = true;
          }
          else
          {
             tCtx = ctxs[PRIVATE_IDX];
          }

          // do a bit of cleanup, preparing for the tenant change
          try
          {
             // invalidate all buffers to avoid leaking from one tenant database to another using the buffers
             // process only the multi-tenant buffers, the shared buffers should not be affected
             Iterator<RecordBuffer> it = tCtx.bufferManager.getBuffersOf(Persistence.this, true);
             while (it.hasNext())
             {
                RecordBuffer buffer = it.next();
                buffer.release(true);
                buffer.updateTenant(tCtx);
             }

             if (!isNew)
             {
                tCtx.closeSessionImpl(false, false);
                tCtx.factory.expire();
                tCtx.initialize(tenant);
             }
          }
          catch (PersistenceException e)
          {
             LOG.log(Level.WARNING, "Failed to re-open the new context for existing buffers of " + tenant);
          }
       }

       /**
        * A hash key for a unique combination of FQL, with the existence of lack of maximum results,
        * and starting row offset options. The actual values for paging are not cached, we need to
        * know only the query form and the actual values are filled when object is used.
        */
       private static class QueryKey
       {
          /** FQL statement */
          private final String fql;

          /** Does the query was generated with maximum results option? */
          private final boolean hasMaxResults;

          /** Does the query was generated with starting row offset option? */
          private final boolean hasStartOffset;

          /** The where string of the recursive DATA-RELATION the query is to FILL for */
          private final String whereStr;

          /** Precalculated hash code */
          private final int hash;

          /** Check if the query should be executed in lazy mode (sensitive to change in real-time) */
          private final boolean lazyMode;

          /**
           * Constructor.
           *
           * @param   hql
           *          FQL statement
           * @param   maxResults
           *          Maximum results.
           * @param   startOffset
           *          Starting row offset.
           */
          QueryKey(String hql, int maxResults, int startOffset, DataRelation relation, boolean lazyMode)
          {
             this.fql = hql;
             this.hasMaxResults = maxResults > 0;
             this.hasStartOffset = startOffset > 0;
             this.whereStr = relation == null ? "" : relation.getWhereString().toJavaType();
             this.lazyMode = lazyMode;

             int result = 17;
             result = 37 * result + hql.hashCode();
             result = 37 * result + (hasMaxResults ? 1 : 0) + (hasStartOffset ? 2 : 0);
             result = 37 * result + whereStr.hashCode();
             result = 37 * result + (lazyMode ? 1 : 0);
             this.hash = result;
          }

          /**
           * Test for equivalence with another object.
           *
           * @param   o
           *          Another <code>ColumnKey</code> instance.
           *
           * @return  <code>true</code> if <code>o</code> has same internal
           *          state as this object;  else <code>false</code>.
           */
          @Override
          public boolean equals(Object o)
          {
             if (!(o instanceof QueryKey))
             {
                return false;
             }
             QueryKey that = (QueryKey) o;

             return this.hash == that.hash                     &&
                    this.hasMaxResults == that.hasMaxResults   &&
                    this.hasStartOffset == that.hasStartOffset &&
                    this.fql.equals(that.fql) &&
                    this.whereStr.equals(that.whereStr) &&
                    this.lazyMode == that.lazyMode;
          }

          /**
           * A hash code implementation consistent with our overridden {@link #equals(Object)} method.
           *
           * @return  Hash code.
           */
          @Override
          public int hashCode()
          {
             return hash;
          }

          /**
           * Get string representation of this object.
           *
           * @return  String representation.
           */
          @Override
          public String toString()
          {
             return fql + "[" + hasMaxResults + ":" + hasStartOffset + "];" + whereStr;
          }
       }

       /**
        * A storage and work area for context-local variables used by the
        * enclosing class.  One instance of this class is created for each
        * client context.  It is used to:
        * <ul>
        *   <li>create and cache query objects;
        *   <li>create, store and close the active database session;
        *   <li>manage the active database transaction.
        * </ul>
        * <p>
        * Special care is taken to not close a database session while other
        * entities are dependent upon it for services.
        */
       public class Context
       {
          /** Cache of FQL strings, with or without max results and start offsets, to queries. */
          private ExpiryCache<QueryKey, Query> staticQueryCache;

          /** Context-local buffer manager */
          private final BufferManager bufferManager = BufferManager.get();

          /** The current tenant information. */
          private Tenant tenant;

          /** Cache for FIND results. */
          private FastFindCache ffCache;

          /** Object which manages newly created records */
          private RecordNursery nursery;

          /** Session listener objects */
          private final WeakHashMap<SessionListener, Finalizable> sessionListeners = new WeakHashMap<>();

          /** Active database session, if any */
          private Session session = null;

          /** Reference count of resources dependent upon current session */
          private int sessionUseCount = 0;

          /** Flag to prevent nested session close attempts */
          private boolean closingSession = false;

          /** Count of open database sessions in this context */
          private volatile int openSessionCount = 0;

          /** Optional, dialect-specific, context-local data */
          private final Object dialectData = dialect.createContext(defaultTenant.database);

          /** rereadnolock command-line parameter, by default deactivated */
          private boolean rereadNoLock = false;

          /** Enable tracing transaction starts */
          private boolean traceTx = false;

          /** Throwable which holds stack trace where last transaction began */
          private Throwable beginTxTrace = null;

          /** A factory that allows a session to be reclaimed */
          private final SessionFactory factory = new SessionFactory();

          /** Flag used to indicate if default exception handling is used. */
          private boolean unsafe = false;

          /**
           * The default constructor reads rereadNoLock from directory.
           *
           * @param   t
           *          The tenant for this context.
           */
          private Context(Tenant t)
          {
             initialize(t);

             // the '-rereadnolock' startup parameter was introduced in Progress 8.3B'
             // only needed for permanent tables, not temp-tables
             rereadNoLock = !temporary && SessionUtils._startupParameters().getRereadNoLock();

             // if the database is multi-tenant, register the persistence only once, when the default context is
             // created
             if (isMultiTenancyEnabled && t.tenantId == TenantManager.DEFAULT_TENANT_ID)
             {
                ConnectionManager cm = ConnectionManager.get();
                String ldbName = cm.getLDBName(tenant.database);
                if (ldbName == null)
                {
                   ldbName = tenant.database.getName().toLowerCase();
                }

                cm.registerTenantListener(Persistence.this, ldbName);
             }
          }

          /**
           * Initialization of the context. The query cache, fast-find cache and the record nursery are
           * re-instantiated with new objects. This method is called from each constructor, but also when the
           * tenant is changed.
           *
           * @param   t
           *          The tenant for this context.
           */
          void initialize(Tenant t)
          {
             this.tenant = t;
             this.staticQueryCache = CacheManager.createLRUCache(Persistence.class, 1024);
             this.ffCache = FastFindCache.getInstance(isTemporary(), tenant.database);
             this.nursery = new RecordNursery(ffCache);
          }

          /**
           * Get the record lock context associated with the current client
           * session.
           *
           * @return  Record lock context for the current session.
           */
          RecordLockContext getRecordLockContext()
          {
             RecordLockContext rlc = tenant.lockContext.get();
             if (rlc == null)
             {
                tenant.lockContext.set(rlc = RecordLockContext.get(
                      Persistence.this, bufferManager, tenant.tenantId == TenantManager.DEFAULT_TENANT_ID));
             }

             return rlc;
          }

          /**
           * Get the {@code Persistence} object associated with this context.
           *
           * @return  Persistence object.
           */
          Persistence getPersistence()
          {
             return Persistence.this;
          }

          /**
           * Increment the session use count, indicating the addition of a dependency on the open
           * session. As long as there are dependencies on the session, it may not close.
           */
          void useSession()
          {
             sessionUseCount++;
    //         LOG.finer("--> (" + sessionUseCount + ") " + database);
          }

          /**
           * Decrement the session use count, indicating the removal of a dependency on the open
           * session. As long as there are dependencies on the session, it may not close.
           */
          void releaseSession()
          {
             sessionUseCount--;
    //         LOG.finer("<-- (" + sessionUseCount + ") " + database);
          }

          /**
           * Indicate whether there is an open session and if so, whether it can be closed. An open
           * session may only be closed if there are no dependencies on it.
           *
           * @return  {@code true} if there is an open session which may be closed; otherwise {@code false}.
           */
          boolean canCloseSession()
          {
             return session != null && sessionUseCount < 1 && session.isOpen();
          }

          /**
           * Get a cached query for the given combination of FQL query string, maximum result rows,
           * and starting row offset, creating and caching a new query if no such query already is
           * cached. The cache is fixed size. When it is full and a new query needs to be added, the
           * least recently used query is removed.
           *
           * @param   fql
           *          FQL query string.
           * @param   maxResults
           *          The maximum number of results to be returned.
           * @param   startOffset
           *          Zero-based offset of first result to be returned (undefined results unless
           *          {@code fql} includes an {@code order by} phrase).
           * @param   relation
           *          The recursive DATA-RELATION the query is to FILL for.
           *
           * @return  A {@code Query} object for the requested {@code fql}.
           *
           * @throws  PersistenceException
           *          if there was an error creating an ORM session.
           */
          Query getQuery(String fql, int maxResults, int startOffset, DataRelation relation)
          throws PersistenceException
          {
             return getQuery(fql, maxResults, startOffset, relation, false);
          }

          /**
           * Get a cached query for the given combination of FQL query string, maximum result rows,
           * and starting row offset, creating and caching a new query if no such query already is
           * cached. The cache is fixed size. When it is full and a new query needs to be added, the
           * least recently used query is removed.
           *
           * @param   fql
           *          FQL query string.
           * @param   maxResults
           *          The maximum number of results to be returned.
           * @param   startOffset
           *          Zero-based offset of first result to be returned (undefined results unless
           *          {@code fql} includes an {@code order by} phrase).
           * @param   relation
           *          The recursive DATA-RELATION the query is to FILL for.
           * @param   lazyMode
           *          Flag that indicates whether the query should be executed in a lazy manner.
           *
           * @return  A {@code Query} object for the requested {@code fql}.
           *
           * @throws  PersistenceException
           *          if there was an error creating an ORM session.
           */
          Query getQuery(String fql, int maxResults, int startOffset, DataRelation relation, boolean lazyMode)
          throws PersistenceException
          {
             Query query = null;
             fql = fql.trim(); // just in case

             // TODO: use the dialect also? I.e: is it possible to have different SQL for same FQL
             //       if different dialects are used?
             QueryKey key = new QueryKey(fql, maxResults, startOffset, relation, lazyMode);

             synchronized (staticQueryCache)
             {
                query = staticQueryCache.get(key);
                if (query == null)
                {
                   query = Session.createQuery(q ->
                   {
                      synchronized (staticQueryCache)
                      {
                         // NOTE: the FQL string is not converted to SQL at this time. The conversion
                         //       is performed once, lazily, when the first navigation is required by a
                         //       call of list(), scroll(), or uniqueResult() on the new query

                         staticQueryCache.put(key, q);
                      }
                   },
                   fql);
                }
             }

             // update [maxResults] and [startOffset] because the cached query might have set
             // an incorrect value from a previous usage
             query.setMaxResults((maxResults > 0) ? maxResults : -1);
             query.setFirstResult(Math.max(startOffset, 0));
             query.setDataRelation(relation);
             query.setLazyMode(lazyMode);
             return query;
          }

          /**
           * Commit the current database transaction. If the transaction cannot be committed, roll it
           * back and raise an error.
           *
           * @throws  IllegalStateException
           *          if no transaction is active in the current session;  this
           *          represents a programming error.
           * @throws  PersistenceException
           *          if there is an error committing the transaction.
           */
          void commit()
          throws PersistenceException
          {
             if (!isTransactionOpen())
             {
                throw new IllegalStateException(message("no current transaction available to commit"));
             }

             boolean explicitRollback = false;

             try
             {
                if (!temporary && !sessionListeners.isEmpty())
                {
                   notifySessionEvent(SessionListener.Event.TRANSACTION_COMMITTING);
                }

                session.commit();

                if (LOG.isLoggable(Level.FINER))
                {
                   LOG.log(Level.FINER, message(" transaction committed"));
                }

                postTransaction(false);
             }
             catch (PersistenceException exc)
             {
                if (isUnsafe())
                {
                   DBUtils.handleException(session.getDatabase(), exc);

                   if (session.inTransaction())
                   {
                      try
                      {
                         explicitRollback = true;
                         rollback(true);
                      }
                      catch (PersistenceException exc1)
                      {
                         // don't need to throw an exception here; this would hide the commit exception,
                         // so just log it
                         if (LOG.isLoggable(Level.SEVERE))
                         {
                            LOG.log(Level.SEVERE, message("rollback error"), exc1);
                         }
                      }
                   }

                   handleException("unable to commit transaction", exc);
                }
                else
                {
                   throw exc;
                }
             }
             finally
             {
                if (!explicitRollback)
                {
                   // if there was an explicit rollback, we will have decremented the session in that logic;
                   // otherwise, do it here
                   releaseSession();
                }
             }
          }

          /**
           * Roll back the current transaction and possibly close the current session.  If the
           * transaction cannot be rolled back, raise an error.
           *
           * @param   error
           *          Indicates whether the rollback is due to an error.
           *
           * @throws  IllegalStateException
           *          if no transaction is active in the current session;  this
           *          represents a programming error.
           * @throws  PersistenceException
           *          if there is an error rolling back the transaction.
           */
          void rollback(boolean error)
          throws PersistenceException
          {
             try
             {
                if (!isTransactionOpen())
                {
                   throw new IllegalStateException(message("no current transaction available to rollback"));
                }

                session.rollback();

                if (LOG.isLoggable(Level.FINER))
                {
                   LOG.log(Level.FINER, message("transaction rolled back"));
                }
             }
             catch (PersistenceException exc)
             {
                error = true;
                handleException(this, exc, "unable to rollback current transaction");
             }
             finally
             {
                releaseSession();
                postTransaction(true);
             }
          }

          /**
           * Set enable mode for transaction tracing.
           *
           * @param   enable
           *          <code>true</code> to enable; <code>false</code> to disable.
           */
          void enableTransactionTrace(boolean enable)
          {
             this.traceTx = enable;
          }

          /**
           * Begin a new database transaction and remember that it is open, for related calls. If a
           * transaction already exists, return {@code false}, and do not begin a new one. A new
           * database session is created if one does not already exist in the current context.
           *
           * @param   savepointManager
           *          A savepoint manager to install in the session while the transaction is active.
           *          May be {@code null} for ad-hoc transactions, but must not be {@code null} when
           *          the databbase transaction is being opened in association with a legacy
           *          application transaction.
           *
           * @return  {@code true} if a new database transaction was opened successfully;
           *          {@code false} if a transaction already exists.
           *
           * @throws  PersistenceException
           *          if there is an error beginning the new transaction.
           */
          boolean beginTransaction(SavepointManager savepointManager)
          throws PersistenceException
          {
             if (getSession().inTransaction())
             {
                return false;
             }

             boolean success = false;

             try
             {
                Session session = getSession();
                success = session.beginTransaction(savepointManager);

                if (session.inTransaction() && traceTx)
                {
                   beginTxTrace = new Throwable();
                }

                useSession();
             }
             catch (PersistenceException exc)
             {
                success = false;
                handleException(this, exc, "error beginning database transaction");
             }

             return success;
          }

          /**
           * Perform post-processing and cleanup after a transaction commit or rollback.
           *
           * @param   rollback
           *          Indicates whether the transaction is being ended after a rollback (as opposed
           *          to a commit).
           *
           * @throws  PersistenceException
           *          if an error occurs performing post-transaction processing, if any.
           */
          void postTransaction(boolean rollback)
          throws PersistenceException
          {
             try
             {
                // give the dialect a chance to perform post-transaction processing
                dialect.postTransaction(Persistence.this, dialectData, rollback);
             }
             finally
             {
                beginTxTrace = null;
             }
          }

          /**
           * Retrieve trace information about where the current transaction, if any, was started.
           * FINER level logging must be enabled, or this method always will return <code>null</code>.
           *
           * @return  <code>Throwable</code> containing stack trace from last transaction start, or
           *          <code>null</code>.
           */
          Throwable getBeginTxTrace()
          {
             return beginTxTrace;
          }

          /**
           * Register a <code>SessionListener</code> to receive session life
           * cycle event notifications.  The listener will be deregistered
           * according to the following rules at the earlier of:
           * <ul>
           *   <li>the point at which it is collected by the garbage collector
           *       (once business logic no longer strongly references it);  or
           *   <li>the end of the scope indicated by the <code>scope</code>
           *       parameter:
           *       <ul>
           *         <li><code>ENCLOSING_EXTERNAL_SCOPE</code>
           *         <li><code>CURRENT_SCOPE</code>
           *         <li><code>NEXT_EXTERNAL_SCOPE</code>
           *       </ul>
           * </ul>
           *
           * @param   listener
           *          Session listener to be registered.
           * @param   scope
           *          <code>SessionListener.Scope</code> enum indicating the scope
           *          to which this listener should be registered.  When the
           *          scope ends, the listener is deregistered as described above.
           */
          void registerSessionListener(final SessionListener listener, SessionListener.Scope scope)
          {
             if (SessionListener.Scope.NONE.equals(scope))
             {
                // Store listener as a key in the map, so it may be released when no
                // longer referenced elsewhere.
                sessionListeners.put(listener, null);

                // Never deregister the listener by scope.
                return;
             }

             // Finalizable which will remove listener at a safe time.  The
             // listener may be removed earlier if not referenced in business
             // logic by the time this Finalizable executes its finished method,
             // and the GC collects it.  In the latter case, the listener will not
             // be notified, but it doesn't matter, since it is no longer
             // referenced by business logic at that point.
             final Finalizable cleaner = new Finalizable()
             {
                public void finished()
                {
                   sessionListeners.remove(listener);
                   listener.deregisteredSessionListener();
                }
                public void deleted() {}
                public void iterate() {}
                public void retry() {}
             };

             // store the finalizable so we can deregister it explicitly if needed
             sessionListeners.put(listener, cleaner);

             switch (scope)
             {
                case GLOBAL:
                   TransactionManager.registerFinalizable(cleaner, true);
                   break;
                case ENCLOSING_EXTERNAL:
                   TransactionManager.registerTopLevelFinalizable(cleaner, true);
                   break;
                case CURRENT:
                   TransactionManager.registerFinalizable(cleaner, false);
                   break;
                case NEXT_EXTERNAL:
                   TransactionManager.registerNextExternal(cleaner);
                   break;
                default:
                   break;
             }
          }

          /**
           * Explicitly deregister a global session listener, which represents a dynamic temp-table
           * resource which has been deleted explicitly.
           *
           * @param   listener
           *          Listener to deregister.
           */
          void deregisterSessionListener(SessionListener listener)
          {
             sessionListeners.remove(listener);
          }

          /**
           * Getter for the {@code ffCache} field.
           *
           * @return  An instance of the fast find cache, assigned to this context.
           */
          FastFindCache getFastFindCache()
          {
             return ffCache;
          }

          /**
           * Get the record nursery.
           *
           * @return  Record nursery.
           */
          RecordNursery getNursery()
          {
             return nursery;
          }

          /**
           * Retrieve the ORM {@code Session} object associated with this context, if any.
           * <p>
           * If the current session is broken, return <code>null</code>.
           *
           * @return  The session object associated with the current context, or
           *          <code>null</code> if none has been created.
           */
          Session getSessionNoCreate()
          {
             return session;
          }

          /**
           * Retrieve the ORM <code>Session</code> object associated with this context, creating it if necessary.
           * The session can be closed by {@link #closeSession}.
           *
           * @return  The session object associated with the current context.
           *
           * @throws  PersistenceException
           *          if an error occurs associating a detached record with the new session.
           * @throws  IllegalArgumentException
           *          if <code>database</code> has not been registered with the persistence service.
           */
          Session getSession()
          throws PersistenceException
          {
             if (session == null)
             {
                if (openSessionCount > 0 && LOG.isLoggable(Level.WARNING))
                {
                   LOG.warning("Opening new database session with " + openSessionCount + " already open!");
                }

                SessionFactory.PersistenceConsumer<Session> initialize = (currentSession) ->
                {
                   session = currentSession;
                   // associate detached records in the current context with the new session
                   Iterator<RecordBuffer> iter = bufferManager.activeBuffers(Persistence.this);
                   RecordBuffer buffer = null;
                   Record record = null;
                   boolean finer = LOG.isLoggable(Level.FINER);
                   while (iter.hasNext())
                   {
                      buffer = iter.next();
                      record = buffer.getCurrentRecord();

                      // do not associate a template record with the new session
                      if (record.primaryKey() < 0)
                      {
                         continue;
                      }

                      if (finer)
                      {
                         LOG.log(Level.FINER, message("associating record with session:  " +
                                                      record.getClass().getName() + "#" +
                                                      record.primaryKey()));
                      }

                      try
                      {
                         session.associate(record);
                      }
                      catch (StaleRecordException exc)
                      {
                         if (finer)
                         {
                            LOG.log(Level.FINE, message("stale record will not be associated with session:  " +
                                                        record.getClass().getName() + "#" +
                                                        record.primaryKey()));
                         }
                      }
                      catch (PersistenceException exc)
                      {
                         DBUtils.handleException(tenant.database, exc);

                         closeSession(true);

                         String msg = "Error re-associating detached DMO with new session"
                                    + (" [" + record.getClass().getName() + "#" + record.primaryKey() + "]");

                         LOG.log(Level.SEVERE, message(msg), exc);

                         handleException(msg, exc);
                      }
                   }
                };

                session = factory.create(tenant.database, tenant.dmoVersion, initialize, tenant.tenantName);
                openSessionCount++;

                if (LOG.isLoggable(Level.FINE))
                {
                   LOG.log(Level.FINE, message("opened ORM session; " + openSessionCount + " open"));
                   /*
                   if (LOG.isLoggable(Level.FINEST))
                   {
                      LOG.log(Level.FINEST, "Stack trace:  ", new Throwable());
                   }
                   */
                }

                bufferManager.registerPersistenceContext(this);
             }

             return session;
          }

          /**
           * The name of the tenant that was assigned to this context, if any.
           *
           * @return  The name of the tenant.
           */
          String getTenantName()
          {
             return tenant.tenantName;
          }

          /**
           * The id of the tenant that was assigned to this context.
           *
           * @return  The id of the tenant.
           */
          public int getTenantId()
          {
             return tenant.tenantId;
          }

          /**
           * Indicate whether a database-level transaction currently is open.
           *
           * @return  <code>true</code> if open, else <code>false</code>.
           */
          boolean isTransactionOpen()
          {
             return session != null && session.isOpen() && session.inTransaction();
          }

          /**
           * Get the context-local, dialect-specific data object, if any.
           *
           * @return  Dialect specific data object, or <code>null</code> if no
           *          such data exists.
           */
          Object getDialectData()
          {
             return dialectData;
          }

          /**
           * Indicate whether auto-commit is needed; this is the case for temp-tables being modified
           * outside of an explicit transaction.
           *
           * @return  {@code true} if auto-commit is needed, else {@code false}.
           */
          boolean isAutoCommit()
          {
             return temporary && !isTransactionOpen();
          }

          /**
           * Attempt to close the database session associated with this context, and indicate whether
           * the attempt was successful. A close attempt will be unsuccessful if this method is
           * called while already in the process of closing the session, or if an attempt is made to
           * close a legacy temp-table database session while temporary tables are still in use.
           * <p>
           * This implementation is tolerant of multiple closes on the same session. If the session
           * already was closed or could not be found, the method simply returns {@code false}.
           *
           * @param   error
           *          {@code true} to indicate the session is closing due to an error;  {@code false}
           *          to indicate the session is closing in the course of normal processing.
           *
           * @return  {@code true} is the session was closed successfully; {@code false} if it was
           *          not closed because either: (a) this method was invoked in the course of an
           *          existing attempt to close the session; or (b) the session is with the legacy
           *          temp-table database while temporary tables are still in use.
           *
           * @throws  PersistenceException
           *          if an open session was found but the attempt to close it failed with a database
           *          error.
           */
          boolean closeSession(boolean error)
          throws PersistenceException
          {
             return closeSessionImpl(error, error);
          }

          /**
           * Attempt to close the database session associated with this context, and indicate whether
           * the attempt was successful. A close attempt will be unsuccessful if this method is
           * called while already in the process of closing the session, or if an attempt is made to
           * close a legacy temp-table database session while temporary tables are still in use.
           * <p>
           * This implementation is tolerant of multiple closes on the same session. If the session
           * already was closed or could not be found, the method simply returns {@code false}.
           *
           * @param   error
           *          {@code true} to indicate the session is closing due to an error; {@code false}
           *          to indicate the session is closing in the course of normal processing.
           * @param   force
           *          {@code true} to force the session to be closed and all database resources
           *          associated with it to be cleaned up.
           *
           * @return  {@code true} is the session was closed successfully; {@code false} if it was
           *          not closed because either: (a) this method was invoked in the course of an
           *          existing attempt to close the session; or (b) the session is with the legacy
           *          temp-table database while temporary tables are still in use.
           *
           * @throws  PersistenceException
           *          if an open session was found but the attempt to close it failed with a database
           *          error.
           */
          private boolean closeSessionImpl(boolean error, boolean force)
          throws PersistenceException
          {
             // prevent nested or invalid close attempts
             if (closingSession || session == null || !session.isOpen())
             {
                return false;
             }

             try
             {
                closingSession = true;

                if (isTransactionOpen())
                {
                   if (LOG.isLoggable(Level.WARNING))
                   {
                      LOG.log(Level.WARNING, message("rolling back orphaned transaction"));
                   }
                   rollback(error);
                }

                if (!error && !force && temporary && TemporaryBuffer.getTableCount() > 0)
                {
                   // allow unused tables to be cleaned up
                   if (!sessionListeners.isEmpty())
                   {
                      notifySessionEvent(SessionListener.Event.SESSION_CLEANUP);
                   }

                   // Special treatment for temp table sessions. These must
                   // remain open as long as any table exists, otherwise the
                   // table and its data will be lost.
                   return false;
                }

                try
                {
                   try
                   {
                      if (!sessionListeners.isEmpty())
                      {
                         SessionListener.Event event = error
                                                       ? SessionListener.Event.SESSION_CLOSING_WITH_ERROR
                                                       : SessionListener.Event.SESSION_CLOSING_NORMALLY;
                         notifySessionEvent(event);
                      }
                   }
                   finally
                   {
                      if (factory.isEnabled())
                      {
                         factory.markReclaimable(session);
                      }
                      else
                      {
                         session.close();
                      }
                   }
                }
                catch (PersistenceException exc)
                {
                   DBUtils.handleException(tenant.database, exc);
                   handleException("error closing database session", exc);
                }

                return true;
             }
             finally
             {
                session = null;
                openSessionCount--;

    // DEBUG            LOG.finer(database.toString() + "...closed database session; sessionUseCount = " + sessionUseCount);
                sessionUseCount = 0;

                if (LOG.isLoggable(Level.FINE))
                {
                   LOG.log(Level.FINE, message("closed database session; " + openSessionCount + " open"));
                   /*
                   if (LOG.isLoggable(Level.FINEST))
                   {
                      LOG.log(Level.FINEST, "Stack trace:  ", new Throwable());
                   }
                   */
                }

                closingSession = false;
             }
          }

          /**
           * Notify all registered session listeners that the current ORM session is about to
           * close or the current, implicit transaction is being committed.  Listeners which were set
           * to be deregistered upon an event are deregistered after they are notified.  There is no
           * guaranteed order in which listeners are notified.
           * <p>
           * All registered listeners are given a chance to process this event, even if one or more
           * raise a <code>PersistenceException</code> (there is no such guarantee for unchecked
           * exceptions). Each exception caught is logged.
           *
           * @param   event
           *          Session event type.
           */
          private void notifySessionEvent(SessionListener.Event event)
          {
             Iterator<SessionListener> iter = sessionListeners.keySet().iterator();
             while (iter.hasNext())
             {
                SessionListener listener = iter.next();
                boolean deregister = false;

                try
                {
                   // allow listener to process session event
                   deregister = listener.sessionEvent(event);
                }
                catch (PersistenceException exc)
                {
                   // deregister listener if there was an error
                   deregister = true;

                   LOG.log(Level.SEVERE, "Error processing session event", exc);
                }
                finally
                {
                   // remove listeners if there was an error, or if the listener indicated it should
                   // be removed in response to the session closing notification
                   if (deregister)
                   {
                      iter.remove();
                      listener.deregisteredSessionListener();
                   }
                }
             }
          }

          /**
           * Cleanup hook invoked at the end of the current client context. Close the currently open
           * database session, if any.
           */
          void cleanup()
          {
             if (isTransactionOpen())
             {
                // Under normal circumstances, there should not be any tx open
                // at the context's end of life, but just in case...

                if (LOG.isLoggable(Level.WARNING))
                {
                   LOG.log(Level.WARNING,
                           message("rolling back open transaction during context cleanup"));
                }

                try
                {
                   rollback(false);
                }
                catch (PersistenceException exc)
                {
                   LOG.log(Level.SEVERE, "Error rolling back open transaction during context cleanup", exc);
                }
             }

             try
             {
                closeSessionImpl(false, true);
             }
             catch (PersistenceException exc)
             {
                LOG.log(Level.SEVERE, "Error closing database session during context cleanup", exc);
             }
             finally
             {
                // Deregister the SessionFactory of the Context from the SessionCloseThread
                // If the factory has an open reclaimable session, it closes it.
                SessionFactory.SessionCloseThread.deregister(factory);

                Database database = tenant.database;
                if (database.isTemporary())
                {
                   PersistenceFactory.remove(database);
                }
             }
          }

          /**
           * Checks if the rereadnolock 4GL command-line option is active for this client context.
           *
           * @return  {@code true} if rereadnolock is specified for this client context
           */
          boolean isRereadNoLock()
          {
             return rereadNoLock;
          }

          /**
           * Decide whether this persistence context is unsafe or not.
           *
           * @param  value
           *         If true the persistence will run in unsafe mode and in safe mode if false.
           */
          void setUnsafe(boolean value)
          {
             unsafe = value;
          }

          /**
           * Checks whether this persistence context is unsafe or not.
           *
           * @return  If true the persistence will run in unsafe mode and in safe mode if false.
           */
          boolean isUnsafe()
          {
             return unsafe;
          }

          /**
           * Return a short description of this object. To be as concise as possible, the returned string contains
           * only the physical database name.
           *
           * @return  a short description of this object.
           */
          @Override
          public String toString()
          {
             return "Persistence$Context{" + tenant.database + "}";
          }
       }
    }