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" +