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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
E|2^4%-~a#s

literal 24576
zcmeHPeQX@Zb>E#`@<{5I-^<nUB595nM`n+r?~Xi4q%8Z6)JZz=QzXUI5fXaITZ(HQ
z?@qUOv}F`VsM>8%7;e%(9RJZ4C|WlL>bgygwk{e6iUqU*0u-<tz(yP-X<Z~m5+L7d
z+B8t#%<SIH9Z8XrRar{+$i3TrJM)`2Z)V=SdGj%QcHAr)csW;C(~EdOkOff`;us5p
z^sj;-?1ih1UCpn-uMC&-5uKaue*xprza3N{fFxcL#7pRT=}qaH^gqI9;ZG1Y9#uWs
z0}qIob47h)BXrE8MIz#5v8XR*jJd@0!tChj<V0e@T3RvIbZavOjm{(!b4fgx7#UCE
zp3MPz?(bod7qa@A(Szp`Goz;yGkra2!&)ks`J$Q2a@11sQl1Nir)N?ViJ3?6Sn`o-
zpx)lm2kW(uMnu8Pri~}8iy08&f?g`-=<~v6P8K3Hw9DQ5JlgT&;%}d#**!Ns?qv5|
zK9kea$tU!+e8$+0-cgnH?Ly)E7iJP?qTz5DpO`vFgfu;q98JxprY8FqP9(Q^lefx(
zngp?xA5NdZsmZzIspL%G=@~pbkr*F`=f3$h{Q`!981Hk^a>VSU;5^Uf%UJ7{nKdgI
zv*y~Oo_5~$UOqbJ(foe#X(pV}T&A>^wd|jUs;u|eR48gH$l8)#qH$}f+mwISEb`T1
zDa}7sSB{>vUes$ymiZ)9A4f(#`GT2UGV^+dqtW_d0B$*yRpg)M468V4JkByzNxix%
zavANzJ$w#(dk>A*YYG2$71I{hLd3plByE$lYUxMh#{IYwa3%0TNMK#83l!kmm=!7=
z#jH`pxn)e7yM;5RRW!2~u%1n0roR@+V{yg6b`RO-1SLV3TCi}wv<NX*j5Id0xY#Vp
z0f9@RiDZ_v|DSvixpvcZCE!Zn&PqU$0*w_FQ||x&R(ec2fu0cmUVP}z@^dezcB$SI
z@M{}MPNCXrm+I_s(AY$QJE|CN^-RW5(nOy#tz0RNemazyr8>S^aj%#bjB}>3Wfa!U
zC8K}i!8%VMs9hfA(q1(#Eo@4kqzJ=1!@Q8i5WvEVr6SH3a_c5^BKWZqut>TPhsTVW
zT?MWdFHtHNu^-Rn@M<>qIDJ9$M?0q#!z#z_1NIEk&u}@SX_mm;XI&NH(M<Pmq-0M(
z)h@4Avu{=8woAUto#T#dKfYovt(5ag`c=xh4WJhc=wS)SHkoiFi#O%v!%}@9r18CK
zBAqtL*iz5Tk+CdI;tZ;Xx?*zqwR>&))~PBst)|zmE&S7BeZZ$(;ILROs-PHY2c&ZU
z|2I-j`W@*{q_0Wezn5F2%eyOqyI2BUa=<TEl%uV5>$~MZkRx+?yskqIsMW75dA}S8
zakLt3Bh@5gs}$5J2Yg)5&(uSWZ|)zY*QM8Md~>(gQ+0dMw(*+X1AZ@84zB-?cv5>l
zVE>==s(?^RKq={I=~eporZ5bDf^htFy|(das_db!cFGrzPu6Q^L)$qTD>`n07=Tso
zZ$cn?N8SjXx*3;0M0w@jnIQmflkaPW0H!=70C~@uA*qo+F)W=qk(+e-3W8X#g+k)x
zhiNBJQ&u&P-fDMy&0BE0GmGAJ+0%)JJK(a1Ix(>@w7Z7q3|ea2pF?l012iOEgKwcr
z`lWOYy?~~XSNxLr2cjts2tN~E71o91ji<O)A81ij?E!%BiwzBp0%_lLDvf@zK_ZM^
zfox9%T|d^WsKH}Z5x5x{?slul4=aj#U!p2<Q7>FDiVL(hFPAczOZZ|*&zQ?5jASVh
zllea<_2XVe4O9ax7jkR6jXt<XQGG{f`i+e&x2y@2vgXB-LGp=VY@IEV^b1a+2b+{o
z2SlPWY7lB{wv)M>6Yw`0m5^`SfFl`b_qr2yyg~76OEzeuU|V`r?C0hlnNDJJ+4c>?
zxVKO+tb8t;ws0|rbxh{3@LDcyWE`{+kKzxG)}nExu~;yTW!v{m=T1rSutXh<ll6cx
zMlpzRY>wCuorGhzv^@H7K4)3xB41n%&Y3#Jr|D&0n6X-&F;=j<$X8ree8Eb2GpwDm
zMMF8<9F-JbV4#K@8e)M=0p_v>NMe{(wLZLV8jm|Mrx3(s`5^LOX|tH5O42(j46Iw&
zEMjXVSIU6<#Q--0a}Gh30<l{T;DiX7IbRkq1LWjenGRsEq-OK(7#b9!ckY9)_7*b!
z-!DETNJCOUd<u<<@1c<RlK4gOE%60(9yOtE@fYaR=w(zuZ=qjFZRoq`s<bRUCCy2n
zlfDPuf-gzWFSzZWI<;=T{rhD9LD3!rR?YnGZ;|~S&Ks~0_jSlH9d3uT=a4_7%Kifm
ztvuK4Z<YP3LnRZ^-JP<3pF?mat$l5>Uvr4GS?%RhEG?xq9tUz(?9ya^(22se9+jsv
z4rxR0?2`QfCw!&GckGjc2SuVV?cCV9n?1Gr<zSG1OBFMF*VZKmef&G7bz7TdRU>59
z=d^UnDn+n39-CEJy^n_BdkUpPRs)n;QUBf+S*3|s41JF#hdRW~+PbM*4*6>7OgT4F
z*|H|dcxs>`vMC%}9-hv%;rdqDr?F))44|${_62PM7hhIoU%)0a2@`E3ZK4uQgMb=r
z+@vlX|39b!P5LGJ5A;W99(9QSD1Jtq6+48th0h7A@NTEiNv{_=bX8TU1waaeW7EDJ
zZuSN%{U#BwT?3e;Z`gYJ6)$ID5PVu`>DN{^!Z76rxV`~D#6i1N+=upfIs1a7#aEi0
z&5H+{yqtMK;#aEI&anp?y_|JHV#5U<^Ban1tij9K0escF1v4Pw@p5Ls=32W8D<D}9
z5M?XChTtkVyWP<UNY!~cBY@T2TeJZlmc5(}z@WG)x@DedD5u{&B6&FzfC13&oh*P!
z1QE*?03Y$z41jSFa#A(`7#=6uUG@JuF(;sPsLhW5AFZP<^a=4@@rS5R{3?159Y+%=
zgs!4*p%<k(DJ-3ky3w`To_{B`;J&|u65u;<Fd6(?m<-)pnhZX^t0yMI-mOdqZIj8+
z#is}+gUaI&lcAILZ?=}_T3ZKCXRoQr(7v_F&~`(Uq4fqPL(A4CLvyvsploF_c(*he
z_H1D?G}V|4_iF!t2dk=^lq&&O0(XN1oc{lA;Kk*^m4GXOJ1YSN{gY59z9FEmqW_k@
zEWUxBytC=MsqJ0@=<~vB+R%`AxsHzd?3uWY*pz28mF$}$jsx5HuAa4Xs4X{|+H(Sx
zXZR{pTRogtj-yt2@+$P|y(`R{(bL;IHeIjXwrMO7qI{E+pc^M9Jhyuiw32BiB3e!y
zk#{6<WNv80QK_e+^1O{0-{&;E<NuRBFFh)K1pWKg^M5<kQP=;!vy0B1|I^+1zq@7r
zkNW?}PT1?N{l9~5b6>j>`1O}SD`asGTq<0!lR+SAfh;~la5ck4mI;x)|HNGI!bQTj
zz~zCf9xifDfctSJ;7Z^glK^SZEkaV*!Tu+@f#?M1*U<wFK<gA-``@+y6ZB-2>+br$
zdtv=w#r`J^xk>m7LHcWHL25#O32Xiu#Fxd73fF;ncYS7>6pyFHFCkChfQEeW(q~`3
z=>5X;@nrAoFSh;RxAVC*6DP81X=bcvIPU%V8=nOBqXRe`!Le9>Bs3Vsks&-VIC40Z
zN{KQ%ab~`;f_Z>o*7=Okzg&IzS|j?V@aEs;^CrfLiv{VS^MUj6_CI}_NF*A?(HM>#
z?vI548=eM^jzj?SP-@-~`xx$QLj~6dxY>959{l4UT)7f0{?(N7^hADX#l%zj6*TgW
z_MLd!E1iVb$YC5k0_^q=9Euzzx6#N*q(3%q2+uOmk9jJf2LW{L#~W|I`?F77>3i;{
z6OVQ6&kr8Oc-&k>{lBZl;vLWIzd}G`ICd0ADCi(CjR#{R1H-BLVdD^-36aT&pJv!+
z>uqc?a3BcS=eo~4tB$`AkA3*L-@EwD$MR3YQ517}N%+i+XI%=%@W2tscK_i}G=jrN
ziBuzydj=_dgF#N$QAk9}O9jX&6-ue}pYc=gp8Uqgmp(<=oi+-_ocv$$*86`>xD5|d
zv>_1f;1LWZHh5%Y5CrfQhIn4CAcg>O=zFA?-ioJw^U${y|HXVBqzZ_=o#Kz;t*2hW
z1TlgK2k^j9|6nK<#)D8^VItJ~=ZAZ1Brq#g!2JN;_c2tu@8q8N<oT<5M|>)OL0`k8
zx}~;@4RPs>-+z>Vhj9!f8tspU4g)x;Ya`+Q7)V0+4&xY>|CZ~lDnLDWv9rDIi|ESH
z&~eXK#y(+@Iy<|RLk~TxeL3DfFc1e+QV+sJx{*UsD6%k!BoRxc7~rH>;gSHJ=?R~U
z>c17A_{I4v-G|?^qNg!%xi%Y;_rzPj{|qKv4&dkz4i7>bhz${jqp^{pl+N%-`)?Kw
z3({4oBn8pe(VwG5^bzsL;%lNM_6zR_&%nz~pNV~nzrzGFTkvl?nPJh1j$yG}Hmw(&
z^GA;12wpSvEG#-Nl#D(cwx5>ujAhtoAYDKEA;s_C1ZOXE)w8gn91hun)zB=RT|v(I
z75{<NS`N#rWOdj9Zy(NdGpM88ieH_rMI}pSGDh}7afK`=T-OUG*_(rHlC$L2a&kNB
zQ~dkJYC)V}i4$kX>Dqj<l#?zB&%&nqCAwHK3fjQ#XOHZKmH$JvAl%|O;Dm{b>;cfT
z<*gS38Wde@Q^ck8xzQ7Z{VgL6cR{`@E9mX=CQE$CMK=7;PgP2u9VX{+I-*wXDzM{n
zF7GYZ-mzc~Mzj+!f|O~pir&UxKh&$pZs~$TC(fx3#UGfejYB>1Gzz`jdW}4NRbFvF
z-L3==RtlY)RjHcr0J<YQvapYywgcxX@GgIcwedD37-X#3`(>QT7H(o?8@$R|QU~o=
ztHQ0TCA)7NG;+Jc%2C$-R|V+{(xTMsuKxv>sJs5xUH{7#h7&dJw6b5n^}p2rPujmI
zy(CC4x&8mnhKk$&(+!?>|LgYuY}+Xl$2R@-ZR-E$_bdK`7i_bw!Lfj<_KeMuQ-a`M
z$IJSvb0kwAJK+gTGF*hCMd37{0&Kc8`W>}K+P@-vQh=TRi?G7~yXeo5iDKff;2f}$
zh=qTKSN}Jk$-Q1MrPXo2mm5jg73-D<64TvYZXjW!y{~b^tk27hBW%R-*x?q35hFD3
zAXhqkax#5$qlgjB%Z(yzjGG)p9PWZ`L*+q)jj@X{L{x=BD~}<ZcsCwG40S?zmxmBe
zoL!9|5*@rj;$*dQg7Yp05RrC(C=VcP2==qx_~B%mmm5FWQEp-QFxBeih7X)|eygL0
lu@)~kdf)(WW$-}SKRy4`_5Zp4KjK_<{eR%D{Qu+s`(F!6{Hp)}

-- 
GitLab