++
++
diff --git a/test/adql/parser/TestADQLParser.java b/test/adql/parser/TestADQLParser.java
new file mode 100644
-index 0000000..10837d5
+index 0000000..3ccfff9
--- /dev/null
+++ b/test/adql/parser/TestADQLParser.java
-@@ -0,0 +1,43 @@
+@@ -0,0 +1,308 @@
+package adql.parser;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
++import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import org.junit.After;
@@ -1921,6 +2218,8 @@
+import org.junit.Test;
+
+import adql.query.ADQLQuery;
++import adql.query.from.ADQLJoin;
++import adql.query.from.ADQLTable;
+import adql.query.operand.StringConstant;
+
+public class TestADQLParser {
@@ -1931,58 +2230,1318 @@
+ @AfterClass
+ public static void tearDownAfterClass() throws Exception{}
+
-+ @Before
-+ public void setUp() throws Exception{}
++ @Before
++ public void setUp() throws Exception{}
++
++ @After
++ public void tearDown() throws Exception{}
++
++ @Test
++ public void testColumnReference(){
++ ADQLParser parser = new ADQLParser();
++ try{
++ // ORDER BY
++ parser.parseQuery("SELECT * FROM cat ORDER BY oid;");
++ parser.parseQuery("SELECT * FROM cat ORDER BY oid ASC;");
++ parser.parseQuery("SELECT * FROM cat ORDER BY oid DESC;");
++ parser.parseQuery("SELECT * FROM cat ORDER BY 1;");
++ parser.parseQuery("SELECT * FROM cat ORDER BY 1 ASC;");
++ parser.parseQuery("SELECT * FROM cat ORDER BY 1 DESC;");
++ // GROUP BY
++ parser.parseQuery("SELECT * FROM cat GROUP BY oid;");
++ parser.parseQuery("SELECT * FROM cat GROUP BY cat.oid;");
++ // JOIN ... USING(...)
++ parser.parseQuery("SELECT * FROM cat JOIN cat2 USING(oid);");
++ }catch(Exception e){
++ e.printStackTrace(System.err);
++ fail("These ADQL queries are strictly correct! No error should have occured. (see stdout for more details)");
++ }
++
++ try{
++ // ORDER BY
++ parser.parseQuery("SELECT * FROM cat ORDER BY cat.oid;");
++ fail("A qualified column name is forbidden in ORDER BY! This test should have failed.");
++ }catch(Exception e){
++ assertEquals(ParseException.class, e.getClass());
++ assertEquals(" Encountered \".\". Was expecting one of: \",\" \";\" \"ASC\" \"DESC\" ", e.getMessage());
++ }
++
++ // Query reported as in error before the bug correction:
++ try{
++ parser.parseQuery("SELECT TOP 10 browndwarfs.cat.jmag FROM browndwarfs.cat ORDER BY browndwarfs.cat.jmag");
++ fail("A qualified column name is forbidden in ORDER BY! This test should have failed.");
++ }catch(Exception e){
++ assertEquals(ParseException.class, e.getClass());
++ assertEquals(" Encountered \".\". Was expecting one of: \",\" \";\" \"ASC\" \"DESC\" ", e.getMessage());
++ }
++
++ try{
++ // GROUP BY with a SELECT item index
++ parser.parseQuery("SELECT * FROM cat GROUP BY 1;");
++ fail("A SELECT item index is forbidden in GROUP BY! This test should have failed.");
++ }catch(Exception e){
++ assertEquals(ParseException.class, e.getClass());
++ assertEquals(" Encountered \"1\". Was expecting one of: \"\\\"\" ", e.getMessage());
++ }
++
++ try{
++ // JOIN ... USING(...)
++ parser.parseQuery("SELECT * FROM cat JOIN cat2 USING(cat.oid);");
++ fail("A qualified column name is forbidden in USING(...)! This test should have failed.");
++ }catch(Exception e){
++ assertEquals(ParseException.class, e.getClass());
++ assertEquals(" Encountered \".\". Was expecting one of: \")\" \",\" ", e.getMessage());
++ }
++
++ try{
++ // JOIN ... USING(...)
++ parser.parseQuery("SELECT * FROM cat JOIN cat2 USING(1);");
++ fail("A column index is forbidden in USING(...)! This test should have failed.");
++ }catch(Exception e){
++ assertEquals(ParseException.class, e.getClass());
++ assertEquals(" Encountered \"1\". Was expecting one of: \"\\\"\" ", e.getMessage());
++ }
++ }
++
++ @Test
++ public void testDelimitedIdentifiersWithDot(){
++ ADQLParser parser = new ADQLParser();
++ try{
++ ADQLQuery query = parser.parseQuery("SELECT * FROM \"B/avo.rad/catalog\";");
++ assertEquals("B/avo.rad/catalog", query.getFrom().getTables().get(0).getTableName());
++ }catch(Exception e){
++ e.printStackTrace(System.err);
++ fail("The ADQL query is strictly correct! No error should have occured. (see stdout for more details)");
++ }
++ }
++
++ @Test
++ public void testJoinTree(){
++ ADQLParser parser = new ADQLParser();
++ try{
++ String[] queries = new String[]{"SELECT * FROM aTable A JOIN aSecondTable B ON A.id = B.id JOIN aThirdTable C ON B.id = C.id;","SELECT * FROM aTable A NATURAL JOIN aSecondTable B NATURAL JOIN aThirdTable C;"};
++ for(String q : queries){
++ ADQLQuery query = parser.parseQuery(q);
++
++ assertTrue(query.getFrom() instanceof ADQLJoin);
++
++ ADQLJoin join = ((ADQLJoin)query.getFrom());
++ assertTrue(join.getLeftTable() instanceof ADQLJoin);
++ assertTrue(join.getRightTable() instanceof ADQLTable);
++ assertEquals("aThirdTable", ((ADQLTable)join.getRightTable()).getTableName());
++
++ join = (ADQLJoin)join.getLeftTable();
++ assertTrue(join.getLeftTable() instanceof ADQLTable);
++ assertEquals("aTable", ((ADQLTable)join.getLeftTable()).getTableName());
++ assertTrue(join.getRightTable() instanceof ADQLTable);
++ assertEquals("aSecondTable", ((ADQLTable)join.getRightTable()).getTableName());
++ }
++ }catch(Exception e){
++ e.printStackTrace(System.err);
++ fail("The ADQL query is strictly correct! No error should have occured. (see stdout for more details)");
++ }
++ }
++
++ @Test
++ public void test(){
++ ADQLParser parser = new ADQLParser();
++ try{
++ ADQLQuery query = parser.parseQuery("SELECT 'truc''machin' 'bidule' --- why not a comment now ^^\n'FIN' FROM foo;");
++ assertNotNull(query);
++ assertEquals("truc'machinbiduleFIN", ((StringConstant)(query.getSelect().get(0).getOperand())).getValue());
++ assertEquals("'truc''machinbiduleFIN'", query.getSelect().get(0).getOperand().toADQL());
++ }catch(Exception ex){
++ fail("String litteral concatenation is perfectly legal according to the ADQL standard.");
++ }
++
++ // With a comment ending the query
++ try{
++ ADQLQuery query = parser.parseQuery("SELECT TOP 1 * FROM ivoa.ObsCore -- comment");
++ assertNotNull(query);
++ }catch(Exception ex){
++ ex.printStackTrace();
++ fail("String litteral concatenation is perfectly legal according to the ADQL standard.");
++ }
++ }
++
++ @Test
++ public void testIncorrectCharacter(){
++ /* An identifier must be written only with digits, an underscore or
++ * regular latin characters: */
++ try{
++ (new ADQLParser()).parseQuery("select gr\u00e9gory FROM aTable");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().startsWith("Incorrect character encountered at l.1, c.10:"));
++ }
++
++ // But in a string, delimited identifier or a comment, it is fine:
++ try{
++ (new ADQLParser()).parseQuery("select 'grégory' FROM aTable");
++ (new ADQLParser()).parseQuery("select \"grégory\" FROM aTable");
++ (new ADQLParser()).parseQuery("select * FROM aTable -- a comment by Grégory");
++ }catch(Throwable t){
++ fail("This error should never occurs because all these queries have an accentuated character but at a correct place.");
++ }
++ }
++
++ @Test
++ public void testMultipleSpacesInOrderAndGroupBy(){
++ try{
++ ADQLParser parser = new ADQLParser();
++
++ // Single space:
++ parser.parseQuery("select * from aTable ORDER BY aCol");
++ parser.parseQuery("select * from aTable GROUP BY aCol");
++
++ // More than one space:
++ parser.parseQuery("select * from aTable ORDER BY aCol");
++ parser.parseQuery("select * from aTable GROUP BY aCol");
++
++ // With any other space character:
++ parser.parseQuery("select * from aTable ORDER\tBY aCol");
++ parser.parseQuery("select * from aTable ORDER\nBY aCol");
++ parser.parseQuery("select * from aTable ORDER \t\nBY aCol");
++
++ parser.parseQuery("select * from aTable GROUP\tBY aCol");
++ parser.parseQuery("select * from aTable GROUP\nBY aCol");
++ parser.parseQuery("select * from aTable GROUP \t\nBY aCol");
++ }catch(Throwable t){
++ t.printStackTrace();
++ fail("Having multiple space characters between the ORDER/GROUP and the BY keywords should not generate any parsing error.");
++ }
++ }
++
++ @Test
++ public void testADQLReservedWord(){
++ ADQLParser parser = new ADQLParser();
++
++ final String hintAbs = "\n(HINT: \"abs\" is a reserved ADQL word. To use it as a column/table/schema name/alias, write it between double quotes.)";
++ final String hintPoint = "\n(HINT: \"point\" is a reserved ADQL word. To use it as a column/table/schema name/alias, write it between double quotes.)";
++ final String hintExists = "\n(HINT: \"exists\" is a reserved ADQL word. To use it as a column/table/schema name/alias, write it between double quotes.)";
++ final String hintLike = "\n(HINT: \"LIKE\" is a reserved ADQL word. To use it as a column/table/schema name/alias, write it between double quotes.)";
++
++ /* TEST AS A COLUMN/TABLE/SCHEMA NAME... */
++ // ...with a numeric function name (but no param):
++ try{
++ parser.parseQuery("select abs from aTable");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith(hintAbs));
++ }
++ // ...with a geometric function name (but no param):
++ try{
++ parser.parseQuery("select point from aTable");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith(hintPoint));
++ }
++ // ...with an ADQL function name (but no param):
++ try{
++ parser.parseQuery("select exists from aTable");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith(hintExists));
++ }
++ // ...with an ADQL syntax item:
++ try{
++ parser.parseQuery("select LIKE from aTable");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith(hintLike));
++ }
++
++ /* TEST AS AN ALIAS... */
++ // ...with a numeric function name (but no param):
++ try{
++ parser.parseQuery("select aCol AS abs from aTable");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith(hintAbs));
++ }
++ // ...with a geometric function name (but no param):
++ try{
++ parser.parseQuery("select aCol AS point from aTable");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith(hintPoint));
++ }
++ // ...with an ADQL function name (but no param):
++ try{
++ parser.parseQuery("select aCol AS exists from aTable");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith(hintExists));
++ }
++ // ...with an ADQL syntax item:
++ try{
++ parser.parseQuery("select aCol AS LIKE from aTable");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith(hintLike));
++ }
++
++ /* TEST AT THE END OF THE QUERY (AND IN A WHERE) */
++ try{
++ parser.parseQuery("select aCol from aTable WHERE toto = abs");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith(hintAbs));
++ }
++ }
++
++ @Test
++ public void testSQLReservedWord(){
++ ADQLParser parser = new ADQLParser();
++
++ try{
++ parser.parseQuery("SELECT rows FROM aTable");
++ fail("\"ROWS\" is an SQL reserved word. This query should not pass.");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith("\n(HINT: \"rows\" is not supported in ADQL, but is however a reserved word. To use it as a column/table/schema name/alias, write it between double quotes.)"));
++ }
++
++ try{
++ parser.parseQuery("SELECT CASE WHEN aCol = 2 THEN 'two' ELSE 'smth else' END as str FROM aTable");
++ fail("ADQL does not support the CASE syntax. This query should not pass.");
++ }catch(Throwable t){
++ assertEquals(ParseException.class, t.getClass());
++ assertTrue(t.getMessage().endsWith("\n(HINT: \"CASE\" is not supported in ADQL, but is however a reserved word. To use it as a column/table/schema name/alias, write it between double quotes.)"));
++ }
++ }
++
++}
+diff --git a/test/adql/parser/TestIdentifierItem.java b/test/adql/parser/TestIdentifierItem.java
+new file mode 100644
+index 0000000..9c0575f
+--- /dev/null
++++ b/test/adql/parser/TestIdentifierItem.java
+@@ -0,0 +1,34 @@
++package adql.parser;
++
++import static adql.parser.ADQLParserConstants.DELIMITED_IDENTIFIER;
++import static adql.parser.ADQLParserConstants.REGULAR_IDENTIFIER;
++import static org.junit.Assert.assertEquals;
++
++import org.junit.Before;
++import org.junit.BeforeClass;
++import org.junit.Test;
++
++import adql.parser.IdentifierItems.IdentifierItem;
++
++public class TestIdentifierItem {
++
++ @BeforeClass
++ public static void setUpBeforeClass() throws Exception{}
++
++ @Before
++ public void setUp() throws Exception{}
++
++ @Test
++ public void testIdentifierItem(){
++ /* A regular identifier (with no special characters) should be returned
++ * as provided: */
++ IdentifierItem identifier = new IdentifierItem(new Token(REGULAR_IDENTIFIER, "m50"), false);
++ assertEquals("m50", identifier.toString());
++
++ /* Ensure doubled double quotes are escaped
++ * (i.e. considered as a single double quote): */
++ identifier = new IdentifierItem(new Token(DELIMITED_IDENTIFIER, "m50\"\""), true);
++ assertEquals("m50\"", identifier.toString());
++ }
++
++}
+diff --git a/test/adql/parser/TestUnknownTypes.java b/test/adql/parser/TestUnknownTypes.java
+new file mode 100644
+index 0000000..2116da0
+--- /dev/null
++++ b/test/adql/parser/TestUnknownTypes.java
+@@ -0,0 +1,163 @@
++package adql.parser;
++
++import static org.junit.Assert.assertEquals;
++import static org.junit.Assert.assertFalse;
++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 TestUnknownTypes {
++
++ @BeforeClass
++ public static void setUpBeforeClass() throws Exception{}
++
++ @AfterClass
++ public static void tearDownAfterClass() throws Exception{
++ DBType.DBDatatype.UNKNOWN.setCustomType(null);
++ }
++
++ @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());
++ assertFalse(fct.isString());
++ assertFalse(fct.isNumeric());
++ assertFalse(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());
++ assertFalse(fct.getParam(0).type.isString());
++ assertFalse(fct.getParam(0).type.isNumeric());
++ assertFalse(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), FOO(C4), C1, C2, C3, C4 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));
++ table1.addColumn(new DefaultDBColumn("C3", new DBType(DBDatatype.VARCHAR), table1));
++ table1.addColumn(new DefaultDBColumn("C4", new DBType(DBDatatype.UNKNOWN_NUMERIC), table1));
++ Collection 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());
++ assertFalse(col.getDatatype().isNumeric());
++ assertFalse(col.getDatatype().isString());
++ assertFalse(col.getDatatype().isGeometry());
++ assertEquals("UNKNOWN", col.getDatatype().toString());
++
++ // Check the type of the column T1.C4:
++ col = table1.getColumn("C4", true);
++ assertNotNull(col);
++ assertNotNull(col.getDatatype());
++ assertTrue(col.getDatatype().isUnknown());
++ assertTrue(col.getDatatype().isNumeric());
++ assertFalse(col.getDatatype().isString());
++ assertFalse(col.getDatatype().isGeometry());
++ assertEquals("UNKNOWN_NUMERIC", col.getDatatype().toString());
++
++ // Define a UDF, and allow all geometrical functions and coordinate systems:
++ FunctionDef udf1 = FunctionDef.parse("FOO(x INTEGER) -> INTEGER");
++ Collection udfList = Arrays.asList(new FunctionDef[]{udf1});
++ Collection geoList = null;
++ Collection 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);
++
++ /* Ensure the type of every ADQLColumn is as expected: */
++ // isNumeric() = true for FOO(C1), but false for the others
++ assertTrue(pq.getSelect().get(0).getOperand().isNumeric());
++ assertFalse(pq.getSelect().get(0).getOperand().isString());
++ assertFalse(pq.getSelect().get(0).getOperand().isGeometry());
++ // isNumeric() = true for FOO(C2), but false for the others
++ assertTrue(pq.getSelect().get(1).getOperand().isNumeric());
++ assertFalse(pq.getSelect().get(1).getOperand().isString());
++ assertFalse(pq.getSelect().get(1).getOperand().isGeometry());
++ // isNumeric() = true for FOO(C4), but false for the others
++ assertTrue(pq.getSelect().get(2).getOperand().isNumeric());
++ assertFalse(pq.getSelect().get(2).getOperand().isString());
++ assertFalse(pq.getSelect().get(2).getOperand().isGeometry());
++ // isNumeric() = isString() = isGeometry() for C1
++ assertTrue(pq.getSelect().get(3).getOperand().isNumeric());
++ assertTrue(pq.getSelect().get(3).getOperand().isString());
++ assertTrue(pq.getSelect().get(3).getOperand().isGeometry());
++ // isNumeric() = isString() = isGeometry() for C2
++ assertTrue(pq.getSelect().get(4).getOperand().isNumeric());
++ assertTrue(pq.getSelect().get(4).getOperand().isString());
++ assertTrue(pq.getSelect().get(4).getOperand().isGeometry());
++ // isString() = true for C3, but false for the others
++ assertFalse(pq.getSelect().get(5).getOperand().isNumeric());
++ assertTrue(pq.getSelect().get(5).getOperand().isString());
++ assertFalse(pq.getSelect().get(5).getOperand().isGeometry());
++ // isString() = true for C4, but false for the others
++ assertTrue(pq.getSelect().get(6).getOperand().isNumeric());
++ assertFalse(pq.getSelect().get(6).getOperand().isString());
++ assertFalse(pq.getSelect().get(6).getOperand().isGeometry());
++ }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)");
++ }
++ }
++}
+diff --git a/test/adql/query/TestADQLObjectPosition.java b/test/adql/query/TestADQLObjectPosition.java
+new file mode 100644
+index 0000000..a03b24c
+--- /dev/null
++++ b/test/adql/query/TestADQLObjectPosition.java
+@@ -0,0 +1,140 @@
++package adql.query;
++
++import static org.junit.Assert.assertEquals;
++import static org.junit.Assert.fail;
++
++import java.util.Iterator;
++import java.util.List;
++
++import org.junit.Before;
++import org.junit.Test;
++
++import adql.parser.ADQLParser;
++import adql.parser.ParseException;
++import adql.query.constraint.Comparison;
++import adql.query.from.ADQLJoin;
++import adql.query.from.ADQLTable;
++import adql.query.operand.ADQLColumn;
++import adql.query.operand.ADQLOperand;
++import adql.query.operand.function.ADQLFunction;
++import adql.search.SimpleSearchHandler;
++
++public class TestADQLObjectPosition {
++
++ private ADQLParser parser = new ADQLParser();
++
++ @Before
++ public void setUp(){
++
++ }
++
++ @Test
++ public void testPositionInAllClauses(){
++ try{
++ ADQLQuery query = parser.parseQuery("SELECT truc, bidule.machin, toto(truc, chose) AS \"super\" FROM foo JOIN bidule USING(id) WHERE truc > 12.5 AND bidule.machin < 5 GROUP BY chose HAVING try > 0 ORDER BY chouetteAlors");
++
++ Iterator results = query.search(new SimpleSearchHandler(true){
++ @Override
++ protected boolean match(ADQLObject obj){
++ return obj.getPosition() == null;
++ }
++ });
++ if (results.hasNext()){
++ System.err.println("OBJECT WITH NO DEFINED POSITION:");
++ while(results.hasNext())
++ System.err.println(" * " + results.next().toADQL());
++ fail("At least one item of the generated ADQL tree does not have a position information! (see System.err for more details)");
++ }
++ }catch(ParseException pe){
++ pe.printStackTrace();
++ fail("No error should have occured here: the ADQL query is syntactically correct!");
++ }
++ }
++
++ private void assertEquality(final TextPosition expected, final TextPosition realPos){
++ assertEquals(expected.beginLine, realPos.beginLine);
++ assertEquals(expected.beginColumn, realPos.beginColumn);
++ assertEquals(expected.endLine, realPos.endLine);
++ assertEquals(expected.endColumn, realPos.endColumn);
++ }
++
++ @Test
++ public void testPositionAccuracy(){
++ try{
++ ADQLQuery query = parser.parseQuery("SELECT TOP 1000 oid FROM foo JOIN bar USING(oid)\nWHERE foo || toto = 'truc'\n AND 2 > 1+0 GROUP BY oid HAVING COUNT(oid) > 10\n\tORDER BY 1 DESC");
++ // Test SELECT
++ assertEquality(new TextPosition(1, 1, 1, 20), query.getSelect().getPosition());
++ // Test ADQLColumn (here: "oid")
++ assertEquality(new TextPosition(1, 17, 1, 20), query.getSelect().get(0).getPosition());
++ // Test FROM & ADQLJoin
++ /* NB: The clause FROM is the only one which is not a list but a single item of type FromContent (JOIN or table).
++ * That's why, it is not possible to get its exact starting position ('FROM') ; the starting position is
++ * the one of the first table of the clause FROM. */
++ assertEquality(new TextPosition(1, 26, 1, 49), query.getFrom().getPosition());
++ // Test ADQLTable
++ List tables = query.getFrom().getTables();
++ assertEquality(new TextPosition(1, 26, 1, 29), tables.get(0).getPosition());
++ assertEquality(new TextPosition(1, 35, 1, 38), tables.get(1).getPosition());
++ // Test the join condition:
++ Iterator itCol = ((ADQLJoin)query.getFrom()).getJoinedColumns();
++ assertEquality(new TextPosition(1, 45, 1, 48), itCol.next().getPosition());
++ // Test WHERE
++ assertEquality(new TextPosition(2, 1, 3, 18), query.getWhere().getPosition());
++ // Test COMPARISON = CONSTRAINT
++ Comparison comp = (Comparison)(query.getWhere().get(0));
++ assertEquality(new TextPosition(2, 7, 2, 27), comp.getPosition());
++ // Test left operand = concatenation:
++ ADQLOperand operand = comp.getLeftOperand();
++ assertEquality(new TextPosition(2, 7, 2, 18), operand.getPosition());
++ Iterator itObj = operand.adqlIterator();
++ // foo
++ assertEquality(new TextPosition(2, 7, 2, 10), itObj.next().getPosition());
++ // toto
++ assertEquality(new TextPosition(2, 14, 2, 18), itObj.next().getPosition());
++ // Test right operand = string:
++ operand = comp.getRightOperand();
++ assertEquality(new TextPosition(2, 21, 2, 27), operand.getPosition());
++ // Test COMPARISON > CONSTRAINT:
++ comp = (Comparison)(query.getWhere().get(1));
++ assertEquality(new TextPosition(3, 11, 3, 18), comp.getPosition());
++ // Test left operand = numeric:
++ operand = comp.getLeftOperand();
++ assertEquality(new TextPosition(3, 11, 3, 12), operand.getPosition());
++ // Test right operand = operation:
++ operand = comp.getRightOperand();
++ assertEquality(new TextPosition(3, 15, 3, 18), operand.getPosition());
++ itObj = operand.adqlIterator();
++ // 1
++ assertEquality(new TextPosition(3, 15, 3, 16), itObj.next().getPosition());
++ // 0
++ assertEquality(new TextPosition(3, 17, 3, 18), itObj.next().getPosition());
++ // Test GROUP BY
++ assertEquality(new TextPosition(3, 19, 3, 31), query.getGroupBy().getPosition());
++ // oid
++ assertEquality(new TextPosition(3, 28, 3, 31), query.getGroupBy().get(0).getPosition());
++ // Test HAVING
++ assertEquality(new TextPosition(3, 32, 3, 54), query.getHaving().getPosition());
++ // Test COMPARISON > CONSTRAINT:
++ comp = (Comparison)(query.getHaving().get(0));
++ assertEquality(new TextPosition(3, 39, 3, 54), comp.getPosition());
++ // Test left operand = COUNT function:
++ operand = comp.getLeftOperand();
++ assertEquality(new TextPosition(3, 39, 3, 49), operand.getPosition());
++ // Test parameter = ADQLColumn oid:
++ assertEquality(new TextPosition(3, 45, 3, 48), ((ADQLFunction)operand).getParameter(0).getPosition());
++ // Test right operand = operation:
++ operand = comp.getRightOperand();
++ assertEquality(new TextPosition(3, 52, 3, 54), operand.getPosition());
++ // Test ORDER BY
++ assertEquality(new TextPosition(4, 9, 4, 24), query.getOrderBy().getPosition());
++ // Test column index:
++ assertEquality(new TextPosition(4, 18, 4, 19), query.getOrderBy().get(0).getPosition());
++
++ }catch(ParseException pe){
++ System.err.println("ERROR IN THE ADQL QUERY AT " + pe.getPosition());
++ pe.printStackTrace();
++ fail("No error should have occured here: the ADQL query is syntactically correct!");
++ }
++ }
++
++}
+diff --git a/test/adql/query/TestADQLQuery.java b/test/adql/query/TestADQLQuery.java
+new file mode 100644
+index 0000000..272622e
+--- /dev/null
++++ b/test/adql/query/TestADQLQuery.java
+@@ -0,0 +1,240 @@
++package adql.query;
++
++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;
++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.constraint.Comparison;
++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.CentroidFunction;
++import adql.query.operand.function.geometry.CircleFunction;
++import adql.query.operand.function.geometry.GeometryFunction;
++import adql.query.operand.function.geometry.GeometryFunction.GeometryValue;
++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;
++import adql.search.SimpleReplaceHandler;
++
++public class TestADQLQuery {
++ private ADQLQuery query = null;
++ private List columns = new ArrayList(8);
++ private List typeObjColumns = new ArrayList(3);
++
++ @Before
++ public void setUp(){
++ query = new ADQLQuery();
++ columns.clear();
++ typeObjColumns.clear();
++
++ columns.add(new ADQLColumn("O", "nameObj")); // 0 = O.nameObj
++ columns.add(new ADQLColumn("O", "typeObj")); // 1 = O.typeObj
++ columns.add(new ADQLColumn("O", "ra")); // 2 = O.ra
++ columns.add(new ADQLColumn("O", "dec")); // 3 = O.dec
++ columns.add(new ADQLColumn("ra")); // 4 = ra
++ columns.add(new ADQLColumn("dec")); // 5 = dec
++ columns.add(new ADQLColumn("typeObj")); // 6 = typeObj
++ columns.add(new ADQLColumn("typeObj")); // 7 = typeObj
++
++ typeObjColumns.add(columns.get(1));
++ typeObjColumns.add(columns.get(6));
++ typeObjColumns.add(columns.get(7));
++
++ // SELECT:
++ ClauseSelect select = query.getSelect();
++ Concatenation concatObj = new Concatenation();
++ concatObj.add(columns.get(0)); // O.nameObj
++ concatObj.add(new StringConstant(" ("));
++ concatObj.add(columns.get(1)); // O.typeObj
++ concatObj.add(new StringConstant(")"));
++ select.add(new SelectItem(new WrappedOperand(concatObj), "Nom objet"));
++ select.add(columns.get(2)); // O.ra
++ select.add(columns.get(3)); // O.dec
++
++ // FROM:
++ ADQLTable table = new ADQLTable("truc.ObsCore");
++ table.setAlias("O");
++ // table.setJoin(new ADQLJoin(JoinType.INNER, new ADQLTable("VO")));
++ query.setFrom(table);
++
++ // WHERE:
++ ClauseConstraints where = query.getWhere();
++ // ra/dec > 1
++ where.add(new Comparison(new Operation(columns.get(4), OperationType.DIV, columns.get(5)), ComparisonOperator.GREATER_THAN, new NumericConstant("1")));
++ ConstraintsGroup constOr = new ConstraintsGroup();
++ // AND (typeObj == 'Star'
++ constOr.add(new Comparison(columns.get(6), ComparisonOperator.EQUAL, new StringConstant("Star")));
++ // OR typeObj LIKE 'Galaxy*')
++ constOr.add("OR", new Comparison(columns.get(7), ComparisonOperator.LIKE, new StringConstant("Galaxy*")));
++ where.add("AND", constOr);
++
++ // ORDER BY:
++ ClauseADQL orderBy = query.getOrderBy();
++ orderBy.add(new ADQLOrder(1, true));
++ }
++
++ @Test
++ public void testADQLQuery(){
++ assertEquals("SELECT (O.nameObj || ' (' || O.typeObj || ')') AS Nom objet , O.ra , O.dec\nFROM truc.ObsCore AS O\nWHERE ra/dec > 1 AND (typeObj = 'Star' OR typeObj LIKE 'Galaxy*')\nORDER BY 1 DESC", query.toADQL());
++ }
++
++ @Test
++ public void testSearch(){
++ ISearchHandler sHandler = new SearchColumnHandler(false);
++ Iterator results = query.search(sHandler);
++ assertEquals(columns.size(), sHandler.getNbMatch());
++ for(ADQLColumn expectedCol : columns)
++ assertEquals(expectedCol, results.next());
++ }
++
++ @Test
++ public void testReplace(){
++ IReplaceHandler sHandler = new SimpleReplaceHandler(false, false){
++ @Override
++ protected boolean match(ADQLObject obj){
++ return (obj instanceof ADQLColumn) && (((ADQLColumn)obj).getColumnName().equalsIgnoreCase("typeObj"));
++ }
++
++ @Override
++ public ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException{
++ return new ADQLColumn("NewTypeObj");
++ }
++ };
++ sHandler.searchAndReplace(query);
++ assertEquals(typeObjColumns.size(), sHandler.getNbMatch());
++ assertEquals(sHandler.getNbMatch(), sHandler.getNbReplacement());
++ Iterator results = sHandler.iterator();
++ for(ADQLColumn expectedCol : typeObjColumns)
++ 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")));
++ select.add(new CentroidFunction(new GeometryValue(new ADQLColumn("aRegion"))));
++ assertEquals(2, query.getResultingColumns().length);
++ for(int i = 0; i < 2; i++)
++ assertEquals(DBDatatype.POINT, query.getResultingColumns()[i].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);
++
++ }
++}
+diff --git a/test/adql/query/TestIdentifierField.java b/test/adql/query/TestIdentifierField.java
+new file mode 100644
+index 0000000..2885d10
+--- /dev/null
++++ b/test/adql/query/TestIdentifierField.java
+@@ -0,0 +1,25 @@
++package adql.query;
++
++import static org.junit.Assert.assertFalse;
++import static org.junit.Assert.assertTrue;
++
++import org.junit.Test;
++
++import adql.query.IdentifierField;
++
++public class TestIdentifierField {
++
++ @Test
++ public void testIsCaseSensitive(){
++ byte b = 0x00;
++ assertFalse(IdentifierField.SCHEMA.isCaseSensitive(b));
++ b = IdentifierField.SCHEMA.setCaseSensitive(b, true);
++ assertTrue(IdentifierField.SCHEMA.isCaseSensitive(b));
++ }
++
++ /*@Test
++ public void testSetCaseSensitive(){
++ fail("Not yet implemented");
++ }*/
++
++}
+diff --git a/test/adql/query/constraint/TestIN.java b/test/adql/query/constraint/TestIN.java
+new file mode 100644
+index 0000000..ab385db
+--- /dev/null
++++ b/test/adql/query/constraint/TestIN.java
+@@ -0,0 +1,95 @@
++package adql.query.constraint;
++
++import static org.junit.Assert.assertEquals;
++import static org.junit.Assert.fail;
++
++import java.util.Iterator;
++
++import org.junit.BeforeClass;
++import org.junit.Test;
++
++import adql.query.ADQLList;
++import adql.query.ADQLObject;
++import adql.query.ADQLOrder;
++import adql.query.ADQLQuery;
++import adql.query.ClauseSelect;
++import adql.query.constraint.In;
++import adql.query.from.ADQLTable;
++import adql.query.operand.ADQLColumn;
++import adql.query.operand.ADQLOperand;
++import adql.query.operand.StringConstant;
++import adql.search.IReplaceHandler;
++import adql.search.SimpleReplaceHandler;
++import adql.translator.ADQLTranslator;
++import adql.translator.PostgreSQLTranslator;
++
++public class TestIN {
++
++ private static ADQLTranslator translator = null;
++
++ @BeforeClass
++ public static void setUpBeforeClass(){
++ translator = new PostgreSQLTranslator();
++ }
++
++ @Test
++ public void testIN(){
++ // Test with a simple list of values (here, string constants):
++ In myIn = new In(new ADQLColumn("typeObj"), new ADQLOperand[]{new StringConstant("galaxy"),new StringConstant("star"),new StringConstant("planet"),new StringConstant("nebula")}, true);
++ // check the ADQL:
++ assertEquals("typeObj NOT IN ('galaxy' , 'star' , 'planet' , 'nebula')", myIn.toADQL());
++ // check the SQL translation:
++ try{
++ assertEquals(myIn.toADQL(), translator.translate(myIn));
++ }catch(Exception ex){
++ ex.printStackTrace();
++ fail("This test should have succeeded because the IN statement is correct and theoretically well supported by the POSTGRESQL translator!");
++ }
++
++ // Test with a sub-query:
++ ADQLQuery subQuery = new ADQLQuery();
++
++ ClauseSelect select = subQuery.getSelect();
++ select.setDistinctColumns(true);
++ select.setLimit(10);
++ select.add(new ADQLColumn("typeObj"));
++
++ subQuery.setFrom(new ADQLTable("Objects"));
++
++ ADQLList orderBy = subQuery.getOrderBy();
++ orderBy.add(new ADQLOrder(1));
++
++ myIn.setSubQuery(subQuery);
++ // check the ADQL:
++ assertEquals("typeObj NOT IN (SELECT DISTINCT TOP 10 typeObj\nFROM Objects\nORDER BY 1 ASC)", myIn.toADQL());
++ // check the SQL translation:
++ try{
++ assertEquals("typeObj NOT IN (SELECT DISTINCT typeObj AS \"typeObj\"\nFROM Objects\nORDER BY 1 ASC\nLimit 10)", translator.translate(myIn));
++ }catch(Exception ex){
++ ex.printStackTrace();
++ fail("This test should have succeeded because the IN statement is correct and theoretically well supported by the POSTGRESQL translator!");
++ }
++
++ // Test after replacement inside this IN statement:
++ IReplaceHandler sHandler = new SimpleReplaceHandler(true){
++
++ @Override
++ public boolean match(ADQLObject obj){
++ return (obj instanceof ADQLColumn) && ((ADQLColumn)obj).getColumnName().equals("typeObj");
++ }
++
++ @Override
++ public ADQLObject getReplacer(ADQLObject objToReplace){
++ return new ADQLColumn("type");
++ }
++ };
++ sHandler.searchAndReplace(myIn);
++ assertEquals(2, sHandler.getNbMatch());
++ assertEquals(sHandler.getNbMatch(), sHandler.getNbReplacement());
++ Iterator results = sHandler.iterator();
++ while(results.hasNext())
++ assertEquals("typeObj", results.next().toADQL());
++ assertEquals("type NOT IN (SELECT DISTINCT TOP 10 type\nFROM Objects\nORDER BY 1 ASC)", myIn.toADQL());
++ }
++
++}
+diff --git a/test/adql/query/from/TestCrossJoin.java b/test/adql/query/from/TestCrossJoin.java
+new file mode 100644
+index 0000000..ce440bc
+--- /dev/null
++++ b/test/adql/query/from/TestCrossJoin.java
+@@ -0,0 +1,95 @@
++package adql.query.from;
++
++import static org.junit.Assert.assertEquals;
++import static org.junit.Assert.assertNotNull;
++import static org.junit.Assert.fail;
++
++import java.util.List;
++
++import org.junit.AfterClass;
++import org.junit.Before;
++import org.junit.Test;
++
++import adql.db.DBColumn;
++import adql.db.DBType;
++import adql.db.DBType.DBDatatype;
++import adql.db.DefaultDBColumn;
++import adql.db.DefaultDBTable;
++import adql.db.SearchColumnList;
++import adql.query.IdentifierField;
++
++public class TestCrossJoin {
++
++ private ADQLTable tableA, tableB;
++
++ @AfterClass
++ public static void tearDownAfterClass() throws Exception{}
++
++ @Before
++ public void setUp() throws Exception{
++ /* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */
++ // Describe the available table:
++ DefaultDBTable metaTableA = new DefaultDBTable("A");
++ metaTableA.setADQLSchemaName("public");
++ DefaultDBTable metaTableB = new DefaultDBTable("B");
++ metaTableB.setADQLSchemaName("public");
++
++ // Describe its columns:
++ metaTableA.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableA));
++ metaTableA.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableA));
++ metaTableB.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableB));
++ metaTableB.addColumn(new DefaultDBColumn("txtb", new DBType(DBDatatype.VARCHAR), metaTableB));
++
++ // Build the ADQL tables:
++ tableA = new ADQLTable("A");
++ tableA.setDBLink(metaTableA);
++ tableB = new ADQLTable("B");
++ tableB.setDBLink(metaTableB);
++ }
++
++ @Test
++ public void testGetDBColumns(){
++ try{
++ ADQLJoin join = new CrossJoin(tableA, tableB);
++ SearchColumnList joinColumns = join.getDBColumns();
++ assertEquals(4, joinColumns.size());
++
++ // check column A.id and B.id
++ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(2, lstFound.size());
++ // A.id
++ assertNotNull(lstFound.get(0).getTable());
++ assertEquals("A", lstFound.get(0).getTable().getADQLName());
++ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
++ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ // B.id
++ assertNotNull(lstFound.get(1).getTable());
++ assertEquals("B", lstFound.get(1).getTable().getADQLName());
++ assertEquals("public", lstFound.get(1).getTable().getADQLSchemaName());
++ assertEquals(1, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
++
++ // check column A.txta
++ lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertNotNull(lstFound.get(0).getTable());
++ assertEquals("A", lstFound.get(0).getTable().getADQLName());
++ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
++ assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "B", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++
++ // check column B.txtb
++ lstFound = joinColumns.search(null, null, null, "txtb", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertNotNull(lstFound.get(0).getTable());
++ assertEquals("B", lstFound.get(0).getTable().getADQLName());
++ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
++ assertEquals(1, joinColumns.search(null, "public", "B", "txtb", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "A", "txtb", IdentifierField.getFullCaseSensitive(true)).size());
++
++ }catch(Exception ex){
++ ex.printStackTrace();
++ fail("This test should have succeeded!");
++ }
++ }
++}
+diff --git a/test/adql/query/from/TestInnerJoin.java b/test/adql/query/from/TestInnerJoin.java
+new file mode 100644
+index 0000000..57c4f87
+--- /dev/null
++++ b/test/adql/query/from/TestInnerJoin.java
+@@ -0,0 +1,158 @@
++package adql.query.from;
++
++import static org.junit.Assert.assertEquals;
++import static org.junit.Assert.assertNotNull;
++import static org.junit.Assert.fail;
++
++import java.util.ArrayList;
++import java.util.List;
++
++import org.junit.Before;
++import org.junit.Test;
++
++import adql.db.DBColumn;
++import adql.db.DBCommonColumn;
++import adql.db.DBType;
++import adql.db.DBType.DBDatatype;
++import adql.db.DefaultDBColumn;
++import adql.db.DefaultDBTable;
++import adql.db.SearchColumnList;
++import adql.query.IdentifierField;
++import adql.query.operand.ADQLColumn;
++
++public class TestInnerJoin {
++
++ private ADQLTable tableA, tableB, tableC;
++
++ @Before
++ public void setUp() throws Exception{
++ /* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */
++ // Describe the available table:
++ DefaultDBTable metaTableA = new DefaultDBTable("A");
++ metaTableA.setADQLSchemaName("public");
++ DefaultDBTable metaTableB = new DefaultDBTable("B");
++ metaTableB.setADQLSchemaName("public");
++ DefaultDBTable metaTableC = new DefaultDBTable("C");
++ metaTableC.setADQLSchemaName("public");
++
++ // Describe its columns:
++ metaTableA.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableA));
++ metaTableA.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableA));
++ metaTableB.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableB));
++ metaTableB.addColumn(new DefaultDBColumn("txtb", new DBType(DBDatatype.VARCHAR), metaTableB));
++ metaTableC.addColumn(new DefaultDBColumn("Id", new DBType(DBDatatype.VARCHAR), metaTableC));
++ metaTableC.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableC));
++ metaTableC.addColumn(new DefaultDBColumn("txtc", new DBType(DBDatatype.VARCHAR), metaTableC));
++
++ // Build the ADQL tables:
++ tableA = new ADQLTable("A");
++ tableA.setDBLink(metaTableA);
++ tableB = new ADQLTable("B");
++ tableB.setDBLink(metaTableB);
++ tableC = new ADQLTable("C");
++ tableC.setDBLink(metaTableC);
++ }
++
++ @Test
++ public void testGetDBColumns(){
++ // Test NATURAL JOIN 1:
++ try{
++ ADQLJoin join = new InnerJoin(tableA, tableB);
++ SearchColumnList joinColumns = join.getDBColumns();
++ assertEquals(3, joinColumns.size());
++ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertEquals(DBCommonColumn.class, lstFound.get(0).getClass());
++ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(1, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ lstFound = joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ lstFound = joinColumns.search(null, "public", "B", "txtb", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ }catch(Exception ex){
++ ex.printStackTrace();
++ fail("This test should have succeeded!");
++ }
++
++ // Test NATURAL JOIN 2:
++ try{
++ ADQLJoin join = new InnerJoin(tableA, tableC);
++ SearchColumnList joinColumns = join.getDBColumns();
++ assertEquals(3, joinColumns.size());
++
++ // check id (column common to table A and C only):
++ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertEquals(DBCommonColumn.class, lstFound.get(0).getClass());
++ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(1, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++
++ // check txta (column common to table A and C only):
++ lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertEquals(DBCommonColumn.class, lstFound.get(0).getClass());
++ assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(1, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++
++ // check txtc (only for table C)
++ lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertNotNull(lstFound.get(0).getTable());
++ assertEquals("C", lstFound.get(0).getTable().getADQLName());
++ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
++
++ }catch(Exception ex){
++ ex.printStackTrace();
++ fail("This test should have succeeded!");
++ }
++
++ // Test with a USING("id"):
++ try{
++ List usingList = new ArrayList(1);
++ usingList.add(new ADQLColumn("id"));
++ ADQLJoin join = new InnerJoin(tableA, tableC, usingList);
++ SearchColumnList joinColumns = join.getDBColumns();
++ assertEquals(4, joinColumns.size());
++
++ // check id (column common to table A and C only):
++ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertEquals(DBCommonColumn.class, lstFound.get(0).getClass());
++ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(1, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++
++ // check A.txta and C.txta:
++ lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(2, lstFound.size());
++ // A.txta
++ assertNotNull(lstFound.get(0).getTable());
++ assertEquals("A", lstFound.get(0).getTable().getADQLName());
++ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
++ assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++ // C.txta
++ assertNotNull(lstFound.get(1).getTable());
++ assertEquals("C", lstFound.get(1).getTable().getADQLName());
++ assertEquals("public", lstFound.get(1).getTable().getADQLSchemaName());
++ assertEquals(1, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "B", "txta", IdentifierField.getFullCaseSensitive(true)).size());
+
-+ @After
-+ public void tearDown() throws Exception{}
++ // check txtc (only for table C):
++ lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertNotNull(lstFound.get(0).getTable());
++ assertEquals("C", lstFound.get(0).getTable().getADQLName());
++ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
++ assertEquals(1, joinColumns.search(null, "public", "C", "txtc", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "A", "txtc", IdentifierField.getFullCaseSensitive(true)).size());
+
-+ @Test
-+ public void test(){
-+ ADQLParser parser = new ADQLParser();
-+ try{
-+ ADQLQuery query = parser.parseQuery("SELECT 'truc''machin' 'bidule' -- why not a comment now ^^\n'FIN' FROM foo;");
-+ assertNotNull(query);
-+ assertEquals("truc'machinbiduleFIN", ((StringConstant)(query.getSelect().get(0).getOperand())).getValue());
-+ assertEquals("'truc''machinbiduleFIN'", query.getSelect().get(0).getOperand().toADQL());
+ }catch(Exception ex){
-+ fail("String litteral concatenation is perfectly legal according to the ADQL standard.");
++ ex.printStackTrace();
++ fail("This test should have succeeded!");
+ }
+ }
+
+}
-diff --git a/test/adql/query/from/TestCrossJoin.java b/test/adql/query/from/TestCrossJoin.java
+diff --git a/test/adql/query/from/TestSQLServer_InnerJoin.java b/test/adql/query/from/TestSQLServer_InnerJoin.java
new file mode 100644
-index 0000000..ce440bc
+index 0000000..70be938
--- /dev/null
-+++ b/test/adql/query/from/TestCrossJoin.java
-@@ -0,0 +1,95 @@
++++ b/test/adql/query/from/TestSQLServer_InnerJoin.java
+@@ -0,0 +1,159 @@
+package adql.query.from;
+
+import static org.junit.Assert.assertEquals;
++import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
++import java.util.ArrayList;
+import java.util.List;
+
-+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+
+import adql.db.DBColumn;
++import adql.db.DBCommonColumn;
+import adql.db.DBType;
+import adql.db.DBType.DBDatatype;
+import adql.db.DefaultDBColumn;
+import adql.db.DefaultDBTable;
+import adql.db.SearchColumnList;
+import adql.query.IdentifierField;
++import adql.query.operand.ADQLColumn;
+
-+public class TestCrossJoin {
-+
-+ private ADQLTable tableA, tableB;
++public class TestSQLServer_InnerJoin {
+
-+ @AfterClass
-+ public static void tearDownAfterClass() throws Exception{}
++ private ADQLTable tableA, tableB, tableC;
+
+ @Before
+ public void setUp() throws Exception{
@@ -1992,236 +3551,439 @@
+ metaTableA.setADQLSchemaName("public");
+ DefaultDBTable metaTableB = new DefaultDBTable("B");
+ metaTableB.setADQLSchemaName("public");
++ DefaultDBTable metaTableC = new DefaultDBTable("C");
++ metaTableC.setADQLSchemaName("public");
+
+ // Describe its columns:
+ metaTableA.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableA));
+ metaTableA.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableA));
+ metaTableB.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableB));
+ metaTableB.addColumn(new DefaultDBColumn("txtb", new DBType(DBDatatype.VARCHAR), metaTableB));
++ metaTableC.addColumn(new DefaultDBColumn("Id", new DBType(DBDatatype.VARCHAR), metaTableC));
++ metaTableC.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableC));
++ metaTableC.addColumn(new DefaultDBColumn("txtc", new DBType(DBDatatype.VARCHAR), metaTableC));
+
+ // Build the ADQL tables:
+ tableA = new ADQLTable("A");
+ tableA.setDBLink(metaTableA);
+ tableB = new ADQLTable("B");
+ tableB.setDBLink(metaTableB);
++ tableC = new ADQLTable("C");
++ tableC.setDBLink(metaTableC);
+ }
+
+ @Test
+ public void testGetDBColumns(){
++ // Test NATURAL JOIN 1:
+ try{
-+ ADQLJoin join = new CrossJoin(tableA, tableB);
++ ADQLJoin join = new SQLServer_InnerJoin(tableA, tableB);
+ SearchColumnList joinColumns = join.getDBColumns();
-+ assertEquals(4, joinColumns.size());
++ assertEquals(3, joinColumns.size());
++ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertNotEquals(DBCommonColumn.class, lstFound.get(0).getClass());
++ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ lstFound = joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ lstFound = joinColumns.search(null, "public", "B", "txtb", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ }catch(Exception ex){
++ ex.printStackTrace();
++ fail("This test should have succeeded!");
++ }
+
-+ // check column A.id and B.id
++ // Test NATURAL JOIN 2:
++ try{
++ ADQLJoin join = new SQLServer_InnerJoin(tableA, tableC);
++ SearchColumnList joinColumns = join.getDBColumns();
++ assertEquals(3, joinColumns.size());
++
++ // check id (only the column of table A should be found):
+ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(2, lstFound.size());
-+ // A.id
++ assertEquals(1, lstFound.size());
++ assertNotEquals(DBCommonColumn.class, lstFound.get(0).getClass());
++ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++
++ // check txta (only the column of table A should be found):
++ lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertNotEquals(DBCommonColumn.class, lstFound.get(0).getClass());
++ assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++
++ // check txtc (only for table C)
++ lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
+ assertNotNull(lstFound.get(0).getTable());
-+ assertEquals("A", lstFound.get(0).getTable().getADQLName());
++ assertEquals("C", lstFound.get(0).getTable().getADQLName());
+ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
++
++ }catch(Exception ex){
++ ex.printStackTrace();
++ fail("This test should have succeeded!");
++ }
++
++ // Test with a USING("id"):
++ try{
++ List usingList = new ArrayList(1);
++ usingList.add(new ADQLColumn("id"));
++ ADQLJoin join = new SQLServer_InnerJoin(tableA, tableC, usingList);
++ SearchColumnList joinColumns = join.getDBColumns();
++ assertEquals(4, joinColumns.size());
++
++ // check id (only the column of table A should be found):
++ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
++ assertEquals(1, lstFound.size());
++ assertNotEquals(DBCommonColumn.class, lstFound.get(0).getClass());
+ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
-+ // B.id
-+ assertNotNull(lstFound.get(1).getTable());
-+ assertEquals("B", lstFound.get(1).getTable().getADQLName());
-+ assertEquals("public", lstFound.get(1).getTable().getADQLSchemaName());
-+ assertEquals(1, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
+ assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
+
-+ // check column A.txta
++ // check A.txta and C.txta:
+ lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(1, lstFound.size());
++ assertEquals(2, lstFound.size());
++ // A.txta
+ assertNotNull(lstFound.get(0).getTable());
+ assertEquals("A", lstFound.get(0).getTable().getADQLName());
+ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
+ assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++ // C.txta
++ assertNotNull(lstFound.get(1).getTable());
++ assertEquals("C", lstFound.get(1).getTable().getADQLName());
++ assertEquals("public", lstFound.get(1).getTable().getADQLSchemaName());
++ assertEquals(1, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size());
+ assertEquals(0, joinColumns.search(null, "public", "B", "txta", IdentifierField.getFullCaseSensitive(true)).size());
+
-+ // check column B.txtb
-+ lstFound = joinColumns.search(null, null, null, "txtb", IdentifierField.getFullCaseSensitive(true));
++ // check txtc (only for table C):
++ lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true));
+ assertEquals(1, lstFound.size());
+ assertNotNull(lstFound.get(0).getTable());
-+ assertEquals("B", lstFound.get(0).getTable().getADQLName());
++ assertEquals("C", lstFound.get(0).getTable().getADQLName());
+ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
-+ assertEquals(1, joinColumns.search(null, "public", "B", "txtb", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(0, joinColumns.search(null, "public", "A", "txtb", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(1, joinColumns.search(null, "public", "C", "txtc", IdentifierField.getFullCaseSensitive(true)).size());
++ assertEquals(0, joinColumns.search(null, "public", "A", "txtc", IdentifierField.getFullCaseSensitive(true)).size());
+
+ }catch(Exception ex){
+ ex.printStackTrace();
+ fail("This test should have succeeded!");
+ }
+ }
++
+}
-diff --git a/test/adql/query/from/TestInnerJoin.java b/test/adql/query/from/TestInnerJoin.java
+diff --git a/test/adql/query/operand/function/geometry/TestCentroidFunction.java b/test/adql/query/operand/function/geometry/TestCentroidFunction.java
new file mode 100644
-index 0000000..57c4f87
+index 0000000..ee17950
--- /dev/null
-+++ b/test/adql/query/from/TestInnerJoin.java
-@@ -0,0 +1,158 @@
-+package adql.query.from;
++++ b/test/adql/query/operand/function/geometry/TestCentroidFunction.java
+@@ -0,0 +1,28 @@
++package adql.query.operand.function.geometry;
++
++import static org.junit.Assert.assertFalse;
++import static org.junit.Assert.assertTrue;
++import static org.junit.Assert.fail;
++
++import org.junit.Test;
++
++import adql.query.operand.NumericConstant;
++import adql.query.operand.StringConstant;
++import adql.query.operand.function.geometry.GeometryFunction.GeometryValue;
++
++public class TestCentroidFunction {
++
++ @Test
++ public void testIsGeometry(){
++ try{
++ CentroidFunction centfc = new CentroidFunction(new GeometryValue(new CircleFunction(new StringConstant("ICRS"), new NumericConstant(128.23), new NumericConstant(0.53), new NumericConstant(2))));
++ assertTrue(centfc.isGeometry());
++ assertFalse(centfc.isNumeric());
++ assertFalse(centfc.isString());
++ }catch(Throwable t){
++ t.printStackTrace(System.err);
++ fail("An error occured while building a simple CentroidFunction! (see the console for more details)");
++ }
++ }
++
++}
+diff --git a/test/adql/search/TestSimpleReplaceHandler.java b/test/adql/search/TestSimpleReplaceHandler.java
+new file mode 100644
+index 0000000..785fddf
+--- /dev/null
++++ b/test/adql/search/TestSimpleReplaceHandler.java
+@@ -0,0 +1,121 @@
++package adql.search;
+
+import static org.junit.Assert.assertEquals;
-+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
-+import java.util.ArrayList;
-+import java.util.List;
++import org.junit.Before;
++import org.junit.Test;
++
++import adql.parser.ADQLParser;
++import adql.query.ADQLObject;
++import adql.query.ADQLQuery;
++import adql.query.operand.function.DefaultUDF;
++import adql.query.operand.function.MathFunction;
++import adql.query.operand.function.MathFunctionType;
++
++public class TestSimpleReplaceHandler {
++
++ @Before
++ public void setUp() throws Exception{}
++
++ @Test
++ public void testReplaceRecursiveMatch(){
++ /* WHY THIS TEST?
++ *
++ * When a match item had also a match item inside it (e.g. function parameter or sub-query),
++ * both matched items (e.g. the parent and the child) must be replaced.
++ *
++ * However, if the parent is replaced first, the reference of the new parent is lost by the SimpleReplaceHandler and so,
++ * the replacement of the child will be performed on the former parent. Thus, after the whole process,
++ * in the final ADQL query, the replacement of the child won't be visible since the former parent is
++ * not referenced any more.
++ */
++
++ String testQuery = "SELECT SQRT(ABS(81)) FROM myTable";
++ try{
++ // Parse the query:
++ ADQLQuery query = (new ADQLParser()).parseQuery(testQuery);
++
++ // Check it is as expected, before the replacements:
++ assertEquals(testQuery, query.toADQL().replaceAll("\\n", " "));
++
++ // Create a replace handler:
++ SimpleReplaceHandler replaceHandler = new SimpleReplaceHandler(){
++ @Override
++ protected boolean match(ADQLObject obj){
++ return obj instanceof MathFunction;
++ }
++
++ @Override
++ protected ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException{
++ return new DefaultUDF("foo", ((MathFunction)objToReplace).getParameters());
++ }
++ };
++
++ // Apply the replacement of all mathematical functions by a dumb UDF:
++ replaceHandler.searchAndReplace(query);
++ assertEquals(2, replaceHandler.getNbMatch());
++ assertEquals(replaceHandler.getNbMatch(), replaceHandler.getNbReplacement());
++ assertEquals("SELECT foo(foo(81)) FROM myTable", query.toADQL().replaceAll("\\n", " "));
++
++ }catch(Exception ex){
++ ex.printStackTrace(System.err);
++ fail("No error should have occured here since nothing is wrong in the ADQL query used for the test. See the stack trace in the console for more details.");
++ }
++
++ }
++
++ @Test
++ public void testWrappingReplacement(){
++ /* WHY THIS TEST?
++ *
++ * In case you just want to wrap a matched object, you replace it by the wrapping object initialized
++ * with the matched object.
++ *
++ * In a first version, the replacement was done and then the ReplaceHandler was going inside the new object to replace
++ * other matching objects. But of course, it will find again the first matched object and will wrap it again, and so on
++ * indefinitely => "nasty" infinite loop.
++ *
++ * So, the replacement of the matched objects should be always done after having looked inside it.
++ */
++
++ String testQuery = "SELECT foo(bar(123)) FROM myTable";
++ try{
++ // Parse the query:
++ ADQLQuery query = (new ADQLParser()).parseQuery(testQuery);
++
++ // Check it is as expected, before the replacements:
++ assertEquals(testQuery, query.toADQL().replaceAll("\\n", " "));
++
++ // Create a replace handler:
++ SimpleReplaceHandler replaceHandler = new SimpleReplaceHandler(){
++ @Override
++ protected boolean match(ADQLObject obj){
++ return obj instanceof DefaultUDF && ((DefaultUDF)obj).getName().toLowerCase().matches("(foo|bar)");
++ }
++
++ @Override
++ protected ADQLObject getReplacer(ADQLObject objToReplace) throws UnsupportedOperationException{
++ try{
++ return new MathFunction(MathFunctionType.ROUND, (DefaultUDF)objToReplace);
++ }catch(Exception e){
++ e.printStackTrace(System.err);
++ fail("No error should have occured here since nothing is wrong in the ADQL query used for the test. See the stack trace in the console for more details.");
++ return null;
++ }
++ }
++ };
++
++ // Apply the wrapping:
++ replaceHandler.searchAndReplace(query);
++ assertEquals(2, replaceHandler.getNbMatch());
++ assertEquals(replaceHandler.getNbMatch(), replaceHandler.getNbReplacement());
++ assertEquals("SELECT ROUND(foo(ROUND(bar(123)))) FROM myTable", query.toADQL().replaceAll("\\n", " "));
++
++ }catch(Exception ex){
++ ex.printStackTrace(System.err);
++ fail("No error should have occured here since nothing is wrong in the ADQL query used for the test. See the stack trace in the console for more details.");
++ }
++ }
++
++}
+diff --git a/test/adql/translator/TestJDBCTranslator.java b/test/adql/translator/TestJDBCTranslator.java
+new file mode 100644
+index 0000000..577b11f
+--- /dev/null
++++ b/test/adql/translator/TestJDBCTranslator.java
+@@ -0,0 +1,136 @@
++package adql.translator;
++
++import static org.junit.Assert.assertEquals;
++import static org.junit.Assert.fail;
++
++import org.junit.Before;
++import org.junit.Test;
++
++import adql.db.DBType;
++import adql.db.STCS.Region;
++import adql.parser.ParseException;
++import adql.query.IdentifierField;
++import adql.query.operand.StringConstant;
++import adql.query.operand.function.geometry.AreaFunction;
++import adql.query.operand.function.geometry.BoxFunction;
++import adql.query.operand.function.geometry.CentroidFunction;
++import adql.query.operand.function.geometry.CircleFunction;
++import adql.query.operand.function.geometry.ContainsFunction;
++import adql.query.operand.function.geometry.DistanceFunction;
++import adql.query.operand.function.geometry.ExtractCoord;
++import adql.query.operand.function.geometry.ExtractCoordSys;
++import adql.query.operand.function.geometry.IntersectsFunction;
++import adql.query.operand.function.geometry.PointFunction;
++import adql.query.operand.function.geometry.PolygonFunction;
++import adql.query.operand.function.geometry.RegionFunction;
++
++public class TestJDBCTranslator {
++
++ @Before
++ public void setUp() throws Exception{}
++
++ @Test
++ public void testTranslateStringConstant(){
++ JDBCTranslator tr = new AJDBCTranslator();
++
++ /* Ensure the translation from ADQL to SQL of strings is correct ;
++ * particularly, ' should be escaped otherwise it would mean the end of a string in SQL
++ *(the way to escape a such character is by doubling the character '): */
++ try{
++ assertEquals("'SQL''s translation'", tr.translate(new StringConstant("SQL's translation")));
++ }catch(TranslationException e){
++ e.printStackTrace(System.err);
++ fail("There should have been no problem to translate a StringConstant object into SQL.");
++ }
++ }
+
-+import org.junit.Before;
-+import org.junit.Test;
++ public final static class AJDBCTranslator extends JDBCTranslator {
+
-+import adql.db.DBColumn;
-+import adql.db.DBCommonColumn;
-+import adql.db.DBType;
-+import adql.db.DBType.DBDatatype;
-+import adql.db.DefaultDBColumn;
-+import adql.db.DefaultDBTable;
-+import adql.db.SearchColumnList;
-+import adql.query.IdentifierField;
-+import adql.query.operand.ADQLColumn;
++ @Override
++ public String translate(ExtractCoord extractCoord) throws TranslationException{
++ return null;
++ }
+
-+public class TestInnerJoin {
++ @Override
++ public String translate(ExtractCoordSys extractCoordSys) throws TranslationException{
++ return null;
++ }
+
-+ private ADQLTable tableA, tableB, tableC;
++ @Override
++ public String translate(AreaFunction areaFunction) throws TranslationException{
++ return null;
++ }
+
-+ @Before
-+ public void setUp() throws Exception{
-+ /* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */
-+ // Describe the available table:
-+ DefaultDBTable metaTableA = new DefaultDBTable("A");
-+ metaTableA.setADQLSchemaName("public");
-+ DefaultDBTable metaTableB = new DefaultDBTable("B");
-+ metaTableB.setADQLSchemaName("public");
-+ DefaultDBTable metaTableC = new DefaultDBTable("C");
-+ metaTableC.setADQLSchemaName("public");
++ @Override
++ public String translate(CentroidFunction centroidFunction) throws TranslationException{
++ return null;
++ }
+
-+ // Describe its columns:
-+ metaTableA.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableA));
-+ metaTableA.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableA));
-+ metaTableB.addColumn(new DefaultDBColumn("id", new DBType(DBDatatype.VARCHAR), metaTableB));
-+ metaTableB.addColumn(new DefaultDBColumn("txtb", new DBType(DBDatatype.VARCHAR), metaTableB));
-+ metaTableC.addColumn(new DefaultDBColumn("Id", new DBType(DBDatatype.VARCHAR), metaTableC));
-+ metaTableC.addColumn(new DefaultDBColumn("txta", new DBType(DBDatatype.VARCHAR), metaTableC));
-+ metaTableC.addColumn(new DefaultDBColumn("txtc", new DBType(DBDatatype.VARCHAR), metaTableC));
++ @Override
++ public String translate(DistanceFunction fct) throws TranslationException{
++ return null;
++ }
+
-+ // Build the ADQL tables:
-+ tableA = new ADQLTable("A");
-+ tableA.setDBLink(metaTableA);
-+ tableB = new ADQLTable("B");
-+ tableB.setDBLink(metaTableB);
-+ tableC = new ADQLTable("C");
-+ tableC.setDBLink(metaTableC);
-+ }
++ @Override
++ public String translate(ContainsFunction fct) throws TranslationException{
++ return null;
++ }
+
-+ @Test
-+ public void testGetDBColumns(){
-+ // Test NATURAL JOIN 1:
-+ try{
-+ ADQLJoin join = new InnerJoin(tableA, tableB);
-+ SearchColumnList joinColumns = join.getDBColumns();
-+ assertEquals(3, joinColumns.size());
-+ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(1, lstFound.size());
-+ assertEquals(DBCommonColumn.class, lstFound.get(0).getClass());
-+ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(1, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(0, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
-+ lstFound = joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(1, lstFound.size());
-+ lstFound = joinColumns.search(null, "public", "B", "txtb", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(1, lstFound.size());
-+ }catch(Exception ex){
-+ ex.printStackTrace();
-+ fail("This test should have succeeded!");
++ @Override
++ public String translate(IntersectsFunction fct) throws TranslationException{
++ return null;
+ }
+
-+ // Test NATURAL JOIN 2:
-+ try{
-+ ADQLJoin join = new InnerJoin(tableA, tableC);
-+ SearchColumnList joinColumns = join.getDBColumns();
-+ assertEquals(3, joinColumns.size());
++ @Override
++ public String translate(PointFunction point) throws TranslationException{
++ return null;
++ }
+
-+ // check id (column common to table A and C only):
-+ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(1, lstFound.size());
-+ assertEquals(DBCommonColumn.class, lstFound.get(0).getClass());
-+ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(1, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ @Override
++ public String translate(CircleFunction circle) throws TranslationException{
++ return null;
++ }
+
-+ // check txta (column common to table A and C only):
-+ lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(1, lstFound.size());
-+ assertEquals(DBCommonColumn.class, lstFound.get(0).getClass());
-+ assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(1, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ @Override
++ public String translate(BoxFunction box) throws TranslationException{
++ return null;
++ }
+
-+ // check txtc (only for table C)
-+ lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(1, lstFound.size());
-+ assertNotNull(lstFound.get(0).getTable());
-+ assertEquals("C", lstFound.get(0).getTable().getADQLName());
-+ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
++ @Override
++ public String translate(PolygonFunction polygon) throws TranslationException{
++ return null;
++ }
+
-+ }catch(Exception ex){
-+ ex.printStackTrace();
-+ fail("This test should have succeeded!");
++ @Override
++ public String translate(RegionFunction region) throws TranslationException{
++ return null;
+ }
+
-+ // Test with a USING("id"):
-+ try{
-+ List usingList = new ArrayList(1);
-+ usingList.add(new ADQLColumn("id"));
-+ ADQLJoin join = new InnerJoin(tableA, tableC, usingList);
-+ SearchColumnList joinColumns = join.getDBColumns();
-+ assertEquals(4, joinColumns.size());
++ @Override
++ public boolean isCaseSensitive(IdentifierField field){
++ return false;
++ }
+
-+ // check id (column common to table A and C only):
-+ List lstFound = joinColumns.search(null, null, null, "id", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(1, lstFound.size());
-+ assertEquals(DBCommonColumn.class, lstFound.get(0).getClass());
-+ assertEquals(1, joinColumns.search(null, "public", "A", "id", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(1, joinColumns.search(null, "public", "C", "id", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(0, joinColumns.search(null, "public", "B", "id", IdentifierField.getFullCaseSensitive(true)).size());
++ @Override
++ public DBType convertTypeFromDB(int dbmsType, String rawDbmsTypeName, String dbmsTypeName, String[] typeParams){
++ return null;
++ }
+
-+ // check A.txta and C.txta:
-+ lstFound = joinColumns.search(null, null, null, "txta", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(2, lstFound.size());
-+ // A.txta
-+ assertNotNull(lstFound.get(0).getTable());
-+ assertEquals("A", lstFound.get(0).getTable().getADQLName());
-+ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
-+ assertEquals(1, joinColumns.search(null, "public", "A", "txta", IdentifierField.getFullCaseSensitive(true)).size());
-+ // C.txta
-+ assertNotNull(lstFound.get(1).getTable());
-+ assertEquals("C", lstFound.get(1).getTable().getADQLName());
-+ assertEquals("public", lstFound.get(1).getTable().getADQLSchemaName());
-+ assertEquals(1, joinColumns.search(null, "public", "C", "txta", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(0, joinColumns.search(null, "public", "B", "txta", IdentifierField.getFullCaseSensitive(true)).size());
++ @Override
++ public String convertTypeToDB(DBType type){
++ return null;
++ }
+
-+ // check txtc (only for table C):
-+ lstFound = joinColumns.search(null, null, null, "txtc", IdentifierField.getFullCaseSensitive(true));
-+ assertEquals(1, lstFound.size());
-+ assertNotNull(lstFound.get(0).getTable());
-+ assertEquals("C", lstFound.get(0).getTable().getADQLName());
-+ assertEquals("public", lstFound.get(0).getTable().getADQLSchemaName());
-+ assertEquals(1, joinColumns.search(null, "public", "C", "txtc", IdentifierField.getFullCaseSensitive(true)).size());
-+ assertEquals(0, joinColumns.search(null, "public", "A", "txtc", IdentifierField.getFullCaseSensitive(true)).size());
++ @Override
++ public Region translateGeometryFromDB(Object jdbcColValue) throws ParseException{
++ return null;
++ }
+
-+ }catch(Exception ex){
-+ ex.printStackTrace();
-+ fail("This test should have succeeded!");
++ @Override
++ public Object translateGeometryToDB(Region region) throws ParseException{
++ return null;
+ }
++
+ }
+
+}
diff --git a/test/adql/translator/TestPgSphereTranslator.java b/test/adql/translator/TestPgSphereTranslator.java
new file mode 100644
-index 0000000..2f34471
+index 0000000..346ed80
--- /dev/null
+++ b/test/adql/translator/TestPgSphereTranslator.java
-@@ -0,0 +1,332 @@
+@@ -0,0 +1,350 @@
+package adql.translator;
+
+import static org.junit.Assert.assertEquals;
@@ -2243,6 +4005,12 @@
+import adql.db.DBType.DBDatatype;
+import adql.db.STCS.Region;
+import adql.parser.ParseException;
++import adql.query.operand.NumericConstant;
++import adql.query.operand.StringConstant;
++import adql.query.operand.function.geometry.CentroidFunction;
++import adql.query.operand.function.geometry.CircleFunction;
++import adql.query.operand.function.geometry.GeometryFunction;
++import adql.query.operand.function.geometry.GeometryFunction.GeometryValue;
+
+public class TestPgSphereTranslator {
+
@@ -2259,6 +4027,18 @@
+ public void tearDown() throws Exception{}
+
+ @Test
++ public void testTranslateCentroidFunction(){
++ try{
++ PgSphereTranslator translator = new PgSphereTranslator();
++ CentroidFunction centfc = new CentroidFunction(new GeometryValue(new CircleFunction(new StringConstant("ICRS"), new NumericConstant(128.23), new NumericConstant(0.53), new NumericConstant(2))));
++ assertEquals("center(scircle(spoint(radians(128.23),radians(0.53)),radians(2)))", translator.translate(centfc));
++ }catch(Throwable t){
++ t.printStackTrace(System.err);
++ fail("An error occured while building a simple CentroidFunction! (see the console for more details)");
++ }
++ }
++
++ @Test
+ public void testConvertTypeFromDB(){
+ PgSphereTranslator translator = new PgSphereTranslator();
+
@@ -2554,254 +4334,163 @@
+ }
+
+}
-diff --git a/test/testtools/CommandExecute.java b/test/testtools/CommandExecute.java
+diff --git a/test/adql/translator/TestPostgreSQLTranslator.java b/test/adql/translator/TestPostgreSQLTranslator.java
new file mode 100644
-index 0000000..2e78978
+index 0000000..01fef8e
--- /dev/null
-+++ b/test/testtools/CommandExecute.java
-@@ -0,0 +1,51 @@
-+package testtools;
-+
-+import java.io.BufferedReader;
-+import java.io.InputStreamReader;
-+
-+/**
-+ * Let's execute any shell command (even with pipes and redirections).
-+ *
-+ * @author Grégory Mantelet (ARI)
-+ * @version 2.0 (09/2014)
-+ */
-+public final class CommandExecute {
-+
-+ /**
-+ * SINGLETON CLASS.
-+ * No instance of this class can be created.
-+ */
-+ private CommandExecute(){}
-+
-+ /**
-+ * Execute the given command (which may include pipe(s) and/or redirection(s)).
-+ *
-+ * @param command Command to execute in the shell.
-+ *
-+ * @return The string returned by the execution of the command.
-+ */
-+ public final static String execute(final String command){
-+
-+ String[] shellCmd = new String[]{"/bin/sh","-c",command};
-+
-+ StringBuffer output = new StringBuffer();
-+
-+ Process p;
-+ try{
-+ p = Runtime.getRuntime().exec(shellCmd);
-+ p.waitFor();
-+ BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
-+
-+ String line = "";
-+ while((line = reader.readLine()) != null){
-+ output.append(line + "\n");
-+ }
++++ b/test/adql/translator/TestPostgreSQLTranslator.java
+@@ -0,0 +1,62 @@
++package adql.translator;
+
-+ }catch(Exception e){
-+ e.printStackTrace();
-+ }
++import static org.junit.Assert.assertEquals;
++import static org.junit.Assert.fail;
+
-+ return output.toString();
++import org.junit.Before;
++import org.junit.Test;
+
-+ }
-+}
-diff --git a/test/testtools/DBTools.java b/test/testtools/DBTools.java
-new file mode 100644
-index 0000000..ba542f9
---- /dev/null
-+++ b/test/testtools/DBTools.java
-@@ -0,0 +1,136 @@
-+package testtools;
++import adql.query.operand.NumericConstant;
++import adql.query.operand.function.MathFunction;
++import adql.query.operand.function.MathFunctionType;
+
-+import java.sql.Connection;
-+import java.sql.DriverManager;
-+import java.sql.ResultSet;
-+import java.sql.SQLException;
-+import java.sql.Statement;
-+import java.util.HashMap;
-+
-+public final class DBTools {
-+
-+ public static int count = 0;
-+
-+ public final static void main(final String[] args) throws Throwable{
-+ for(int i = 0; i < 3; i++){
-+ Thread t = new Thread(new Runnable(){
-+ @Override
-+ public void run(){
-+ count++;
-+ try{
-+ Connection conn = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd");
-+ System.out.println("Start - " + count + ": ");
-+ String query = "SELECT * FROM gums.smc WHERE magg BETWEEN " + (15 + count) + " AND " + (20 + count) + ";";
-+ System.out.println(query);
-+ ResultSet rs = DBTools.select(conn, query);
-+ try{
-+ rs.last();
-+ System.out.println("Nb rows: " + rs.getRow());
-+ }catch(SQLException e){
-+ e.printStackTrace();
-+ }
-+ if (DBTools.closeConnection(conn))
-+ System.out.println("[DEBUG] Connection closed!");
-+ }catch(DBToolsException e){
-+ e.printStackTrace();
-+ }
-+ System.out.println("End - " + count);
-+ count--;
++public class TestPostgreSQLTranslator {
++
++ @Before
++ public void setUp() throws Exception{}
++
++ @Test
++ public void testTranslateMathFunction(){
++ // Check that all math functions, except PI, operates a cast to their DOUBLE/REAL parameters:
++ PostgreSQLTranslator trans = new PostgreSQLTranslator();
++ MathFunctionType[] types = MathFunctionType.values();
++ NumericConstant num = new NumericConstant("1.234"), prec = new NumericConstant("2");
++ for(MathFunctionType type : types){
++ try{
++ switch(type){
++ case PI:
++ assertEquals("PI()", trans.translate(new MathFunction(type)));
++ break;
++ case RAND:
++ assertEquals("random()", trans.translate(new MathFunction(type)));
++ assertEquals("random()", trans.translate(new MathFunction(type, num)));
++ break;
++ case LOG:
++ assertEquals("ln(CAST(1.234 AS numeric))", trans.translate(new MathFunction(type, num)));
++ break;
++ case LOG10:
++ assertEquals("log(10, CAST(1.234 AS numeric))", trans.translate(new MathFunction(type, num)));
++ break;
++ case TRUNCATE:
++ assertEquals("trunc(CAST(1.234 AS numeric))", trans.translate(new MathFunction(type, num)));
++ assertEquals("trunc(CAST(1.234 AS numeric), 2)", trans.translate(new MathFunction(type, num, prec)));
++ break;
++ case ROUND:
++ assertEquals("round(CAST(1.234 AS numeric))", trans.translate(new MathFunction(type, num)));
++ assertEquals("round(CAST(1.234 AS numeric), 2)", trans.translate(new MathFunction(type, num, prec)));
++ break;
++ default:
++ if (type.nbMaxParams() == 1 || type.nbMinParams() == 1)
++ assertEquals(type + "(CAST(1.234 AS numeric))", trans.translate(new MathFunction(type, num)));
++ if (type.nbMaxParams() == 2)
++ assertEquals(type + "(CAST(1.234 AS numeric), CAST(1.234 AS numeric))", trans.translate(new MathFunction(type, num, num)));
++ break;
+ }
-+ });
-+ t.start();
++ }catch(Exception ex){
++ ex.printStackTrace();
++ fail("Translation exception for the type \"" + type + "\": " + ex.getMessage());
++ }
+ }
+ }
+
-+ public static class DBToolsException extends Exception {
++}
+diff --git a/test/adql/translator/TestSQLServerTranslator.java b/test/adql/translator/TestSQLServerTranslator.java
+new file mode 100644
+index 0000000..ed605d8
+--- /dev/null
++++ b/test/adql/translator/TestSQLServerTranslator.java
+@@ -0,0 +1,86 @@
++package adql.translator;
+
-+ private static final long serialVersionUID = 1L;
++import static org.junit.Assert.assertEquals;
++import static org.junit.Assert.fail;
+
-+ public DBToolsException(){
-+ super();
-+ }
++import java.util.ArrayList;
++import java.util.List;
+
-+ public DBToolsException(String message, Throwable cause){
-+ super(message, cause);
-+ }
++import org.junit.Before;
++import org.junit.Test;
+
-+ public DBToolsException(String message){
-+ super(message);
-+ }
++import adql.db.DBChecker;
++import adql.db.DBTable;
++import adql.db.DefaultDBColumn;
++import adql.db.DefaultDBTable;
++import adql.parser.ADQLParser;
++import adql.parser.ParseException;
++import adql.parser.SQLServer_ADQLQueryFactory;
++import adql.query.ADQLQuery;
+
-+ public DBToolsException(Throwable cause){
-+ super(cause);
-+ }
++public class TestSQLServerTranslator {
+
-+ }
++ private List tables = null;
+
-+ public final static HashMap VALUE_JDBC_DRIVERS = new HashMap(4);
-+ static{
-+ VALUE_JDBC_DRIVERS.put("oracle", "oracle.jdbc.OracleDriver");
-+ VALUE_JDBC_DRIVERS.put("postgresql", "org.postgresql.Driver");
-+ VALUE_JDBC_DRIVERS.put("mysql", "com.mysql.jdbc.Driver");
-+ VALUE_JDBC_DRIVERS.put("sqlite", "org.sqlite.JDBC");
++ @Before
++ public void setUp() throws Exception{
++ tables = new ArrayList(2);
++ DefaultDBTable t = new DefaultDBTable("aTable");
++ t.addColumn(new DefaultDBColumn("id", t));
++ t.addColumn(new DefaultDBColumn("name", t));
++ t.addColumn(new DefaultDBColumn("aColumn", t));
++ tables.add(t);
++ t = new DefaultDBTable("anotherTable");
++ t.addColumn(new DefaultDBColumn("id", t));
++ t.addColumn(new DefaultDBColumn("name", t));
++ t.addColumn(new DefaultDBColumn("anotherColumn", t));
++ tables.add(t);
+ }
+
-+ private DBTools(){}
-+
-+ public final static Connection createConnection(String dbms, final String server, final String port, final String dbName, final String user, final String passwd) throws DBToolsException{
-+ // 1. Resolve the DBMS and get its JDBC driver:
-+ if (dbms == null)
-+ throw new DBToolsException("Missing DBMS (expected: oracle, postgresql, mysql or sqlite)!");
-+ dbms = dbms.toLowerCase();
-+ String jdbcDriver = VALUE_JDBC_DRIVERS.get(dbms);
-+ if (jdbcDriver == null)
-+ throw new DBToolsException("Unknown DBMS (\"" + dbms + "\")!");
-+
-+ // 2. Load the JDBC driver:
-+ try{
-+ Class.forName(jdbcDriver);
-+ }catch(ClassNotFoundException e){
-+ throw new DBToolsException("Impossible to load the JDBC driver: " + e.getMessage(), e);
-+ }
++ @Test
++ public void testNaturalJoin(){
++ final String adqlquery = "SELECT id, name, aColumn, anotherColumn FROM aTable A NATURAL JOIN anotherTable B;";
+
-+ // 3. Establish the connection:
-+ Connection connection = null;
+ try{
-+ connection = DriverManager.getConnection("jdbc:" + dbms + ":" + ((server != null && server.trim().length() > 0) ? "//" + server + ((port != null && port.trim().length() > 0) ? (":" + port) : "") + "/" : "") + dbName, user, passwd);
-+ }catch(SQLException e){
-+ throw new DBToolsException("Connection failed: " + e.getMessage(), e);
-+ }
++ ADQLQuery query = (new ADQLParser(new DBChecker(tables), new SQLServer_ADQLQueryFactory())).parseQuery(adqlquery);
++ SQLServerTranslator translator = new SQLServerTranslator();
+
-+ if (connection == null)
-+ throw new DBToolsException("Failed to make connection!");
++ // Test the FROM part:
++ assertEquals("\"aTable\" AS \"a\" INNER JOIN \"anotherTable\" AS \"b\" ON \"a\".\"id\"=\"b\".\"id\" AND \"a\".\"name\"=\"b\".\"name\"", translator.translate(query.getFrom()));
+
-+ return connection;
-+ }
++ // Test the SELECT part (in order to ensure the usual common columns (due to NATURAL) are actually translated as columns of the first joined table):
++ assertEquals("SELECT \"a\".\"id\" AS \"id\" , \"a\".\"name\" AS \"name\" , \"a\".\"aColumn\" AS \"aColumn\" , \"b\".\"anotherColumn\" AS \"anotherColumn\"", translator.translate(query.getSelect()));
+
-+ public final static boolean closeConnection(final Connection conn) throws DBToolsException{
-+ try{
-+ if (conn != null && !conn.isClosed()){
-+ conn.close();
-+ try{
-+ Thread.sleep(200);
-+ }catch(InterruptedException e){
-+ System.err.println("WARNING: can't wait/sleep before testing the connection close status! [" + e.getMessage() + "]");
-+ }
-+ return conn.isClosed();
-+ }else
-+ return true;
-+ }catch(SQLException e){
-+ throw new DBToolsException("Closing connection failed: " + e.getMessage(), e);
++ }catch(ParseException pe){
++ pe.printStackTrace();
++ fail("The given ADQL query is completely correct. No error should have occurred while parsing it. (see the console for more details)");
++ }catch(TranslationException te){
++ te.printStackTrace();
++ fail("No error was expected from this translation. (see the console for more details)");
+ }
+ }
+
-+ public final static ResultSet select(final Connection conn, final String selectQuery) throws DBToolsException{
-+ if (conn == null || selectQuery == null || selectQuery.trim().length() == 0)
-+ throw new DBToolsException("One parameter is missing!");
++ @Test
++ public void testJoinWithUSING(){
++ final String adqlquery = "SELECT B.id, name, aColumn, anotherColumn FROM aTable A JOIN anotherTable B USING(name);";
+
+ try{
-+ Statement stmt = conn.createStatement();
-+ return stmt.executeQuery(selectQuery);
-+ }catch(SQLException e){
-+ throw new DBToolsException("Can't execute the given SQL query: " + e.getMessage(), e);
-+ }
-+ }
++ ADQLQuery query = (new ADQLParser(new DBChecker(tables), new SQLServer_ADQLQueryFactory())).parseQuery(adqlquery);
++ SQLServerTranslator translator = new SQLServerTranslator();
+
-+}
-diff --git a/test/testtools/MD5Checksum.java b/test/testtools/MD5Checksum.java
-new file mode 100644
-index 0000000..d4941c5
---- /dev/null
-+++ b/test/testtools/MD5Checksum.java
-@@ -0,0 +1,46 @@
-+package testtools;
-+
-+import java.io.ByteArrayInputStream;
-+import java.io.InputStream;
-+import java.security.MessageDigest;
-+
-+public class MD5Checksum {
-+
-+ public static byte[] createChecksum(InputStream input) throws Exception{
-+ byte[] buffer = new byte[1024];
-+ MessageDigest complete = MessageDigest.getInstance("MD5");
-+ int numRead;
-+
-+ do{
-+ numRead = input.read(buffer);
-+ if (numRead > 0){
-+ complete.update(buffer, 0, numRead);
-+ }
-+ }while(numRead != -1);
-+ return complete.digest();
-+ }
++ // Test the FROM part:
++ assertEquals("\"aTable\" AS \"a\" INNER JOIN \"anotherTable\" AS \"b\" ON \"a\".\"name\"=\"b\".\"name\"", translator.translate(query.getFrom()));
+
-+ // see this How-to for a faster way to convert
-+ // a byte array to a HEX string
-+ public static String getMD5Checksum(InputStream input) throws Exception{
-+ byte[] b = createChecksum(input);
-+ String result = "";
++ // Test the SELECT part (in order to ensure the usual common columns (due to USING) are actually translated as columns of the first joined table):
++ assertEquals("SELECT \"b\".\"id\" AS \"id\" , \"a\".\"name\" AS \"name\" , \"a\".\"aColumn\" AS \"aColumn\" , \"b\".\"anotherColumn\" AS \"anotherColumn\"", translator.translate(query.getSelect()));
+
-+ for(int i = 0; i < b.length; i++){
-+ result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1);
++ }catch(ParseException pe){
++ pe.printStackTrace();
++ fail("The given ADQL query is completely correct. No error should have occurred while parsing it. (see the console for more details)");
++ }catch(TranslationException te){
++ te.printStackTrace();
++ fail("No error was expected from this translation. (see the console for more details)");
+ }
-+ return result;
-+ }
-+
-+ public static String getMD5Checksum(final String content) throws Exception{
-+ return getMD5Checksum(new ByteArrayInputStream(content.getBytes()));
+ }
+
-+ public static void main(String args[]){
-+ try{
-+ System.out.println(getMD5Checksum("Blabla et Super blabla"));
-+ }catch(Exception e){
-+ e.printStackTrace();
-+ }
-+ }
+}
diff -Nru adql-1.3/debian/patches/Fix-test.patch adql-1.4/debian/patches/Fix-test.patch
--- adql-1.3/debian/patches/Fix-test.patch 2017-03-14 13:40:07.000000000 +0000
+++ adql-1.4/debian/patches/Fix-test.patch 2018-03-21 13:25:27.000000000 +0000
@@ -13,10 +13,10 @@
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/test/adql/db/TestDBChecker.java b/test/adql/db/TestDBChecker.java
-index 3f9924a..ae4d165 100644
+index 3e675ae..cca6d20 100644
--- a/test/adql/db/TestDBChecker.java
+++ b/test/adql/db/TestDBChecker.java
-@@ -566,7 +566,7 @@ public class TestDBChecker {
+@@ -718,7 +718,7 @@ public class TestDBChecker {
fail("Geometrical UDFs are not allowed for the moment in the ADQL language: this test should have failed!");
}catch(ParseException e1){
assertTrue(e1 instanceof ParseException);
diff -Nru adql-1.3/debian/patches/Remove-unneeded-import-of-tap.data.DataReadException.patch adql-1.4/debian/patches/Remove-unneeded-import-of-tap.data.DataReadException.patch
--- adql-1.3/debian/patches/Remove-unneeded-import-of-tap.data.DataReadException.patch 2017-03-14 13:40:07.000000000 +0000
+++ adql-1.4/debian/patches/Remove-unneeded-import-of-tap.data.DataReadException.patch 1970-01-01 00:00:00.000000000 +0000
@@ -1,28 +0,0 @@
-From: Ole Streicher
-Date: Sat, 17 Dec 2016 18:13:50 +0100
-Subject: Remove unneeded import of tap.data.DataReadException
-
----
- src/adql/translator/JDBCTranslator.java | 2 --
- 1 file changed, 2 deletions(-)
-
-diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java
-index 0ba1b0d..a93eb9e 100644
---- a/src/adql/translator/JDBCTranslator.java
-+++ b/src/adql/translator/JDBCTranslator.java
-@@ -23,7 +23,6 @@ import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.Iterator;
-
--import tap.data.DataReadException;
- import adql.db.DBColumn;
- import adql.db.DBTable;
- import adql.db.DBType;
-@@ -873,7 +872,6 @@ public abstract class JDBCTranslator implements ADQLTranslator {
- * {@link #convertTypeFromDB(int, String, String, String[])}. So the value should always
- * be of the expected type and format. However, if it turns out that the type is wrong
- * and that the conversion is finally impossible, this function SHOULD throw a
-- * {@link DataReadException}.
- *
- *
- * @param jdbcColValue A JDBC column value (returned by ResultSet.getObject(int)).
diff -Nru adql-1.3/debian/patches/series adql-1.4/debian/patches/series
--- adql-1.3/debian/patches/series 2017-03-14 13:40:07.000000000 +0000
+++ adql-1.4/debian/patches/series 2018-03-21 13:25:27.000000000 +0000
@@ -1,5 +1,5 @@
-Remove-unneeded-import-of-tap.data.DataReadException.patch
Add-unit-tests.patch
Add-build.xml.patch
Fix-ant-build.patch
Fix-test.patch
+Test-Compare-only-the-first-8-digits-in-string-comparison.patch
diff -Nru adql-1.3/debian/patches/Test-Compare-only-the-first-8-digits-in-string-comparison.patch adql-1.4/debian/patches/Test-Compare-only-the-first-8-digits-in-string-comparison.patch
--- adql-1.3/debian/patches/Test-Compare-only-the-first-8-digits-in-string-comparison.patch 1970-01-01 00:00:00.000000000 +0000
+++ adql-1.4/debian/patches/Test-Compare-only-the-first-8-digits-in-string-comparison.patch 2018-03-21 13:25:27.000000000 +0000
@@ -0,0 +1,36 @@
+From: Ole Streicher
+Date: Wed, 21 Mar 2018 13:00:21 +0100
+Subject: Test: Compare only the first 8 digits in string comparison
+
+In TestPgSphereTranslator, two strings are compared
+containing (double) floating point numbers. These numbers are slightly
+different with different Java versions. To overcome this, only the
+first eight fractional digits are compared.
+
+Closes: #893193
+---
+ test/adql/translator/TestPgSphereTranslator.java | 4 +++-
+ 1 file changed, 3 insertions(+), 1 deletion(-)
+
+diff --git a/test/adql/translator/TestPgSphereTranslator.java b/test/adql/translator/TestPgSphereTranslator.java
+index 346ed80..2d09fc3 100644
+--- a/test/adql/translator/TestPgSphereTranslator.java
++++ b/test/adql/translator/TestPgSphereTranslator.java
+@@ -7,6 +7,7 @@ import static org.junit.Assert.assertTrue;
+ import static org.junit.Assert.fail;
+
+ import java.sql.Types;
++import java.util.regex.Pattern;
+
+ import org.junit.After;
+ import org.junit.AfterClass;
+@@ -315,7 +316,8 @@ public class TestPgSphereTranslator {
+ pgo = (PGobject)translator.translateGeometryToDB(r);
+ assertNotNull(pgo);
+ assertEquals("spoly", pgo.getType());
+- assertEquals("{(46.2d,0.0d),(46.176942336483876d,0.2341083864193539d),(46.108655439013546d,0.4592201188381077d),(45.99776353476305d,0.6666842796235226d),(45.848528137423855d,0.8485281374238569d),(45.666684279623524d,0.9977635347630542d),(45.45922011883811d,1.1086554390135441d),(45.23410838641935d,1.1769423364838765d),(45.0d,1.2d),(44.76589161358065d,1.1769423364838765d),(44.54077988116189d,1.1086554390135441d),(44.333315720376476d,0.9977635347630543d),(44.151471862576145d,0.848528137423857d),(44.00223646523695d,0.6666842796235226d),(43.891344560986454d,0.4592201188381073d),(43.823057663516124d,0.23410838641935325d),(43.8d,-9.188564877424678E-16d),(43.823057663516124d,-0.23410838641935505d),(43.891344560986454d,-0.45922011883810904d),(44.00223646523695d,-0.6666842796235241d),(44.151471862576145d,-0.8485281374238584d),(44.333315720376476d,-0.9977635347630555d),(44.540779881161896d,-1.108655439013545d),(44.76589161358065d,-1.176942336483877d),(45.0d,-1.2d),(45.23410838641936d,-1.1769423364838758d),(45.45922011883811d,-1.1086554390135428d),(45.666684279623524d,-0.9977635347630521d),(45.84852813742386d,-0.8485281374238541d),(45.99776353476306d,-0.6666842796235192d),(46.108655439013546d,-0.45922011883810354d),(46.176942336483876d,-0.23410838641934922d)}", pgo.getValue());
++ Pattern fp8 = Pattern.compile("(\\.\\d{8})\\d+d");
++ assertEquals("{(46.2d,0.0d),(46.17694233d,0.23410838d),(46.10865543d,0.45922011d),(45.99776353d,0.66668427d),(45.84852813d,0.84852813d),(45.66668427d,0.99776353d),(45.45922011d,1.10865543d),(45.23410838d,1.17694233d),(45.0d,1.2d),(44.76589161d,1.17694233d),(44.54077988d,1.10865543d),(44.33331572d,0.99776353d),(44.15147186d,0.84852813d),(44.00223646d,0.66668427d),(43.89134456d,0.45922011d),(43.82305766d,0.23410838d),(43.8d,-9.188564877424678E-16d),(43.82305766d,-0.23410838d),(43.89134456d,-0.45922011d),(44.00223646d,-0.66668427d),(44.15147186d,-0.84852813d),(44.33331572d,-0.99776353d),(44.54077988d,-1.10865543d),(44.76589161d,-1.17694233d),(45.0d,-1.2d),(45.23410838d,-1.17694233d),(45.45922011d,-1.10865543d),(45.66668427d,-0.99776353d),(45.84852813d,-0.84852813d),(45.99776353d,-0.66668427d),(46.10865543d,-0.45922011d),(46.17694233d,-0.23410838d)}", fp8.matcher(pgo.getValue()).replaceAll("$1d"));
+
+ // BOX
+ r = new Region(null, new double[]{45,0}, 1.2, 5);
diff -Nru adql-1.3/debian/upstream/metadata adql-1.4/debian/upstream/metadata
--- adql-1.3/debian/upstream/metadata 1970-01-01 00:00:00.000000000 +0000
+++ adql-1.4/debian/upstream/metadata 2018-03-21 12:20:17.000000000 +0000
@@ -0,0 +1,3 @@
+Bug-Database: https://github.com/gmantele/taplib/issues
+Name: ADQL
+Repository: https://github.com/gmantele/taplib
diff -Nru adql-1.3/debian/watch adql-1.4/debian/watch
--- adql-1.3/debian/watch 2017-03-14 13:40:07.000000000 +0000
+++ adql-1.4/debian/watch 2018-03-21 12:48:54.000000000 +0000
@@ -1,2 +1,2 @@
version=3
-https://github.com/gmantele/taplib/releases/latest .+/adql-(.*)_src.jar debian jh_repack
+https://github.com/gmantele/taplib/releases/ .*/adql-(.*)_src.jar debian jh_repack
diff -Nru adql-1.3/META-INF/MANIFEST.MF adql-1.4/META-INF/MANIFEST.MF
--- adql-1.3/META-INF/MANIFEST.MF 2015-05-04 09:50:28.000000000 +0000
+++ adql-1.4/META-INF/MANIFEST.MF 2018-02-26 15:18:18.000000000 +0000
@@ -1,4 +1,4 @@
Manifest-Version: 1.0
-Ant-Version: Apache Ant 1.8.4
-Created-By: 1.7.0_51-b13 (Oracle Corporation)
+Ant-Version: Apache Ant 1.9.6
+Created-By: 1.7.0_151-b01 (Oracle Corporation)
diff -Nru adql-1.3/src/adql/db/DBChecker.java adql-1.4/src/adql/db/DBChecker.java
--- adql-1.3/src/adql/db/DBChecker.java 2015-05-04 09:24:46.000000000 +0000
+++ adql-1.4/src/adql/db/DBChecker.java 2018-01-12 13:49:48.000000000 +0000
@@ -2,22 +2,22 @@
/*
* This file is part of ADQLLibrary.
- *
+ *
* ADQLLibrary is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
- *
+ *
* ADQLLibrary 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 Lesser General Public License for more details.
- *
+ *
* You should have received a copy of the GNU Lesser General Public License
* along with ADQLLibrary. If not, see .
- *
- * Copyright 2011,2013-2015 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
- * Astronomisches Rechen Institut (ARI)
+ *
+ * Copyright 2011-2017 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
+ * Astronomisches Rechen Institut (ARI)
*/
import java.lang.reflect.Constructor;
@@ -27,6 +27,7 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
import java.util.Stack;
@@ -42,6 +43,7 @@
import adql.query.ADQLIterator;
import adql.query.ADQLObject;
import adql.query.ADQLQuery;
+import adql.query.ClauseADQL;
import adql.query.ClauseSelect;
import adql.query.ColumnReference;
import adql.query.IdentifierField;
@@ -76,33 +78,33 @@
*
Check whether all used coordinate systems are supported
*
Check that types of columns and UDFs match with their context
*
- *
+ *
*
Check tables and columns
*
* In addition to check the existence of tables and columns referenced in the query,
* this checked will also attach database metadata on these references ({@link ADQLTable}
* and {@link ADQLColumn} instances when they are resolved.
*
- *
+ *
*
These information are:
*
*
the corresponding {@link DBTable} or {@link DBColumn} (see getter and setter for DBLink in {@link ADQLTable} and {@link ADQLColumn})
*
the link between an {@link ADQLColumn} and its {@link ADQLTable}
*
- *
+ *
*
Note:
* Knowing DB metadata of {@link ADQLTable} and {@link ADQLColumn} is particularly useful for the translation of the ADQL query to SQL,
* because the ADQL name of columns and tables can be replaced in SQL by their DB name, if different. This mapping is done automatically
* by {@link adql.translator.JDBCTranslator}.
*
- *
+ *
* @author Grégory Mantelet (CDS;ARI)
- * @version 1.3 (05/2015)
+ * @version 1.4 (11/2017)
*/
public class DBChecker implements QueryChecker {
/** List of all available tables ({@link DBTable}). */
- protected SearchTableList lstTables;
+ protected SearchTableApi lstTables;
/**
List of all allowed geometrical functions (i.e. CONTAINS, REGION, POINT, COORD2, ...).
*
@@ -149,7 +151,7 @@
/* ************ */
/**
*
Builds a {@link DBChecker} with an empty list of tables.
- *
+ *
*
Verifications done by this object after creation:
*
*
Existence of tables and columns: NO (even unknown or fake tables and columns are allowed)
@@ -164,7 +166,7 @@
/**
*
Builds a {@link DBChecker} with the given list of known tables.
- *
+ *
*
Verifications done by this object after creation:
*
*
Existence of tables and columns: OK
@@ -172,7 +174,7 @@
*
Support of geometrical functions: NO (all valid geometrical functions are allowed)
*
Support of coordinate systems: NO (all valid coordinate systems are allowed)
*
- *
+ *
* @param tables List of all available tables.
*/
public DBChecker(final Collection extends DBTable> tables){
@@ -181,7 +183,7 @@
/**
*
Builds a {@link DBChecker} with the given list of known tables and with a restricted list of user defined functions.
- *
+ *
*
Verifications done by this object after creation:
*
*
Existence of tables and columns: OK
@@ -189,13 +191,13 @@
*
Support of geometrical functions: NO (all valid geometrical functions are allowed)
*
Support of coordinate systems: NO (all valid coordinate systems are allowed)
*
- *
+ *
* @param tables List of all available tables.
* @param allowedUdfs List of all allowed user defined functions.
* If NULL, no verification will be done (and so, all UDFs are allowed).
* If empty list, no "unknown" (or UDF) is allowed.
* Note: match with items of this list are done case insensitively.
- *
+ *
* @since 1.3
*/
public DBChecker(final Collection extends DBTable> tables, final Collection extends FunctionDef> allowedUdfs){
@@ -215,7 +217,9 @@
tmp[cnt++] = udf;
}
// make a copy of the array:
- this.allowedUdfs = Arrays.copyOf(tmp, cnt, FunctionDef[].class);
+ this.allowedUdfs = new FunctionDef[cnt];
+ System.arraycopy(tmp, 0, this.allowedUdfs, 0, cnt);
+
tmp = null;
// sort the values:
Arrays.sort(this.allowedUdfs);
@@ -224,7 +228,7 @@
/**
*
Builds a {@link DBChecker} with the given list of known tables and with a restricted list of user defined functions.
- *
+ *
*
Verifications done by this object after creation:
*
*
Existence of tables and columns: OK
@@ -232,7 +236,7 @@
*
Support of geometrical functions: OK
*
Support of coordinate systems: OK
*
- *
+ *
* @param tables List of all available tables.
* @param allowedGeoFcts List of all allowed geometrical functions (i.e. CONTAINS, POINT, UNION, CIRCLE, COORD1).
* If NULL, no verification will be done (and so, all geometries are allowed).
@@ -245,7 +249,7 @@
* For instance: "ICRS (GEOCENTER|heliocenter) *".
* If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed).
* If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: '').
- *
+ *
* @since 1.3
*/
public DBChecker(final Collection extends DBTable> tables, final Collection allowedGeoFcts, final Collection allowedCoordSys) throws ParseException{
@@ -254,7 +258,7 @@
/**
*
Builds a {@link DBChecker}.
- *
+ *
*
Verifications done by this object after creation:
*
*
Existence of tables and columns: OK
@@ -262,7 +266,7 @@
*
Support of geometrical functions: OK
*
Support of coordinate systems: OK
*
- *
+ *
* @param tables List of all available tables.
* @param allowedUdfs List of all allowed user defined functions.
* If NULL, no verification will be done (and so, all UDFs are allowed).
@@ -279,7 +283,7 @@
* For instance: "ICRS (GEOCENTER|heliocenter) *".
* If the given list is NULL, no verification will be done (and so, all coordinate systems are allowed).
* If it is empty, no coordinate system is allowed (except the default values - generally expressed by an empty string: '').
- *
+ *
* @since 1.3
*/
public DBChecker(final Collection extends DBTable> tables, final Collection extends FunctionDef> allowedUdfs, final Collection allowedGeoFcts, final Collection allowedCoordSys) throws ParseException{
@@ -297,11 +301,11 @@
/**
* Transform the given collection of string elements in a sorted array.
* Only non-NULL and non-empty strings are kept.
- *
+ *
* @param items Items to copy and sort.
- *
+ *
* @return A sorted array containing all - except NULL and empty strings - items of the given collection.
- *
+ *
* @since 1.3
*/
protected final static String[] specialSort(final Collection items){
@@ -318,7 +322,8 @@
}
// Make an adjusted array copy:
- String[] copy = Arrays.copyOf(tmp, cnt);
+ String[] copy = new String[cnt];
+ System.arraycopy(tmp, 0, copy, 0, cnt);
// Sort the values:
Arrays.sort(copy);
@@ -331,19 +336,20 @@
/* ****** */
/**
*
Sets the list of all available tables.
- *
+ *
*
Note:
- * Only if the given collection is NOT an instance of {@link SearchTableList},
- * the collection will be copied inside a new {@link SearchTableList}, otherwise it is used as provided.
+ * Only if the given collection is NOT an implementation of
+ * {@link SearchTableApi}, the collection will be copied inside a new
+ * {@link SearchTableList}, otherwise it is used as provided.
*
- *
+ *
* @param tables List of {@link DBTable}s.
*/
public final void setTables(final Collection extends DBTable> tables){
if (tables == null)
lstTables = new SearchTableList();
- else if (tables instanceof SearchTableList)
- lstTables = (SearchTableList)tables;
+ else if (tables instanceof SearchTableApi)
+ lstTables = (SearchTableApi)tables;
else
lstTables = new SearchTableList(tables);
}
@@ -353,16 +359,16 @@
/* ************* */
/**
*
Check all the columns, tables and UDFs references inside the given query.
- *
+ *
*
* Note: This query has already been parsed ; thus it is already syntactically correct.
* Only the consistency with the published tables, columns and all the defined UDFs must be checked.
*
- *
+ *
* @param query The query to check.
- *
+ *
* @throws ParseException An {@link UnresolvedIdentifiersException} if some tables or columns can not be resolved.
- *
+ *
* @see #check(ADQLQuery, Stack)
*/
@Override
@@ -372,7 +378,7 @@
/**
*
Process several (semantic) verifications in the given ADQL query.
- *
+ *
*
Main verifications done in this function:
*
*
Existence of DB items (tables and columns)
@@ -381,17 +387,17 @@
*
Support of every encountered geometries (functions, coordinate systems and STC-S expressions)
*
Consistency of types still unknown (because the syntactic parser could not yet resolve them)
*
- *
+ *
* @param query The query to check.
* @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries.
* Each item of this stack is a list of columns available in each father-level query.
* Note: this parameter is NULL if this function is called with the root/father query as parameter.
- *
+ *
* @throws UnresolvedIdentifiersException An {@link UnresolvedIdentifiersException} if one or several of the above listed tests have detected
* some semantic errors (i.e. unresolved table, columns, function).
- *
+ *
* @since 1.2
- *
+ *
* @see #checkDBItems(ADQLQuery, Stack, UnresolvedIdentifiersException)
* @see #checkSubQueries(ADQLQuery, Stack, SearchColumnList, UnresolvedIdentifiersException)
* @see #checkUDFs(ADQLQuery, UnresolvedIdentifiersException)
@@ -428,26 +434,26 @@
/**
*
Check DB items (tables and columns) used in the given ADQL query.
- *
+ *
*
Operations done in this function:
*
*
Resolve all found tables
*
Get the whole list of all available columns Note: this list is returned by this function.
*
Resolve all found columns
*
- *
+ *
* @param query Query in which the existence of DB items must be checked.
* @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries.
* Each item of this stack is a list of columns available in each father-level query.
* Note: this parameter is NULL if this function is called with the root/father query as parameter.
* @param errors List of errors to complete in this function each time an unknown table or column is encountered.
- *
+ *
* @return List of all columns available in the given query.
- *
+ *
* @see #resolveTables(ADQLQuery, Stack, UnresolvedIdentifiersException)
* @see FromContent#getDBColumns()
* @see #resolveColumns(ADQLQuery, Stack, Map, SearchColumnList, UnresolvedIdentifiersException)
- *
+ *
* @since 1.3
*/
protected SearchColumnList checkDBItems(final ADQLQuery query, final Stack fathersList, final UnresolvedIdentifiersException errors){
@@ -470,29 +476,54 @@
}
/**
- *
Search all table references inside the given query, resolve them against the available tables, and if there is only one match,
- * attach the matching metadata to them.
- *
+ * Search all table references inside the given query, resolve them against
+ * the available tables, and if there is only one match, attach the matching
+ * metadata to them.
+ *
* Management of sub-query tables
*
- * If a table is not a DB table reference but a sub-query, this latter is first checked (using {@link #check(ADQLQuery, Stack)} ;
- * but the father list must not contain tables of the given query, because on the same level) and then corresponding table metadata
- * are generated (using {@link #generateDBTable(ADQLQuery, String)}) and attached to it.
+ * If a table is not a DB table reference but a sub-query, this latter is
+ * first checked (using {@link #check(ADQLQuery, Stack)} ; but the father
+ * list must not contain tables of the given query, because on the same
+ * level) and then corresponding table metadata are generated (using
+ * {@link #generateDBTable(ADQLQuery, String)}) and attached to it.
*
- *
+ *
* Management of "{table}.*" in the SELECT clause
*
- * For each of this SELECT item, this function tries to resolve the table name. If only one match is found, the corresponding ADQL table object
- * is got from the list of resolved tables and attached to this SELECT item (thus, the joker item will also have the good metadata,
- * particularly if the referenced table is a sub-query).
+ * For each of this SELECT item, this function tries to resolve the table
+ * name. If only one match is found, the corresponding ADQL table object
+ * is got from the list of resolved tables and attached to this SELECT item
+ * (thus, the joker item will also have the good metadata, particularly if
+ * the referenced table is a sub-query).
*
- *
- * @param query Query in which the existence of tables must be checked.
- * @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries.
- * Each item of this stack is a list of columns available in each father-level query.
- * Note: this parameter is NULL if this function is called with the root/father query as parameter.
- * @param errors List of errors to complete in this function each time an unknown table or column is encountered.
- *
+ *
+ * Table alias
+ *
+ * When a simple table (i.e. not a sub-query) is aliased, the metadata of
+ * this table will be wrapped inside a {@link DBTableAlias} in order to
+ * keep the original metadata but still declare use the table with the
+ * alias instead of its original name. The original name will be used
+ * only when translating the corresponding FROM item ; the rest of the time
+ * (i.e. for references when using a column), the alias name must be used.
+ *
+ *
+ * In order to avoid unpredictable behavior at execution of the SQL query,
+ * the alias will be put in lower case if not defined between double
+ * quotes.
+ *
+ *
+ * @param query Query in which the existence of tables must be
+ * checked.
+ * @param fathersList List of all columns available in the father queries
+ * and that should be accessed in sub-queries.
+ * Each item of this stack is a list of columns
+ * available in each father-level query.
+ * Note: this parameter is NULL if this function is
+ * called with the root/father query as parameter.
+ * @param errors List of errors to complete in this function each
+ * time an unknown table or column is encountered.
+ *
* @return An associative map of all the resolved tables.
*/
protected Map resolveTables(final ADQLQuery query, final Stack fathersList, final UnresolvedIdentifiersException errors){
@@ -515,8 +546,9 @@
dbTable = generateDBTable(table.getSubQuery(), table.getAlias());
}else{
dbTable = resolveTable(table);
- if (table.hasAlias())
- dbTable = dbTable.copy(null, table.getAlias());
+ // wrap this table metadata if an alias should be used:
+ if (dbTable != null && table.hasAlias())
+ dbTable = new DBTableAlias(dbTable, (table.isCaseSensitive(IdentifierField.ALIAS) ? table.getAlias() : table.getAlias().toLowerCase()));
}
// link with the matched DBTable:
@@ -542,7 +574,7 @@
// first, try to resolve the table by table alias:
if (table.getTableName() != null && table.getSchemaName() == null){
- ArrayList tables = query.getFrom().getTablesByAlias(table.getTableName(), table.isCaseSensitive(IdentifierField.TABLE));
+ List tables = query.getFrom().getTablesByAlias(table.getTableName(), table.isCaseSensitive(IdentifierField.TABLE));
if (tables.size() == 1)
dbTable = tables.get(0).getDBLink();
}
@@ -563,22 +595,22 @@
/**
* Resolve the given table, that's to say search for the corresponding {@link DBTable}.
- *
+ *
* @param table The table to resolve.
- *
+ *
* @return The corresponding {@link DBTable} if found, null otherwise.
- *
+ *
* @throws ParseException An {@link UnresolvedTableException} if the given table can't be resolved.
*/
protected DBTable resolveTable(final ADQLTable table) throws ParseException{
- ArrayList tables = lstTables.search(table);
+ List tables = lstTables.search(table);
// good if only one table has been found:
if (tables.size() == 1)
return tables.get(0);
// but if more than one: ambiguous table name !
else if (tables.size() > 1)
- throw new UnresolvedTableException(table, tables.get(0).getADQLSchemaName() + "." + tables.get(0).getADQLName(), tables.get(1).getADQLSchemaName() + "." + tables.get(1).getADQLName());
+ throw new UnresolvedTableException(table, (tables.get(0).getADQLSchemaName() == null ? "" : tables.get(0).getADQLSchemaName() + ".") + tables.get(0).getADQLName(), (tables.get(1).getADQLSchemaName() == null ? "" : tables.get(1).getADQLSchemaName() + ".") + tables.get(1).getADQLName());
// otherwise (no match): unknown table !
else
throw new UnresolvedTableException(table);
@@ -587,7 +619,7 @@
/**
*
Search all column references inside the given query, resolve them thanks to the given tables' metadata,
* and if there is only one match, attach the matching metadata to them.
* A column reference is not only a direct reference to a table column using a column name.
@@ -599,7 +631,7 @@
* These references are also checked, in a second step, in this function. Thus, column metadata are
* also attached to them, as common columns.
*
- *
+ *
* @param query Query in which the existence of tables must be checked.
* @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries.
* Each item of this stack is a list of columns available in each father-level query.
@@ -612,7 +644,7 @@
ISearchHandler sHandler;
// Check the existence of all columns:
- sHandler = new SearchColumnHandler();
+ sHandler = new SearchColumnOutsideGroupByHandler();
sHandler.search(query);
for(ADQLObject result : sHandler){
try{
@@ -627,12 +659,30 @@
}
}
+ // Check the GROUP BY items:
+ ClauseSelect select = query.getSelect();
+ sHandler = new SearchColumnHandler();
+ sHandler.search(query.getGroupBy());
+ for(ADQLObject result : sHandler){
+ try{
+ ADQLColumn adqlColumn = (ADQLColumn)result;
+ // resolve the column:
+ DBColumn dbColumn = checkGroupByItem(adqlColumn, select, list);
+ // link with the matched DBColumn:
+ if (dbColumn != null){
+ adqlColumn.setDBLink(dbColumn);
+ adqlColumn.setAdqlTable(mapTables.get(dbColumn.getTable()));
+ }
+ }catch(ParseException pe){
+ errors.addException(pe);
+ }
+ }
+
// Check the correctness of all column references (= references to selected columns):
/* Note: no need to provide the father tables when resolving column references,
- * because no father column can be used in ORDER BY and/or GROUP BY. */
+ * because no father column can be used in ORDER BY. */
sHandler = new SearchColReferenceHandler();
sHandler.search(query);
- ClauseSelect select = query.getSelect();
for(ADQLObject result : sHandler){
try{
ColumnReference colRef = (ColumnReference)result;
@@ -650,26 +700,26 @@
/**
*
Resolve the given column, that's to say search for the corresponding {@link DBColumn}.
- *
+ *
*
* The third parameter is used only if this function is called inside a sub-query. In this case,
* the column is tried to be resolved with the first list (dbColumns). If no match is found,
* the resolution is tried with the father columns list (fathersList).
*
- *
+ *
* @param column The column to resolve.
* @param dbColumns List of all available {@link DBColumn}s.
* @param fathersList List of all columns available in the father queries and that should be accessed in sub-queries.
* Each item of this stack is a list of columns available in each father-level query.
* Note: this parameter is NULL if this function is called with the root/father query as parameter.
- *
+ *
* @return The corresponding {@link DBColumn} if found. Otherwise an exception is thrown.
- *
+ *
* @throws ParseException An {@link UnresolvedColumnException} if the given column can't be resolved
* or an {@link UnresolvedTableException} if its table reference can't be resolved.
*/
protected DBColumn resolveColumn(final ADQLColumn column, final SearchColumnList dbColumns, Stack fathersList) throws ParseException{
- ArrayList foundColumns = dbColumns.search(column);
+ List foundColumns = dbColumns.search(column);
// good if only one column has been found:
if (foundColumns.size() == 1)
@@ -693,18 +743,50 @@
}
/**
+ * Check whether the given column corresponds to a selected item's alias or to an existing column.
+ *
+ * @param col The column to check.
+ * @param select The SELECT clause of the ADQL query.
+ * @param dbColumns The list of all available columns.
+ *
+ * @return The corresponding {@link DBColumn} if this column corresponds to an existing column,
+ * NULL otherwise.
+ *
+ * @throws ParseException An {@link UnresolvedColumnException} if the given column can't be resolved
+ * or an {@link UnresolvedTableException} if its table reference can't be resolved.
+ *
+ * @see ClauseSelect#searchByAlias(String)
+ * @see #resolveColumn(ADQLColumn, SearchColumnList, Stack)
+ *
+ * @since 1.4
+ */
+ protected DBColumn checkGroupByItem(final ADQLColumn col, final ClauseSelect select, final SearchColumnList dbColumns) throws ParseException{
+ /* If the column name is not qualified, it may be a SELECT-item's alias.
+ * So, try resolving the name as an alias.
+ * If it fails, perform the normal column resolution.*/
+ if (col.getTableName() == null){
+ List founds = select.searchByAlias(col.getColumnName(), col.isCaseSensitive(IdentifierField.COLUMN));
+ if (founds.size() == 1)
+ return null;
+ else if (founds.size() > 1)
+ throw new UnresolvedColumnException(col, founds.get(0).getAlias(), founds.get(1).getAlias());
+ }
+ return resolveColumn(col, dbColumns, null);
+ }
+
+ /**
* Check whether the given column reference corresponds to a selected item (column or an expression with an alias)
* or to an existing column.
- *
+ *
* @param colRef The column reference which must be checked.
* @param select The SELECT clause of the ADQL query.
* @param dbColumns The list of all available columns.
- *
+ *
* @return The corresponding {@link DBColumn} if this reference is actually the name of a column, null otherwise.
- *
+ *
* @throws ParseException An {@link UnresolvedColumnException} if the given column can't be resolved
* or an {@link UnresolvedTableException} if its table reference can't be resolved.
- *
+ *
* @see ClauseSelect#searchByAlias(String)
* @see #resolveColumn(ADQLColumn, SearchColumnList, Stack)
*/
@@ -720,18 +802,16 @@
}else
throw new ParseException("Column index out of bounds: " + index + " (must be between 1 and " + select.size() + ") !", colRef.getPosition());
}else{
- ADQLColumn col = new ADQLColumn(colRef.getColumnName());
+ ADQLColumn col = new ADQLColumn(null, colRef.getColumnName());
col.setCaseSensitive(colRef.isCaseSensitive());
col.setPosition(colRef.getPosition());
// search among the select_item aliases:
- if (col.getTableName() == null){
- ArrayList founds = select.searchByAlias(colRef.getColumnName(), colRef.isCaseSensitive());
- if (founds.size() == 1)
- return null;
- else if (founds.size() > 1)
- throw new UnresolvedColumnException(col, founds.get(0).getAlias(), founds.get(1).getAlias());
- }
+ List founds = select.searchByAlias(colRef.getColumnName(), colRef.isCaseSensitive());
+ if (founds.size() == 1)
+ return null;
+ else if (founds.size() > 1)
+ throw new UnresolvedColumnException(col, founds.get(0).getAlias(), founds.get(1).getAlias());
// check the corresponding column:
return resolveColumn(col, dbColumns, null);
@@ -741,12 +821,12 @@
/**
* Generate a {@link DBTable} corresponding to the given sub-query with the given table name.
* This {@link DBTable} will contain all {@link DBColumn} returned by {@link ADQLQuery#getResultingColumns()}.
- *
+ *
* @param subQuery Sub-query in which the specified table must be searched.
* @param tableName Name of the table to search.
- *
+ *
* @return The corresponding {@link DBTable} if the table has been found in the given sub-query, null otherwise.
- *
+ *
* @throws ParseException Can be used to explain why the table has not been found. Note: not used by default.
*/
public static DBTable generateDBTable(final ADQLQuery subQuery, final String tableName) throws ParseException{
@@ -766,7 +846,7 @@
/**
*