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 + "}";
      }
   }
}