From 37e26a115e9a85088e281657e30b78f5fdb45866 Mon Sep 17 00:00:00 2001
From: gmantele <gmantele@ari.uni-heidelberg.de>
Date: Mon, 20 Jul 2015 17:19:19 +0200
Subject: [PATCH] [ADQL,TAP] Add the ability to declare columns and UDF with an
 UNKNOWN type. (merge with branch 'unknownFctType')

---
 src/adql/db/DBType.java                       | 135 +++++++++++++++++-
 src/adql/db/FunctionDef.java                  |  66 ++++++---
 src/adql/translator/PgSphereTranslator.java   |   6 +-
 src/adql/translator/PostgreSQLTranslator.java |   8 +-
 src/tap/data/ResultSetTableIterator.java      |  18 +--
 src/tap/data/VOTableIterator.java             |   8 +-
 src/tap/db/JDBCConnection.java                |   4 +-
 src/tap/formatter/JSONFormat.java             |  11 +-
 src/tap/formatter/VOTableFormat.java          |  11 +-
 src/tap/metadata/TAPColumn.java               |  10 +-
 test/adql/db/TestFunctionDef.java             |  50 ++++---
 test/adql/parser/UnknownTypes.java            | 118 +++++++++++++++
 12 files changed, 370 insertions(+), 75 deletions(-)
 create mode 100644 test/adql/parser/UnknownTypes.java

