FQLPreprocessor.java

/*
** Module   : FQLPreprocessor.java
** Abstract : Rewrites FQL where clauses under certain conditions
**
** Copyright (c) 2004-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 ECF 20060317   @25230 Created initial version. Identifies certain
**                           patterns in a where clause string, which
**                           indicate unsupported HQL "shortcut" syntax,
**                           and rewrites the where clause to be compliant
**                           with supported, HQL syntax.
** 002 ECF 20060713   @28049 Expanded purpose of this class to modify HQL
**                           to trim trailing whitespace from all text
**                           properties. A SQL RTRIM function is inserted
**                           around each qualified reference to a text
**                           property.
** 003 ECF 20060814   @28689 Added support for ternary clauses. These are
**                           implemented using CASE WHEN... clauses.
** 004 ECF 20060901   @29152 Changed exception processing. We now throw
**                           IllegalArgumentException with a verbose
**                           message from preprocessTextNodes() upon
**                           encountering an unrecognized alias.
** 005 ECF 20060906   @29257 Workaround for Hibernate HQL parser defect.
**                           The 'THEN' and 'ELSE' component expressions
**                           of a 'CASE WHEN' clause must be enclosed in
**                           parentheses to prevent the parser from
**                           blowing up.
** 006 ECF 20061004   @30153 Fix for trailing trim (rtrim) processing.
**                           Functions which return text must be enclosed
**                           within the rtrim() function as well.
** 007 ECF 20061020   @30588 Added support for string concatenation
**                           operator (||). Added to the set of supported,
**                           binary operators.
** 008 ECF 20061030   @30792 Commented code which rewrites unary logical
**                           expressions. This code would rewrite all such
**                           expressions as binary, equality comparisons
**                           with the logical constant 'true', in order to
**                           work around a defect in the Hibernate HQL
**                           parser. That defect has now been patched,
**                           rendering the workaround obsolete with our
**                           modified version of Hibernate 3.0.5. However,
**                           moving to a standard Hibernate release which
**                           does not have this patch will require that
**                           this code be reactivated.
** 009 ECF 20061119   @31324 Added collection of restriction criteria
**                           property names. These are needed when
**                           processing ChangeBroker notifications about
**                           DMO state changes to determine whether a
**                           preselected result set should be invalidated
**                           by AdaptiveQuery.
** 010 ECF 20061121   @31344 Fixed NPE. Added safety code to new method
**                           getRestrictionProperties().
** 011 ECF 20070328   @32645 Added caching of HQLPreprocessor instances.
**                           Made constructor private and added factory
**                           methods to retrieve cached or new instances.
** 012 ECF 20070502   @33370 Added limited support for subselect phrases.
**                           Only those features required to support the
**                           server-side implementation of converted,
**                           nested CAN-FIND statements are implemented.
**                           Also removed need to pass RecordBuffer
**                           instances to factory method.
** 013 ECF 20070723   @34653 Fixed generation of ANSI join clauses. In
**                           cases where multiple different indices in a
**                           composite inner class were being referenced,
**                           we were only creating a single join for that
**                           composite.
** 014 ECF 20071216   @36492 Added injection of error handling functions
**                           around certain expressions. This allows
**                           proper error handling of converted CAN-FIND
**                           statements which are nested within a query's
**                           where clause.
** 015 ECF 20080306   @37477 Added significant, dialect-driven capability.
**                           Added support for computed columns. In cases
**                           where a database dialect does not support
**                           embedding functions within indices but uses
**                           computed instead, we replace certain function
**                           signatures within where clauses with
**                           references to those computed columns. This
**                           presumably enables the database's query
**                           planner to choose matching indices more
**                           easily. Added "manual" user defined function
**                           overloading for cases where the backing
**                           dialect does not support natural overloading.
** 016 ECF 20080328   @37724 Fixed mainWalk(). An attempt to check the
**                           type of a standalone alias (i.e., no property
**                           child) was causing an NPE in DataTypeHelper.
** 017 ECF 20080330   @37738 Modified to support method change in
**                           DatabaseManager.
** 018 ECF 20080508   @38485 Added support for function overloading in
**                           dirty databases. Functions are now registered
**                           by Database object rather than database name.
** 019 ECF 20080606   @38606 Added new factory method. Accepts Database
**                           P2JDialect args rather than Persistence. This
**                           is necessary for dirty database use of this
**                           class, where there is no Persistence instance
**                           associated with a dirty database. Changed
**                           CacheKey to use Database instead of
**                           Persistence for the same reason.
** 020 ECF 20080817   @39555 Map restriction properties to DMO entity name
**                           instead of DMO implementation class.
** 021 ECF 20080930   @40078 Added substitution parameter inlining. Query
**                           substitution parameters which represent an
**                           operand in subexpressions which perform range
**                           checks (i.e., >, >=, <, <=, some forms of
**                           LIKE) may be inlined into the where clause.
**                           With certain database dialects, this may
**                           allow better query plans to be chosen, at the
**                           expense of reparsing the query.
** 022 ECF 20081015   @40092 Fixed regression caused by #021 (@40078).
**                           Ensure substitution parameters are properly
**                           preprocessed to avoid trailing whitespace in
**                           strings and out-of-range date values.
** 023 ECF 20081015   @40094 Fixed inlineSubstitutionParameter(). Use a
**                           dialect-specific date format to avoid query
**                           failure with BC dates.
** 024 ECF 20081031   @40307 Fixed parameter inlining and caching. Certain
**                           HQLPreprocessor instances were being cached
**                           without taking inlining status into account.
**                           Also, inlineSubstitutionParameter() must
**                           resolve FieldReferences, but not other types
**                           of Resolvables.
** 025 SVL 20090118   @41170 Comparisons with the unknown are handled
**                           properly.
** 026 SVL 20090121   @41188 <= ? is translated to "true" and < ? is
**                           translated to "is not null".
** 027 SVL 20090122   @41198 Do not look into cache if we have a query
**                           with unknown parameter(s).
** 028 ECF 20090310   @41501 Fixed performance problem with is [not] null
**                           refactoring. In cases where such a test is
**                           made against a non-nullable column/property,
**                           the expression can be optimized to avoid any
**                           record reads. For example, 'table.id is null'
**                           must always be false and 'table.id is not
**                           null' must always be true. With some database
**                           implementations, this avoids expensive query
**                           plan choices.
** 029 SVL 20090415   @41797 Added roll up of the expressions containing
**                           booleans (during HQL tree processing).
** 030 GES 20090421   @41830 Matched utility class package change.
** 031 SVL 20090428   @42028 Conjunction of index(alias_compositeXXXX)=Y
**                           conditions is added to the top level of the AST
**                           (instead of keeping this conditions linked to the
**                           original conditions and therefore spreading
**                           associations between the original conditions on
**                           them too).
** 032 GES 20090429   @42050 Match package and class name changes.
** 033 GES 20090518   @42390 Import change.
** 034 ECF 20090601   @42584 Added support for Progress isolation leak quirk.
**                           The execution of some queries (the current
**                           understanding is any where clause which checks a
**                           database field for inequality with any operand)
**                           will trigger the premature publication of all
**                           uncommitted changes in related DMO types to all
**                           sessions. The publication is handled by the dirty
**                           database manager, but the identification of the
**                           trigger conditions is managed here.
** 035 ECF 20090617   @42764 Fixed function overloading. Overloading was not
**                           being performed in cases where no substitution
**                           parameters were in use.
** 036 ECF 20090716   @43218 Added support for lazy initialization of record
**                           buffers. resolveBuffer() now calls buffer's
**                           initialize() method, to ensure buffers referenced
**                           in subselects are initialized.
** 037 SVL 20090810   @43577 "then ? else ?" statement is replaced with "then
**                           cast(? as <datatype>) else cast(? as <datatype>)"
**                           for H2 dialect.
** 038 SVL 20110622          Convert \\ to \ for the H2 dialect. Call
**                           postprocessStringLiteral for newly inlined string
**                           literals.
** 039 SVL 20110814   @43700 Improved unknown handling for "? <comparison
**                           operator> {const|subst}", "? <comparison
**                           operator> ?" and standalone unknowns.
** 040 SVL 20120331          Upgraded to Hibernate 4. Make explicit cast in ternary also when
**                           we have a single substitution parameter. Added handling for DATETIME
**                           and DATETIMETZ types.
** 041 OM  20130602          Added support for named parameters for datetime-tz datatype.
**                           Added preprocessing to inject session attributes into hql.
** 042 CA  20130822          The DATE-FORMAT attribute must be accessed via SessionUtils.
** 043 SVL 20140210          Use HQLExpression instead of HQL string.
** 044 VMN 20140328          Added support for custom denormalization of fields with extent.
** 045 ECF 20140403          Integrated Aast API changes; replaced Apache commons logging with
**                           J2SE logging.
** 046 OM  20140410          Fixed support case-sensitive fields in indexes.
** 047 OM  20140917          Added unix-escapes support for MATCHES / LIKE operator.
**                           Removed needSingleBackslash as all dialects behave the same.
** 048 OM  20141111          Refactored setDateOrder() to setDateFormat() in SessionUtils.
** 049 SVL 20150205          Fields of temporary tables should not be denormalized.
** 050 OM  20150422          Reworked relational operations to match the Progress semantics from
**                           the POV of NULL values (in P4GL, ? is handled as a normal value).
** 051 OM  20150421          Removed queries with unknown parameters from cache.
** 052 OM  20150618          Avoided name collisions of NUM_LITERAL and SUBST in rewriteAlias().
** 053 OM  20150619          Removed field trimming when they are operands of MATCHES or BEGINS.
** 054 ECF 20150625          Account for possible functions which can enclose alias.property
**                           references w.r.t. #050.
** 055 ECF 20150801          Added support for compound query optimization.
** 056 ECF 20151201          Made further improvements to unknown value handling in binary
**                           expressions.
** 057 OM  20151130          Rewrite rtrim function for SQL Server dialects. Fixed HQL/SQL
**                           function mapping (registerFunction and manuallyOverload).
** 058 ECF 20151218          Fixed performance regression introduced by #054.
**     OM  20160112          Removed completely boolean literals if the dialect does not support
**                           them. Inlined logical parameters used in equality tests expressions.
** 059 ECF 20160130          Omit checkError wrapping of native, built-in database functions.
** 060 EVL 20160223          Javadoc fixes to make compatible with Oracle Java 8 for Solaris 10.
** 061 ECF 20160225          Changes required by PropertyHelper rewrite.
** 062 IAS 20160331          Fixed CacheKey.equals()
** 063 OM  20160316          Checking if the query should performed on recid/rowid lookup table.
** 064 CA  20160513          Fixed normalization of (not) equal fields which are non-mandatory
**                           (they were attached to the OR themselves, not a fresh copy).
**                           Ternary is normalized at runtime into an equivalent logical 
**                           expression.
** 065 ECF 20160606          Minor performance fixes.
** 066 OM  20160603          Fixed rtrimming to space character only.
** 067 OM  20160629          Applied 4GL semantics for comparing indexed fields to unknown values.
** 068 OM  20160715          Improved detection of 'FindByRowid' queries.
** 069 ECF 20160720          Fixed buffer lookup; rationalized multiple factory methods into a
**                           single one.
** 070 OM  20160823          Avoid double rtrimming char fields for dialects that require CCs.
** 071 OM  20160907          matchesList() UDF morphed into IN operator where possible. Dropped
**                           trimming where possible.
** 072 ECF 20160901          Added diagnostic logging.
** 073 OM  20161216          rtrim() nodes were injected too aggressive.
** 074 HC  20170122          Added method getRegisteredFunction.
** 075 OM  20171120          Do not 'trim' if a node is marked as unknown.
** 076 OM  20171206          Fixed normalized composition of extent fields. Workaround Hibernate
**                           blemish that caused the field having the same name as the table to
**                           be replaced by id field if the table is part of both inner and outer
**                           selects in a nested query. Added support for subselects nested in 
**                           quick delete statements.
**     ECF 20171217          Fixed composite AST in generateJoins; cleanup parse-time resources
**                           after walk, so they don't bloat saved instances.
**     OM  20171219          Added support for FieldReference parameters in getFindByRowid().
** 077 OM  20180301          Removed unused import.
** 078 OM  20180704          Fixed [replacementAlias] emitted in case of nested CAN-FINDs.
** 079 ECF 20180713          Prevent emitting rtrim nested within another trimming function.
** 080 OM  20180730          Fixed HQLAst handling in trySimplifyBooleans().
** 081 ECF 20181226          Cast a query substitution parameter to its data type if it is the
**                           child of an arithmetic operator, since Hibernate may infer the wrong
**                           type in this case.
** 082 ECF 20190306          Modified getDenormalizedFields due to TableMapper change.
** 083 ECF 20190620          EmptyIterator API change.
** 084 VVT 20190805          Fix for bug #4100: after a node is detached, the 'parent' field must
**                           be updated.
** 085 CA  20190722          Fixed datetimetz query arguments - they are treated like a ISO date,
**                           in string representation. UDF are used to compare them.
** 086 CA  20191009          Added where and sort clause translation in case of bound buffers.
** 087 ECF 20200906          New ORM implementation.
** 088 AIL 20200831          Skipped parenthesis inside simplifyUnknowns.
** 089 OM  20201001          Dropped redundant ORDER BY elements in multi-table queries.
**     OM  20210309          Do not use DmoMeta as key in TableMapper because temp-tables share the same
**                           DmoMeta instance.
**     OM  20210412          Replaced Hibernate-specific casts with dialect-specific ones.
**     OM  20210423          Simplified contains() functions with false when the pattern is empty. Operation
**                           is performed earlier in order to benefit from later logicals simplifications.
**     IAS 20210727          Re-write toString(value, fmt) UDF call adding value of the 
**                           SESSION:DATE-FORMAT as a third argument when 'value' is date or datetime.
**     IAS 20210804          Support for non-Java UDFs
**     IAS 20210903          Re-write toString(datetime, fmt) SQL UDF call adding value of the
**                           SESSION:TIMEZONE as a 4th argument.
**     IAS 20210905          Re-working re-writing queries with the values of the mutable SESSION 
**                           attributes
**     IAS 20210906          Re-write toString(long, fmt) SQL UDF call adding value of the
**                           SESSION:TIMEZONE as a 3rd argument (required if fmt starts with 'HH:MM').
**     IAS 20210913          Rtrim the result of the toString() UDF.
**     OM  20210917          Small copy/paste bug fixed in equals() method.
**     IAS 20210922          Added 'dependsOnSessionAttribute' flag.
**     IAS 20210924          Do not trim UDF arguments.
**     OM  20210927          FFC invalidates the records whose changed fields are used in query predicates.
**     AL2 20211101          Return a copy of the hql when calling getFQL().
**     IAS 20220321          Suppress grafting SESSION:TIMEZONE to the format-driven SQL STRING UDFs calls.
**     OM  20220516          Avoid multiple initialisation of convertCanDoToIn in mainWalk().
**     IAS 20220610          Fixed "guarded_" prefix prepending.
**     OM  20220819          Keep LPARENS around SUBSELECT nodes when cleaning up TERNARY.
**     TJD 20220504          Upgrade do Java 11 minor changes
**     IAS 20220919          Fixed session attributes injection.
**     IAS 20220921          Dialect-specific UDF schema and UDF detection logic.
**     OM  20221031          If the dialect supports character typed columns which handle case-insensivity
**                           and rtrimming the same way as 4GL, do not wrap them in those functions.
**     OM  20221103          Renamed class to FQLPreprocessor. Dropped UPPER wrapping for CI dialects.
**     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.
**     IAS 20230209          Fixed ${TZ} placeholder value
** 090 AL2 20230210          Use prop matches to identify if the where clause is a look-up on unique index.
**     DDF 20230306          Simplify rtrim if isAutoRtrimAndCi() is true and avoid rtrimming further 
**                           into preprocessing (refs: #7108).
**     DDF 20230309          Add node to needTrim in some cases only if dialect allows so.
** 091 DDF 20230406          Replaced isAutoRtrimAndCi with isAutoRtrim and isAutoCi. In simplifyProperties,
**                           only remove the property that the dialect handles automatically.
** 092 DDF 20230411          Added back the simplifyProperties guard.
** 093 DDF 20230426          Use exists(select 1 from <table>) instead of exists(from <table>).
** 094 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
** 095 OM  20230511          Added single buffer translation mode.
** 096 DDF 20230608          Made the size of prepared instances cache configurable.
**     DDF 20230613          Removed the static block that reads the configuration size of the cache.
**                           CacheManager will handle the finding of the configuration size and the
**                           cache creation.
**     DDF 20230627          Reordered class members.
** 097 AB  20230925          Added the attribute "hasContains" and the method hasContainsKeyword(). 
**                           Changed FQLPreprocessor.fixEmptyContains() to change the value of 
**                           "hasContains" when it's the case.
**     AB 20230926           Renamed the variable "hasContains" to "hasNonConstantContains".
**                           Added isConstant(HQLAst node) method.
**     AB 20230929           Rename variable "hasNonConstantContains" to "nonConstantContains" for more
**                           clarity. 
**                           Changed javadoc for isConstant() and nonConstantContains.
**                           Moved the responsability of assigning "nonConstantContains" to mainWalk().
** 098 DDF 20230818          Simplify upper and rtrim methods when the parent is of type FUNCTION. 
** 099 DDF 20230911          Cache parsed HQLAsts of where clauses.
**     DDF 20231101          Made astCache configurable using CacheManager.
** 100 IAS 20230208          Dialect-specific error handler support.
** 101 OM  20240106          Do not optimize queries with "contains()" function in their predicate.
** 102 AL2 20240225          Better handle expanded extent fields in the queryProperties field.
** 103 RAA 20240319          Skip augmentForUnknownValue if the dialect permits null equality checks.
** 104 DDF 20240319          Reconstruct clause for augmented constructs when comparing two character fields,
**                           which eventually boosts performance of the query.
**     DDF 20240320          Renamed areTwoFieldsAugmented() method, adjusted the condition for
**                           reconstruction because the augmented node parents should be the same and
**                           fixed root rewrite as the node content could not be removed.
**     DDF 20240322          Allow reconstruction in honorDisjunctiveForm() even when the constructs are
**                           not alone in the clause (there are additional conditions).
**     DDF 20240325          Reuse parent node when there are other conditions in the clause aside from
**                           the construct that needs to be restructured.
** 105 CA  20240324          Allow caching preprocessed FQL's with inlined literals or unknown.
**     CA  20240331          Use two caches, one with arguments the other without.
** 106 SP  20240514          Changed augmentForUnknownValue() to augment with UDF operators when an operand 
**                           can be unknown and the other part is an UDF call (just NOT_EQ case)
** 107 AL2 20240603          Removed processCompareToUnknowns as it is duplicating simplifyUnknowns.
**                           Even so, it was setting the inlined flag too early, causing bad caching.
** 108 SP  20240611          Skip GUARDED prefix for checkError and initError UDFs.
** 109 ES  20240617          Remove superfluous toDate function from generated fql.
** 110 AS  20240628          Added support for CAST token in manuallyOverload()
** 111 SP  20240614          Use the saved text instead of calling next.getText() more than once.
** 112 OM  20240909          Improved Database API.
** 113 TJD 20230508          Extend earlyPublishEntities to store entities for both EQUALS and NOT_EQ 
**                           as they both trigger dirty share leaking  
**     TJD 20230724          Migrate earlyPublishEntities from List to Set to improve performance
**     TJD 20240103          Include EQ RECID into earlyPublishEntities
** 114 SP  20241002          Fixed fixEmptyContains(): call trim() before checking if the text is empty.
** 115 SP  20241016          Added caching of translate() output.
** 116 SP  20241018          If the where clause is null in translate(), just exit and return null,
**                           as there is nothing to translate or cache.
** 117 SP  20250108          Added dmo interfaces to the TranslateCacheKey.
** 118 RNC 20241126          Added extra 'where' clause processing logic to honor comparisons between
**                           fields and unknown value while using indexes.
** 119 ICP 20250114          Added support for detecting when a subselect inside a join is unnecessary.
** 120 DDF 20250225          If the alias was already replaced, do not do it a second time because it
**                           will be null.
** 121 AL2 20250527          Avoid caching FQL preprocessors with arguments that can pin context state. 
**     AL2 20250530          Made cached context-local.
*/

/*
** 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.List;
import java.util.function.*;
import java.util.logging.*;
import java.util.stream.*;
import com.goldencode.p2j.persist.orm.*;
import antlr.collections.*;
import com.goldencode.ast.*;
import com.goldencode.cache.LRUCache;
import com.goldencode.util.*;
import com.goldencode.p2j.persist.dialect.*;
import com.goldencode.p2j.persist.hql.*;
import com.goldencode.p2j.persist.pl.*;
import com.goldencode.p2j.security.ContextLocal;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.logging.CentralLogger;

/**
 * A preprocessor for where clauses written in FQL (extension of HQL). It performs the following functions:
 * <ul>
 *   <li>Handles references to text properties in a dialect-specific manner.
 *       In cases where functions are usable within index definitions,
 *       encloses each qualified reference to a text value within a SQL
 *       RTRIM function, so that trailing whitespace (specifically space,
 *       tab, line feed, and carriage return characters) is trimmed by the
 *       database server at query execution time.  This is necessary to
 *       emulate Progress' treatment of character fields in where clauses.
 *       Qualifying references may be DMO properties which store text values
 *       or server-side functions which return text data.  In cases where
 *       functions cannot be embedded within index definitions, replaces text
 *       property references with computed column references.
 *   <li>Rewrites where clauses written with an enhanced form of FQL syntax on the fly, in order to facilitate
 *       rewritten it to SQL, further downstream.
 *   <li>Rewrites unary logical expressions as binary comparisons with {@code true} in order to work around a
 *       defect in the ORM's FQL parser which disallows unary logical conditions (TODO: is this still the case?).
 *   <li>Identifies and preserves sets of DMO properties used as restriction
 *       criteria in the preprocessed where clause.  The names of these
 *       properties are stored as sets, indexed in a map by the associated
 *       DMO implementation class(es).
 *   <li>Injects error handling functions around expressions which need to be
 *       protected.  Expressions which meet the following criteria must be
 *       enclosed in the <code>checkError()</code> function.  The first
 *       parameter to this function is <code>initError(false)</code>, and the
 *       second parameter is the expression requiring error handling.  The
 *       criteria are:
 *       <ul>
 *         <li>include a server-side PL function;
 *         <li>evaluate to a boolean result;
 *         <li>reside within the where clause of a subselect phrase.
 *       </ul>
 *       <p>
 *       The call to <code>initError(false)</code> initializes the error
 *       handler for the current <code>checkError()</code> scope.  The
 *       execution of the expression may or may not report an error to the
 *       error handler.  The <code>checkError()</code> function checks the
 *       error handler's current scope for an error and handles it as
 *       necessary.
 *   <li>Handles "manual" overloading of user defined functions. Multiple user
 *       defined functions can be registered for the same base functions name.
 *       At preprocessing time, the parameter signature determines which
 *       overloaded function will be used, and the overloaded function name is
 *       replaced in FQL with a specific backing function.
 *   <li>In certain cases, inlines query substitution parameters directly into
 *       the where clause string.  Only those parameters which participate in
 *       range matches (e.g., &gt;, &gt;=, &lt;, &lt;=, and some forms of
 *       LIKE) are inlined, and then only if the database dialect supports it,
 *       and the caller has requested it.  Instances of this class which have
 *       had parameters inlined are not cached.  The purpose of inlining is to
 *       allow the backing database to choose a better query plan when it
 *       prepares the query, since it has more information about range matches
 *       than if this information were provided later, at query execution
 *       time.
 *   <li>Collects equality matches between the current buffer's properties and substitution
 *       parameter placeholders or literal values, for the purpose of enabling a rudimentary
 *       analysis of the where clause's complexity and suitability for participation in a
 *       server-side join with other tables.
 * </ul>
 * <p>
 * Furthermore, each instance of this class provides the necessary contextual information required by the
 * enclosing query to properly embed the rewritten where clause into an overall FQL statement.
 * <p>
 * For example, the FWD environment allows the following syntax:
 * <pre>
 *    from SomeDMOImpl as alias
 *    where alias.propertyABC[5] = ?
 * </pre>
 * as shorthand for something like:
 * <pre>
 *    from SomeDMOImpl as alias
 *    join alias.composite32 as alias_composite32
 *    where alias_composite32.propertyABC = ? and index(alias_composite32) = 5
 * </pre>
 * <p>
 * The former syntax is not directly supported by Hibernate and so the where
 * clause <code>alias.propertyABC[5] = ?</code> is expanded to the latter
 * form internally, within the P2J persistence framework.
 * <p>
 * This rewriting is only necessary in the particular situation where a DMO
 * class contains a list of composite elements, and a where clause references
 * a property of such a composite element.  The above example corresponds to
 * a Hibernate mapping of:
 * <pre>
 *    ...
 *    [class name="SomeDMOImpl" ...]
 *       ...
 *       [list name="composite32" ]
 *          ...
 *          [composite-element class="SomeDMOImpl$Composite32"]
 *             [property name="propertyABC" ... /]
 *             ...
 *          [/composite-element]
 *       [/list]
 *       ...
 *    [/class]
 *    ...
 * </pre>
 * <p>
 * Allowing the shorthand syntax hides the complexity of the DMO
 * implementation underlying the Hibernate mapping, and prevents the public
 * API of the persistence runtime from becoming more complicated.  The latter
 * point is made because, in addition to the expansion of the where clause,
 * one or more ANSI-style joins will be added to the overall FQL statement,
 * and the order of query substitution parameters may be changed.  The
 * following service methods are provided to enable enclosing queries to
 * embed the preprocessed where clause in the FQL statement, and to organize
 * substitution parameters in the proper order:
 * <ul>
 *   <li>{@link #getFQL()}
 *   <li>{@link #getParameterIndices()}
 *   <li>{@link #ansiJoins()}
 * </ul>
 * <p>
 * A where clause is preprocessed during construction of an instance of this class. Parsing errors are logged,
 * but are otherwise ignored. Ultimately, the ORM will report an FQL problem if the input where clause is
 * invalid.
 */
