Commit 0003e343 authored by gmantele's avatar gmantele
Browse files

[ADQL] Set a type to a query's resulting column when it is not originally a column.

This is easily possible for concatenations, string constants and User Defined
Functions having a FunctionDef. A new special datatype was needed for
numeric functions and operations: UNKNOWN_NUMERIC. This special type
can not be set with FunctionDef.parse(...) and it behaves exactly like the type
UNKNOWN, except that DBType.isNumeric() returns true (as .isUnknown()).
Thus, while writing the metadata of a result in TAP, nothing changes:
an UNKNOWN_NUMERIC type will be processed similarly as an UNKNOWN type:
to use the type returned from the database ResultSet or to set VARCHAR.
(no modification of TAP was needed for that)
parent 475fcb65
Loading
Loading
Loading
Loading
+28 −19
Original line number Diff line number Diff line
@@ -16,11 +16,10 @@ 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-2015 - Astronomisches Rechen Institut (ARI)
 * Copyright 2014-2016 - Astronomisches Rechen Institut (ARI)
 */

/**
 * 
 * <p>
 * 	Describe a full column type as it is described in the IVOA document of TAP.
 * 	Thus, this object contains 2 attributes: <code>type</code> (or datatype) and <code>length</code> (or size).
@@ -32,7 +31,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.4 (08/2015)
 * @version 1.4 (03/2016)
 * @since 1.3
 */
public class DBType {
@@ -41,13 +40,21 @@ 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.4 (06/2015)
	 * @version 1.4 (03/2016)
	 * @since 1.3
	 */
	public static enum DBDatatype{
		SMALLINT, INTEGER, BIGINT, REAL, DOUBLE, BINARY, VARBINARY, CHAR, VARCHAR, BLOB, CLOB, TIMESTAMP, POINT, REGION,
		/** @since 1.4 */
		UNKNOWN;
		/** Type to use when the precise datatype is unknown. 
		 * @since 1.4 */
		UNKNOWN,
		/** <p>Type to use when the type is known as numeric but there is no precise datatype
		 * (e.g. double, float, integer, ...).</p>
		 * <p>It is particularly used when creating a {@link DefaultDBColumn} from an ADQL function
		 * or operation while listing resulting columns of a sub-query.</p>
		 * <p>This type is similar to {@link #UNKNOWN}.</p>
		 * @since 1.4 */
		UNKNOWN_NUMERIC;

