QueryComponent.java
/*
** Module : QueryComponent.java
** Abstract : A joinable, single-table query component used in a preselect query.
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 ECF 20150801 Refactored from PreselectQuery, which previously contained this as an inner class.
** 002 ECF 20150907 Reimplemented silent error mode processing of query substitution parameters.
** 003 CA 20160518 Change ParameterIndices usage to collect all substitution parameters, as their
** number might change (up or down) after ternary is normalized.
** 004 ECF 20160610 Changed access modifier of isIdOnly to protected.
** 005 OM 20160629 HQLPreprocessor needs the list of fields of the current index.
** 006 ECF 20160720 Fixed buffer lookup during HQL preprocessing.
** 007 ECF 20160825 Added missing javadoc.
** 008 ECF 20171212 Added handling for errors processing substitution parameters.
** 009 OM 20171212 HQLPreprocessor.get() has a new parameter used by subselects.
** SortCriterion.toSortExpression() is subselect aware.
** 010 OM 20180515 Added dynamic filters and dynamic sort.
** 011 OM 20180901 Improved dynamic filtering.
** 012 OM 20180918 Fixed addDynamicFilter() return value. Filtering is case insensitive and
** uses LIKE patterns.
** OM 20180924 Filtering on decimal fields uses STRING(f, format) function.
** 013 CA 20190722 Fixed datetimetz query arguments - they are treated like a ISO date, in string
** representation. UDF are used to compare them.
** CA 20190829 Allow Java native values as arguments.
** 014 CA 20191009 Added where and sort clause translation in case of bound buffers.
** 015 ECF 20200906 New ORM implementation.
** 016 ECF 20210820 Retrofit for query substitution parameter preprocessing fixes.
** 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.
** 017 CA 20230227 Fixed where clause translation when the external buffer's binding and definition does not
** match.
** 018 OM 20230404 The join navigation key may contain null/unknown values.
** 019 IAS 20230706 Runtime changes for CONTAINS with non-constant search expression.
** 020 OM 20231115 Fixed generation of nested multiplexed sub-queries.
** 021 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.
** 022 AL2 20240625 Query parameters that throw an exception on evaluation will now return ?.
** 023 TJD 20230724 Migrate earlyPublishEntities from List to Set to improve performance
** 024 ICP 20240114 The subselect inside a join will no longer be generated if it matches optimization
* criteria.
** 025 ICP 20250129 Used integer.of and int64.of to leverage caches instances.
*/
/*
** 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.util.*;
import java.util.function.*;
import com.goldencode.p2j.persist.lock.*;
import com.goldencode.p2j.util.*;
/**
* A helper container for a query component. A query component defines the buffer to be updated,
* the type of join to perform with the previous component (if any), the lock type for records
* fetched into the buffer, the where clause to be applied, and any query substitution parameters.
*/
class QueryComponent
implements QueryConstants
{
/** Joinable query which contains this query component */
private final Joinable query;
/** Original where clause, <i>before</i> preprocessing */
private String where;
/** A function to translate the where clause, just before it gets executed. */
private Supplier<String> translateWhere = null;
/** The where clause, including the dynamic filters, <i>before</i> preprocessing. */
private String dfWhere = null;
/** Record buffer updated by this component of the query */
private RecordBuffer buffer;
/** For bound buffers, this is the definition buffer*/
private final RecordBuffer definition;
/** Helper object for a join via a foreign key relation */
private final AbstractJoin join;
/** Record lock type */
private final LockType lockType;
/** Original arguments before query substitution arguments preprocessing. */
private final Object[] origArgs;
/** Base substitution arguments for this component */
private final Object[] args;
/** The substitution arguments with dynamic filtered ones inserted in front. */
private Object[] dfArgs = null;
/** {@code true} if component forms outer join with previous component; else {@code false} */
private final boolean outer;
/** Whether query should return primary key ID only, or entire DMO */
private final boolean idOnly;
/** Is this component the top/outermost among the query's components? */
private boolean top = true;
/**
* Array of field reference names and nulls used to replace substitution parameters when
* converting a nested looping (client-side) join to a server-side join
*/
private String[] referenceSubs;
/** Set of related buffers, if component participates in a server-side join */
private Set<RecordBuffer> relatedBuffers = null;
/** Iteration/retrieval mode (first, last, next, etc.) */
private int iteration;
/** Order by clause for FIRST/LAST iteration components */
private String sort;
/** FQL preprocessor */
private FQLPreprocessor fqlPreprocessor;
/** Position of {@code null} elements in argument list with which FQL was preprocessed */
private boolean[] nullArguments = null;
/** Sort index associated with this query component */
private SortIndex sortIndex;
/** Intermediately resolved arguments for this component */
private Object[] currentArgs = null;
/** Index information string as it is returned by INDEX-INFORMATION. */
private String indexInfo = 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;
/**
* Constructor.
*
* @param query
* Enclosing query.
* @param proxy
* DMO proxy which references the record buffer updated by this component of the query.
* @param join
* Helper object for a join via a foreign key relation. May be <code>null</code>.
* @param where
* Restriction criteria in FQL where clause format.
* @param translateWhere
* A function to translate the where clause, just before it gets executed.
* @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
* Requested record lock type.
* @param args
* Default (unresolved) query substitution arguments.
* @param iteration
* Type of iteration: FIRST, LAST, NEXT, PREVIOUS, or UNIQUE.
* @param outer
* <code>true</code> if this component is joined with the
* previous component via an outer join; <code>false</code>
* if there is no previous component, or if this component is
* joined with the previous component via an inner join.
*/
protected QueryComponent(Joinable query,
BufferReference proxy,
AbstractJoin join,
String where,
Supplier<String> translateWhere,
String indexInfo,
LockType lockType,
Object[] args,
int iteration,
boolean outer)
{
if (lockType == null)
{
throw new NullPointerException("Lock type must be specified");
}
if (args != null)
{
this.origArgs = new Object[args.length];
System.arraycopy(args, 0, origArgs, 0, args.length);
}
else
{
this.origArgs = null;
}
RecordBuffer buffer = proxy.buffer();
AbstractQuery.preprocessSubstitutionArguments(buffer.getBufferManager(),
query::isFindByRowid,
true,
args);
this.join = join;
this.where = where;
this.translateWhere = translateWhere;
this.args = args;
this.dfArgs = this.args;
this.dfWhere = this.where;
this.query = query;
this.buffer = buffer;
this.definition = proxy.definition();
this.iteration = iteration;
this.outer = outer;
this.indexInfo = indexInfo;
// override lock type if this is a temporary buffer to allow some downstream optimizations
this.lockType = (buffer.isTemporary() ? LockType.NONE : lockType);
// pull back entire record if possible, to avoid additional round trips to the database
this.idOnly = !LockType.NONE.equals(this.lockType);
}
/**
* Prepare the information needed to convert a nested looping (client-side) join of two
* tables to a server-side join of those tables.
*
* @param joinList
* List of enclosing query components, from outermost to innermost (if any), from the
* original nested query. Does not include the component currently being visited.
* @param buffer
* Record buffer associated with the query component currently being visited.
* @param join
* Original join helper object which joins the current query component with an outer
* component or external buffer.
* @param args
* Query substitution arguments for the where clause of the current query component.
*
* @return An object containing the information needed to convert the join from client-side
* to server-side.
*/
static ServerJoinData prepareServerJoinData(List<? extends QueryComponent> joinList,
RecordBuffer buffer,
AbstractJoin join,
Object[] args)
{
Object[] processedArgs = args;
String[] referenceSubs = null;
AbstractJoin newJoin = join;
Set<RecordBuffer> relatedBuffers = null;
if (!joinList.isEmpty())
{
relatedBuffers = new HashSet<>();
for (QueryComponent related : joinList)
{
relatedBuffers.add(related.getBuffer());
}
if (args != null)
{
int len = args.length;
List<Object> survivingArgs = new ArrayList<>();
referenceSubs = new String[len];
for (int i = 0; i < len; i++)
{
Object next = args[i];
if (next instanceof FieldReference)
{
FieldReference ref = (FieldReference) next;
if (relatedBuffers.contains(ref.getParentBuffer()))
{
referenceSubs[i] = ref.toString();
continue;
}
}
survivingArgs.add(next);
}
int survived = survivingArgs.size();
if (survived < len)
{
processedArgs = survivingArgs.toArray();
}
}
if (join != null)
{
RecordBuffer inverseBuffer = join.getInverse().buffer();
if (relatedBuffers.contains(inverseBuffer))
{
newJoin = new ServerLegacyKeyJoin(buffer, inverseBuffer);
}
}
}
return new ServerJoinData(newJoin, processedArgs, relatedBuffers, referenceSubs);
}
/**
* Translate the {@link #where} clause, using the {@link #translateWhere} function.
*/
protected void translateWhere()
{
if (translateWhere == null)
{
return;
}
this.where = translateWhere.get();
this.dfWhere = this.where;
this.translateWhere = null;
}
/**
* Get the FQL preprocessor associated with this component.
*
* @return FQL preprocessor.
*
* @throws PersistenceException
* if the preprocessor must first be created, and there is an error during its creation.
*/
protected FQLPreprocessor getFQLPreprocessor()
throws PersistenceException
{
if (fqlPreprocessor == null)
{
// NOTE: dynamic filtering must be done before resolveArgs() !
processDynamicFilters();
Object[] resolvedArgs = resolveArgs();
fqlPreprocessor = prepareFQLPreprocessor(resolvedArgs);
if (resolvedArgs == null || resolvedArgs.length == 0)
{
nullArguments = null;
}
else
{
nullArguments = new boolean[resolvedArgs.length];
for (int k = 0; k < resolvedArgs.length; k++)
{
Object param = resolvedArgs[k];
if (param instanceof FieldReference)
{
param = ((FieldReference) param).resolve();
}
nullArguments[k] = param instanceof BaseDataType &&
((BaseDataType) param).isUnknown();
}
}
}
return fqlPreprocessor;
}
/**
* Create the <code>FQLPreprocessor</code> which backs this component. If this component
* represents a nested FIRST or LAST iteration, the where clause portion associated with this
* component will be embedded within a subselect. To avoid conflicts with the enclosing
* where clause, this portion will be emitted without DMO alias qualifiers for its references
* to the associated buffer.
*
* @param queryArgs
* Query substitution parameters used in the preprocessor's creation.
*
* @return FQL preprocessor.
*
* @throws PersistenceException
* if there is an error creating the preprocessor.
*/
protected FQLPreprocessor prepareFQLPreprocessor(Object[] queryArgs)
throws PersistenceException
{
boolean subselect = !top && (iteration == FIRST || iteration == LAST);
RecordBuffer buffer = getBuffer();
String dropAlias = subselect ? buffer.getDMOAlias() : null;
String replacementAlias = dropAlias != null ? DBUtils.getSubselectAlias(dropAlias) : null;
return FQLPreprocessor.get(dfWhere,
buffer.getDatabase(),
buffer.getDialect(),
getReferencedBuffers(),
dfArgs,
referenceSubs,
true,
dropAlias,
replacementAlias,
false,
null,
true);
}
/**
* Get an array of all record buffers referenced by this query, including those directly
* involved in the query, as well as those (if any) indirectly referenced by inlined,
* converted, CAN-FIND expressions, and those related via a server-side join (if any).
*
* @return All record buffers referenced by this query.
*/
protected RecordBuffer[] getReferencedBuffers()
{
RecordBuffer[] qbufs = query.getReferencedBuffers();
if (relatedBuffers == null || relatedBuffers.isEmpty())
{
return qbufs;
}
int len1 = qbufs.length;
int len = len1 + relatedBuffers.size();
RecordBuffer[] all = new RecordBuffer[len];
int i = 0;
while (i < len1)
{
all[i] = qbufs[i];
i++;
}
for (RecordBuffer buf : relatedBuffers)
{
all[i] = buf;
i++;
}
return all;
}
/**
* Get substitution arguments for this component, resolving them first if necessary.
*
* @return Substitution arguments.
*
* @throws PersistenceException
* if there is any error creating the FQL preprocessor, which
* is necessary to properly filter/order the arguments.
*/
protected Object[] getArgs()
throws PersistenceException
{
if (dfArgs != null && currentArgs == null)
{
currentArgs = filterArgs(getFQLPreprocessor());
}
return currentArgs;
}
/**
* Reset current arguments, so that they will need to be resolved the next time they are
* requested. The fqlPreprocessor is dropped only if it was inlined (and the inlined nodes
* need to be re-evaluated with the new arguments) or if the argument list contains some
* other configuration of null values (they were also replaced with "is [not] null" fql constructs).
*
* @param forceOrigArgs
* Flag indicating that the original arguments used at the construction of the query must be
* re-calculated.
*/
protected void resetArgs(boolean forceOrigArgs)
{
if (forceOrigArgs && origArgs != null)
{
System.arraycopy(origArgs, 0, args, 0, origArgs.length);
AbstractQuery.preprocessSubstitutionArguments(buffer.getBufferManager(),
query::isFindByRowid,
true,
args);
dfArgs = args;
}
currentArgs = null;
// Since the arguments were reset, the FQL preprocessor may need to be reset, too.
if (fqlPreprocessor == null)
{
return; // nothing to do
}
// if some FQL nodes were inlined (?)
if (fqlPreprocessor.wasInlined())
{
resetFQLPreprocessor();
return;
}
// check the size of the parameter list (should always be the same, anyway)
processDynamicFilters();
Object[] params = resolveArgs();
if ((params == null && nullArguments != null) || (params != null && nullArguments == null))
{
resetFQLPreprocessor();
return;
}
if (params == null)
{
// nullArguments is null too, so we keep the fql
return;
}
if (params.length != nullArguments.length)
{
resetFQLPreprocessor();
return;
}
// if params and nullArguments have the same length check for null pattern
for (int k = 0; k < params.length; k++)
{
Object param = params[k];
if (!forceOrigArgs && param instanceof FieldReference)
{
param = ((FieldReference) param).resolve();
}
if (nullArguments[k] != (param instanceof BaseDataType &&
((BaseDataType) param).isUnknown()))
{
resetFQLPreprocessor();
return;
}
}
}
/**
* Reset the FQL preprocessor, so that it needs to be recreated the next time it is requested.
*/
protected void resetFQLPreprocessor()
{
fqlPreprocessor = null;
nullArguments = null; // keep in sync
}
/**
* Get the (mostly) resolved query substitution parameters required by
* this query component and return them in an array. The array's
* elements will match the positional placeholders in the query's where
* clause <em>after</em> it has been preprocessed. The preprocessing
* step may have reordered or dropped certain parameters. The only
* parameters which will not have been fully resolved will be those
* <code>FieldReference</code> parameters which are actually resolved by
* the query itself, when the query's full FQL is being assembled and
* when the final array of substitution parameters is prepared.
*
* @param preproc
* FQL preprocessor which maps parameter indices to the original parameters, in case any were
* changed during FQL where clause preprocessing.
*
* @return Array of filtered, resolved parameters.
*/
protected Object[] filterArgs(FQLPreprocessor preproc)
{
// in this case the processDynamicFilters() is not required to be called before calling
// resolveArgs() because it was called before constructing the preprocessor
Object[] intermediate = resolveArgs();
if (intermediate == null)
{
return null;
}
ParameterIndices pi = preproc.getParameterIndices();
int len = pi.getCount();
Object[] filteredArgs = new Object[len];
for (int i = 0, count = 0; count < len; i++)
{
Integer nextIndex = pi.getIndex(i);
if (nextIndex == null)
{
// original substitution parameter was inlined into FQL by preprocessor; skip it
continue;
}
// 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 = intermediate[nextIndex];
filteredArgs[count++] = next;
}
return filteredArgs;
}
/**
* Set the array of field reference names and nulls used when converting a nested loop where
* clause to a server-join where clause.
*
* @param referenceSubs
* Array of field reference names and nulls.
*/
protected void setReferenceSubs(String[] referenceSubs)
{
this.referenceSubs = referenceSubs;
}
/**
* Store a set of outer component buffers to which this component is related through a server
* side join.
*
* @param relatedBuffers
* Set of related buffers; may be {@code null}.
*/
protected void setRelatedBuffers(Set<RecordBuffer> relatedBuffers)
{
this.relatedBuffers = relatedBuffers;
}
/**
* Processes all dynamic filters altering the original {@code where} predicate and inserting
* optional substitution arguments in the list of static ones.
* <p>
* This method must be always called before accessing the where predicate or substitutions
* arguments, otherwise the dynamic filtering will not be correctly applied.
*/
void processDynamicFilters()
{
if (dynamicFilters == null || dynamicFilters.isEmpty())
{
dfArgs = args;
dfWhere = where;
return;
}
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;
}
}
/**
* Resolve all resolvable query substitution parameters, with the exception of
* {@code FieldReference}s. The latter are only query substitution parameters when it
* is necessary to join the enclosing query's results with those of a related query, as
* would be the case when each query is a component of a {@link CompoundQuery}.
*
* @return Array of query substitution parameters after all non-{@code FieldReference}
* resolvable parameters have been resolved.
*/
private Object[] resolveArgs()
{
if (dfArgs == null)
{
return null;
}
int len = dfArgs.length;
Object[] resolvedArgs = new Object[len];
for (int i = 0; i < len; i++)
{
resolvedArgs[i] = resolveArg(dfArgs[i]);
}
return resolvedArgs;
}
/**
* 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 Resolvable)
{
return ((Resolvable) arg).resolve();
}
else if (arg instanceof BaseDataType)
{
return ((BaseDataType) arg).duplicate();
}
else if (arg instanceof Integer)
{
return integer.of((Integer) arg);
}
else if (arg instanceof Long)
{
return int64.of((Long) arg);
}
else if (arg instanceof Double)
{
return new decimal((Double) arg);
}
else if (arg instanceof Float)
{
return new decimal((Float) arg);
}
else if (arg instanceof String)
{
return new character((String) arg);
}
else if (arg instanceof P2JQuery.ParamResolver)
{
return arg;
}
else
{
throw new IllegalArgumentException("Argument type " + arg.getClass() + " is not known.");
}
}
/**
* Indicate whether the query component should return a primary key ID
* only, or the entire DMO instance.
*
* @return <code>true</code> for a projection query which returns only
* the ID; <code>false</code> to fetch the entire DMO.
*/
protected boolean isIdOnly()
{
return idOnly;
}
/**
* Get the query which encloses this component.
*
* @return Enclosing query.
*/
protected Joinable getEnclosingQuery()
{
return query;
}
/**
* Enable the {@link #translateWhere} for this component (if not already enabled).
*
* @param translate
* The translation function, which will receive as argument the {@link #where} predicate.
*/
void enableTranslateWhere(Function<String, String> translate)
{
if (translateWhere != null)
{
return;
}
translateWhere = () -> translate.apply(where);
}
/**
* Get record buffer for this component of the query.
*
* @return Record buffer.
*/
RecordBuffer getBuffer()
{
return buffer;
}
/**
* Set the record buffer for this component's query.
*
* @param buffer
* Record buffer.
*/
void setBuffer(RecordBuffer buffer)
{
this.buffer = buffer;
}
/**
* Get definition record buffer for this component of the query.
*
* @return Record buffer.
*/
RecordBuffer getDefinitionBuffer()
{
return definition;
}
/**
* Get restriction criteria in FQL where clause format for this component (before the FQL
* preprocessor has preprocessed it). If some dynamic filters were added, the returned string
* contains them, too.
*
* @return Un-preprocessed where clause.
*/
String getOriginalWhere()
{
return dfWhere;
}
/**
* Get helper object for a join via a foreign key relation, if any.
*
* @return Join helper, or <code>null</code> if there is none.
*/
AbstractJoin getJoin()
{
return join;
}
/**
* Report if component forms a sub select inside a join.
*
* @return <code>true</code> if there is a sub select in the join; else <code>false</code>.
*/
public boolean isJoinWithSubselect()
throws PersistenceException
{
return !top && (iteration == FIRST || iteration == LAST) && getFQLPreprocessor().isSubselectNeeded();
}
/**
* Get restriction criteria in FQL where clause format for this component (pre-processed).
*
* @return Where clause.
*
* @throws PersistenceException
* if there is an error creating the {@code FQLPreprocessor} from which the
* preprocessed where clause is retrieved.
*/
FQLExpression getWhere()
throws PersistenceException
{
FQLExpression baseWhere = getFQLPreprocessor().getFQL();
if (!isJoinWithSubselect())
{
return baseWhere;
}
// generate a where expression which tests for equality between the DMO's primary key and
// the primary key of the record found using the core where clause in a sub-select which
// finds a single, FIRST or LAST record, as determined by the sort clause of this component:
// dmo.id = (select from DMO where <baseWhere> order by <sort> single)
// 'single' is a custom FQL extension which instructs Hibernate to emit dialect-specific
// SQL to limit the result to 1 row
String dmoAlias = buffer.getDMOAlias();
String uniqueAlias = DBUtils.getSubselectAlias(dmoAlias);
FQLExpression exp = new FQLExpression();
boolean multiplex = buffer.isMultiplexed();
exp.append(dmoAlias).append(".").append(DatabaseManager.PRIMARY_KEY)
.append(" = (select ").append(uniqueAlias).append(".").append(DatabaseManager.PRIMARY_KEY)
.append(" from ").append(buffer.getDMOImplementationName()).append(" as ").append(uniqueAlias);
if (baseWhere != null || join != null || multiplex)
{
exp.append(" where ");
if (multiplex)
{
boolean joinOrWhere = (join != null || baseWhere != null);
if (joinOrWhere)
{
exp.append("(");
}
exp.append(uniqueAlias);
exp.append(".");
exp.append(TemporaryBuffer.MULTIPLEX_FIELD_NAME);
exp.append(" = ", true);
if (joinOrWhere)
{
exp.append(") and ");
}
exp.setMultiplexed(true); // mark the exp this component as already multiplexed
}
if (baseWhere != null)
{
exp.append("(").append(baseWhere).append(")");
}
if (join != null)
{
if (baseWhere != null)
{
exp.append(" and ");
}
// need to strip out DMO aliases
Object[] joinArgs = join.getUnresolvedParameters().toArray();
FQLExpression[][] fqls = join.getFQL();
FQLExpression joinWhere = new FQLExpression();
for (int i = 0; i < fqls.length; i++)
{
boolean nullArg = (joinArgs.length != 0) &&
((joinArgs[i] == null) ||
(joinArgs[i] instanceof BaseDataType &&
((BaseDataType) joinArgs[i]).isUnknown()));
joinWhere.append(fqls[i][nullArg ? AbstractJoin.NULL_FIELD : AbstractJoin.NOT_NULL_FIELD]);
}
FQLPreprocessor preproc = FQLPreprocessor.get(joinWhere.toFinalExpression(),
buffer.getDatabase(),
buffer.getDialect(),
query.getReferencedBuffers(),
joinArgs,
null,
false,
dmoAlias,
uniqueAlias,
false,
null);
FQLExpression fql = preproc.getFQL();
if (fql != null)
{
exp.append("(").append(fql).append(")");
}
}
}
exp.append(" order by ").append(sort).append(" single)");
return exp;
}
/**
* 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.
*/
boolean addDynamicFilter(FieldReference fr, BaseDataType val, String format)
{
if (dynamicFilters == null)
{
dynamicFilters = new LinkedHashMap<>();
dynamicFormats = new HashMap<>();
}
dynamicFilters.remove(fr); // the list is reordered, not reassigned to same position
dynamicFilters.put(fr, val);
dynamicFormats.put(fr, format);
currentArgs = null;
resetFQLPreprocessor(); // force re-parsing the enriched FQL
return true;
}
/**
* Clears any dynamically filters added at runtime. The query predicate will be restored to its
* form from generated at conversion time.
*/
void clearDynamicFilters()
{
currentArgs = null;
dynamicFilters = null;
dynamicFormats = null;
resetFQLPreprocessor(); // force re-parsing the enriched FQL
}
/**
* Get lock type for records fetched into this component's buffer.
*
* @return Lock type.
*/
LockType getLockType()
{
return lockType;
}
/**
* Get unresolved substitution arguments for this component, if any.
*
* @return Substitution arguments, or <code>null</code> if none.
*/
Object[] getRawArgs()
{
return dfArgs;
}
/**
* Get iteration/retrieval mode (first, last, next, etc.).
*
* @return Retrieval mode from {@link QueryConstants}.
*/
int getIteration()
{
return iteration;
}
/**
* Set iteration/retrieval mode (first, last, next, etc.).
*
* @param iteration
* Retrieval mode from {@link QueryConstants}.
* @param sort
* Order by clause which should be associated with this query component. Must be
* non-<code>null</code> for iteration types <code>FIRST</code> and
* <code>LAST</code>.
*
* @throws IllegalArgumentException
* if <code>iteration</code> type is <code>FIRST</code> or <code>LAST</code>, but
* <code>sort</code> is <code>null</code>.
* @throws PersistenceException
* if <code>sort</code> clause is invalid.
*/
void setIteration(int iteration, String sort)
throws PersistenceException
{
if (iteration == FIRST || iteration == LAST)
{
if (sort == null)
{
throw new IllegalArgumentException(
"Sort cannot be null with FIRST/LAST iteration type");
}
// generate an order by clause using no DMO aliases, suitable for a sub-select
List<SortCriterion> criteria = SortCriterion.parse(sort, buffer);
StringBuilder buf = new StringBuilder();
for (SortCriterion sc : criteria)
{
if (buf.length() > 0)
{
buf.append(", ");
}
buf.append(sc.toSortExpression(iteration == LAST, false, !top, true));
}
this.sort = buf.toString();
}
else
{
this.sort = null;
}
this.iteration = iteration;
}
/**
* Mark this query component as being nested within an outer component (i.e., not the top
* component in a nested looping query).
*/
void setNotTop()
{
this.top = false;
}
/**
* Indicate whether this component is joined with the previous component
* via a left outer join.
*
* @return <code>true</code> if an outer join, <code>false</code> if
* an inner join or no join (i.e., no previous component).
*/
boolean isOuter()
{
return outer;
}
/**
* Get an iterator on the ANSI-style join subexpressions which must be
* merged by the enclosing query into the overall FQL query statement
* in order to properly join associated composite element lists (if
* any).
*
* @return An iterator on join subexpressions. The iterator may be
* empty, but will not be <code>null</code>.
*
* @throws PersistenceException
* if there is an error creating the
* <code>FQLPreprocessor</code> from which the composite join
* subexpressions are gathered.
*/
Iterator<String> compositeJoins()
throws PersistenceException
{
return getFQLPreprocessor().ansiJoins();
}
/**
* Get an unmodifiable map of DMO entity names to sets of the names of
* the DMO properties used as restriction criteria in the preprocessed
* where clause.
*
* @return Map of DMO entity names to sets of property names, or
* <code>null</code> if the where clause for this component
* contained no restriction properties.
*
* @throws PersistenceException
* if there is an error creating the <code>FQLPreprocessor</code> from which the restriction
* properties are gathered.
*/
Map<String, Set<String>> getRestrictionProperties()
throws PersistenceException
{
return getFQLPreprocessor().getRestrictionProperties();
}
/**
* Get sort criteria in FQL order by clause format for this component.
*
* @return Sort index associated with this component.
*/
SortIndex getSortIndex()
{
return sortIndex;
}
/**
* Set sort criteria in FQL order by clause format for this component.
*
* @param sortIndex
* Sort index.
*/
void setSortIndex(SortIndex sortIndex)
{
this.sortIndex = sortIndex;
}
/**
* Get the index information string as it is returned by INDEX-INFORMATION.
*
* @return Index information string.
*/
String getIndexInfo()
{
return indexInfo;
}
/**
* Information required to create a server-side join between two tables.
*/
static class ServerJoinData
{
/** Helper object which generates a where clause snippet to join two tables */
final AbstractJoin join;
/** Query substitution arguments used in the where clause snippet, which survive the join */
final Object[] args;
/** Set of related buffers referenced by the join */
final Set<RecordBuffer> relatedBuffers;
/** Array of field reference names and nulls */
final String[] referenceSubs;
/**
* Constructor.
*
* @param join
* Helper object which generates a where clause snippet to join two tables.
* @param args
* Query substitution arguments used in the where clause snippet, which survive
* the join.
* @param relatedBuffers
* Set of related buffers referenced by the join.
* @param referenceSubs
* Array of field reference names and nulls, where the names are to be substituted
* for the positionally equivalent substitution parameters when converting a
* where clause used by a nested loop to a where clause using a server-side join.
*/
ServerJoinData(AbstractJoin join,
Object[] args,
Set<RecordBuffer> relatedBuffers,
String[] referenceSubs)
{
this.join = join;
this.args = args;
this.relatedBuffers = relatedBuffers;
this.referenceSubs = referenceSubs;
}
}
}