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);
   }
}