Mercurial > hg > monetdb-java
changeset 713:c3c424a90a42
Improve implementation of ResultSet.getMetaData().
The current implementation creates a new ResultSetMetaData each time this method is called
which is quite costly if it is called from inside a fetch-loop such as in the example on:
https://en.wikipedia.org/wiki/Java_Database_Connectivity#Examples
try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM MyTable")) {
while (rs.next()) {
int numColumns = rs.getMetaData().getColumnCount();
for (int i = 1; i <= numColumns; i++) {
// Column numbers start at 1.
// Also there are many methods on the result set to return
// the column as a particular type. Refer to the Sun documentation
// for the list of valid conversions.
System.out.println( "COLUMN " + i + " = " + rs.getObject(i));
}
}
}
As the ResultSetMetaData is static for a ResultSet it is better to create it once,
cache it in the ResultSet object and return the cached object for next calls to ResultSet.getMetaData().
author | Martin van Dinther <martin.van.dinther@monetdbsolutions.com> |
---|---|
date | Wed, 04 Jan 2023 23:34:14 +0100 (2023-01-04) |
parents | 7cec464246f2 |
children | ad7b08ef7745 |
files | src/main/java/org/monetdb/jdbc/MonetResultSet.java src/main/java/org/monetdb/jdbc/MonetResultSetMetaData.java tests/JDBC_API_Tester.java |
diffstat | 3 files changed, 1163 insertions(+), 835 deletions(-) [+] |
line wrap: on
line diff
--- a/src/main/java/org/monetdb/jdbc/MonetResultSet.java +++ b/src/main/java/org/monetdb/jdbc/MonetResultSet.java @@ -82,9 +82,13 @@ public class MonetResultSet private final String[] columns; /** The MonetDB types of the columns in this ResultSet */ private final String[] types; - /** The JDBC SQL types of the columns in this ResultSet. The content will be derived from the MonetDB types[] */ + /** The JDBC SQL types of the columns in this ResultSet. + * The content will be derived once from the MonetDB String[] types */ private final int[] JdbcSQLTypes; + /** A cache to reduce the number of ResultSetMetaData objects created by getMetaData() to maximum 1 per ResultSet */ + private ResultSetMetaData rsmd; + // the following have protected access modifier for the MonetVirtualResultSet subclass // they are accessed from MonetVirtualResultSet.absolute() /** The current line of the buffer split in columns */ @@ -324,6 +328,7 @@ public class MonetResultSet if (header != null && !header.isClosed()) { header.close(); } + rsmd = null; if (statement instanceof MonetStatement) ((MonetStatement)statement).closeIfCompletion(); } @@ -1237,10 +1242,6 @@ public class MonetResultSet return getLong(findColumn(columnLabel)); } - - /* helper for the anonymous class inside getMetaData */ - private abstract class rsmdw extends MonetWrapper implements ResultSetMetaData {} - /** * Retrieves the number, types and properties of this ResultSet object's * columns. @@ -1249,777 +1250,67 @@ public class MonetResultSet */ @Override public ResultSetMetaData getMetaData() throws SQLException { - // return inner class which implements the ResultSetMetaData interface - return new rsmdw() { - private final String[] schemas = (header != null) ? header.getSchemaNames() : null; - private final String[] tables = (header != null) ? header.getTableNames() : null; - private final int[] lengths = (header != null) ? header.getColumnLengths() : null; - private final int[] precisions = (header != null) ? header.getColumnPrecisions() : null; - private final int[] scales = (header != null) ? header.getColumnScales() : null; - private final MonetConnection conn = (MonetConnection)getStatement().getConnection(); - - // For the methods: isNullable() and isAutoIncrement(), we need to query the server. - // To do this efficiently we query many columns combined in one query and cache the results. - private final int array_size = columns.length + 1; // add 1 as in JDBC columns start from 1 (array from 0). - private final boolean[] _is_queried = new boolean[array_size]; - private final boolean[] _is_fetched = new boolean[array_size]; - private final int[] _isNullable = new int[array_size]; - private final boolean[] _isAutoincrement = new boolean[array_size]; - private int nextUpperbound = array_size; - - /** - * A private utility method to check validity of column index number - * @throws SQLDataException when invalid column index number - */ - private final void checkColumnIndexValidity(final int column) throws SQLDataException { - if (column < 1 || column > columns.length) - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - - /** - * A private method to fetch the isNullable and isAutoincrement values - * combined for a specific column. - * The fetched values are stored in the above array caches. - */ - private final void fetchColumnInfo(final int column) throws SQLException { - // for debug: System.out.println("fetchColumnInfo(" + column + ")"); - checkColumnIndexValidity(column); - if (_is_fetched[column] != true) { - // fetch column info for multiple columns combined in one go - fetchManyColumnsInfo(column); - } - - if (_is_fetched[column]) - return; - - // apparently no data could be fetched for this resultset column, fall back to defaults - _isNullable[column] = columnNullableUnknown; - _isAutoincrement[column] = false; - } - - /** - * A private method to fetch the isNullable and isAutoincrement values - * for many fully qualified columns combined in one SQL query to reduce the number of queries sent. - * As fetching this meta information from the server per column is costly we combine the querying of - * the isNullable and isAutoincrement values and cache it in internal arrays. - * We also do this for many columns combined in one query to reduce - * the number of queries needed for fetching this metadata for all resultset columns. - * Many generic JDBC database tools (e.g. SQuirreL, DBeaver) request this meta data for each - * column of each resultset, so these optimisations reduces the number of meta data queries significantly. - */ - private final void fetchManyColumnsInfo(final int column) throws SQLException { - // for debug: System.out.println("fetchManyColumnsInfo(" + column + ")"); - - // Most queries have less than 80 resultset columns - // So 80 is a good balance between speedup (up to 79x) and size of generated query sent to server - final int MAX_COLUMNS_PER_QUERY = 80; - - // Determine the optimal startcol to make use of fetching up to 80 columns in one query. - int startcol = column; - if ((startcol > 1) && (startcol + MAX_COLUMNS_PER_QUERY >= nextUpperbound)) { - // we can fetch info from more columns in one query if we start with a lower startcol - startcol = nextUpperbound - MAX_COLUMNS_PER_QUERY; - if (startcol < 1) { - startcol = 1; - } else - if (startcol > column) { - startcol = column; - } - nextUpperbound = startcol; // next time this nextUpperbound value will be used - // for debug: System.out.println("fetchManyColumnsInfo(" + column + ")" + (startcol != column ? " changed into startcol: " + startcol : "") + " nextUpperbound: " + nextUpperbound); - } - - final StringBuilder query = new StringBuilder(410 + (MAX_COLUMNS_PER_QUERY * 150)); - /* next SQL query is a simplified version of query in MonetDatabaseMetaData.getColumns(), to fetch only the needed attributes of a column */ - query.append("SELECT " + - "s.\"name\" AS schnm, " + - "t.\"name\" AS tblnm, " + - "c.\"name\" AS colnm, " + - "cast(CASE c.\"null\" WHEN true THEN ").append(ResultSetMetaData.columnNullable) - .append(" WHEN false THEN ").append(ResultSetMetaData.columnNoNulls) - .append(" ELSE ").append(ResultSetMetaData.columnNullableUnknown) - .append(" END AS int) AS nullable, ").append( - "cast(CASE WHEN c.\"default\" IS NOT NULL AND c.\"default\" LIKE 'next value for %' THEN true ELSE false END AS boolean) AS isautoincrement " + - "FROM \"sys\".\"columns\" c " + - "JOIN \"sys\".\"tables\" t ON c.\"table_id\" = t.\"id\" " + - "JOIN \"sys\".\"schemas\" s ON t.\"schema_id\" = s.\"id\" " + - "WHERE "); - - /* combine the conditions for multiple (up to 80) columns into the WHERE-clause */ - String schName = null; - String tblName = null; - String colName = null; - int queriedcolcount = 0; - for (int col = startcol; col < array_size && queriedcolcount < MAX_COLUMNS_PER_QUERY; col++) { - if (_is_fetched[col] != true) { - if (_is_queried[col] != true) { - _isNullable[col] = columnNullableUnknown; - _isAutoincrement[col] = false; - schName = getSchemaName(col); - if (schName != null && !schName.isEmpty()) { - tblName = getTableName(col); - if (tblName != null && !tblName.isEmpty()) { - colName = getColumnName(col); - if (colName != null && !colName.isEmpty()) { - if (queriedcolcount > 0) - query.append(" OR "); - query.append("(s.\"name\" = ").append(MonetWrapper.sq(schName)); - query.append(" AND t.\"name\" = ").append(MonetWrapper.sq(tblName)); - query.append(" AND c.\"name\" = ").append(MonetWrapper.sq(colName)); - query.append(")"); - _is_queried[col] = true; // flag it - queriedcolcount++; - } - } - } - if (_is_queried[col] != true) { - // make sure we do not try to query it again next time as it is not queryable - _is_fetched[col] = true; - } - } - } - } - - if (queriedcolcount == 0) - return; - - // execute query to get information on queriedcolcount (or less) columns. - final Statement stmt = conn.createStatement(); - if (stmt != null) { - // for debug: System.out.println("SQL (len " + query.length() + "): " + query.toString()); - final ResultSet rs = stmt.executeQuery(query.toString()); - if (rs != null) { - String rsSchema = null; - String rsTable = null; - String rsColumn = null; - while (rs.next()) { - rsSchema = rs.getString(1); // col 1 is schnm - rsTable = rs.getString(2); // col 2 is tblnm - rsColumn = rs.getString(3); // col 3 is colnm - // find the matching schema.table.column entry in the array - for (int col = 1; col < array_size; col++) { - if (_is_fetched[col] != true && _is_queried[col]) { - colName = getColumnName(col); - if (colName != null && colName.equals(rsColumn)) { - tblName = getTableName(col); - if (tblName != null && tblName.equals(rsTable)) { - schName = getSchemaName(col); - if (schName != null && schName.equals(rsSchema)) { - // found matching entry - // for debug: System.out.println("Found match at [" + col + "] for " + schName + "." + tblName + "." + colName); - _isNullable[col] = rs.getInt(4); // col 4 is nullable (or "NULLABLE") - _isAutoincrement[col] = rs.getBoolean(5); // col 5 is isautoincrement (or "IS_AUTOINCREMENT") - _is_fetched[col] = true; - queriedcolcount--; - // we found the match, exit the for-loop - col = array_size; - } - } - } - } - } - } - rs.close(); - } - stmt.close(); - } - - if (queriedcolcount != 0) { - // not all queried columns have resulted in a returned data row. - // make sure we do not match those columns again next run - for (int col = startcol; col < array_size; col++) { - if (_is_fetched[col] != true && _is_queried[col]) { - _is_fetched[col] = true; - // for debug: System.out.println("Found NO match at [" + col + "] for " + getSchemaName(col) + "." + getTableName(col) + "." + getColumnName(col)); - } - } - } - } - - /** - * Returns the number of columns in this ResultSet object. - * - * @return the number of columns - */ - @Override - public int getColumnCount() { - return columns.length; - } - - /** - * Indicates whether the designated column is automatically numbered. - * - * This method is currently very expensive for BIGINT, - * INTEGER, SMALLINT and TINYINT result column types - * as it needs to retrieve the information from the - * database using an SQL meta data query. - * - * @param column the first column is 1, the second is 2, ... - * @return true if so; false otherwise - * @throws SQLException if a database access error occurs - */ - @Override - public boolean isAutoIncrement(final int column) throws SQLException { - // only few integer types can be auto incrementable in MonetDB - // see: https://www.monetdb.org/Documentation/SQLReference/DataTypes/SerialDatatypes - switch (getColumnType(column)) { - case Types.BIGINT: - case Types.INTEGER: - case Types.SMALLINT: - case Types.TINYINT: - try { - if (_is_fetched[column] != true) { - fetchColumnInfo(column); - } - return _isAutoincrement[column]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - - return false; - } - - /** - * Indicates whether a column's case matters. - * - * @param column the first column is 1, the second is 2, ... - * @return true for all character string columns else false - */ - @Override - public boolean isCaseSensitive(final int column) throws SQLException { - switch (getColumnType(column)) { - case Types.CHAR: - case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness - case Types.CLOB: - return true; - case Types.VARCHAR: - final String monettype = getColumnTypeName(column); - if (monettype != null && monettype.length() == 4) { - // data of type inet or uuid is not case sensitive - if ("inet".equals(monettype) - || "uuid".equals(monettype)) - return false; - } - return true; - } - - return false; - } - - /** - * Indicates whether the designated column can be used in a - * where clause. - * - * Returning true for all here, even for CLOB, BLOB. - * - * @param column the first column is 1, the second is 2, ... - * @return true - */ - @Override - public boolean isSearchable(final int column) throws SQLException { - checkColumnIndexValidity(column); - return true; - } - - /** - * Indicates whether the designated column is a cash value. - * From the MonetDB database perspective it is by definition - * unknown whether the value is a currency, because there are - * no currency datatypes such as MONEY. With this knowledge - * we can always return false here. - * - * @param column the first column is 1, the second is 2, ... - * @return false - */ - @Override - public boolean isCurrency(final int column) throws SQLException { - checkColumnIndexValidity(column); - return false; - } - - /** - * Indicates whether values in the designated column are signed - * numbers. - * Within MonetDB all numeric types (except oid and ptr) are signed. - * - * @param column the first column is 1, the second is 2, ... - * @return true if so; false otherwise - */ - @Override - public boolean isSigned(final int column) throws SQLException { - // we can hardcode this, based on the colum type - switch (getColumnType(column)) { - case Types.TINYINT: - case Types.SMALLINT: - case Types.INTEGER: - case Types.REAL: - case Types.FLOAT: - case Types.DOUBLE: - case Types.DECIMAL: - case Types.NUMERIC: - return true; - case Types.BIGINT: - final String monettype = getColumnTypeName(column); - if (monettype != null && monettype.length() == 3) { - // data of type oid or ptr is not signed - if ("oid".equals(monettype) - || "ptr".equals(monettype)) - return false; - } - return true; - // All other types should return false - // case Types.BOOLEAN: - // case Types.DATE: // can year be negative? - // case Types.TIME: // can time be negative? - // case Types.TIME_WITH_TIMEZONE: - // case Types.TIMESTAMP: // can year be negative? - // case Types.TIMESTAMP_WITH_TIMEZONE: - default: - return false; - } - } - - /** - * Indicates the designated column's normal maximum width in - * characters. - * - * @param column the first column is 1, the second is 2, ... - * @return the normal maximum number of characters allowed as the - * width of the designated column - * @throws SQLException if there is no such column - */ - @Override - public int getColumnDisplaySize(final int column) throws SQLException { - checkColumnIndexValidity(column); - if (lengths != null) { - try { - return lengths[column - 1]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - return 1; - } - - /** - * Get the designated column's schema name. - * - * @param column the first column is 1, the second is 2, ... - * @return schema name or "" if not applicable - * @throws SQLException if a database access error occurs - */ - @Override - public String getSchemaName(final int column) throws SQLException { - checkColumnIndexValidity(column); - if (schemas != null) { - try { - return schemas[column - 1]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - return ""; - } + if (rsmd == null) { + rsmd = new MonetResultSetMetaData((MonetConnection) getStatement().getConnection(), header); + } + return rsmd; + } - /** - * Gets the designated column's table name. - * - * @param column the first column is 1, the second is 2, ... - * @return table name or "" if not applicable - */ - @Override - public String getTableName(final int column) throws SQLException { - checkColumnIndexValidity(column); - if (tables != null) { - try { - return tables[column - 1]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - return ""; - } - - /** - * Get the designated column's specified column size. - * For numeric data, this is the maximum precision. - * For character data, this is the length in characters. - * For datetime datatypes, this is the length in characters - * of the String representation (assuming the maximum - * allowed precision of the fractional seconds component). - * For binary data, this is the length in bytes. - * For the ROWID datatype, this is the length in bytes. - * 0 is returned for data types where the column size is not applicable. - * - * @param column the first column is 1, the second is 2, ... - * @return precision - * @throws SQLException if a database access error occurs - */ - @Override - public int getPrecision(final int column) throws SQLException { - switch (getColumnType(column)) { - case Types.BIGINT: - return 19; - case Types.INTEGER: - return 10; - case Types.SMALLINT: - return 5; - case Types.TINYINT: - return 3; - case Types.REAL: - return 7; - case Types.FLOAT: - case Types.DOUBLE: - return 15; - case Types.DECIMAL: - case Types.NUMERIC: - // these data types have a variable precision (max precision is 38) - if (precisions != null) { - try { - return precisions[column - 1]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - return 18; - case Types.CHAR: - case Types.VARCHAR: - case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness - case Types.CLOB: - // these data types have a variable length - if (precisions != null) { - try { - int prec = precisions[column - 1]; - if (prec <= 0) { - // apparently no positive precision or max length could be fetched - // use columnDisplaySize() value as fallback - prec = getColumnDisplaySize(column); - precisions[column - 1] = prec; - } - return prec; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - // apparently no precisions array is available - // use columnDisplaySize() value as alternative - return getColumnDisplaySize(column); - case Types.BINARY: - case Types.VARBINARY: - case Types.BLOB: - // these data types have a variable length - if (precisions != null) { - try { - int prec = precisions[column - 1]; - if (prec <= 0) { - // apparently no positive precision or max length could be fetched - // use columnDisplaySize() value as fallback - // It expect number of bytes, not number of hex chars - prec = (getColumnDisplaySize(column) / 2) +1; - precisions[column - 1] = prec; - } - return prec; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - // apparently no precisions array is available - // use columnDisplaySize() value as alternative - // It expect number of bytes, not number of hex chars - return (getColumnDisplaySize(column) / 2) +1; - case Types.DATE: - return 10; // 2020-10-08 - case Types.TIME: - return 15; // 21:51:34.399753 - case Types.TIME_WITH_TIMEZONE: - return 21; // 21:51:34.399753+02:00 - case Types.TIMESTAMP: - return 26; // 2020-10-08 21:51:34.399753 - case Types.TIMESTAMP_WITH_TIMEZONE: - return 32; // 2020-10-08 21:51:34.399753+02:00 - case Types.BOOLEAN: - return 1; - default: - // All other types should return 0 - return 0; - } - } - - /** - * Gets the designated column's number of digits to right of - * the decimal point. - * 0 is returned for data types where the scale is not applicable. - * - * @param column the first column is 1, the second is 2, ... - * @return scale - * @throws SQLException if a database access error occurs - */ - @Override - public int getScale(final int column) throws SQLException { - switch (getColumnType(column)) { - case Types.DECIMAL: - case Types.NUMERIC: - { - // these data types may have a variable scale, max scale is 38 - - // Special handling for: day_interval and sec_interval as they are - // mapped to Types.NUMERIC and Types.DECIMAL types (see MonetDriver typeMap) - // They appear to have a fixed scale (tested against Oct2020) - final String monettype = getColumnTypeName(column); - if ("interval day".equals(monettype)) - return 0; - if ("interval second".equals(monettype)) - return 3; - - if (scales != null) { - try { - return scales[column - 1]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - return 0; - } - case Types.TIME: - case Types.TIME_WITH_TIMEZONE: - case Types.TIMESTAMP: - case Types.TIMESTAMP_WITH_TIMEZONE: - if (scales != null) { - try { - return scales[column - 1]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - // support microseconds, so scale 6 - return 6; // 21:51:34.399753 - - // All other types should return 0 - // case Types.BIGINT: - // case Types.INTEGER: - // case Types.SMALLINT: - // case Types.TINYINT: - // case Types.REAL: - // case Types.FLOAT: - // case Types.DOUBLE: - // case Types.CHAR: - // case Types.VARCHAR: - // case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness - // case Types.CLOB: - // case Types.BINARY: - // case Types.VARBINARY: - // case Types.BLOB: - // case Types.DATE: - // case Types.BOOLEAN: - default: - return 0; - } - } + /** + * Returns the Class object for a given java.sql.Types value. + * + * @param type a value from java.sql.Types + * @return a Class object from which an instance would be returned + */ + static final Class<?> getClassForType(final int type) { + /** + * This switch returns the types as objects according to table B-3 from + * Oracle's JDBC specification 4.1 + */ + switch(type) { + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + return String.class; + case Types.NUMERIC: + case Types.DECIMAL: + return BigDecimal.class; + case Types.BOOLEAN: + return Boolean.class; + case Types.TINYINT: + case Types.SMALLINT: + return Short.class; + case Types.INTEGER: + return Integer.class; + case Types.BIGINT: + return Long.class; + case Types.REAL: + return Float.class; + case Types.FLOAT: + case Types.DOUBLE: + return Double.class; + case Types.BINARY: // MonetDB currently does not support these + case Types.VARBINARY: // see treat_blob_as_binary property + case Types.LONGVARBINARY: + return byte[].class; + case Types.DATE: + return java.sql.Date.class; + case Types.TIME: + case Types.TIME_WITH_TIMEZONE: + return Time.class; + case Types.TIMESTAMP: + case Types.TIMESTAMP_WITH_TIMEZONE: + return Timestamp.class; + case Types.CLOB: + return Clob.class; + case Types.BLOB: + return Blob.class; - /** - * Indicates the nullability of values in the designated column. - * - * This method is currently very expensive as it needs to - * retrieve the information from the database using an SQL - * meta data query. - * - * @param column the first column is 1, the second is 2, ... - * @return the nullability status of the given column; one of - * columnNoNulls, columnNullable or columnNullableUnknown - * @throws SQLException if a database access error occurs - */ - @Override - public int isNullable(final int column) throws SQLException { - checkColumnIndexValidity(column); - try { - if (_is_fetched[column] != true) { - fetchColumnInfo(column); - } - return _isNullable[column]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - - /** - * Gets the designated column's table's catalog name. - * MonetDB does not support the catalog naming concept as in: catalog.schema.table naming scheme - * - * @param column the first column is 1, the second is 2, ... - * @return the name of the catalog for the table in which the given - * column appears or "" if not applicable - */ - @Override - public String getCatalogName(final int column) throws SQLException { - checkColumnIndexValidity(column); - return null; // MonetDB does NOT support catalog qualifiers - - } - - /** - * Indicates whether the designated column is definitely not - * writable. MonetDB does not support cursor updates, so - * nothing is writable. - * - * @param column the first column is 1, the second is 2, ... - * @return true if so; false otherwise - */ - @Override - public boolean isReadOnly(final int column) throws SQLException { - checkColumnIndexValidity(column); - return true; - } - - /** - * Indicates whether it is possible for a write on the - * designated column to succeed. - * - * @param column the first column is 1, the second is 2, ... - * @return true if so; false otherwise - */ - @Override - public boolean isWritable(final int column) throws SQLException { - checkColumnIndexValidity(column); - return false; - } - - /** - * Indicates whether a write on the designated column will - * definitely succeed. - * - * @param column the first column is 1, the second is 2, ... - * @return true if so; false otherwise - */ - @Override - public boolean isDefinitelyWritable(final int column) throws SQLException { - checkColumnIndexValidity(column); - return false; - } - - /** - * Returns the fully-qualified name of the Java class whose - * instances are manufactured if the method - * ResultSet.getObject is called to retrieve a value from - * the column. ResultSet.getObject may return a subclass of - * the class returned by this method. - * - * @param column the first column is 1, the second is 2, ... - * @return the fully-qualified name of the class in the Java - * programming language that would be used by the method - * ResultSet.getObject to retrieve the value in the - * specified column. This is the class name used for custom - * mapping. - * @throws SQLException if there is no such column - */ - @Override - public String getColumnClassName(final int column) throws SQLException { - checkColumnIndexValidity(column); - try { - final String MonetDBType = types[column - 1]; - Class<?> type = null; - if (conn != null) { - final Map<String,Class<?>> map = conn.getTypeMap(); - if (map != null && map.containsKey(MonetDBType)) { - type = (Class)map.get(MonetDBType); - } - } - if (type == null) { - // fallback to the standard SQL type Class mappings - type = getClassForType(JdbcSQLTypes[column - 1]); - } - if (type != null) { - return type.getCanonicalName(); - } - throw new SQLException("column type mapping null: " + MonetDBType, "M0M03"); - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - - /** - * Gets the designated column's suggested title for use in - * printouts and displays. The suggested title is usually - * specified by the SQL AS clause. If a SQL AS is not specified, - * the value returned from getColumnLabel will be the same as - * the value returned by the getColumnName method. - * - * @param column the first column is 1, the second is 2, ... - * @return the suggested column title - * @throws SQLException if there is no such column - */ - @Override - public String getColumnLabel(final int column) throws SQLException { - return getColumnName(column); - } - - /** - * Gets the designated column's name - * - * @param column the first column is 1, the second is 2, ... - * @return the column name - * @throws SQLException if there is no such column - */ - @Override - public String getColumnName(final int column) throws SQLException { - checkColumnIndexValidity(column); - try { - return columns[column - 1]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - - /** - * Retrieves the designated column's SQL type. - * - * @param column the first column is 1, the second is 2, ... - * @return SQL type from java.sql.Types - * @throws SQLException if there is no such column - */ - @Override - public int getColumnType(final int column) throws SQLException { - checkColumnIndexValidity(column); - try { - return JdbcSQLTypes[column - 1]; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - - /** - * Retrieves the designated column's database-specific type name. - * - * @param column the first column is 1, the second is 2, ... - * @return type name used by the database. If the column type is a - * user-defined type, then a fully-qualified type name is - * returned. - * @throws SQLException if there is no such column - */ - @Override - public String getColumnTypeName(final int column) throws SQLException { - checkColumnIndexValidity(column); - try { - final String monettype = types[column - 1]; - if (monettype.endsWith("_interval")) { - /* convert the interval type names to valid SQL data type names, - * such that generic applications can use them in create table statements - */ - if ("day_interval".equals(monettype)) - return "interval day"; - if ("month_interval".equals(monettype)) - return "interval month"; - if ("sec_interval".equals(monettype)) - return "interval second"; - } - return monettype; - } catch (IndexOutOfBoundsException e) { - throw MonetResultSet.newSQLInvalidColumnIndexException(column); - } - } - - }; // end of new rsmdw() - } // end of getMetaData() + // all the rest are currently not implemented and used + default: + return String.class; + } + } /** * Gets the value of the designated column in the current row of this @@ -2437,64 +1728,6 @@ public class MonetResultSet } /** - * Helper method to support the getObject and - * ResultsetMetaData.getColumnClassName JDBC methods. - * - * @param type a value from java.sql.Types - * @return a Class object from which an instance would be returned - */ - final static Class<?> getClassForType(final int type) { - /** - * This switch returns the types as objects according to table B-3 from - * Oracle's JDBC specification 4.1 - */ - // keep this switch regarding the returned classes aligned with getObject(int, Map) ! - switch(type) { - case Types.CHAR: - case Types.VARCHAR: - case Types.LONGVARCHAR: - return String.class; - case Types.NUMERIC: - case Types.DECIMAL: - return BigDecimal.class; - case Types.BOOLEAN: - return Boolean.class; - case Types.TINYINT: - case Types.SMALLINT: - return Short.class; - case Types.INTEGER: - return Integer.class; - case Types.BIGINT: - return Long.class; - case Types.REAL: - return Float.class; - case Types.FLOAT: - case Types.DOUBLE: - return Double.class; - case Types.BINARY: // MonetDB currently does not support these - case Types.VARBINARY: // see treat_blob_as_binary property - case Types.LONGVARBINARY: - return byte[].class; - case Types.DATE: - return java.sql.Date.class; - case Types.TIME: - case Types.TIME_WITH_TIMEZONE: - return Time.class; - case Types.TIMESTAMP: - case Types.TIMESTAMP_WITH_TIMEZONE: - return Timestamp.class; - case Types.CLOB: - return Clob.class; - case Types.BLOB: - return Blob.class; - - // all the rest are currently not implemented and used - default: - return String.class; - } - } - - /** * Gets the value of the designated column in the current row of this * ResultSet object as an Object in the Java programming language. *
new file mode 100644 --- /dev/null +++ b/src/main/java/org/monetdb/jdbc/MonetResultSetMetaData.java @@ -0,0 +1,896 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2023 MonetDB B.V. + */ + +package org.monetdb.jdbc; + +import java.sql.SQLException; +import java.sql.ResultSetMetaData; +import java.sql.Types; +import java.util.Map; + +/** + *<pre> + * A {@link ResultSetMetaData} suitable for the MonetDB database. + * + * An object that can be used to get information about the types and + * properties of the columns in a ResultSet object. + *</pre> + * + * @author Martin van Dinther + * @version 1.0 + */ +public final class MonetResultSetMetaData + extends MonetWrapper + implements ResultSetMetaData +{ + /** The parental Connection object */ + private final MonetConnection conn; + + /** A ResultSetResponse object to retrieve resultset metadata from */ + private final MonetConnection.ResultSetResponse header; + + /** The schema names of the columns in this ResultSet */ + private final String[] schemas; + /** The table names of the columns in this ResultSet */ + private final String[] tables; + /** The names of the columns in this ResultSet */ + private final String[] columns; + /** The MonetDB type names of the columns in this ResultSet */ + private final String[] types; + /** The JDBC SQL type codes of the columns in this ResultSet. + * The content will be derived once from the MonetDB String[] types */ + private final int[] JdbcSQLTypes; + /** The lengths of the columns in this ResultSet */ + private final int[] lengths; + /** The precisions of the columns in this ResultSet */ + private final int[] precisions; + /** The scales of the columns in this ResultSet */ + private final int[] scales; + + /* For the methods: isNullable() and isAutoIncrement(), we need to query the server. + * To do this efficiently we query many columns combined in one SELECT + * query and cache the results in following arrays. + */ + private final int array_size; + /** Whether info for a column is already queried or not */ + private final boolean[] _is_queried; + /** Whether info for a column is already fetched or not */ + private final boolean[] _is_fetched; + /** The nullability of the columns in this ResultSet */ + private final int[] _isNullable; + /** The auto increment property of the columns in this ResultSet */ + private final boolean[] _isAutoincrement; + /** an upper bound value to calculate the range of columns to query */ + private int nextUpperbound; + + + /** + * Main constructor backed by the given connection and header. + * + * @param connection the parent connection + * @param header a ResultSetResponse containing the metadata + * @throws IllegalArgumentException if called with null for one of the arguments + */ + MonetResultSetMetaData( + final MonetConnection connection, + final MonetConnection.ResultSetResponse header) + throws IllegalArgumentException + { + if (connection == null) { + throw new IllegalArgumentException("Connection may not be null!"); + } + if (header == null) { + throw new IllegalArgumentException("Header may not be null!"); + } + this.conn = connection; + this.header = header; + schemas = header.getSchemaNames(); + tables = header.getTableNames(); + columns = header.getNames(); + lengths = header.getColumnLengths(); + types = header.getTypes(); + precisions = header.getColumnPrecisions(); + scales = header.getColumnScales(); + + if (columns.length != tables.length || columns.length != types.length ) { + throw new IllegalArgumentException("Inconsistent Header metadata"); + } + + // derive the JDBC SQL type codes from the types[] names once + JdbcSQLTypes = new int[types.length]; + for (int i = 0; i < types.length; i++) { + int javaSQLtype = MonetDriver.getJdbcSQLType(types[i]); + if (javaSQLtype == Types.CLOB) { + if (connection.mapClobAsVarChar()) + javaSQLtype = Types.VARCHAR; + } else + if (javaSQLtype == Types.BLOB) { + if (connection.mapBlobAsVarBinary()) + javaSQLtype = Types.VARBINARY; + } + JdbcSQLTypes[i] = javaSQLtype; + } + + // initialize structures for storing columns info on nullability and autoincrement + array_size = columns.length + 1; // add 1 as in JDBC columns start from 1 (array from 0). + _is_queried = new boolean[array_size]; + _is_fetched = new boolean[array_size]; + _isNullable = new int[array_size]; + _isAutoincrement = new boolean[array_size]; + nextUpperbound = array_size; + } + + /** + * Returns the number of columns in this ResultSet object. + * + * @return the number of columns + */ + @Override + public int getColumnCount() { + // for debug: System.out.println("In rsmd.getColumnCount() = " + columns.length + ". this rsmd object = " + this.toString()); + return columns.length; + } + + /** + * Indicates whether the designated column is automatically numbered. + * + * This method is currently very expensive for BIGINT, + * INTEGER, SMALLINT and TINYINT result column types + * as it needs to retrieve the information from the + * database using an SQL meta data query. + * + * @param column the first column is 1, the second is 2, ... + * @return true if so; false otherwise + * @throws SQLException if there is no such column or a database access error occurs + */ + @Override + public boolean isAutoIncrement(final int column) throws SQLException { + // only few integer types can be auto incrementable in MonetDB + // see: https://www.monetdb.org/Documentation/SQLReference/DataTypes/SerialDatatypes + switch (getColumnType(column)) { + case Types.BIGINT: + case Types.INTEGER: + case Types.SMALLINT: + case Types.TINYINT: + try { + if (_is_fetched[column] != true) { + fetchColumnInfo(column); + } + return _isAutoincrement[column]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + + return false; + } + + /** + * Indicates whether a column's case matters. + * + * @param column the first column is 1, the second is 2, ... + * @return true for all character string columns else false + * @throws SQLException if there is no such column + */ + @Override + public boolean isCaseSensitive(final int column) throws SQLException { + switch (getColumnType(column)) { + case Types.CHAR: + case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness + case Types.CLOB: + return true; + case Types.VARCHAR: + final String monettype = getColumnTypeName(column); + if (monettype != null && monettype.length() == 4) { + // data of type inet or uuid is not case sensitive + if ("inet".equals(monettype) + || "uuid".equals(monettype)) + return false; + } + return true; + } + + return false; + } + + /** + * Indicates whether the designated column can be used in a + * where clause. + * + * Returning true for all here, even for CLOB, BLOB. + * + * @param column the first column is 1, the second is 2, ... + * @return true + * @throws SQLException if there is no such column + */ + @Override + public boolean isSearchable(final int column) throws SQLException { + checkColumnIndexValidity(column); + return true; + } + + /** + * Indicates whether the designated column is a cash value. + * From the MonetDB database perspective it is by definition + * unknown whether the value is a currency, because there are + * no currency datatypes such as MONEY. With this knowledge + * we can always return false here. + * + * @param column the first column is 1, the second is 2, ... + * @return false + * @throws SQLException if there is no such column + */ + @Override + public boolean isCurrency(final int column) throws SQLException { + checkColumnIndexValidity(column); + return false; + } + + /** + * Indicates the nullability of values in the designated column. + * + * This method is currently very expensive as it needs to + * retrieve the information from the database using an SQL + * meta data query. + * + * @param column the first column is 1, the second is 2, ... + * @return the nullability status of the given column; one of + * columnNoNulls, columnNullable or columnNullableUnknown + * @throws SQLException if there is no such column or a database access error occurs + */ + @Override + public int isNullable(final int column) throws SQLException { + checkColumnIndexValidity(column); + try { + if (_is_fetched[column] != true) { + fetchColumnInfo(column); + } + return _isNullable[column]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + + /** + * Indicates whether values in the designated column are signed + * numbers. + * Within MonetDB all numeric types (except oid and ptr) are signed. + * + * @param column the first column is 1, the second is 2, ... + * @return true if so; false otherwise + * @throws SQLException if there is no such column + */ + @Override + public boolean isSigned(final int column) throws SQLException { + // we can hardcode this, based on the colum type + switch (getColumnType(column)) { + case Types.TINYINT: + case Types.SMALLINT: + case Types.INTEGER: + case Types.REAL: + case Types.FLOAT: + case Types.DOUBLE: + case Types.DECIMAL: + case Types.NUMERIC: + return true; + case Types.BIGINT: + final String monettype = getColumnTypeName(column); + if (monettype != null && monettype.length() == 3) { + // data of type oid or ptr is not signed + if ("oid".equals(monettype) + || "ptr".equals(monettype)) + return false; + } + return true; + // All other types should return false + // case Types.BOOLEAN: + // case Types.DATE: // can year be negative? + // case Types.TIME: // can time be negative? + // case Types.TIME_WITH_TIMEZONE: + // case Types.TIMESTAMP: // can year be negative? + // case Types.TIMESTAMP_WITH_TIMEZONE: + default: + return false; + } + } + + /** + * Indicates the designated column's normal maximum width in + * characters. + * + * @param column the first column is 1, the second is 2, ... + * @return the normal maximum number of characters allowed as the + * width of the designated column + * @throws SQLException if there is no such column + */ + @Override + public int getColumnDisplaySize(final int column) throws SQLException { + checkColumnIndexValidity(column); + if (lengths != null) { + try { + return lengths[column - 1]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + return 1; + } + + /** + * Gets the designated column's suggested title for use in + * printouts and displays. The suggested title is usually + * specified by the SQL AS clause. If a SQL AS is not specified, + * the value returned from getColumnLabel will be the same as + * the value returned by the getColumnName method. + * + * @param column the first column is 1, the second is 2, ... + * @return the suggested column title + * @throws SQLException if there is no such column + */ + @Override + public String getColumnLabel(final int column) throws SQLException { + return getColumnName(column); + } + + /** + * Gets the designated column's name + * + * @param column the first column is 1, the second is 2, ... + * @return the column name + * @throws SQLException if there is no such column + */ + @Override + public String getColumnName(final int column) throws SQLException { + checkColumnIndexValidity(column); + try { + return columns[column - 1]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + + /** + * Gets the designated column's table's catalog name. + * MonetDB does not support the catalog naming concept as in: catalog.schema.table naming scheme + * + * @param column the first column is 1, the second is 2, ... + * @return null or the name of the catalog for the table in which the given + * column appears or "" if not applicable + * @throws SQLException if there is no such column + */ + @Override + public String getCatalogName(final int column) throws SQLException { + checkColumnIndexValidity(column); + return null; // MonetDB does NOT support catalog qualifiers + + } + + /** + * Get the designated column's schema name. + * + * @param column the first column is 1, the second is 2, ... + * @return schema name or "" if not applicable + * @throws SQLException if there is no such column + */ + @Override + public String getSchemaName(final int column) throws SQLException { + checkColumnIndexValidity(column); + if (schemas != null) { + try { + return schemas[column - 1]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + return ""; + } + + /** + * Gets the designated column's table name. + * + * @param column the first column is 1, the second is 2, ... + * @return table name or "" if not applicable + * @throws SQLException if there is no such column + */ + @Override + public String getTableName(final int column) throws SQLException { + checkColumnIndexValidity(column); + if (tables != null) { + try { + return tables[column - 1]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + return ""; + } + + /** + * Retrieves the designated column's SQL type. + * + * @param column the first column is 1, the second is 2, ... + * @return SQL type from java.sql.Types + * @throws SQLException if there is no such column + */ + @Override + public int getColumnType(final int column) throws SQLException { + checkColumnIndexValidity(column); + try { + return JdbcSQLTypes[column - 1]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + + /** + * Retrieves the designated column's database-specific type name. + * + * @param column the first column is 1, the second is 2, ... + * @return type name used by the database. If the column type is a + * user-defined type, then a fully-qualified type name is + * returned. + * @throws SQLException if there is no such column + */ + @Override + public String getColumnTypeName(final int column) throws SQLException { + checkColumnIndexValidity(column); + try { + final String monettype = types[column - 1]; + if (monettype.endsWith("_interval")) { + /* convert the interval type names to valid SQL data type names, + * such that generic applications can use them in create table statements + */ + if ("day_interval".equals(monettype)) + return "interval day"; + if ("month_interval".equals(monettype)) + return "interval month"; + if ("sec_interval".equals(monettype)) + return "interval second"; + } + return monettype; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + + /** + * Get the designated column's specified column size. + * For numeric data, this is the maximum precision. + * For character data, this is the length in characters. + * For datetime datatypes, this is the length in characters + * of the String representation (assuming the maximum + * allowed precision of the fractional seconds component). + * For binary data, this is the length in bytes. + * For the ROWID datatype, this is the length in bytes. + * 0 is returned for data types where the column size is not applicable. + * + * @param column the first column is 1, the second is 2, ... + * @return precision + * @throws SQLException if there is no such column + */ + @Override + public int getPrecision(final int column) throws SQLException { + switch (getColumnType(column)) { + case Types.BIGINT: + return 19; + case Types.INTEGER: + return 10; + case Types.SMALLINT: + return 5; + case Types.TINYINT: + return 3; + case Types.REAL: + return 7; + case Types.FLOAT: + case Types.DOUBLE: + return 15; + case Types.DECIMAL: + case Types.NUMERIC: + // these data types have a variable precision (max precision is 38) + if (precisions != null) { + try { + return precisions[column - 1]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + return 18; + case Types.CHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness + case Types.CLOB: + // these data types have a variable length + if (precisions != null) { + try { + int prec = precisions[column - 1]; + if (prec <= 0) { + // apparently no positive precision or max length could be fetched + // use columnDisplaySize() value as fallback + prec = getColumnDisplaySize(column); + precisions[column - 1] = prec; + } + return prec; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + // apparently no precisions array is available + // use columnDisplaySize() value as alternative + return getColumnDisplaySize(column); + case Types.BINARY: + case Types.VARBINARY: + case Types.BLOB: + // these data types have a variable length + if (precisions != null) { + try { + int prec = precisions[column - 1]; + if (prec <= 0) { + // apparently no positive precision or max length could be fetched + // use columnDisplaySize() value as fallback + // It expect number of bytes, not number of hex chars + prec = (getColumnDisplaySize(column) / 2) +1; + precisions[column - 1] = prec; + } + return prec; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + // apparently no precisions array is available + // use columnDisplaySize() value as alternative + // It expect number of bytes, not number of hex chars + return (getColumnDisplaySize(column) / 2) +1; + case Types.DATE: + return 10; // 2020-10-08 + case Types.TIME: + return 15; // 21:51:34.399753 + case Types.TIME_WITH_TIMEZONE: + return 21; // 21:51:34.399753+02:00 + case Types.TIMESTAMP: + return 26; // 2020-10-08 21:51:34.399753 + case Types.TIMESTAMP_WITH_TIMEZONE: + return 32; // 2020-10-08 21:51:34.399753+02:00 + case Types.BOOLEAN: + return 1; + default: + // All other types should return 0 + return 0; + } + } + + /** + * Gets the designated column's number of digits to right of + * the decimal point. + * 0 is returned for data types where the scale is not applicable. + * + * @param column the first column is 1, the second is 2, ... + * @return scale + * @throws SQLException if there is no such column + */ + @Override + public int getScale(final int column) throws SQLException { + switch (getColumnType(column)) { + case Types.DECIMAL: + case Types.NUMERIC: + { + // these data types may have a variable scale, max scale is 38 + + // Special handling for: day_interval and sec_interval as they are + // mapped to Types.NUMERIC and Types.DECIMAL types (see MonetDriver typeMap) + // They appear to have a fixed scale (tested against Oct2020) + final String monettype = getColumnTypeName(column); + if ("interval day".equals(monettype)) + return 0; + if ("interval second".equals(monettype)) + return 3; + + if (scales != null) { + try { + return scales[column - 1]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + return 0; + } + case Types.TIME: + case Types.TIME_WITH_TIMEZONE: + case Types.TIMESTAMP: + case Types.TIMESTAMP_WITH_TIMEZONE: + if (scales != null) { + try { + return scales[column - 1]; + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + // support microseconds, so scale 6 + return 6; // 21:51:34.399753 + + // All other types should return 0 + // case Types.BIGINT: + // case Types.INTEGER: + // case Types.SMALLINT: + // case Types.TINYINT: + // case Types.REAL: + // case Types.FLOAT: + // case Types.DOUBLE: + // case Types.CHAR: + // case Types.VARCHAR: + // case Types.LONGVARCHAR: // MonetDB doesn't use type LONGVARCHAR, it's here for completeness + // case Types.CLOB: + // case Types.BINARY: + // case Types.VARBINARY: + // case Types.BLOB: + // case Types.DATE: + // case Types.BOOLEAN: + default: + return 0; + } + } + + /** + * Indicates whether the designated column is definitely not + * writable. MonetDB does not support cursor updates, so + * nothing is writable. + * + * @param column the first column is 1, the second is 2, ... + * @return true if so; false otherwise + * @throws SQLException if there is no such column + */ + @Override + public boolean isReadOnly(final int column) throws SQLException { + checkColumnIndexValidity(column); + return true; + } + + /** + * Indicates whether it is possible for a write on the + * designated column to succeed. + * + * @param column the first column is 1, the second is 2, ... + * @return true if so; false otherwise + * @throws SQLException if there is no such column + */ + @Override + public boolean isWritable(final int column) throws SQLException { + checkColumnIndexValidity(column); + return false; + } + + /** + * Indicates whether a write on the designated column will + * definitely succeed. + * + * @param column the first column is 1, the second is 2, ... + * @return true if so; false otherwise + * @throws SQLException if there is no such column + */ + @Override + public boolean isDefinitelyWritable(final int column) throws SQLException { + checkColumnIndexValidity(column); + return false; + } + + /** + * Returns the fully-qualified name of the Java class whose instances + * are manufactured if the method ResultSet.getObject is called to + * retrieve a value from the column. + * ResultSet.getObject may return a subclass of the class returned by + * this method. + * + * @param column the first column is 1, the second is 2, ... + * @return the fully-qualified name of the class in the Java programming + * language that would be used by the method ResultSet.getObject + * to retrieve the value in the specified column. This is the + * class name used for custom mapping. + * @throws SQLException if there is no such column + */ + @Override + public String getColumnClassName(final int column) throws SQLException { + checkColumnIndexValidity(column); + try { + final String MonetDBType = types[column - 1]; + Class<?> type = null; + if (conn != null) { + final Map<String,Class<?>> map = conn.getTypeMap(); + if (map != null && map.containsKey(MonetDBType)) { + type = (Class)map.get(MonetDBType); + } + } + if (type == null) { + // fallback to the standard SQL type Class mappings + type = MonetResultSet.getClassForType(JdbcSQLTypes[column - 1]); + } + if (type != null) { + return type.getCanonicalName(); + } + throw new SQLException("column type mapping null: " + MonetDBType, "M0M03"); + } catch (IndexOutOfBoundsException e) { + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + } + + + /** + * A private utility method to check validity of column index number + * + * @param column the first column is 1, the second is 2, ... + * @throws SQLDataException when invalid column index number + */ + private final void checkColumnIndexValidity(final int column) throws java.sql.SQLDataException { + if (column < 1 || column > columns.length) + throw MonetResultSet.newSQLInvalidColumnIndexException(column); + } + + /** + * A private method to fetch the isNullable and isAutoincrement values + * combined for a specific column. + * The fetched values are stored in the array caches. + * + * @param column the first column is 1, the second is 2, ... + * @throws SQLException if there is no such column + */ + private final void fetchColumnInfo(final int column) throws SQLException { + // for debug: System.out.println("fetchColumnInfo(" + column + ")"); + checkColumnIndexValidity(column); + if (_is_fetched[column] != true) { + // fetch column info for multiple columns combined in one go + fetchManyColumnsInfo(column); + } + if (_is_fetched[column]) + return; + + // apparently no data could be fetched for this resultset column, fall back to defaults + _isNullable[column] = columnNullableUnknown; + _isAutoincrement[column] = false; + } + + /** + * A private method to fetch the isNullable and isAutoincrement values + * for many fully qualified columns combined in one SQL query to reduce the number of queries sent. + * As fetching this meta information from the server per column is costly we combine the querying of + * the isNullable and isAutoincrement values and cache it in internal arrays. + * We also do this for many columns combined in one query to reduce + * the number of queries needed for fetching this metadata for all resultset columns. + * Many generic JDBC database tools (e.g. SQuirreL, DBeaver) request this meta data for each + * column of each resultset, so these optimisations reduces the number of meta data queries significantly. + * + * @param column the first column is 1, the second is 2, ... + * @throws SQLException if a database access error occurs + */ + private final void fetchManyColumnsInfo(final int column) throws SQLException { + // for debug: System.out.println("fetchManyColumnsInfo(" + column + ")"); + + // Most queries have less than 80 resultset columns + // So 80 is a good balance between speedup (up to 79x) and size of generated query sent to server + final int MAX_COLUMNS_PER_QUERY = 80; + + // Determine the optimal startcol to make use of fetching up to 80 columns in one query. + int startcol = column; + if ((startcol > 1) && (startcol + MAX_COLUMNS_PER_QUERY >= nextUpperbound)) { + // we can fetch info from more columns in one query if we start with a lower startcol + startcol = nextUpperbound - MAX_COLUMNS_PER_QUERY; + if (startcol < 1) { + startcol = 1; + } else + if (startcol > column) { + startcol = column; + } + nextUpperbound = startcol; // next time this nextUpperbound value will be used + // for debug: System.out.println("fetchManyColumnsInfo(" + column + ")" + (startcol != column ? " changed into startcol: " + startcol : "") + " nextUpperbound: " + nextUpperbound); + } + + final StringBuilder query = new StringBuilder(410 + (MAX_COLUMNS_PER_QUERY * 150)); + /* next SQL query is a simplified version of query in MonetDatabaseMetaData.getColumns(), to fetch only the needed attributes of a column */ + query.append("SELECT " + + "s.\"name\" AS schnm, " + + "t.\"name\" AS tblnm, " + + "c.\"name\" AS colnm, " + + "cast(CASE c.\"null\" WHEN true THEN ").append(ResultSetMetaData.columnNullable) + .append(" WHEN false THEN ").append(ResultSetMetaData.columnNoNulls) + .append(" ELSE ").append(ResultSetMetaData.columnNullableUnknown) + .append(" END AS int) AS nullable, ").append( + "cast(CASE WHEN c.\"default\" IS NOT NULL AND c.\"default\" LIKE 'next value for %' THEN true ELSE false END AS boolean) AS isautoincrement " + + "FROM \"sys\".\"columns\" c " + + "JOIN \"sys\".\"tables\" t ON c.\"table_id\" = t.\"id\" " + + "JOIN \"sys\".\"schemas\" s ON t.\"schema_id\" = s.\"id\" " + + "WHERE "); + + /* combine the conditions for multiple (up to 80) columns into the WHERE-clause */ + String schName = null; + String tblName = null; + String colName = null; + int queriedcolcount = 0; + for (int col = startcol; col < array_size && queriedcolcount < MAX_COLUMNS_PER_QUERY; col++) { + if (_is_fetched[col] != true) { + if (_is_queried[col] != true) { + _isNullable[col] = columnNullableUnknown; + _isAutoincrement[col] = false; + schName = getSchemaName(col); + if (schName != null && !schName.isEmpty()) { + tblName = getTableName(col); + if (tblName != null && !tblName.isEmpty()) { + colName = getColumnName(col); + if (colName != null && !colName.isEmpty()) { + if (queriedcolcount > 0) + query.append(" OR "); + query.append("(s.\"name\" = ").append(MonetWrapper.sq(schName)); + query.append(" AND t.\"name\" = ").append(MonetWrapper.sq(tblName)); + query.append(" AND c.\"name\" = ").append(MonetWrapper.sq(colName)); + query.append(")"); + _is_queried[col] = true; // flag it + queriedcolcount++; + } + } + } + if (_is_queried[col] != true) { + // make sure we do not try to query it again next time as it is not queryable + _is_fetched[col] = true; + } + } + } + } + + if (queriedcolcount == 0) + return; + + // execute query to get information on queriedcolcount (or less) columns. + final java.sql.Statement stmt = conn.createStatement(); + if (stmt != null) { + // for debug: System.out.println("SQL (len " + query.length() + "): " + query.toString()); + final java.sql.ResultSet rs = stmt.executeQuery(query.toString()); + if (rs != null) { + String rsSchema = null; + String rsTable = null; + String rsColumn = null; + while (rs.next()) { + rsSchema = rs.getString(1); // col 1 is schnm + rsTable = rs.getString(2); // col 2 is tblnm + rsColumn = rs.getString(3); // col 3 is colnm + // find the matching schema.table.column entry in the array + for (int col = 1; col < array_size; col++) { + if (_is_fetched[col] != true && _is_queried[col]) { + colName = getColumnName(col); + if (colName != null && colName.equals(rsColumn)) { + tblName = getTableName(col); + if (tblName != null && tblName.equals(rsTable)) { + schName = getSchemaName(col); + if (schName != null && schName.equals(rsSchema)) { + // found matching entry + // for debug: System.out.println("Found match at [" + col + "] for " + schName + "." + tblName + "." + colName); + _isNullable[col] = rs.getInt(4); // col 4 is nullable (or "NULLABLE") + _isAutoincrement[col] = rs.getBoolean(5); // col 5 is isautoincrement (or "IS_AUTOINCREMENT") + _is_fetched[col] = true; + queriedcolcount--; + // we found the match, exit the for-loop + col = array_size; + } + } + } + } + } + } + rs.close(); + } + stmt.close(); + } + + if (queriedcolcount != 0) { + // not all queried columns have resulted in a returned data row. + // make sure we do not match those columns again next run + for (int col = startcol; col < array_size; col++) { + if (_is_fetched[col] != true && _is_queried[col]) { + _is_fetched[col] = true; + // for debug: System.out.println("Found NO match at [" + col + "] for " + getSchemaName(col) + "." + getTableName(col) + "." + getColumnName(col)); + } + } + } + } +} +
--- a/tests/JDBC_API_Tester.java +++ b/tests/JDBC_API_Tester.java @@ -30,7 +30,7 @@ import org.monetdb.jdbc.types.URL; * This speeds up testing considerably as the overhead of starting a JVM and * loading the java test program class and MonetDB JDBC driver is now reduced * to only one time instead of 40+ times. - * Also all output is no longer send to system out/err but collected in a StringBuilder. + * Also all output is no longer send to system out/err but collected in a global StringBuilder. * The contents of it is compared with the expected output at the end of each test. * Only when it deviates the output is sent to system err, see compareExpectedOutput(). * @@ -89,6 +89,7 @@ final public class JDBC_API_Tester { jt.Test_Rpositioning(); jt.Test_Rsqldata(); jt.Test_Rtimedate(); + jt.Test_RSgetMetaData(); jt.Test_Sbatching(); jt.Test_Smoreresults(); jt.Test_Wrapper(); @@ -3501,6 +3502,204 @@ final public class JDBC_API_Tester { "0. true true\n"); } + private void Test_RSgetMetaData() { + sb.setLength(0); // clear the output log buffer + + Statement stmt = null; + ResultSet rs = null; + try { + con.setAutoCommit(false); + // >> false: auto commit should be off now + sb.append("0. false\t").append(con.getAutoCommit()).append("\n"); + + stmt = con.createStatement(); + stmt.executeUpdate("CREATE TABLE Test_RSmetadata ( myint int, mydouble double, mybool boolean, myvarchar varchar(15), myclob clob )"); + + // all NULLs + stmt.executeUpdate("INSERT INTO Test_RSmetadata VALUES (NULL, NULL, NULL, NULL, NULL)"); + // all filled in + stmt.executeUpdate("INSERT INTO Test_RSmetadata VALUES (2 , 3.0, true, 'A string', 'bla bla bla')"); + + rs = stmt.executeQuery("SELECT * FROM Test_RSmetadata"); + + sb.append("0. ").append(rs.getMetaData().getColumnCount()).append(" columns:\n"); + for (int col = 1; col <= rs.getMetaData().getColumnCount(); col++) { + sb.append("Colnr ").append(col).append(".\n"); + sb.append("\tclassname ").append(rs.getMetaData().getColumnClassName(col)).append("\n"); + sb.append("\tdisplaysize ").append(rs.getMetaData().getColumnDisplaySize(col)).append("\n"); + sb.append("\tlabel ").append(rs.getMetaData().getColumnLabel(col)).append("\n"); + sb.append("\tname ").append(rs.getMetaData().getColumnName(col)).append("\n"); + sb.append("\ttype ").append(rs.getMetaData().getColumnType(col)).append("\n"); + sb.append("\ttypename ").append(rs.getMetaData().getColumnTypeName(col)).append("\n"); + sb.append("\tprecision ").append(rs.getMetaData().getPrecision(col)).append("\n"); + sb.append("\tscale ").append(rs.getMetaData().getScale(col)).append("\n"); + sb.append("\tcatalogname ").append(rs.getMetaData().getCatalogName(col)).append("\n"); + sb.append("\tschemaname ").append(rs.getMetaData().getSchemaName(col)).append("\n"); + sb.append("\ttablename ").append(rs.getMetaData().getTableName(col)).append("\n"); + sb.append("\tautoincrement ").append(rs.getMetaData().isAutoIncrement(col)).append("\n"); + sb.append("\tcasesensitive ").append(rs.getMetaData().isCaseSensitive(col)).append("\n"); + sb.append("\tcurrency ").append(rs.getMetaData().isCurrency(col)).append("\n"); + sb.append("\tdefwritable ").append(rs.getMetaData().isDefinitelyWritable(col)).append("\n"); + sb.append("\tnullable ").append(rs.getMetaData().isNullable(col)).append("\n"); + sb.append("\treadonly ").append(rs.getMetaData().isReadOnly(col)).append("\n"); + sb.append("\tsearchable ").append(rs.getMetaData().isSearchable(col)).append("\n"); + sb.append("\tsigned ").append(rs.getMetaData().isSigned(col)).append("\n"); + sb.append("\twritable ").append(rs.getMetaData().isWritable(col)).append("\n"); + } + + for (int i = 6; rs.next(); i++) { + for (int col = 1; col <= rs.getMetaData().getColumnCount(); col++) { + Object obj = rs.getObject(col); + String type = rs.getMetaData().getColumnClassName(col); + String isInstance = "(null)"; + if (obj != null && type != null) { + try { + Class<?> c = Class.forName(type); + if (c.isInstance(obj)) { + isInstance = (obj.getClass().getName() + " is an instance of " + type); + } else { + isInstance = (obj.getClass().getName() + " is NOT an instance of " + type); + } + } catch (ClassNotFoundException e) { + isInstance = "No such class: " + type; + } + } + sb.append(i).append(".\t").append(isInstance).append("\n"); + } + } + rs.close(); + + con.rollback(); + con.setAutoCommit(true); + // >> true: auto commit was just switched on + sb.append("0. true\t").append(con.getAutoCommit()).append("\n"); + } catch (SQLException e) { + sb.append("FAILED: ").append(e.getMessage()).append("\n"); + } + + closeStmtResSet(stmt, rs); + + compareExpectedOutput("Test_RSgetMetaData", + "0. false false\n" + + "0. 5 columns:\n" + + "Colnr 1.\n" + + " classname java.lang.Integer\n" + + " displaysize 1\n" + + " label myint\n" + + " name myint\n" + + " type 4\n" + + " typename int\n" + + " precision 10\n" + + " scale 0\n" + + " catalogname null\n" + + " schemaname sys\n" + + " tablename test_rsmetadata\n" + + " autoincrement false\n" + + " casesensitive false\n" + + " currency false\n" + + " defwritable false\n" + + " nullable 1\n" + + " readonly true\n" + + " searchable true\n" + + " signed true\n" + + " writable false\n" + + "Colnr 2.\n" + + " classname java.lang.Double\n" + + " displaysize 24\n" + + " label mydouble\n" + + " name mydouble\n" + + " type 8\n" + + " typename double\n" + + " precision 15\n" + + " scale 0\n" + + " catalogname null\n" + + " schemaname sys\n" + + " tablename test_rsmetadata\n" + + " autoincrement false\n" + + " casesensitive false\n" + + " currency false\n" + + " defwritable false\n" + + " nullable 1\n" + + " readonly true\n" + + " searchable true\n" + + " signed true\n" + + " writable false\n" + + "Colnr 3.\n" + + " classname java.lang.Boolean\n" + + " displaysize 5\n" + + " label mybool\n" + + " name mybool\n" + + " type 16\n" + + " typename boolean\n" + + " precision 1\n" + + " scale 0\n" + + " catalogname null\n" + + " schemaname sys\n" + + " tablename test_rsmetadata\n" + + " autoincrement false\n" + + " casesensitive false\n" + + " currency false\n" + + " defwritable false\n" + + " nullable 1\n" + + " readonly true\n" + + " searchable true\n" + + " signed false\n" + + " writable false\n" + + "Colnr 4.\n" + + " classname java.lang.String\n" + + " displaysize 8\n" + + " label myvarchar\n" + + " name myvarchar\n" + + " type 12\n" + + " typename varchar\n" + + " precision 15\n" + + " scale 0\n" + + " catalogname null\n" + + " schemaname sys\n" + + " tablename test_rsmetadata\n" + + " autoincrement false\n" + + " casesensitive true\n" + + " currency false\n" + + " defwritable false\n" + + " nullable 1\n" + + " readonly true\n" + + " searchable true\n" + + " signed false\n" + + " writable false\n" + + "Colnr 5.\n" + + " classname java.lang.String\n" + + " displaysize 11\n" + + " label myclob\n" + + " name myclob\n" + + " type 12\n" + + " typename clob\n" + + " precision 11\n" + + " scale 0\n" + + " catalogname null\n" + + " schemaname sys\n" + + " tablename test_rsmetadata\n" + + " autoincrement false\n" + + " casesensitive true\n" + + " currency false\n" + + " defwritable false\n" + + " nullable 1\n" + + " readonly true\n" + + " searchable true\n" + + " signed false\n" + + " writable false\n" + + "6. (null)\n" + + "6. (null)\n" + + "6. (null)\n" + + "6. (null)\n" + + "6. (null)\n" + + "7. java.lang.Integer is an instance of java.lang.Integer\n" + + "7. java.lang.Double is an instance of java.lang.Double\n" + + "7. java.lang.Boolean is an instance of java.lang.Boolean\n" + + "7. java.lang.String is an instance of java.lang.String\n" + + "7. java.lang.String is an instance of java.lang.String\n" + + "0. true true\n"); + } + private void Test_RfetchManyColumnsInfo() { sb.setLength(0); // clear the output log buffer @@ -4195,7 +4394,7 @@ final public class JDBC_API_Tester { ResultSetMetaData rsmd = rs.getMetaData(); checkIsWrapperFor("ResultSetMetaData", rsmd, jdbc_pkg, "ResultSetMetaData"); checkIsWrapperFor("ResultSetMetaData", rsmd, monetdb_jdbc_pkg, "MonetResultSet"); - checkIsWrapperFor("ResultSetMetaData", rsmd, monetdb_jdbc_pkg, "MonetResultSet$rsmdw"); // it is a private class of MonetResultSet + checkIsWrapperFor("ResultSetMetaData", rsmd, monetdb_jdbc_pkg, "MonetResultSetMetaData"); checkIsWrapperFor("ResultSetMetaData", rsmd, jdbc_pkg, "Statement"); checkIsWrapperFor("ResultSetMetaData", rsmd, monetdb_jdbc_pkg, "MonetStatement"); @@ -4253,7 +4452,7 @@ final public class JDBC_API_Tester { "ResultSet. isWrapperFor(MonetStatement) returns: false\n" + "ResultSetMetaData. isWrapperFor(ResultSetMetaData) returns: true Called unwrap(). Returned object is not null, so oke\n" + "ResultSetMetaData. isWrapperFor(MonetResultSet) returns: false\n" + - "ResultSetMetaData. isWrapperFor(MonetResultSet$rsmdw) returns: true Called unwrap(). Returned object is not null, so oke\n" + + "ResultSetMetaData. isWrapperFor(MonetResultSetMetaData) returns: true Called unwrap(). Returned object is not null, so oke\n" + "ResultSetMetaData. isWrapperFor(Statement) returns: false\n" + "ResultSetMetaData. isWrapperFor(MonetStatement) returns: false\n" + "Statement. isWrapperFor(Statement) returns: true Called unwrap(). Returned object is not null, so oke\n" +