Mercurial > hg > monetdb-java
view src/main/java/org/monetdb/jdbc/MonetResultSetMetaData.java @ 943:ff075ed5ce81
Spell check.
author | Sjoerd Mullender <sjoerd@acm.org> |
---|---|
date | Thu, 09 Jan 2025 10:56:14 +0100 (2 months ago) |
parents | d416e9b6b3d0 |
children |
line wrap: on
line source
/* * SPDX-License-Identifier: MPL-2.0 * * 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 2024, 2025 MonetDB Foundation; * Copyright August 2008 - 2023 MonetDB B.V.; * Copyright 1997 - July 2008 CWI. */ package org.monetdb.jdbc; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Types; /** *<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 */ final class MonetResultSetMetaData extends MonetWrapper implements ResultSetMetaData { /** The parental Connection object */ private final MonetConnection conn; /** The number of columns, it can be zero !! */ private final int colCount; /** 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; schemas = header.getSchemaNames(); tables = header.getTableNames(); columns = header.getNames(); types = header.getTypes(); lengths = header.getColumnLengths(); precisions = header.getColumnPrecisions(); scales = header.getColumnScales(); if (schemas == null) { throw new IllegalArgumentException("Schemas may not be null!"); } if (tables == null) { throw new IllegalArgumentException("Tables may not be null!"); } if (columns == null) { throw new IllegalArgumentException("Columns may not be null!"); } if (types == null) { throw new IllegalArgumentException("MonetDB Types may not be null!"); } if (lengths == null) { throw new IllegalArgumentException("Lengths may not be null!"); } // Note: the precisions and scales arrays are null when the statement is a PLAN, EXPLAIN or TRACE statement !! colCount = columns.length; 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 = colCount + 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; } /** * Alternative constructor backed by the given connection and metadata arrays. * It is used by MonetPreparedStatement. * * @param connection the parent connection * @param colcount the number of result columns, it can be zero !! * @param schemas the schema names * @param tables the table names * @param columns the column names * @param types the MonetDB type names * @param jdbcTypes the JDBC SQL type codes * @param lengths the maximum display length for each column * @param precisions the precision for each column * @param scales the scale for each column * @throws IllegalArgumentException if called with null for one of the arguments */ MonetResultSetMetaData( final MonetConnection connection, final int colcount, final String[] schemas, final String[] tables, final String[] columns, final String[] types, final int[] jdbcTypes, final int[] lengths, final int[] precisions, final int[] scales) throws IllegalArgumentException { if (connection == null) { throw new IllegalArgumentException("Connection may not be null!"); } if (schemas == null) { throw new IllegalArgumentException("Schemas may not be null!"); } if (tables == null) { throw new IllegalArgumentException("Tables may not be null!"); } if (columns == null) { throw new IllegalArgumentException("Columns may not be null!"); } if (types == null) { throw new IllegalArgumentException("MonetDB Types may not be null!"); } if (jdbcTypes == null) { throw new IllegalArgumentException("JDBC Types may not be null!"); } if (lengths == null) { throw new IllegalArgumentException("Lengths may not be null!"); } if (precisions == null) { throw new IllegalArgumentException("Precisions may not be null!"); } if (scales == null) { throw new IllegalArgumentException("Scales may not be null!"); } if (columns.length != tables.length || columns.length != types.length ) { throw new IllegalArgumentException("Inconsistent Header metadata"); } this.conn = connection; this.colCount = colcount; this.schemas = schemas; this.tables = tables; this.columns = columns; this.lengths = lengths; this.types = types; this.JdbcSQLTypes = jdbcTypes; this.precisions = precisions; this.scales = scales; // initialize structures for storing columns info on nullability and autoincrement array_size = colCount + 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() = " + colCount + ". this rsmd object = " + this.toString()); return colCount; } /** * 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 */ case Types.CLOB: return true; case Types.VARCHAR: try { final String monettype = types[column - 1]; // data of type inet or uuid is not case sensitive if ("inet".equals(monettype) || "uuid".equals(monettype)) return false; } catch (IndexOutOfBoundsException e) { throw MonetResultSet.newSQLInvalidColumnIndexException(column); } 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 column 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: case Types.DATE: // year can be negative case Types.TIMESTAMP: // year can be negative case Types.TIMESTAMP_WITH_TIMEZONE: return true; case Types.BIGINT: try { final String monettype = types[column - 1]; // data of type oid or ptr is not signed if ("oid".equals(monettype) || "ptr".equals(monettype)) return false; } catch (IndexOutOfBoundsException e) { throw MonetResultSet.newSQLInvalidColumnIndexException(column); } return true; 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); try { // Special handling for interval types final String monettype = types[column - 1]; if (monettype != null && monettype.endsWith("_interval")) { /* for interval types, precisions[] contains the interval subtype code */ int prec = -1; if (precisions != null) { prec = precisions[column - 1]; } switch (prec) { case 1: return 4; // interval year case 2: return 6; // interval year to month case 3: return 6; // interval month case 4: return 9; // interval day case 5: return 11; // interval day to hour case 6: return 13; // interval day to minute case 7: return 15; // interval day to second case 8: return 11; // interval hour case 9: return 13; // interval hour to minute case 10: return 15; // interval hour to second case 11: return 13; // interval minute case 12: return 15; // interval minute to second case 13: return 15; // interval second default: { // fall back to the 3 available monettype names if ("sec_interval".equals(monettype)) return 15; if ("day_interval".equals(monettype)) return 9; if ("month_interval".equals(monettype)) return 6; } } } final int len = lengths[column - 1]; if (len == 0) { // in case of inet it always has 0 as length. we need to correct it. if ("inet".equals(monettype)) { return 18; // 128.127.126.125/24 } } return len; } 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); } } /** * 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); try { return schemas[column - 1]; } catch (IndexOutOfBoundsException e) { throw MonetResultSet.newSQLInvalidColumnIndexException(column); } } /** * 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); try { return tables[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 != null && 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 */ /* for interval types, precisions[] contains the interval subtype code */ int prec = -1; if (precisions != null) { prec = precisions[column - 1]; } switch (prec) { case 1: return "interval year"; case 2: return "interval year to month"; case 3: return "interval month"; case 4: return "interval day"; case 5: return "interval day to hour"; case 6: return "interval day to minute"; case 7: return "interval day to second"; case 8: return "interval hour"; case 9: return "interval hour to minute"; case 10: return "interval hour to second"; case 11: return "interval minute"; case 12: return "interval minute to second"; case 13: return "interval second"; default: // when no precisions array was available { // fall back to the 3 available monettype names 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 maximum precision or length in characters * @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 { // Special handling for: day_interval and sec_interval as they are // mapped to Types.NUMERIC and Types.DECIMAL types (see MonetDriver typeMap) final String monettype = types[column - 1]; if (monettype != null && monettype.endsWith("_interval")) { /* for interval types, precisions[] contains the interval subtype code */ switch (precisions[column - 1]) { case 1: return 4; // interval year case 2: return 6; // interval year to month case 3: return 6; // interval month case 4: return 9; // interval day case 5: return 11; // interval day to hour case 6: return 13; // interval day to minute case 7: return 15; // interval day to second case 8: return 11; // interval hour case 9: return 13; // interval hour to minute case 10: return 15; // interval hour to second case 11: return 13; // interval minute case 12: return 15; // interval minute to second case 13: return 15; // interval second default: { // fall back to the 3 available monettype names if ("sec_interval".equals(monettype)) return 15; if ("day_interval".equals(monettype)) return 9; if ("month_interval".equals(monettype)) return 6; } } } 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 */ 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.LONGVARBINARY: // MonetDB doesn't use type LONGVARBINARY */ 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 try { // 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 = types[column - 1]; if ("day_interval".equals(monettype)) return 0; if ("sec_interval".equals(monettype)) return 3; if (scales != null) { return scales[column - 1]; } return 0; } catch (IndexOutOfBoundsException e) { throw MonetResultSet.newSQLInvalidColumnIndexException(column); } case Types.TIME: case Types.TIME_WITH_TIMEZONE: case Types.TIMESTAMP: case Types.TIMESTAMP_WITH_TIMEZONE: try { if (scales != null) { return scales[column - 1]; } // support microseconds, so scale 6 return 6; // 21:51:34.399753 } catch (IndexOutOfBoundsException e) { throw MonetResultSet.newSQLInvalidColumnIndexException(column); } // 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 // case Types.CLOB: // case Types.BINARY: // case Types.VARBINARY: // case Types.LONGVARBINARY: // MonetDB doesn't use type LONGVARBINARY // 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 java.util.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 = MonetDriver.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 java.sql.SQLDataException when invalid column index number */ private final void checkColumnIndexValidity(final int column) throws java.sql.SQLDataException { if (column < 1 || column > colCount) 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 + (java.lang.Math.min(MAX_COLUMNS_PER_QUERY, colCount) * 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 " + ResultSetMetaData.columnNullable + " WHEN false THEN " + ResultSetMetaData.columnNoNulls + " ELSE " + ResultSetMetaData.columnNullableUnknown + " END AS int) AS nullable, " + "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)); } } } } }