		/** String to return when {@link #toString()} is called.
		 * @since 1.4*/
@@ -60,11 +67,11 @@ public class DBType {

		/**
		 * <p>This function lets define the name of the type as provided
		 * <b>ONLY FOR {@link #UNKNOWN} {@link DBDatatype}</b>.</p>
		 * <b>ONLY FOR {@link #UNKNOWN} and {@link #UNKNOWN_NUMERIC} {@link DBDatatype}s</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.
		 * 	If this {@link DBDatatype} is not {@link #UNKNOWN} or {@link #UNKNOWN_NUMERIC} or
		 * 	if the given name is NULL or empty, this function has no effect.
		 * </i></p>
		 * 
		 * @param typeName	User type name.
@@ -72,7 +79,7 @@ public class DBType {
		 * @since 1.4
		 */
		public void setCustomType(final String typeName){
			if (this == UNKNOWN && typeName != null && typeName.trim().length() > 0)
			if ((this == UNKNOWN || this == UNKNOWN_NUMERIC) && typeName != null && typeName.trim().length() > 0)
				strExp = "?" + typeName.trim() + "?";
		}
	}
@@ -121,7 +128,8 @@ public class DBType {
	 * 	Since {@link DBDatatype#UNKNOWN UNKNOWN} is an unresolved type, it can potentially be anything.
	 * 	But, in order to avoid incorrect operation while expecting a numeric although the type is unknown
	 * 	and is in fact not really a numeric, this function will return <code>false</code> if the type is
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN} <b>BUT</b> <code>true</code> if
	 * 	{@link DBDatatype#UNKNOWN_NUMERIC UNKNOWN_NUMERIC}.
	 * </i></p>
	 * 
	 * @return	<code>true</code> if this type is a numeric, <code>false</code> otherwise.
@@ -138,6 +146,7 @@ public class DBType {
			case BINARY:
			case VARBINARY:
			case BLOB:
			case UNKNOWN_NUMERIC:
				return true;
			default:
				return false;
@@ -155,7 +164,7 @@ public class DBType {
	 * 	Since {@link DBDatatype#UNKNOWN UNKNOWN} is an unresolved type, it can potentially be anything.
	 * 	But, in order to avoid incorrect operation while expecting a binary although the type is unknown
	 * 	and is in fact not really a binary, this function will return <code>false</code> if the type is
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN} or {@link DBDatatype#UNKNOWN_NUMERIC UNKNOWN_NUMERIC}.
	 * </i></p>
	 * 
	 * @return	<code>true</code> if this type is a binary, <code>false</code> otherwise.
@@ -183,7 +192,7 @@ public class DBType {
	 * 	Since {@link DBDatatype#UNKNOWN UNKNOWN} is an unresolved type, it can potentially be anything.
	 * 	But, in order to avoid incorrect operation while expecting a string although the type is unknown
	 * 	and is in fact not really a string, this function will return <code>false</code> if the type is
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN} or {@link DBDatatype#UNKNOWN_NUMERIC UNKNOWN_NUMERIC}
	 * </i></p>
	 * 
	 * @return	<code>true</code> if this type is a string, <code>false</code> otherwise.
@@ -211,7 +220,7 @@ public class DBType {
	 * 	Since {@link DBDatatype#UNKNOWN UNKNOWN} is an unresolved type, it can potentially be anything.
	 * 	But, in order to avoid incorrect operation while expecting a geometry although the type is unknown
	 * 	and is in fact not really a geometry, this function will return <code>false</code> if the type is
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN} or {@link DBDatatype#UNKNOWN_NUMERIC UNKNOWN_NUMERIC}.
	 * </i></p>
	 * 
	 * @return	<code>true</code> if this type is a geometry, <code>false</code> otherwise.
@@ -223,8 +232,8 @@ public class DBType {
	/**
	 * <p>Tell whether this type has been resolved or not.</p>
	 * 
	 * <p><i>Concerned type:
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN}.
	 * <p><i>Concerned types:
	 * 	{@link DBDatatype#UNKNOWN UNKNOWN} and {@link DBDatatype#UNKNOWN_NUMERIC UNKNOWN_NUMERIC}.
	 * </i></p>
	 * 
	 * @return	<code>true</code> if this type has NOT been resolved, <code>false</code> otherwise.
@@ -232,7 +241,7 @@ public class DBType {
	 * @since 1.4
	 */
	public boolean isUnknown(){
		return type == DBDatatype.UNKNOWN;
		return type == DBDatatype.UNKNOWN || type == DBDatatype.UNKNOWN_NUMERIC;
	}

	/**
@@ -240,8 +249,8 @@ public class DBType {
	 * 
	 * <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>.
	 * 	If one of the two types is {@link DBDatatype#UNKNOWN unknown} or {@link DBDatatype#UNKNOWN_NUMERIC unknown_numeric},
	 * 	this function will consider them as compatible and will return <code>true</code>.
	 * </p>
	 * 
	 * @param t	The type to compare to.
+2 −2
Original line number Diff line number Diff line
@@ -59,7 +59,7 @@ public class FunctionDef implements Comparable<FunctionDef> {
	/** Rough regular expression for a function return type or a parameter type.
	 * The exact type is not checked here ; just the type name syntax is tested, not its value.
	 * This regular expression allows a type to have exactly one parameter (which is generally the length of a character or binary string. */
	protected final static String typeRegExp = "([a-zA-Z]+[ 0-9a-zA-Z]*)(\\(\\s*([0-9]+)\\s*\\))?";
	protected final static String typeRegExp = "([a-zA-Z_]+[ 0-9a-zA-Z_]*)(\\(\\s*([0-9]+)\\s*\\))?";
	/** Rough regular expression for a function parameters' list. */
	protected final static String fctParamsRegExp = "\\s*[^,]+\\s*(,\\s*[^,]+\\s*)*";
	/** Rough regular expression for a function parameter: a name (see {@link #regularIdentifierRegExp}) and a type (see {@link #typeRegExp}). */
@@ -350,7 +350,7 @@ public class FunctionDef implements Comparable<FunctionDef> {
	 * <p>
	 * 	<em>This function must be able to parse functions as defined by TAPRegExt (section 2.3).</em>
	 * 	Hence, allowed parameter types and return types should be one of the types listed by the UPLOAD section of the TAP recommendation document.
	 * 	These types are listed in the enumeration object {@link DBType}.
	 * 	These types are listed in the enumeration object {@link DBDatatype}.
	 * 	However, other types should be accepted like the common database types...but it should be better to not rely on that
	 * 	since the conversion of those types to TAP types should not be exactly what is expected (because depending from the used DBMS);
	 *  a default interpretation of database types is nevertheless processed by this parser.
+42 −3
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@ package adql.query;
 * 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-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 * Copyright 2012-2016 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 *                       Astronomisches Rechen Institut (ARI)
 */

@@ -25,12 +25,20 @@ import java.util.Iterator;
import java.util.NoSuchElementException;

import adql.db.DBColumn;
import adql.db.DBType;
import adql.db.DBType.DBDatatype;
import adql.db.DefaultDBColumn;
import adql.parser.ADQLParser;
import adql.parser.ParseException;
import adql.query.from.FromContent;
import adql.query.operand.ADQLColumn;
import adql.query.operand.ADQLOperand;
import adql.query.operand.function.DefaultUDF;
import adql.query.operand.function.geometry.BoxFunction;
import adql.query.operand.function.geometry.CircleFunction;
import adql.query.operand.function.geometry.PointFunction;
import adql.query.operand.function.geometry.PolygonFunction;
import adql.query.operand.function.geometry.RegionFunction;
import adql.search.ISearchHandler;

/**
@@ -38,7 +46,7 @@ import adql.search.ISearchHandler;
 * <p>The resulting object of the {@link ADQLParser} is an object of this class.</p>
 * 
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 1.4 (06/2015)
 * @version 1.4 (03/2016)
 */
public class ADQLQuery implements ADQLObject {

@@ -311,19 +319,50 @@ public class ADQLQuery implements ADQLObject {
					// Here, this error should not occur any more, since it must have been caught by the DBChecker!
				}
			}else{
				// Create the DBColumn:
				DBColumn col = null;
				// ...whose the name will be set with the SELECT item's alias: 
				if (item.hasAlias()){
					if (operand instanceof ADQLColumn && ((ADQLColumn)operand).getDBLink() != null){
						col = ((ADQLColumn)operand).getDBLink();
						col = col.copy(col.getDBName(), item.getAlias(), col.getTable());
					}else
						col = new DefaultDBColumn(item.getAlias(), null);
				}else{
				}
				// ...or whose the name will be the name of the SELECT item:
				else{
					if (operand instanceof ADQLColumn && ((ADQLColumn)operand).getDBLink() != null)
						col = ((ADQLColumn)operand).getDBLink();
					if (col == null)
						col = new DefaultDBColumn(item.getName(), null);
				}

				/* For columns created by default (from functions and operations generally), 
				 * set the adequate type if known: */
				// CASE: Well-defined UDF
				if (operand instanceof DefaultUDF && ((DefaultUDF)operand).getDefinition() != null){
					DBType type = ((DefaultUDF)operand).getDefinition().returnType;
					((DefaultDBColumn)col).setDatatype(type);
				}
				// CASE: Point type:
				else if (operand instanceof PointFunction)
					((DefaultDBColumn)col).setDatatype(new DBType(DBDatatype.POINT));
				// CASE: Region type:
				else if (operand instanceof RegionFunction || operand instanceof CircleFunction || operand instanceof BoxFunction || operand instanceof PolygonFunction)
					((DefaultDBColumn)col).setDatatype(new DBType(DBDatatype.REGION));
				// CASE: String and numeric types
				else if (col instanceof DefaultDBColumn && col.getDatatype() == null && operand.isNumeric() != operand.isString()){
					// CASE: String types
					if (operand.isString())
						((DefaultDBColumn)col).setDatatype(new DBType(DBDatatype.VARCHAR));
					// CASE: Numeric types:
					/* Note: a little special case here since a numeric could be a real, double, integer, or anything
					 *       else and that we don't know precisely here. So we set the special UNKNOWN NUMERIC type. */
					else
						((DefaultDBColumn)col).setDatatype(new DBType(DBDatatype.UNKNOWN_NUMERIC));
				}

				// Add the new column to the list:
				columns.add(col);
			}
		}
+5 −5
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@ package adql.query.operand;
 * 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-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 * Copyright 2012-2016 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
 *                       Astronomisches Rechen Institut (ARI)
 */

@@ -32,7 +32,7 @@ import adql.query.from.ADQLTable;
 * Represents the complete (literal) reference to a column ({schema(s)}.{table}.{column}).
 * 
 * @author Gr&eacute;gory Mantelet (CDS;ARI)
 * @version 1.4 (06/2015)
 * @version 1.4 (03/2016)
 */
public class ADQLColumn implements ADQLOperand, UnknownType {

@@ -469,17 +469,17 @@ public class ADQLColumn implements ADQLOperand, UnknownType {

	@Override
	public boolean isNumeric(){
		return (dbLink == null || dbLink.getDatatype() == null || dbLink.getDatatype().isUnknown() || dbLink.getDatatype().isNumeric());
		return (dbLink == null || dbLink.getDatatype() == null || dbLink.getDatatype().isNumeric() || dbLink.getDatatype().isUnknown());
	}

	@Override
	public boolean isString(){
		return (dbLink == null || dbLink.getDatatype() == null || dbLink.getDatatype().isUnknown() || dbLink.getDatatype().isString());
		return (dbLink == null || dbLink.getDatatype() == null || dbLink.getDatatype().isString() || (dbLink.getDatatype().isUnknown() && !dbLink.getDatatype().isNumeric()));
	}

	@Override
	public boolean isGeometry(){
		return (dbLink == null || dbLink.getDatatype() == null || dbLink.getDatatype().isUnknown() || dbLink.getDatatype().isGeometry());
		return (dbLink == null || dbLink.getDatatype() == null || dbLink.getDatatype().isGeometry() || (dbLink.getDatatype().isUnknown() && !dbLink.getDatatype().isNumeric()));
	}

	@Override
+115 −0
Original line number Diff line number Diff line
package adql;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.Iterator;
@@ -9,6 +11,9 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;

import adql.db.DBType;
import adql.db.DBType.DBDatatype;
import adql.db.FunctionDef;
import adql.query.ADQLObject;
import adql.query.ADQLOrder;
import adql.query.ADQLQuery;
@@ -21,12 +26,23 @@ import adql.query.constraint.ComparisonOperator;
import adql.query.constraint.ConstraintsGroup;
import adql.query.from.ADQLTable;
import adql.query.operand.ADQLColumn;
import adql.query.operand.ADQLOperand;
import adql.query.operand.Concatenation;
import adql.query.operand.NumericConstant;
import adql.query.operand.Operation;
import adql.query.operand.OperationType;
import adql.query.operand.StringConstant;
import adql.query.operand.WrappedOperand;
import adql.query.operand.function.DefaultUDF;
import adql.query.operand.function.MathFunction;
import adql.query.operand.function.MathFunctionType;
import adql.query.operand.function.SQLFunction;
import adql.query.operand.function.SQLFunctionType;
import adql.query.operand.function.geometry.BoxFunction;
import adql.query.operand.function.geometry.CircleFunction;
import adql.query.operand.function.geometry.PointFunction;
import adql.query.operand.function.geometry.PolygonFunction;
import adql.query.operand.function.geometry.RegionFunction;
import adql.search.IReplaceHandler;
import adql.search.ISearchHandler;
import adql.search.SearchColumnHandler;
@@ -124,4 +140,103 @@ public class TestADQLQuery {
			assertEquals(expectedCol, results.next());
		assertEquals("SELECT (O.nameObj || ' (' || NewTypeObj || ')') AS Nom objet , O.ra , O.dec\nFROM truc.ObsCore AS O\nWHERE ra/dec > 1 AND (NewTypeObj = 'Star' OR NewTypeObj LIKE 'Galaxy*')\nORDER BY 1 DESC", query.toADQL());
	}

	@Test
	public void testTypeResultingColumns(){
		ADQLQuery query = new ADQLQuery();
		query.setFrom(new ADQLTable("foo"));
		ClauseSelect select = new ClauseSelect();
		query.setSelect(select);

		// Test with a numeric constant:
		select.add(new NumericConstant(2.3));
		assertEquals(1, query.getResultingColumns().length);
		assertEquals(DBDatatype.UNKNOWN_NUMERIC, query.getResultingColumns()[0].getDatatype().type);

		// Test with a math operation:
		select.clear();
		select.add(new Operation(new Operation(new NumericConstant(2), OperationType.MULT, new NumericConstant(3.14)), OperationType.DIV, new NumericConstant(5)));
		assertEquals(1, query.getResultingColumns().length);
		assertEquals(DBDatatype.UNKNOWN_NUMERIC, query.getResultingColumns()[0].getDatatype().type);

		// Test with a math function:
		try{
			select.clear();
			select.add(new MathFunction(MathFunctionType.SQRT, new ADQLColumn("col1")));
			assertEquals(1, query.getResultingColumns().length);
			assertEquals(DBDatatype.UNKNOWN_NUMERIC, query.getResultingColumns()[0].getDatatype().type);
		}catch(Exception ex){
			ex.printStackTrace();
			fail("The mathematical function SQRT is well defined. This error should have occurred.");
		}

		// Test with an aggregation function:
		select.clear();
		select.add(new SQLFunction(SQLFunctionType.SUM, new ADQLColumn("col1")));
		assertEquals(1, query.getResultingColumns().length);
		assertEquals(DBDatatype.UNKNOWN_NUMERIC, query.getResultingColumns()[0].getDatatype().type);

		// Test with a string constant:
		select.clear();
		select.add(new StringConstant("blabla"));
		assertEquals(1, query.getResultingColumns().length);
		assertEquals(DBDatatype.VARCHAR, query.getResultingColumns()[0].getDatatype().type);

		// Test with a concatenation:
		select.clear();
		Concatenation concat = new Concatenation();
		concat.add(new StringConstant("super "));
		concat.add(new ADQLColumn("foo", "col"));
		select.add(concat);
		assertEquals(1, query.getResultingColumns().length);
		assertEquals(DBDatatype.VARCHAR, query.getResultingColumns()[0].getDatatype().type);

		// Test with a POINT:
		try{
			select.clear();
			select.add(new PointFunction(new StringConstant(""), new ADQLColumn("ra"), new ADQLColumn("dec")));
			assertEquals(1, query.getResultingColumns().length);
			assertEquals(DBDatatype.POINT, query.getResultingColumns()[0].getDatatype().type);
		}catch(Exception ex){
			ex.printStackTrace();
			fail("The POINT function is well defined. This error should have occurred.");
		}

		// Test with a REGION (CIRCLE, BOX, POLYGON and REGION functions):
		try{
			select.clear();
			select.add(new CircleFunction(new StringConstant(""), new ADQLColumn("ra"), new ADQLColumn("dec"), new NumericConstant(1)));
			select.add(new BoxFunction(new StringConstant(""), new ADQLColumn("ra"), new ADQLColumn("dec"), new NumericConstant(10), new NumericConstant(20)));
			ADQLOperand[] points = new ADQLOperand[6];
			points[0] = new ADQLColumn("point1");
			points[1] = new ADQLColumn("point2");
			points[2] = new ADQLColumn("point3");
			points[3] = new ADQLColumn("point4");
			points[4] = new ADQLColumn("point5");
			points[5] = new ADQLColumn("point6");
			select.add(new PolygonFunction(new StringConstant(""), points));
			select.add(new RegionFunction(new StringConstant("CIRCLE '' ra dec 2.3")));
			assertEquals(4, query.getResultingColumns().length);
			for(int i = 0; i < 4; i++)
				assertEquals(DBDatatype.REGION, query.getResultingColumns()[i].getDatatype().type);
		}catch(Exception ex){
			ex.printStackTrace();
			fail("The geometrical functions are well defined. This error should have occurred.");
		}

		// Test with a UDF having no definition:
		select.clear();
		select.add(new DefaultUDF("foo", new ADQLOperand[0]));
		assertEquals(1, query.getResultingColumns().length);
		assertNull(query.getResultingColumns()[0].getDatatype());

		// Test with a UDF having a definition:
		select.clear();
		DefaultUDF udf = new DefaultUDF("foo", new ADQLOperand[0]);
		udf.setDefinition(new FunctionDef("foo", new DBType(DBDatatype.INTEGER)));
		select.add(udf);
		assertEquals(1, query.getResultingColumns().length);
		assertEquals(DBDatatype.INTEGER, query.getResultingColumns()[0].getDatatype().type);

	}
}
Loading