From 5c5b468178ed84a6680f36934f4f7b2d1d2e254e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gr=C3=A9gory=20Mantelet?=
 <gregory.mantelet@astro.unistra.fr>
Date: Tue, 30 Jul 2019 19:51:06 +0200
Subject: [PATCH] [ADQL] Make UDF optional features. A special LanguageFeature
 constructor must be used: `LanguageFeature(FunctionDef)`. This way, the
 syntax of the function definition string is checked and function name and
 parameters can be correctly extracted. These information are particularly
 important to determine at the ADQLParser level whether a DefaultUDF is
 supported or not while not knowing the parameters and function types.

---
 src/adql/db/FunctionDef.java                  |  49 +++++---
 src/adql/parser/feature/FeatureSet.java       |  30 ++++-
 src/adql/parser/feature/LanguageFeature.java  | 115 ++++++++++++++++--
 .../query/operand/function/DefaultUDF.java    |  45 ++++++-
 test/adql/db/TestDBChecker.java               |   6 +-
 test/adql/parser/feature/TestFeatureSet.java  |   5 +-
 6 files changed, 210 insertions(+), 40 deletions(-)

diff --git a/src/adql/db/FunctionDef.java b/src/adql/db/FunctionDef.java
index 55d9ef4..b1e27b1 100644
--- a/src/adql/db/FunctionDef.java
+++ b/src/adql/db/FunctionDef.java
@@ -15,7 +15,7 @@ package adql.db;
  *
  * You should have received a copy of the GNU Lesser General Public License
  * along with ADQLLibrary.  If not, see <http://www.gnu.org/licenses/>.
- *
+ * 
  * Copyright 2015-2020 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
  *                       Astronomisches Rechen Institut (ARI)
  */
@@ -26,6 +26,7 @@ import java.util.regex.Pattern;
 
 import adql.db.DBType.DBDatatype;
 import adql.parser.ParseException;
+import adql.parser.feature.LanguageFeature;
 import adql.query.operand.ADQLOperand;
 import adql.query.operand.function.ADQLFunction;
 import adql.query.operand.function.DefaultUDF;
