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