diff --git a/src/adql/db/DBType.java b/src/adql/db/DBType.java
index 87f6c05..18a09e4 100644
--- a/src/adql/db/DBType.java
+++ b/src/adql/db/DBType.java
@@ -16,7 +16,7 @@ package adql.db;
  * You should have received a copy of the GNU Lesser General Public License
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
- * Copyright 2014 - Astronomisches Rechen Institut (ARI)
+ * Copyright 2014-2015 - Astronomisches Rechen Institut (ARI)
  */
 
 /**
@@ -32,7 +32,7 @@ package adql.db;
  * It is used to set the attribute type/datatype of this class.</p>
  *  
  * @author Gr&eacute;gory Mantelet (ARI)
- * @version 1.3 (10/2014)
+ * @version 1.4 (07/2015)
  * @since 1.3
  */
 public class DBType {
@@ -41,11 +41,40 @@ public class DBType {
 	 * List of all datatypes declared in the IVOA recommendation of TAP (in the section UPLOAD).
 	 * 
 	 * @author Gr&eacute;gory Mantelet (ARI)
-	 * @version 1.3 (10/2014)
+	 * @version 1.4 (06/2015)
 	 * @since 1.3
 	 */
 	public static enum DBDatatype{
-		SMALLINT, INTEGER, BIGINT, REAL, DOUBLE, BINARY, VARBINARY, CHAR, VARCHAR, BLOB, CLOB, TIMESTAMP, POINT, REGION;
+		SMALLINT, INTEGER, BIGINT, REAL, DOUBLE, BINARY, VARBINARY, CHAR, VARCHAR, BLOB, CLOB, TIMESTAMP, POINT, REGION,
+		/** @since 1.4 */
+		UNKNOWN;
+
+		/** String to return when {@link #toString()} is called.
+		 * @since 1.4*/
+		private String strExp = this.name();
+
+		@Override
+		public String toString(){
+			return strExp;
+		}
+
+		/**
+		 * <p>This function lets define the name of the type as provided
+		 * <b>ONLY FOR {@link #UNKNOWN} {@link DBDatatype}</b>.</p>
+		 * 
+		 * <p><i><b>Important:</b>
+		 * 	If this {@link DBDatatype} is not {@link #UNKNOWN} or if the given name is NULL or empty,
+		 * 	this function has no effect.
+		 * </i></p>
+		 * 
+		 * @param typeName	User type name.
+		 * 
+		 * @since 1.4
+		 */
+		public void setCustomType(final String typeName){
+			if (this == UNKNOWN && typeName != null && typeName.trim().length() > 0)
+				strExp = "?" + typeName.trim() + "?";
+		}
 	}
 
 	/** Special value in case no length/size is specified. */
@@ -79,6 +108,23 @@ public class DBType {
 		this.length = length;
 	}
 
+	/**
+	 * <p>Tells whether this type is a numeric.</p>
+	 * 
+	 * <p><i>Concerned types:
+	 * 	{@link DBDatatype#SMALLINT SMALLINT}, {@link DBDatatype#INTEGER INTEGER}, {@link DBDatatype#BIGINT BIGINT},
+	 * 	{@link DBDatatype#REAL REAL}, {@link DBDatatype#DOUBLE DOUBLE}, {@link DBDatatype#BINARY BINARY},
+	 * 	{@link DBDatatype#VARBINARY VARBINARY} and {@link DBDatatype#BLOB BLOB}.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note</b>:
+	 * 	Since {@link DBDatatype#UNKNOWN UNKNOWN} is an unresolved type, it can potentially be anything.
+	 * 	That's why, this function will also returned <code>true</code> if the type is
+	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
+	 * </i></p>
+	 * 
+	 * @return	<code>true</code> if this type is a numeric, <code>false</code> otherwise.
+	 */
 	public boolean isNumeric(){
 		switch(type){
 			case SMALLINT:
@@ -91,42 +137,121 @@ public class DBType {
 			case BINARY:
 			case VARBINARY:
 			case BLOB:
+			case UNKNOWN:
 				return true;
 			default:
 				return false;
 		}
 	}
 
+	/**
+	 * <p>Tells whether this type is a list of bytes.</p>
+	 * 
+	 * <p><i>Concerned types:
+	 * 	{@link DBDatatype#BINARY BINARY}, {@link DBDatatype#VARBINARY VARBINARY} and {@link DBDatatype#BLOB BLOB}.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note</b>:
+	 * 	Since {@link DBDatatype#UNKNOWN UNKNOWN} is an unresolved type, it can potentially be anything.
+	 * 	That's why, this function will also returned <code>true</code> if the type is
+	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
+	 * </i></p>
+	 * 
+	 * @return	<code>true</code> if this type is a binary, <code>false</code> otherwise.
+	 */
 	public boolean isBinary(){
 		switch(type){
 			case BINARY:
 			case VARBINARY:
 			case BLOB:
+			case UNKNOWN:
 				return true;
 			default:
 				return false;
 		}
 	}
 
+	/**
+	 * <p>Tells whether this type is about characters.</p>
+	 * 
+	 * <p><i>Concerned types:
+	 * 	{@link DBDatatype#CHAR CHAR}, {@link DBDatatype#VARCHAR VARCHAR}, {@link DBDatatype#CLOB CLOB}
+	 * 	and {@link DBDatatype#TIMESTAMP TIMESTAMP}.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note</b>:
+	 * 	Since {@link DBDatatype#UNKNOWN UNKNOWN} is an unresolved type, it can potentially be anything.
+	 * 	That's why, this function will also returned <code>true</code> if the type is
+	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
+	 * </i></p>
+	 * 
+	 * @return	<code>true</code> if this type is a string, <code>false</code> otherwise.
+	 */
 	public boolean isString(){
 		switch(type){
 			case CHAR:
 			case VARCHAR:
 			case CLOB:
 			case TIMESTAMP:
+			case UNKNOWN:
 				return true;
 			default:
 				return false;
 		}
 	}
 
+	/**
+	 * <p>Tells whether this type is a geometrical region.</p>
+	 * 
+	 * <p><i>Concerned types:
+	 * 	{@link DBDatatype#POINT POINT} and {@link DBDatatype#REGION REGION}.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note</b>:
+	 * 	Since {@link DBDatatype#UNKNOWN UNKNOWN} is an unresolved type, it can potentially be anything.
+	 * 	That's why, this function will also returned <code>true</code> if the type is
+	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
+	 * </i></p>
+	 * 
+	 * @return	<code>true</code> if this type is a geometry, <code>false</code> otherwise.
+	 */
 	public boolean isGeometry(){
-		return (type == DBDatatype.POINT || type == DBDatatype.REGION);
+		return (type == DBDatatype.POINT || type == DBDatatype.REGION || type == DBDatatype.UNKNOWN);
 	}
 
+	/**
+	 * <p>Tell whether this type has been resolved or not.</p>
+	 * 
+	 * <p><i>Concerned type:
+	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
+	 * </i></p>
+	 * 
+	 * @return	<code>true</code> if this type has NOT been resolved, <code>false</code> otherwise.
+	 * 
+	 * @since 1.4
+	 */
+	public boolean isUnknown(){
+		return type == DBDatatype.UNKNOWN;
+	}
+
+	/**
+	 * <p>Tell whether this {@link DBType} is compatible with the given one.</p>
+	 * 
+	 * <p>
+	 * 	Two {@link DBType}s are said compatible if they are both binary, numeric, geometric or string.
+	 * 	If one of the two types is {@link DBDatatype#UNKNOWN unknown}, this function will consider them
+	 * 	as compatible and will return <code>true</code>.
+	 * </p>
+	 * 
+	 * @param t	The type to compare to.
+	 * 
+	 * @return	<code>true</code> if this type is compatible with the given one, <code>false</code> otherwise.
+	 */
 	public boolean isCompatible(final DBType t){
 		if (t == null)
 			return false;
+		else if (isUnknown() || t.isUnknown())
+			return true;
 		else if (isBinary() == t.isBinary())
 			return (type == DBDatatype.BLOB && t.type == DBDatatype.BLOB) || (type != DBDatatype.BLOB && t.type != DBDatatype.BLOB);
 		else if (isNumeric() == t.isNumeric())
diff --git a/src/adql/db/FunctionDef.java b/src/adql/db/FunctionDef.java
index 82107d0..42e29a4 100644
--- a/src/adql/db/FunctionDef.java
+++ b/src/adql/db/FunctionDef.java
@@ -49,7 +49,7 @@ import adql.query.operand.function.UserDefinedFunction;
  * </p>
  * 
  * @author Gr&eacute;gory Mantelet (ARI)
- * @version 1.3 (02/2015)
+ * @version 1.4 (07/2015)
  * 
  * @since 1.3
  */
@@ -86,6 +86,14 @@ public class FunctionDef implements Comparable<FunctionDef> {
 	protected final boolean isNumeric;
 	/** Indicate whether the return type is a geometry. */
 	protected final boolean isGeometry;
+	/** Indicate whether the return type is an unknown type.
+	 * <p><i><u>Note:</u>
+	 * 	If <code>true</code>, {@link #isString}, {@link #isNumeric}
+	 * 	and {@link #isGeometry} are <code>false</code>. Otherwise,
+	 * 	at least one of these attributes is set to <code>true</code>.
+	 * </i></p>
+	 * @since 1.4 */
+	protected final boolean isUnknown;
 
 	/** Total number of parameters. */
 	public final int nbParams;
@@ -122,7 +130,7 @@ public class FunctionDef implements Comparable<FunctionDef> {
 	 * <p>This definition is composed of two items: the name and the type of the parameter.</p>
 	 * 
 	 * @author Gr&eacute;gory Mantelet (ARI)
-	 * @version 1.3 (10/2014)
+	 * @version 1.4 (07/2015)
 	 * @since 1.3
 	 */
 	public static final class FunctionParam {
@@ -135,15 +143,13 @@ public class FunctionDef implements Comparable<FunctionDef> {
 		 * Create a function parameter.
 		 * 
 		 * @param paramName	Name of the parameter to create. <i>MUST NOT be NULL</i>
-		 * @param paramType	Type of the parameter to create. <i>MUST NOT be NULL</i>
+		 * @param paramType	Type of the parameter to create. <i>If NULL, an {@link DBDatatype#UNKNOWN UNKNOWN} type will be created and set instead.</i>
 		 */
 		public FunctionParam(final String paramName, final DBType paramType){
 			if (paramName == null)
 				throw new NullPointerException("Missing name! The function parameter can not be created.");
-			if (paramType == null)
-				throw new NullPointerException("Missing type! The function parameter can not be created.");
 			this.name = paramName;
-			this.type = paramType;
+			this.type = (paramType == null) ? new DBType(DBDatatype.UNKNOWN) : paramType;
 		}
 	}
 
@@ -193,13 +199,11 @@ public class FunctionDef implements Comparable<FunctionDef> {
 		this.nbParams = (params == null) ? 0 : params.length;
 
 		// Set the return type;
-		this.returnType = returnType;
-		if (returnType != null){
-			isNumeric = returnType.isNumeric();
-			isString = returnType.isString();
-			isGeometry = returnType.isGeometry();
-		}else
-			isNumeric = isString = isGeometry = false;
+		this.returnType = (returnType != null) ? returnType : new DBType(DBDatatype.UNKNOWN);
+		isUnknown = this.returnType.isUnknown();
+		isNumeric = isUnknown || this.returnType.isNumeric();
+		isString = isUnknown || this.returnType.isString();
+		isGeometry = isUnknown || this.returnType.isGeometry();
 
 		// Serialize in Strings (serializedForm and compareForm) this function definition:
 		StringBuffer bufSer = new StringBuffer(name), bufCmp = new StringBuffer(name.toLowerCase());
@@ -244,6 +248,20 @@ public class FunctionDef implements Comparable<FunctionDef> {
 		return isGeometry;
 	}
 
+	/**
+	 * <p>Tell whether this function returns an unknown type.</p>
+	 * 
+	 * <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> 
+	 * 
+	 * @return	<i>true</i> if this function returns an unknown/unresolved/unsupported type, <i>false</i> otherwise.
+	 */
+	public final boolean isUnknown(){
+		return isUnknown;
+	}
+
 	/**
 	 * Get the number of parameters required by this function.
 	 * 
@@ -357,8 +375,10 @@ public class FunctionDef implements Comparable<FunctionDef> {
 			DBType returnType = null;
 			if (m.group(3) != null){
 				returnType = parseType(m.group(5), (m.group(7) == null) ? DBType.NO_LENGTH : Integer.parseInt(m.group(7)));
-				if (returnType == null)
-					throw new ParseException("Unknown return type: \"" + m.group(4).trim() + "\"!");
+				if (returnType == null){
+					returnType = new DBType(DBDatatype.UNKNOWN);
+					returnType.type.setCustomType(m.group(4));
+				}
 			}
 
 			// Get the parameters, if any:
@@ -384,10 +404,11 @@ public class FunctionDef implements Comparable<FunctionDef> {
 						paramType = parseType(m.group(2), (m.group(4) == null) ? DBType.NO_LENGTH : Integer.parseInt(m.group(4)));
 
 						// ...build the parameter definition object:
-						if (paramType == null)
-							throw new ParseException("Unknown type for the parameter \"" + m.group(1) + "\": \"" + m.group(2) + ((m.group(3) == null) ? "" : m.group(3)) + "\"!");
-						else
-							params[i] = new FunctionParam(m.group(1), paramType);
+						if (paramType == null){
+							paramType = new DBType(DBDatatype.UNKNOWN);
+							paramType.type.setCustomType(m.group(2) + ((m.group(3) == null) ? "" : m.group(3)));
+						}
+						params[i] = new FunctionParam(m.group(1), paramType);
 					}else
 						// note: should never happen because we have already check the syntax of the whole parameters list before parsing each individual parameter.
 						throw new ParseException("Wrong syntax for the " + (i + 1) + "-th parameter: \"" + paramsSplit[i].trim() + "\"! Expected syntax: \"(<regular_identifier> <type_name> (, <regular_identifier> <type_name>)*)\", where <regular_identifier>=\"[a-zA-Z]+[a-zA-Z0-9_]*\", <type_name> should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"()\", \"(param INTEGER)\", \"(param1 INTEGER, param2 DOUBLE)\"");
@@ -410,7 +431,8 @@ public class FunctionDef implements Comparable<FunctionDef> {
 	 *              	<i>Note: This length will be used only for binary (BINARY and VARBINARY)
 	 *              	and character (CHAR and VARCHAR) types.</i> 
 	 * 
-	 * @return	The object representation of the specified datatype.
+	 * @return	The object representation of the specified datatype
+	 *        	or NULL if the specified datatype can not be resolved.
 	 */
 	private static DBType parseType(String datatype, int length){
 		if (datatype == null)
@@ -519,7 +541,9 @@ public class FunctionDef implements Comparable<FunctionDef> {
 		// If equals, compare the parameters' type:
 		if (comp == 0){
 			for(int i = 0; comp == 0 && i < nbParams && i < fct.getNbParameters(); i++){
-				if (params[i].type.isNumeric() == fct.getParameter(i).isNumeric()){
+				if (fct.getParameter(i).isNumeric() && fct.getParameter(i).isString() && fct.getParameter(i).isGeometry())
+					comp = 0;
+				else if (params[i].type.isNumeric() == fct.getParameter(i).isNumeric()){
 					if (params[i].type.isString() == fct.getParameter(i).isString()){
 						if (params[i].type.isGeometry() == fct.getParameter(i).isGeometry())
 							comp = 0;
diff --git a/src/adql/translator/PgSphereTranslator.java b/src/adql/translator/PgSphereTranslator.java
index 96c509c..6474ab3 100644
--- a/src/adql/translator/PgSphereTranslator.java
+++ b/src/adql/translator/PgSphereTranslator.java
@@ -16,7 +16,7 @@ package adql.translator;
  * 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 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
+ * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
  *                       Astronomisches Rechen Institut (ARI)
  */
 
@@ -48,7 +48,7 @@ import adql.query.operand.function.geometry.PolygonFunction;
  * The other functions are managed by {@link PostgreSQLTranslator}.</p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 1.3 (11/2014)
+ * @version 1.4 (07/2015)
  */
 public class PgSphereTranslator extends PostgreSQLTranslator {
 
@@ -202,7 +202,7 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params){
 		// If no type is provided return VARCHAR:
 		if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0)
-			return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH);
+			return null;
 
 		// Put the dbmsTypeName in lower case for the following comparisons:
 		dbmsTypeName = dbmsTypeName.toLowerCase();
diff --git a/src/adql/translator/PostgreSQLTranslator.java b/src/adql/translator/PostgreSQLTranslator.java
index 4b2d013..9c500b3 100644
--- a/src/adql/translator/PostgreSQLTranslator.java
+++ b/src/adql/translator/PostgreSQLTranslator.java
@@ -16,7 +16,7 @@ package adql.translator;
  * 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 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
+ * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
  *                       Astronomisches Rechen Institut (ARI)
  */
 
@@ -50,7 +50,7 @@ import adql.query.operand.function.geometry.RegionFunction;
  * </i></p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 1.3 (11/2014)
+ * @version 1.4 (07/2015)
  * 
  * @see PgSphereTranslator
  */
@@ -196,7 +196,7 @@ public class PostgreSQLTranslator extends JDBCTranslator {
 	public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params){
 		// If no type is provided return VARCHAR:
 		if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0)
-			return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH);
+			return null;
 
 		// Put the dbmsTypeName in lower case for the following comparisons:
 		dbmsTypeName = dbmsTypeName.toLowerCase();
@@ -247,7 +247,7 @@ public class PostgreSQLTranslator extends JDBCTranslator {
 			return new DBType(DBDatatype.TIMESTAMP);
 		// Default:
 		else
-			return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH);
+			return null;
 	}
 
 	@Override
diff --git a/src/tap/data/ResultSetTableIterator.java b/src/tap/data/ResultSetTableIterator.java
index 9c906a1..220d9b7 100644
--- a/src/tap/data/ResultSetTableIterator.java
+++ b/src/tap/data/ResultSetTableIterator.java
@@ -16,7 +16,7 @@ package tap.data;
  * You should have received a copy of the GNU Lesser General Public License
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
- * Copyright 2014 - Astronomisches Rechen Institut (ARI)
+ * Copyright 2014-2015 - Astronomisches Rechen Institut (ARI)
  */
 
 import java.sql.ResultSet;
@@ -42,7 +42,7 @@ import adql.translator.JDBCTranslator;
  * </i></p>
  * 
  * @author Gr&eacute;gory Mantelet (ARI)
- * @version 2.0 (11/2014)
+ * @version 2.1 (07/2015)
  * @since 2.0
  */
 public class ResultSetTableIterator implements TableIterator {
@@ -363,7 +363,7 @@ public class ResultSetTableIterator implements TableIterator {
 				if (o instanceof Timestamp)
 					o = ISO8601Format.format(((Timestamp)o).getTime());
 				// if the column value is a geometrical object, it must be serialized in STC-S:
-				else if (translator != null && colType.isGeometry()){
+				else if (translator != null && colType != null && colType.isGeometry()){
 					Region region = translator.translateGeometryFromDB(o);
 					if (region != null)
 						o = region.toSTCS();
@@ -409,7 +409,8 @@ public class ResultSetTableIterator implements TableIterator {
 	 * @param dbmsType	DBMS column data-type name.
 	 * @param dbms		Lower-case string which indicates which DBMS the ResultSet is coming from. <i>note: MAY be NULL.</i>
 	 * 
-	 * @return	The best suited {@link DBType} object.
+	 * @return	The best suited {@link DBType} object,
+	 *        	or an {@link DBDatatype#UNKNOWN UNKNOWN} type if none can be found.
 	 * 
 	 * @see JDBCTranslator#convertTypeFromDB(int, String, String, String[])
 	 * @see #defaultTypeConversion(String, String[], String)
@@ -417,7 +418,7 @@ public class ResultSetTableIterator implements TableIterator {
 	protected DBType convertType(final int dbmsType, String dbmsTypeName, final String dbms) throws DataReadException{
 		// If no type is provided return VARCHAR:
 		if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0)
-			return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH);
+			return new DBType(DBDatatype.UNKNOWN);
 
 		// Extract the type prefix and lower-case it:
 		int startParamIndex = dbmsTypeName.indexOf('('), endParamIndex = dbmsTypeName.indexOf(')');
@@ -468,7 +469,8 @@ public class ResultSetTableIterator implements TableIterator {
 	 * @param params		The eventual type parameters (e.g. char string length).
 	 * @param dbms			The targeted DBMS.
 	 * 
-	 * @return	The corresponding ADQL/TAP type. <i>NEVER NULL</i>
+	 * @return	The corresponding ADQL/TAP type. <i>NEVER NULL ;
+	 *        	an {@link DBDatatype#UNKNOWN UNKNOWN} type is returned in case no match can be found.</i>
 	 */
 	protected final DBType defaultTypeConversion(final String dbmsTypeName, final String[] params, final String dbms){
 		// Get the length parameter (always in first position):
@@ -495,7 +497,7 @@ public class ResultSetTableIterator implements TableIterator {
 				return new DBType(DBDatatype.BLOB);
 			// Default:
 			else
-				return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH);
+				return new DBType(DBDatatype.UNKNOWN);
 		}
 		// CASE: OTHER DBMS
 		else{
@@ -537,7 +539,7 @@ public class ResultSetTableIterator implements TableIterator {
 				return new DBType(DBDatatype.TIMESTAMP);
 			// Default:
 			else
-				return new DBType(DBDatatype.VARCHAR, DBType.NO_LENGTH);
+				return new DBType(DBDatatype.UNKNOWN);
 		}
 	}
 
diff --git a/src/tap/data/VOTableIterator.java b/src/tap/data/VOTableIterator.java
index 95d2d56..277b5b0 100644
--- a/src/tap/data/VOTableIterator.java
+++ b/src/tap/data/VOTableIterator.java
@@ -42,7 +42,7 @@ import adql.db.DBType;
  * <p>{@link #getColType()} will return TAP type based on the type declared in the VOTable metadata part.</p>
  * 
  * @author Gr&eacute;gory Mantelet (ARI)
- * @version 2.0 (04/2015)
+ * @version 2.1 (07/2015)
  * @since 2.0
  */
 public class VOTableIterator implements TableIterator {
@@ -460,14 +460,14 @@ public class VOTableIterator implements TableIterator {
 	 * @param arraysize		Attribute value of VOTable corresponding to the arraysize.
 	 * @param xtype			Attribute value of VOTable corresponding to the xtype.
 	 * 
-	 * @return	The resolved VOTable field type, or a CHAR(*) type if the specified type can not be resolved.
+	 * @return	The resolved VOTable field type.
 	 * 
-	 * @throws DataReadException	If a field datatype is unknown.
+	 * @throws DataReadException	If a field datatype is unknown or missing.
 	 */
 	public static VotType resolveVotType(final String datatype, final String arraysize, final String xtype) throws DataReadException{
 		// If no datatype is specified, return immediately a CHAR(*) type:
 		if (datatype == null || datatype.trim().length() == 0)
-			return new VotType(VotDatatype.CHAR, "*");
+			throw new DataReadException("missing VOTable required field: \"datatype\"!");
 
 		// Identify the specified datatype:
 		VotDatatype votdatatype;
diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java
index 06044ad..f8dc32d 100644
--- a/src/tap/db/JDBCConnection.java
+++ b/src/tap/db/JDBCConnection.java
@@ -784,7 +784,7 @@ public class JDBCConnection implements DBConnection {
 				// ...build the column type:
 				DBType type;
 				if (tapDatatype == null)
-					type = new DBType(DBDatatype.VARCHAR);
+					type = new DBType(DBDatatype.UNKNOWN);
 				else
 					type = new DBType(tapDatatype, size);
 
@@ -1960,7 +1960,7 @@ public class JDBCConnection implements DBConnection {
 	 * 
 	 * @param datatype	Column TAP type.
 	 * 
-	 * @return	The corresponding DB type, or NULL if the given type is not managed or is NULL.
+	 * @return	The corresponding DB type, or VARCHAR if the given type is not managed or is NULL.
 	 */
 	protected String defaultTypeConversion(DBType datatype){
 		if (datatype == null)
diff --git a/src/tap/formatter/JSONFormat.java b/src/tap/formatter/JSONFormat.java
index 4e80889..515e43a 100644
--- a/src/tap/formatter/JSONFormat.java
+++ b/src/tap/formatter/JSONFormat.java
@@ -42,7 +42,7 @@ import adql.db.DBType.DBDatatype;
  * Format any given query (table) result into JSON.
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (04/2015)
+ * @version 2.1 (07/2015)
  */
 public class JSONFormat implements OutputFormat {
 
@@ -178,9 +178,12 @@ public class JSONFormat implements OutputFormat {
 	 * @return	The most appropriate metadata.
 	 */
 	protected TAPColumn getValidColMeta(final DBColumn typeFromQuery, final TAPColumn typeFromResult){
-		if (typeFromQuery != null && typeFromQuery instanceof TAPColumn)
-			return (TAPColumn)typeFromQuery;
-		else if (typeFromResult != null){
+		if (typeFromQuery != null && typeFromQuery instanceof TAPColumn){
+			TAPColumn colMeta = (TAPColumn)typeFromQuery;
+			if (colMeta.getDatatype().type == DBDatatype.UNKNOWN && typeFromResult != null && typeFromResult.getDatatype().type != DBDatatype.UNKNOWN)
+				colMeta.setDatatype(typeFromResult.getDatatype());
+			return colMeta;
+		}else if (typeFromResult != null){
 			if (typeFromQuery != null)
 				return (TAPColumn)typeFromResult.copy(typeFromQuery.getDBName(), typeFromQuery.getADQLName(), null);
 			else
diff --git a/src/tap/formatter/VOTableFormat.java b/src/tap/formatter/VOTableFormat.java
index f38b17e..6156d21 100644
--- a/src/tap/formatter/VOTableFormat.java
+++ b/src/tap/formatter/VOTableFormat.java
@@ -83,7 +83,7 @@ import adql.db.DBType.DBDatatype;
  * </p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (04/2015)
+ * @version 2.1 (07/2015)
  */
 public class VOTableFormat implements OutputFormat {
 
@@ -462,9 +462,12 @@ public class VOTableFormat implements OutputFormat {
 	 * @return	The most appropriate metadata.
 	 */
 	protected static final TAPColumn getValidColMeta(final DBColumn typeFromQuery, final TAPColumn typeFromResult){
-		if (typeFromQuery != null && typeFromQuery instanceof TAPColumn)
-			return (TAPColumn)typeFromQuery;
-		else if (typeFromResult != null){
+		if (typeFromQuery != null && typeFromQuery instanceof TAPColumn){
+			TAPColumn colMeta = (TAPColumn)typeFromQuery;
+			if (colMeta.getDatatype().type == DBDatatype.UNKNOWN && typeFromResult != null && typeFromResult.getDatatype().type != DBDatatype.UNKNOWN)
+				colMeta.setDatatype(typeFromResult.getDatatype());
+			return colMeta;
+		}else if (typeFromResult != null){
 			if (typeFromQuery != null)
 				return (TAPColumn)typeFromResult.copy(typeFromQuery.getDBName(), typeFromQuery.getADQLName(), null);
 			else
diff --git a/src/tap/metadata/TAPColumn.java b/src/tap/metadata/TAPColumn.java
index 30d4d1d..289dbe9 100644
--- a/src/tap/metadata/TAPColumn.java
+++ b/src/tap/metadata/TAPColumn.java
@@ -16,7 +16,7 @@ package tap.metadata;
  * You should have received a copy of the GNU Lesser General Public License
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
- * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
+ * Copyright 2012-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
  *                       Astronomisches Rechen Institut (ARI)
  */
 
@@ -67,7 +67,7 @@ import adql.db.DBType.DBDatatype;
  * </p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (02/2015)
+ * @version 2.1 (07/2015)
  */
 public class TAPColumn implements DBColumn {
 
@@ -102,7 +102,7 @@ public class TAPColumn implements DBColumn {
 
 	/** Type of this column.
 	 * <i>Note: Standard TAP column field ; CAN'T be NULL.</i> */
-	private DBType datatype = new DBType(DBDatatype.VARCHAR);
+	private DBType datatype = new DBType(DBDatatype.UNKNOWN);
 
 	/** Flag indicating whether this column is one of those that should be returned by default.
 	 * <i>Note: Standard TAP column field ; FALSE by default.</i> */
@@ -494,7 +494,7 @@ public class TAPColumn implements DBColumn {
 	 * <p>Set the type of the column's values.</p>
 	 * 
 	 * <p><i>Note:
-	 * 	The datatype won't be changed, if the given type is NULL.
+	 * 	If the given type is NULL, a {@link DBDatatype#UNKNOWN UNKNOWN} type will be set instead.
 	 * </i></p>
 	 * 
 	 * @param type	Its new datatype.
@@ -502,6 +502,8 @@ public class TAPColumn implements DBColumn {
 	public final void setDatatype(final DBType type){
 		if (type != null)
 			datatype = type;
+		else
+			datatype = new DBType(DBDatatype.UNKNOWN);
 	}
 
 	/**
diff --git a/test/adql/db/TestFunctionDef.java b/test/adql/db/TestFunctionDef.java
index c3d738a..20ae1da 100644
--- a/test/adql/db/TestFunctionDef.java
+++ b/test/adql/db/TestFunctionDef.java
@@ -27,6 +27,7 @@ public class TestFunctionDef {
 				case VARCHAR:
 				case TIMESTAMP:
 				case CLOB:
+				case UNKNOWN:
 					assertTrue(new FunctionDef("foo", new DBType(type)).isString);
 					break;
 				default:
@@ -41,6 +42,7 @@ public class TestFunctionDef {
 			switch(type){
 				case POINT:
 				case REGION:
+				case UNKNOWN:
 					assertTrue(new FunctionDef("foo", new DBType(type)).isGeometry);
 					break;
 				default:
@@ -170,18 +172,26 @@ public class TestFunctionDef {
 			assertEquals("Wrong syntax for the 1-th parameter: \"param\"! Expected syntax: \"(<regular_identifier> <type_name> (, <regular_identifier> <type_name>)*)\", where <regular_identifier>=\"[a-zA-Z]+[a-zA-Z0-9_]*\", <type_name> should be one of the types described in the UPLOAD section of the TAP documentation. Examples of good syntax: \"()\", \"(param INTEGER)\", \"(param1 INTEGER, param2 DOUBLE)\"", ex.getMessage());
 		}
 		try{
-			FunctionDef.parse("foo()->aType");
-			fail("Wrong (return) type!");
+			FunctionDef fct = FunctionDef.parse("foo()->aType");
+			assertTrue(fct.isUnknown);
+			assertTrue(fct.isString);
+			assertTrue(fct.isNumeric);
+			assertTrue(fct.isGeometry);
+			assertEquals("?aType?", fct.returnType.type.toString());
 		}catch(Exception ex){
-			assertTrue(ex instanceof ParseException);
-			assertEquals("Unknown return type: \"aType\"!", ex.getMessage());
+			ex.printStackTrace(System.err);
+			fail("Unknown types MUST be allowed!");
 		}
 		try{
-			FunctionDef.parse("foo()->aType(10)");
-			fail("Wrong (return) type!");
+			FunctionDef fct = FunctionDef.parse("foo()->aType(10)");
+			assertTrue(fct.isUnknown);
+			assertTrue(fct.isString);
+			assertTrue(fct.isNumeric);
+			assertTrue(fct.isGeometry);
+			assertEquals("?aType(10)?", fct.returnType.type.toString());
 		}catch(Exception ex){
-			assertTrue(ex instanceof ParseException);
-			assertEquals("Unknown return type: \"aType(10)\"!", ex.getMessage());
+			ex.printStackTrace(System.err);
+			fail("Unknown types MUST be allowed!");
 		}
 		try{
 			FunctionDef.parse("foo() -> ");
@@ -205,18 +215,26 @@ public class TestFunctionDef {
 			assertEquals(WRONG_PARAM_SYNTAX, ex.getMessage());
 		}
 		try{
-			FunctionDef.parse("foo(param1 aType)");
-			fail("Wrong parameter type!");
+			FunctionDef fct = FunctionDef.parse("foo(param1 aType)");
+			assertTrue(fct.getParam(0).type.isUnknown());
+			assertTrue(fct.getParam(0).type.isString());
+			assertTrue(fct.getParam(0).type.isNumeric());
+			assertTrue(fct.getParam(0).type.isGeometry());
+			assertEquals("?aType?", fct.getParam(0).type.toString());
 		}catch(Exception ex){
-			assertTrue(ex instanceof ParseException);
-			assertEquals("Unknown type for the parameter \"param1\": \"aType\"!", ex.getMessage());
+			ex.printStackTrace(System.err);
+			fail("Unknown types MUST be allowed!");
 		}
 		try{
-			FunctionDef.parse("foo(param1 aType(10))");
-			fail("Wrong parameter type!");
+			FunctionDef fct = FunctionDef.parse("foo(param1 aType(10))");
+			assertTrue(fct.getParam(0).type.isUnknown());
+			assertTrue(fct.getParam(0).type.isString());
+			assertTrue(fct.getParam(0).type.isNumeric());
+			assertTrue(fct.getParam(0).type.isGeometry());
+			assertEquals("?aType(10)?", fct.getParam(0).type.toString());
 		}catch(Exception ex){
-			assertTrue(ex instanceof ParseException);
-			assertEquals("Unknown type for the parameter \"param1\": \"aType(10)\"!", ex.getMessage());
+			ex.printStackTrace(System.err);
+			fail("Unknown types MUST be allowed!");
 		}
 	}
 
diff --git a/test/adql/parser/UnknownTypes.java b/test/adql/parser/UnknownTypes.java
new file mode 100644
index 0000000..0ba4e0b
--- /dev/null
+++ b/test/adql/parser/UnknownTypes.java
@@ -0,0 +1,118 @@
+package adql.parser;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import adql.db.DBChecker;
+import adql.db.DBColumn;
+import adql.db.DBTable;
+import adql.db.DBType;
+import adql.db.DBType.DBDatatype;
+import adql.db.DefaultDBColumn;
+import adql.db.DefaultDBTable;
+import adql.db.FunctionDef;
+import adql.query.ADQLQuery;
+
+public class UnknownTypes {
+
+	@BeforeClass
+	public static void setUpBeforeClass() throws Exception{}
+
+	@AfterClass
+	public static void tearDownAfterClass() throws Exception{}
+
+	@Before
+	public void setUp() throws Exception{}
+
+	@After
+	public void tearDown() throws Exception{}
+
+	public void testForFctDef(){
+		// Test with the return type:
+		try{
+			FunctionDef fct = FunctionDef.parse("foo()->aType");
+			assertTrue(fct.isUnknown());
+			assertTrue(fct.isString());
+			assertTrue(fct.isNumeric());
+			assertTrue(fct.isGeometry());
+			assertEquals("?aType?", fct.returnType.type.toString());
+		}catch(Exception ex){
+			ex.printStackTrace(System.err);
+			fail("Unknown types MUST be allowed!");
+		}
+
+		// Test with a parameter type:
+		try{
+			FunctionDef fct = FunctionDef.parse("foo(param1 aType)");
+			assertTrue(fct.getParam(0).type.isUnknown());
+			assertTrue(fct.getParam(0).type.isString());
+			assertTrue(fct.getParam(0).type.isNumeric());
+			assertTrue(fct.getParam(0).type.isGeometry());
+			assertEquals("?aType?", fct.getParam(0).type.toString());
+		}catch(Exception ex){
+			ex.printStackTrace(System.err);
+			fail("Unknown types MUST be allowed!");
+		}
+	}
+
+	@Test
+	public void testForColumns(){
+		final String QUERY_TXT = "SELECT FOO(C1), FOO(C2) FROM T1";
+
+		try{
+			// Create the parser:
+			ADQLParser parser = new ADQLParser();
+
+			// Create table/column metadata:
+			DefaultDBTable table1 = new DefaultDBTable("T1");
+			table1.addColumn(new DefaultDBColumn("C1", table1));
+			table1.addColumn(new DefaultDBColumn("C2", new DBType(DBDatatype.UNKNOWN), table1));
+			Collection<DBTable> tList = Arrays.asList(new DBTable[]{table1});
+
+			// Check the type of the column T1.C1:
+			DBColumn col = table1.getColumn("C1", true);
+			assertNotNull(col);
+			assertNull(col.getDatatype());
+
+			// Check the type of the column T1.C2:
+			col = table1.getColumn("C2", true);
+			assertNotNull(col);
+			assertNotNull(col.getDatatype());
+			assertTrue(col.getDatatype().isUnknown());
+			assertTrue(col.getDatatype().isNumeric());
+			assertTrue(col.getDatatype().isString());
+			assertTrue(col.getDatatype().isGeometry());
+			assertEquals("UNKNOWN", col.getDatatype().toString());
+
+			// Define a UDF, and allow all geometrical functions and coordinate systems:
+			FunctionDef udf1 = FunctionDef.parse("FOO(x INTEGER) -> INTEGER");
+			Collection<FunctionDef> udfList = Arrays.asList(new FunctionDef[]{udf1});
+			Collection<String> geoList = null;
+			Collection<String> csList = null;
+
+			// Create the Query checker:
+			QueryChecker checker = new DBChecker(tList, udfList, geoList, csList);
+
+			// Parse the query:
+			ADQLQuery pq = parser.parseQuery(QUERY_TXT);
+
+			// Check the parsed query:
+			checker.check(pq);
+		}catch(Exception ex){
+			ex.printStackTrace(System.err);
+			fail("The construction, configuration and usage of the parser are correct. Nothing should have failed here. (see console for more details)");
+		}
+	}
+}
-- 
GitLab