From f90bce45692999ac0c82319e5dedd1ec63713dff Mon Sep 17 00:00:00 2001
From: gmantele <gmantele@ari.uni-heidelberg.de>
Date: Tue, 5 Aug 2014 18:15:23 +0200
Subject: [PATCH] [TAP,ADQL] Improve and remake a part of the database
 connection. Missing javadoc has been added when missing in the modified
 tables.

---
 src/adql/translator/ADQLTranslator.java       |    6 +-
 src/adql/translator/JDBCTranslator.java       |  803 ++++++
 src/adql/translator/PgSphereTranslator.java   |   36 +-
 src/adql/translator/PostgreSQLTranslator.java |  691 +----
 src/tap/ADQLExecutor.java                     |   10 +-
 src/tap/data/LimitedTableIterator.java        |  227 ++
 src/tap/data/ResultSetTableIterator.java      |  306 +-
 src/tap/data/TableIterator.java               |   14 +-
 src/tap/data/VOTableIterator.java             |    9 +
 src/tap/db/DBConnection.java                  |  171 +-
 src/tap/db/JDBCConnection.java                | 2477 +++++++++++++++--
 src/tap/db/JDBCTAPFactory.java                |  228 --
 src/tap/log/TAPLog.java                       |    2 +
 src/tap/metadata/TAPColumn.java               |  446 ++-
 src/tap/metadata/TAPDM.java                   |   52 -
 src/tap/metadata/TAPMetadata.java             |  524 +++-
 src/tap/metadata/TAPSchema.java               |  281 +-
 src/tap/metadata/TAPTable.java                |  583 +++-
 src/tap/upload/LimitedSizeInputStream.java    |    2 +-
 src/tap/upload/Uploader.java                  |    7 +-
 test/adql/IdentifierFieldTest.java            |   25 +
 test/adql/SearchColumnListTest.java           |    9 +-
 test/tap/data/ResultSetTableIteratorTest.java |   21 +-
 test/tap/data/VOTableIteratorTest.java        |   32 +-
 test/tap/db/JDBCConnectionTest.java           | 1015 +++++++
 test/tap/db/TestTAPDb.db                      |  Bin 0 -> 23552 bytes
 test/tap/db/upload_example.vot                |   75 +
 test/tap/formatter/JSONFormatTest.java        |    9 +-
 test/tap/formatter/SVFormatTest.java          |    9 +-
 test/tap/formatter/TextFormatTest.java        |    9 +-
 test/tap/formatter/VOTableFormatTest.java     |   13 +-
 test/testtools/DBTools.java                   |    2 +-
 test/testtools/MD5Checksum.java               |   46 +
 33 files changed, 6794 insertions(+), 1346 deletions(-)
 create mode 100644 src/adql/translator/JDBCTranslator.java
 create mode 100644 src/tap/data/LimitedTableIterator.java
 delete mode 100644 src/tap/db/JDBCTAPFactory.java
 delete mode 100644 src/tap/metadata/TAPDM.java
 create mode 100644 test/adql/IdentifierFieldTest.java
 create mode 100644 test/tap/db/JDBCConnectionTest.java
 create mode 100644 test/tap/db/TestTAPDb.db
 create mode 100644 test/tap/db/upload_example.vot
 create mode 100644 test/testtools/MD5Checksum.java

diff --git a/src/adql/translator/ADQLTranslator.java b/src/adql/translator/ADQLTranslator.java
index 7ec6bf9..1174f08 100644
--- a/src/adql/translator/ADQLTranslator.java
+++ b/src/adql/translator/ADQLTranslator.java
@@ -16,7 +16,8 @@ package adql.translator;
  * You should have received a copy of the GNU Lesser General Public License
  * along with ADQLLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
- * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
+ * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
 import adql.query.ADQLList;
@@ -28,7 +29,6 @@ import adql.query.ClauseSelect;
 import adql.query.ColumnReference;
 import adql.query.SelectAllColumns;
 import adql.query.SelectItem;
-
 import adql.query.constraint.ADQLConstraint;
 import adql.query.constraint.Between;
 import adql.query.constraint.Comparison;
@@ -60,11 +60,11 @@ 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.GeometryFunction;
+import adql.query.operand.function.geometry.GeometryFunction.GeometryValue;
 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;
-import adql.query.operand.function.geometry.GeometryFunction.GeometryValue;
 
 /**
  * Translates ADQL objects into any language (i.e. SQL).
diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java
new file mode 100644
index 0000000..5f791cf
--- /dev/null
+++ b/src/adql/translator/JDBCTranslator.java
@@ -0,0 +1,803 @@
+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 2014 - Astronomisches Rechen Institut (ARI)
+ */
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+
+import adql.db.DBColumn;
+import adql.db.DBTable;
+import adql.db.exception.UnresolvedJoin;
+import adql.query.ADQLList;
+import adql.query.ADQLObject;
+import adql.query.ADQLOrder;
+import adql.query.ADQLQuery;
+import adql.query.ClauseConstraints;
+import adql.query.ClauseSelect;
+import adql.query.ColumnReference;
+import adql.query.IdentifierField;
+import adql.query.SelectAllColumns;
+import adql.query.SelectItem;
+import adql.query.constraint.ADQLConstraint;
+import adql.query.constraint.Between;
+import adql.query.constraint.Comparison;
+import adql.query.constraint.ConstraintsGroup;
+import adql.query.constraint.Exists;
+import adql.query.constraint.In;
+import adql.query.constraint.IsNull;
+import adql.query.constraint.NotConstraint;
+import adql.query.from.ADQLJoin;
+import adql.query.from.ADQLTable;
+import adql.query.from.FromContent;
+import adql.query.operand.ADQLColumn;
+import adql.query.operand.ADQLOperand;
+import adql.query.operand.Concatenation;
+import adql.query.operand.NegativeOperand;
+import adql.query.operand.NumericConstant;
+import adql.query.operand.Operation;
+import adql.query.operand.StringConstant;
+import adql.query.operand.WrappedOperand;
+import adql.query.operand.function.ADQLFunction;
+import adql.query.operand.function.MathFunction;
+import adql.query.operand.function.SQLFunction;
+import adql.query.operand.function.SQLFunctionType;
+import adql.query.operand.function.UserDefinedFunction;
+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.GeometryFunction;
+import adql.query.operand.function.geometry.GeometryFunction.GeometryValue;
+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>Implementation of {@link ADQLTranslator} which translates ADQL queries in SQL queries.</p>
+ * 
+ * <p>
+ * 	It is already able to translate all SQL standard features, but lets abstract the translation of all
+ * 	geometrical functions. So, this translator must be extended as {@link PostgreSQLTranslator} and
+ * 	{@link PgSphereTranslator} are doing.
+ * </p>
+ * 
+ * <p><i>Note:
+ * 	Its default implementation of the SQL syntax has been inspired by the PostgreSQL one.
+ * 	However, it should work also with SQLite and MySQL, but some translations might be needed
+ * 	(as it is has been done for PostgreSQL about the mathematical functions).
+ * </i></p>
+ * 
+ * <h3>PostgreSQLTranslator and PgSphereTranslator</h3>
+ * 
+ * <p>
+ * 	{@link PgSphereTranslator} extends {@link PostgreSQLTranslator} and is just translating geometrical
+ * 	functions according to the syntax given by PgSphere.
+ * </p>
+ * 
+ * <p>
+ * 	{@link PostgreSQLTranslator} overwrites the translation of mathematical functions whose some have
+ * 	a different name or signature. Besides, it is also implementing the translation of the geometrical
+ * 	functions. However, it does not really translate them. It is just returning the ADQL expression
+ * 	(by calling {@link #getDefaultADQLFunction(ADQLFunction)}).
+ * 	And so, of course, the execution of a SQL query containing geometrical functions and translated
+ * 	using this translator will not work. It is just a default implementation in case there is no interest
+ * 	of these geometrical functions.
+ * </p>
+ * 
+ * <h3>SQL with or without case sensitivity?</h3>
+ * 
+ * <p>
+ * 	In ADQL and in SQL, it is possible to tell the parser to respect the exact case or not of an identifier (schema, table or column name)
+ * 	by surrounding it with double quotes. However ADQL identifiers and SQL ones may be different. In that way, the case sensitivity specified
+ * 	in ADQL on the different identifiers can not be kept in SQL. That's why this translator lets specify a general rule on which types of
+ * 	SQL identifier must be double quoted. This can be done by implementing the abstract function {@link #isCaseSensitive(IdentifierField)}.
+ * 	The functions translating column and table names will call this function in order to surround the identifiers by double quotes or not.
+ * 	So, <b>be careful if you want to override the functions translating columns and tables!</b>
+ * </p>
+ * 
+ * <h3>Translation of "SELECT TOP"</h3>
+ * 
+ * <p>
+ * 	The default behavior of this translator is to translate the ADQL "TOP" into the SQL "LIMIT" at the end of the query.
+ * 	This is ok for some DBMS, but not all. So, if your DBMS does not know the "LIMIT" keyword, you should override the function
+ * 	translating the whole query: {@link #translate(ADQLQuery)}. Here is its current implementation: 
+ * </p>
+ * <pre>
+ * 	StringBuffer sql = new StringBuffer(translate(query.getSelect()));
+ * 	sql.append("\nFROM ").append(translate(query.getFrom()));
+ *	if (!query.getWhere().isEmpty())
+ *		sql.append('\n').append(translate(query.getWhere()));
+ *	if (!query.getGroupBy().isEmpty())
+ *		sql.append('\n').append(translate(query.getGroupBy()));
+ *	if (!query.getHaving().isEmpty())
+ *		sql.append('\n').append(translate(query.getHaving()));
+ *	if (!query.getOrderBy().isEmpty())
+ *		sql.append('\n').append(translate(query.getOrderBy()));
+ *	if (query.getSelect().hasLimit())
+ *		sql.append("\nLimit ").append(query.getSelect().getLimit());
+ *	return sql.toString();
+ * </pre>
+ * 
+ * <h3>Translation of ADQL functions</h3>
+ * 
+ * <p>
+ * 	All ADQL functions are by default not translated. Consequently, the SQL translation is
+ * 	actually the ADQL expression. Generally the ADQL expression is generic enough. However some mathematical functions may need
+ * 	to be translated differently. For instance {@link PostgreSQLTranslator} is translating differently: LOG, LOG10, RAND and TRUNC. 
+ * </p>
+ * 
+ * <p><i>Note:
+ * 	Geometrical function have not been translated here. They stay abstract because it is obviously impossible to have a generic
+ * 	translation ; it totally depends from the database system.
+ * </i></p>
+ * 
+ * <h3>Translation of "FROM" with JOINs</h3>
+ * 
+ * <p>
+ * 	The FROM clause is translated into SQL as written in ADQL. There is no differences except the identifiers that are replaced.
+ * 	The tables' aliases and their case sensitivity are kept like in ADQL.
+ * </p>
+ * 
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 2.0 (08/2014)
+ * @since 2.0
+ * 
+ * @see PostgreSQLTranslator
+ * @see PgSphereTranslator
+ */
+public abstract class JDBCTranslator implements ADQLTranslator {
+
+	/**
+	 * <p>Tell whether the specified identifier MUST be translated so that being interpreted case sensitively or not.
+	 * By default, an identifier that must be translated with case sensitivity will be surrounded by double quotes.
+	 * But, if this function returns FALSE, the SQL name will be written just as given in the metadata, without double quotes.</p>
+	 * 
+	 * <p><b>WARNING</b>:
+	 * 	An {@link IdentifierField} object can be a SCHEMA, TABLE, COLUMN and ALIAS. However, in this translator,
+	 * 	aliases are translated like in ADQL (so, with the same case sensitivity specification as in ADQL).
+	 * 	So, this function will never be used to know the case sensitivity to apply to an alias. It is then
+	 * 	useless to write a special behavior for the ALIAS value.
+	 * </p>
+	 * 
+	 * @param field	The identifier whose the case sensitive to apply is asked.
+	 * 
+	 * @return	<i>true</i> if the specified identifier must be translated case sensitivity, <i>false</i> otherwise (included if ALIAS or NULL).
+	 */
+	public abstract boolean isCaseSensitive(final IdentifierField field);
+
+	/**
+	 * <p>Get the qualified DB name of the schema containing the given table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function will, by default, add double quotes if the schema name must be case sensitive in the SQL query.
+	 * 	This information is provided by {@link #isCaseSensitive(IdentifierField)}.
+	 * </i></p>
+	 * 
+	 * @param table	A table of the schema whose the qualified DB name is asked.
+	 * 
+	 * @return	The qualified (with DB catalog name prefix if any, and with double quotes if needed) DB schema name,
+	 *        	or an empty string if there is no schema or no DB name.
+	 */
+	public String getQualifiedSchemaName(final DBTable table){
+		if (table == null || table.getDBSchemaName() == null)
+			return "";
+
+		StringBuffer buf = new StringBuffer();
+
+		if (table.getDBCatalogName() != null)
+			appendIdentifier(buf, table.getDBCatalogName(), IdentifierField.CATALOG).append('.');
+
+		appendIdentifier(buf, table.getDBSchemaName(), IdentifierField.SCHEMA);
+
+		return buf.toString();
+	}
+
+	/**
+	 * <p>Get the qualified DB name of the given table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function will, by default, add double quotes if the table name must be case sensitive in the SQL query.
+	 * 	This information is provided by {@link #isCaseSensitive(IdentifierField)}.
+	 * </i></p>
+	 * 
+	 * @param table	The table whose the qualified DB name is asked.
+	 * 
+	 * @return	The qualified (with DB catalog and schema prefix if any, and with double quotes if needed) DB table name,
+	 *        	or an empty string if the given table is NULL or if there is no DB name.
+	 */
+	public String getQualifiedTableName(final DBTable table){
+		if (table == null)
+			return "";
+
+		StringBuffer buf = new StringBuffer(getQualifiedSchemaName(table));
+		if (buf.length() > 0)
+			buf.append('.');
+
+		appendIdentifier(buf, table.getDBName(), IdentifierField.TABLE);
+
+		return buf.toString();
+	}
+
+	/**
+	 * <p>Get the DB name of the given column</p>
+	 *  
+	 * <p><i>Note:
+	 * 	This function will, by default, add double quotes if the column name must be case sensitive in the SQL query.
+	 * 	This information is provided by {@link #isCaseSensitive(IdentifierField)}.
+	 * </i></p>
+	 * 
+	 * <p><b>Caution:
+	 * 	The given column may be NULL and in this case an empty string will be returned.
+	 * 	But if the given column is not NULL, its DB name MUST NOT BE NULL!
+	 * </b></p>
+	 * 
+	 * @param column	The column whose the DB name is asked.
+	 * 
+	 * @return	The DB column name (with double quotes if needed),
+	 *        	or an empty string if the given column is NULL.
+	 */
+	public String getColumnName(final DBColumn column){
+		return (column == null) ? "" : appendIdentifier(new StringBuffer(), column.getDBName(), IdentifierField.COLUMN).toString();
+	}
+
+	/**
+	 * Appends the given identifier in the given StringBuffer.
+	 * 
+	 * @param str		The string buffer.
+	 * @param id		The identifier to append.
+	 * @param field		The type of identifier (column, table, schema, catalog or alias ?).
+	 * 
+	 * @return			The string buffer + identifier.
+	 */
+	public final StringBuffer appendIdentifier(final StringBuffer str, final String id, final IdentifierField field){
+		return appendIdentifier(str, id, isCaseSensitive(field));
+	}
+
+	/**
+	 * Appends the given identifier to the given StringBuffer.
+	 * 
+	 * @param str				The string buffer.
+	 * @param id				The identifier to append.
+	 * @param caseSensitive		<i>true</i> to format the identifier so that preserving the case sensitivity, <i>false</i> otherwise.
+	 * 
+	 * @return					The string buffer + identifier.
+	 */
+	public static final StringBuffer appendIdentifier(final StringBuffer str, final String id, final boolean caseSensitive){
+		if (caseSensitive)
+			return str.append('"').append(id).append('"');
+		else
+			return str.append(id);
+	}
+
+	@Override
+	@SuppressWarnings({"unchecked","rawtypes"})
+	public String translate(ADQLObject obj) throws TranslationException{
+		if (obj instanceof ADQLQuery)
+			return translate((ADQLQuery)obj);
+		else if (obj instanceof ADQLList)
+			return translate((ADQLList)obj);
+		else if (obj instanceof SelectItem)
+			return translate((SelectItem)obj);
+		else if (obj instanceof ColumnReference)
+			return translate((ColumnReference)obj);
+		else if (obj instanceof ADQLTable)
+			return translate((ADQLTable)obj);
+		else if (obj instanceof ADQLJoin)
+			return translate((ADQLJoin)obj);
+		else if (obj instanceof ADQLOperand)
+			return translate((ADQLOperand)obj);
+		else if (obj instanceof ADQLConstraint)
+			return translate((ADQLConstraint)obj);
+		else
+			return obj.toADQL();
+	}
+
+	@Override
+	public String translate(ADQLQuery query) throws TranslationException{
+		StringBuffer sql = new StringBuffer(translate(query.getSelect()));
+
+		sql.append("\nFROM ").append(translate(query.getFrom()));
+
+		if (!query.getWhere().isEmpty())
+			sql.append('\n').append(translate(query.getWhere()));
+
+		if (!query.getGroupBy().isEmpty())
+			sql.append('\n').append(translate(query.getGroupBy()));
+
+		if (!query.getHaving().isEmpty())
+			sql.append('\n').append(translate(query.getHaving()));
+
+		if (!query.getOrderBy().isEmpty())
+			sql.append('\n').append(translate(query.getOrderBy()));
+
+		if (query.getSelect().hasLimit())
+			sql.append("\nLimit ").append(query.getSelect().getLimit());
+
+		return sql.toString();
+	}
+
+	/* *************************** */
+	/* ****** LIST & CLAUSE ****** */
+	/* *************************** */
+	@Override
+	public String translate(ADQLList<? extends ADQLObject> list) throws TranslationException{
+		if (list instanceof ClauseSelect)
+			return translate((ClauseSelect)list);
+		else if (list instanceof ClauseConstraints)
+			return translate((ClauseConstraints)list);
+		else
+			return getDefaultADQLList(list);
+	}
+
+	/**
+	 * Gets the default SQL output for a list of ADQL objects.
+	 * 
+	 * @param list	List to format into SQL.
+	 * 
+	 * @return		The corresponding SQL.
+	 * 
+	 * @throws TranslationException If there is an error during the translation.
+	 */
+	protected String getDefaultADQLList(ADQLList<? extends ADQLObject> list) throws TranslationException{
+		String sql = (list.getName() == null) ? "" : (list.getName() + " ");
+
+		for(int i = 0; i < list.size(); i++)
+			sql += ((i == 0) ? "" : (" " + list.getSeparator(i) + " ")) + translate(list.get(i));
+
+		return sql;
+	}
+
+	@Override
+	public String translate(ClauseSelect clause) throws TranslationException{
+		String sql = null;
+
+		for(int i = 0; i < clause.size(); i++){
+			if (i == 0){
+				sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : "");
+			}else
+				sql += " " + clause.getSeparator(i);
+
+			sql += " " + translate(clause.get(i));
+		}
+
+		return sql;
+	}
+
+	@Override
+	public String translate(ClauseConstraints clause) throws TranslationException{
+		if (clause instanceof ConstraintsGroup)
+			return "(" + getDefaultADQLList(clause) + ")";
+		else
+			return getDefaultADQLList(clause);
+	}
+
+	@Override
+	public String translate(SelectItem item) throws TranslationException{
+		if (item instanceof SelectAllColumns)
+			return translate((SelectAllColumns)item);
+
+		StringBuffer translation = new StringBuffer(translate(item.getOperand()));
+		if (item.hasAlias()){
+			translation.append(" AS ");
+			appendIdentifier(translation, item.getAlias(), item.isCaseSensitive());
+		}else
+			translation.append(" AS ").append(item.getName());
+
+		return translation.toString();
+	}
+
+	@Override
+	public String translate(SelectAllColumns item) throws TranslationException{
+		HashMap<String,String> mapAlias = new HashMap<String,String>();
+
+		// Fetch the full list of columns to display:
+		Iterable<DBColumn> dbCols = null;
+		if (item.getAdqlTable() != null && item.getAdqlTable().getDBLink() != null){
+			ADQLTable table = item.getAdqlTable();
+			dbCols = table.getDBLink();
+			if (table.hasAlias()){
+				String key = getQualifiedTableName(table.getDBLink());
+				mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias());
+			}
+		}else if (item.getQuery() != null){
+			try{
+				dbCols = item.getQuery().getFrom().getDBColumns();
+			}catch(UnresolvedJoin pe){
+				throw new TranslationException("Due to a join problem, the ADQL to SQL translation can not be completed!", pe);
+			}
+			ArrayList<ADQLTable> tables = item.getQuery().getFrom().getTables();
+			for(ADQLTable table : tables){
+				if (table.hasAlias()){
+					String key = getQualifiedTableName(table.getDBLink());
+					mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias());
+				}
+			}
+		}
+
+		// Write the DB name of all these columns:
+		if (dbCols != null){
+			StringBuffer cols = new StringBuffer();
+			for(DBColumn col : dbCols){
+				if (cols.length() > 0)
+					cols.append(',');
+				if (col.getTable() != null){
+					String fullDbName = getQualifiedTableName(col.getTable());
+					if (mapAlias.containsKey(fullDbName))
+						appendIdentifier(cols, mapAlias.get(fullDbName), false).append('.');
+					else
+						cols.append(fullDbName).append('.');
+				}
+				appendIdentifier(cols, col.getDBName(), IdentifierField.COLUMN);
+				cols.append(" AS \"").append(col.getADQLName()).append('\"');
+			}
+			return (cols.length() > 0) ? cols.toString() : item.toADQL();
+		}else{
+			return item.toADQL();
+		}
+	}
+
+	@Override
+	public String translate(ColumnReference ref) throws TranslationException{
+		if (ref instanceof ADQLOrder)
+			return translate((ADQLOrder)ref);
+		else
+			return getDefaultColumnReference(ref);
+	}
+
+	/**
+	 * Gets the default SQL output for a column reference.
+	 * 
+	 * @param ref	The column reference to format into SQL.
+	 * 
+	 * @return		The corresponding SQL.
+	 * 
+	 * @throws TranslationException If there is an error during the translation.
+	 */
+	protected String getDefaultColumnReference(ColumnReference ref) throws TranslationException{
+		if (ref.isIndex()){
+			return "" + ref.getColumnIndex();
+		}else{
+			if (ref.getDBLink() == null){
+				return (ref.isCaseSensitive() ? ("\"" + ref.getColumnName() + "\"") : ref.getColumnName());
+			}else{
+				DBColumn dbCol = ref.getDBLink();
+				StringBuffer colName = new StringBuffer();
+				// Use the table alias if any:
+				if (ref.getAdqlTable() != null && ref.getAdqlTable().hasAlias())
+					appendIdentifier(colName, ref.getAdqlTable().getAlias(), ref.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.');
+
+				// Use the DBTable if any:
+				else if (dbCol.getTable() != null)
+					colName.append(getQualifiedTableName(dbCol.getTable())).append('.');
+
+				appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN);
+
+				return colName.toString();
+			}
+		}
+	}
+
+	@Override
+	public String translate(ADQLOrder order) throws TranslationException{
+		return getDefaultColumnReference(order) + (order.isDescSorting() ? " DESC" : " ASC");
+	}
+
+	/* ************************** */
+	/* ****** TABLE & JOIN ****** */
+	/* ************************** */
+	@Override
+	public String translate(FromContent content) throws TranslationException{
+		if (content instanceof ADQLTable)
+			return translate((ADQLTable)content);
+		else if (content instanceof ADQLJoin)
+			return translate((ADQLJoin)content);
+		else
+			return content.toADQL();
+	}
+
+	@Override
+	public String translate(ADQLTable table) throws TranslationException{
+		StringBuffer sql = new StringBuffer();
+
+		// CASE: SUB-QUERY:
+		if (table.isSubQuery())
+			sql.append('(').append(translate(table.getSubQuery())).append(')');
+
+		// CASE: TABLE REFERENCE:
+		else{
+			// Use the corresponding DB table, if known:
+			if (table.getDBLink() != null)
+				sql.append(getQualifiedTableName(table.getDBLink()));
+			// Otherwise, use the whole table name given in the ADQL query:
+			else
+				sql.append(table.getFullTableName());
+		}
+
+		// Add the table alias, if any:
+		if (table.hasAlias()){
+			sql.append(" AS ");
+			appendIdentifier(sql, table.getAlias(), table.isCaseSensitive(IdentifierField.ALIAS));
+		}
+
+		return sql.toString();
+	}
+
+	@Override
+	public String translate(ADQLJoin join) throws TranslationException{
+		StringBuffer sql = new StringBuffer(translate(join.getLeftTable()));
+
+		if (join.isNatural())
+			sql.append(" NATURAL");
+
+		sql.append(' ').append(join.getJoinType()).append(' ').append(translate(join.getRightTable())).append(' ');
+
+		if (!join.isNatural()){
+			if (join.getJoinCondition() != null)
+				sql.append(translate(join.getJoinCondition()));
+			else if (join.hasJoinedColumns()){
+				StringBuffer cols = new StringBuffer();
+				Iterator<ADQLColumn> it = join.getJoinedColumns();
+				while(it.hasNext()){
+					ADQLColumn item = it.next();
+					if (cols.length() > 0)
+						cols.append(", ");
+					if (item.getDBLink() == null)
+						appendIdentifier(cols, item.getColumnName(), item.isCaseSensitive(IdentifierField.COLUMN));
+					else
+						appendIdentifier(cols, item.getDBLink().getDBName(), IdentifierField.COLUMN);
+				}
+				sql.append("USING (").append(cols).append(')');
+			}
+		}
+
+		return sql.toString();
+	}
+
+	/* ********************* */
+	/* ****** OPERAND ****** */
+	/* ********************* */
+	@Override
+	public String translate(ADQLOperand op) throws TranslationException{
+		if (op instanceof ADQLColumn)
+			return translate((ADQLColumn)op);
+		else if (op instanceof Concatenation)
+			return translate((Concatenation)op);
+		else if (op instanceof NegativeOperand)
+			return translate((NegativeOperand)op);
+		else if (op instanceof NumericConstant)
+			return translate((NumericConstant)op);
+		else if (op instanceof StringConstant)
+			return translate((StringConstant)op);
+		else if (op instanceof WrappedOperand)
+			return translate((WrappedOperand)op);
+		else if (op instanceof Operation)
+			return translate((Operation)op);
+		else if (op instanceof ADQLFunction)
+			return translate((ADQLFunction)op);
+		else
+			return op.toADQL();
+	}
+
+	@Override
+	public String translate(ADQLColumn column) throws TranslationException{
+		// Use its DB name if known:
+		if (column.getDBLink() != null){
+			DBColumn dbCol = column.getDBLink();
+			StringBuffer colName = new StringBuffer();
+			// Use the table alias if any:
+			if (column.getAdqlTable() != null && column.getAdqlTable().hasAlias())
+				appendIdentifier(colName, column.getAdqlTable().getAlias(), column.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.');
+
+			// Use the DBTable if any:
+			else if (dbCol.getTable() != null && dbCol.getTable().getDBName() != null)
+				colName.append(getQualifiedTableName(dbCol.getTable())).append('.');
+
+			// Otherwise, use the prefix of the column given in the ADQL query:
+			else if (column.getTableName() != null)
+				colName = column.getFullColumnPrefix().append('.');
+
+			appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN);
+
+			return colName.toString();
+		}
+		// Otherwise, use the whole name given in the ADQL query:
+		else
+			return column.getFullColumnName();
+	}
+
+	@Override
+	public String translate(Concatenation concat) throws TranslationException{
+		return translate((ADQLList<ADQLOperand>)concat);
+	}
+
+	@Override
+	public String translate(NegativeOperand negOp) throws TranslationException{
+		return "-" + translate(negOp.getOperand());
+	}
+
+	@Override
+	public String translate(NumericConstant numConst) throws TranslationException{
+		return numConst.getValue();
+	}
+
+	@Override
+	public String translate(StringConstant strConst) throws TranslationException{
+		return "'" + strConst.getValue() + "'";
+	}
+
+	@Override
+	public String translate(WrappedOperand op) throws TranslationException{
+		return "(" + translate(op.getOperand()) + ")";
+	}
+
+	@Override
+	public String translate(Operation op) throws TranslationException{
+		return translate(op.getLeftOperand()) + op.getOperation().toADQL() + translate(op.getRightOperand());
+	}
+
+	/* ************************ */
+	/* ****** CONSTRAINT ****** */
+	/* ************************ */
+	@Override
+	public String translate(ADQLConstraint cons) throws TranslationException{
+		if (cons instanceof Comparison)
+			return translate((Comparison)cons);
+		else if (cons instanceof Between)
+			return translate((Between)cons);
+		else if (cons instanceof Exists)
+			return translate((Exists)cons);
+		else if (cons instanceof In)
+			return translate((In)cons);
+		else if (cons instanceof IsNull)
+			return translate((IsNull)cons);
+		else if (cons instanceof NotConstraint)
+			return translate((NotConstraint)cons);
+		else
+			return cons.toADQL();
+	}
+
+	@Override
+	public String translate(Comparison comp) throws TranslationException{
+		return translate(comp.getLeftOperand()) + " " + comp.getOperator().toADQL() + " " + translate(comp.getRightOperand());
+	}
+
+	@Override
+	public String translate(Between comp) throws TranslationException{
+		return translate(comp.getLeftOperand()) + " BETWEEN " + translate(comp.getMinOperand()) + " AND " + translate(comp.getMaxOperand());
+	}
+
+	@Override
+	public String translate(Exists exists) throws TranslationException{
+		return "EXISTS(" + translate(exists.getSubQuery()) + ")";
+	}
+
+	@Override
+	public String translate(In in) throws TranslationException{
+		return translate(in.getOperand()) + " " + in.getName() + " (" + (in.hasSubQuery() ? translate(in.getSubQuery()) : translate(in.getValuesList())) + ")";
+	}
+
+	@Override
+	public String translate(IsNull isNull) throws TranslationException{
+		return translate(isNull.getColumn()) + " IS " + (isNull.isNotNull() ? "NOT " : "") + "NULL";
+	}
+
+	@Override
+	public String translate(NotConstraint notCons) throws TranslationException{
+		return "NOT " + translate(notCons.getConstraint());
+	}
+
+	/* *********************** */
+	/* ****** FUNCTIONS ****** */
+	/* *********************** */
+	@Override
+	public String translate(ADQLFunction fct) throws TranslationException{
+		if (fct instanceof GeometryFunction)
+			return translate((GeometryFunction)fct);
+		else if (fct instanceof MathFunction)
+			return translate((MathFunction)fct);
+		else if (fct instanceof SQLFunction)
+			return translate((SQLFunction)fct);
+		else if (fct instanceof UserDefinedFunction)
+			return translate((UserDefinedFunction)fct);
+		else
+			return getDefaultADQLFunction(fct);
+	}
+
+	/**
+	 * Gets the default SQL output for the given ADQL function.
+	 * 
+	 * @param fct	The ADQL function to format into SQL.
+	 * 
+	 * @return		The corresponding SQL.
+	 * 
+	 * @throws TranslationException	If there is an error during the translation.
+	 */
+	protected final String getDefaultADQLFunction(ADQLFunction fct) throws TranslationException{
+		String sql = fct.getName() + "(";
+
+		for(int i = 0; i < fct.getNbParameters(); i++)
+			sql += ((i == 0) ? "" : ", ") + translate(fct.getParameter(i));
+
+		return sql + ")";
+	}
+
+	@Override
+	public String translate(SQLFunction fct) throws TranslationException{
+		if (fct.getType() == SQLFunctionType.COUNT_ALL)
+			return "COUNT(" + (fct.isDistinct() ? "DISTINCT " : "") + "*)";
+		else
+			return fct.getName() + "(" + (fct.isDistinct() ? "DISTINCT " : "") + translate(fct.getParameter(0)) + ")";
+	}
+
+	@Override
+	public String translate(MathFunction fct) throws TranslationException{
+		return getDefaultADQLFunction(fct);
+	}
+
+	@Override
+	public String translate(UserDefinedFunction fct) throws TranslationException{
+		return getDefaultADQLFunction(fct);
+	}
+
+	/* *********************************** */
+	/* ****** GEOMETRICAL FUNCTIONS ****** */
+	/* *********************************** */
+	@Override
+	public String translate(GeometryFunction fct) throws TranslationException{
+		if (fct instanceof AreaFunction)
+			return translate((AreaFunction)fct);
+		else if (fct instanceof BoxFunction)
+			return translate((BoxFunction)fct);
+		else if (fct instanceof CentroidFunction)
+			return translate((CentroidFunction)fct);
+		else if (fct instanceof CircleFunction)
+			return translate((CircleFunction)fct);
+		else if (fct instanceof ContainsFunction)
+			return translate((ContainsFunction)fct);
+		else if (fct instanceof DistanceFunction)
+			return translate((DistanceFunction)fct);
+		else if (fct instanceof ExtractCoord)
+			return translate((ExtractCoord)fct);
+		else if (fct instanceof ExtractCoordSys)
+			return translate((ExtractCoordSys)fct);
+		else if (fct instanceof IntersectsFunction)
+			return translate((IntersectsFunction)fct);
+		else if (fct instanceof PointFunction)
+			return translate((PointFunction)fct);
+		else if (fct instanceof PolygonFunction)
+			return translate((PolygonFunction)fct);
+		else if (fct instanceof RegionFunction)
+			return translate((RegionFunction)fct);
+		else
+			return getDefaultADQLFunction(fct);
+	}
+
+	@Override
+	public String translate(GeometryValue<? extends GeometryFunction> geomValue) throws TranslationException{
+		return translate(geomValue.getValue());
+	}
+
+}
diff --git a/src/adql/translator/PgSphereTranslator.java b/src/adql/translator/PgSphereTranslator.java
index efe3807..fd02dfa 100644
--- a/src/adql/translator/PgSphereTranslator.java
+++ b/src/adql/translator/PgSphereTranslator.java
@@ -16,12 +16,12 @@ package adql.translator;
  * You should have received a copy of the GNU Lesser General Public License
  * along with ADQLLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
- * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
+ * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
 import adql.query.constraint.Comparison;
 import adql.query.constraint.ComparisonOperator;
-
 import adql.query.operand.function.geometry.AreaFunction;
 import adql.query.operand.function.geometry.BoxFunction;
 import adql.query.operand.function.geometry.CircleFunction;
@@ -32,24 +32,19 @@ import adql.query.operand.function.geometry.IntersectsFunction;
 import adql.query.operand.function.geometry.PointFunction;
 import adql.query.operand.function.geometry.PolygonFunction;
 
-import adql.translator.PostgreSQLTranslator;
-import adql.translator.TranslationException;
-
 /**
  * <p>Translates all ADQL objects into the SQL adaptation of Postgres+PgSphere.
  * Actually only the geometrical functions are translated in this class.
  * The other functions are managed by {@link PostgreSQLTranslator}.</p>
  * 
- * @author Gr&eacute;gory Mantelet (CDS)
- * @version 01/2012
- * 
- * @see PostgreSQLTranslator
+ * @author Gr&eacute;gory Mantelet (CDS;ARI)
+ * @version 2.0 (08/2014)
  */
 public class PgSphereTranslator extends PostgreSQLTranslator {
 
 	/**
-	 * Builds a PgSphereTranslator which takes into account the case sensitivity on column names.
-	 * It means that column names which have been written between double quotes, will be also translated between double quotes.
+	 * Builds a PgSphereTranslator 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.
 	 * 
 	 * @see PostgreSQLTranslator#PostgreSQLTranslator()
 	 */
@@ -58,23 +53,24 @@ public class PgSphereTranslator extends PostgreSQLTranslator {
 	}
 
 	/**
-	 * Builds a PgSphereTranslator.
+	 * Builds a PgSphereTranslator 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 column	<i>true</i> to take into account the case sensitivity of column names, <i>false</i> otherwise.
+	 * @param allCaseSensitive	<i>true</i> to translate all identifiers in a case sensitive manner (surrounded by double quotes), <i>false</i> for case insensitivity. 
 	 * 
 	 * @see PostgreSQLTranslator#PostgreSQLTranslator(boolean)
 	 */
-	public PgSphereTranslator(boolean column){
-		super(column);
+	public PgSphereTranslator(boolean allCaseSensitive){
+		super(allCaseSensitive);
 	}
 
 	/**
-	 * Builds a PgSphereTranslator.
+	 * Builds a PgSphereTranslator which will always translate in SQL identifiers with the defined case sensitivity.
 	 * 
-	 * @param catalog	<i>true</i> to take into account the case sensitivity of catalog names, <i>false</i> otherwise.
-	 * @param schema	<i>true</i> to take into account the case sensitivity of schema names, <i>false</i> otherwise.
-	 * @param table		<i>true</i> to take into account the case sensitivity of table names, <i>false</i> otherwise.
-	 * @param column	<i>true</i> to take into account the case sensitivity of column names, <i>false</i> otherwise.
+	 * @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.
 	 * 
 	 * @see PostgreSQLTranslator#PostgreSQLTranslator(boolean, boolean, boolean, boolean)
 	 */
diff --git a/src/adql/translator/PostgreSQLTranslator.java b/src/adql/translator/PostgreSQLTranslator.java
index 58cdc7a..9698462 100644
--- a/src/adql/translator/PostgreSQLTranslator.java
+++ b/src/adql/translator/PostgreSQLTranslator.java
@@ -17,50 +17,11 @@ package adql.translator;
  * along with ADQLLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
  * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
- *                       Astronomisches Rechen Institute (ARI)
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-
-import adql.db.DBColumn;
-import adql.db.DBTable;
-import adql.db.exception.UnresolvedJoin;
-import adql.query.ADQLList;
-import adql.query.ADQLObject;
-import adql.query.ADQLOrder;
-import adql.query.ADQLQuery;
-import adql.query.ClauseConstraints;
-import adql.query.ClauseSelect;
-import adql.query.ColumnReference;
 import adql.query.IdentifierField;
-import adql.query.SelectAllColumns;
-import adql.query.SelectItem;
-import adql.query.constraint.ADQLConstraint;
-import adql.query.constraint.Between;
-import adql.query.constraint.Comparison;
-import adql.query.constraint.ConstraintsGroup;
-import adql.query.constraint.Exists;
-import adql.query.constraint.In;
-import adql.query.constraint.IsNull;
-import adql.query.constraint.NotConstraint;
-import adql.query.from.ADQLJoin;
-import adql.query.from.ADQLTable;
-import adql.query.from.FromContent;
-import adql.query.operand.ADQLColumn;
-import adql.query.operand.ADQLOperand;
-import adql.query.operand.Concatenation;
-import adql.query.operand.NegativeOperand;
-import adql.query.operand.NumericConstant;
-import adql.query.operand.Operation;
-import adql.query.operand.StringConstant;
-import adql.query.operand.WrappedOperand;
-import adql.query.operand.function.ADQLFunction;
 import adql.query.operand.function.MathFunction;
-import adql.query.operand.function.SQLFunction;
-import adql.query.operand.function.SQLFunctionType;
-import adql.query.operand.function.UserDefinedFunction;
 import adql.query.operand.function.geometry.AreaFunction;
 import adql.query.operand.function.geometry.BoxFunction;
 import adql.query.operand.function.geometry.CentroidFunction;
@@ -69,54 +30,61 @@ 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.GeometryFunction;
-import adql.query.operand.function.geometry.GeometryFunction.GeometryValue;
 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>Translates all ADQL objects into the SQL adaptation of Postgres.</p>
+ * <p>Translates all ADQL objects into an SQL interrogation query designed for PostgreSQL.</p>
  * 
- * <p><b><u>IMPORTANT:</u> The geometrical functions are translated exactly as in ADQL.
- * You will probably need to extend this translator to correctly manage the geometrical functions.
- * An extension is already available for PgSphere: {@link PgSphereTranslator}.</b></p>
+ * <p><i><b>Important</b>:
+ * 	The geometrical functions are translated exactly as in ADQL.
+ * 	You will probably need to extend this translator to correctly manage the geometrical functions.
+ * 	An extension is already available for PgSphere: {@link PgSphereTranslator}.
+ * </i></p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 1.2 (03/2014)
+ * @version 2.0 (08/2014)
  * 
  * @see PgSphereTranslator
  */
-public class PostgreSQLTranslator implements ADQLTranslator {
+public class PostgreSQLTranslator extends JDBCTranslator {
 
-	protected boolean inSelect = false;
+	/** <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 a PostgreSQLTranslator which takes into account the case sensitivity on column names.
-	 * It means that column names which have been written between double quotes, will be also translated between double quotes.
+	 * Builds a PostgreSQLTranslator 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 PostgreSQLTranslator(){
-		this(true);
+		caseSensitivity = 0x0F;
 	}
 
 	/**
-	 * Builds a PostgreSQLTranslator.
+	 * Builds a PostgreSQLTranslator 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 column	<i>true</i> to take into account the case sensitivity of column names, <i>false</i> otherwise.
+	 * @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 PostgreSQLTranslator(final boolean column){
-		caseSensitivity = IdentifierField.COLUMN.setCaseSensitive(caseSensitivity, column);
+	public PostgreSQLTranslator(final boolean allCaseSensitive){
+		caseSensitivity = allCaseSensitive ? (byte)0x0F : (byte)0x00;
 	}
 
 	/**
-	 * Builds a PostgreSQLTranslator.
+	 * Builds a PostgreSQLTranslator which will always translate in SQL identifiers with the defined case sensitivity.
 	 * 
-	 * @param catalog	<i>true</i> to take into account the case sensitivity of catalog names, <i>false</i> otherwise.
-	 * @param schema	<i>true</i> to take into account the case sensitivity of schema names, <i>false</i> otherwise.
-	 * @param table		<i>true</i> to take into account the case sensitivity of table names, <i>false</i> otherwise.
-	 * @param column	<i>true</i> to take into account the case sensitivity of column names, <i>false</i> otherwise.
+	 * @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 PostgreSQLTranslator(final boolean catalog, final boolean schema, final boolean table, final boolean column){
 		caseSensitivity = IdentifierField.CATALOG.setCaseSensitive(caseSensitivity, catalog);
@@ -125,521 +93,9 @@ public class PostgreSQLTranslator implements ADQLTranslator {
 		caseSensitivity = IdentifierField.COLUMN.setCaseSensitive(caseSensitivity, column);
 	}
 
-	/**
-	 * Appends the full name of the given table to the given StringBuffer.
-	 * 
-	 * @param str		The string buffer.
-	 * @param dbTable	The table whose the full name must be appended.
-	 * 
-	 * @return			The string buffer + full table name.
-	 */
-	public final StringBuffer appendFullDBName(final StringBuffer str, final DBTable dbTable){
-		if (dbTable != null){
-			if (dbTable.getDBCatalogName() != null)
-				appendIdentifier(str, dbTable.getDBCatalogName(), IdentifierField.CATALOG).append('.');
-
-			if (dbTable.getDBSchemaName() != null)
-				appendIdentifier(str, dbTable.getDBSchemaName(), IdentifierField.SCHEMA).append('.');
-
-			appendIdentifier(str, dbTable.getDBName(), IdentifierField.TABLE);
-		}
-		return str;
-	}
-
-	/**
-	 * Appends the given identifier in the given StringBuffer.
-	 * 
-	 * @param str		The string buffer.
-	 * @param id		The identifier to append.
-	 * @param field		The type of identifier (column, table, schema, catalog or alias ?).
-	 * 
-	 * @return			The string buffer + identifier.
-	 */
-	public final StringBuffer appendIdentifier(final StringBuffer str, final String id, final IdentifierField field){
-		return appendIdentifier(str, id, field.isCaseSensitive(caseSensitivity));
-	}
-
-	/**
-	 * Appends the given identifier to the given StringBuffer.
-	 * 
-	 * @param str				The string buffer.
-	 * @param id				The identifier to append.
-	 * @param caseSensitive		<i>true</i> to format the identifier so that preserving the case sensitivity, <i>false</i> otherwise.
-	 * 
-	 * @return					The string buffer + identifier.
-	 */
-	public static final StringBuffer appendIdentifier(final StringBuffer str, final String id, final boolean caseSensitive){
-		if (caseSensitive)
-			return str.append('\"').append(id).append('\"');
-		else
-			return str.append(id);
-	}
-
-	@Override
-	@SuppressWarnings("unchecked")
-	public String translate(ADQLObject obj) throws TranslationException{
-		if (obj instanceof ADQLQuery)
-			return translate((ADQLQuery)obj);
-		else if (obj instanceof ADQLList)
-			return translate((ADQLList)obj);
-		else if (obj instanceof SelectItem)
-			return translate((SelectItem)obj);
-		else if (obj instanceof ColumnReference)
-			return translate((ColumnReference)obj);
-		else if (obj instanceof ADQLTable)
-			return translate((ADQLTable)obj);
-		else if (obj instanceof ADQLJoin)
-			return translate((ADQLJoin)obj);
-		else if (obj instanceof ADQLOperand)
-			return translate((ADQLOperand)obj);
-		else if (obj instanceof ADQLConstraint)
-			return translate((ADQLConstraint)obj);
-		else
-			return obj.toADQL();
-	}
-
-	@Override
-	public String translate(ADQLQuery query) throws TranslationException{
-		StringBuffer sql = new StringBuffer(translate(query.getSelect()));
-
-		sql.append("\nFROM ").append(translate(query.getFrom()));
-
-		if (!query.getWhere().isEmpty())
-			sql.append('\n').append(translate(query.getWhere()));
-
-		if (!query.getGroupBy().isEmpty())
-			sql.append('\n').append(translate(query.getGroupBy()));
-
-		if (!query.getHaving().isEmpty())
-			sql.append('\n').append(translate(query.getHaving()));
-
-		if (!query.getOrderBy().isEmpty())
-			sql.append('\n').append(translate(query.getOrderBy()));
-
-		if (query.getSelect().hasLimit())
-			sql.append("\nLimit ").append(query.getSelect().getLimit());
-
-		return sql.toString();
-	}
-
-	/* *************************** */
-	/* ****** LIST & CLAUSE ****** */
-	/* *************************** */
-	@Override
-	public String translate(ADQLList<? extends ADQLObject> list) throws TranslationException{
-		if (list instanceof ClauseSelect)
-			return translate((ClauseSelect)list);
-		else if (list instanceof ClauseConstraints)
-			return translate((ClauseConstraints)list);
-		else
-			return getDefaultADQLList(list);
-	}
-
-	/**
-	 * Gets the default SQL output for a list of ADQL objects.
-	 * 
-	 * @param list	List to format into SQL.
-	 * 
-	 * @return		The corresponding SQL.
-	 * 
-	 * @throws TranslationException If there is an error during the translation.
-	 */
-	protected String getDefaultADQLList(ADQLList<? extends ADQLObject> list) throws TranslationException{
-		String sql = (list.getName() == null) ? "" : (list.getName() + " ");
-
-		boolean oldInSelect = inSelect;
-		inSelect = (list.getName() != null) && list.getName().equalsIgnoreCase("select");
-
-		try{
-			for(int i = 0; i < list.size(); i++)
-				sql += ((i == 0) ? "" : (" " + list.getSeparator(i) + " ")) + translate(list.get(i));
-		}finally{
-			inSelect = oldInSelect;
-		}
-
-		return sql;
-	}
-
-	@Override
-	public String translate(ClauseSelect clause) throws TranslationException{
-		String sql = null;
-
-		for(int i = 0; i < clause.size(); i++){
-			if (i == 0){
-				sql = clause.getName() + (clause.distinctColumns() ? " DISTINCT" : "");
-			}else
-				sql += " " + clause.getSeparator(i);
-
-			sql += " " + translate(clause.get(i));
-		}
-
-		return sql;
-	}
-
-	@Override
-	public String translate(ClauseConstraints clause) throws TranslationException{
-		if (clause instanceof ConstraintsGroup)
-			return "(" + getDefaultADQLList(clause) + ")";
-		else
-			return getDefaultADQLList(clause);
-	}
-
-	@Override
-	public String translate(SelectItem item) throws TranslationException{
-		if (item instanceof SelectAllColumns)
-			return translate((SelectAllColumns)item);
-
-		StringBuffer translation = new StringBuffer(translate(item.getOperand()));
-		if (item.hasAlias()){
-			translation.append(" AS ");
-			appendIdentifier(translation, item.getAlias(), item.isCaseSensitive());
-		}else
-			translation.append(" AS ").append(item.getName());
-
-		return translation.toString();
-	}
-
-	@Override
-	public String translate(SelectAllColumns item) throws TranslationException{
-		HashMap<String,String> mapAlias = new HashMap<String,String>();
-
-		// Fetch the full list of columns to display:
-		Iterable<DBColumn> dbCols = null;
-		if (item.getAdqlTable() != null && item.getAdqlTable().getDBLink() != null){
-			ADQLTable table = item.getAdqlTable();
-			dbCols = table.getDBLink();
-			if (table.hasAlias()){
-				String key = appendFullDBName(new StringBuffer(), table.getDBLink()).toString();
-				mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias());
-			}
-		}else if (item.getQuery() != null){
-			try{
-				dbCols = item.getQuery().getFrom().getDBColumns();
-			}catch(UnresolvedJoin pe){
-				throw new TranslationException("Due to a join problem, the ADQL to SQL translation can not be completed!", pe);
-			}
-			ArrayList<ADQLTable> tables = item.getQuery().getFrom().getTables();
-			for(ADQLTable table : tables){
-				if (table.hasAlias()){
-					String key = appendFullDBName(new StringBuffer(), table.getDBLink()).toString();
-					mapAlias.put(key, table.isCaseSensitive(IdentifierField.ALIAS) ? ("\"" + table.getAlias() + "\"") : table.getAlias());
-				}
-			}
-		}
-
-		// Write the DB name of all these columns:
-		if (dbCols != null){
-			StringBuffer cols = new StringBuffer();
-			for(DBColumn col : dbCols){
-				if (cols.length() > 0)
-					cols.append(',');
-				if (col.getTable() != null){
-					String fullDbName = appendFullDBName(new StringBuffer(), col.getTable()).toString();
-					if (mapAlias.containsKey(fullDbName))
-						appendIdentifier(cols, mapAlias.get(fullDbName), false).append('.');
-					else
-						cols.append(fullDbName).append('.');
-				}
-				appendIdentifier(cols, col.getDBName(), IdentifierField.COLUMN);
-				cols.append(" AS \"").append(col.getADQLName()).append('\"');
-			}
-			return (cols.length() > 0) ? cols.toString() : item.toADQL();
-		}else{
-			return item.toADQL();
-		}
-	}
-
-	@Override
-	public String translate(ColumnReference ref) throws TranslationException{
-		if (ref instanceof ADQLOrder)
-			return translate((ADQLOrder)ref);
-		else
-			return getDefaultColumnReference(ref);
-	}
-
-	/**
-	 * Gets the default SQL output for a column reference.
-	 * 
-	 * @param ref	The column reference to format into SQL.
-	 * 
-	 * @return		The corresponding SQL.
-	 * 
-	 * @throws TranslationException If there is an error during the translation.
-	 */
-	protected String getDefaultColumnReference(ColumnReference ref) throws TranslationException{
-		if (ref.isIndex()){
-			return "" + ref.getColumnIndex();
-		}else{
-			if (ref.getDBLink() == null){
-				return (ref.isCaseSensitive() ? ("\"" + ref.getColumnName() + "\"") : ref.getColumnName());
-			}else{
-				DBColumn dbCol = ref.getDBLink();
-				StringBuffer colName = new StringBuffer();
-				// Use the table alias if any:
-				if (ref.getAdqlTable() != null && ref.getAdqlTable().hasAlias())
-					appendIdentifier(colName, ref.getAdqlTable().getAlias(), ref.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.');
-
-				// Use the DBTable if any:
-				else if (dbCol.getTable() != null)
-					appendFullDBName(colName, dbCol.getTable()).append('.');
-
-				appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN);
-
-				return colName.toString();
-			}
-		}
-	}
-
-	@Override
-	public String translate(ADQLOrder order) throws TranslationException{
-		return getDefaultColumnReference(order) + (order.isDescSorting() ? " DESC" : " ASC");
-	}
-
-	/* ************************** */
-	/* ****** TABLE & JOIN ****** */
-	/* ************************** */
-	@Override
-	public String translate(FromContent content) throws TranslationException{
-		if (content instanceof ADQLTable)
-			return translate((ADQLTable)content);
-		else if (content instanceof ADQLJoin)
-			return translate((ADQLJoin)content);
-		else
-			return content.toADQL();
-	}
-
-	@Override
-	public String translate(ADQLTable table) throws TranslationException{
-		StringBuffer sql = new StringBuffer();
-
-		// CASE: SUB-QUERY:
-		if (table.isSubQuery())
-			sql.append('(').append(translate(table.getSubQuery())).append(')');
-
-		// CASE: TABLE REFERENCE:
-		else{
-			// Use the corresponding DB table, if known:
-			if (table.getDBLink() != null)
-				appendFullDBName(sql, table.getDBLink());
-			// Otherwise, use the whole table name given in the ADQL query:
-			else
-				sql.append(table.getFullTableName());
-		}
-
-		// Add the table alias, if any:
-		if (table.hasAlias()){
-			sql.append(" AS ");
-			appendIdentifier(sql, table.getAlias(), table.isCaseSensitive(IdentifierField.ALIAS));
-		}
-
-		return sql.toString();
-	}
-
-	@Override
-	public String translate(ADQLJoin join) throws TranslationException{
-		StringBuffer sql = new StringBuffer(translate(join.getLeftTable()));
-
-		if (join.isNatural())
-			sql.append(" NATURAL");
-
-		sql.append(' ').append(join.getJoinType()).append(' ').append(translate(join.getRightTable())).append(' ');
-
-		if (!join.isNatural()){
-			if (join.getJoinCondition() != null)
-				sql.append(translate(join.getJoinCondition()));
-			else if (join.hasJoinedColumns()){
-				StringBuffer cols = new StringBuffer();
-				Iterator<ADQLColumn> it = join.getJoinedColumns();
-				while(it.hasNext()){
-					ADQLColumn item = it.next();
-					if (cols.length() > 0)
-						cols.append(", ");
-					if (item.getDBLink() == null)
-						appendIdentifier(cols, item.getColumnName(), item.isCaseSensitive(IdentifierField.COLUMN));
-					else
-						appendIdentifier(cols, item.getDBLink().getDBName(), IdentifierField.COLUMN);
-				}
-				sql.append("USING (").append(cols).append(')');
-			}
-		}
-
-		return sql.toString();
-	}
-
-	/* ********************* */
-	/* ****** OPERAND ****** */
-	/* ********************* */
-	@Override
-	public String translate(ADQLOperand op) throws TranslationException{
-		if (op instanceof ADQLColumn)
-			return translate((ADQLColumn)op);
-		else if (op instanceof Concatenation)
-			return translate((Concatenation)op);
-		else if (op instanceof NegativeOperand)
-			return translate((NegativeOperand)op);
-		else if (op instanceof NumericConstant)
-			return translate((NumericConstant)op);
-		else if (op instanceof StringConstant)
-			return translate((StringConstant)op);
-		else if (op instanceof WrappedOperand)
-			return translate((WrappedOperand)op);
-		else if (op instanceof Operation)
-			return translate((Operation)op);
-		else if (op instanceof ADQLFunction)
-			return translate((ADQLFunction)op);
-		else
-			return op.toADQL();
-	}
-
-	@Override
-	public String translate(ADQLColumn column) throws TranslationException{
-		// Use its DB name if known:
-		if (column.getDBLink() != null){
-			DBColumn dbCol = column.getDBLink();
-			StringBuffer colName = new StringBuffer();
-			// Use the table alias if any:
-			if (column.getAdqlTable() != null && column.getAdqlTable().hasAlias())
-				appendIdentifier(colName, column.getAdqlTable().getAlias(), column.getAdqlTable().isCaseSensitive(IdentifierField.ALIAS)).append('.');
-
-			// Use the DBTable if any:
-			else if (dbCol.getTable() != null && dbCol.getTable().getDBName() != null)
-				appendFullDBName(colName, dbCol.getTable()).append('.');
-
-			// Otherwise, use the prefix of the column given in the ADQL query:
-			else if (column.getTableName() != null)
-				colName = column.getFullColumnPrefix().append('.');
-
-			appendIdentifier(colName, dbCol.getDBName(), IdentifierField.COLUMN);
-
-			return colName.toString();
-		}
-		// Otherwise, use the whole name given in the ADQL query:
-		else
-			return column.getFullColumnName();
-	}
-
-	@Override
-	public String translate(Concatenation concat) throws TranslationException{
-		return translate((ADQLList<ADQLOperand>)concat);
-	}
-
-	@Override
-	public String translate(NegativeOperand negOp) throws TranslationException{
-		return "-" + translate(negOp.getOperand());
-	}
-
-	@Override
-	public String translate(NumericConstant numConst) throws TranslationException{
-		return numConst.getValue();
-	}
-
-	@Override
-	public String translate(StringConstant strConst) throws TranslationException{
-		return "'" + strConst.getValue() + "'";
-	}
-
-	@Override
-	public String translate(WrappedOperand op) throws TranslationException{
-		return "(" + translate(op.getOperand()) + ")";
-	}
-
-	@Override
-	public String translate(Operation op) throws TranslationException{
-		return translate(op.getLeftOperand()) + op.getOperation().toADQL() + translate(op.getRightOperand());
-	}
-
-	/* ************************ */
-	/* ****** CONSTRAINT ****** */
-	/* ************************ */
-	@Override
-	public String translate(ADQLConstraint cons) throws TranslationException{
-		if (cons instanceof Comparison)
-			return translate((Comparison)cons);
-		else if (cons instanceof Between)
-			return translate((Between)cons);
-		else if (cons instanceof Exists)
-			return translate((Exists)cons);
-		else if (cons instanceof In)
-			return translate((In)cons);
-		else if (cons instanceof IsNull)
-			return translate((IsNull)cons);
-		else if (cons instanceof NotConstraint)
-			return translate((NotConstraint)cons);
-		else
-			return cons.toADQL();
-	}
-
-	@Override
-	public String translate(Comparison comp) throws TranslationException{
-		return translate(comp.getLeftOperand()) + " " + comp.getOperator().toADQL() + " " + translate(comp.getRightOperand());
-	}
-
 	@Override
-	public String translate(Between comp) throws TranslationException{
-		return translate(comp.getLeftOperand()) + " BETWEEN " + translate(comp.getMinOperand()) + " AND " + translate(comp.getMaxOperand());
-	}
-
-	@Override
-	public String translate(Exists exists) throws TranslationException{
-		return "EXISTS(" + translate(exists.getSubQuery()) + ")";
-	}
-
-	@Override
-	public String translate(In in) throws TranslationException{
-		return translate(in.getOperand()) + " " + in.getName() + " (" + (in.hasSubQuery() ? translate(in.getSubQuery()) : translate(in.getValuesList())) + ")";
-	}
-
-	@Override
-	public String translate(IsNull isNull) throws TranslationException{
-		return translate(isNull.getColumn()) + " IS " + (isNull.isNotNull() ? "NOT " : "") + "NULL";
-	}
-
-	@Override
-	public String translate(NotConstraint notCons) throws TranslationException{
-		return "NOT " + translate(notCons.getConstraint());
-	}
-
-	/* *********************** */
-	/* ****** FUNCTIONS ****** */
-	/* *********************** */
-	@Override
-	public String translate(ADQLFunction fct) throws TranslationException{
-		if (fct instanceof GeometryFunction)
-			return translate((GeometryFunction)fct);
-		else if (fct instanceof MathFunction)
-			return translate((MathFunction)fct);
-		else if (fct instanceof SQLFunction)
-			return translate((SQLFunction)fct);
-		else if (fct instanceof UserDefinedFunction)
-			return translate((UserDefinedFunction)fct);
-		else
-			return getDefaultADQLFunction(fct);
-	}
-
-	/**
-	 * Gets the default SQL output for the given ADQL function.
-	 * 
-	 * @param fct	The ADQL function to format into SQL.
-	 * 
-	 * @return		The corresponding SQL.
-	 * 
-	 * @throws TranslationException	If there is an error during the translation.
-	 */
-	protected String getDefaultADQLFunction(ADQLFunction fct) throws TranslationException{
-		String sql = fct.getName() + "(";
-
-		for(int i = 0; i < fct.getNbParameters(); i++)
-			sql += ((i == 0) ? "" : ", ") + translate(fct.getParameter(i));
-
-		return sql + ")";
-	}
-
-	@Override
-	public String translate(SQLFunction fct) throws TranslationException{
-		if (fct.getType() == SQLFunctionType.COUNT_ALL)
-			return "COUNT(" + (fct.isDistinct() ? "DISTINCT " : "") + "*)";
-		else
-			return fct.getName() + "(" + (fct.isDistinct() ? "DISTINCT " : "") + translate(fct.getParameter(0)) + ")";
+	public boolean isCaseSensitive(final IdentifierField field){
+		return field == null ? false : field.isCaseSensitive(caseSensitivity);
 	}
 
 	@Override
@@ -658,125 +114,64 @@ public class PostgreSQLTranslator implements ADQLTranslator {
 		}
 	}
 
-	@Override
-	public String translate(UserDefinedFunction fct) throws TranslationException{
-		return getDefaultADQLFunction(fct);
-	}
-
-	/* *********************************** */
-	/* ****** GEOMETRICAL FUNCTIONS ****** */
-	/* *********************************** */
-	@Override
-	public String translate(GeometryFunction fct) throws TranslationException{
-		if (fct instanceof AreaFunction)
-			return translate((AreaFunction)fct);
-		else if (fct instanceof BoxFunction)
-			return translate((BoxFunction)fct);
-		else if (fct instanceof CentroidFunction)
-			return translate((CentroidFunction)fct);
-		else if (fct instanceof CircleFunction)
-			return translate((CircleFunction)fct);
-		else if (fct instanceof ContainsFunction)
-			return translate((ContainsFunction)fct);
-		else if (fct instanceof DistanceFunction)
-			return translate((DistanceFunction)fct);
-		else if (fct instanceof ExtractCoord)
-			return translate((ExtractCoord)fct);
-		else if (fct instanceof ExtractCoordSys)
-			return translate((ExtractCoordSys)fct);
-		else if (fct instanceof IntersectsFunction)
-			return translate((IntersectsFunction)fct);
-		else if (fct instanceof PointFunction)
-			return translate((PointFunction)fct);
-		else if (fct instanceof PolygonFunction)
-			return translate((PolygonFunction)fct);
-		else if (fct instanceof RegionFunction)
-			return translate((RegionFunction)fct);
-		else
-			return getDefaultGeometryFunction(fct);
-	}
-
-	/**
-	 * <p>Gets the default SQL output for the given geometrical function.</p>
-	 * 
-	 * <p><i><u>Note:</u> By default, only the ADQL serialization is returned.</i></p>
-	 * 
-	 * @param fct	The geometrical function to translate.
-	 * 
-	 * @return		The corresponding SQL.
-	 * 
-	 * @throws TranslationException If there is an error during the translation.
-	 */
-	protected String getDefaultGeometryFunction(GeometryFunction fct) throws TranslationException{
-		if (inSelect)
-			return "'" + fct.toADQL().replaceAll("'", "''") + "'";
-		else
-			return getDefaultADQLFunction(fct);
-	}
-
-	@Override
-	public String translate(GeometryValue<? extends GeometryFunction> geomValue) throws TranslationException{
-		return translate(geomValue.getValue());
-	}
-
 	@Override
 	public String translate(ExtractCoord extractCoord) throws TranslationException{
-		return getDefaultGeometryFunction(extractCoord);
+		return getDefaultADQLFunction(extractCoord);
 	}
 
 	@Override
 	public String translate(ExtractCoordSys extractCoordSys) throws TranslationException{
-		return getDefaultGeometryFunction(extractCoordSys);
+		return getDefaultADQLFunction(extractCoordSys);
 	}
 
 	@Override
 	public String translate(AreaFunction areaFunction) throws TranslationException{
-		return getDefaultGeometryFunction(areaFunction);
+		return getDefaultADQLFunction(areaFunction);
 	}
 
 	@Override
 	public String translate(CentroidFunction centroidFunction) throws TranslationException{
-		return getDefaultGeometryFunction(centroidFunction);
+		return getDefaultADQLFunction(centroidFunction);
 	}
 
 	@Override
 	public String translate(DistanceFunction fct) throws TranslationException{
-		return getDefaultGeometryFunction(fct);
+		return getDefaultADQLFunction(fct);
 	}
 
 	@Override
 	public String translate(ContainsFunction fct) throws TranslationException{
-		return getDefaultGeometryFunction(fct);
+		return getDefaultADQLFunction(fct);
 	}
 
 	@Override
 	public String translate(IntersectsFunction fct) throws TranslationException{
-		return getDefaultGeometryFunction(fct);
+		return getDefaultADQLFunction(fct);
 	}
 
 	@Override
 	public String translate(BoxFunction box) throws TranslationException{
-		return getDefaultGeometryFunction(box);
+		return getDefaultADQLFunction(box);
 	}
 
 	@Override
 	public String translate(CircleFunction circle) throws TranslationException{
-		return getDefaultGeometryFunction(circle);
+		return getDefaultADQLFunction(circle);
 	}
 
 	@Override
 	public String translate(PointFunction point) throws TranslationException{
-		return getDefaultGeometryFunction(point);
+		return getDefaultADQLFunction(point);
 	}
 
 	@Override
 	public String translate(PolygonFunction polygon) throws TranslationException{
-		return getDefaultGeometryFunction(polygon);
+		return getDefaultADQLFunction(polygon);
 	}
 
 	@Override
 	public String translate(RegionFunction region) throws TranslationException{
-		return getDefaultGeometryFunction(region);
+		return getDefaultADQLFunction(region);
 	}
 
 }
diff --git a/src/tap/ADQLExecutor.java b/src/tap/ADQLExecutor.java
index a7aee6f..93a8dc3 100644
--- a/src/tap/ADQLExecutor.java
+++ b/src/tap/ADQLExecutor.java
@@ -17,7 +17,7 @@ package tap;
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
  * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
- *                       Astronomisches Rechen Institute (ARI)
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
 import java.io.IOException;
@@ -261,8 +261,12 @@ public class ADQLExecutor {
 	}
 
 	protected void writeResult(TableIterator queryResult, OutputFormat formatter, OutputStream output) throws InterruptedException, TAPException{
-		//logger.info("Job "+report.jobID+" - 5/5 Writing result file...");
-		formatter.writeResult(queryResult, output, report, thread);
+		try{
+			//logger.info("Job "+report.jobID+" - 5/5 Writing result file...");
+			formatter.writeResult(queryResult, output, report, thread);
+		}finally{
+			queryResult.close();
+		}
 	}
 
 	protected void dropUploadedTables() throws TAPException{
diff --git a/src/tap/data/LimitedTableIterator.java b/src/tap/data/LimitedTableIterator.java
new file mode 100644
index 0000000..96c7c35
--- /dev/null
+++ b/src/tap/data/LimitedTableIterator.java
@@ -0,0 +1,227 @@
+package tap.data;
+
+/*
+ * This file is part of TAPLibrary.
+ * 
+ * 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 2014 - Astronomisches Rechen Institut (ARI)
+ */
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.NoSuchElementException;
+
+import tap.ServiceConnection.LimitUnit;
+import tap.metadata.TAPColumn;
+import tap.metadata.TAPType;
+import tap.upload.LimitedSizeInputStream;
+
+import com.oreilly.servlet.multipart.ExceededSizeException;
+
+/**
+ * <p>Wrap a {@link TableIterator} in order to limit its reading to a fixed number of rows.</p>
+ * 
+ * <p>
+ * 	This wrapper can be "mixed" with a {@link LimitedSizeInputStream}, by wrapping the original input stream by a {@link LimitedSizeInputStream}
+ * 	and then by wrapping the {@link TableIterator} based on this wrapped input stream by {@link LimitedTableIterator}.
+ * 	Thus, this wrapper will be able to detect embedded {@link ExceededSizeException} thrown by a {@link LimitedSizeInputStream} through another {@link TableIterator}.
+ * 	If a such exception is detected, it will declare this wrapper as overflowed as it would be if a rows limit is reached.
+ * </p>
+ * 
+ * <p><b>Warning:</b>
+ * 	To work together with a {@link LimitedSizeInputStream}, this wrapper relies on the hypothesis that any {@link IOException} (including {@link ExceededSizeException})
+ * 	will be embedded in a {@link DataReadException} as cause of this exception (using {@link DataReadException#DataReadException(Throwable)}
+ * 	or {@link DataReadException#DataReadException(String, Throwable)}). If it is not the case, no overflow detection could be done and the exception will just be forwarded.
+ * </p>
+ * 
+ * <p>
+ *	If a limit - either of rows or of bytes - is reached, a flag "overflow" is set to true. This flag can be got with {@link #isOverflow()}.
+ *	Thus, when a {@link DataReadException} is caught, it will be easy to detect whether the error occurred because of an overflow
+ *	or of another problem. 
+ * </p>
+ * 
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 2.0 (07/2014)
+ * @since 2.0
+ */
+public class LimitedTableIterator implements TableIterator {
+
+	/** The wrapped {@link TableIterator}. */
+	private final TableIterator innerIt;
+
+	/** Limit on the number of rows to read. <i>note: a negative value means "no limit".</i> */
+	private final int maxNbRows;
+
+	/** The number of rows already read. */
+	private int countRow = 0;
+
+	/** Indicate whether a limit (rows or bytes) has been reached or not. */
+	private boolean overflow = false;
+
+	/**
+	 * Wrap the given {@link TableIterator} so that limiting the number of rows to read to the given value.
+	 * 
+	 * @param it		The iterator to wrap. <i>MUST NOT be NULL</i>
+	 * @param maxNbRows	Maximum number of rows that can be read. There is overflow if more than this number of rows is asked. <i>A negative value means "no limit".</i>
+	 */
+	public LimitedTableIterator(final TableIterator it, final int nbMaxRows) throws DataReadException{
+		if (it == null)
+			throw new NullPointerException("Missing TableIterator to wrap!");
+		innerIt = it;
+		this.maxNbRows = nbMaxRows;
+	}
+
+	/**
+	 * Wrap the given {@link TableIterator} so that limiting the number of rows to read to the given value.
+	 * 
+	 * @param it		The iterator to wrap. <i>MUST NOT be NULL</i>
+	 * @param maxNbRows	Maximum number of rows that can be read. There is overflow if more than this number of rows is asked. <i>A negative value means "no limit".</i>
+	 */
+	public < T extends TableIterator > LimitedTableIterator(final Class<T> classIt, final InputStream input, final LimitUnit type, final int limit) throws DataReadException{
+		try{
+			Constructor<T> construct = classIt.getConstructor(InputStream.class);
+			if (type == LimitUnit.bytes){
+				maxNbRows = -1;
+				innerIt = construct.newInstance(new LimitedSizeInputStream(input, limit));
+			}else{
+				innerIt = construct.newInstance(input);
+				maxNbRows = (type == null) ? -1 : limit;
+			}
+		}catch(InvocationTargetException ite){
+			Throwable t = ite.getCause();
+			if (t != null && t instanceof DataReadException){
+				ExceededSizeException exceedEx = getExceededSizeException(t);
+				// if an error caused by an ExceedSizeException occurs, set this iterator as overflowed and throw the exception: 
+				if (exceedEx != null)
+					throw new DataReadException(exceedEx.getMessage(), exceedEx);
+				else
+					throw (DataReadException)t;
+			}else
+				throw new DataReadException("Can not create a LimitedTableIterator!", ite);
+		}catch(Exception ex){
+			throw new DataReadException("Can not create a LimitedTableIterator!", ex);
+		}
+	}
+
+	/**
+	 * Get the iterator wrapped by this {@link TableIterator} instance.
+	 * 
+	 * @return	The wrapped iterator.
+	 */
+	public final TableIterator getWrappedIterator(){
+		return innerIt;
+	}
+
+	/**
+	 * <p>Tell whether a limit (in rows or bytes) has been reached.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	If <i>true</i> is returned (that's to say, if a limit has been reached) no more rows or column values
+	 * 	can be read ; an {@link IllegalStateException} would then be thrown.
+	 * </i></p>
+	 * 
+	 * @return	<i>true</i> if a limit has been reached, <i>false</i> otherwise.
+	 */
+	public final boolean isOverflow(){
+		return overflow;
+	}
+
+	@Override
+	public void close() throws DataReadException{
+		innerIt.close();
+	}
+
+	@Override
+	public TAPColumn[] getMetadata(){
+		return innerIt.getMetadata();
+	}
+
+	@Override
+	public boolean nextRow() throws DataReadException{
+		// Test the overflow flag and proceed only if not overflowed:
+		if (overflow)
+			throw new DataReadException("Data read overflow: the limit has been reached! No more data can be read.");
+
+		// Read the next row:
+		boolean nextRow;
+		try{
+			nextRow = innerIt.nextRow();
+			countRow++;
+		}catch(DataReadException ex){
+			ExceededSizeException exceedEx = getExceededSizeException(ex);
+			// if an error caused by an ExceedSizeException occurs, set this iterator as overflowed and throw the exception: 
+			if (exceedEx != null){
+				overflow = true;
+				throw new DataReadException(exceedEx.getMessage());
+			}else
+				throw ex;
+		}
+
+		// If, counting this one, the number of rows exceeds the limit, set this iterator as overflowed and throw an exception:
+		if (nextRow && maxNbRows >= 0 && countRow > maxNbRows){
+			overflow = true;
+			throw new DataReadException("Data read overflow: the limit of " + maxNbRows + " rows has been reached!");
+		}
+
+		// Send back the value returned by the inner iterator:
+		return nextRow;
+	}
+
+	@Override
+	public boolean hasNextCol() throws IllegalStateException, DataReadException{
+		testOverflow();
+		return innerIt.hasNextCol();
+	}
+
+	@Override
+	public Object nextCol() throws NoSuchElementException, IllegalStateException, DataReadException{
+		testOverflow();
+		return innerIt.nextCol();
+	}
+
+	@Override
+	public TAPType getColType() throws IllegalStateException, DataReadException{
+		testOverflow();
+		return innerIt.getColType();
+	}
+
+	/**
+	 * Test the overflow flag and throw an {@link IllegalStateException} if <i>true</i>.
+	 * 
+	 * @throws IllegalStateException	If this iterator is overflowed (because of either a bytes limit or a rows limit).
+	 */
+	private void testOverflow() throws IllegalStateException{
+		if (overflow)
+			throw new IllegalStateException("Data read overflow: the limit has been reached! No more data can be read.");
+	}
+
+	/**
+	 * Get the first {@link ExceededSizeException} found in the given {@link Throwable} trace.
+	 * 
+	 * @param ex	A {@link Throwable}
+	 * 
+	 * @return	The first {@link ExceededSizeException} encountered, or NULL if none has been found.
+	 */
+	private ExceededSizeException getExceededSizeException(Throwable ex){
+		if (ex == null)
+			return null;
+		while(!(ex instanceof ExceededSizeException) && ex.getCause() != null)
+			ex = ex.getCause();
+		return (ex instanceof ExceededSizeException) ? (ExceededSizeException)ex : null;
+	}
+
+}
diff --git a/src/tap/data/ResultSetTableIterator.java b/src/tap/data/ResultSetTableIterator.java
index ae2fd1d..d632e72 100644
--- a/src/tap/data/ResultSetTableIterator.java
+++ b/src/tap/data/ResultSetTableIterator.java
@@ -24,17 +24,20 @@ import java.sql.ResultSetMetaData;
 import java.sql.SQLException;
 import java.util.NoSuchElementException;
 
-import tap.db.JDBCTAPFactory;
 import tap.metadata.TAPColumn;
 import tap.metadata.TAPType;
+import tap.metadata.TAPType.TAPDatatype;
+import adql.db.DBColumn;
 
 /**
  * <p>{@link TableIterator} which lets iterate over a SQL {@link ResultSet}.</p>
  * 
- * <p>{@link #getColType()} will return a TAP type base on the one declared in the {@link ResultSetMetaData} object.</p>
+ * <p><i>Note:
+ * 	{@link #getColType()} will return a TAP type based on the one declared in the {@link ResultSetMetaData} object.
+ * </i></p>
  * 
- * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
- * @version 2.0 (06/2014)
+ * @author Gr&eacute;gory Mantelet (ARI)
+ * @version 2.0 (08/2014)
  * @since 2.0
  */
 public class ResultSetTableIterator implements TableIterator {
@@ -55,27 +58,128 @@ public class ResultSetTableIterator implements TableIterator {
 	private int colIndex;
 
 	/**
-	 * Build a TableIterator able to read rows and columns of the given ResultSet.
+	 * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p>
 	 * 
-	 * @param dataSet	Dataset over which this iterator must iterate.
+	 * <p>
+	 * 	In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype
+	 * 	from the DBMS column datatype (using {@link #convertType(String, String)}).
+	 * </p>
+	 * 
+	 * <h3>Type guessing</h3>
+	 * 
+	 * <p>
+	 * 	In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)}
+	 * 	which deals with all standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby.
+	 * </p>
+	 * 
+	 * <p><i><b>Important</b>:
+	 * 	To guess the TAP type from a DBMS type, {@link #convertType(String, String)} may not need to know the DBMS,
+	 * 	except for SQLite. Indeed, SQLite has so many datatype restrictions that it is absolutely needed to know
+	 * 	it is the DBMS from which the ResultSet is coming. Without this information, type guessing will be unpredictable!
+	 * 
+	 * 	<b>So, if your ResultSet is coming from a SQLite connection, you SHOULD really use one of the 2 other constructors</b>
+	 * 	and provide "sqlite" as value for the second parameter. 
+	 * </i></p>
+	 * 
+	 * @param dataSet		Dataset over which this iterator must iterate.
 	 * 
 	 * @throws NullPointerException	If NULL is given in parameter.
-	 * @throws DataReadException	If the given ResultSet is closed
-	 *                              or if the metadata (columns count and types) can not be fetched.
+	 * @throws DataReadException	If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched.
+	 * 
+	 * @see #convertType(String, String)
+	 * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, String, DBColumn[])
 	 */
 	public ResultSetTableIterator(final ResultSet dataSet) throws NullPointerException, DataReadException{
+		this(dataSet, null, null);
+	}
+
+	/**
+	 * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p>
+	 * 
+	 * <p>
+	 * 	In order to provide the metadata through {@link #getMetadata()}, this constructor is trying to guess the datatype
+	 * 	from the DBMS column datatype (using {@link #convertType(String, String)}).
+	 * </p>
+	 * 
+	 * <h3>Type guessing</h3>
+	 * 
+	 * <p>
+	 * 	In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)}
+	 * 	which deals with all standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby.
+	 * </p>
+	 * 
+	 * <p><i><b>Important</b>:
+	 * 	The second parameter of this constructor is given as second parameter of {@link #convertType(String, String)}.
+	 * 	<b>This parameter is really used ONLY when the DBMS is SQLite ("sqlite").</b> Indeed, SQLite has so many datatype
+	 * 	restrictions that it is absolutely needed to know it is the DBMS from which the ResultSet is coming. Without this
+	 * 	information, type guessing will be unpredictable! 
+	 * </i></p>
+	 * 
+	 * @param dataSet		Dataset over which this iterator must iterate.
+	 * @param dbms			Lower-case string which indicates from which DBMS the given ResultSet is coming. <i>note: MAY be NULL.</i>
+	 * 
+	 * @throws NullPointerException	If NULL is given in parameter.
+	 * @throws DataReadException	If the given ResultSet is closed or if the metadata (columns count and types) can not be fetched.
+	 * 
+	 * @see #convertType(String, String)
+	 * @see ResultSetTableIterator#ResultSetTableIterator(ResultSet, String)
+	 */
+	public ResultSetTableIterator(final ResultSet dataSet, final String dbms) throws NullPointerException, DataReadException{
+		this(dataSet, dbms, null);
+	}
+
+	/**
+	 * <p>Build a TableIterator able to read rows and columns of the given ResultSet.</p>
+	 * 
+	 * <p>
+	 * 	In order to provide the metadata through {@link #getMetadata()}, this constructor is reading first the given metadata (if any),
+	 * 	and then, try to guess the datatype from the DBMS column datatype (using {@link #convertType(String, String)}).
+	 * </p>
+	 * 
+	 * <h3>Provided metadata</h3>
+	 * 
+	 * <p>The third parameter of this constructor aims to provide the metadata expected for each column of the ResultSet.</p>
+	 * 
+	 * <p>
+	 * 	For that, it is expected that all these metadata are {@link TAPColumn} objects. Indeed, simple {@link DBColumn}
+	 * 	instances do not have the type information. If just {@link DBColumn}s are provided, the ADQL name it provides will be kept
+	 * 	but the type will be guessed from the type provide by the ResultSetMetadata.
+	 * </p>
+	 * 
+	 * <p><i>Note:
+	 * 	If this parameter is incomplete (array length less than the column count returned by the ResultSet or some array items are NULL),
+	 * 	column metadata will be associated in the same order as the ResultSet columns. Missing metadata will be built from the
+	 * 	{@link ResultSetMetaData} and so the types will be guessed.
+	 * </i></p>
+	 * 
+	 * <h3>Type guessing</h3>
+	 * 
+	 * <p>
+	 * 	In order to guess a TAP type from a DBMS type, this constructor will call {@link #convertType(String, String)}
+	 * 	which deals with all standard datatypes known in Postgres, SQLite, MySQL, Oracle and JavaDB/Derby.
+	 * </p>
+	 * 
+	 * <p><i><b>Important</b>:
+	 * 	The second parameter of this constructor is given as second parameter of {@link #convertType(String, String)}.
+	 * 	<b>This parameter is really used ONLY when the DBMS is SQLite ("sqlite").</b> Indeed, SQLite has so many datatype
+	 * 	restrictions that it is absolutely needed to know it is the DBMS from which the ResultSet is coming. Without this
+	 * 	information, type guessing will be unpredictable! 
+	 * </i></p>
+	 * 
+	 * @param dataSet		Dataset over which this iterator must iterate.
+	 * @param dbms			Lower-case string which indicates from which DBMS the given ResultSet is coming. <i>note: MAY be NULL.</i>
+	 * @param resultMeta	List of expected columns. <i>note: these metadata are expected to be really {@link TAPColumn} objects ; MAY be NULL.</i>
+	 * 
+	 * @throws NullPointerException	If NULL is given in parameter.
+	 * @throws DataReadException	If the metadata (columns count and types) can not be fetched.
+	 * 
+	 * @see #convertType(String, String)
+	 */
+	public ResultSetTableIterator(final ResultSet dataSet, final String dbms, final DBColumn[] resultMeta) throws NullPointerException, DataReadException{
 		// A dataset MUST BE provided:
 		if (dataSet == null)
 			throw new NullPointerException("Missing ResultSet object over which to iterate!");
 
-		// It MUST also BE OPEN:
-		try{
-			if (dataSet.isClosed())
-				throw new DataReadException("Closed ResultSet: impossible to iterate over it!");
-		}catch(SQLException se){
-			throw new DataReadException("Can not know whether the ResultSet is open!", se);
-		}
-
 		// Keep a reference to the ResultSet:
 		data = dataSet;
 
@@ -88,14 +192,32 @@ public class ResultSetTableIterator implements TableIterator {
 			// determine their type:
 			colMeta = new TAPColumn[nbColumns];
 			for(int i = 1; i <= nbColumns; i++){
-				TAPType datatype = JDBCTAPFactory.toTAPType(metadata.getColumnTypeName(i));
-				colMeta[i - 1] = new TAPColumn(metadata.getColumnLabel(i), datatype);
+				if (resultMeta != null && (i - 1) < resultMeta.length && resultMeta[i - 1] != null){
+					try{
+						colMeta[i - 1] = (TAPColumn)resultMeta[i - 1];
+					}catch(ClassCastException cce){
+						TAPType datatype = convertType(metadata.getColumnTypeName(i), dbms);
+						colMeta[i - 1] = new TAPColumn(resultMeta[i - 1].getADQLName(), datatype);
+					}
+				}else{
+					TAPType datatype = convertType(metadata.getColumnTypeName(i), dbms);
+					colMeta[i - 1] = new TAPColumn(metadata.getColumnLabel(i), datatype);
+				}
 			}
 		}catch(SQLException se){
 			throw new DataReadException("Can not get the column types of the given ResultSet!", se);
 		}
 	}
 
+	@Override
+	public void close() throws DataReadException{
+		try{
+			data.close();
+		}catch(SQLException se){
+			throw new DataReadException("Can not close the iterated ResultSet!", se);
+		}
+	}
+
 	@Override
 	public TAPColumn[] getMetadata(){
 		return colMeta;
@@ -122,6 +244,7 @@ public class ResultSetTableIterator implements TableIterator {
 	 * 	<li>the row iteration has started = the first row has been read = a first call of {@link #nextRow()} has been done</li>
 	 * 	<li>AND the row iteration is not finished = the last row has been read.</li>
 	 * </ul>
+	 * 
 	 * @throws IllegalStateException
 	 */
 	private void checkReadState() throws IllegalStateException{
@@ -169,4 +292,151 @@ public class ResultSetTableIterator implements TableIterator {
 		return colMeta[colIndex - 1].getDatatype();
 	}
 
+	/**
+	 * <p>Convert the given DBMS type into the better matching {@link TAPType} instance.
+	 * This function is used to guess the TAP type of a column when it is not provided in the constructor.
+	 * It aims not to be exhaustive, but just to provide a type when the given TAP metadata are incomplete.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Any unknown DBMS datatype will be considered and translated as a VARCHAR.
+	 * 	The same type will be returned if the given parameter is an empty string or NULL.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	This type conversion function has been designed to work with all standard datatypes of the following DBMS:
+	 * 	PostgreSQL, SQLite, MySQL, Oracle and JavaDB/Derby.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important</b>:
+	 * 	<b>The second parameter is REALLY NEEDED when the DBMS is SQLite ("sqlite")!</b>
+	 * 	Indeed, SQLite has a so restrictive list of datatypes that this function can reliably convert its types
+	 * 	only if it knows the DBMS is SQLite. Otherwise, the conversion result would be unpredictable.
+	 * 	</i>In this default implementation of this function, all other DBMS values are ignored.<i>
+	 * </i></p>
+	 * 
+	 * <p><b>Warning</b>:
+	 * 	This function is not translating the geometrical datatypes. If a such datatype is encountered,
+	 * 	it will considered as unknown and so, a VARCHAR TAP type will be returned.
+	 * </p>
+	 * 
+	 * @param dbmsType	DBMS column datatype name.
+	 * @param dbms		Lower-case string which indicates which DBMS the ResultSet is coming from. <i>note: MAY be NULL.</i>
+	 * 
+	 * @return	The best suited {@link TAPType} object.
+	 */
+	protected TAPType convertType(String dbmsType, final String dbms){
+		// If no type is provided return VARCHAR:
+		if (dbmsType == null || dbmsType.trim().length() == 0)
+			return new TAPType(TAPDatatype.VARCHAR, TAPType.NO_LENGTH);
+
+		// Extract the type prefix and lower-case it:
+		dbmsType = dbmsType.toLowerCase();
+		int paramIndex = dbmsType.indexOf('(');
+		String dbmsTypePrefix = (paramIndex <= 0) ? dbmsType : dbmsType.substring(0, paramIndex);
+		int firstParam = getLengthParam(dbmsTypePrefix, paramIndex);
+
+		// CASE: SQLITE
+		if (dbms != null && dbms.equals("sqlite")){
+			// INTEGER -> SMALLINT, INTEGER, BIGINT
+			if (dbmsTypePrefix.equals("integer"))
+				return new TAPType(TAPDatatype.BIGINT);
+			// REAL -> REAL, DOUBLE
+			else if (dbmsTypePrefix.equals("real"))
+				return new TAPType(TAPDatatype.DOUBLE);
+			// TEXT -> CHAR, VARCHAR, CLOB, TIMESTAMP
+			else if (dbmsTypePrefix.equals("text"))
+				return new TAPType(TAPDatatype.VARCHAR);
+			// BLOB -> BINARY, VARBINARY, BLOB
+			else if (dbmsTypePrefix.equals("blob"))
+				return new TAPType(TAPDatatype.BLOB);
+			// Default:
+			else
+				return new TAPType(TAPDatatype.VARCHAR, TAPType.NO_LENGTH);
+		}
+		// CASE: OTHER DBMS
+		else{
+			// SMALLINT
+			if (dbmsTypePrefix.equals("smallint") || dbmsTypePrefix.equals("int2"))
+				return new TAPType(TAPDatatype.SMALLINT);
+			// INTEGER
+			else if (dbmsTypePrefix.equals("integer") || dbmsTypePrefix.equals("int") || dbmsTypePrefix.equals("int4"))
+				return new TAPType(TAPDatatype.INTEGER);
+			// BIGINT
+			else if (dbmsTypePrefix.equals("bigint") || dbmsTypePrefix.equals("int8") || dbmsTypePrefix.equals("int4") || dbmsTypePrefix.equals("number"))
+				return new TAPType(TAPDatatype.BIGINT);
+			// REAL
+			else if (dbmsTypePrefix.equals("float4") || (dbmsTypePrefix.equals("float") && firstParam <= 63))
+				return new TAPType(TAPDatatype.REAL);
+			// DOUBLE
+			else if (dbmsTypePrefix.equals("double") || dbmsTypePrefix.equals("double precision") || dbmsTypePrefix.equals("float8") || (dbmsTypePrefix.equals("float") && firstParam > 63))
+				return new TAPType(TAPDatatype.DOUBLE);
+			// BINARY
+			else if (dbmsTypePrefix.equals("binary") || dbmsTypePrefix.equals("raw") || ((dbmsTypePrefix.equals("char") || dbmsTypePrefix.equals("character")) && dbmsType.endsWith(" for bit data")))
+				return new TAPType(TAPDatatype.BINARY, firstParam);
+			// VARBINARY
+			else if (dbmsTypePrefix.equals("varbinary") || dbmsTypePrefix.equals("long raw") || ((dbmsTypePrefix.equals("varchar") || dbmsTypePrefix.equals("character varying")) && dbmsType.endsWith(" for bit data")))
+				return new TAPType(TAPDatatype.VARBINARY, firstParam);
+			// CHAR
+			else if (dbmsTypePrefix.equals("char") || dbmsTypePrefix.equals("character"))
+				return new TAPType(TAPDatatype.CHAR, firstParam);
+			// VARCHAR
+			else if (dbmsTypePrefix.equals("varchar") || dbmsTypePrefix.equals("varchar2") || dbmsTypePrefix.equals("character varying"))
+				return new TAPType(TAPDatatype.VARBINARY, firstParam);
+			// BLOB
+			else if (dbmsTypePrefix.equals("bytea") || dbmsTypePrefix.equals("blob") || dbmsTypePrefix.equals("binary large object"))
+				return new TAPType(TAPDatatype.BLOB);
+			// CLOB
+			else if (dbmsTypePrefix.equals("text") || dbmsTypePrefix.equals("clob") || dbmsTypePrefix.equals("character large object"))
+				return new TAPType(TAPDatatype.CLOB);
+			// TIMESTAMP
+			else if (dbmsTypePrefix.equals("timestamp"))
+				return new TAPType(TAPDatatype.TIMESTAMP);
+			// Default:
+			else
+				return new TAPType(TAPDatatype.VARCHAR, TAPType.NO_LENGTH);
+		}
+	}
+
+	/**
+	 * <p>Extract the 'length' parameter of a DBMS type string.</p>
+	 * 
+	 * <p>
+	 * 	If the given type string does not contain any parameter
+	 * 	OR if the first parameter can not be casted into an integer,
+	 * 	{@link TAPType#NO_LENGTH} will be returned.
+	 * </p>
+	 * 
+	 * @param dbmsType		DBMS type string (containing the datatype and the 'length' parameter).
+	 * @param paramIndex	Index of the open bracket.
+	 * 
+	 * @return	The 'length' parameter value if found, {@link TAPType#NO_LENGTH} otherwise.
+	 */
+	protected final int getLengthParam(final String dbmsType, final int paramIndex){
+		// If no parameter has been previously detected, no length parameter:
+		if (paramIndex <= 0)
+			return TAPType.NO_LENGTH;
+
+		// If there is one and that at least ONE parameter is provided....
+		else{
+			int lengthParam = TAPType.NO_LENGTH;
+			String paramsStr = dbmsType.substring(paramIndex + 1);
+
+			// ...extract the 'length' parameter:
+			/* note: we suppose here that no other parameter is possible ;
+			 *       but if there are, they are ignored and we try to consider the first parameter
+			 *       as the length */
+			int paramEndIndex = paramsStr.indexOf(',');
+			if (paramEndIndex <= 0)
+				paramEndIndex = paramsStr.indexOf(')');
+
+			// ...cast it into an integer:
+			try{
+				lengthParam = Integer.parseInt(paramsStr.substring(0, paramEndIndex));
+			}catch(Exception ex){}
+
+			// ...and finally return it:
+			return lengthParam;
+		}
+	}
+
 }
diff --git a/src/tap/data/TableIterator.java b/src/tap/data/TableIterator.java
index 3051b3a..0aed73c 100644
--- a/src/tap/data/TableIterator.java
+++ b/src/tap/data/TableIterator.java
@@ -43,11 +43,15 @@ import tap.metadata.TAPType;
  * 		}
  * 	}catch(DataReadException dre){
  * 		...
+ * 	}finally{
+ * 		try{
+ * 			it.close();
+ * 		}catch(DataReadException dre){ ... }
  * 	}
  * </pre>
  * 
  * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
- * @version 2.0 (06/2014)
+ * @version 2.0 (08/2014)
  * @since 2.0
  */
 public interface TableIterator {
@@ -123,4 +127,12 @@ public interface TableIterator {
 	 * @throws DataReadException		If an error occurs while reading the table dataset.
 	 */
 	public TAPType getColType() throws IllegalStateException, DataReadException;
+
+	/**
+	 * Close the stream or input over which this class iterates.
+	 * 
+	 * @throws DataReadException	If any error occurs while closing it.
+	 */
+	public void close() throws DataReadException;
+
 }
diff --git a/src/tap/data/VOTableIterator.java b/src/tap/data/VOTableIterator.java
index 13628d5..861ac52 100644
--- a/src/tap/data/VOTableIterator.java
+++ b/src/tap/data/VOTableIterator.java
@@ -202,6 +202,15 @@ public class VOTableIterator implements TableIterator {
 			throw new IllegalStateException("End of VOTable file already reached!");
 	}
 
+	@Override
+	public void close() throws DataReadException{
+		try{
+			rowSeq.close();
+		}catch(IOException ioe){
+			throw new DataReadException("Can not close the iterated VOTable!", ioe);
+		}
+	}
+
 	@Override
 	public TAPColumn[] getMetadata(){
 		return colMeta;
diff --git a/src/tap/db/DBConnection.java b/src/tap/db/DBConnection.java
index 00a5dba..c3e92e3 100644
--- a/src/tap/db/DBConnection.java
+++ b/src/tap/db/DBConnection.java
@@ -17,15 +17,17 @@ package tap.db;
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
  * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
- *                       Astronomisches Rechen Institute (ARI)
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
 import tap.data.DataReadException;
 import tap.data.TableIterator;
-import tap.metadata.TAPDM;
+import tap.metadata.TAPColumn;
+import tap.metadata.TAPMetadata;
 import tap.metadata.TAPTable;
 import uws.service.log.UWSLogType;
 import adql.query.ADQLQuery;
+import adql.translator.ADQLTranslator;
 
 /**
  * <p>Connection to the "database" (whatever is the type or whether it is linked to a true DBMS connection).</p>
@@ -52,37 +54,125 @@ public interface DBConnection {
 	public String getID();
 
 	/**
-	 * <p>Let executing the given ADQL query.</p>
+	 * <p>Fetch the whole content of TAP_SCHEMA.</p>
 	 * 
-	 * <p>The result of this query must be formatted as a table, and so must be iterable using a {@link TableIterator}.</p>
+	 * <p>
+	 * 	This function SHOULD be used only once: at the starting of the TAP service. It is an alternative way
+	 * 	to get the published schemas, tables and columns. The other way is to build a {@link TAPMetadata} object
+	 * 	yourself in function of the schemas/tables/columns you want to publish (i.e. which can be done by reading
+	 * 	metadata from a XML document - following the same schema - XSD- as for the TAP resource <i>tables</i>)
+	 * 	and then to load them in the DB (see {@link #setTAPSchema(TAPMetadata, boolean)} for more details).
+	 * </p>
 	 * 
-	 * <p><i>note: the interpretation of the ADQL query is up to the implementation. In most of the case, it is just needed
-	 * to translate this ADQL query into an SQL query (understandable by the chosen DBMS).</i></p>
+	 * <p><b>CAUTION:
+	 * 	This function MUST NOT be used if the tables to publish or the standard TAP_SCHEMA tables have names in DB different from the
+	 * 	ones defined by the TAP standard. So, if DB names are different from the ADQL names, you have to write yourself a way to get
+	 * 	the metadata from the DB.
+	 * </b></p>
 	 * 
-	 * @param adqlQuery	ADQL query to execute.
+	 * <p><i><b>Important note:</b>
+	 * 	If the schema or some standard tables or columns are missing, TAP_SCHEMA will be considered as incomplete
+	 * 	and an exception will be thrown.
+	 * </i></p>
 	 * 
-	 * @return	The table result.
+	 * <p><i>Note:
+	 * 	This function MUST be able to read the standard tables and columns described by the IVOA. All other tables/columns
+	 * 	will be merely ignored.
+	 * </i></p>
 	 * 
-	 * @throws DBException	If any errors occurs while executing the query.
+	 * @return	Content of TAP_SCHEMA inside the DB.
+	 * 
+	 * @throws DBException	If TAP_SCHEMA can not be found, is incomplete or if some important metadata can not be retrieved.
+	 * 
+	 * @since 2.0
 	 */
-	public TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException;
+	public TAPMetadata getTAPSchema() throws DBException;
 
 	/**
-	 * <p>Add or update the specified TAP_SCHEMA table with the given data.</p>
-	 * 
-	 * <p><i><b>Warning:</b> It is expected that the given data SHOULD be the only ones inside the specified table.
-	 * So, the table SHOULD probably be cleared before the insertion of the given data. However, this behavior MAY depend of the
-	 * implementation and more particularly of the way the TAP_SCHEMA is updated.</i></p>
-	 * 
-	 * @param tapTableName	Name of the TAP_SCHEMA table to add/update.
-	 * @param data			Data to use in order to fill the specified table.
-	 * 
-	 * @return				<i>true</i> if the specified table has been successfully added/updated, <i>false</i> otherwise.
+	 * <p>Empty and then fill all the TAP_SCHEMA tables with the given list of metadata.</p>
+	 * 
+	 * <p>
+	 * 	This function SHOULD be used only once: at the starting of the TAP service,
+	 * 	when metadata are loaded from a XML document (following the same schema - XSD-
+	 * 	as for the TAP resource <i>tables</i>).
+	 * </p>
+	 * 
+	 * <p>
+	 * 	<i>THIS FUNCTION IS MANIPULATING THE SCHEMAS AND TABLES OF YOUR DATABASE.
+	 * 	SO IT SHOULD HAVE A SPECIFIC BEHAVIOR DESCRIBED BELOW.
+	 * 	<b>SO PLEASE READ THE FOLLOWINGS AND TRY TO RESPECT IT AS MUCH AS POSSIBLE IN THE IMPLEMENTATIONS</b>
+	 * </i></p>
+	 * 
+	 * <h3>TAP_SCHEMA CREATION</h3>
+	 * <p>
+	 * 	This function is MAY drop and then re-create the schema TAP_SCHEMA and all
+	 * 	its tables listed in the TAP standard (TAP_SCHEMA.schemas, .tables, .columns, .keys and .key_columns).
+	 * 	<i>All other tables inside TAP_SCHEMA SHOULD NOT be modified!</i>
+	 * </p>
+	 * 
+	 * <p>
+	 * 	The schema and the tables MUST be created using either the <b>standard definition</b> or the
+	 * 	<b>definition provided in the {@link TAPMetadata} object</b> (if provided). Indeed, if your definition of these TAP tables
+	 * 	is different from the standard (the standard + new elements), you MUST provide your modifications in parameter
+	 *	through the {@link TAPMetadata} object so that they can be applied and taken into account in TAP_SCHEMA.
+	 * </p>
+	 * 
+	 * <p><i>Note:
+	 * 	DB names provided in the given TAPMetadata (see {@link TAPTable#getDBSchemaName()}, {@link TAPTable#getDBName()} and {@link TAPColumn#getDBName()})
+	 * 	are used for the creation and filling of the tables.
+	 * 
+	 * 	Whether these requests must be case sensitive or not SHOULD be managed by ADQLTranslator (see {@link ADQLTranslator#getQualifiedSchemaName(adql.db.DBTable)},
+	 * {@link ADQLTranslator#getQualifiedTableName(adql.db.DBTable)} and {@link ADQLTranslator#getColumnName(adql.db.DBColumn)}).
+	 * </i></p>
+	 * 
+	 * <h3>TAPMetadata PARAMETER</h3>
+	 * <p>
+	 * 	This object MUST contain all schemas, tables and columns that MUST be published. All its content will be
+	 * 	used in order to fill the TAP_SCHEMA tables.
+	 * </p>
+	 * <p>
+	 * 	Of course, TAP_SCHEMA and its tables MAY be provided in this object. But:
+	 * </p>
+	 * <ul>
+	 * 		<li><b>(a) if TAP_SCHEMA tables are NOT provided</b>:
+	 * 			this function SHOULD consider their definition as exactly the one provided by
+	 * 			the TAP standard/protocol. If so, the standard definition MUST be automatically added
+	 * 			into the {@link TAPMetadata} object AND into TAP_SCHEMA. 
+	 * 		</li>
+	 * 		<li><b>(b) if TAP_SCHEMA tables ARE provided</b>:
+	 * 			the definition of all given elements will be taken into account while updating the TAP_SCHEMA.
+	 * 			Each element definition not provided MUST be considered as exactly the same as the standard one
+	 * 			and MUST be added into the {@link TAPMetadata} object AND into TAP_SCHEMA.
+	 *		</li>
+	 * </ul>
+	 * 
+	 * <p><i>Note: By default, all implementations of this interface in the TAP library will fill only standard columns and tables of TAP_SCHEMA.
+	 * To fill your own, you MUST implement yourself this interface or to extend an existing implementation.</i></p>
+	 * 
+	 * <p><i><b>WARNING</b>:
+	 * 	(b) lets consider a TAP_SCHEMA different from the standard one. BUT, these differences MUST be only additions,
+	 * 	NOT modifications or deletion of the standard definition! This function MUST be able to work AT LEAST on a
+	 * 	standard definition of TAP_SCHEMA.
+	 * </p>
+	 * 
+	 * <h3>FILLING BEHAVIOUR</h3>
+	 * <p>
+	 * 	The TAP_SCHEMA tables SHOULD be completely emptied (in SQL: "DELETE FROM &lt;table_name&gt;;" or merely "DROP TABLE &lt;table_name&gt;") before insertions can be processed.
+	 * </p>
+	 * 
+	 * <h3>ERRORS MANAGEMENT</h3>
+	 * <p>
+	 * 	If any error occurs while executing any "DB" queries (in SQL: DROP, DELETE, INSERT, CREATE, ...), all queries executed
+	 * 	before in this function MUST be canceled (in SQL: ROLLBACK).
+	 * </p>
+	 * 
+	 * @param metadata			List of all schemas, tables, columns and foreign keys to insert in the TAP_SCHEMA.
 	 * 
 	 * @throws DBException			If any error occurs while updating the database.
-	 * @throws DataReadException	If any error occurs while reading the given data.
+	 * 
+	 * @since 2.0
 	 */
-	public boolean updateTAPTable(final TAPDM tapTableName, final TableIterator data) throws DBException, DataReadException;
+	public void setTAPSchema(final TAPMetadata metadata) throws DBException;
 
 	/**
 	 * Add the defined and given table inside the TAP_UPLOAD schema.
@@ -93,27 +183,52 @@ public interface DBConnection {
 	 * 
 	 * @param tableDef	Definition of the table to upload (list of all columns and of their type).
 	 * @param data		Rows and columns of the table to upload.
-	 * @param maxNbRows	Maximum number of rows allowed to be inserted. Beyond this limit, a
-	 *                  {@link DataReadException} MUST be sent. <i>A negative or a NULL value means "no limit".</i>
 	 * 
 	 * @return			<i>true</i> if the given table has been successfully added, <i>false</i> otherwise.
 	 * 
 	 * @throws DBException			If any error occurs while adding the table.
-	 * @throws DataReadException	If any error occurs while reading the given data.
+	 * @throws DataReadException	If any error occurs while reading the given data (particularly if any limit - in byte or row - set in the TableIterator is reached).
+	 * 
+	 * @since 2.0
 	 */
-	public boolean addUploadedTable(final TAPTable tableDef, final TableIterator data, final int maxNbRows) throws DBException, DataReadException;
+	public boolean addUploadedTable(final TAPTable tableDef, final TableIterator data) throws DBException, DataReadException;
 
 	/**
 	 * <p>Drop the specified uploaded table from the database.
 	 * More precisely, it means dropping a table from the TAP_UPLOAD schema.</p>
 	 * 
-	 * @param tableName	Name (in the database) of the uploaded table to drop.
+	 * <p><i>Note:
+	 * 	This function SHOULD drop only one table. So, if more than one table match in the "database" to the given one, an exception MAY be thrown.
+	 * 	This behavior is implementation-dependent.
+	 * </i></p>
+	 * 
+	 * @param tableDef	Definition of the uploaded table to drop (the whole object is needed in order to get the DB schema and tables names).
 	 * 
 	 * @return	<i>true</i> if the specified table has been successfully dropped, <i>false</i> otherwise.
 	 * 
 	 * @throws DBException	If any error occurs while dropping the specified uploaded table.
+	 * 
+	 * @since 2.0
 	 */
-	public boolean dropUploadedTable(final String tableName) throws DBException;
+	public boolean dropUploadedTable(final TAPTable tableDef) throws DBException;
+
+	/**
+	 * <p>Let executing the given ADQL query.</p>
+	 * 
+	 * <p>The result of this query must be formatted as a table, and so must be iterable using a {@link TableIterator}.</p>
+	 * 
+	 * <p><i>note: the interpretation of the ADQL query is up to the implementation. In most of the case, it is just needed
+	 * to translate this ADQL query into an SQL query (understandable by the chosen DBMS).</i></p>
+	 * 
+	 * @param adqlQuery	ADQL query to execute.
+	 * 
+	 * @return	The table result.
+	 * 
+	 * @throws DBException	If any errors occurs while executing the query.
+	 * 
+	 * @since 2.0
+	 */
+	public TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException;
 
 	/**
 	 * <p>Close the connection (if needed).</p>
diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java
index 94be3f6..d68a948 100644
--- a/src/tap/db/JDBCConnection.java
+++ b/src/tap/db/JDBCConnection.java
@@ -17,79 +17,292 @@ package tap.db;
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
  * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
- *                       Astronomishes Rechen Institute (ARI)
+ *                       Astronomishes Rechen Institut (ARI)
  */
 
+import java.io.PrintStream;
 import java.sql.Connection;
+import java.sql.DatabaseMetaData;
 import java.sql.DriverManager;
+import java.sql.PreparedStatement;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
 
+import tap.data.DataReadException;
+import tap.data.ResultSetTableIterator;
+import tap.data.TableIterator;
 import tap.log.TAPLog;
 import tap.metadata.TAPColumn;
+import tap.metadata.TAPForeignKey;
+import tap.metadata.TAPMetadata;
+import tap.metadata.TAPMetadata.STDSchema;
+import tap.metadata.TAPMetadata.STDTable;
+import tap.metadata.TAPSchema;
 import tap.metadata.TAPTable;
+import tap.metadata.TAPTable.TableType;
+import tap.metadata.TAPType;
+import tap.metadata.TAPType.TAPDatatype;
 import adql.query.ADQLQuery;
-import cds.savot.model.SavotTR;
-import cds.savot.model.TDSet;
+import adql.query.IdentifierField;
+import adql.translator.ADQLTranslator;
+import adql.translator.JDBCTranslator;
+import adql.translator.TranslationException;
 
 /**
- * Simple implementation of the {@link DBConnection} interface.
- * It creates and manages a JDBC connection to a specified database.
- * Thus results of any executed SQL query will be a {@link ResultSet}.
+ * <p>This {@link DBConnection} implementation is theoretically able to deal with any DBMS connection.</p>
+ * 
+ * <p><i>Note:
+ * 	"Theoretically", because its design has been done using information about Postgres, SQLite, Oracle, MySQL and Java DB (Derby).
+ * 	Then it has been really tested successfully with Postgres and SQLite.
+ * </i></p>
+ * 
+ * <h3>Deal with different DBMS features</h3>
+ * 
+ * <p>Update queries are taking into account whether the following features are supported by the DBMS:</p>
+ * <ul>
+ * 	<li><b>data definition</b>: when not supported, no update operation will be possible.
+ * 	                            All corresponding functions will then throw a {@link DBException} ;
+ * 	                            only {@link #executeQuery(ADQLQuery)} will be possibly called.</li>
+ * 
+ * 	<li><b>transactions</b>: when not supported, no transaction is started or merely used.
+ * 	                         It means that in case of update failure, no rollback will be possible
+ * 	                         and that already done modification will remain in the database.</li>
+ * 
+ * 	<li><b>schemas</b>: when the DBMS does not have the notion of schema (like SQLite), no schema creation or dropping will be obviously processed.
+ * 	                    Besides, if not already done, database name of all tables will be prefixed by the schema name.
+ * 	                    The prefix to apply is returned by {@link #getTablePrefix(String)}.</li>
+ * 
+ * 	<li><b>batch updates</b>: when not supported, updates will just be done, "normally, one by one.
+ * 	                          In one word, there will be merely no optimization.
+ * 	                          Anyway, this feature concerns only the insertions into tables.</li>
+ * 
+ * 	<li><b>case sensitivity of identifiers</b>: the case sensitivity of quoted identifier varies from the used DBMS. This {@link DBConnection}
+ * 	                                            implementation is able to adapt itself in function of the way identifiers are stored and
+ * 	                                            researched in the database. How the case sensitivity is managed by the DBMS is the problem
+ * 	                                            of only one function (which can be overwritten if needed): {@link #equals(String, String, boolean)}.</li>
+ * </ul>
+ * 
+ * <p><i><b>Warning</b>:
+ * 	All these features have no impact at all on ADQL query executions ({@link #executeQuery(ADQLQuery)}).
+ * </i></p>
+ * 
+ * <h3>Datatypes</h3>
+ * 
+ * <p>Column types are converted from DBMS to TAP types with {@link #getTAPType(String)} and from TAP to DBMS types with {@link #getDBMSDatatype(TAPType)}.</p>
+ * 
+ * <p>
+ * 	All typical DBMS datatypes are taken into account, <b>EXCEPT the geometrical types</b> (POINT and REGION). For these types, the only object having this
+ * 	information is the translator thanks to {@link JDBCTranslator#isPointType(String)}, {@link JDBCTranslator#isRegionType(String)},
+ * 	{@link JDBCTranslator#getPointType()} and {@link JDBCTranslator#getRegionType()}. The two first functions are used to identify a DBMS type as a point or
+ * 	a region (note: several DBMS datatypes may be identified as a geometry type). The two others provide the DBMS type corresponding the best to the TAP types
+ * 	POINT and REGION.
+ * </p>
+ * 
+ * <p><i><b>Warning:</b>
+ * 	The TAP type REGION can be either a circle, a box or a polygon. Since several DBMS types correspond to one TAP type, {@link JDBCTranslator#getRegionType()}
+ * 	MUST return a type covering all these region datatypes. Generally, it will be a VARCHAR whose the values would be STC-S expressions.
+ * 	Note that this function is used ONLY WHEN tables with a geometrical value is uploaded. On the contrary, {@link JDBCTranslator#isRegionType(String)}
+ * 	is used much more often: in order to write the metadata part of a query result.
+ * </i></p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 1.1 (04/2014)
+ * @version 2.0 (07/2014)
+ * @since 2.0
  */
 public class JDBCConnection implements DBConnection {
 
-	/** JDBC prefix of any database URL (for instance: jdbc:postgresql://127.0.0.1/myDB or jdbc:postgresql:myDB). */
-	public final static String JDBC_PREFIX = "jdbc";
+	/** DBMS name of PostgreSQL used in the database URL. */
+	protected final static String DBMS_POSTGRES = "postgresql";
+
+	/** DBMS name of SQLite used in the database URL. */
+	protected final static String DBMS_SQLITE = "sqlite";
+
+	/** DBMS name of MySQL used in the database URL. */
+	protected final static String DBMS_MYSQL = "mysql";
 
-	/** Connection ID (typically, the job ID). */
+	/** DBMS name of Oracle used in the database URL. */
+	protected final static String DBMS_ORACLE = "oracle";
+
+	/** Connection ID (typically, the job ID). It lets identify the DB errors linked to the Job execution in the logs. */
 	protected final String ID;
 
 	/** JDBC connection (created and initialized at the creation of this {@link JDBCConnection} instance). */
 	protected final Connection connection;
 
-	/** Logger to use if any message needs to be printed to the server manager. */
+	/** The translator this connection must use to translate ADQL into SQL. It is also used to get information about the case sensitivity of all types of identifier (schema, table, column). */
+	protected final JDBCTranslator translator;
+
+	/** Object to use if any message needs to be logged. <i>note: this logger may be NULL. If NULL, messages will never be printed.</i> */
 	protected final TAPLog logger;
 
+	/* JDBC URL MANAGEMENT */
+
+	/** JDBC prefix of any database URL (for instance: jdbc:postgresql://127.0.0.1/myDB or jdbc:postgresql:myDB). */
+	public final static String JDBC_PREFIX = "jdbc";
+
+	/** Name (in lower-case) of the DBMS with which the connection is linked. */
+	protected final String dbms;
+
+	/* DBMS SUPPORTED FEATURES */
+
+	/** Indicate whether the DBMS supports transactions (start, commit, rollback and end). <i>note: If no transaction is possible, no transaction will be used, but then, it will never possible to cancel modifications in case of error.</i> */
+	protected boolean supportsTransaction;
+
+	/** Indicate whether the DBMS supports the definition of data (create, update, drop, insert into schemas and tables). <i>note: If not supported, it will never possible to create TAP_SCHEMA from given metadata (see {@link #setTAPSchema(TAPMetadata)}) and to upload/drop tables (see {@link #addUploadedTable(TAPTable, TableIterator)} and {@link #dropUploadedTable(TAPTable)}).</i> */
+	protected boolean supportsDataDefinition;
+
+	/** Indicate whether the DBMS supports several updates in once (using {@link Statement#addBatch(String)} and {@link Statement#executeBatch()}). <i>note: If not supported, every updates will be done one by one. So it is not really a problem, but just a loss of optimization.</i> */
+	protected boolean supportsBatchUpdates;
+
+	/** Indicate whether the DBMS has the notion of SCHEMA. Most of the DBMS has it, but not SQLite for instance. <i>note: If not supported, the DB table name will be prefixed by the DB schema name followed by the character "_". Nevertheless, if the DB schema name is NULL, the DB table name will never be prefixed.</i> */
+	protected boolean supportsSchema;
+
+	/* CASE SENSITIVITY SUPPORT */
+
+	/** Indicate whether UNquoted identifiers will be considered as case INsensitive and stored in mixed case by the DBMS. <i>note: If FALSE, unquoted identifiers will still be considered as case insensitive for the researches, but will be stored in lower or upper case (in function of {@link #lowerCaseUnquoted} and {@link #upperCaseUnquoted}). If none of these two flags is TRUE, the storage case will be though considered as mixed.</i> */
+	protected boolean supportsMixedCaseUnquotedIdentifier;
+	/** Indicate whether the unquoted identifiers are stored in lower case in the DBMS. */
+	protected boolean lowerCaseUnquoted;
+	/** Indicate whether the unquoted identifiers are stored in upper case in the DBMS. */
+	protected boolean upperCaseUnquoted;
+
+	/** Indicate whether quoted identifiers will be considered as case INsensitive and stored in mixed case by the DBMS. <i>note: If FALSE, quoted identifiers will be considered as case sensitive and will be stored either in lower, upper or in mixed case (in function of {@link #lowerCaseQuoted}, {@link #upperCaseQuoted} and {@link #mixedCaseQuoted}). If none of these three flags is TRUE, the storage case will be mixed case.</i> */
+	protected boolean supportsMixedCaseQuotedIdentifier;
+	/** Indicate whether the quoted identifiers are stored in lower case in the DBMS. */
+	protected boolean lowerCaseQuoted;
+	/** Indicate whether the quoted identifiers are stored in mixed case in the DBMS. */
+	protected boolean mixedCaseQuoted;
+	/** Indicate whether the quoted identifiers are stored in upper case in the DBMS. */
+	protected boolean upperCaseQuoted;
+
 	/**
-	 * <p>
-	 * 	Creates a JDBC connection to the specified database and with the specified JDBC driver.
-	 * 	This connection is established using the given user name and password.
-	 * <p>
-	 * <p><i><u>note:</u> the JDBC driver is loaded using <pre>Class.forName(driverPath)</pre>.</i></p>
+	 * <p>Creates a JDBC connection to the specified database and with the specified JDBC driver.
+	 * This connection is established using the given user name and password.<p>
+	 * 
+	 * <p><i><u>note:</u> the JDBC driver is loaded using <pre>Class.forName(driverPath)</pre> and the connection is created with <pre>DriverManager.getConnection(dbUrl, dbUser, dbPassword)</pre>.</i></p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	This constructor really creates a new SQL connection. Creating a SQL connection is time consuming!
+	 * 	That's why it is recommended to use a pool of connections. When doing so, you should use the other constructor of this class
+	 * 	({@link #JDBCConnection(Connection, String, TAPLog)}).
+	 * </i></p>
 	 * 
 	 * @param driverPath	Full class name of the JDBC driver.
 	 * @param dbUrl			URL to the database. <i><u>note</u> This URL may not be prefixed by "jdbc:". If not, the prefix will be automatically added.</i>
-	 * @param dbUser		Name of the database user (supposed to be the database owner).
+	 * @param dbUser		Name of the database user.
 	 * @param dbPassword	Password of the given database user.
-	 * @param logger		Logger to use if any message needs to be printed to the server admin.
+	 * @param translator	{@link ADQLTranslator} to use in order to get SQL from an ADQL query and to get qualified DB table names.
+	 * @param connID		ID of this connection. <i>note: may be NULL ; but in this case, logs concerning this connection will be more difficult to localize.</i>
+	 * @param logger		Logger to use in case of need. <i>note: may be NULL ; in this case, error will never be logged, but sometimes DBException may be raised.</i>
+	 * 
+	 * @throws DBException	If the driver can not be found or if the connection can not merely be created (usually because DB parameters are wrong).
+	 */
+	public JDBCConnection(final String driverPath, final String dbUrl, final String dbUser, final String dbPassword, final JDBCTranslator translator, final String connID, final TAPLog logger) throws DBException{
+		this(createConnection(driverPath, dbUrl, dbUser, dbPassword), translator, connID, logger);
+	}
+
+	/**
+	 * <p>Create a JDBC connection by wrapping the given connection.</p>
 	 * 
-	 * @throws DBException	If the specified driver can not be found, or if the database URL or user is incorrect.
+	 * <p><i><b>Warning:</b>
+	 * 	Calling {@link #close()} will call the function close() of the given connection.
+	 * 	So, if this connection is coming from a pool, it is here supposed that a call to this function will not close the connection but gives it back to the pool.
+	 * 	If it's not the case, {@link #close()} must be overwritten in order to apply the good "close" behavior.
+	 * </i></p>
+	 * 
+	 * @param conn			Connection to wrap.
+	 * @param translator	{@link ADQLTranslator} to use in order to get SQL from an ADQL query and to get qualified DB table names.
+	 * @param connID		ID of this connection. <i>note: may be NULL ; but in this case, logs concerning this connection will be more difficult to localize.</i>
+	 * @param logger		Logger to use in case of need. <i>note: may be NULL ; in this case, error will never be logged, but sometimes DBException may be raised.</i>
 	 */
-	public JDBCConnection(final String ID, final String driverPath, final String dbUrl, final String dbUser, final String dbPassword, final TAPLog logger) throws DBException{
+	public JDBCConnection(final Connection conn, final JDBCTranslator translator, final String connID, final TAPLog logger) throws DBException{
+		if (conn == null)
+			throw new NullPointerException("Missing SQL connection! => can not create a JDBCConnection object.");
+		if (translator == null)
+			throw new NullPointerException("Missing ADQL translator! => can not create a JDBCConnection object.");
+
+		this.connection = conn;
+		this.translator = translator;
+		this.ID = connID;
 		this.logger = logger;
-		this.ID = ID;
 
+		// Set the supporting features' flags + DBMS type:
+		try{
+			DatabaseMetaData dbMeta = connection.getMetaData();
+			dbms = getDBMSName(dbMeta.getURL());
+			supportsTransaction = dbMeta.supportsTransactions();
+			supportsBatchUpdates = dbMeta.supportsBatchUpdates();
+			supportsDataDefinition = dbMeta.supportsDataDefinitionAndDataManipulationTransactions();
+			supportsSchema = dbMeta.supportsSchemasInTableDefinitions();
+			lowerCaseUnquoted = dbMeta.storesLowerCaseIdentifiers();
+			upperCaseUnquoted = dbMeta.storesUpperCaseIdentifiers();
+			supportsMixedCaseUnquotedIdentifier = dbMeta.supportsMixedCaseIdentifiers();
+			lowerCaseQuoted = dbMeta.storesLowerCaseQuotedIdentifiers();
+			mixedCaseQuoted = dbMeta.storesMixedCaseQuotedIdentifiers();
+			upperCaseQuoted = dbMeta.storesUpperCaseQuotedIdentifiers();
+			supportsMixedCaseQuotedIdentifier = dbMeta.supportsMixedCaseQuotedIdentifiers();
+		}catch(SQLException se){
+			throw new DBException("Unable to access to one or several DB metadata (url, supportsTransaction, supportsBatchUpdates, supportsDataDefinitionAndDataManipulationTransactions, supportsSchemasInTableDefinitions, storesLowerCaseIdentifiers, storesUpperCaseIdentifiers, supportsMixedCaseIdentifiers, storesLowerCaseQuotedIdentifiers, storesMixedCaseQuotedIdentifiers, storesUpperCaseQuotedIdentifiers and supportsMixedCaseQuotedIdentifiers) from the given Connection!");
+		}
+	}
+
+	/**
+	 * Extract the DBMS name from the given database URL.
+	 * 
+	 * @param dbUrl	JDBC URL to access the database. <b>This URL must start with "jdbc:" ; otherwise an exception will be thrown.</b>
+	 * 
+	 * @return	The DBMS name as found in the given URL.
+	 * 
+	 * @throws DBException	If NULL has been given, if the URL is not a JDBC one (starting with "jdbc:") or if the DBMS name is missing.
+	 */
+	protected static final String getDBMSName(String dbUrl) throws DBException{
+		if (dbUrl == null)
+			throw new DBException("Missing database URL!");
+
+		if (!dbUrl.startsWith(JDBC_PREFIX + ":"))
+			throw new DBException("This DBConnection implementation is only able to deal with JDBC connection! (the DB URL must start with \"" + JDBC_PREFIX + ":\" ; given url: " + dbUrl + ")");
+
+		dbUrl = dbUrl.substring(5);
+		int indSep = dbUrl.indexOf(':');
+		if (indSep <= 0)
+			throw new DBException("Incorrect database URL: " + dbUrl);
+
+		return dbUrl.substring(0, indSep).toLowerCase();
+	}
+
+	/**
+	 * Create a {@link Connection} instance using the specified JDBC Driver and the given database parameters.
+	 * 
+	 * @param driverPath	Path to the JDBC driver.
+	 * @param dbUrl			JDBC URL to connect to the database. <i><u>note</u> This URL may not be prefixed by "jdbc:". If not, the prefix will be automatically added.</i>
+	 * @param dbUser		Name of the user to use to connect to the database.
+	 * @param dbPassword	Password of the user to use to connect to the database.
+	 * 
+	 * @return	A new DB connection.
+	 * 
+	 * @throws DBException	If the driver can not be found or if the connection can not merely be created (usually because DB parameters are wrong).
+	 * 
+	 * @see DriverManager#getConnection(String, String, String)
+	 */
+	private final static Connection createConnection(final String driverPath, final String dbUrl, final String dbUser, final String dbPassword) throws DBException{
 		// Load the specified JDBC driver:
 		try{
 			Class.forName(driverPath);
 		}catch(ClassNotFoundException cnfe){
-			logger.dbError("Impossible to find the JDBC driver \"" + driverPath + "\" !", cnfe);
 			throw new DBException("Impossible to find the JDBC driver \"" + driverPath + "\" !", cnfe);
 		}
 
 		// Build a connection to the specified database:
 		String url = dbUrl.startsWith(JDBC_PREFIX) ? dbUrl : (JDBC_PREFIX + dbUrl);
 		try{
-			connection = DriverManager.getConnection(url, dbUser, dbPassword);
-			logger.connectionOpened(this, (dbUrl.lastIndexOf('/') > 0 ? dbUrl.substring(dbUrl.lastIndexOf('/')) : dbUrl.substring(dbUrl.lastIndexOf(':'))));
+			return DriverManager.getConnection(url, dbUser, dbPassword);
 		}catch(SQLException se){
-			logger.dbError("Impossible to establish a connection to the database \"" + url + "\" !", se);
 			throw new DBException("Impossible to establish a connection to the database \"" + url + "\" !", se);
 		}
 	}
@@ -100,213 +313,2141 @@ public class JDBCConnection implements DBConnection {
 	}
 
 	@Override
-	public void startTransaction() throws DBException{
+	public void close() throws DBException{
 		try{
-			Statement st = connection.createStatement();
-			st.execute("begin");
-			logger.transactionStarted(this);
+			connection.close();
+			log(0, "Connection CLOSED.", null);
 		}catch(SQLException se){
-			logger.dbError("Impossible to begin a transaction !", se);
-			throw new DBException("Impossible to begin a transaction !", se);
+			log(1, "CLOSING connection impossible!", se);
+			throw new DBException("Impossible to close the database connection !", se);
 		}
 	}
 
+	/* ********************* */
+	/* INTERROGATION METHODS */
+	/* ********************* */
 	@Override
-	public void cancelTransaction() throws DBException{
+	public TableIterator executeQuery(final ADQLQuery adqlQuery) throws DBException{
+		String sql = null;
+		ResultSet result = null;
 		try{
-			connection.rollback();
-			logger.transactionCancelled(this);
+			// 1. Translate the ADQL query into SQL:
+			log(0, "Translating ADQL : " + adqlQuery.toADQL(), null);
+			sql = translator.translate(adqlQuery);
+
+			// 2. Execute the SQL query:
+			Statement stmt = connection.createStatement();
+			log(0, "Executing SQL : " + sql, null);
+			result = stmt.executeQuery(sql);
+
+			// 3. Return the result through a TableIterator object:
+			log(0, "Returning result...", null);
+			return new ResultSetTableIterator(result, dbms, adqlQuery.getResultingColumns());
+
 		}catch(SQLException se){
-			logger.dbError("Impossible to cancel/rollback a transaction !", se);
-			throw new DBException("Impossible to cancel (rollback) the transaction !", se);
+			close(result);
+			log(2, "Unexpected error while EXECUTING SQL query!", se);
+			throw new DBException("Unexpected error while executing a SQL query: " + se.getMessage(), se);
+		}catch(TranslationException te){
+			close(result);
+			log(2, "Unexpected error while TRANSLATING ADQL into SQL!", te);
+			throw new DBException("Unexpected error while translating ADQL into SQL: " + te.getMessage(), te);
+		}catch(DataReadException dre){
+			close(result);
+			throw new DBException("Impossible to read the query result, because: " + dre.getMessage(), dre);
+		}
+	}
+
+	/* *********************** */
+	/* TAP_SCHEMA MANIPULATION */
+	/* *********************** */
+
+	/**
+	 * Tell when, compared to the other TAP standard tables, a given standard TAP table should be created.
+	 * 
+	 * @param table	Standard TAP table.
+	 * 
+	 * @return	An index between 0 and 4 (included) - 0 meaning the first table to create whereas 4 is the last one.
+	 *          -1 is returned if NULL is given in parameter of if the standard table is not taken into account here.
+	 */
+	protected int getCreationOrder(final STDTable table){
+		if (table == null)
+			return -1;
+
+		switch(table){
+			case SCHEMAS:
+				return 0;
+			case TABLES:
+				return 1;
+			case COLUMNS:
+				return 2;
+			case KEYS:
+				return 3;
+			case KEY_COLUMNS:
+				return 4;
+			default:
+				return -1;
 		}
 	}
 
+	/* ************************************ */
+	/* GETTING TAP_SCHEMA FROM THE DATABASE */
+	/* ************************************ */
+
+	/**
+	 * <p>In this implementation, this function is first creating a virgin {@link TAPMetadata} object
+	 * that will be filled progressively by calling the following functions:</p>
+	 * <ol>
+	 * 	<li>{@link #loadSchemas(TAPTable, TAPMetadata, Statement)}</li>
+	 * 	<li>{@link #loadTables(TAPTable, TAPMetadata, Statement)}</li>
+	 * 	<li>{@link #loadColumns(TAPTable, List, Statement)}</li>
+	 * 	<li>{@link #loadKeys(TAPTable, TAPTable, List, Statement)}</li>
+	 * </ol>
+	 * 
+	 * <p><i>Note:
+	 * 	If schemas are not supported by this DBMS connection, the DB name of all tables will be set to NULL
+	 * 	and the DB name of all tables will be prefixed by the ADQL name of their respective schema (using {@link #getTablePrefix(String)}).
+	 * </i></p>
+	 * 
+	 * @see tap.db.DBConnection#getTAPSchema()
+	 */
 	@Override
-	public void endTransaction() throws DBException{
+	public TAPMetadata getTAPSchema() throws DBException{
+		// Build a virgin TAP metadata:
+		TAPMetadata metadata = new TAPMetadata();
+
+		// Get the definition of the standard TAP_SCHEMA tables:
+		TAPSchema tap_schema = TAPMetadata.getStdSchema();
+
+		// If schemas are not supported by the DBMS connection, the schema must not be translated in the DB:
+		if (!supportsSchema){
+			String namePrefix = getTablePrefix(tap_schema.getADQLName());
+			tap_schema.setDBName(null);
+			for(TAPTable t : tap_schema)
+				t.setDBName(namePrefix + t.getDBName());
+		}
+
+		// LOAD ALL METADATA FROM THE STANDARD TAP TABLES:
+		Statement stmt = null;
 		try{
-			connection.commit();
-			logger.transactionEnded(this);
+			// create a common statement for all loading functions:
+			stmt = connection.createStatement();
+
+			// load all schemas from TAP_SCHEMA.schemas:
+			log(0, "Loading TAP_SCHEMA.schemas.", null);
+			loadSchemas(tap_schema.getTable(STDTable.SCHEMAS.label), metadata, stmt);
+
+			// load all tables from TAP_SCHEMA.tables:
+			log(0, "Loading TAP_SCHEMA.tables.", null);
+			List<TAPTable> lstTables = loadTables(tap_schema.getTable(STDTable.TABLES.label), metadata, stmt);
+
+			// load all columns from TAP_SCHEMA.columns:
+			log(0, "Loading TAP_SCHEMA.columns.", null);
+			loadColumns(tap_schema.getTable(STDTable.COLUMNS.label), lstTables, stmt);
+
+			// load all foreign keys from TAP_SCHEMA.keys and TAP_SCHEMA.key_columns:
+			log(0, "Loading TAP_SCHEMA.keys and TAP_SCHEMA.key_columns.", null);
+			loadKeys(tap_schema.getTable(STDTable.KEYS.label), tap_schema.getTable(STDTable.KEY_COLUMNS.label), lstTables, stmt);
+
 		}catch(SQLException se){
-			logger.dbError("Impossible to end/commit a transaction !", se);
-			throw new DBException("Impossible to end/commit the transaction !", se);
+			log(2, "Impossible to create a Statement!", se);
+			throw new DBException("Can not create a Statement!", se);
+		}finally{
+			close(stmt);
 		}
+
+		return metadata;
 	}
 
-	@Override
-	public void close() throws DBException{
+	/**
+	 * <p>Load into the given metadata all schemas listed in TAP_SCHEMA.schemas.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	If schemas are not supported by this DBMS connection, the DB name of the loaded schemas is set to NULL.
+	 * </i></p>
+	 * 
+	 * @param tablesDef		Definition of the table TAP_SCHEMA.schemas.
+	 * @param metadata		Metadata to fill with all found schemas.
+	 * @param stmt			Statement to use in order to interact with the database.
+	 * 
+	 * @throws DBException	If any error occurs while interacting with the database. 
+	 */
+	protected void loadSchemas(final TAPTable tableDef, final TAPMetadata metadata, final Statement stmt) throws DBException{
+		ResultSet rs = null;
 		try{
-			connection.close();
-			logger.connectionClosed(this);
+			// Build the SQL query:
+			StringBuffer sqlBuf = new StringBuffer("SELECT ");
+			sqlBuf.append(translator.getColumnName(tableDef.getColumn("schema_name")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("description")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("utype")));
+			sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(tableDef)).append(';');
+
+			// Execute the query:
+			rs = stmt.executeQuery(sqlBuf.toString());
+
+			// Create all schemas:
+			while(rs.next()){
+				String schemaName = rs.getString(1), description = rs.getString(2), utype = rs.getString(3);
+
+				// create the new schema:
+				TAPSchema newSchema = new TAPSchema(schemaName, nullifyIfNeeded(description), nullifyIfNeeded(utype));
+
+				// If schemas are not supported by the DBMS connection, the schema must not be translated in the DB:
+				if (!supportsSchema)
+					newSchema.setDBName(null);
+
+				// add the new schema inside the given metadata:
+				metadata.addSchema(newSchema);
+			}
 		}catch(SQLException se){
-			logger.dbError("Impossible to close a database transaction !", se);
-			throw new DBException("Impossible to close the database transaction !", se);
+			log(2, "Impossible to load schemas from TAP_SCHEMA.schemas!", se);
+			throw new DBException("Impossible to load schemas from TAP_SCHEMA.schemas!", se);
+		}finally{
+			close(rs);
 		}
 	}
 
-	/* ********************* */
-	/* INTERROGATION METHODS */
-	/* ********************* */
-	@Override
-	public ResultSet executeQuery(final String sqlQuery, final ADQLQuery adqlQuery) throws DBException{
+	/**
+	 * <p>Load into the corresponding metadata all tables listed in TAP_SCHEMA.tables.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Schemas are searched in the given metadata by their ADQL name and case sensitively.
+	 * 	If they can not be found a {@link DBException} is thrown.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If schemas are not supported by this DBMS connection, the DB name of the loaded {@link TAPTable}s is prefixed by the ADQL name of their respective schema.
+	 * 	The table prefix is built by {@link #getTablePrefix(String)}.  
+	 * </i></p>
+	 * 
+	 * @param tablesDef		Definition of the table TAP_SCHEMA.tables.
+	 * @param metadata		Metadata (containing already all schemas listed in TAP_SCHEMA.schemas).
+	 * @param stmt			Statement to use in order to interact with the database.
+	 * 
+	 * @return	The complete list of all loaded tables. <i>note: this list is required by {@link #loadColumns(TAPTable, List, Statement)}.</i> 
+	 * 
+	 * @throws DBException	If a schema can not be found, or if any other error occurs while interacting with the database.
+	 * 
+	 * @see #getTablePrefix(String) 
+	 */
+	protected List<TAPTable> loadTables(final TAPTable tableDef, final TAPMetadata metadata, final Statement stmt) throws DBException{
+		ResultSet rs = null;
 		try{
-			Statement stmt = connection.createStatement();
-			logger.sqlQueryExecuting(this, sqlQuery);
-			ResultSet result = stmt.executeQuery(sqlQuery);
-			logger.sqlQueryExecuted(this, sqlQuery);
-			return result;
+			// Build the SQL query:
+			StringBuffer sqlBuf = new StringBuffer("SELECT ");
+			sqlBuf.append(translator.getColumnName(tableDef.getColumn("schema_name")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("table_name")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("table_type")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("description")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("utype")));
+			sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(tableDef)).append(';');
+
+			// Execute the query:
+			rs = stmt.executeQuery(sqlBuf.toString());
+
+			// Create all tables:
+			ArrayList<TAPTable> lstTables = new ArrayList<TAPTable>();
+			while(rs.next()){
+				String schemaName = rs.getString(1), tableName = rs.getString(2), typeStr = rs.getString(3), description = rs.getString(4), utype = rs.getString(5);
+
+				// get the schema:
+				TAPSchema schema = metadata.getSchema(schemaName);
+				if (schema == null){
+					log(2, "Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!", null);
+					throw new DBException("Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!");
+				}
+
+				// resolve the table type (if any) ; by default, it will be "table":
+				TableType type = TableType.table;
+				if (typeStr != null){
+					try{
+						type = TableType.valueOf(typeStr.toLowerCase());
+					}catch(IllegalArgumentException iae){}
+				}
+
+				// create the new table:
+				TAPTable newTable = new TAPTable(tableName, type, nullifyIfNeeded(description), nullifyIfNeeded(utype));
+
+				// If schemas are not supported by the DBMS connection, the DB table name must be prefixed by the schema name:
+				if (!supportsSchema)
+					newTable.setDBName(getTablePrefix(schema.getADQLName()) + newTable.getDBName());
+
+				// add the new table inside its corresponding schema:
+				schema.addTable(newTable);
+				lstTables.add(newTable);
+			}
+
+			return lstTables;
 		}catch(SQLException se){
-			logger.sqlQueryError(this, sqlQuery, se);
-			throw new DBException("Unexpected error while executing a SQL query: " + se.getMessage(), se);
+			log(2, "Impossible to load tables from TAP_SCHEMA.tables!", se);
+			throw new DBException("Impossible to load tables from TAP_SCHEMA.tables!", se);
+		}finally{
+			close(rs);
 		}
 	}
 
-	/* ************** */
-	/* UPLOAD METHODS */
-	/* ************** */
-	@Override
-	public void createSchema(final String schemaName) throws DBException{
-		String sql = "CREATE SCHEMA " + schemaName + ";";
+	/**
+	 * <p>Load into the corresponding tables all columns listed in TAP_SCHEMA.columns.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Tables are searched in the given list by their ADQL name and case sensitively.
+	 * 	If they can not be found a {@link DBException} is thrown.
+	 * </i></p>
+	 * 
+	 * @param columnsDef	Definition of the table TAP_SCHEMA.columns.
+	 * @param lstTables		List of all published tables (= all tables listed in TAP_SCHEMA.tables).
+	 * @param stmt			Statement to use in order to interact with the database.
+	 * 
+	 * @throws DBException	If a table can not be found, or if any other error occurs while interacting with the database. 
+	 */
+	protected void loadColumns(final TAPTable tableDef, final List<TAPTable> lstTables, final Statement stmt) throws DBException{
+		ResultSet rs = null;
 		try{
-			Statement stmt = connection.createStatement();
-			stmt.executeUpdate(sql);
-			logger.schemaCreated(this, schemaName);
+			// Build the SQL query:
+			StringBuffer sqlBuf = new StringBuffer("SELECT ");
+			sqlBuf.append(translator.getColumnName(tableDef.getColumn("table_name")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("column_name")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("description")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("unit")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("ucd")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("utype")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("datatype")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("size")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("principal")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("indexed")));
+			sqlBuf.append(", ").append(translator.getColumnName(tableDef.getColumn("std")));
+			sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(tableDef)).append(';');
+
+			// Execute the query:
+			rs = stmt.executeQuery(sqlBuf.toString());
+
+			// Create all tables:
+			while(rs.next()){
+				String tableName = rs.getString(1), columnName = rs.getString(2), description = rs.getString(3), unit = rs.getString(4), ucd = rs.getString(5), utype = rs.getString(6), datatype = rs.getString(7);
+				int size = rs.getInt(8);
+				boolean principal = toBoolean(rs.getObject(9)), indexed = toBoolean(rs.getObject(10)), std = toBoolean(rs.getObject(11));
+
+				// get the table:
+				TAPTable table = searchTable(tableName, lstTables.iterator());
+				if (table == null){
+					log(2, "Impossible to find the table of the column \"" + columnName + "\": \"" + tableName + "\"!", null);
+					throw new DBException("Impossible to find the table of the column \"" + columnName + "\": \"" + tableName + "\"!");
+				}
+
+				// resolve the column type (if any) ; by default, it will be "VARCHAR" if unknown or missing:
+				TAPDatatype tapDatatype = null;
+				// ...try to resolve the datatype in function of all datatypes declared by the TAP standard.
+				if (datatype != null){
+					try{
+						tapDatatype = TAPDatatype.valueOf(datatype.toUpperCase());
+					}catch(IllegalArgumentException iae){}
+				}
+				// ...build the column type:
+				TAPType type;
+				if (tapDatatype == null)
+					type = new TAPType(TAPDatatype.VARCHAR);
+				else
+					type = new TAPType(tapDatatype, size);
+
+				// create the new column:
+				TAPColumn newColumn = new TAPColumn(columnName, type, nullifyIfNeeded(description), nullifyIfNeeded(unit), nullifyIfNeeded(ucd), nullifyIfNeeded(utype));
+				newColumn.setPrincipal(principal);
+				newColumn.setIndexed(indexed);
+				newColumn.setStd(std);
+
+				// add the new column inside its corresponding table:
+				table.addColumn(newColumn);
+			}
 		}catch(SQLException se){
-			logger.dbError("Impossible to create the schema \"" + schemaName + "\" !", se);
-			throw new DBException("Impossible to create the schema \"" + schemaName + "\" !", se);
+			log(2, "Impossible to load columns from TAP_SCHEMA.columns!", se);
+			throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se);
+		}finally{
+			close(rs);
 		}
 	}
 
-	@Override
-	public void dropSchema(final String schemaName) throws DBException{
-		String sql = "DROP SCHEMA IF EXISTS " + schemaName + " CASCADE;";
+	/**
+	 * <p>Load into the corresponding tables all keys listed in TAP_SCHEMA.keys and detailed in TAP_SCHEMA.key_columns.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Tables and columns are searched in the given list by their ADQL name and case sensitively.
+	 * 	If they can not be found a {@link DBException} is thrown.
+	 * </i></p>
+	 * 
+	 * @param keysDef		Definition of the table TAP_SCHEMA.keys.
+	 * @param keyColumnsDef	Definition of the table TAP_SCHEMA.key_columns.
+	 * @param lstTables		List of all published tables (= all tables listed in TAP_SCHEMA.tables).
+	 * @param stmt			Statement to use in order to interact with the database.
+	 * 
+	 * @throws DBException	If a table or a column can not be found, or if any other error occurs while interacting with the database. 
+	 */
+	protected void loadKeys(final TAPTable keysDef, final TAPTable keyColumnsDef, final List<TAPTable> lstTables, final Statement stmt) throws DBException{
+		ResultSet rs = null;
+		PreparedStatement keyColumnsStmt = null;
 		try{
-			Statement stmt = connection.createStatement();
-			stmt.executeUpdate(sql);
-			logger.schemaDropped(this, schemaName);
+			// Prepare the query to get the columns of each key:
+			StringBuffer sqlBuf = new StringBuffer("SELECT ");
+			sqlBuf.append(translator.getColumnName(keyColumnsDef.getColumn("key_id")));
+			sqlBuf.append(", ").append(translator.getColumnName(keyColumnsDef.getColumn("from_column")));
+			sqlBuf.append(", ").append(translator.getColumnName(keyColumnsDef.getColumn("target_column")));
+			sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(keyColumnsDef));
+			sqlBuf.append(" WHERE ").append(translator.getColumnName(keyColumnsDef.getColumn("key_id"))).append(" = ?").append(';');
+			keyColumnsStmt = connection.prepareStatement(sqlBuf.toString());
+
+			// Build the SQL query to get the keys:
+			sqlBuf.delete(0, sqlBuf.length());
+			sqlBuf.append("SELECT ").append(translator.getColumnName(keysDef.getColumn("key_id")));
+			sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("from_table")));
+			sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("target_table")));
+			sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("description")));
+			sqlBuf.append(", ").append(translator.getColumnName(keysDef.getColumn("utype")));
+			sqlBuf.append(" FROM ").append(translator.getQualifiedTableName(keysDef)).append(';');
+
+			// Execute the query:
+			rs = stmt.executeQuery(sqlBuf.toString());
+
+			// Create all foreign keys:
+			while(rs.next()){
+				String key_id = rs.getString(1), from_table = rs.getString(2), target_table = rs.getString(3), description = rs.getString(4), utype = rs.getString(5);
+
+				// get the two tables (source and target):
+				TAPTable sourceTable = searchTable(from_table, lstTables.iterator());
+				if (sourceTable == null){
+					log(2, "Impossible to find the source table of the foreign key \"" + key_id + "\": \"" + from_table + "\"!", null);
+					throw new DBException("Impossible to find the source table of the foreign key \"" + key_id + "\": \"" + from_table + "\"!");
+				}
+				TAPTable targetTable = searchTable(target_table, lstTables.iterator());
+				if (targetTable == null){
+					log(2, "Impossible to find the target table of the foreign key \"" + key_id + "\": \"" + target_table + "\"!", null);
+					throw new DBException("Impossible to find the target table of the foreign key \"" + key_id + "\": \"" + target_table + "\"!");
+				}
+
+				// get the list of columns joining the two tables of the foreign key:
+				HashMap<String,String> columns = new HashMap<String,String>();
+				ResultSet rsKeyCols = null;
+				try{
+					keyColumnsStmt.setString(1, key_id);
+					rsKeyCols = keyColumnsStmt.executeQuery();
+					while(rsKeyCols.next())
+						columns.put(rsKeyCols.getString(1), rsKeyCols.getString(2));
+				}catch(SQLException se){
+					log(2, "Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se);
+					throw new DBException("Impossible to load key columns from TAP_SCHEMA.key_columns for the foreign key: \"" + key_id + "\"!", se);
+				}finally{
+					close(rsKeyCols);
+				}
+
+				// create and add the new foreign key inside the source table:
+				try{
+					sourceTable.addForeignKey(key_id, targetTable, columns, nullifyIfNeeded(description), nullifyIfNeeded(utype));
+				}catch(Exception ex){
+					log(2, "Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex);
+					throw new DBException("Impossible to create the foreign key \"" + key_id + "\" because: " + ex.getMessage(), ex);
+				}
+			}
 		}catch(SQLException se){
-			logger.dbError("Impossible to drop the schema \"" + schemaName + "\" !", se);
-			throw new DBException("Impossible to drop the schema \"" + schemaName + "\" !", se);
+			log(2, "Impossible to load columns from TAP_SCHEMA.columns!", se);
+			throw new DBException("Impossible to load columns from TAP_SCHEMA.columns!", se);
+		}finally{
+			close(rs);
+			close(keyColumnsStmt);
 		}
 	}
 
+	/* ********************************** */
+	/* SETTING TAP_SCHEMA IN THE DATABASE */
+	/* ********************************** */
+
+	/**
+	 * <p>This function is just calling the following functions:</p>
+	 * <ol>
+	 * 	<li>{@link #mergeTAPSchemaDefs(TAPMetadata)}</li>
+	 * 	<li>{@link #startTransaction()}</li>
+	 * 	<li>{@link #resetTAPSchema(Statement, TAPTable[])}</li>
+	 * 	<li>{@link #createTAPSchemaTable(TAPTable, Statement)} for each standard TAP_SCHEMA table</li>
+	 * 	<li>{@link #fillTAPSchema(TAPMetadata)}</li>
+	 * 	<li>{@link #createTAPTableIndexes(TAPTable, Statement)} for each standard TA_SCHEMA table</li>
+	 * 	<li>{@link #commit()} or {@link #rollback()}</li>
+	 * 	<li>{@link #endTransaction()}</li>
+	 * </ol>
+	 * 
+	 * <p><i><b>Important note:
+	 * 	If the connection does not support transactions, then there will be merely no transaction.
+	 * 	Consequently, any failure (exception/error) will not clean the partial modifications done by this function.
+	 * </i></p>
+	 * 
+	 * @see tap.db.DBConnection#setTAPSchema(tap.metadata.TAPMetadata)
+	 */
 	@Override
-	public void createTable(final TAPTable table) throws DBException{
-		// Build the SQL query:
-		StringBuffer sqlBuf = new StringBuffer();
-		sqlBuf.append("CREATE TABLE ").append(table.getDBSchemaName()).append('.').append(table.getDBName()).append("(");
-		Iterator<TAPColumn> it = table.getColumns();
-		while(it.hasNext()){
-			TAPColumn col = it.next();
-			sqlBuf.append('"').append(col.getDBName()).append("\" ").append(' ').append(getDBType(col.getDatatype(), col.getArraySize(), logger));
-			if (it.hasNext())
-				sqlBuf.append(',');
-		}
-		sqlBuf.append(");");
+	public void setTAPSchema(final TAPMetadata metadata) throws DBException{
+		Statement stmt = null;
 
-		// Execute the creation query:
-		String sql = sqlBuf.toString();
 		try{
-			Statement stmt = connection.createStatement();
-			stmt.executeUpdate(sql);
-			logger.tableCreated(this, table);
+			// A. GET THE DEFINITION OF ALL STANDARD TAP TABLES:
+			TAPTable[] stdTables = mergeTAPSchemaDefs(metadata);
+
+			startTransaction();
+
+			// B. RE-CREATE THE STANDARD TAP_SCHEMA TABLES:
+			stmt = connection.createStatement();
+
+			// 1. Ensure TAP_SCHEMA exists and drop all its standard TAP tables:
+			log(0, "Cleaning TAP_SCHEMA.", null);
+			resetTAPSchema(stmt, stdTables);
+
+			// 2. Create all standard TAP tables:
+			log(0, "Creating TAP_SCHEMA tables.", null);
+			for(TAPTable table : stdTables)
+				createTAPSchemaTable(table, stmt);
+
+			// C. FILL THE NEW TABLE USING THE GIVEN DATA ITERATOR:
+			log(0, "Filling TAP_SCHEMA tables.", null);
+			fillTAPSchema(metadata);
+
+			// D. CREATE THE INDEXES OF ALL STANDARD TAP TABLES:
+			log(0, "Creating TAP_SCHEMA tables' indexes.", null);
+			for(TAPTable table : stdTables)
+				createTAPTableIndexes(table, stmt);
+
+			commit();
 		}catch(SQLException se){
-			logger.dbError("Impossible to create the table \"" + table.getFullName() + "\" !", se);
-			throw new DBException("Impossible to create the table \"" + table.getFullName() + "\" !", se);
+			rollback();
+			throw new DBException("Impossible to SET TAP_SCHEMA in DB!", se);
+		}finally{
+			close(stmt);
+			endTransaction();
 		}
 	}
 
 	/**
-	 * Gets the database type corresponding to the given {@link TAPColumn} type.
+	 * <p>Merge the definition of TAP_SCHEMA tables given in parameter with the definition provided in the TAP standard.</p>
+	 * 
+	 * <p>
+	 * 	The goal is to get in output the list of all standard TAP_SCHEMA tables. But it must take into account the customized
+	 * 	definition given in parameter if there is one. Indeed, if a part of TAP_SCHEMA is not provided, it will be completed here by the
+	 * 	definition provided in the TAP standard. And so, if the whole TAP_SCHEMA is not provided at all, the returned tables will be those
+	 * 	of the IVOA standard.
+	 * </p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	If the TAP_SCHEMA definition is missing or incomplete in the given metadata, it will be added or completed automatically
+	 * 	by this function with the definition provided in the IVOA TAP standard.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	Only the standard tables of TAP_SCHEMA are considered. The others are skipped (that's to say: never returned by this function ;
+	 *  however, they will stay in the given metadata).
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If schemas are not supported by this DBMS connection, the DB name of schemas is set to NULL and
+	 * 	the DB name of tables is prefixed by the schema name (using {@link #getTablePrefix(String)}).
+	 * </i></p>
 	 * 
-	 * @param datatype		Column datatype (short, int, long, float, double, boolea, char or unsignedByte).
-	 * @param arraysize		Size of the array type (1 if not an array, a value &gt; 1 for an array).
-	 * @param logger		Object to use to print warnings (for instance, if a given datatype is unknown).
+	 * @param metadata	Metadata (with or without TAP_SCHEMA schema or some of its table). <i>Must not be NULL</i>
 	 * 
-	 * @return				The corresponding database type or the given datatype if unknown.
+	 * @return	The list of all standard TAP_SCHEMA tables, ordered by creation order (see {@link #getCreationOrder(STDTable)}).
+	 * 
+	 * @see TAPMetadata#resolveStdTable(String)
+	 * @see TAPMetadata#getStdSchema()
+	 * @see TAPMetadata#getStdTable(STDTable)
 	 */
-	public static String getDBType(String datatype, final int arraysize, final TAPLog logger){
-		datatype = (datatype == null) ? null : datatype.trim().toLowerCase();
+	protected TAPTable[] mergeTAPSchemaDefs(final TAPMetadata metadata){
+		// 1. Get the TAP_SCHEMA schema from the given metadata:
+		TAPSchema tapSchema = null;
+		Iterator<TAPSchema> itSchema = metadata.iterator();
+		while(tapSchema == null && itSchema.hasNext()){
+			TAPSchema schema = itSchema.next();
+			if (schema.getADQLName().equalsIgnoreCase(STDSchema.TAPSCHEMA.label))
+				tapSchema = schema;
+		}
+
+		// 2. Get the provided definition of the standard TAP tables:
+		TAPTable[] customStdTables = new TAPTable[5];
+		if (tapSchema != null){
+
+			/* if the schemas are not supported with this DBMS,
+			 * remove its DB name: */
+			if (!supportsSchema)
+				tapSchema.setDBName(null);
 
-		if (datatype == null || datatype.isEmpty()){
-			if (logger != null)
-				logger.warning("undefined datatype => considered as VARCHAR !");
-			return "VARCHAR";
+			// retrieve only the standard TAP tables:
+			Iterator<TAPTable> itTable = tapSchema.iterator();
+			while(itTable.hasNext()){
+				TAPTable table = itTable.next();
+				int indStdTable = getCreationOrder(TAPMetadata.resolveStdTable(table.getADQLName()));
+				if (indStdTable > -1)
+					customStdTables[indStdTable] = table;
+			}
 		}
 
-		if (datatype.equals("short"))
-			return (arraysize == 1) ? "INT2" : "BYTEA";
-		else if (datatype.equals("int"))
-			return (arraysize == 1) ? "INT4" : "BYTEA";
-		else if (datatype.equals("long"))
-			return (arraysize == 1) ? "INT8" : "BYTEA";
-		else if (datatype.equals("float"))
-			return (arraysize == 1) ? "FLOAT4" : "BYTEA";
-		else if (datatype.equals("double"))
-			return (arraysize == 1) ? "FLOAT8" : "BYTEA";
-		else if (datatype.equals("boolean"))
-			return (arraysize == 1) ? "BOOL" : "BYTEA";
-		else if (datatype.equals("char"))
-			return (arraysize == 1) ? "CHAR(1)" : ((arraysize <= 0) ? "VARCHAR" : ("VARCHAR(" + arraysize + ")"));
-		else if (datatype.equals("unsignedbyte"))
-			return "BYTEA";
-		else{
-			if (logger != null)
-				logger.dbInfo("Warning: unknown datatype: \"" + datatype + "\" => considered as \"" + datatype + "\" !");
-			return datatype;
+		// 3. Build a common TAPSchema, if needed:
+		if (tapSchema == null){
+
+			// build a new TAP_SCHEMA definition based on the standard definition:
+			tapSchema = TAPMetadata.getStdSchema();
+
+			/* if the schemas are not supported with this DBMS,
+			 * remove its DB name: */
+			if (!supportsSchema)
+				tapSchema.setDBName(null);
+
+			// add the new TAP_SCHEMA definition in the given metadata object:
+			metadata.addSchema(tapSchema);
+		}
+
+		// 4. Finally, build the join between the standard tables and the custom ones:
+		TAPTable[] stdTables = new TAPTable[]{TAPMetadata.getStdTable(STDTable.SCHEMAS),TAPMetadata.getStdTable(STDTable.TABLES),TAPMetadata.getStdTable(STDTable.COLUMNS),TAPMetadata.getStdTable(STDTable.KEYS),TAPMetadata.getStdTable(STDTable.KEY_COLUMNS)};
+		for(int i = 0; i < stdTables.length; i++){
+
+			// CASE: no custom definition:
+			if (customStdTables[i] == null){
+				/* if the schemas are not supported with this DBMS,
+				 * prefix the DB name with "tap_schema_": */
+				if (!supportsSchema)
+					stdTables[i].setDBName(getTablePrefix(tapSchema.getADQLName()) + stdTables[i].getDBName());
+				// add the table to the fetched or built-in schema:
+				tapSchema.addTable(stdTables[i]);
+			}
+			// CASE: custom definition
+			else
+				stdTables[i] = customStdTables[i];
 		}
+
+		return stdTables;
 	}
 
-	@Override
-	public void dropTable(final TAPTable table) throws DBException{
-		String sql = "DROP TABLE " + table.getDBSchemaName() + "." + table.getDBName() + ";";
+	/**
+	 * <p>Ensure the TAP_SCHEMA schema exists in the database AND it must especially drop all of its standard tables
+	 * (schemas, tables, columns, keys and key_columns), if they exist.</p>
+	 * 
+	 * <p><i><b>Important note</b>:
+	 * 	If TAP_SCHEMA already exists and contains other tables than the standard ones, they will not be dropped and they will stay in place.
+	 * </i></p>
+	 * 
+	 * @param stmt			The statement to use in order to interact with the database.
+	 * @param stdTables		List of all standard tables that must be (re-)created.
+	 *                      They will be used just to know the name of the standard tables that should be dropped here. 
+	 * 
+	 * @throws SQLException	If any error occurs while querying or updating the database.
+	 */
+	protected void resetTAPSchema(final Statement stmt, final TAPTable[] stdTables) throws SQLException{
+		DatabaseMetaData dbMeta = connection.getMetaData();
+
+		// 1. Get the qualified DB schema name:
+		String dbSchemaName = stdTables[0].getDBSchemaName();
+
+		/* 2. Test whether the schema TAP_SCHEMA exists
+		 *    and if it does not, create it: */
+		if (dbSchemaName != null){
+			// test whether the schema TAP_SCHEMA exists:
+			boolean hasTAPSchema = isSchemaExisting(dbSchemaName, dbMeta);
+
+			// create TAP_SCHEMA if it does not exist:
+			if (!hasTAPSchema)
+				stmt.executeUpdate("CREATE SCHEMA " + translator.getQualifiedSchemaName(stdTables[0]) + ";");
+		}
+
+		// 2-bis. Drop all its standard tables:
+		dropTAPSchemaTables(stdTables, stmt, dbMeta);
+	}
+
+	/**
+	 * <p>Remove/Drop all standard TAP_SCHEMA tables given in parameter.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	To test the existence of tables to drop, {@link DatabaseMetaData#getTables(String, String, String, String[])} is called.
+	 * 	Then the schema and table names are compared with the case sensitivity defined by the translator.
+	 * 	Only tables matching with these comparisons will be dropped.
+	 * </i></p>
+	 * 
+	 * @param stdTables	Tables to drop. (they should be provided ordered by their creation order (see {@link #getCreationOrder(STDTable)})).
+	 * @param stmt		Statement to use in order to interact with the database.
+	 * @param dbMeta	Database metadata. Used to list all existing tables.
+	 * 
+	 * @throws SQLException	If any error occurs while querying or updating the database.
+	 * 
+	 * @see ADQLTranslator#isCaseSensitive(IdentifierField)
+	 */
+	private void dropTAPSchemaTables(final TAPTable[] stdTables, final Statement stmt, final DatabaseMetaData dbMeta) throws SQLException{
+		String[] stdTablesToDrop = new String[]{null,null,null,null,null};
+
+		ResultSet rs = null;
 		try{
-			Statement stmt = connection.createStatement();
-			stmt.executeUpdate(sql);
-			logger.tableDropped(this, table);
-		}catch(SQLException se){
-			logger.dbError("Impossible to drop the table \"" + table.getFullName() + "\" !", se);
-			throw new DBException("Impossible to drop the table \"" + table.getFullName() + "\" !", se);
+			// Retrieve only the schema name and determine whether the search should be case sensitive:
+			String tapSchemaName = stdTables[0].getDBSchemaName();
+			boolean schemaCaseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA);
+			boolean tableCaseSensitive = translator.isCaseSensitive(IdentifierField.TABLE);
+
+			// Identify which standard TAP tables must be dropped:
+			rs = dbMeta.getTables(null, null, null, null);
+			while(rs.next()){
+				String rsSchema = nullifyIfNeeded(rs.getString(2)), rsTable = rs.getString(3);
+				if (!supportsSchema || (tapSchemaName == null && rsSchema == null) || equals(rsSchema, tapSchemaName, schemaCaseSensitive)){
+					int indStdTable;
+					indStdTable = getCreationOrder(isStdTable(rsTable, stdTables, tableCaseSensitive));
+					if (indStdTable > -1){
+						stdTablesToDrop[indStdTable] = (rsSchema != null ? "\"" + rsSchema + "\"." : "") + "\"" + rsTable + "\"";
+					}
+				}
+			}
+		}finally{
+			close(rs);
+		}
+
+		// Drop the existing tables (in the reverse order of creation):
+		for(int i = stdTablesToDrop.length - 1; i >= 0; i--){
+			if (stdTablesToDrop[i] != null)
+				stmt.executeUpdate("DROP TABLE " + stdTablesToDrop[i] + ";");
 		}
 	}
 
-	@Override
-	public void insertRow(final SavotTR row, final TAPTable table) throws DBException{
-		StringBuffer sql = new StringBuffer("INSERT INTO ");
-		sql.append(table.getDBSchemaName()).append('.').append(table.getDBName()).append(" VALUES (");
+	/**
+	 * <p>Create the specified standard TAP_SCHEMA tables into the database.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	Only standard TAP_SCHEMA tables (schemas, tables, columns, keys and key_columns) can be created here.
+	 * 	If the given table is not part of the schema TAP_SCHEMA (comparison done on the ADQL name case-sensitively)
+	 * 	and is not a standard TAP_SCHEMA table (comparison done on the ADQL name case-sensitively),
+	 * 	this function will do nothing and will throw an exception.
+	 * </i></p>
+	 * 
+	 * @param table	Table to create.
+	 * @param stmt	Statement to use in order to interact with the database.
+	 * 
+	 * @throws DBException	If the given table is not a standard TAP_SCHEMA table.
+	 * @throws SQLException	If any error occurs while querying or updating the database.
+	 */
+	protected void createTAPSchemaTable(final TAPTable table, final Statement stmt) throws DBException, SQLException{
+		// 1. ENSURE THE GIVEN TABLE IS REALLY A TAP_SCHEMA TABLE (according to the ADQL names):
+		if (!table.getADQLSchemaName().equalsIgnoreCase(STDSchema.TAPSCHEMA.label) || TAPMetadata.resolveStdTable(table.getADQLName()) == null)
+			throw new DBException("Forbidden table creation: " + table + " is not a standard table of TAP_SCHEMA!");
+
+		// 2. BUILD THE SQL QUERY TO CREATE THE TABLE:
+		StringBuffer sql = new StringBuffer("CREATE TABLE ");
 
-		TDSet cells = row.getTDs();
+		// a. Write the fully qualified table name:
+		sql.append(translator.getQualifiedTableName(table));
+
+		// b. List all the columns:
+		sql.append('(');
 		Iterator<TAPColumn> it = table.getColumns();
-		String datatype, value;
-		TAPColumn col;
-		int i = 0;
 		while(it.hasNext()){
-			col = it.next();
-			if (i > 0)
+			TAPColumn col = it.next();
+
+			// column name:
+			sql.append(translator.getColumnName(col));
+
+			// column type:
+			sql.append(' ').append(getDBMSDatatype(col.getDatatype()));
+
+			// last column ?
+			if (it.hasNext())
 				sql.append(',');
-			datatype = col.getDatatype();
-			value = cells.getContent(i);
-			if (value == null || value.isEmpty())
-				sql.append("NULL");
-			else if (datatype.equalsIgnoreCase("char") || datatype.equalsIgnoreCase("varchar") || datatype.equalsIgnoreCase("unsignedByte"))
-				sql.append('\'').append(value.replaceAll("'", "''").replaceAll("\0", "")).append('\'');
-			else{
-				if (value.equalsIgnoreCase("nan"))
-					sql.append("'NaN'");
-				else
-					sql.append(value.replaceAll("\0", ""));
-			}
-			i++;
 		}
-		sql.append(");");
 
-		try{
-			Statement stmt = connection.createStatement();
-			int nbInsertedRows = stmt.executeUpdate(sql.toString());
-			logger.rowsInserted(this, table, nbInsertedRows);
-		}catch(SQLException se){
-			logger.dbError("Impossible to insert a row into the table \"" + table.getFullName() + "\" !", se);
-			throw new DBException("Impossible to insert a row in the table \"" + table.getFullName() + "\" !", se);
+		// c. Append the primary key definition, if needed:
+		String primaryKey = getPrimaryKeyDef(table.getADQLName());
+		if (primaryKey != null)
+			sql.append(',').append(primaryKey);
+
+		// d. End the query:
+		sql.append(')').append(';');
+
+		// 3. FINALLY CREATE THE TABLE:
+		stmt.executeUpdate(sql.toString());
+	}
+
+	/**
+	 * <p>Get the primary key corresponding to the specified table.</p>
+	 * 
+	 * <p>If the specified table is not a standard TAP_SCHEMA table, NULL will be returned.</p>
+	 * 
+	 * @param tableName	ADQL table name.
+	 * 
+	 * @return	The primary key definition (prefixed by a space) corresponding to the specified table (ex: " PRIMARY KEY(schema_name)"),
+	 *          or NULL if the specified table is not a standard TAP_SCHEMA table.
+	 */
+	private String getPrimaryKeyDef(final String tableName){
+		STDTable stdTable = TAPMetadata.resolveStdTable(tableName);
+		if (stdTable == null)
+			return null;
+
+		boolean caseSensitive = translator.isCaseSensitive(IdentifierField.COLUMN);
+		switch(stdTable){
+			case SCHEMAS:
+				return " PRIMARY KEY(" + (caseSensitive ? "\"schema_name\"" : "schema_name") + ")";
+			case TABLES:
+				return " PRIMARY KEY(" + (caseSensitive ? "\"schema_name\"" : "schema_name") + ", " + (caseSensitive ? "\"table_name\"" : "table_name") + ")";
+			case COLUMNS:
+				return " PRIMARY KEY(" + (caseSensitive ? "\"table_name\"" : "table_name") + ", " + (caseSensitive ? "\"column_name\"" : "column_name") + ")";
+			case KEYS:
+			case KEY_COLUMNS:
+				return " PRIMARY KEY(" + (caseSensitive ? "\"key_id\"" : "key_id") + ")";
+			default:
+				return null;
+		}
+	}
+
+	/**
+	 * <p>Create the DB indexes corresponding to the given TAP_SCHEMA table.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	Only standard TAP_SCHEMA tables (schemas, tables, columns, keys and key_columns) can be created here.
+	 * 	If the given table is not part of the schema TAP_SCHEMA (comparison done on the ADQL name case-sensitively)
+	 * 	and is not a standard TAP_SCHEMA table (comparison done on the ADQL name case-sensitively),
+	 * 	this function will do nothing and will throw an exception.
+	 * </i></p>
+	 * 
+	 * @param table	Table whose indexes must be created here.
+	 * @param stmt	Statement to use in order to interact with the database.
+	 * 
+	 * @throws DBException	If the given table is not a standard TAP_SCHEMA table.
+	 * @throws SQLException	If any error occurs while querying or updating the database.
+	 */
+	protected void createTAPTableIndexes(final TAPTable table, final Statement stmt) throws DBException, SQLException{
+		// 1. Ensure the given table is really a TAP_SCHEMA table (according to the ADQL names):
+		if (!table.getADQLSchemaName().equalsIgnoreCase(STDSchema.TAPSCHEMA.label) || TAPMetadata.resolveStdTable(table.getADQLName()) == null)
+			throw new DBException("Forbidden index creation: " + table + " is not a standard table of TAP_SCHEMA!");
+
+		// Build the fully qualified DB name of the table: 
+		final String dbTableName = translator.getQualifiedTableName(table);
+
+		// Build the name prefix of all the indexes to create: 
+		final String indexNamePrefix = "INDEX_" + ((table.getADQLSchemaName() != null) ? (table.getADQLSchemaName() + "_") : "") + table.getADQLName() + "_";
+
+		Iterator<TAPColumn> it = table.getColumns();
+		while(it.hasNext()){
+			TAPColumn col = it.next();
+			// Create an index only for columns that have the 'indexed' flag:
+			if (col.isIndexed() && !isPartOfPrimaryKey(col.getADQLName()))
+				stmt.executeUpdate("CREATE INDEX " + indexNamePrefix + col.getADQLName() + " ON " + dbTableName + "(" + translator.getColumnName(col) + ");");
+		}
+	}
+
+	/**
+	 * Tell whether the specified column is part of the primary key of its table.
+	 * 
+	 * @param adqlName	ADQL name of a column.
+	 * 
+	 * @return	<i>true</i> if the specified column is part of the primary key,
+	 *          <i>false</i> otherwise.
+	 */
+	private boolean isPartOfPrimaryKey(final String adqlName){
+		if (adqlName == null)
+			return false;
+		else
+			return (adqlName.equalsIgnoreCase("schema_name") || adqlName.equalsIgnoreCase("table_name") || adqlName.equalsIgnoreCase("column_name") || adqlName.equalsIgnoreCase("key_id"));
+	}
+
+	/**
+	 * <p>Fill all the standard tables of TAP_SCHEMA (schemas, tables, columns, keys and key_columns).</p>
+	 * 
+	 * <p>This function just call the following functions:</p>
+	 * <ol>
+	 * 	<li>{@link #fillSchemas(TAPTable, Iterator)}</li>
+	 * 	<li>{@link #fillTables(TAPTable, Iterator)}</li>
+	 * 	<li>{@link #fillColumns(TAPTable, Iterator)}</li>
+	 * 	<li>{@link #fillKeys(TAPTable, TAPTable, Iterator)}</li>
+	 * </ol>
+	 * 
+	 * @param meta	All schemas and tables to list inside the TAP_SCHEMA tables.
+	 * 
+	 * @throws DBException	If rows can not be inserted because the SQL update query has failed.
+	 * @throws SQLException	If any other SQL exception occurs.
+	 */
+	protected void fillTAPSchema(final TAPMetadata meta) throws SQLException, DBException{
+		TAPTable metaTable;
+
+		// 1. Fill SCHEMAS:
+		metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.SCHEMAS.label);
+		Iterator<TAPTable> allTables = fillSchemas(metaTable, meta.iterator());
+
+		// 2. Fill TABLES:
+		metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.TABLES.label);
+		Iterator<TAPColumn> allColumns = fillTables(metaTable, allTables);
+		allTables = null;
+
+		// Fill COLUMNS:
+		metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.COLUMNS.label);
+		Iterator<TAPForeignKey> allKeys = fillColumns(metaTable, allColumns);
+		allColumns = null;
+
+		// Fill KEYS and KEY_COLUMNS:
+		metaTable = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.KEYS.label);
+		TAPTable metaTable2 = meta.getTable(STDSchema.TAPSCHEMA.label, STDTable.KEY_COLUMNS.label);
+		fillKeys(metaTable, metaTable2, allKeys);
+	}
+
+	/**
+	 * <p>Fill the standard table TAP_SCHEMA.schemas with the list of all published schemas.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Batch updates may be done here if its supported by the DBMS connection.
+	 * 	In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed.
+	 * </i></p>
+	 * 
+	 * @param metaTable	Description of TAP_SCHEMA.schemas.
+	 * @param itSchemas	Iterator over the list of schemas.
+	 * 
+	 * @return	Iterator over the full list of all tables (whatever is their schema).
+	 * 
+	 * @throws DBException	If rows can not be inserted because the SQL update query has failed.
+	 * @throws SQLException	If any other SQL exception occurs.
+	 */
+	private Iterator<TAPTable> fillSchemas(final TAPTable metaTable, final Iterator<TAPSchema> itSchemas) throws SQLException, DBException{
+		List<TAPTable> allTables = new ArrayList<TAPTable>();
+
+		// Build the SQL update query:
+		StringBuffer sql = new StringBuffer("INSERT INTO ");
+		sql.append(translator.getQualifiedTableName(metaTable)).append(" (");
+		sql.append(translator.getColumnName(metaTable.getColumn("schema_name")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("description")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("utype")));
+		sql.append(") VALUES (?, ?, ?);");
+
+		// Prepare the statement:
+		PreparedStatement stmt = null;
+		try{
+			stmt = connection.prepareStatement(sql.toString());
+
+			// Execute the query for each schema:
+			int nbRows = 0;
+			while(itSchemas.hasNext()){
+				TAPSchema schema = itSchemas.next();
+				nbRows++;
+
+				// list all tables of this schema:
+				appendAllInto(allTables, schema.iterator());
+
+				// add the schema entry into the DB:
+				stmt.setString(1, schema.getADQLName());
+				stmt.setString(2, schema.getDescription());
+				stmt.setString(3, schema.getUtype());
+				executeUpdate(stmt, nbRows);
+			}
+			executeBatchUpdates(stmt, nbRows);
+		}finally{
+			close(stmt);
+		}
+
+		return allTables.iterator();
+	}
+
+	/**
+	 * <p>Fill the standard table TAP_SCHEMA.tables with the list of all published tables.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Batch updates may be done here if its supported by the DBMS connection.
+	 * 	In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed.
+	 * </i></p>
+	 * 
+	 * @param metaTable	Description of TAP_SCHEMA.tables.
+	 * @param itTables	Iterator over the list of tables.
+	 * 
+	 * @return	Iterator over the full list of all columns (whatever is their table).
+	 * 
+	 * @throws DBException	If rows can not be inserted because the SQL update query has failed.
+	 * @throws SQLException	If any other SQL exception occurs.
+	 */
+	private Iterator<TAPColumn> fillTables(final TAPTable metaTable, final Iterator<TAPTable> itTables) throws SQLException, DBException{
+		List<TAPColumn> allColumns = new ArrayList<TAPColumn>();
+
+		// Build the SQL update query:
+		StringBuffer sql = new StringBuffer("INSERT INTO ");
+		sql.append(translator.getQualifiedTableName(metaTable)).append(" (");
+		sql.append(translator.getColumnName(metaTable.getColumn("schema_name")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("table_name")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("table_type")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("description")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("utype")));
+		sql.append(") VALUES (?, ?, ?, ?, ?);");
+
+		// Prepare the statement:
+		PreparedStatement stmt = null;
+		try{
+			stmt = connection.prepareStatement(sql.toString());
+
+			// Execute the query for each table:
+			int nbRows = 0;
+			while(itTables.hasNext()){
+				TAPTable table = itTables.next();
+				nbRows++;
+
+				// list all columns of this table:
+				appendAllInto(allColumns, table.getColumns());
+
+				// add the table entry into the DB:
+				stmt.setString(1, table.getADQLSchemaName());
+				stmt.setString(2, table.getADQLName());
+				stmt.setString(3, table.getType().toString());
+				stmt.setString(4, table.getDescription());
+				stmt.setString(5, table.getUtype());
+				executeUpdate(stmt, nbRows);
+			}
+			executeBatchUpdates(stmt, nbRows);
+		}finally{
+			close(stmt);
+		}
+
+		return allColumns.iterator();
+	}
+
+	/**
+	 * <p>Fill the standard table TAP_SCHEMA.columns with the list of all published columns.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Batch updates may be done here if its supported by the DBMS connection.
+	 * 	In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed.
+	 * </i></p>
+	 * 
+	 * @param metaTable	Description of TAP_SCHEMA.columns.
+	 * @param itColumns	Iterator over the list of columns.
+	 * 
+	 * @return	Iterator over the full list of all foreign keys.
+	 * 
+	 * @throws DBException	If rows can not be inserted because the SQL update query has failed.
+	 * @throws SQLException	If any other SQL exception occurs.
+	 */
+	private Iterator<TAPForeignKey> fillColumns(final TAPTable metaTable, final Iterator<TAPColumn> itColumns) throws SQLException, DBException{
+		List<TAPForeignKey> allKeys = new ArrayList<TAPForeignKey>();
+
+		// Build the SQL update query:
+		StringBuffer sql = new StringBuffer("INSERT INTO ");
+		sql.append(translator.getQualifiedTableName(metaTable)).append(" (");
+		sql.append(translator.getColumnName(metaTable.getColumn("table_name")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("column_name")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("description")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("unit")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("ucd")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("utype")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("datatype")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("size")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("principal")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("indexed")));
+		sql.append(", ").append(translator.getColumnName(metaTable.getColumn("std")));
+		sql.append(") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
+
+		// Prepare the statement:
+		PreparedStatement stmt = null;
+		try{
+			stmt = connection.prepareStatement(sql.toString());
+
+			// Execute the query for each column:
+			int nbRows = 0;
+			while(itColumns.hasNext()){
+				TAPColumn col = itColumns.next();
+				nbRows++;
+
+				// list all foreign keys of this column:
+				appendAllInto(allKeys, col.getTargets());
+
+				// add the column entry into the DB:
+				stmt.setString(1, col.getTable().getADQLName());
+				stmt.setString(2, col.getADQLName());
+				stmt.setString(3, col.getDescription());
+				stmt.setString(4, col.getUnit());
+				stmt.setString(5, col.getUcd());
+				stmt.setString(6, col.getUtype());
+				stmt.setString(7, col.getDatatype().type.toString());
+				stmt.setInt(8, col.getDatatype().length);
+				stmt.setInt(9, col.isPrincipal() ? 1 : 0);
+				stmt.setInt(10, col.isIndexed() ? 1 : 0);
+				stmt.setInt(11, col.isStd() ? 1 : 0);
+				executeUpdate(stmt, nbRows);
+			}
+			executeBatchUpdates(stmt, nbRows);
+		}finally{
+			close(stmt);
+		}
+
+		return allKeys.iterator();
+	}
+
+	/**
+	 * <p>Fill the standard tables TAP_SCHEMA.keys and TAP_SCHEMA.key_columns with the list of all published foreign keys.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Batch updates may be done here if its supported by the DBMS connection.
+	 * 	In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed.
+	 * </i></p>
+	 * 
+	 * @param metaKeys			Description of TAP_SCHEMA.keys.
+	 * @param metaKeyColumns	Description of TAP_SCHEMA.key_columns.
+	 * @param itKeys			Iterator over the list of foreign keys.
+	 * 
+	 * @throws DBException	If rows can not be inserted because the SQL update query has failed.
+	 * @throws SQLException	If any other SQL exception occurs.
+	 */
+	private void fillKeys(final TAPTable metaKeys, final TAPTable metaKeyColumns, final Iterator<TAPForeignKey> itKeys) throws SQLException, DBException{
+		// Build the SQL update query for KEYS:
+		StringBuffer sqlKeys = new StringBuffer("INSERT INTO ");
+		sqlKeys.append(translator.getQualifiedTableName(metaKeys)).append(" (");
+		sqlKeys.append(translator.getColumnName(metaKeys.getColumn("key_id")));
+		sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("from_table")));
+		sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("target_table")));
+		sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("description")));
+		sqlKeys.append(", ").append(translator.getColumnName(metaKeys.getColumn("utype")));
+		sqlKeys.append(") VALUES (?, ?, ?, ?, ?);");
+
+		PreparedStatement stmtKeys = null, stmtKeyCols = null;
+		try{
+			// Prepare the statement for KEYS:
+			stmtKeys = connection.prepareStatement(sqlKeys.toString());
+
+			// Build the SQL update query for KEY_COLUMNS:
+			StringBuffer sqlKeyCols = new StringBuffer("INSERT INTO ");
+			sqlKeyCols.append(translator.getQualifiedTableName(metaKeyColumns)).append(" (");
+			sqlKeyCols.append(translator.getColumnName(metaKeyColumns.getColumn("key_id")));
+			sqlKeyCols.append(", ").append(translator.getColumnName(metaKeyColumns.getColumn("from_column")));
+			sqlKeyCols.append(", ").append(translator.getColumnName(metaKeyColumns.getColumn("target_column")));
+			sqlKeyCols.append(") VALUES (?, ?, ?);");
+
+			// Prepare the statement for KEY_COLUMNS:
+			stmtKeyCols = connection.prepareStatement(sqlKeyCols.toString());
+
+			// Execute the query for each column:
+			int nbKeys = 0, nbKeyColumns = 0;
+			while(itKeys.hasNext()){
+				TAPForeignKey key = itKeys.next();
+				nbKeys++;
+
+				// add the key entry into KEYS:
+				stmtKeys.setString(1, key.getKeyId());
+				stmtKeys.setString(2, key.getFromTable().getFullName());
+				stmtKeys.setString(3, key.getTargetTable().getFullName());
+				stmtKeys.setString(4, key.getDescription());
+				stmtKeys.setString(5, key.getUtype());
+				executeUpdate(stmtKeys, nbKeys);
+
+				// add the key columns into KEY_COLUMNS:
+				Iterator<Map.Entry<String,String>> itAssoc = key.iterator();
+				while(itAssoc.hasNext()){
+					nbKeyColumns++;
+					Map.Entry<String,String> assoc = itAssoc.next();
+					stmtKeyCols.setString(1, key.getKeyId());
+					stmtKeyCols.setString(2, assoc.getKey());
+					stmtKeyCols.setString(3, assoc.getValue());
+					executeUpdate(stmtKeyCols, nbKeyColumns);
+				}
+			}
+
+			executeBatchUpdates(stmtKeys, nbKeys);
+			executeBatchUpdates(stmtKeyCols, nbKeyColumns);
+		}finally{
+			close(stmtKeys);
+			close(stmtKeyCols);
+		}
+	}
+
+	/* ***************** */
+	/* UPLOAD MANAGEMENT */
+	/* ***************** */
+
+	/**
+	 * <p><i><b>Important note:</b>
+	 * 	Only tables uploaded by users can be created in the database. To ensure that, the schema name of this table MUST be {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD") in ADQL.
+	 * 	If it has another ADQL name, an exception will be thrown. Of course, the DB name of this schema MAY be different.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function may modify the given {@link TAPTable} object in the following cases:.
+	 * </i></p>
+	 * <ul>
+	 * 	<li><i>
+	 * 		If no schema is set to the given {@link TAPTable} object, one will be set automatically by this function.
+	 * 		This schema will have the same ADQL and DB name: {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD").
+	 * 	</i></li>
+	 * 	<li><i>
+	 * 		If schemas are not supported by this connection, this function will prefix the table DB name by the schema DB name directly
+	 * 		inside the given {@link TAPTable} object (building the prefix with {@link #getTablePrefix(String)}). Then the DB name
+	 * 		of the schema will be set to NULL.
+	 * 	</i></li>
+	 * </ul>
+	 * 
+	 * <p><i>Note:
+	 * 	If the upload schema does not already exist in the database, it will be created.
+	 * </i></p>
+	 * 
+	 * @see tap.db.DBConnection#addUploadedTable(tap.metadata.TAPTable, tap.data.TableIterator)
+	 * @see #checkUploadedTableDef(TAPTable)
+	 */
+	@Override
+	public boolean addUploadedTable(TAPTable tableDef, TableIterator data) throws DBException, DataReadException{
+		// If no table to upload, consider it has been dropped and return TRUE:
+		if (tableDef == null)
+			return true;
+
+		// Check the table is well defined (and particularly the schema is well set with an ADQL name = TAP_UPLOAD):
+		checkUploadedTableDef(tableDef);
+
+		Statement stmt = null;
+		try{
+
+			// Start a transaction:
+			startTransaction();
+			// ...create a statement:
+			stmt = connection.createStatement();
+
+			DatabaseMetaData dbMeta = connection.getMetaData();
+
+			// 1. Create the upload schema, if it does not already exist:
+			if (!isSchemaExisting(tableDef.getDBSchemaName(), dbMeta))
+				stmt.executeUpdate("CREATE SCHEMA " + translator.getQualifiedSchemaName(tableDef) + ";");
+
+			// 1bis. Ensure the table does not already exist and if it is the case, throw an understandable exception:
+			else if (isTableExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), dbMeta))
+				throw new DBException("Impossible to create the user uploaded table in the database: " + translator.getQualifiedTableName(tableDef) + "! This table already exists.");
+
+			// 2. Create the table:
+			// ...build the SQL query:
+			StringBuffer sqlBuf = new StringBuffer("CREATE TABLE ");
+			sqlBuf.append(translator.getQualifiedTableName(tableDef)).append(" (");
+			Iterator<TAPColumn> it = tableDef.getColumns();
+			while(it.hasNext()){
+				TAPColumn col = it.next();
+				// column name:
+				sqlBuf.append(translator.getColumnName(col));
+				// column type:
+				sqlBuf.append(' ').append(getDBMSDatatype(col.getDatatype()));
+				// last column ?
+				if (it.hasNext())
+					sqlBuf.append(',');
+			}
+			sqlBuf.append(");");
+			// ...execute the update query:
+			stmt.executeUpdate(sqlBuf.toString());
+
+			// 3. Fill the table:
+			fillUploadedTable(tableDef, data);
+
+			// Commit the transaction:
+			commit();
+
+			return true;
+
+		}catch(SQLException se){
+			rollback();
+			log(1, "Impossible to create the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se);
+			throw new DBException("Impossible to create the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se);
+		}catch(DBException de){
+			rollback();
+			throw de;
+		}catch(DataReadException dre){
+			rollback();
+			throw dre;
+		}finally{
+			close(stmt);
+			endTransaction();
+		}
+	}
+
+	/**
+	 * <p>Fill the table uploaded by the user with the given data.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Batch updates may be done here if its supported by the DBMS connection.
+	 * 	In case of any failure while using this feature, it will be flagged as unsupported and one-by-one updates will be processed.
+	 * </i></p>
+	 * 
+	 * @param metaTable	Description of the updated table.
+	 * @param data		Iterator over the rows to insert.
+	 * 
+	 * @return	Number of inserted rows.
+	 * 
+	 * @throws DBException			If rows can not be inserted because the SQL update query has failed.
+	 * @throws SQLException			If any other SQL exception occurs.
+	 * @throws DataReadException	If there is any error while reading the data from the given {@link TableIterator} (and particularly if a limit - in byte or row - has been reached).
+	 */
+	protected int fillUploadedTable(final TAPTable metaTable, final TableIterator data) throws SQLException, DBException, DataReadException{
+		// 1. Build the SQL update query:
+		StringBuffer sql = new StringBuffer("INSERT INTO ");
+		StringBuffer varParam = new StringBuffer();
+		// ...table name:
+		sql.append(translator.getQualifiedTableName(metaTable)).append(" (");
+		// ...list of columns:
+		TAPColumn[] cols = data.getMetadata();
+		for(int c = 0; c < cols.length; c++){
+			if (c > 0){
+				sql.append(", ");
+				varParam.append(", ");
+			}
+			sql.append(translator.getColumnName(cols[c]));
+			varParam.append('?');
+		}
+		// ...values pattern:
+		sql.append(") VALUES (").append(varParam).append(");");
+
+		// 2. Prepare the statement:
+		PreparedStatement stmt = null;
+		int nbRows = 0;
+		try{
+			stmt = connection.prepareStatement(sql.toString());
+
+			// 3. Execute the query for each given row:
+			while(data.nextRow()){
+				nbRows++;
+				int c = 1;
+				while(data.hasNextCol())
+					stmt.setObject(c++, data.nextCol());
+				executeUpdate(stmt, nbRows);
+			}
+			executeBatchUpdates(stmt, nbRows);
+
+			return nbRows;
+
+		}finally{
+			close(stmt);
+		}
+	}
+
+	/**
+	 * <p><i><b>Important note:</b>
+	 * 	Only tables uploaded by users can be dropped from the database. To ensure that, the schema name of this table MUST be {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD") in ADQL.
+	 * 	If it has another ADQL name, an exception will be thrown. Of course, the DB name of this schema MAY be different.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function may modify the given {@link TAPTable} object in the following cases:.
+	 * </i></p>
+	 * <ul>
+	 * 	<li><i>
+	 * 		If no schema is set to the given {@link TAPTable} object, one will be set automatically by this function.
+	 * 		This schema will have the same ADQL and DB name: {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD").
+	 * 	</i></li>
+	 * 	<li><i>
+	 * 		If schemas are not supported by this connection, this function will prefix the table DB name by the schema DB name directly
+	 * 		inside the given {@link TAPTable} object (building the prefix with {@link #getTablePrefix(String)}). Then the DB name
+	 * 		of the schema will be set to NULL.
+	 * 	</i></li>
+	 * </ul>
+	 * 
+	 * <p><i>Note:
+	 * 	This implementation is able to drop only one uploaded table. So if this function finds more than one table matching to the given one,
+	 * 	an exception will be thrown and no table will be dropped.
+	 * </i></p>
+	 * 
+	 * @see tap.db.DBConnection#dropUploadedTable(tap.metadata.TAPTable)
+	 * @see #checkUploadedTableDef(TAPTable)
+	 */
+	@Override
+	public boolean dropUploadedTable(final TAPTable tableDef) throws DBException{
+		// If no table to upload, consider it has been dropped and return TRUE:
+		if (tableDef == null)
+			return true;
+
+		// Check the table is well defined (and particularly the schema is well set with an ADQL name = TAP_UPLOAD):
+		checkUploadedTableDef(tableDef);
+
+		Statement stmt = null;
+		try{
+
+			// Check the existence of the table to drop:
+			if (!isTableExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), connection.getMetaData()))
+				return true;
+
+			// Execute the update:
+			stmt = connection.createStatement();
+			int cnt = stmt.executeUpdate("DROP TABLE " + translator.getQualifiedTableName(tableDef) + ";");
+
+			// Ensure the update is successful:
+			return (cnt == 0);
+
+		}catch(SQLException se){
+			log(1, "Impossible to drop the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se);
+			throw new DBException("Impossible to drop the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se);
+		}finally{
+			close(stmt);
+		}
+	}
+
+	/**
+	 * <p>Ensures that the given table MUST be inside the upload schema in ADQL.</p>
+	 * 
+	 * <p>Thus, the following cases are taken into account:</p>
+	 * <ul>
+	 * 	<li>
+	 * 		If no schema is set to the given {@link TAPTable} object, one will be set automatically by this function.
+	 * 		This schema will have the same ADQL and DB name: {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD").
+	 * 	</li>
+	 * 	<li>
+	 * 		The schema name of the given table , if provided, MUST be {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD") in ADQL.
+	 * 		If it has another ADQL name, an exception will be thrown. Of course, the DB name of this schema MAY be different.
+	 * 	</li>
+	 * 	<li>
+	 * 		If schemas are not supported by this connection, this function will prefix the table DB name by the schema DB name directly
+	 * 		inside the given {@link TAPTable} object (building the prefix with {@link #getTablePrefix(String)}). Then the DB name
+	 * 		of the schema will be set to NULL.
+	 * 	</li>
+	 * </ul>
+	 * 
+	 * @param tableDef	Definition of the table to create/drop.
+	 * 
+	 * @throws DBException	If the ADQL schema name of the given table is not {@link STDSchema#UPLOADSCHEMA} ("TAP_UPLOAD").
+	 */
+	protected void checkUploadedTableDef(final TAPTable tableDef) throws DBException{
+		// If no schema is provided, consider it is the default and standard upload schema - TAP_UPLOAD: 
+		if (tableDef.getSchema() == null){
+			TAPSchema uploadSchema = new TAPSchema(STDSchema.UPLOADSCHEMA.label, "Schema for tables uploaded by users.");
+			uploadSchema.addTable(tableDef);
+		}
+		// But if the ADQL name of the provided schema is not TAP_UPLOAD, throw an exception:
+		else if (!tableDef.getSchema().getADQLName().equals(STDSchema.UPLOADSCHEMA.label))
+			throw new DBException("Missing upload schema! An uploaded table must be inside a schema whose the ADQL name is strictly equals to \"" + STDSchema.UPLOADSCHEMA.label + "\" (but the DB name may be different).");
+
+		// If schemas are not supported, prefix the table name and set to NULL the DB schema name:
+		if (!supportsSchema && tableDef.getDBSchemaName() != null){
+			tableDef.setDBName(getTablePrefix(tableDef.getDBSchemaName()) + tableDef.getDBName());
+			tableDef.getSchema().setDBName(null);
+		}
+	}
+
+	/* ************** */
+	/* TOOL FUNCTIONS */
+	/* ************** */
+
+	/**
+	 * <p>Log the given message and/or exception with the given level.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	If no logger has been provided, only the WARNING and ERROR messages are printed in the standard error output stream.
+	 * </i></p>
+	 * 
+	 * @param level		<=0: INFO, 1: WARNING, >=2: ERROR 
+	 * @param message	Message to log. <i>may be NULL</i>
+	 * @param ex		Exception to log. <i>may be NULL</i>
+	 */
+	protected void log(final int level, String message, final Exception ex){
+		//if (logger != null){
+		if (message != null)
+			message = message.replaceAll("(\t|\r?\n)+", " ");
+		else
+			message = (ex != null ? ex.getClass().getName() : "");
+		message = "{" + getID() + "} " + message;
+		PrintStream out = (level <= 0) ? System.out : System.err;
+		out.println((level <= 0 ? "[INFO] " : (level == 1 ? "[WARNING] " : "[ERROR] ")) + message + (ex != null ? " CAUSED BY: " + ex.getMessage() : "")); // TODO rmb debug msg
+		if (ex != null)
+			ex.printStackTrace(out);
+		// TODO JDBCConnection.log(int, String, Exception)
+		//}
+	}
+
+	/**
+	 * <p>Get the DBMS compatible datatype corresponding to the given column TAPType.</p>
+	 * 
+	 * <p><i>Note 1:
+	 * 	This function is able to generate a DB datatype compatible with the currently used DBMS.
+	 * 	In this current implementation, only Postgresql, Oracle, SQLite, MySQL and Java DB/Derby have been considered.
+	 * 	Most of the TAP types have been tested only with Postgresql and SQLite without any problem.
+	 * 	If the DBMS you are using has not been considered, note that this function will return the TAP type expression by default.
+	 * </i></p>
+	 * 
+	 * <p><i>Note 2:
+	 * 	In case the given datatype is NULL or not managed here, the DBMS type corresponding to "VARCHAR" will be returned.
+	 * </i></p>
+	 * 
+	 * <p><i>Note 3:
+	 * 	The special TAP types POINT and REGION are converted into the DBMS type corresponding to "VARCHAR".
+	 * </i></p>
+	 * 
+	 * @param datatype	Column TAP type.
+	 * 
+	 * @return	The corresponding DB type, or NULL if the given type is not managed or is NULL.
+	 */
+	protected String getDBMSDatatype(TAPType datatype){
+		if (datatype == null)
+			datatype = new TAPType(TAPDatatype.VARCHAR);
+
+		switch(datatype.type){
+
+			case SMALLINT:
+				return dbms.equals("sqlite") ? "INTEGER" : "SMALLINT";
+
+			case INTEGER:
+			case REAL:
+				return datatype.type.toString();
+
+			case BIGINT:
+				if (dbms.equals("oracle"))
+					return "NUMBER(19,0)";
+				else if (dbms.equals("sqlite"))
+					return "INTEGER";
+				else
+					return "BIGINT";
+
+			case DOUBLE:
+				if (dbms.equals("postgresql") || dbms.equals("oracle"))
+					return "DOUBLE PRECISION";
+				else if (dbms.equals("sqlite"))
+					return "REAL";
+				else
+					return "DOUBLE";
+
+			case BINARY:
+				if (dbms.equals("postgresql"))
+					return "bytea";
+				else if (dbms.equals("sqlite"))
+					return "BLOB";
+				else if (dbms.equals("oracle"))
+					return "RAW" + (datatype.length > 0 ? "(" + datatype.length + ")" : "");
+				else if (dbms.equals("derby"))
+					return "CHAR" + (datatype.length > 0 ? "(" + datatype.length + ")" : "") + " FOR BIT DATA";
+				else
+					return datatype.type.toString();
+
+			case VARBINARY:
+				if (dbms.equals("postgresql"))
+					return "bytea";
+				else if (dbms.equals("sqlite"))
+					return "BLOB";
+				else if (dbms.equals("oracle"))
+					return "LONG RAW" + (datatype.length > 0 ? "(" + datatype.length + ")" : "");
+				else if (dbms.equals("derby"))
+					return "VARCHAR" + (datatype.length > 0 ? "(" + datatype.length + ")" : "") + " FOR BIT DATA";
+				else
+					return datatype.type.toString();
+
+			case CHAR:
+				if (dbms.equals("sqlite"))
+					return "TEXT";
+				else
+					return "CHAR";
+
+			case BLOB:
+				if (dbms.equals("postgresql"))
+					return "bytea";
+				else
+					return "BLOB";
+
+			case CLOB:
+				if (dbms.equals("postgresql") || dbms.equals("mysql") || dbms.equals("sqlite"))
+					return "TEXT";
+				else
+					return "CLOB";
+
+			case TIMESTAMP:
+				if (dbms.equals("sqlite"))
+					return "TEXT";
+				else
+					return "TIMESTAMP";
+
+			case POINT:
+			case REGION:
+			case VARCHAR:
+			default:
+				if (dbms.equals("sqlite"))
+					return "TEXT";
+				else
+					return "VARCHAR";
+		}
+	}
+
+	/**
+	 * <p>Start a transaction.</p>
+	 * 
+	 * <p>
+	 * 	Basically, if transactions are supported by this connection, the flag AutoCommit is just turned off.
+	 * 	It will be turned on again when {@link #endTransaction()} is called.
+	 * </p>
+	 * 
+	 * <p>If transactions are not supported by this connection, nothing is done.</p>
+	 * 
+	 * <p><b><i>Important note:</b>
+	 * 	If any error interrupts the START TRANSACTION operation, transactions will be afterwards considered as not supported by this connection.
+	 * 	So, subsequent call to this function (and any other transaction related function) will never do anything.
+	 * </i></p>
+	 * 
+	 * @throws DBException	If it is impossible to start a transaction though transactions are supported by this connection.
+	 *                    	If these are not supported, this error can never be thrown.
+	 */
+	protected void startTransaction() throws DBException{
+		try{
+			if (supportsTransaction){
+				connection.setAutoCommit(false);
+				log(0, "Transaction STARTED.", null);
+			}
+		}catch(SQLException se){
+			supportsTransaction = false;
+			log(2, "Transaction STARTing impossible!", se);
+			throw new DBException("Transaction STARTing impossible!", se);
+		}
+	}
+
+	/**
+	 * <p>Commit the current transaction.</p>
+	 * 
+	 * <p>
+	 * 	{@link #startTransaction()} must have been called before. If it's not the case the connection
+	 * 	may throw a {@link SQLException} which will be transformed into a {@link DBException} here.
+	 * </p>
+	 * 
+	 * <p>If transactions are not supported by this connection, nothing is done.</p>
+	 * 
+	 * <p><b><i>Important note:</b>
+	 * 	If any error interrupts the COMMIT operation, transactions will be afterwards considered as not supported by this connection.
+	 * 	So, subsequent call to this function (and any other transaction related function) will never do anything.
+	 * </i></p>
+	 * 
+	 * @throws DBException	If it is impossible to commit a transaction though transactions are supported by this connection..
+	 *                    	If these are not supported, this error can never be thrown.
+	 */
+	protected void commit() throws DBException{
+		try{
+			if (supportsTransaction){
+				connection.commit();
+				log(0, "Transaction COMMITED.", null);
+			}
+		}catch(SQLException se){
+			supportsTransaction = false;
+			log(2, "Transaction COMMIT impossible!", se);
+			throw new DBException("Transaction COMMIT impossible!", se);
+		}
+	}
+
+	/**
+	 * <p>Rollback the current transaction.</p>
+	 * 
+	 * <p>
+	 * 	{@link #startTransaction()} must have been called before. If it's not the case the connection
+	 * 	may throw a {@link SQLException} which will be transformed into a {@link DBException} here.
+	 * </p>
+	 * 
+	 * <p>If transactions are not supported by this connection, nothing is done.</p>
+	 * 
+	 * <p><b><i>Important note:</b>
+	 * 	If any error interrupts the ROLLBACK operation, transactions will considered afterwards as not supported by this connection.
+	 * 	So, subsequent call to this function (and any other transaction related function) will never do anything.
+	 * </i></p>
+	 * 
+	 * @throws DBException	If it is impossible to rollback a transaction though transactions are supported by this connection..
+	 *                    	If these are not supported, this error can never be thrown.
+	 */
+	protected void rollback(){
+		try{
+			if (supportsTransaction){
+				connection.rollback();
+				log(0, "Transaction ROLLBACKED.", null);
+			}
+		}catch(SQLException se){
+			supportsTransaction = false;
+			log(2, "Transaction ROLLBACK impossible!", se);
+		}
+	}
+
+	/**
+	 * <p>End the current transaction.</p>
+	 * 
+	 * <p>
+	 * 	Basically, if transactions are supported by this connection, the flag AutoCommit is just turned on.
+	 * </p>
+	 * 
+	 * <p>If transactions are not supported by this connection, nothing is done.</p>
+	 * 
+	 * <p><b><i>Important note:</b>
+	 * 	If any error interrupts the END TRANSACTION operation, transactions will be afterwards considered as not supported by this connection.
+	 * 	So, subsequent call to this function (and any other transaction related function) will never do anything.
+	 * </i></p>
+	 * 
+	 * @throws DBException	If it is impossible to end a transaction though transactions are supported by this connection.
+	 *                    	If these are not supported, this error can never be thrown.
+	 */
+	protected void endTransaction(){
+		try{
+			if (supportsTransaction){
+				connection.setAutoCommit(true);
+				log(0, "Transaction ENDED.", null);
+			}
+		}catch(SQLException se){
+			supportsTransaction = false;
+			log(2, "Transaction ENDing impossible!", se);
+		}
+	}
+
+	/**
+	 * <p>Close silently the given {@link ResultSet}.</p>
+	 * 
+	 * <p>If the given {@link ResultSet} is NULL, nothing (even exception/error) happens.</p>
+	 * 
+	 * <p>
+	 * 	If any {@link SQLException} occurs during this operation, it is caught and just logged (see {@link #log(int, String, Exception)}).
+	 * 	No error is thrown and nothing else is done.
+	 * </p>
+	 * 
+	 * @param rs	{@link ResultSet} to close.
+	 */
+	protected final void close(final ResultSet rs){
+		try{
+			if (rs != null)
+				rs.close();
+		}catch(SQLException se){
+			log(1, "Can not close a ResultSet!", null);
+		}
+	}
+
+	/**
+	 * <p>Close silently the given {@link Statement}.</p>
+	 * 
+	 * <p>If the given {@link Statement} is NULL, nothing (even exception/error) happens.</p>
+	 * 
+	 * <p>
+	 * 	If any {@link SQLException} occurs during this operation, it is caught and just logged (see {@link #log(int, String, Exception)}).
+	 * 	No error is thrown and nothing else is done.
+	 * </p>
+	 * 
+	 * @param rs	{@link Statement} to close.
+	 */
+	protected final void close(final Statement stmt){
+		try{
+			if (stmt != null)
+				stmt.close();
+		}catch(SQLException se){
+			log(1, "Can not close a Statement!", null);
+		}
+	}
+
+	/**
+	 * <p>Transform the given column value in a boolean value.</p>
+	 * 
+	 * <p>The following cases are taken into account in function of the given value's type:</p>
+	 * <ul>
+	 * 	<li><b>NULL</b>: <i>false</i> is always returned.</li>
+	 * 
+	 * 	<li><b>{@link Boolean}</b>: the boolean value is returned as provided (but casted in boolean).</li>
+	 * 
+	 * 	<li><b>{@link Integer}</b>: <i>true</i> is returned only if the integer value is strictly greater than 0, otherwise <i>false</i> is returned.</li>
+	 * 
+	 * 	<li><b>Other</b>: toString().trim() is first called on this object. Then, an integer value is tried to be extracted from it.
+	 *                    If it succeeds, the previous rule is applied. If it fails, <i>true</i> will be returned only if the string is "t" or "true" (case insensitively).</li>
+	 * </ul>
+	 * 
+	 * @param colValue	The column value to transform in boolean.
+	 * 
+	 * @return	Its corresponding boolean value.
+	 */
+	protected final boolean toBoolean(final Object colValue){
+		// NULL => false:
+		if (colValue == null)
+			return false;
+
+		// Boolean value => cast in boolean and return this value:
+		else if (colValue instanceof Boolean)
+			return ((Boolean)colValue).booleanValue();
+
+		// Integer value => cast in integer and return true only if the value is positive and not null:
+		else if (colValue instanceof Integer){
+			int intFlag = ((Integer)colValue).intValue();
+			return (intFlag > 0);
+		}
+		// Otherwise => get the string representation and:
+		//     1/ try to cast it into an integer and apply the same test as before
+		//     2/ if the cast fails, return true only if the value is "t" or "true" (case insensitively):
+		else{
+			String strFlag = colValue.toString().trim();
+			try{
+				int intFlag = Integer.parseInt(strFlag);
+				return (intFlag > 0);
+			}catch(NumberFormatException nfe){
+				return strFlag.equalsIgnoreCase("t") || strFlag.equalsIgnoreCase("true");
+			}
+		}
+	}
+
+	/**
+	 * Return NULL if the given column value is an empty string (or it just contains space characters) or NULL.
+	 * Otherwise the given given is returned as provided.
+	 * 
+	 * @param dbValue	Value to nullify if needed.
+	 * 
+	 * @return	NULL if the given string is NULL or empty, otherwise the given value.
+	 */
+	protected final String nullifyIfNeeded(final String dbValue){
+		return (dbValue != null && dbValue.trim().length() <= 0) ? null : dbValue;
+	}
+
+	/**
+	 * Search a {@link TAPTable} instance whose the ADQL name matches (case sensitively) to the given one.
+	 * 
+	 * @param tableName	ADQL name of the table to search.
+	 * @param itTables	Iterator over the set of tables in which the research must be done.
+	 * 
+	 * @return	The found table, or NULL if not found.
+	 */
+	private TAPTable searchTable(String tableName, final Iterator<TAPTable> itTables){
+		// Get the schema name, if any prefix the given table name:
+		String schemaName = null;
+		int indSep = tableName.indexOf('.');
+		if (indSep > 0){
+			schemaName = tableName.substring(0, indSep);
+			tableName = tableName.substring(indSep + 1);
+		}
+
+		// Search by schema name (if any) and then by table name:
+		while(itTables.hasNext()){
+			// get the table:
+			TAPTable table = itTables.next();
+			// test the schema name (if one was prefixing the table name) (case sensitively):
+			if (schemaName != null){
+				if (table.getADQLSchemaName() == null || !schemaName.equals(table.getADQLSchemaName()))
+					continue;
+			}
+			// test the table name (case sensitively):
+			if (tableName.equals(table.getADQLName()))
+				return table;
+		}
+
+		// NULL if no table matches:
+		return null;
+	}
+
+	/**
+	 * <p>Tell whether the specified schema exists in the database.
+	 * 	To do so, it is using the given {@link DatabaseMetaData} object to query the database and list all existing schemas.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function is completely useless if the connection is not supporting schemas.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	Test on the schema name is done considering the case sensitivity indicated by the translator
+	 * 	(see {@link ADQLTranslator#isCaseSensitive(IdentifierField)}).
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	This functions is used by {@link #addUploadedTable(TAPTable, TableIterator)} and {@link #resetTAPSchema(Statement, TAPTable[])}.
+	 * </i></p>
+	 * 
+	 * @param schemaName	DB name of the schema whose the existence must be checked.
+	 * @param dbMeta		Metadata about the database, and mainly the list of all existing schemas.
+	 * 
+	 * @return	<i>true</i> if the specified schema exists, <i>false</i> otherwise.
+	 * 
+	 * @throws SQLException	If any error occurs while interrogating the database about existing schema.
+	 */
+	protected boolean isSchemaExisting(String schemaName, final DatabaseMetaData dbMeta) throws SQLException{
+		if (schemaName == null || schemaName.length() == 0)
+			return true;
+
+		// Determine the case sensitivity to use for the equality test:
+		boolean caseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA);
+
+		ResultSet rs = null;
+		try{
+			// List all schemas available and stop when a schema name matches ignoring the case:
+			rs = dbMeta.getSchemas();
+			boolean hasSchema = false;
+			while(!hasSchema && rs.next())
+				hasSchema = equals(rs.getString(1), schemaName, caseSensitive);
+			return hasSchema;
+		}finally{
+			close(rs);
+		}
+	}
+
+	/**
+	 * <p>Tell whether the specified table exists in the database.
+	 * 	To do so, it is using the given {@link DatabaseMetaData} object to query the database and list all existing tables.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	If schemas are not supported by this connection but a schema name is even though provided in parameter,
+	 * 	the table name will be prefixed by the schema name using {@link #getTablePrefix(String)}.
+	 * 	The research will then be done with NULL as schema name and this prefixed table name.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	Test on the schema name is done considering the case sensitivity indicated by the translator
+	 * 	(see {@link ADQLTranslator#isCaseSensitive(IdentifierField)}).
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function is used by {@link #addUploadedTable(TAPTable, TableIterator)} and {@link #dropUploadedTable(TAPTable)}.
+	 * </i></p>
+	 * 
+	 * @param schemaName	DB name of the schema in which the table to search is. <i>If NULL, the table is expected in any schema but ONLY one MUST exist.</i>
+	 * @param tableName		DB name of the table to search.
+	 * @param dbMeta		Metadata about the database, and mainly the list of all existing tables.
+	 * 
+	 * @return	<i>true</i> if the specified table exists, <i>false</i> otherwise.
+	 * 
+	 * @throws SQLException	If any error occurs while interrogating the database about existing tables.
+	 */
+	protected boolean isTableExisting(String schemaName, String tableName, final DatabaseMetaData dbMeta) throws DBException, SQLException{
+		if (tableName == null || tableName.length() == 0)
+			return true;
+
+		// Determine the case sensitivity to use for the equality test:
+		boolean schemaCaseSensitive = translator.isCaseSensitive(IdentifierField.SCHEMA);
+		boolean tableCaseSensitive = translator.isCaseSensitive(IdentifierField.TABLE);
+
+		ResultSet rs = null;
+		try{
+			// Prefix the table name by the schema name if needed (if schemas are not supported by this connection):
+			if (!supportsSchema)
+				tableName = getTablePrefix(schemaName) + tableName;
+
+			// List all matching tables:
+			if (supportsSchema){
+				String schemaPattern = schemaCaseSensitive ? schemaName : null;
+				String tablePattern = tableCaseSensitive ? tableName : null;
+				rs = dbMeta.getTables(null, schemaPattern, tablePattern, null);
+			}else{
+				String tablePattern = tableCaseSensitive ? tableName : null;
+				rs = dbMeta.getTables(null, null, tablePattern, null);
+			}
+
+			// Stop on the first table which match completely (schema name + table name in function of their respective case sensitivity):
+			int cnt = 0;
+			while(rs.next()){
+				String rsSchema = nullifyIfNeeded(rs.getString(2));
+				String rsTable = rs.getString(3);
+				if (!supportsSchema || schemaName == null || equals(rsSchema, schemaName, schemaCaseSensitive)){
+					if (equals(rsTable, tableName, tableCaseSensitive))
+						cnt++;
+				}
+			}
+
+			if (cnt > 1){
+				log(2, "More than one table match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + "))!", null);
+				throw new DBException("More than one table match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + "))!");
+			}
+
+			return cnt == 1;
+
+		}finally{
+			close(rs);
+		}
+	}
+
+	/**
+	 * <p>Build a table prefix with the given schema name.</p>
+	 * 
+	 * <p>By default, this function returns: schemaName + "_".</p>
+	 * 
+	 * <p><b>CAUTION:
+	 * 	This function is used only when schemas are not supported by the DBMS connection.
+	 * 	It aims to propose an alternative of the schema notion by prefixing the table name by the schema name.
+	 * </b></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given schema is NULL or is an empty string, an empty string will be returned.
+	 * 	Thus, no prefix will be set....which is very useful when the table name has already been prefixed
+	 * 	(in such case, the DB name of its schema has theoretically set to NULL).
+	 * </i></p>
+	 * 
+	 * @param schemaName	(DB) Schema name.
+	 * 
+	 * @return	The corresponding table prefix, or "" if the given schema name is an empty string or NULL.
+	 */
+	protected String getTablePrefix(final String schemaName){
+		if (schemaName != null && schemaName.trim().length() > 0)
+			return schemaName + "_";
+		else
+			return "";
+	}
+
+	/**
+	 * Tell whether the specified table (using its DB name only) is a standard one or not.
+	 * 
+	 * @param dbTableName	DB (unqualified) table name.
+	 * @param stdTables		List of all tables to consider as the standard ones.
+	 * @param caseSensitive	Indicate whether the equality test must be done case sensitively or not.
+	 * 
+	 * @return	The corresponding {@link STDTable} if the specified table is a standard one,
+	 *        	NULL otherwise.
+	 * 
+	 * @see TAPMetadata#resolveStdTable(String)
+	 */
+	protected final STDTable isStdTable(final String dbTableName, final TAPTable[] stdTables, final boolean caseSensitive){
+		if (dbTableName != null){
+			for(TAPTable t : stdTables){
+				if (equals(dbTableName, t.getDBName(), caseSensitive))
+					return TAPMetadata.resolveStdTable(t.getADQLName());
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * <p>"Execute" the query update. <i>This update must concern ONLY ONE ROW.</i></p>
+	 * 
+	 * <p>
+	 * 	Note that the "execute" action will be different in function of whether batch update queries are supported or not by this connection:
+	 * </p>
+	 * <ul>
+	 * 	<li>
+	 * 		If <b>batch update queries are supported</b>, just {@link PreparedStatement#addBatch()} will be called.
+	 * 		It means, the query will be appended in a list and will be executed only if
+	 * 		{@link #executeBatchUpdates(PreparedStatement, int)} is then called. 
+	 * 	</li>
+	 * 	<li>
+	 * 		If <b>they are NOT supported</b>, {@link PreparedStatement#executeUpdate()} will merely be called.
+	 * 	</li>
+	 * </ul>
+	 * 
+	 * <p>
+	 *	Before returning, and only if batch update queries are not supported, this function is ensuring that exactly one row has been updated.
+	 *	If it is not the case, a {@link DBException} is thrown.
+	 * </p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	If the function {@link PreparedStatement#addBatch()} fails by throwing an {@link SQLException}, batch updates
+	 * 	will be afterwards considered as not supported by this connection. Besides, if this row is the first one in a batch update (parameter indRow=1),
+	 * 	then, the error will just be logged and an {@link PreparedStatement#executeUpdate()} will be tried. However, if the row is not the first one,
+	 * 	the error will be logged but also thrown as a {@link DBException}. In both cases, a subsequent call to
+	 * 	{@link #executeBatchUpdates(PreparedStatement, int)} will have obviously no effect.
+	 * </i></p>
+	 * 
+	 * @param stmt		{@link PreparedStatement} in which the update query has been prepared.
+	 * @param indRow	Index of the row in the whole update process. It is used only for error management purpose.
+	 * 
+	 * @throws SQLException	If {@link PreparedStatement#executeUpdate()} fails.</i>
+	 * @throws DBException	If {@link PreparedStatement#addBatch()} fails and this update does not concern the first row, or if the number of updated rows is different from 1.
+	 */
+	protected final void executeUpdate(final PreparedStatement stmt, int indRow) throws SQLException, DBException{
+		// BATCH INSERTION: (the query is queued and will be executed later)
+		if (supportsBatchUpdates){
+			// Add the prepared query in the batch queue of the statement:
+			try{
+				stmt.addBatch();
+			}catch(SQLException se){
+				supportsBatchUpdates = false;
+				/*
+				 * If the error happens for the first row, it is still possible to insert all rows
+				 * with the non-batch function - executeUpdate().
+				 * 
+				 * Otherwise, it is impossible to insert the previous batched rows ; an exception must be thrown
+				 * and must stop the whole TAP_SCHEMA initialization.
+				 */
+				if (indRow == 1)
+					log(1, "BATCH query impossible => TRYING AGAIN IN A NORMAL EXECUTION (executeUpdate())!", se);
+				else{
+					log(2, "BATCH query impossible!", se);
+					throw new DBException("BATCH query impossible!", se);
+				}
+			}
+		}
+
+		// NORMAL INSERTION: (immediate insertion)
+		if (!supportsBatchUpdates){
+
+			// Insert the row prepared in the given statement:
+			int nbRowsWritten = stmt.executeUpdate();
+
+			// Check the row has been inserted with success:
+			if (nbRowsWritten != 1){
+				log(2, "ROW " + indRow + " not inserted!", null);
+				throw new DBException("ROW " + indRow + " not inserted!");
+			}
+		}
+	}
+
+	/**
+	 * <p>Execute all batched queries.</p>
+	 * 
+	 * <p>To do so, {@link PreparedStatement#executeBatch()} and then, if the first was successful, {@link PreparedStatement#clearBatch()} is called.</p>
+	 * 
+	 * <p>
+	 *	Before returning, this function is ensuring that exactly the given number of rows has been updated.
+	 *	If it is not the case, a {@link DBException} is thrown.
+	 * </p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function has no effect if batch queries are not supported.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	In case {@link PreparedStatement#executeBatch()} fails by throwing an {@link SQLException},
+	 * 	batch update queries will be afterwards considered as not supported by this connection.
+	 * </i></p>
+	 * 
+	 * @param stmt		{@link PreparedStatement} in which the update query has been prepared.
+	 * @param nbRows	Number of rows that should be updated.
+	 * 
+	 * @throws DBException	If {@link PreparedStatement#executeBatch()} fails, or if the number of updated rows is different from the given one.
+	 */
+	protected final void executeBatchUpdates(final PreparedStatement stmt, int nbRows) throws DBException{
+		if (supportsBatchUpdates){
+			// Execute all the batch queries:
+			int[] rows;
+			try{
+				rows = stmt.executeBatch();
+			}catch(SQLException se){
+				supportsBatchUpdates = false;
+				log(2, "BATCH execution impossible!", se);
+				throw new DBException("BATCH execution impossible!", se);
+			}
+
+			// Remove executed queries from the statement:
+			try{
+				stmt.clearBatch();
+			}catch(SQLException se){
+				log(1, "CLEAR BATCH impossible!", se);
+			}
+
+			// Count the updated rows:
+			int nbRowsUpdated = 0;
+			for(int i = 0; i < rows.length; i++)
+				nbRowsUpdated += rows[i];
+
+			// Check all given rows have been inserted with success:
+			if (nbRowsUpdated != nbRows){
+				log(2, "ROWS not all update (" + nbRows + " to update ; " + nbRowsUpdated + " updated)!", null);
+				throw new DBException("ROWS not all updated (" + nbRows + " to update ; " + nbRowsUpdated + " updated)!");
+			}
+		}
+	}
+
+	/**
+	 * Append all items of the iterator inside the given list.
+	 * 
+	 * @param lst	List to update.
+	 * @param it	All items to append inside the list.
+	 */
+	private < T > void appendAllInto(final List<T> lst, final Iterator<T> it){
+		while(it.hasNext())
+			lst.add(it.next());
+	}
+
+	/**
+	 * <p>Tell whether the given DB name is equals (case sensitively or not, in function of the given parameter)
+	 * 	to the given name coming from a {@link TAPMetadata} object.</p>
+	 * 
+	 * <p>If at least one of the given name is NULL, <i>false</i> is returned.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The comparison will be done in function of the specified case sensitivity BUT ALSO of the case supported and stored by the DBMS.
+	 * 	For instance, if it has been specified a case insensitivity and that mixed case is not supported by unquoted identifier,
+	 * 	the comparison must be done, surprisingly, by considering the case if unquoted identifiers are stored in lower or upper case.
+	 * 	Thus, this special way to evaluate equality should be as closed as possible to the identifier storage and research policies of the used DBMS.
+	 * </i></p> 
+	 * 
+	 * @param dbName		Name provided by the database.
+	 * @param metaName		Name provided by a {@link TAPMetadata} object.
+	 * @param caseSensitive	<i>true</i> if the equality test must be done case sensitively, <i>false</i> otherwise.
+	 * 
+	 * @return	<i>true</i> if both names are equal, <i>false</i> otherwise.
+	 */
+	protected final boolean equals(final String dbName, final String metaName, final boolean caseSensitive){
+		if (dbName == null || metaName == null)
+			return false;
+
+		if (caseSensitive){
+			if (supportsMixedCaseQuotedIdentifier || mixedCaseQuoted)
+				return dbName.equals(metaName);
+			else if (lowerCaseQuoted)
+				return dbName.equals(metaName.toLowerCase());
+			else if (upperCaseQuoted)
+				return dbName.equals(metaName.toUpperCase());
+			else
+				return dbName.equalsIgnoreCase(metaName);
+		}else{
+			if (supportsMixedCaseUnquotedIdentifier)
+				return dbName.equalsIgnoreCase(metaName);
+			else if (lowerCaseUnquoted)
+				return dbName.equals(metaName.toLowerCase());
+			else if (upperCaseUnquoted)
+				return dbName.equals(metaName.toUpperCase());
+			else
+				return dbName.equalsIgnoreCase(metaName);
 		}
 	}
 }
diff --git a/src/tap/db/JDBCTAPFactory.java b/src/tap/db/JDBCTAPFactory.java
deleted file mode 100644
index 8ef12bf..0000000
--- a/src/tap/db/JDBCTAPFactory.java
+++ /dev/null
@@ -1,228 +0,0 @@
-package tap.db;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import tap.metadata.TAPType;
-import tap.metadata.TAPType.TAPDatatype;
-
-public class JDBCTAPFactory {
-
-	public static enum DBMS{
-		POSTGRES;
-	}
-
-	public static interface DbmsTypeConverter< T, C > {
-		public C convert(final T typeToConvert);
-	}
-
-	public static Map<String,TAPDatatype> mapTypeAliases;
-	public static Map<String,DbmsTypeConverter<String,TAPType>> mapDbmsToTap;
-	public static Map<DBMS,Map<TAPDatatype,DbmsTypeConverter<TAPType,String>>> mapTapToDbms;
-
-	static{
-		/* DECLARE DBMS TYPE ALIASES */
-		mapTypeAliases = new HashMap<String,TAPType.TAPDatatype>();
-		mapTypeAliases.put("int8", TAPDatatype.BIGINT);
-		mapTypeAliases.put("bigserial", TAPDatatype.BIGINT);
-		mapTypeAliases.put("bit", TAPDatatype.VARCHAR);
-		mapTypeAliases.put("bit varying", TAPDatatype.VARCHAR);
-		mapTypeAliases.put("varbit", TAPDatatype.VARCHAR);
-		mapTypeAliases.put("boolean", TAPDatatype.SMALLINT);
-		mapTypeAliases.put("bytea", TAPDatatype.VARBINARY);
-		mapTypeAliases.put("character varying", TAPDatatype.VARCHAR);
-		mapTypeAliases.put("character", TAPDatatype.CHAR);
-		mapTypeAliases.put("double precision", TAPDatatype.DOUBLE);
-		mapTypeAliases.put("float8", TAPDatatype.DOUBLE);
-		mapTypeAliases.put("integer", TAPDatatype.INTEGER);
-		mapTypeAliases.put("int4", TAPDatatype.INTEGER);
-		mapTypeAliases.put("float4", TAPDatatype.REAL);
-		mapTypeAliases.put("int2", TAPDatatype.SMALLINT);
-		mapTypeAliases.put("serial", TAPDatatype.INTEGER);
-		mapTypeAliases.put("serial4", TAPDatatype.INTEGER);
-		mapTypeAliases.put("text", TAPDatatype.VARCHAR);
-
-		/* DECLARE SPECIAL DBMS->TAP CONVERSIONS */
-		mapDbmsToTap = new HashMap<String,JDBCTAPFactory.DbmsTypeConverter<String,TAPType>>();
-		mapDbmsToTap.put("numeric", new DbmsTypeConverter<String,TAPType>(){
-			@Override
-			public TAPType convert(String typeToConvert){
-				return new TAPType(TAPDatatype.DOUBLE);
-			}
-		});
-		mapDbmsToTap.put("decimal", new DbmsTypeConverter<String,TAPType>(){
-			@Override
-			public TAPType convert(String typeToConvert){
-				return new TAPType(TAPDatatype.DOUBLE);
-			}
-		});
-
-		/* DECLARE SPECIAL TAP->DBMS CONVERSIONS */
-		mapTapToDbms = new HashMap<DBMS,Map<TAPDatatype,DbmsTypeConverter<TAPType,String>>>();
-		// POSTGRES
-		HashMap<TAPDatatype,DbmsTypeConverter<TAPType,String>> postgresConverters = new HashMap<TAPDatatype,JDBCTAPFactory.DbmsTypeConverter<TAPType,String>>();
-		postgresConverters.put(TAPDatatype.DOUBLE, new DbmsTypeConverter<TAPType,String>(){
-			@Override
-			public String convert(TAPType typeToConvert){
-				return "double precision";
-			}
-		});
-		DbmsTypeConverter<TAPType,String> binaryConverter = new DbmsTypeConverter<TAPType,String>(){
-			@Override
-			public String convert(TAPType typeToConvert){
-				return "bytea";
-			}
-		};
-		postgresConverters.put(TAPDatatype.VARBINARY, binaryConverter);
-		postgresConverters.put(TAPDatatype.BINARY, binaryConverter);
-		postgresConverters.put(TAPDatatype.BLOB, binaryConverter);
-		postgresConverters.put(TAPDatatype.CLOB, binaryConverter);
-		mapTapToDbms.put(DBMS.POSTGRES, postgresConverters);
-	}
-
-	public JDBCTAPFactory(){
-		// TODO Auto-generated constructor stub
-	}
-
-	/**
-	 * <p>Convert the given TAP column type into a column type compatible with the specified DBMS.</p>
-	 * 
-	 * <p><i>Note 1: if no {@link TAPType} is provided, the returned DBMS type will correspond to a
-	 * VARCHAR.</i></p>
-	 * 
-	 * <p><i>Note 2: if no DBMS is specified or if the conversion has failed, the given TAP type will be
-	 * just "stringified" (by calling {@link TAPType#toString()})</i></p>
-	 * 
-	 * @param tapType	A TAP column type.
-	 * @param dbms		DBMS target in which the given TAP column type must be converted.
-	 * 
-	 * @return	The corresponding DBMS column type.
-	 */
-	public static String toDbmsType(TAPType tapType, final DBMS dbms){
-		// If no TAP type is specified, consider it by default as a VARCHAR type:
-		if (tapType == null)
-			tapType = new TAPType(TAPDatatype.VARCHAR);
-
-		// By default, just "stringify" the given TAP type:
-		String dbmsType = tapType.toString();
-
-		// If some converters are defined for the specified DBMS...
-		if (dbms != null && mapTapToDbms.containsKey(dbms)){
-			Map<TAPDatatype,DbmsTypeConverter<TAPType,String>> dbmsMap = mapTapToDbms.get(dbms);
-			// ...and if a converter exists for the given TAP datatype...
-			DbmsTypeConverter<TAPType,String> converter = dbmsMap.get(tapType.type);
-			if (converter != null){
-				// ...convert the given TAP type:
-				String conversion = converter.convert(tapType);
-				// ...and set the DBMS conversion if NOT NULL:
-				if (conversion != null)
-					dbmsType = conversion;
-			}
-		}
-
-		return dbmsType;
-	}
-
-	/**
-	 * <p>Convert the given DBMS column type into a compatible TAP datatype.</p>
-	 * 
-	 * <p><i>Note: If no DBMS type is specified or if the DBMS type can not be identified,
-	 * it will be converted as a VARCHAR.</i></p>
-	 * 
-	 * @param dbmsType	DBMS column datatype.
-	 * 
-	 * @return	The corresponding TAP column datatype.
-	 */
-	public static TAPType toTAPType(final String dbmsType){
-		// If no type is provided return VARCHAR:
-		if (dbmsType == null || dbmsType.trim().length() == 0)
-			return new TAPType(TAPDatatype.VARCHAR);
-
-		// Extract the type prefix and lower-case it:
-		int paramIndex = dbmsType.indexOf('(');
-		String dbmsTypePrefix = (paramIndex <= 0) ? dbmsType : dbmsType.substring(0, paramIndex);
-		dbmsTypePrefix = dbmsTypePrefix.toLowerCase();
-
-		// Use this type prefix as key to determine if it's a DBMS type alias and get its corresponding TAP datatype:
-		TAPDatatype datatype = mapTypeAliases.get(dbmsTypePrefix);
-
-		// If it's an alias, build the corresponding TAP type:
-		if (datatype != null)
-			return new TAPType(datatype, getLengthParam(dbmsType, paramIndex));
-
-		// If it's not an alias, use the type prefix as key to get a corresponding converter:
-		DbmsTypeConverter<String,TAPType> converter = mapDbmsToTap.get(dbmsTypePrefix);
-
-		// Try the type conversion using this converter:
-		TAPType taptype = null;
-		if (converter != null)
-			taptype = converter.convert(dbmsType);
-
-		/* 
-		 * If no converter was found OR if the type conversion has failed,
-		 * consider the given type as equivalent to a declared TAP type.
-		 * 
-		 * /!\ But if no equivalent exists, the given type will be ignore and
-		 *     VARCHAR will be returned!
-		 */
-		if (taptype == null){
-			try{
-
-				// Try to find an equivalent TAPType:
-				datatype = TAPDatatype.valueOf(dbmsTypePrefix.toUpperCase());
-
-				// If there is one return directly the TAPType:
-				taptype = new TAPType(datatype, getLengthParam(dbmsType, paramIndex));
-
-			}catch(IllegalArgumentException iae){
-				// If none exists, return VARCHAR:
-				taptype = new TAPType(TAPDatatype.VARCHAR, TAPType.NO_LENGTH);
-			}
-		}
-
-		return taptype;
-	}
-
-	/**
-	 * <p>Extract the 'length' parameter of a DBMS type string.</p>
-	 * 
-	 * <p>
-	 * 	If the given type string does not contain any parameter
-	 * 	OR if the first parameter can not be casted into an integer,
-	 * 	{@link TAPType#NO_LENGTH} will be returned.
-	 * </p>
-	 * 
-	 * @param dbmsType		DBMS type string (containing the datatype and the 'length' parameter).
-	 * @param paramIndex	Index of the open bracket.
-	 * 
-	 * @return	The 'length' parameter value if found, {@link TAPType#NO_LENGTH} otherwise.
-	 */
-	private static int getLengthParam(final String dbmsType, final int paramIndex){
-		// If no parameter has been previously detected, no length parameter:
-		if (paramIndex <= 0)
-			return TAPType.NO_LENGTH;
-
-		// If there is one and that at least ONE parameter is provided....
-		else{
-			int lengthParam = TAPType.NO_LENGTH;
-			String paramsStr = dbmsType.substring(paramIndex + 1);
-
-			// ...extract the 'length' parameter:
-			/* note: we suppose here that no other parameter is possible ;
-			 *       but if there are, they are ignored and we try to consider the first parameter
-			 *       as the length */
-			int paramEndIndex = paramsStr.indexOf(',');
-			if (paramEndIndex <= 0)
-				paramEndIndex = paramsStr.indexOf(')');
-
-			// ...cast it into an integer:
-			try{
-				lengthParam = Integer.parseInt(paramsStr.substring(0, paramEndIndex));
-			}catch(Exception ex){}
-
-			// ...and finally return it:
-			return lengthParam;
-		}
-	}
-
-}
diff --git a/src/tap/log/TAPLog.java b/src/tap/log/TAPLog.java
index 6d15c8f..33e6a62 100644
--- a/src/tap/log/TAPLog.java
+++ b/src/tap/log/TAPLog.java
@@ -43,6 +43,8 @@ public interface TAPLog extends UWSLog {
 
 	public void tapMetadataLoaded(final TAPMetadata metadata);
 
+	public void connectionOpened(final DBConnection connection);
+
 	public void connectionClosed(final DBConnection connection);
 
 	public void sqlQueryExecuting(final DBConnection connection, final String sql);
diff --git a/src/tap/metadata/TAPColumn.java b/src/tap/metadata/TAPColumn.java
index 9b91e85..4c2bbb5 100644
--- a/src/tap/metadata/TAPColumn.java
+++ b/src/tap/metadata/TAPColumn.java
@@ -16,46 +16,136 @@ package tap.metadata;
  * You should have received a copy of the GNU Lesser General Public License
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
- * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
+ * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
+import java.awt.List;
 import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.Map;
 
 import tap.metadata.TAPType.TAPDatatype;
 import adql.db.DBColumn;
 import adql.db.DBTable;
 
+/**
+ * <p>Represent a column as described by the IVOA standard in the TAP protocol definition.</p>
+ * 
+ * <p>
+ * 	This object representation has exactly the same fields as the column of the table TAP_SCHEMA.columns.
+ * 	But it also provides a way to add other data. For instance, if information not listed in the standard
+ * 	may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be
+ * 	a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc...
+ * </p>
+ * 
+ * <p><i><b>Important note:</b>
+ * 	A {@link TAPColumn} object MUST always have a DB name. That's why by default, at the creation
+ * 	the DB name is the ADQL name. Once created, it is possible to set the DB name with {@link #setDBName(String)}.
+ * 	This DB name MUST be UNqualified and without double quotes. If a NULL or empty value is provided,
+ * 	nothing is done and the object keeps its former DB name.
+ * </i></p>
+ * 
+ * <h3>Set a table</h3>
+ * 
+ * <p>
+ *	By default a column is detached (not part of a table). To specify the table in which this column is,
+ *	you must use {@link TAPTable#addColumn(TAPColumn)}. By doing this, the table link inside this column
+ *	will be set automatically and you will be able to get the table with {@link #getTable()}. 
+ * </p>
+ * 
+ * <h3>Foreign keys</h3>
+ * 
+ * <p>
+ * 	In case this column is linked to one or several of other tables, it will be possible to list all
+ * 	foreign keys where the target columns is with {@link #getTargets()}. In the same way, it will be
+ * 	possible to list all foreign keys in which this column is a target with {@link #getSources()}.
+ * 	However, in order to ensure the consistency between all metadata, these foreign key's links are
+ * 	set at the table level by the table itself using {@link #addSource(TAPForeignKey)} and
+ * 	{@link #addTarget(TAPForeignKey)}.
+ * </p>
+ * 
+ * @author Gr&eacute;gory Mantelet (CDS;ARI)
+ * @version 2.0 (08/2014)
+ */
 public class TAPColumn implements DBColumn {
 
+	/** Name that this column MUST have in ADQL queries. */
 	private final String adqlName;
 
+	/** Name that this column have in the database.
+	 * <i>Note: It CAN NOT be NULL. By default, it is the ADQL name.</i> */
 	private String dbName = null;
 
+	/** Table which owns this column.
+	 * <i>Note: It should be NULL only at the construction or for a quick representation of a column.
+	 * 	Then, this attribute is automatically set by a {@link TAPTable} when adding this column inside it
+	 * 	with {@link TAPTable#addColumn(TAPColumn)}.</i> */
 	private DBTable table = null;
 
+	/** Description of this column.
+	 * <i>Note: Standard TAP column field ; MAY be NULL.</i> */
 	private String description = null;
 
+	/** Unit of this column's values.
+	 * <i>Note: Standard TAP column field ; MAY be NULL.</i> */
 	private String unit = null;
 
+	/** UCD describing the scientific content of this column.
+	 * <i>Note: Standard TAP column field ; MAY be NULL.</i> */
 	private String ucd = null;
 
+	/** UType associating this column with a data-model.
+	 * <i>Note: Standard TAP column field ; MAY be NULL.</i> */
 	private String utype = null;
 
+	/** Type of this column.
+	 * <i>Note: Standard TAP column field ; CAN'T be NULL.</i> */
 	private TAPType datatype = new TAPType(TAPDatatype.VARCHAR);
 
+	/** Flag indicating whether this column is one of those that should be returned by default.
+	 * <i>Note: Standard TAP column field ; FALSE by default.</i> */
 	private boolean principal = false;
 
+	/** Flag indicating whether this column is indexed in the database.
+	 * <i>Note: Standard TAP column field ; FALSE by default.</i> */
 	private boolean indexed = false;
 
+	/** Flag indicating whether this column is defined by a standard.
+	 * <i>Note: Standard TAP column field ; FALSE by default.</i> */
 	private boolean std = false;
 
+	/** Let add some information in addition of the ones of the TAP protocol.
+	 * <i>Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ...
+	 * Its content is totally free and never used or checked.</i> */
 	protected Object otherData = null;
 
+	/** List all foreign keys in which this column is a source.
+	 * <p><b>CAUTION: For consistency consideration, this attribute SHOULD never be modified!
+	 * 	It is set by the constructor and filled ONLY by the table.</b></p> */
 	protected final ArrayList<TAPForeignKey> lstTargets;
 
+	/** List all foreign keys in which this column is a target.
+	 * <p><b>CAUTION: For consistency consideration, this attribute SHOULD never be modified!
+	 * 	It is set by the constructor and filled ONLY by the table.</b></p> */
 	protected final ArrayList<TAPForeignKey> lstSources;
 
+	/**
+	 * <p>Build a {@link TAPColumn} instance with the given ADQL name.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * 	The datatype is set by default to VARCHAR.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name),
+	 * 	this prefix will be removed. Only the part after the '.' character will be kept.
+	 * </i></p>
+	 * 
+	 * @param columnName	Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 */
 	public TAPColumn(String columnName){
 		if (columnName == null || columnName.trim().length() == 0)
 			throw new NullPointerException("Missing column name !");
@@ -66,21 +156,114 @@ public class TAPColumn implements DBColumn {
 		lstSources = new ArrayList<TAPForeignKey>(1);
 	}
 
+	/**
+	 * <p>Build a {@link TAPColumn} instance with the given ADQL name and datatype.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name),
+	 * 	this prefix will be removed. Only the part after the '.' character will be kept.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 *	The datatype is set by calling the function {@link #setDatatype(TAPType)} which does not do
+	 *	anything if the given datatype is NULL.
+	 * </i></p>
+	 * 
+	 * @param columnName	Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 * @param type			Datatype of this column. <i>If NULL, VARCHAR will be the datatype of this column</i>
+	 * 
+	 * @see #setDatatype(TAPType)
+	 */
 	public TAPColumn(String columnName, TAPType type){
 		this(columnName);
 		setDatatype(type);
 	}
 
+	/**
+	 * <p>Build a {@link TAPColumn} instance with the given ADQL name, datatype and description.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name),
+	 * 	this prefix will be removed. Only the part after the '.' character will be kept.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 *	The datatype is set by calling the function {@link #setDatatype(TAPType)} which does do
+	 *	anything if the given datatype is NULL.
+	 * </i></p>
+	 * 
+	 * @param columnName	Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 * @param type			Datatype of this column. <i>If NULL, VARCHAR will be the datatype of this column</i>
+	 * @param description	Description of the column's content. <i>May be NULL</i>
+	 */
 	public TAPColumn(String columnName, TAPType type, String description){
 		this(columnName, type);
 		this.description = description;
 	}
 
+	/**
+	 * <p>Build a {@link TAPColumn} instance with the given field.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name),
+	 * 	this prefix will be removed. Only the part after the '.' character will be kept.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 *	The datatype is set by calling the function {@link #setDatatype(TAPType)} which does do
+	 *	anything if the given datatype is NULL.
+	 * </i></p>
+	 * 
+	 * @param columnName	Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 * @param type			Datatype of this column. <i>If NULL, VARCHAR will be the datatype of this column</i>
+	 * @param description	Description of the column's content. <i>May be NULL</i>
+	 * @param unit			Unit of the column's values. <i>May be NULL</i>
+	 */
 	public TAPColumn(String columnName, TAPType type, String description, String unit){
 		this(columnName, type, description);
 		this.unit = unit;
 	}
 
+	/**
+	 * <p>Build a {@link TAPColumn} instance with the given field.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given ADQL name is prefixed (= it has some text separated by a '.' before the column name),
+	 * 	this prefix will be removed. Only the part after the '.' character will be kept.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 *	The datatype is set by calling the function {@link #setDatatype(TAPType)} which does do
+	 *	anything if the given datatype is NULL.
+	 * </i></p>
+	 * 
+	 * @param columnName	Name that this column MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 * @param type			Datatype of this column. <i>If NULL, VARCHAR will be the datatype of this column</i>
+	 * @param description	Description of the column's content. <i>May be NULL</i>
+	 * @param unit			Unit of the column's values. <i>May be NULL</i>
+	 * @param ucd			UCD describing the scientific content of this column.
+	 * @param utype			UType associating this column with a data-model.
+	 */
 	public TAPColumn(String columnName, TAPType type, String description, String unit, String ucd, String utype){
 		this(columnName, type, description, unit);
 		this.ucd = ucd;
@@ -88,8 +271,13 @@ public class TAPColumn implements DBColumn {
 	}
 
 	/**
-	 * @return The name.
+	 * Get the ADQL name (the name this column MUST have in ADQL queries).
+	 * 
+	 * @return	Its ADQL name.
+	 * @see #getADQLName()
+	 * @deprecated	Does not do anything special: just call {@link #getADQLName()}.
 	 */
+	@Deprecated
 	public final String getName(){
 		return getADQLName();
 	}
@@ -104,188 +292,372 @@ public class TAPColumn implements DBColumn {
 		return dbName;
 	}
 
+	/**
+	 * <p>Change the name that this column MUST have in the database (i.e. in SQL queries).</p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given value is NULL or an empty string, nothing is done ; the DB name keeps is former value.
+	 * </i></p>
+	 * 
+	 * @param name	The new database name of this column.
+	 */
 	public final void setDBName(String name){
 		name = (name != null) ? name.trim() : name;
-		dbName = (name == null || name.length() == 0) ? adqlName : name;
+		if (name != null && name.length() > 0)
+			dbName = name;
 	}
 
-	/**
-	 * @return The table.
-	 */
 	@Override
 	public final DBTable getTable(){
 		return table;
 	}
 
 	/**
-	 * @param table The table to set.
+	 * <p>Set the table in which this column is.</p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	For consistency reasons, this function SHOULD be called only by the {@link TAPTable}
+	 * 	that owns this column.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	If this column was already linked with another {@link TAPTable} object, the previous link is removed
+	 * 	here, but also in the table (by calling {@link TAPTable#removeColumn(String)}).
+	 * </i></p>
+	 * 
+	 * @param table	The table that owns this column.
 	 */
-	public final void setTable(DBTable table){
+	protected final void setTable(final DBTable table){
+		if (this.table != null && this.table instanceof TAPTable && (table == null || !table.equals(this.table)))
+			((TAPTable)this.table).removeColumn(adqlName);
 		this.table = table;
 	}
 
 	/**
-	 * @return The description.
+	 * Get the description of this column.
+	 * 
+	 * @return	Its description. <i>MAY be NULL</i>
 	 */
 	public final String getDescription(){
 		return description;
 	}
 
 	/**
-	 * @param description The description to set.
+	 * Set the description of this column.
+	 * 
+	 * @param description	Its new description. <i>MAY be NULL</i>
 	 */
 	public final void setDescription(String description){
 		this.description = description;
 	}
 
 	/**
-	 * @return The unit.
+	 * Get the unit of the column's values.
+	 * 
+	 * @return	Its unit. <i>MAY be NULL</i>
 	 */
 	public final String getUnit(){
 		return unit;
 	}
 
 	/**
-	 * @param unit The unit to set.
+	 * Set the unit of the column's values.
+	 * 
+	 * @param unit	Its new unit. <i>MAY be NULL</i>
 	 */
 	public final void setUnit(String unit){
 		this.unit = unit;
 	}
 
 	/**
-	 * @return The ucd.
+	 * Get the UCD describing the scientific content of this column.
+	 * 
+	 * @return	Its UCD. <i>MAY be NULL</i>
 	 */
 	public final String getUcd(){
 		return ucd;
 	}
 
 	/**
-	 * @param ucd The ucd to set.
+	 * Set the UCD describing the scientific content of this column.
+	 * 
+	 * @param ucd	Its new UCD. <i>MAY be NULL</i>
 	 */
 	public final void setUcd(String ucd){
 		this.ucd = ucd;
 	}
 
 	/**
-	 * @return The utype.
+	 * Get the UType associating this column with a data-model.
+	 * 
+	 * @return	Its UType. <i>MAY be NULL</i>
 	 */
 	public final String getUtype(){
 		return utype;
 	}
 
 	/**
-	 * @param utype The utype to set.
+	 * Set the UType associating this column with a data-model.
+	 * 
+	 * @param utype	Its new UType. <i>MAY be NULL</i>
 	 */
 	public final void setUtype(String utype){
 		this.utype = utype;
 	}
 
 	/**
-	 * @return The datatype.
+	 * Get the type of the column's values.
+	 * 
+	 * @return	Its datatype. <i>CAN'T be NULL</i>
 	 */
 	public final TAPType getDatatype(){
 		return datatype;
 	}
 
 	/**
-	 * @param type	The new column datatype.
+	 * <p>Set the type of the column's values.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The datatype won't be changed, if the given type is NULL.
+	 * </i></p>
+	 * 
+	 * @param type	Its new datatype.
 	 */
 	public final void setDatatype(final TAPType type){
-		datatype = type;
+		if (type != null)
+			datatype = type;
 	}
 
 	/**
-	 * @return The principal.
+	 * Tell whether this column is one of those returned by default.
+	 * 
+	 * @return	<i>true</i> if this column should be returned by default, <i>false</i> otherwise.
 	 */
 	public final boolean isPrincipal(){
 		return principal;
 	}
 
 	/**
-	 * @param principal The principal to set.
+	 * Set whether this column should be one of those returned by default.
+	 * 
+	 * @param  principal	<i>true</i> if this column should be returned by default, <i>false</i> otherwise.
 	 */
 	public final void setPrincipal(boolean principal){
 		this.principal = principal;
 	}
 
 	/**
-	 * @return The indexed.
+	 * Tell whether this column is indexed.
+	 * 
+	 * @return	<i>true</i> if this column is indexed, <i>false</i> otherwise.
 	 */
 	public final boolean isIndexed(){
 		return indexed;
 	}
 
 	/**
-	 * @param indexed The indexed to set.
+	 * Set whether this column is indexed or not.
+	 * 
+	 * @param  indexed	<i>true</i> if this column is indexed, <i>false</i> otherwise.
 	 */
 	public final void setIndexed(boolean indexed){
 		this.indexed = indexed;
 	}
 
 	/**
-	 * @return The std.
+	 * Tell whether this column is defined by a standard.
+	 * 
+	 * @return	<i>true</i> if this column is defined by a standard, <i>false</i> otherwise.
 	 */
 	public final boolean isStd(){
 		return std;
 	}
 
 	/**
-	 * @param std The std to set.
+	 * Set whether this column is defined by a standard.
+	 * 
+	 * @param  std	<i>true</i> if this column is defined by a standard, <i>false</i> otherwise.
 	 */
 	public final void setStd(boolean std){
 		this.std = std;
 	}
 
+	/**
+	 * <p>Get the other (piece of) information associated with this column.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	By default, NULL is returned, but it may be any kind of value ({@link Integer},
+	 * 	{@link String}, {@link Map}, {@link List}, ...).
+	 * </i></p>
+	 * 
+	 * @return	The other (piece of) information. <i>MAY be NULL</i>
+	 */
 	public Object getOtherData(){
 		return otherData;
 	}
 
+	/**
+	 * Set the other (piece of) information associated with this column.
+	 * 
+	 * @param data	Another information about this column. <i>MAY be NULL</i>
+	 */
 	public void setOtherData(Object data){
 		otherData = data;
 	}
 
+	/**
+	 * <p>Let add a foreign key in which this column is a source (= which is targeting another column).</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Nothing is done if the given value is NULL.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	For consistency reasons, this function SHOULD be called only by the {@link TAPTable}
+	 * 	that owns this column or that is part of the foreign key. 
+	 * </i></p>
+	 * 
+	 * @param key	A foreign key.
+	 */
 	protected void addTarget(TAPForeignKey key){
 		if (key != null)
 			lstTargets.add(key);
 	}
 
-	protected int getNbTargets(){
+	/**
+	 * Get the number of times this column is targeting another column.
+	 * 
+	 * @return	How many this column is source in a foreign key.
+	 */
+	public int getNbTargets(){
 		return lstTargets.size();
 	}
 
-	protected Iterator<TAPForeignKey> getTargets(){
+	/**
+	 * Get the list of foreign keys in which this column is a source (= is targeting another column).
+	 * 
+	 * @return	List of foreign keys in which this column is a source.
+	 */
+	public Iterator<TAPForeignKey> getTargets(){
 		return lstTargets.iterator();
 	}
 
+	/**
+	 * <p>Remove the fact that this column is a source (= is targeting another column)
+	 * in the given foreign key.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Nothing is done if the given value is NULL.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	For consistency reasons, this function SHOULD be called only by the {@link TAPTable}
+	 * 	that owns this column or that is part of the foreign key. 
+	 * </i></p>
+	 * 
+	 * @param key	Foreign key in which this column was targeting another column.
+	 */
 	protected void removeTarget(TAPForeignKey key){
-		lstTargets.remove(key);
+		if (key != null)
+			lstTargets.remove(key);
 	}
 
+	/**
+	 * <p>Remove the fact that this column is a source (= is targeting another column)
+	 * in any foreign key in which it was.</p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	For consistency reasons, this function SHOULD be called only by the {@link TAPTable}
+	 * 	that owns this column or that is part of the foreign key. 
+	 * </i></p>
+	 * 
+	 * @param key	Foreign key in which this column was targeting another column.
+	 */
 	protected void removeAllTargets(){
 		lstTargets.clear();
 	}
 
+	/**
+	 * <p>Let add a foreign key in which this column is a target (= which is targeted by another column).</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Nothing is done if the given value is NULL.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	For consistency reasons, this function SHOULD be called only by the {@link TAPTable}
+	 * 	that owns this column or that is part of the foreign key. 
+	 * </i></p>
+	 * 
+	 * @param key	A foreign key.
+	 */
 	protected void addSource(TAPForeignKey key){
 		if (key != null)
 			lstSources.add(key);
 	}
 
-	protected int getNbSources(){
+	/**
+	 * Get the number of times this column is targeted by another column.
+	 * 
+	 * @return	How many this column is target in a foreign key.
+	 */
+	public int getNbSources(){
 		return lstSources.size();
 	}
 
-	protected Iterator<TAPForeignKey> getSources(){
+	/**
+	 * Get the list of foreign keys in which this column is a target (= is targeted another column).
+	 * 
+	 * @return	List of foreign keys in which this column is a target.
+	 */
+	public Iterator<TAPForeignKey> getSources(){
 		return lstSources.iterator();
 	}
 
+	/**
+	 * <p>Remove the fact that this column is a target (= is targeted by another column)
+	 * in the given foreign key.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	Nothing is done if the given value is NULL.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	For consistency reasons, this function SHOULD be called only by the {@link TAPTable}
+	 * 	that owns this column or that is part of the foreign key. 
+	 * </i></p>
+	 * 
+	 * @param key	Foreign key in which this column was targeted by another column.
+	 */
 	protected void removeSource(TAPForeignKey key){
 		lstSources.remove(key);
 	}
 
+	/**
+	 * <p>Remove the fact that this column is a target (= is targeted by another column)
+	 * in any foreign key in which it was.</p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	For consistency reasons, this function SHOULD be called only by the {@link TAPTable}
+	 * 	that owns this column or that is part of the foreign key. 
+	 * </i></p>
+	 * 
+	 * @param key	Foreign key in which this column was targeted by another column.
+	 */
 	protected void removeAllSources(){
 		lstSources.clear();
 	}
 
+	/**
+	 * <p><i><b>Warning:</b>
+	 * 	Since the type of the other data is not known, the copy of its value
+	 * 	can not be done properly. So, this column and its copy will share the same other data object.
+	 * 	If it is also needed to make a deep copy of this other data object, this function MUST be
+	 * 	overridden.
+	 * </i></b>
+	 * 
+	 * @see adql.db.DBColumn#copy(java.lang.String, java.lang.String, adql.db.DBTable)
+	 */
 	@Override
 	public DBColumn copy(final String dbName, final String adqlName, final DBTable dbTable){
 		TAPColumn copy = new TAPColumn((adqlName == null) ? this.adqlName : adqlName, datatype, description, unit, ucd, utype);
@@ -300,6 +672,18 @@ public class TAPColumn implements DBColumn {
 		return copy;
 	}
 
+	/**
+	 * <p>Provide a deep copy (included the other data) of this column.</p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	Since the type of the other data is not known, the copy of its value
+	 * 	can not be done properly. So, this column and its copy will share the same other data object.
+	 * 	If it is also needed to make a deep copy of this other data object, this function MUST be
+	 * 	overridden.
+	 * </i></b>
+	 * 
+	 * @return	The deep copy of this column.
+	 */
 	public DBColumn copy(){
 		TAPColumn copy = new TAPColumn(adqlName, datatype, description, unit, ucd, utype);
 		copy.setDBName(dbName);
@@ -317,7 +701,7 @@ public class TAPColumn implements DBColumn {
 			return false;
 
 		TAPColumn col = (TAPColumn)obj;
-		return col.getTable().equals(table) && col.getName().equals(adqlName);
+		return col.getTable().equals(table) && col.getADQLName().equals(adqlName);
 	}
 
 	@Override
diff --git a/src/tap/metadata/TAPDM.java b/src/tap/metadata/TAPDM.java
deleted file mode 100644
index 5a0548f..0000000
--- a/src/tap/metadata/TAPDM.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package tap.metadata;
-
-/*
- * This file is part of TAPLibrary.
- * 
- * TAPLibrary 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.
- * 
- * TAPLibrary 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 TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
- * 
- * Copyright 2014 - Astronomisches Rechen Institute (ARI)
- */
-
-/**
- * Enumeration of all schemas and tables of the TAP datamodel (and particularly of TAP_SCHEMA).
- * 
- * @author Gr&eacute;gory Mantelet (ARI)
- * @version 2.0 (07/2014)
- * @since 2.0
- */
-public enum TAPDM{
-	TAPSCHEMA("TAP_SCHEMA"), SCHEMAS("schemas"), TABLES("tables"), COLUMNS("columns"), FOREIGN_KEYS("foreign_keys"), UPLOADSCHEMA("TAP_UPLOAD");
-
-	/** Real name of the schema/table. */
-	private final String label;
-
-	private TAPDM(final String name){
-		this.label = name;
-	}
-
-	/**
-	 * Get the real name of the schema/table of the TAP datamodel.
-	 * 
-	 * @return	Real name of the schema/table.
-	 */
-	public String getLabel(){
-		return label;
-	}
-
-	@Override
-	public String toString(){
-		return label;
-	}
-}
diff --git a/src/tap/metadata/TAPMetadata.java b/src/tap/metadata/TAPMetadata.java
index fde3103..3f1c38f 100644
--- a/src/tap/metadata/TAPMetadata.java
+++ b/src/tap/metadata/TAPMetadata.java
@@ -16,7 +16,8 @@ package tap.metadata;
  * You should have received a copy of the GNU Lesser General Public License
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
- * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
+ * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
 import java.io.IOException;
@@ -32,29 +33,106 @@ import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
+import tap.metadata.TAPTable.TableType;
+import tap.metadata.TAPType.TAPDatatype;
 import tap.resource.Capabilities;
 import tap.resource.TAPResource;
 import tap.resource.VOSIResource;
 import adql.db.DBTable;
 
+/**
+ * <p>Let listing all schemas, tables and columns available in a TAP service.
+ * This list also corresponds to the TAP resource "/tables".</p>
+ * 
+ * <p>
+ * 	Only schemas are stored in this object. So that's why only schemas can be added and removed
+ * 	from this class. However, {@link TAPSchema} objects are listing tables, whose the object
+ * 	representation is listing columns. So to add tables, you must first embed them in a schema.
+ * </p>
+ * 
+ * <p>
+ * 	All metadata have two names: one to use in ADQL queries and the other to use when really querying
+ * 	the database. This is very useful to hide the real complexity of the database and propose
+ * 	a simpler view of the query-able data. It is particularly useful if a schema does not exist in the
+ * 	database but has been added in the TAP schema for more logical separation on the user point of view.
+ * 	In a such case, the schema would have an ADQL name but no DB name (NULL value ; which is possible only
+ * 	with {@link TAPSchema} objects).
+ * </p>
+ * 
+ * @author Gr&eacute;gory Mantelet (CDS;ARI)
+ * @version 2.0 (08/2014)
+ */
 public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResource {
 
+	/** Resource name of the TAP metadata. This name is also used - in this class - in the TAP URL to identify this resource.
+	 * Here it corresponds to the following URI: ".../tables". */
 	public static final String RESOURCE_NAME = "tables";
 
+	/** List of all schemas available through the TAP service. */
 	protected final Map<String,TAPSchema> schemas;
+
+	/** Part of the TAP URI which identify this TAP resource.
+	 * By default, it is the resource name ; so here, the corresponding TAP URI would be: "/tables". */
 	protected String accessURL = getName();
 
+	/**
+	 * <p>Build an empty list of metadata.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	By default, a TAP service must have at least a TAP_SCHEMA schema which contains a set of 5 tables
+	 * 	(schemas, tables, columns, keys and key_columns). This schema is not created here by default
+	 * 	because it can be customized by the service implementor. Besides, the DB name may be different.
+	 * 	However, you can easily get this schema thanks to the function {@link #getStdSchema()}
+	 * 	which returns the standard definition of this schema (including all tables and columns described
+	 * 	by the standard). For a standard definition of this schema, you can then write the following:
+	 * </i></p>
+	 * <pre>
+	 * TAPMetadata meta = new TAPMetadata();
+	 * meta.addSchema(TAPMetadata.getStdSchema());
+	 * </pre>
+	 * <p><i>
+	 * 	Of course, this schema (and its tables and their columns) can be customized after if needed.
+	 * 	Otherwise, if you want customize just some part of this schema, you can also use the function
+	 * 	{@link #getStdTable(STDTable)} to get just the standard definition of some of its tables, either
+	 * 	to customize them or to merely get them and keep them like they are.
+	 * </i></p> 
+	 */
 	public TAPMetadata(){
 		schemas = new HashMap<String,TAPSchema>();
 	}
 
+	/**
+	 * <p>Add the given schema inside this TAP metadata set.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given schema is NULL, nothing will be done.
+	 * </i></p>
+	 * 
+	 * @param s	The schema to add.
+	 */
 	public final void addSchema(TAPSchema s){
-		if (s != null && s.getName() != null)
-			schemas.put(s.getName(), s);
+		if (s != null && s.getADQLName() != null)
+			schemas.put(s.getADQLName(), s);
 	}
 
+	/**
+	 * <p>Build a new {@link TAPSchema} object with the given ADQL name.
+	 * Then, add it inside this TAP metadata set.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The built {@link TAPSchema} object is returned, so that being modified afterwards if needed.
+	 * </i></p>
+	 * 
+	 * @param schemaName	ADQL name of the schema to create and add inside this TAP metadata set.
+	 * 
+	 * @return	The created and added schema,
+	 *        	or NULL if the given schema is NULL or an empty string.
+	 * 
+	 * @see TAPSchema#TAPSchema(String)
+	 * @see #addSchema(TAPSchema)
+	 */
 	public TAPSchema addSchema(String schemaName){
-		if (schemaName == null)
+		if (schemaName == null || schemaName.trim().length() <= 0)
 			return null;
 
 		TAPSchema s = new TAPSchema(schemaName);
@@ -62,6 +140,24 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		return s;
 	}
 
+	/**
+	 * <p>Build a new {@link TAPSchema} object with the given ADQL name.
+	 * Then, add it inside this TAP metadata set.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The built {@link TAPSchema} object is returned, so that being modified afterwards if needed.
+	 * </i></p>
+	 * 
+	 * @param schemaName	ADQL name of the schema to create and add inside this TAP metadata set.
+	 * @param description	Description of the new schema. <i>MAY be NULL</i>
+	 * @param utype			UType associating the new schema with a data-model. <i>MAY be NULL</i>
+	 * 
+	 * @return	The created and added schema,
+	 *        	or NULL if the given schema is NULL or an empty string.
+	 * 
+	 * @see TAPSchema#TAPSchema(String, String, String)
+	 * @see #addSchema(TAPSchema)
+	 */
 	public TAPSchema addSchema(String schemaName, String description, String utype){
 		if (schemaName == null)
 			return null;
@@ -71,6 +167,17 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		return s;
 	}
 
+	/**
+	 * <p>Tell whether there is a schema with the given ADQL name.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * @param schemaName	ADQL name of the schema whose the existence must be checked.
+	 * 
+	 * @return	<i>true</i> if a schema with the given ADQL name exists, <i>false</i> otherwise.
+	 */
 	public final boolean hasSchema(String schemaName){
 		if (schemaName == null)
 			return false;
@@ -78,6 +185,18 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 			return schemas.containsKey(schemaName);
 	}
 
+	/**
+	 * <p>Search for a schema having the given ADQL name.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * @param schemaName	ADQL name of the schema to search.
+	 * 
+	 * @return	The schema having the given ADQL name,
+	 *        	or NULL if no such schema can be found.
+	 */
 	public final TAPSchema getSchema(String schemaName){
 		if (schemaName == null)
 			return null;
@@ -85,14 +204,44 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 			return schemas.get(schemaName);
 	}
 
+	/**
+	 * Get the number of schemas contained in this TAP metadata set.
+	 * 
+	 * @return	Number of all schemas.
+	 */
 	public final int getNbSchemas(){
 		return schemas.size();
 	}
 
+	/**
+	 * Tell whether this TAP metadata set contains no schema.
+	 * 
+	 * @return	<i>true</i> if this TAP metadata set has no schema,
+	 *        	<i>false</i> if it contains at least one schema.
+	 */
 	public final boolean isEmpty(){
 		return schemas.isEmpty();
 	}
 
+	/**
+	 * <p>Remove the schema having the given ADQL name.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * <p><i><b>WARNING:</b>
+	 * 	If the goal of this function's call is to delete definitely the specified schema
+	 * 	from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()} on the
+	 * 	removed table. Indeed, foreign keys of this table would still link the removed table
+	 * 	with other tables AND columns of the whole metadata set.
+	 * </i></p>
+	 * 
+	 * @param schemaName	ADQL name of the schema to remove from this TAP metadata set.
+	 * 
+	 * @return	The removed schema,
+	 *        	or NULL if no such schema can be found.
+	 */
 	public final TAPSchema removeSchema(String schemaName){
 		if (schemaName == null)
 			return null;
@@ -100,6 +249,9 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 			return schemas.remove(schemaName);
 	}
 
+	/**
+	 * Remove all schemas of this metadata set.
+	 */
 	public final void removeAllSchemas(){
 		schemas.clear();
 	}
@@ -109,10 +261,27 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		return schemas.values().iterator();
 	}
 
+	/**
+	 * Get the list of all tables available in this TAP metadata set.
+	 * 
+	 * @return	An iterator over the list of all tables contained in this TAP metadata set.
+	 */
 	public Iterator<TAPTable> getTables(){
 		return new TAPTableIterator(this);
 	}
 
+	/**
+	 * <p>Tell whether this TAP metadata set contains the specified table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * @param schemaName	ADQL name of the schema owning the table to search.
+	 * @param tableName		ADQL name of the table to search.
+	 * 
+	 * @return	<i>true</i> if the specified table exists, <i>false</i> otherwise.
+	 */
 	public boolean hasTable(String schemaName, String tableName){
 		TAPSchema s = getSchema(schemaName);
 		if (s != null)
@@ -121,6 +290,17 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 			return false;
 	}
 
+	/**
+	 * <p>Tell whether this TAP metadata set contains a table with the given ADQL name, whatever is its schema.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * @param tableName	ADQL name of the table to search.
+	 * 
+	 * @return	<i>true</i> if the specified table exists, <i>false</i> otherwise.
+	 */
 	public boolean hasTable(String tableName){
 		for(TAPSchema s : this)
 			if (s.hasTable(tableName))
@@ -128,7 +308,19 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		return false;
 	}
 
-	//		@Override
+	/**
+	 * <p>Search for the specified table in this TAP metadata set.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * @param schemaName	ADQL name of the schema owning the table to search.
+	 * @param tableName		ADQL name of the table to search.
+	 * 
+	 * @return	The table which has the given ADQL name and which is inside the specified schema,
+	 *        	or NULL if no such table can be found.
+	 */
 	public TAPTable getTable(String schemaName, String tableName){
 		TAPSchema s = getSchema(schemaName);
 		if (s != null)
@@ -137,7 +329,19 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 			return null;
 	}
 
-	//		@Override
+	/**
+	 * <p>Search in this TAP metadata set for all tables whose the ADQL name matches the given one,
+	 * whatever is their schema.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * @param tableName		ADQL name of the tables to search.
+	 * 
+	 * @return	A list of all the tables which have the given ADQL name,
+	 *        	or an empty list if no such table can be found.
+	 */
 	public ArrayList<DBTable> getTable(String tableName){
 		ArrayList<DBTable> tables = new ArrayList<DBTable>();
 		for(TAPSchema s : this)
@@ -146,6 +350,11 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		return tables;
 	}
 
+	/**
+	 * Get the number of all tables contained in this TAP metadata set.
+	 * 
+	 * @return	Number of all its tables.
+	 */
 	public int getNbTables(){
 		int nbTables = 0;
 		for(TAPSchema s : this)
@@ -153,7 +362,13 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		return nbTables;
 	}
 
-	public static class TAPTableIterator implements Iterator<TAPTable> {
+	/**
+	 * Let iterating over the list of all tables contained in a given {@link TAPMetadata} object.
+	 * 
+	 * @author Gr&eacute;gory Mantelet (CDS;ARI)
+	 * @version 2.0 (08/2014)
+	 */
+	protected static class TAPTableIterator implements Iterator<TAPTable> {
 		private Iterator<TAPSchema> it;
 		private Iterator<TAPTable> itTables;
 
@@ -229,14 +444,10 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 	}
 
 	@Override
-	public void init(ServletConfig config) throws ServletException{
-		;
-	}
+	public void init(ServletConfig config) throws ServletException{}
 
 	@Override
-	public void destroy(){
-		;
-	}
+	public void destroy(){}
 
 	@Override
 	public boolean executeResource(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
@@ -260,11 +471,35 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		return false;
 	}
 
+	/**
+	 * <p>Format in XML the given schema and then write it in the given writer.</p>
+	 * 
+	 * <p>Written lines:</p>
+	 * <pre>
+	 * &lt;schema&gt;
+	 * 	&lt;name&gt;...&lt;/name&gt;
+	 * 	&lt;description&gt;...&lt;/description&gt;
+	 * 	&lt;utype&gt;...&lt;/utype&gt;
+	 * 		// call #writeTable(TAPTable, PrintWriter) for each table
+	 * &lt;/schema&gt;
+	 * </pre>
+	 * 
+	 * <p><i>Note:
+	 * 	When NULL an attribute or a field is not written. Here this rule concerns: description and utype.
+	 * </i></p>
+	 * 
+	 * @param s			The schema to format and to write in XML.
+	 * @param writer	Output in which the XML serialization of the given schema must be written.
+	 * 
+	 * @throws IOException	If there is any error while writing the XML in the given writer.
+	 * 
+	 * @see #writeTable(TAPTable, PrintWriter)
+	 */
 	private void writeSchema(TAPSchema s, PrintWriter writer) throws IOException{
 		final String prefix = "\t\t";
 		writer.println("\t<schema>");
 
-		writeAtt(prefix, "name", s.getName(), writer);
+		writeAtt(prefix, "name", s.getADQLName(), writer);
 		writeAtt(prefix, "description", s.getDescription(), writer);
 		writeAtt(prefix, "utype", s.getUtype(), writer);
 
@@ -274,11 +509,34 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		writer.println("\t</schema>");
 	}
 
+	/**
+	 * <p>Format in XML the given table and then write it in the given writer.</p>
+	 * 
+	 * <p>Written lines:</p>
+	 * <pre>
+	 * &lt;table type="..."&gt;
+	 * 	&lt;name&gt;...&lt;/name&gt;
+	 * 	&lt;description&gt;...&lt;/description&gt;
+	 * 	&lt;utype&gt;...&lt;/utype&gt;
+	 * 		// call #writeColumn(TAPColumn, PrintWriter) for each column
+	 * 		// call #writeForeignKey(TAPForeignKey, PrintWriter) for each foreign key
+	 * &lt;/table&gt;
+	 * </pre>
+	 * 
+	 * <p><i>Note:
+	 * 	When NULL an attribute or a field is not written. Here this rule concerns: description and utype.
+	 * </i></p>
+	 * 
+	 * @param t			The table to format and to write in XML.
+	 * @param writer	Output in which the XML serialization of the given table must be written.
+	 * 
+	 * @throws IOException	If there is any error while writing the XML in the given writer.
+	 */
 	private void writeTable(TAPTable t, PrintWriter writer) throws IOException{
 		final String prefix = "\t\t\t";
 
 		writer.print("\t\t<table type=\"");
-		writer.print(t.getType().equalsIgnoreCase("table") ? "base_table" : t.getType());
+		writer.print(t.getType().toString());
 		writer.println("\">");
 
 		writeAtt(prefix, "name", t.getFullName(), writer);
@@ -296,6 +554,32 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		writer.println("\t\t</table>");
 	}
 
+	/**
+	 * <p>Format in XML the given column and then write it in the given writer.</p>
+	 * 
+	 * <p>Written lines:</p>
+	 * <pre>
+	 * &lt;column std="true|false"&gt; // the value of this field is TAPColumn#isStd()
+	 * 	&lt;name&gt;...&lt;/name&gt;
+	 * 	&lt;description&gt;...&lt;/description&gt;
+	 * 	&lt;unit&gt;...&lt;/unit&gt;
+	 * 	&lt;utype&gt;...&lt;/utype&gt;
+	 * 	&lt;ucd&gt;...&lt;/ucd&gt;
+	 * 	&lt;dataType xsi:type="vod:TAPType" size="..."&gt;...&lt;/dataType&gt;
+	 * 	&lt;flag&gt;indexed&lt;/flag&gt; // if TAPColumn#isIndexed()
+	 * 	&lt;flag&gt;primary&lt;/flag&gt; // if TAPColumn#isPrincipal()
+	 * &lt;/column&gt;
+	 * </pre>
+	 * 
+	 * <p><i>Note:
+	 * 	When NULL an attribute or a field is not written. Here this rule concerns: description, unit, utype, ucd and flags.
+	 * </i></p>
+	 * 
+	 * @param c			The column to format and to write in XML.
+	 * @param writer	Output in which the XML serialization of the given column must be written.
+	 * 
+	 * @throws IOException	If there is any error while writing the XML in the given writer.
+	 */
 	private void writeColumn(TAPColumn c, PrintWriter writer) throws IOException{
 		final String prefix = "\t\t\t\t";
 
@@ -303,7 +587,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		writer.print(c.isStd());
 		writer.println("\">");
 
-		writeAtt(prefix, "name", c.getName(), writer);
+		writeAtt(prefix, "name", c.getADQLName(), writer);
 		writeAtt(prefix, "description", c.getDescription(), writer);
 		writeAtt(prefix, "unit", c.getUnit(), writer);
 		writeAtt(prefix, "utype", c.getUtype(), writer);
@@ -330,6 +614,32 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		writer.println("\t\t\t</column>");
 	}
 
+	/**
+	 * <p>Format in XML the given foreign key and then write it in the given writer.</p>
+	 * 
+	 * <p>Written lines:</p>
+	 * <pre>
+	 * &lt;foreignKey&gt;
+	 * 	&lt;targetTable&gt;...&lt;/targetTable&gt;
+	 * 	&lt;description&gt;...&lt;/description&gt;
+	 * 	&lt;utype&gt;...&lt;/utype&gt;
+	 * 	&lt;fkColumn&gt;
+	 * 		&lt;fromColumn&gt;...&lt;/fromColumn&gt;
+	 * 		&lt;targetColumn&gt;...&lt;/targetColumn&gt;
+	 * 	&lt;/fkColumn&gt;
+	 * 	...
+	 * &lt;/foreignKey&gt;
+	 * </pre>
+	 * 
+	 * <p><i>Note:
+	 * 	When NULL an attribute or a field is not written. Here this rule concerns: description and utype.
+	 * </i></p>
+	 * 
+	 * @param fk		The foreign key to format and to write in XML.
+	 * @param writer	Output in which the XML serialization of the given foreign key must be written.
+	 * 
+	 * @throws IOException	If there is any error while writing the XML in the given writer.
+	 */
 	private void writeForeignKey(TAPForeignKey fk, PrintWriter writer) throws IOException{
 		final String prefix = "\t\t\t\t";
 
@@ -352,6 +662,16 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		writer.println("\t\t\t</foreignKey>");
 	}
 
+	/**
+	 * Write the specified metadata attribute as a simple XML node.
+	 * 
+	 * @param prefix			Prefix of the XML node. (generally, space characters)
+	 * @param attributeName		Name of the metadata attribute to write (= Name of the XML node).
+	 * @param attributeValue	Value of the metadata attribute (= Value of the XML node).
+	 * @param writer			Output in which the XML node must be written.
+	 * 
+	 * @throws IOException	If there is a problem while writing the XML node inside the given writer.
+	 */
 	private void writeAtt(String prefix, String attributeName, String attributeValue, PrintWriter writer) throws IOException{
 		if (attributeValue != null){
 			StringBuffer xml = new StringBuffer(prefix);
@@ -360,4 +680,176 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		}
 	}
 
+	/**
+	 * <p>
+	 * 	Get the definition of the whole standard TAP_SCHEMA. Thus, all standard TAP_SCHEMA tables
+	 * 	(with all their columns) are also included in this object.
+	 * </p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function create the {@link TAPSchema} and all its {@link TAPTable}s objects on the fly.
+	 * </p>
+	 * 
+	 * @return	The whole TAP_SCHEMA definition.
+	 * 
+	 * @see STDSchema#TAPSCHEMA
+	 * @see STDTable
+	 * @see #getStdTable(STDTable)
+	 * 
+	 * @since 2.0
+	 */
+	public static final TAPSchema getStdSchema(){
+		TAPSchema tap_schema = new TAPSchema(STDSchema.TAPSCHEMA.toString(), "Set of tables listing and describing the schemas, tables and columns published in this TAP service.", null);
+		for(STDTable t : STDTable.values()){
+			TAPTable table = getStdTable(t);
+			tap_schema.addTable(table);
+		}
+		return tap_schema;
+	}
+
+	/**
+	 * <p>Get the definition of the specified standard TAP table.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	The returned table is not linked at all with a schema, on the contrary of {@link #getStdSchema()} which returns tables linked with the returned schema.
+	 * 	So, you may have to linked this table to schema (by using {@link TAPSchema#addTable(TAPTable)}) whose the ADQL name is TAP_SCHEMA after calling this function.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function create the {@link TAPTable} object on the fly.
+	 * </p>
+	 * 
+	 * @param tableId	ID of the TAP table to return.
+	 * 
+	 * @return	The corresponding table definition (with no schema).
+	 * 
+	 * @since 2.0
+	 */
+	public static final TAPTable getStdTable(final STDTable tableId){
+		switch(tableId){
+
+			case SCHEMAS:
+				TAPTable schemas = new TAPTable(STDTable.SCHEMAS.toString(), TableType.table, "List of schemas published in this TAP service.", null);
+				schemas.addColumn("schema_name", new TAPType(TAPDatatype.VARCHAR), "schema name, possibly qualified", null, null, null, true, true, true);
+				schemas.addColumn("description", new TAPType(TAPDatatype.VARCHAR), "brief description of schema", null, null, null, false, false, true);
+				schemas.addColumn("utype", new TAPType(TAPDatatype.VARCHAR), "UTYPE if schema corresponds to a data model", null, null, null, false, false, true);
+				return schemas;
+
+			case TABLES:
+				TAPTable tables = new TAPTable(STDTable.TABLES.toString(), TableType.table, "List of tables published in this TAP service.", null);
+				tables.addColumn("schema_name", new TAPType(TAPDatatype.VARCHAR), "the schema name from TAP_SCHEMA.schemas", null, null, null, true, true, true);
+				tables.addColumn("table_name", new TAPType(TAPDatatype.VARCHAR), "table name as it should be used in queries", null, null, null, true, true, true);
+				tables.addColumn("table_type", new TAPType(TAPDatatype.VARCHAR), "one of: table, view", null, null, null, false, false, true);
+				tables.addColumn("description", new TAPType(TAPDatatype.VARCHAR), "brief description of table", null, null, null, false, false, true);
+				tables.addColumn("utype", new TAPType(TAPDatatype.VARCHAR), "UTYPE if table corresponds to a data model", null, null, null, false, false, true);
+				return tables;
+
+			case COLUMNS:
+				TAPTable columns = new TAPTable(STDTable.COLUMNS.toString(), TableType.table, "List of columns of all tables listed in TAP_SCHEMA.TABLES and published in this TAP service.", null);
+				columns.addColumn("table_name", new TAPType(TAPDatatype.VARCHAR), "table name from TAP_SCHEMA.tables", null, null, null, true, true, true);
+				columns.addColumn("column_name", new TAPType(TAPDatatype.VARCHAR), "column name", null, null, null, true, true, true);
+				columns.addColumn("description", new TAPType(TAPDatatype.VARCHAR), "brief description of column", null, null, null, false, false, true);
+				columns.addColumn("unit", new TAPType(TAPDatatype.VARCHAR), "unit in VO standard format", null, null, null, false, false, true);
+				columns.addColumn("ucd", new TAPType(TAPDatatype.VARCHAR), "UCD of column if any", null, null, null, false, false, true);
+				columns.addColumn("utype", new TAPType(TAPDatatype.VARCHAR), "UTYPE of column if any", null, null, null, false, false, true);
+				columns.addColumn("datatype", new TAPType(TAPDatatype.VARCHAR), "ADQL datatype as in section 2.5", null, null, null, false, false, true);
+				columns.addColumn("size", new TAPType(TAPDatatype.INTEGER), "length of variable length datatypes", null, null, null, false, false, true);
+				columns.addColumn("principal", new TAPType(TAPDatatype.INTEGER), "a principal column; 1 means true, 0 means false", null, null, null, false, false, true);
+				columns.addColumn("indexed", new TAPType(TAPDatatype.INTEGER), "an indexed column; 1 means true, 0 means false", null, null, null, false, false, true);
+				columns.addColumn("std", new TAPType(TAPDatatype.INTEGER), "a standard column; 1 means true, 0 means false", null, null, null, false, false, true);
+				return columns;
+
+			case KEYS:
+				TAPTable keys = new TAPTable(STDTable.KEYS.toString(), TableType.table, "List all foreign keys but provides just the tables linked by the foreign key. To know which columns of these tables are linked, see in TAP_SCHEMA.key_columns using the key_id.", null);
+				keys.addColumn("key_id", new TAPType(TAPDatatype.VARCHAR), "unique key identifier", null, null, null, true, true, true);
+				keys.addColumn("from_table", new TAPType(TAPDatatype.VARCHAR), "fully qualified table name", null, null, null, false, false, true);
+				keys.addColumn("target_table", new TAPType(TAPDatatype.VARCHAR), "fully qualified table name", null, null, null, false, false, true);
+				keys.addColumn("description", new TAPType(TAPDatatype.VARCHAR), "description of this key", null, null, null, false, false, true);
+				keys.addColumn("utype", new TAPType(TAPDatatype.VARCHAR), "utype of this key", null, null, null, false, false, true);
+				return keys;
+
+			case KEY_COLUMNS:
+				TAPTable key_columns = new TAPTable(STDTable.KEY_COLUMNS.toString(), TableType.table, "List all foreign keys but provides just the columns linked by the foreign key. To know the table of these columns, see in TAP_SCHEMA.keys using the key_id.", null);
+				key_columns.addColumn("key_id", new TAPType(TAPDatatype.VARCHAR), "unique key identifier", null, null, null, true, true, true);
+				key_columns.addColumn("from_column", new TAPType(TAPDatatype.VARCHAR), "key column name in the from_table", null, null, null, false, false, true);
+				key_columns.addColumn("target_column", new TAPType(TAPDatatype.VARCHAR), "key column name in the target_table", null, null, null, false, false, true);
+				return key_columns;
+
+			default:
+				return null;
+		}
+	}
+
+	/**
+	 * <p>Tell whether the given table name is a standard TAP table.</p>
+	 * 
+	 * <p><i>Note:
+	 *	This function is case sensitive. Indeed TAP_SCHEMA tables are defined by the TAP standard by a given case.
+	 *	Thus, this case is expected here.  
+	 * </i></p>
+	 * 
+	 * @param tableName	Unqualified table name.
+	 *  
+	 * @return	The corresponding {@link STDTable} or NULL if the given table is not part of the TAP standard.
+	 * 
+	 * @since 2.0
+	 */
+	public static final STDTable resolveStdTable(String tableName){
+		if (tableName == null || tableName.trim().length() == 0)
+			return null;
+
+		for(STDTable t : STDTable.values()){
+			if (t.label.equals(tableName))
+				return t;
+		}
+
+		return null;
+	}
+
+	/**
+	 * Enumeration of all schemas defined in the TAP standard.
+	 * 
+	 * @author Gr&eacute;gory Mantelet (ARI)
+	 * @version 2.0 (07/2014)
+	 * @since 2.0
+	 */
+	public enum STDSchema{
+		TAPSCHEMA("TAP_SCHEMA"), UPLOADSCHEMA("TAP_UPLOAD");
+
+		/** Real name of the schema. */
+		public final String label;
+
+		private STDSchema(final String name){
+			this.label = name;
+		}
+
+		@Override
+		public String toString(){
+			return label;
+		}
+	}
+
+	/**
+	 * Enumeration of all tables of TAP_SCHEMA.
+	 * 
+	 * @author Gr&eacute;gory Mantelet (ARI)
+	 * @version 2.0 (07/2014)
+	 * @since 2.0
+	 */
+	public enum STDTable{
+		SCHEMAS("schemas"), TABLES("tables"), COLUMNS("columns"), KEYS("keys"), KEY_COLUMNS("key_columns");
+
+		/** Real name of the table. */
+		public final String label;
+
+		private STDTable(final String name){
+			this.label = name;
+		}
+
+		@Override
+		public String toString(){
+			return label;
+		}
+	}
+
 }
diff --git a/src/tap/metadata/TAPSchema.java b/src/tap/metadata/TAPSchema.java
index faced37..984d087 100644
--- a/src/tap/metadata/TAPSchema.java
+++ b/src/tap/metadata/TAPSchema.java
@@ -16,37 +16,122 @@ package tap.metadata;
  * You should have received a copy of the GNU Lesser General Public License
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
- * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
+ * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
+import java.awt.List;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.Map;
 
+import tap.metadata.TAPTable.TableType;
+
+/**
+ * <p>Represent a schema as described by the IVOA standard in the TAP protocol definition.</p>
+ * 
+ * <p>
+ * 	This object representation has exactly the same fields as the column of the table TAP_SCHEMA.schemas.
+ * 	But it also provides a way to add other data. For instance, if information not listed in the standard
+ * 	may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be
+ * 	a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc...
+ * </p>
+ * 
+ * <p><i>Note:
+ * 	On the contrary to {@link TAPColumn} and {@link TAPTable}, a {@link TAPSchema} object MAY have no DB name.
+ * 	But by default, at the creation the DB name is the ADQL name. Once created, it is possible to set the DB
+ * 	name with {@link #setDBName(String)}. This DB name MAY be qualified, BUT MUST BE without double quotes.
+ * </i></p>
+ * 
+ * @author Gr&eacute;gory Mantelet (CDS;ARI)
+ * @version 2.0 (08/2014)
+ */
 public class TAPSchema implements Iterable<TAPTable> {
 
+	/** Name that this schema MUST have in ADQL queries. */
 	private final String adqlName;
 
+	/** Name that this schema have in the database.
+	 * <i>Note: It MAY be NULL. By default, it is the ADQL name.</i> */
 	private String dbName = null;
 
+	/** Description of this schema.
+	 * <i>Note: Standard TAP schema field ; MAY be NULL.</i> */
 	private String description = null;
 
+	/** UType describing the scientific content of this schema.
+	 * <i>Note: Standard TAP schema field ; MAY be NULL.</i> */
 	private String utype = null;
 
+	/** Let add some information in addition of the ones of the TAP protocol.
+	 * <i>Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ...
+	 * Its content is totally free and never used or checked.</i> */
 	protected Object otherData = null;
 
+	/** List all tables contained inside this schema. */
 	protected final Map<String,TAPTable> tables;
 
+	/**
+	 * <p>Build a {@link TAPSchema} instance with the given ADQL name.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given ADQL name is prefixed (= it has some text separated by a '.' before the schema name),
+	 * 	this prefix will be removed. Only the part after the '.' character will be kept.
+	 * </i></p>
+	 * 
+	 * @param schemaName	Name that this schema MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 */
 	public TAPSchema(String schemaName){
-		adqlName = schemaName;
+		if (schemaName == null || schemaName.trim().length() == 0)
+			throw new NullPointerException("Missing schema name !");
+		int indPrefix = schemaName.lastIndexOf('.');
+		adqlName = (indPrefix >= 0) ? schemaName.substring(indPrefix + 1).trim() : schemaName.trim();
 		dbName = adqlName;
 		tables = new HashMap<String,TAPTable>();
 	}
 
+	/**
+	 * <p>Build a {@link TAPSchema} instance with the given ADQL name and description.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given ADQL name is prefixed (= it has some text separated by a '.' before the schema name),
+	 * 	this prefix will be removed. Only the part after the '.' character will be kept.
+	 * </i></p>
+	 * 
+	 * @param schemaName	Name that this schema MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 * @param description	Description of this schema. <i>MAY be NULL</i>
+	 */
 	public TAPSchema(String schemaName, String description){
 		this(schemaName, description, null);
 	}
 
+	/**
+	 * <p>Build a {@link TAPSchema} instance with the given ADQL name, description and UType.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given ADQL name is prefixed (= it has some text separated by a '.' before the schema name),
+	 * 	this prefix will be removed. Only the part after the '.' character will be kept.
+	 * </i></p>
+	 * 
+	 * @param schemaName	Name that this schema MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 * @param description	Description of this schema. <i>MAY be NULL</i>
+	 * @param utype			UType associating this schema with a data-model. <i>MAY be NULL</i>
+	 */
 	public TAPSchema(String schemaName, String description, String utype){
 		this(schemaName);
 		this.description = description;
@@ -54,68 +139,143 @@ public class TAPSchema implements Iterable<TAPTable> {
 	}
 
 	/**
-	 * @return The name.
+	 * Get the ADQL name (the name this schema MUST have in ADQL queries).
+	 * 
+	 * @return	Its ADQL name.
+	 * @see #getADQLName()
+	 * @deprecated	Does not do anything special: just call {@link #getADQLName()}.
 	 */
+	@Deprecated
 	public final String getName(){
 		return getADQLName();
 	}
 
+	/**
+	 * Get the name this schema MUST have in ADQL queries.
+	 * 
+	 * @return	Its ADQL name. <i>CAN'T be NULL</i>
+	 */
 	public final String getADQLName(){
 		return adqlName;
 	}
 
+	/**
+	 * Get the name this schema MUST have in the database.
+	 * 
+	 * @return	Its DB name. <i>MAY be NULL</i>
+	 */
 	public final String getDBName(){
 		return dbName;
 	}
 
+	/**
+	 * Set the name this schema MUST have in the database.
+	 * 
+	 * @param name	Its new DB name. <i>MAY be NULL</i>
+	 */
 	public final void setDBName(String name){
 		name = (name != null) ? name.trim() : name;
-		dbName = (name == null || name.length() == 0) ? adqlName : name;
+		dbName = name;
 	}
 
 	/**
-	 * @return The description.
+	 * Get the description of this schema.
+	 * 
+	 * @return	Its description. <i>MAY be NULL</i>
 	 */
 	public final String getDescription(){
 		return description;
 	}
 
 	/**
-	 * @param description The description to set.
+	 * Set the description of this schema.
+	 * 
+	 * @param description	Its new description. <i>MAY be NULL</i>
 	 */
 	public final void setDescription(String description){
 		this.description = description;
 	}
 
 	/**
-	 * @return The utype.
+	 * Get the UType associating this schema with a data-model.
+	 * 
+	 * @return	Its UType. <i>MAY be NULL</i>
 	 */
 	public final String getUtype(){
 		return utype;
 	}
 
 	/**
-	 * @param utype The utype to set.
+	 * Set the UType associating this schema with a data-model.
+	 * 
+	 * @param utype	Its new UType. <i>MAY be NULL</i>
 	 */
 	public final void setUtype(String utype){
 		this.utype = utype;
 	}
 
+	/**
+	 * <p>Get the other (piece of) information associated with this schema.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	By default, NULL is returned, but it may be any kind of value ({@link Integer},
+	 * 	{@link String}, {@link Map}, {@link List}, ...).
+	 * </i></p>
+	 * 
+	 * @return	The other (piece of) information. <i>MAY be NULL</i>
+	 */
 	public Object getOtherData(){
 		return otherData;
 	}
 
+	/**
+	 * Set the other (piece of) information associated with this schema.
+	 * 
+	 * @param data	Another information about this schema. <i>MAY be NULL</i>
+	 */
 	public void setOtherData(Object data){
 		otherData = data;
 	}
 
+	/**
+	 * <p>Add the given table inside this schema.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given table is NULL, nothing will be done.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	By adding the given table inside this schema, it
+	 * 	will be linked with this schema using {@link TAPTable#setSchema(TAPSchema)}.
+	 * 	In this function, if the table was already linked with another {@link TAPSchema},
+	 * 	the former link is removed using {@link TAPSchema#removeTable(String)}.
+	 * </i></p>
+	 * 
+	 * @param newTable	Table to add inside this schema.
+	 */
 	public final void addTable(TAPTable newTable){
-		if (newTable != null && newTable.getName() != null){
-			tables.put(newTable.getName(), newTable);
+		if (newTable != null && newTable.getADQLName() != null){
+			tables.put(newTable.getADQLName(), newTable);
 			newTable.setSchema(this);
 		}
 	}
 
+	/**
+	 * <p>Build a {@link TAPTable} object whose the ADQL and DB name will the given one.
+	 * Then, add this table inside this schema.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The built {@link TAPTable} object is returned, so that being modified afterwards if needed.
+	 * </i></p>
+	 * 
+	 * @param tableName	ADQL name (and indirectly also the DB name) of the table to create and add.
+	 * 
+	 * @return	The created and added {@link TAPTable} object,
+	 *        	or NULL if the given name is NULL or an empty string.
+	 * 
+	 * @see TAPTable#TAPTable(String)
+	 * @see #addTable(TAPTable)
+	 */
 	public TAPTable addTable(String tableName){
 		if (tableName == null)
 			return null;
@@ -125,15 +285,48 @@ public class TAPSchema implements Iterable<TAPTable> {
 		return t;
 	}
 
-	public TAPTable addTable(String tableName, String tableType, String description, String utype){
+	/**
+	 * <p>Build a {@link TAPTable} object whose the ADQL and DB name will the given one.
+	 * Then, add this table inside this schema.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The built {@link TAPTable} object is returned, so that being modified afterwards if needed.
+	 * </i></p>
+	 * 
+	 * @param tableName		ADQL name (and indirectly also the DB name) of the table to create and add.
+	 * @param tableType		Type of the new table. <i>If NULL, "table" will be the type of the created table.</i>
+	 * @param description	Description of the new table. <i>MAY be NULL</i>
+	 * @param unit			Unit of the new table's values. <i>MAY be NULL</i>
+	 * @param ucd			UCD describing the scientific content of the new column. <i>MAY be NULL</i>
+	 * @param utype			UType associating the new column with a data-model. <i>MAY be NULL</i>
+	 * 
+	 * @return	The created and added {@link TAPTable} object,
+	 *        	or NULL if the given name is NULL or an empty string.
+	 * 
+	 * @see TAPTable#TAPTable(String, TableType, String, String, String, String)
+	 * @see #addTable(TAPSchema)
+	 */
+	public TAPTable addTable(String tableName, TableType tableType, String description, String utype){
 		if (tableName == null)
 			return null;
 
 		TAPTable t = new TAPTable(tableName, tableType, description, utype);
 		addTable(t);
+
 		return t;
 	}
 
+	/**
+	 * <p>Tell whether this schema contains a table having the given ADQL name.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * @param tableName	Name of the table whose the existence in this schema must be checked.
+	 * 
+	 * @return	<i>true</i> if a table with the given ADQL name exists, <i>false</i> otherwise.
+	 */
 	public final boolean hasTable(String tableName){
 		if (tableName == null)
 			return false;
@@ -141,6 +334,18 @@ public class TAPSchema implements Iterable<TAPTable> {
 			return tables.containsKey(tableName);
 	}
 
+	/**
+	 * <p>Search for a table having the given ADQL name.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * @param tableName	ADQL name of the table to search.
+	 * 
+	 * @return	The table having the given ADQL name,
+	 *        	or NULL if no such table can be found.
+	 */
 	public final TAPTable getTable(String tableName){
 		if (tableName == null)
 			return null;
@@ -148,33 +353,79 @@ public class TAPSchema implements Iterable<TAPTable> {
 			return tables.get(tableName);
 	}
 
+	/**
+	 * Get the number of all tables contained inside this schema.
+	 * 
+	 * @return	Number of its tables.
+	 */
 	public final int getNbTables(){
 		return tables.size();
 	}
 
+	/**
+	 * Tell whether this schema contains no table.
+	 * 
+	 * @return	<i>true</i> if this schema contains no table,
+	 *        	<i>false</i> if it has at least one table.
+	 */
 	public final boolean isEmpty(){
 		return tables.isEmpty();
 	}
 
+	/**
+	 * <p>Remove the table having the given ADQL name.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the specified table is removed, its schema link is also deleted.
+	 * </i></p>
+	 * 
+	 * <p><i><b>WARNING:</b>
+	 * 	If the goal of this function's call is to delete definitely the specified table
+	 * 	from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()}.
+	 * 	Indeed, foreign keys of the table would still link the removed table with other tables
+	 * 	AND columns of the whole metadata set.
+	 * </i></p>
+	 * 
+	 * @param tableName	ADQL name of the table to remove from this schema.
+	 *  
+	 * @return	The removed table,
+	 *        	or NULL if no table with the given ADQL name can be found.
+	 */
 	public final TAPTable removeTable(String tableName){
 		if (tableName == null)
 			return null;
 
 		TAPTable removedTable = tables.remove(tableName);
-		if (removedTable != null){
+		if (removedTable != null)
 			removedTable.setSchema(null);
-			removedTable.removeAllForeignKeys();
-		}
 		return removedTable;
 	}
 
+	/**
+	 * <p>Remove all the tables contained inside this schema.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	When a table is removed, its schema link is also deleted.
+	 * </i></p>
+	 * 
+	 * <p><b>CAUTION:
+	 * 	If the goal of this function's call is to delete definitely all the tables of this schema
+	 * 	from the metadata, you SHOULD also call {@link TAPTable#removeAllForeignKeys()}
+	 * 	on all tables before calling this function.
+	 * 	Indeed, foreign keys of the tables would still link the removed tables with other tables
+	 * 	AND columns of the whole metadata set.
+	 * </b></p>
+	 */
 	public final void removeAllTables(){
 		Iterator<Map.Entry<String,TAPTable>> it = tables.entrySet().iterator();
 		while(it.hasNext()){
 			Map.Entry<String,TAPTable> entry = it.next();
 			it.remove();
 			entry.getValue().setSchema(null);
-			entry.getValue().removeAllForeignKeys();
 		}
 	}
 
diff --git a/src/tap/metadata/TAPTable.java b/src/tap/metadata/TAPTable.java
index 5486377..770e868 100644
--- a/src/tap/metadata/TAPTable.java
+++ b/src/tap/metadata/TAPTable.java
@@ -16,39 +16,109 @@ package tap.metadata;
  * You should have received a copy of the GNU Lesser General Public License
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
- * Copyright 2012 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
+ * Copyright 2012,2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS)
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
+import java.awt.List;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import tap.TAPException;
 import adql.db.DBColumn;
 import adql.db.DBTable;
 
+/**
+ * <p>Represent a table as described by the IVOA standard in the TAP protocol definition.</p>
+ * 
+ * <p>
+ * 	This object representation has exactly the same fields as the column of the table TAP_SCHEMA.tables.
+ * 	But it also provides a way to add other data. For instance, if information not listed in the standard
+ * 	may be stored here, they can be using the function {@link #setOtherData(Object)}. This object can be
+ * 	a single value (integer, string, ...), but also a {@link Map}, {@link List}, etc...
+ * </p>
+ * 
+ * <p><i><b>Important note:</b>
+ * 	A {@link TAPTable} object MUST always have a DB name. That's why by default, at the creation
+ * 	the DB name is the ADQL name. Once created, it is possible to set the DB name with {@link #setDBName(String)}.
+ * 	This DB name MUST be UNqualified and without double quotes. If a NULL or empty value is provided,
+ * 	nothing is done and the object keeps its former DB name.
+ * </i></p>
+ * 
+ * @author Gr&eacute;gory Mantelet (CDS;ARI)
+ * @version 2.0 (08/2014)
+ */
 public class TAPTable implements DBTable {
 
+	/**
+	 * Different types of table according to the TAP protocol.
+	 * The default one should be "table".
+	 * 
+	 * @author Gr&eacute;gory Mantelet (ARI)
+	 * @version 2.0 (08/2014)
+	 * 
+	 * @since 2.0
+	 */
+	public enum TableType{
+		output, table, view;
+	}
+
+	/** Name that this table MUST have in ADQL queries. */
 	private final String adqlName;
 
+	/** Name that this table have in the database.
+	 * <i>Note: It CAN'T be NULL. By default, it is the ADQL name.</i> */
 	private String dbName = null;
 
+	/** The schema which owns this table.
+	 *  <i>Note: It is NULL only at the construction.
+	 * 	Then, this attribute is automatically set by a {@link TAPSchema} when adding this table inside it
+	 * 	with {@link TAPSchema#addTable(TAPTable)}.</i> */
 	private TAPSchema schema = null;
 
-	private String type = "table";
+	/** Type of this table.
+	 * <i>Note: Standard TAP table field ; CAN NOT be NULL ; by default, it is "table".</i> */
+	private TableType type = TableType.table;
 
+	/** Description of this table.
+	 * <i>Note: Standard TAP table field ; MAY be NULL.</i> */
 	private String description = null;
 
+	/** UType associating this table with a data-model.
+	 * <i>Note: Standard TAP table field ; MAY be NULL.</i> */
 	private String utype = null;
 
+	/** List of columns composing this table.
+	 * <i>Note: all columns of this list are linked to this table from the moment they are added inside it.</i> */
 	protected final Map<String,TAPColumn> columns;
 
+	/** List of all foreign keys linking this table to others. */
 	protected final ArrayList<TAPForeignKey> foreignKeys;
 
+	/** Let add some information in addition of the ones of the TAP protocol.
+	 * <i>Note: This object can be anything: an {@link Integer}, a {@link String}, a {@link Map}, a {@link List}, ...
+	 * Its content is totally free and never used or checked.</i> */
 	protected Object otherData = null;
 
+	/**
+	 * <p>Build a {@link TAPTable} instance with the given ADQL name.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * 	The table type is set by default to "table".
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given ADQL name is prefixed (= it has some text separated by a '.' before the table name),
+	 * 	this prefix will be removed. Only the part after the '.' character will be kept.
+	 * </i></p>
+	 * 
+	 * @param tableName	Name that this table MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 */
 	public TAPTable(String tableName){
 		if (tableName == null || tableName.trim().length() == 0)
 			throw new NullPointerException("Missing table name !");
@@ -59,27 +129,84 @@ public class TAPTable implements DBTable {
 		foreignKeys = new ArrayList<TAPForeignKey>();
 	}
 
-	public TAPTable(String tableName, String tableType){
+	/**
+	 * <p>Build a {@link TAPTable} instance with the given ADQL name and table type.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 *	The table type is set by calling the function {@link #setType(TableType)} which does not do
+	 *	anything if the given table type is NULL.
+	 * </i></p>
+	 * 
+	 * @param tableName	Name that this table MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 * @param tableType	Type of this table. <i>If NULL, "table" will be the type of this table.</i>
+	 * 
+	 * @see #setType(TableType)
+	 */
+	public TAPTable(String tableName, TableType tableType){
 		this(tableName);
-		type = tableType;
+		setType(tableType);
 	}
 
-	public TAPTable(String tableName, String tableType, String description, String utype){
+	/**
+	 * <p>Build a {@link TAPTable} instance with the given ADQL name, table type, description and UType.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The DB name is set by default with the ADQL name. To set the DB name,
+	 * 	you MUST call then {@link #setDBName(String)}.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 *	The table type is set by calling the function {@link #setType(TableType)} which does not do
+	 *	anything if the given table type is NULL.
+	 * </i></p>
+	 * 
+	 * @param tableName		Name that this table MUST have in ADQL queries. <i>CAN'T be NULL ; this name can never be changed after.</i>
+	 * @param tableType		Type of this table. <i>If NULL, "table" will be the type of this table.</i>
+	 * @param description	Description of this table. <i>MAY be NULL.</i>
+	 * @param utype			UType associating this table with a data-model. <i>MAY be NULL</i>
+	 * 
+	 * @see #setType(TableType)
+	 */
+	public TAPTable(String tableName, TableType tableType, String description, String utype){
 		this(tableName, tableType);
 		this.description = description;
 		this.utype = utype;
 	}
 
+	/**
+	 * <p>Get the qualified name of this table.</p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	The part of the returned full name won't be double quoted!
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If this table is not attached to a schema, this function will just return
+	 * 	the ADQL name of this table.
+	 * </i></p>
+	 * 
+	 * @return	Qualified ADQL name of this table.
+	 */
 	public final String getFullName(){
 		if (schema != null)
-			return schema.getName() + "." + adqlName;
+			return schema.getADQLName() + "." + adqlName;
 		else
 			return adqlName;
 	}
 
 	/**
-	 * @return The name.
+	 * Get the ADQL name (the name this table MUST have in ADQL queries).
+	 * 
+	 * @return	Its ADQL name.
+	 * @see #getADQLName()
+	 * @deprecated	Does not do anything special: just call {@link #getADQLName()}.
 	 */
+	@Deprecated
 	public final String getName(){
 		return getADQLName();
 	}
@@ -94,6 +221,15 @@ public class TAPTable implements DBTable {
 		return dbName;
 	}
 
+	/**
+	 * <p>Change the name that this table MUST have in the database (i.e. in SQL queries).</p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given value is NULL or an empty string, nothing is done ; the DB name keeps is former value.
+	 * </i></p>
+	 * 
+	 * @param name	The new database name of this table.
+	 */
 	public final void setDBName(String name){
 		name = (name != null) ? name.trim() : name;
 		dbName = (name == null || name.length() == 0) ? adqlName : name;
@@ -111,87 +247,167 @@ public class TAPTable implements DBTable {
 
 	@Override
 	public final String getADQLSchemaName(){
-		return schema.getADQLName();
+		return schema == null ? null : schema.getADQLName();
 	}
 
 	@Override
 	public final String getDBSchemaName(){
-		return schema.getDBName();
+		return schema == null ? null : schema.getDBName();
 	}
 
 	/**
-	 * @return The schema.
+	 * Get the schema that owns this table.
+	 * 
+	 * @return Its schema. <i>MAY be NULL</i>
 	 */
 	public final TAPSchema getSchema(){
 		return schema;
 	}
 
 	/**
-	 * @param schema The schema to set.
+	 * <p>Set the schema in which this schema is.</p>
+	 * 
+	 * <p><i><b>Warning:</b>
+	 * 	For consistency reasons, this function SHOULD be called only by the {@link TAPSchema}
+	 * 	that owns this table. 
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	If this table was already linked with another {@link TAPSchema} object, the previous link is removed
+	 * 	here, but also in the schema (by calling {@link TAPSchema#removeTable(String)}).
+	 * </i></p>
+	 * 
+	 * @param schema	The schema that owns this table.
 	 */
-	protected final void setSchema(TAPSchema schema){
+	protected final void setSchema(final TAPSchema schema){
+		if (this.schema != null && (schema == null || !schema.equals(this.schema)))
+			this.schema.removeTable(adqlName);
 		this.schema = schema;
 	}
 
 	/**
-	 * @return The type.
+	 * Get the type of this table.
+	 * 
+	 * @return	Its type.
 	 */
-	public final String getType(){
+	public final TableType getType(){
 		return type;
 	}
 
 	/**
-	 * @param type The type to set.
+	 * <p>Set the type of this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given type is NULL, nothing will be done ; the type of this table won't be changed.
+	 * </i></p>
+	 * 
+	 * @param type	Its new type.
 	 */
-	public final void setType(String type){
-		this.type = type;
+	public final void setType(TableType type){
+		if (type != null)
+			this.type = type;
 	}
 
 	/**
-	 * @return The description.
+	 * Get the description of this table.
+	 * 
+	 * @return	Its description. <i>MAY be NULL</i>
 	 */
 	public final String getDescription(){
 		return description;
 	}
 
 	/**
-	 * @param description The description to set.
+	 * Set the description of this table.
+	 * 
+	 * @param description	Its new description. <i>MAY be NULL</i>
 	 */
 	public final void setDescription(String description){
 		this.description = description;
 	}
 
 	/**
-	 * @return The utype.
+	 * Get the UType associating this table with a data-model.
+	 * 
+	 * @return	Its UType. <i>MAY be NULL</i>
 	 */
 	public final String getUtype(){
 		return utype;
 	}
 
 	/**
-	 * @param utype The utype to set.
+	 * Set the UType associating this table with a data-model.
+	 * 
+	 * @param utype	Its new UType. <i>MAY be NULL</i>
 	 */
 	public final void setUtype(String utype){
 		this.utype = utype;
 	}
 
+	/**
+	 * <p>Get the other (piece of) information associated with this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	By default, NULL is returned, but it may be any kind of value ({@link Integer},
+	 * 	{@link String}, {@link Map}, {@link List}, ...).
+	 * </i></p>
+	 * 
+	 * @return	The other (piece of) information. <i>MAY be NULL</i>
+	 */
 	public Object getOtherData(){
 		return otherData;
 	}
 
+	/**
+	 * Set the other (piece of) information associated with this table.
+	 * 
+	 * @param data	Another information about this table. <i>MAY be NULL</i>
+	 */
 	public void setOtherData(Object data){
 		otherData = data;
 	}
 
-	public final void addColumn(TAPColumn newColumn){
-		if (newColumn != null && newColumn.getName() != null){
-			columns.put(newColumn.getName(), newColumn);
+	/**
+	 * <p>Add a column to this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given column is NULL, nothing will be done.
+	 * </i></p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	By adding the given column inside this table, it
+	 * 	will be linked with this table using {@link TAPColumn#setTable(DBTable)}.
+	 * 	In this function, if the column was already linked with another {@link TAPTable},
+	 * 	the former link is removed using {@link TAPTable#removeColumn(String)}.
+	 * </i></p>
+	 * 
+	 * @param newColumn	Column to add inside this table.
+	 */
+	public final void addColumn(final TAPColumn newColumn){
+		if (newColumn != null && newColumn.getADQLName() != null){
+			columns.put(newColumn.getADQLName(), newColumn);
 			newColumn.setTable(this);
 		}
 	}
 
+	/**
+	 * <p>Build a {@link TAPColumn} object whose the ADQL and DB name will the given one.
+	 * Then, add this column inside this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The built {@link TAPColumn} object is returned, so that being modified afterwards if needed.
+	 * </i></p>
+	 * 
+	 * @param columnName	ADQL name (and indirectly also the DB name) of the column to create and add.
+	 * 
+	 * @return	The created and added {@link TAPColumn} object,
+	 *        	or NULL if the given name is NULL or an empty string.
+	 * 
+	 * @see TAPColumn#TAPColumn(String)
+	 * @see #addColumn(TAPColumn)
+	 */
 	public final TAPColumn addColumn(String columnName){
-		if (columnName == null)
+		if (columnName == null || columnName.trim().length() <= 0)
 			return null;
 
 		TAPColumn c = new TAPColumn(columnName);
@@ -199,8 +415,29 @@ public class TAPTable implements DBTable {
 		return c;
 	}
 
+	/**
+	 * <p>Build a {@link TAPColumn} object whose the ADQL and DB name will the given one.
+	 * Then, add this column inside this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The built {@link TAPColumn} object is returned, so that being modified afterwards if needed.
+	 * </i></p>
+	 * 
+	 * @param columnName	ADQL name (and indirectly also the DB name) of the column to create and add.
+	 * @param datatype		Type of the new column's values. <i>If NULL, VARCHAR will be the type of the created column.</i>
+	 * @param description	Description of the new column. <i>MAY be NULL</i>
+	 * @param unit			Unit of the new column's values. <i>MAY be NULL</i>
+	 * @param ucd			UCD describing the scientific content of the new column. <i>MAY be NULL</i>
+	 * @param utype			UType associating the new column with a data-model. <i>MAY be NULL</i>
+	 * 
+	 * @return	The created and added {@link TAPColumn} object,
+	 *        	or NULL if the given name is NULL or an empty string.
+	 * 
+	 * @see TAPColumn#TAPColumn(String, TAPType, String, String, String, String)
+	 * @see #addColumn(TAPColumn)
+	 */
 	public TAPColumn addColumn(String columnName, TAPType datatype, String description, String unit, String ucd, String utype){
-		if (columnName == null)
+		if (columnName == null || columnName.trim().length() <= 0)
 			return null;
 
 		TAPColumn c = new TAPColumn(columnName, datatype, description, unit, ucd, utype);
@@ -208,8 +445,35 @@ public class TAPTable implements DBTable {
 		return c;
 	}
 
+	/**
+	 * <p>Build a {@link TAPColumn} object whose the ADQL and DB name will the given one.
+	 * Then, add this column inside this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The built {@link TAPColumn} object is returned, so that being modified afterwards if needed.
+	 * </i></p>
+	 * 
+	 * @param columnName	ADQL name (and indirectly also the DB name) of the column to create and add.
+	 * @param datatype		Type of the new column's values. <i>If NULL, VARCHAR will be the type of the created column.</i>
+	 * @param description	Description of the new column. <i>MAY be NULL</i>
+	 * @param unit			Unit of the new column's values. <i>MAY be NULL</i>
+	 * @param ucd			UCD describing the scientific content of the new column. <i>MAY be NULL</i>
+	 * @param utype			UType associating the new column with a data-model. <i>MAY be NULL</i>
+	 * @param principal		<i>true</i> if the new column should be returned by default, <i>false</i> otherwise.
+	 * @param indexed		<i>true</i> if the new column is indexed, <i>false</i> otherwise.
+	 * @param std			<i>true</i> if the new column is defined by a standard, <i>false</i> otherwise.
+	 * 
+	 * @return	The created and added {@link TAPColumn} object,
+	 *        	or NULL if the given name is NULL or an empty string.
+	 * 
+	 * @see TAPColumn#TAPColumn(String, TAPType, String, String, String, String)
+	 * @see TAPColumn#setPrincipal(boolean)
+	 * @see TAPColumn#setIndexed(boolean)
+	 * @see TAPColumn#setStd(boolean)
+	 * @see #addColumn(TAPColumn)
+	 */
 	public TAPColumn addColumn(String columnName, TAPType datatype, String description, String unit, String ucd, String utype, boolean principal, boolean indexed, boolean std){
-		if (columnName == null)
+		if (columnName == null || columnName.trim().length() <= 0)
 			return null;
 
 		TAPColumn c = new TAPColumn(columnName, datatype, description, unit, ucd, utype);
@@ -220,6 +484,17 @@ public class TAPTable implements DBTable {
 		return c;
 	}
 
+	/**
+	 * <p>Tell whether this table contains a column with the given ADQL name.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function is case sensitive.
+	 * </i></p>
+	 * 
+	 * @param columnName	ADQL name (case sensitive) of the column whose the existence must be checked.
+	 * 
+	 * @return	<i>true</i> if a column having the given ADQL name exists in this table, <i>false</i> otherwise.
+	 */
 	public final boolean hasColumn(String columnName){
 		if (columnName == null)
 			return false;
@@ -227,6 +502,11 @@ public class TAPTable implements DBTable {
 			return columns.containsKey(columnName);
 	}
 
+	/**
+	 * Get the list of all columns contained in this table.
+	 * 
+	 * @return	An iterator over the list of this table's columns.
+	 */
 	public Iterator<TAPColumn> getColumns(){
 		return columns.values().iterator();
 	}
@@ -239,7 +519,7 @@ public class TAPTable implements DBTable {
 			if (colName != null && colName.length() > 0){
 				Collection<TAPColumn> collColumns = columns.values();
 				for(TAPColumn column : collColumns){
-					if (column.getDBName().equalsIgnoreCase(colName))
+					if (column.getDBName().equals(colName))
 						return column;
 				}
 			}
@@ -247,6 +527,18 @@ public class TAPTable implements DBTable {
 		}
 	}
 
+	/**
+	 * <p>Search a column inside this table having the given ADQL name.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function is case sensitive.
+	 * </i></p>
+	 * 
+	 * @param columnName	ADQL name of the column to search.
+	 * 
+	 * @return	The matching column,
+	 *        	or NULL if no column with this ADQL name has been found.
+	 */
 	public final TAPColumn getColumn(String columnName){
 		if (columnName == null)
 			return null;
@@ -254,18 +546,64 @@ public class TAPTable implements DBTable {
 			return columns.get(columnName);
 	}
 
+	/**
+	 * <p>Tell whether this table contains a column with the given ADQL or DB name.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This functions is just calling {@link #getColumn(String, boolean)} and compare its result
+	 * 	with NULL in order to check the existence of the specified column.
+	 * </i></p>
+	 * 
+	 * @param colName		ADQL or DB name that the column to search must have.
+	 * @param byAdqlName	<i>true</i> to search the column by ADQL name, <i>false</i> to search by DB name.
+	 * 
+	 * @return	<i>true</i> if a column has been found inside this table with the given ADQL or DB name,
+	 *        	<i>false</i> otherwise.
+	 * 
+	 * @see #getColumn(String, boolean)
+	 */
 	public boolean hasColumn(String colName, boolean byAdqlName){
 		return (getColumn(colName, byAdqlName) != null);
 	}
 
+	/**
+	 * Get the number of columns composing this table.
+	 * 
+	 * @return	Number of its columns.
+	 */
 	public final int getNbColumns(){
 		return columns.size();
 	}
 
+	/**
+	 * Tell whether this table contains no column.
+	 * 
+	 * @return	<i>true</i> if this table is empty (no column),
+	 *        	<i>false</i> if it contains at least one column.
+	 */
 	public final boolean isEmpty(){
 		return columns.isEmpty();
 	}
 
+	/**
+	 * <p>Remove the specified column.</p>
+	 * 
+	 * <p><i><b>Important note:</b>
+	 * 	This function is case sensitive!
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If some foreign keys were associating the column to remove,
+	 * 	they will be also deleted.
+	 * </i></p>
+	 * 
+	 * @param columnName	ADQL name of the column to remove.
+	 * 
+	 * @return	The removed column,
+	 *        	or NULL if no column with the given ADQL name has been found.
+	 *        
+	 * @see #deleteColumnRelations(TAPColumn)
+	 */
 	public final TAPColumn removeColumn(String columnName){
 		if (columnName == null)
 			return null;
@@ -273,9 +611,15 @@ public class TAPTable implements DBTable {
 		TAPColumn removedColumn = columns.remove(columnName);
 		if (removedColumn != null)
 			deleteColumnRelations(removedColumn);
+
 		return removedColumn;
 	}
 
+	/**
+	 * Delete all foreign keys having the given column in the sources or the targets list.
+	 * 
+	 * @param col	A column.
+	 */
 	protected final void deleteColumnRelations(TAPColumn col){
 		// Remove the relation between the column and this table:
 		col.setTable(null);
@@ -292,6 +636,10 @@ public class TAPTable implements DBTable {
 		}
 	}
 
+	/**
+	 * Remove all columns composing this table.
+	 * Foreign keys will also be deleted.
+	 */
 	public final void removeAllColumns(){
 		Iterator<Map.Entry<String,TAPColumn>> it = columns.entrySet().iterator();
 		while(it.hasNext()){
@@ -301,7 +649,31 @@ public class TAPTable implements DBTable {
 		}
 	}
 
-	public final void addForeignKey(TAPForeignKey key) throws Exception{
+	/**
+	 * <p>Add the given foreign key to this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function will do nothing if the given foreign key is NULL.
+	 * </i></p>
+	 * 
+	 * <p><i><b>WARNING:</b>
+	 * 	The source table ({@link TAPForeignKey#getFromTable()}) of the given foreign key MUST be this table
+	 * 	and the foreign key MUST be completely defined.
+	 * 	If not, an exception will be thrown and the key won't be added.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given foreign key is added to this table, all the columns of this key will be
+	 * 	linked to the foreign key using either {@link TAPColumn#addSource(TAPForeignKey)} or
+	 * 	{@link TAPColumn#addTarget(TAPForeignKey)}.
+	 * </i></p>
+	 * 
+	 * @param key	Foreign key (whose the FROM table is this table) to add inside this table.
+	 * 
+	 * @throws TAPException	If the source table of the given foreign key is not this table
+	 *                     	or if the given key is not completely defined.
+	 */
+	public final void addForeignKey(TAPForeignKey key) throws TAPException{
 		if (key == null)
 			return;
 
@@ -309,57 +681,120 @@ public class TAPTable implements DBTable {
 		final String errorMsgPrefix = "Impossible to add the foreign key \"" + keyId + "\" because ";
 
 		if (key.getFromTable() == null)
-			throw new Exception(errorMsgPrefix + "no source table is specified !");
+			throw new TAPException(errorMsgPrefix + "no source table is specified !");
 
 		if (!this.equals(key.getFromTable()))
-			throw new Exception(errorMsgPrefix + "the source table is not \"" + getName() + "\"");
+			throw new TAPException(errorMsgPrefix + "the source table is not \"" + getADQLName() + "\"");
 
 		if (key.getTargetTable() == null)
-			throw new Exception(errorMsgPrefix + "no target table is specified !");
+			throw new TAPException(errorMsgPrefix + "no target table is specified !");
 
 		if (key.isEmpty())
-			throw new Exception(errorMsgPrefix + "it defines no relation !");
+			throw new TAPException(errorMsgPrefix + "it defines no relation !");
 
 		if (foreignKeys.add(key)){
 			try{
 				TAPTable targetTable = key.getTargetTable();
 				for(Map.Entry<String,String> relation : key){
 					if (!hasColumn(relation.getKey()))
-						throw new Exception(errorMsgPrefix + "the source column \"" + relation.getKey() + "\" doesn't exist in \"" + getName() + "\" !");
+						throw new TAPException(errorMsgPrefix + "the source column \"" + relation.getKey() + "\" doesn't exist in \"" + getName() + "\" !");
 					else if (!targetTable.hasColumn(relation.getValue()))
-						throw new Exception(errorMsgPrefix + "the target column \"" + relation.getValue() + "\" doesn't exist in \"" + targetTable.getName() + "\" !");
+						throw new TAPException(errorMsgPrefix + "the target column \"" + relation.getValue() + "\" doesn't exist in \"" + targetTable.getName() + "\" !");
 					else{
 						getColumn(relation.getKey()).addTarget(key);
 						targetTable.getColumn(relation.getValue()).addSource(key);
 					}
 				}
-			}catch(Exception ex){
+			}catch(TAPException ex){
 				foreignKeys.remove(key);
 				throw ex;
 			}
 		}
 	}
 
-	public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map<String,String> columns) throws Exception{
+	/**
+	 * <p>Build a foreign key using the ID, the target table and the given list of columns.
+	 * Then, add the created foreign key to this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The source table of the created foreign key ({@link TAPForeignKey#getFromTable()}) will be this table.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given foreign key is added to this table, all the columns of this key will be
+	 * 	linked to the foreign key using either {@link TAPColumn#addSource(TAPForeignKey)} or
+	 * 	{@link TAPColumn#addTarget(TAPForeignKey)}.
+	 * </i></p>
+	 * 
+	 * @return	The created and added foreign key.
+	 * 
+	 * @throws TAPException	If the specified key is not completely or correctly defined.
+	 * 
+	 * @see TAPForeignKey#TAPForeignKey(String, TAPTable, TAPTable, Map)
+	 */
+	public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map<String,String> columns) throws TAPException{
 		TAPForeignKey key = new TAPForeignKey(keyId, this, targetTable, columns);
 		addForeignKey(key);
 		return key;
 	}
 
-	public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map<String,String> columns, String description, String utype) throws Exception{
+	/**
+	 * <p>Build a foreign key using the ID, the target table, the given list of columns, the given description and the given UType.
+	 * Then, add the created foreign key to this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	The source table of the created foreign key ({@link TAPForeignKey#getFromTable()}) will be this table.
+	 * </i></p>
+	 * 
+	 * <p><i>Note:
+	 * 	If the given foreign key is added to this table, all the columns of this key will be
+	 * 	linked to the foreign key using either {@link TAPColumn#addSource(TAPForeignKey)} or
+	 * 	{@link TAPColumn#addTarget(TAPForeignKey)}.
+	 * </i></p>
+	 * 
+	 * @return	The created and added foreign key.
+	 * 
+	 * @throws TAPException	If the specified key is not completely or correctly defined.
+	 * 
+	 * @see TAPForeignKey#TAPForeignKey(String, TAPTable, TAPTable, Map, String, String)
+	 */
+	public TAPForeignKey addForeignKey(String keyId, TAPTable targetTable, Map<String,String> columns, String description, String utype) throws TAPException{
 		TAPForeignKey key = new TAPForeignKey(keyId, this, targetTable, columns, description, utype);
 		addForeignKey(key);
 		return key;
 	}
 
+	/**
+	 * Get the list of all foreign keys associated whose the source is this table.
+	 * 
+	 * @return	An iterator over all its foreign keys.
+	 */
 	public final Iterator<TAPForeignKey> getForeignKeys(){
 		return foreignKeys.iterator();
 	}
 
+	/**
+	 * Get the number of all foreign keys whose the source is this table
+	 * 
+	 * @return	Number of all its foreign keys.
+	 */
 	public final int getNbForeignKeys(){
 		return foreignKeys.size();
 	}
 
+	/**
+	 * <p>Remove the given foreign key from this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function will also delete the link between the columns of the foreign key
+	 * 	and the foreign key, using {@link #deleteRelations(TAPForeignKey)}.
+	 * </i></p>
+	 * 
+	 * @param keyToRemove	Foreign key to removed from this table.
+	 * 
+	 * @return	<i>true</i> if the key has been successfully removed,
+	 *        	<i>false</i> otherwise.
+	 */
 	public final boolean removeForeignKey(TAPForeignKey keyToRemove){
 		if (foreignKeys.remove(keyToRemove)){
 			deleteRelations(keyToRemove);
@@ -368,6 +803,14 @@ public class TAPTable implements DBTable {
 			return false;
 	}
 
+	/**
+	 * <p>Remove all the foreign keys whose the source is this table.</p>
+	 * 
+	 * <p><i>Note:
+	 * 	This function will also delete the link between the columns of all the removed foreign keys
+	 * 	and the foreign keys, using {@link #deleteRelations(TAPForeignKey)}.
+	 * </i></p>
+	 */
 	public final void removeAllForeignKeys(){
 		Iterator<TAPForeignKey> it = foreignKeys.iterator();
 		while(it.hasNext()){
@@ -376,6 +819,13 @@ public class TAPTable implements DBTable {
 		}
 	}
 
+	/**
+	 * Delete the link between all columns of the given foreign key
+	 * and this foreign key. Thus, these columns won't be anymore source or target
+	 * of this foreign key.
+	 * 
+	 * @param key	A foreign key whose links with its columns must be deleted.
+	 */
 	protected final void deleteRelations(TAPForeignKey key){
 		for(Map.Entry<String,String> relation : key){
 			TAPColumn col = key.getFromTable().getColumn(relation.getKey());
@@ -412,60 +862,7 @@ public class TAPTable implements DBTable {
 
 	@Override
 	public String toString(){
-		return ((schema != null) ? (schema.getName() + ".") : "") + adqlName;
-	}
-
-	public static void main(String[] args) throws Exception{
-		TAPSchema schema1 = new TAPSchema("monSchema1");
-		TAPSchema schema2 = new TAPSchema("monSchema2");
-
-		TAPTable tRef = schema1.addTable("ToRef");
-		tRef.addColumn("monMachin");
-
-		TAPTable t = schema2.addTable("Test");
-		t.addColumn("machin");
-		t.addColumn("truc");
-		HashMap<String,String> mapCols = new HashMap<String,String>();
-		mapCols.put("machin", "monMachin");
-		TAPForeignKey key = new TAPForeignKey("KeyID", t, tRef, mapCols);
-		t.addForeignKey(key);
-		mapCols = new HashMap<String,String>();
-		mapCols.put("truc", "monMachin");
-		key = new TAPForeignKey("2ndKey", t, tRef, mapCols);
-		t.addForeignKey(key);
-
-		printSchema(schema1);
-		printSchema(schema2);
-
-		System.out.println();
-
-		schema2.removeTable("Test");
-		printSchema(schema1);
-		printSchema(schema2);
-	}
-
-	public static void printSchema(TAPSchema schema){
-		System.out.println("*** SCHEMA \"" + schema.getName() + "\" ***");
-		for(TAPTable t : schema)
-			printTable(t);
-	}
-
-	public static void printTable(TAPTable t){
-		System.out.println("TABLE: " + t + "\nNb Columns: " + t.getNbColumns() + "\nNb Relations: " + t.getNbForeignKeys());
-		Iterator<TAPColumn> it = t.getColumns();
-		while(it.hasNext()){
-			TAPColumn col = it.next();
-			System.out.print("\t- " + col + "( ");
-			Iterator<TAPForeignKey> keys = col.getTargets();
-			while(keys.hasNext())
-				for(Map.Entry<String,String> relation : keys.next())
-					System.out.print(">" + relation.getKey() + "/" + relation.getValue() + " ");
-			keys = col.getSources();
-			while(keys.hasNext())
-				for(Map.Entry<String,String> relation : keys.next())
-					System.out.print("<" + relation.getKey() + "/" + relation.getValue() + " ");
-			System.out.println(")");
-		}
+		return ((schema != null) ? (schema.getADQLName() + ".") : "") + adqlName;
 	}
 
 	@Override
diff --git a/src/tap/upload/LimitedSizeInputStream.java b/src/tap/upload/LimitedSizeInputStream.java
index dd95527..2836545 100644
--- a/src/tap/upload/LimitedSizeInputStream.java
+++ b/src/tap/upload/LimitedSizeInputStream.java
@@ -49,7 +49,7 @@ public final class LimitedSizeInputStream extends InputStream {
 			counter += nbReads;
 			if (counter > sizeLimit){
 				exceed = true;
-				throw new ExceededSizeException();
+				throw new ExceededSizeException("Data read overflow: the limit of " + sizeLimit + " bytes has been reached!");
 			}
 		}
 	}
diff --git a/src/tap/upload/Uploader.java b/src/tap/upload/Uploader.java
index 76bbab4..8cef0db 100644
--- a/src/tap/upload/Uploader.java
+++ b/src/tap/upload/Uploader.java
@@ -17,7 +17,7 @@ package tap.upload;
  * along with TAPLibrary.  If not, see <http://www.gnu.org/licenses/>.
  * 
  * Copyright 2012-2014 - UDS/Centre de Données astronomiques de Strasbourg (CDS),
- *                       Astronomisches Rechen Institute (ARI)
+ *                       Astronomisches Rechen Institut (ARI)
  */
 
 import java.io.IOException;
@@ -30,7 +30,7 @@ import tap.data.DataReadException;
 import tap.data.VOTableIterator;
 import tap.db.DBConnection;
 import tap.metadata.TAPColumn;
-import tap.metadata.TAPDM;
+import tap.metadata.TAPMetadata.STDSchema;
 import tap.metadata.TAPSchema;
 import tap.metadata.TAPTable;
 
@@ -105,7 +105,7 @@ public class Uploader {
 	 * @see DBConnection#addUploadedTable(TAPTable, tap.data.TableIterator)
 	 */
 	public TAPSchema upload(final TableLoader[] loaders) throws TAPException{
-		TAPSchema uploadSchema = new TAPSchema(TAPDM.UPLOADSCHEMA.getLabel());
+		TAPSchema uploadSchema = new TAPSchema(STDSchema.UPLOADSCHEMA.getLabel());
 		InputStream votable = null;
 		String tableName = null;
 		try{
@@ -137,6 +137,7 @@ public class Uploader {
 
 				// Close the VOTable stream:
 				votable.close();
+				votable = null;
 			}
 		}catch(DataReadException dre){
 			if (dre.getCause() instanceof ExceededSizeException)
diff --git a/test/adql/IdentifierFieldTest.java b/test/adql/IdentifierFieldTest.java
new file mode 100644
index 0000000..8caeceb
--- /dev/null
+++ b/test/adql/IdentifierFieldTest.java
@@ -0,0 +1,25 @@
+package adql;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import adql.query.IdentifierField;
+
+public class IdentifierFieldTest {
+
+	@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/SearchColumnListTest.java b/test/adql/SearchColumnListTest.java
index 0a3b48d..b2e5988 100644
--- a/test/adql/SearchColumnListTest.java
+++ b/test/adql/SearchColumnListTest.java
@@ -8,6 +8,7 @@ import java.util.Map;
 import tap.metadata.TAPColumn;
 import tap.metadata.TAPSchema;
 import tap.metadata.TAPTable;
+import tap.metadata.TAPTable.TableType;
 import tap.metadata.TAPType;
 import tap.metadata.TAPType.TAPDatatype;
 import adql.db.DBColumn;
@@ -25,10 +26,10 @@ public class SearchColumnListTest {
 
 		/* SET THE TABLES AND COLUMNS NEEDED FOR THE TEST */
 		// Describe the available table:
-		TAPTable tableA = new TAPTable("A", "TABLE", "NATURAL JOIN Test table", null);
-		TAPTable tableB = new TAPTable("B", "TABLE", "NATURAL JOIN Test table", null);
-		TAPTable tableC = new TAPTable("C", "TABLE", "NATURAL JOIN Test table", null);
-		TAPTable tableD = new TAPTable("D", "TABLE", "NATURAL JOIN Test table", null);
+		TAPTable tableA = new TAPTable("A", TableType.table, "NATURAL JOIN Test table", null);
+		TAPTable tableB = new TAPTable("B", TableType.table, "NATURAL JOIN Test table", null);
+		TAPTable tableC = new TAPTable("C", TableType.table, "NATURAL JOIN Test table", null);
+		TAPTable tableD = new TAPTable("D", TableType.table, "NATURAL JOIN Test table", null);
 
 		// Describe its columns:
 		tableA.addColumn(new TAPColumn("id", new TAPType(TAPDatatype.VARCHAR), "Object ID"));
diff --git a/test/tap/data/ResultSetTableIteratorTest.java b/test/tap/data/ResultSetTableIteratorTest.java
index 99d7057..bc78917 100644
--- a/test/tap/data/ResultSetTableIteratorTest.java
+++ b/test/tap/data/ResultSetTableIteratorTest.java
@@ -33,16 +33,18 @@ public class ResultSetTableIteratorTest {
 			new ResultSetTableIterator(null);
 			fail("The constructor should have failed, because: the given ResultSet is NULL.");
 		}catch(Exception ex){
-			assertEquals(ex.getClass().getName(), "java.lang.NullPointerException");
+			assertEquals("java.lang.NullPointerException", ex.getClass().getName());
+			assertEquals("Missing ResultSet object over which to iterate!", ex.getMessage());
 		}
 	}
 
 	@Test
 	public void testWithData(){
+		TableIterator it = null;
 		try{
 			ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");
 
-			TableIterator it = new ResultSetTableIterator(rs);
+			it = new ResultSetTableIterator(rs);
 			// TEST there is column metadata before starting the iteration:
 			assertTrue(it.getMetadata() != null);
 			final int expectedNbLines = 10, expectedNbColumns = 4;
@@ -68,15 +70,22 @@ public class ResultSetTableIteratorTest {
 		}catch(Exception ex){
 			ex.printStackTrace(System.err);
 			fail("An exception occurs while reading a correct ResultSet (containing some valid rows).");
+		}finally{
+			if (it != null){
+				try{
+					it.close();
+				}catch(DataReadException dre){}
+			}
 		}
 	}
 
 	@Test
 	public void testWithEmptySet(){
+		TableIterator it = null;
 		try{
 			ResultSet rs = DBTools.select(conn, "SELECT * FROM gums WHERE id = 'foo';");
 
-			TableIterator it = new ResultSetTableIterator(rs);
+			it = new ResultSetTableIterator(rs);
 			// TEST there is column metadata before starting the iteration:
 			assertTrue(it.getMetadata() != null);
 			int countLines = 0;
@@ -89,6 +98,12 @@ public class ResultSetTableIteratorTest {
 		}catch(Exception ex){
 			ex.printStackTrace(System.err);
 			fail("An exception occurs while reading a correct ResultSet (containing some valid rows).");
+		}finally{
+			if (it != null){
+				try{
+					it.close();
+				}catch(DataReadException dre){}
+			}
 		}
 	}
 
diff --git a/test/tap/data/VOTableIteratorTest.java b/test/tap/data/VOTableIteratorTest.java
index cc78ad3..1bf1bf0 100644
--- a/test/tap/data/VOTableIteratorTest.java
+++ b/test/tap/data/VOTableIteratorTest.java
@@ -35,9 +35,10 @@ public class VOTableIteratorTest {
 	@Test
 	public void testWithData(){
 		InputStream input = null;
+		TableIterator it = null;
 		try{
 			input = new BufferedInputStream(new FileInputStream(dataVOTable));
-			TableIterator it = new VOTableIterator(input);
+			it = new VOTableIterator(input);
 			// TEST there is column metadata before starting the iteration:
 			assertTrue(it.getMetadata() != null);
 			final int expectedNbLines = 100, expectedNbColumns = 4;
@@ -70,15 +71,21 @@ public class VOTableIteratorTest {
 			}catch(IOException e){
 				e.printStackTrace();
 			}
+			if (it != null){
+				try{
+					it.close();
+				}catch(DataReadException dre){}
+			}
 		}
 	}
 
 	@Test
 	public void testWithBinary(){
 		InputStream input = null;
+		TableIterator it = null;
 		try{
 			input = new BufferedInputStream(new FileInputStream(binaryVOTable));
-			TableIterator it = new VOTableIterator(input);
+			it = new VOTableIterator(input);
 			// TEST there is column metadata before starting the iteration:
 			assertTrue(it.getMetadata() != null);
 			final int expectedNbLines = 100, expectedNbColumns = 4;
@@ -111,15 +118,21 @@ public class VOTableIteratorTest {
 			}catch(IOException e){
 				e.printStackTrace();
 			}
+			if (it != null){
+				try{
+					it.close();
+				}catch(DataReadException dre){}
+			}
 		}
 	}
 
 	@Test
 	public void testWithEmptySet(){
 		InputStream input = null;
+		TableIterator it = null;
 		try{
 			input = new BufferedInputStream(new FileInputStream(emptyVOTable));
-			TableIterator it = new VOTableIterator(input);
+			it = new VOTableIterator(input);
 			// TEST there is column metadata before starting the iteration:
 			assertTrue(it.getMetadata() != null);
 			int countLines = 0;
@@ -139,15 +152,21 @@ public class VOTableIteratorTest {
 			}catch(IOException e){
 				e.printStackTrace();
 			}
+			if (it != null){
+				try{
+					it.close();
+				}catch(DataReadException dre){}
+			}
 		}
 	}
 
 	@Test
 	public void testWithEmptyBinarySet(){
 		InputStream input = null;
+		TableIterator it = null;
 		try{
 			input = new BufferedInputStream(new FileInputStream(emptyBinaryVOTable));
-			TableIterator it = new VOTableIterator(input);
+			it = new VOTableIterator(input);
 			// TEST there is column metadata before starting the iteration:
 			assertTrue(it.getMetadata() != null);
 			int countLines = 0;
@@ -167,6 +186,11 @@ public class VOTableIteratorTest {
 			}catch(IOException e){
 				e.printStackTrace();
 			}
+			if (it != null){
+				try{
+					it.close();
+				}catch(DataReadException dre){}
+			}
 		}
 	}
 }
diff --git a/test/tap/db/JDBCConnectionTest.java b/test/tap/db/JDBCConnectionTest.java
new file mode 100644
index 0000000..2cd7457
--- /dev/null
+++ b/test/tap/db/JDBCConnectionTest.java
@@ -0,0 +1,1015 @@
+package tap.db;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import tap.data.DataReadException;
+import tap.data.TableIterator;
+import tap.data.VOTableIterator;
+import tap.metadata.TAPColumn;
+import tap.metadata.TAPForeignKey;
+import tap.metadata.TAPMetadata;
+import tap.metadata.TAPMetadata.STDSchema;
+import tap.metadata.TAPMetadata.STDTable;
+import tap.metadata.TAPSchema;
+import tap.metadata.TAPTable;
+import tap.metadata.TAPType;
+import tap.metadata.TAPType.TAPDatatype;
+import testtools.DBTools;
+import adql.db.DBChecker;
+import adql.db.DBColumn;
+import adql.db.DBTable;
+import adql.parser.ADQLParser;
+import adql.parser.ParseException;
+import adql.query.ADQLQuery;
+import adql.query.IdentifierField;
+import adql.translator.PostgreSQLTranslator;
+
+public class JDBCConnectionTest {
+
+	private static Connection pgConnection;
+	private static JDBCConnection pgJDBCConnection;
+	private static JDBCConnection sensPgJDBCConnection;
+
+	private static Connection sqliteConnection;
+	private static JDBCConnection sqliteJDBCConnection;
+	private static JDBCConnection sensSqliteJDBCConnection;
+
+	private static String uploadExamplePath;
+
+	@BeforeClass
+	public static void setUpBeforeClass() throws Exception{
+
+		String projectDir = (new File("")).getAbsolutePath();
+		uploadExamplePath = projectDir + "/test/tap/db/upload_example.vot";
+
+		final String sqliteDbFile = projectDir + "/test/tap/db/TestTAPDb.db";
+
+		pgConnection = DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd");
+		pgJDBCConnection = new JDBCConnection(pgConnection, new PostgreSQLTranslator(false), "POSTGRES", null);
+		sensPgJDBCConnection = new JDBCConnection(pgConnection, new PostgreSQLTranslator(true, true, true, true), "SensitivePSQL", null);
+
+		sqliteConnection = DBTools.createConnection("sqlite", null, null, sqliteDbFile, null, null);
+		sqliteJDBCConnection = new JDBCConnection(sqliteConnection, new PostgreSQLTranslator(false), "SQLITE", null);
+		sensSqliteJDBCConnection = new JDBCConnection(sqliteConnection, new PostgreSQLTranslator(true), "SensitiveSQLite", null);
+
+	}
+
+	@AfterClass
+	public static void tearDownAfterClass() throws Exception{
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+			dropSchema(STDSchema.TAPSCHEMA.label, conn);
+			dropSchema(STDSchema.UPLOADSCHEMA.label, conn);
+		}
+		pgConnection.close();
+		sqliteConnection.close();
+	}
+
+	/* ***** */
+	/* TESTS */
+	/* ***** */
+
+	@Test
+	public void testGetTAPSchemaTablesDef(){
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+			TAPMetadata meta = createCustomSchema();
+			TAPTable customColumns = meta.getTable(STDSchema.TAPSCHEMA.toString(), STDTable.COLUMNS.toString());
+			TAPTable[] tapTables = conn.mergeTAPSchemaDefs(meta);
+			TAPSchema stdSchema = TAPMetadata.getStdSchema();
+			assertEquals(5, tapTables.length);
+			assertTrue(equals(tapTables[0], stdSchema.getTable(STDTable.SCHEMAS.label)));
+			assertEquals(customColumns.getSchema(), tapTables[0].getSchema());
+			assertTrue(equals(tapTables[1], stdSchema.getTable(STDTable.TABLES.label)));
+			assertEquals(customColumns.getSchema(), tapTables[1].getSchema());
+			assertTrue(equals(tapTables[2], customColumns));
+			assertTrue(equals(tapTables[3], stdSchema.getTable(STDTable.KEYS.label)));
+			assertEquals(customColumns.getSchema(), tapTables[3].getSchema());
+			assertTrue(equals(tapTables[4], stdSchema.getTable(STDTable.KEY_COLUMNS.label)));
+			assertEquals(customColumns.getSchema(), tapTables[4].getSchema());
+		}
+	}
+
+	@Test
+	public void testSetTAPSchema(){
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+			short cnt = -1;
+			while(cnt < 1){
+				/* NO CUSTOM DEFINITION */
+				// Prepare the test:
+				if (cnt == -1)
+					dropSchema(STDSchema.TAPSCHEMA.label, conn);
+				else
+					createTAPSchema(conn);
+				// Do the test:
+				try{
+					TAPMetadata meta = new TAPMetadata();
+					int[] expectedCounts = getStats(meta);
+					conn.setTAPSchema(meta);
+					int[] effectiveCounts = getStats(conn, meta);
+					for(int i = 0; i < expectedCounts.length; i++)
+						assertEquals(expectedCounts[i], effectiveCounts[i]);
+				}catch(DBException dbe){
+					dbe.printStackTrace(System.err);
+					fail("[" + conn.getID() + ";no def] No error should happen here ; when an empty list of metadata is given, at least the TAP_SCHEMA should be created and filled with a description of itself.");
+				}
+
+				/* CUSTOM DEFINITION */
+				// Prepare the test:
+				if (cnt == -1)
+					dropSchema(STDSchema.TAPSCHEMA.label, conn);
+				// Do the test:
+				try{
+					TAPMetadata meta = createCustomSchema();
+					int[] expectedCounts = getStats(meta);
+					conn.setTAPSchema(meta);
+					int[] effectiveCounts = getStats(conn, meta);
+					for(int i = 0; i < expectedCounts.length; i++)
+						assertEquals(expectedCounts[i], effectiveCounts[i]);
+				}catch(DBException dbe){
+					dbe.printStackTrace(System.err);
+					fail("[" + conn.getID() + ";custom def] No error should happen here!");
+				}
+
+				cnt++;
+			}
+		}
+	}
+
+	@Test
+	public void testGetCreationOrder(){
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+			assertEquals(-1, conn.getCreationOrder(null));
+			assertEquals(0, conn.getCreationOrder(STDTable.SCHEMAS));
+			assertEquals(1, conn.getCreationOrder(STDTable.TABLES));
+			assertEquals(2, conn.getCreationOrder(STDTable.COLUMNS));
+			assertEquals(3, conn.getCreationOrder(STDTable.KEYS));
+			assertEquals(4, conn.getCreationOrder(STDTable.KEY_COLUMNS));
+		}
+	}
+
+	@Test
+	public void testGetDBMSDatatype(){
+		assertEquals("VARCHAR", pgJDBCConnection.getDBMSDatatype(null));
+		assertEquals("TEXT", sqliteJDBCConnection.getDBMSDatatype(null));
+
+		assertEquals("bytea", pgJDBCConnection.getDBMSDatatype(new TAPType(TAPDatatype.VARBINARY)));
+		assertEquals("BLOB", sqliteJDBCConnection.getDBMSDatatype(new TAPType(TAPDatatype.VARBINARY)));
+	}
+
+	@Test
+	public void testMergeTAPSchemaDefs(){
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+
+			// TEST WITH NO METADATA OBJECT:
+			// -> expected: throws a NULL exception.
+			try{
+				conn.mergeTAPSchemaDefs(null);
+			}catch(Exception e){
+				assertEquals(NullPointerException.class, e.getClass());
+			}
+
+			// TEST WITH EMPTY METADATA OBJECT:
+			// -> expected: returns at least the 5 tables of the TAP_SCHEMA.
+			TAPTable[] stdTables = conn.mergeTAPSchemaDefs(new TAPMetadata());
+
+			assertEquals(5, stdTables.length);
+
+			for(TAPTable t : stdTables)
+				assertEquals(STDSchema.TAPSCHEMA.toString(), t.getADQLSchemaName());
+
+			assertEquals(STDTable.SCHEMAS.toString(), stdTables[0].getADQLName());
+			assertEquals(STDTable.TABLES.toString(), stdTables[1].getADQLName());
+			assertEquals(STDTable.COLUMNS.toString(), stdTables[2].getADQLName());
+			assertEquals(STDTable.KEYS.toString(), stdTables[3].getADQLName());
+			assertEquals(STDTable.KEY_COLUMNS.toString(), stdTables[4].getADQLName());
+
+			// TEST WITH INCOMPLETE TAP_SCHEMA TABLES LIST + 1 CUSTOM TAP_SCHEMA TABLE (here: TAP_SCHEMA.columns):
+			// -> expected: the 5 tables of the TAP_SCHEMA including the modification of the standard tables & ignore the additional table(s) if any (which is the case here).
+			TAPMetadata customMeta = createCustomSchema();
+			stdTables = conn.mergeTAPSchemaDefs(customMeta);
+
+			assertEquals(5, stdTables.length);
+
+			for(TAPTable t : stdTables)
+				assertEquals(STDSchema.TAPSCHEMA.toString(), t.getADQLSchemaName());
+
+			assertEquals(STDTable.SCHEMAS.toString(), stdTables[0].getADQLName());
+			assertEquals(STDTable.TABLES.toString(), stdTables[1].getADQLName());
+			assertEquals(STDTable.COLUMNS.toString(), stdTables[2].getADQLName());
+			assertEquals("Columns", stdTables[2].getDBName());
+			assertNotNull(stdTables[2].getColumn("TestNewColumn"));
+			assertEquals(STDTable.KEYS.toString(), stdTables[3].getADQLName());
+			assertEquals(STDTable.KEY_COLUMNS.toString(), stdTables[4].getADQLName());
+		}
+	}
+
+	@Test
+	public void testEquals(){
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+			// NULL tests:
+			assertFalse(conn.equals("tap_schema", null, false));
+			assertFalse(conn.equals("tap_schema", null, true));
+			assertFalse(conn.equals(null, "tap_schema", false));
+			assertFalse(conn.equals(null, "tap_schema", true));
+			assertFalse(conn.equals(null, null, false));
+			assertFalse(conn.equals(null, null, true));
+
+			// CASE SENSITIVE tests:
+			if (conn.supportsMixedCaseQuotedIdentifier || conn.mixedCaseQuoted){
+				assertFalse(conn.equals("tap_schema", "TAP_SCHEMA", true));
+				assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true));
+				assertFalse(conn.equals("TAP_SCHEMA", "tap_schema", true));
+				assertFalse(conn.equals("Columns", "columns", true));
+				assertFalse(conn.equals("columns", "Columns", true));
+			}else if (conn.lowerCaseQuoted){
+				assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", true));
+				assertFalse(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true));
+				assertFalse(conn.equals("TAP_SCHEMA", "tap_schema", true));
+				assertFalse(conn.equals("Columns", "columns", true));
+				assertTrue(conn.equals("columns", "Columns", true));
+			}else if (conn.upperCaseQuoted){
+				assertFalse(conn.equals("tap_schema", "TAP_SCHEMA", true));
+				assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true));
+				assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", true));
+				assertFalse(conn.equals("Columns", "columns", true));
+				assertFalse(conn.equals("columns", "Columns", true));
+			}else{
+				assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", true));
+				assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", true));
+				assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", true));
+				assertTrue(conn.equals("Columns", "columns", true));
+				assertTrue(conn.equals("columns", "Columns", true));
+			}
+
+			// CASE INSENSITIVE tests:
+			if (conn.supportsMixedCaseUnquotedIdentifier){
+				assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", false));
+				assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false));
+				assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", false));
+				assertTrue(conn.equals("Columns", "columns", false));
+				assertTrue(conn.equals("columns", "Columns", false));
+			}else if (conn.lowerCaseUnquoted){
+				assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", false));
+				assertFalse(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false));
+				assertFalse(conn.equals("TAP_SCHEMA", "tap_schema", false));
+				assertFalse(conn.equals("Columns", "columns", false));
+				assertTrue(conn.equals("columns", "Columns", false));
+			}else if (conn.upperCaseUnquoted){
+				assertFalse(conn.equals("tap_schema", "TAP_SCHEMA", false));
+				assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false));
+				assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", false));
+				assertFalse(conn.equals("Columns", "columns", false));
+				assertFalse(conn.equals("columns", "Columns", false));
+			}else{
+				assertTrue(conn.equals("tap_schema", "TAP_SCHEMA", false));
+				assertTrue(conn.equals("TAP_SCHEMA", "TAP_SCHEMA", false));
+				assertTrue(conn.equals("TAP_SCHEMA", "tap_schema", false));
+				assertTrue(conn.equals("Columns", "columns", false));
+				assertTrue(conn.equals("columns", "Columns", false));
+			}
+		}
+	}
+
+	@Test
+	public void testGetTAPSchema(){
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+			try{
+				// Prepare the test:
+				createTAPSchema(conn);
+				// Try to get it (which should work without any problem here):
+				conn.getTAPSchema();
+			}catch(DBException de){
+				de.printStackTrace(System.err);
+				fail("No pbm should happen here (either for the creation of a std TAP_SCHEMA or for its reading)! CAUSE: " + de.getMessage());
+			}
+
+			try{
+				// Prepare the test:
+				dropSchema(STDSchema.TAPSCHEMA.label, conn);
+				// Try to get it (which should work without any problem here):
+				conn.getTAPSchema();
+				fail("DBException expected, because none of the TAP_SCHEMA tables exist.");
+			}catch(DBException de){
+				assertTrue(de.getMessage().equals("Impossible to load schemas from TAP_SCHEMA.schemas!"));
+			}
+		}
+	}
+
+	@Test
+	public void testIsTableExisting(){
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+			try{
+				// Get the database metadata:
+				DatabaseMetaData dbMeta = conn.connection.getMetaData();
+
+				// Prepare the test:
+				createTAPSchema(conn);
+				// Test the existence of all TAP_SCHEMA tables:
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.SCHEMAS.label, dbMeta));
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.TABLES.label, dbMeta));
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.COLUMNS.label, dbMeta));
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.KEYS.label, dbMeta));
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.KEY_COLUMNS.label, dbMeta));
+				// Test the non-existence of any other table:
+				assertFalse(conn.isTableExisting(null, "foo", dbMeta));
+
+				// Prepare the test:
+				dropSchema(STDSchema.TAPSCHEMA.label, conn);
+				// Test the non-existence of all TAP_SCHEMA tables:
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.SCHEMAS.label, dbMeta));
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.TABLES.label, dbMeta));
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.COLUMNS.label, dbMeta));
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.KEYS.label, dbMeta));
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, STDTable.KEY_COLUMNS.label, dbMeta));
+			}catch(Exception ex){
+				ex.printStackTrace(System.err);
+				fail("{" + conn.getID() + "} Testing the existence of a table should not throw an error!");
+			}
+		}
+	}
+
+	@Test
+	public void testAddUploadedTable(){
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		TAPTable tableDef = null;
+		for(JDBCConnection conn : connections){
+			InputStream io = null;
+			try{
+				io = new FileInputStream(uploadExamplePath);
+				TableIterator it = new VOTableIterator(io);
+
+				TAPColumn[] cols = it.getMetadata();
+				tableDef = new TAPTable("UploadExample");
+				for(TAPColumn c : cols)
+					tableDef.addColumn(c);
+
+				// Prepare the test: no TAP_UPLOAD schema and no table TAP_UPLOAD.UploadExample:
+				dropSchema(STDSchema.UPLOADSCHEMA.label, conn);
+				// Test:
+				try{
+					assertTrue(conn.addUploadedTable(tableDef, it));
+				}catch(Exception ex){
+					ex.printStackTrace(System.err);
+					fail("{" + conn.ID + "} This error should not happen: no TAP_UPLOAD schema.");
+				}
+
+				close(io);
+				io = new FileInputStream(uploadExamplePath);
+				it = new VOTableIterator(io);
+
+				// Prepare the test: the TAP_UPLOAD schema exist but not the table TAP_UPLOAD.UploadExample:
+				dropTable(tableDef.getDBSchemaName(), tableDef.getDBName(), conn);
+				// Test:
+				try{
+					assertTrue(conn.addUploadedTable(tableDef, it));
+				}catch(Exception ex){
+					ex.printStackTrace(System.err);
+					fail("{" + conn.ID + "} This error should not happen: no TAP_UPLOAD schema.");
+				}
+
+				close(io);
+				io = new FileInputStream(uploadExamplePath);
+				it = new VOTableIterator(io);
+
+				// Prepare the test: the TAP_UPLOAD schema and the table TAP_UPLOAD.UploadExample BOTH exist:
+				;
+				// Test:
+				try{
+					assertFalse(conn.addUploadedTable(tableDef, it));
+				}catch(Exception ex){
+					if (ex instanceof DBException)
+						assertEquals("Impossible to create the user uploaded table in the database: " + conn.translator.getQualifiedTableName(tableDef) + "! This table already exists.", ex.getMessage());
+					else{
+						ex.printStackTrace(System.err);
+						fail("{" + conn.ID + "} DBException was the expected exception!");
+					}
+				}
+
+			}catch(Exception ex){
+				ex.printStackTrace(System.err);
+				fail("{" + conn.ID + "} This error should never happen except there is a problem with the file (" + uploadExamplePath + ").");
+			}finally{
+				close(io);
+			}
+		}
+	}
+
+	@Test
+	public void testDropUploadedTable(){
+		TAPTable tableDef = new TAPTable("TableToDrop");
+		TAPSchema uploadSchema = new TAPSchema(STDSchema.UPLOADSCHEMA.label);
+		uploadSchema.addTable(tableDef);
+
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+			try{
+				// 1st TEST CASE: the schema TAP_UPLOAD does not exist -> no error should be raised!
+				// drop the TAP_UPLOAD schema:
+				dropSchema(uploadSchema.getDBName(), conn);
+				// try to drop the table:
+				assertTrue(conn.dropUploadedTable(tableDef));
+
+				// 2nd TEST CASE: the table does not exists -> no error should be raised!
+				// create the TAP_UPLOAD schema, but not the table:
+				createSchema(uploadSchema.getDBName(), conn);
+				// try to drop the table:
+				assertTrue(conn.dropUploadedTable(tableDef));
+
+				// 3rd TEST CASE: the table and the schema exist -> the table should be created without any error!
+				// create the fake uploaded table:
+				createFooTable(tableDef.getDBSchemaName(), tableDef.getDBName(), conn);
+				// try to drop the table:
+				assertTrue(conn.dropUploadedTable(tableDef));
+
+			}catch(Exception ex){
+				ex.printStackTrace(System.err);
+				fail("{" + conn.ID + "} This error should not happen. The table should be dropped and even if it does not exist, no error should be thrown.");
+			}
+		}
+	}
+
+	@Test
+	public void testExecuteQuery(){
+		TAPSchema schema = TAPMetadata.getStdSchema();
+		ArrayList<DBTable> tables = new ArrayList<DBTable>(schema.getNbTables());
+		for(TAPTable t : schema)
+			tables.add(t);
+
+		ADQLParser parser = new ADQLParser(new DBChecker(tables));
+		parser.setDebug(false);
+
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		for(JDBCConnection conn : connections){
+			if (conn.ID.equalsIgnoreCase("SQLITE")){
+				for(DBTable t : tables){
+					TAPTable tapT = (TAPTable)t;
+					tapT.getSchema().setDBName(null);
+					tapT.setDBName(tapT.getSchema().getADQLName() + "_" + tapT.getDBName());
+				}
+			}
+
+			TableIterator result = null;
+			try{
+				// Prepare the test: create the TAP_SCHEMA:
+				dropSchema(STDSchema.TAPSCHEMA.label, conn);
+				// Build the ADQLQuery object:
+				ADQLQuery query = parser.parseQuery("SELECT table_name FROM TAP_SCHEMA.tables;");
+				// Execute the query:
+				result = conn.executeQuery(query);
+				fail("{" + conn.ID + "} This test should have failed because TAP_SCHEMA was supposed to not exist!");
+			}catch(DBException de){
+				assertTrue(de.getMessage().startsWith("Unexpected error while executing a SQL query: "));
+				assertTrue(de.getMessage().indexOf("tap_schema") > 0 || de.getMessage().indexOf("TAP_SCHEMA") > 0);
+			}catch(ParseException pe){
+				pe.printStackTrace(System.err);
+				fail("There should be no pbm to parse the ADQL expression!");
+			}finally{
+				if (result != null){
+					try{
+						result.close();
+					}catch(DataReadException de){}
+					result = null;
+				}
+			}
+
+			try{
+				// Prepare the test: create the TAP_SCHEMA:
+				createTAPSchema(conn);
+				// Build the ADQLQuery object:
+				ADQLQuery query = parser.parseQuery("SELECT table_name FROM TAP_SCHEMA.tables;");
+				// Execute the query:
+				result = conn.executeQuery(query);
+				assertEquals(1, result.getMetadata().length);
+				int cntRow = 0;
+				while(result.nextRow()){
+					cntRow++;
+					assertTrue(result.hasNextCol());
+					assertNotNull(TAPMetadata.resolveStdTable((String)result.nextCol()));
+					assertFalse(result.hasNextCol());
+				}
+				assertEquals(5, cntRow);
+			}catch(DBException de){
+				de.printStackTrace(System.err);
+				fail("No ADQL/SQL query error was expected here!");
+			}catch(ParseException pe){
+				fail("There should be no pbm to parse the ADQL expression!");
+			}catch(DataReadException e){
+				e.printStackTrace(System.err);
+				fail("There should be no pbm when accessing rows and the first (and only) columns of the result!");
+			}catch(Exception ex){
+				ex.printStackTrace(System.err);
+				fail("There should be no pbm when reading the query result!");
+			}finally{
+				if (result != null){
+					try{
+						result.close();
+					}catch(DataReadException de){}
+					result = null;
+				}
+			}
+		}
+	}
+
+	/* ************** */
+	/* TOOL FUNCTIONS */
+	/* ************** */
+
+	public final static void main(final String[] args) throws Throwable{
+		JDBCConnection conn = new JDBCConnection(DBTools.createConnection("postgresql", "127.0.0.1", null, "gmantele", "gmantele", "pwd"), new PostgreSQLTranslator(), "TEST_POSTGRES", null);
+		JDBCConnectionTest.createTAPSchema(conn);
+		JDBCConnectionTest.dropSchema(STDSchema.TAPSCHEMA.label, conn);
+	}
+
+	private static void dropSchema(final String schemaName, final JDBCConnection conn){
+		Statement stmt = null;
+		ResultSet rs = null;
+		try{
+			stmt = conn.connection.createStatement();
+
+			final boolean caseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA);
+			if (conn.supportsSchema)
+				stmt.executeUpdate("DROP SCHEMA IF EXISTS " + formatIdentifier(schemaName, caseSensitive) + " CASCADE;");
+			else{
+				startTransaction(conn);
+				final String tablePrefix = conn.getTablePrefix(schemaName);
+				final int prefixLen = tablePrefix.length();
+				if (prefixLen <= 0)
+					return;
+				rs = conn.connection.getMetaData().getTables(null, null, null, null);
+				ArrayList<String> tablesToDrop = new ArrayList<String>();
+				while(rs.next()){
+					String table = rs.getString(3);
+					if (table.length() > prefixLen){
+						if (equals(schemaName, table.substring(0, prefixLen - 1), caseSensitive))
+							tablesToDrop.add(table);
+					}
+				}
+				close(rs);
+				rs = null;
+				for(String t : tablesToDrop)
+					stmt.executeUpdate("DROP TABLE IF EXISTS \"" + t + "\";");
+				commit(conn);
+			}
+		}catch(Exception ex){
+			rollback(conn);
+			ex.printStackTrace(System.err);
+			fail("{" + conn.ID + "} Impossible to prepare a test by: dropping the schema " + schemaName + "!");
+		}finally{
+			close(rs);
+			close(stmt);
+		}
+	}
+
+	private static void dropTable(final String schemaName, final String tableName, final JDBCConnection conn){
+		Statement stmt = null;
+		ResultSet rs = null;
+		try{
+			final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA);
+			final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE);
+			stmt = conn.connection.createStatement();
+			if (conn.supportsSchema)
+				stmt.executeUpdate("DROP TABLE IF EXISTS " + formatIdentifier(schemaName, sCaseSensitive) + "." + formatIdentifier(tableName, tCaseSensitive) + ";");
+			else{
+				final String tablePrefix = conn.getTablePrefix(schemaName);
+				final int prefixLen = tablePrefix.length();
+				rs = conn.connection.getMetaData().getTables(null, null, null, null);
+				String tableToDrop = null;
+				while(rs.next()){
+					String table = rs.getString(3);
+					if (prefixLen <= 0 && equals(tableName, table, tCaseSensitive)){
+						tableToDrop = table;
+						break;
+					}else if (prefixLen > 0 && table.length() > prefixLen){
+						if (equals(schemaName, table.substring(0, prefixLen - 1), sCaseSensitive) && equals(tableName, table.substring(prefixLen + 1), tCaseSensitive)){
+							tableToDrop = table;
+							break;
+						}
+					}
+				}
+				close(rs);
+				if (tableToDrop != null)
+					stmt.executeUpdate("DROP TABLE IF EXISTS \"" + tableToDrop + "\";");
+			}
+		}catch(Exception ex){
+			ex.printStackTrace(System.err);
+			fail("{" + conn.ID + "} Impossible to prepare a test by: dropping the table " + schemaName + "." + tableName + "!");
+		}finally{
+			close(rs);
+			close(stmt);
+		}
+	}
+
+	private static void createSchema(final String schemaName, final JDBCConnection conn){
+		if (!conn.supportsSchema)
+			return;
+
+		dropSchema(schemaName, conn);
+
+		Statement stmt = null;
+		ResultSet rs = null;
+		try{
+			final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA);
+			stmt = conn.connection.createStatement();
+			stmt.executeUpdate("CREATE SCHEMA " + formatIdentifier(schemaName, sCaseSensitive) + ";");
+		}catch(Exception ex){
+			ex.printStackTrace(System.err);
+			fail("{" + conn.ID + "} Impossible to prepare a test by: creating the schema " + schemaName + "!");
+		}finally{
+			close(rs);
+			close(stmt);
+		}
+	}
+
+	private static void createFooTable(final String schemaName, final String tableName, final JDBCConnection conn){
+		dropTable(schemaName, tableName, conn);
+
+		Statement stmt = null;
+		ResultSet rs = null;
+		try{
+			final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA);
+			final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE);
+			String tablePrefix = formatIdentifier(schemaName, sCaseSensitive);
+			if (tablePrefix == null)
+				tablePrefix = "";
+			else
+				tablePrefix += (conn.supportsSchema ? "." : "_");
+			stmt = conn.connection.createStatement();
+			stmt.executeUpdate("CREATE TABLE " + tablePrefix + formatIdentifier(tableName, tCaseSensitive) + " (ID integer);");
+		}catch(Exception ex){
+			ex.printStackTrace(System.err);
+			fail("{" + conn.ID + "} Impossible to prepare a test by: creating the table " + schemaName + "." + tableName + "!");
+		}finally{
+			close(rs);
+			close(stmt);
+		}
+	}
+
+	private static void createTAPSchema(final JDBCConnection conn){
+		dropSchema(STDSchema.TAPSCHEMA.label, conn);
+
+		Statement stmt = null;
+		try{
+			final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA);
+			final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE);
+			String[] tableNames = new String[]{STDTable.SCHEMAS.label,STDTable.TABLES.label,STDTable.COLUMNS.label,STDTable.KEYS.label,STDTable.KEY_COLUMNS.label};
+			if (conn.supportsSchema){
+				for(int i = 0; i < tableNames.length; i++)
+					tableNames[i] = formatIdentifier(STDSchema.TAPSCHEMA.label, sCaseSensitive) + "." + formatIdentifier(tableNames[i], tCaseSensitive);
+			}else{
+				for(int i = 0; i < tableNames.length; i++)
+					tableNames[i] = formatIdentifier(conn.getTablePrefix(STDSchema.TAPSCHEMA.label) + tableNames[i], tCaseSensitive);
+			}
+
+			startTransaction(conn);
+
+			stmt = conn.connection.createStatement();
+
+			if (conn.supportsSchema)
+				stmt.executeUpdate("CREATE SCHEMA " + formatIdentifier(STDSchema.TAPSCHEMA.label, sCaseSensitive) + ";");
+
+			stmt.executeUpdate("CREATE TABLE " + tableNames[0] + "(\"schema_name\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR, PRIMARY KEY(\"schema_name\"));");
+			stmt.executeUpdate("DELETE FROM " + tableNames[0] + ";");
+
+			stmt.executeUpdate("CREATE TABLE " + tableNames[1] + "(\"schema_name\" VARCHAR,\"table_name\" VARCHAR,\"table_type\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR, PRIMARY KEY(\"schema_name\", \"table_name\"));");
+			stmt.executeUpdate("DELETE FROM " + tableNames[1] + ";");
+
+			stmt.executeUpdate("CREATE TABLE " + tableNames[2] + "(\"table_name\" VARCHAR,\"column_name\" VARCHAR,\"description\" VARCHAR,\"unit\" VARCHAR,\"ucd\" VARCHAR,\"utype\" VARCHAR,\"datatype\" VARCHAR,\"size\" INTEGER,\"principal\" INTEGER,\"indexed\" INTEGER,\"std\" INTEGER, PRIMARY KEY(\"table_name\", \"column_name\"));");
+			stmt.executeUpdate("DELETE FROM " + tableNames[2] + ";");
+
+			stmt.executeUpdate("CREATE TABLE " + tableNames[3] + "(\"key_id\" VARCHAR,\"from_table\" VARCHAR,\"target_table\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR, PRIMARY KEY(\"key_id\"));");
+			stmt.executeUpdate("DELETE FROM " + tableNames[3] + ";");
+
+			stmt.executeUpdate("CREATE TABLE " + tableNames[4] + "(\"key_id\" VARCHAR,\"from_column\" VARCHAR,\"target_column\" VARCHAR, PRIMARY KEY(\"key_id\"));");
+			stmt.executeUpdate("DELETE FROM " + tableNames[4] + ";");
+
+			TAPMetadata metadata = new TAPMetadata();
+			metadata.addSchema(TAPMetadata.getStdSchema());
+
+			ArrayList<TAPTable> lstTables = new ArrayList<TAPTable>();
+			for(TAPSchema schema : metadata){
+				stmt.executeUpdate("INSERT INTO " + tableNames[0] + " VALUES('" + schema.getADQLName() + "','" + schema.getDescription() + "','" + schema.getUtype() + "')");
+				for(TAPTable t : schema)
+					lstTables.add(t);
+			}
+
+			ArrayList<DBColumn> lstCols = new ArrayList<DBColumn>();
+			for(TAPTable table : lstTables){
+				stmt.executeUpdate("INSERT INTO " + tableNames[1] + " VALUES('" + table.getADQLSchemaName() + "','" + table.getADQLName() + "','" + table.getType() + "','" + table.getDescription() + "','" + table.getUtype() + "')");
+				for(DBColumn c : table)
+					lstCols.add(c);
+
+			}
+			lstTables = null;
+
+			for(DBColumn c : lstCols){
+				TAPColumn col = (TAPColumn)c;
+				stmt.executeUpdate("INSERT INTO " + tableNames[2] + " VALUES('" + col.getTable().getADQLName() + "','" + col.getADQLName() + "','" + col.getDescription() + "','" + col.getUnit() + "','" + col.getUcd() + "','" + col.getUtype() + "','" + col.getDatatype().type + "'," + col.getDatatype().length + "," + (col.isPrincipal() ? 1 : 0) + "," + (col.isIndexed() ? 1 : 0) + "," + (col.isStd() ? 1 : 0) + ")");
+			}
+
+			commit(conn);
+
+		}catch(Exception ex){
+			rollback(conn);
+			ex.printStackTrace(System.err);
+			fail("{" + conn.ID + "} Impossible to prepare a test by: creating TAP_SCHEMA!");
+		}finally{
+			close(stmt);
+		}
+	}
+
+	private static void startTransaction(final JDBCConnection conn){
+		try{
+			conn.connection.setAutoCommit(false);
+		}catch(SQLException se){}
+	}
+
+	private static void commit(final JDBCConnection conn){
+		try{
+			conn.connection.commit();
+			conn.connection.setAutoCommit(true);
+		}catch(SQLException se){}
+
+	}
+
+	private static void rollback(final JDBCConnection conn){
+		try{
+			conn.connection.rollback();
+			conn.connection.setAutoCommit(true);
+		}catch(SQLException se){}
+
+	}
+
+	private static String formatIdentifier(final String identifier, final boolean caseSensitive){
+		if (identifier == null)
+			return null;
+		else if (identifier.charAt(0) == '"')
+			return identifier;
+		else if (caseSensitive)
+			return "\"" + identifier + "\"";
+		else
+			return identifier;
+	}
+
+	private static boolean equals(final String name1, final String name2, final boolean caseSensitive){
+		return (name1 != null && name2 != null && (caseSensitive ? name1.equals(name2) : name1.equalsIgnoreCase(name2)));
+	}
+
+	private static boolean equals(final TAPTable table1, final TAPTable table2){
+		if (table1 == null || table2 == null){
+			//System.out.println("[EQUALS] tables null!");
+			return false;
+		}
+
+		if (!table1.getFullName().equals(table2.getFullName())){
+			//System.out.println("[EQUALS] tables name different: " + table1.getFullName() + " != " + table2.getFullName() + "!");
+			return false;
+		}
+
+		if (table1.getType() != table2.getType()){
+			//System.out.println("[EQUALS] tables type different: " + table1.getType() + " != " + table2.getType() + "!");
+			return false;
+		}
+
+		if (table1.getNbColumns() != table2.getNbColumns()){
+			//System.out.println("[EQUALS] tables length different: " + table1.getNbColumns() + " columns != " + table2.getNbColumns() + " columns!");
+			return false;
+		}
+
+		Iterator<TAPColumn> it = table1.getColumns();
+		while(it.hasNext()){
+			TAPColumn col1 = it.next();
+			if (!equals(col1, table2.getColumn(col1.getADQLName()))){
+				//System.out.println("[EQUALS] tables columns different!");
+				return false;
+			}
+		}
+
+		return true;
+	}
+
+	private static boolean equals(final TAPColumn col1, final TAPColumn col2){
+		if (col1 == null || col2 == null){
+			//System.out.println("[EQUALS] columns null!");
+			return false;
+		}
+
+		if (!col1.getADQLName().equals(col2.getADQLName())){
+			//System.out.println("[EQUALS] columns name different: " + col1.getADQLName() + " != " + col2.getADQLName() + "!");
+			return false;
+		}
+
+		if (!equals(col1.getDatatype(), col2.getDatatype())){
+			//System.out.println("[EQUALS] columns type different: " + col1.getDatatype() + " != " + col2.getDatatype() + "!");
+			return false;
+		}
+
+		if (col1.getUnit() != col2.getUnit()){
+			//System.out.println("[EQUALS] columns unit different: " + col1.getUnit() + " != " + col2.getUnit() + "!");
+			return false;
+		}
+
+		if (col1.getUcd() != col2.getUcd()){
+			//System.out.println("[EQUALS] columns ucd different: " + col1.getUcd() + " != " + col2.getUcd() + "!");
+			return false;
+		}
+
+		return true;
+	}
+
+	private static boolean equals(final TAPType type1, final TAPType type2){
+		return type1 != null && type2 != null && type1.type == type2.type && type1.length == type2.length;
+	}
+
+	private static TAPMetadata createCustomSchema(){
+		TAPMetadata tapMeta = new TAPMetadata();
+		TAPSchema tapSchema = new TAPSchema(STDSchema.TAPSCHEMA.toString());
+		TAPTable customColumns = (TAPTable)TAPMetadata.getStdTable(STDTable.COLUMNS).copy("Columns", STDTable.COLUMNS.label);
+		customColumns.addColumn("TestNewColumn", new TAPType(TAPDatatype.VARCHAR), "This is a fake column, just for test purpose.", null, null, null);
+		tapSchema.addTable(customColumns);
+		TAPTable addTable = new TAPTable("AdditionalTable");
+		addTable.addColumn("Blabla");
+		addTable.addColumn("Foo");
+		tapSchema.addTable(addTable);
+		tapMeta.addSchema(tapSchema);
+		return tapMeta;
+	}
+
+	/**
+	 * <p>Get the expected counts after a call of {@link JDBCConnection#setTAPSchema(TAPMetadata)}.</p>
+	 * 
+	 * <p>Counts are computed from the given metadata ; the same metadata that will be given to {@link JDBCConnection#setTAPSchema(TAPMetadata)}.</p>
+	 *  
+	 * @param meta
+	 * 
+	 * @return	An integer array with the following values: [0]=nbSchemas, [1]=nbTables, [2]=nbColumns, [3]=nbKeys and [4]=nbKeyColumns.
+	 */
+	private static int[] getStats(final TAPMetadata meta){
+		int[] counts = new int[]{1,5,0,0,0};
+
+		int[] stdColCounts = new int[]{3,5,11,5,3};
+		for(int c = 0; c < stdColCounts.length; c++)
+			counts[2] += stdColCounts[c];
+
+		Iterator<TAPSchema> itSchemas = meta.iterator();
+		while(itSchemas.hasNext()){
+			TAPSchema schema = itSchemas.next();
+
+			boolean isTapSchema = (schema.getADQLName().equalsIgnoreCase(STDSchema.TAPSCHEMA.toString()));
+			if (!isTapSchema)
+				counts[0]++;
+
+			Iterator<TAPTable> itTables = schema.iterator();
+			while(itTables.hasNext()){
+				TAPTable table = itTables.next();
+				if (isTapSchema && TAPMetadata.resolveStdTable(table.getADQLName()) != null){
+					int ind = pgJDBCConnection.getCreationOrder(TAPMetadata.resolveStdTable(table.getADQLName()));
+					counts[2] -= stdColCounts[ind];
+				}else
+					counts[1]++;
+
+				Iterator<DBColumn> itColumns = table.iterator();
+				while(itColumns.hasNext()){
+					itColumns.next();
+					counts[2]++;
+				}
+
+				Iterator<TAPForeignKey> itKeys = table.getForeignKeys();
+				while(itKeys.hasNext()){
+					TAPForeignKey fk = itKeys.next();
+					counts[3]++;
+					counts[4] += fk.getNbRelations();
+				}
+			}
+		}
+
+		return counts;
+	}
+
+	/**
+	 * <p>Get the effective counts after a call of {@link JDBCConnection#setTAPSchema(TAPMetadata)}.</p>
+	 * 
+	 * <p>Counts are computed directly from the DB using the given connection; the same connection used to set the TAP schema in {@link JDBCConnection#setTAPSchema(TAPMetadata)}.</p>
+	 *  
+	 * @param conn
+	 * @param meta	Metadata, in order to get the standard TAP tables' name.
+	 * 
+	 * @return	An integer array with the following values: [0]=nbSchemas, [1]=nbTables, [2]=nbColumns, [3]=nbKeys and [4]=nbKeyColumns.
+	 */
+	private static int[] getStats(final JDBCConnection conn, final TAPMetadata meta){
+		int[] counts = new int[5];
+
+		Statement stmt = null;
+		try{
+			stmt = conn.connection.createStatement();
+
+			TAPSchema tapSchema = meta.getSchema(STDSchema.TAPSCHEMA.toString());
+
+			String schemaPrefix = formatIdentifier(tapSchema.getDBName(), conn.translator.isCaseSensitive(IdentifierField.SCHEMA));
+			if (schemaPrefix == null)
+				schemaPrefix = "";
+			else
+				schemaPrefix += (conn.supportsSchema ? "." : "_");
+
+			boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE);
+			TAPTable tapTable = tapSchema.getTable(STDTable.SCHEMAS.toString());
+			counts[0] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName());
+
+			tapTable = tapSchema.getTable(STDTable.TABLES.toString());
+			counts[1] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName());
+
+			tapTable = tapSchema.getTable(STDTable.COLUMNS.toString());
+			counts[2] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName());
+
+			tapTable = tapSchema.getTable(STDTable.KEYS.toString());
+			counts[3] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName());
+
+			tapTable = tapSchema.getTable(STDTable.KEY_COLUMNS.toString());
+			counts[4] = count(stmt, schemaPrefix + formatIdentifier(tapTable.getDBName(), tCaseSensitive), tapSchema.getADQLName() + "." + tapTable.getADQLName());
+
+		}catch(SQLException se){
+			fail("Can not create a statement!");
+		}finally{
+			try{
+				if (stmt != null)
+					stmt.close();
+			}catch(SQLException ex){}
+		}
+		return counts;
+	}
+
+	private static int count(final Statement stmt, final String qualifiedTableName, final String adqlTableName){
+		ResultSet rs = null;
+		try{
+			rs = stmt.executeQuery("SELECT COUNT(*) FROM " + qualifiedTableName + ";");
+			rs.next();
+			return rs.getInt(1);
+		}catch(Exception e){
+			e.printStackTrace(System.err);
+			fail("Can not count! Maybe " + qualifiedTableName + " (in ADQL: " + adqlTableName + ") does not exist.");
+			return -1;
+		}finally{
+			close(rs);
+		}
+	}
+
+	private static void close(final ResultSet rs){
+		if (rs == null)
+			return;
+		try{
+			rs.close();
+		}catch(SQLException se){}
+	}
+
+	private static void close(final Statement stmt){
+		try{
+			if (stmt != null)
+				stmt.close();
+		}catch(SQLException se){}
+	}
+
+	private static void close(final InputStream io){
+		try{
+			if (io != null)
+				io.close();
+		}catch(IOException ioe){}
+	}
+
+}
diff --git a/test/tap/db/TestTAPDb.db b/test/tap/db/TestTAPDb.db
new file mode 100644
index 0000000000000000000000000000000000000000..227549965a7b445848f09562dc48441528f00dfd
GIT binary patch
literal 23552
zcmeHPeQX=YmEYN2iTdE`n_5#F+e5Ocqf(|xO0+E}PDsgeEL*Z<NmiR0S9K+=tc@v>
z=8{TW+y#}dxfVr`e*DqqkG4S3rYX?$E5Y6MfTnQk-l0H&0+(x%;8Ns@>!Cfg2V6hX
z9(qOY&CKp{NKuyLYf?K|LMtw3XMXeM&CHwk-n`k_a}#FCz$^LUnqI<#f-E2;q%jr*
zsZ9_B6|PQpwLA&mGF;9FIX7E>1IEFBKcGmmAfjgk^o;le@dwhg(zEEV(6{+gSDRXW
z>ha^~uOlUWaU;CPrzR3;BM(2a#$0B4VOEk+tDAwPou8hV%A8m@U&!Y5<*|qLwZhiv
z9iua2nYl4Mml>HD!(jqhuXq^W6<(OhoKD8$@i2xW69eIe6Ju4s3sxA<jlE-TAbeI|
zGo1TVC&GCA?A+M>V>1KcQ#0I;`8EBbgXKaq02i74oGmzL>z0`_IgD9zZBbulPy4Up
zJAA69q4%kjJ)`+-c`awzUrp5wwNt})g=yY}97pmJEm)PbY*<T0vrsbgIfs;T&Mdk2
zOEwJy;d1Fp!EOAqUefvBmiZod=CY$uG;>R4LC<n&W^UPd*Z?>;msR3_%^6ndtnm<|
z->x4|&x}uIW*)$2#vZ6@&;Snejp^?{xVKTwXh@*7SeLUf!WZ;%DNlbd*i2bS*f-5E
z68~2n9!48hWx$P|8k@{4Tr#dKFldVo!%eAe)%>oX3D{47OqY8r#r&F$>+rUu7cUy6
z>cr{>+YtTzK<?zi@K&v>Pr^c?etNq?rXkE~=@;b2`*<AiIPmA-z&dIO72(>L6)H1z
z)+pip3Z_!Y!dcTQnYoKt&n;snsTax5(yD>2rB!21w+5W7B#SM#aG|^iC0C7QZ02yO
z4ap&atD=Q8mdyWO{W)6g)#-7-<G}070YwTmS451v{{Nlypmaif82tmf_w}XccCPJ8
zqc5bX8)KY6wZ$%#*%KhKi2!#*G2H6etRtj}JZDn5(j75sq0CYlUoE&-O$*GIX>1wA
zb#uvxZQRq~3q{ncqg>sbx>~89)ClICVP4E($Y9~catRlT`E?T(5&U2oNF+;$Lu1y=
zT>`2Wuh3MkU<}XY@ugh;A^L#g)5U4ku-wuEK%PO?8LmaN&N5guY^h>uY;0p(_JsoK
z)l1dnGYy>bZHIKbi{rLrKeTEtt-9?b>nbJP2GENJtgr-Ri$*wx0N&J=w@8hlC|Jss
zMrW3nO=3mq**Rju(kf1ijWAYBE|<M()2Gf*vD0c+?b^;ig&IRa^&*GGT2TeXlD0u|
z*Z;qj^3vZ(pO(Ha{rq;Gksj?H2X0{w^vWR(Rpf*%bQ}BRP=rh7<ak53912uFvgCbo
zD9WYPNE@l95?iE*J#r|>t^7<pkoCVocuEkSBHlSk2JhT+(sMiqxAo02r-)rkRqibv
zay!dKU4P6^x9bAcenHRwM|-=<*~i9SRnnW(|F<!KeYcVS?@Xvs{nPpXL`9I$%L00t
z&VBIBOWzcxgemkqdP2M)wupV`_u_}er^TZ9y!c0{Q~arTU0RVIk>;eQq@RJd@D1r>
zoV56p(gwKl#<@P9dgu_kdXA3Zis)Qm@@rFqyroB$6#!=}6IE3sfAjQUDZE>|-t6F>
zS9@#uI>0~8;rI7vv_|#(A=`mF8f;YGp4fpm*A}o<o?JUk+KNlNcZB~=jYiSc5T%#n
zEm7iYe)_j$G|61w=S^X2T;5e_yW%l3CBoDre&4z14tPi?t70HYP~Ob*zX2whD7`|a
za6tNl^osa|I4%0oH_%6si3Ww=2+s=Z!q^SB-mW$sYF7g4A%M_OQ&Y1*m`XQ@=s#v8
zg7pf?^O>2^Q<<4p?`~59k-Mu3a6Ql6)+>CUq6F^DR28;&Pv}ruDQB}+@a3|eHCIfq
zWN9KMJ6}%i$NfqmR1H9PWOi75s8tCBkI?#?n^|k2X|TNmdu5l)25Bb-+d5Yw^%tE=
z540%JZYV@~)FjmCY$x-1C*#|im1uCwj3ZevD0L_AM3bVaOEze;V2cCF_VaTOOpjr6
z#r6$@-CHagRw18Tws0wrbxd~c@LGP^$U0~vK1GX+)}nE}u~;;X72EercL_=Nuu2__
zlZ}9JhGG!r*c7oZDskB@?T-Om$Xk}V$aj~6bGktZs=7-H?HV?y+=j7^-9fw3vJ#9`
z`kP^G*DRXLq2{Qh1Ve*0)X*FYWCh6Q4B*5uHq-|2x@kP*l$;WwBx?uZ2dkTvBtw$k
zQD$J>!e$9utNC&k+%E>W8JKejh7_>fasVe0H0FZKU<SzXx3V3;V9ChF7114|S)q8(
zUijDDPVE1E=n+90mO|(eaTNVnjH1t@ukU94xA#>mmGJwfwc^%sqjW*VveYbm|Bbe~
zLz_n3)qT_EGsh(8x}dBbK(-NBwdK99UDmptC$KfXw_AoSe><nWd45Mg*7iGT?k2m|
zA!`9AmF&d#^~l;@C&AgO4|d9$>LgNmv4^j*ytJ(HI3x<7SCzGhQ-qs60Y05GDtdZl
zE#%~{jQH-oa^wIa0@H3y)y{X9CPyOtQ*NugvsaD;`DaY(cC^U>l_WFq(B2~lD1yb&
z*cOljchWrkN}+Vife=lth<|^(9H5n0482vAqupq;v~KB>qruvACY+loZ`qV&G&NBc
z*(n@ZK0ckB!;KwsP(^HN)6Qw=m4gvGfh#Wu<Y357W*jEcNZOQ2B#j8Ek!FU;Rpt2q
zVK%DLAH@F@KPk?O-RPgtN6;+l7G40?z$JLL-RGp=4-0xA5TF_W=?soc`}SCyKT=sY
z3484kz&L%w)ib8}ISqs0Q@+rD^>l<|lpdh^2J{dIZN0Mhw)#2!g4D$in(ft#ds_UQ
zdO^xphS&C`2b=wzc0o$RwViTtrpeFg0sPQ&S4)<-6Ez^?^K)u|4aRln4zz%=Mu2d&
z02_jR?M4Y0Z}4+U05>1pR?)PbH0nOt&*=aRYP&YkT+X_CMDlYg00W@^x6=U5icrwi
z0QiD8rvOYKXo;%;Fg%X6J6ivBqE5i<=xuZS|KhsXE4~Z8g#KG>K;IP~7mtgRVpO~?
z{+sxu)F8#B6H=e}$_*_4?IeWv{I%u)zkq|vpw&?s`s%6-L4K(xD#M<7DucR7W$5K=
z1eGDc;}Dghhpun7l;>t!H=oX_3|;kAhRz$R3>`O68QSZs3~kjagHlgr@YhutTI;9`
zEj22`?YjQI*5lK|_c-8j;1=V6v;N;=WO?Lx9Pl{sI&i?T|D~@=@0Q*w{zrZP|2Bkp
z{{QVwbkF~<@5c1^{Qs|||DW3bbSKVR|F@BC-eZpgZ(t6%>;G4z1*t{+BE<YOp{LQ?
zg;#|y++xk1X;FN>c1;p}q5Y~DOqW0Y^kx4uA4`w*KlfzkCw^4OubDWLTb5?dgpZ~D
zzx~#Gf&Szmjwf&`6-z{ik~lGp2Zu%ukB^TdnVqSfZ?0hOCzuUEBl<7b?|Y?Ld|CMC
zzb_O_j5C*u(!Cc#7t&pyd4aGbnZ(HyP8^P<qJRy*29J&;0Q2DZynzN7?rc*9Hwd`7
z7YFY7)PG&OmMndFN_pR8VQJOGQ-xJ=<VE!->CP|p5Ly$5aq<X|8yh^BI7)7liIGGs
zHE#%yG0=DVDxe1dbnTZLFTC`d4_q5~{MVBY_U<bT9mROUTohyPS5xWkNB3PLpedX>
ziW3xc2#Cf*sgc2B<MYRigK#D+n?)aH*ykE;Y!uob0qpaArymPUJdsYl<?#<*e)++|
zd*CSAoL&|_GUHp9;we0M1ezT?98D&0{3zjS0$R@?g&#7==>`fZlKu(+<WvB9sr<Y2
z{V$#T;a@C$fRa0H6pcCgf6^Uy{gzN0AEIc(z}lfB7<z2z$jA^d;5!WQf?Pq20^;z`
zNH;y79)HKdA1T`9LIJo6i2Xh27wL}szk~^50uK%1!Qt3YG!@4~&|h)F)Vt=7_1AD<
zR;qw&06y?ev3%#r*7Vs6*Y)o7RN<n&hDUWP(1n`P(zibJZUP?1Dd1=_mW&<-a5C0L
z;;|HPLih=z7`Ff2F&hwI96Z_6)$n!k+R^B7-*?Wu%OYcTb}293`<VL8bl2cu8c@kN
zh!gH64kn??;=qzjYJ8jlo<$WZ3E=5){CraX%k<>$FI?+8{9`M53ImmEvr)M<-SP8B
zF`;r0Cx>x-2<AX)m=K&yjSP?L3=f`@PX}DBaP`6kCm95y7LXl!2`&*X5+($v1O*>l
zjc^gM;fJdoF7loLQr`P`9Pl`Bn{j|l<OUl5D=OkYk{ICItO0K#a-}Z|Un6nqR~zWA
zFBeKgLImt>wyM-!#{X`l_W5qd_}}CE8&xm<m%ZJjs{h?lu~ifveg8+hFeYr{|0B8q
z(Fw@AQkvj$eltD5^Z$ANzYL4)_TqnUhxp%$|Bp=IHsP=!U6;yIMEqCrbK;`-R`g5s
zHDsZf@S^Z2Jlyn|+^cBaCM2_fe|zbHpc5TKuv~6gFFNlZxf>_&nxW?)=)71q25{W|
zwW4P&!+!bGtLN@eG;I@{9p<X%;6OQjTa<lvdq<FSnxgH$R7;^7C9A^;c>AqXyFnf8
zQ?$TrEh-6_$r`zfrBxD6xULsXa_Ae|NlwVE<+OG*sAzl7)PgwKGAGVW(Aazu%1ML5
zb8x7Bi3Tes!5d)Dvm<*T@_)D%gvLX%;5eX!bqhHIpy%A97lSHvU20RorRDRZCkXj<
z3mxl)c2^?k?fxbqKI9??{^zGEJ<nby=TJHls5(_(m*t{?>c$hYYz{`U2QVU(Xc9$l
zW3Vr}tH5rnPI=tlt!SaC+A=I;uO{-YHb$NW)j5?<cPWtrl}_h$t*X#vNOzb=;OX0T
z;Jpfb?5Kk{(Wyitj5PbajHA7A;m%v&RbojUw7WYLF0PiGzH!jV?Ka0!$oxmr=LPul
zJ{|`=4&0&~nBS*p2QC6}WW{ZABw*E<kvZ~~5ct#aihjv?CCdPN!wZOHT!MEh!rMTK
zaOld2Il_aj|Ca>m8EH}K_u~KIaH1Fg@5TSKgCRtXb0*3gFaDqI|C9Nz2=5i(-2Wm(
z`2SS=tZ0fU^hbCPSQ%mAd+_MB`JCP32UR*Sq4_yW!dC2V+K`y;^K*uTjrRYtBW8ns
z&W^AVUEAR%&4>}&cHk>DpPWYT&WadO{hSqHW9-t1INS@zhFl}U#@K-kkqkhmxi*AT
z?#3p>a1ZpiYeG0>c4R?hx_N=*G_~@s;vE<ei7tR}4G0^8eQjkwob2>-_JdvICd`Ma
Z4nJo;aN7CJS`TO1{hal{0p5)9@PA%N4UYf-

literal 0
HcmV?d00001

diff --git a/test/tap/db/upload_example.vot b/test/tap/db/upload_example.vot
new file mode 100644
index 0000000..84f83a8
--- /dev/null
+++ b/test/tap/db/upload_example.vot
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<VOTABLE version="1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xmlns="http://www.ivoa.net/xml/VOTable/v1.2"
+  xsi:schemaLocation="http://www.ivoa.net/xml/VOTable/v1.2 http://www.ivoa.net/xml/VOTable/v1.2">
+ <DESCRIPTION>
+   VizieR Astronomical Server vizier.u-strasbg.fr
+    Date: 2014-07-18T16:16:29 [V1.99+ (14-Oct-2013)]
+   Explanations and Statistics of UCDs:			See LINK below
+   In case of problem, please report to:	cds-question@unistra.fr
+   In this version, NULL integer columns are written as an empty string
+   &lt;TD&gt;&lt;/TD&gt;, explicitely possible from VOTable-1.3
+ </DESCRIPTION>
+<!-- VOTable description at http://www.ivoa.net/Documents/latest/VOT.html -->
+<INFO ID="VERSION" name="votable-version" value="1.99+ (14-Oct-2013)"/>
+<INFO ID="Ref" name="-ref" value="VIZ53c9485768a5"/>
+<RESOURCE ID="yCat_2220" name="II/220">
+  <DESCRIPTION>Polarisation of Be stars (McDavid, 1986-1999)</DESCRIPTION>
+  <COOSYS ID="J2000" system="eq_FK5" equinox="J2000"/>
+  <TABLE ID="II_220_stars" name="II/220/stars">
+    <DESCRIPTION>Standard and Program Be stars</DESCRIPTION>
+    <!-- Definitions of GROUPs and FIELDs -->
+    <FIELD name="_RAJ2000" ucd="pos.eq.ra" ref="J2000" datatype="double" width="8" precision="4" unit="deg"><!-- ucd="POS_EQ_RA" -->
+      <DESCRIPTION>Right ascension (FK5, Equinox=J2000.0) (computed by VizieR, not part of the original data)</DESCRIPTION>
+    </FIELD>
+    <FIELD name="_DEJ2000" ucd="pos.eq.dec" ref="J2000" datatype="double" width="8" precision="4" unit="deg"><!-- ucd="POS_EQ_DEC" -->
+      <DESCRIPTION>Declination (FK5, Equinox=J2000.0) (computed by VizieR, not part of the original data)</DESCRIPTION>
+    </FIELD>
+    <FIELD name="ps" ucd="meta.ref.url" datatype="char" arraysize="1"><!-- ucd="DATA_LINK" -->
+      <DESCRIPTION>[p/s] Program or Standard star</DESCRIPTION>
+    </FIELD>
+    <FIELD name="Name" ucd="meta.id;meta.main" datatype="char" arraysize="7"><!-- ucd="ID_MAIN" -->
+      <DESCRIPTION>Star name</DESCRIPTION>
+    </FIELD>
+    <FIELD name="HD" ucd="meta.id" datatype="int" width="6"><!-- ucd="ID_ALTERNATIVE" -->
+      <DESCRIPTION>HD (Cat. &lt;III/135&gt;) catalog number</DESCRIPTION>
+    </FIELD>
+    <FIELD name="HR" ucd="meta.id" datatype="short" width="4"><!-- ucd="ID_ALTERNATIVE" -->
+      <DESCRIPTION>BS (Cat. &lt;V/50&gt;) catalog number</DESCRIPTION>
+    </FIELD>
+    <FIELD name="Vmag" ucd="meta.ref;pos.frame" datatype="float" width="5" precision="2" unit="mag"><!-- ucd="?" -->
+      <DESCRIPTION>Visual magnitude (BSC4, See Cat. &lt;V/50&gt;)</DESCRIPTION>
+    </FIELD>
+    <FIELD name="RAJ2000" ucd="pos.eq.ra;meta.main" ref="J2000" datatype="char" arraysize="10" unit="&quot;h:m:s&quot;"><!-- ucd="?" -->
+      <DESCRIPTION>Right Ascension J2000</DESCRIPTION>
+    </FIELD>
+    <FIELD name="DEJ2000" ucd="pos.eq.dec;meta.main" ref="J2000" datatype="char" arraysize="9" unit="&quot;d:m:s&quot;"><!-- ucd="?" -->
+      <DESCRIPTION>Declination J2000</DESCRIPTION>
+    </FIELD>
+    <FIELD name="Sp" ucd="src.spType" datatype="char" arraysize="15"><!-- ucd="?" -->
+      <DESCRIPTION>MK Spectral type (1)</DESCRIPTION>
+    </FIELD>
+    <FIELD name="vsini" ucd="phys.veloc.rotat" datatype="short" width="3" unit="km/s"><!-- ucd="?" -->
+      <DESCRIPTION>? projected rotational velocity (1) [NULL integer written as an empty string]</DESCRIPTION>
+      <VALUES null="-32768" />
+    </FIELD>
+    <FIELD name="Simbad" ucd="meta.ref" datatype="char" arraysize="6"><!-- ucd="DATA_LINK" -->
+      <DESCRIPTION>ask the {\bf\fg{FireBrick}Simbad} data-base about this object</DESCRIPTION>
+    </FIELD>
+<DATA><TABLEDATA>
+<TR><TD>052.2671</TD><TD>+59.9403</TD><TD>s</TD><TD>2H  Cam</TD><TD>21291</TD><TD>1035</TD><TD>4.23</TD><TD>03 29 04.1</TD><TD>+59 56 25</TD><TD>B9Ia</TD><TD></TD><TD>Simbad</TD></TR>
+<TR><TD>245.1587</TD><TD>-24.1689</TD><TD>s</TD><TD>omi Sco</TD><TD>147084</TD><TD>6081</TD><TD>4.55</TD><TD>16 20 38.1</TD><TD>-24 10 08</TD><TD>A5II</TD><TD></TD><TD>Simbad</TD></TR>
+<TR><TD>014.1758</TD><TD>+60.7169</TD><TD>p</TD><TD>gam Cas</TD><TD>5394</TD><TD>264</TD><TD>2.47</TD><TD>00 56 42.2</TD><TD>+60 43 01</TD><TD>B0.5IVe</TD><TD>230</TD><TD>Simbad</TD></TR>
+<TR><TD>025.9142</TD><TD>+50.6889</TD><TD>p</TD><TD>phi Per</TD><TD>10516</TD><TD>496</TD><TD>4.07</TD><TD>01 43 39.4</TD><TD>+50 41 20</TD><TD>B1.5(V:)e-shell</TD><TD>400</TD><TD>Simbad</TD></TR>
+<TR><TD>062.1646</TD><TD>+47.7131</TD><TD>p</TD><TD>48  Per</TD><TD>25940</TD><TD>1273</TD><TD>4.04</TD><TD>04 08 39.5</TD><TD>+47 42 47</TD><TD>B4Ve</TD><TD>200</TD><TD>Simbad</TD></TR>
+<TR><TD>084.4108</TD><TD>+21.1428</TD><TD>p</TD><TD>zet Tau</TD><TD>37202</TD><TD>1910</TD><TD>3.00</TD><TD>05 37 38.6</TD><TD>+21 08 34</TD><TD>B1IVe-shell</TD><TD>220</TD><TD>Simbad</TD></TR>
+<TR><TD>239.5471</TD><TD>-14.2792</TD><TD>p</TD><TD>48  Lib</TD><TD>142983</TD><TD>5941</TD><TD>4.88</TD><TD>15 58 11.3</TD><TD>-14 16 45</TD><TD>B3:IV:e-shell</TD><TD>400</TD><TD>Simbad</TD></TR>
+<TR><TD>246.7554</TD><TD>-18.4558</TD><TD>p</TD><TD>chi Oph</TD><TD>148184</TD><TD>6118</TD><TD>4.42</TD><TD>16 27 01.3</TD><TD>-18 27 21</TD><TD>B1.5Ve</TD><TD>140</TD><TD>Simbad</TD></TR>
+<TR><TD>336.3187</TD><TD>+1.3772</TD><TD>p</TD><TD>pi  Aqr</TD><TD>212571</TD><TD>8539</TD><TD>4.66</TD><TD>22 25 16.5</TD><TD>+01 22 38</TD><TD>B1III-IVe</TD><TD>300</TD><TD>Simbad</TD></TR>
+<TR><TD>345.4796</TD><TD>+42.3261</TD><TD>p</TD><TD>omi And</TD><TD>217675</TD><TD>8762</TD><TD>3.62</TD><TD>23 01 55.1</TD><TD>+42 19 34</TD><TD>B6III</TD><TD>260</TD><TD>Simbad</TD></TR>
+</TABLEDATA></DATA>
+</TABLE>
+
+<INFO name="Warning" value="No center provided++++"/>
+</RESOURCE>
+</VOTABLE>
diff --git a/test/tap/formatter/JSONFormatTest.java b/test/tap/formatter/JSONFormatTest.java
index a037575..4eb9449 100644
--- a/test/tap/formatter/JSONFormatTest.java
+++ b/test/tap/formatter/JSONFormatTest.java
@@ -75,8 +75,9 @@ public class JSONFormatTest {
 
 	@Test
 	public void testWriteResult(){
+		ResultSet rs = null;
 		try{
-			ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");
+			rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");
 
 			HashMap<String,Object> tapParams = new HashMap<String,Object>(1);
 			tapParams.put(TAPJob.PARAM_MAX_REC, "100");
@@ -98,6 +99,12 @@ public class JSONFormatTest {
 		}catch(Exception t){
 			t.printStackTrace();
 			fail("Unexpected exception!");
+		}finally{
+			if (rs != null){
+				try{
+					rs.close();
+				}catch(SQLException se){}
+			}
 		}
 	}
 
diff --git a/test/tap/formatter/SVFormatTest.java b/test/tap/formatter/SVFormatTest.java
index f44dda5..29d34a3 100644
--- a/test/tap/formatter/SVFormatTest.java
+++ b/test/tap/formatter/SVFormatTest.java
@@ -74,8 +74,9 @@ public class SVFormatTest {
 
 	@Test
 	public void testWriteResult(){
+		ResultSet rs = null;
 		try{
-			ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");
+			rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");
 
 			HashMap<String,Object> tapParams = new HashMap<String,Object>(1);
 			tapParams.put(TAPJob.PARAM_MAX_REC, "100");
@@ -96,6 +97,12 @@ public class SVFormatTest {
 		}catch(Exception t){
 			t.printStackTrace();
 			fail("Unexpected exception!");
+		}finally{
+			if (rs != null){
+				try{
+					rs.close();
+				}catch(SQLException se){}
+			}
 		}
 	}
 
diff --git a/test/tap/formatter/TextFormatTest.java b/test/tap/formatter/TextFormatTest.java
index 6739994..ca6631d 100644
--- a/test/tap/formatter/TextFormatTest.java
+++ b/test/tap/formatter/TextFormatTest.java
@@ -74,8 +74,9 @@ public class TextFormatTest {
 
 	@Test
 	public void testWriteResult(){
+		ResultSet rs = null;
 		try{
-			ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");
+			rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");
 
 			HashMap<String,Object> tapParams = new HashMap<String,Object>(1);
 			tapParams.put(TAPJob.PARAM_MAX_REC, "100");
@@ -96,6 +97,12 @@ public class TextFormatTest {
 		}catch(Exception t){
 			t.printStackTrace();
 			fail("Unexpected exception!");
+		}finally{
+			if (rs != null){
+				try{
+					rs.close();
+				}catch(SQLException se){}
+			}
 		}
 	}
 
diff --git a/test/tap/formatter/VOTableFormatTest.java b/test/tap/formatter/VOTableFormatTest.java
index 4cbb841..d54c80d 100644
--- a/test/tap/formatter/VOTableFormatTest.java
+++ b/test/tap/formatter/VOTableFormatTest.java
@@ -75,8 +75,9 @@ public class VOTableFormatTest {
 
 	@Test
 	public void testWriteResult(){
+		ResultSet rs = null;
 		try{
-			ResultSet rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");
+			rs = DBTools.select(conn, "SELECT id, ra, deg, gmag FROM gums LIMIT 10;");
 
 			HashMap<String,Object> tapParams = new HashMap<String,Object>(1);
 			tapParams.put(TAPJob.PARAM_MAX_REC, "100");
@@ -86,7 +87,7 @@ public class VOTableFormatTest {
 
 			TableIterator it = new ResultSetTableIterator(rs);
 
-			VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.FITS);
+			VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.TABLEDATA);
 			OutputStream output = new BufferedOutputStream(new FileOutputStream(votableFile));
 			formatter.writeResult(it, output, report, Thread.currentThread());
 			output.close();
@@ -101,6 +102,12 @@ public class VOTableFormatTest {
 		}catch(Exception t){
 			t.printStackTrace();
 			fail("Unexpected exception!");
+		}finally{
+			if (rs != null){
+				try{
+					rs.close();
+				}catch(SQLException se){}
+			}
 		}
 	}
 
@@ -118,7 +125,7 @@ public class VOTableFormatTest {
 
 			TableIterator it = new ResultSetTableIterator(rs);
 
-			VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.FITS);
+			VOTableFormat formatter = new VOTableFormat(serviceConn, DataFormat.TABLEDATA);
 			OutputStream output = new BufferedOutputStream(new FileOutputStream(votableFile));
 			formatter.writeResult(it, output, report, Thread.currentThread());
 			output.close();
diff --git a/test/testtools/DBTools.java b/test/testtools/DBTools.java
index a5ef0fe..5c837ba 100644
--- a/test/testtools/DBTools.java
+++ b/test/testtools/DBTools.java
@@ -96,7 +96,7 @@ public final class DBTools {
 		// 3. Establish the connection:
 		Connection connection = null;
 		try{
-			connection = DriverManager.getConnection("jdbc:" + dbms + "://" + server + ((port != null && port.trim().length() > 0) ? (":" + port) : "") + "/" + dbName, user, passwd);
+			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);
 		}
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();
+	}
+
+	// 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 = "";
+
+		for(int i = 0; i < b.length; i++){
+			result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1);
+		}
+		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();
+		}
+	}
+}
-- 
GitLab