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é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é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é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é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é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é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> * <schema> * <name>...</name> + * <title>...</title> * <description>...</description> * <utype>...</utype> * // 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> * <table type="..."> * <name>...</name> + * <title>...</title> * <description>...</description> * <utype>...</utype> * // 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é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é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é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é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