@@ -51,10 +52,10 @@ import adql.query.operand.function.UserDefinedFunction;
  * 	A description of this function may be set thanks to the public class
  * 	attribute {@link #description}.
  * </p>
- *
+ * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
  * @version 2.0 (08/2020)
- *
+ * 
  * @since 1.3
  */
 public class FunctionDef implements Comparable<FunctionDef> {
@@ -194,12 +195,12 @@ public class FunctionDef implements Comparable<FunctionDef> {
 
 	/**
 	 * Translation to apply for this User Defined Function.
-	 *
+	 * 
 	 * <p>
 	 * 	It can be any string. Any <code>$i</code> substring (i being an integer
 	 * 	&gt;0) will be replaced by the corresponding function argument.
 	 * </p>
-	 *
+	 * 
 	 * <p>For instance, for the UDF signature</p>
 	 * <pre>foo(p1 VARCHAR, p2 DOUBLE)</pre>
 	 * <p>, the translation pattern</p>
@@ -287,13 +288,13 @@ public class FunctionDef implements Comparable<FunctionDef> {
 
 	/**
 	 * Create a function definition.
-	 *
+	 * 
 	 * <p>
 	 * 	The created function will have <b>no return type</b> and some parameters
 	 * 	(except if the given array is NULL or empty).
 	 * </p>
-	 *
-	 * @param fctName	Name of the function.
+	 * 
+	 * @param fctName		Name of the function.
 	 * @param params	Parameters of this function.
 	 *              	<i>If NULL or empty, this function will have no
 	 *              	parameter.</i>
@@ -367,14 +368,14 @@ public class FunctionDef implements Comparable<FunctionDef> {
 
 	/**
 	 * Tell whether this function returns an unknown type.
-	 *
+	 * 
 	 * <p>
 	 * 	If this function returns <code>true</code>, {@link #isNumeric()},
 	 * 	{@link #isString()} and {@link #isGeometry()} <b>MUST ALL</b> return
 	 * 	<code>false</code>. Otherwise, one of these 3 last functions MUST
 	 * 	return <code>true</code>.
-	 * </p>
-	 *
+	 * </p> 
+	 * 
 	 * @return	<code>true</code> if this function returns an unknown/unresolved
 	 *        	/unsupported type,
 	 *        	<code>false</code> otherwise.
@@ -414,7 +415,7 @@ public class FunctionDef implements Comparable<FunctionDef> {
 	/**
 	 * Get the class of the {@link UserDefinedFunction} able to represent the
 	 * function defined here in an ADQL tree.
-	 *
+	 * 
 	 * <p><i><b>Note:</b>
 	 * 	This getter should return always NULL if the function defined here is
 	 * 	not a user defined function.
@@ -426,7 +427,7 @@ public class FunctionDef implements Comparable<FunctionDef> {
 	 * 	instance) of the defined function has a different signature (e.g. a
 	 * 	different name) in the target language (e.g. SQL).
 	 * </i></p>
-	 *
+	 * 
 	 * @return	The corresponding {@link UserDefinedFunction}.
 	 *        	<i>MAY BE NULL</i>
 	 */
@@ -437,7 +438,7 @@ public class FunctionDef implements Comparable<FunctionDef> {
 	/**
 	 * Set the class of the {@link UserDefinedFunction} able to represent the
 	 * function defined here in an ADQL tree.
-	 *
+	 * 
 	 * <p><i><b>Note:</b>
 	 * 	If this {@link FunctionDef} defines an ordinary ADQL function - and not
 	 * 	a user defined function - no class should be set here.
@@ -450,12 +451,12 @@ public class FunctionDef implements Comparable<FunctionDef> {
 	 * 	will be created on the fly by the library when needed if it turns out
 	 * 	that no UDF class is set.
 	 * </i></p>
-	 *
+	 * 
 	 * <p><i><b>WARNING:</b>
 	 * 	If successful, this operation will reset to NULL any translation pattern
 	 * 	already set with {@link #setTranslationPattern(String)}.
 	 * </i></p>
-	 *
+	 * 
 	 * @param udfClass	Class to use to represent in an ADQL tree the User
 	 *                	Defined Function defined in this {@link FunctionDef}.
 	 *
@@ -479,7 +480,7 @@ public class FunctionDef implements Comparable<FunctionDef> {
 			// Set to NULL the translation pattern (if any):
 			this.translationPattern = null;
 
-		} catch(SecurityException e) {
+		}catch(SecurityException e){
 			throw new IllegalArgumentException("A security problem occurred while trying to get constructor from the class " + udfClass.getName() + ": " + e.getMessage());
 		} catch(NoSuchMethodException e) {
 			throw new IllegalArgumentException("The given class (" + udfClass.getName() + ") does not provide any constructor with a single parameter of type ADQLOperand[]!");
@@ -489,7 +490,7 @@ public class FunctionDef implements Comparable<FunctionDef> {
 	/**
 	 * Get the translation pattern to apply on any ADQL function implementing
 	 * this UDF definition.
-	 *
+	 * 
 	 * @return	The corresponding {@link UserDefinedFunction}.
 	 *        	<i>NULL if no translation pattern is defined.</i>
 	 *
@@ -724,6 +725,18 @@ public class FunctionDef implements Comparable<FunctionDef> {
 		}
 	}
 
+	/**
+	 * Create a {@link LanguageFeature} corresponding and linked to this
+	 * {@link FunctionDef}.
+	 *
+	 * @return	The corresponding LanguageFeature.
+	 *
+	 * @since 2.0
+	 */
+	public final LanguageFeature toLanguageFeature() {
+		return new LanguageFeature(this);
+	}
+
 	@Override
 	public String toString() {
 		return serializedForm;
diff --git a/src/adql/parser/feature/FeatureSet.java b/src/adql/parser/feature/FeatureSet.java
index dafea9a..04b7a61 100644
--- a/src/adql/parser/feature/FeatureSet.java
+++ b/src/adql/parser/feature/FeatureSet.java
@@ -19,6 +19,7 @@ package adql.parser.feature;
  * Copyright 2019 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
  */
 
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -26,6 +27,7 @@ import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Set;
 
+import adql.db.FunctionDef;
 import adql.query.operand.function.geometry.AreaFunction;
 import adql.query.operand.function.geometry.BoxFunction;
 import adql.query.operand.function.geometry.CentroidFunction;
@@ -550,6 +552,22 @@ public class FeatureSet implements Iterable<LanguageFeature> {
 		return getSupportedFeatures();
 	}
 
+	/**
+	 * Get the list of the definition of all declared UDFs.
+	 *
+	 * @return	List of all supported UDFs.
+	 */
+	public final Collection<FunctionDef> getSupportedUDFList() {
+		Set<LanguageFeature> supportedUDFs = supportedFeatures.get(LanguageFeature.TYPE_UDF);
+		if (supportedUDFs != null) {
+			Set<FunctionDef> definitions = new HashSet<FunctionDef>(supportedUDFs.size());
+			for(LanguageFeature feature : supportedUDFs)
+				definitions.add(feature.udfDefinition);
+			return definitions;
+		} else
+			return new HashSet<FunctionDef>(0);
+	}
+
 	/* **********************************************************************
 	   *                                                                    *
 	   *    ALL AVAILABLE FEATURES (according to the ADQL Language)         *
@@ -557,22 +575,22 @@ public class FeatureSet implements Iterable<LanguageFeature> {
 	   ********************************************************************** */
 
 	/*public static final LanguageFeature ILIKE = new LanguageFeature(FeatureType.ADQL_STRING, "ILIKE"); // TODO ILIKE
-	
+
 	public static final LanguageFeature UNION = new LanguageFeature(FeatureType.ADQL_SETS, "UNION"); // TODO UNION
 	public static final LanguageFeature EXCEPT = new LanguageFeature(FeatureType.ADQL_SETS, "EXCEPT"); // TODO EXCEPT
 	public static final LanguageFeature INTERSECT = new LanguageFeature(FeatureType.ADQL_SETS, "INTERSECT");  // TODO INTERSECT
-	
+
 	public static final LanguageFeature WITH = new LanguageFeature(FeatureType.ADQL_COMMON_TABLE, "WITH");  // TODO WITH
-	
+
 	public static final LanguageFeature CAST = new LanguageFeature(FeatureType.ADQL_TYPE, "CAST");  // TODO CAST
-	
+
 	public static final LanguageFeature IN_UNIT = new LanguageFeature(FeatureType.ADQL_UNIT, "IN_UNIT");  // TODO IN_UNIT
-	
+
 	public static final LanguageFeature BIT_AND = new LanguageFeature(FeatureType.ADQL_BITWISE, "BIT_AND");  // TODO BIT_AND
 	public static final LanguageFeature BIT_OR = new LanguageFeature(FeatureType.ADQL_BITWISE, "BIT_OR");  // TODO BIT_OR
 	public static final LanguageFeature BIT_XOR = new LanguageFeature(FeatureType.ADQL_BITWISE, "BIT_XOR");  // TODO BIT_XOR
 	public static final LanguageFeature BIT_NOT = new LanguageFeature(FeatureType.ADQL_BITWISE, "BIT_NOT");  // TODO BIT_NOT
-	
+
 	public static final LanguageFeature OFFSET = new LanguageFeature(FeatureType.ADQL_OFFSET, "OFFSET");  // TODO OFFSET*/
 
 	/** All standard features available.
diff --git a/src/adql/parser/feature/LanguageFeature.java b/src/adql/parser/feature/LanguageFeature.java
index f4c0a47..abca273 100644
--- a/src/adql/parser/feature/LanguageFeature.java
+++ b/src/adql/parser/feature/LanguageFeature.java
@@ -21,6 +21,8 @@ package adql.parser.feature;
 
 import java.util.Objects;
 
+import adql.db.FunctionDef;
+
 /**
  * Description of an ADQL's language feature.
  *
@@ -42,13 +44,20 @@ import java.util.Objects;
  * 	set to NULL.
  * </i></p>
  *
+ * <p><i><b>IMPORTANT note about UDF:</b>
+ * 	To create a UDF feature (i.e. a {@link LanguageFeature} with the type
+ * 	{@link #TYPE_UDF}), ONLY ONE constructor can be used:
+ * 	{@link #LanguageFeature(FunctionDef, String)}. Any attempt with another
+ * 	public constructor will fail.
+ * </i></p>
+ *
  * @author Gr&eacute;gory Mantelet (CDS)
  * @version 2.0 (07/2019)
  * @since 2.0
  *
  * @see FeatureSet
  */
-public class LanguageFeature {
+public final class LanguageFeature {
 
 	/** Unique identifier of this language feature.
 	 * <p><i><b>MANDATORY</b></i></p>
@@ -96,6 +105,10 @@ public class LanguageFeature {
 	 * </ul> */
 	public final String form;
 
+	/** Definition of the UDF represented by this {@link LanguageFeature}.
+	 * <p><i><b>OPTIONAL</b></i></p> */
+	public final FunctionDef udfDefinition;
+
 	/** Is this feature optional in the ADQL grammar?
 	 * <p><i><b>MANDATORY</b></i></p>
 	 * <p>
@@ -113,17 +126,18 @@ public class LanguageFeature {
 	public final boolean optional;
 
 	/** Description of this feature.
-	 * <p><i><b>OPTIONAL</b></i></p>
-	 * <p><i><b>Note:</b>
-	 * 	This field is generally set when declaring a User Defined Function
-	 * 	(UDF).
-	 * </i></p> */
-	public final String description;
+	 * <p><i><b>OPTIONAL</b></i></p> */
+	public String description;
 
 	/**
 	 * Create a <em>de-facto supported</em> (i.e. non-optional) language
 	 * feature.
 	 *
+	 * <p><i><b>IMPORTANT note:</b>
+	 * 	To create a UDF feature, DO NOT use this constructor.
+	 * 	You MUST use instead {@link #LanguageFeature(FunctionDef, String)}.
+	 * </i></p>
+	 *
 	 * @param type			[OPTIONAL] Category of the language feature.
 	 *            			<em>(see all static attributes starting with
 	 *            			<code>TYPE_</code>)</em>
@@ -139,6 +153,11 @@ public class LanguageFeature {
 	/**
 	 * Create a language feature.
 	 *
+	 * <p><i><b>IMPORTANT note:</b>
+	 * 	To create a UDF feature, DO NOT use this constructor.
+	 * 	You MUST use instead {@link #LanguageFeature(FunctionDef, String)}.
+	 * </i></p>
+	 *
 	 * @param type			[OPTIONAL] Category of the language feature.
 	 *            			<em>(see all static attributes starting with
 	 *            			<code>TYPE_</code>)</em>
@@ -158,6 +177,11 @@ public class LanguageFeature {
 	/**
 	 * Create a language feature.
 	 *
+	 * <p><i><b>IMPORTANT note:</b>
+	 * 	To create a UDF feature, DO NOT use this constructor.
+	 * 	You MUST use instead {@link #LanguageFeature(FunctionDef, String)}.
+	 * </i></p>
+	 *
 	 * @param type			[OPTIONAL] Category of the language feature.
 	 *            			<em>(see all static attributes starting with
 	 *            			<code>TYPE_</code>)</em>
@@ -172,12 +196,69 @@ public class LanguageFeature {
 	 * @throws NullPointerException	If given form is missing.
 	 */
 	public LanguageFeature(final String type, final String form, final boolean optional, final String description) throws NullPointerException {
+		this(type, form, null, optional, description);
+	}
+
+	/**
+	 * Create a UDF feature.
+	 *
+	 * @param udfDef		[REQUIRED] Detailed definition of the UDF feature.
+	 *
+	 * @throws NullPointerException	If given {@link FunctionDef} is missing.
+	 */
+	public LanguageFeature(final FunctionDef udfDef) throws NullPointerException {
+		this(udfDef, null);
+	}
+
+	/**
+	 * Create a UDF feature.
+	 *
+	 * @param udfDef		[REQUIRED] Detailed definition of the UDF feature.
+	 * @param description	[OPTIONAL] Description overwriting the description
+	 *                   	provided in the given {@link FunctionDef}.
+	 *                   	<em>If NULL, the description of the
+	 *                   	{@link FunctionDef} will be used. If empty string,
+	 *                   	no description will be set.</em>
+	 *
+	 * @throws NullPointerException	If given {@link FunctionDef} is missing.
+	 */
+	public LanguageFeature(final FunctionDef udfDef, final String description) throws NullPointerException {
+		this(LanguageFeature.TYPE_UDF, udfDef.toString(), udfDef, true, (description == null ? udfDef.description : (description.trim().isEmpty() ? null : description)));
+	}
+
+	/**
+	 * Create a language feature.
+	 *
+	 * <p><i><b>IMPORTANT note:</b>
+	 * 	To create a UDF feature, the parameter udfDef MUST be NON-NULL.
+	 * </i></p>
+	 *
+	 * @param type			[OPTIONAL] Category of the language feature.
+	 *            			<em>(see all static attributes starting with
+	 *            			<code>TYPE_</code>)</em>
+	 * @param form			[REQUIRED] Name (or function signature) of the
+	 *            			language feature.
+	 * @param udfDef		[REQUIRED if type=UDF] Detailed definition of the
+	 *              		UDF feature.
+	 * @param optional		[REQUIRED] <code>true</code> if the feature is by
+	 *                		default supported in the ADQL standard,
+	 *                		<code>false</code> if the ADQL client must declare
+	 *                		it as supported in order to use it.
+	 * @param description	[OPTIONAL] Description of this feature.
+	 *
+	 * @throws NullPointerException	If given form or udfDef is missing.
+	 */
+	private LanguageFeature(final String type, final String form, final FunctionDef udfDef, final boolean optional, final String description) throws NullPointerException {
 		this.type = (type == null || type.trim().isEmpty()) ? null : type.trim();
 
 		if (form == null || form.trim().isEmpty())
 			throw new NullPointerException("Missing form/name of the language feature to create!");
 		this.form = form.trim();
 
+		if (TYPE_UDF.equals(this.type) && udfDef == null)
+			throw new NullPointerException("Missing UDF definition! To declare a UDF feature, you MUST use the constructor LanguageFeature(FunctionDef, ...) with a non-NULL FunctionDef instance.");
+		this.udfDefinition = udfDef;
+
 		this.id = (this.type == null ? "" : this.type) + "!" + this.form;
 
 		this.optional = optional;
@@ -187,15 +268,25 @@ public class LanguageFeature {
 
 	@Override
 	public boolean equals(final Object obj) {
-		if ((obj != null) && (obj instanceof LanguageFeature))
-			return id.equals(((LanguageFeature)obj).id);
-		else
-			return false;
+		if ((obj != null) && (obj instanceof LanguageFeature)) {
+			// Equals IF SAME ID:
+			if (id.equals(((LanguageFeature)obj).id))
+				return true;
+			// If UDF, equals IF SAME NAME and SAME NB PARAMETERS:
+			else if (TYPE_UDF.equals(type) && type.equals(((LanguageFeature)obj).type)) {
+				FunctionDef udfDefinition2 = ((LanguageFeature)obj).udfDefinition;
+				return udfDefinition.name.equalsIgnoreCase(udfDefinition2.name) && (udfDefinition.nbParams == udfDefinition2.nbParams);
+			}
+		}
+		return false;
 	}
 
 	@Override
 	public int hashCode() {
-		return Objects.hash(id, form);
+		if (udfDefinition != null)
+			return Objects.hash(type, udfDefinition.name.toLowerCase(), udfDefinition.nbParams);
+		else
+			return Objects.hash(type, form, -1);
 	}
 
 	@Override
diff --git a/src/adql/query/operand/function/DefaultUDF.java b/src/adql/query/operand/function/DefaultUDF.java
index 01e2bec..b67b40d 100644
--- a/src/adql/query/operand/function/DefaultUDF.java
+++ b/src/adql/query/operand/function/DefaultUDF.java
@@ -22,7 +22,10 @@ import java.util.regex.Matcher;
  *                       Astronomisches Rechen Institut (ARI)
  */
 
+import adql.db.DBType;
+import adql.db.DBType.DBDatatype;
 import adql.db.FunctionDef;
+import adql.db.FunctionDef.FunctionParam;
 import adql.parser.feature.LanguageFeature;
 import adql.query.ADQLList;
 import adql.query.ADQLObject;
@@ -67,7 +70,7 @@ public final class DefaultUDF extends UserDefinedFunction {
 			for(ADQLOperand p : params)
 				parameters.add(p);
 		}
-		languageFeature = new LanguageFeature(LanguageFeature.TYPE_UDF, functionName + "(...)", true, null);
+		generateLanguageFeature();
 	}
 
 	/**
@@ -122,11 +125,49 @@ public final class DefaultUDF extends UserDefinedFunction {
 	 * @since 1.3
 	 */
 	public final void setDefinition(final FunctionDef def) throws IllegalArgumentException {
+		// Ensure the definition is compatible with this ADQL function:
 		if (def != null && (def.name == null || !functionName.equalsIgnoreCase(def.name)))
 			throw new IllegalArgumentException("The parsed function name (" + functionName + ") does not match to the name of the given UDF definition (" + def.name + ").");
 
+		// Set the new definition (may be NULL):
 		this.definition = def;
-		languageFeature = new LanguageFeature(LanguageFeature.TYPE_UDF, this.definition.toString(), true, this.definition.description);
+
+		// Update the Language Feature of this ADQL function:
+		// ...if no definition, generate a default LanguageFeature:
+		if (this.definition == null)
+			generateLanguageFeature();
+		// ...otherwise, use the definition to set the LanguageFeature:
+		else
+			languageFeature = new LanguageFeature(this.definition);
+	}
+
+	/**
+	 * Generate and set a default {@link LanguageFeature} for this ADQL
+	 * function.
+	 *
+	 * <p><i><b>Note:</b>
+	 * 	Knowing neither the parameters name nor their type, the generated
+	 * 	LanguageFeature will just set an unknown type and set a default
+	 * 	parameter name (index prefixed with `$`).
+	 * </i></p>
+	 *
+	 * @since 2.0
+	 */
+	private void generateLanguageFeature() {
+		// Create an unknown DBType:
+		DBType unknownType = new DBType(DBDatatype.UNKNOWN);
+		unknownType.type.setCustomType("type");
+
+		// Create the list of input parameters:
+		FunctionParam[] inputParams = new FunctionParam[parameters.size()];
+		for(int i = 1; i <= parameters.size(); i++)
+			inputParams[i - 1] = new FunctionParam("param" + i, unknownType);
+
+		// Create the Function Definition:
+		FunctionDef fctDef = new FunctionDef(functionName, unknownType, inputParams);
+
+		// Finally create the LanguageFeature:
+		languageFeature = new LanguageFeature(fctDef);
 	}
 
 	@Override
diff --git a/test/adql/db/TestDBChecker.java b/test/adql/db/TestDBChecker.java
index cb60297..72ca3dd 100644
--- a/test/adql/db/TestDBChecker.java
+++ b/test/adql/db/TestDBChecker.java
@@ -267,6 +267,7 @@ public class TestDBChecker {
 	public void testUDFManagement() {
 		// UNKNOWN FUNCTIONS ARE NOT ALLOWED:
 		ADQLParser parser = parserFactory.createParser();
+		parser.getSupportedFeatures().allowAnyUdf(true);
 		parser.setQueryChecker(new DBChecker(tables, new ArrayList<FunctionDef>(0)));
 
 		// Test with a simple ADQL query without unknown or user defined function:
@@ -291,6 +292,7 @@ public class TestDBChecker {
 		// DECLARE THE UDFs:
 		FunctionDef[] udfs = new FunctionDef[]{ new FunctionDef("toto", new DBType(DBDatatype.VARCHAR)), new FunctionDef("tata", new DBType(DBDatatype.INTEGER)) };
 		parser = parserFactory.createParser();
+		parser.getSupportedFeatures().allowAnyUdf(true);
 		parser.setQueryChecker(new DBChecker(tables, Arrays.asList(udfs)));
 
 		// Test again:
@@ -341,6 +343,7 @@ public class TestDBChecker {
 		udfs = new FunctionDef[]{ new FunctionDef("toto", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{ new FunctionParam("txt", new DBType(DBDatatype.VARCHAR)) }) };
 		udfs[0].setUDFClass(UDFToto.class);
 		parser = parserFactory.createParser();
+		parser.getSupportedFeatures().allowAnyUdf(true);
 		parser.setQueryChecker(new DBChecker(tables, Arrays.asList(udfs)));
 		try {
 			ADQLQuery query = parser.parseQuery("SELECT toto('blabla') FROM foo;");
@@ -374,6 +377,7 @@ public class TestDBChecker {
 		udfs = new FunctionDef[]{ new FunctionDef("toto", new DBType(DBDatatype.VARCHAR), new FunctionParam[]{ new FunctionParam("txt", new DBType(DBDatatype.VARCHAR)) }) };
 		udfs[0].setUDFClass(WrongUDFToto.class);
 		parser = parserFactory.createParser();
+		parser.getSupportedFeatures().allowAnyUdf(true);
 		parser.setQueryChecker(new DBChecker(tables, Arrays.asList(udfs)));
 		try {
 			parser.parseQuery("SELECT toto('blabla') FROM foo;");
@@ -893,7 +897,7 @@ public class TestDBChecker {
 	}
 
 	public static class UDFToto extends UserDefinedFunction {
-		private LanguageFeature FEATURE = new LanguageFeature(LanguageFeature.TYPE_UDF, getName() + "(VARCHAR) -> VARCHAR");
+		private LanguageFeature FEATURE = (new FunctionDef(getName(), new DBType(DBDatatype.VARCHAR), new FunctionParam[]{ new FunctionParam("txt", new DBType(DBDatatype.VARCHAR)) })).toLanguageFeature();
 
 		protected StringConstant fakeParam;
 
diff --git a/test/adql/parser/feature/TestFeatureSet.java b/test/adql/parser/feature/TestFeatureSet.java
index e58fb75..3d256aa 100644
--- a/test/adql/parser/feature/TestFeatureSet.java
+++ b/test/adql/parser/feature/TestFeatureSet.java
@@ -11,6 +11,9 @@ import java.util.Set;
 
 import org.junit.Test;
 
+import adql.db.DBType;
+import adql.db.DBType.DBDatatype;
+import adql.db.FunctionDef;
 import adql.query.ColumnReference;
 import adql.query.operand.function.geometry.BoxFunction;
 import adql.query.operand.function.geometry.PolygonFunction;
@@ -217,7 +220,7 @@ public class TestFeatureSet {
 
 		/* here is a custom Language Feature (i.e. not part of the
 		 * availableFeatures list): */
-		set.support(new LanguageFeature(LanguageFeature.TYPE_UDF, "foo(VARCHAR) -> BOOLEAN", true));
+		set.support(new LanguageFeature(new FunctionDef("foo", new DBType(DBDatatype.SMALLINT), new FunctionDef.FunctionParam[]{ new FunctionDef.FunctionParam("", new DBType(DBDatatype.VARCHAR)) })));
 
 		// unsupport all currently supported features:
 		set.unsupportAll();
-- 
GitLab