From ecb7500ab34d5ef86ba3c47a0fe2042371dcfc1b Mon Sep 17 00:00:00 2001
From: gmantele <gmantele@ari.uni-heidelberg.de>
Date: Fri, 6 Feb 2015 20:20:01 +0100
Subject: [PATCH] [TAP] Add an XML TableSet parser. The main modification done
 in JDBCConnection is about the schema prefix of table when the DBMS does not
 support schemas: now, only standard tables are expected with the prefix
 'TAP_SCHEMA_' and the upload tables also with 'TAP_UPLOAD_'.

---
 src/adql/translator/JDBCTranslator.java |  35 ++-
 src/tap/data/VOTableIterator.java       |  60 ++---
 src/tap/db/JDBCConnection.java          | 292 ++++++++++++++++++------
 src/tap/formatter/VOTableFormat.java    |   8 +-
 src/tap/log/TAPLog.java                 |   1 +
 src/tap/metadata/TAPColumn.java         |  30 ++-
 src/tap/metadata/TAPMetadata.java       |  53 ++++-
 src/tap/metadata/TAPSchema.java         |  33 ++-
 src/tap/metadata/TAPTable.java          |  70 +++++-
 src/tap/metadata/VotType.java           |  18 +-
 test/tap/db/JDBCConnectionTest.java     | 156 +++++++++----
 test/tap/db/TestTAPDb.db                | Bin 24576 -> 26624 bytes
 12 files changed, 570 insertions(+), 186 deletions(-)

diff --git a/src/adql/translator/JDBCTranslator.java b/src/adql/translator/JDBCTranslator.java
index 6ea5ae3..362e67c 100644
--- a/src/adql/translator/JDBCTranslator.java
+++ b/src/adql/translator/JDBCTranslator.java
@@ -167,7 +167,7 @@ import adql.query.operand.function.geometry.RegionFunction;
  * </p>
  * 
  * @author Gr&eacute;gory Mantelet (ARI)
- * @version 1.3 (11/2014)
+ * @version 2.0 (02/2015)
  * @since 1.3
  * 
  * @see PostgreSQLTranslator
