changeset 719:2f42195e9c58

Improved implementation of PreparedStatement.getParameterMetaData(). The previous implementation created a new ParameterMetaData object each time this method is called which is quite costly if it is called from inside a loop. As the ParameterMetaData is static for a PreparedStatement it is better to create it once, cache it in the PreparedStatement object and return the cached object for next calls to PreparedStatement.getParameterMetaData(). We also now create dedicated 1-based meta data arrays, such that we no longer have to call getParamIdx(param) in methods of ParameterMetaData.
author Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
date Thu, 19 Jan 2023 16:46:44 +0100 (2023-01-19)
parents 7138bbc6952e
children 99baab703566
files src/main/java/org/monetdb/jdbc/MonetParameterMetaData.java src/main/java/org/monetdb/jdbc/MonetPreparedStatement.java tests/JDBC_API_Tester.java
diffstat 3 files changed, 416 insertions(+), 279 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/src/main/java/org/monetdb/jdbc/MonetParameterMetaData.java
@@ -0,0 +1,376 @@
+/*
+ * 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.ParameterMetaData;
+import java.sql.SQLDataException;
+import java.sql.SQLException;
+import java.sql.Types;
+
+/**
+ *<pre>
+ * A {@link ParameterMetaData} suitable for the MonetDB database.
+ *
+ * An object that can be used to get information about the types and properties
+ * for each parameter marker in a PreparedStatement or CallableStatement object.
+ *</pre>
+ *
+ * @author Martin van Dinther
+ * @version 1.0
+ */
+final class MonetParameterMetaData
+	extends MonetWrapper
+	implements ParameterMetaData
+{
+	/** The parental Connection object */
+	private final MonetConnection conn;
+
+	/** The number of parameters, it can be zero !! */
+	private final int paramCount;
+
+	/** The MonetDB type names of the parameters in the PreparedStatement */
+	private final String[] monetdbTypes;
+	/** The JDBC SQL type codes of the parameters in the PreparedStatement */
+	private final int[] JdbcSQLTypes;
+	/** The precisions of the parameters in the PreparedStatement */
+	private final int[] precisions;
+	/** The scales of the parameters in the PreparedStatement */
+	private final int[] scales;
+
+	/**
+	 * Constructor backed by the given connection and metadata arrays.
+	 * It is used by MonetPreparedStatement.
+	 *
+	 * @param connection the parent connection
+	 * @param paramCount the number of parameters, it can be zero !!
+	 * @param types the MonetDB type names
+	 * @param jdbcTypes the JDBC SQL type codes
+	 * @param precisions the precision for each parameter
+	 * @param scales the scale for each parameter
+	 * @throws IllegalArgumentException if called with null for one of the arguments
+	 */
+	MonetParameterMetaData(
+		final MonetConnection connection,
+		final int paramcount,
+		final String[] types,
+		final int[] jdbcTypes,
+		final int[] precisions,
+		final int[] scales)
+		throws IllegalArgumentException
+	{
+		if (connection == null) {
+			throw new IllegalArgumentException("Connection 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 (precisions == null) {
+			throw new IllegalArgumentException("Precisions may not be null!");
+		}
+		if (scales == null) {
+			throw new IllegalArgumentException("Scales may not be null!");
+		}
+		if (types.length != precisions.length || types.length != (paramcount +1)) {
+			throw new IllegalArgumentException("Inconsistent Parameters metadata");
+		}
+		this.conn = connection;
+		this.paramCount = paramcount;
+		this.monetdbTypes = types;
+		this.JdbcSQLTypes = jdbcTypes;
+		this.precisions = precisions;
+		this.scales = scales;
+	}
+
+	/**
+	 * Retrieves the number of parameters in the PreparedStatement object
+	 * for which this ParameterMetaData object contains information.
+	 *
+	 * @return the number of parameters
+	 */
+	@Override
+	public int getParameterCount() {
+		return paramCount;
+	}
+
+	/**
+	 * Retrieves whether null values are allowed in the
+	 * designated parameter.
+	 *
+	 * This is currently always unknown for MonetDB/SQL.
+	 *
+	 * @param param - the first parameter is 1, the second is 2, ...
+	 * @return the nullability status of the given parameter;
+	 *         one of ParameterMetaData.parameterNoNulls,
+	 *         ParameterMetaData.parameterNullable, or
+	 *         ParameterMetaData.parameterNullableUnknown
+	 */
+	@Override
+	public int isNullable(final int param) throws SQLException {
+		checkParameterIndexValidity(param);
+		return ParameterMetaData.parameterNullableUnknown;
+	}
+
+	/**
+	 * Retrieves whether values for the designated parameter can
+	 * be signed numbers.
+	 *
+	 * @param param - the first parameter is 1, the second is 2, ...
+	 * @return true if so; false otherwise
+	 * @throws SQLException if a database access error occurs
+	 */
+	@Override
+	public boolean isSigned(final int param) throws SQLException {
+		// we can hardcode this, based on the parameter type
+		switch (getParameterType(param)) {
+			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 = getParameterTypeName(param);
+				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;
+		}
+	}
+
+	/**
+	 * Retrieves the designated parameter's specified column size.
+	 * The returned value represents the maximum column size for
+	 * the given parameter.
+	 * 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 param - the first parameter is 1, the second is 2, ...
+	 * @return precision
+	 * @throws SQLException if a database access error occurs
+	 */
+	@Override
+	public int getPrecision(final int param) throws SQLException {
+		switch (getParameterType(param)) {
+			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)
+				try {
+					return precisions[param];
+				} catch (IndexOutOfBoundsException e) {
+					throw newSQLInvalidParameterIndexException(param);
+				}
+			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
+				try {
+					return precisions[param];
+				} catch (IndexOutOfBoundsException e) {
+					throw newSQLInvalidParameterIndexException(param);
+				}
+			case Types.BINARY:
+			case Types.VARBINARY:
+			case Types.BLOB:
+				// these data types have a variable length
+				// It expect number of bytes, not number of hex chars
+				try {
+					return precisions[param];
+				} catch (IndexOutOfBoundsException e) {
+					throw newSQLInvalidParameterIndexException(param);
+				}
+			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;
+		}
+	}
+
+	/**
+	 * Retrieves the designated parameter's number of digits to
+	 * right of the decimal point.
+	 * 0 is returned for data types where the scale is not applicable.
+	 *
+	 * @param param - the first parameter is 1, the second is 2, ...
+	 * @return scale
+	 * @throws SQLException if a database access error occurs
+	 */
+	@Override
+	public int getScale(final int param) throws SQLException {
+		checkParameterIndexValidity(param);
+		try {
+			return scales[param];
+		} catch (IndexOutOfBoundsException e) {
+			throw newSQLInvalidParameterIndexException(param);
+		}
+	}
+
+	/**
+	 * Retrieves the designated parameter's SQL type.
+	 *
+	 * @param param - the first parameter is 1, the second is 2, ...
+	 * @return SQL type from java.sql.Types
+	 * @throws SQLException if a database access error occurs
+	 */
+	@Override
+	public int getParameterType(final int param) throws SQLException {
+		checkParameterIndexValidity(param);
+		try {
+			return JdbcSQLTypes[param];
+		} catch (IndexOutOfBoundsException e) {
+			throw newSQLInvalidParameterIndexException(param);
+		}
+	}
+
+	/**
+	 * Retrieves the designated parameter's database-specific type name.
+	 *
+	 * @param param - the first parameter is 1, the second is 2, ...
+	 * @return type the name used by the database.  If the
+	 *         parameter type is a user-defined type, then a
+	 *         fully-qualified type name is returned.
+	 * @throws SQLException if a database access error occurs
+	 */
+	@Override
+	public String getParameterTypeName(final int param) throws SQLException {
+		checkParameterIndexValidity(param);
+		try {
+			final String monettype = monetdbTypes[param];
+			if (monettype.endsWith("_interval")) {
+				/* convert the interval type names to valid SQL data type 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 newSQLInvalidParameterIndexException(param);
+		}
+	}
+
+	/**
+	 * Retrieves the fully-qualified name of the Java class whose instances
+	 * should be passed to the method PreparedStatement.setObject.
+	 *
+	 * @param param - the first parameter 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 PreparedStatement.setObject to set the
+	 *         value in the specified parameter. This is the
+	 *         class name used for custom mapping.
+	 * @throws SQLException if a database access error occurs
+	 */
+	@Override
+	public String getParameterClassName(final int param) throws SQLException {
+		final String MonetDBType = getParameterTypeName(param);
+		final java.util.Map<String,Class<?>> map = conn.getTypeMap();
+		final Class<?> type;
+		if (map != null && map.containsKey(MonetDBType)) {
+			type = (Class)map.get(MonetDBType);
+		} else {
+			type = MonetDriver.getClassForType(getParameterType(param));
+		}
+		if (type != null) {
+			return type.getCanonicalName();
+		}
+		throw new SQLException("parameter type mapping null: " + MonetDBType, "M0M03");
+	}
+
+	/**
+	 * Retrieves the designated parameter's mode.
+	 * For MonetDB/SQL we currently only support INput parameters.
+	 *
+	 * @param param - the first parameter is 1, the second is 2, ...
+	 * @return mode of the parameter; one of
+	 *         ParameterMetaData.parameterModeIn,
+	 *         ParameterMetaData.parameterModeOut,
+	 *         ParameterMetaData.parameterModeInOut or
+	 *         ParameterMetaData.parameterModeUnknown.
+	 */
+	@Override
+	public int getParameterMode(final int param) throws SQLException {
+		checkParameterIndexValidity(param);
+		return ParameterMetaData.parameterModeIn;
+	}
+
+
+	/**
+	 * A private utility method to check validity of parameter index number
+	 *
+	 * @param param - the first parameter is 1, the second is 2, ...
+	 * @throws SQLDataException when invalid parameter index number
+	 */
+	private final void checkParameterIndexValidity(final int param) throws SQLDataException {
+		if (param < 1 || param > paramCount)
+			throw newSQLInvalidParameterIndexException(param);
+	}
+
+	/**
+	 * Small helper method that formats the "Invalid Parameter Index number ..."
+	 * message and creates a new SQLDataException object whose SQLState is set
+	 * to "22010": invalid indicator parameter value.
+	 *
+	 * @param paramIdx the parameter index number
+	 * @return a new created SQLDataException object with SQLState 22010
+	 */
+	private final SQLDataException newSQLInvalidParameterIndexException(final int paramIdx) {
+		return new SQLDataException("Invalid Parameter Index number: " + paramIdx, "22010");
+	}
+}
--- a/src/main/java/org/monetdb/jdbc/MonetPreparedStatement.java
+++ b/src/main/java/org/monetdb/jdbc/MonetPreparedStatement.java
@@ -38,7 +38,6 @@ import java.sql.Timestamp;
 import java.sql.Types;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
-import java.util.Map;
 
 /**
  *<pre>
@@ -82,9 +81,14 @@ public class MonetPreparedStatement
 	private int paramCount = 0;
 	private final String[] paramValues;
 
-	/** A cache to reduce the number of ResultSetMetaData objects created by getMetaData() to maximum 1 per PreparedStatement */
+	/** A cache to reduce the number of ResultSetMetaData objects created
+	 * by getMetaData() to maximum 1 per PreparedStatement */
 	private ResultSetMetaData rsmd;
 
+	/** A cache to reduce the number of ParameterMetaData objects created
+	 * by getParameterMetaData() to maximum 1 per PreparedStatement */
+	private ParameterMetaData pmd;
+
 	/* placeholders for date/time pattern formats created once (only when needed), used multiple times */
 	/** Format of a timestamp with RFC822 time zone */
 	private SimpleDateFormat mTimestampZ;
@@ -357,7 +361,8 @@ public class MonetPreparedStatement
 			// ResultSetMetaData object once and reuse it for all next calls
 			int rescolcount = 0;
 			for (int i = 0; i < size; i++) {
-				/* when column[i] == null it is a parameter, when column[i] != null it is a result column of the prepared query */
+				/* when column[i] == null it is a parameter,
+				   when column[i] != null it is a result column of the prepared query */
 				if (column[i] == null)
 					continue;
 				rescolcount++;
@@ -380,7 +385,8 @@ public class MonetPreparedStatement
 			// now fill the arrays with only the resultset columns metadata
 			rescolcount = 0;
 			for (int i = 0; i < size; i++) {
-				/* when column[i] == null it is a parameter, when column[i] != null it is a result column of the prepared query */
+				/* when column[i] == null it is a parameter,
+				   when column[i] != null it is a result column of the prepared query */
 				if (column[i] == null)
 					continue;
 				schemas[rescolcount] = schema[i];
@@ -440,278 +446,44 @@ public class MonetPreparedStatement
 		return rsmd;
 	}
 
-	/* helper class for the anonymous class in getParameterMetaData */
-	private abstract class pmdw extends MonetWrapper implements ParameterMetaData {}
 	/**
 	 * Retrieves the number, types and properties of this
 	 * PreparedStatement object's parameters.
 	 *
 	 * @return a ParameterMetaData object that contains information
-	 *         about the number, types and properties of this
-	 *         PreparedStatement object's parameters
+	 *         about the number, types and properties for each parameter
+	 *         marker of this PreparedStatement object
 	 * @throws SQLException if a database access error occurs
 	 */
 	@Override
 	public ParameterMetaData getParameterMetaData() throws SQLException {
-		return new pmdw() {
-			/**
-			 * Retrieves the number of parameters in the
-			 * PreparedStatement object for which this ParameterMetaData
-			 * object contains information.
-			 *
-			 * @return the number of parameters
-			 */
-			@Override
-			public int getParameterCount() {
-				return paramCount;
-			}
-
-			/**
-			 * Retrieves whether null values are allowed in the
-			 * designated parameter.
-			 *
-			 * This is currently always unknown for MonetDB/SQL.
-			 *
-			 * @param param the first parameter is 1, the second is 2, ...
-			 * @return the nullability status of the given parameter;
-			 *         one of ParameterMetaData.parameterNoNulls,
-			 *         ParameterMetaData.parameterNullable, or
-			 *         ParameterMetaData.parameterNullableUnknown
-			 */
-			@Override
-			public int isNullable(final int param) {
-				return ParameterMetaData.parameterNullableUnknown;
-			}
-
-			/**
-			 * Retrieves whether values for the designated parameter can
-			 * be signed numbers.
-			 *
-			 * @param param the first parameter is 1, the second is 2, ...
-			 * @return true if so; false otherwise
-			 * @throws SQLException if a database access error occurs
-			 */
-			@Override
-			public boolean isSigned(final int param) throws SQLException {
-				// we can hardcode this, based on the colum type
-				switch (getParameterType(param)) {
-					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 = getParameterTypeName(param);
-						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;
-				}
+		if (pmd == null) {
+			// first use, construct the arrays with metadata and a
+			// ParameterMetaData object once and reuse it for all next calls
+			final int array_size = paramCount +1;
+			// create arrays for storing only the parameters meta data
+			final String[] types = new String[array_size];
+			final int[] jdbcTypes = new int[array_size];
+			final int[] precisions = new int[array_size];
+			final int[] scales = new int[array_size];
+			int param = 1;	// parameters in JDBC start from 1
+			// now fill the arrays with only the parameters metadata
+			for (int i = 0; i < size; i++) {
+				/* when column[i] == null it is a parameter,
+				   when column[i] != null it is a result column of the prepared query */
+				if (column[i] != null)
+					continue;
+				types[param] = monetdbType[i];
+				jdbcTypes[param] = javaType[i];
+				precisions[param] = digits[i];
+				scales[param] = scale[i];
+				param++;
 			}
 
-			/**
-			 * Retrieves the designated parameter's specified column size.
-			 * The returned value represents the maximum column size for
-			 * the given parameter.
-			 * 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 param the first parameter is 1, the second is 2, ...
-			 * @return precision
-			 * @throws SQLException if a database access error occurs
-			 */
-			@Override
-			public int getPrecision(final int param) throws SQLException {
-				switch (getParameterType(param)) {
-					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)
-						try {
-							return digits[getParamIdx(param)];
-						} catch (IndexOutOfBoundsException e) {
-							throw newSQLInvalidParameterIndexException(param);
-						}
-					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
-						try {
-							return digits[getParamIdx(param)];
-						} catch (IndexOutOfBoundsException e) {
-							throw newSQLInvalidParameterIndexException(param);
-						}
-					case Types.BINARY:
-					case Types.VARBINARY:
-					case Types.BLOB:
-						// these data types have a variable length
-						// It expect number of bytes, not number of hex chars
-						try {
-							return digits[getParamIdx(param)];
-						} catch (IndexOutOfBoundsException e) {
-							throw newSQLInvalidParameterIndexException(param);
-						}
-					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;
-				}
-			}
-
-			/**
-			 * Retrieves the designated parameter's number of digits to
-			 * right of the decimal point.
-			 * 0 is returned for data types where the scale is not applicable.
-			 *
-			 * @param param the first parameter is 1, the second is 2, ...
-			 * @return scale
-			 * @throws SQLException if a database access error occurs
-			 */
-			@Override
-			public int getScale(final int param) throws SQLException {
-				try {
-					return scale[getParamIdx(param)];
-				} catch (IndexOutOfBoundsException e) {
-					throw newSQLInvalidParameterIndexException(param);
-				}
-			}
-
-			/**
-			 * Retrieves the designated parameter's SQL type.
-			 *
-			 * @param param the first parameter is 1, the second is 2, ...
-			 * @return SQL type from java.sql.Types
-			 * @throws SQLException if a database access error occurs
-			 */
-			@Override
-			public int getParameterType(final int param) throws SQLException {
-				try {
-					return javaType[getParamIdx(param)];
-				} catch (IndexOutOfBoundsException e) {
-					throw newSQLInvalidParameterIndexException(param);
-				}
-			}
-
-			/**
-			 * Retrieves the designated parameter's database-specific
-			 * type name.
-			 *
-			 * @param param the first parameter is 1, the second is 2, ...
-			 * @return type the name used by the database.  If the
-			 *         parameter type is a user-defined type, then a
-			 *         fully-qualified type name is returned.
-			 * @throws SQLException if a database access error occurs
-			 */
-			@Override
-			public String getParameterTypeName(final int param) throws SQLException {
-				try {
-					final String monettype = monetdbType[getParamIdx(param)];
-					if (monettype.endsWith("_interval")) {
-						/* convert the interval type names to valid SQL data type 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 newSQLInvalidParameterIndexException(param);
-				}
-			}
-
-			/**
-			 * Retrieves the fully-qualified name of the Java class
-			 * whose instances should be passed to the method
-			 * PreparedStatement.setObject.
-			 *
-			 * @param param the first parameter 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 PreparedStatement.setObject to set the
-			 *         value in the specified parameter. This is the
-			 *         class name used for custom mapping.
-			 * @throws SQLException if a database access error occurs
-			 */
-			@Override
-			public String getParameterClassName(final int param) throws SQLException {
-				final String MonetDBType = getParameterTypeName(param);
-				final Map<String,Class<?>> map = getConnection().getTypeMap();
-				final Class<?> c;
-				if (map != null && map.containsKey(MonetDBType)) {
-					c = (Class)map.get(MonetDBType);
-				} else {
-					c = MonetDriver.getClassForType(getParameterType(param));
-				}
-				if (c != null)
-					return c.getCanonicalName();
-				throw new SQLException("column type mapping null: " + MonetDBType, "M0M03");
-			}
-
-			/**
-			 * Retrieves the designated parameter's mode.
-			 * For MonetDB/SQL we currently only support INput parameters.
-			 *
-			 * @param param - the first parameter is 1, the second is 2, ...
-			 * @return mode of the parameter; one of
-			 *         ParameterMetaData.parameterModeIn,
-			 *         ParameterMetaData.parameterModeOut, or
-			 *         ParameterMetaData.parameterModeInOut
-			 *         ParameterMetaData.parameterModeUnknown.
-			 */
-			@Override
-			public int getParameterMode(final int param) {
-				return ParameterMetaData.parameterModeIn;
-			}
-		};
+			pmd = new MonetParameterMetaData((MonetConnection) getConnection(),
+					paramCount, types, jdbcTypes, precisions, scales);
+		}
+		return pmd;
 	}
 
 	/**
@@ -2453,6 +2225,7 @@ public class MonetPreparedStatement
 		}
 		clearParameters();
 		rsmd = null;
+		pmd = null;
 		mTimestampZ = null;
 		mTimestamp = null;
 		mTimeZ = null;
@@ -2569,16 +2342,4 @@ public class MonetPreparedStatement
 		execStmt.append(')');
 		return execStmt.toString();
 	}
-
-	/**
-	 * Small helper method that formats the "Invalid Parameter Index number ..." message
-	 * and creates a new SQLDataException object whose SQLState is set
-	 * to "22010": invalid indicator parameter value.
-	 *
-	 * @param paramIdx the parameter index number
-	 * @return a new created SQLDataException object with SQLState 22010
-	 */
-	private static final SQLDataException newSQLInvalidParameterIndexException(final int paramIdx) {
-		return new SQLDataException("Invalid Parameter Index number: " + paramIdx, "22010");
-	}
 }
--- a/tests/JDBC_API_Tester.java
+++ b/tests/JDBC_API_Tester.java
@@ -4419,7 +4419,7 @@ final public class JDBC_API_Tester {
 			ParameterMetaData pmd = pstmt.getParameterMetaData();
 			checkIsWrapperFor("ParameterMetaData", pmd, jdbc_pkg, "ParameterMetaData");
 			checkIsWrapperFor("ParameterMetaData", pmd, monetdb_jdbc_pkg, "MonetPreparedStatement");
-			checkIsWrapperFor("ParameterMetaData", pmd, monetdb_jdbc_pkg, "MonetPreparedStatement$pmdw");  // it is a private class of MonetPreparedStatement
+			checkIsWrapperFor("ParameterMetaData", pmd, monetdb_jdbc_pkg, "MonetParameterMetaData");
 			checkIsWrapperFor("ParameterMetaData", pmd, jdbc_pkg, "Connection");
 			checkIsWrapperFor("ParameterMetaData", pmd, monetdb_jdbc_pkg, "MonetConnection");
 
@@ -4467,7 +4467,7 @@ final public class JDBC_API_Tester {
 				"PreparedStatement. isWrapperFor(MonetConnection) returns: false\n" +
 				"ParameterMetaData. isWrapperFor(ParameterMetaData) returns: true	Called unwrap(). Returned object is not null, so oke\n" +
 				"ParameterMetaData. isWrapperFor(MonetPreparedStatement) returns: false\n" +
-				"ParameterMetaData. isWrapperFor(MonetPreparedStatement$pmdw) returns: true	Called unwrap(). Returned object is not null, so oke\n" +
+				"ParameterMetaData. isWrapperFor(MonetParameterMetaData) returns: true	Called unwrap(). Returned object is not null, so oke\n" +
 				"ParameterMetaData. isWrapperFor(Connection) returns: false\n" +
 				"ParameterMetaData. isWrapperFor(MonetConnection) returns: false\n" +
 				"PrepStmt ResultSetMetaData. isWrapperFor(ResultSetMetaData) returns: true	Called unwrap(). Returned object is not null, so oke\n" +