# 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', ' ')
             //    + "}");

          }
       }
    }