RandomAccessQuery.java
/*
** Module : RandomAccessQuery.java
** Abstract : Backing query for converted queries which require dynamic record retrieval
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20051007 @23259 Created initial version. Backing query for
** converted queries which require a dynamic
** record retrieval semantic.
** 002 ECF 20051111 @23386 Adapted to use with CompoundQuery. Implements
** Joinable, which is used by CompoundQuery when
** adding query components.
** 003 ECF 20051129 @23574 Added lenient-off-end mode. When in this
** mode, QueryOffEndExceptions thrown by the
** underlying buffer during next and previous
** operations are suppressed.
** 004 ECF 20051216 @23747 Added scrolling support. Now extends the new
** DynamicQuery base class, which provides
** cursoring support services. Modified
** retrieval logic to allow for repositioning.
** 005 ECF 20060104 @23828 Added accumulation support. Record retrieval
** methods first, last, next, and previous now
** update any registered accumulators after
** successfully retrieving a record.
** 006 GES 20060202 @24226 Removed main().
** 007 ECF 20060202 @24355 Added temp table support. Add buffer's
** multiplex ID as a query substitution argument
** for temp table queries.
** 008 ECF 20060208 @24365 Changed arguments processing in constructor.
** Store duplicates of BaseDataType arguments
** rather than references to the original
** objects.
** 009 ECF 20060215 @24723 Added support for client-side where clause
** processing. Major changes to execute() were
** necessary to support this. All constructors
** now accept an optional WhereExpression
** parameter. Also improved handling of query
** substitution parameter resolution to be more
** correct.
** 010 ECF 20060302 @25023 Add support for foreign key joins. Added new
** constructor variants which mirror existing
** constructors, but accept an additional,
** inverse DMO parameter. Modified query
** execution to integrate natural joins.
** 011 ECF 20060317 @25171 Fixed defect related to extent fields. Use
** mapping of query substitution parameters,
** which may have been reordered during HQL
** where clause preprocessing.
** 012 ECF 20060330 @25256 Removed final modifier from class. FindQuery
** extends this class.
** 013 ECF 20060424 @25696 Added new constructor variants. Each existing
** variant now has a counterpart which does not
** expect a WhereExpression parameter.
** 014 ECF 20060424 @25700 Roll back previous change.
** 015 ECF 20060529 @26645 Added initializeBuffer method. Resets buffer
** during query construction.
** 016 ECF 20060531 @26851 Replaced EndConditionException with
** QueryOffEndException. The latter is a more
** specific version of the former, used to
** indicate a query has run off the end of its
** result set.
** 017 ECF 20060610 @27102 Fixed lock processing defect. A record must
** not be stored in the buffer after a NO-WAIT
** record lock attempt fails.
** 018 ECF 20060621 @27506 Logging improvements.
** 019 ECF 20060713 @28046 Use new DBUtils class.
** 020 ECF 20060720 @28118 Minor changes driven by RecordBuffer naming
** changes. RecordBuffer.getLatestRecord() was
** changed to getSnapshot().
** 021 ECF 20060728 @28272 Minor change to accommodate RecordBuffer
** changes. Enclose RecordBuffer.setTempRecord()
** call in a try-catch block.
** 022 ECF 20060802 @28353 Added break value support. Implemented new
** getBreakValue() method.
** 023 ECF 20060803 @28404 Added updateBuffer() method. Allows
** subclasses to override setting current record
** into buffer.
** 024 ECF 20060925 @29964 Check for a pending error before retrieving a
** record. Retrieval is not attempted if an
** error is pending in the ErrorManager.
** 025 ECF 20061003 @30155 Call prepareFetch() instead of obsolete
** method checkPendingError(). Implemented
** releaseBuffers() to override do-nothing
** AbstractQuery implementation.
** 026 ECF 20061019 @30530 Changed reset() method. If the query is in
** scrolling mode, the cursor is reset as part
** of this method's processing.
** 027 ECF 20061023 @30609 Moved errorIfNull logic to AbstractQuery base
** class. Method setErrorIfNull() is now
** required by the P2JQuery interface and is
** implemented in AbstractQuery instead of here.
** 028 ECF 20061027 @30755 Replaced Object with DataModelObject for DMO
** arguments to methods/constructors. Required
** for compile-time type safety.
** 029 ECF 20061115 @31210 Changed call to set current buffer record.
** RecordBuffer API has changed to permit proper
** management of placeholder snapshot record.
** Added isResetSnapshot() method to allow
** subclasses to override this behavior.
** 030 ECF 20070216 @32165 Explicitly set errorIfNull to false. This is
** required to override the default setting of
** true made by AbstractQuery.
** 031 ECF 20070228 @32248 Optimized no-lock and temp table fetches. In
** these cases, it is not necessary to fetch the
** record in two steps. The no-lock case makes
** no guarantee that the record in memory has
** not been updated by other sessions. The temp
** table case guarantees that no other session
** can make an update to the record in memory,
** because a temp table record is inherently
** scoped to a session.
** 032 ECF 20070308 @32335 Fixed behavior for first/last. Upon failure
** to find a record, if not in looping mode,
** these methods must report a 'FIND FIRST/LAST
** failed...' message.
** 033 ECF 20070412 @32985 Retrofit to use RecordLockContext for record
** locking. Accommodate method name change in
** RecordBuffer: setCurrentRecord --> setRecord.
** 034 ECF 20070416 @33028 Integrated user interrupt handling.
** 035 ECF 20070504 @33410 Retrofitted load() method to use modified
** signature specified by Joinable.
** 036 ECF 20070508 @33460 Fixed executeImpl() to properly handle a
** validation exception. New requirement based
** on a change to RecordBuffer.flush().
** 037 ECF 20070518 @33684 Added getRecordBuffers() and recordBuffers().
** The former is required by the Joinable
** interface. The latter is required by the
** RecordChangeListener interface.
** 038 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.
** 039 ECF 20070718 @34571 Fixed findNext()/findPrevious() and their
** callers. These methods were not properly
** updating locks when necessary.
** 040 ECF 20070713 @34858 Moved all CAN-FIND specific processing to the
** FindQuery subclass. Exposed some private
** methods as protected and refactored class to
** support this.
** 041 ECF 20070827 @35002 Minor optimization for temp table queries.
** Omit multiplex ID from query substitution
** parameters. HQLHelper now hard-codes the ID
** into the where clause, since it is known at
** the time the where clause is generated.
** 042 ECF 20070926 @35246 Fix placeholder record defect. In cases where
** the referenceRecord DMO is modified, we need
** to null it out and use the buffer's current
** snapshot as our placeholder record instead.
** 043 ECF 20071130 @36123 Removed isRefreshSnapshot() method. The
** RecordBuffer placeholder snapshot is no
** longer cleared by any query type when the
** query does not find a record to place in the
** buffer. Integrated generics.
** 044 ECF 20071204 @36266 Fixed off-end tracking. Replaced boolean flag
** with OffEnd enum to discern between off-end
** directions (i.e., off back vs off front of
** result set). Current off-end status and
** active sort index are reported to the record
** buffer when fetching a record. Off-end status
** is checked when searching for a new record.
** 045 ECF 20080112 @37006 Removed restriction of dealing only with IDs.
** This query can now handle both Persistables
** and primary key IDs when retrieving data.
** This is necessary when the full record is
** needed immediately, not just an ID, such as
** in presort cases.
** 046 CA 20080311 @37430 Removed the listener code from c'tor and
** added it to the new registerChangeListener()
** method.
** 047 ECF 20080310 @37482 Made changes necessary to support embedded,
** H2 database.
** 048 SVL 20080418 @38041 Added support for the inverse sorting.
** 049 SVL 20080421 @38082 Added support for legacy key join helper.
** 050 ECF 20080508 @38245 Override base lock type for temporary tables.
** This allows some downstream optimizations.
** 051 ECF 20080510 @38610 Implemented support for dirty database
** checking. Looks into a shared database of
** uncommitted updates and possibly overrides
** data found in primary database.
** 052 ECF 20080609 @38652 Removed overly aggressive optimization in
** executeImpl(). Queries with non-NO-LOCK types
** must also search among uncommitted updates,
** and locks must be acquired on dirty records.
** 053 ECF 20080610 @38690 Fixed dirty check processing in executeImpl()
** method. We were not correctly handling the
** case of finding a dirty version of the same
** DMO found in the primary database.
** 054 ECF 20080611 @38712 Fixed processResults(). When acquiring a lock
** for a dirty record, the record should be
** reloaded, in case it disappeared while we
** were waiting on the lock.
** 055 CA 20080618 @38868 When the JOINed foreign buffer does not refer
** any record, the query must return no result
** as it can not be executed.
** 056 SVL 20080630 @38985 Fixed executeImpl() for the case when a dirty
** record is inserted and modified.
** 057 SVL 20080730 @39222 Added dmoSorter instance variable.
** 058 CA 20080815 @39465 Support API change in DBUtils.
** 060 ECF 20081009 @40082 Integrated new support for inlining query
** substitution parameters. Queries supported by
** this class don't actually have their
** parameters inlined, but changes were needed
** to integrate HQLPreprocessor API changes and
** the ParameterIndices class.
** 061 ECF 20081105 @40312 Implemented registerRecordChangeListeners().
** Required by the Joinable interface.
** 062 ECF 20081215 @40917 Modified executeImpl(). A validation error
** ignores silent error mode, since the flushing
** of the previous record in the buffer is not
** masked by silent error mode.
** 063 ECF 20090303 @41599 Fixed to better support being a Joinable
** component of a CompoundQuery. Implemented
** RecordChangeListener, added getOffEnd()
** method. Replaced activeBundleKey Object with
** an int to support changes in HQLHelper.
** 064 ECF 20090317 @41627 Removed early calls to getHelper(). This
** method must not be invoked until the query is
** actually being executed, because of possible
** dependencies in HQLHelper.obtain() on the
** state of the query's substitution parameters.
** Specifically, a FieldReference parameter may
** not be backed by a record if this method is
** invoked arbitrarily early.
** 065 ECF 20090512 @42154 Modified prepareBuffer(). Now invokes flushAll()
** instead of flush() on backing buffer. This
** ensures all like buffers are allowed to flush
** their transient records properly.
** 066 ECF 20090603 @42596 Added support for Progress isolation leak quirk.
** Modified executeImpl() and processResults() to
** use new features in HQLPreprocessor and in
** DirtyShareContext to leak uncommitted changes to
** other sessions and to select appropriate record.
** 067 ECF 20090609 @42635 Fixed executeImpl(). We now evict, if possible,
** those DMOs read into the Hibernate Session, but
** not loaded into the record buffer. This includes
** DMOs whose uncommitted changes are leaked to
** other sessions early, and DMOs which are found in
** the primary database, but overridden by records
** found by the dirty share manager.
** 068 ECF 20090623 @42941 Fixed executeImpl(). Handle the case of a unique
** search finding information in the dirty database.
** Also added back multiplex ID as a substitution
** parameter for temp table queries.
** 069 ECF 20090703 @43058 Reimplemented load() method. This method now sets
** the backing buffer to unknown mode and throws
** MissingRecordException if a target record cannot
** be loaded.
** 070 ECF 20090716 @43221 Enabled lazy record buffer initialization. Added
** RecordBuffer.initialize() call in c'tor.
** 071 ECF 20090724 @43421 Added implicit transaction support. In various
** methods which execute queries, we push/pop an
** implicit transaction.
** 072 CA 20090731 @43470 Register the query for off-end listener
** registration after block initialization (done
** only for block types FOR and FOR EACH). Added
** getOffEndListeners, which returns a list of all
** off-end listeners for all used buffers.
** 073 SVL 20091030 @44293 getOffEnd() function was made public in order to
** support API change into P2JQuery.
** 074 OM 20130829 Added change detection to buffer on current() methods.
** Updated calls to match the new Persistence.load() signatures.
** 075 ECF 20131028 Import change.
** 076 OM 20140107 Added TODO for missing fatal error message.
** 077 SVL 20140604 Added support for index information field.
** 078 ECF 20140814 Replaced RecordBuffer.flushAll with flush in prepareBuffer method.
** Fixed dirty share logic to work with new flush/validate algorithm.
** Replaced Apache commons logging with J2SE logging.
** 079 OM 20150507 Removed listener from global scope of ChangeBroker.
** 080 ECF 20150801 Implemented new Joinable methods necessary for compound query
** optimization. Error message fix. Enabled database statistics.
** 081 ECF 20150906 Reimplemented silent error mode processing of query substitution
** parameters. Fixed NPE related to outer joins.
** 082 SVL 20151112 Continue normally if reposition failed in load().
** 083 EVL 20160223 Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 084 SVL 20160314 Added setter for referenceRecord.
** 085 OM 20160316 Signature of DirtyShareContext.getDirtyInfo() has changed.
** ECF 20160504 Pass new parameter to Persistence.load. Minor change to isIdOnly.
** 086 SVL 20160608 The query can handle null IDs (for scrolling compound queries
** with OUTER join).
** 087 ECF 20160615 Bias toward fetching full records when possible.
** 088 IAS 20160701 Fixed legacy name in the [565] error message.
** 089 IAS 20160714 Use legacy name instead of DMO alias.
** 090 OM 20160715 Added support for template records.
** Persistence.popImplicitTransaction() changed signature.
** 091 ECF 20160725 Fixed buffer lookup during HQL preprocessing.
** 092 ECF 20160823 Improved executeImpl to better detect missing records and avoid
** deadlock.
** 093 ECF 20160901 DirtyShareContext.isDirtyDelete signature change.
** 094 GES 20160804 Switched from WhereExpression to lambda usage.
** 20160919 All initialization is now done with a default constructor followed
** by a call to initialize(). This enables query instance members in
** converted code to be local variables that are effectively final.
** This is needed to support lambdas without moving queries to be
** instance members. Queries as instance members breaks recursion
** use cases.
** 095 CA 20161012 Fixed scope processing when the query is from a persistent
** procedure, and is part of a QUERY resource.
** 096 ECF 20161014 Fixed implementation of current() method in the event there is no
** current record in the buffer to reload, and this is not considered
** an error in the context of the query (e.g., query represents an
** outer join and record can be null).
** 097 ECF 20171212 Reworked query substitution parameter error handling and error
** condition handling.
** 098 OM 20171129 releaseBuffers() is overwritten from P2JQuery.
** ECF 20171217 Added workaround to executeImpl to avoid dirty share manager blowup,
** but this is not a long-term solution.
** 099 ECF 20180201 Removed initializeBuffer, which released the current record too
** aggressively.
** 100 OM 20180220 Fixed LEFT OUTER JOIN queries.
** 101 OM 20180321 Fixed NPE caused by a null substitution array.
** 102 ECF 20180418 Fixed sort clause generation in makeAdaptiveServerJoinComponent.
** 103 OM 20180515 Added dynamic filtering of results set.
** 104 OM 20180901 Improved dynamic filtering.
** 105 OM 20180918 Filtering is case insensitive and uses LIKE patterns.
** OM 20180924 Filtering on decimal fields uses STRING(f, format) function.
** 106 OM 20181023 Added external buffers when making the adaptive server join components.
** 107 OM 20190330 Ignore close request when query not fully initialized.
** 108 ECF 20190813 Change to RecordBuffer.release and RecordBuffer.setRecord APIs.
** 109 SVL 20191002 Log if sort index was not found.
** 110 CA 20191005 A query must know if it is dynamic, when closing.
** 111 CA 20191009 Added where and sort clause translation in case of bound buffers.
** 112 CA 20200110 If the index is missing, assume the primary index (encountered when
** the sort clause uses fields in a WORD index).
** EVL 20200206 Adding NPE protection to initialize method.
** 113 ECF 20200419 Removed Hibernate dependencies.
** 114 OM 20200915 Handled query navigation on fatal error cases.
** ECF 20200919 Persistence.load API signature change.
** CA 20200924 Replaced Method.invoke with ReflectASM.
** OM 20201120 Added javadoc note about a possible incorrect logged error.
** CA 20210310 A dynamic predicate must set the default lock to NONE, instead of SHARE.
** IAS 20210402 Do not use fast-find cache for meta tables.
** ECF 20210723 Changed signature of query close worker method.
** ECF 20210806 Fixed query substitution parameter preprocessing, and handle a fatal error
** differently than raising a 565 error.
** IAS 20210922 Do not cache the result if the query depends on mutable SESSION attributes.
** OM 20210927 FFC invalidates the records whose changed fields are used in query predicates.
** OM 20220216 Fine-tuned logs when the query index is not found or an error occurs.
** SVL 20220518 next/previous perform first/last if the previous search using the same index has
** failed.
** OM 20220623 Added P2JQuery.Parameter in resolveArg().
** OM 20220726 Added recordAlreadyFound flag for block next/prev iterations on unique
** find-by-rowid queries. Cleanup setRecordAlreadyFound() method.
** OM 20221125 Display error 12378 on attempts to reference uninitialized temp-table.
** OM 20221103 New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
** CA 20221130 Refactored the WHERE clause translation (when the bound and definition buffers
** are not the same), to be aware of the external buffers (i.e. added for CAN-FIND
** sub-select clauses). The translate will be performed before the query is being
** executed.
** 115 CA 20230227 Fixed where clause translation when the external buffer's binding and definition
** does not match.
** 116 AL2 20230210 Allow direct access for recid/unique index look-ups.
** 20230227 Added locking if query is find-by-row-id and record is found in cache.
** 20230307 Honor record nursery before direct access.
** 117 DDF 20230314 Added hasContraints() that checks for where clause/arguments/join/external
** buffers involved in the query.
** AL2 20230314 Moved find-by-rowid short-circuit in execute to cover more cases.
** 20230317 Prepare buffer before attempting direct-access.
** 118 OM 20230404 The join navigation key may contain null/unknown values.
** 119 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
** 120 RAA 20230522 Added instance of P2JQueryStatistics for storing some statistics.
** RAA 20230522 Function executeImpl now goes through P2JQueryLogger
** in case logging is intended.
** 121 SVL 20230516 Support for more granular configuration of dirty sharing functionality.
** 122 RAA 20230607 P2J Queries are now executed in P2JQueryExecutor instead of P2JQueryLogger.
** 123 AL2 20230608 Small refactoring to compute isSimpleQuery only once.
** 124 DDF 20230627 Replaced static variables of DirtyShareSupport with static method calls.
** 125 IAS 20230706 Runtime changes for CONTAINS with non-constant search expression.
** 126 DDF 20230904 Short-circuit when the table is empty, but only after the records are flushed.
** 127 AL2 20230926 Make honorRecordNursery package-private.
** 128 DDF 20231103 Remove short-circuit for empty table. It is called earlier in FindQuery
** by using buffer.fastHasRecords().
** 129 RAA 20231006 Added getBundleFql and replaced parameters used in P2JQueryExecutor.
** RAA 20231010 Removed DatabaseStatistics usage.
** RAA 20231122 Added activeBundle FQL caching.
** 130 RAA 20231220 Surrounded P2JQueryExecutor.execute in a try-catch, in execute function.
** 131 ES 20231203 Changed first, last, next, previous, current, unique methods to return boolean,
** instead of void. These methods now returns true if operation was successful, and false
** not and lenientOffEnd is set on true, otherwise it throws an QQE exception.
** ES 20231203 Added new setLenientOffEnd method.
** 132 OM 20231002 Avoided name collision in execute() method.
** 133 AI 20240221 Updated addDynamicFilter to add the processed parameter.
** 134 CA 20240422 Allow the query to be reset to its initialization state, so the same instance can
** be used to execute child buffer FILL operations.
** 135 AL2 20240313 Fixed helper reset on .reset() if inlined.
** Check if args are unknown even for non-outer.
** 136 ES 20240626 Avoid logging DirectedAccessExceptions in expected cases.
** 137 SP 20240614 Check in executeDirectAccess if the query uses contains operator.
** 138 CA 20240626 'findNext' and 'findPrev' must not find a record of offend is BACK or FRONT.
** 139 AL2 20240626 Don't halt query if AbstractQuery.preprocessSubstitutionArguments fails.
** 140 RAA 20240613 Added tenant and database id parameters when calling FastFindCache.getInstance.
** RAA 20240617 Rearranged ffCache parameters.
** 141 CA 20240809 Avoid iterator usage, for performance improvement.
** 142 SP 20240801 Added new getTranslatedWhere method to use for translateWhere.
** 143 EAB 20240819 Preventing dirty records from being added to ffCache.
** 144 DDF 20240814 Added silent method equivalents to first, next, previous, last, unique, current
** to avoid using ErrorManager.silent() which requires a lambda.
** DDF 20240821 lenientOffEnd flag is already available in AbstractQuery and it should be
** set for each silent mode method because the silent flag retrieved through
** the ErrorHelper is set after the FindQuery constructor.
** DDF 20240822 Made ErrorHelper instance a class member for performance improvements.
** DDF 20240828 Removed silent mode methods that are accessible through AbstractQuery and
** moved the errorHelper property to AbstractQuery.
** 145 OM 20240909 FFCAche does not require knowledge of current tenant.
** 146 CA 20230924 Further reduce context-local lookup.
** 147 CA 20241001 A 'find-by-rowid' (be it via FOR EACH or FIND queries) must always throw a
** QueryOffEndException. This is the same behavior as before H144, where
** RAQ.lenientOffEnd field was set to 'true' by AdaptiveQuery$DynamicResults, and
** the read was done via AbstractQuery getter, which read the wrong field.
** 148 TJD 20221129 NPE fix in processDirtyResults when there is nothing to compare to current DMO
** TJD 20230508 Bringing back support for records leaks to other sessions
** TJD 20230724 Migrate earlyPublishEntities from List to Set to improve performance
** TJD 20230825 Fix NPE for NULL session when checking cache
** TJD 20240110 Fixes for DirtyShare database cases
** TJD 20240126 Dont NPE when ffCache is null.
** TJD 20240308 Optimize dirtyContext.updateSnapshots call, dont cache non-session DMOs
** TJD 20240319 Do not try to compare dirty results if DMOSorter is null
** TJD 20240705 Support for doubled parameters for NEXT/PREV queries
** TJD 20240730 Allow offEnd.FRONT to be treated like offEnd.BACK for prev/last queries
** TJD 20240912 DirtyShareSupport flags logic dependency update
** 149 AL2 20241021 Allow hinting that this is a CAN-FIND query and the buffer is not going to be
** loaded by the found DMO.
** 150 RNC 20241031 Added dirty-intra-share check to honor the answer from
** the dirty database in RAQ.executeImpl().
** 151 AL2 20241126 Allow processing dirty result when intra-session dirty-share is enabled.
** 152 OM 20241128 Multi-tenant runtime support: selected the proper persistence context, eventually
** based on [sharedDb] parameter.
** Implementation of [setMultiTenantAlternative] method.
** 153 AL2 20241217 Prepare buffer before honoring the record nursery. This way, the record nursery
** won't push the changes on the buffer to the dirty database *before* flushing it.
** 154 ES 20250407 Use timeout parameter in the call to Persistence.load.
** 155 LS 20250414 Updated getHelper() to use the included/excluded fields.
** Added setters for included/excluded arrays.
** 156 ES 20250508 Synchronize the query buffer if it is AUTO-SYNCHRONIZE attribute is set on true.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
import java.util.logging.*;
import com.goldencode.p2j.persist.dirty.*;
import com.goldencode.p2j.persist.event.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.ErrorManager;
import com.goldencode.p2j.util.ErrorManager.ErrorHelper;
import com.goldencode.p2j.util.logging.CentralLogger;
import com.goldencode.util.*;
/**
* The backing query for converted queries which require a dynamic record
* retrieval semantic. This means that a single record is retrieved at a
* time and the query manages state about that record's position relative
* to others which also match the query's search criteria. Furthermore,
* any change to that record's data may change its position relative to other
* matching records, and these changes must be supported while client code is
* using the query to visit result records. Client code can jump among
* results randomly. These moves are specified as either relative requests
* (next, previous, current), or as absolute requests (first, last, unique).
* <p>
* This class slavishly implements the Progress semantic of fully dynamic
* query iteration. That is, each record is searched for and retrieved
* individually, so that any change to an existing record can be reflected
* immediately in the remaining navigation performed on the query. This
* means that an update to the current record may cause it to reappear later
* in the query results, if the changes made to the record would cause it to
* be sorted differently than when it was first retrieved (note that this can
* cause infinite looping, just as in the pre-conversion, Progress code).
* <p>
* To implement the single-record retrieval semantic, this high level query
* may issue multiple, database-level, select statements to the database for
* each record retrieval attempt. For details of how this is performed, see
* {@link FQLHelper} and {@link FQLBundle}. The result of a record retrieval
* is a Data Model Object (DMO), which is stored in a {@link RecordBuffer}.
* A record buffer is associated with this object at construction.
* <p>
* Because of this implementation requirement, <code>RandomAccessQuery</code>
* represents <i>an extremely inefficient mechanism</i> by which to query a
* database for a set of records. As such, <b>it should not be used for new
* development; it is intended only to support legacy, converted code</b>,
* insofar as that code may rely on the dynamic nature of the Progress
* semantic.
* <p>
* This class implements the <code>Joinable</code> interface so that it can
* be added as a component to a {@link CompoundQuery}. When used in such a
* capacity, the query should be configured to raise an end condition instead
* of an error condition, in the case that a request to get the first, last,
* or a unique record fails. This will allow the compound query to continue
* processing normally in this case. This is accomplished by invoking {@link
* #setErrorIfNull} with the parameter <code>true</code>.
* <p>
* When used to represent a Progress query (as created with DEFINE or OPEN
* QUERY), {@link #setLenientOffEnd} must be set to <code>true</code>, which
* suppresses end condition exceptions for <code>next</code> and
* <code>previous</code> commands which run off the end of the query's
* results.
* <p>
* <strong>Transaction Isolation (Dirty Reads)</strong>
* <br>
* This class supports Progress' peculiar form of transaction isolation, where
* uncommitted updates, inserts, and deletes that affect database indexes are
* visible across sessions. This is emulated using a shared database (the
* "dirty" database) which temporarily stores information about uncommitted
* changes made by other contexts. When those other sessions commit or roll
* back their changes, this transient information is cleared from the dirty
* database and is no longer visible.
* <p>
* The implementation of this feature requires that the results found in the
* primary database backing a query be treated as provisional. Results found
* in the dirty database may override, if it is determined that the "dirty"
* result is the more correct result, given the query criteria.
*/
public class RandomAccessQuery
extends DynamicQuery
implements Joinable,
QueryConstants,
RecordChangeListener
{
/** Logger */
private static final CentralLogger LOG = CentralLogger.get(RandomAccessQuery.class.getName());
/** Record buffer which backs this query */
private RecordBuffer buffer;
/** Helper object for dynamic join via a foreign key relation */
private AbstractJoin join;
/** Client-side where clause expression, if any */
private Supplier<logical> whereExpr;
/** The original arguments (un-evaluated) used at the query initialization. */
private Object[] origArgs;
/** Base substitution parameters for FQL queries before applying any dynamic filter. */
private Object[] args;
/** Base substitution parameters for FQL queries, including the dynamic filtering components, if any. */
private Object[] dfArgs;
/** Lock type to apply to records retrieved */
private LockType lockType;
/**
* Original where clause (only stored until helper is built). It is only used for building the
* {@code dfWhere} which also contains the information on eventual dynamic filters.
*/
private String where = null;
/**
* Original where clause (only stored until helper is built), including the dynamic filtering
* components, if any.
*/
private String dfWhere = null;
/** Original sort clause (only stored until helper is built) */
private String sort = null;
/** DMO sorter associated with query's sort clause */
private DMOSorter dmoSorter = null;
/** The id of the index in use for this query. */
private int index = 0;
/** Index information string as it is returned by INDEX-INFORMATION. */
private String indexInfo = null;
/** Builds FQL statements for various Hibernate queries. */
private FQLHelper helper = null;
/** Cached helper for non-null arguments in left outer join queries. */
private FQLHelper nonNullArgsHelper = null;
/** FQL data necessary for the current query operation */
private FQLBundle activeBundle = null;
/** Key which indicates active bundle choice */
private int activeBundleKey = NONE;
/** The FQL of the whole activeBundle. */
private String activeBundleFql = null;
/** Substitution parameter values for the next query execution */
private Object[] currentArgs = null;
/** Record whose data defines current "place" in query results */
private Record referenceRecord = null;
/** Break value indicating a new sort band if non-<code>null</code> */
private Object breakValue = null;
/** Force query to retrieve full records, rather than primary keys only */
private boolean fullRecords = false;
/** Was last record found retrieved from the dirty database? */
private boolean dirtyCopy = false;
/** RecordChangeListener which nulls out placeholder reference record */
private RecordChangeListener placeholderCleaner = null;
/** Determines if this will be unregistered from ChangeBroker on cleanup. */
private boolean unregisterOnCleanup = false;
/**
* The list of arguments for currently applied dynamic filters.
* Note that its size may be smaller than the number of dynamic filters because the null /
* unknown values were automatically converted to {@code is null} so they lack the argument.
*/
private List<Object> dynamicFilterArgs = null;
/**
* The list of dynamic filters. These are filters added dynamically, at runtime and can also be
* removed dynamically.
*/
private Map<FieldReference, BaseDataType> dynamicFilters = null;
/**
* The list of format used by dynamic filters. These are filters added dynamically, at runtime
* and can also be removed dynamically.
*/
private Map<FieldReference, String> dynamicFormats = null;
/** The fast-find cache associated to this query. */
private FastFindCache ffCache = null;
/**
* In case this is a FIND-BY-ROWID unique query, this flag marks whether the target record was found in a
* previous iteration and allow the OFF-END condition to be thrown.
*/
private boolean recordAlreadyFound = false;
/** A function to translate the where clause, just before it gets executed. */
private Supplier<String> translateWhere = null;
/**
* Default constructor. Initialization is deferred until <code>initialize()</code> is called.
*/
public RandomAccessQuery()
{
}
/**
* Initialization logic which defaults record lock type to <code>LockType.SHARE</code>.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
*
* @return This query instance.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort)
{
return initialize(dmo, where, whereExpr, sort, null, null, null, getDefaultLock());
}
/**
* Initialization logic which defaults record lock type to <code>LockType.SHARE</code>.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
*
* @return This query instance.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo)
{
return initialize(dmo, where, whereExpr, sort, indexInfo, null, null, getDefaultLock());
}
/**
* Initialization logic which is used when joining to another table using a foreign
* key. Defaults record lock type to <code>LockType.SHARE</code>.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
*
* @return This query instance.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
DataModelObject inverse)
{
return initialize(dmo, where, whereExpr, sort, null, inverse, null, getDefaultLock());
}
/**
* Initialization logic which is used when joining to another table using a foreign
* key. Defaults record lock type to <code>LockType.SHARE</code>.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
*
* @return This query instance.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo,
DataModelObject inverse)
{
return initialize(dmo, where, whereExpr, sort, indexInfo, inverse, null, getDefaultLock());
}
/**
* Initialization logic which defaults record lock type to
* <code>LockType.SHARE</code> and accepts default substitution parameters.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param args
* Substitution parameters for FQL queries. These will be used
* if not overridden by a record retrieval method.
*
* @return This query instance.
*
* @throws IllegalArgumentException
* if any substitution argument provided is not of a supported
* type.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
Object[] args)
{
return initialize(dmo, where, whereExpr, sort, null, null, args, getDefaultLock());
}
/**
* Initialization logic which defaults record lock type to
* <code>LockType.SHARE</code> and accepts default substitution parameters.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
* @param args
* Substitution parameters for FQL queries. These will be used
* if not overridden by a record retrieval method.
*
* @return This query instance.
*
* @throws IllegalArgumentException
* if any substitution argument provided is not of a supported
* type.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo,
Object[] args)
{
return initialize(dmo, where, whereExpr, sort, indexInfo, null, args, getDefaultLock());
}
/**
* Initialization logic which is used when joining to another table using a foreign
* key. Defaults record lock type to <code>LockType.SHARE</code> and
* accepts default substitution parameters.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters for FQL queries. These will be used
* if not overridden by a record retrieval method.
*
* @return This query instance.
*
* @throws IllegalArgumentException
* if any substitution argument provided is not of a supported
* type.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
DataModelObject inverse,
Object[] args)
{
return initialize(dmo, where, whereExpr, sort, null, inverse, args, getDefaultLock());
}
/**
* Initialization logic which is used when joining to another table using a foreign
* key. Defaults record lock type to <code>LockType.SHARE</code> and
* accepts default substitution parameters.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters for FQL queries. These will be used
* if not overridden by a record retrieval method.
*
* @return This query instance.
*
* @throws IllegalArgumentException
* if any substitution argument provided is not of a supported
* type.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo,
DataModelObject inverse,
Object[] args)
{
return initialize(dmo, where, whereExpr, sort, indexInfo, inverse, args, getDefaultLock());
}
/**
* Initialization logic which sets an explicit, default, record lock type.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param lockType
* Lock type to apply to records retrieved, if not overridden by
* a record retrieval method.
*
* @return This query instance.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
LockType lockType)
{
return initialize(dmo, where, whereExpr, sort, null, null, null, lockType);
}
/**
* Initialization logic which sets an explicit, default, record lock type.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
* @param lockType
* Lock type to apply to records retrieved, if not overridden by
* a record retrieval method.
*
* @return This query instance.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo,
LockType lockType)
{
return initialize(dmo, where, whereExpr, sort, indexInfo, null, null, lockType);
}
/**
* Initialization logic which is used when joining to another table using a foreign
* key. Sets an explicit, default, record lock type.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param lockType
* Lock type to apply to records retrieved, if not overridden by
* a record retrieval method.
*
* @return This query instance.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
DataModelObject inverse,
LockType lockType)
{
return initialize(dmo, where, whereExpr, sort, null, inverse, null, lockType);
}
/**
* Initialization logic which is used when joining to another table using a foreign
* key. Sets an explicit, default, record lock type.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param lockType
* Lock type to apply to records retrieved, if not overridden by
* a record retrieval method.
*
* @return This query instance.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo,
DataModelObject inverse,
LockType lockType)
{
return initialize(dmo, where, whereExpr, sort, indexInfo, inverse, null, lockType);
}
/**
* Initialization logic which sets an explicit, default, record lock type and
* accepts default substitution parameters.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param args
* Substitution parameters for FQL queries. These will be used
* if not overridden by a record retrieval method.
* @param lockType
* Lock type to apply to records retrieved, if not overridden by
* a record retrieval method.
*
* @return This query instance.
*
* @throws IllegalArgumentException
* if any substitution argument provided is not of a supported
* type.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
Object[] args,
LockType lockType)
{
return initialize(dmo, where, whereExpr, sort, null, null, args, lockType);
}
/**
* Initialization logic which sets an explicit, default, record lock type and
* accepts default substitution parameters.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
* @param args
* Substitution parameters for FQL queries. These will be used
* if not overridden by a record retrieval method.
* @param lockType
* Lock type to apply to records retrieved, if not overridden by
* a record retrieval method.
*
* @return This query instance.
*
* @throws IllegalArgumentException
* if any substitution argument provided is not of a supported
* type.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo,
Object[] args,
LockType lockType)
{
return initialize(dmo, where, whereExpr, sort, indexInfo, null, args, lockType);
}
/**
* Initialization logic which is used when joining to another table using a foreign
* key. Sets an explicit, default, record lock type and accepts default
* substitution parameters.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters for FQL queries. These will be used
* if not overridden by a record retrieval method.
* @param lockType
* Lock type to apply to records retrieved, if not overridden by
* a record retrieval method.
*
* @return This query instance.
*
* @throws IllegalArgumentException
* if any substitution argument provided is not of a supported
* type.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
DataModelObject inverse,
Object[] args,
LockType lockType)
{
return initialize(dmo, where, whereExpr, sort, null, inverse, args, lockType);
}
/**
* Initialization logic which is used when joining to another table using a foreign
* key. Sets an explicit, default, record lock type and accepts default
* substitution parameters.
* <p>
* This code is intentionally separated from the constructor. The purpose of this separation
* is to allow construction to occur in the prior scope while initialization can still occur
* within a method or lambda that is inside the next block's scope.
*
* @param dmo
* Data model object which determines the record buffer into which
* records are retrieved.
* @param where
* Where clause, using FQL syntax. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Order by clause, using FQL syntax. May be <code>null</code>
* only if query is to be navigated only using one of the
* <code>unique</code> method variants.
* @param indexInfo
* Index information string as it is returned by INDEX-INFORMATION.
* May be <code>null</code> if it is not an OPEN QUERY case or if
* you don't need debug information about selected indexes.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters for FQL queries. These will be used
* if not overridden by a record retrieval method.
* @param lockType
* Lock type to apply to records retrieved, if not overridden by
* a record retrieval method.
*
* @return This query instance.
*
* @throws IllegalArgumentException
* if any substitution argument provided is not of a supported
* type.
*/
public RandomAccessQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo,
DataModelObject inverse,
Object[] args,
LockType lockType)
{
this.buffer = ((BufferReference) dmo).buffer();
RecordBuffer defBuffer = ((BufferReference) dmo).definition();
if (!buffersMatch(buffer, defBuffer))
{
sort = translateSort(buffer, defBuffer, sort);
if (where != null)
{
this.translateWhere = () ->
{
ArrayList<RecordBuffer> binding = new ArrayList<>();
binding.add(buffer);
ArrayList<RecordBuffer> definition = new ArrayList<>();
definition.add(defBuffer);
return translateWhere(binding, definition, where);
};
}
}
if (buffer.isReadonly())
{
// TODO: readonly buffers cannot be used in a FIND statement but can be used in CAN-FIND functions
}
buffer.initialize();
this.where = where;
this.dfWhere = where;
this.sort = sort;
this.whereExpr = whereExpr;
if (args != null)
{
this.origArgs = new Object[args.length];
System.arraycopy(args, 0, origArgs, 0, args.length);
}
this.args = args;
this.dfArgs = args;
this.indexInfo = indexInfo;
DmoMeta dmoMeta = buffer.getDmoInfo();
int idxCount = dmoMeta.getIndexCount(true) + dmoMeta.getIndexCount(false);
String indexName = null;
boolean argResolved = false;
if (idxCount > 0)
{
// get index, which is needed for dirty checking work and FastFind caching
try
{
IndexHelper indexHelper = buffer.getIndexHelper();
indexName = indexHelper.getIndexForSort(buffer, sort);
if (indexName == null)
{
P2JIndex p2jIndex = indexHelper.getPrimaryIndex(buffer.getEntityName());
if (p2jIndex != null)
{
indexName = p2jIndex.getName();
}
}
if (buffer.getDirtyContext() != null)
{
this.dmoSorter = indexHelper.getSorterForSortPhrase(buffer, sort);
}
if (indexName != null)
{
index = buffer.getDmoInfo().getRecordMeta().getIndexIdByName(indexName);
}
// in case of find-by-rowid the [index] is not used, anyway
if (index == 0 && LOG.isLoggable(Level.WARNING))
{
// when accessing the preprocessor, the arguments must be already resolved
resolveArgs(false);
argResolved = true; // avoid double call to resolveArgs() method
if (!getHelper().getFQLPreprocessor().isFindByRowid())
{
LOG.log(Level.WARNING,
"Could not locate index for sort phrase [" + sort + "]. Search may be slow.");
}
}
}
catch (PersistenceException exc)
{
if (LOG.isLoggable(Level.SEVERE))
{
LOG.log(Level.SEVERE,
"Error encountered while locating index for sort phrase [" + sort +
"]. Incremental search may be slow.",
exc);
}
}
}
if (inverse != null)
{
if (Persistence.isForeignKeysEnabled())
{
join = new DynamicJoin(buffer, ((BufferReference) inverse).buffer());
}
else
{
join = new DynamicLegacyKeyJoin(buffer, ((BufferReference) inverse).buffer());
}
}
else
{
join = null;
}
if (!dmoMeta.isMeta() && index != 0)
{
this.ffCache = FastFindCache.getInstance(buffer.isTemporary(), buffer.getDatabase());
}
// override lock type if this is a temporary buffer; this allows some downstream optimizations
this.lockType = (buffer.isTemporary() ? LockType.NONE : lockType);
AbstractQuery.preprocessSubstitutionArguments(buffer.getBufferManager(),
this::isFindByRowid,
suppressNestedQueryError(),
args);
setErrorIfNull(false);
if (!argResolved)
{
resolveArgs(false);
}
registerChangeListener();
buffer.getTxHelper().registerOffEndQuery(this);
return this;
}
/**
* Translate the {@link #where} clause, using the {@link #translateWhere} function.
*/
@Override
public void translateWhere()
{
if (translateWhere == null)
{
return;
}
this.where = translateWhere.get();
this.dfWhere = this.where;
this.translateWhere = null;
}
/**
* Force the query to retrieve full records, rather than primary keys only.
*/
public void setFullRecords()
{
this.fullRecords = true;
}
/**
* Navigate to the first record which meets the query criteria and retrieve
* it into its record buffer. Use the default substitution parameters and
* lock type.
*
* @return <code>true</code> if query could retrieve the first record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean first()
{
return first(getCurrentArgs(), lockType);
}
/**
* Navigate to the first record which meets the query criteria and retrieve
* it into its record buffer. Use the default lock type, but override the
* default substitution parameters with those provided.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean first(Object[] values)
{
return first(values, lockType);
}
/**
* Navigate to the first record which meets the query criteria and retrieve
* it into its record buffer. Use the default substitution parameters, but
* override the default lock type with that provided.
*
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean first(LockType lockType)
{
return first(getCurrentArgs(), lockType);
}
/**
* Navigate to the first record which meets the query criteria and retrieve
* it into its record buffer. Override both the default substitution
* parameters, and the default lock type with those provided.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean first(Object[] values, LockType lockType)
{
if (!prepareFetch())
{
if (throwExceptionOnFatalError())
{
// the message is not actually used, but will make things easier when debugging
String msg = "Query aborted due a fatal error: " + ErrorManager.getErrorText(1).toStringMessage();
List<QueryOffEndListener> listeners = getOffEndListeners();
throw new QueryOffEndException(msg, listeners.toArray(new QueryOffEndListener[listeners.size()]));
}
else
{
errorFindFirstLast();
return false;
}
}
if (outer)
{
// reset now the helper. Another one will be created if any of the arguments are null.
// Otherwise, the not-null helper will be used if already constructed.
helper = null;
}
if (getHelper().getFQLPreprocessor().isFindByRowid())
{
// direct record access using recid/rowid is behaving like unique for all search types
boolean available = unique(values, lockType);
if (available)
{
recordAlreadyFound = true;
}
return available;
}
// If a lock type is not specified, assume the query's lock type.
if (lockType == null)
{
lockType = this.lockType;
}
Record dmo = null;
try
{
// if query is scrolling, check the cached result list first
boolean scrolling = (cursor != null);
if (scrolling)
{
dmo = loadByValue(cursor.getFirst(), lockType);
}
// if not scrolling or wasn't in the cached result list, perform the query and cache the
// result (if scrolling)
if (dmo == null)
{
activateFirst();
dmo = execute(values, lockType, false, true);
if (scrolling)
{
cursor.addResultFirst(getIDs(dmo));
}
}
finalizeFind(lockType, dmo, this::errorFindFirstLast, false);
if (dmo == null)
{
return false;
}
}
catch (QueryOffEndException exc)
{
// rethrow without buffer release; this catch block is here, so we don't hit the
// ErrorConditionException catch block (QOEE's ancestor) and release the buffer
throw exc;
}
catch (ErrorConditionException exc)
{
// if the exception is set to force an override of silent error mode behavior, it is
// because we encountered a validation error while flushing a record from the buffer
// to make way for the query result; don't try to release/flush the invalid record
// again here
if (!exc.isForce())
{
buffer.release(false);
}
throw exc;
}
return true;
}
/**
* Navigate to the first record which meets the query criteria and retrieve
* it into its record buffer. Use the default lock type, but override the
* default substitution parameters with those provided.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean silentFirst(Object[] values)
{
return silentFirst(values, lockType);
}
/**
* Navigate to the first record which meets the query criteria and retrieve
* it into its record buffer. Override both the default substitution
* parameters, and the default lock type with those provided.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean silentFirst(Object[] values, LockType lockType)
{
ErrorHelper eh = getErrorHelper();
boolean wasSilent = eh.isSilent();
if (!wasSilent)
{
eh.setSilent(true);
}
boolean wasLenientOffEnd = isLenientOffEnd();
if (!wasLenientOffEnd)
{
setLenientOffEnd(true);
}
try
{
return first(values, lockType);
}
catch (LegacyErrorException lex)
{
ErrorManager.processLegacyErrorException(lex, eh);
}
catch (ErrorConditionException err)
{
// normally we just eat the exception because it is being used to abort processing
// exactly where the error occurred, which will restart processing after the given
// code block; however, there is an exception for some deferred error processing use
// cases where we must bypass silent error mode (throwError(NumberedException, boolean))
if (err.isForce())
{
// don't honor silent mode, no suppression of errors here
throw err;
}
}
finally
{
// disable further silent error processing
if (!wasSilent)
{
eh.setSilent(false);
}
if (!wasLenientOffEnd)
{
setLenientOffEnd(false);
}
eh.handleForwardPending();
}
return eh.getErrorStatus();
}
/**
* Navigate to the last record which meets the query criteria and retrieve
* it into its record buffer. Use the default substitution parameters and
* lock type.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean last()
{
return last(getCurrentArgs(), lockType);
}
/**
* Navigate to the last record which meets the query criteria and retrieve
* it into its record buffer. Use the default lock type, but override the
* default substitution parameters with those provided.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean last(Object[] values)
{
return last(values, lockType);
}
/**
* Navigate to the last record which meets the query criteria and retrieve
* it into its record buffer. Use the default substitution parameters, but
* override the default lock type with that provided.
*
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean last(LockType lockType)
{
return last(getCurrentArgs(), lockType);
}
/**
* Navigate to the last record which meets the query criteria and retrieve
* it into its record buffer. Override both the default substitution
* parameters, and the default lock type with those provided.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean last(Object[] values, LockType lockType)
{
if (!prepareFetch())
{
if (throwExceptionOnFatalError())
{
// the message is not actually used, but will make things easier when debugging
String msg = "Query aborted due a fatal error: " + ErrorManager.getErrorText(1).toStringMessage();
List<QueryOffEndListener> listeners = getOffEndListeners();
throw new QueryOffEndException(msg, listeners.toArray(new QueryOffEndListener[listeners.size()]));
}
else
{
errorFindFirstLast();
return false;
}
}
if (outer)
{
// reset now the helper. Another one will be created if any of the arguments are null.
// Otherwise, the not-null helper will be used if already constructed.
helper = null;
}
if (getHelper().getFQLPreprocessor().isFindByRowid())
{
// direct record access using recid/rowid is behaving like unique for all search types
boolean avail = unique(values, lockType);
if (avail)
{
recordAlreadyFound = true;
}
return avail;
}
// If a lock type is not specified, assume the query's lock type.
if (lockType == null)
{
lockType = this.lockType;
}
Record dmo = null;
try
{
// If query is scrolling, check the cached result list first.
boolean scrolling = (cursor != null);
if (scrolling)
{
dmo = loadByValue(cursor.getLast(), lockType);
}
// If not scrolling or wasn't in the cached result list, perform the
// query and cache the result (if scrolling).
if (dmo == null)
{
activateLast();
dmo = execute(values, lockType, false, true);
if (scrolling)
{
cursor.addResultLast(getIDs(dmo));
}
}
// store result in the record buffer, even if null.
finalizeFind(lockType, dmo, this::errorFindFirstLast, false);
if (dmo == null)
{
return false;
}
}
catch (QueryOffEndException exc)
{
// rethrow without buffer release; this catch block is here, so we don't hit the
// ErrorConditionException catch block (QOEE's ancestor) and release the buffer
throw exc;
}
catch (ErrorConditionException exc)
{
// if the exception is set to force an override of silent error mode behavior, it is
// because we encountered a validation error while flushing a record from the buffer
// to make way for the query result; don't try to release/flush the invalid record
// again here
if (!exc.isForce())
{
buffer.release(false);
}
throw exc;
}
return true;
}
/**
* Navigate to the last record which meets the query criteria and retrieve
* it into its record buffer. Use the default lock type, but override the
* default substitution parameters with those provided.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean silentLast(Object[] values)
{
return silentLast(values, lockType);
}
/**
* Navigate to the last record which meets the query criteria and retrieve
* it into its record buffer. Override both the default substitution
* parameters, and the default lock type with those provided.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the last record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if no record
* could be found when operating in standalone mode.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean silentLast(Object[] values, LockType lockType)
{
ErrorHelper eh = getErrorHelper();
boolean wasSilent = eh.isSilent();
if (!wasSilent)
{
eh.setSilent(true);
}
boolean wasLenientOffEnd = isLenientOffEnd();
if (!wasLenientOffEnd)
{
setLenientOffEnd(true);
}
try
{
return last(values, lockType);
}
catch (LegacyErrorException lex)
{
ErrorManager.processLegacyErrorException(lex, eh);
}
catch (ErrorConditionException err)
{
// normally we just eat the exception because it is being used to abort processing
// exactly where the error occurred, which will restart processing after the given
// code block; however, there is an exception for some deferred error processing use
// cases where we must bypass silent error mode (throwError(NumberedException, boolean))
if (err.isForce())
{
// don't honor silent mode, no suppression of errors here
throw err;
}
}
finally
{
// disable further silent error processing
if (!wasSilent)
{
eh.setSilent(false);
}
if (!wasLenientOffEnd)
{
setLenientOffEnd(false);
}
eh.handleForwardPending();
}
return eh.getErrorStatus();
}
/**
* Navigate to the next record which meets the query criteria and retrieve
* it into its record buffer. Use the default substitution parameters and
* lock type.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the next record to visit. If no
* record has yet been loaded into the buffer, retrieve the first record
* which meets the query criteria.
*
* @return <code>true</code> if query could retrieve the next record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean next()
{
return next(getCurrentArgs(), lockType);
}
/**
* Navigate to the next record which meets the query criteria and retrieve
* it into its record buffer. Use the default lock type, but override the
* default substitution parameters with those provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the next record to visit. If no
* record has yet been loaded into the buffer, retrieve the first record
* which meets the query criteria.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the next record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean next(Object[] values)
{
return next(values, lockType);
}
/**
* Navigate to the next record which meets the query criteria and retrieve
* it into its record buffer. Use the default substitution parameters, but
* override the default lock type with that provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the next record to visit. If no
* record has yet been loaded into the buffer, retrieve the first record
* which meets the query criteria.
*
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the next record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean next(LockType lockType)
{
return next(getCurrentArgs(), lockType);
}
/**
* Navigate to the next record which meets the query criteria and retrieve
* it into its record buffer. Override both the default substitution
* parameters, and the default lock type with those provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the next record to visit. If no
* record has yet been loaded into the buffer, retrieve the first record
* which meets the query criteria.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the next record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found and <code>lenientOffEnd</code> is
* <code>false</code>.
*/
public boolean next(Object[] values, LockType lockType)
{
if (!prepareFetch())
{
return false;
}
if (outer)
{
// reset now the helper. Another one will be created if any of the arguments are null.
// Otherwise, the not-null helper will be used if already constructed.
helper = null;
}
if (getHelper().getFQLPreprocessor().isFindByRowid())
{
// if the record was found in a previous iteration, raise the OFF-END exception
if (recordAlreadyFound)
{
updateBuffer(null, lockType, false, OffEnd.BACK);
referenceRecord = null;
// this is required to always be throw for a 'by-rowid' query, be it a FOR EACH or a FIND.
// previously AdaptiveQuery$DynamicResults was setting 'lenientOffEnd' flag at the RAQ field,
// but the read was done via the getter at AbstractQuery, so the wrong value was read, which
// was always false.
throw new QueryOffEndException("", null);
}
// direct record access using recid/rowid is behaving like unique for all search types
boolean avail = unique(values, lockType);
if (avail)
{
recordAlreadyFound = true;
}
return avail;
}
// If a lock type is not specified, assume the query's lock type.
if (lockType == null)
{
lockType = this.lockType;
}
Record dmo = null;
try
{
// If query is scrolling, check the cached result list first.
boolean scrolling = (cursor != null);
if (scrolling)
{
dmo = loadByValue(cursor.getNext(), lockType);
}
// If not scrolling or wasn't in the cached result list, perform the
// query and cache the result (if scrolling).
if (dmo == null)
{
OffEnd offEnd = buffer.getOffEnd(getHelper().getSortIndex(), inverseSorting);
boolean isFirst;
if (offEnd != OffEnd.NONE && // we have off-end for the *same* database index
!buffer.isAvailable())
{
isFirst = true;
buffer.resetSnapshot();
}
else
{
isFirst = (buffer.getSnapshot() == null &&
referenceRecord == null &&
offEnd == OffEnd.FRONT);
}
dmo = findNext(values, lockType, true);
if (scrolling)
{
if (isFirst)
{
cursor.addResultFirst(getIDs(dmo));
}
else
{
cursor.addResultNext(getIDs(dmo));
}
}
}
// Store result in the record buffer, even if null.
OffEnd offEnd = (dmo == null ? OffEnd.BACK : OffEnd.NONE);
updateBuffer(dmo, lockType, false, offEnd);
referenceRecord = dmo;
// Notify accumulators of an iteration.
accumulate();
if (dmo == null)
{
return false;
}
}
catch (QueryOffEndException exc)
{
if (!lenientOffEnd)
{
throw exc;
}
return false;
}
catch (ErrorConditionException exc)
{
// if the exception is set to force an override of silent error mode behavior, it is
// because we encountered a validation error while flushing a record from the buffer
// to make way for the query result; don't try to release/flush the invalid record
// again here
if (!exc.isForce())
{
buffer.release(false);
}
throw exc;
}
return true;
}
/**
* Navigate to the next record which meets the query criteria and retrieve
* it into its record buffer. Use the default lock type, but override the
* default substitution parameters with those provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the next record to visit. If no
* record has yet been loaded into the buffer, retrieve the first record
* which meets the query criteria.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the next record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean silentNext(Object[] values)
{
return silentNext(values, lockType);
}
/**
* Navigate to the next record which meets the query criteria and retrieve
* it into its record buffer. Override both the default substitution
* parameters, and the default lock type with those provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the next record to visit. If no
* record has yet been loaded into the buffer, retrieve the first record
* which meets the query criteria.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the next record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean silentNext(Object[] values, LockType lockType)
{
ErrorHelper eh = getErrorHelper();
boolean wasSilent = eh.isSilent();
if (!wasSilent)
{
eh.setSilent(true);
}
boolean wasLenientOffEnd = isLenientOffEnd();
if (!wasLenientOffEnd)
{
setLenientOffEnd(true);
}
try
{
return next(values, lockType);
}
catch (LegacyErrorException lex)
{
ErrorManager.processLegacyErrorException(lex, eh);
}
catch (ErrorConditionException err)
{
// normally we just eat the exception because it is being used to abort processing
// exactly where the error occurred, which will restart processing after the given
// code block; however, there is an exception for some deferred error processing use
// cases where we must bypass silent error mode (throwError(NumberedException, boolean))
if (err.isForce())
{
// don't honor silent mode, no suppression of errors here
throw err;
}
}
finally
{
// disable further silent error processing
if (!wasSilent)
{
eh.setSilent(false);
}
if (!wasLenientOffEnd)
{
setLenientOffEnd(false);
}
eh.handleForwardPending();
}
return eh.getErrorStatus();
}
/**
* Navigate to the previous record which meets the query criteria and
* retrieve it into its record buffer. Use the default substitution
* parameters and lock type.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the previous record to visit. If no
* record has yet been loaded into the buffer, retrieve the last record
* which meets the query criteria.
*
* @return <code>true</code> if query could retrieve the previous record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean previous()
{
return previous(getCurrentArgs(), lockType);
}
/**
* Navigate to the previous record which meets the query criteria and
* retrieve it into its record buffer. Use the default lock type, but
* override the default substitution parameters with those provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the previous record to visit. If no
* record has yet been loaded into the buffer, retrieve the last record
* which meets the query criteria.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the previous record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean previous(Object[] values)
{
return previous(values, lockType);
}
/**
* Navigate to the previous record which meets the query criteria and
* retrieve it into its record buffer. Use the default substitution
* parameters, but override the default lock type with that provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the previous record to visit. If no
* record has yet been loaded into the buffer, retrieve the last record
* which meets the query criteria.
*
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the previous record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean previous(LockType lockType)
{
return previous(getCurrentArgs(), lockType);
}
/**
* Navigate to the previous record which meets the query criteria and
* retrieve it into its record buffer. Override both the default
* substitution parameters, and the default lock type with those provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the previous record to visit. If no
* record has yet been loaded into the buffer, retrieve the last record
* which meets the query criteria.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the previous record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found and <code>lenientOffEnd</code> is
* <code>false</code>.
*/
public boolean previous(Object[] values, LockType lockType)
{
if (!prepareFetch())
{
return false;
}
if (outer)
{
// reset now the helper. Another one will be created if any of the arguments are null.
// Otherwise, the not-null helper will be used if already constructed.
helper = null;
}
if (getHelper().getFQLPreprocessor().isFindByRowid())
{
// if the record was found in a previous iteration, raise the OFF-END exception
if (recordAlreadyFound)
{
updateBuffer(null, lockType, false, OffEnd.FRONT);
referenceRecord = null;
// this is required to always be throw for a 'by-rowid' query, be it a FOR EACH or a FIND.
// previously AdaptiveQuery$DynamicResults was setting 'lenientOffEnd' flag at the RAQ field,
// but the read was done via the getter at AbstractQuery, so the wrong value was read, which
// was always false.
throw new QueryOffEndException("", null);
}
// direct record access using recid/rowid is behaving like unique for all search types
boolean avail = unique(values, lockType);
if (avail)
{
recordAlreadyFound = true;
}
return avail;
}
// If a lock type is not specified, assume the query's lock type.
if (lockType == null)
{
lockType = this.lockType;
}
Record dmo = null;
try
{
// If query is scrolling, check the cached result list first.
boolean scrolling = (cursor != null);
if (scrolling)
{
dmo = loadByValue(cursor.getPrevious(), lockType);
}
// If not scrolling or wasn't in the cached result list, perform the
// query and cache the result (if scrolling).
if (dmo == null)
{
OffEnd offEnd = buffer.getOffEnd(getHelper().getSortIndex(), inverseSorting || buffer.isOffEndReversible());
boolean isLast;
if (offEnd != OffEnd.NONE && // we have off-end for the *same* database index
!buffer.isAvailable())
{
isLast = true;
buffer.resetSnapshot();
}
else
{
isLast= offEnd == OffEnd.BACK ||
(buffer.getSnapshot() == null &&
referenceRecord == null &&
offEnd == OffEnd.FRONT);
}
dmo = findPrevious(values, lockType, true);
if (scrolling)
{
if (isLast)
{
cursor.addResultLast(getIDs(dmo));
}
else
{
cursor.addResultPrevious(getIDs(dmo));
}
}
}
// Store result in the record buffer, even if null.
OffEnd offEnd = (dmo == null ? OffEnd.FRONT : OffEnd.NONE);
updateBuffer(dmo, lockType, false, offEnd);
referenceRecord = dmo;
// Notify accumulators of an iteration.
accumulate();
if (dmo == null)
{
return false;
}
}
catch (QueryOffEndException exc)
{
if (!lenientOffEnd)
{
throw exc;
}
return false;
}
catch (ErrorConditionException exc)
{
// if the exception is set to force an override of silent error mode behavior, it is
// because we encountered a validation error while flushing a record from the buffer
// to make way for the query result; don't try to release/flush the invalid record
// again here
if (!exc.isForce())
{
buffer.release(false);
}
throw exc;
}
finally
{
// Reset dirty flag.
dirtyCopy = false;
}
return true;
}
/**
* Navigate to the previous record which meets the query criteria and
* retrieve it into its record buffer. Use the default lock type, but
* override the default substitution parameters with those provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the previous record to visit. If no
* record has yet been loaded into the buffer, retrieve the last record
* which meets the query criteria.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the previous record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean silentPrevious(Object[] values)
{
return silentPrevious(values, lockType);
}
/**
* Navigate to the previous record which meets the query criteria and
* retrieve it into its record buffer. Override both the default
* substitution parameters, and the default lock type with those provided.
* <p>
* Use the record most recently loaded into the backing buffer as the
* reference point when determining the previous record to visit. If no
* record has yet been loaded into the buffer, retrieve the last record
* which meets the query criteria.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the previous record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval.
* @throws QueryOffEndException
* if no record could be found.
*/
public boolean silentPrevious(Object[] values, LockType lockType)
{
ErrorHelper eh = getErrorHelper();
boolean wasSilent = eh.isSilent();
if (!wasSilent)
{
eh.setSilent(true);
}
boolean wasLenientOffEnd = isLenientOffEnd();
if (!wasLenientOffEnd)
{
setLenientOffEnd(true);
}
try
{
return previous(values, lockType);
}
catch (LegacyErrorException lex)
{
ErrorManager.processLegacyErrorException(lex, eh);
}
catch (ErrorConditionException err)
{
// normally we just eat the exception because it is being used to abort processing
// exactly where the error occurred, which will restart processing after the given
// code block; however, there is an exception for some deferred error processing use
// cases where we must bypass silent error mode (throwError(NumberedException, boolean))
if (err.isForce())
{
// don't honor silent mode, no suppression of errors here
throw err;
}
}
finally
{
// disable further silent error processing
if (!wasSilent)
{
eh.setSilent(false);
}
if (!wasLenientOffEnd)
{
setLenientOffEnd(false);
}
eh.handleForwardPending();
}
return eh.getErrorStatus();
}
/**
* Reload the record most recently loaded into the backing record buffer,
* if any. Use the default lock type.
*
* @return <code>true</code> if query could retrieve the current record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record reload.
*/
public boolean current()
{
return current(lockType);
}
/**
* Reload the record most recently loaded into the backing record buffer,
* if any. Override the default lock type.
*
* @param lockType
* New lock type to apply to the record.
*
* @return <code>true</code> if query could retrieve the current record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record reload, or the lock could
* not be transitioned to the requested type.
*/
public boolean current(LockType lockType)
{
if (!prepareFetch())
{
return false;
}
buffer.armCurrentChanged();
buffer.reload(lockType, isErrorIfNull());
buffer.updateCurrentChanged();
return true;
}
/**
* Navigate to a particular record which meets the query criteria and
* retrieve it into its record buffer. No more than one record may match
* the criteria. Use the default substitution parameters and lock type.
*
* @return <code>true</code> if query could retrieve the unique record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if more than
* one record is found which match the criteria.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean unique()
{
return unique(getCurrentArgs(), lockType);
}
/**
* Navigate to a particular record which meets the query criteria and
* retrieve it into its record buffer. No more than one record may match
* the criteria. Use the default lock type, but override the default
* substitution parameters with those provided.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the unique record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if more than
* one record is found which match the criteria.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean unique(Object[] values)
{
return unique(values, lockType);
}
/**
* Navigate to a particular record which meets the query criteria and
* retrieve it into its record buffer. No more than one record may match
* the criteria. Use the default substitution parameters, but override
* the default lock type with that provided.
*
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the unique record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if more than
* one record is found which match the criteria.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean unique(LockType lockType)
{
return unique(getCurrentArgs(), lockType);
}
/**
* Navigate to a particular record which meets the query criteria and
* retrieve it into its record buffer. No more than one record may match
* the criteria. Override both the default substitution parameters, and
* the default lock type with those provided.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the unique record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if more than
* one record is found which match the criteria.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean unique(Object[] values, LockType lockType)
{
if (!prepareFetch())
{
if (throwExceptionOnFatalError())
{
// the message is not actually used, but will make things easier when debugging
String msg = "Query aborted due a fatal error: " + ErrorManager.getErrorText(1).toStringMessage();
List<QueryOffEndListener> listeners = getOffEndListeners();
throw new QueryOffEndException(msg, listeners.toArray(new QueryOffEndListener[listeners.size()]));
}
else
{
buffer.errorNotOnFile();
return false;
}
}
if (outer)
{
// reset now the helper. Another one will be created if any of the arguments are null.
// Otherwise, the not-null helper will be used if already constructed.
helper = null;
}
FQLHelper cachedHelper = getHelper();
FQLPreprocessor fqlPreprocessor = cachedHelper.getFQLPreprocessor();
if (!buffer.isTemporary() &&
cachedHelper.isFindTemplate(values, buffer.getDMOImplementationClass()))
{
buffer.loadTemplateRecord(fqlPreprocessor.getFindByRowid(values));
referenceRecord = null; // reset reference record
dirtyCopy = false; // reset dirty flag
return true;
}
else if (fqlPreprocessor.isFindByRowid() && fqlPreprocessor.getFindByRowid(values) < 0)
{
// according to article P12127, looking for an illegal rowid is equivalent to releasing
// the buffer. Backstage, error 18 is printed in database log but this is not an effect
// we need to duplicate in P2J.
buffer.release(true);
referenceRecord = null; // reset reference record
dirtyCopy = false; // reset dirty flag
if (isErrorIfNull())
{
buffer.errorNotOnFile();
}
return false;
}
try
{
if (lockType == null)
{
lockType = this.lockType;
}
activateUnique();
Record dmo = execute(values, lockType, true, true);
finalizeFind(lockType, dmo, () -> buffer.errorNotOnFile(), true);
if (dmo == null)
{
return false;
}
}
catch (ErrorConditionException exc)
{
// if the exception is set to force an override of silent error mode behavior, it is
// because we encountered a validation error while flushing a record from the buffer
// to make way for the query result; don't try to release/flush the invalid record again here
if (!exc.isForce())
{
buffer.release(false);
}
throw exc;
}
return true;
}
/**
* Navigate to a particular record which meets the query criteria and
* retrieve it into its record buffer. No more than one record may match
* the criteria. Use the default lock type, but override the default
* substitution parameters with those provided.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
*
* @return <code>true</code> if query could retrieve the unique record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if more than
* one record is found which match the criteria.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean silentUnique(Object[] values)
{
return silentUnique(values, lockType);
}
/**
* Navigate to a particular record which meets the query criteria and
* retrieve it into its record buffer. No more than one record may match
* the criteria. Override both the default substitution parameters, and
* the default lock type with those provided.
* <p>
* This operation is executed in silent mode.
*
* @param values
* Substitution values to use when issuing FQL queries.
* @param lockType
* Lock type to acquire for the retrieved record.
*
* @return <code>true</code> if query could retrieve the unique record successfully;
* <code>false</code> if unsuccessful and <code>lenientOffEnd</code> is set on true.
*
* @throws ErrorConditionException
* if an error occurred during record retrieval or if more than
* one record is found which match the criteria.
* @throws QueryOffEndException
* if no record could be found when operating in compound query
* mode.
*/
public boolean silentUnique(Object[] values, LockType lockType)
{
ErrorHelper eh = getErrorHelper();
boolean wasSilent = eh.isSilent();
if (!wasSilent)
{
eh.setSilent(true);
}
boolean wasLenientOffEnd = isLenientOffEnd();
if (!wasLenientOffEnd)
{
setLenientOffEnd(true);
}
try
{
return unique(values, lockType);
}
catch (LegacyErrorException lex)
{
ErrorManager.processLegacyErrorException(lex, eh);
}
catch (ErrorConditionException err)
{
// normally we just eat the exception because it is being used to abort processing
// exactly where the error occurred, which will restart processing after the given
// code block; however, there is an exception for some deferred error processing use
// cases where we must bypass silent error mode (throwError(NumberedException, boolean))
if (err.isForce())
{
// don't honor silent mode, no suppression of errors here
throw err;
}
}
finally
{
// disable further silent error processing
if (!wasSilent)
{
eh.setSilent(false);
}
if (!wasLenientOffEnd)
{
setLenientOffEnd(false);
}
eh.handleForwardPending();
}
return eh.getErrorStatus();
}
/**
* Set the buffer backing the query to unknown mode.
*
* @see RecordBuffer#setUnknownMode
*/
public void setUnknownRecord()
{
buffer.setUnknownMode();
}
/**
* Clear the current results from the query, so the same query instance can be executed again, without
* having to {@link #open() re-open} it.
*/
@Override
public void clearResults()
{
super.clearResults();
reset(true, true);
}
/**
* Reset the query by clearing its state to a known default. This is
* used to ensure that subsequent requests for FIRST and LAST record
* retrievals operate on a known query/buffer state.
*
* @param resolveArgs
* <code>true</code> if query substitution arguments should be
* resolved as part of the reset; <code>false</code> if existing
* argument values should be used for the next query execution.
* @param forceOrigArgs
* Flag indicating that the original arguments used at the construction of the query must be
* re-calculated.
*/
@Override
public void reset(boolean resolveArgs, boolean forceOrigArgs)
{
if (resolveArgs || forceOrigArgs)
{
resolveArgs(forceOrigArgs);
}
referenceRecord = null;
buffer.reset();
if (cursor != null)
{
cursor.reset();
}
if (helper != null &&
helper.getFQLPreprocessor() != null &&
helper.getFQLPreprocessor().wasInlined())
{
helper = null;
return;
}
}
/**
* Get the number of tables joined by this query.
*
* @return Always 1.
*/
public int getTableCount()
{
return 1;
}
/**
* Get all the off-end listeners associated with this query.
*
* @return the off-end listeners associated with this query.
*/
public ArrayList<QueryOffEndListener> getOffEndListeners()
{
ArrayList<QueryOffEndListener> listeners = new ArrayList<>(1);
listeners.add(buffer.getQueryOffEndListener());
return listeners;
}
/**
* Load a record into its associated buffer, given an array of size one,
* containing a primary key ID value of the record to be loaded, or the DMO
* itself.
* <p>
* In the event an expected value cannot be loaded (e.g., the record has
* been deleted or is otherwise no longer available), this method throws
* {@link MissingRecordException}, after setting the associated buffer
* into {@link RecordBuffer#setUnknownMode() unknown mode}. Note that a
* record being locked by another session does not constitute a "missing"
* record.
*
* @param data
* Array containing a single primary key ID or DMO.
* @param lockType
* Lock type which should by used. Set to <code>null</code> to
* allow query to use its current lock type.
* @param silentIfNullId
* If <code>true</code>, do not raise {@link MissingRecordException}
* if some of the provided IDs are <code>null</code>. <code>null</code>
* IDs are valid for queries with OUTER join.
*
* @throws MissingRecordException
* if any record cannot be loaded, because it is no longer available
* (e.g., deleted, etc.).
* @throws PersistenceException
* if there is an error loading data.
*
* @see #getRow()
*/
public void load(Object[] data, LockType lockType, boolean silentIfNullId)
throws PersistenceException
{
try
{
LockType lt = (lockType != null ? lockType : this.lockType);
verifyScrolling();
Long[] ids = new Long[1];
Object datum = data[0];
ids[0] = datum instanceof Record ? ((Record) datum).primaryKey() : (Long) datum;
if (cursor.reposition(ids, false))
{
cursor.next();
}
Record dmo = loadByValue(data, lt, silentIfNullId);
OffEnd offEnd = (dmo == null ? OffEnd.BACK : OffEnd.NONE);
updateBuffer(dmo, lt, silentIfNullId, offEnd);
if (dmo == null)
{
buffer.setUnknownMode();
if (!(datum == null && silentIfNullId))
{
String table = buffer.getTable();
RecordIdentifier<String> ident = new RecordIdentifier<>(table, ids[0]);
throw new MissingRecordException(ident);
}
}
}
finally
{
// Reset dirty flag.
dirtyCopy = false;
}
}
/**
* Set the given results object into this query. Only works if cursor is not null.
*
* @param results
* New result set for this query.
*/
public void setResults(Results results)
{
if (cursor != null)
{
cursor.reset();
results.reset();
while (results.next())
{
cursor.addResultNext(results.get());
}
results.reset();
}
}
/**
* Assemble an array of size one and store in it the primary key ID for
* the current record in the buffer underlying this query. This method
* essentially takes a lightweight snapshot of the currently loaded
* record, so that this may be restored later.
*
* @return Array containing a single primary key ID.
*
* @see #load
*/
public Object[] getRow()
{
Record dmo = referenceRecord != null ? referenceRecord : buffer.getSnapshot();
if (dmo == null && !buffer.isUnknownMode())
{
throw new NullPointerException("No reference record available");
}
return (new Object[] { dmo });
}
/**
* Fetch the array of primary key IDs associated with the first result in
* this query's result list.
* <p>
* This implementation always returns an array with a single element,
* (or <code>null</code>), since this query type uses a single record
* buffer.
*
* @return Array of record IDs reflecting the first query result, or
* <code>null</code> if there is no such result.
*/
public Object[] peekFirst()
{
if (!prepareFetch())
{
return null;
}
buffer.release(false);
activateFirst();
referenceRecord = execute(getCurrentArgs(), LockType.NONE, false, false);
if (referenceRecord == null)
{
return null;
}
return new Object[] { referenceRecord };
}
/**
* Fetch the array of primary key IDs associated with the last result in
* this query's result list.
* <p>
* This implementation always returns an array with a single element,
* (or <code>null</code>), since this query type uses a single record
* buffer.
*
* @return Array of record IDs reflecting the last query result, or
* <code>null</code> if there is no such result.
*/
public Object[] peekLast()
{
if (!prepareFetch())
{
return null;
}
buffer.release(false);
activateLast();
referenceRecord = execute(getCurrentArgs(), LockType.NONE, false, false);
if (referenceRecord == null)
{
return null;
}
return new Object[] { referenceRecord };
}
/**
* Fetch the array of primary key IDs associated with the next result in
* this query's result list.
* <p>
* This implementation always returns an array with a single element,
* (or <code>null</code>), since this query type uses a single record
* buffer.
*
* @return Array of record IDs reflecting the next query result, or
* <code>null</code> if there is no such result.
*/
public Object[] peekNext()
{
if (!prepareFetch())
{
return null;
}
buffer.release(false);
referenceRecord = findNext(getCurrentArgs(), LockType.NONE, false);
if (referenceRecord == null)
{
return null;
}
return (new Object[] { referenceRecord });
}
/**
* Fetch the array of primary key IDs associated with the previous result
* in this query's result list.
* <p>
* This implementation always returns an array with a single element,
* (or <code>null</code>), since this query type uses a single record
* buffer.
*
* @return Array of record IDs reflecting the previous query result, or
* <code>null</code> if there is no such result.
*/
public Object[] peekPrevious()
{
if (!prepareFetch())
{
return null;
}
buffer.release(false);
referenceRecord = findPrevious(getCurrentArgs(), LockType.NONE, false);
if (referenceRecord == null)
{
return null;
}
return (new Object[] { referenceRecord });
}
/**
* Get an array of all record buffers managed by this query.
*
* @return All record buffers managed by this query.
*/
@Override
public RecordBuffer[] getRecordBuffers()
{
return (new RecordBuffer[] { buffer });
}
/**
* Get the query substitution arguments associated with this query, if any.
*
* @return Substitution arguments, in order, or <code>null</code> if there are none.
*/
@Override
public Object[] getArgs()
{
return dfArgs;
}
/**
* Get the legacy natural join, if any, associated with this query.
*
* @return Legacy join object, or <code>null</code> if none.
*/
@Override
public AbstractJoin getJoin()
{
return join;
}
/**
* Get the original, FQL where clause associated with this query.
*
* @return Original where clause, or <code>null</code> if there is none.
*/
@Override
public String getOriginalWhere()
{
return dfWhere;
}
/**
* Force this query to operate in dynamic retrieval mode, if it supports this mode.
* <p>
* This implementation does nothing, since this query type always is in dynamic retrieval
* mode.
*/
@Override
public void forceDynamicOperation()
{
// no-op
}
/**
* Get the external buffers, if any, associated with this query.
*
* @return Array of external buffers or {@code null}.
*/
@Override
public RecordBuffer[] getExternalBuffers()
{
return super.getExternalBuffers();
}
/**
* Get the sort phrase associated with this query.
*
* @return Sort phrase.
*/
public String getSortPhrase()
{
return sort;
}
/**
* Create an adaptive query component based on the information in this query (which is
* presumed to be a single-table query), and given a list of adaptive query components which
* represent outer, nested query loops, which will perform a server-side join to the
* innermost, nested query component.
*
* @param joinList
* List of adaptive components representing nested query loops which will contain
* the adaptive query component returned by this method. The last component in the
* list represents the nested query loop immediately containing the returned query
* component, with which the join will be made.
* @param query
* Adaptive query which will manage the server-side join of <code>joinList</code> and
* the returned query component.
* @param iteration
* Iteration type: FIRST, LAST, or NEXT.
* @param outer
* <code>true</code> if the join should be a left outer join. This type of join is
* not supported at the time of this writing, so this parameter should always be
* <code>false</code>. It is here to support a planned, future enhancement.
* @param fallback
* A compound query component to use in the event the multi-table adaptive query
* switches from preselect to dynamic retrieval mode.
*
* @return An adaptive query component suitable for a server-side join, based on the
* information in this query.
*/
@Override
public AdaptiveComponent makeAdaptiveServerJoinComponent(List<AdaptiveComponent> joinList,
AdaptiveQuery query,
int iteration,
boolean outer,
CompoundComponent fallback)
{
// NOTE: dynamic components must be already processed (dfWhere and dfArgs configured)
QueryComponent.ServerJoinData sjd =
QueryComponent.prepareServerJoinData(joinList, buffer, join, dfArgs);
BufferReference inverse = (join != null ? join.getInverse() : null);
AdaptiveComponent comp = new AdaptiveComponent(query,
buffer,
sjd.join,
dfWhere,
null,
sort,
indexInfo,
lockType,
sjd.args,
iteration,
outer,
inverse);
comp.setReferenceSubs(sjd.referenceSubs);
// gather all related buffers, including [relatedBuffers] from [sjd] and eventual external
// buffers from original query component
Set<RecordBuffer> relatedBuffers = new HashSet<>();
if (sjd.relatedBuffers != null)
{
relatedBuffers.addAll(sjd.relatedBuffers);
}
RecordBuffer[] externalBuffers = fallback.getQuery().getExternalBuffers();
if (externalBuffers != null && externalBuffers.length != 0)
{
relatedBuffers.addAll(Arrays.asList(externalBuffers));
}
if (!relatedBuffers.isEmpty())
{
comp.setRelatedBuffers(relatedBuffers);
}
comp.setFallback(fallback);
if (!joinList.isEmpty())
{
comp.setNotTop();
}
if (iteration == FIRST || iteration == LAST)
{
try
{
comp.setIteration(iteration, sort);
}
catch (PersistenceException exc)
{
// should only happen if we have a runtime programming error or conversion error
// (or with bad, hand-written business logic)
throw new IllegalArgumentException(exc);
}
}
return comp;
}
/**
* Create a preselect query component based on the information in this query (which is
* presumed to be a single-table query), and given a list of preselect query components which
* represent outer, nested query loops, which will perform a server-side join to the
* innermost, nested query component.
*
* @param joinList
* List of preselect components representing nested query loops which will contain
* the preselect query component returned by this method. The last component in the
* list represents the nested query loop immediately containing the returned query
* component, with which the join will be made.
* @param iteration
* Iteration type: FIRST, LAST, or NEXT.
* @param outer
* <code>true</code> if the join should be a left outer join. This type of join is
* not supported at the time of this writing, so this parameter should always be
* <code>false</code>. It is here to support a planned, future enhancement.
*
* @return A preselect query component suitable for a server-side join, based on the
* information in this query.
*/
@Override
public QueryComponent makePreselectServerJoinComponent(List<QueryComponent> joinList,
int iteration,
boolean outer)
{
// NOTE: dynamic components must be already processed (dfWhere and dfArgs configured)
QueryComponent.ServerJoinData sjd =
QueryComponent.prepareServerJoinData(joinList, buffer, join, dfArgs);
QueryComponent comp = new QueryComponent(this,
buffer,
sjd.join,
dfWhere,
null,
indexInfo,
lockType,
sjd.args,
iteration,
outer);
comp.setReferenceSubs(sjd.referenceSubs);
comp.setRelatedBuffers(sjd.relatedBuffers);
if (iteration == FIRST || iteration == LAST)
{
try
{
comp.setIteration(iteration, sort);
}
catch (PersistenceException exc)
{
// should only happen if we have a runtime programming error or conversion error
// (or with bad, hand-written business logic)
throw new IllegalArgumentException(exc);
}
}
return comp;
}
/**
* Indicate whether this query has a client-side where clause expression associated with it.
*
* @return <code>true</code> if there is a where expression; else <code>false</code>.
*/
@Override
public boolean hasWhereExpression()
{
return (whereExpr != null);
}
/**
* Report the record buffer for whose changes this object is interested
* in listening. This listener will be registered with the {@link
* ChangeBroker} to receive notifications of any change to DMOs whose
* interface types match that returned by the {@link
* RecordBuffer#getDMOInterface} method of the returned buffer.
*
* @return An iterator on a single object: the record buffer backing this query.
*/
public Iterator<RecordBuffer> recordBuffers()
{
Iterator<RecordBuffer> iter = new Iterator<RecordBuffer>()
{
private boolean avail = true;
public boolean hasNext() { return avail; }
public RecordBuffer next() { avail = false; return buffer; }
public void remove() { throw new UnsupportedOperationException(); }
};
return iter;
}
/**
* Register all <code>RecordChangeListener</code>s associated with this
* query (if any), with the context-local <code>ChangeBroker</code> at the
* indicated scope.
*
* @param scope
* Scope at which listeners should be registered with the change broker.
*/
@Override
public void registerRecordChangeListeners(Integer scope)
{
ChangeBroker broker = ChangeBroker.get();
if (scope == null)
{
broker.addListener(this);
}
else
{
broker.addListener(this, scope);
}
if (placeholderCleaner != null)
{
if (scope == null)
{
broker.addListener(placeholderCleaner);
}
else
{
broker.addListener(this, scope);
}
}
// if these were added as global listeners in ChangeBroker remember to remove them
// when the query is destroyed. Otherwise, the ChangeBroker will drop them automatically
// when the respective scope is finished.
if (scope != null && scope == 0)
{
unregisterOnCleanup = true;
}
}
/**
* Explicitly close the prepared query.
*/
@Override
public void close()
{
close(false);
}
/**
* Explicitly close the prepared query.
*
* @param inResource
* Flag indicating this query is used in a QUERY legacy resource.
*/
@Override
public void close(boolean inResource)
{
if (inResource && buffer == null)
{
// nothing to close, the query was not initialized yet.
// This happens only for dynamic queries, when the query was explicitly closed or
// re-prepared before having the chance to be fully initialized with query-open
return;
}
if (inResource && !unregisterOnCleanup)
{
// remove it explicitly
ChangeBroker broker = ChangeBroker.get();
broker.removeListener(this);
}
super.close();
}
/**
* The cleanup method is overridden to allow the listener(s) to be removed from the
* {@code ChangeBroker} if they were added in the global scope, otherwise, the
* {@code ChangeBroker} will drop them automatically when the respective scope is finished.
*/
@Override
public void cleanup()
{
recordAlreadyFound = false;
if (unregisterOnCleanup)
{
ChangeBroker broker = ChangeBroker.get();
broker.removeFromGlobal(this);
if (placeholderCleaner != null)
{
broker.removeFromGlobal(placeholderCleaner);
}
unregisterOnCleanup = false;
}
// let the super class do the rest of the cleanup
super.cleanup();
}
/**
* Respond to a record change event.
*
* @param event
* Event which describes the DMO state change.
*
* @throws PersistenceException
* if any error occurs while processing the state change event.
*/
public void stateChanged(RecordChangeEvent event)
throws PersistenceException
{
if (cursor == null)
{
return;
}
if (event.isInsert())
{
cursor.reset();
return;
}
if (event.isDelete())
{
int idx = -1;
RecordBuffer source = event.getSource();
Iterator<RecordBuffer> iter = recordBuffers();
for (int i = 0; iter.hasNext() && idx < 0; i++)
{
if (source == iter.next())
{
idx = i;
}
}
// Reset the cache if it contains a reference to the deleted DMO.
if (idx >= 0 && cursor.contains(event.getDMO().primaryKey(), idx))
{
cursor.reset();
}
}
}
/**
* Get the off-end status of this query, which indicates whether the query has run off either
* end of its results.
*
* @return One of the {@link OffEnd} constants {@code NONE}, {@code FRONT}, or {@code BACK}.
*/
public OffEnd getOffEnd()
{
if (helper == null)
{
return OffEnd.NONE;
}
SortIndex si = helper.getSortIndex();
return buffer.getOffEnd(si, inverseSorting);
}
/**
* Conversion of INDEX-INFORMATION attribute (KW_IDX_INFO).
*
* Getter of INDEX-INFORMATION attribute
*
* @return A character string consisting of a comma-separated list of the index or
* indexes the query uses at the level of join specified.
*/
@LegacyAttribute(name = "INDEX-INFORMATION")
public character indexInformation(NumberType n)
{
return new character(indexInfo);
}
/**
* Release the current record, if any, from the buffer which is used by
* this query.
*
* @throws QueryOffEndException
* if the buffer contained a record and it was released.
*/
public void releaseBuffers()
{
// this method is called in the context of this query being a component in an iterating
// query, so do not allow write trigger to fire
buffer.release(false);
}
/**
* Adds a new constraint term to the where predicate of this query, effectively filtering out
* any rows whose referenced field does not match the requested value. Semantically, the query
* will select rows that match
* <p>
* {@code new-where := (fr = val) AND (old-query)}.
* <p>
* Note: The constraints are exclusive meaning that if constraints will be added for same field of same
* buffer, the query will return an empty result because the field cannot be at the same time equals
* to two different values.
* <p>
* Use {@link #clearDynamicFilters} to clean all dynamically added criteria and restore the
* sorting order of the query to its original form.
*
* @param fr
* A reference to field used as filtering criterion. Only the rows that matches the
* specified value for this field will be selected. Must not be {@code null}.
* @param val
* The filtering value for this constraint. Can be {@code null} or {@code unknown}, in
* which case the SQL {@code null} value will be assumed. In this case the predicate
* will look like:
* <p>
* {@code new-where := (fr IS NULL) AND (old-query)}
* @param format
* The format to be used when comparing values. The reason for this parameter is that
* for some datatype (ex: {@code decimal}) the on-screen value can be different from
* the database (because of rounding).
*
* @return {@code true} if the filtering constraint was successfully added. If the field
* reference is not related to the buffer(s) of this query, {@code false} is returned.
*/
@Override
public boolean addDynamicFilter(FieldReference fr, BaseDataType val, String format)
{
if (fr.getParentBuffer() != getBuffer())
{
return false; // not my buffer
}
if (dynamicFilters == null)
{
dynamicFilters = new LinkedHashMap<>(5);
dynamicFormats = new HashMap<>(5);
}
dynamicFilters.remove(fr);
dynamicFilters.put(fr, val);
dynamicFormats.put(fr, format);
StringBuilder sb = new StringBuilder();
List<Object> dArgs = new ArrayList<>(dynamicFilters.size());
Iterator<Map.Entry<FieldReference, BaseDataType>> dynamicFilters =
this.dynamicFilters.entrySet().iterator();
while (dynamicFilters.hasNext())
{
Map.Entry<FieldReference, BaseDataType> filter = dynamicFilters.next();
BaseDataType bdtVal = filter.getValue();
String fieldName = filter.getKey().toString();
boolean isNull = (bdtVal == null) || bdtVal.isUnknown();
boolean isCharType = bdtVal instanceof Text;
sb.append("(");
if (isNull)
{
sb.append(fieldName).append(" is null");
}
else
{
if (isCharType)
{
// the user will not know the case-sensitivity of the database fields, so it is
// best to make the filter for text fields always be case-insensitive. This has the
// greatest likelihood of using an index (most fields are case-insensitive), and it
// is the most user-friendly.
sb.append("upper(trim(").append(fieldName).append("))").append(" like ?");
String pattern = TextOps.trim(TextOps.toUpperCase((Text) bdtVal)).getValue();
bdtVal = new character("%" + pattern + "%");
}
else if (bdtVal instanceof decimal)
{
// because of rounding, the displayed value may not be equals to the one from
// database. To make sure we filter the right rows, we will use the format matching
// the one used by shown data
String fmt = dynamicFormats.get(filter.getKey());
if (fmt != null)
{
sb.append("toString(").append(fieldName).append(",'").append(fmt).append("')= ?");
bdtVal = new character(bdtVal.toString(fmt));
}
else
{
// format not provided, we cannot use it, compare directly instead
sb.append(fieldName).append(" = ?");
}
}
else
{
sb.append(fieldName).append(" = ?");
}
dArgs.add(bdtVal);
}
if (dynamicFilters.hasNext() || where != null)
{
sb.append(") and ");
}
else
{
sb.append(")");
}
}
if (where != null)
{
sb.append("(").append(where).append(")");
}
dfWhere = sb.toString();
int dArgCount = dArgs.size();
if (args != null && dArgCount != 0)
{
dfArgs = new Object[args.length + dArgCount];
System.arraycopy(dArgs.toArray(), 0, dfArgs, 0, dArgCount);
System.arraycopy(args, 0, dfArgs, dArgCount, args.length);
}
else if (dArgCount != 0)
{
dfArgs = new Object[dArgCount];
dArgs.toArray(dfArgs);
}
else
{
dfArgs = null;
}
helper = null;
nonNullArgsHelper = null;
return true;
}
/**
* Clears any dynamically filters added at runtime. The query predicate will be restored to its
* form from generated at conversion time.
*/
@Override
public void clearDynamicFilters()
{
if (dynamicFilters == null)
{
return;
}
// restore the where predicate and parameters:
dfWhere = where;
dfArgs = args;
// cleanup and prepare for re-initialization
dynamicFilters = null;
dynamicFormats = null;
helper = null;
nonNullArgsHelper = null;
dynamicFilterArgs = null;
}
/**
* Add one or more buffers to the list of those which this query references externally. These typically
* will be buffers referenced by converted CAN-FIND expressions embedded in the query's FQL expression,
* which are not the primary buffers associated with the query.
* <p>
* If the bound buffer differs from the definition buffer (see {@link #buffersMatch}) for any of the
* external buffers, then the {@link #translateWhere} will be enabled (if not already enabled).
*
* @param dmos
* One or more external buffers.
*
* @return This query instance.
*/
@Override
public P2JQuery addExternalBuffers(DataModelObject... dmos)
{
super.addExternalBuffers(dmos);
if (translateWhere != null)
{
// the translation is already registered, external buffers will be automatically picked up
return this;
}
RecordBuffer[] buffers = getExternalBuffers();
if (buffers == null)
{
return this;
}
RecordBuffer[] defs = getExternalBuffersDefs();
boolean translate = false;
for (int i = 0; i < buffers.length && !translate; i++)
{
translate = !buffersMatch(buffers[i], defs[i]);
}
if (translate)
{
translateWhere = this::getTranslatedWhere;
}
return this;
}
/**
* Adds an alternative execution query, for the multi-tenant case in which the CAN-FIND nested buffer and
* the main buffer of query component do not belong to same physical database.
* <p>
* This method will be generated only in case of multi-tenant projects, in the condition above-mentioned.
*
* @param where
* FQL where clause. May be {@code null}.
* @param whereExpr
* Client-side where clause expression. May be {@code null} but this is probably caused by an
* incorrect conversion.
* @param sort
* The sort order.
*
* @return Self object so that the call to this method can be optionally chained with the constructor.
*/
@Override
public RandomAccessQuery setMultiTenantAlternative(String where, Supplier<logical> whereExpr, String sort)
{
return setMultiTenantAlternative(where, whereExpr, sort, null);
}
/**
* Adds an alternative execution query, for the multi-tenant case in which the CAN-FIND nested buffer and
* the main buffer of query component do not belong to same physical database.
* <p>
* This method will be generated only in case of multi-tenant projects, in the condition above-mentioned.
*
* @param where
* FQL where clause. May be {@code null}.
* @param whereExpr
* Client-side where clause expression. May be {@code null} but this is probably caused by an
* incorrect conversion.
* @param sort
* The sort order.
* @param args
* Substitution parameters for FQL queries. These will be used if not overridden by a record
* retrieval method.
*
* @return Self object so that the call to this method can be optionally chained with the constructor.
*/
@Override
public RandomAccessQuery setMultiTenantAlternative(String where,
Supplier<logical> whereExpr,
String sort,
Object[] args)
{
RecordBuffer[] buffers = getExternalBuffers();
if (buffers == null || buffers.length == 0)
{
return this; // no-op. This should not happen
}
Persistence.Context privateContext = buffer.getPersistence().getContext(Persistence.PRIVATE_CTX);
if (privateContext.getTenantId() == TenantManager.DEFAULT_TENANT_ID)
{
// running as default tenant, the query is intra-database (using the default database)
return this;
}
boolean isMt = buffer.getDmoInfo().multiTenant;
for (RecordBuffer eb : buffers)
{
if (eb.getDmoInfo().multiTenant != isMt)
{
// if the query is cross-physical database, re-initialize with the alternative execution path
return initialize(buffer, where, whereExpr, sort, null, null, args, lockType);
}
}
// if we reach this point, all table are either public or all private, so the optimised query can be
// safely executed
return this;
}
/**
* Create and return the FQL of this query.
*
* This function is used for logging purposes.
*
* @return String
* The FQL of the query.
*/
public String getBundleFql()
{
if (activeBundleFql != null)
{
return activeBundleFql;
}
if (activeBundle == null)
{
return null;
}
Iterator<String> fqls = activeBundle.statements();
if (fqls == null || !fqls.hasNext())
{
return null;
}
StringBuilder fql = new StringBuilder();
while (true)
{
fql.append(fqls.next());
if (fqls.hasNext())
{
fql.append(", ");
}
else
{
break;
}
}
activeBundleFql = fql.toString();
return activeBundleFql;
}
/**
* Set the included fields.
*
* @param included
* The included properties for each DMO (FIELDS option).
*/
public void overrideIncludeFields(Map<DataModelObject, Property[]> included)
{
this.included = included;
}
/**
* Set the excluded fields.
*
* @param excluded
* The excluded properties for each DMO (EXCEPT option).
*/
public void overrideExcludeFields(Map<DataModelObject, Property[]> excluded)
{
this.excluded = excluded;
}
/**
* Prepare to perform a fetch. This involves checking for a pending error
* in the error manager and releasing the current buffer record if an
* error is pending. Releasing the record may result in an exception
* being thrown by the record buffer.
*
* @return <code>false</code> if an error is pending, else
* <code>true</code>.
*/
@Override
protected boolean prepareFetch()
{
translateWhere();
return super.prepareFetch();
}
/**
* Prepare buffer associated with this query for the query's execution.
* This resets certain per-query state tracked by the record buffer.
*
* @throws ValidationException
* if the parent's implementation is invoked and a buffer flush
* triggers a validation error for the record currently stored in
* the buffer.
*/
protected void prepareBuffer()
throws ValidationException
{
buffer.flush();
buffer.setLocked(false);
}
/**
* Update the record buffer with the given DMO, applying the specified
* lock type and error-if-null setting. This implementation causes the
* record buffer's internal placeholder snapshot to be reset if
* <code>dmo</code> is null.
*
* @param dmo
* Record to be stored in buffer.
* @param lockType
* Lock type associated with the record, which will be remembered
* by the buffer.
* @param errorIfNull
* If <code>true</code>, setting a <code>null</code> value as the
* current record in an underlying buffer will raise an error
* condition (if not in silent error mode); if <code>false</code>,
* this action will raise an end condition instead.
* @param offEnd
* Enum indicating whether query is off-end, and if so, in which
* direction.
*/
protected void updateBuffer(Record dmo,
LockType lockType,
boolean errorIfNull,
OffEnd offEnd)
{
SortIndex sortIndex = getHelper().getSortIndex();
buffer.setRecord(dmo,
lockType,
errorIfNull,
lenientOffEnd,
sortIndex,
offEnd,
inverseSorting,
dirtyCopy,
true);
Buffer bufferImpl = (Buffer) buffer.getDMOProxy();
if (bufferImpl.autoSynchronize().booleanValue())
{
bufferImpl.querySynchronize();
}
}
/**
* Return an object which represents a property value which defines the
* end of a <i>sort band</i>. A <i>sort band</i> is a series of one or
* more records which share the same value for a particular property,
* where that property is used as the coarsest sorting criterion defined
* by this query's <code>order by</code> clause.
* <p>
* If the break value for this query currently is non-<code>null</code>,
* this indicates that the most recently retrieved record has crossed a
* <i>sort band</i> boundary, and that a new band has begun. This value
* will only be non-<code>null</code> at the point at which such a record
* is found, and will be <code>null</code> for all other records.
* <p>
* Note: break values are only tracked for invocations of
* <code>next</code> and <code>previous</code>. Other retrievals will
* results in break value being <code>null</code>.
*
* @return Break value if dynamic query crossed a sort band boundary,
* else <code>null</code>.
*/
protected Object getBreakValue()
{
return breakValue;
}
/**
* Get the record buffer associated with this query.
*
* @return Record buffer.
*/
protected RecordBuffer getBuffer()
{
return buffer;
}
/**
* Retrieve this query's arguments, as currently resolved.
*
* @return Current arguments.
*/
protected Object[] getCurrentArgs()
{
return currentArgs;
}
/**
* Retrieve this query's lock type.
*
* @return Lock type
*/
protected LockType getLockType()
{
return lockType;
}
/**
* Indicate whether a nested query error should be suppressed (i.e., reported only as a warning, but not
* interrupt control flow), when detected while preprocessing this query's substitution parameters.
*
* @return {@code true}, when this query is used outside of the context of a converted FIND statement.
*/
protected boolean suppressNestedQueryError()
{
return true;
}
/**
* Execute each query stored in the active FQL bundle in turn until we
* find a result or have no more statements to execute. Each statement is
* decreasingly specific.
* <p>
* The latest record to be stored in the record buffer, if any, marks our
* current position in the query's results. Its data is used to substitute
* into the placeholder parameters in the query statements' augmented where
* clauses. These are added to the <code>values</code> provided as part of
* the base query.
* <p>
* <b>Client-Side Where Clause Handling</b><br>
* If this query involves a client-side where clause expression, an
* additional layer of criteria checking takes place. In addition to the
* search described above, each returned result is temporarily stored in
* the associated record buffer, and the where expression is executed.
* If the expression indicates a match, the record is returned. Otherwise,
* additional records are retrieved and tested until either a match is
* found or no more records are available in the requested navigation
* direction.
* <p>
* Unique queries combined with client-side where clauses require special
* handling and may perform particularly poorly as a result. In this
* case, every record returned by the first-pass query must be tested
* against the client-side where expression, even once a result has been
* found. This must be done to ensure <i>only one</i> record matches the
* specified criteria. The only exception is the case where a second
* record is actually found, in which case the uniqueness requirement has
* been violated, and the remainder of the scan is aborted due to the
* error.
*
* @param values
* Array of substitution parameter values for the base where
* clause (i.e., the un-augmented portion of the clause).
* @param lockType
* Type of lock to acquire for the found record.
* @param unique
* <code>true</code> if there should be no more than one match
* for the conditions specified; <code>false</code> if multiple
* matches are possible.
* @param updateLock
* <code>true</code> if the status of the lock on the retrieved
* record should be modified; else <code>false</code>. This is
* set to <code>true</code> for actual retrievals, and to
* <code>false</code> when only detecting whether a record would
* be found, or when peeking a record.
*
* @return The record resulting from the query, or <code>null</code> if
* no record was found.
*
* @throws ErrorConditionException
* if a recoverable error occurred while executing.
*/
protected Record execute(Object[] values, LockType lockType, boolean unique, boolean updateLock)
{
if (buffer.isTemporary() && buffer.referenceOnly() && buffer.getMultiplexID() == null)
{
BufferReference proxy = buffer.getDMOProxy();
if (((BufferImpl) proxy).ref() == proxy)
{
// not bound?
((AbstractTempTable) buffer.getParentTable()).displayUninitialized();
return null;
}
}
// if the JOINed buffer has no record, then we must return NULL
if (join != null)
{
Record record = join.getInverse().buffer().getCurrentRecord();
if (record == null)
{
return null;
}
}
if (whereExpr == null)
{
FQLPreprocessor fqlPreproc = helper.getFQLPreprocessor();
RecordIdentifier<String> cacheResult = null;
FastFindCache.Key key = null;
boolean simpleQuery = isSimpleQuery();
String entityName = buffer.getEntityName();
DirtyShareContext dirtyContext = buffer.getDirtyContext();
if (DirtyShareSupport.isEnabledCrossSession() &&
dirtyContext != null &&
fqlPreproc.getEarlyPublishEntities().contains(entityName))
{
try
{
// we only need to leak entities for the current buffer
List<Record> dmos = dirtyContext.updateSnapshots(Collections.singleton(entityName),
buffer.getPersistence());
for (Record dmo : dmos)
{
// as only current buffer entities are in scope we are save to refer it
ffCache.invalidate(buffer.getDmoInfo().getId(), buffer.getMultiplexID());
// Make sure DMOs do not bloat the ORM session.
buffer.evictDMOIfUnused(dmo);
}
}
catch (PersistenceException e)
{
// fall-back to execute method
}
}
// rule out find-by-rowid queries fast. If we already have the record in our session cache,
// avoid doing extra work on retrieving it from the database or ffcache.
if (fqlPreproc.isFindByRowid() && whereExpr == null && simpleQuery)
{
Session session = buffer.getSession(true);
try
{
Class<? extends Record> dmoClass = buffer.getDmoInfo().getImplementationClass();
Record dmo = session.getCached(dmoClass, fqlPreproc.getFindByRowid(values));
// If it is incomplete, let the ffCache or direct-access do its job faster
if (dmo != null && !dmo.checkState(DmoState.STALE) && !dmo.checkState(DmoState.INCOMPLETE))
{
RecordLockContext lockCtx = buffer.getRecordLockContext();
lockCtx.lock(new RecordIdentifier<>(buffer.getTable(), dmo.primaryKey()), lockType);
return dmo;
}
}
catch (PersistenceException e)
{
// fall-back to execute method
}
}
// attempt to get the result from fast-find cache, avoiding the expensive database trip.
// This is not possible and the cache lookup is skipped when:
// - in NEXT/PREVIOUS find modes
// - the query has a join
// - the query has an embedded CAN-FIND
// - the query has a client-side where expression
if (ffCache != null && simpleQuery)
{
BitSet properties = fqlPreproc.getQueryProperties();
key = FastFindCache.createKey(buffer, index, where, properties, activeBundleKey, values);
Session session = buffer.getSession(true);
cacheResult = ffCache.get(key);
if (cacheResult != null)
{
if (cacheResult == FastFindCache.NO_RECORD)
{
// record will not be found in database
return null;
}
else
{
try
{
// if we get a hit, get the requested lock with RecordLockContext
RecordLockContext lockCtx = buffer.getRecordLockContext();
lockCtx.lock(new RecordIdentifier<>(buffer.getTable(), cacheResult.getRecordID()),
lockType);
// the lock operation might have taken some time. Check whether the result still exists
// in the cache, now that we hold the lock
if (session != null && cacheResult.equals(ffCache.get(key)))
{
// the result is still in the cache, meaning no one changed the index(es) while we
// were acquiring the lock; it is safe to use:
try
{
Record dmo = session.get(cacheResult);
if (dmo != null)
{
return dmo;
}
}
catch (PersistenceException e)
{
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"Failed to get fast-find cached value for " + cacheResult,
e);
}
}
}
}
catch (LockUnavailableException exc)
{
// in case of a quick-return NO-WAIT lock request: do not throw now error condition 445:
// "XXX record is locked", let the normal executeImpl() handle it
}
}
// set result to null so the cache will be updated/refreshed with value from full executeImpl()
cacheResult = null;
}
}
Record record = null;
// use direct-access after ffCache failed
Optional<Record> directAccessResult = executeDirectAccess(values);
if (directAccessResult != null)
{
record = directAccessResult.isPresent() ? directAccessResult.get() : null;
}
else
{
try
{
record = P2JQueryExecutor.getInstance().execute(buffer.getDatabase(),
(String) null,
RandomAccessQuery::executeImpl,
this,
values,
lockType,
unique,
updateLock);
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
}
if (ffCache != null &&
(record == null || record.checkState(DmoState.CACHED)) &&
cacheResult == null && // NOTE: always null ?
key != null &&
!fqlPreproc.isDependsOnSessionAttribute() &&
!dirtyCopy)
{
// save the result if cacheable and result was not found in cache, even if null (not found)
ffCache.put(key, record);
}
return record;
}
Record dmo = null;
Record oldRefRec = referenceRecord;
FQLBundle bundle0 = activeBundle;
FQLBundle bundle1;
FQLBundle bundle2;
// prepare the FQL bundles which will be used in the iterative, first pass scan
FQLHelper cachedHelper = getHelper();
BitSet joinKey = getJoinKey();
switch (activeBundleKey)
{
case NEXT:
bundle1 = bundle2 = cachedHelper.next(joinKey);
break;
case PREVIOUS:
bundle1 = bundle2 = cachedHelper.previous(joinKey);
break;
case FIRST:
case UNIQUE:
bundle1 = cachedHelper.first(joinKey);
bundle2 = cachedHelper.next(joinKey);
break;
case LAST:
bundle1 = cachedHelper.last(joinKey);
bundle2 = cachedHelper.previous(joinKey);
break;
default:
throw new IllegalStateException("No active FQL bundle");
}
Persistence persistence = buffer.getPersistence();
try
{
Record uniqueDMO = null;
buffer.pushTempContext();
for (int i = 0; ; i++)
{
activeBundle = (i == 0 ? bundle1 : bundle2);
// Find a candidate record, but do not lock it yet.
dmo = P2JQueryExecutor.getInstance().execute(buffer.getDatabase(),
(String) null,
RandomAccessQuery::executeImpl,
this,
values,
LockType.NONE,
false,
false);
if (dmo == null)
{
break;
}
referenceRecord = dmo;
// Execute where expression.
buffer.setTempRecord(dmo);
if (whereExpr.get().booleanValue())
{
if (unique)
{
if (uniqueDMO == null)
{
uniqueDMO = dmo;
}
else
{
persistence.uniqueResultViolation(buffer, null);
}
}
else
{
break;
}
}
}
if (unique)
{
dmo = uniqueDMO;
}
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
return null;
}
finally
{
try
{
buffer.popTempContext();
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
referenceRecord = oldRefRec;
activeBundle = bundle0;
}
// If a record was found and successfully passed the client-side where
// expression filter, lock it now, if necessary.
if (dmo != null && !lockType.equals(LockType.NONE) && updateLock)
{
try
{
String table = buffer.getTable();
RecordIdentifier<String> ident = new RecordIdentifier<>(table, dmo.primaryKey());
RecordLockContext lockCtx = buffer.getRecordLockContext();
lockCtx.lock(ident, lockType);
}
catch (LockUnavailableException exc)
{
ErrorManager.recordOrThrowError(445, buffer.getLegacyName() + " record is locked", exc);
// Cannot return record if it could not be locked.
dmo = null;
}
}
return dmo;
}
/**
* This method is invoked when a locking exception is caught during query
* execution. This implementation delegates handling of the error to the
* <code>ErrorManager</code>.
* <p>
* Subclasses which require different handling must override this method.
*
* @param exc
* Cause exception.
*/
protected void handleExecuteException(LockUnavailableException exc)
{
buffer.setLocked(true);
ErrorManager.recordOrThrowError(445, buffer.getLegacyName() + " record is locked", exc);
}
/**
* This method is invoked when a validation exception is caught during
* query execution. This implementation delegates handling of the error
* to the <code>ErrorManager</code>.
* <p>
* Subclasses which require different handling must override this method.
*
* @param exc
* Cause exception.
*/
protected void handleExecuteException(ValidationException exc)
{
ErrorManager.recordOrThrowError(exc);
}
/**
* This method is invoked when a persistence exception is caught during
* query execution. This implementation delegates handling of the error
* to the <code>ErrorManager</code>.
* <p>
* Subclasses which require different handling must override this method.
*
* @param exc
* Cause exception.
*/
protected void handleExecuteException(PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
/**
* Obtain the {@link FQLHelper} object which supports this query, creating it first if
* necessary.
* <p>
* In particular, for LEFT OUTER-JOINs, when the helper is reset at each iteration, a new
* instance will be created unless all the elements from the argument list are not
* {@code null} / {@code unknown}. In this case the cached instance will be reused.
*
* @return FQL helper.
*/
protected FQLHelper getHelper()
{
if (helper == null)
{
boolean hasUnknowns = false;
if (dfArgs != null)
{
for (Object arg : dfArgs)
{
if (arg == null)
{
hasUnknowns = true;
break;
}
if (arg instanceof FieldReference)
{
arg = ((FieldReference) arg).resolve(); // NOTE: result is not saved
}
if (arg instanceof BaseDataType && ((BaseDataType) arg).isUnknown())
{
hasUnknowns = true;
break;
}
}
}
if (!hasUnknowns && nonNullArgsHelper != null)
{
// reuse the already built helper
helper = nonNullArgsHelper;
}
else
{
Property[] iprops = included == null ? null : included.get(buffer.getDMOProxy());
Property[] xprops = excluded == null ? null : excluded.get(buffer.getDMOProxy());
helper = FQLHelper.obtain(buffer,
getReferencedBuffers(),
dfArgs,
isIdOnly(),
dfWhere,
sort,
join,
iprops,
xprops);
if (!hasUnknowns)
{
// cache it for future use when we encounter non-null args
nonNullArgsHelper = helper;
}
}
if (helper.getSortIndex().isInverseSortPhrase(sort))
{
inverseSorting = true;
}
}
return helper;
}
/**
* Set as active the FQL bundle corresponding with a search for the single
* record, if any, which matches this query's criteria.
*/
protected void activateUnique()
{
activeBundleKey = UNIQUE;
activeBundle = getHelper().unique(getJoinKey());
}
/**
* Set as active the FQL bundle corresponding with a search for the first
* record matching this query's criteria.
*/
protected void activateFirst()
{
activeBundleKey = FIRST;
activeBundle = getHelper().first(getJoinKey());
}
/**
* Test whether the {@code first} / {@code last} navigation will throw {@code QueryOffEndException} if the
* initialization failed. It is used to test whether the query loop must be broken when the initialization
* failed with fatal error.
*
* @return always {@code true}.
*/
protected boolean throwExceptionOnFatalError()
{
return true;
}
/**
* Register the RecordChangeListener instance with the ChangeBroker.
*/
protected void registerChangeListener()
{
// Listen for changes to our reference record. If it is modified, we
// can no longer rely upon it as an accurate placeholder.
placeholderCleaner = new RecordChangeListener()
{
public Iterator<RecordBuffer> recordBuffers()
{
return Collections.singletonList(buffer).iterator();
}
public void stateChanged(RecordChangeEvent event)
throws PersistenceException
{
if (event.getDMO() == referenceRecord &&
event.isUpdate() &&
!event.isExtentFieldChangeOnly())
{
// Rely on buffer snapshot as placeholder instead.
referenceRecord = null;
}
}
};
ChangeBroker.get().addListener(placeholderCleaner);
}
/**
* Indicate whether this query should be a projection query, retrieving only the primary key
* of the target record, or whether it should retrieve the entire record as early as possible.
*
* @return <code>true</code> for a projection query, else <code>false</code>.
*/
protected boolean isIdOnly()
{
// Generally, we want to execute as few queries as possible to retrieve a record. The
// minimum is one, whereby we get all the columns in one pass. However, we have to
// consider that for NEXT and PREVIOUS requests, we may execute a query per sort component
// (plus 1 for the primary key if the sort clause does not represent a unique index).
// In that case, we want to execute a projection query (i.e., get only the primary key).
// Also we can only retrieve all the columns in the first pass for a no-lock query,
// because in order to ensure integrity of a locked record, we have to apply the lock
// before retrieving the full data. Finally, honor the fullRecords request if possible.
switch (activeBundleKey)
{
// TODO: if a next/previous sort clause contains only 1 component, we could be less
// strict about forcing a projection query here; however, that would require making
// a separate call to SortCriterion.parse(String, RecordBuffer), which is somewhat
// expensive, and we are about to do it momentarily in FQLHelper
case NEXT:
case PREVIOUS:
return true;
default:
return !(fullRecords || LockType.NONE.equals(lockType) || buffer.isTemporary());
// return !(fullRecords || buffer.isTemporary());
// return !buffer.isTemporary();
// return true;
// return !fullRecords;
}
}
/**
* Checks if the buffer associated with this query will be loaded as the result of this query execution.
* This should always hold, except for CAN-FIND queries that do not modify the state of the buffer.
*
* @return {@code true} if the buffer is going to be loaded by the query.
*/
protected boolean isBufferLoadedByQuery()
{
return true;
}
/**
* Marks this query with recordAlreadyFound flag. This is only used when an {@code AdaptiveQuery} created a
* simple delegate query, and it has meaning only in case of an FIND-BY-ROWID query.
*
* @param b
* The new value for the flag. Usually {@code true}.
*/
void setRecordAlreadyFound(boolean b)
{
recordAlreadyFound = b;
}
/**
* Set the record whose data defines current "place" in query results.
*
* @param referenceRecord
* The record whose data defines current "place" in query results.
*/
void setReferenceRecord(Record referenceRecord)
{
this.referenceRecord = referenceRecord;
}
/**
* Set as active the FQL bundle corresponding with a search for the last
* record matching this query's criteria.
*/
private void activateLast()
{
activeBundleKey = LAST;
activeBundle = getHelper().last(getJoinKey());
}
/**
* Set as active the FQL bundle corresponding with a search for the next
* record matching this query's criteria, relative to the most recently
* fetched record.
*/
private void activateNext()
{
activeBundleKey = NEXT;
activeBundle = getHelper().next(getJoinKey());
}
/**
* Set as active the FQL bundle corresponding with a search for the
* previous record matching this query's criteria, relative to the most
* recently fetched record.
*/
private void activatePrevious()
{
activeBundleKey = PREVIOUS;
activeBundle = getHelper().previous(getJoinKey());
}
/**
* Finalizes a find (FIRST / LAST / UNIQUE) operation, by updating buffer to new {@code dmo}, and
* optionally throwing an error if record not found and notifying the accumulators.
*
* @param lockType
* Lock type to acquire for the retrieved record.
* @param dmo
* The record found or {@code null} if operation failed.
* @param errorReport
* How will be the error reported?
* @param unique
* When called for a UNIQUE FIND. In this case the {@code referenceRecord} is not reset and the
* accumulators are not notified.
*/
private void finalizeFind(LockType lockType, Record dmo, Runnable errorReport, boolean unique)
{
boolean errNull = isErrorIfNull();
updateBuffer(dmo, lockType, errNull, dmo == null ? OffEnd.BACK : OffEnd.NONE);
if (!unique)
{
// reset reference record
referenceRecord = null;
}
// reset dirty flag
dirtyCopy = false;
if (dmo == null && errNull)
{
errorReport.run();
}
if (dmo != null && !unique)
{
// notify accumulators of an iteration
accumulate();
}
}
/**
* Translate this {@link #where} clause to use the bound DMO's alias and property names,
* instead the definition (conversion-time) alias and property names.
*
* @return The translated FQL.
*/
private String getTranslatedWhere()
{
// just create two empty lists where the binding from the external buffers will be collected.
List<RecordBuffer> binding = new ArrayList<>();
List<RecordBuffer> definition = new ArrayList<>();
return translateWhere(binding, definition, where);
};
/**
* Resolve query substitution arguments in preparation for query execution.
* This method is invoked only once, at construction time, for simple
* queries (i.e., those corresponding with simple FIND statements in the
* original Progress code). For iterating queries (i.e., those which
* correspond with FOR loops in the original Progress code), it is
* invoked once per each entrance into the loop. Thus, for an outer loop,
* it is only invoked once, for an inner loop it is invoked once per each
* iteration of the <i>outer</i> loop, and so on.
* <p>
* This causes all variable (i.e. <code>BaseDataType</code>) arguments to
* be duplicated, and all <code>Resolvable</code>s, <i>except</i>
* <code>FieldReference</code>s, to be resolved and the result stored.
* <code>FieldReference</code>s are simply stored, and are resolved
* separately, on demand, each time the query is executed.
*
* @param forceOrigArgs
* Flag indicating that the original arguments used at the construction of the query must be
* re-calculated.
*/
private void resolveArgs(boolean forceOrigArgs)
{
if (forceOrigArgs && origArgs != null)
{
System.arraycopy(origArgs, 0, args, 0, origArgs.length);
AbstractQuery.preprocessSubstitutionArguments(buffer.getBufferManager(),
this::isFindByRowid,
suppressNestedQueryError(),
args);
dfArgs = args;
}
// nothing to process or there is an error pending
if (dfArgs == null || isFatalError())
{
return;
}
// resolve regular query substitution parameters
int len = dfArgs.length;
int dLen = (dynamicFilterArgs == null) ? 0 : dynamicFilterArgs.size();
currentArgs = new Object[len + dLen];
// dynamic filters always come in front
if (dynamicFilterArgs != null)
{
for (int k = 0; k < dLen; k++)
{
currentArgs[k] = resolveArg(dynamicFilterArgs.get(k));
}
}
for (int i = 0; i < len; i++)
{
currentArgs[dLen + i] = resolveArg(dfArgs[i]);
}
}
/**
* Resolve a single resolvable query substitution parameter, with the exception of
* {@code FieldReference}s.
*
* @return The resolved query substitution parameter or {@code arg} itself if it is an
* instance of {@code FieldReference}.
*/
private Object resolveArg(Object arg)
{
if (arg instanceof FieldReference)
{
return arg;
}
else if (arg instanceof P2JQuery.Parameter)
{
return ((P2JQuery.Parameter) arg).resolve();
}
else if (arg instanceof Resolvable)
{
return ((Resolvable) arg).resolve();
}
else if (arg instanceof P2JQuery.ParamResolver)
{
return arg;
}
else
{
return ((BaseDataType) arg).duplicate();
}
}
/**
* Find the next record matching the search criteria, or the first such
* record if no previous search has been executed against this query.
*
* @param values
* Array of substitution parameter values for the base where
* clause (i.e., the un-augmented portion of the clause).
* @param lockType
* Type of lock to acquire for the found record.
* @param updateLock
* <code>true</code> if the status of the lock on the retrieved
* record should be modified; else <code>false</code>. This is
* set to <code>true</code> for actual retrievals, and to
* <code>false</code> when only detecting whether a record would
* be found, or when peeking a record.
*
* @return Next (or first) record to match the query criteria.
*/
private Record findNext(Object[] values, LockType lockType, boolean updateLock)
{
SortIndex sortIndex = getHelper().getSortIndex();
buffer.disableReversibleOffEnd();
OffEnd offEnd = buffer.getOffEnd(sortIndex, inverseSorting);
if (offEnd == OffEnd.BACK)
{
return null;
}
if (offEnd == OffEnd.FRONT || (buffer.getSnapshot() == null && referenceRecord == null))
{
activateFirst();
}
else
{
activateNext();
}
return execute(values, lockType, false, updateLock);
}
/**
* Find the previous record matching the search criteria, or the last such
* record if no previous search has been executed against this query.
*
* @param values
* Array of substitution parameter values for the base where
* clause (i.e., the un-augmented portion of the clause).
* @param lockType
* Type of lock to acquire for the found record.
* @param updateLock
* <code>true</code> if the status of the lock on the retrieved
* record should be modified; else <code>false</code>. This is
* set to <code>true</code> for actual retrievals, and to
* <code>false</code> when only detecting whether a record would
* be found, or when peeking a record.
*
* @return Previous (or last) record to match the query criteria.
*/
private Record findPrevious(Object[] values, LockType lockType, boolean updateLock)
{
SortIndex sortIndex = getHelper().getSortIndex();
OffEnd offEnd = buffer.getOffEnd(sortIndex, inverseSorting || buffer.isOffEndReversible());
buffer.disableReversibleOffEnd();
if (offEnd == OffEnd.FRONT)
{
return null;
}
if (offEnd == OffEnd.BACK || (buffer.getSnapshot() == null && referenceRecord == null))
{
activateLast();
}
else
{
activatePrevious();
}
return execute(values, lockType, false, updateLock);
}
/**
* Retrieve a record, given an array of size one, containing a primary key
* ID value of the record to be retrieved, or the record itself. Acquire
* the specified lock type.
*
* @param data
* Array containing a single primary key ID or DMO.
* @param lockType
* Type of lock to be applied to the record loaded.
*
* @return The record associated with the given ID, or <code>null</code>
* if there is no record matching the ID.
*/
private Record loadByValue(Object[] data, LockType lockType)
{
return loadByValue(data, lockType, false);
}
/**
* Retrieve a record, given an array of size one, containing a primary key
* ID value of the record to be retrieved, or the record itself. Acquire
* the specified lock type.
*
* @param data
* Array containing a single primary key ID or DMO.
* @param lockType
* Type of lock to be applied to the record loaded.
* @param silentIfNullId
* If <code>true</code>, do not raise error if some of the provided
* IDs are <code>null</code>. <code>null</code> IDs are valid for
* queries with OUTER join.
*
* @return The record associated with the given ID, or <code>null</code>
* if there is no record matching the ID.
*/
private Record loadByValue(Object[] data, LockType lockType, boolean silentIfNullId)
{
Record dmo = null;
try
{
if (data != null)
{
Object datum = data[0];
boolean isDMO = (datum instanceof Record);
if (isDMO && lockType.equals(LockType.NONE))
{
dmo = (Record) datum;
}
else
{
Long id;
if (isDMO)
{
dmo = (Record) datum;
id = dmo.primaryKey();
}
else
{
id = (Long) datum;
}
if (id == null && silentIfNullId)
{
releaseBuffers();
return null;
}
else
{
Persistence persistence = buffer.getPersistence();
long timeout = persistence.getTimeOut();
dmo = persistence.load(buffer.getDMOImplementationClass(), id, lockType, timeout, true);
}
}
}
}
catch (MissingRecordException exc)
{
// record must have been deleted in another context
dmo = null;
}
catch (PersistenceException exc)
{
ErrorManager.recordOrThrowError(exc);
}
return dmo;
}
/**
* Create an array of size one, containing the primary key ID of the given
* DMO.
*
* @param dmo
* Data record whose ID is to be stored in the array.
*
* @return Single element array or <code>null</code> if <code>dmo</code>
* is <code>null</code>.
*/
private Object[] getIDs(Record dmo)
{
if (dmo == null)
{
return null;
}
return (new Object[] { dmo.primaryKey() });
}
/**
* Record or throw a FIND FIRST/LAST failed error condition for the current DMO type. In the event a
* fatal error already has been processed, use an anonymous error to leave the error flag set but with
* no error number or text recorded (to mimic 4GL behavior).
*
* @throws ErrorConditionException
* if silent error mode is suppressed.
*/
private void errorFindFirstLast()
{
if (isFatalError())
{
ErrorManager.noRecordOrThrowError("");
}
else
{
String msg = "FIND FIRST/LAST failed for table " + buffer.getLegacyName();
ErrorManager.recordOrThrowError(565, msg);
}
}
/**
* Execute each query stored in the active FQL bundle in turn until we
* find a result or have no more statements to execute. Each statement is
* decreasingly specific.
* <p>
* The latest record to be stored in the record buffer, if any, marks our
* current position in the query's results. Its data is used to substitute
* into the placeholder parameters in the query statements' augmented where
* clauses. These are added to the <code>values</code> provided as part of
* the base query.
*
* @param values
* Array of substitution parameter values for the base where
* clause (i.e., the un-augmented portion of the clause).
* @param lockType
* Type of lock to apply to the found record.
* @param unique
* <code>true</code> if there should be no more than one match
* for the conditions specified; <code>false</code> if multiple
* matches are possible.
* @param updateLock
* <code>true</code> if the status of the lock on the retrieved
* record should be modified; else <code>false</code>. This is
* set to <code>true</code> for actual retrievals, and to
* <code>false</code> when only detecting whether a record would
* be found (i.e., the <code>hasXXXX()</code> methods).
*
* @return The record resulting from the query, or <code>null</code> if
* no record was found.
*
* @throws ErrorConditionException
* if a recoverable error occurred while executing.
*/
private Record executeImpl(Object[] values, LockType lockType, boolean unique, boolean updateLock)
{
boolean debug = LOG.isLoggable(Level.FINE);
DirtyShareContext dirtyContext = buffer.getDirtyContext();
ParameterIndices pi = activeBundle.getParameterIndices();
int origArgCount = (values == null ? 0 : values.length);
int baseArgCount = (pi == null ? origArgCount : pi.getCount());
// query should be continued from explicitly set referenceRecord or DirtyShare version of current record
// as DirtyShare version affects record order
Record placeholder = referenceRecord != null ? referenceRecord : buffer.getSnapshot();
if ((DirtyShareSupport.isEnabledCrossSession() || DirtyShareSupport.isEnabledIntraSession())
&& dirtyContext != null && referenceRecord == null && placeholder != null)
{
// we should start from the last record or Dirty DB version of that record if present
try
{
Record dirtyDMO = dirtyContext.getDirtyDMO(buffer.getEntityName(), placeholder.primaryKey());
if (dirtyDMO != null)
{
placeholder = referenceRecord = dirtyDMO;
}
}
catch (PersistenceException e)
{
LOG.warning("PersistenceException while retrieving DirtyDMO:" + buffer.getEntityName() + " "
+ placeholder.primaryKey() , e);
}
}
Record dmo = null;
Set<Record> primaryDMOs = null;
Integer mpxID = buffer.getMultiplexID();
try
{
// do a prepare buffer before honoring the record nursery - this way we can flush
// the record in the buffer and avoid dirty work with the nursery.
prepareBuffer();
FQLPreprocessor fqlPreproc = getHelper().getFQLPreprocessor();
honorRecordNursery(fqlPreproc);
List<Serializable> parameters = null;
List<FqlType> parameterTypes = null;
if (join != null)
{
parameters = join.getParameters();
parameterTypes = join.getParameterTypes();
if (parameterTypes != null)
{
int s = parameterTypes.size();
for (Serializable param : parameters)
{
if (isNull(param))
{
s--; // do not take null parameters into consideration
// the equality test was replaced by "is null"
}
}
baseArgCount += s;
origArgCount += s;
}
}
Persistence persistence = buffer.getPersistence();
boolean multiplex = buffer.isMultiplexed();
if (multiplex)
{
baseArgCount++;
}
Object[] baseArgs = new Object[baseArgCount];
int startIndex = 0;
if (multiplex)
{
startIndex++;
baseArgs[0] = mpxID;
}
// Add the join parameters (if available) as the next arguments.
if (join != null && parameterTypes != null)
{
int size = parameterTypes.size();
for (int i = 0; i < size; i++)
{
Serializable param = parameters.get(i);
if (!isNull(param))
{
baseArgs[startIndex] = param;
// skip null/unknown parameters
startIndex++;
}
}
}
boolean needsLock = updateLock && lockType != LockType.NONE;
boolean findByRowid = fqlPreproc.isFindByRowid();
while (true)
{
// Iterate all arguments provided for the base portion of the
// where clause only. If any of these are FieldReferences,
// resolve these parameters.
for (int j = 0, i = startIndex; i < baseArgCount; j++)
{
int k = -1;
if (pi == null)
{
k = j;
}
else
{
Integer idx = pi.getIndex(j);
if (idx == null)
{
// original substitution parameter was inlined into FQL by preprocessor; skip it
continue;
}
k = idx;
}
// Substitution placeholders may have been reordered if this
// statement was rewritten by the FQL preprocessor. Make sure
// we access the correct substitution parameter.
Object next = values[k];
// store parameters in the base args array, resolving them first if necessary
if (next instanceof Resolvable)
{
baseArgs[i] = ((Resolvable) next).resolve();
}
else
{
baseArgs[i] = next;
}
i++;
}
// determine call count
int callCount = (placeholder != null ? activeBundle.gettersSize() : 0);
// Collect all arguments required by the augmented portion of the
// where clause only, by invoking getters on the latest DMO stored
// in the buffer.
// each query for NEXT/PREV mode requires two parameters
Object[] augmentArgs = new Object[callCount * 2];
Iterator<Method> getters = activeBundle.getters();
// each query for NEXT/PREV mode requires two parameters
for (int i = 0; getters.hasNext(); i += 2)
{
Method method = getters.next();
// Arguments to this call can be null, because the augmented
// part of the where is based on non-extent properties only.
augmentArgs[i] = Utils.invoke(method, placeholder);
// each query for NEXT/PREV mode requires two parameters
// just duplicate second query segment argument
augmentArgs[i + 1] = augmentArgs[i];
}
// Collect the full arrays of parameters and types, including base
// arguments and augmented. In the loop below, we will copy the
// necessary portions of these arrays to temporary ones.
// each query for NEXT/PREV mode requires two parameters
int fullCount = baseArgCount + (callCount * 2);
Object[] allArgs = new Object[fullCount];
// copy base parameters and types into full arrays
System.arraycopy(baseArgs, 0, allArgs, 0, baseArgCount);
// copy augmented parameters into full array
// each query for NEXT/PREV mode requires two parameters
System.arraycopy(augmentArgs, 0, allArgs, baseArgCount, (callCount * 2));
// possibly overriding result from uncommitted transaction in another session
DirtyInfo info = null;
// execute FQL statements in turn, until we find a result (or run out of statements)
Iterator<String> statements = activeBundle.statements();
while (dmo == null && info == null && statements.hasNext())
{
String fql = statements.next();
// each query for NEXT/PREV mode requires two parameters
int length = baseArgCount + (callCount * 2);
Object[] parms = new Object[length];
// copy arguments needed for this pass from full argument list
System.arraycopy(allArgs, 0, parms, 0, length);
if (callCount > 0)
{
callCount--;
}
FibonacciCounter fib;
long timeout;
needsLock = updateLock && lockType != LockType.NONE;
if (needsLock)
{
fib = new FibonacciCounter();
timeout = fib.next();
}
else
{
fib = null;
timeout = 0L;
}
do
{
// find and optionally lock the record
try
{
dmo = persistence.load(
buffer, fql, parms, lockType, timeout, unique, updateLock, false, findByRowid);
// either got the lock or didn't need one
needsLock = false;
break;
}
catch (LockTimeoutException exc)
{
// a DMO was found but could not be locked and we could not determine it
// has been deleted in an uncommitted transaction in another session
RecordIdentifier<String> ident = exc.getIdentifier();
if (LOG.isLoggable(Level.FINE))
{
LOG.fine("[" +
Utils.describeContext() +
"] FIND lock timeout: "
+ timeout
+ " ms: "
+ ident);
}
Long id = ident.getRecordID();
String entity = buffer.getEntityName();
RecordIdentifier<String> entityIdent = new RecordIdentifier<>(entity, id);
// try to load the DMO from our session
dmo = persistence.quickLoad(entityIdent, !buffer.getDmoInfo().multiTenant);
// If the record came back null, this indicates another session deleted it
// and has since (as in, since we timed out on the lock but before we were
// able to quick-load the DMO) committed its transaction. We have to
// execute the current query again with the same arguments to try to get
// (and lock) another qualifying record. Don't recalculate the timeout in
// this case.
// if DMO came back non-null, we need to hold a reference to it temporarily
// as a placeholder, in case we have to execute another query
if (dmo != null)
{
// evict from the session if not otherwise in use
buffer.evictDMOIfUnused(dmo);
if (dirtyContext != null &&
DirtyShareSupport.isEnabledCrossSession() &&
dirtyContext.isDirtyDelete(entity, id, true))
{
// a record was found but it is being deleted in another session's
// uncommitted transaction
info = new DirtyInfo();
info.setDeleted();
// NOTE: we are breaking out of the load/lock loop WITHOUT acquiring
// the requested lock! This only works because the logic below forces
// the DMO back to null before we can use it. We don't null it here
// because we may need it as a placeholder record for another pass.
break;
}
}
// prepare for another pass
if (dmo == null)
{
// if same query is to be executed again, reset the timeout value
fib.reset();
timeout = fib.next();
}
else
{
// otherwise, steadily increase the timeout value
timeout = fib.next();
// log deadlock warning if timeout is getting too long and wait indefinitely
if (timeout > 3600000L)
{
timeout = 0L;
if (LOG.isLoggable(Level.WARNING))
{
LOG.log(Level.WARNING,
"[" + Utils.describeContext() + "] Possible deadlocked record: " + ident,
new Throwable());
}
}
}
}
catch (MissingRecordException exc)
{
if (needsLock)
{
// we got the lock, but the record was deleted in another session's
// transaction, which has since been committed; reset the timeout and
// try to find another qualifying record
fib.reset();
timeout = fib.next();
}
else
{
break;
}
}
} while (true);
if (debug)
{
logQueryDetails(fql, parms, dmo);
}
}
if (dmo != null &&
callCount == 0 &&
augmentArgs != null &&
augmentArgs.length > 0)
{
// This won't pick up break value for FIRST and LAST, but we're
// only concerned with NEXT and PREV anyway.
breakValue = augmentArgs[0];
}
// check the dirty share manager for an overriding result
// TODO: checking the number of referenced buffers is a temporary workaround;
// a query with server-side joins will not return the right information from the
// dirty database and may cause a fatal error, if the joined table does not yet
// exist in the dirty database; however, this is not a correct solution!
if (index != 0 &&
info == null &&
dirtyContext != null &&
getReferencedBuffers().length == 1)
{
info = dirtyContext.getDirtyInfo(buffer,
index,
activeBundleKey,
activeBundle,
allArgs,
dmo,
fqlPreproc.isFindByRowid(),
isBufferLoadedByQuery());
}
// if check found nothing of interest, we're done
if (info == null)
{
break;
}
// keep track of the DMO(s) we found in the primary database; in the event we don't
// use them otherwise, we'll have to evict them from the session
if (dmo != null)
{
if (primaryDMOs == null)
{
primaryDMOs = new HashSet<>();
}
primaryDMOs.add(dmo);
}
// can't trigger revalidation of AdaptiveQuery if dirty database result is in use
breakValue = null;
// Get the DMO found in the dirty database, if any. This will be
// a deep copy of the original, so it is safe to use.
Record dirtyDMO = info.getDirtyDMO();
// If found DMO was deleted, update the placeholder and try again.
// Also handle the case where the DMO found in the primary
// database has been modified in such a way that it may no longer
// satisfy the search criteria.
if ((DirtyShareSupport.isEnabledCrossSession() || DirtyShareSupport.isEnabledIntraSession()) &&
activeBundleKey != UNIQUE &&
(info.isDeleted() ||
(info.isModified() &&
((!info.isInserted() && !info.isLeaked()) ||
(dirtyDMO != null && dirtyDMO.primaryKey().equals(dmo.primaryKey()))))))
{
// Test if the record has moved along the index we're walking.
if (info.isModified() && dirtyDMO != null && dmoSorter != null)
{
Record compDirtyDMO = info.getComparableDMO();
int comp = dmoSorter.compare(dmo, compDirtyDMO);
if (comp == 0)
{
// Both the primary DMO and dirty DMO sort equivalently
// on the index being navigated. If the dirty DMO does
// not represent the full set of uncommitted changes
// being tracked in the dirty database, that indicates we
// published uncommitted changes early and we must use
// the dirty DMO as our result...
if (!info.isFullyPublished())
{
dmo = dirtyDMO;
dirtyCopy = true;
}
// ...otherwise, we use the primary DMO.
break;
}
}
// We are about to update the placeholder and try another pass,
// so if the navigation request was for the first or last
// record, we have to modify the mode to be next or previous,
// respectively. Otherwise, we'll just find the same record in
// an infinite loop.
switch (activeBundleKey)
{
case FIRST:
activateNext();
break;
case LAST:
activatePrevious();
break;
}
// The found record must be skipped because it was either
// deleted or modified on the index we're walking. Update the
// placeholder to be the record we actually found...
placeholder = dmo;
dmo = null;
// ...then try another pass.
continue;
}
if (needsLock && dmo != null)
{
// If we are here, we determined a provisional DMO we found was deleted in another
// session's uncommitted transaction and we broke out of the load/lock loop
// without acquiring the requested lock. The DMO was loaded only for placeholder
// purposes. Null it out now, since we haven't got the lock; we will either find
// another qualifying record or give up. A bit redundant with the delete logic
// in processDirtyResults, but safety first.
dmo = null;
}
// determine which of the two candidate DMOs is most appropriate as a result
dmo = processDirtyResults(persistence, dmo, info, lockType, updateLock);
break;
}
if (dirtyCopy && primaryDMOs != null)
{
// We are not going to use the primary DMO(s) we found, so make
// sure they are evicted from the session if not in use elsewhere
// in this context.
for (Record pDMO : primaryDMOs)
{
buffer.evictDMOIfUnused(pDMO);
}
}
}
catch (IllegalAccessException exc)
{
throw new RuntimeException(exc);
}
catch (InvocationTargetException exc)
{
DBUtils.handleException(buffer.getDatabase(), exc);
throw new RuntimeException(exc);
}
catch (LockUnavailableException exc)
{
handleExecuteException(exc);
// Cannot return record if it could not be locked.
dmo = null;
}
catch (ValidationException exc)
{
// a validation error can only result from flushing an unrelated record currently in
// the buffer in preparation for a query result; since this will not be within the
// context of any silent error mode under which this query is running, we instruct
// the error manager to not suppress the message
ErrorManager.throwError(exc, false);
}
catch (PersistenceException exc)
{
handleExecuteException(exc);
}
finally
{
if (dmo == null)
{
breakValue = null;
}
}
return dmo;
}
/**
* Core method of attempting to retrieve the record for this query using direct access.
* This does all the prerequisites checks and is safely implemented such that a failed direct
* access allows easy recovery.
* <p>
* A simple query (no join, external buffers and single result) over the temporary database
* can be resolved using a direct access driver (currently supported by H2). This avoids generating
* an FQL/SQL and simply uses the internal H2 structures (tables, indexes, etc.) to find the
* searched record.
* <p>
* Currently, this can be used only if the H2 row structure is very similar to the FWD record
* structure (to allow hydration) and the searched row can be uniquely identified: using recid
* or an unique index.
*
* @param values
* The resolved substitution parameters to be used by the query.
*
* @return An optional depending on the success of this attempt. If a direct access exception
* occurred, allow fallback by returning an empty optional. If a record could be retrieved,
* get it through the optional.
*/
private Optional<Record> executeDirectAccess(Object[] values)
{
try
{
// reduce expenses; don't create a session just to check direct access
Session currentSession = buffer.getSession(false);
DmoMeta meta = buffer.getDmoInfo();
FQLPreprocessor fqlPreproc = helper.getFQLPreprocessor();
if (currentSession != null &&
isSimpleQuery() &&
buffer.isTemporary() && // prerequisites
fqlPreproc.isUniqueFind(true) && // this is the core condition right now
DirectAccessHelper.hasDirectAccess(currentSession) && // only if we have direct access to H2
!meta.hasComputedColumns(false) && // only if all H2 columns represent the DMO fields
!fqlPreproc.hasContains())
{
Record r;
// first prepare the buffer to force a flush, so that the record nursery won't deal with
// dirty data from the same buffer.
prepareBuffer();
honorRecordNursery(fqlPreproc);
if (fqlPreproc.isFindByRowid())
{
long recid = fqlPreproc.getFindByRowid(values);
r = DirectAccessHelper.findByRowid(currentSession, meta, recid);
}
else if (fqlPreproc.isUniqueFind(false))
{
// make a copy to add the custom multiplex
UniqueIndexLookup lookup = new UniqueIndexLookup(fqlPreproc.getUniqueIndexLookup());
if (!lookup.fill(values))
{
throw new DirectAccessException("Invalid fill operation");
}
lookup.addMapping(TemporaryBuffer.MULTIPLEX_FIELD_NAME, buffer.getMultiplexID());
r = DirectAccessHelper.findByUniqueIndex(currentSession, meta, lookup);
}
else
{
throw new DirectAccessException("Invalid state for direct access");
// not a valid state
}
// the record can be null; make sure we let the caller know that we succeeded
return Optional.ofNullable(r);
}
}
catch (PersistenceException e)
{
// this is not ideal as it assumes that we failed doing some persistence work already
handleExecuteException(e);
return null;
}
catch (ValidationException exc)
{
// a validation error can only result from flushing an unrelated record currently in
// the buffer in preparation for a query result; since this will not be within the
// context of any silent error mode under which this query is running, we instruct
// the error manager to not suppress the message
ErrorManager.throwError(exc, false);
}
catch (DirectAccessException e)
{
if (LOG.isLoggable(Level.WARNING) && !e.isExpected())
{
LOG.log(Level.WARNING, "Failed direct-access due to " + e.getMessage());
}
return null;
}
// don't use exceptions if we fail here; if we do, there will be a lot of exception being thrown
// it is faster to just let the caller know that we failed or not using DirectAccessResult
return null;
}
/**
* Convenient common method which exposes the nursery records to the persistence layer. This should be
* used to honor transient records when executing a query. This way, the look-up is able to find
* the records which are not yet flushed to the database and are stored in the record nursery. The logic
* is not trivial, as it validates and flushes record to the database, honoring triggers. Use this
* before attempting to use the database.
*
* @param preproc
* The FQL preprocessor instance of {@code null} if it should be retrieved here.
*
* @throws PersistenceException
* if there is any error persisting the record to its primary or dirty database.
*/
void honorRecordNursery(FQLPreprocessor preproc)
throws PersistenceException
{
FQLPreprocessor fqlPreproc = preproc == null ? getHelper().getFQLPreprocessor() : preproc;
// find by recid is a special case using index "0"; otherwise, use index associated with find
int idxUid = fqlPreproc.isFindByRowid() ? 0 : index;
RecordNursery nursery = buffer.getNursery();
nursery.makeVisible(buffer.getDmoInfo().getId(), buffer.getMultiplexID(), idxUid);
}
/**
* Process the results of a dirty index check and return the most
* appropriate result for the query. This method may acquire a record lock
* for the selected result.
*
* @param persistence
* Persistence services object.
* @param primaryDMO
* Candidate DMO found in the primary database. Will not be
* <code>null</code>.
* @param info
* Information found during the dirty database check.
* @param lockType
* Type of lock to apply to the found record.
* @param updateLock
* <code>true</code> if the status of the lock on the retrieved
* record should be modified; else <code>false</code>. This is
* set to <code>true</code> for actual retrievals, and to
* <code>false</code> when only detecting whether a record would
* be found (i.e., the <code>hasXXXX()</code> methods).
*
* @return Either <code>primaryDMO</code> or <code>dirtyDMO</code>,
* depending upon which record is most appropriate in terms of the
* index being walked, and possibly the most recently found
* record, in the case of a relative move (NEXT/PREVIOUS).
*
* @throws PersistenceException
* if any error occurs retrieving a record from the primary
* database, after determining its ID in the dirty database.
* @throws LockUnavailableException
* if a no-wait lock requested on <code>dirtyDMO</code> is
* currently unavailable.
*/
private Record processDirtyResults(Persistence persistence,
Record primaryDMO,
DirtyInfo info,
LockType lockType,
boolean updateLock)
throws PersistenceException
{
Record dmo = primaryDMO;
Record dirtyDMO = info.getDirtyDMO();
Record compDirtyDMO = info.getComparableDMO();
boolean useDirtyDMO = true;
// check if there is anything to compare to
if (compDirtyDMO == null)
{
return primaryDMO;
}
if (primaryDMO != null && activeBundleKey != UNIQUE && dmoSorter != null)
{
// At this point, we have both a candidate DMO from the primary
// database (dmo), and a candidate DMO from the dirty database
// (dirtyDMO). We need to sort them to determine which one is
// the appropriate "next" record in the index being walked.
int comp = dmoSorter.compare(primaryDMO, compDirtyDMO);
if (activeBundleKey == LAST || activeBundleKey == PREVIOUS)
{
comp *= -1;
}
useDirtyDMO = (comp >= 0);
}
if (useDirtyDMO)
{
// all below can be gathered in a single if statement, but they are separated on purpose to allow
// analysis of specific cases on intra- and cross- session DirtyShare
if (info.isInserted() && LockType.NONE.equals(lockType))
{
// Use the dirty copy from inserts if no lock has been requested,
dmo = dirtyDMO;
// mark the record as dirty and potentially from another context
dirtyCopy = true;
}
else if (info.isLeaked() && LockType.NONE.equals(lockType))
{
// Use the dirty copy from leaks if no lock has been requested
dmo = dirtyDMO;
// mark the record as dirty and potentially from another context
dirtyCopy = true;
}
else if (info.isLocal())
{
// the dirty record actually is the local original. This comes into play when a DMO is created
// in one buffer and is queried by another buffer in the same session, before the first buffer has
// validated and flushed the record (it is validated as a side effect of the
// getDirtyInfo() call). The record is not found in primary database (since it has
// not yet been flushed), but it is found in the dirty database, and the dirty
// context substitutes the original DMO for the dirty one in getDirtyInfo().
dmo = dirtyDMO;
// mark the record as dirty and potentially from another, since this prevents us from trying
// to reassociate the record with the current session more than once, in certain cases (see
// Persistence$Context.getSession())
dirtyCopy = true;
}
else if (dirtyDMO != null)
{
// Just use the primary key of the record found in the dirty
// database, but reload the record from the primary database.
// Note: it might not be there, if the record existed only in
// another context, and was never committed.
// in that case return primaryDMO as there is no dirtyDMO to replace it
Long id = dirtyDMO.primaryKey();
try
{
long timeout = persistence.getTimeOut();
dmo = persistence.load(buffer.getDMOImplementationClass(), id, lockType, timeout, updateLock);
}
catch (MissingRecordException exc)
{
dmo = primaryDMO;
}
if (dmo == null)
{
dmo = primaryDMO;
}
}
else if (info.isDeleted() || info.isModified())
{
dmo = null;
}
}
return dmo;
}
/**
* Check if this is a "simple" query. The term simple is vague. It refers to queries which can be
* easier resolved through fast-find cache or direct access. "simple" means no join, no external
* buffer and no dependent bundle key (FIRST, LAST and UNIQUE do not depend on a previous
* sorted query).
*
* @return {@code true} if this query can be used by fast-find or direct access.
*/
private boolean isSimpleQuery()
{
return (activeBundleKey == FIRST || activeBundleKey == LAST || activeBundleKey == UNIQUE) &&
join == null && getExternalBuffers() == null;
}
/**
* Check if constraints are present in the query.
*
* @return {@code true} if there are constraints present in the query
*/
protected boolean hasConstraints()
{
return where != null || args != null || join != null ||
getExternalBuffers() != null || whereExpr != null;
}
/**
* Log debug details about the inputs and results of a query.
*
* @param fql
* FQL query statement.
* @param parms
* Query substitution parameters
* @param dmo
* DMO record found, if any.
*/
private void logQueryDetails(String fql, Object[] parms, Record dmo)
{
LOG.log(Level.FINE, "FQL: " + fql);
StringBuilder buf = new StringBuilder("PARMS: ");
int len = parms.length;
if (len == 0)
{
buf.append("N/A");
}
for (int k = 0; k < len; k++)
{
if (k > 0)
{
buf.append(", ");
}
Object next = parms[k];
if (next instanceof BaseDataType)
{
buf.append(((BaseDataType) next).toStringMessage());
}
else
{
buf.append(next);
}
}
LOG.log(Level.FINE, buf.toString());
buf.setLength(0);
buf.append("DMO: ");
if (dmo != null)
{
buf.append(dmo.primaryKey());
}
else
{
buf.append("<not found>");
}
LOG.log(Level.FINE, buf.toString());
if (LOG.isLoggable(Level.FINEST))
{
buf.setLength(0);
buf.append("DMO Detail: ");
buf.append(buffer.toString(dmo));
LOG.log(Level.FINEST, buf.toString());
}
}
/**
* Analyses the set of join parameters and return a bit set key for the join pare of the where predicate.
* The key contains 1 on position 1 iif the value is not null (not unknown). If the {@code join} is
* {@code null} or all its elements are not null, {@code null} value is returned.
*
* @return A key for join clauses of the where predicate, as described above.
*/
private BitSet getJoinKey()
{
if (join == null)
{
return null;
}
List<Serializable> joinParams = join.getParameters();
BitSet ret = null;
if (joinParams != null)
{
boolean allKnown = true;
ret = new BitSet(joinParams.size());
for (int i = 0; i < joinParams.size(); i++)
{
Serializable param = joinParams.get(i);
boolean isNotNull = !isNull(param);
allKnown &= isNotNull;
ret.set(i, isNotNull);
}
if (allKnown)
{
return null;
}
}
return ret;
}
/**
* Test whether an object is {@code null} or {@code unknown}.
*
* @param param
* The {@code Object} to test.
*
* @return {@code true} iif the object is {@code null} or {@code unknown}.
*/
private static boolean isNull(Object param)
{
if (param == null)
{
return true;
}
if (param instanceof BaseDataType)
{
return ((BaseDataType) param).isUnknown();
}
return false;
}
}