PresortQuery.java
/*
** Module : PresortQuery.java
** Abstract : A query whose results are preselected and sorted by a client
** expression or field reference
**
** Copyright (c) 2004-2024, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060103 @23795 Created initial version. A specialized
** preselect query which uses client-side record
** sorting to presort query results before loop
** iteration occurs.
** 002 ECF 20060208 @24722 Changed to support client-side where clause
** processing. Also restructured inner classes
** to take better advantage of shared Results
** implementations.
** 003 ECF 20060425 @25720 Added Initialization logics for single
** table instances. These mirror the
** constructors of the superclass, only without
** the order by (sort) string parameter.
** Tentatively removed existing, single table
** constructors, which are most likely obsolete.
** 004 ECF 20060503 @25959 Partially reworked break group processing.
** Added isLastOfBreakGroup implementation.
** 005 ECF 20060602 @26953 Use DMO instances instead of primary key IDs.
** Corresponds with changes in parent class.
** 006 ECF 20060608 @27003 Removed assembleSelectClause() method. It is
** no longer different than the superclass'
** implementation.
** 007 ECF 20060720 @28117 Backed out #005-006 (@26953, @27003). The
** parent has been refitted to collect primary
** keys instead of full records in the first
** pass query.
** 008 ECF 20060725 @28189 Modified createResults() signature to match
** changes in the superclass.
** 009 ECF 20060730 @28271 Code cleanup. Removed unused constructors.
** 010 ECF 20060803 @28400 Simplified createResults() method. Use
** getRecordBuffers() method instead of building
** up list manually.
** 011 ECF 20061027 @30753 Replaced Object with DataModelObject for DMO
** arguments to methods/constructors. Required
** for compile-time type safety.
** 012 ECF 20070225 @32228 Plug results resource leak. Clean up results
** resources once results have been sorted. This
** was causing both prepared statements and
** result sets to remain unclosed in the backing
** JDBC connection.
** 013 ECF 20070709 @34420 Fixed assembleSelectClause(). Changed
** parameter type to StringBuilder to properly
** override the same method of the parent class.
** 014 ECF 20070815 @34855 Support converted nested CAN-FIND statements.
** Use RecordBuffer's new API to push/pop temp
** record contexts.
** 015 ECF 20080125 @36996 Refactored sorting logic to Presorter class.
** PresortQuery now delegates all sorting work
** to Presorter.
** 016 SVL 20080523 @38373 Generated HQL query is ordered by <dmo>.id.
** 017 ECF 20090313 @41601 Forced full-records mode. Added overridden
** addComponent() method which calls
** setFullRecords(). Changed createResults().
** 018 SVL 20090316 @41612 Initialization logics that take the sort
** string parameter were added.
** 019 SVL 20090505 @42093 More Initialization logics added.
** 020 ECF 20090618 @42769 Added executeScroll(). Overrides parent's
** implementation to use FORWARD_ONLY scroll mode,
** which may permit certain JDBC drivers to use
** server-side cursors.
** 021 SVL 20090723 @43345 Make this type of queries not natively
** scrollable, but enable scrolling by default.
** Use IncrementalResults instead of SortedResults
** for some cases.
** 022 SVL 20090730 @43460 Re-activated #021.
** 023 SVL 20111111 Make the class implement PresortDelegate.
** 024 ECF 20131028 Import change.
** 025 SVL 20140604 Added support for INDEX-INFORMATION.
** 026 ECF 20150801 Retrofitted API changes and refactoring for compound query
** optimization.
** 027 ECF 20151214 Prevent releasing the current buffer record(s) when break groups are
** enabled and no record is found by the query.
** 028 ECF 20160614 Pass entity names associated with this query to Persistence.scroll.
** 029 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.
** 030 OM 20180515 Added dynamic filters and dynamic sort.
** 031 ECF 20181106 Added runtime support for {FIRST|LAST}-OF methods.
** 032 ECF 20200906 New ORM implementation.
** 033 OM 20201001 Dropped redundant ORDER BY elements in multi-table queries.
** OM 20201007 Optimized SortCriterion by using DmoMeta data instead of map lookups.
** CA 20210310 A dynamic predicate must set the default lock to NONE, instead of SHARE.
** OM 20220701 Dropped overriding methods which were duplicating code from super class.
** SVL 20230109 Reflected PreselectQuery.components() now providing direct access to the
** components array in order to improve performance.
** 034 IAS 20230116 Added DATE-RELATION to the 'scroll' parameters
** 035 SR 20230615 Fixed duplicate recid bug on assembleOrderByClause.
** SR 20230630 Extracted .size() outside for and changed .getUnqualifiedName.equals() into
** .isPrimaryKey().
** 036 AL2 20231027 Use ForwardResults to honor TYPE_FORWARD_ONLY properly.
** AL2 20231027 Made preserveBuffersOnEmptyResults return true only when results are truly empty
** 037 AD 20231124 Added override for shouldRegisterRecordChangeListener().
** 038 CA 20240331 Use ArrayList and Java for loops in query assembler.
** 039 ES 20240404 Overridden isPesort method.
** 040 LS 20241101 Changed assembleSelectClause to return both recIds and full records.
** Created getComponentsRequiringFullRecords to identify which tables require full
** records.
** Added a constant to establish a threshold for determining which tables should
** avoid full record retrieval.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.persist;
import java.sql.*;
import java.util.*;
import java.util.function.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.persist.orm.*;
import com.goldencode.p2j.util.*;
/**
* A specialized preselect query which uses one or more instances of
* <code>Resolvable</code> provided by business application code to sort the
* result set returned by the database. This class supports the Progress
* semantics of "client-side" results sorting and break groups. Multiple
* levels of nested break groups are supported.
* <p>
* Sorting criteria is specified using the <code>addSortCriterion</code>
* method. The order in which criteria are added is significant; the first
* criterion added dictates the coarsest level of sorting. Each criterion
* added subsequently only sorts rows within that criterion added immediately
* previous to it.
*/
public final class PresortQuery
extends PreselectQuery
implements Presortable,
PresortDelegate
{
/** The maximum number of fields allowed in a table for full record retrieval. */
private static final int MAX_TABLE_FIELDS = 20;
/** Helper which sorts preselected, unsorted results */
private Presorter sorter;
/**
* Default constructor. Initialization is deferred until <code>initialize()</code> is
* called.
*/
public PresortQuery()
{
}
/**
* Initialization logic which is typically used for a multi-table query. Details
* specific to each table component of the query are added via one of the
* <code>addComponent</code> method variants.
* <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.
*
* @return This query instance.
*/
public PresortQuery initialize()
{
super.initialize(null);
sorter = new Presorter(this);
setScrolling();
return this;
}
/**
* Initialization logic which is typically used for a multi-table query. Details
* specific to each table component of the query are added via one of the
* <code>addComponent</code> method variants.
* <p>
* This variant is used by code which is converted from an OPEN QUERY
* statement, which permits advancing off the end of a result set without
* raising a <code>QueryOffEndException</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 lenientOffEnd
* <code>true</code> if advancing past either end of the result
* set should NOT raise a <code>QueryOffEndException</code>;
* <code>false</code> if doing so should raise the exception.
*
* @return This query instance.
*/
public PresortQuery initialize(boolean lenientOffEnd)
{
super.initialize(lenientOffEnd, null);
sorter = new Presorter(this);
setScrolling();
return this;
}
/**
* Initialization logic designed for a single-table preselect query.
* The where clause must expect no substitution parameters. A share lock
* will be applied to retrieved records.
* <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
* DMO proxy which defines buffer and record type.
* @param where
* HQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
*
* @return This query instance.
*/
public PresortQuery initialize(DataModelObject dmo, String where, Supplier<logical> whereExpr)
{
return initialize(dmo, where, whereExpr, null, null, null, null, null);
}
/**
* Initialization logic designed for a single-table preselect query.
* The where clause must expect no substitution parameters. A share lock
* will be applied to retrieved records.
* <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
* DMO proxy which defines buffer and record type.
* @param where
* HQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
*
* @return This query instance.
*/
public PresortQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
DataModelObject inverse)
{
return initialize(dmo, where, whereExpr, null, null, inverse, null, null);
}
/**
* Initialization logic designed for a single-table preselect query.
* A share lock will be applied to retrieved records.
* <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
* DMO proxy which defines buffer and record type.
* @param where
* HQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
*
* @return This query instance.
*/
public PresortQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
Object[] args)
{
return initialize(dmo, where, whereExpr, null, null, null, args, null);
}
/**
* Initialization logic designed for a single-table preselect query.
* A share lock will be applied to retrieved records.
* <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
* DMO proxy which defines buffer and record type.
* @param where
* HQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
*
* @return This query instance.
*/
public PresortQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
DataModelObject inverse,
Object[] args)
{
return initialize(dmo, where, whereExpr, null, null, inverse, args, null);
}
/**
* Initialization logic designed for a single-table preselect query.
* The where clause must expect no 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
* DMO proxy which defines buffer and record type.
* @param where
* HQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param lockType
* Lock type to apply to records retrieved by this query.
*
* @return This query instance.
*/
public PresortQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
LockType lockType)
{
return initialize(dmo, where, whereExpr, null, null, null, null, lockType);
}
/**
* Initialization logic designed for a single-table preselect query.
* The where clause must expect no 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
* DMO proxy which defines buffer and record type.
* @param where
* HQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @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 by this query.
*
* @return This query instance.
*/
public PresortQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
DataModelObject inverse,
LockType lockType)
{
return initialize(dmo, where, whereExpr, null, null, inverse, null, lockType);
}
/**
* Initialization logic designed for a single-table preselect query.
* <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
* DMO proxy which defines buffer and record type.
* @param where
* HQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
* @param lockType
* Lock type to apply to records retrieved by this query.
*
* @return This query instance.
*/
public PresortQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
Object[] args,
LockType lockType)
{
return initialize(dmo, where, whereExpr, null, null, null, args, lockType);
}
/**
* Initialization logic designed for a single-table preselect query.
* <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
* DMO proxy which defines buffer and record type.
* @param where
* HQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param inverse
* DMO to which this query should join via a foreign relation.
* May be <code>null</code>.
* @param args
* Substitution parameters to be inserted into placeholders within
* the where clause.
* @param lockType
* Lock type to apply to records retrieved by this query.
*
* @return This query instance.
*/
public PresortQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
DataModelObject inverse,
Object[] args,
LockType lockType)
{
return initialize(dmo, where, whereExpr, null, null, inverse, args, lockType);
}
/**
* Initialization logic designed for a single-table preselect query.
* <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
* DMO proxy which defines buffer and record type.
* @param where
* HQL where clause. May be <code>null</code>.
* @param whereExpr
* Client-side where clause expression. May be <code>null</code>.
* @param sort
* Sort clause that corresponds the index which defines the sort
* order for this query. May be <code>null</code>.
* @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 to be inserted into placeholders within
* the where clause.
* @param lockType
* Lock type to apply to records retrieved by this query. If {@code null}, the default lock level
* is assumed.
*
* @return This query instance.
*/
public PresortQuery initialize(DataModelObject dmo,
String where,
Supplier<logical> whereExpr,
String sort,
String indexInfo,
DataModelObject inverse,
Object[] args,
LockType lockType)
{
super.initialize(dmo, where, whereExpr, sort, indexInfo, inverse, args, lockType);
sorter = new Presorter(this);
setScrolling();
return this;
}
/**
* Add a single sort criterion, in the form of a <code>Resolvable</code>
* object which is resolved at sort time. It is the value of the resolved
* result which is used for the sort.
* <p>
* The sort is ascending, from lowest resolved value to highest.
*
* @param sort
* A resolvable object whose resolved result is used for sorting.
*/
public void addSortCriterion(Resolvable sort)
{
sorter.addSortCriterion(sort);
}
/**
* Add a single sort criterion, in the form of a <code>Resolvable</code>
* object which is resolved at runtime. It is the value of the resolved
* result which is used for the sort.
*
* @param descending
* if <code>true</code>, the sort is descending, from highest
* resolved value to lowest; otherwise, the sort is ascending.
*
* @param sort
* A resolvable object whose resolved result is used for sorting.
*/
public void addSortCriterion(Resolvable sort, boolean descending)
{
sorter.addSortCriterion(sort, descending);
}
/**
* Add a single dynamic sort criterion, in the form of a {@code Resolvable} object which is
* resolved at runtime. It is the value of the resolved result which is used for the sort.
* <p>
* Unlike {@link #addSortCriterion} methods, the criterion is added with higher priority.
*
* @param sort
* A resolvable object whose resolved result is used for sorting.
* @param ascending
* if {@code true}, the sort is ascending, from lowest resolved value to highest;
* otherwise, the sort is descending.
*/
@Override
public void addDynamicSortCriterion(Resolvable sort, boolean ascending)
{
sorter.addDynamicSortCriterion(sort, ascending);
}
/**
* Clears all sort criteria added dynamically.
*/
@Override
public void clearDynamicSortCriteria()
{
sorter.clearDynamicSortCriteria();
}
/**
* Enable all sort criteria set for this query to act as break group
* categories. When the query's results are presorted, the contents of
* each break group are determined.
*/
public void enableBreakGroups()
{
sorter.enableBreakGroups();
}
/**
* Check if this type of query should register record change listeners. Use this
* to avoid computing any scope for registration and end up with a no registration
* behavior.
*
* @return {@code true} if we should invest time into computing the scope in order
* to call {@link #registerRecordChangeListeners} or {@code false} if we
* can skip doing any work to honor {@link #registerRecordChangeListeners}.
*/
@Override
public boolean shouldRegisterRecordChangeListeners()
{
return false;
}
/**
* Determine whether the query result row currently being visited is the
* first row in the presorted results list.
*
* @return <code>true</code> if the current result row is the first row,
* else <code>false</code>.
*/
public logical isFirst()
{
return sorter.isFirst();
}
/**
* Indicate whether the query is of presort type.
*
* @param iterating
* <code>true</code> indicating that a query of presort type.
*/
@Override
public boolean isPresort()
{
return true;
}
/**
* Determine whether the query result row currently being visited is the
* first row within a break group whose category is identified by the
* specified resolvable object.
*
* @param key
* Break group category key. Must be the same object reference
* as was specified previously when invoking one of the
* <code>addSortCriterion</code> method variants.
*
* @return <code>true</code> if break groups are enabled and current row
* is first in the specified break group;
* <code>false</code> if break groups have not been enabled or
* current row is not first in the specified break group.
*/
public logical isFirstOfGroup(Resolvable key)
{
return sorter.isFirstOfGroup(key);
}
/**
* Determine whether the query result row currently being visited is the first row of the
* query's result set or the first row within a break group whose category is identified by
* the specified BREAK-BY level.
*
* @param level
* Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
* criterion, etc. Must not be negative or greater than the number of sort criteria.
*
* @return {@code true} if the current row is first in the result set or within the specified
* break group; else {@code false}.
*/
public logical isFirstOfGroup(integer level)
{
return sorter.isFirstOf(level);
}
/**
* Determine whether the query result row currently being visited is the first row of the
* query's result set or the first row within a break group whose category is identified by
* the specified BREAK-BY level.
*
* @param level
* Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
* criterion, etc. Must not be negative or greater than the number of sort criteria.
*
* @return {@code true} if the current row is first in the result set or within the specified
* break group; else {@code false}.
*/
public logical isFirstOfGroup(int level)
{
return isFirstOfGroup(new integer(level));
}
/**
* Determine whether the query result row currently being visited is the
* last row in the presorted results list.
*
* @return <code>true</code> if the current result row is the last row,
* else <code>false</code>.
*/
public logical isLast()
{
return sorter.isLast();
}
/**
* Determine whether the query result row currently being visited is the
* last row within a break group whose category is identified by the
* specified resolvable object.
*
* @param key
* Break group category key. Must be the same object reference
* as was specified previously when invoking one of the
* <code>addSortCriterion</code> method variants.
*
* @return <code>true</code> if break groups are enabled and current row
* is last in the specified break group;
* <code>false</code> if break groups have not been enabled or
* current row is not last in the specified break group.
*/
public logical isLastOfGroup(Resolvable key)
{
return sorter.isLastOfGroup(key);
}
/**
* Determine whether the query result row currently being visited is the last row of the
* query's result set or the last row within a break group whose category is identified by
* the specified BREAK-BY level.
*
* @param level
* Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
* criterion, etc. Must not be negative or greater than the number of sort criteria.
* Must not be unknown value.
*
* @return {@code true} if the current row is last in the result set or within the specified
* break group; else {@code false}.
*/
public logical isLastOfGroup(integer level)
{
return sorter.isLastOf(level);
}
/**
* Determine whether the query result row currently being visited is the last row of the
* query's result set or the last row within a break group whose category is identified by
* the specified BREAK-BY level.
*
* @param level
* Break group level, where 0 is the whole query, 1 is the first BREAK-BY sort
* criterion, etc. Must not be negative or greater than the number of sort criteria.
*
* @return {@code true} if the current row is last in the result set or within the specified
* break group; else {@code false}.
*/
public logical isLastOfGroup(int level)
{
return isLastOfGroup(new integer(level));
}
/**
* Access the current, sorted results for this query.
*
* @return <code>Results</code> object used to retrieve sorted results.
*/
public Results getSortedResults()
{
return getResults();
}
/**
* Add a component to the query.
*
* @param comp
* Query component to be added.
*
* @throws UnsupportedOperationException
* if attempting an outer join (temporary);
* if iteration type is not <code>NEXT</code> (temporary);
* if the component's DMO's associated database is different than
* that of a previously added component's database (cross-database
* preselect joins are not possible).
*/
protected void addComponent(QueryComponent comp)
{
setFullRecords();
super.addComponent(comp);
}
/**
* Does this query support non-NEXT iteration components?
*
* @return <code>true</code>
*/
protected boolean supportsArbitraryIteration()
{
return true;
}
/**
* Indicate whether the record buffer(s) current record(s) should be preserved, in the event
* the query does not find any results.
* <p>
* This implementation returns true only if break groups are enabled. Also, this checks if the
* results are truly empty. For a presort query, the results are always "full".
*
* @param results
* The results instance to decide if they are truly empty. The results are not visible
* in some sub-classes, so let them be passed by parameter.
*
* @return <code>true</code> if break groups are enabled, else <code>false</code>.
*/
@Override
protected boolean preserveBuffersOnEmptyResults(Results results)
{
if (!(results instanceof FullResults))
{
return false;
}
return sorter.isBreakGroupsEnabled() && ((FullResults) results).isEmpty();
}
/**
* Determine whether the query result row currently being visited is the
* last row within a break group whose category is identified by the
* specified resolvable object.
*
* @param key
* Break group category key. Must be the same object reference
* as was specified previously when invoking one of the
* <code>addSortCriterion</code> method variants.
*
* @return <code>true</code> if break groups are enabled and current row
* is last in the specified break group;
* <code>false</code> if break groups have not been enabled or
* current row is not last in the specified break group.
*/
protected boolean isLastOfBreakGroup(Resolvable key)
{
return sorter.isLastOfBreakGroup(key);
}
/**
* Determine whether the query result row currently being visited is the
* first row within a break group whose category is identified by the
* specified resolvable object.
* <p>
* This is the backing worker method for {@link #isFirstOfGroup} and is
* used internally by the query to manage accumulators.
*
* @param key
* Break group category key. Must be the same object reference
* as was specified previously when invoking one of the
* <code>addSortCriterion</code> method variants.
*
* @return <code>true</code> if current row is first in a break group;
* <code>false</code> if it is not; unknown value if break groups
* have not been enabled.
*/
protected boolean isNewBreakGroup(Resolvable key)
{
return sorter.isNewBreakGroup(key);
}
/**
* Execute the query to create a first level result set.
* <p>
* This default implementation returns an object which is a thin wrapper around
* {@code ScrollableResults}. This overrides the parent method as it needs a
* specific {@code Results} implementation: {@code ForwardResults}.
* <p>
* In case when the {@code fql} represents a valid template record query (a test on recid /
* rowid of the record against the unique {@code _file._template} for current record of the
* single {@code QueryComponent}), a special {@link Results} object is returned with a single
* element that is the template record for the buffer of the component.
*
* @param persistence
* Object which is used to execute the query.
* @param fql
* FQL query string.
* @param args
* Substitution parameters to be inserted into placeholders within the FQL query string.
*
* @return An instance of {@code ForwardResults} which wrappers the
* {@code ScrollableResults} object returned by the execution of the query.
*
* @throws PersistenceException
* if there is an error executing the query.
*/
protected Results executeQuery(Persistence persistence, String fql, Object[] args)
throws PersistenceException
{
Long templateRowid = getTemplateQueryRowid(args);
if (templateRowid != null)
{
return super.executeQuery(persistence, fql, args);
}
// this query will always use the forward only mode
return new ForwardResults(persistence, executeScroll(persistence, fql, args));
}
/**
* Execute the query to create a {@code ScrollableResults} result set.
* <p>
* Override the parent's implementation to use a {@code ScrollMode} of {@code FORWARD_ONLY},
* which may permit the use of a server-side cursor by the JDBC driver.
*
* @param persistence
* Object which is used to execute the query.
* @param hql
* HQL query string.
* @param args
* Substitution parameters to be inserted into placeholders within the HQL query
* string.
*
* @return The {@code ScrollableResults} object returned by the execution of the query.
*
* @throws PersistenceException
* if there is an error executing the query.
*/
@Override
protected <T> ScrollableResults<T> executeScroll(Persistence persistence,
String hql,
Object[] args)
throws PersistenceException
{
return persistence.scroll(getEntities(),
hql,
args,
0,
0,
getDataRelation(),
ResultSet.TYPE_FORWARD_ONLY);
}
/**
* Given the scrollable results set for the query, create an object which
* implements the <code>Results</code> interface. It is this object which
* will actually be used to retrieve the primary key values of records to
* be loaded from the database.
* <p>
* This implementation returns a specialized {@link Results} object,
* which sorts the results obtained from the caller, according to one or
* more sort criterion provided by client code.
*
* @param results
* An object used to access the records which meet this query's criteria.
*
* @return <code>Results</code> object which presorts the results.
*
* @throws PersistenceException
* if there is any error creating results
*/
protected Results createResults(Results results)
throws PersistenceException
{
if (isClientWhere())
{
// Need to override FilteredResults to store entire records, since
// this is what SortedResults expects.
results = new FilteredResults(results,
Arrays.asList(getRecordBuffers()),
getWhereExpressions(),
false);
}
// Use incremental results for single-table queries (multi-table queries
// do not use sort phrase) which back {{DO | REPEAT} PRESLECT |
// FOR EACH} BREAK BY statements and which sort criteria added to the
// Presorter matches the leading components of the query's sort phrase.
if (getTableCount() == 1 && sorter.isBreakGroupsEnabled())
{
List<SortCriterion> criteria =
SortCriterion.parse(sort, components().get(0).getBuffer());
if (sorter.sortCriteriaMatchSortPhrase(criteria))
{
return sorter.createIncrementalResults(results);
}
}
return sorter.createSortedResults(results, false);
}
/**
* Indicate whether this query type is scrolling by its nature.
* <p>
* Subclasses which are not natively scrolling should override this method.
*
* @return <code>false</code>.
*/
protected boolean isNativelyScrolling()
{
return false;
}
/**
* Assemble a mixed select clause for an HQL query that returns recIds or full records.
* <ul>
* <li>
* For the tables that have any fields used in the BREAK BY clause
* or the number of columns is lower than the threshold,
* return all fields.
* </li>
* <li>
* Otherwise, retrieve only the recId.
* </li>
* </ul>
* @param buf
* String buffer into which clause is assembled.
*/
protected void assembleSelectClause(StringBuilder buf)
{
buf.append("select ");
ArrayList<QueryComponent> components = components();
if (components.size() == 1)
{
assembleComponent(buf, components.get(0), false);
}
else
{
Set<Integer> ids = getComponentsRequiringFullRecords(components);
for (int i = 0; i < components.size(); i++)
{
QueryComponent comp = components.get(i);
if (i > 0)
{
buf.append(", ");
}
boolean onlyPk = !ids.contains(i);
assembleComponent(buf, comp, onlyPk);
}
}
buf.append(" ");
}
/**
* Assemble the {@code order by} clause for an HQL query.
*
* @return List of <code>SortCriterion</code> objects which describe the sort behavior for this query.
*
* @throws PersistenceException
* never.
*/
protected ArrayList<SortCriterion> assembleOrderByClause()
throws PersistenceException
{
ArrayList<SortCriterion> sortCriteria = (sort == null) ? new ArrayList<>() : super.buildSortCriteria(sort);
Set<String> recIdsSet = null;
int sortCriteriaSize = sortCriteria.size();
for (int i = 0; i < sortCriteriaSize; i++)
{
if (sortCriteria.get(i).isPrimaryKey())
{
if (recIdsSet == null)
{
recIdsSet = new HashSet<>();
}
recIdsSet.add(sortCriteria.get(i).getAlias());
}
}
ArrayList<QueryComponent> components = components();
int componentsSize = components.size();
if (recIdsSet == null || recIdsSet.size() != componentsSize)
{
for (int i = 0; i < componentsSize; i++)
{
QueryComponent comp = components.get(i);
RecordBuffer buffer = comp.getBuffer();
String bufferAlias = buffer.getDMOAlias();
if (recIdsSet == null || !recIdsSet.contains(bufferAlias))
{
String critName = bufferAlias + "." + DatabaseManager.PRIMARY_KEY + " asc";
SortCriterion crit = new SortCriterion(buffer, critName);
sortCriteria.add(crit);
}
}
}
return assembleOrderByClause(sortCriteria);
}
/**
* Loop through a list of components and save the indices of elements from which all fields are needed.
*
* @param components
* List of query components.
*
* @return a {@code Set<Integer>} containing the indices of elements that meet the condition.
*/
private Set<Integer> getComponentsRequiringFullRecords(List<QueryComponent> components)
{
Set<Integer> ids = new HashSet<>();
Set<String> usedTables = new HashSet<>();
Iterator<Resolvable> iter = sorter.getSortKeys().iterator();
boolean hasOnlyFieldReferenceSortKeys = true;
for (int i = 0; iter.hasNext() && hasOnlyFieldReferenceSortKeys; i++)
{
Resolvable resolver = iter.next();
if (resolver instanceof FieldReference)
{
usedTables.add(((FieldReference) resolver).getDMOInterface().getName());
}
else
{
hasOnlyFieldReferenceSortKeys = false;
}
}
for (int i = 0; i < components.size(); i++)
{
// could not determine all tables used in the break by clause, therefore
// full records will be retrieved for all tables
if (!hasOnlyFieldReferenceSortKeys)
{
ids.add(i);
}
else
{
QueryComponent comp = components.get(i);
RecordBuffer buffer = comp.getBuffer();
// fetch full records if the number of columns < threshold or
// if fields from this table are used in the sorting
if (buffer.getNumFields() < MAX_TABLE_FIELDS ||
usedTables.contains(buffer.getDMOInterface().getName()))
{
ids.add(i);
}
}
}
return ids;
}
}