From a2fb29ade2cd2ab7bafb90d5956b6ecb387fb8b6 Mon Sep 17 00:00:00 2001
From: gmantele <gmantele@ari.uni-heidelberg.de>
Date: Thu, 17 Mar 2016 19:31:27 +0100
Subject: [PATCH] [ADQL] Add an ADQL translator for MS SQL Server. This
 particular translator deals with NATURAL JOINs and JOINs using the keyword
 USING so that being supported by SQL Server. Basically, they are translated
 as a list of ON conditions. Warning: This translator is just guaranteed to
 solve the NATURAL and USING issue. Support for datatypes conversion and case
 sensitivity has to be reviewed. Besides no geometrical function is translated
 for SQL Server.

---
 .../parser/SQLServer_ADQLQueryFactory.java    |  75 ++++
 src/adql/query/from/SQLServer_InnerJoin.java  | 206 +++++++++
 src/adql/query/from/SQLServer_OuterJoin.java  | 122 ++++++
 src/adql/translator/SQLServerTranslator.java  | 398 ++++++++++++++++++
 .../query/from/TestSQLServer_InnerJoin.java   | 159 +++++++
 .../translator/TestSQLServerTranslator.java   |  85 ++++
 6 files changed, 1045 insertions(+)
 create mode 100644 src/adql/parser/SQLServer_ADQLQueryFactory.java
 create mode 100644 src/adql/query/from/SQLServer_InnerJoin.java
 create mode 100644 src/adql/query/from/SQLServer_OuterJoin.java
 create mode 100644 src/adql/translator/SQLServerTranslator.java
 create mode 100644 test/adql/query/from/TestSQLServer_InnerJoin.java
 create mode 100644 test/adql/translator/TestSQLServerTranslator.java

