# DataTypeHelper.java

    /*
    ** Module   : DataTypeHelper.java
    ** Abstract : helper to manage data types of objects referenced by HQL
    **
    ** Copyright (c) 2004-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
    ** 001 ECF 20080310   @37492 Created initial version. Provides static helper methods to determine
    **                           the data types of various constructs found within HQL.
    ** 002 ECF 20080609   @38681 Fixed initBasicTokens(). Added STRING/TYPE constant mapping to
    **                          tokens map.
    ** 003 ECF 20080804   @39310 Added rowid support. Map Long and rowid classes, and long primitive,
    **                           to HQLTypes.LONG enum.
    ** 004 SVL 20090225   @41366 Added recid support (mapped to HQLTypes.INTEGER).
    ** 005 GES 20090518   @42391 Import change.
    ** 006 ECF 20090817   @43683 Added persistence *Field types to classes map.
    ** 007 SVL 20120331          Upgraded to Hibernate 4.
    ** 008 OM  20130501          Added support for 64 bit data types.
    ** 009 OM  20130527          Added mapping for Timestamp.class as HQLTypes.DATETIME and
    **                           Object[] for HQLTypes.DATETIMETZ (don't know if this is correct).
    **                           For the moment this is disabled (1584#150).
    ** 010 VMN 20141127          Renamed DatetimeTzField.class to DatetimetzField.class.
    ** 011 CA  20151025          Registered the handle.class type.
    ** 012 OM  20151130          Added HQLFunction support in initFunction() method.
    ** 013 ECF 20160114          Removed references to obsolete *Field classes.
    ** 014 ECF 20160226          Changes required by new PropertyHelper implementation.
    ** 015 CA  20160226          In a ternary, if 'then' expression is unknown, then resolve the
    **                           datatype from the 'else' expresson.
    ** 016 ECF 20160720          Fixed buffer lookup during HQL preprocessing.
    ** 017 CA  20180517          Added comhandle data type.
    ** 018 OM  20190202          Added support for object data type.
    ** 019 CA  20190722          Fixed datetimetz query arguments - they are treated like a ISO date,
    **                           in string representation. UDF are used to compare them.
    ** 020 CA  20200610          Initialize this class, as can be used outside of the FWD environment, for
    **                           appserver clients.
    ** 021 CA  20200927          Use IdentityHashMap instead of plain map when the key is a Class.
    ** 022 IAS 20201123          Added ARRAY type
    **     IAS 20220926          Process extra parentheses in the expressionType() method.
    **     OM  20221103          New class names for FQLPreprocessor, FQLExpression, FQLBundle, and FQLCache.
    ** 023 AL2 20230210          Allow permissive look-up buffer (without exception of fail).
    ** 024 CA  20240331          Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime, so
    **                           logical type checks must include sub-classes, too.
    ** 025 DDF 20240410          Added ValueTimestampTimeZone H2 type for FqlType.DATETIMETZ when
    **                           initializing classes.
    ** 026 SP  20240516          Process the function name in getTypeFunction() when there is an underscore
    ** 027 CA  20241107          Added character constant support.
    ** 028 ICP 20250123          Added support for integer and int64 constants.
    */

    /*
    ** 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.hql;

    import java.lang.reflect.*;
    import java.math.*;
    import java.sql.*;
    import java.sql.Date;
    import java.sql.Timestamp;
    import java.util.*;
    import com.goldencode.ast.*;
    import com.goldencode.p2j.persist.*;
    import com.goldencode.p2j.persist.pl.*;
    import com.goldencode.p2j.util.*;
    import org.h2.api.*;
    import org.h2.value.ValueTimestampTimeZone;

    /**
     * A helper to manage data types of objects referenced by FQL statements
     * (specifically, where clauses).  This class provides static helper methods
     * to determine the data type of:
     * <ul>
     *   <li>certain non-volatile tokens (i.e., constants or other token types
     *       whose data types cannot change based on other factors)
     *   <li>standard functions, such as <code>upper()</code>
     *   <li>user defined functions, such as all the built-in functions migrated
     *       from Progress
     *   <li>query substitution parameters
     *   <li>qualified and unqualified references to DMO properties
     * </ul>
     * <p>
     * The {@link FqlType} enum is used to rationalize all the various type
     * constructs (token types, Java class types, Hibernate types) into a common
     * construct.  Any request for type information will be in terms of an
     * <code>HQLTypes</code> enum value.
     */
    public final class DataTypeHelper
    implements HQLParserTokenTypes
    {
       /** Map of token types to HQL types */
       private static final Map<Integer, FqlType> tokens = new HashMap<>();

       /** Map of functions to HQL types */
       private static final Map<FunctionKey, FqlType> functions = new HashMap<>();

       /** Map of Java class types to HQL types */
       private static final Map<Class<?>, FqlType> classes = new IdentityHashMap<>();

       /** Map of SQL types to BDT classes */
       private static final Map<Integer, Class<? extends BaseDataType>> bdts = new HashMap<>();

       static
       {
          initialize();
       }

       /**
        * Private constructor;  access is via static methods.
        */
       private DataTypeHelper()
       {
       }

       /**
        * Initialize the data structures needed to enable this helper.  This
        * should be invoked early during initialization of the persistence
        * runtime.
        */
       public static void initialize()
       {
          // Initialize Java data type wrapper mappings.
          initJavaClasses();

          // Initialize basic token type mappings.
          initBasicTokens();

          // Initialize standard built-in function return type mappings.
          initFunctions();
       }

       /**
        * Get the FQL type associated with a particular Java class.
        *
        * @param   javaType
        *          A Java class.
        *
        * @return  The FQL type associated with the given class, or {@link
        *          FqlType#UNKNOWN} if no mapping is available.
        */
       public static FqlType getTypeClass(Class<?> javaType)
       {
          FqlType type = classes.get(javaType);

          return (type != null) ? type : FqlType.UNKNOWN;
       }

       public static Class<? extends BaseDataType> getBdtClass(int sqlType)
       {
          return bdts.get(sqlType);
       }

       /**
        * Get the FQL type associated with a particular token type.  Only those
        * token types whose data type cannot vary (i.e., constants and certain
        * operators) are handled by this method.
        *
        * @param   tokenType
        *          A token type from {@link HQLParserTokenTypes}.
        *
        * @return  The FQL type associated with the given token type, or {@link
        *          FqlType#UNKNOWN} if no mapping is available.
        */
       public static FqlType getTypeToken(int tokenType)
       {
          FqlType type = tokens.get(tokenType);

          return (type != null) ? type : FqlType.UNKNOWN;
       }

       /**
        * Get the FQL type associated with a particular {@link FunctionKey}.
        *
        * @param   key
        *          A <code>FunctionKey</code> which describes a standard function
        *          or a built-in function.
        *
        * @return  The FQL type associated with the given function key, or {@link
        *          FqlType#UNKNOWN} if no mapping is available.
        */
       public static FqlType getTypeFunction(FunctionKey key)
       {
          FqlType type = functions.get(key);

          return (type != null) ? type : FqlType.UNKNOWN;
       }

       /**
        * Get the FQL type associated with a particular, qualified DMO property,
        * as represented by the given AST.
        *
        * @param   aliasAst
        *          An AST which represents a DMO alias, whose first (and only)
        *          child represents a property of that DMO.
        * @param   bufferMap
        *          A map of DMO aliases to their corresponding record buffers, where {@code null}
        *          is mapped to the buffer whose property references in the FQL expression are
        *          unqualified (if any).
        *
        * @return  The FQL type associated with the given property, or {@link
        *          FqlType#UNKNOWN} if no mapping is available.
        *
        * @throws  PersistenceException
        *          if the property does not exist, or if there is some error
        *          determining its data type.
        */
       public static FqlType getTypeQualifiedProperty(Aast aliasAst,
                                                      Map<String, RecordBuffer> bufferMap)
       throws PersistenceException
       {
          return getTypeClass(getJavaTypeQualifiedProperty(aliasAst, bufferMap));
       }

       /**
        * Get the FQL type associated with a particular, unqualified DMO property,
        * as represented by the given AST.
        *
        * @param   propAst
        *          An AST which represents a property of a DMO.
        * @param   bufferMap
        *          A map of DMO aliases to their corresponding record buffers, where {@code null}
        *          is mapped to the buffer whose property references in the FQL expression are
        *          unqualified (if any).
        *
        * @return  The FQL type associated with the given property, or {@link
        *          FqlType#UNKNOWN} if no mapping is available.
        *
        * @throws  PersistenceException
        *          if the property does not exist, or if there is some error
        *          determining its data type.
        */
       public static FqlType getTypeUnqualifiedProperty(Aast propAst,
                                                        Map<String, RecordBuffer> bufferMap)
       throws PersistenceException
       {
          return getTypeClass(getJavaTypeUnqualifiedProperty(propAst, bufferMap));
       }

       /**
        * Get the Java type associated with a particular, qualified DMO property, as represented by the given AST.
        *
        * @param   aliasAst
        *          An AST which represents a DMO alias, whose first (and only)
        *          child represents a property of that DMO.
        * @param   bufferMap
        *          A map of DMO aliases to their corresponding record buffers, where {@code null} is mapped to the
        *          buffer whose property references in the FQL expression are unqualified (if any).
        *
        * @return  The Java type associated with the given property.
        *
        * @throws  PersistenceException
        *          if the property does not exist, or if there is some error
        *          determining its data type.
        */
       public static Class<?> getJavaTypeQualifiedProperty(Aast aliasAst, Map<String, RecordBuffer> bufferMap)
       throws PersistenceException
       {
          Aast propAst = aliasAst.getChildAt(0);
          String property = propAst.getText();
          RecordBuffer buffer = lookupBuffer(aliasAst, bufferMap, null);
          Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
          Class<?> javaType = DBUtils.getDMOPropertyType(dmoIface, property);

          return javaType;
       }

       /**
        * Get the Java type associated with a particular, unqualified DMO property, as represented by the given AST.
        *
        * @param   propAst
        *          An AST which represents a property of a DMO.
        * @param   bufferMap
        *          A map of DMO aliases to their corresponding record buffers, where {@code null} is mapped to the
        *          buffer whose property references in the FQL expression are unqualified (if any).
        *
        * @return  The Java type associated with the given property.
        *
        * @throws  PersistenceException
        *          if the property does not exist, or if there is some error
        *          determining its data type.
        */
       public static Class<?> getJavaTypeUnqualifiedProperty(Aast propAst, Map<String, RecordBuffer> bufferMap)
       throws PersistenceException
       {
          Aast parent = propAst.getParent();
          Aast aliasAst = parent != null && parent.getType() == ALIAS ? parent : null;
          RecordBuffer buffer = lookupBuffer(aliasAst, bufferMap, null);
          Class<? extends DataModelObject> dmoIface = buffer.getDMOInterface();
          String property = propAst.getText();
          Class<?> javaType = DBUtils.getDMOPropertyType(dmoIface, property);

          return javaType;
       }

       /**
        * Determine the FQL type of the expression represented by the given AST node, which may be
        * a single leaf node, or the head of a more complex expression.
        *
        * @param   node
        *          AST node whose FQL type is to be determined.
        * @param   bufferMap
        *          A map of DMO aliases to their corresponding record buffers, where {@code null}
        *          is mapped to the buffer whose property references in the FQL expression are
        *          unqualified (if any).
        *
        * @return  The FQL type which represents the data type to which the given
        *          expression would evaluate.
        *
        * @throws  PersistenceException
        *          if a DMO property referenced within the expression does not
        *          exist, or if there is some error determining its data type.
        */
       public static FqlType expressionType(Aast node, Map<String, RecordBuffer> bufferMap)
       throws PersistenceException
       {
          FqlType c0;
          FqlType c1;

          int tokType = node.getType();
          switch (tokType)
          {
             case ALIAS:
                return getTypeQualifiedProperty(node, bufferMap);

             case PROPERTY:
                return getTypeUnqualifiedProperty(node, bufferMap);

             case FUNCTION:
                return getTypeFunction(node, bufferMap);

             case SUBST:
                return FqlType.valueOf((String) node.getAnnotation("datatype"));

             case TERNARY:
                c0 = expressionType(node.getChildAt(1), bufferMap); // case...then
                c1 = expressionType(node.getChildAt(2), bufferMap); // case...else
                if (c0 == FqlType.DECIMAL || c1 == FqlType.DECIMAL)
                {
                   return FqlType.DECIMAL;
                }
                return (c0 == FqlType.LONG || c1 == FqlType.LONG)
                         ? FqlType.LONG
                         : (c0 == FqlType.UNKNOWN ? c1 : c0);

             case PLUS:
             case MINUS:
             case MULTIPLY:
             case DIVIDE:
                c0 = expressionType(node.getChildAt(0), bufferMap);  // lvalue
                c1 = expressionType(node.getChildAt(1), bufferMap);  // rvalue
                if (c0 == FqlType.DECIMAL || c1 == FqlType.DECIMAL)
                {
                   return FqlType.DECIMAL;
                }
                return (c0 == FqlType.LONG || c1 == FqlType.LONG)
                       ? FqlType.LONG
                       : FqlType.INTEGER;

             case UN_MINUS:
                return expressionType(node.getChildAt(0), bufferMap);

             case LPARENS:
                if (node.getNumImmediateChildren() == 1)
                {
                   return expressionType((Aast)node.getFirstChild(), bufferMap);
                }
                // fall-through if none or more than one childs
             default:
                return getTypeToken(tokType);
          }
       }

       /**
        * Attempt to look up a record buffer by alias from the given map. The DMO alias is extracted
        * from the {@code alias} AST. If there is no matching entry in the buffer map, an exception
        * is thrown.
        * <p>
        * The returned buffer is initialized by side effect.
        *
        * @param   alias
        *          DMO alias AST. If {@code null}, a {@code null} lookup key is used.
        * @param   bufferMap
        *          A map of DMO aliases to record buffers. If no alias is provided (e.g., for an
        *          unqualified property reference, the appropriate buffer is expected to be mapped
        *          to the {@code null} key.
        * @param   fql
        *          Optional FQL expression from which the alias is parsed.
        *
        * @return  A record buffer matching the given alias.
        */
       public static RecordBuffer lookupBuffer(Aast alias,
                                               Map<String, RecordBuffer> bufferMap,
                                               FQLExpression fql)
       {
          String aliasName = alias != null ? alias.getText() : null;
          RecordBuffer rb = lookupBuffer(aliasName, bufferMap, fql);
          if (rb == null)
          {
             throw new IllegalArgumentException("Unrecognized buffer at column " +
                (alias != null ? alias.getColumn() : "<unknown>") +
                " in where clause:  '" +
                aliasName +
                "' [" +
                (fql != null ? fql.toString() : "<unavailable>") +
                "]"
             );
          };
          return rb;
       }

       /**
        * Attempt to look up a record buffer by alias from the given map. The DMO alias is extracted
        * from the {@code alias} AST. If there is no matching entry in the buffer map, an exception
        * is thrown.
        * <p>
        * The returned buffer is initialized by side effect.
        *
        * @param   aliasName
        *          Alias for DMO. If {@code null}, a {@code null} lookup key is used.
        * @param   bufferMap
        *          A map of DMO aliases to record buffers. If no alias is provided (e.g., for an
        *          unqualified property reference, the appropriate buffer is expected to be mapped
        *          to the {@code null} key.
        * @param   fql
        *          Optional FQL expression from which the alias is parsed.
        *
        * @return  A record buffer matching the given alias.
        */
       public static RecordBuffer lookupBuffer(String aliasName,
                                                Map<String, RecordBuffer> bufferMap,
                                                FQLExpression fql)
       {
          RecordBuffer buffer = bufferMap.get(aliasName);

          if (buffer == null)
          {
             return null;
          }

          buffer.initialize();

          return buffer;
       }

       /**
        * Get the FQL type which will be returned when the given function is executed.
        *
        * @param   function
        *          AST node representing a standard or built-in function expression.
        * @param   bufferMap
        *          A map of DMO aliases to their corresponding record buffers, where {@code null}
        *          is mapped to the buffer whose property references in the FQL expression are
        *          unqualified (if any).
        *
        * @return  The FQL type associated with the given function, or {@link FqlType#UNKNOWN} if
        *          no mapping is available.
        *
        * @throws  PersistenceException
        *          if a DMO property referenced within the expression does not exist, or if there
        *          is some error determining its data type.
        */
       private static FqlType getTypeFunction(Aast function, Map<String, RecordBuffer> bufferMap)
       throws PersistenceException
       {
          String name = function.getText();
          int k = name.indexOf('_');
          if (k > 0)
          {
             name = name.substring(0, k);
          }
          int len = function.getNumImmediateChildren();
          FqlType[] fqlSig = new FqlType[len];
          int i = 0;
          Aast child = (Aast) function.getFirstChild();
          while (child != null)
          {
             fqlSig[i++] = expressionType(child, bufferMap);
             child = (Aast) child.getNextSibling();
          }
          FunctionKey key = new FunctionKey(name, fqlSig);

          return getTypeFunction(key);
       }

       /**
        * Initialize the mapping of Java classes to {@link FqlType} for those
        * classes which are known to be used in FQL where clauses.
        */
       private static void initJavaClasses()
       {
          // Java primitive types.
          classes.put(Boolean.TYPE     , FqlType.BOOLEAN);
          classes.put(Integer.TYPE     , FqlType.INTEGER);
          classes.put(Long.TYPE        , FqlType.LONG);
          classes.put(Double.TYPE      , FqlType.DOUBLE);

          // Standard Java wrapper types.
          classes.put(String.class     , FqlType.TEXT);
          classes.put(Boolean.class    , FqlType.BOOLEAN);
          classes.put(Integer.class    , FqlType.INTEGER);
          classes.put(Long.class       , FqlType.LONG);
          classes.put(BigDecimal.class , FqlType.DECIMAL);
          classes.put(Date.class       , FqlType.DATE);
          classes.put(Timestamp.class  , FqlType.DATETIME);
          classes.put(TimestampWithTimeZone.class, FqlType.DATETIMETZ);
          classes.put(ValueTimestampTimeZone.class, FqlType.DATETIMETZ);

          classes.put(String[].class   , FqlType.ARRAY);
          //classes.put(Object[].class   , FqlTypes.DATETIMETZ); // datetimetz is represented by a 2-uple (Timestamp, Offset)

          // P2J wrapper types.
          classes.put(logical.class    , FqlType.BOOLEAN);
          classes.put(integer.class    , FqlType.INTEGER);
          classes.put(recid.class      , FqlType.INTEGER);
          classes.put(decimal.class    , FqlType.DECIMAL);
          classes.put(date.class       , FqlType.DATE);
          classes.put(raw.class        , FqlType.RAW);
          classes.put(character.class  , FqlType.TEXT);
          classes.put(rowid.class      , FqlType.LONG);
          classes.put(int64.class      , FqlType.LONG);
          classes.put(datetime.class   , FqlType.DATETIME);
          classes.put(datetimetz.class , FqlType.DATETIMETZ);
          classes.put(handle.class     , FqlType.TEXT);
          classes.put(comhandle.class  , FqlType.TEXT);
          classes.put(object.class     , FqlType.TEXT);
          classes.put(ObjectVar.class  , FqlType.TEXT);

          // BDT constants
          classes.put(logical.logicalConstant.class, FqlType.BOOLEAN);
          classes.put(character.characterConstant.class, FqlType.TEXT);
          classes.put(integer.integerConstant.class, FqlType.INTEGER);
          classes.put(int64.int64Constant.class, FqlType.LONG);

          bdts.put(Types.BOOLEAN,                   logical.class);
          bdts.put(Types.INTEGER,                   integer.class);
    //      bdts.put(Types.?,     recid.class);
          bdts.put(Types.DECIMAL,                   decimal.class);
          bdts.put(Types.DATE,                      date.class);
    //      bdts.put(Types.?,     raw.class);
          bdts.put(Types.VARCHAR,                   character.class);
    //      bdts.put(Types.?,     rowid.class);
          bdts.put(Types.BIGINT,                    int64.class);
          bdts.put(Types.TIMESTAMP,                 datetime.class);
          bdts.put(Types.TIMESTAMP_WITH_TIMEZONE,   datetimetz.class);
    //      bdts.put(Types.?,     handle.class);
    //      bdts.put(Types.?,     comhandle.class);
    //      bdts.put(Types.?,     object.class);
    //      bdts.put(Types.?,     ObjectVar.class);
       }

       /**
        * Initialize the mapping of basic token types to {@link FqlType}.  Only
        * those token types whose data types will not change (i.e., constants and
        * certain operators) are mapped here.
        */
       private static void initBasicTokens()
       {
          tokens.put(BOOL_TRUE   , FqlType.BOOLEAN);
          tokens.put(BOOL_FALSE  , FqlType.BOOLEAN);
          tokens.put(IS_NULL     , FqlType.BOOLEAN);
          tokens.put(NOT_NULL    , FqlType.BOOLEAN);
          tokens.put(EQUALS      , FqlType.BOOLEAN);
          tokens.put(NOT_EQ      , FqlType.BOOLEAN);
          tokens.put(GT          , FqlType.BOOLEAN);
          tokens.put(LT          , FqlType.BOOLEAN);
          tokens.put(LTE         , FqlType.BOOLEAN);
          tokens.put(GTE         , FqlType.BOOLEAN);
          tokens.put(AND         , FqlType.BOOLEAN);
          tokens.put(OR          , FqlType.BOOLEAN);
          tokens.put(NOT         , FqlType.BOOLEAN);
          tokens.put(NUM_LITERAL , FqlType.INTEGER);
          tokens.put(LONG_LITERAL, FqlType.LONG);
          tokens.put(DEC_LITERAL , FqlType.DECIMAL);
          tokens.put(STRING      , FqlType.TEXT);
       }

       /**
        * Initialize the mappings of {@link FunctionKey} to {@link FqlType} for
        * all standard functions used in FQL (including built-in functions which
        * emulate Progress functionality).
        */
       private static void initFunctions()
       {
          initFunction("upper", FqlType.TEXT, FqlType.TEXT);

          for (Method method : BuiltIns.getStandardFunctions())
          {
             initFunction(method);
          }
       }

       /**
        * Given a Java method which represents a user-defined function accessible
        * from FQL, create a mapping of {@link FunctionKey} to {@link FqlType} for that function.
        *
        * @param   method
        *          Java method to be mapped.
        */
       private static void initFunction(Method method)
       {
          HQLFunction hqlFunction = method.getAnnotation(HQLFunction.class);
          if (hqlFunction == null)
          {
             throw new IllegalArgumentException(
                   "Method " + method + " is not backing a hql function");
          }
          String name = hqlFunction.name();
          // use the annotation to obtain the correct function name generated in the code
          if (name.isEmpty())
          {
             name = method.getName();
             // if not explicitly set, it matches the name of method
          }
          Class<?> returns = method.getReturnType();
          FqlType hqlReturns = classes.get(returns);

          if (hqlReturns == null)
          {
             throw new IllegalArgumentException(
                "No HQL type found for user defined function return type " + returns.getName());
          }

          Class<?>[] sig = method.getParameterTypes();
          FqlType[] hqlSig = new FqlType[sig.length];
          for (int i = 0; i < sig.length; i++)
          {
             Class<?> next = sig[i];
             FqlType hqlParam = classes.get(next);
             if (hqlParam == null)
             {
                throw new IllegalArgumentException(
                   "No HQL type found for user defined function parameter type " + next.getName());
             }
             hqlSig[i] = hqlParam;
          }

          FunctionKey key = new FunctionKey(name, hqlSig);
          functions.put(key, hqlReturns);
       }

       /**
        * Given the name and signature of a user-defined function accessible from
        * HQL, create a mapping of {@link FunctionKey} to {@link FqlType} for that function.
        *
        * @param   name
        *          Function name
        * @param   returns
        *          Data type that the function returns.
        * @param   paramTypes
        *          Data types of function parameters, if any.
        */
       private static void initFunction(String name, FqlType returns, FqlType... paramTypes)
       {
          FunctionKey key = new FunctionKey(name, paramTypes);
          functions.put(key, returns);
       }
    }