@@ -232,15 +232,40 @@ public abstract class JDBCTranslator implements ADQLTranslator {
 	 * 
 	 * @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.
+	 * 
+	 * @see #getTableName(DBTable, boolean)
 	 */
 	public String getQualifiedTableName(final DBTable table){
+		return getTableName(table, true);
+	}
+
+	/**
+	 * <p>Get the DB name of the given table.
+	 * The second parameter lets specify whether the table name must be prefixed by the qualified schema name or not.</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 DB name is asked.
+	 * @param withSchema	<i>true</i> if the qualified schema name must prefix the table name, <i>false</i> otherwise. 
+	 * 
+	 * @return	The DB table name (prefixed by the qualified schema name if asked, and with double quotes if needed),
+	 *        	or an empty string if the given table is NULL or if there is no DB name.
+	 * 
+	 * @since 2.0
+	 */
+	public String getTableName(final DBTable table, final boolean withSchema){
 		if (table == null)
 			return "";
 
-		StringBuffer buf = new StringBuffer(getQualifiedSchemaName(table));
-		if (buf.length() > 0)
-			buf.append('.');
-
+		StringBuffer buf = new StringBuffer();
+		if (withSchema){
+			buf.append(getQualifiedSchemaName(table));
+			if (buf.length() > 0)
+				buf.append('.');
+		}
 		appendIdentifier(buf, table.getDBName(), IdentifierField.TABLE);
 
 		return buf.toString();
diff --git a/src/tap/data/VOTableIterator.java b/src/tap/data/VOTableIterator.java
index e60d5a8..f659dcc 100644
--- a/src/tap/data/VOTableIterator.java
+++ b/src/tap/data/VOTableIterator.java
@@ -23,7 +23,7 @@ import adql.db.DBType;
  * <p>{@link #getColType()} will return TAP type based on the type declared in the VOTable metadata part.</p>
  * 
  * @author Gr&eacute;gory Mantelet (ARI)
- * @version 2.0 (12/2014)
+ * @version 2.0 (02/2015)
  * @since 2.0
  */
 public class VOTableIterator implements TableIterator {
@@ -42,7 +42,7 @@ public class VOTableIterator implements TableIterator {
 	 * </p> 
 	 * 
 	 * @author Gr&eacute;gory Mantelet (ARI)
-	 * @version 2.0 (12/2014)
+	 * @version 2.0 (01/2015)
 	 * @since 2.0
 	 */
 	protected static class StreamVOTableSink implements TableSink {
@@ -288,34 +288,6 @@ public class VOTableIterator implements TableIterator {
 			return (value != null) ? value.getValue().toString() : null;
 		}
 
-		/**
-		 * Resolve a VOTable field type by using the datatype, arraysize and xtype strings as specified in a VOTable document.
-		 * 
-		 * @param datatype		Attribute value of VOTable corresponding to the datatype.
-		 * @param arraysize		Attribute value of VOTable corresponding to the arraysize.
-		 * @param xtype			Attribute value of VOTable corresponding to the xtype.
-		 * 
-		 * @return	The resolved VOTable field type, or a CHAR(*) type if the specified type can not be resolved.
-		 * 
-		 * @throws DataReadException	If a field datatype is unknown.
-		 */
-		protected VotType resolveVotType(final String datatype, final String arraysize, final String xtype) throws DataReadException{
-			// If no datatype is specified, return immediately a CHAR(*) type:
-			if (datatype == null || datatype.trim().length() == 0)
-				return new VotType(VotDatatype.CHAR, "*");
-
-			// Identify the specified datatype:
-			VotDatatype votdatatype;
-			try{
-				votdatatype = VotDatatype.valueOf(datatype.toUpperCase());
-			}catch(IllegalArgumentException iae){
-				throw new DataReadException("unknown field datatype: \"" + datatype + "\"");
-			}
-
-			// Build the VOTable type:
-			return new VotType(votdatatype, arraysize, xtype);
-		}
-
 	}
 
 	/** Stream containing the VOTable on which this {@link TableIterator} is iterating. */
@@ -458,4 +430,32 @@ public class VOTableIterator implements TableIterator {
 			throw new IllegalStateException("End of VOTable file already reached!");
 	}
 
+	/**
+	 * Resolve a VOTable field type by using the datatype, arraysize and xtype strings as specified in a VOTable document.
+	 * 
+	 * @param datatype		Attribute value of VOTable corresponding to the datatype.
+	 * @param arraysize		Attribute value of VOTable corresponding to the arraysize.
+	 * @param xtype			Attribute value of VOTable corresponding to the xtype.
+	 * 
+	 * @return	The resolved VOTable field type, or a CHAR(*) type if the specified type can not be resolved.
+	 * 
+	 * @throws DataReadException	If a field datatype is unknown.
+	 */
+	public static VotType resolveVotType(final String datatype, final String arraysize, final String xtype) throws DataReadException{
+		// If no datatype is specified, return immediately a CHAR(*) type:
+		if (datatype == null || datatype.trim().length() == 0)
+			return new VotType(VotDatatype.CHAR, "*");
+
+		// Identify the specified datatype:
+		VotDatatype votdatatype;
+		try{
+			votdatatype = VotDatatype.valueOf(datatype.toUpperCase());
+		}catch(IllegalArgumentException iae){
+			throw new DataReadException("unknown field datatype: \"" + datatype + "\"");
+		}
+
+		// Build the VOTable type:
+		return new VotType(votdatatype, arraysize, xtype);
+	}
+
 }
diff --git a/src/tap/db/JDBCConnection.java b/src/tap/db/JDBCConnection.java
index 8a69226..07210e9 100644
--- a/src/tap/db/JDBCConnection.java
+++ b/src/tap/db/JDBCConnection.java
@@ -121,7 +121,7 @@ import adql.translator.TranslationException;
  * </p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (01/2015)
+ * @version 2.0 (02/2015)
  * @since 2.0
  */
 public class JDBCConnection implements DBConnection {
@@ -138,6 +138,9 @@ public class JDBCConnection implements DBConnection {
 	/** DBMS name of Oracle used in the database URL. */
 	protected final static String DBMS_ORACLE = "oracle";
 
+	/** Name of the database column giving the database name of a TAP column, table or schema. */
+	protected final static String DB_NAME_COLUMN = "dbname";
+
 	/** Connection ID (typically, the job ID). It lets identify the DB errors linked to the Job execution in the logs. */
 	protected final String ID;
 
@@ -321,8 +324,10 @@ public class JDBCConnection implements DBConnection {
 		// Build a connection to the specified database:
 		try{
 			Properties p = new Properties();
-			p.setProperty("user", dbUser);
-			p.setProperty("password", dbPassword);
+			if (dbUser != null)
+				p.setProperty("user", dbUser);
+			if (dbPassword != null)
+				p.setProperty("password", dbPassword);
 			Connection con = d.connect(url, p);
 			return con;
 		}catch(SQLException se){
@@ -464,15 +469,7 @@ public class JDBCConnection implements DBConnection {
 		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());
-		}
+		TAPSchema tap_schema = TAPMetadata.getStdSchema(supportsSchema);
 
 		// LOAD ALL METADATA FROM THE STANDARD TAP TABLES:
 		Statement stmt = null;
@@ -527,26 +524,29 @@ public class JDBCConnection implements DBConnection {
 	protected void loadSchemas(final TAPTable tableDef, final TAPMetadata metadata, final Statement stmt) throws DBException{
 		ResultSet rs = null;
 		try{
+			// Determine whether the dbName column exists:
+			/* note: if the schema notion is not supported by this DBMS, the column "dbname" is ignored. */
+			boolean hasDBName = supportsSchema && isColumnExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), DB_NAME_COLUMN, connection.getMetaData());
+
 			// 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(';');
+			if (hasDBName)
+				sqlBuf.append(", ").append(DB_NAME_COLUMN);
+			sqlBuf.append(" FROM ").append(translator.getTableName(tableDef, supportsSchema)).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);
+				String schemaName = rs.getString(1), description = rs.getString(2), utype = rs.getString(3), dbName = (hasDBName ? rs.getString(4) : null);
 
 				// 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);
+				newSchema.setDBName(dbName);
 
 				// add the new schema inside the given metadata:
 				metadata.addSchema(newSchema);
@@ -586,6 +586,9 @@ public class JDBCConnection implements DBConnection {
 	protected List<TAPTable> loadTables(final TAPTable tableDef, final TAPMetadata metadata, final Statement stmt) throws DBException{
 		ResultSet rs = null;
 		try{
+			// Determine whether the dbName column exists:
+			boolean hasDBName = isColumnExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), DB_NAME_COLUMN, connection.getMetaData());
+
 			// Build the SQL query:
 			StringBuffer sqlBuf = new StringBuffer("SELECT ");
 			sqlBuf.append(translator.getColumnName(tableDef.getColumn("schema_name")));
@@ -593,7 +596,9 @@ public class JDBCConnection implements DBConnection {
 			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(';');
+			if (hasDBName)
+				sqlBuf.append(", ").append(DB_NAME_COLUMN);
+			sqlBuf.append(" FROM ").append(translator.getTableName(tableDef, supportsSchema)).append(';');
 
 			// Execute the query:
 			rs = stmt.executeQuery(sqlBuf.toString());
@@ -601,7 +606,7 @@ public class JDBCConnection implements DBConnection {
 			// 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);
+				String schemaName = rs.getString(1), tableName = rs.getString(2), typeStr = rs.getString(3), description = rs.getString(4), utype = rs.getString(5), dbName = (hasDBName ? rs.getString(6) : null);
 
 				// get the schema:
 				TAPSchema schema = metadata.getSchema(schemaName);
@@ -611,6 +616,19 @@ public class JDBCConnection implements DBConnection {
 					throw new DBException("Impossible to find the schema of the table \"" + tableName + "\": \"" + schemaName + "\"!");
 				}
 
+				// If the table name is qualified, check its prefix (it must match to the schema name):
+				int endPrefix = tableName.indexOf('.');
+				if (endPrefix >= 0){
+					if (endPrefix == 0)
+						throw new DBException("Incorrect table name syntax: \"" + tableName + "\"! Missing schema name (before '.').");
+					else if (endPrefix == tableName.length() - 1)
+						throw new DBException("Incorrect table name syntax: \"" + tableName + "\"! Missing table name (after '.').");
+					else if (schemaName == null)
+						throw new DBException("Incorrect schema prefix for the table \"" + tableName.substring(endPrefix + 1) + "\": this table is not in a schema, according to the column \"schema_name\" of TAP_SCHEMA.tables!");
+					else if (!tableName.substring(0, endPrefix).trim().equalsIgnoreCase(schemaName))
+						throw new DBException("Incorrect schema prefix for the table \"" + schemaName + "." + tableName.substring(tableName.indexOf('.') + 1) + "\": " + tableName + "! Mismatch between the schema specified in prefix of the column \"table_name\" and in the column \"schema_name\".");
+				}
+
 				// resolve the table type (if any) ; by default, it will be "table":
 				TableType type = TableType.table;
 				if (typeStr != null){
@@ -621,10 +639,7 @@ public class JDBCConnection implements DBConnection {
 
 				// 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());
+				newTable.setDBName(dbName);
 
 				// add the new table inside its corresponding schema:
 				schema.addTable(newTable);
@@ -658,6 +673,9 @@ public class JDBCConnection implements DBConnection {
 	protected void loadColumns(final TAPTable tableDef, final List<TAPTable> lstTables, final Statement stmt) throws DBException{
 		ResultSet rs = null;
 		try{
+			// Determine whether the dbName column exists:
+			boolean hasDBName = isColumnExisting(tableDef.getDBSchemaName(), tableDef.getDBName(), DB_NAME_COLUMN, connection.getMetaData());
+
 			// Build the SQL query:
 			StringBuffer sqlBuf = new StringBuffer("SELECT ");
 			sqlBuf.append(translator.getColumnName(tableDef.getColumn("table_name")));
@@ -671,14 +689,16 @@ public class JDBCConnection implements DBConnection {
 			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(';');
+			if (hasDBName)
+				sqlBuf.append(", ").append(DB_NAME_COLUMN);
+			sqlBuf.append(" FROM ").append(translator.getTableName(tableDef, supportsSchema)).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);
+				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), dbName = (hasDBName ? rs.getString(12) : null);
 				int size = rs.getInt(8);
 				boolean principal = toBoolean(rs.getObject(9)), indexed = toBoolean(rs.getObject(10)), std = toBoolean(rs.getObject(11));
 
@@ -710,6 +730,7 @@ public class JDBCConnection implements DBConnection {
 				newColumn.setPrincipal(principal);
 				newColumn.setIndexed(indexed);
 				newColumn.setStd(std);
+				newColumn.setDBName(dbName);
 
 				// add the new column inside its corresponding table:
 				table.addColumn(newColumn);
@@ -747,7 +768,7 @@ public class JDBCConnection implements DBConnection {
 			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(" FROM ").append(translator.getTableName(keyColumnsDef, supportsSchema));
 			sqlBuf.append(" WHERE ").append(translator.getColumnName(keyColumnsDef.getColumn("key_id"))).append(" = ?").append(';');
 			keyColumnsStmt = connection.prepareStatement(sqlBuf.toString());
 
@@ -758,7 +779,7 @@ public class JDBCConnection implements DBConnection {
 			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(';');
+			sqlBuf.append(" FROM ").append(translator.getTableName(keysDef, supportsSchema)).append(';');
 
 			// Execute the query:
 			rs = stmt.executeQuery(sqlBuf.toString());
@@ -953,12 +974,7 @@ public class JDBCConnection implements DBConnection {
 		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);
+			tapSchema = TAPMetadata.getStdSchema(supportsSchema);
 
 			// add the new TAP_SCHEMA definition in the given metadata object:
 			metadata.addSchema(tapSchema);
@@ -970,10 +986,8 @@ public class JDBCConnection implements DBConnection {
 
 			// 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());
+					stdTables[i].setDBName(STDSchema.TAPSCHEMA.label + "_" + stdTables[i].getADQLName());
 				// add the table to the fetched or built-in schema:
 				tapSchema.addTable(stdTables[i]);
 			}
@@ -1003,7 +1017,7 @@ public class JDBCConnection implements DBConnection {
 		DatabaseMetaData dbMeta = connection.getMetaData();
 
 		// 1. Get the qualified DB schema name:
-		String dbSchemaName = stdTables[0].getDBSchemaName();
+		String dbSchemaName = (supportsSchema ? stdTables[0].getDBSchemaName() : null);
 
 		/* 2. Test whether the schema TAP_SCHEMA exists
 		 *    and if it does not, create it: */
@@ -1080,6 +1094,11 @@ public class JDBCConnection implements DBConnection {
 	 * 	this function will do nothing and will throw an exception.
 	 * </i></p>
 	 * 
+	 * <p><i>Note:
+	 * 	An extra column is added in TAP_SCHEMA.schemas, TAP_SCHEMA.tables and TAP_SCHEMA.columns: {@value #DB_NAME_COLUMN}.
+	 * 	This column is particularly used when getting the TAP metadata from the database to alias some schema, table and/or column names in ADQL. 
+	 * </i></p>
+	 * 
 	 * @param table	Table to create.
 	 * @param stmt	Statement to use in order to interact with the database.
 	 * 
@@ -1095,7 +1114,7 @@ public class JDBCConnection implements DBConnection {
 		StringBuffer sql = new StringBuffer("CREATE TABLE ");
 
 		// a. Write the fully qualified table name:
-		sql.append(translator.getQualifiedTableName(table));
+		sql.append(translator.getTableName(table, supportsSchema));
 
 		// b. List all the columns:
 		sql.append('(');
@@ -1114,6 +1133,10 @@ public class JDBCConnection implements DBConnection {
 				sql.append(',');
 		}
 
+		// b bis. Add the extra dbName column (giving the database name of a schema, table or column): 
+		if ((supportsSchema && table.getADQLName().equalsIgnoreCase(STDTable.SCHEMAS.label)) || table.getADQLName().equalsIgnoreCase(STDTable.TABLES.label) || table.getADQLName().equalsIgnoreCase(STDTable.COLUMNS.label))
+			sql.append(',').append(DB_NAME_COLUMN).append(" VARCHAR");
+
 		// c. Append the primary key definition, if needed:
 		String primaryKey = getPrimaryKeyDef(table.getADQLName());
 		if (primaryKey != null)
@@ -1179,7 +1202,7 @@ public class JDBCConnection implements DBConnection {
 			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);
+		final String dbTableName = translator.getTableName(table, supportsSchema);
 
 		// Build the name prefix of all the indexes to create: 
 		final String indexNamePrefix = "INDEX_" + ((table.getADQLSchemaName() != null) ? (table.getADQLSchemaName() + "_") : "") + table.getADQLName() + "_";
@@ -1268,11 +1291,15 @@ public class JDBCConnection implements DBConnection {
 
 		// Build the SQL update query:
 		StringBuffer sql = new StringBuffer("INSERT INTO ");
-		sql.append(translator.getQualifiedTableName(metaTable)).append(" (");
+		sql.append(translator.getTableName(metaTable, supportsSchema)).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 (?, ?, ?);");
+		if (supportsSchema){
+			sql.append(", ").append(DB_NAME_COLUMN);
+			sql.append(") VALUES (?, ?, ?, ?);");
+		}else
+			sql.append(") VALUES (?, ?, ?);");
 
 		// Prepare the statement:
 		PreparedStatement stmt = null;
@@ -1292,6 +1319,8 @@ public class JDBCConnection implements DBConnection {
 				stmt.setString(1, schema.getADQLName());
 				stmt.setString(2, schema.getDescription());
 				stmt.setString(3, schema.getUtype());
+				if (supportsSchema)
+					stmt.setString(4, (schema.getDBName() == null || schema.getDBName().equals(schema.getADQLName())) ? null : schema.getDBName());
 				executeUpdate(stmt, nbRows);
 			}
 			executeBatchUpdates(stmt, nbRows);
@@ -1323,13 +1352,14 @@ public class JDBCConnection implements DBConnection {
 
 		// Build the SQL update query:
 		StringBuffer sql = new StringBuffer("INSERT INTO ");
-		sql.append(translator.getQualifiedTableName(metaTable)).append(" (");
+		sql.append(translator.getTableName(metaTable, supportsSchema)).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 (?, ?, ?, ?, ?);");
+		sql.append(", ").append(DB_NAME_COLUMN);
+		sql.append(") VALUES (?, ?, ?, ?, ?, ?);");
 
 		// Prepare the statement:
 		PreparedStatement stmt = null;
@@ -1347,10 +1377,14 @@ public class JDBCConnection implements DBConnection {
 
 				// add the table entry into the DB:
 				stmt.setString(1, table.getADQLSchemaName());
-				stmt.setString(2, table.getADQLName());
+				if (table.isInitiallyQualified())
+					stmt.setString(2, table.getADQLSchemaName() + "." + table.getADQLName());
+				else
+					stmt.setString(2, table.getADQLName());
 				stmt.setString(3, table.getType().toString());
 				stmt.setString(4, table.getDescription());
 				stmt.setString(5, table.getUtype());
+				stmt.setString(6, (table.getDBName() == null || table.getDBName().equals(table.getADQLName())) ? null : table.getDBName());
 				executeUpdate(stmt, nbRows);
 			}
 			executeBatchUpdates(stmt, nbRows);
@@ -1382,7 +1416,7 @@ public class JDBCConnection implements DBConnection {
 
 		// Build the SQL update query:
 		StringBuffer sql = new StringBuffer("INSERT INTO ");
-		sql.append(translator.getQualifiedTableName(metaTable)).append(" (");
+		sql.append(translator.getTableName(metaTable, supportsSchema)).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")));
@@ -1394,7 +1428,8 @@ public class JDBCConnection implements DBConnection {
 		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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
+		sql.append(", ").append(DB_NAME_COLUMN);
+		sql.append(") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);");
 
 		// Prepare the statement:
 		PreparedStatement stmt = null;
@@ -1411,7 +1446,10 @@ public class JDBCConnection implements DBConnection {
 				appendAllInto(allKeys, col.getTargets());
 
 				// add the column entry into the DB:
-				stmt.setString(1, col.getTable().getADQLName());
+				if (!(col.getTable() instanceof TAPTable) || ((TAPTable)col.getTable()).isInitiallyQualified())
+					stmt.setString(1, col.getTable().getADQLSchemaName() + "." + col.getTable().getADQLName());
+				else
+					stmt.setString(1, col.getTable().getADQLName());
 				stmt.setString(2, col.getADQLName());
 				stmt.setString(3, col.getDescription());
 				stmt.setString(4, col.getUnit());
@@ -1422,6 +1460,7 @@ public class JDBCConnection implements DBConnection {
 				stmt.setInt(9, col.isPrincipal() ? 1 : 0);
 				stmt.setInt(10, col.isIndexed() ? 1 : 0);
 				stmt.setInt(11, col.isStd() ? 1 : 0);
+				stmt.setString(12, (col.getDBName() == null || col.getDBName().equals(col.getADQLName())) ? null : col.getDBName());
 				executeUpdate(stmt, nbRows);
 			}
 			executeBatchUpdates(stmt, nbRows);
@@ -1450,7 +1489,7 @@ public class JDBCConnection implements DBConnection {
 	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.getTableName(metaKeys, supportsSchema)).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")));
@@ -1465,7 +1504,7 @@ public class JDBCConnection implements DBConnection {
 
 			// Build the SQL update query for KEY_COLUMNS:
 			StringBuffer sqlKeyCols = new StringBuffer("INSERT INTO ");
-			sqlKeyCols.append(translator.getQualifiedTableName(metaKeyColumns)).append(" (");
+			sqlKeyCols.append(translator.getTableName(metaKeyColumns, supportsSchema)).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")));
@@ -1482,8 +1521,14 @@ public class JDBCConnection implements DBConnection {
 
 				// add the key entry into KEYS:
 				stmtKeys.setString(1, key.getKeyId());
-				stmtKeys.setString(2, key.getFromTable().getFullName());
-				stmtKeys.setString(3, key.getTargetTable().getFullName());
+				if (key.getFromTable().isInitiallyQualified())
+					stmtKeys.setString(2, key.getFromTable().getADQLSchemaName() + "." + key.getFromTable().getADQLName());
+				else
+					stmtKeys.setString(2, key.getFromTable().getADQLName());
+				if (key.getTargetTable().isInitiallyQualified())
+					stmtKeys.setString(3, key.getTargetTable().getADQLSchemaName() + "." + key.getTargetTable().getADQLName());
+				else
+					stmtKeys.setString(3, key.getTargetTable().getADQLName());
 				stmtKeys.setString(4, key.getDescription());
 				stmtKeys.setString(5, key.getUtype());
 				executeUpdate(stmtKeys, nbKeys);
@@ -1558,7 +1603,7 @@ public class JDBCConnection implements DBConnection {
 			}
 			// 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)){
-				DBException de = new DBException("Impossible to create the user uploaded table in the database: " + translator.getQualifiedTableName(tableDef) + "! This table already exists.");
+				DBException de = new DBException("Impossible to create the user uploaded table in the database: " + translator.getTableName(tableDef, supportsSchema) + "! This table already exists.");
 				if (logger != null)
 					logger.logDB(LogLevel.ERROR, this, "ADD_UPLOAD_TABLE", de.getMessage(), de);
 				throw de;
@@ -1567,7 +1612,7 @@ public class JDBCConnection implements DBConnection {
 			// 2. Create the table:
 			// ...build the SQL query:
 			StringBuffer sqlBuf = new StringBuffer("CREATE TABLE ");
-			sqlBuf.append(translator.getQualifiedTableName(tableDef)).append(" (");
+			sqlBuf.append(translator.getTableName(tableDef, supportsSchema)).append(" (");
 			Iterator<TAPColumn> it = tableDef.getColumns();
 			while(it.hasNext()){
 				TAPColumn col = it.next();
@@ -1591,15 +1636,15 @@ public class JDBCConnection implements DBConnection {
 
 			// Log the end:
 			if (logger != null)
-				logger.logDB(LogLevel.INFO, this, "TABLE_CREATED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getQualifiedTableName(tableDef) + ") created.", null);
+				logger.logDB(LogLevel.INFO, this, "TABLE_CREATED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getTableName(tableDef, supportsSchema) + ") created.", null);
 
 			return true;
 
 		}catch(SQLException se){
 			rollback();
 			if (logger != null)
-				logger.logDB(LogLevel.WARNING, this, "ADD_UPLOAD_TABLE", "Impossible to create the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se);
-			throw new DBException("Impossible to create the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se);
+				logger.logDB(LogLevel.WARNING, this, "ADD_UPLOAD_TABLE", "Impossible to create the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se);
+			throw new DBException("Impossible to create the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se);
 		}catch(DBException de){
 			rollback();
 			throw de;
@@ -1638,7 +1683,7 @@ public class JDBCConnection implements DBConnection {
 		StringBuffer sql = new StringBuffer("INSERT INTO ");
 		StringBuffer varParam = new StringBuffer();
 		// ...table name:
-		sql.append(translator.getQualifiedTableName(metaTable)).append(" (");
+		sql.append(translator.getTableName(metaTable, supportsSchema)).append(" (");
 		// ...list of columns:
 		TAPColumn[] cols = data.getMetadata();
 		for(int c = 0; c < cols.length; c++){
@@ -1744,23 +1789,23 @@ public class JDBCConnection implements DBConnection {
 
 			// Execute the update:
 			stmt = connection.createStatement();
-			int cnt = stmt.executeUpdate("DROP TABLE " + translator.getQualifiedTableName(tableDef) + ";");
+			int cnt = stmt.executeUpdate("DROP TABLE " + translator.getTableName(tableDef, supportsSchema) + ";");
 
 			// Log the end:
 			if (logger != null){
-				if (cnt == 0)
-					logger.logDB(LogLevel.INFO, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getQualifiedTableName(tableDef) + ") dropped.", null);
+				if (cnt >= 0)
+					logger.logDB(LogLevel.INFO, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getTableName(tableDef, supportsSchema) + ") dropped.", null);
 				else
-					logger.logDB(LogLevel.ERROR, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getQualifiedTableName(tableDef) + ") NOT dropped.", null);
+					logger.logDB(LogLevel.ERROR, this, "TABLE_DROPPED", "Table \"" + tableDef.getADQLName() + "\" (in DB: " + translator.getTableName(tableDef, supportsSchema) + ") NOT dropped.", null);
 			}
 
 			// Ensure the update is successful:
-			return (cnt == 0);
+			return (cnt >= 0);
 
 		}catch(SQLException se){
 			if (logger != null)
-				logger.logDB(LogLevel.WARNING, this, "DROP_UPLOAD_TABLE", "Impossible to drop the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se);
-			throw new DBException("Impossible to drop the uploaded table: " + translator.getQualifiedTableName(tableDef) + "!", se);
+				logger.logDB(LogLevel.WARNING, this, "DROP_UPLOAD_TABLE", "Impossible to drop the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se);
+			throw new DBException("Impossible to drop the uploaded table: " + translator.getTableName(tableDef, supportsSchema) + "!", se);
 		}finally{
 			close(stmt);
 		}
@@ -1792,10 +1837,11 @@ public class JDBCConnection implements DBConnection {
 		if (tableDef.getSchema() == null || !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);
+		if (!supportsSchema){
+			if (tableDef.getADQLSchemaName() != null && tableDef.getADQLSchemaName().trim().length() > 0 && !tableDef.getDBName().startsWith(tableDef.getADQLSchemaName() + "_"))
+				tableDef.setDBName(tableDef.getADQLSchemaName() + "_" + tableDef.getDBName());
+			if (tableDef.getSchema() != null)
+				tableDef.getSchema().setDBName(null);
 		}
 	}
 
@@ -2156,7 +2202,7 @@ public class JDBCConnection implements DBConnection {
 
 	/**
 	 * 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.
+	 * Otherwise the given string is returned as provided.
 	 * 
 	 * @param dbValue	Value to nullify if needed.
 	 * 
@@ -2226,7 +2272,7 @@ public class JDBCConnection implements DBConnection {
 	 * @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)
+		if (!supportsSchema || schemaName == null || schemaName.length() == 0)
 			return true;
 
 		// Determine the case sensitivity to use for the equality test:
@@ -2282,9 +2328,6 @@ public class JDBCConnection implements DBConnection {
 
 		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){
@@ -2321,6 +2364,103 @@ public class JDBCConnection implements DBConnection {
 	}
 
 	/**
+	 * <p>Tell whether the specified column exists in the specified table of the database.
+	 * 	To do so, it is using the given {@link DatabaseMetaData} object to query the database and list all existing columns.</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 #loadSchemas(TAPTable, TAPMetadata, Statement)}, {@link #loadTables(TAPTable, TAPMetadata, Statement)}
+	 * 	and {@link #loadColumns(TAPTable, List, Statement)}.
+	 * </i></p>
+	 * 
+	 * @param schemaName	DB name of the table schema. <i>MAY BE NULL</i>
+	 * @param tableName		DB name of the table containing the column to search. <i>MAY BE NULL</i>
+	 * @param columnName	DB name of the column to search.
+	 * @param dbMeta		Metadata about the database, and mainly the list of all existing tables.
+	 * 
+	 * @return	<i>true</i> if the specified column exists, <i>false</i> otherwise.
+	 * 
+	 * @throws SQLException	If any error occurs while interrogating the database about existing columns.
+	 */
+	protected boolean isColumnExisting(String schemaName, String tableName, String columnName, final DatabaseMetaData dbMeta) throws DBException, SQLException{
+		if (columnName == null || columnName.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);
+		boolean columnCaseSensitive = translator.isCaseSensitive(IdentifierField.COLUMN);
+
+		ResultSet rsT = null, rsC = null;
+		try{
+			/* Note:
+			 * 
+			 *     The DatabaseMetaData.getColumns(....) function does not work properly
+			 * with the SQLite driver: when all parameters are set to null, meaning all columns of the database
+			 * must be returned, absolutely no rows are selected.
+			 * 
+			 *     The solution proposed here, is to first search all (matching) tables, and then for each table get
+			 * all its columns and find the matching one(s).
+			 */
+
+			// List all matching tables:
+			if (supportsSchema){
+				String schemaPattern = schemaCaseSensitive ? schemaName : null;
+				String tablePattern = tableCaseSensitive ? tableName : null;
+				rsT = dbMeta.getTables(null, schemaPattern, tablePattern, null);
+			}else{
+				String tablePattern = tableCaseSensitive ? tableName : null;
+				rsT = dbMeta.getTables(null, null, tablePattern, null);
+			}
+
+			// For each matching table:
+			int cnt = 0;
+			String columnPattern = columnCaseSensitive ? columnName : null;
+			while(rsT.next()){
+				String rsSchema = nullifyIfNeeded(rsT.getString(2));
+				String rsTable = rsT.getString(3);
+				// test the schema name:
+				if (!supportsSchema || schemaName == null || equals(rsSchema, schemaName, schemaCaseSensitive)){
+					// test the table name:
+					if ((tableName == null || equals(rsTable, tableName, tableCaseSensitive))){
+						// list its columns:
+						rsC = dbMeta.getColumns(null, rsSchema, rsTable, columnPattern);
+						// count all matching columns:
+						while(rsC.next()){
+							String rsColumn = rsC.getString(4);
+							if (equals(rsColumn, columnName, columnCaseSensitive))
+								cnt++;
+						}
+						close(rsC);
+					}
+				}
+			}
+
+			if (cnt > 1){
+				if (logger != null)
+					logger.logDB(LogLevel.ERROR, this, "COLUMN_EXIST", "More than one column match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + ") && column=" + columnName + " (case sensitive?" + columnCaseSensitive + "))!", null);
+				throw new DBException("More than one column match to these criteria (schema=" + schemaName + " (case sensitive?" + schemaCaseSensitive + ") && table=" + tableName + " (case sensitive?" + tableCaseSensitive + ") && column=" + columnName + " (case sensitive?" + columnCaseSensitive + "))!");
+			}
+
+			return cnt == 1;
+
+		}finally{
+			close(rsT);
+			close(rsC);
+		}
+	}
+
+	/*
 	 * <p>Build a table prefix with the given schema name.</p>
 	 * 
 	 * <p>By default, this function returns: schemaName + "_".</p>
@@ -2339,13 +2479,13 @@ public class JDBCConnection implements DBConnection {
 	 * @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.
diff --git a/src/tap/formatter/VOTableFormat.java b/src/tap/formatter/VOTableFormat.java
index 2a4d1ae..07c80e1 100644
--- a/src/tap/formatter/VOTableFormat.java
+++ b/src/tap/formatter/VOTableFormat.java
@@ -569,11 +569,11 @@ public class VOTableFormat implements OutputFormat {
 				return isScalar ? Boolean.class : boolean[].class;
 			case DOUBLE:
 				return isScalar ? Double.class : double[].class;
-			case DOUBLE_COMPLEX:
+			case DOUBLECOMPLEX:
 				return double[].class;
 			case FLOAT:
 				return isScalar ? Float.class : float[].class;
-			case FLOAT_COMPLEX:
+			case FLOATCOMPLEX:
 				return float[].class;
 			case INT:
 				return isScalar ? Integer.class : int[].class;
@@ -581,10 +581,10 @@ public class VOTableFormat implements OutputFormat {
 				return isScalar ? Long.class : long[].class;
 			case SHORT:
 				return isScalar ? Short.class : short[].class;
-			case UNSIGNED_BYTE:
+			case UNSIGNEDBYTE:
 				return isScalar ? Short.class : short[].class;
 			case CHAR:
-			case UNICODE_CHAR:
+			case UNICODECHAR:
 			default: /* If the type is not know (theoretically, never happens), return char[*] by default. */
 				return isScalar ? Character.class : String.class;
 		}
diff --git a/src/tap/log/TAPLog.java b/src/tap/log/TAPLog.java
index 2e9118f..a332080 100644
--- a/src/tap/log/TAPLog.java
+++ b/src/tap/log/TAPLog.java
@@ -46,6 +46,7 @@ public interface TAPLog extends UWSLog {
 	 * 	<li>CLEAN_TAP_SCHEMA</li>
 	 * 	<li>CREATE_TAP_SCHEMA</li>
 	 * 	<li>TABLE_EXIST</li>
+	 * 	<li>COLUMN_EXIST</li>
 	 * 	<li>EXEC_UPDATE</li>
 	 * 	<li>ADD_UPLOAD_TABLE</li>
 	 * 	<li>DROP_UPLOAD_TABLE</li>
diff --git a/src/tap/metadata/TAPColumn.java b/src/tap/metadata/TAPColumn.java
index 13cb81a..a8eaa61 100644
--- a/src/tap/metadata/TAPColumn.java
+++ b/src/tap/metadata/TAPColumn.java
@@ -67,7 +67,7 @@ import adql.db.DBType.DBDatatype;
  * </p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (09/2014)
+ * @version 2.0 (02/2015)
  */
 public class TAPColumn implements DBColumn {
 
@@ -112,6 +112,11 @@ public class TAPColumn implements DBColumn {
 	 * <i>Note: Standard TAP column field ; FALSE by default.</i> */
 	private boolean indexed = false;
 
+	/** Flag indicating whether this column can be set to NULL in the database.
+	 * <i>Note: Standard TAP column field ; FALSE by default.</i>
+	 * @since 2.0 */
+	private boolean nullable = 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;
@@ -480,6 +485,7 @@ public class TAPColumn implements DBColumn {
 	 * 
 	 * @return	Its datatype. <i>CAN'T be NULL</i>
 	 */
+	@Override
 	public final DBType getDatatype(){
 		return datatype;
 	}
@@ -534,6 +540,28 @@ public class TAPColumn implements DBColumn {
 		this.indexed = indexed;
 	}
 
+	/**
+	 * Tell whether this column is nullable.
+	 * 
+	 * @return	<i>true</i> if this column is nullable, <i>false</i> otherwise.
+	 * 
+	 * @since 2.0
+	 */
+	public final boolean isNullable(){
+		return nullable;
+	}
+
+	/**
+	 * Set whether this column is nullable or not.
+	 * 
+	 * @param  nullable	<i>true</i> if this column is nullable, <i>false</i> otherwise.
+	 * 
+	 * @since 2.0
+	 */
+	public final void setNullable(boolean nullable){
+		this.nullable = nullable;
+	}
+
 	/**
 	 * Tell whether this column is defined by a standard.
 	 * 
diff --git a/src/tap/metadata/TAPMetadata.java b/src/tap/metadata/TAPMetadata.java
index 7690784..08f329d 100644
--- a/src/tap/metadata/TAPMetadata.java
+++ b/src/tap/metadata/TAPMetadata.java
@@ -23,8 +23,8 @@ package tap.metadata;
 import java.io.IOException;
 import java.io.PrintWriter;
 import java.util.ArrayList;
-import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.NoSuchElementException;
 
@@ -62,7 +62,7 @@ import adql.db.DBType.DBDatatype;
  * </p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (10/2014)
+ * @version 2.0 (02/2015)
  */
 public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResource {
 
@@ -100,7 +100,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 	 * </i></p> 
 	 */
 	public TAPMetadata(){
-		schemas = new HashMap<String,TAPSchema>();
+		schemas = new LinkedHashMap<String,TAPSchema>();
 	}
 
 	/**
@@ -456,7 +456,21 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		response.setContentType("application/xml");
 
 		PrintWriter writer = response.getWriter();
+		write(writer);
 
+		return false;
+	}
+
+	/**
+	 * Format in XML this whole metadata set and write it in the given writer.
+	 * 
+	 * @param writer	Stream in which the XML representation of this metadata must be written.
+	 * 
+	 * @throws IOException	If there is any error while writing the XML in the given writer.
+	 * 
+	 * @since 2.0
+	 */
+	public void write(final PrintWriter writer) throws IOException{
 		writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
 
 		/* TODO The XSD schema for VOSITables should be fixed soon! This schema should be changed here before the library is released!
@@ -470,8 +484,6 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		writer.println("</vosi:tableset>");
 
 		writer.flush();
-
-		return false;
 	}
 
 	/**
@@ -481,6 +493,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 	 * <pre>
 	 * &lt;schema&gt;
 	 * 	&lt;name&gt;...&lt;/name&gt;
+	 * 	&lt;title&gt;...&lt;/title&gt;
 	 * 	&lt;description&gt;...&lt;/description&gt;
 	 * 	&lt;utype&gt;...&lt;/utype&gt;
 	 * 		// call #writeTable(TAPTable, PrintWriter) for each table
@@ -503,6 +516,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		writer.println("\t<schema>");
 
 		writeAtt(prefix, "name", s.getADQLName(), false, writer);
+		writeAtt(prefix, "title", s.getTitle(), true, writer);
 		writeAtt(prefix, "description", s.getDescription(), true, writer);
 		writeAtt(prefix, "utype", s.getUtype(), true, writer);
 
@@ -519,6 +533,7 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 	 * <pre>
 	 * &lt;table type="..."&gt;
 	 * 	&lt;name&gt;...&lt;/name&gt;
+	 * 	&lt;title&gt;...&lt;/title&gt;
 	 * 	&lt;description&gt;...&lt;/description&gt;
 	 * 	&lt;utype&gt;...&lt;/utype&gt;
 	 * 		// call #writeColumn(TAPColumn, PrintWriter) for each column
@@ -539,10 +554,17 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 		final String prefix = "\t\t\t";
 
 		writer.print("\t\t<table");
-		writer.print(VOSerializer.formatAttribute("type", t.getType().toString()));
+		if (t.getType() != null){
+			if (t.getType() != TableType.table)
+				writer.print(VOSerializer.formatAttribute("type", t.getType().toString()));
+		}
 		writer.println(">");
 
-		writeAtt(prefix, "name", t.getADQLName(), false, writer);
+		if (t.isInitiallyQualified())
+			writeAtt(prefix, "name", t.getADQLSchemaName() + "." + t.getADQLName(), false, writer);
+		else
+			writeAtt(prefix, "name", t.getADQLName(), false, writer);
+		writeAtt(prefix, "title", t.getTitle(), true, writer);
 		writeAtt(prefix, "description", t.getDescription(), true, writer);
 		writeAtt(prefix, "utype", t.getUtype(), true, writer);
 
@@ -586,9 +608,10 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 	private void writeColumn(TAPColumn c, PrintWriter writer) throws IOException{
 		final String prefix = "\t\t\t\t";
 
-		writer.print("\t\t\t<column std=\"");
-		writer.print(c.isStd());
-		writer.println("\">");
+		writer.print("\t\t\t<column");
+		if (c.isStd())
+			writer.print(" std=\"true\"");
+		writer.println(">");
 
 		writeAtt(prefix, "name", c.getADQLName(), false, writer);
 		writeAtt(prefix, "description", c.getDescription(), true, writer);
@@ -613,6 +636,8 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 			writeAtt(prefix, "flag", "indexed", true, writer);
 		if (c.isPrincipal())
 			writeAtt(prefix, "flag", "primary", true, writer);
+		if (c.isNullable())
+			writeAtt(prefix, "flag", "nullable", true, writer);
 
 		writer.println("\t\t\t</column>");
 	}
@@ -696,6 +721,8 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 	 * 	This function create the {@link TAPSchema} and all its {@link TAPTable}s objects on the fly.
 	 * </p>
 	 * 
+	 * @param isSchemaSupported	<i>false</i> if the DB name must be prefixed by "TAP_SCHEMA_", <i>true</i> otherwise.
+	 * 
 	 * @return	The whole TAP_SCHEMA definition.
 	 * 
 	 * @see STDSchema#TAPSCHEMA
@@ -704,10 +731,14 @@ public class TAPMetadata implements Iterable<TAPSchema>, VOSIResource, TAPResour
 	 * 
 	 * @since 2.0
 	 */
-	public static final TAPSchema getStdSchema(){
+	public static final TAPSchema getStdSchema(final boolean isSchemaSupported){
 		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);
+		if (!isSchemaSupported)
+			tap_schema.setDBName(null);
 		for(STDTable t : STDTable.values()){
 			TAPTable table = getStdTable(t);
+			if (!isSchemaSupported)
+				table.setDBName(STDSchema.TAPSCHEMA.label + "_" + table.getADQLName());
 			tap_schema.addTable(table);
 		}
 		return tap_schema;
diff --git a/src/tap/metadata/TAPSchema.java b/src/tap/metadata/TAPSchema.java
index 984d087..c43b44e 100644
--- a/src/tap/metadata/TAPSchema.java
+++ b/src/tap/metadata/TAPSchema.java
@@ -21,8 +21,8 @@ package tap.metadata;
  */
 
 import java.awt.List;
-import java.util.HashMap;
 import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.Map;
 
 import tap.metadata.TAPTable.TableType;
@@ -44,7 +44,7 @@ import tap.metadata.TAPTable.TableType;
  * </i></p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (08/2014)
+ * @version 2.0 (02/2015)
  */
 public class TAPSchema implements Iterable<TAPTable> {
 
@@ -55,6 +55,11 @@ public class TAPSchema implements Iterable<TAPTable> {
 	 * <i>Note: It MAY be NULL. By default, it is the ADQL name.</i> */
 	private String dbName = null;
 
+	/** Descriptive, human-interpretable name of the schema.
+	 * <i>Note: Standard TAP schema field ; MAY be NULL.</i>
+	 * @since 2.0 */
+	private String title = null;
+
 	/** Description of this schema.
 	 * <i>Note: Standard TAP schema field ; MAY be NULL.</i> */
 	private String description = null;
@@ -92,7 +97,7 @@ public class TAPSchema implements Iterable<TAPTable> {
 		int indPrefix = schemaName.lastIndexOf('.');
 		adqlName = (indPrefix >= 0) ? schemaName.substring(indPrefix + 1).trim() : schemaName.trim();
 		dbName = adqlName;
-		tables = new HashMap<String,TAPTable>();
+		tables = new LinkedHashMap<String,TAPTable>();
 	}
 
 	/**
@@ -178,6 +183,28 @@ public class TAPSchema implements Iterable<TAPTable> {
 		dbName = name;
 	}
 
+	/**
+	 * Get the title of this schema.
+	 * 
+	 * @return	Its title. <i>MAY be NULL</i>
+	 * 
+	 * @since 2.0
+	 */
+	public final String getTitle(){
+		return title;
+	}
+
+	/**
+	 * Set the title of this schema.
+	 * 
+	 * @param title	Its new title. <i>MAY be NULL</i>
+	 * 
+	 * @since 2.0
+	 */
+	public final void setTitle(final String title){
+		this.title = title;
+	}
+
 	/**
 	 * Get the description of this schema.
 	 * 
diff --git a/src/tap/metadata/TAPTable.java b/src/tap/metadata/TAPTable.java
index 9f001f3..f3030d0 100644
--- a/src/tap/metadata/TAPTable.java
+++ b/src/tap/metadata/TAPTable.java
@@ -50,7 +50,7 @@ import adql.db.DBType;
  * </i></p>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (08/2014)
+ * @version 2.0 (02/2015)
  */
 public class TAPTable implements DBTable {
 
@@ -70,6 +70,11 @@ public class TAPTable implements DBTable {
 	/** Name that this table MUST have in ADQL queries. */
 	private final String adqlName;
 
+	/** <p>Indicate whether the ADQL name has been given at creation with a schema prefix or not.</p>
+	 * <p><i>Note: This information is used only when writing TAP_SCHEMA.tables or when writing the output of the resource /tables.</i></p>
+	 * @since 2.0 */
+	private boolean isInitiallyQualified;
+
 	/** 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;
@@ -84,6 +89,11 @@ public class TAPTable implements DBTable {
 	 * <i>Note: Standard TAP table field ; CAN NOT be NULL ; by default, it is "table".</i> */
 	private TableType type = TableType.table;
 
+	/** Descriptive, human-interpretable name of the table.
+	 * <i>Note: Standard TAP table field ; MAY be NULL.</i>
+	 * @since 2.0 */
+	private String title = null;
+
 	/** Description of this table.
 	 * <i>Note: Standard TAP table field ; MAY be NULL.</i> */
 	private String description = null;
@@ -125,6 +135,7 @@ public class TAPTable implements DBTable {
 			throw new NullPointerException("Missing table name !");
 		int indPrefix = tableName.lastIndexOf('.');
 		adqlName = (indPrefix >= 0) ? tableName.substring(indPrefix + 1).trim() : tableName.trim();
+		isInitiallyQualified = (indPrefix >= 0);
 		dbName = adqlName;
 		columns = new LinkedHashMap<String,TAPColumn>();
 		foreignKeys = new ArrayList<TAPForeignKey>();
@@ -217,6 +228,38 @@ public class TAPTable implements DBTable {
 		return adqlName;
 	}
 
+	/**
+	 * <p>Tells whether the ADQL name of this table must be qualified in the "table_name" column of TAP_SCHEMA.tables
+	 * and in the /schema/table/name field of the resource /tables.</p>
+	 * 
+	 * <p><i>Note: this value is set automatically by the constructor: "true" if the table name was qualified,
+	 * "false" otherwise. It can be changed with the function {@link #setInitiallyQualifed(boolean)}, BUT by doing so
+	 * you may generate a mismatch between the table name of TAP_SCHEMA.tables and the one of /tables.</i></p>
+	 * 
+	 * @return	<i>true</i> if the table name must be qualified in TAP_SCHEMA.tables and in /tables, <i>false</i> otherwise.
+	 * 
+	 * @since 2.0
+	 */
+	public final boolean isInitiallyQualified(){
+		return isInitiallyQualified;
+	}
+
+	/**
+	 * <p>Let specifying whether the table name must be qualified in TAP_SCHEMA.tables and in the resource /tables.</p>
+	 * 
+	 * <p><b>WARNING: Calling this function may generate a mismatch between the table name of TAP_SCHEMA.tables and
+	 * the one of the resource /tables. So, be sure to change this flag before setting the content of TAP_SCHEMA.tables
+	 * using {@link tap.db.JDBCConnection#setTAPSchema(TAPMetadata)}.</b></p>
+	 * 
+	 * @param mustBeQualified	<i>true</i> if the table name in TAP_SCHEMA.tables and in the resource /tables must be qualified by the schema name,
+	 *                       	<i>false</i> otherwise.
+	 * 
+	 * @since 2.0
+	 */
+	public final void setInitiallyQualifed(final boolean mustBeQualified){
+		isInitiallyQualified = mustBeQualified;
+	}
+
 	@Override
 	public final String getDBName(){
 		return dbName;
@@ -233,7 +276,8 @@ public class TAPTable implements DBTable {
 	 */
 	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;
 	}
 
 	@Override
@@ -309,6 +353,28 @@ public class TAPTable implements DBTable {
 			this.type = type;
 	}
 
+	/**
+	 * Get the title of this table.
+	 * 
+	 * @return	Its title. <i>MAY be NULL</i>
+	 * 
+	 * @since 2.0
+	 */
+	public final String getTitle(){
+		return title;
+	}
+
+	/**
+	 * Set the title of this table.
+	 * 
+	 * @param title	Its new title. <i>MAY be NULL</i>
+	 * 
+	 * @since 2.0
+	 */
+	public final void setTitle(final String title){
+		this.title = title;
+	}
+
 	/**
 	 * Get the description of this table.
 	 * 
diff --git a/src/tap/metadata/VotType.java b/src/tap/metadata/VotType.java
index 35f8bab..3f27046 100644
--- a/src/tap/metadata/VotType.java
+++ b/src/tap/metadata/VotType.java
@@ -20,10 +20,10 @@ package tap.metadata;
  *                       Astronomisches Rechen Institut (ARI)
  */
 
-import adql.db.DBType;
-import adql.db.DBType.DBDatatype;
 import tap.TAPException;
 import uk.ac.starlink.votable.VOSerializer;
+import adql.db.DBType;
+import adql.db.DBType.DBDatatype;
 
 /**
  * <p>Describes a full VOTable type. Thus it includes the following field attributes:</p>
@@ -34,18 +34,18 @@ import uk.ac.starlink.votable.VOSerializer;
  * </ul>
  * 
  * @author Gr&eacute;gory Mantelet (CDS;ARI)
- * @version 2.0 (07/2014)
+ * @version 2.0 (02/2015)
  */
 public final class VotType {
 	/**
 	 * All possible values for a VOTable datatype (i.e. boolean, short, char, ...).
 	 * 
 	 * @author Gr&eacute;gory Mantelet (ARI) - gmantele@ari.uni-heidelberg.de
-	 * @version 2.0 (07/2014)
+	 * @version 2.0 (01/2015)
 	 * @since 2.0
 	 */
 	public static enum VotDatatype{
-		BOOLEAN("boolean"), BIT("bit"), UNSIGNED_BYTE("unsignedByte"), SHORT("short"), INT("int"), LONG("long"), CHAR("char"), UNICODE_CHAR("unicodeChar"), FLOAT("float"), DOUBLE("double"), FLOAT_COMPLEX("floatComplex"), DOUBLE_COMPLEX("doubleComplex");
+		BOOLEAN("boolean"), BIT("bit"), UNSIGNEDBYTE("unsignedByte"), SHORT("short"), INT("int"), LONG("long"), CHAR("char"), UNICODECHAR("unicodeChar"), FLOAT("float"), DOUBLE("double"), FLOATCOMPLEX("floatComplex"), DOUBLECOMPLEX("doubleComplex");
 
 		private final String strExpr;
 
@@ -164,7 +164,7 @@ public final class VotType {
 				break;
 
 			case BINARY:
-				this.datatype = VotDatatype.UNSIGNED_BYTE;
+				this.datatype = VotDatatype.UNSIGNEDBYTE;
 				this.arraysize = Integer.toString(tapType.length > 0 ? tapType.length : 1);
 				this.xtype = null;
 				break;
@@ -173,13 +173,13 @@ public final class VotType {
 				/* TODO HOW TO MANAGE VALUES WHICH WHERE ORIGINALLY NUMERIC ARRAYS ?
 				 * (cf the IVOA document TAP#Upload: votable numeric arrays should be converted into VARBINARY...no more array information and particularly the datatype)
 				 */
-				this.datatype = VotDatatype.UNSIGNED_BYTE;
+				this.datatype = VotDatatype.UNSIGNEDBYTE;
 				this.arraysize = (tapType.length > 0 ? tapType.length + "*" : "*");
 				this.xtype = null;
 				break;
 
 			case BLOB:
-				this.datatype = VotDatatype.UNSIGNED_BYTE;
+				this.datatype = VotDatatype.UNSIGNEDBYTE;
 				this.arraysize = "*";
 				this.xtype = VotType.XTYPE_BLOB;
 				break;
@@ -282,7 +282,7 @@ public final class VotType {
 				return convertNumericType(DBDatatype.DOUBLE);
 
 				/* BINARY TYPES */
-			case UNSIGNED_BYTE:
+			case UNSIGNEDBYTE:
 				// BLOB exception:
 				if (xtype != null && xtype.equalsIgnoreCase(XTYPE_BLOB))
 					return new DBType(DBDatatype.BLOB);
diff --git a/test/tap/db/JDBCConnectionTest.java b/test/tap/db/JDBCConnectionTest.java
index bd43211..b018612 100644
--- a/test/tap/db/JDBCConnectionTest.java
+++ b/test/tap/db/JDBCConnectionTest.java
@@ -98,7 +98,7 @@ public class JDBCConnectionTest {
 			TAPMetadata meta = createCustomSchema();
 			TAPTable customColumns = meta.getTable(STDSchema.TAPSCHEMA.toString(), STDTable.COLUMNS.toString());
 			TAPTable[] tapTables = conn.mergeTAPSchemaDefs(meta);
-			TAPSchema stdSchema = TAPMetadata.getStdSchema();
+			TAPSchema stdSchema = TAPMetadata.getStdSchema(conn.supportsSchema);
 			assertEquals(5, tapTables.length);
 			assertTrue(equals(tapTables[0], stdSchema.getTable(STDTable.SCHEMAS.label)));
 			assertEquals(customColumns.getSchema(), tapTables[0].getSchema());
@@ -340,22 +340,22 @@ public class JDBCConnectionTest {
 				// 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));
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.SCHEMAS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.SCHEMAS.label), dbMeta));
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.TABLES.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.TABLES.label), dbMeta));
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.COLUMNS.label), dbMeta));
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEYS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEYS.label), dbMeta));
+				assertTrue(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEY_COLUMNS.label : 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));
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.SCHEMAS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.SCHEMAS.label), dbMeta));
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.TABLES.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.TABLES.label), dbMeta));
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.COLUMNS.label), dbMeta));
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEYS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEYS.label), dbMeta));
+				assertFalse(conn.isTableExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEY_COLUMNS.label : 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!");
@@ -363,6 +363,43 @@ public class JDBCConnectionTest {
 		}
 	}
 
+	@Test
+	public void testIsColumnExisting(){
+		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
+		JDBCConnection[] connections = new JDBCConnection[]{pgJDBCConnection,sensPgJDBCConnection,sqliteJDBCConnection,sensSqliteJDBCConnection};
+		int i = -1;
+		for(JDBCConnection conn : connections){
+			i++;
+			try{
+				// Get the database metadata:
+				DatabaseMetaData dbMeta = conn.connection.getMetaData();
+
+				// Prepare the test:
+				createTAPSchema(conn);
+				// Test the existence of one column for all TAP_SCHEMA tables:
+				assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.SCHEMAS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.SCHEMAS.label), "schema_name", dbMeta));
+				assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.TABLES.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.TABLES.label), "table_name", dbMeta));
+				assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.COLUMNS.label), "column_name", dbMeta));
+				assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEYS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEYS.label), "key_id", dbMeta));
+				assertTrue(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEY_COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEY_COLUMNS.label), "key_id", dbMeta));
+				// Test the non-existence of any column:
+				assertFalse(conn.isColumnExisting(null, null, "foo", dbMeta));
+
+				// Prepare the test:
+				dropSchema(STDSchema.TAPSCHEMA.label, conn);
+				// Test the non-existence of the same column for all TAP_SCHEMA tables:
+				assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.SCHEMAS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.SCHEMAS.label), "schema_name", dbMeta));
+				assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.TABLES.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.TABLES.label), "table_name", dbMeta));
+				assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.COLUMNS.label), "column_name", dbMeta));
+				assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEYS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEYS.label), "key_id", dbMeta));
+				assertFalse(conn.isColumnExisting(STDSchema.TAPSCHEMA.label, (conn.supportsSchema ? STDTable.KEY_COLUMNS.label : STDSchema.TAPSCHEMA.label + "_" + STDTable.KEY_COLUMNS.label), "key_id", dbMeta));
+			}catch(Exception ex){
+				ex.printStackTrace(System.err);
+				fail("{" + conn.getID() + "} Testing the existence of a column should not throw an error!");
+			}
+		}
+	}
+
 	@Test
 	public void testAddUploadedTable(){
 		// There should be no difference between a POSTGRESQL connection and a SQLITE one!
@@ -427,7 +464,7 @@ public class JDBCConnectionTest {
 					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());
+						assertEquals("Impossible to create the user uploaded table in the database: " + conn.translator.getTableName(tableDef, conn.supportsSchema) + "! This table already exists.", ex.getMessage());
 					else{
 						ex.printStackTrace(System.err);
 						fail("{" + conn.ID + "} DBException was the expected exception!");
@@ -480,24 +517,25 @@ public class JDBCConnectionTest {
 
 	@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")){
+
+			TAPSchema schema = TAPMetadata.getStdSchema(conn.supportsSchema);
+			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);
+
+			/*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{
@@ -571,6 +609,33 @@ public class JDBCConnectionTest {
 		JDBCConnectionTest.dropSchema(STDSchema.TAPSCHEMA.label, conn);
 	}
 
+	/**
+	 * <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 static String getTablePrefix(final String schemaName){
+		if (schemaName != null && schemaName.trim().length() > 0)
+			return schemaName + "_";
+		else
+			return "";
+	}
+
 	private static void dropSchema(final String schemaName, final JDBCConnection conn){
 		Statement stmt = null;
 		ResultSet rs = null;
@@ -582,7 +647,7 @@ public class JDBCConnectionTest {
 				stmt.executeUpdate("DROP SCHEMA IF EXISTS " + formatIdentifier(schemaName, caseSensitive) + " CASCADE;");
 			else{
 				startTransaction(conn);
-				final String tablePrefix = conn.getTablePrefix(schemaName);
+				final String tablePrefix = getTablePrefix(schemaName);
 				final int prefixLen = tablePrefix.length();
 				if (prefixLen <= 0)
 					return;
@@ -621,20 +686,13 @@ public class JDBCConnectionTest {
 			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)){
+					if (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);
@@ -680,10 +738,10 @@ public class JDBCConnectionTest {
 			final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA);
 			final boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE);
 			String tablePrefix = formatIdentifier(schemaName, sCaseSensitive);
-			if (tablePrefix == null)
+			if (!conn.supportsSchema || tablePrefix == null)
 				tablePrefix = "";
 			else
-				tablePrefix += (conn.supportsSchema ? "." : "_");
+				tablePrefix += ".";
 			stmt = conn.connection.createStatement();
 			stmt.executeUpdate("CREATE TABLE " + tablePrefix + formatIdentifier(tableName, tCaseSensitive) + " (ID integer);");
 		}catch(Exception ex){
@@ -695,9 +753,10 @@ public class JDBCConnectionTest {
 		}
 	}
 
-	private static void createTAPSchema(final JDBCConnection conn){
+	private static TAPMetadata createTAPSchema(final JDBCConnection conn){
 		dropSchema(STDSchema.TAPSCHEMA.label, conn);
 
+		TAPMetadata metadata = new TAPMetadata();
 		Statement stmt = null;
 		try{
 			final boolean sCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.SCHEMA);
@@ -708,7 +767,7 @@ public class JDBCConnectionTest {
 					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);
+					tableNames[i] = formatIdentifier(getTablePrefix(STDSchema.TAPSCHEMA.label) + tableNames[i], tCaseSensitive);
 			}
 
 			startTransaction(conn);
@@ -718,13 +777,13 @@ public class JDBCConnectionTest {
 			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("CREATE TABLE " + tableNames[0] + "(\"schema_name\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR,\"dbname\" 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("CREATE TABLE " + tableNames[1] + "(\"schema_name\" VARCHAR,\"table_name\" VARCHAR,\"table_type\" VARCHAR,\"description\" VARCHAR,\"utype\" VARCHAR,\"dbname\" 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("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,\"dbname\" VARCHAR, 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\"));");
@@ -733,19 +792,24 @@ public class JDBCConnectionTest {
 			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());
+			/*if (!conn.supportsSchema){
+				TAPSchema stdSchema = TAPMetadata.getStdSchema();
+				for(TAPTable t : stdSchema)
+					t.setDBName(getTablePrefix(STDSchema.TAPSCHEMA.label) + t.getADQLName());
+				metadata.addSchema(stdSchema);
+			}else*/
+			metadata.addSchema(TAPMetadata.getStdSchema(conn.supportsSchema));
 
 			ArrayList<TAPTable> lstTables = new ArrayList<TAPTable>();
 			for(TAPSchema schema : metadata){
-				stmt.executeUpdate("INSERT INTO " + tableNames[0] + " VALUES('" + schema.getADQLName() + "','" + schema.getDescription() + "','" + schema.getUtype() + "')");
+				stmt.executeUpdate("INSERT INTO " + tableNames[0] + " VALUES('" + schema.getADQLName() + "','" + schema.getDescription() + "','" + schema.getUtype() + "','" + schema.getDBName() + "')");
 				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() + "')");
+				stmt.executeUpdate("INSERT INTO " + tableNames[1] + " VALUES('" + table.getADQLSchemaName() + "','" + table.getADQLName() + "','" + table.getType() + "','" + table.getDescription() + "','" + table.getUtype() + "','" + table.getDBName() + "')");
 				for(DBColumn c : table)
 					lstCols.add(c);
 
@@ -754,7 +818,7 @@ public class JDBCConnectionTest {
 
 			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) + ")");
+				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) + ",'" + col.getDBName() + "')");
 			}
 
 			commit(conn);
@@ -766,6 +830,8 @@ public class JDBCConnectionTest {
 		}finally{
 			close(stmt);
 		}
+
+		return metadata;
 	}
 
 	private static void startTransaction(final JDBCConnection conn){
@@ -956,10 +1022,10 @@ public class JDBCConnectionTest {
 			TAPSchema tapSchema = meta.getSchema(STDSchema.TAPSCHEMA.toString());
 
 			String schemaPrefix = formatIdentifier(tapSchema.getDBName(), conn.translator.isCaseSensitive(IdentifierField.SCHEMA));
-			if (schemaPrefix == null)
+			if (!conn.supportsSchema || schemaPrefix == null)
 				schemaPrefix = "";
 			else
-				schemaPrefix += (conn.supportsSchema ? "." : "_");
+				schemaPrefix += ".";
 
 			boolean tCaseSensitive = conn.translator.isCaseSensitive(IdentifierField.TABLE);
 			TAPTable tapTable = tapSchema.getTable(STDTable.SCHEMAS.toString());
diff --git a/test/tap/db/TestTAPDb.db b/test/tap/db/TestTAPDb.db
index d722315944c5da861381aede0e50b8951ae85fb2..c36006ea9746e081c41096322c5a83eae4b678d4 100644
GIT binary patch
literal 26624
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

literal 24576
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

-- 
GitLab