public final class FQLPreprocessor
implements HQLParserTokenTypes
{
   /** Prefix for guarded versions of UDFs */
   private static final String GUARDED = "guarded_";

   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(FQLPreprocessor.class.getName());
   
   /** A do-nothing instance for empty where clauses */
   private static final FQLPreprocessor NOP_INSTANCE;
   
   /** Map of FQL function keys to overloaded SQL (unique) function names */
   private static final Map<FunctionKey, String> overloadedFunctions = new HashMap<>(256);
   
   /** The template for the normalized ternary which was used in a logical expression. */
   private static final HQLAst TERNARY_LOGICAL_EXP_TEMPLATE;
   
   /** The template for the normalized ternary which was used in a comparison expression. */
   private static final HQLAst TERNARY_COMPARE_EXP_TEMPLATE;

   /** Cache of translate output, indexed by bound/definition aliases and where clause */
   private static LRUCache<TranslateCacheKey, String> translateCache;

   /** Cache of parsed ASTs representing where clauses */
   private static LRUCache<String, HQLAst> astCache;

   /** Context local work area. */
   private static final ContextLocal<WorkArea> context = new ContextLocal<WorkArea>()
   {
      protected WorkArea initialValue()
      {
         return new WorkArea();
      }
   };

   static
   {
      try
      {
         NOP_INSTANCE = new FQLPreprocessor(null,
                                            null,
                                            null,
                                            null,
                                            null,
                                            null,
                                            null,
                                            false,
                                            false,
                                            null,
                                            null,
                                            false,
                                            null,
                                            false);
      }
      catch (PersistenceException exc)
      {
         throw new RuntimeException(exc);
      }
      
      {
         TERNARY_LOGICAL_EXP_TEMPLATE = createAstNode(LPARENS, "(", null);
            HQLAst kwOr = createAstNode(OR, "or", TERNARY_LOGICAL_EXP_TEMPLATE);
               HQLAst kwAnd1 = createAstNode(AND, "and", kwOr);
                  HQLAst lp1 = createAstNode(LPARENS, "(", kwAnd1);
                  HQLAst lp2 = createAstNode(LPARENS, "(", kwAnd1);
               HQLAst kwAnd2 = createAstNode(AND, "and", kwOr);
                  HQLAst kwNot = createAstNode(NOT, "not", kwAnd2);
                     HQLAst lp3 = createAstNode(LPARENS, "(", kwNot);
                        HQLAst kwEq = createAstNode(EQUALS, "=", lp3);
                           HQLAst lp4 = createAstNode(LPARENS, "(", kwEq);
                           HQLAst bool = createAstNode(BOOL_TRUE, "TRUE", kwEq);
                           bool.putAnnotation("is-literal", Boolean.TRUE);
                  HQLAst lp5 = createAstNode(LPARENS, "(", kwAnd2);
      }
      
      {
         TERNARY_COMPARE_EXP_TEMPLATE = createAstNode(LPARENS, "(", null);
            HQLAst kwOr = createAstNode(OR, "or", TERNARY_COMPARE_EXP_TEMPLATE);
               HQLAst kwAnd1 = createAstNode(AND, "and", kwOr);
                  HQLAst lp1 = createAstNode(LPARENS, "(", kwAnd1);
                  HQLAst op1 = createAstNode(0, "", kwAnd1);
                     HQLAst lp2 = createAstNode(LPARENS, "(", op1);
                     HQLAst lp3 = createAstNode(LPARENS, "(", op1);
               HQLAst kwAnd2 = createAstNode(AND, "and", kwOr);
                  HQLAst kwNot = createAstNode(NOT, "not", kwAnd2);
                     HQLAst lp4 = createAstNode(LPARENS, "(", kwNot);
                        HQLAst kwEq = createAstNode(EQUALS, "=", lp4);
                           HQLAst lp5 = createAstNode(LPARENS, "(", kwEq);
                           HQLAst bool = createAstNode(BOOL_TRUE, "TRUE", kwEq);
                           bool.putAnnotation("is-literal", Boolean.TRUE);
                  HQLAst op2 = createAstNode(0, "", kwAnd2);
                     HQLAst lp6 = createAstNode(LPARENS, "(", op2);
                     HQLAst lp7 = createAstNode(LPARENS, "(", op2);
      }
   }
   
   /** Database associated with this object */
   private final Database database;
   
   /** DMO alias to drop during FQL where clause generation or <code>null</code> if none */
   private String dropAlias;
   
   /**
    * If not {@code null} the {@code dropAlias} is forced instead of dropping it. This is needed
    * in order to allow the ORM to properly resolve the fields in a query in a very specific
    * case:
    *  - nested queries;
    *  - the {@code dropAlias} table is part of both outer and inner selects;
    *  - the {@code dropAlias} table has a field with same name.
    * For unknown cause, the ORM will generate a SQL query that instead of this particular
    * field, the {@code id} field of the table from outer select will be used.
    * <p>
    * Valid only if {@code dropAlias} is not {@code null}.
    */
   private String replacementAlias;
   
   /** 
    * The stack of default buffers when sub-SELECTs are processed. The number of elements in this
    * collection gives the nesting level at any given moment when emitting the FQL. It is not used
    * at parsing time.  
    */
   private Deque<RecordBuffer> defaultBuffers = new LinkedList<>();
   
   /**
    * The stack of original aliases names when sub-SELECTs are processed. The number of elements
    * in this collection gives the nesting level at any given moment when emitting the FQL. It is
    * not used at parsing time.  
    */
   private LinkedList<String> dropAliases = new LinkedList<>();
   
   /**
    * The stack of unique aliases for the outer sub-SELECTs are processed. The number of elements
    * in this collection gives the nesting level at any given moment when emitting the FQL. It is
    * not used at parsing time.  
    */
   private LinkedList<String> replacementAliases = new LinkedList<>();
   
   /** FQL subexpressions to perform ANSI joins of associated lists */
   private LinkedHashSet<String> ansiJoins = null;
   
   /** Flags any {@code contains} method encountered while processing the predicate. */
   private boolean hasContains = false;

   /** FQL-compliant where clause (possibly rewritten) */
   private FQLExpression fql = null;
   
   /** Whether substitution parameter inlining is permitted/was performed */
   private boolean inline = false;
   
   /**
    * Flag indicating if a ternary then/else was removed based on substitution parameter value,
    * thus caching will not be possible.
    */
   private boolean inlinedTernary = false;
   
   /** Mapping of index positions for substitution parameters after rewrite */
   private ParameterIndices paramIndices = null;
   
   /** Map of properties used by this where clause, keyed by DMO entity */
   private Map<String, Set<String>> restrictionProperties = null;
   
   /** List of entities which publish uncommitted changes prematurely */
   private Set<String> earlyPublishEntities = null;
   
   /** List of property matches detected in a where clause */
   private List<PropertyMatch> propertyMatches = null;
   
   /** Flag the query that it should performed on recid/rowid lookup table. */
   private boolean findByRowid = false;
   
   /** Helper that helps indentifying if this where clause is in fact a unique index look-up. */
   private UniqueIndexLookup uniqueIndexLookup = null;
   
   /** 
    * The actual {@code rowid} value that is to be loaded if it is a constant detected in 
    * {@code fql} preprocessing. If it is {@code null} but {@code findByRowid = true} then
    * the row index was not yet evaluated because it is a SUBST node. The actual value cannot be
    * cached in this object because it may be different at each call. To detect whether the query
    * looks for the template record of a table, the {@code parameter[0]} of the query iteration
    * must be analyzed.
    */
   private Long findByRowidValue = null;
   
   /** Temporary map of dmo alias names to record buffers */
   private Map<String, RecordBuffer> bufferMap = null;
   
   /**
    * The substitution pairs. They are optionally computed in {@code preprocess} if {@code replacementAliases}
    * are present.*/
   private List<PropertyPair> substPairs = null;
   
   /** The set of properties used in the query predicate. */
   private BitSet queryProperties = null;
   
   /** Flag indicating that the query depends on mutable SESSION attribute(s). */
   private boolean dependsOnSessionAttribute = false;
   
   /** Flag indicating that the FQL has a "contains" keyword with a non-constant expression. */
   private boolean nonConstantContains = false;

   /** Flag indicating if we need to detect the need for a subselect inside a join */
   private boolean detectJoinWithSubselect = false;

   /** Flag indicating if we need to construct a subselect inside a join */
   private Boolean joinSubselectNeeded;

   /**
    * Factory method which accepts an unprocessed where clause, the database
    * instance and the database dialect associated with the enclosing query,
    * the DMO implementation class, and the substitution parameters, if any,
    * for the query.  This method may return a shared, immutable instance from
    * a cache if such an object was created previously for the given where
    * clause and parameter types.
    *
    * @param   where
    *          An unprocessed where clause which will be scanned, parsed, and possibly rewritten.
    *          Should not include the leading {@code where} keyword.
    * @param   database
    *          Database object associated with the client query.
    * @param   dialect
    *          Database dialect in use.
    * @param   buffers
    *          All buffers referenced by the {@code where} clause.
    * @param   parameters
    *          Query substitution parameters for the where clause.
    * @param   referenceSubs
    *          TODO: add description 
    * @param   inline
    *          {@code true} to permit inlining of substitution parameters involved in range
    *          matches (&gt;, &gt;=, &lt;, &lt;=, like); {@code false} to disallow such inlining.
    *          Inlining will embed such parameters into the FQL string directly.
    * @param   dropAlias
    *          If non-{@code null}, suppress this alias qualifier when emitting the final
    *          FQL where clause string; if {@code null}, emit aliases normally.
    * @param   replacementAlias
    *          In the case of a nested SELECT, use this unique alias as a replacement for default
    *          alias that may cause the ORM to generate bad SQL.
    * @param   informational
    *          Flag indicating preprocessing is analytical only and query will not be executed.
    * @param   indexedFields
    *          The list of indexed fields. In all cases, except for indexed FIND queries:
    *          <ul>
    *             <li>{@code t.f <= ?} and {@code ? >= t.f} expressions are always evaluated to
    *                   {@code true} only if <code>t.f = ?</code></li>
    *             <li>{@code t.f < ?} and {@code ? > t.f} are always evaluated to {@code false}</li>
    *          </ul>
    *          because the unknown value ({@code ?}) is sorted high in P4GL index.
    *          <p>
    *          In the case of FIND queries where the compared field is not part of the current
    *          index, P4GL has a flaw that evaluates the mentioned expressions:
    *          <ul>
    *             <li>{@code t.f <= ?} and {@code ? >= t.f} to {@code true} only if {@code t.f}
    *                   is unknown</li>
    *             <li>{@code t.f < ?} and {@code ? > t.f} are always {@code false}.</li>
    *          </ul>
    * @param   detectJoinWithSubselect
    *          Flag for detecting the need for a subselect inside a join.
    *
    * @return  An instance of this class.  At the time this object is returned, all preprocessing
    *          work is complete.
    *
    * @throws  PersistenceException
    *          if there is any error rewriting the where clause.
    */
   public static FQLPreprocessor get(String where,
                                     Database database,
                                     Dialect dialect,
                                     RecordBuffer[] buffers,
                                     Object[] parameters,
                                     String[] referenceSubs,
                                     boolean inline,
                                     String dropAlias,
                                     String replacementAlias,
                                     boolean informational,
                                     Set<String> indexedFields,
                                     boolean detectJoinWithSubselect)
   throws PersistenceException
   {
      if (where == null)
      {
         return NOP_INSTANCE;
      }

      StringBuilder sortCacheKeySb = new StringBuilder();
      if (indexedFields != null)
      {
         for (String field : indexedFields)
         {
            sortCacheKeySb.append(field);
            sortCacheKeySb.append(", ");
         }
         sortCacheKeySb.setLength(sortCacheKeySb.length() - 2);
      }

      String sortCacheKey = sortCacheKeySb.toString();

      FQLPreprocessor preproc = null;
      FqlType[] paramTypes = null;
      boolean manualOverload = !dialect.supportsFunctionOverloading();
      boolean unknownParams = false;
      Object[] paramArray = parameters;
      if (parameters != null)
      {
         paramTypes = DBUtils.makeTypeArray(parameters);
         inline = inline && dialect.isQueryRangeParameterInlined();
         
         // resolve all parameters and check whether we have any unknown value(s)
         int len = parameters.length;
         for (int i = 0; i < len; i++)
         {
            Object parm = parameters[i];
            if (parm instanceof FieldReference)
            {
               if (paramArray == parameters)
               {
                  paramArray = new Object[len];
                  System.arraycopy(parameters, 0, paramArray, 0, len);
               }
               FieldReference ref = (FieldReference) parm;
               if (!informational)
               {
                  parm = ref.resolve();
               }
               else
               {
                  // in certain circumstances (e.g., analyzing FQL for informational purposes),
                  // we can arrive here without a FieldReference argument having been realized;
                  // in such a case, treat the value as unknown
                  parm = ref.unknownValue();
               }
               paramArray[i] = parm;
            }
            if (parm instanceof BaseDataType && ((BaseDataType) parm).isUnknown())
            {
               unknownParams = true;
            }
         }
      }
      
      CacheKey keyWithArgs = null;
      CacheKey keyNoArgs = null;
      
      // if (!unknownParams || informational)
      {
         int bufs = buffers.length;
         String[] aliases = new String[bufs];
         Class<? extends DataModelObject>[] dmoIfaces = new Class[bufs];
         for (int i = 0; i < bufs; i++)
         {
            RecordBuffer buffer = buffers[i];
            aliases[i] = buffer.getDMOAlias();
            dmoIfaces[i] = buffer.getDMOInterface();
         }
         
         keyWithArgs = CacheKey.get(database,
                                    aliases,
                                    dmoIfaces,
                                    where,
                                    sortCacheKey,
                                    paramTypes,
                                    paramArray,
                                    referenceSubs,
                                    inline,
                                    dropAlias,
                                    replacementAlias,
                                    informational);
         
         if (!unknownParams || informational)
         {
            keyNoArgs = CacheKey.get(database,
                                     aliases,
                                     dmoIfaces,
                                     where,
                                     sortCacheKey,
                                     paramTypes,
                                     null,
                                     referenceSubs,
                                     inline,
                                     dropAlias,
                                     replacementAlias,
                                     informational);
            if (keyNoArgs != null)
            {
               preproc = context.get().getCacheNoArgs(keyNoArgs);
            }
         }
         
         if (preproc == null && keyWithArgs != null)
         {
            preproc = context.get().getCacheWithArgs(keyWithArgs);
         }
      }
      
      // This synchronization approach may result in duplicate instances of
      // the preprocessor being created in separate threads for the same
      // where clause.  However, this would occur only in relatively rare
      // race condition cases.  Better that than having every thread block on
      // the global monitor while any preprocessor does its grunt work.
      if (preproc == null)
      {
         boolean debug = LOG.isLoggable(Level.FINE);
         
         if (debug)
         {
            LOG.log(Level.FINE, "BEF: [ " + where + " ]");
         }
         
         // create temporary alias-to-buffer map (not stored with cached instances)
         Map<String, RecordBuffer> bufferMap = new HashMap<>();
         for (RecordBuffer buf : buffers)
         {
            bufferMap.put(buf.getDMOAlias(), buf);
         }
         if (dropAlias != null)
         {
            // if there is an alias to be dropped, store its corresponding buffer under the
            // null key, so it can be looked up without knowing the alias
            bufferMap.put(null, bufferMap.get(dropAlias));
         }
         else if (buffers.length == 1)
         {
            // if there is only one buffer, store it under the null key, in case this is a where
            // clause with unqualified property references (e.g., a delete statement)
            bufferMap.put(null, buffers[0]);
         }
         
         preproc = new FQLPreprocessor(database,
                                       where,
                                       bufferMap,
                                       paramArray,
                                       paramTypes,
                                       referenceSubs,
                                       dialect,
                                       manualOverload,
                                       inline,
                                       dropAlias,
                                       replacementAlias,
                                       informational,
                                       indexedFields,
                                       detectJoinWithSubselect);
         
         if (debug)
         {
            LOG.log(Level.FINE, "AFT: [ " + preproc.fql + " ]");
         }
         
         // WARNING: we are changing the behavior of the cache, based on logging level
         // (we do not cache when level is FINE or higher)
         if (!debug)
         {
            if (keyNoArgs != null && !preproc.wasInlined())
            {
               context.get().putCacheNoArgs(keyNoArgs, preproc);
            }

            if (keyWithArgs != null)
            {
               // all inlining is done via parameters - as these are at the key, too, we can cache it
               context.get().putCacheWithArgs(keyWithArgs, preproc);
            }
         }
      }
      
      return preproc;
   }

   /**
    * Overloaded factory method to create an {@link FQLPreprocessor} instance for a given unprocessed where
    * clause. This method behaves identically to the primary {@code get} method but omits the {@code
    * detectJoinWithSubselect} parameter, defaulting it to {@code false}.
    *
    * @param   where
    *          An unprocessed where clause which will be scanned, parsed, and possibly rewritten.
    *          Should not include the leading {@code where} keyword.
    * @param   database
    *          Database object associated with the client query.
    * @param   dialect
    *          Database dialect in use.
    * @param   buffers
    *          All buffers referenced by the {@code where} clause.
    * @param   parameters
    *          Query substitution parameters for the where clause.
    * @param   referenceSubs
    *          Placeholder strings for substitution parameters.
    * @param   inline
    *          {@code true} to permit inlining of substitution parameters involved in range
    *          matches (&gt;, &gt;=, &lt;, &lt;=, like); {@code false} to disallow such inlining.
    *          Inlining will embed such parameters into the FQL string directly.
    * @param   dropAlias
    *          If non-{@code null}, suppress this alias qualifier when emitting the final
    *          FQL where clause string; if {@code null}, emit aliases normally.
    * @param   replacementAlias
    *          In the case of a nested SELECT, use this unique alias as a replacement for default
    *          alias that may cause the ORM to generate bad SQL.
    * @param   informational
    *          Flag indicating preprocessing is analytical only and query will not be executed.
    * @param   indexedFields
    *          The list of indexed fields.
    * @return  An instance of this class.  At the time this object is returned, all preprocessing
    *          work is complete.
    *
    * @throws  PersistenceException
    *          if there is any error rewriting the where clause.
    */
   public static FQLPreprocessor get(String where,
                                     Database database,
                                     Dialect dialect,
                                     RecordBuffer[] buffers,
                                     Object[] parameters,
                                     String[] referenceSubs,
                                     boolean inline,
                                     String dropAlias,
                                     String replacementAlias,
                                     boolean informational,
                                     Set<String> indexedFields)
   throws PersistenceException
   {
      return FQLPreprocessor.get(where,
                                 database,
                                 dialect,
                                 buffers,
                                 parameters,
                                 referenceSubs,
                                 inline,
                                 dropAlias,
                                 replacementAlias,
                                 informational,
                                 indexedFields,
                                 false);
   }
   
   /**
    * Register a user-defined function to the specified backing method for the given database.
    * 
    * @param   database
    *          Database for which user-defined function is registered.
    * @param   fqlFunction
    *          FQL function name (as generated in java code).
    * @param   sqlFunction
    *          SQL function name that will be used in queries.
    * @param   method
    *          Method which will back the given function and provide function signature.
    */
   public static void registerFunction(Database database,
                                       String fqlFunction,
                                       String sqlFunction,
                                       Method method)
   {
      Class<?>[] paramTypes = method.getParameterTypes();
      int len = paramTypes.length;
      FqlType[] signature = new FqlType[len];
      for (int i = 0; i < len; i++)
      {
         signature[i] = DataTypeHelper.getTypeClass(paramTypes[i]);
      }
      
      FunctionKey key = new FunctionKey(database, fqlFunction, signature);
      
      if (!database.isTemporary() && overloadedFunctions.containsKey(key))
      {
         throw new IllegalArgumentException("User defined function '" + key + "' already registered");
      }
      
      overloadedFunctions.put(key, sqlFunction);
      
      if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE, "Registering function '" + key + "' to '" + sqlFunction + "'");
      }
   }
   
   /**
    * Returns the SQL name of the user-defined function previously registered by
    * {@link #registerFunction(Database, String, String, Method)}.
    *
    * @param   database
    *          Database for which user-defined function is registered.
    * @param   fqlFunction
    *          FQL function name (as generated in java code).
    * @param   paramTypes
    *          The function signature.
    *
    * @return  SQL name of the registered user-defined function or null if not found.
    */
   static String getRegisteredFunction(Database database,
                                       String fqlFunction,
                                       Class<?>[] paramTypes)
   {
      int len = paramTypes == null ? 0 : paramTypes.length;
      FqlType[] signature = new FqlType[len];
      for (int i = 0; i < len; i++)
      {
         signature[i] = DataTypeHelper.getTypeClass(paramTypes[i]);
      }
      FunctionKey key = new FunctionKey(database, fqlFunction, signature);
      
      return overloadedFunctions.get(key);
   }
   
   /**
    * Translate this {@code where} clause to use the bound DMO's alias and property names, instead the
    * definition (conversion-time) alias and property names.
    * 
    * @param   bound
    *          The runtime-bound buffers.
    * @param   definition
    *          The buffers as it was used to generate the {@code where} clause at conversion time.
    * @param   where
    *          The FQL to be translated.
    * @param   singleBuffer
    *          Single buffer mode. If activated, a standalone property is assumed to belong the only bound
    *          buffer. Requires {@code bound} and {@code definition} to have a single element.
    *
    * @return   The translated FQL.
    */
   static String translate(List<RecordBuffer> bound,
                           List<RecordBuffer> definition,
                           String where,
                           boolean singleBuffer)
   {
      if (where == null)
      {
         return null;
      }

      int bounds = bound.size();
      if (bounds != 1)
      {
         singleBuffer = false;
      }
      String[] boundAliases = new String[bounds];
      String[] defAliases = new String[bounds];
      Class<?>[] boundDmoIfaces = new Class<?>[bounds];
      Class<?>[] defDmoIfaces = new Class<?>[bounds];

      for (int i = 0; i < bounds; i++)
      {
         RecordBuffer boundBuf = bound.get(i);
         RecordBuffer defBuf = bound.get(i);
         boundAliases[i] = boundBuf.getDMOAlias();
         defAliases[i] = defBuf.getDMOAlias();
         boundDmoIfaces[i] = boundBuf.getDMOInterface();
         defDmoIfaces[i] = defBuf.getDMOInterface();
      }

      TranslateCacheKey translateCacheKey = new TranslateCacheKey(boundAliases,
                                                                  defAliases,
                                                                  boundDmoIfaces,
                                                                  defDmoIfaces,
                                                                  where);

      String finalTranslate;
      synchronized (translateCache)
      {
         finalTranslate = translateCache.get(translateCacheKey);
      }

      if (finalTranslate == null)
      {
         FQLPreprocessor fqlPreprocessor = new FQLPreprocessor(bound, definition, where, singleBuffer);
         finalTranslate = fqlPreprocessor.fql.toFinalExpression(false);
         synchronized (translateCache)
         {
            translateCache.put(translateCacheKey, finalTranslate);
         }
      }

      return finalTranslate;
   }

   /**
    * Starting from {@code node}, check if the current expreesion is constant or not.
    *
    * @param   node
    *          The HQLAst node from where we start the check.
    *          
    * @return  true if the current expression is constant, false otherwise.
    * 
    */
   static boolean isConstant(HQLAst node)
   {
      Iterator<Aast> iter = node.iterator();
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         if (next.getType() == PROPERTY)
         {
            return false;
         }
      }
      return true;
   }
   
   /**
    * Translate this {@code where} clause to use the bound DMO's alias and property names, instead the
    * definition (conversion-time) alias and property names.
    * 
    * @param   bound
    *          The runtime-bound buffers.
    * @param   definition
    *          The buffers as it was used to generate the {@code where} clause at conversion time.
    * @param   where
    *          The FQL to be translated.
    * @param   singleBuffer
    *          Single buffer mode. If activated, a standalone property is assumed to belong the only bound
    *          buffer. Requires {@code bound} and {@code definition} to have a single element.
    */
   private FQLPreprocessor(List<RecordBuffer> bound,
                           List<RecordBuffer> definition,
                           String where,
                           boolean singleBuffer)
   {
      bufferMap = new HashMap<>();
      for (RecordBuffer buf : definition)
      {
         bufferMap.put(buf.getDMOAlias(), buf);
      }
      
      // this is only possible with the temporary database
      database = DatabaseManager.TEMP_TABLE_DB;
      fql = new FQLExpression(where);
      HQLAst root = null;
      String fqlExpr = null;
      
      try
      {
         fqlExpr = fql.toFinalExpression();
         StringReader reader = new StringReader(fqlExpr);
         HQLLexer lexer = new HQLLexer(reader);
         HQLParser parser = new HQLParser(lexer);
         
         parser.expression();
         root = (HQLAst) parser.getAST();
         root.fixups(null, null);
      }
      catch (Exception exc)
      {
         String msg = "Error parsing FQL";
         if (fqlExpr != null)
         {
            msg += (" [ " + fqlExpr + " ]");
         }
         
         if (LOG.isLoggable(Level.WARNING))
         {
            if (LOG.isLoggable(Level.FINE))
            {
               LOG.log(Level.FINE, msg, exc);
            }
            else
            {
               LOG.log(Level.WARNING, msg);
            }
         }
         
         throw new RuntimeException(msg, exc);
      }
      
      Map<String, Map<String, String>> aliasToMappings = new HashMap<>();
      Map<String, String> aliases = new HashMap<>();
      for (int i = 0; i < bound.size(); i++)
      {
         RecordBuffer buf = bound.get(i);
         RecordBuffer def = definition.get(i);
         
         String alias = buf.getDMOAlias();
         String defAlias = def.getDMOAlias();
         aliases.put(defAlias, alias);
         
         Map<String, String> mappings = def.getBoundPropertyMappings();
         aliasToMappings.put(defAlias, mappings);
      }
      
      Map<String, String> singleMapping = null;
      if (singleBuffer)
      {
         Set<Map.Entry<String, Map<String, String>>> entries = aliasToMappings.entrySet();
         Map.Entry<String, Map<String, String>>[] asArray = new Map.Entry[1];
         entries.toArray(asArray);
         singleMapping = asArray[0].getValue();
      }
      
      Iterator<Aast> iter = root.iterator();
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         
         if (next.getType() == ALIAS)
         {
            String alias = next.getText();
            Map<String, String> mappings = aliasToMappings.get(alias);
            if (mappings != null)
            {
               // update the alias, too
               next.setText(aliases.get(alias));
               
               // if the alias is for the definition buffer, then rewrite this and its property
               
               // we don't override the alias - as this can lead to collisions; when binding, the
               // bound buffer's variable (DMO alias) is set to the definition's alias, and 
               // restored when the top-level block is exit.
               
               Iterator<Aast> piter = next.iterator();
               while (piter.hasNext())
               {
                  HQLAst prop = (HQLAst) piter.next();
                  if (prop.getType() == PROPERTY)
                  {
                     prop.setText(mappings.get(prop.getText()));
                  }
               }
            }
         }
         else if (singleBuffer && next.getType() == PROPERTY)
         {
            // check if the alias was already replaced when going over the ALIAS type above
            String alias = next.getText();
            if (singleMapping.containsValue(alias))
            {
               continue;
            }
            next.setText(singleMapping.get(alias));
         }
      }
      
      fql = emit(root, true);
   }
   
   /**
    * Constructor which accepts an unprocessed where clause.
    * 
    * @param   database
    *          Database within which the associated query will be run.
    * @param   where
    *          An unprocessed where clause which will be scanned, parsed, and possibly rewritten.
    *          Should not include the leading {@code where} keyword.
    * @param   bufferMap
    *          A map of DMO aliases to their corresponding record buffers. If {@code dropAlias}
    *          is not {@code null}, a {@code null} key will be mapped to the buffer corresponding
    *          with this alias.
    * @param   parameters
    *          Query substitution parameters for the where clause.
    * @param   paramTypes
    *          The query substitution parameter types.
    * @param   dialect
    *          Database dialect in use.
    * @param   overload
    *          {@code true} to manually overload user-defined functions.
    * @param   inline
    *          {@code true} to allow substitution parameters involved in range checks to be
    *          inlined;  {@code false} to disallow such inlining.  If inlining occurs, the
    *          preprocessor will not be cached.
    * @param   dropAlias
    *          If non-{@code null}, suppress this alias qualifier when emitting the final
    *          FQL where clause string; if {@code null}, emit aliases normally.
    * @param   informational
    *          Flag indicating preprocessing is analytical only and query will not be executed.
    * @param   indexedFields
    *          The list of indexed fields. In all cases, except for indexed FIND queries:
    *          <ul>
    *             <li>{@code t.f <= ?} and {@code ? >= t.f} expressions are always evaluated to
    *                   {@code true};</li>
    *             <li>{@code t.f < ?} and {@code ? > t.f} expressions are equivalent to 
    *                   {@code t.f <> ?}</li>
    *          </ul>
    *          because the unknown value ({@code ?}) is sorted high in P4GL index.
    *          <p>
    *          In the case of FIND queries where the compared field is not part of the current
    *          index, P4GL has a flaw that evaluates the mentioned expressions:
    *          <ul>
    *             <li>{@code t.f <= ?} and {@code ? >= t.f} to {@code true} only if {@code t.f}
    *                   is unknown</li>
    *             <li>{@code t.f < ?} and {@code ? > t.f} are always {@code false}.</li>
    *          </ul>
    * @param   detectJoinWithSubselect
    *          Flag for detecting the need for a subselect inside a join.
    *
    * @throws  PersistenceException
    *          if there is any error rewriting the where clause.
    */
   private FQLPreprocessor(Database database,
                           String where,
                           Map<String, RecordBuffer> bufferMap,
                           Object[] parameters,
                           FqlType[] paramTypes,
                           String[] referenceSubs,
                           Dialect dialect,
                           boolean overload,
                           boolean inline,
                           String dropAlias,
                           String replacementAlias,
                           boolean informational,
                           Set<String> indexedFields,
                           boolean detectJoinWithSubselect)
   throws PersistenceException
   {
      this.database = database;
      this.inline = inline;
      this.dropAlias = dropAlias;
      this.replacementAlias = replacementAlias;
      this.detectJoinWithSubselect = detectJoinWithSubselect;
      if (where != null)
      {
         Map<String, Integer> indexedFieldsMap = new HashMap<>();
         if (indexedFields != null)
         {
            int index = 0;
            for (String indexedField : indexedFields)
            {
               indexedFieldsMap.put(indexedField, index++);
            }
         }

         this.bufferMap = bufferMap;
         preprocess(where,
                    parameters,
                    paramTypes,
                    referenceSubs,
                    dialect,
                    overload,
                    informational,
                    indexedFieldsMap);
      }
      
      // clean up resources only needed during preprocessing
      this.bufferMap = null;
      this.dropAlias = null;
      this.replacementAlias = null;
      this.dropAliases = null;
      this.replacementAliases = null;
      this.defaultBuffers = null;
   }
   
   /**
    * Get the (possibly rewritten) where clause, which is compliant with FQL syntax.
    *
    * @return  FQL where clause, with no leading {@code where} keyword.
    */
   FQLExpression getFQL()
   {
      return fql == null ? null : new FQLExpression(fql);
   }
   
   /**
    * Obtain the substitution pair list, if any.
    * 
    * @return  the substitution pair list or {@code null} if none was detected.
    */
   public List<PropertyPair> getSubstPairs()
   {
      return substPairs;
   }
   
   /**
    * Obtains the list of properties the preprocessed FQL is based on.
    * 
    * @return  The set of properties accessed by the FQL predicate.
    */
   public BitSet getQueryProperties()
   {
      return queryProperties;
   }
      
   /**
    * Checks if the query depends on mutable SESSION attribute(s).
    * 
    * @return  {@code true} if the query depends on mutable SESSION attribute(s).
    */
   public boolean isDependsOnSessionAttribute()
   {
      return dependsOnSessionAttribute;
   }
   
   /**
    * Check if the preprocessed query is a simple RECID/ROWID lookup query.
    * <p>
    * Generally, we can easily find such records using persistent database or dirty database for
    * uncommitted transient records. Using normal queries, the transient record that has not
    * validated its index, won't break the transaction isolation idiom and normally should not be 
    * shared to any context. However, using RECID/ROWID lookup query the record should be find
    * across transactions and contexts, by direct accessing the record lookup table.
    * <p> 
    * The predicate of such query will only test for equality of the RECID/ROWID of the record
    * against a SUBST value. Using LTE or GTE will fail to find the requested record.
    * 
    * @return  {@code true} if the preprocessed query is a simple RECID/ROWID lookup query.
    */
   public boolean isFindByRowid()
   {
      return findByRowid;
   }
   
   /**
    * If this query is a ROWID/RECID lookup, get the record ID we are looking for. Otherwise 
    * the result should be ignored.
    * <p>
    * By convention, if the result is negative, the template record should be loaded. Values
    * strictly positive are looked up into the database.
    * 
    * @param   queryParams
    *          The current parameter list for the query.
    * 
    * @return  the row id of the record in case of direct access using RECID/ROWID query.
    */
   public long getFindByRowid(Object[] queryParams)
   {
      if (findByRowidValue != null)
      {
         // the value is hardcoded, it was extracted when the fql was preprocessed 
         return findByRowidValue;
      }
      
      // otherwise, the record to be found is passed in as SUBST, we need to analyze the query
      // parameters:
      Object params = queryParams[0];
      if (params instanceof FieldReference)
      {
         // in case of a field reference, extract the value and analyze as it would have been
         // passed in as a standalone value
         params = ((FieldReference) params).get();
      }
      
      if (params instanceof NumberType)
      {
         // integer and recid datatypes
         return ((NumberType) params).longValue();
      }
      else if (params instanceof rowid)
      {
         return ((rowid) params).getValue();
      }
      else if (params instanceof BaseDataType)
      {
         // character datatype special case
         return new int64(((BaseDataType) params)).longValue();
      }
      else if (params instanceof Integer)
      {
         return ((Integer) params).longValue();
      }
      else if (params instanceof Long)
      {
         return ((Long) params).longValue();
      }
         
      if (LOG.isLoggable(Level.WARNING))
      {
         LOG.log(Level.WARNING, "Incompatible query parameters");
      }
      return 0;
   }
   
   /**
    * Check if the where clause is a conjunction of equality clauses that strictly cover an 
    * unique index. This way, it is guaranteed that there is zero or one record fetched at 
    * the end.
    * 
    * @param    includeRowid
    *           {@code true} if the function should consider rowid as a field of an unique index.
    *           
    * @return   {@code true} if the target where clause if for zero or one record due to 
    *           an unique index look-up.
    */
   public boolean isUniqueFind(boolean includeRowid)
   {
      return uniqueIndexLookup != null || (includeRowid ? isFindByRowid() : false);
   }
   
   /**
    * Retrieve the unique index loop-up object, responsible for storing information on the
    * unique index that is used. Note that the properties form a unique index, but not necessarily
    * in the provided order. Also, some values may be null due to the fact that they are substitution.
    * 
    * @return   an object which may assist faster look-up based on unique indexes
    */
   public UniqueIndexLookup getUniqueIndexLookup()
   {
      return uniqueIndexLookup;
   }
   
   /**
    * Initializes the cache using CacheManager. The default value of the cache
    * is used when there is no size available from the configuration.
    */
   public static void initializeCache()
   {
      translateCache = CacheManager.createLRUCache(FQLPreprocessor.class, null, 2048);
      astCache = CacheManager.createLRUCache(FQLPreprocessor.class, "ast", 8192);
   }
   
   /**
    * Get the <code>ParameterIndices</code> object associated with this query.
    * This object maintains an array of zero-based indices into the array of
    * query substitution arguments for the where clause, as it exists
    * <i>after</i> preprocessing is complete.  This additional level of
    * indirection is necessary because the query substitution placeholders may
    * have been reordered within the FQL where clause expression during the
    * FQL preprocessing rewrite step, and some parameters may have been
    * inlined into the where clause.
    *
    * @return  An object which provides a mapping of indices into the query
    *          substitution parameter list.
    */
   ParameterIndices getParameterIndices()
   {
      return paramIndices;
   }
   
   /**
    * 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>.
    */
   Iterator<String> ansiJoins()
   {
      return (ansiJoins != null) ? ansiJoins.iterator(): EmptyIterator.get();
   }
   
   /**
    * Report whether any ANSI-style join subexpressions are present.
    * 
    * @return  <code>true</code> if there are ANSI joins; else <code>false</code>.
    */
   boolean hasAnsiJoins()
   {
      return ansiJoins != null;
   }
   
   /**
    * Reports whether the predicate uses {@code contains()} function at least once.
    * 
    * @return  {@code true} if the {@code contains()} function was encountered while parsing the predicate.
    */
   boolean hasContains()
   {
      return hasContains;
   }
   
   /**
    * Report whether "contains" keyword with a non-constant expression is present in the FQL.
    * 
    * @return <code>true</code> if "contains" keyword is present; else <code>false</code>
    */
   boolean hasNonConstantContains()
   {
      return nonConstantContains;
   }
   
   /**
    * Retrieve the list of DMO entity names which will trigger the premature
    * publication of uncommitted changes across sessions, once the query
    * containing this preprocessor's underlying where clause is executed.
    * <p>
    * This method is intended to support the emulation of a quirk/bug in
    * Progress whereby the execution of a query (in the generic sense of the
    * term) in a session after uncommitted changes, will trigger those changes
    * to be prematurely published to other sessions.
    * <p>
    * TODO:  this behavior may represent a version-specific bug in Progress
    * which should be disabled in P2J for other versions than 9.1C.
    * 
    * @return  The list of entities, if any, which will have their uncommitted
    *          changes published prematurely if the underlying where clause is
    *          executed in the current session.  The returned list may be
    *          empty, but it will never be <code>null</code>.
    */
   Set<String> getEarlyPublishEntities()
   {
      if (earlyPublishEntities == null)
         return Collections.emptySet();
      return earlyPublishEntities;
   }
   
   /**
    * Get an unmodifiable list of the property matches collected for this where clause.
    * 
    * @return  Property matches in the order visited and collected.
    */
   List<PropertyMatch> getPropertyMatches()
   {
      return propertyMatches;
   }
   
   /**
    * 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 contained no restriction
    *          properties.
    */
   Map<String, Set<String>> getRestrictionProperties()
   {
      return (restrictionProperties != null
              ? Collections.unmodifiableMap(restrictionProperties)
              : null);
   }
   
   /**
    * Indicate whether query substitution parameters participating in range
    * checks were inlined directly into the query string.  If called during
    * construction (before inlining actually occurs), this method will return
    * whether this object <i>can</i> be inlined.  If called after construction
    * completes, it will return whether this object actually <i>was</i>
    * inlined.  That is, even if inlining was permitted, it will not have
    * occurred if no suitable subexpressions were detected in the where clause
    * provided.
    * 
    * @return  <code>true</code> if inlining was performed (with the caveats
    *          noted above);  else <code>false</code>.
    */
   boolean wasInlined()
   {
      return inline || inlinedTernary;
   }
   
   /**
    * Look up the record buffer associated with the given alias.
    *
    * @param   alias
    *          AST representing the alias (variable name) with which the buffer was defined in
    *          business logic.
    *
    * @return  Record buffer associated with the alias.
    *
    * @throws  IllegalArgumentException
    *          if <code>alias</code> does not match any buffer whose scope is currently open.
    */
   private RecordBuffer lookupBuffer(Aast alias)
   {
      return DataTypeHelper.lookupBuffer(alias, bufferMap, fql);
   }
   
   /**
    * The main worker method which drives preprocessing. Performs the following steps:
    * <ol>
    *   <li>parse the input where clause;
    *   <li>restructure ASTs representing qualified names of text properties and functions to
    *       embed these names with an SQL function which trims trailing whitespace;
    *   <li>restructure ASTs representing unary logical expressions to instead be binary logical
    *       comparisons with {@code true}.
    *   <li>analyze the resulting AST to identify subtrees which require restructuring;
    *   <li>possibly inline certain query substitution parameters;
    *   <li>restructure the AST to account for the joins;
    *   <li>generate any necessary ANSI join subexpressions;
    *   <li>emit the rewritten result as a string and stores it for later access;
    *   <li>create the substitution parameter index mapping.
    * </ol>
    * <p>
    * If the analysis performed in step 3 determines that rewriting is not necessary, the
    * following three steps are skipped, and the original where clause simply is stored.
    *
    * @param   where
    *          An unprocessed where clause which will be scanned, parsed, and possibly rewritten.
    *          Should not include the leading {@code where} keyword.
    * @param   parameters
    *          Query substitution parameters for the where clause.
    * @param   paramTypes
    *          The query substitution parameter types.
    * @param   dialect
    *          Database dialect in use.
    * @param   overload
    *          {@code true} to manually overload user-defined functions.
    * @param   informational
    *          Flag indicating preprocessing is analytical only and query will not be executed.
    * @param   indexedFields
    *          The list of indexed fields and their position.
    *          In all cases, except for indexed FIND queries:
    *          <ul>
    *             <li>{@code t.f <= ?} and {@code ? >= t.f} expressions are always evaluated to
    *                   {@code true};</li>
    *             <li>{@code t.f < ?} and {@code ? > t.f} expressions are equvalent to 
    *                   {@code t.f <> ?}</li>
    *          </ul>
    *          because the unknown value ({@code ?}) is sorted high in P4GL index.
    *          <p>
    *          In the case of FIND queries where the compared field is not part of the current
    *          index, P4GL has a flaw that evaluates the mentioned expressions:
    *          <ul>
    *             <li>{@code t.f <= ?} and {@code ? >= t.f} to {@code true} only if {@code t.f}
    *                   is unknown</li>
    *             <li>{@code t.f < ?} and {@code ? > t.f} are always {@code false}.</li>
    *          </ul>
    *
    * @throws  PersistenceException
    *          if there is any error rewriting the where clause.
    */
   private void preprocess(String where,
                           Object[] parameters,
                           FqlType[] paramTypes,
                           String[] referenceSubs,
                           Dialect dialect,
                           boolean overload,
                           boolean informational,
                           Map<String, Integer> indexedFields)
   throws PersistenceException
   {
      this.fql = new FQLExpression(where);
      HQLAst root = parse();
      if (root != null)
      {
         if (referenceSubs != null)
         {
            substPairs = inlineReferenceSubs(root, referenceSubs);
         }
         
         // Annotate query substitution parameter nodes with their data types.
         if (paramTypes != null)
         {
            annotateParameters(root, paramTypes);
         }
         
         //if (informational && parameters != null)
         // at the moment, this method is only interesting for server-side join optimization
         // analysis, which presumes at least one field reference parameter; if this becomes
         // more useful generally, we can remove the conditional test above
         // it is useful for H2 direct access to identify queries on unique indexes
         collectPropertyMatches(root, !detectJoinWithSubselect);
         
         if (dialect.isAutoCi() || dialect.isAutoRtrim())
         {
            // eliminate unneeded [upper] functions for case insensitive properties
            // and [rtrim] functions depending on dialect
            simplifyProperties(root, dialect.isAutoCi(), dialect.isAutoRtrim());
         }
         
         // normalize ternary operators; as substitution parameters may be reused, we need to rebuild them
         root = normalizeTernary(root, parameters);
         
         // replace [contains] function node with [false] is the second argument is an empty string 
         root = fixEmptyContains(root, parameters);
         
         // walk the AST, fixing up certain nodes in place
         root = mainWalk(root, parameters, paramTypes, dialect, overload, indexedFields);
         
         if (!dialect.useNullEquality())
         {
            // re-walk the modified tree, augmenting the expression for unknown value semantics
            root = augmentForUnknownValue(root, dialect);
         }
         
         if (dialect.preferDisjunctiveForm())
         {
            root = honorDisjunctiveForm(root);
         }
         
         /* See header entry #008
            // Work around a defect in the ORM's FQL parser which disallows unary logical conditions.
            root = preprocessBooleanExpressions(root);
         */
         
         Iterable<HQLAst> denormalizedFields = getDenormalizedFields(root);
         if (denormalizedFields != null)
         {
            for (HQLAst property : denormalizedFields)
            {
               inlineDenormalizedField(property, parameters);
            }
            
            if (LOG.isLoggable(Level.FINEST))
            {
               String sep = System.getProperty("line.separator");
               LOG.log(Level.FINEST,
                       "[" + database + "] Denormalized FQL:  " + fql + sep + root.dumpTree());
            }
         }
         
         Map<HQLAst, String> targets = analyzeComposites(root);
         if (targets != null)
         {
            // Restructure the target ASTs.
            root = restructure(root, targets);
            
            // ANSI joins must be generated after restructure, since targets
            // are modified during restructuring to have appropriate composite names.
            this.ansiJoins = generateJoins(targets);
         }

         this.findByRowid = checkFindByRowid(root);
         
         this.uniqueIndexLookup = checkUniqueFind();

         if (detectJoinWithSubselect && dropAlias != null && replacementAlias != null && !isSubselectNeeded())
         {
            dropAlias = null;
            replacementAlias = null;
            if (bufferMap != null)
            {
               bufferMap.remove(null);
            }
         }

         // Emit the preprocessed FQL expression text.
         this.fql = emit(root, false);
         
         if (LOG.isLoggable(Level.FINEST))
         {
            String sep = System.getProperty("line.separator");
            LOG.log(Level.FINEST,
                    "[" + database + "] Preprocessed FQL:  " + fql + sep + root.dumpTree());
         }
         
         this.paramIndices = createParameterIndices(root, parameters);
      }
   }

   /**
    * This method checks whether the where clause requires a subselect inside the join by evaluating if it is
    * optimizable. The result is cached in the {@code joinSubselectNeeded} field to avoid redundant
    * computations for subsequent calls.
    *
    * @return  {@code true} if a subselect in the join is needed; {@code false} otherwise.
    */
   public boolean isSubselectNeeded()
   {
      if (fql == null)
      {
         return false;
      }

      if (joinSubselectNeeded != null)
      {
         return joinSubselectNeeded;
      }

      boolean result = !isOptimizable();
      joinSubselectNeeded = result;
      return result;
   }

   /**
    * Evaluates whether the query can be optimized based on property matches and their relationships to the
    * associated record buffers. A query is considered optimizable if:
    * <ul>
    *   <li>All property matches use equality operators.</li>
    *   <li>The properties are consistently mapped to their respective buffers without mixing clauses.</li>
    *   <li>The properties in the join buffer form a complete unique index.</li>
    *   <li>The corresponding properties in the other buffer are mandatory.</li>
    * </ul>
    * If these conditions are not met, the query is deemed not optimizable.
    *
    * @return {@code true} if the query is optimizable; {@code false} otherwise.
    */
   private boolean isOptimizable()
   {
      if (bufferMap == null || dropAlias == null)
      {
         return false;
      }

      List<PropertyMatch> matches = getPropertyMatches();
      if (matches == null)
      {
         return false;
      }

      RecordBuffer targetFirstBuffer = null;
      RecordBuffer targetSecondBuffer = null;
      List<String> firstProperties = new ArrayList<>();
      List<String> secondProperties = new ArrayList<>();

      for (int i = 0; i < matches.size(); i++)
      {
         PropertyMatch match = matches.get(i);

         if (match.secondAlias == null || match.alias == null || match.operator != EQUALS)
         {
            return false;
         }

         RecordBuffer firstBuffer = DataTypeHelper.lookupBuffer(match.alias, bufferMap, fql);
         RecordBuffer secondBuffer = DataTypeHelper.lookupBuffer(match.secondAlias, bufferMap, fql);

         if (firstBuffer == null || secondBuffer == null)
         {
            return false;
         }

         if (targetFirstBuffer == null)
         {
            targetFirstBuffer = firstBuffer;
         }
         else if (targetFirstBuffer != firstBuffer)
         {
            // mixed where clauses
            return false;
         }

         if (targetSecondBuffer == null)
         {
            targetSecondBuffer = secondBuffer;
         }
         else if (targetSecondBuffer != secondBuffer)
         {
            // mixed where clauses
            return false;
         }

         firstProperties.add(match.property);
         secondProperties.add(match.secondProperty);
      }

      if (firstProperties.isEmpty() || secondProperties.isEmpty())
      {
         return false;
      }

      RecordBuffer joinBuffer = DataTypeHelper.lookupBuffer(dropAlias, bufferMap, fql);
      RecordBuffer otherBuffer = (joinBuffer == targetFirstBuffer) ?
                                 targetSecondBuffer :
                                 (joinBuffer == targetSecondBuffer) ? targetFirstBuffer : null;
      if (otherBuffer == null)
      {
         return false;
      }

      List<String> joinProperties = (joinBuffer == targetFirstBuffer) ? firstProperties : secondProperties;
      List<String> otherProperties = (joinBuffer == targetFirstBuffer) ? secondProperties : firstProperties;

      List<Set<String>> uniqueIndexes = joinBuffer.getDmoInfo().getUniqueConstraints();
      for (Set<String> uniqueIndex : uniqueIndexes)
      {
         // Check if joinProperties form a complete unique index
         if (joinProperties.containsAll(uniqueIndex))
         {
            if (areCorrespondingPropertiesMandatory(uniqueIndex,
                                                    joinProperties,
                                                    otherBuffer,
                                                    otherProperties))
            {
               return true; // Optimization possible
            }
         }
      }

      return false;
   }

   /**
    * Validates that all fields in a unique index map correctly to mandatory fields in another buffer.
    *
    * @param   uniqueIndex
    *          The unique index fields to validate.
    * @param   joinProperties
    *          Properties from the join buffer.
    * @param   otherBuffer
    *          The buffer containing the corresponding properties.
    * @param   otherProperties
    *          Properties from the {@code otherBuffer}.
    *
    * @return  {@code true} if the mapping is valid and all corresponding fields are mandatory;
    *          {@code false} otherwise.
    */
   private boolean areCorrespondingPropertiesMandatory(Set<String> uniqueIndex,
                                                       List<String> joinProperties,
                                                       RecordBuffer otherBuffer,
                                                       List<String> otherProperties)
   {
      for (String uniqueField : uniqueIndex)
      {
         int index = joinProperties.indexOf(uniqueField);
         if (joinProperties.size() != otherProperties.size() ||
            index < 0                                        ||
            index >= otherProperties.size())
         {
            return false; // Invalid mapping
         }

         String correspondingProperty = otherProperties.get(index);
         P2JField field = otherBuffer.getDmoInfo().getExistingField(correspondingProperty);
         if (field == null || !field.isMandatory())
         {
            return false; // Corresponding property is not mandatory
         }
      }
      return true;
   }


   /**
    * Replaces {@code contains} function node with {@code false} is the second argument is an empty string.
    *
    * @param   root
    *          The parsed FQL root.
    * @param   parameters
    *          The query parameters.
    *
    * @return  the new root.
    */
   private HQLAst fixEmptyContains(HQLAst root, Object[] parameters)
   {
      Set<HQLAst> toProcess = new HashSet<>();
      Iterator<Aast> iter = root.iterator();
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         if (next.getType() == FUNCTION && "contains".equalsIgnoreCase(next.getText()))
         {  
            HQLAst child1 = (HQLAst) next.getFirstChild();
            HQLAst child2 = (HQLAst) child1.getNextSibling();
            if (child2.getType() == STRING && child2.getText().trim().isEmpty())
            {
               toProcess.add(next);
            }
            else if (child2.getType() == SUBST)
            {
               int idx = ((Long) child2.getAnnotation("index")).intValue();
               Object subst = parameters[idx];
               String sVal = subst instanceof String ? (String) subst :
                     subst instanceof Text ? ((Text) subst).toJavaType() :
                           subst.toString();
               if (sVal.trim().isEmpty())
               {
                  next.putAnnotation("index", (long) idx); // in advance, to avoid recompute [idx]
                  next.putAnnotation("inlined", Boolean.TRUE);
                  toProcess.add(next);
               }
            }
         }
      }
      
      for (HQLAst next : toProcess)
      {
         HQLAst child1 = (HQLAst) next.getFirstChild();
         HQLAst child2 = (HQLAst) child1.getNextSibling();
         next.setType(BOOL_FALSE);
         next.setText("false");
         child2.remove();
         child1.remove();
      }
      
      return root;
   }
   
   /**
    * Eliminates unneeded [upper] functions for case-insensitive properties
    * and [rtrim] functions for automatically rtrimmed properties.
    *
    * @param   root
    *          The parsed FQL root.
    * @param   eliminateUpper
    *          {@code true} if upper functions should be eliminated
    * @param   eliminateRtrim
    *          {@code true} if rtrim functions should be eliminated
    */
   private void simplifyProperties(HQLAst root, boolean eliminateUpper, boolean eliminateRtrim)
   {
      List<Aast> toRemove = new ArrayList<>();
      Iterator<Aast> iter = root.iterator();
      
      while (iter.hasNext())
      {
         Aast node = iter.next();
         if (node.getType() == FUNCTION &&
             (("upper".equals(node.getText()) && eliminateUpper) ||
              ("rtrim".equals(node.getText()) && eliminateRtrim)))
         {
            Aast parent = node.getParent();
            switch (parent.getType())
            {
               case GT:
               case GTE:
               case LT:
               case LTE:
               case EQUALS:
               case NOT_EQ:
               case FUNCTION:
                  toRemove.add(node);
                  break;
            }
         }
      }
      
      if (toRemove.isEmpty())
      {
         return; // nothing to do
      }
      
      for (int i = 0; i < toRemove.size(); i++)
      {
         Aast node = toRemove.get(i);
         Aast parent = node.getParent();
         int pos = node.getIndexPos();
         Aast child = (Aast) node.getFirstChild();
         node.remove();
         
         // replace the node with its child at the same position
         parent.graftAt(child, pos);
      }
   }
   
   /**
    * Normalize a ternary clause into an equivalent logical expression.
    * 
    * @param   root
    *          The parsed FQL root.
    * @param   parameters
    *          The query parameters.
    *
    * @return  the new root.
    */
   private HQLAst normalizeTernary(HQLAst root, Object[] parameters)
   {
      Predicate<Integer> compareOps =  (optype -> optype == GT || optype == LT || 
                                                  optype == GTE || optype == LTE || 
                                                  optype == EQUALS || optype == NOT_EQ);
      Predicate<Integer> logicalOps =  (optype -> optype == AND || optype == OR || optype == NOT);
      boolean ternaryProcessed = false;
      List<Aast> ternaries = null;
      inlinedTernary = false;
      
      do
      {
         ternaries = null;
         
         // walk the tree and identify all nodes requiring attention.
         Iterator<Aast> iter = root.iterator();
         while (iter.hasNext())
         {
            Aast next = iter.next();
            
            if (next.getType() == TERNARY)
            {
               // all ancestors until WHERE is encountered must be compare ops, logical ops or lparens
               boolean ok = true;
               
               Aast parent = next.getParent();
               while (parent != null)
               {
                  int optype = parent.getType();
                  
                  if (!(compareOps.test(optype) || logicalOps.test(optype) || optype == LPARENS))
                  {
                     ok = false;
                     break;
                  }
                  
                  parent = parent.getParent();
               }
               
               if (ok)
               {
                  if (ternaries == null)
                  {
                     ternaries = new LinkedList<>();
                  }
                  
                  ternaries.add(next);
               }
            }
         }
         
         if (ternaries == null)
         {
            break;
         }
         
         ternaryProcessed = true;
         
         for (Aast ternary : ternaries)
         {
            Aast parent = ternary.getParent();
            int ternaryPos = ternary.getIndexPos();
            
            while (parent != null && parent.getType() == LPARENS)
            {
               ternaryPos = parent.getIndexPos();
               
               // go up the LPARENS nodes
               parent = parent.getParent();
            }
            
            Aast reftest = ternary.getChildAt(0);
            Aast refthen = ternary.getChildAt(1);
            Aast refelse = ternary.getChildAt(2);

            Boolean testValue = null;
            
            if (reftest.getType() == SUBST)
            {
               int idx = ((Long) reftest.getAnnotation("index")).intValue();
               Object param = parameters[idx];
               
               if (param instanceof logical)
               {
                  testValue = ((logical) param).booleanValue();
                  inlinedTernary = true;
               }
            }
            
            if (parent == null || logicalOps.test(parent.getType()))
            {
               // refactor logical
               Aast ref = null;
               
               if (testValue == null)
               {
                  ref = TERNARY_LOGICAL_EXP_TEMPLATE.duplicateFresh();
                  
                  // go down the LPARENS node; ref is on first KW_OR
                  ref = (Aast) ref.getFirstChild();
                  
                  // build the KW_OR/KW_AND node
                  Aast ref2 = ref.getChildAt(0);
                  ref2.getChildAt(0).graft(reftest.duplicateFresh());
                  ref2.getChildAt(1).graft(refthen.duplicateFresh());
                  
                  // build the KW_OR/KW_AND/KW_NOT/LPARENS/EQUALS/LPARENS/<test> node
                  ref2 = ref.getChildAt(1) // KW_AND
                            .getChildAt(0) // KW_NOT
                            .getChildAt(0) // LPARENS
                            .getChildAt(0) // EQUALS
                            .getChildAt(0); // LPARENS
                  ref2.graft(reftest.duplicateFresh());
                  
                  // build the KW_OR/KW_AND/LPARENS/<else> node
                  ref2 = ref.getChildAt(1).getChildAt(1);
                  ref2.graft(refelse.duplicateFresh());
                  
                  // go back up the lparens node
                  ref = ref.getParent();
               }
               else
               {
                  ref = createAstNode(LPARENS, "(", null);
                  
                  if (testValue)
                  {
                     // only THEN branch survives
                     ref.graft(refthen.duplicateFresh());
                  }
                  else
                  {
                     // only ELSE branch survives
                     ref.graft(refelse.duplicateFresh());
                  }
               }

               if (parent == null)
               {
                  root = (HQLAst) ref;
               }
               else
               {
                  // remove the entire ternary expression, including any LPARENS
                  parent.getChildAt(ternaryPos).remove();

                  parent.graftAt(ref, ternaryPos);
               }
            }
            else if (compareOps.test(parent.getType()))
            {
               boolean doRefactor = true;
               
               Aast lthen = null; 
               Aast rthen = null;
               Aast lelse = null;
               Aast relse = null;

               if (ternaryPos == 0)
               {
                  Aast refother = parent.getChildAt(1);

                  // refactor compare left
                  lthen = refthen;
                  rthen = refother;
                  lelse = refelse;
                  relse = refother;
               }
               else if (parent.getChildAt(0).getType() != TERNARY)
               {
                  Aast refother = parent.getChildAt(0);

                  // refactor compare right
                  lthen = refother;
                  rthen = refthen;
                  lelse = refother;
                  relse = refelse;
               }
               else
               {
                  doRefactor = false;
               }
               
               if (doRefactor)
               {
                  if (testValue == null)
                  {
                     Aast refop = parent;
                     int parentPos = parent.getIndexPos();
                     Aast granpa = parent.getParent();
                     
                     // find a non-lparens ancestor, to which the new expr will be attached
                     while (granpa != null && granpa.getType() == LPARENS)
                     {
                        parentPos = granpa.getIndexPos();
                        granpa = granpa.getParent();
                     }

                     Aast ref = TERNARY_COMPARE_EXP_TEMPLATE.duplicateFresh();
   
                     if (granpa == null)
                     {
                        root = (HQLAst) ref;
                     }
                     else
                     {
                        granpa.getChildAt(parentPos).remove();
                        
                        // remove the entire comparison expression in which the ternary if was used
                        granpa.graftAt(ref, parentPos);
                     }
   
                     
                     // go down the LPARENS node; ref is KW_OR on lvl 1
                     ref = (Aast) ref.getFirstChild();
   
                     // build the KW_OR/KW_AND/LPARENS/<test> node
                     Aast ref2 = ref.getChildAt(0).getChildAt(0);
                     ref2.graft(reftest.duplicateFresh());
   
                     // build the KW_OR/KW_AND/<operator>(1)/LPARENS/<[l|r]then> node
                     ref2 = ref.getChildAt(0).getChildAt(1);
                     // set the op1 text/type
                     ref2.setType(refop.getType());
                     ref2.setText(refop.getText());
                     // set the [l|r]then nodes
                     ref2.getChildAt(0).graft(lthen.duplicateFresh());
                     ref2.getChildAt(1).graft(rthen.duplicateFresh());
   
                     // build the KW_OR/KW_AND(1)/KW_NOT/LPARENS/EQUALS/LPARENS/<test> node
                     ref2 = ref.getChildAt(1) // KW_AND(1)
                               .getChildAt(0) // KW_NOT
                               .getChildAt(0) // LPARENS
                               .getChildAt(0) // EQUALS
                               .getChildAt(0); // LPARENS
                     ref2.graft(reftest.duplicateFresh());
   
                     // build the KW_OR/KW_AND(1)/<operator>(1)/LPARENS/<[l/r]else> node
                     ref2 = ref.getChildAt(1).getChildAt(1);
                     // set the op2 text/type
                     ref2.setType(refop.getType());
                     ref2.setText(refop.getText());
                     // set the [l|r]else nodes
                     ref2.getChildAt(0).graft(lelse.duplicateFresh());
                     ref2.getChildAt(1).graft(relse.duplicateFresh());
                  }
                  else 
                  {
                     Aast ref = createAstNode(LPARENS, "(", null);
                     
                     if (testValue)
                     {
                        // only THEN branch survives
                        ref.graft(refthen.duplicateFresh());
                     }
                     else
                     {
                        // only ELSE branch survives
                        ref.graft(refelse.duplicateFresh());
                     }

                     // remove the entire ternary expression, including any LPARENS
                     parent.getChildAt(ternaryPos).remove();

                     parent.graftAt(ref, ternaryPos);
                  }
               }
            }
         }
      }
      while (true);
      
      if (ternaryProcessed)
      {
         List<Aast> lparens = null;
         
         Iterator<Aast> iter = root.iterator();
         while (iter.hasNext())
         {
            Aast next = iter.next();
            
            // collect superfluous LPARENS nodes, to be removed
            // TODO: this approach is not correct. The SQL operators precedence table must be consulted
            if (next.getType() == LPARENS && next.getNumImmediateChildren() == 1)
            {
               int firstChildType = next.getFirstChild().getType();
               
               if (firstChildType != OR        &&
                   firstChildType != NOT_NULL  &&
                   firstChildType != IS_NULL   &&
                   firstChildType != GTE       &&
                   firstChildType != GT        &&
                   firstChildType != LTE       &&
                   firstChildType != LT        &&
                   firstChildType != SUBSELECT &&
                   (next.getParent() == null || next.getParent().getType() != NOT))
               {
                  if (lparens == null)
                  {
                     lparens = new LinkedList<>();
                  }
                  
                  lparens.add(next);
               }
            }
         }
         
         if (lparens != null)
         {
            for (Aast lp : lparens)
            {
               Aast parref = lp.getParent();
               Aast ref = (Aast) lp.getFirstChild();
               int pos = lp.getIndexPos();
               lp.remove();
               ref.move(parref, pos);
            }
         }
      }
      
      return root;
   }
   
   /**
    * Analyze the query preprocessed tree and checks if it represents a simple ROWID/RECID lookup.
    * <p>
    * A simple ROWID/RECID lookup query only test for equality of the {@code id} field.
    *
    * @param   root
    *          The root of the tree obtained from preprocessing the FQL query.
    *
    * @return  {@code true} if the {@code root} represent the tree of a simple ROWID/RECID lookup
    *          query and {@code false} for any other kind of query.
    */
   private boolean checkFindByRowid(HQLAst root)
   {
      // this kind of tests require the operator to be EQUAL. LTE and GTE won't work!
      if (root.getType() != EQUALS)
      {
         return false;
      }
      
      HQLAst ch1 = (HQLAst) root.getFirstChild();
      HQLAst ch2 = (HQLAst) ch1.getNextSibling();
      // both ch1 and ch2 should not be null (their parent is EQUALS) if the query is well-formed
      if (ch1.getType() != ALIAS && ch2.getType() != ALIAS ||
          ch1.getType() == ALIAS && ch2.getType() == ALIAS)
      {
         return false; // none or both operands are database fields ?
      }
      HQLAst alias;
      HQLAst op;
      if (ch1.getType() == ALIAS) 
      {
         // NOTE: when comparing with RECID, the function MUST always be in the left of = (ch1),
         // when using the newer ROWID function, the operand may be switched
         alias = ch1;
         op = ch2;
      }
      else
      {
         alias = ch2;
         op = ch1;
      }
      
      HQLAst prop = (HQLAst) alias.getFirstChild();
      if (prop.getType() != PROPERTY || !DatabaseManager.PRIMARY_KEY.equals(prop.getText()))
      {
         // primary key property is converted the RECID/ROWID function
         return false; // not the property we are looking for
      }
      
      if (op.getType() == SUBST) 
      {
         // the RECID to be found is in param array. Save this information somewhere.
         return true;
      }
      
      if (op.getType() == UN_MINUS)
      {
         // the ROWID is a negative constant. Evaluate and save it.
         HQLAst absVal = (HQLAst) op.getFirstChild();
         if (absVal.getType() == NUM_LITERAL)
         {
            findByRowidValue = -Long.parseLong(absVal.getText());
            return true;
         }
         else
         {
            // something went wrong. TODO: need to check if this 'else' branch is ever accessible
            return false;
         }
      }
      if (op.getType() == NUM_LITERAL)
      {
         // rowid is a non-negative constant. Evaluate RECID and save it.
         findByRowidValue = Long.parseLong(op.getText());
         return true;
      }
      
      // otherwise must be other kind of equality test.
      return false;
   }
   
   /**
    * Check if the where clause is a look-up over a unique index.
    * <p>
    * The goal here is to identify a common business logic pattern: find by an unique index, usually
    * primary. The implementation doesn't detect if the where clauses combination suggest that 
    * zero or one record is expected. It just checks if the property matches match an unique 
    * index. Callers rely on this. Extending the method with heuristics for uniqueness beyond
    * unique indexes may cause problems.
    * 
    * @return   {@code true} if the where clause is a conjunction of equalities covering all
    *           the fields of an unique index.
    */
   private UniqueIndexLookup checkUniqueFind()
   {      
      List<PropertyMatch> matches = getPropertyMatches();
      if (matches == null)
      {
         return null;
      }
      
      RecordBuffer targetBuffer = null;
      List<String> properties = new ArrayList<>();
      for (int i = 0; i < matches.size(); i++)
      {
         PropertyMatch match = matches.get(i);
         
         if (match.operator != EQUALS)
         {
            return null;
         }
         
         RecordBuffer buffer = DataTypeHelper.lookupBuffer(match.alias, bufferMap, fql);
         if (buffer == null)
         {
            // this is unexpected; return false for fault tolerance
            return null;
         }
         
         if (targetBuffer == null)
         {
            targetBuffer = buffer;
         }
         else if (targetBuffer != buffer)
         {
            // mixed where clauses
            return null;
         }

         properties.add(match.property);
      }
      
      final DmoMeta meta = targetBuffer.getDmoInfo();
      final Dialect dialect = targetBuffer.getDialect();
      Function<String, String> honorComputed = (originalProp) -> 
      {
         Property propMeta = meta.getFieldInfo(originalProp);
         if (propMeta._isCharacter && dialect.needsComputedColumns())
         {
            Boolean ccIgnoreCase = meta.isIndexedIgnoreCase(originalProp);
            if (ccIgnoreCase != null)
            {
               return dialect.getComputedColumnPrefix(!ccIgnoreCase) + propMeta.column;
            }
         }
         
         return propMeta.column;
      };
      
      Iterator<Set<String>> uniques = meta.getUniqueConstraints().iterator();
      while (uniques.hasNext())
      {
         Set<String> compSet = uniques.next();
         // match exactly an unique index; 
         // having less fields doesn't make the where unique
         // having more fields allows the where clause to return zero records even if a record is 
         // found according to the unqiue index
         if (compSet.size() == properties.size() && properties.containsAll(compSet))
         {
            UniqueIndexLookup lookup = new UniqueIndexLookup();
            for (int i = 0; i < matches.size(); i++)
            {
               PropertyMatch match = matches.get(i);
               Object arg = null;
               switch (match.rvalType)
               {
                  case SUBST:
                     arg = null;
                     break;
                  case BOOL_TRUE:
                  case BOOL_FALSE:
                     arg = match.rvalType == BOOL_TRUE;
                     break;
                  case NUM_LITERAL:
                     arg = Long.parseLong(match.rval);
                     break;
                  case STRING:
                     if (match.rval.startsWith("'") && match.rval.endsWith("'"))
                     {
                        arg = match.rval.substring(1,  match.rval.length() - 1);
                     }
                     else
                     {
                        // panic
                        return null;
                     }
                     break;
                  case DEC_LITERAL:
                     arg = Double.parseDouble(match.rval);
                     break;
                  default:
                     return null; // safety
               }
               String sqlProp = honorComputed.apply(match.property);
               lookup.addMapping(sqlProp, arg);
            }
            return lookup;
         }
      }
      
      return null;
   }
   
   /**
    * Scan and parse the input where clause and return it in the form of an
    * abstract syntax tree.  Rewriting is not performed at this stage.
    *
    * @return  AST representing the original where clause.
    * 
    * @throws  PersistenceException
    *          if there is any error parsing the FQL.
    */
   private HQLAst parse()
   throws PersistenceException
   {
      HQLAst root = null;
      String fqlExpr = null;
      
      try
      {
         fqlExpr = fql.toFinalExpression();
         synchronized (astCache)
         {
            root = astCache.get(fqlExpr);
         }
         
         if (root != null)
         {
            return (HQLAst) root.duplicate();
         }
         
         StringReader reader = new StringReader(fqlExpr);
         HQLLexer lexer = new HQLLexer(reader);
         HQLParser parser = new HQLParser(lexer);
         
         parser.expression();
         root = (HQLAst) parser.getAST();
         root.fixups(null, null);
         
         synchronized (astCache)
         {
            astCache.put(fqlExpr, (HQLAst) root.duplicate());
         }
         
         if (LOG.isLoggable(Level.FINEST))
         {
            String sep = System.getProperty("line.separator");
            LOG.log(Level.FINEST,
                    "[" + database + "] Original FQL:  " + fql + sep + root.dumpTree());
         }
      }
      catch (Exception exc)
      {
         String msg = "Error parsing FQL";
         if (fqlExpr != null)
         {
            msg += (" [ " + fqlExpr + " ]");
         }
         
         if (LOG.isLoggable(Level.WARNING))
         {
            if (LOG.isLoggable(Level.FINE))
            {
               LOG.log(Level.FINE, msg, exc);
            }
            else
            {
               LOG.log(Level.WARNING, msg);
            }
         }
         
         throw new PersistenceException(msg, exc);
      }
      
      return root;
   }
   
   /**
    * Given an array containing one or more strings and zero or more nulls, replace those query substitution
    * parameters in the where clause AST which positionally coincide with the non-null strings in the array.
    * Each non-null string in the array represents a field reference to a specific DMO property. The SUBST
    * AST node whose index coincides with that of a field reference in the array is replaced with an ALIAS
    * node which has a PROPERTY child. If the field reference represents an extent field element, the PROPERTY
    * node has an LBRACKET child and an INDEX grandchild.
    * 
    * @param   root
    *          FQL AST root node.
    * @param   referenceSubs
    *          Array of field references and nulls.
    *
    * @return  A list of pairs of properties (belonging to different tables) whose equality is guaranteed by
    *          the where predicate. These will be used to simplify the sort order when the SQL query is
    *          composed.
    */
   private List<PropertyPair> inlineReferenceSubs(HQLAst root, String[] referenceSubs)
   {
      int len = referenceSubs.length;
      Map<HQLAst, String> replacements = new HashMap<>();
      
      Iterator<Aast> iter = root.iterator();
      int i = 0;
      int indexAdjustment = 0;
      while (iter.hasNext() && i < len)
      {
         Aast ast = iter.next();
         if (ast.getType() == SUBST)
         {
            String ref = referenceSubs[i++];
            if (ref != null)
            {
               // collect this node for replacement below
               replacements.put((HQLAst) ast, ref);
               indexAdjustment--;
            }
            else
            {
               // this substitution node stays, but its index must be adjusted if any earlier
               // substitution nodes have been slated for replacement
               if (indexAdjustment < 0)
               {
                  int index = ((Long) ast.getAnnotation("index")).intValue() + indexAdjustment;
                  ast.putAnnotation("index", Long.valueOf(index));
               }
            }
         }
      }
      
      if (replacements.isEmpty())
      {
         return null;
      }
      
      List<PropertyPair> ret = new ArrayList<>(replacements.size());
      for (Map.Entry<HQLAst, String> next : replacements.entrySet())
      {
         HQLAst aliasAst = next.getKey();
         String ref = next.getValue();
         
         // parse components from property reference
         int dot = ref.indexOf('.');
         String alias = ref.substring(0, dot);
         boolean upper = alias.startsWith("upper(");
         if (upper)
         {
            alias = alias.substring(6);
         }
         String property = ref.substring(dot + 1);
         int lbracket = property.indexOf('[');
         String index = null;
         if (lbracket > -1)
         {
            int rbracket = property.lastIndexOf(']');
            index = property.substring(lbracket + 1, rbracket);
            property = property.substring(0, lbracket);
         }
         else if (upper)
         {
            property = property.substring(0, property.length() - 1);
         }
         
         // rewrite AST branch
         aliasAst.setType(ALIAS);
         aliasAst.setText(alias);
         HQLAst propAst = createAstNode(PROPERTY, property, aliasAst);
         if (index != null)
         {
            createAstNode(INDEX, index, createAstNode(LBRACKET, "[", propAst));
         }
         
         // re-parent ALIAS with upper function if needed
         HQLAst parent = (HQLAst) aliasAst.getParent();
         if (upper)
         {
            int idxPos = aliasAst.getIndexPos();
            
            HQLAst upperAst = createAstNode(FUNCTION, "upper", null);
            
            aliasAst.remove();
            parent.graftAt(upperAst, idxPos);
            upperAst.graft(aliasAst);
            aliasAst = upperAst; 
         }
         
         if (parent == null)
         {
            return null;
         }
         
         // check if all the ancestors are AND (and parenthesis) nodes
         Aast it = parent.getParent();
         while (it != null)
         {
            if (it.getType() == AND || it.getType() == LPARENS)
            {
               it = it.getParent();
            }
            else
            {
               break;
            }
         }
         
         // if [it] isn't null then it represents the node different from AND and LPARENS in the parent's path
         if (it == null)
         {
            // analyze the left side of the operator
            AST refNode = parent.getFirstChild();
            if (refNode == aliasAst)
            {
               // handle [A op B] and [B op A] cases
               refNode = refNode.getNextSibling();
            }
            while (refNode.getType() == FUNCTION && 
                   (refNode.getText().equals("upper") || refNode.getText().equals("rtrim")))
            {
               refNode = refNode.getFirstChild();
            }
            // TODO: need to check whether refNode.type == ALIAS and refNode.firstChild.type == PROPERTY ? 
            
            ret.add(new PropertyPair(
                  refNode.getText(), refNode.getFirstChild().getText(), alias, property, parent.getText()));
         }
      }
      
      return ret;
   }
   
   /**
    * Walk the FQL AST, annotating query substitution parameter nodes with their data types as they are
    * visited. The annotation name used for this is {@code datatype}.
    *
    * @param   root
    *          Root AST node at which we begin the walk.
    * @param   paramTypes
    *          The type of each substitution parameter.
    */
   private void annotateParameters(HQLAst root, FqlType[] paramTypes)
   {
      int counter = 0;
      int len = paramTypes.length;
      
      Iterator<Aast> iter = root.iterator();
      while (iter.hasNext() && counter < len)
      {
         Aast ast = iter.next();
         if (ast.getType() == SUBST)
         {
            ast.putAnnotation("datatype", paramTypes[counter].toString());
            counter++;
         }
      }
   }
   
   /**
    * Walk the where clause AST and collect all property matches. For simple queries this can be a single,
    * binary, equality comparison of a database property to a substitution parameter or a literal
    * value; or it can be a group of such comparisons ANDed together. Functions enclosing alias
    * and property references are ignored. If any other expression is encountered, the analysis
    * is aborted and no property matches are stored. When simple is false, the property references are also
    * collected.
    * <p>
    * The purpose of this collection is to conduct a rudimentary analysis of the complexity of
    * the where clause and its suitability to participate as a sub-expression in a server-side
    * join.
    * 
    * @param   root
    *          FQL AST root node.
    * @param   simple
    *          Flag indicating if the collection will happen for simple or complex queries.
    */
   private void collectPropertyMatches(final HQLAst root, boolean simple)
   {
      // only [EQUALS] and [AND] nodes are valid types at the root
      switch (root.getType())
      {
         case AND:
         case EQUALS:
            break;
         default:
            return;
      }
      
      new AstWalkListener()
      {
         /** Depth in the tree; should not exceed 1 */
         private int level = 0;

         /** DMO alias names (can hold up to two aliases for complex conditions) */
         private String[] aliases = new String[2];

         /** DMO property names (can hold up to two properties for complex conditions) */
         private String[] properties = new String[2];
         
         /** Substitution parameter symbol or literal value */
         private String rval = null;
         
         /** Operator token type */
         private int operator = -1;
         
         /** Rvalue token type */
         private int rvalType = -1;

         /** Flag indicating if the alias is the first or second operand. */
         private boolean isAliasFirst = false;

         /** The index of the SUBST node relative to the 'where' clause parameters. */
         private long substIndex = -1;
         
         /** Error flag */
         private boolean error = false;
         
         /** Collected property matches */
         private ArrayList<PropertyMatch> matches = null;
         
         {
            if (walk() && matches != null)
            {
               matches.trimToSize();
               propertyMatches = Collections.unmodifiableList(matches);
            }
         }
         
         /**
          * Construct and add a property match to the list of collected matches.
          *
          * @param   simple
          *          Flag indicating if complex statements are analyzed.
          */
         private void addPropertyMatch(boolean simple)
         {
            if (aliases[0] == null       ||
                properties[0] == null    ||
                operator < 0             ||
                (simple && rvalType < 0) ||
                (simple && rval == null && rvalType != NULL))
            {
               error = true;
               return;
            }
            
            if (matches == null)
            {
               matches = new ArrayList<>();
            }

            // Handle simple or complex matches
            if (simple)
            {
               matches.add(new PropertyMatch(aliases[0].intern(),
                                             properties[0].intern(),
                                             rval != null ? rval.intern() : null,
                                             operator,
                                             rvalType,
                                             isAliasFirst,
                                             substIndex));
            }
            else
            {
               matches.add(new PropertyMatch(aliases[0].intern(),
                                             properties[0].intern(),
                                             aliases[1] != null ? aliases[1].intern() : null,
                                             properties[1] != null ? properties[1].intern() : null,
                                             operator));
            }

            // Reset for the next match
            aliases[0] = null;
            aliases[1] = null;
            properties[0] = null;
            properties[1] = null;
            rval = null;
            operator = -1;
            rvalType = -1;
            isAliasFirst = false;
            substIndex = -1;
         }
         
         /**
          * Walk the tree.
          * 
          * @return  <code>true</code> if no error was encountered, else <code>false</code>.
          *          An error indicates something unexpected or undesirable was encountered in
          *          the analysis and preempts the storage of any property matches collected so 
          *          far.
          */
         private boolean walk()
         {
            Iterator<Aast> iter = root.iterator(0, this);
            while (!error && iter.hasNext())
            {
               Aast next = iter.next();
               int type = next.getType();
               
               switch (type)
               {
                  case ALIAS:
                     if (level > 1 || (simple && aliases[0] != null))
                     {
                        return false; // Too deep or invalid for a simple match
                     }
                     if (aliases[0] == null)
                     {
                        aliases[0] = next.getText();
                        isAliasFirst = next.getIndexPos() == 0;
                     }
                     else
                     {
                        aliases[1] = next.getText(); // For complex match
                     }
                     break;

                  case PROPERTY:
                     if (level > 1 || (simple && properties[0] != null))
                     {
                        return false; // Too deep or invalid for a simple match
                     }
                     if (properties[0] == null)
                     {
                        properties[0] = next.getText();
                     }
                     else
                     {
                        properties[1] = next.getText(); // For complex match
                     }
                     break;
                     
                  case SUBST:
                     substIndex = (Long) next.getAnnotation("index");
                  case BOOL_TRUE:
                  case BOOL_FALSE:
                  case NUM_LITERAL:
                  case DEC_LITERAL:
                  case STRING:
                     rval = next.getText();
                     rvalType = type;
                     break;
                     
                  case IS_NULL:
                  case NOT_NULL:
                     rval = null;
                     rvalType = NULL;
                     // fall-through intentional
                  case EQUALS:
                  case NOT_EQ:
                  case GT:
                  case GTE:
                  case LT:
                  case LTE:
                     if (++level > 1)
                     {
                        // tree should be flat
                        return false;
                     }
                     operator = type;
                     break;
                     
                  case AND:
                  case FUNCTION:
                     break;
                     
                  default:
                     // everything else is an invalid node type that halts the analysis; we're
                     // expecting a single property match or a set of them ANDed together
                     return false;
               }
            }
            
            return !error;
         }
         
         /**
          * Upon encountering certain node types on ascent, add a property match, Called for
          * each ascent event.
          * 
          * @param   ast
          *          Parent node for the ascent event.
          */
         public void ascent(Aast ast)
         {
            if (ast == null)
            {
               return;
            }
            
            int type = ast.getType();
            switch (type)
            {
               case EQUALS:
               case NOT_EQ:
               case GT:
               case GTE:
               case LT:
               case LTE:
               case IS_NULL:
               case NOT_NULL:
                  level--;
                  addPropertyMatch(simple);
                  break;
                  
               default:
                  break;
            }
         }
         
         /**
          * Called for each descent event. No-op.
          * 
          * @param   ast
          *          Parent node for descent event.
          */
         public void descent(Aast ast)
         {
         }
         
         /**
          * Called for each next-child event. No-op.
          * 
          * @param   ast
          *          Parent node for next-child event.
          */
         public void nextChild(Aast ast, int index)
         {
         }
      };
   }
   
   /**
    * Walk the AST depth-first from root to leaves and rewrite certain nodes.
    * The following modifications are made in place in the given AST:
    * <ul>
    *   <li>A node which represents the qualified name of a DMO text property
    *       is restructured such that a new node representing an SQL trimming
    *       function is inserted between the qualified property name and its
    *       existing parent.  A string node representing the characters to be
    *       trimmed is inserted as the property name's next sibling (i.e.,
    *       the second parameter to the trimming function).
    *   <li>The first child of a FROM node represents the unqualified DMO
    *       interface name provided by business logic as part of a subselect
    *       phrase.  This must be replaced with the unqualified name of the
    *       the DMO implementation class which implements this interface.
    *       The second child of the FROM node represents the record buffer
    *       alias which uniquely identifies the record buffer in scope.  This
    *       information is used to retrieve the buffer and determine the DMO
    *       implementation name to use.
    * </ul>
    *
    * @param   root
    *          Root node of the AST for the FQL expression.
    * @param   parameters
    *          Query substitution parameters for the where clause.
    * @param   paramTypes
    *          The query substitution parameter types.
    * @param   dialect
    *          Database dialect in use.
    * @param   overload
    *          <code>true</code> to manually overload user-defined functions.
    * @param   indexedFields
    *          Map with keys being the fields from the 'order by' clause and values representing
    *          the order index.
    *
    * @throws  PersistenceException
    *          if any error occurred during rewriting the AST.
    * @throws  IllegalArgumentException
    *          if an alias encountered in the where clause is not mapped in
    *          the buffer manager.
    *
    * @return  The new root node of the AST for the FQL expression (it may change during processing).
    */
   private HQLAst mainWalk(HQLAst root,
                           Object[] parameters,
                           FqlType[] paramTypes,
                           Dialect dialect,
                           boolean overload,
                           Map<String, Integer> indexedFields)
   throws PersistenceException
   {
      boolean useSQLUdfs = dialect.isNativeUDFsSupported() &&
               !DatabaseManager.getConfiguration(database).isUseJavaUDFs();
      
      final Set<HQLAst> needTrim = new HashSet<>();
      final Set<HQLAst> needErrorHandling = new HashSet<>();
      Set<HQLAst> needRemove = null;
      Set<HQLAst> unknowns = null;
      Set<HQLAst> needSimplification = null;
      Set<HQLAst> needExplicitCastInsideTernary = null;
      Set<HQLAst> needExplicitCast = null;
      Set<HQLAst> needSQLBooleanConversion = dialect.supportsBooleanDatatype() ? null : new HashSet<>();
      Set<HQLAst> boolReplaceParent = null;
      Set<HQLAst> boolReplaceParentNegated = null;
      Set<HQLAst> convertCanDoToIn = null;
      Set<HQLAst> nullsInInSet = null;
      Set<HQLAst> droppedTrims = null;

      boolean hasPropertyMatch = propertyMatches != null;
      Set<String> coreIndexedFields = new HashSet<>();
      Map<String, Boolean> fieldsEqualState = new HashMap<>();
      for (Map.Entry<String, Integer> entry : indexedFields.entrySet())
      {
         fieldsEqualState.put(entry.getKey(), false);
      }

      boolean doInline = inline;
      inline = false;
      HQLAst parent;
      
      // walk the tree and identify all nodes requiring attention.
      Iterator<Aast> iter = root.iterator();
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         switch (next.getType())
         {
            case FUNCTION:
               FqlType funcType = null;
               String text = next.getText();
               
               // try to get rid of the opaque useless trimming function they will be replaced
               // with the expected result, ie empty string. 
               if ("trimws".equalsIgnoreCase(text))
               {
                  final HQLAst child1 = (HQLAst) next.getFirstChild();
                  boolean drop = child1.getType() == STRING && "''".equals(child1.getText());
                  
                  if (!drop && child1.getType() == SUBST)
                  {
                     int idx = ((Long) child1.getAnnotation("index")).intValue();
                     final Object subst = parameters[idx];
                     String patt = subst instanceof String ? (String) subst :
                                   subst instanceof Text ? ((Text) subst).toJavaType() :
                                   subst.toString();
                     drop = patt.isEmpty();
                  }
                  
                  if (drop)
                  {
                     droppedTrims = new HashSet<>();
                     droppedTrims.add(next);
                     continue; // this node will be dropped
                  }
               }
               if ("todate".equalsIgnoreCase(text) && next.getNumImmediateChildren() == 1)
               {
                  FqlType type = DataTypeHelper.expressionType(next.getChildAt(0), bufferMap);
                  
                  if (type == FqlType.DATE)
                  {
                     if (needRemove == null)
                     {
                        needRemove = new HashSet<>();
                     }
                     
                     needRemove.add(next);
                  }
               }
               if ("tostring".equalsIgnoreCase(text) && 
                    dialect.isNativeUDFsSupported() &&
                    next.getNumImmediateChildren() == 2 )
               {
                  FqlType type = DataTypeHelper.expressionType(next.getChildAt(0), bufferMap);
                  if (type == FqlType.DATE || type == FqlType.DATETIME)
                  {
                     HQLAst formatNode = new HQLAst();
                     formatNode.setType(STRING);
                     formatNode.setText(SessionAttr.DATE_FORMAT.placeHolder);
                     next.graftAt(formatNode, 2); 
                     dependsOnSessionAttribute = true;
                  }
                  // TODO: a more detailed analisys of how SESSION:TIMEZONE
                  //       affects format-driven conversion of timestamps to string is required.
                  
                  if (useSQLUdfs && type == FqlType.DATETIME)
                  {
                     HQLAst tzNode = new HQLAst();
                     tzNode.setType(STRING);
                     tzNode.setText(SessionAttr.TIMEZONE.placeHolder);
                     next.graftAt(tzNode, 3); 
                     dependsOnSessionAttribute = true;
                  }
                  if (useSQLUdfs && type == FqlType.LONG)
                  {
                     HQLAst tzNode = new HQLAst();
                     tzNode.setType(STRING);
                     tzNode.setText(SessionAttr.TIMEZONE.placeHolder);
                     next.graftAt(tzNode, 2); 
                     dependsOnSessionAttribute = true;
                  }
                  if (next.getParent().getType() == EQUALS &&
                      !dialect.isAutoRtrim())
                  {
                     needTrim.add(next);
                  }
               }
               if (!"upper".equalsIgnoreCase(text) && next.ancestor(-1, SUBSELECT))
               {
                  HQLAst hit = null;
                  funcType = DataTypeHelper.expressionType(next, bufferMap);
                  if (funcType == FqlType.BOOLEAN)
                  {
                     hit = next;
                  }
                  else
                  {
                     parent = (HQLAst) next.getParent();
                     if (DataTypeHelper.expressionType(parent, bufferMap) == FqlType.BOOLEAN)
                     {
                        hit = parent;
                     }
                  }
                  
                  if (hit != null)
                  {
                     needErrorHandling.add(hit);
                  }
               }
               
               if (funcType == null)
               {
                  funcType = DataTypeHelper.expressionType(next, bufferMap);
               }
               
               // identify whether this is a text node which needs to be reparented with a
               // trimming function. Avoid superfluous trimming (if [next] is itself a [rtrim]
               // function or [trimws] with second parameter missing)
               if (funcType == FqlType.TEXT           && 
                   !"upper".equalsIgnoreCase(text)    &&
                   !"rtrim".equalsIgnoreCase(text)    &&
                   (!"trimws".equalsIgnoreCase(text) ||
                    next.getFirstChild().getNextSibling() != null))
               {
                  needTrim.add(next);
               }
               
               // there is a bug in PostgreSQL when calling pl/java functions with no arguments
               // it will display "Too many parameters - expected 0"
               // however, we can "inline" the value of the static function at this moment by
               // replacing the FUNCTION node with an integer with actual value;
               // this is faster as no supplementary pl/java call will be done on sql-server side
               if (doInline && next.getNumImmediateChildren() == 0)
               {
                  if ("getMtime".equals(text))
                  {
                     next.setType(NUM_LITERAL);
                     next.setText(datetime.now().getTime() + "");
                     next.putAnnotation("inlined", Boolean.TRUE);
                     inline = true;
                  }
                  
                  if ("getTimezone".equals(text))
                  {
                     next.setType(NUM_LITERAL);
                     next.setText(date.getDefaultOffset() + "");
                     next.putAnnotation("inlined", Boolean.TRUE);
                     inline = true;
                  }
               }
               
               // toDate(string) / toDatetime(string) / toDatetimeTz(string) functions must be 
               // used in the 2nd form (with format) because the SESSION:DATE-FORMAT and 
               // WINDOWING-YEAR are not accessible sql-server-side
               if (next.getNumImmediateChildren() == 1    && 
                   (next.getChildAt(0).getType() == STRING || 
                    next.getChildAt(0).getType() == FUNCTION) &&
                   ("toDate".equals(text)     ||
                    "toDatetime".equals(text) ||
                    "toDatetimeTz".equals(text)))
               {
                  HQLAst formatNode = new HQLAst();
                  formatNode.setType(STRING);
                  formatNode.setText(SessionAttr.DATE_FORMAT.placeHolder);
                  next.graftAt(formatNode, 1); // as 2nd parameter
                  dependsOnSessionAttribute = true;
                  
                  HQLAst windowingNode = new HQLAst();
                  windowingNode.setType(NUM_LITERAL);
                  windowingNode.setText(date.getWindowingYear() + "");
                  next.graftAt(windowingNode, 2); // as 3rd parameter
               }
               
               boolean overloadThis = overload; 
               // this must be checked before overloading the node, as a preferable alternative
               if ("matchesList".equals(text))
               {
                  final HQLAst child1 = (HQLAst) next.getFirstChild();
                  if (child1.getType() == SUBST)
                  {
                     int idx = ((Long) child1.getAnnotation("index")).intValue();
                     final Object subst = parameters[idx];
                     String patt = subst instanceof String ? (String) subst :
                                   subst instanceof Text ? ((Text) subst).toJavaType() :
                                   subst.toString();
                     // the pattern must not contain '*' or '!' reserved characters otherwise
                     // cannot it be changed into a [IN] node 
                     if (patt != null && patt.indexOf('*') == -1 && patt.indexOf('!') == -1)
                     {
                        if (convertCanDoToIn == null)
                        {
                           convertCanDoToIn = new HashSet<>();
                        }
                        convertCanDoToIn.add(next);
                        next.putAnnotation("pattern", patt); // cache it
                        overloadThis = false;
                     }
                  }
               }
               
               // if manual function overloading is required, do so at this time
               if (overloadThis)
               {
                  manuallyOverload(next);
               }
               if (useSQLUdfs && isUDF(dialect, next) && !"contains".equalsIgnoreCase(text))
               {
                  next.setText(dialect.udfSchema() + next.getText());
               }
               if (next.getParent() == null && !dialect.supportsBooleanDatatype())
               {
                  // this is a predicate root-level boolean function. Replace with boolFn() = 1
                  needSQLBooleanConversion.add(next);
               }
               
               if ("exists".equals(text) && next.getFirstChild().getType() == SUBSELECT)
               {
                  HQLAst subSelectNode = (HQLAst) next.getFirstChild();
                  
                  if (subSelectNode.getFirstChild().getType() == SELECT)
                  {
                     break;
                  }
                  
                  HQLAst selectNode = new HQLAst();
                  selectNode.setType(SELECT);
                  selectNode.setText("select");
                  subSelectNode.graftAt(selectNode, 0);
                  
                  HQLAst selectValueNode = new HQLAst();
                  selectValueNode.setType(NUM_LITERAL);
                  selectValueNode.setText("1");
                  subSelectNode.graftAt(selectValueNode, 1);
               }
               
               if ("contains".equalsIgnoreCase(text))
               {
                  hasContains = true;
                  nonConstantContains = nonConstantContains || !isConstant(next);
               }
               
               break;
               
            case PROPERTY:
               if (next.getParent().getType() == ALIAS)
               {
                  RecordBuffer targetBuf = bufferMap.get(null);
                  if (targetBuf != null && targetBuf.getDMOAlias().equals(next.getParent().getText()))
                  {
                     String nextText = next.getText();
                     DmoMeta dmoInfo = targetBuf.getDmoInfo();
                     RecordMeta recordMeta = dmoInfo.getRecordMeta();
                     int bitIdx = recordMeta.getIndexOfProperty(nextText);
                     if (bitIdx >= 0)
                     {
                        // handling only genuine properties / SQL columns
                        if (queryProperties == null)
                        {
                           queryProperties = new BitSet(recordMeta.getPropertyMeta(false).length);
                        }
                        queryProperties.set(bitIdx);
                     }
                     else
                     {
                        if (!nextText.equals(Session.PK))
                        {
                           // this is usually happening for <table>.<expanded_extent_field>[?] syntax
                           // just mark all extent fields as being queried in this case
                           List<Property> props = dmoInfo.getCustomExtentFieldInfo(nextText);
                           if (props != null && !props.isEmpty())
                           {
                              if (queryProperties == null)
                              {
                                 queryProperties = new BitSet(recordMeta.getPropertyMeta(false).length);
                              }
                              
                              Property prop = props.get(0);
                              int from = recordMeta.getIndexOfProperty(prop.name);
                              queryProperties.set(from, from + prop.extent);
                           }
                           else
                           {
                              if (LOG.isLoggable(Level.WARNING))
                              {
                                 LOG.log(Level.WARNING, "Unidentified DMO property in FQL: " + nextText + 
                                          " of " + targetBuf.getDMOInterface().getName());
                              }
                           }
                        }
                     }
                  }

                  // already processed as qualified property
                  continue;
               }
               // normally these nodes occur when internally processing HQLs
               if (!dialect.isAutoRtrim() &&
                   DataTypeHelper.expressionType(next, bufferMap) == FqlType.TEXT && 
                   DataTypeHelper.getJavaTypeUnqualifiedProperty(next, bufferMap) != handle.class)
               {
                  needTrim.add(next);
               }
               break;
               
            case ALIAS:
               boolean isMatchOperand = false;
               boolean isBeginsOperand = false;
               
               HQLAst child0 = (HQLAst) next.getChildAt(0);
               HQLAst grandpa = (HQLAst) next.getAncestor(2);
               
               if (grandpa != null)
               {
                  // LIKE (aka MATCH operator) must not be trimmed
                  isMatchOperand = (grandpa.getType() == LIKE);
                  // begins_n (aka BEGINS operator) also must not be trimmed
                  isBeginsOperand = (grandpa.getType() == FUNCTION &&
                                     grandpa.getText().startsWith("begins"));
               }
               
               // Identify whether this is a text node which needs to be reparented with a
               // trimming function or replaced with a computed column reference.
               if (child0 != null   && // omit standalone alias
                   !isMatchOperand  && // skip MATCHES operands
                   !isBeginsOperand && // skip BEGINS operands
                   !dialect.isAutoRtrim() &&
                   DataTypeHelper.expressionType(next, bufferMap) == FqlType.TEXT &&
                   DataTypeHelper.getJavaTypeQualifiedProperty(next, bufferMap) != handle.class)
               {
                  needTrim.add(next);
               }
               
               // Determine whether this where clause represents a trigger for
               // the Progress quirk which leaks a snapshot of all uncommitted
               // changes to other sessions.  Record the affected entity in
               // the list of entities who will have their uncommitted changes
               // so published.
               if (child0 != null && child0.getType() == PROPERTY)
               {
                  parent = (HQLAst) next.getParent();
                  while (parent != null && !isBinaryOperator(parent))
                  {
                     parent = (HQLAst) parent.getParent();
                  }
                  
                  if (parent != null)
                  {
                     switch (parent.getType())
                     {
                        // EQ RECID comparison should be included in record leaks
                        
                        case EQUALS:
                           if (child0.getText() == null || !child0.getText().toLowerCase().equals("recid"))
                           {
                              break;
                           }
                           
                        // not EQ simple comparison should be included in record leaks
                        case NOT_EQ:
                           RecordBuffer buffer = lookupBuffer(next);
                           String entity = buffer.getEntityName();
                           if (!buffer.isTemporary())
                           {
                              if (earlyPublishEntities == null)
                              {
                                 earlyPublishEntities = new LinkedHashSet<String>();
                              }
                              earlyPublishEntities.add(entity);
                           }
                           break;
                           
                        default:
                           break;
                     }
                  }
               }
               
               continue;
               
            // Use the DMO alias to look up the DMO implementation name and
            // replace the text of the first child of a FROM clause.
            case FROM:
               HQLAst c0 = (HQLAst) next.getChildAt(0);
               HQLAst c1 = (HQLAst) next.getChildAt(1);
               if (c1 == null)
               {
                  // Create default alias by lowercasing the unqualified DMO name.
                  String name = c0.getText();
                  c1 = createAstNode(ALIAS, StringHelper.changeCase(name, false), null);
               }
               RecordBuffer buffer = lookupBuffer(c1);
               c0.setText(buffer.getDMOImplementationName());
               
               continue;
            
            case NULL:
               if (isBinaryOperator(next.getParent()))
               {
                  if (unknowns == null)
                  {
                    unknowns = new HashSet<>();
                  }
                  
                  unknowns.add(next);
               }
               
               break;
               
            // Possibly inline certain query substitution parameters.
            case SUBST:
               parent = (HQLAst) next.getParent();
               if (parent != null)
               {
                  int index = ((Long) next.getAnnotation("index")).intValue();
                  Object param = parameters[index];
                  
                  if (param instanceof datetimetz)
                  {
                     // next.putAnnotation("positional-params", Long.valueOf(2));
                  }
                  
                  // check for unknown value
                  if (param instanceof BaseDataType && ((BaseDataType) param).isUnknown())
                  {
                     if (parent.getType() == IN)
                     {
                        // we handle this individually because there is no "opposite operand" in 
                        // the IN set
                        if (nullsInInSet == null)
                        {
                           nullsInInSet = new HashSet<>();
                        }
                        nullsInInSet.add(next);
                        continue;
                     }
                     
                     if (unknowns == null)
                     {
                        unknowns = new HashSet<>();
                     }
                     
                     // if we have an unknown value as subscript, the extent
                     // field value is unknown (i.e. field[?] = ?), and we
                     // need to add it to the set instead of subscript
                     if (parent.getType() == LBRACKET)
                     {
                        parent = (HQLAst) parent.getParent();
                        if (parent != null && parent.getType() == PROPERTY)
                        {
                           HQLAst alias = (HQLAst) parent.getParent();
                           if (alias != null && alias.getType() == ALIAS)
                           {
                              next = alias;
                           }
                           else
                           {
                              next = parent;
                           }
                        }
                     }
                     
                     // if unknown value has a wrapping function, value of
                     // this function is also unknown
                     while (true)
                     {
                        parent = (HQLAst) next.getParent();
                        if (parent != null && parent.getType() == FUNCTION)
                        {
                           next = parent;
                        }
                        else
                        {
                           break;
                        }
                     }

                     unknowns.add(next);

                     continue;
                  }
                  
                  /* we CAN detect expressions like "(bool_subexpr) = %p" but at this moment we
                     CANNOT "(bool_subexpr_1) = (bool_subexpr_2)", which non-boolean dialects
                     don't support (comparing 2 booleans since such datatype does not exist) */
                  if ((param instanceof logical) &&
                      !((logical) param).isUnknown() &&
                      !dialect.supportsBooleanDatatype() && 
                      (parent.getType() == EQUALS || parent.getType() == NOT_EQ))
                  {
                     logical boolParam = (logical) param;
                     HQLAst boolExpr = (HQLAst) parent.getChildAt(1 - next.getIndexPos());
                     if (boolExpr.getType() == LPARENS)
                     {
                        boolExpr = (HQLAst) boolExpr.getFirstChild();
                     }
                     // we have an issue, the expression, starting with parent needs morphing
                     // the depending on both parent & param, see below
                     if (parent.getType() == EQUALS && boolParam.toJavaType() ||
                         parent.getType() == NOT_EQ && !boolParam.toJavaType())
                     {
                        // EQUALS(boolExp, true),  EQUALS(true, boolExp)  => (boolExp)
                        // NOT_EQ(boolExp, false), NOT_EQ(false, boolExp) => (boolExp)
                        if (boolReplaceParent == null)
                        {
                           boolReplaceParent = new HashSet<>();
                        }
                        boolReplaceParent.add(boolExpr); // replace parent by (boolExpr)
                     }
                     else
                     {
                        // EQUALS(boolExp, false), EQUALS(false, boolExp) => NOT(boolExp)
                        // NOT_EQ(boolExp, true),  NOT_EQ(true, boolExp)  => NOT(boolExp)
                        if (boolReplaceParentNegated == null)
                        {
                           boolReplaceParentNegated = new HashSet<>();
                        }
                        boolReplaceParentNegated.add(boolExpr); // replace parent by NOT(boolExpr)
                     }
                  }
                  
                  if (doInline)
                  {
                     switch (parent.getType())
                     {
                        case LIKE:
                        case GT:
                        case LT:
                        case GTE:
                        case LTE:
                        case OR:
                        case AND:
                           int tokenType = inlineSubstitutionParameter(next, parameters, dialect);
                           if (tokenType == BOOL_TRUE || tokenType == BOOL_FALSE)
                           {
                              if (needSimplification == null)
                              {
                                 needSimplification = new HashSet<>();
                              }
                              needSimplification.add(next);
                           }
                           break;
                           
                        default:
                           continue;
                     }
                  }
                  else
                  {
                     // a query substitution parameter which is the child of an arithmetic
                     // operator gets an explicit cast to prevent the ORM from inferring the
                     // wrong data type; generally, it will get the type right for logical
                     // comparison operations, so we don't clutter up the expression in those
                     // cases (refs #3838)
                     switch (parent.getType())
                     {
                        case PLUS:
                        case MINUS:
                        case MULTIPLY:
                        case DIVIDE:
                           if (needExplicitCast == null)
                           {
                              needExplicitCast = new HashSet<>();
                           }
                           needExplicitCast.add(next);
                           break;
                     }
                  }
               }
               break;
               
            case TERNARY:
               HQLAst condAst = (HQLAst) next.getChildAt(0);
               if (!dialect.supportsBooleanDatatype() &&
                   (condAst.getType() == SUBST ||
                    (condAst.getType() == FUNCTION && !"exists".equals(condAst.getText()))))
               {
                  // exists SQL function already returns boolean, skip it. 
                  needSQLBooleanConversion.add(condAst);
               }
               
               if (dialect.requiresExplicitCastInsideTernary())
               {
                  if (needExplicitCastInsideTernary == null)
                  {
                     needExplicitCastInsideTernary = new HashSet<>();
                  }
                  needExplicitCastInsideTernary.add(next);
               }
               break;
            
            case NOT:
               if (!dialect.supportsBooleanDatatype())
               {
                  // replace NOT(boolFn()) with (boolFn() = 0)
                  // in case of SUBST, the value is a boolean
                  
                  HQLAst t1 = (HQLAst) next.getChildAt(0);
                  if (t1.getType() == LPARENS)
                  {
                     // (only in case of NOT) multiple wrapped LPARENS shouldn't be encountered
                     t1 = (HQLAst) t1.getChildAt(0);
                  }
                  if ((t1.getType() == FUNCTION && !"exists".equals(t1.getText())) ||
                      t1.getType() == SUBST)
                  {
                     // exists SQL function already returns boolean, skip it.
                     needSQLBooleanConversion.add(next);
                  }
               }
               break;
            
            case AND:
            case OR:
               if (!dialect.supportsBooleanDatatype())
               {
                  // replace OR(boolFn1(), boolFn2())  with (boolFn1() + boolFn2() > 0)
                  // replace AND(boolFn1(), boolFn2()) with (boolFn1() + boolFn2() = 2)
                  // in case of SUBST, the value is a boolean
                  
                  HQLAst t1 = (HQLAst) next.getChildAt(0);
                  if ((t1.getType() == FUNCTION && !"exists".equals(t1.getText())) || 
                      t1.getType() == SUBST)
                  {
                     // exists SQL function already returns boolean, skip it.
                     needSQLBooleanConversion.add(t1);
                  }
                  
                  HQLAst t2 = (HQLAst) next.getChildAt(1);
                  if ((t2.getType() == FUNCTION && !"exists".equals(t2.getText())) || 
                      t2.getType() == SUBST)
                  {
                     // exists SQL function already returns boolean, skip it.
                     needSQLBooleanConversion.add(t2);
                  }
               }
               
               // TODO: add rules for:
               //       AND(X, TRUE) -> X          AND(TRUE, X) -> X
               //       AND(X, FALSE) -> FALSE     AND(FALSE, X) -> FALSE
               //       OR(X, TRUE/FALSE) -> X     OR(TRUE/FALSE, X) -> X
               
               break;
            
            case LIKE:
               // first we should check if the ESCAPE is not already set (normally it's not)
               // do this only for MS SQL Server Dialect whose escape sequence for _ is [_]
               if (dialect instanceof P2JSQLServer2008Dialect &&
                   next.getImmediateChild(ESCAPE, null) == null)
               {
                  HQLAst escape = new HQLAst();
                  escape.setType(ESCAPE);
                  escape.setText("escape");
                  
                  HQLAst escapeStr = new HQLAst();
                  escapeStr.setType(STRING);
                  escapeStr.setText("'\\'"); // backslash between single quotes
                  escape.graft(escapeStr);   // add as the only child
                  
                  next.graft(escape);        // add as last child
               }
               break;
            
            case BOOL_FALSE:
            case BOOL_TRUE:
               if (!dialect.supportsBooleanDatatype())
               {
                  // this is a strange case of WHERE predicate dynamically generated
                  needSQLBooleanConversion.add(next);
               }
               break;

            default:
               continue;
         }
      }
      
      try
      {
         // if any nodes were identified as needing the rtrim function, reparent them with this
         // function now or replace them with computed column references.
         for (HQLAst next : needTrim)
         {
            // if a node is marked as unknown, it should not be trimmed. The tree is more
            // complex to process and the result is also unknown.
            if (unknowns != null && unknowns.contains(next))
            {
               continue;
            }
            // Do not trim UDF argument
            if (isUDFArgument(dialect, next))
            {
               continue;
            }   
            parent = (HQLAst) next.getParent();
            boolean handled = false;
            
            // now try to use computed column references, if the dialect calls for it.
            // Only valid for ALIAS nodes, not FUNCTION nodes.
            if (next.getType() == ALIAS)
            {
               handled = injectComputedColumn(next); // next is a qualified property node
               if (!handled && dialect.needsComputedColumns())
               {
                  // double check: the computed column might have already been 'injected'
                  // (this is the case of compound queries / server join optimizations)
                  // note: the injectComputedColumn() will return false for already decorated
                  //       CCs because DatabaseManager.getIgnoreCase() fails to recognise them
                  HQLAst property = (HQLAst) next.getFirstChild();
                  if (dialect.isComputedColumn(property.getText()))
                  {
                     handled = true;
                  }
               }
            }
            else if (next.getType() == PROPERTY)
            {
               // handle unqualified properties 
               handled = dialect.isComputedColumn(next.getText()) || // already injected? 
                         injectComputedColumn(next); // next is an unqualified property node
            }
            
            if (!handled && dialect.isAutoRtrim() && dialect.isAutoCi())
            {
               handled = true; // rtrim and upper not need; they are handled automatically
            }
            
            // do not rtrim if under [concat] node
            if (!handled)
            {
               HQLAst ref = (HQLAst) next.getParent();
               if (ref != null && ref.getType() == FUNCTION && ref.getText().equals("upper"))
               {
                  // get up the ladder if an [upper] was injected at conversion time.
                  ref = (HQLAst) ref.getParent();
               }
               if (ref != null               && 
                   ref.getType() == FUNCTION && 
                   ref.getText().startsWith("concat"))
               {
                  // the [concat] node might and should have been preprocessed already
                  handled = true;
               }
            }
            
            // if already parented by rtrim or trimws, don't inject another rtrim
            if (!handled)
            {
               parent = (HQLAst) next.getParent();
               String pText = parent != null ? parent.getText() : null;
               handled = parent != null &&
                         parent.getType() == FUNCTION &&
                         ("rtrim".equals(pText) || "trimws".equals(pText));
            }
            
            // if not handled with a computed column reference, inject an rtrim() function call
            // around the qualified property name.
            if (!handled)
            {
               int idxPos = next.getIndexPos();
               next.remove();
               HQLAst trimFunc = createAstNode(FUNCTION, "rtrim", null);
               trimFunc.graft(next);
               parent.graftAt(trimFunc, idxPos);
               
               // annotate the node with a reference to its contained alias to make downstream
               // processing simpler
               HQLAst marked = null;
               if (parent.getType() == FUNCTION && "upper".equalsIgnoreCase(parent.getText()))
               {
                  marked = parent;
               }
               else
               {
                  marked = trimFunc;
               }
               marked.putAnnotationObject("alias-ref", next);
            }
         }
         
         if (needRemove != null)
         {            
            // remove superfluous nodes
            for (HQLAst nodeToRemove: needRemove)
            {
               HQLAst removedNodeParent = (HQLAst) nodeToRemove.getParent();
               int childIndex = nodeToRemove.getIndexPos();
               HQLAst removedNodeChild = (HQLAst) nodeToRemove.getChildAt(0);
               nodeToRemove.remove();
               
               removedNodeParent.graftAt(removedNodeChild, childIndex);
            }
         }
         
         // if any nodes were identified as needing error handling, reparent
         // them with the checkError() function now.
         for (HQLAst next : needErrorHandling)
         {
            String udfSchema = dialect.udfSchema();
            int idxPos = next.getIndexPos();
            parent = (HQLAst) next.getParent();
            
            String checkErrorFn = (useSQLUdfs ? udfSchema : "") + dialect.checkErrorFn();
            String initErrorFn = (useSQLUdfs ? udfSchema : "") + dialect.initErrorFn();
            
            HQLAst checkErrFunc = createAstNode(FUNCTION, checkErrorFn, null);
            HQLAst initErrFunc = createAstNode(FUNCTION, initErrorFn, checkErrFunc);
            HQLAst falseParm = createAstNode(BOOL_FALSE, "false", initErrFunc);
            
            next.remove();
            parent.graftAt(checkErrFunc, idxPos);
            
            int type = next.getType();
            if (!dialect.supportsBooleanDatatype() &&
                (type == EQUALS || type == NOT_EQ || type == GT || type == GTE ||
                 type == LT || type == LTE || type == AND || type == OR || type == NOT))
            {
               // this dialect does not accept true boolean parameters for UDF, we need to 
               // convert the results of these operators into BITs: 0 and 1 in order to be
               // accepted by our 'checkError' UDF 
               
               HQLAst caseNode = createAstNode(TERNARY, "when", checkErrFunc);
                  caseNode.graft(next);
                  createAstNode(NUM_LITERAL, "1", caseNode);
                  createAstNode(NUM_LITERAL, "0", caseNode);
            }
            else
            {
               checkErrFunc.graft(next);
            }
            if (!dialect.supportsBooleanDatatype() &&
                (parent.getType() == NOT || parent.getType() == OR || parent.getType() == AND))
            {
               // in the case of this dialect, we need to switch back to genuine boolean
               // datatypes because the dbo.checkError_bb returns BIT which is incompatible
               // as operand of the logical operators:
               needSQLBooleanConversion.add(checkErrFunc);
            }
            if (useSQLUdfs)
            {
               Iterator<Aast> it = next.iterator();
               while (it.hasNext())
               {
                  HQLAst n = (HQLAst) it.next();
                  if (n.getType() == FUNCTION && useSQLUdfs && isUDF(dialect, n))
                  {
                     String fn = n.getText();
                     if (fn.equals(checkErrorFn) || fn.equals(initErrorFn))
                     {
                        continue;
                     }
                     if (fn.startsWith(udfSchema))
                     {
                        fn = fn.substring(udfSchema.length());
                     }
                     n.setText(udfSchema + GUARDED + fn);
                  }
               }                 
            }
         }

         // special processing to evaluate comparisons between fields and unknown value
         if (hasPropertyMatch)
         {
            for (int i = 0, len = propertyMatches.size(); i < len; i++)
            {
               PropertyMatch match = propertyMatches.get(i);
               String fullName = match.alias + "." + match.property;

               if (!indexedFields.containsKey(fullName))
               {
                  continue;
               }

               boolean isNullEqualState = fieldsEqualState.get(fullName) == null;
               int substIndex = (int) match.substIndex;
               boolean isUnknownSubst = substIndex != -1 &&
                                        parameters[substIndex] instanceof BaseDataType &&
                                        ((BaseDataType)parameters[substIndex]).isUnknown();
               if (isUnknownSubst)
               {
                  if (match.operator != EQUALS && !isNullEqualState)
                  {
                     fieldsEqualState.put(fullName, null);
                  }

                  if (((match.operator == LT || match.operator == LTE) && match.isAliasFirst) ||
                      ((match.operator == GT || match.operator == GTE) && !match.isAliasFirst))
                  {
                     // it enters here only on tt.f < ?, tt.f <= ?, ? > tt.f and ? >= tt.f
                     coreIndexedFields.add(fullName);
                  }
               }

               if (match.operator == EQUALS && !isNullEqualState)
               {
                  fieldsEqualState.put(fullName, true);
               }
            }
         }

         // get rid of unknowns
         if (unknowns != null)
         {
            Set<HQLAst> createdBooleans = simplifyUnknowns(unknowns, fieldsEqualState,
                                                           coreIndexedFields, indexedFields);
            if (createdBooleans != null)
            {
               if (needSimplification == null)
               {
                  needSimplification = new HashSet<>();
               }
               needSimplification.addAll(createdBooleans);
            }
         }
         
         // perform simplification of expressions containing boolean operands
         // (see trySimplifyBooleans() function description)
         if (needSimplification != null)
         {
            for (HQLAst next : needSimplification)
            {
               root = trySimplifyBooleans(next, root);
            }
            if (needSQLBooleanConversion != null &&
                (root.getType() == BOOL_FALSE || root.getType() == BOOL_TRUE))
            {
               // in the case the predicate was simplified to a boolean value, in the case of 
               // SQL Server dialect, it must be rewritten since there boolean literals are not
               // supported 
               needSQLBooleanConversion.add(root);
            }
         }
         
         // replace "then ? else ?" with "then cast(? as <datatype>) else
         // cast(? as <datatype>)" for H2 dialect
         if (needExplicitCastInsideTernary != null)
         {
            for (HQLAst next : needExplicitCastInsideTernary)
            {
               makeExplicitCast(next, 1, dialect);
               makeExplicitCast(next, 2, dialect);
            }
         }
         
         if (needExplicitCast != null)
         {
            for (HQLAst next : needExplicitCast)
            {
               makeExplicitCast((HQLAst) next.getParent(), next.getIndexPos(), dialect);
            }
         }
         
         if (needSQLBooleanConversion != null && !needSQLBooleanConversion.isEmpty())
         {
            // needSQLBooleanConversion isn't null only for dialects that doesn't support boolean
            for (HQLAst ast : needSQLBooleanConversion)
            {
               int relPos = ast.getIndexPos();
               HQLAst parentNode = (HQLAst) ast.getParent();
               ast.remove(); // cut from its parent
               
               int astType = ast.getType();
               if (astType == NOT)
               {
                  // replace NOT(boolFn()) with (boolFn() = 0)
                  
                  HQLAst newParent = createAstNode(LPARENS, "(", null);
                  HQLAst eqTest = createAstNode(EQUALS, "=", newParent);
                  HQLAst ch = (HQLAst) ast.getChildAt(0);
                  if (ch.getType() == LPARENS)  // skip extra LPARENS
                  {
                     ch = (HQLAst) ch.getChildAt(0);
                  }
                  eqTest.graft(ch);
                  createAstNode(NUM_LITERAL, "0", eqTest);
                  
                  if (parentNode != null)
                  {
                     parentNode.graftAt(newParent, relPos);  // replaces with newly created ( node
                  }
                  else
                  {
                     root = newParent; // this NOT was the root of the WHERE clause
                  }
               }
               else if (astType == FUNCTION || astType == SUBST || 
                        astType == BOOL_FALSE || astType == BOOL_TRUE) // null parent for these 2
               {
                  if (parentNode == null)
                  {
                     // if the parent is null, then this is the predicate's root. It must be a 
                     // genuine boolean not a BIT so we replace boolFn() with boolFn() = 1
                     if (astType == BOOL_FALSE || astType == BOOL_TRUE) 
                     {
                        // if the root is a logical literal, we also replace it with the simplest
                        // possible boolean expression: 1=1 (true) or 0=1 (false)
                        ast.setType(NUM_LITERAL);
                        ast.setText(astType == BOOL_TRUE ? "1" : "0");
                     }
                     HQLAst eqTest = createAstNode(EQUALS, "=", null);
                     eqTest.graft(ast);
                     createAstNode(NUM_LITERAL, "1", eqTest);
                     root = eqTest;
                  }
                  else if (astType == BOOL_FALSE || astType == BOOL_TRUE)
                  {
                     // a logical literal whose parentNode is not null: replace it by BIT literal
                     // this way, the LogicalUserType is not used any more.
                     final HQLAst bitLiteral =
                        createAstNode(NUM_LITERAL, astType == BOOL_TRUE ? "1" : "0", null);
                     parentNode.graftAt(bitLiteral, relPos);
                  }
                  else
                  {
                     // logical FUNCTION or SUBST with non-null parent
                     HQLAst newParent = createAstNode(LPARENS, "(", null);
                     HQLAst eqTest = createAstNode(EQUALS, "=", newParent);
                        eqTest.graft(ast);
                        createAstNode(NUM_LITERAL, "1", eqTest);
                     
                     parentNode.graftAt(newParent, relPos);  // replace with newly created one
                  }
               }
            }
         }
         
         // replace the parents of items from this set with the items themselves. To be sure the
         // next processing works correctly we force a LPARENS as container
         if (boolReplaceParent != null)
         {
            for (HQLAst node : boolReplaceParent)
            {
               parent = (HQLAst) node.getParent();
               if (parent.getType() == LPARENS)
               {
                  parent = (HQLAst) parent.getParent();
               }
               // at this moment, parent is either EQUALS or NOT_EQ
               
               HQLAst gDad = (HQLAst) parent.getParent();
               int index = parent.getIndexPos();
               node.remove();
               parent.remove();
               
               if (gDad != null)
               {
                  HQLAst newParent = createAstNode(LPARENS, "(", null);
                  newParent.graft(node);
                  gDad.graftAt(newParent, index);
               }
               else
               {
                  node.remove();
                  root = node;
               }
            }
            inline = true; // we 'inlined' a param, prevent caching this FQL
         }
         
         // replace the parents of items from this set with the negated items.
         if (boolReplaceParentNegated != null)
         {
            for (HQLAst node : boolReplaceParentNegated)
            {
               parent = (HQLAst) node.getParent();
               if (parent.getType() == LPARENS)
               {
                  parent = (HQLAst) parent.getParent();
               }
               // at this moment, parent is either EQUALS or NOT_EQ
               
               HQLAst gDad = (HQLAst) parent.getParent();
               int index = parent.getIndexPos();
               parent.remove();
               node.remove();
               
               HQLAst newParent = createAstNode(NOT, "not", null);
               newParent.graft(node);
               if (gDad != null)
               {
                  gDad.graftAt(newParent, index);
               }
               else
               {
                  root = newParent;
               }
            }
            inline = true; // we 'inlined' a param, prevent caching this FQL
         }
         
         if (convertCanDoToIn != null)
         {
            for (HQLAst node : convertCanDoToIn)
            {
               // morph node itself
               node.setType(IN);
               node.setText("in");
               
               // remove the fist node (containing the pattern)
               HQLAst child1 = (HQLAst) node.getFirstChild();
               child1.remove();
               
               // now the [in] node contains only the 2nd child which became 1st. Add the list of
               // tokens from patterns as new nodes
               String pattern = (String) node.getAnnotation("pattern");
               int a = 0;
               int b = 0;
               while (true)
               {
                  b = pattern.indexOf(',', a); 
                  if (b == -1)
                  {
                     // the last token
                     createAstNode(STRING, '\'' + pattern.substring(a) + '\'', node);
                     break;
                  }
                  else
                  {
                     createAstNode(STRING, '\'' + pattern.substring(a, b) + '\'', node);
                     a = b + 1; // skip the COMMA separator
                  }
               }
            }
            inline = true; // we 'inlined' a param, prevent caching this FQL
         }
         
         if (droppedTrims != null)
         {
            for (HQLAst aTrim : droppedTrims)
            {
               parent = (HQLAst) aTrim.getParent();
               int index = aTrim.getIndexPos();
               
               // replace the possibly complex and opaque for sql planner [trim] node with its
               // result, empty string (for this very particular case)
               aTrim.remove();
               parent.graftAt(createAstNode(STRING, "''", null), index);
            }
            inline = true; // we 'inlined' a param, prevent caching this FQL
         }
         
         if (nullsInInSet != null)
         {
            for (HQLAst nullSubst : nullsInInSet)
            {
               // transformation: IN(r0, r1, r2, ..., nullR-1, nullR, nullR+1, .. rN) to
               //                 OR(IN(r0, r1, r2, ..., nullR-1, nullR+1, .. rN), r0 is null)
                parent = (HQLAst) nullSubst.getParent();
               // we need to extract this value as IS NULL and OR it with the remaining of IN set
               if (!parent.isAnnotation("nullOut"))
               {
                  HQLAst gparent = (HQLAst) parent.getParent();
                  int idx = parent.getIndexPos();
                  parent.remove();
                  
                  HQLAst or = createAstNode(OR, "or", null);
                  HQLAst isNull = createAstNode(IS_NULL, "is null", or);
                  isNull.graft(((HQLAst) parent.getFirstChild()).duplicate());
                  isNull.putAnnotation("inlined", Boolean.TRUE);
                  if (gparent != null)
                  {
                     gparent.graftAt(or, idx);
                     gparent.putAnnotation("inlined", Boolean.TRUE);
                  }
                  else
                  {
                     root = or;
                  }
                  
                  parent.putAnnotation("nullOut", true); // null value has been taken out
                  parent.putAnnotation("inlined", Boolean.TRUE);
                  or.graft(parent);
               }
               
               nullSubst.remove(); // drop it
               
               if (parent.getNumImmediateChildren() == 1) 
               {
                  // all nodes in IN set were null/unknown
                  // remove both OR and IN, we keep only the IS-NULL
                  HQLAst or = (HQLAst) parent.getParent();
                  int idx = or.getIndexPos();
                  HQLAst gparent = (HQLAst) or.getParent();
                  or.remove();
                  
                  HQLAst isNull = (HQLAst) or.getFirstChild();
                  isNull.remove();
                  if (gparent == null)
                  {
                     root = isNull;
                  }
                  else 
                  {
                     gparent.graftAt(isNull, idx);
                  }
               }
               
               inline = true;
            }
         }
      }
      catch (AstException exc)
      {
         // TODO:  use more specific error number.
         throw new PersistenceException("Error rewriting FQL expression", -1, exc);
      }
      
      if (earlyPublishEntities != null)
      {
         // freeze entity List
         earlyPublishEntities = Collections.unmodifiableSet(earlyPublishEntities);
      }
      
      return root;
   }
   
   /**
    * Walk the tree, which has been simplified/rolled-up {@link #simplifyUnknowns(Set)} by now,
    * and augment certain binary sub-expressions to match Progress' unknown value semantics.
    * 
    * @param   root
    *          Root node of the AST for the FQL expression.
    * @param   dialect
    *          Database dialect in use.
    *
    * @throws  PersistenceException
    *          if any error occurred during rewriting the AST.
    *
    * @return  The new root node of the AST for the FQL expression (it may change during processing).
    */
   private HQLAst augmentForUnknownValue(HQLAst root, Dialect dialect)
   throws PersistenceException
   {
      Set<HQLAst> needAugmentation = null;
      boolean useSQLUdfs = dialect.isNativeUDFsSupported() &&
               !DatabaseManager.getConfiguration(database).isUseJavaUDFs();
      boolean manualOverload = !dialect.supportsFunctionOverloading();
      
      // walk the tree and identify all nodes requiring attention.
      Iterator<Aast> iter = root.iterator();
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         
         switch (next.getType())
         {
            case EQUALS:
            case NOT_EQ:
            case LT:
            case LTE:
            case GT:
            case GTE:
               int operType = next.getType();
               HQLAst operand1 = (HQLAst) next.getChildAt(0);
               HQLAst operand2 = (HQLAst) next.getChildAt(1);
               Object aliasRef = null;
               
               HQLAst alias1 = operand1;
               int op1Type = alias1.getType();
               if (op1Type == FUNCTION &&
                   (aliasRef = operand1.getAnnotation("alias-ref")) != null)
               {
                  alias1 = (HQLAst) aliasRef;
                  op1Type = alias1.getType();
               }
               
               HQLAst alias2 = operand2;
               int op2Type = alias2.getType();
               if (op2Type == FUNCTION &&
                   (aliasRef = operand2.getAnnotation("alias-ref")) != null)
               {
                  alias2 = (HQLAst) aliasRef;
                  op2Type = alias2.getType();
               }
               
               if (op1Type == ALIAS && op2Type == ALIAS)
               {
                  if (operType != LT && operType != GT)
                  {
                     HQLAst f1 = (HQLAst) alias1.getChildAt(0);
                     HQLAst f2 = (HQLAst) alias2.getChildAt(0);
                     if (f1.getType() == PROPERTY && f2.getType() == PROPERTY)
                     {
                        boolean mand1 = isMandatoryProperty(alias1, f1);
                        boolean mand2 = isMandatoryProperty(alias2, f2);
                        HQLAst target = null;
                        
                        if (mand1 || mand2)
                        {
                           // if either field is mandatory, it changes the augmentation strategy,
                           // depending on the operator type
                           switch (operType)
                           {
                              case EQUALS:
                              case LTE:
                              case GTE:
                                 // if either field is mandatory, the case of both fields being
                                 // null is impossible, so don't augment
                                 break;
                              case NOT_EQ:
                                 if (!mand1)
                                 {
                                    // operand1 needs augmentation
                                    target = operand1;
                                 }
                                 else if (!mand2)
                                 {
                                    // operand2 needs augmentation
                                    target = operand2;
                                 }
                                 // else both fields are mandatory and no augmentation is needed
                                 break;
                           }
                        }
                        else
                        {
                           // both fields are non-mandatory; we indicate this by storing the
                           // operator node itself as the target of the augmentation
                           target = next;
                        }
                        
                        if (target != null)
                        {
                           if (needAugmentation == null)
                           {
                              needAugmentation = new HashSet<>();
                           }
                           needAugmentation.add(next);
                           
                           next.putAnnotationObject("unknown-target", target);
                        }
                     }
                  }
               }
               else if (op1Type == ALIAS || op2Type == ALIAS)
               {
                  HQLAst operand = (op1Type == ALIAS ? operand1 : operand2);
                  HQLAst alias = (op1Type == ALIAS ? alias1 : alias2);
                  HQLAst field = (HQLAst) alias.getChildAt(0);
                  if (field.getType() == PROPERTY && !isMandatoryProperty(alias, field))
                  {
                     // equality operation between a field and non-field is not augmented;
                     // inequality operation between a field and non-field is augmented;
                     // unknown value sorts high in Progress, so only augment GT and GTE if the
                     // field reference is the first operand; likewise, only augment LT and LTE
                     // if the field reference is the second operand
                     // TODO: it's actually more complicated than described above; in the GT, GTE,
                     // LT, and LTE cases, we should only augment if the field in the expression
                     // is in the index Progress would select for the query being executed;
                     // disabling augmentation for those types for now, as this caused regressions
                     //int idx = operand.getIndexPos();
                     boolean augment = false;
                     String augmentUdf = null;
                     switch (operType)
                     {
                        case NOT_EQ:
                           augment = true;
                           augmentUdf = (op1Type == FUNCTION || op2Type == FUNCTION) ? "ne" : null;
                           break;
                        /*
                        case GT:
                        case GTE:
                           augment = (idx == 0);
                           break;
                        case LT:
                        case LTE:
                           augment = (idx == 1);
                           break;
                        */
                     }
                     
                     if (augment)
                     {
                        if (needAugmentation == null)
                        {
                           needAugmentation = new HashSet<>();
                        }
                        needAugmentation.add(next);
                        
                        if (augmentUdf != null)
                        {
                           // when the other part is a UDF function call which can return unknown
                           next.putAnnotation("augment-udf", augmentUdf);
                        }
                        else
                        {
                           // only the field reference side of the operation needs augmentation
                           next.putAnnotationObject("unknown-target", operand);
                        }
                     }
                  }
               }
               break;
            
            default:
               continue;
         }
      }
      
      try
      {
         if (needAugmentation != null)
         {
            for (HQLAst node : needAugmentation)
            {
               HQLAst parentNode = (HQLAst) node.getParent();
               
               int relPos = node.getIndexPos();
               HQLAst topParens = null;
               
               if (parentNode != null)
               {
                  // remove operator node; will be added back as child of top OR
                  node.remove();
                  
                  // avoid redundant parentheses
                  if (parentNode.getType() == LPARENS)
                  {
                     topParens = parentNode;
                  }
                  else
                  {
                     topParens = createAstNode(LPARENS, "(", null);
                     parentNode.graftAt(topParens, relPos);  // replaces this node
                  }
               }
               else
               {
                  // a simple where where the operator was the root.
                  topParens = createAstNode(LPARENS, "(", null);
                  root = topParens;
               }
               
               // convert t1.f1 <> op() to UDF operator ne(t1.f1, op())
               String augmentUdf = (String) node.getAnnotation("augment-udf");
               if (augmentUdf != null)
               {
                  HQLAst t1 = (HQLAst) node.getChildAt(0);
                  HQLAst t2 = (HQLAst) node.getChildAt(1);
                  node.remove();
                  HQLAst topUdf = createAstNode(FUNCTION, augmentUdf, topParens);
                  topUdf.graft(t1.duplicateFresh());
                  topUdf.graft(t2.duplicateFresh());

                  if (manualOverload)
                  {
                     manuallyOverload(topUdf);
                  }
                  if (useSQLUdfs)
                  {
                     topUdf.setText(dialect.udfSchema() + topUdf.getText());
                  }
                  continue;
               }
               
               HQLAst topOr = createAstNode(OR, "or", topParens);
               topOr.graft(node.duplicateFresh());
               
               HQLAst target = (HQLAst) node.getAnnotation("unknown-target");
               
               // if target == node, both sides of operation need augmentation
               if (target == node)
               {
                  // convert
                  //    [t1.f1 = t2.f2]
                  // to
                  //    [(t1.f1 = t2.f2 or (t1.f1 is null and t2.f2 is null))]
                  // in order to match the Progress semantics. The new sub-tree will look like:
                  // LPARENS [ ( ]               <--- force ( because lower precedence of operator
                  // +-- OR [ or ]
                  //     +-- EQUALS [ = ]        <--- current node
                  //     |   +-- ALIAS [ t1 ]
                  //     |   |   +-- PROPERTY [ f1 ]
                  //     |   +-- ALIAS [ t2 ]
                  //     |       +-- PROPERTY [ f2 ]
                  //     +-- AND [ and ]
                  //         + IS_NULL [ is null ]
                  //         |   +-- ALIAS [ t1 ]
                  //         |       +-- PROPERTY [ f1 ]
                  //         + IS_NULL [ is null ]
                  //             +-- ALIAS [ t2 ]
                  //                 +-- PROPERTY [ f2 ]
                  HQLAst t1 = (HQLAst) node.getChildAt(0);
                  HQLAst t2 = (HQLAst) node.getChildAt(1);
                  
                  t1 = (HQLAst) t1.duplicateFresh();
                  t2 = (HQLAst) t2.duplicateFresh();
                  
                  if (node.getType() != NOT_EQ)
                  {
                     HQLAst innerAnd = createAstNode(AND, "and", topOr);
                        HQLAst t1IsNull = createAstNode(IS_NULL, "is null", innerAnd);
                           duplicateAugmentedOperand(t1, t1IsNull);
                        HQLAst t2IsNull = createAstNode(IS_NULL, "is null", innerAnd);
                           duplicateAugmentedOperand(t2, t2IsNull);
                     topOr.putAnnotation("augmented", true);
                  }
                  else
                  {
                     // in the case of NOT_EQ the second term of OR gets messier:
                     // t1.f2 is null and t2.f2 is not null or t1.f2 is not null and t2.f2 is null
                     HQLAst innerOr = createAstNode(OR, "or", topOr);
                        HQLAst firstAnd = createAstNode(AND, "and", innerOr);
                           HQLAst t1IsNull = createAstNode(IS_NULL, "is null", firstAnd);
                              duplicateAugmentedOperand(t1, t1IsNull);
                           HQLAst t2IsNotNull = createAstNode(NOT_NULL, "is not null", firstAnd);
                              duplicateAugmentedOperand(t2, t2IsNotNull);
                        HQLAst secondAnd = createAstNode(AND, "and", innerOr);
                           HQLAst t1IsNotNull = createAstNode(NOT_NULL, "is not null", secondAnd);
                              duplicateAugmentedOperand(t1, t1IsNotNull);
                           HQLAst t2IsNull = createAstNode(IS_NULL, "is null", secondAnd);
                              duplicateAugmentedOperand(t2, t2IsNull);
                     
                     // alternate solution:
                     // tt2.f2 <> tt1.f2 or ((tt1.f2 is null) <> (tt2.f2 is null));
                     //HQLAst innerNotEq = createAstNode(NOT_EQ, "<>", topOr);
                     //   HQLAst t1IsNull = createAstNode(IS_NULL, "is null", innerNotEq);
                     //      HQLAst t1Alias = createAstNode(ALIAS, t1.getText(), t1IsNull);
                     //         createAstNode(PROPERTY, f1.getText(), t1Alias);
                     //   HQLAst t2IsNull = createAstNode(IS_NULL, "is null", innerNotEq);
                     //      HQLAst t2Alias = createAstNode(ALIAS, t2.getText(), t2IsNull);
                     //         createAstNode(PROPERTY, f2.getText(), t2Alias);
                     // This not working because of a possible flaw in Hibernate 
                  }
               }
               // if target != node, only one operand requires augmentation
               else
               {
                  // convert
                  //    [table.field <> op2]
                  // to
                  //    [(table.field <> op2 or table.field is null)]
                  // in order to match the Progress semantics. The new sub-tree will look like:
                  // LPARENS [ ( ]               <--- force ( because lower precedence of operator
                  // +-- OR [ or ]
                  //     +-- NOT_EQ [ <> ]       <--- current node
                  //     |   +-- ALIAS [ table ]
                  //     |   |   +-- PROPERTY [ field ]
                  //     |   +-- ALIAS [ op2 ]
                  //     + IS_NULL [ is null ]
                  //     |   +-- ALIAS [ table ]
                  //     |       +-- PROPERTY [ field ]
                  //
                  // same principle applies to:
                  //    [op1 <> table.field]
                  //    [table.field > op2]
                  //    [table.field >= op2]
                  //    [op1 < table.field]
                  //    [op1 <= table.field]
                  //
                  // equality tests are NOT augmented
                  
                  HQLAst tableIsNull = createAstNode(IS_NULL, "is null", topOr);
                  duplicateAugmentedOperand(target, tableIsNull);
               }
            }
         }
      }
      catch (AstException exc)
      {
         // TODO:  use more specific error number.
         throw new PersistenceException("Error rewriting FQL expression", -1, exc);
      }
      
      return root;
   }
   
   /**
    * The method receives a HQLAst and searches for augmented nodes and rewrites them if possible.
    * A node is augmented if it has the "augmented" annotation (see {@link #augmentForUnknownValue}).
    * The reconstruct occurs when only two augmented nodes are present, even if there are other conditions
    * in the clause. At the same time, the augmented nodes must have the same AND parent.
    * 
    * This method converts the following construct
    *    [
    *       (t1.f1 = t2.f2 or (t1.f1 is null and t2.f2 is null))  and 
    *       (t1.f3 = t2.f4 or (t1.f3 is null and t2.f4 is null))
    *    ]
    * into it's DNF (Disjunctive Normal Form)
    *    [
    *       (t1.f1 = t2.f2 and t1.f3 = t2.f4)                                          or 
    *       (t1.f1 = t2.f2 and (t1.f3 is null and t2.f4 is null))                      or
    *       ((t1.f1 is null and t2.f2 is null) and t1.f3 = t2.f4)                      or
    *       ((t1.f1 is null and t2.f2 is null) and (t1.f3 is null and t2.f4 is null))
    *    ]
    * which is a very specific case where two character fields are compared and augmented.
    * 
    * There are only a few cases that are handled:
    * 
    * a. There are only two augmented nodes. Reconstruction is as mentioned above.
    *    [
    *       (t1.f1 = t2.f2 or (t1.f1 is null and t2.f2 is null))  and 
    *       (t1.f3 = t2.f4 or (t1.f3 is null and t2.f4 is null))
    *    ]
    * b. There are additional conditions in the clause. Reconstruction is as mentioned above,
    * the parent of the construct becomes the next AND and the clause will look like
    * (<new_construct>) OPERATOR <other_conditions>.
    *    [
    *       (t1.f1 = t2.f2 or (t1.f1 is null and t2.f2 is null))  and 
    *       (t1.f3 = t2.f4 or (t1.f3 is null and t2.f4 is null))  OPERATOR
    *       <other_conditions>
    *    ]
    *    
    * There are also cases that are not handled:
    * a. The parent of (t1.f1 = t2.f2 or (t1.f1 is null and t2.f2 is null)) is the first OR,
    * while the parent of (t1.f3 = t2.f4 or (t1.f3 is null and t2.f4 is null)) is the first AND.
    *    [
    *       t2.f5 = ''                                            or
    *       (t1.f1 = t2.f2 or (t1.f1 is null and t2.f2 is null))  and 
    *       (t1.f3 = t2.f4 or (t1.f3 is null and t2.f4 is null))
    *    ]
    * b. There are multiple augmented nodes, even if the two pairs of augmented nodes do not interfere
    * with each other.
    *    [
    *       ((t1.f1 = t2.f2 or (t1.f1 is null and t2.f2 is null))  and 
    *       (t1.f3 = t2.f4 or (t1.f3 is null and t2.f4 is null)))  or
    *       ((p1.f1 = p2.f2 or (p1.f1 is null and p2.f2 is null))  and 
    *       (p1.f3 = p2.f4 or (p1.f3 is null and p2.f4 is null)))
    *    ]
    *    
    * @param   root
    *          Root node of the AST for the FQL expression.
    *          
    * @return  The new root node of the AST for the FQL expression (it may change during processing).
    */
   private HQLAst honorDisjunctiveForm(HQLAst root)
   {
      Set<HQLAst> needRewrite = null;
      
      // Walk the tree and identify all "augmented" nodes
      Iterator<Aast> iter = root.iterator();
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         if (next.getAnnotation("augmented") != null)
         {
            if (needRewrite == null)
            {
               needRewrite = new HashSet<>();
            }
            
            needRewrite.add(next);
         }
      }
      
      // Can only rewrite the root when there are only 2 augmented fields
      if (needRewrite == null || needRewrite.size() != 2)
      {
         return root;
      }
      
      Iterator<HQLAst> iterator = needRewrite.iterator();
      HQLAst node1 = (HQLAst) iterator.next();
      HQLAst node2 = (HQLAst) iterator.next();
      
      // The first parent should be a LPARENS and the second should be an AND
      HQLAst parentNode1 = (HQLAst) node1.getParent().getParent();
      HQLAst parentNode2 = (HQLAst) node2.getParent().getParent();
      // The AND parent must be the same for both nodes or the reconstruction is not possible
      if (!(parentNode1 == parentNode2 && parentNode1.getType() == AND))
      {
         return root;
      }
      
      // convert
      //    [
      //       (t1.f1 = t2.f2 or (t1.f1 is null and t2.f2 is null))  and
      //       (t1.f3 = t2.f4 or (t1.f3 is null and t2.f4 is null))
      //    ]
      // to
      //    [
      //       (t1.f1 = t2.f2 and t1.f3 = t2.f4)                                          or 
      //       (t1.f1 = t2.f2 and (t1.f3 is null and t2.f4 is null))                      or
      //       ((t1.f1 is null and t2.f2 is null) and t1.f3 = t2.f4)                      or
      //       ((t1.f1 is null and t2.f2 is null) and (t1.f3 is null and t2.f4 is null))
      //    ]
      //
      // the main components used in the mentioned clause are:
      // leftInnerEqual    -> t1.f1 = t2.f2
      // leftInnerAnd      -> (t1.f1 is null and t2.f2 is null)
      // rightInnerEqual   -> t1.f3 = t2.f4
      // rightInnerAnd     -> (t1.f3 is null and t2.f4 is null)
      
      // Get a copy of each component.
      HQLAst leftInnerEqual = (HQLAst) node1.getChildAt(0).duplicateFresh();
      HQLAst leftInnerAnd = (HQLAst) node1.getChildAt(1).duplicateFresh();
      
      HQLAst rightInnerEqual = (HQLAst) node2.getChildAt(0).duplicateFresh();
      HQLAst rightInnerAnd = (HQLAst) node2.getChildAt(1).duplicateFresh();

      HQLAst topParent = null;
      HQLAst currentParent = (HQLAst) parentNode1.getParent();
      if (currentParent == null)
      {
         // This is the only construct so it can directly replace the root.
         topParent = createAstNode(LPARENS, "(", null);
         root = topParent;
      }
      else
      {
         // There are more conditions present so it's necessary to connect them in the following way:
         // (<new_construct>) OPERATOR <other_conditions>
         int relPos = parentNode1.getIndexPos();
         
         // Remove the node because it will be replaced
         parentNode1.remove();
         // There is no need to worry about an OPERATOR because there is one that already
         // connects the construct with the other conditions, so it will be used.
         topParent = createAstNode(LPARENS, "(", null);
         currentParent.graftAt(topParent, relPos);
      }
      
      // Create the replacement for the construct
      HQLAst firstOr = createAstNode(OR, "or", topParent);
         HQLAst firstParent = createAstNode(LPARENS, "(", firstOr);
            HQLAst firstAnd = createAstNode(AND, "and", firstParent);
               duplicateAugmentedOperand(leftInnerEqual, firstAnd);
               duplicateAugmentedOperand(rightInnerEqual, firstAnd);
         HQLAst secondOr = createAstNode(OR, "or", firstOr);
            HQLAst secondParent = createAstNode(LPARENS, "(", secondOr);
               HQLAst secondAnd = createAstNode(AND, "and", secondParent);
                  duplicateAugmentedOperand(leftInnerEqual, secondAnd);
                  duplicateAugmentedOperand(rightInnerAnd, secondAnd);
            HQLAst thirdOr = createAstNode(OR, "or", secondOr);
               HQLAst thirdParent = createAstNode(LPARENS, "(", thirdOr);
                  HQLAst thirdAnd = createAstNode(AND, "and", thirdParent);
                     duplicateAugmentedOperand(leftInnerAnd, thirdAnd);
                     duplicateAugmentedOperand(rightInnerEqual, thirdAnd);
               HQLAst fourthParent = createAstNode(LPARENS, "(", thirdOr);
                  HQLAst fourthAnd = createAstNode(AND, "and", fourthParent);
                     duplicateAugmentedOperand(leftInnerAnd, fourthAnd);
                     duplicateAugmentedOperand(rightInnerAnd, fourthAnd);

      return root;
   }
   
   /**
    * Duplicate the given source operand node and graft the duplicate under <code>parent</code>.
    * If <code>source</code> represents an extent field, annotate the duplicate node to indicate
    * that an index check node should not be created (it would be redundant) when rewriting the
    * alias node.
    * 
    * @param   source
    *          Original alias node (or enclosing function).
    * @param   parent
    *          Parent node onto which to graft the duplicate of <code>source</code> as a child.
    */
   private void duplicateAugmentedOperand(HQLAst source, HQLAst parent)
   {
      HQLAst ref = (HQLAst) source.duplicateFresh();
      parent.graft(ref);
      
      // if a composite field, annotate that we don't want to create an index check node when
      // rewriting the alias node
      
      HQLAst alias = null;
      
      loop:
      while (ref != null)
      {
         switch (ref.getType())
         {
            case ALIAS:
               alias = ref;
               break;
            case LBRACKET:
               alias.putAnnotation("omit-index-check", Boolean.TRUE);
               break loop;
            default:
               break;
         }
         ref = (HQLAst) ref.getFirstChild();
      }
   }
   
   /**
    * Determine whether the given property in the context of the DMO class represented by the
    * given buffer alias represents a mandatory (i.e., non-nullable) database column.
    * 
    * @param   alias
    *          Buffer alias AST
    * @param   property
    *          Property AST.
    * 
    * @return  <code>true</code> if the property is mandatory, else <code>false</code>.
    */
   private boolean isMandatoryProperty(HQLAst alias, HQLAst property)
   {
      RecordBuffer buf = lookupBuffer(alias);
      String propName = property.getText();
      
      return TableMapper.isLegacyFieldMandatory(buf, propName).booleanValue();
   }

   /**
    * Evaluate the situation where in a FIND query there is an indexed field which appear
    * in the 'where' clause as {@code tt.f <= ?},  {@code ? >= tt.f}, {@code tt.f < ?} or
    * {@code ? > tt.f}. In this scenario, 4GL has a quirk where the result of the comparison
    * differs in certain scenarios.
    *
    * @param   fieldsEqualState
    *          The nodes in the 'where' clause that are involved in an equality comparison.
    * @param   coreIndexedFields
    *          The nodes that should be changed to {@code true} or {@code false}, according to
    *          internal rules.
    * @param   indexedFields
    *          Map with keys being the fields from the 'order by' clause and values representing
    *          the order index.
    *
    * @return  the value with which {@code coreIndexedField} should be substituted.
    */
   private boolean evaluateIndexedFieldReplacement(Map<String, Boolean> fieldsEqualState,
                                                   Set<String> coreIndexedFields,
                                                   Map<String, Integer> indexedFields)
   {
      for (String coreIndexedField : coreIndexedFields)
      {
         int coreFieldIndex = indexedFields.get(coreIndexedField);
         for (Map.Entry<String, Integer> entry : indexedFields.entrySet())
         {
            if (entry.getKey().equals(coreIndexedField))
            {
               continue;
            }

            int indexedFieldIndex = entry.getValue();
            if (indexedFieldIndex > coreFieldIndex)
            {
               continue;
            }

            Boolean equalState = fieldsEqualState.get(entry.getKey());
            if (equalState != null && equalState)
            {
               continue;
            }

            return false;
         }
      }

      return true;
   }
   
   /**
    * Get rid of unknowns in an FQL AST by evaluating relations in which these unknowns
    * participate.
    *
    * @param   unknowns
    *          Set of FQL nodes which represent all unknowns in the FQL AST. Nodes can be
    *          substitution parameters or whole statements which evaluate to unknown values.
    * @param   fieldsEqualState
    *          The state of the nodes from 'where' clause that are involved in an equality comparison.
    * @param   coreIndexedFields
    *          The nodes that should be changed to {@code true} or {@code false}, according to
    *          internal rules.
    * @param   indexedFields
    *          Map with keys being the fields from the 'order by' clause and values representing
    *          the order index.
    *
    * @return  Set of FQL {@code BOOL_TRUE} and {@code BOOL_FALSE} nodes which were
    *          created during the simplifications of unknowns. May be {@code null}.
    */
   private Set<HQLAst> simplifyUnknowns(Set<HQLAst> unknowns,
                                        Map<String, Boolean> fieldsEqualState,
                                        Set<String> coreIndexedFields,
                                        Map<String, Integer> indexedFields)
   {
      Set<HQLAst> createdBooleans = null;
      Set<HQLAst> skipSet         = null;
      HQLAst next                 = null;
      Iterator<HQLAst> iter       = unknowns.iterator();

      Function<HQLAst, String> getFullFieldName = (alias) ->
      {
         if (alias == null)
         {
            return null;
         }
         HQLAst property = (HQLAst) alias.getChildAt(0);
         if (property == null || property.getType() != PROPERTY)
         {
            return null;
         }

         return alias.getText() + "." + property.getText();
      };
      boolean substitutionFlag = false;
      boolean shouldSubstitute = !coreIndexedFields.isEmpty();
      if (shouldSubstitute)
      {
         substitutionFlag = evaluateIndexedFieldReplacement(fieldsEqualState, coreIndexedFields,
                                                            indexedFields);
      }

      while (true)
      {
         if (next == null)
         {
            if (iter.hasNext())
            {
               next = iter.next();
            }
            else
            {
               break;
            }
         }
         
         if (skipSet != null && skipSet.contains(next))
         {
            next = null;
            continue;
         }
         
         HQLAst parent = (HQLAst) next.getParent();
         
         int disposition = -1;
         if (parent != null)
         {
            int pType = parent.getType();
            int idx = next.getIndexPos();
            HQLAst oppositeOperand = (HQLAst) ((idx == 0)
                  ? next.getNextSibling()
                  : next.getPrevSibling());
            
            // Decide what transformation is needed.
            if (oppositeOperand == null)
            {
               if (parent.getType() == IS_NULL)
               {
                  disposition = BOOL_TRUE;
               }
               else if (parent.getType() == NOT_NULL)
               {
                  disposition = BOOL_FALSE;
               }
               else if (parent.getType() == LPARENS || parent.getType() == RPARENS)
               {
                  next = parent;
                  continue;
               }
               else
               {
                  throw new IllegalStateException(
                        "Unexpected usage of standalone unknown value.");
               }
            }
            else if (unknowns.contains(oppositeOperand))
            {
               // comparing two unknowns
               disposition = (pType == EQUALS || pType == GTE || pType == LTE)
                     ? BOOL_TRUE
                     : BOOL_FALSE;
               
               // skip processing of the opposite unknown operand in the future
               if (skipSet == null)
               {
                  skipSet = new HashSet<>();
               }
               skipSet.add(oppositeOperand);
            }
            else if (oppositeOperand.getType() == SUBST       ||
                     oppositeOperand.getType() == NUM_LITERAL ||
                     oppositeOperand.getType() == DEC_LITERAL ||
                     oppositeOperand.getType() == BOOL_FALSE  ||
                     oppositeOperand.getType() == BOOL_TRUE   ||
                     oppositeOperand.getType() == STRING)
            {
               // we have a simple "? <comparison operator> {CONST|SUBST}" statement
               if (pType == EQUALS)
               {
                  disposition = BOOL_FALSE;
               }
               else if (pType == NOT_EQ)
               {
                  disposition = BOOL_TRUE;
               }
               else
               {
                  // results of comparison is unknown value, process it on the next iteration
                  next = parent;
                  continue;
               }
            }
            else
            {
               // this code correctly handles "field <comparison operator> ?"
               // comparisons, results for more complex statements may be incorrect
               // TODO: handle complex comparisons, e.g. "subst <operator>
               // {const|subst} <comparison operator> ?"
               
               boolean nullable = isOppositeOperandNullable(next, idx);
               
               if (pType == EQUALS ||
                   (pType == GTE && idx == 1) ||
                   (pType == LTE && idx == 0))
               {
                  // this handles EQUALS and when the field is GTE than the NULL value or
                  // the NULL value is LTE than the field
                  disposition = (nullable ? IS_NULL : BOOL_FALSE);
               }
               else if (pType == NOT_EQ ||
                        (pType == LT && idx == 1) ||
                        (pType == GT && idx == 0))
               {
                  // this handles NOT_EQ and when the field is LT than the NULL value or
                  // the NULL value is GT than the field
                  disposition = (nullable ? NOT_NULL : BOOL_TRUE);
               }
               else if ((pType == GT && idx == 1) ||
                        (pType == LT && idx == 0))
               {
                  // this handles the case when the field is GT than the NULL value or
                  // the NULL value is LT than the field
                  disposition = BOOL_FALSE;
               }
               else if ((pType == LTE && idx == 1) ||
                        (pType == GTE && idx == 0))
               {
                  // this handles the case when the field is LTE than the NULL value or
                  // the NULL value is GTE than the field
                  disposition = (nullable ? IS_NULL : BOOL_TRUE);
               }
            }

            boolean trySubstitution = shouldSubstitute &&
                                      oppositeOperand != null &&
                                      coreIndexedFields.contains(getFullFieldName.apply(oppositeOperand));
            if (trySubstitution)
            {
               if (substitutionFlag || pType != LTE)
               {
                  // the only case where the flow doesn't enter here, but it enters the outer if
                  // is when the comparison is "tt.f <= ?" and substitutionFlag = false,
                  // meaning the final value should be 'is null',
                  // or 'false' if the operand is not nullable
                  disposition = substitutionFlag ? BOOL_TRUE : BOOL_FALSE;
               }
            }
         }
         else
         {
            // top-level unknown value is translated to "false"
            disposition = BOOL_FALSE;
            parent = next;
         }
         
         // Transform the expression.
         switch (disposition)
         {
            case IS_NULL:
               parent.setType(IS_NULL);
               parent.setText("is null");
               parent.putAnnotation("inlined", Boolean.TRUE);
               next.remove();
               inline = true;
               break;
               
            case NOT_NULL:
               parent.setType(NOT_NULL);
               parent.setText("is not null");
               parent.putAnnotation("inlined", Boolean.TRUE);
               next.remove();
               inline = true;
               break;
               
            case BOOL_FALSE:
               parent.setType(BOOL_FALSE);
               parent.setText("false");
               parent.putAnnotation("inlined", Boolean.TRUE);
               parent.removeChildren();
               inline = true;
               
               if (createdBooleans == null)
               {
                  createdBooleans = new HashSet<>();
               }
               createdBooleans.add(parent);
               break;
               
            case BOOL_TRUE:
               parent.setType(BOOL_TRUE);
               parent.setText("true");
               parent.putAnnotation("inlined", Boolean.TRUE);
               parent.removeChildren();
               inline = true;
               
               if (createdBooleans == null)
               {
                  createdBooleans = new HashSet<>();
               }
               createdBooleans.add(parent);
               break;
               
            default:
               break;
         }
         
         next = null;
      }
      
      return createdBooleans;
   }
   
   /**
    * Replaces the specified child node of a given parent with {@code cast(? as <datatype>)} if it represents
    * a substitution parameter. E.g. the element will be replaced with {@code cast(? as big_decimal)} is we
    * have a {@code decimal} variable.
    *
    * @param   parent
    *          Parent of the node for which the cast must be done.
    * @param   index
    *          Index of the child element.
    * @param   dialect
    *          The dialect the query will be using.
    */
   private void makeExplicitCast(HQLAst parent, int index, Dialect dialect)
   {
      HQLAst casted = (HQLAst) parent.getChildAt(index);
      
      if (casted == null || casted.getType() != SUBST)
      {
         return;
      }
      
      String datatype = (String) casted.getAnnotation("datatype");            // FqlType.toString()
      datatype = dialect.getSpecificTypeAsString(FqlType.valueOf(datatype));  // dialect specific
      
      HQLAst castFunc = createAstNode(CAST, "cast", null);
      HQLAst castedType = createAstNode(SYMBOL, datatype, null);
      
      casted.remove();
      parent.graftAt(castFunc, index);
      castFunc.graft(casted);
      castFunc.graft(castedType);
   }
   
   /**
    * Tries to roll up subexpressions according to the following rules:
    * <ul>
    *   <li>{condition} or false --&gt; {condition}</li>
    *   <li>{condition} or  true  --&gt; true</li>
    *   <li>{condition} and true  --&gt; {condition}</li>
    *   <li>{condition} and false --&gt; false</li>
    * </ul><p>
    * If an expression has been rolled up to a boolean value, it tries to roll
    * this value up with other conditions and so on.
    *
    * @param  booleanOperand
    *         A node which represents a boolean operand in the expressions above.
    * @param  root
    *         The root node of the whole FQL tree.
    *
    * @return The new root node of the whole FQL tree (the root node may be
    *         replaced if we have rolled up the first-level leaves).
    *
    * @throws AstException
    *         If there is any error grafting subtrees in the process of
    *         restructuring the subtree.
    */
   private HQLAst trySimplifyBooleans(HQLAst booleanOperand, HQLAst root)
   throws AstException
   {
      // check that the branch which contains this operand wasn't thrown away
      // during the processing before
      Aast par = booleanOperand.getParent();
      boolean valid = false;
      while (par != null)
      {
         if (par == root)
         {
            valid = true;
            break;
         }
         else
         {
            par = par.getParent();
         }
      }
      
      if (!valid)
      {
         return root;
      }
      
      HQLAst parent       = (HQLAst) booleanOperand.getParent();
      int type            = booleanOperand.getType();
      int pType           = parent.getType();
      HQLAst otherOperand = (HQLAst) (booleanOperand.getIndexPos() == 0
                                      ? booleanOperand.getNextSibling()
                                      : booleanOperand.getPrevSibling());
      boolean newBooleanElementAdded = false;
      
      // note that in the processing below, when we replace the node we break
      // all links with old nodes in both directions in order to ensure that  
      // the detection of detached branches above works correctly
      switch (pType)
      {
         case AND:
            switch (type)
            {
               case BOOL_TRUE:
                  Aast operandParent = parent.getParent();
                  if (operandParent == null) // [parent] is [root]
                  {
                     for (int i = 0; i < root.getNumberOfChildren(); i++)
                     {
                        root.getChildAt(i).remove();
                     }
                     root = otherOperand;
                     root.setParent(null);
                  }
                  else
                  {
                     int operandIndex = parent.getIndexPos();
                     operandParent.graftAt(otherOperand, operandIndex);
                     parent.remove();
                     parent.setParent(null);
                  }
                  inline = true;
                  break;
               case BOOL_FALSE:
                  parent.setType(BOOL_FALSE);
                  parent.setText("false");
                  parent.putAnnotation("inlined", Boolean.TRUE);
                  
                  for (int i = 0; i < parent.getNumberOfChildren(); i++)
                  {
                     parent.getChildAt(i).setParent(null);
                  }
                  parent.removeChildren();
                  inline = true;
                  newBooleanElementAdded = true;
                  break;
            }
            break;
         case OR:
            switch (type)
            {
               case BOOL_TRUE:
                  parent.setType(BOOL_TRUE);
                  parent.setText("true");
                  parent.putAnnotation("inlined", Boolean.TRUE);
                  
                  for (int i = 0; i < parent.getNumberOfChildren(); i++)
                  {
                     parent.getChildAt(i).setParent(null);
                  }
                  parent.removeChildren();
                  inline = true;
                  newBooleanElementAdded = true;
                  break;
               case BOOL_FALSE:
                  Aast operandParent = parent.getParent();
                  if (operandParent == null)
                  {
                     for (int i = 0; i < root.getNumberOfChildren(); i++)
                     {
                        root.getChildAt(i).setParent(null);
                     }
                     root = otherOperand;
                  }
                  else
                  {
                     int operandIndex = parent.getIndexPos();
                     operandParent.graftAt(otherOperand, operandIndex);
                     parent.remove();
                     parent.setParent(null);
                  }
                  inline = true;
                  break;
            }
            break;
         case LPARENS:
            if (parent.getNumberOfChildren() == 1)
            {
               parent.setType(type);
               parent.setText(booleanOperand.getText());
               parent.putAnnotation("inlined", Boolean.TRUE);
               parent.getChildAt(0).setParent(null);
               parent.removeChildren();
               inline = true;
               newBooleanElementAdded = true;
            }
            break;
      }
      
      // perform recursive roll up
      if (newBooleanElementAdded)
      {
         root = trySimplifyBooleans(parent, root);
      }
      
      return root;
   }
   
   /**
    * Replace legacy indexed property with extent with custom property. Mark AST as inlined.
    *
    * @param   ast
    *          A node which represents a denormalized field with extent.
    * @param   parameters
    *          Query substitution parameters for the where clause.
    */
   private void inlineDenormalizedField(HQLAst ast, Object[] parameters)
   {
      HQLAst subst = (HQLAst) ast.getChildAt(0).getChildAt(0);
      
      int extentIndex;
      switch (subst.getType())
      {
         case SUBST:
            String dataType = (String) subst.getAnnotation("datatype");
            int index = ((Long) subst.getAnnotation("index")).intValue();
            Object parm = parameters[index];
            
            FqlType fqlType = Enum.valueOf(FqlType.class, dataType);
            switch (fqlType)
            {
               case INTEGER:
                  extentIndex = ((integer) parm).intValue();
                  break;
               case LONG:
                  extentIndex = ((int64) parm).intValue();
                  break;
               default:
                  return;
            }
            break;
         case NUM_LITERAL:
            extentIndex = Integer.parseInt(subst.getText());
            break;
         default:
            return;
      }
      
      Aast parent = ast.getParent();
      Aast alias = parent != null && parent.getType() == ALIAS ? parent : null;
      RecordBuffer rbuff = lookupBuffer(alias);
      String denormalizedProperty = TableMapper.getDenormalizedProperty(rbuff, ast.getText(), extentIndex);
      ast.setText(denormalizedProperty);
      ast.removeChildren();
      ast.putAnnotation("inlined", Boolean.TRUE);
      inline = true;
   }
   
   /**
    * Attempt to inline a single query substitution parameter, if it is a
    * numeric, text, or date type.  This involves retasking a substitution
    * parameter node to a be a different type, and changing its text, so that
    * the original placeholder (<code>?</code>) is replaced with the text of
    * the parameter's value.  The node is annotated as "inlined" to enable
    * downstream processing to accurately track the remaining placeholders.
    * 
    * @param   ast
    *          AST node which represents the query substitution parameter to
    *          be inlined.
    * @param   parameters
    *          The array of substitution parameters from which the value of
    *          the inlined parameter will be determined.
    * @param   dialect
    *          Database dialect in use.
    *
    * @return  The FQL type of the inlined parameter, or <code>SUBST</code> if
    *          the parameter wasn't inlined.
    */
   private int inlineSubstitutionParameter(HQLAst ast, Object[] parameters, Dialect dialect)
   {
      String text = null;
      String dataType = (String) ast.getAnnotation("datatype");
      int index = ((Long) ast.getAnnotation("index")).intValue();
      Object parm = parameters[index];
      if (parm instanceof Resolvable && !(parm instanceof FieldReference))
      {
         // Do not inline Resolvables.  If we get here, it represents a
         // programming error.  Warn, but don't fail.
         if (LOG.isLoggable(Level.SEVERE))
         {
            String msg = "Resolvable substitution parameter @" + index + 
                         " (" + parm + ") cannot be inlined [" + fql + "]";
            LOG.log(Level.SEVERE, msg, new Throwable());
         }
         
         return SUBST;
      }
      
      FqlType fqlType = Enum.valueOf(FqlType.class, dataType);
      int tokType = SUBST;
      
      switch (fqlType)
      {
         case INTEGER:
         case LONG:
            tokType = NUM_LITERAL;
            break;
            
         case DECIMAL:
         case DOUBLE:
            tokType = DEC_LITERAL;
            break;
            
         case DATE:
            parm = Persistence.preprocessQueryParameter(parm, dialect);
            tokType = STRING;
            text = dialect.formatDate((date) parm);
            break;
            
         case TEXT:
            parm = Persistence.preprocessQueryParameter(parm, dialect);
            text = ((character) parm).toStringMessage();
            tokType = STRING;
            break;
         
         case BOOLEAN:
            if (((logical) parm).booleanValue())
            {
               tokType = BOOL_TRUE;
               text = "true";
            }
            else
            {
               tokType = BOOL_FALSE;
               text = "false";
            }
            break;
         
         //TODO handle DATETIME and DATETIMETZ
         
         default:
            // Do not inline any other type.
            break;
      }
      
      if (tokType != SUBST)
      {
         if (text == null)
         {
            if (parm instanceof BaseDataType)
            {
               text = ((BaseDataType) parm).toStringMessage();
            }
            else
            {
               text = parm.toString();
            }
         }
         else if (tokType == STRING)
         {
            // escape single quotes and enclose text in single quotes
            text = "'" + text.replace("'", "''") + "'";
         }
         
         ast.setText(text);
         ast.setType(tokType);
         ast.putAnnotation("inlined", Boolean.TRUE);
         inline = true;
      }
      
      return tokType;
   }
   
   /**
    * Manually disambiguate a user defined function by taking its overloaded
    * function name and data type signature and replacing the name in its AST
    * with a unique name generated when the function was registered.
    * 
    * @param   function
    *          AST node for the function.
    * 
    * @throws  PersistenceException
    *          if there is an error determining the data type of any parameter
    *          of the function.
    */
   private void manuallyOverload(HQLAst function)
   throws PersistenceException
   {
      String name = function.getText();
      int len = function.getNumImmediateChildren();
      FqlType[] signature = new FqlType[len];
      int i = 0;
      HQLAst child = (HQLAst) function.getFirstChild();
      while (child != null) 
      {
         FqlType argType = DataTypeHelper.expressionType(child, bufferMap);
         if (child.getType() == CAST)
         {
            signature[i++] = FqlType.valueOf(child.getChildAt(1).getText().toUpperCase());
            child = (HQLAst) child.getNextSibling();
            continue;
         }
         if (argType == FqlType.TEXT)
         {
            Optional<SessionAttr> attr = SessionAttr.sessionAttr(child.getText());
            if ( attr.isPresent())
            {
               argType = attr.get().type;
            }
         }
         signature[i++] = argType;
         child = (HQLAst) child.getNextSibling();
      }
      
      FunctionKey key = new FunctionKey(database, name, signature);
      String newName = overloadedFunctions.get(key);
      
      if (LOG.isLoggable(Level.FINEST))
      {
         LOG.log(Level.FINEST, "Overloading function '" + key + "' with '" + newName + "'");
      }
      
      if (newName != null)
      {
         function.setText(newName);
         function.putAnnotation("is_udf", true);
      }
      else if (LOG.isLoggable(Level.FINE))
      {
         LOG.log(Level.FINE, "Unable to overload function [" + key + "]");
         if (LOG.isLoggable(Level.FINEST))
         {
            LOG.log(Level.FINEST, "Available overloads:\n" + overloadedFunctions);
         }
      }
   }
   
   /**
    * Given an AST which represents a qualified or an unqualified text property, replace the
    * property's text (the property name) with a name which represents a computed column. The
    * computed column is a mechanism used by some databases to embed expressions into a synthetic
    * column, particularly for use in index definitions.
    * <p>
    * If the property reference is already embedded within an {@code upper()} function call, that
    * call is discarded if it is redundant with the computed column definition.
    *
    * @param   astNode
    *          Qualified or unqualified property AST node.
    *
    * @return  {@code true} if the computed column reference was injected;
    *          {@code false} if the current database dialect does not use computed columns, or if
    *          the given property is not a text property.
    *
    * @throws  AstException
    *          if there is an error restructuring the alias node or its parent node.
    */
   private boolean injectComputedColumn(HQLAst astNode)
   throws AstException
   {
      boolean qualified = astNode.getType() == ALIAS; // know what we are dealing with
      
      RecordBuffer buffer = qualified
            ? lookupBuffer(astNode)
            : bufferMap.get(null); // unqualified props are found in bufferMap under the [null] key
      Dialect dialect = database.isDirty()
            ? DatabaseManager.getDialect(database)
            : buffer.getDialect();
      if (!dialect.needsComputedColumns())
      {
         return false;
      }
      
      HQLAst propAst = qualified ? (HQLAst) astNode.getFirstChild() : astNode;
      String property = propAst.getText();
      Boolean ignoreCase = DatabaseManager.getIgnoreCase(buffer.getDMOImplementationClass(),
                                                         property);
      if (ignoreCase == null)
      {
         return false;
      }
      
      HQLAst parent = (HQLAst) astNode.getParent();
      
      // if the computed column is designed to ignore case (i.e., has an embedded upper()
      // function) AND the astNode already is contained within its own upper() function in the
      // tree, we need to replace the upper() function node with the astNode node.
      if (ignoreCase &&
          parent.getType() == FUNCTION &&
          "upper".equalsIgnoreCase(parent.getText()))
      {
         int idxPos = parent.getIndexPos();
         HQLAst grandparent = (HQLAst) parent.getParent();
         parent.remove();
         grandparent.graftAt(astNode, idxPos);
      }
      
      // prepend the computed column prefix to the property name.
      propAst.setText(dialect.getComputedColumnPrefix(!ignoreCase) + property);
      
      return true;
   }
   
   /*
    * Work around a defect in the Hibernate HQL parser which requires that an
    * expression which returns a boolean always be an operand in a binary
    * comparison test.  This requires that simple where clauses such as
    * <code>where true</code> be rewritten as <code>where true = true</code>,
    * and a case statement such as <code>case when ? then ? else ? end</code>
    * be rewritten as <code>case when ? = true then ? else ? end</code>.
    *
    * @param   root
    *          Root node of the AST for the HQL expression.
    *
    * @return  The root node of the AST for the HQL expression, which may be
    *          a different node than the root passed in.
    *
    * @throws  PersistenceException
    *          if any error occurred during rewriting the AST.
    */
   /* See header entry #008
   private HQLAst preprocessBooleanExpressions(HQLAst root)
   throws PersistenceException
   {
      Set targets = null;
      
      // Walk the tree and identify logical conditions which are not parented
      // by a comparison operator.  Rewrite them so that they emit as:
      // <expr> = true.
      Iterator iter = root.iterator();
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         HQLAst target = null;
         
         switch (next.getType())
         {
            case BOOL_TRUE:
            case BOOL_FALSE:
            case SUBST:
            case FUNCTION:
               HQLAst parent = (HQLAst) next.getParent();
               if (parent == null)
               {
                  target = next;
               }
               else
               {
                  switch (parent.getType())
                  {
                     case AND:
                     case OR:
                     case NOT:
                        target = next;
                        break;
                     case TERNARY:
                        if (next.getIndexPos() == 0)
                        {
                           target = next;
                        }
                        break;
                     default:
                        break;
                  }
               }
               break;
               
            default:
               break;
         }
         
         if (target != null)
         {
            if (targets == null)
            {
               targets = new HashSet();
            }
            targets.add(next);
         }
      }
      
      if (targets != null)
      {
         try
         {
            iter = targets.iterator();
            while (iter.hasNext())
            {
               HQLAst next = (HQLAst) iter.next();
               HQLAst parent = (HQLAst) next.getParent();
               
               HQLAst equals = new HQLAst();
               equals.setType(EQUALS);
               equals.setText("=");
               
               HQLAst boolTrue = new HQLAst();
               boolTrue.setType(BOOL_TRUE);
               boolTrue.setText("true");
               
               if (parent == null)
               {
                  root = parent = equals;
               }
               else
               {
                  int idxPos = next.getIndexPos();
                  next.remove();
                  parent.graftAt(equals, idxPos);
               }
               
               equals.graft(next);
               equals.graft(boolTrue);
            }
         }
         catch (AstException exc)
         {
            // TODO:  use more specific error number.
            throw new PersistenceException("Error rewriting HQL expression", -1, exc);
         }
      }
      
      return root;
   }
   */
   
   /**
    * Analyze the AST generated for the FQL expression, identifying those
    * subtrees which are targets for conversion.  A subtree is a target if
    * it is an alias whose first child is a property reference with a
    * subscript, AND the alias/property combination is associated with a
    * composite element in the ORM configuration.
    * <p>
    * The AST subtree structure which represents our target is as follows:
    * <pre>
    *    ALIAS
    *    |
    *    +--PROPERTY
    *       |
    *       +--LBRACKET
    *          |
    *          +--{...} (SUBST or NUM_LITERAL)
    * </pre>
    * <p>
    * In its original form, this would look like:
    * <pre>
    *    alias.property[subscript]
    * </pre>
    * <p>
    * If any such constructs are identified, they are added to a map of
    * targets and returned.
    *
    * @param   root
    *          Root node of the AST for the FQL expression.
    *
    * @return  Map of target AST subtrees to composite names, or {@code null} if no targets were found. The
    *          composite names are retrieved by the {@link DatabaseManager} from the appropriate ORM
    *          configurations.
    */
   private Map<HQLAst, String> analyzeComposites(HQLAst root)
   {
      Map<HQLAst, String> targets = null;
      
      Iterator<Aast> iter = root.iterator();
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         
         HQLAst alias = null;
         HQLAst property = null;
         HQLAst subscript = null;
         
         Class<? extends Record> dmoClass = null;
         
         // Do we have an alias node?
         if (next.getType() == ALIAS)
         {
            alias = next;
            property = (HQLAst) next.getChildAt(0);
            
            // Does the alias have a property child?
            if (property != null && property.getType() == PROPERTY)
            {
               // Look up DMO implementation class which corresponds with alias.
               RecordBuffer buffer = lookupBuffer(alias);
               dmoClass = buffer.getDMOImplementationClass();
               String entity = buffer.getEntityName();
               
               // At this point, we have a DMO entity and a property.  Though
               // not part of the composite analysis, this is a convenient
               // place to store off our restriction property names without
               // requiring a separate tree walk.
               addRestrictionProperty(entity, property.getText());
               
               // Look for an LBRACKET, which indicates a possible composite.
               HQLAst lbracket = (HQLAst) property.getChildAt(0);
               
               // Does the property have a subscript?
               if (lbracket != null && lbracket.getType() == LBRACKET)
               {
                  subscript = (HQLAst) lbracket.getChildAt(0);
               }
            }
            else
            {
               property = null;
            }
         }
         else
         {
            alias = null;
         }
         
         if (alias == null || property == null || subscript == null)
         {
            continue;
         }
         
         /*
         TODO: this code does nothing in HQLPreprocessor.  FQLToSQLConverter is responsible for computing the
         joins.
         RecordBuffer buf = bufferMap.get(alias.getText());
         DmoMeta dmoMeta = DmoMetadataManager.getDmoInfo(buf.getDMOImplementationClass());
         String propName = property.getText();
         Property extProperty = dmoMeta.getFieldInfo(propName);
         int extent = extProperty.index() > 0 ? 0 : extProperty.extent();
         if (false && extent > 0)
         {
            String composite = dmoMeta.getSqlTableName() + "__" + extent;
         
            // Add composite entry to the map.
            if (targets == null)
            {
               targets = new HashMap<>();
            }
            
            targets.put(alias, composite);
         }
         */
      }
      
      return targets;
   }
   
   /**
    * Get list of denormalized fields with extent for where clause AST.
    *
    * @param   root
    *          Root node of the AST for the FQL expression.
    *
    * @return  List of AST of denormalized fields with extent.
    */
   private Iterable<HQLAst> getDenormalizedFields(HQLAst root)
   {
      List<HQLAst> denormalizedFields = null;
      
      Iterator<Aast> iter = root.iterator();
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         
         HQLAst alias;
         HQLAst property = null;
         HQLAst subscript = null;
         
         // Do we have an alias node?
         if (next.getType() == ALIAS)
         {
            alias = next;
            property = (HQLAst) next.getChildAt(0);
            
            // Does the alias have a property child?
            if (property != null && property.getType() == PROPERTY)
            {
               RecordBuffer buffer = lookupBuffer(alias);
               if (buffer.isTemporary())
               {
                  continue;
               }
               
               if (TableMapper.getDenormalizedProperty(buffer, property.getText(), 0) != null)
               {
                  // Look for an LBRACKET, which indicates a possible composite.
                  HQLAst lbracket = (HQLAst) property.getChildAt(0);
                  
                  // Does the property have a subscript?
                  if (lbracket != null && lbracket.getType() == LBRACKET)
                  {
                     subscript = (HQLAst) lbracket.getChildAt(0);
                  }
               }
               else
               {
                  property = null;
               }
            }
            else
            {
               property = null;
            }
         }
         else
         {
            alias = null;
         }
         
         if (alias == null || property == null || subscript == null)
         {
            continue;
         }
         
         if (denormalizedFields == null)
         {
            denormalizedFields = new ArrayList<>();
         }
         
         denormalizedFields.add(property);
      }
      
      return denormalizedFields;
   }
   
   /**
    * Add the given property name to the set of properties used by this where
    * clause as restriction criteria.  Each set of properties is mapped to
    * the name of the DMO entity with which the property is associated.
    * <p>
    * In the current implementation, the nature of the restriction is not
    * preserved (e.g., equality match, function parameter, etc.).
    *
    * @param   entity
    *          DMO entity containing the given property.
    * @param   property
    *          Name of a property used by this where clause as a restrictive criterion.
    */
   private void addRestrictionProperty(String entity, String property)
   {
      if (restrictionProperties == null)
      {
         restrictionProperties = new HashMap<>();
      }
      
      Set<String> props = restrictionProperties.get(entity);
      if (props == null)
      {
         props = new HashSet<>();
         restrictionProperties.put(entity, props);
      }
      
      props.add(property);
   }
   
   /**
    * Restructure the AST subtrees (if any) defined as rewrite targets by the
    * {@link #analyzeComposites composite analysis step}.  This involves the expansion of an
    * expression, for instance:...
    * <pre>
    *    OR [ or ]
    *    |
    *    +--EQUALS [ = ]
    *    |  |
    *    |  +--ALIAS [ alias ]
    *    |  |  |
    *    |  |  +--PROPERTY [ propertyABC ]
    *    |  |     |
    *    |  |     +--LBRACKET [ [ ]
    *    |  |        |
    *    |  |        +--NUM_LITERAL [ 0 ]
    *    |  |
    *    |  +--SUBST [ ? ]
    *    |
    *    +--EQUALS [ = ]
    *       |
    *       +--ALIAS [ alias ]
    *       |  |
    *       |  +--PROPERTY [ propertyABC ]
    *       |     |
    *       |     +--LBRACKET [ [ ]
    *       |        |
    *       |        +--NUM_LITERAL [ 1 ]
    *       |
    *       +--SUBST [ ? ]
    * </pre>
    * ...to an expression which consists of the modified original expression and index
    * specifications added to the lop level of the tree using logical AND operators:
    * <pre>
    *   AND [ and ]
    *   |
    *   +--AND [ and ]
    *   |  |
    *   |  +--LPARENS [ ( ]
    *   |  |  |
    *   |  |  +--OR [ or ]
    *   |  |     |
    *   |  |     +--EQUALS [ = ]
    *   |  |     |  |
    *   |  |     |  +--ALIAS [ alias_composite2_0 ]
    *   |  |     |  |  |
    *   |  |     |  |  +--PROPERTY [ propertyABC ]
    *   |  |     |  |
    *   |  |     |  +--SUBST [ ? ]
    *   |  |     |
    *   |  |     +--EQUALS [ = ]
    *   |  |        |
    *   |  |        +--ALIAS [ alias_composite2_1 ]
    *   |  |        |  |
    *   |  |        |  +--PROPERTY [ propertyABC ]
    *   |  |        |
    *   |  |        +--SUBST [ ? ]
    *   |  |
    *   |  +--EQUALS [ = ]
    *   |     |
    *   |     +--FUNCTION [ index ]
    *   |     |   |
    *   |     |   +--ALIAS [ alias_composite2_0 ]
    *   |     |
    *   |     +--NUM_LITERAL [ 0 ]
    *   |
    *   +--EQUALS [ = ]
    *      |
    *      +--FUNCTION [ index ]
    *      |   |
    *      |   +--ALIAS [ alias_composite2_1 ]
    *      |
    *      +--NUM_LITERAL [ 1 ]
    * </pre>
    * <p>
    * This corresponds to the following expression expansion (for this expression, two composite
    * targets will be passed to the function):
    * <pre>
    *    alias.propertyABC[0] = ? or alias.propertyABC[1] = ?
    * </pre>
    * ...to this one:
    * <pre>
    *    (alias_composite2_0.propertyABC = ? or
    *      alias_composite2_1.propertyABC = ?) and
    *      index(alias_composite2_0) = 0 and
    *      index(alias_composite2_1) = 1
    * </pre>
    * However, the infix example above is informational only;  the generation of the final, infix
    * form of the expression is performed in a later step (see {@link #emit}).
    *
    * @param   targets
    *          Map of AST subtrees targeted for rewriting by the analysis step.  The map's keys
    *          are the subtrees themselves;  the map's values are the names of the composite list
    *          entities retrieved by the {@link DatabaseManager}.
    * @param   root
    *          The root node of the AST of the whole FQL expression.
    *
    * @return  The (possible new) root node of the restructured ASTs.
    *
    * @throws  PersistenceException
    *          if there is any error restructuring the ASTs.
    */
   private HQLAst restructure(HQLAst root, Map<HQLAst, String> targets)
   throws PersistenceException
   {
      try
      {
         for (Map.Entry<HQLAst, String> next : targets.entrySet())
         {
            HQLAst alias = next.getKey();
            String compName = next.getValue();
            boolean possibleNewRoot = false;
            HQLAst expNode = null;
            
            // find the innermost WHERE node if such node exist. Otherwise use the root.
            // The index for alias will be injected at this level
            HQLAst whereNode = (HQLAst) alias.getAncestor(-1, WHERE);
            if (whereNode == null)
            {
               expNode = root;
               possibleNewRoot = true;
            }
            else
            {
               expNode = (HQLAst) whereNode.getFirstChild(); 
            }
            if (expNode.getType() != LPARENS)
            {
               // parenthesize the whole original expression in current where
               HQLAst lparens = createAstNode(LPARENS, "(", (HQLAst) expNode.getParent());
               expNode.remove();
               lparens.graft(expNode);
               if (possibleNewRoot)
               {
                  root = lparens;
               }
               expNode = lparens;
            }
            rewriteAlias(alias, expNode, compName);
            if (possibleNewRoot && root.getParent() != null)
            {
               root = (HQLAst) root.getParent();
            }
         }
         
         return root; // it might have changed to a new LPARENS
      }
      catch (AstException exc)
      {
         // TODO:  use more specific error number.
         throw new PersistenceException("Error rewriting FQL expression", -1, exc);
      }
   }
   
   /**
    * Given a particular, target subtree and a composite list entity name, rewrite the AST as
    * described in the {@link #restructure} method.
    *
    * @param   alias
    *          Alias subtree to be expanded.
    * @param   relRoot
    *          The injection point for the index condition for the alias.
    * @param   compName
    *          Name of the composite list associated with the combination of the alias name and
    *          property name encountered in the {@code alias} subtree.
    *
    * @throws  AstException
    *          if there is any error grafting subtrees in the process of
    *          restructuring the subtree.
    */
   private void rewriteAlias(HQLAst alias, HQLAst relRoot, String compName)
   throws AstException
   {
      // Get the ASTs we need to manipulate.
      HQLAst property = (HQLAst) alias.getChildAt(0);
      HQLAst subscript = (HQLAst) property.getChildAt(0).getChildAt(0);
      String subIndex = (subscript.getType() == SUBST)
                        ? (subscript.getAnnotation("index") + "s")
                        : subscript.getText();
      
      // create qualified name of composite alias.
      String verboseName = alias.getText() + "_" + compName + "_" + subIndex;
      
      // reset alias AST's text to the composite name;  store original name in an annotation;
      // we will need it when generating join clauses for this composite.
      alias.putAnnotation("original_name", alias.getText());
      alias.setText(verboseName);
      
      if (!alias.isAnnotation("omit-index-check"))
      {
         // create the AND operator node to accommodate the second half of the test (the index
         // check). It will replace our relative root in its tree
         HQLAst and = createAstNode(AND, "and", (HQLAst) relRoot.getParent());
         relRoot.remove();
         and.graft(relRoot);
         
         // create the equality operator for the index check as the second child of the AND.
         HQLAst equals = createAstNode(EQUALS, "=", and);
         
         // create a function node for the FQL index function.
         HQLAst index = createAstNode(FUNCTION, "index", equals);
         
         // create a parameter node for the index function, using the new composite alias name.
         createAstNode(ALIAS, verboseName, index);
         
         // add a copy of the original subscript as the second child of the
         // index check equality operator.
         equals.graft(subscript.duplicateFresh());
      }
      
      // remove subscript from its original location.
      subscript.getParent().remove();
   }
   
   /**
    * Emit the {@link #restructure restructured} where clause AST into an infix notation expression, which is
    * compliant with the FQL syntax supported by the ORM. This is the post-rewrite, "finished product"
    * expression which will be returned by the {@link #getFQL} method.
    *
    * @param   root
    *          Root of the restructured where clause AST.
    * @param   translate
    *          Flag indicating if this processing is for a query translate.  In this case, the 
    *          {@link #dropAlias} and {@link #replacementAlias} is not used.
    *          
    * @return  Rewritten FQL where clause subexpression.
    */
   private FQLExpression emit(HQLAst root, boolean translate)
   {
      final FQLExpression buf = new FQLExpression();
      
      // define a walk listener which will inject content into the output string buffer during
      // appropriate tree walk events.
      AstWalkListener awl = new AstWalkListener()
      {
         /**
          * Called when the AST walk ascends between levels of the tree. Used to close off certain
          * constructs when emitting FQL.
          *
          * @param   ast
          *          AST node to which the walk is ascending.
          */
         public void ascent(Aast ast)
         {
            if (ast == null)
            {
               return;
            }
            
            switch (ast.getType())
            {
               case IN:
               case LPARENS:
               case FUNCTION:
               case CAST:
                  buf.append(")");
                  break;
                  
               case TERNARY:
                  buf.append(") end");
                  break;
                  
               case LBRACKET:
                  buf.append("]");
                  break;
                  
               case IS_NULL:
                  buf.append(" is null");
                  break;
                  
               case NOT_NULL:
                  buf.append(" is not null");
                  break;
                  
               case SELECT:
               case FROM:
               case JOIN:
                  buf.append(" ");
                  break;
               
               case SUBSELECT:
                  // grab them back from stacks:
                  dropAlias = dropAliases.pop();
                  replacementAlias = replacementAliases.pop();
                  bufferMap.put(null, defaultBuffers.pop());
                  break;
                  
               default:
                  break;
            }
         }
         
         /**
          * Called when the AST walk descends between levels of the tree. Initially used to emit a
          * dot operator after an alias, in order to dereference a property, it also adds a space
          * to separate the text from certain nodes from the text of the first child.
          *
          * @param   ast
          *          AST node from which the walk is descending.
          */
         public void descent(Aast ast)
         {
            if (ast == null)
            {
               return;
            }
            
            switch (ast.getType())
            {
               case ALIAS:
                  if (replacementAlias != null || !ast.getText().equals(dropAlias))
                  {
                     buf.append(".");
                  }
                  break;
               
               case SUBSELECT:
                  Aast from = ast.getImmediateChild(FROM, null);
                  defaultBuffers.push(bufferMap.get(null));
                  // save to stacks:
                  dropAliases.push(dropAlias);
                  replacementAliases.push(replacementAlias);
                  
                  // update to new default table
                  Aast alias = from.getImmediateChild(ALIAS, null);
                  dropAlias = alias.getText();
                  replacementAlias = DBUtils.getSubselectAlias(dropAlias);
                  bufferMap.put(null, bufferMap.get(dropAlias));
                  break;
               
               case SELECT:
               case FROM:
               case JOIN:
               case WHERE:
               case ESCAPE:
               case NOT:
                  buf.append(" ");
                  break;
                  
               default:
                  break;
            }
         }
         
         /**
          * Called when the AST walk moves laterally between sibling nodes. Used to emit the
          * symbol of a binary operator between operands of a binary operation, or to emit a comma
          * or other separators between function parameters or complete the operator's structure.
          *
          * @param   ast
          *          AST parent node of the siblings between which the walk
          *          is moving laterally.
          * @param   index
          *          Zero-based index of the sibling node about to be visited.
          */
         public void nextChild(Aast ast, int index)
         {
            if (ast == null)
            {
               return;
            }
            
            if (isBinaryOperator(ast))
            {
               buf.append(" ");
               if (index == 1 || ast.getType() != LIKE)
               {
                  // LIKE is not a truly binary operator like +, -, || to use chaining
                  // in fact the other comparing operators are not, as well
                  buf.append(ast.getText());
                  buf.append(" ");
               }
               if (index == 1 && ast.getType() == IN)
               {
                  buf.append("("); // start the list
               }
            }
            else if (ast.getType() == IN)
            {
               if (index == 1)
               {
                  buf.append(" in ("); // start the list
               }
               else 
               {
                  buf.append(", "); // separate the list elements 
               }
            }
            else
            {
               switch (ast.getType())
               {
                  case FUNCTION:
                     buf.append(", ");
                     break;
                     
                  case CAST:
                     buf.append(" as ");
                     break;
                     
                  case TERNARY:
                     switch (index)
                     {
                        case 1:
                           buf.append(" then (");
                           break;
                        case 2:
                           buf.append(") else (");
                           break;
                        default:
                           break;
                     }
                     break;
                     
                  case FROM:
                  case JOIN:
                     // FROM/JOIN will only occur in sub-selects
                     Aast visited = ast.getChildAt(index);
                     if (replacementAlias != null || dropAlias == null || !dropAlias.equals(visited.getText()))
                     {
                        // emit [as] only if this is not the alias to be dropped 
                        buf.append(" as ");
                     }
                     break;
                  case SUBSELECT:
                     Aast child = ast.getChildAt(index);
                     if (child.getType() == NUM_LITERAL)
                     {
                        if (ast.getChildAt(index - 1).getType() != SELECT)
                        {
                           buf.append(",");
                        }
                        buf.append(" ");
                     }
                     
                     if (child.getType() == FROM)
                     {
                        buf.append(" ");
                     }
                     break;
                     
                  default:
                     break;
               }
            }
         }
      };
      
      // Walk the full tree and emit the expression into the string buffer.
      Iterator<Aast> iter = root.iterator(0, awl);
      int posIndex = 0;
      while (iter.hasNext())
      {
         HQLAst next = (HQLAst) iter.next();
         int ttype = next.getType();
         
         if (!isBinaryOperator(next))
         {
            String text = next.getText();
            switch (ttype)
            {
               case SUBST:
                  Long paramCount = (Long) next.getAnnotation("positional-params"); 
                  if (paramCount == null || paramCount < 2)
                  {
                     buf.append("", true);
                  }
                  else 
                  {
                     buf.append(":px").append(Integer.toString(++posIndex));
                  }
                  break;
                  
               case FUNCTION:
               case CAST:
                  buf.append(text);
                  buf.append("(");
                  if (next.getNumImmediateChildren() == 0)
                  {
                     buf.append(")");
                  }
                  break;
                  
               case TERNARY:
                  buf.append("case when ");
                  break;
                  
               case SUBSELECT:
               case IS_NULL:
               case NOT_NULL:
                  break;
                  
               case IN:
                  break; // in is emitted in infixed form, similar to binary operators
               
               case ALIAS:
                  // replace the [dropAlias] with proper [replacementAlias] 
                  if (!translate && text.equals(dropAlias) && replacementAlias != null)
                  {
                     buf.append(replacementAlias);
                     break;
                  }
                  
                  // search for alias in the outer scopes
                  int aliasScope = dropAliases.indexOf(text);
                  if (aliasScope > -1)
                  {
                     String scopedAlias = replacementAliases.get(aliasScope);
                     if (scopedAlias != null)
                     {
                        // if [dropAliases] and [replacementAliases] are in sync, [scopedAlias]
                        //    should never be [null]
                        buf.append(scopedAlias);
                        break;
                     }
                  }
                  
                  buf.append(text);
                  break;
                  
               default:
                  buf.append(text);
                  break;
            }
         }
      }
      
      return buf;
   }
   
   /**
    * Define the set of ANSI-style join statements required to join the
    * "master" DMO with one or more associated lists of composite elements.
    * These will be integrated by the enclosing query into the overall FQL statement.
    * <p>
    * Duplicates are eliminated.  The order in which joins are detected is
    * preserved in the resulting data structure, such that later iterations
    * over the set will return these in the same order in which they were
    * added.
    *
    * @param   targets
    *          Map of AST subtrees targeted for rewriting by the analysis
    *          step.  The map's keys are the subtrees themselves;  the map's
    *          values are the names of the composite list entities retrieved
    *          by the {@link DatabaseManager}.
    *
    * @return  Set of join subexpression strings.  May be empty if no targets
    *          were specified, but will not be <code>null</code>.
    */
   private LinkedHashSet<String> generateJoins(Map<HQLAst, String> targets)
   {
      LinkedHashSet<String> joins = new LinkedHashSet<>();
      for (Map.Entry<HQLAst, String> next : targets.entrySet())
      {
         HQLAst alias = next.getKey();
         String compName = next.getValue();
         String aliasName = (String) alias.getAnnotation("original_name");
         String verboseName = alias.getText();
         String joinStr = generateJoin(compName, aliasName, verboseName);
         Aast subselect = alias.getAncestor(-1, SUBSELECT);
         boolean injected = false;
         
         if (subselect != null)
         {
            HQLAst from = (HQLAst) subselect.getImmediateChild(FROM, null);
            HQLAst fromAlias = (HQLAst) from.getImmediateChild(ALIAS, null);
            
            // only join composite of same table, otherwise look up the tree
            // TODO: current implementation only supports a single nested level
            // FIXME: attempt to find upper subselect and try again
            if (aliasName.equals(fromAlias.getText()))
            {
               HQLAst exJoin = (HQLAst) subselect.getImmediateChild(JOIN, from);
               boolean found = false;
               while (exJoin != null)
               {
                  if (joinStr.equals(exJoin.getAnnotation("joinStr")))
                  {
                     found = true; // the join was already injected
                     break;
                  }
                  exJoin = (HQLAst) subselect.getImmediateChild(JOIN, exJoin);
               }
               
               if (!found)
               {
                  // in case of a sub-query inject the composition join at proper location
                  int pos = -1;
                  Aast where = subselect.getImmediateChild(WHERE, null);
                  if (where != null)
                  {
                     pos = where.getIndexPos();
                  }
                  
                  HQLAst join = createAstNode(JOIN, "join", null);
                  join.putAnnotation("joinStr", joinStr);
                  HQLAst compAlias = createAstNode(ALIAS, aliasName, join);
                  createAstNode(PROPERTY, compName, compAlias);
                  createAstNode(ALIAS, verboseName, join);
                  // inject the JOIN node before the WHERE if it exists or at the end otherwise
                  subselect.graftAt(join, pos);
               }
               // otherwise (strJoin was found in an existing JOIN node) keep it from adding to
               // result to be returned
               
               injected = true;
            }
         }
         
         if (!injected)
         {
            joins.add(joinStr);
         }
      }
      
      return joins;
   }
   
   /**
    * Generate a single FQL subexpression for an ANSI-style join, which joins
    * a "master" DMO record with an associated list of composite elements.
    *
    * @param   compName
    *          Name of the associated list of composite elements.
    * @param   alias
    *          Alias for the "master" DMO record to which the composite list
    *          is to be joined.
    * @param   verboseAlias
    *          Verbose alias name, including the composite portion and
    *          subscript qualifier.
    * 
    * @return  Join text snippet.
    */
   private String generateJoin(String compName, String alias, String verboseAlias)
   {
      return "join " + alias + "." + compName + " as " + verboseAlias;
   }
   
   /**
    * Create the mapping of substitution parameters to their corresponding
    * substitution placeholders in the rewritten FQL where clause.  These
    * placeholders (<code>?</code>) may have been reordered or removed (due to
    * inlining) during the rewrite process.
    * <p>
    * During parsing of the FQL clause, an index annotation was added to each
    * AST of type <code>SUBST</code> (token type for the query substitution
    * parameter placeholder), denoting that placeholder's position in the
    * original where clause.  We now walk the rewritten AST, extracting this
    * annotation and storing it in a new list.  The resulting array defines
    * the new order of the substitution parameters.  For parameters which
    * were dropped, due to inlining, a null placeholder will appear in the new
    * list.
    *
    * @param   root
    *          Root node of the rewritten where clause AST.
    * @param   parameters
    *          Query substitution parameters for the where clause.
    *
    * @return  Object which maps new query substitution parameter positions to their positions in the original
    *          parameters array. These indices are zero-based.
    */
   private ParameterIndices createParameterIndices(HQLAst root, Object[] parameters)
   {
      if (parameters == null)
      {
         return (new ParameterIndices(null));
      }
      
      List<Integer> indices = new ArrayList<>(parameters.length);
      
      // Extract the "index" annotation from each SUBST node, in order of
      // the tree walk, which will correspond to the order of the placeholders
      // in the final, infix expression.
      Iterator<Aast> iter = root.iterator();
      while (iter.hasNext())
      {
         Long index = null;
         HQLAst next = (HQLAst) iter.next();
         switch (next.getType())
         {
            case SUBST:
               index = (Long) next.getAnnotation("index");
               indices.add(index.intValue());
               break;
               
            case NUM_LITERAL:
            case DEC_LITERAL:
            case STRING:
            case IS_NULL:
            case NOT_NULL:
            case BOOL_FALSE:
               if (inline && next.isAnnotation("inlined"))
               {
                  // Allow null to remain at this position in the array.
                  indices.add(null);
               }
               break;
               
            default:
               break;
         }
      }
      
      Integer[] idx = indices.toArray(new Integer[indices.size()]);
      
      return (new ParameterIndices(idx));
   }
   
   /**
    * Convenience method to detect whether an AST represents a binary
    * operator, based on its token type.
    *
    * @param   ast
    *          AST node to test.
    *
    * @return  <code>true</code> if the AST's token type indicates a binary
    *          operator, else <code>false</code>.
    */
   private boolean isBinaryOperator(Aast ast)
   {
      switch (ast.getType())
      {
         case OR:
         case AND:
         case EQUALS:
         case NOT_EQ:
         case LIKE:
         case GT:
         case LT:
         case GTE:
         case LTE:
         case PLUS:
         case MINUS:
         case MULTIPLY:
         case DIVIDE:
         case CONCAT:
            return true;
      }
      
      return false;
   }
   
   /**
    * Given an AST and its index, and assuming it is one of two operands in a
    * binary comparison, indicate whether the opposite operand is nullable.
    * It is considered nullable if it is not a non-nullable DMO property.
    * <p>
    * TODO:  currently, we only test if the property is the reserved, primary
    * key property.  We should augment this test to check for other
    * non-nullable properties.
    * 
    * @param   ast
    *          The operand opposite the operand we wish to test.
    * @param   index
    *          The index of <code>ast</code> among its siblings.  This must be
    *          0 or 1, since the expression is binary.
    * 
    * @return  <code>true</code> if the operand opposite <code>ast</code> is
    *          nullable, else <code>false</code>.
    */
   private boolean isOppositeOperandNullable(HQLAst ast, int index)
   {
      boolean nullable = true;
      HQLAst target = (HQLAst) (index == 0 ? ast.getNextSibling() : ast.getPrevSibling());
      HQLAst property = null;

      do
      {
         switch (target.getType())
         {
            case PROPERTY:
               property = target;
               break;
            case ALIAS:
               target = (HQLAst) target.getFirstChild();
               break;
            default:
               target = null;
         }
      } while (property == null && target != null);
      
      if (property != null)
      {
         nullable = !DatabaseManager.PRIMARY_KEY.equals(property.getText());
      }
      
      return nullable;
   }

   /**
    * Utility method for creating a designed HQLAst node. Optionally the newly created node is
    * grafted to a parent as the last child.
    *
    * @param   type
    *          The type of the new HQLAst node.
    * @param   text
    *          The text of the new HQLAst node.
    * @param   parent
    *          The parent of the new HQLAst node (if not null).
    *
    * @return  the newly created and optionally grafted HQLAst node.
    */
   private static HQLAst createAstNode(int type, String text, HQLAst parent)
   {
      HQLAst newAst = new HQLAst();
      newAst.setType(type);
      newAst.setText(text);
      if (parent != null)
      {
         parent.graft(newAst);
      }
      
      return newAst;
   }
   
   /**
    * Object which contains information about a single comparison of a database property to a
    * substitution parameter or literal value. Used for very rudimentary analysis of the
    * complexity of a where clause and its suitability to be used as a component of a server-side
    * join query.
    */
   static class PropertyMatch
   {
      /** First DMO alias name (mandatory) */
      final String alias;

      /** First DMO property name (mandatory) */
      final String property;

      /** Second DMO alias name (optional, for complex matches) */
      final String secondAlias;

      /** Second DMO property name (optional, for complex matches) */
      final String secondProperty;
      
      /** Substitution parameter symbol or literal value */
      final String rval;
      
      /** Operator token type */
      final int operator;
      
      /** Rvalue token type */
      final int rvalType;

      /** Flag indicating if the alias is the first or second operand. */
      final boolean isAliasFirst;

      /** The index of the SUBST node relative to the 'where' clause parameters. */
      final long substIndex;
      
      /**
       * Constructor.
       * 
       * @param   alias
       *          DMO alias name.
       * @param   property
       *          DMO property name.
       * @param   rval
       *          Substitution parameter symbol or literal value.
       * @param   operator
       *          Operator token type.
       * @param   rvalType
       *          Rvalue token type.
       * @param   isAliasFirst
       *          {@code true} if the alias node is the first operand, {@code false} otherwise
       * @param   substIndex
       *          The index number of the SUBST node.
       */
      private PropertyMatch(String alias,
                            String property,
                            String rval,
                            int operator,
                            int rvalType,
                            boolean isAliasFirst,
                            long substIndex)
      {
         this.alias = alias;
         this.property = property;
         this.secondAlias = null; // Not applicable for simple matches
         this.secondProperty = null; // Not applicable for simple matches
         this.rval = rval;
         this.operator = operator;
         this.rvalType = rvalType;
         this.isAliasFirst = isAliasFirst;
         this.substIndex = substIndex;
      }

      /**
       * Constructor for complex matches (alias1.property1 = alias2.property2).
       *
       * @param   alias
       *          First DMO alias name.
       * @param   property
       *          First DMO property name.
       * @param   secondAlias
       *          Second DMO alias name.
       * @param   secondProperty
       *          Second DMO property name.
       * @param   operator
       *          Operator token type.
       */
      private PropertyMatch(String alias,
                            String property,
                            String secondAlias,
                            String secondProperty,
                            int operator)
      {
         this.alias = alias;
         this.property = property;
         this.secondAlias = secondAlias;
         this.secondProperty = secondProperty;
         this.rval = null;
         this.operator = operator;
         this.rvalType = HQLParserTokenTypes.PROPERTY;
         this.isAliasFirst = false;
         this.substIndex = -1;
      }
   }
   
   /**
    * Instances of this class are used as keys to cache instances of <code>HQLPreprocessor</code>.
    */
   private static class CacheKey
   {
      /** Empty array of parameters. */
      private static final Object[] EMPTY_PARAMETERS = new Object[0];
      
      /** Empty array of types. */
      private static final FqlType[] EMPTY_TYPES = new FqlType[0];
      
      /** Empty array of field reference strings. */
      private static final String[] EMPTY_REFSUBS = new String[0];
      
      /** Database instance */
      private final Database database;
      
      /** DMO aliases */
      private final String[] aliases;
      
      /** DMO interfaces, possibly <code>null</code> */
      private final Class<?>[] dmoIfaces;
      
      /** Where clause */
      private final String where;

      /** Sort clause */
      private final String sort;
      
      /** Query substitution parameter types */
      private final FqlType[] types;
      
      /** Query substitution parameters (resolved). */
      private final Object[] parameters;
      
      /** Array of field reference substitution replacements strings and nulls */
      private final String[] referenceSubs;
      
      /** Whether query substitution parameters <em>may</em> be inlined */
      private final boolean inlinable;
      
      /** Alias to drop, if any */
      private final String dropAlias;
      
      /** Replacement alias, if any */
      private final String replacementAlias;
      
      /** Flag indicating preprocessing is analytical only and query will not be executed */
      private final boolean informational;
      
      /** The cache code.*/
      private final int cachedHash;
      
      /**
       * Constructor.
       * 
       * @param   database
       *          Database instance.
       * @param   aliases
       *          One or more DMO aliases, corresponding with {@code dmoIfaces}.
       * @param   dmoIfaces
       *          One or more DMO interfaces, corresponding with {@code aliases}.
       * @param   where
       *          Where clause.
       * @param   sort
       *          Sort clause.
       * @param   types
       *          Query substitution parameter types.
       * @param   parameters
       *          Query substitution parameters (resolved).
       * @param   referenceSubs
       *          Array of field reference substitution replacements strings and nulls.
       * @param   inlinable
       *          Whether query substitution parameters <em>may</em> be inlined.
       * @param   dropAlias
       *          Alias to drop, if any.
       * @param   informational
       *          Flag indicating preprocessing is analytical only and query will not be executed.
       */
      private CacheKey(Database database,
                       String[] aliases,
                       Class<?>[] dmoIfaces,
                       String where,
                       String sort,
                       FqlType[] types,
                       Object[] parameters,
                       String[] referenceSubs,
                       boolean inlinable,
                       String dropAlias,
                       String replacementAlias,
                       boolean informational)
      {
         this.database = database;
         this.aliases = aliases;
         this.dmoIfaces = dmoIfaces;
         this.where = where;
         this.sort = sort;
         this.types = (types != null ? types : EMPTY_TYPES);
         this.parameters = (parameters != null ? parameters : EMPTY_PARAMETERS);
         this.referenceSubs = (referenceSubs != null ? referenceSubs : EMPTY_REFSUBS);
         this.inlinable = inlinable;
         this.dropAlias = (dropAlias != null ? dropAlias : "");
         this.replacementAlias = (replacementAlias != null ? replacementAlias : "");
         this.informational = informational;
         
         // since this object is immutable (all fields are private/final) we can compute now the
         // hash code and cache it for when needed, to improve performance 
         this.cachedHash = _hashCode(); 
      }
      
      /**
       * Factory of cache keys. This ensures that the cache key won't have values that will pin
       * down session level resources like handles or objects.
       * 
       * @param   database
       *          Database instance.
       * @param   aliases
       *          One or more DMO aliases, corresponding with {@code dmoIfaces}.
       * @param   dmoIfaces
       *          One or more DMO interfaces, corresponding with {@code aliases}.
       * @param   where
       *          Where clause.
       * @param   sort
       *          Sort clause.
       * @param   types
       *          Query substitution parameter types.
       * @param   parameters
       *          Query substitution parameters (resolved).
       * @param   referenceSubs
       *          Array of field reference substitution replacements strings and nulls.
       * @param   inlinable
       *          Whether query substitution parameters <em>may</em> be inlined.
       * @param   dropAlias
       *          Alias to drop, if any.
       * @param   informational
       *          Flag indicating preprocessing is analytical only and query will not be executed.
       */
      public static CacheKey get(Database database,
                                 String[] aliases,
                                 Class<?>[] dmoIfaces,
                                 String where,
                                 String sort,
                                 FqlType[] types,
                                 Object[] parameters,
                                 String[] referenceSubs,
                                 boolean inlinable,
                                 String dropAlias,
                                 String replacementAlias,
                                 boolean informational)
      {
         if (parameters != null)
         {
            Object[] cachableParameters = null;
            for (int i = 0; i < parameters.length; i++) 
            {
               if (parameters[i] instanceof BaseDataType)
               {
                  // even if the caching is per-context, we do not want to pin down data that is
                  // may retain a lot of data inside the cache keys. handles, com-handles, clobs, etc. 
                  Object cachableParam = ((BaseDataType) parameters[i]).getIndependentFromContext();
                  
                  if (cachableParam == null)
                  {
                     // we detected a parameter that can't be cached
                     return null;
                  }
                  
                  // if the parameter is possible to be cached using a different representation
                  // then attempt to build a parameter list that is able to be cached properly
                  if (cachableParam != parameters[i])
                  {
                     if (cachableParameters == null)
                     {
                        cachableParameters = parameters.clone();
                     }
                     
                     cachableParameters[i] = cachableParam;
                  }
               }
               else if (parameters[i] instanceof Long)
               {
                  // long parameters are usually used with rowid and are able to be cached. 
               }
               else 
               {
                  return null;
               }
            }
            
            if (cachableParameters != null)
            {
               parameters = cachableParameters;
            }
         }
         
         return new CacheKey(database, 
                             aliases, 
                             dmoIfaces, 
                             where, 
                             sort, 
                             types, 
                             parameters, 
                             referenceSubs, 
                             inlinable, 
                             dropAlias,
                             replacementAlias,
                             informational);
      }

      /**
       * Return a hash code consistent with {@link #equals}.
       * 
       * @return  Hash code.
       */
      public int hashCode()
      {
         return cachedHash;
      }
      
      /**
       * Check this object for equivalence with another instance of this class.
       * 
       * @param   o
       *          Another instance of this class.
       * 
       * @return  <code>true</code> if instances are equivalent, else <code>false</code>.
       */
      public boolean equals(Object o)
      {
         if (!(o instanceof CacheKey) )
         {
            return false;
         }
         
         CacheKey that = (CacheKey) o;
         
         if (inlinable != that.inlinable                     ||
             informational != that.informational             ||
             !Arrays.equals(aliases, that.aliases)           ||
             !Arrays.equals(dmoIfaces, that.dmoIfaces)       ||
             !Arrays.equals(types, that.types)               ||
             !Arrays.equals(parameters, that.parameters)     ||
             !dropAlias.equals(that.dropAlias)               ||
             !replacementAlias.equals(that.replacementAlias) ||
             !database.equals(that.database)                 ||
             !where.equals(that.where)                       ||
             !sort.equals(that.sort))
         {
            return false;
         }
         
         int len1 = referenceSubs.length;
         int len2 = that.referenceSubs.length;
         
         if (len1 != len2)
         {
            return false;
         }
         
         for (int i = 0; i < len1; i++)
         {
            String thisNext = referenceSubs[i];
            String thatNext = that.referenceSubs[i];
            
            if (thisNext == null || thatNext == null)
            {
               if (thisNext != thatNext)
               {
                  // one null and not the other; no match
                  return false;
               }
               
               // both null; keep going
               continue;
            }
            
            if (!thisNext.equals(thatNext))
            {
               return false;
            }
         }
         
         return true;
      }
      
      /**
       * Return a hash code consistent with {@link #equals}.
       *
       * @return  Hash code.
       */
      private int _hashCode()
      {
         int result = 17;
         result = 37 * result + database.hashCode();
         result = 37 * result + where.hashCode();
         result = 37 * result + sort.hashCode();
         result = 37 * result + Arrays.hashCode(aliases);
         result = 37 * result + Arrays.hashCode(dmoIfaces);
         result = 37 * result + Arrays.hashCode(types);
         result = 37 * result + Arrays.hashCode(parameters);
         
         for (String s : referenceSubs)
         {
            result *= 37;
            if (s != null)
            {
               result += s.hashCode();
            }
         }
         
         result = 37 * result + (inlinable ? 0 : 1);
         result = 37 * result + dropAlias.hashCode();
         result = 37 * result + replacementAlias.hashCode();
         result = 37 * result + (informational ? 0 : 1);
         
         return result;
      }
   }

   /**
   * Instances of this class are used as keys to cache translate output.
   */
   private static class TranslateCacheKey
   {
      /** Bound buffers aliases */
      private final String[] boundAliases;

      /** Definition buffers aliases */
      private final String[] defAliases;

      /** Bound buffers dmo interfaces */
      private final Class<?>[] boundDmoIfaces;

      /** Definition buffers dmo interfaces */
      private final Class<?>[] defDmoIfaces;

      /** The where clause to be translated */
      private final String where;

      /** The cached hash code.*/
      private final int cachedHash;

      /**
      * Constructor.
      *
      * @param   boundAliases
      *          Bound buffers aliases
      * @param   defAliases
      *          Definition buffers aliases
      * @param   boundDmoIfaces
      *          Bound buffers dmo interfaces
      * @param   defDmoIfaces
      *          Definition buffers dmo interfaces
      * @param   where
      *          The where clause to be translated.
      */
      TranslateCacheKey(String[] boundAliases,
                        String[] defAliases,
                        Class<?>[] boundDmoIfaces,
                        Class<?>[] defDmoIfaces,
                        String where)
      {
         this.boundAliases = boundAliases;
         this.defAliases = defAliases;
         this.boundDmoIfaces = boundDmoIfaces;
         this.defDmoIfaces = defDmoIfaces;
         this.where = where;
         // since this object is immutable (all fields are private/final) we can compute now the
         // hash code and cache it for when needed, to improve performance
         this.cachedHash = _hashCode();
      }

      /**
      * Return a hash code consistent with {@link #equals}.
      *
      * @return  Hash code.
      */
      public int hashCode()
      {
         return cachedHash;
      }

      /**
      * Check this object for equivalence with another instance of this class.
      *
      * @param   o
      *          Another instance of this class.
      *
      * @return  <code>true</code> if instances are equivalent, else <code>false</code>.
      */
      public boolean equals(Object o)
      {
         if (!(o instanceof TranslateCacheKey) )
         {
             return false;
         }

         TranslateCacheKey that = (TranslateCacheKey) o;

         if (!Arrays.equals(boundAliases, that.boundAliases)     ||
             !Arrays.equals(defAliases, that.defAliases)         ||
             !Arrays.equals(boundDmoIfaces, that.boundDmoIfaces) ||
             !Arrays.equals(defDmoIfaces, that.defDmoIfaces)     ||
             !where.equals(that.where))
         {
             return false;
         }

         return true;
      }

      /**
      * Return a hash code consistent with {@link #equals}.
      *
      * @return  Hash code.
      */
      private int _hashCode()
      {
         int result = 17;
         result = 37 * result + where.hashCode();
         result = 37 * result + Arrays.hashCode(boundAliases);
         result = 37 * result + Arrays.hashCode(defAliases);
         result = 37 * result + Arrays.hashCode(boundDmoIfaces);
         result = 37 * result + Arrays.hashCode(defDmoIfaces);
         return result;
      }
   }
   
   /** A public container for storing a pair of properties. It is immutable, used for transferring data. */
   public static final class PropertyPair
   {
      /** Operator used for joining the properties. */
      private final String operator;
      
      /** The full name of the first (left) property. */
      private final String name1;
      
      /** The full name of the second (right) property. */
      private final String name2;
      
      /**
       *  The constructor initializes all the fields.
       * 
       * @param   alias1
       *          The alias (table) of the first (left) property.
       * @param   property1
       *          The name of the property from first (left) table.
       * @param   alias2
       *          The alias (table) of the second (right) property.
       * @param   property2
       *          The name of the property from second (right) table.
       * @param   operator
       *          The operator which creates a relations between the two fields. We are interested only in
       *          "=" operator, but this leave room for other usages of this class.
       */
      public PropertyPair(String alias1, String property1, String alias2, String property2, String operator)
      {
         this.operator = operator;
         this.name1 = alias1 + "." + property1;
         this.name2 = alias2 + "." + property2;
      }
      
      /**
       * Obtain the full name of the first (left) property.
       *
       * @return  the full name of the first (left) property.
       */
      public String getLeft()
      {
         return name1;
      }
      
      /**
       * Obtain the full name of the second (right) property.
       *
       * @return  the full name of the second (right) property.
       */
      public String getRight()
      {
         return name2;
      }
      
      /**
       * Obtain the operator used for joining the properties..
       *
       * @return the operator used for joining the properties.
       */
      public String getOperator()
      {
         return operator;
      }
   }
   
   /**
    * Check if node represents UDF.
    * @param dialect 
    *        database dialect
    * @param n
    *        node to be checked
    * @return <code>true</code> if node represents UDF.
    */
   private static boolean isUDF(Dialect dialect, HQLAst n)
   {
      String udfSchema = dialect.udfSchema();
      if (n.getAnnotation("is_udf") != null)
      {
         return true;
      }
      String fn = n.getText().toLowerCase() + "/" + n.getNumImmediateChildren();
      if (fn.startsWith(udfSchema))
      {
         fn = fn.substring(udfSchema.length());
      }
      int pos = fn.lastIndexOf("_");
      if (pos > 0)
      {
         fn = fn.substring(0, pos);
      }
      return UdfNamesHolder.UDF_NAMES.contains(fn); 
   }
   
   /**
    * Check if node represents UDF argument.
    * @param dialect 
    *        database dialect
    * @param n
    *        node to be checked
    * @return <code>true</code> if node represents UDF argument.
    */
   private static boolean isUDFArgument(Dialect dialect, HQLAst n)
   {
      while (true)
      {
         n = (HQLAst)n.getParent();
         if (n == null || n.getType() != FUNCTION)
         {
            return false;
         }
         if (isUDF(dialect, n))
         {
            return true;
         }
      }
   }

   /**
    * Holds the set of UDFs synthetic names.  
    */
   private static class UdfNamesHolder
   {
      /** The set of UDFs synthetic names */ 
      public static final Set<String> UDF_NAMES = Collections.unmodifiableSet(
               BuiltIns.getStandardFunctions().stream().map(UdfNamesHolder::methodName).
                        map(String::toLowerCase).
                        collect(Collectors.toSet())
      );  

      /**
       * Get the synthetic method name (includes number of arguments).
       * @param m
       *        method
       * @return the synthetic method name.
       */
      private static String methodName(Method m)
      {
         HQLFunction fqlFunction = m.getAnnotation(HQLFunction.class);
         String fn = fqlFunction.name(); 
         if (fn == null || "".equals(fn.trim()))
         {
            fn = m.getName();
         }
         return fn.toLowerCase() + "/" + m.getParameterCount();
      }
      
   }
   
   /**
    * Mutable SESSION attributes added to the UDF calls  
    */
   public static enum SessionAttr
   {
      /** SESSION:DATE-FORMAT */
      DATE_FORMAT("'${DF}'", 
                  (d) -> "'" + SessionUtils.getDateFormat().toStringMessage() + "'",
                  FqlType.TEXT),
      /** SESSION:TIMEZONE */
      TIMEZONE("'${TZ}'", SessionAttr::tz,FqlType.INTEGER);
      
      /**
       * Get timezone for UDF argument
       * 
       * @return SESSION:TIMEZONE if defined or 'gettimezone()' UDF call
       */
      private static String tz(Dialect dialect)
      {
         integer tz = SessionUtils.getSessionTimeZone();
         return tz == null || tz.isUnknown() ? 
                "cast(" + dialect.udfSchema() + "gettimezone() as integer)" : 
                String.valueOf(tz.intValue());
      }
      
      /** 
       * Map of values by placeholeder
       */
      private static final Map<String, SessionAttr> ATTRS = 
               Collections.unmodifiableMap(
                  Arrays.stream(values()).
                  collect(Collectors.toMap(a -> a.placeHolder, a -> a))
      );
      /**  Placeholder used in re-writing*/
      public final String placeHolder;

      /** Placeholder value supplier */
      public final Function<Dialect, String> value;
      
      /** Placeholder value type */
      public final FqlType type;
      

      /** 
       * Map placeholder to attribute
       * @param placeHolder
       *        attribute placeHolder
       * @return Optional attribute for a placeholder   
       */
      public static Optional<SessionAttr> sessionAttr(String placeHolder)
      {
         return Optional.ofNullable(ATTRS.get(placeHolder));
      }
      
      /** 
       * Constructor.
       * @param placeHolder
       *        Placeholder used in re-writing.
       * @param value
       *        Placeholder value supplier.
       * @param type
       *        Placeholder value type.
       */
      private SessionAttr(String placeHolder, Function<Dialect, String> value, FqlType type)
      {
         this.placeHolder = placeHolder;
         this.value = value;
         this.type = type;
      }
   }
   
   /**
    * Context local work area.
    */
   private static class WorkArea
   {      
      /** Cache of prepared instances, indexed by Persistence and where clause */
      private LRUCache<CacheKey, FQLPreprocessor> cacheWithArgs;
      
      /** Cache of prepared instances, indexed by Persistence and where clause */
      private LRUCache<CacheKey, FQLPreprocessor> cacheNoArgs;
      
      WorkArea()
      {
         cacheNoArgs = CacheManager.createLRUCache(FQLPreprocessor.class, "!noArgs", 2048);
         cacheWithArgs = CacheManager.createLRUCache(FQLPreprocessor.class, "!witArgs", 2048);
      }

      /**
       * Retrieve the FQL preprocessor from the cache based on a key that doesn't include
       * the arguments.
       * 
       * @param   keyNoArgs
       *          The cache key that doesn't contain the arguments.
       *          
       * @return  A cached FQL preprocessor or {@code null}.
       */
      public FQLPreprocessor getCacheNoArgs(CacheKey keyNoArgs)
      {
         return cacheNoArgs.get(keyNoArgs);
      }

      /**
       * Retrieve the FQL preprocessor from the cache based on a key that does include
       * the arguments.
       * 
       * @param   keyWithArgs
       *          The cache key that does contain the arguments.
       *          
       * @return  A cached FQL preprocessor or {@code null}.
       */
      public FQLPreprocessor getCacheWithArgs(CacheKey keyWithArgs)
      {
         return cacheWithArgs.get(keyWithArgs);
      }

      /**
       * Insert an FQL preprocessor into the cache based on a key that doesn't include
       * the arguments.
       * 
       * @param   keyNoArgs
       *          The cache key that doesn't contain the arguments.
       * @param   preproc
       *          The preprocessor to be cached.
       */
      public void putCacheNoArgs(CacheKey keyNoArgs, FQLPreprocessor preproc)
      {
         cacheNoArgs.put(keyNoArgs, preproc);
         // TODO: LOG this on FINE level
         // System.out.println("\t" + (unknownParams ? "? " : "  ") + "Put in cache: (" + key.where + ") -> {" +
         //    preproc.getFQL().toString().replace('\t', ' ').replace('\n', ' ').replace('\r', ' ')
         //    + "}");
      }
      
      /**
       * Insert an FQL preprocessor into the cache based on a key that does include
       * the arguments.
       * 
       * @param   keyNoArgs
       *          The cache key that does contain the arguments.
       * @param   preproc
       *          The preprocessor to be cached.
       */
      public void putCacheWithArgs(CacheKey keyWithArgs, FQLPreprocessor preproc)
      {
         cacheWithArgs.put(keyWithArgs, preproc);
         // TODO: LOG this on FINE level
         // System.out.println("\t" + (unknownParams ? "? " : "  ") + "Put in cache: (" + key.where + ") -> {" +
         //    preproc.getFQL().toString().replace('\t', ' ').replace('\n', ' ').replace('\r', ' ')
         //    + "}");
         
      }
   }
}