diff --git a/src/adql/parser/SQLServer_ADQLQueryFactory.java b/src/adql/parser/SQLServer_ADQLQueryFactory.java
new file mode 100644
index 0000000..18b2b54
--- /dev/null
+++ b/src/adql/parser/SQLServer_ADQLQueryFactory.java
@@ -0,0 +1,75 @@
+package adql.parser;
+
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright 2016 - Astronomisches Rechen Institut (ARI)
+ */
+
+import adql.query.from.ADQLJoin;
+import adql.query.from.CrossJoin;
+import adql.query.from.FromContent;
+import adql.query.from.InnerJoin;
+import adql.query.from.OuterJoin;
+import adql.query.from.SQLServer_InnerJoin;
+import adql.query.from.SQLServer_OuterJoin;
+import adql.query.from.OuterJoin.OuterType;
+import adql.translator.SQLServerTranslator;
+
+/**
+ * <p>Special extension of {@link ADQLQueryFactory} for MS SQL Server.</p>
+ * 
+ * <p><b>Important:</b>
+ * 	This class is generally used when an ADQL translator for MS SQL Server is needed.
+ * 	See {@link SQLServerTranslator} for more details.
+ * </p>
+ * 
+ * <p>
+ * 	The only difference with {@link ADQLQueryFactory} is the creation of an
+ * 	{@link ADQLJoin}. Instead of creating {@link InnerJoin} and {@link OuterJoin},
+ * 	{@link SQLServer_InnerJoin} and {@link SQLServer_OuterJoin} are respectively created.
+ * 	The only difference between these last classes and the first ones is in the processing
+ * 	of NATURAL JOINs and JOINs using the keyword USING.
+ * </p>
+ * 
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 1.4 (03/2016)
+ * @since 1.4
+ * 
+ * @see SQLServer_InnerJoin
+ * @see SQLServer_OuterJoin
+ * @see SQLServerTranslator
+ */
+public class SQLServer_ADQLQueryFactory extends ADQLQueryFactory {
+
+	public ADQLJoin createJoin(JoinType type, FromContent leftTable, FromContent rightTable) throws Exception{
+		switch(type){
+			case CROSS:
+				return new CrossJoin(leftTable, rightTable);
+			case INNER:
+				return new SQLServer_InnerJoin(leftTable, rightTable);
+			case OUTER_LEFT:
+				return new SQLServer_OuterJoin(leftTable, rightTable, OuterType.LEFT);
+			case OUTER_RIGHT:
+				return new SQLServer_OuterJoin(leftTable, rightTable, OuterType.RIGHT);
+			case OUTER_FULL:
+				return new SQLServer_OuterJoin(leftTable, rightTable, OuterType.FULL);
+			default:
+				throw new Exception("Unknown join type: " + type);
+		}
+	}
+	
+}
\ No newline at end of file
diff --git a/src/adql/query/from/SQLServer_InnerJoin.java b/src/adql/query/from/SQLServer_InnerJoin.java
new file mode 100644
index 0000000..432e1ff
--- /dev/null
+++ b/src/adql/query/from/SQLServer_InnerJoin.java
@@ -0,0 +1,206 @@
+package adql.query.from;
+
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright 2016 - Astronomisches Rechen Institut (ARI)
+ */
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import adql.db.DBColumn;
+import adql.db.DBCommonColumn;
+import adql.db.SearchColumnList;
+import adql.db.exception.UnresolvedJoinException;
+import adql.parser.SQLServer_ADQLQueryFactory;
+import adql.query.ClauseConstraints;
+import adql.query.IdentifierField;
+import adql.query.operand.ADQLColumn;
+
+/**
+ * <p>Special implementation of {@link InnerJoin} for MS SQL Server.</p>
+ * 
+ * <p><b>Important:</b>
+ * 	Instances of this class are created only by {@link SQLServer_ADQLQueryFactory}.
+ * </p>
+ * 
+ * <p>
+ * 	This implementation just changes the behavior the {@link #getDBColumns()}.
+ * 	In MS SQL Server, there is no keyword NATURAL and USING. That's why the {@link DBColumn}s
+ * 	returned by {@link DBColumn} can not contain any {@link DBCommonColumn}. Instead,
+ * 	the {@link DBColumn} of the first joined table (i.e. the left one) is returned.
+ * </p>
+ * 
+ * <p>
+ * 	Since this special behavior is also valid for {@link OuterJoin}, a special implementation
+ * 	of this class has been also created: {@link SQLServer_OuterJoin}. Both must have exactly the
+ * 	same behavior when {@link #getDBColumns()} is called. That's why the static function
+ * 	{@link #getDBColumns(ADQLJoin)} has been created. It is called by {@link SQLServer_InnerJoin}
+ * 	and {@link SQLServer_OuterJoin}.
+ * </p>
+ * 
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 1.4 (03/2016)
+ * @since 1.4
+ * 
+ * @see SQLServer_ADQLQueryFactory
+ */
+public class SQLServer_InnerJoin extends InnerJoin {
+
+	/**
+	 * Builds a NATURAL INNER JOIN between the two given "tables".
+	 * 
+	 * @param left	Left "table".
+	 * @param right	Right "table".
+	 * 
+	 * @see InnerJoin#InnerJoin(FromContent, FromContent)
+	 */
+	public SQLServer_InnerJoin(FromContent left, FromContent right) {
+		super(left, right);
+	}
+
+	/**
+	 * Builds an INNER JOIN between the two given "tables" with the given condition.
+	 * 
+	 * @param left		Left "table".
+	 * @param right		Right "table".
+	 * @param condition	Join condition.
+	 * 
+	 * @see InnerJoin#InnerJoin(FromContent, FromContent, ClauseConstraints)
+	 */
+	public SQLServer_InnerJoin(FromContent left, FromContent right, ClauseConstraints condition) {
+		super(left, right, condition);
+	}
+
+	/**
+	 * Builds an INNER JOIN between the two given "tables" with the given condition.
+	 * 
+	 * @param left		Left "table".
+	 * @param right		Right "table".
+	 * @param condition	Join condition.
+	 * 
+	 * @see InnerJoin#InnerJoin(FromContent, FromContent, Collection)
+	 */
+	public SQLServer_InnerJoin(FromContent left, FromContent right, Collection<ADQLColumn> lstColumns) {
+		super(left, right, lstColumns);
+	}
+
+	/**
+	 * Builds a copy of the given INNER join.
+	 * 
+	 * @param toCopy		The INNER join to copy.
+	 * 
+	 * @throws Exception	If there is an error during the copy.
+	 * 
+	 * @see InnerJoin#InnerJoin(InnerJoin)
+	 */
+	public SQLServer_InnerJoin(InnerJoin toCopy) throws Exception {
+		super(toCopy);
+	}
+
+	@Override
+	public SearchColumnList getDBColumns() throws UnresolvedJoinException {
+		return getDBColumns(this);
+	}
+
+	/**
+	 * <p>Gets the list of all columns (~ database metadata) available in this FROM part.
+	 * Columns implied in a NATURAL join or in a USING list, are not returned as a {@link DBCommonColumn} ; 
+	 * actually, just the corresponding {@link DBColumn} of the left table is returned.</p>
+	 * 
+	 * @return	All the available {@link DBColumn}s.
+	 * @throws UnresolvedJoinException If a join is not possible.
+	 */
+	public static SearchColumnList getDBColumns(final ADQLJoin join) throws UnresolvedJoinException{
+		try{
+			SearchColumnList list = new SearchColumnList();
+			SearchColumnList leftList = join.getLeftTable().getDBColumns();
+			SearchColumnList rightList = join.getRightTable().getDBColumns();
+
+			/* 1. Figure out duplicated columns */
+			HashMap<String,DBColumn> mapDuplicated = new HashMap<String,DBColumn>();
+			// CASE: NATURAL
+			if (join.isNatural()){
+				// Find duplicated items between the two lists and add one common column in mapDuplicated for each
+				DBColumn rightCol;
+				for(DBColumn leftCol : leftList){
+					// search for at most one column with the same name in the RIGHT list
+					// and throw an exception is there are several matches:
+					rightCol = findAtMostOneColumn(leftCol.getADQLName(), (byte)0, rightList, false);
+					// if there is one...
+					if (rightCol != null){
+						// ...check there is only one column with this name in the LEFT list,
+						// and throw an exception if it is not the case:
+						findExactlyOneColumn(leftCol.getADQLName(), (byte)0, leftList, true);
+						// ...add the left column:
+						mapDuplicated.put(leftCol.getADQLName().toLowerCase(), leftCol);
+					}
+				}
+
+			}
+			// CASE: USING
+			else if (join.hasJoinedColumns()){
+				// For each columns of usingList, check there is in each list exactly one matching column, and then, add it in mapDuplicated
+				DBColumn leftCol;
+				ADQLColumn usingCol;
+				Iterator<ADQLColumn> itCols = join.getJoinedColumns();
+				while(itCols.hasNext()){
+					usingCol = itCols.next();
+					// search for exactly one column with the same name in the LEFT list
+					// and throw an exception if there is none, or if there are several matches:
+					leftCol = findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), leftList, true);
+					// idem in the RIGHT list:
+					findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), rightList, false);
+					// add the left column:
+					mapDuplicated.put((usingCol.isCaseSensitive(IdentifierField.COLUMN) ? ("\"" + usingCol.getColumnName() + "\"") : usingCol.getColumnName().toLowerCase()), leftCol);
+				}
+
+			}
+			// CASE: NO DUPLICATION TO FIGURE OUT
+			else{
+				// Return the union of both lists:
+				list.addAll(leftList);
+				list.addAll(rightList);
+				return list;
+			}
+
+			/* 2. Add all columns of the left list except the ones identified as duplications */
+			addAllExcept2(leftList, list, mapDuplicated);
+
+			/* 3. Add all columns of the right list except the ones identified as duplications */
+			addAllExcept2(rightList, list, mapDuplicated);
+
+			/* 4. Add all common columns of mapDuplicated */
+			list.addAll(0, mapDuplicated.values());
+
+			return list;
+		}catch(UnresolvedJoinException uje){
+			uje.setPosition(join.getPosition());
+			throw uje;
+		}
+	}
+	
+	public final static void addAllExcept2(final SearchColumnList itemsToAdd, final SearchColumnList target, final Map<String,DBColumn> exception){
+		for(DBColumn col : itemsToAdd){
+			if (!exception.containsKey(col.getADQLName().toLowerCase()) && !exception.containsKey("\"" + col.getADQLName() + "\""))
+				target.add(col);
+		}
+	}
+
+}
diff --git a/src/adql/query/from/SQLServer_OuterJoin.java b/src/adql/query/from/SQLServer_OuterJoin.java
new file mode 100644
index 0000000..8f24e49
--- /dev/null
+++ b/src/adql/query/from/SQLServer_OuterJoin.java
@@ -0,0 +1,122 @@
+package adql.query.from;
+
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright 2016 - Astronomisches Rechen Institut (ARI)
+ */
+
+import java.util.Collection;
+
+import adql.db.DBColumn;
+import adql.db.DBCommonColumn;
+import adql.db.SearchColumnList;
+import adql.db.exception.UnresolvedJoinException;
+import adql.parser.SQLServer_ADQLQueryFactory;
+import adql.query.ClauseConstraints;
+import adql.query.operand.ADQLColumn;
+
+/**
+ * <p>Special implementation of {@link OuterJoin} for MS SQL Server.</p>
+ * 
+ * <p><b>Important:</b>
+ * 	Instances of this class are created only by {@link SQLServer_ADQLQueryFactory}.
+ * </p>
+ * 
+ * <p>
+ * 	This implementation just changes the behavior the {@link #getDBColumns()}.
+ * 	In MS SQL Server, there is no keyword NATURAL and USING. That's why the {@link DBColumn}s
+ * 	returned by {@link DBColumn} can not contain any {@link DBCommonColumn}. Instead,
+ * 	the {@link DBColumn} of the first joined table (i.e. the left one) is returned.
+ * </p>
+ * 
+ * <p>
+ * 	Since this special behavior is also valid for {@link InnerJoin}, a special implementation
+ * 	of this class has been also created: {@link SQLServer_InnerJoin}. Both must have exactly the
+ * 	same behavior when {@link #getDBColumns()} is called. That's why the static function
+ * 	{@link InnerJoin#getDBColumns(ADQLJoin)} has been created. It is called by {@link SQLServer_InnerJoin}
+ * 	and {@link SQLServer_OuterJoin}.
+ * </p>
+ * 
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 1.4 (03/2016)
+ * @since 1.4
+ * 
+ * @see SQLServer_ADQLQueryFactory
+ * @see SQLServer_InnerJoin
+ */
+public class SQLServer_OuterJoin extends OuterJoin {
+
+	/**
+	 * Builds a NATURAL OUTER join between the two given "tables".
+	 * 
+	 * @param left	Left "table".
+	 * @param right	Right "table".
+	 * @param type	OUTER join type (left, right or full).
+	 * 
+	 * @see OuterJoin#OuterJoin(FromContent, FromContent, OuterType)
+	 */
+	public SQLServer_OuterJoin(FromContent left, FromContent right, OuterType type) {
+		super(left, right, type);
+	}
+
+	/**
+	 * Builds an OUTER join between the two given "tables" with the given condition.
+	 * 
+	 * @param left		Left "table".
+	 * @param right		Right "table".
+	 * @param type		Outer join type (left, right or full).
+	 * @param condition	Join condition.
+	 * 
+	 * @see OuterJoin#OuterJoin(FromContent, FromContent, OuterType, ClauseConstraints)
+	 */
+	public SQLServer_OuterJoin(FromContent left, FromContent right, OuterType type, ClauseConstraints condition) {
+		super(left, right, type, condition);
+	}
+
+	/**
+	 * Builds an OUTER join between the two given "tables" with a list of columns to join.
+	 * 
+	 * @param left			Left "table".
+	 * @param right			Right "table".
+	 * @param type			Outer join type.
+	 * @param lstColumns	List of columns to join.
+	 * 
+	 * @see OuterJoin#OuterJoin(FromContent, FromContent, OuterType, Collection)
+	 */
+	public SQLServer_OuterJoin(FromContent left, FromContent right, OuterType type, Collection<ADQLColumn> lstColumns) {
+		super(left, right, type, lstColumns);
+	}
+
+	/**
+	 * Builds a copy of the given OUTER join.
+	 * 
+	 * @param toCopy		The OUTER join to copy.
+	 * 
+	 * @throws Exception	If there is an error during the copy.
+	 * 
+	 * @see OuterJoin#OuterJoin(OuterJoin)
+	 */
+	public SQLServer_OuterJoin(OuterJoin toCopy) throws Exception {
+		super(toCopy);
+	}
+
+	@Override
+	public SearchColumnList getDBColumns() throws UnresolvedJoinException{
+		return SQLServer_InnerJoin.getDBColumns(this);
+	}
+
+}
diff --git a/src/adql/translator/SQLServerTranslator.java b/src/adql/translator/SQLServerTranslator.java
new file mode 100644
index 0000000..775f1b4
--- /dev/null
+++ b/src/adql/translator/SQLServerTranslator.java
@@ -0,0 +1,398 @@
+package adql.translator;
+
+/*
+ * 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 <http://www.gnu.org/licenses/>.
+ * 
+ * Copyright 2016 - Astronomisches Rechen Institut (ARI)
+ */
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import adql.db.DBChecker;
+import adql.db.DBColumn;
+import adql.db.DBTable;
+import adql.db.DBType;
+import adql.db.DefaultDBColumn;
+import adql.db.DefaultDBTable;
+import adql.db.SearchColumnList;
+import adql.db.DBType.DBDatatype;
+import adql.db.STCS.Region;
+import adql.db.exception.UnresolvedJoinException;
+import adql.parser.ADQLParser;
+import adql.parser.ParseException;
+import adql.parser.SQLServer_ADQLQueryFactory;
+import adql.query.ADQLQuery;
+import adql.query.IdentifierField;
+import adql.query.from.ADQLJoin;
+import adql.query.operand.ADQLColumn;
+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;
+
+/**
+ * <p>MS SQL Server translator.</p>
+ * 
+ * <p><b>Important:</b>
+ * 	This translator works correctly ONLY IF {@link SQLServer_ADQLQueryFactory} has been used
+ * 	to create any ADQL query this translator is asked to translate.
+ * </p>
+ * 
+ * TODO See how case sensitivity is supported by MS SQL Server and modify this translator accordingly.
+ * 
+ * TODO Extend this class for each MS SQL Server extension supporting geometry and particularly
+ *      {@link #translateGeometryFromDB(Object)}, {@link #translateGeometryToDB(Region)} and all this other
+ *      translate(...) functions for the ADQL's geometrical functions.
+ * 
+ * TODO Check MS SQL Server datatypes (see {@link #convertTypeFromDB(int, String, String, String[])},
+ *      {@link #convertTypeToDB(DBType)}).
+ * 
+ * <p><i><b>Important note:</b>
+ * 	Geometrical functions are not translated ; the translation returned for them is their ADQL expression.
+ * </i></p>
+ * 
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 1.4 (03/2016)
+ * @since 1.4
+ * 
+ * @see SQLServer_ADQLQueryFactory
+ */
+public class SQLServerTranslator extends JDBCTranslator {
+	
+	/* TODO Temporary MAIN function.
+	 *      TO REMOVE for the release. */
+	public final static void main(final String[] args) throws Exception {
+		final String adqlquery = "SELECT id, name, aColumn, anotherColumn FROM aTable A NATURAL JOIN anotherTable B;";
+		System.out.println("ADQL Query:\n"+adqlquery);
+		
+		ArrayList<DBTable> tables = new ArrayList<DBTable>(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);
+		
+		ADQLQuery query = (new ADQLParser(new DBChecker(tables), new SQLServer_ADQLQueryFactory())).parseQuery(adqlquery);
+		
+		SQLServerTranslator translator = new SQLServerTranslator();
+		System.out.println("\nIn MS SQL Server:\n"+translator.translate(query));
+	}
+
+	/** <p>Indicate the case sensitivity to apply to each SQL identifier (only SCHEMA, TABLE and COLUMN).</p>
+	 * 
+	 * <p><i>Note:
+	 * 	In this implementation, this field is set by the constructor and never modified elsewhere.
+	 * 	It would be better to never modify it after the construction in order to keep a certain consistency.
+	 * </i></p>
+	 */
+	protected byte caseSensitivity = 0x00;
+
+	/**
+	 * Builds an SQLServerTranslator which always translates in SQL all identifiers (schema, table and column) in a case sensitive manner ;
+	 * in other words, schema, table and column names will be surrounded by double quotes in the SQL translation.
+	 */
+	public SQLServerTranslator(){
+		caseSensitivity = 0x0F;
+	}
+
+	/**
+	 * Builds an SQLServerTranslator which always translates in SQL all identifiers (schema, table and column) in the specified case sensitivity ;
+	 * in other words, schema, table and column names will all be surrounded or not by double quotes in the SQL translation.
+	 * 
+	 * @param allCaseSensitive	<i>true</i> to translate all identifiers in a case sensitive manner (surrounded by double quotes), <i>false</i> for case insensitivity. 
+	 */
+	public SQLServerTranslator(final boolean allCaseSensitive){
+		caseSensitivity = allCaseSensitive ? (byte)0x0F : (byte)0x00;
+	}
+
+	/**
+	 * Builds an SQLServerTranslator which will always translate in SQL identifiers with the defined case sensitivity.
+	 * 
+	 * @param catalog	<i>true</i> to translate catalog names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise.
+	 * @param schema	<i>true</i> to translate schema names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise.
+	 * @param table		<i>true</i> to translate table names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise.
+	 * @param column	<i>true</i> to translate column names with double quotes (case sensitive in the DBMS), <i>false</i> otherwise.
+	 */
+	public SQLServerTranslator(final boolean catalog, final boolean schema, final boolean table, final boolean column){
+		caseSensitivity = IdentifierField.CATALOG.setCaseSensitive(caseSensitivity, catalog);
+		caseSensitivity = IdentifierField.SCHEMA.setCaseSensitive(caseSensitivity, schema);
+		caseSensitivity = IdentifierField.TABLE.setCaseSensitive(caseSensitivity, table);
+		caseSensitivity = IdentifierField.COLUMN.setCaseSensitive(caseSensitivity, column);
+	}
+
+	@Override
+	public boolean isCaseSensitive(final IdentifierField field) {
+		return field == null ? false : field.isCaseSensitive(caseSensitivity);
+	}
+
+	@Override
+	public String translate(final ADQLJoin join) throws TranslationException {
+		StringBuffer sql = new StringBuffer(translate(join.getLeftTable()));
+
+		sql.append(' ').append(join.getJoinType()).append(' ').append(translate(join.getRightTable())).append(' ');
+
+		// CASE: NATURAL
+		if (join.isNatural()){
+			try{
+				StringBuffer buf = new StringBuffer();
+			
+				// Find duplicated items between the two lists and translate them as ON conditions:
+				DBColumn rightCol;
+				SearchColumnList leftList = join.getLeftTable().getDBColumns();
+				SearchColumnList rightList = join.getRightTable().getDBColumns();
+				for(DBColumn leftCol : leftList){
+					// search for at most one column with the same name in the RIGHT list
+					// and throw an exception is there are several matches:
+					rightCol = ADQLJoin.findAtMostOneColumn(leftCol.getADQLName(), (byte)0, rightList, false);
+					// if there is one...
+					if (rightCol != null){
+						// ...check there is only one column with this name in the LEFT list,
+						// and throw an exception if it is not the case:
+						ADQLJoin.findExactlyOneColumn(leftCol.getADQLName(), (byte)0, leftList, true);
+						// ...append the corresponding join condition:
+						if (buf.length() > 0)
+							buf.append(" AND ");
+						buf.append(getQualifiedTableName(leftCol.getTable())).append('.').append(getColumnName(leftCol));
+						buf.append("=");
+						buf.append(getQualifiedTableName(rightCol.getTable())).append('.').append(getColumnName(rightCol));
+					}
+				}
+	
+				sql.append("ON ").append(buf.toString());
+			}catch(UnresolvedJoinException uje){
+				throw new TranslationException("Impossible to resolve the NATURAL JOIN between "+join.getLeftTable().toADQL()+" and "+join.getRightTable().toADQL()+"!", uje);
+			}
+		}
+		// CASE: USING
+		else if (join.hasJoinedColumns()){
+			try{
+				StringBuffer buf = new StringBuffer();
+				
+				// For each columns of usingList, check there is in each list exactly one matching column, and then, translate it as ON condition:
+				DBColumn leftCol, rightCol;
+				ADQLColumn usingCol;
+				SearchColumnList leftList = join.getLeftTable().getDBColumns();
+				SearchColumnList rightList = join.getRightTable().getDBColumns();
+				Iterator<ADQLColumn> itCols = join.getJoinedColumns();
+				while(itCols.hasNext()){
+					usingCol = itCols.next();
+					// search for exactly one column with the same name in the LEFT list
+					// and throw an exception if there is none, or if there are several matches:
+					leftCol = ADQLJoin.findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), leftList, true);
+					// idem in the RIGHT list:
+					rightCol = ADQLJoin.findExactlyOneColumn(usingCol.getColumnName(), usingCol.getCaseSensitive(), rightList, false);
+					// append the corresponding join condition:
+					if (buf.length() > 0)
+						buf.append(" AND ");
+					buf.append(getQualifiedTableName(leftCol.getTable())).append('.').append(getColumnName(leftCol));
+					buf.append("=");
+					buf.append(getQualifiedTableName(rightCol.getTable())).append('.').append(getColumnName(rightCol));
+				}
+				
+				sql.append("ON ").append(buf.toString());
+			}catch(UnresolvedJoinException uje){
+				throw new TranslationException("Impossible to resolve the JOIN USING between "+join.getLeftTable().toADQL()+" and "+join.getRightTable().toADQL()+"!", uje);
+			}
+		}
+		// DEFAULT CASE:
+		else
+			sql.append(translate(join.getJoinCondition()));
+
+		return sql.toString();
+	}
+	
+	@Override
+	public String translate(final ExtractCoord extractCoord) throws TranslationException {
+		return getDefaultADQLFunction(extractCoord);
+	}
+
+	@Override
+	public String translate(final ExtractCoordSys extractCoordSys) throws TranslationException {
+		return getDefaultADQLFunction(extractCoordSys);
+	}
+
+	@Override
+	public String translate(final AreaFunction areaFunction) throws TranslationException {
+		return getDefaultADQLFunction(areaFunction);
+	}
+
+	@Override
+	public String translate(final CentroidFunction centroidFunction) throws TranslationException {
+		return getDefaultADQLFunction(centroidFunction);
+	}
+
+	@Override
+	public String translate(final DistanceFunction fct) throws TranslationException {
+		return getDefaultADQLFunction(fct);
+	}
+
+	@Override
+	public String translate(final ContainsFunction fct) throws TranslationException {
+		return getDefaultADQLFunction(fct);
+	}
+
+	@Override
+	public String translate(final IntersectsFunction fct) throws TranslationException {
+		return getDefaultADQLFunction(fct);
+	}
+
+	@Override
+	public String translate(final PointFunction point) throws TranslationException {
+		return getDefaultADQLFunction(point);
+	}
+
+	@Override
+	public String translate(final CircleFunction circle) throws TranslationException {
+		return getDefaultADQLFunction(circle);
+	}
+
+	@Override
+	public String translate(final BoxFunction box) throws TranslationException {
+		return getDefaultADQLFunction(box);
+	}
+
+	@Override
+	public String translate(final PolygonFunction polygon) throws TranslationException {
+		return getDefaultADQLFunction(polygon);
+	}
+
+	@Override
+	public String translate(final RegionFunction region) throws TranslationException {
+		return getDefaultADQLFunction(region);
+	}
+
+	@Override
+	public DBType convertTypeFromDB(final int dbmsType, final String rawDbmsTypeName, String dbmsTypeName, final String[] params){
+		// If no type is provided return VARCHAR:
+		if (dbmsTypeName == null || dbmsTypeName.trim().length() == 0)
+			return null;
+
+		// Put the dbmsTypeName in lower case for the following comparisons:
+		dbmsTypeName = dbmsTypeName.toLowerCase();
+
+		// Extract the length parameter (always the first one):
+		int lengthParam = DBType.NO_LENGTH;
+		if (params != null && params.length > 0){
+			try{
+				lengthParam = Integer.parseInt(params[0]);
+			}catch(NumberFormatException nfe){}
+		}
+
+		// SMALLINT
+		if (dbmsTypeName.equals("smallint") || dbmsTypeName.equals("tinyint") || dbmsTypeName.equals("bit"))
+			return new DBType(DBDatatype.SMALLINT);
+		// INTEGER
+		else if (dbmsTypeName.equals("int"))
+			return new DBType(DBDatatype.INTEGER);
+		// BIGINT
+		else if (dbmsTypeName.equals("bigint"))
+			return new DBType(DBDatatype.BIGINT);
+		// REAL (cf https://msdn.microsoft.com/fr-fr/library/ms173773(v=sql.120).aspx)
+		else if (dbmsTypeName.equals("real") || (dbmsTypeName.equals("float") && lengthParam >= 1 && lengthParam <= 24))
+			return new DBType(DBDatatype.REAL);
+		// DOUBLE (cf https://msdn.microsoft.com/fr-fr/library/ms173773(v=sql.120).aspx)
+		else if (dbmsTypeName.equals("float") || dbmsTypeName.equals("decimal") || dbmsTypeName.equals("numeric"))
+			return new DBType(DBDatatype.DOUBLE);
+		// BINARY
+		else if (dbmsTypeName.equals("binary"))
+			return new DBType(DBDatatype.BINARY, lengthParam);
+		// VARBINARY
+		else if (dbmsTypeName.equals("varbinary"))
+			return new DBType(DBDatatype.VARBINARY, lengthParam);
+		// CHAR
+		else if (dbmsTypeName.equals("char") || dbmsTypeName.equals("nchar"))
+			return new DBType(DBDatatype.CHAR, lengthParam);
+		// VARCHAR
+		else if (dbmsTypeName.equals("varchar") || dbmsTypeName.equals("nvarchar"))
+			return new DBType(DBDatatype.VARCHAR, lengthParam);
+		// BLOB
+		else if (dbmsTypeName.equals("image"))
+			return new DBType(DBDatatype.BLOB);
+		// CLOB
+		else if (dbmsTypeName.equals("text") || dbmsTypeName.equals("ntext"))
+			return new DBType(DBDatatype.CLOB);
+		// TIMESTAMP
+		else if (dbmsTypeName.equals("timestamp") || dbmsTypeName.equals("datetime") || dbmsTypeName.equals("datetime2") || dbmsTypeName.equals("datetimeoffset") || dbmsTypeName.equals("smalldatetime") || dbmsTypeName.equals("time") || dbmsTypeName.equals("date") || dbmsTypeName.equals("date"))
+			return new DBType(DBDatatype.TIMESTAMP);
+		// Default:
+		else
+			return null;
+	}
+
+	@Override
+	public String convertTypeToDB(final DBType type){
+		if (type == null)
+			return "varchar";
+
+		switch(type.type){
+
+			case SMALLINT:
+			case REAL:
+			case BIGINT:
+			case CHAR:
+			case VARCHAR:
+			case BINARY:
+			case VARBINARY:
+				return type.type.toString().toLowerCase();
+
+			case INTEGER:
+				return "int";
+
+			// (cf https://msdn.microsoft.com/fr-fr/library/ms173773(v=sql.120).aspx)
+			case DOUBLE:
+				return "float(53)";
+
+			case TIMESTAMP:
+				return "datetime";
+
+			case BLOB:
+				return "image";
+
+			case CLOB:
+				return "text";
+
+			case POINT:
+			case REGION:
+			default:
+				return "varchar";
+		}
+	}
+
+	@Override
+	public Region translateGeometryFromDB(final Object jdbcColValue) throws ParseException{
+		throw new ParseException("Unsupported geometrical value! The value \"" + jdbcColValue + "\" can not be parsed as a region.");
+	}
+
+	@Override
+	public Object translateGeometryToDB(final Region region) throws ParseException{
+		throw new ParseException("Geometries can not be uploaded in the database in this implementation!");
+	}
+
+}
diff --git a/test/adql/query/from/TestSQLServer_InnerJoin.java b/test/adql/query/from/TestSQLServer_InnerJoin.java
new file mode 100644
index 0000000..70be938
--- /dev/null
+++ 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.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 TestSQLServer_InnerJoin {
+
+	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 SQLServer_InnerJoin(tableA, tableB);
+			SearchColumnList joinColumns = join.getDBColumns();
+			assertEquals(3, joinColumns.size());
+			List<DBColumn> 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!");
+		}
+
+		// 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<DBColumn> 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", "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("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<ADQLColumn> usingList = new ArrayList<ADQLColumn>(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<DBColumn> 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", "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());
+
+			// 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());
+
+		}catch(Exception ex){
+			ex.printStackTrace();
+			fail("This test should have succeeded!");
+		}
+	}
+
+}
diff --git a/test/adql/translator/TestSQLServerTranslator.java b/test/adql/translator/TestSQLServerTranslator.java
new file mode 100644
index 0000000..d3a0290
--- /dev/null
+++ b/test/adql/translator/TestSQLServerTranslator.java
@@ -0,0 +1,85 @@
+package adql.translator;
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+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 class TestSQLServerTranslator {
+	
+	private List<DBTable> tables = null;
+
+	@Before
+	public void setUp() throws Exception {
+		tables = new ArrayList<DBTable>(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);
+	}
+
+	@Test
+	public void testNaturalJoin() {
+		final String adqlquery = "SELECT id, name, aColumn, anotherColumn FROM aTable A NATURAL JOIN anotherTable B;";
+		
+		try{
+			ADQLQuery query = (new ADQLParser(new DBChecker(tables), new SQLServer_ADQLQueryFactory())).parseQuery(adqlquery);
+			SQLServerTranslator translator = new SQLServerTranslator();
+			
+			// Test the FROM part:
+			assertEquals("\"aTable\" AS A INNER JOIN \"anotherTable\" AS B ON \"aTable\".\"id\"=\"anotherTable\".\"id\" AND \"aTable\".\"name\"=\"anotherTable\".\"name\"", translator.translate(query.getFrom()));
+			
+			// 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()));
+			
+		}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)");
+		}
+	}
+
+	@Test
+	public void testJoinWithUSING() {
+		final String adqlquery = "SELECT B.id, name, aColumn, anotherColumn FROM aTable A JOIN anotherTable B USING(name);";
+		
+		try{
+			ADQLQuery query = (new ADQLParser(new DBChecker(tables), new SQLServer_ADQLQueryFactory())).parseQuery(adqlquery);
+			SQLServerTranslator translator = new SQLServerTranslator();
+			
+			// Test the FROM part:
+			assertEquals("\"aTable\" AS A INNER JOIN \"anotherTable\" AS B ON \"aTable\".\"name\"=\"anotherTable\".\"name\"", translator.translate(query.getFrom()));
+			
+			// 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()));
+			
+		}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)");
+		}
+	}
+
+}
-- 
GitLab