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., >, >=, <, <=, 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 (>, >=, <, <=, 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 (>, >=, <, <=, 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 --> {condition}</li>
* <li>{condition} or true --> true</li>
* <li>{condition} and true --> {condition}</li>
* <li>{condition} and false --> 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', ' ')
// + "}");
}
}
}