changeset 271:4880267d0fe1

Added implementation of java.sql.CallableStatement interface, test program and updated the ChangeLog and release notes. This implementation resolves request: https://www.monetdb.org/bugzilla/show_bug.cgi?id=6402
author Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
date Thu, 21 Mar 2019 18:57:07 +0100 (2019-03-21)
parents 926afbe567f5
children 928df7febec4
files ChangeLog release.txt src/main/java/nl/cwi/monetdb/jdbc/MonetCallableStatement.java src/main/java/nl/cwi/monetdb/jdbc/MonetConnection.java tests/Test_CallableStmt.java tests/build.xml
diffstat 6 files changed, 861 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,11 @@
 # ChangeLog file for monetdb-java
 # This file is updated with Maddlog
 
+* Thu Mar 21 2019 Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
+- Added implementation of java.sql.CallableStatement interface. Some standard
+  Java applications require this JDBC interface for executing SQL stored procedures.
+  This implementation resolves request: https://www.monetdb.org/bugzilla/show_bug.cgi?id=6402
+
 * Thu Mar  7 2019 Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
 - Improved MonetDatabaseMetaData methods:
   - getNumericFunctions(): it now includes functions: degrees, fuse, ms_round, ms_str, ms_trunc and radians.
--- a/release.txt
+++ b/release.txt
@@ -55,11 +55,9 @@ Currently implemented JDBC 4.1 interface
   * java.sql.Driver
 
   * java.sql.Connection
-    The next features/methods are NOT implemented:
+    The next features/methods are NOT useable/supported:
     - createArrayOf, createNClob, createStruct, createSQLXML
     - createStatement with result set holdability
-    - prepareCall (CallableStatement is not supported)
-       see also: https://www.monetdb.org/bugzilla/show_bug.cgi?id=6402
     - prepareStatement with array of column indices or column names
     - setHoldability (close/hold cursors over commit is not configurable)
 
@@ -70,7 +68,7 @@ Currently implemented JDBC 4.1 interface
   * java.sql.DatabaseMetaData
 
   * java.sql.Statement
-    The next features/methods are NOT implemented:
+    The next features/methods are NOT useable/supported:
     - cancel (query execution cannot be terminated, once started)
        see also: https://www.monetdb.org/bugzilla/show_bug.cgi?id=6222
     - execute with column indices or names
@@ -80,7 +78,7 @@ Currently implemented JDBC 4.1 interface
     - setEscapeProcessing on
 
   * java.sql.PreparedStatement
-    The next features/methods are NOT implemented:
+    The next features/methods are NOT useable/supported:
     - setArray
     - setAsciiStream, setBinaryStream, setUnicodeStream
     - setBlob
@@ -89,8 +87,17 @@ Currently implemented JDBC 4.1 interface
 
   * java.sql.ParameterMetaData
 
+  * java.sql.CallableStatement
+    The next methods are NOT useable/supported:
+    - all getXyz(parameterIndex/parameterName, ...) methods because
+      output parameters in stored procedures are not supported by MonetDB
+    - all registerOutParameter(parameterIndex/parameterName, int sqlType, ...) methods
+      because output parameters in stored procedures are not supported by MonetDB
+    - wasNull() method because output parameters in stored procedures are
+      not supported by MonetDB
+
   * java.sql.ResultSet
-    The next features/methods are NOT implemented:
+    The next features/methods are NOT useable/supported:
     - getArray
     - getAsciiStream, getUnicodeStream
     - getNClob
@@ -105,12 +112,12 @@ Currently implemented JDBC 4.1 interface
 
   * java.sql.Blob
     A simple implementation using a byte[] to store the whole BLOB
-    The next features/methods are NOT implemented:
+    The next features/methods are NOT useable/supported:
     - setBinaryStream
 
   * java.sql.Clob
     A simple implementation using a StringBuilder to store the whole CLOB
-    The next features/methods are NOT implemented:
+    The next features/methods are NOT useable/supported:
     - getAsciiStream
     - setAsciiStream
     - setCharacterStream
@@ -124,8 +131,6 @@ Currently implemented JDBC 4.1 interface
 
 The next java.sql.* interfaces are NOT implemented:
   * java.sql.Array
-  * java.sql.CallableStatement  (use Statement or PreparedStatement instead)
-     see also: https://www.monetdb.org/bugzilla/show_bug.cgi?id=6402
   * java.sql.NClob
   * java.sql.Ref
   * java.sql.Rowid
new file mode 100644
--- /dev/null
+++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetCallableStatement.java
@@ -0,0 +1,635 @@
+/*
+ * 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 - 2019 MonetDB B.V.
+ */
+
+package nl.cwi.monetdb.jdbc;
+
+import java.io.InputStream;
+import java.io.Reader;
+import java.math.BigDecimal;
+import java.net.URL;
+import java.sql.Array;
+import java.sql.Blob;
+import java.sql.CallableStatement;
+import java.sql.Clob;
+import java.sql.Date;
+import java.sql.NClob;
+import java.sql.PreparedStatement;
+import java.sql.Ref;
+import java.sql.RowId;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.sql.SQLXML;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Calendar;
+import java.util.Map;
+
+/**
+ * A {@link CallableStatement} suitable for the MonetDB database.
+ *
+ * The interface used to execute SQL stored procedures.
+ * The JDBC API provides a stored procedure SQL escape syntax that allows stored procedures to be called in a standard way for all RDBMSs.
+ * This escape syntax has one form that includes a result parameter (MonetDB does not support this) and one that does not.
+ * If used, the result parameter must be registered as an OUT parameter (MonetDB does not support this).
+ * The other parameters can be used for input, output or both. Parameters are referred to sequentially, by number, with the first parameter being 1.
+ *
+ * <code>
+ *  { call procedure-name [ (arg1, arg2, ...) ] }
+ *  { ?= call procedure-name [ (arg1, arg2, ...) ] }
+ * </code>
+ *
+ * IN parameter values are set using the set methods inherited from PreparedStatement.
+ * The type of all OUT parameters must be registered prior to executing the stored procedure;
+ * their values are retrieved after execution via the get methods provided here.
+ * Note: MonetDB does not support OUT or INOUT parameters. Only input parameters are supported.
+ *
+ * A CallableStatement can return one ResultSet object or multiple ResultSet objects.
+ * Multiple ResultSet objects are handled using operations inherited from Statement.
+ *
+ * For maximum portability, a call's ResultSet objects and update counts should be processed prior to getting the values of output parameters.
+ *
+ * This implementation of the CallableStatement interface reuses the implementation of MonetPreparedStatement for
+ * preparing the call statement, bind parameter values and execute the call, possibly multiple times with different parameter values.
+ *
+ * Note: currently we can not implement:
+ * - all getXyz(parameterIndex/parameterName, ...) methods
+ * - all registerOutParameter(parameterIndex/parameterName, int sqlType, ...) methods
+ * - wasNull() method
+ * because output parameters in stored procedures are not supported by MonetDB.
+ *
+ * @author Martin van Dinther
+ * @version 1.0
+ */
+
+public class MonetCallableStatement
+	extends MonetPreparedStatement
+	implements CallableStatement
+{
+	/**
+	 * MonetCallableStatement constructor which checks the arguments for validity.
+	 * A MonetCallableStatement is backed by a {@link MonetPreparedStatement},
+	 * which deals with most of the required stuff of this class.
+	 *
+	 * @param connection the connection that created this Statement
+	 * @param resultSetType type of {@link ResultSet} to produce
+	 * @param resultSetConcurrency concurrency of ResultSet to produce
+	 * @param callQuery - an SQL CALL statement that may contain one or more '?' parameter placeholders.
+	 *	Typically this statement is specified using JDBC call escape syntax:
+	 *	{ call procedure_name [(?,?, ...)] }
+	 *	or
+	 *	{ ?= call procedure_name [(?,?, ...)] }
+	 * @throws SQLException if an error occurs during creation
+	 * @throws IllegalArgumentException is one of the arguments is null or empty
+	 */
+	MonetCallableStatement(
+			MonetConnection connection,
+			int resultSetType,
+			int resultSetConcurrency,
+			int resultSetHoldability,
+			String callQuery)
+		throws SQLException, IllegalArgumentException
+	{
+		super(
+			connection,
+			resultSetType,
+			resultSetConcurrency,
+			resultSetHoldability,
+			removeEscapes(callQuery)
+		);
+	}
+
+	/** parse call query string on
+	 *  { [?=] call <procedure-name> [(<arg1>,<arg2>, ...)] }
+	 * and remove the JDBC escapes pairs: { and }
+	 */
+	private static String removeEscapes(String query) {
+		if (query == null)
+			return null;
+
+		int firstAccOpen = query.indexOf("{");
+		if (firstAccOpen == -1)
+			// nothing to remove
+			return query;
+
+		int len = query.length();
+		StringBuilder buf = new StringBuilder(len);
+		int countAccolades = 0;
+		// simple scanner which copies all characters except the first '{' and next '}' character
+		// we currently do not check if 'call' appears after the first '{' and before the '}' character
+		// we currently also do not deal correctly with { or } appearing as comment or as part of a string value
+		for (int i = 0; i < len; i++) {
+			char c = query.charAt(i);
+			switch (c) {
+			case '{':
+				countAccolades++;
+				if (i == firstAccOpen)
+					continue;
+				else
+					buf.append(c);
+				break;
+			case '}':
+				countAccolades--;
+				if (i > firstAccOpen && countAccolades == 0)
+					continue;
+				else
+					buf.append(c);
+				break;
+			default:
+				buf.append(c);
+			}
+		}
+		return buf.toString();
+	}
+
+	/** utility method to convert a parameter name to an int (which represents the parameter index)
+	 *  this will only succeed for strings like: "1", "2", "3", etc
+	 *  throws SQLException if it cannot convert the string to an integer number
+	 */
+	private int nameToIndex(String parameterName) throws SQLException {
+		if (parameterName == null)
+			throw new SQLException("Missing parameterName value", "22010");
+		try {
+			return Integer.parseInt(parameterName);
+		} catch (NumberFormatException nfe) {
+			throw new SQLException("Cannot convert parameterName '" + parameterName + "'to integer value", "22010");
+		}
+	}
+
+
+	// methods of interface CallableStatement
+
+	// all getXyz(parameterIndex/parameterName, ...) methods are NOT supported
+	// because output parameters in stored procedures are not supported by MonetDB
+	@Override
+	public Array getArray(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getArray");
+	}
+	@Override
+	public Array getArray(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getArray");
+	}
+	@Override
+	public BigDecimal getBigDecimal(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getBigDecimal");
+	}
+	@Override
+	@Deprecated
+	public BigDecimal getBigDecimal(int parameterIndex, int scale) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getBigDecimal");
+	}
+	@Override
+	public BigDecimal getBigDecimal(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getBigDecimal");
+	}
+	@Override
+	public Blob getBlob(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getBlob");
+	}
+	@Override
+	public Blob getBlob(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getBlob");
+	}
+	@Override
+	public boolean getBoolean(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getBoolean");
+	}
+	@Override
+	public boolean getBoolean(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getBoolean");
+	}
+	@Override
+	public byte getByte(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getByte");
+	}
+	@Override
+	public byte getByte(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getByte");
+	}
+	@Override
+	public byte[] getBytes(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getBytes");
+	}
+	@Override
+	public byte[] getBytes(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getBytes");
+	}
+	@Override
+	public Reader getCharacterStream(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getCharacterStream");
+	}
+	@Override
+	public Reader getCharacterStream(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getCharacterStream");
+	}
+	@Override
+	public Clob getClob(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getClob");
+	}
+	@Override
+	public Clob getClob(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getClob");
+	}
+	@Override
+	public Date getDate(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getDate");
+	}
+	@Override
+	public Date getDate(int parameterIndex, Calendar cal) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getDate");
+	}
+	@Override
+	public Date getDate(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getDate");
+	}
+	@Override
+	public Date getDate(String parameterName, Calendar cal) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getDate");
+	}
+	@Override
+	public double getDouble(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getDouble");
+	}
+	@Override
+	public double getDouble(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getDouble");
+	}
+	@Override
+	public float getFloat(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getFloat");
+	}
+	@Override
+	public float getFloat(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getFloat");
+	}
+	@Override
+	public int getInt(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getInt");
+	}
+	@Override
+	public int getInt(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getInt");
+	}
+	@Override
+	public long getLong(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getLong");
+	}
+	@Override
+	public long getLong(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getLong");
+	}
+	@Override
+	public Reader getNCharacterStream(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getNCharacterStream");
+	}
+	@Override
+	public Reader getNCharacterStream(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getNCharacterStream");
+	}
+	@Override
+	public NClob getNClob(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getNClob");
+	}
+	@Override
+	public NClob getNClob(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getNClob");
+	}
+	@Override
+	public String getNString(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getNString");
+	}
+	@Override
+	public String getNString(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getNString");
+	}
+	@Override
+	public Object getObject(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getObject");
+	}
+	@Override
+	public <T> T getObject(int parameterIndex, Class<T> type) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getObject");
+	}
+	@Override
+	public Object getObject(int parameterIndex, Map<String,Class<?>> map) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getObject");
+	}
+	@Override
+	public Object getObject(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getObject");
+	}
+	@Override
+	public <T> T getObject(String parameterName, Class<T> type) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getObject");
+	}
+	@Override
+	public Object getObject(String parameterName, Map<String,Class<?>> map) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getObject");
+	}
+	@Override
+	public Ref getRef(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getRef");
+	}
+	@Override
+	public Ref getRef(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getRef");
+	}
+	@Override
+	public RowId getRowId(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getRowId");
+	}
+	@Override
+	public RowId getRowId(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getRowId");
+	}
+	@Override
+	public short getShort(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getShort");
+	}
+	@Override
+	public short getShort(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getShort");
+	}
+	@Override
+	public SQLXML getSQLXML(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getSQLXML");
+	}
+	@Override
+	public SQLXML getSQLXML(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getSQLXML");
+	}
+	@Override
+	public String getString(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getString");
+	}
+	@Override
+	public String getString(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getString");
+	}
+	@Override
+	public Time getTime(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getTime");
+	}
+	@Override
+	public Time getTime(int parameterIndex, Calendar cal) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getTime");
+	}
+	@Override
+	public Time getTime(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getTime");
+	}
+	@Override
+	public Time getTime(String parameterName, Calendar cal) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getTime");
+	}
+	@Override
+	public Timestamp getTimestamp(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getTimestamp");
+	}
+	@Override
+	public Timestamp getTimestamp(int parameterIndex, Calendar cal) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getTimestamp");
+	}
+	@Override
+	public Timestamp getTimestamp(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getTimestamp");
+	}
+	@Override
+	public Timestamp getTimestamp(String parameterName, Calendar cal) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getTimestamp");
+	}
+	@Override
+	public URL getURL(int parameterIndex) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getURL");
+	}
+	@Override
+	public URL getURL(String parameterName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("getURL");
+	}
+
+
+	// all registerOutParameter(parameterIndex/parameterName, int sqlType, ...) methods are NOT supported
+	// because output parameters in stored procedures are not supported by MonetDB
+	@Override
+	public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException {
+		throw newSQLFeatureNotSupportedException("registerOutParameter");
+	}
+	@Override
+	public void registerOutParameter(int parameterIndex, int sqlType, int scale) throws SQLException {
+		throw newSQLFeatureNotSupportedException("registerOutParameter");
+	}
+	@Override
+	public void registerOutParameter(int parameterIndex, int sqlType, String typeName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("registerOutParameter");
+	}
+	@Override
+	public void registerOutParameter(String parameterName, int sqlType) throws SQLException {
+		throw newSQLFeatureNotSupportedException("registerOutParameter");
+	}
+	@Override
+	public void registerOutParameter(String parameterName, int sqlType, int scale) throws SQLException {
+		throw newSQLFeatureNotSupportedException("registerOutParameter");
+	}
+	@Override
+	public void registerOutParameter(String parameterName, int sqlType, String typeName) throws SQLException {
+		throw newSQLFeatureNotSupportedException("registerOutParameter");
+	}
+
+
+	// all setXyz(parameterName, ...) methods are mapped to setXyz(nameToIndex(parameterName), ...) methods
+	// this only works for parameter names "1", "2", "3", etc.
+	@Override
+	public void setAsciiStream(String parameterName, InputStream x) throws SQLException {
+		setAsciiStream(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setAsciiStream(String parameterName, InputStream x, int length) throws SQLException {
+		setAsciiStream(nameToIndex(parameterName), x, length);
+	}
+	@Override
+	public void setAsciiStream(String parameterName, InputStream x, long length) throws SQLException {
+		setAsciiStream(nameToIndex(parameterName), x, length);
+	}
+	@Override
+	public void setBigDecimal(String parameterName, BigDecimal x) throws SQLException {
+		setBigDecimal(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setBinaryStream(String parameterName, InputStream x) throws SQLException {
+		setBinaryStream(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setBinaryStream(String parameterName, InputStream x, int length) throws SQLException {
+		setBinaryStream(nameToIndex(parameterName), x, length);
+	}
+	@Override
+	public void setBinaryStream(String parameterName, InputStream x, long length) throws SQLException {
+		setBinaryStream(nameToIndex(parameterName), x, length);
+	}
+	@Override
+	public void setBlob(String parameterName, Blob x) throws SQLException {
+		setBlob(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setBlob(String parameterName, InputStream inputStream) throws SQLException {
+		setBlob(nameToIndex(parameterName), inputStream);
+	}
+	@Override
+	public void setBlob(String parameterName, InputStream inputStream, long length) throws SQLException {
+		setBlob(nameToIndex(parameterName), inputStream, length);
+	}
+	@Override
+	public void setBoolean(String parameterName, boolean x) throws SQLException {
+		setBoolean(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setByte(String parameterName, byte x) throws SQLException {
+		setByte(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setBytes(String parameterName, byte[] x) throws SQLException {
+		setBytes(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setCharacterStream(String parameterName, Reader reader) throws SQLException {
+		setCharacterStream(nameToIndex(parameterName), reader);
+	}
+	@Override
+	public void setCharacterStream(String parameterName, Reader reader, int length) throws SQLException {
+		setCharacterStream(nameToIndex(parameterName), reader, length);
+	}
+	@Override
+	public void setCharacterStream(String parameterName, Reader reader, long length) throws SQLException {
+		setCharacterStream(nameToIndex(parameterName), reader, length);
+	}
+	@Override
+	public void setClob(String parameterName, Clob x) throws SQLException {
+		setClob(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setClob(String parameterName, Reader reader) throws SQLException {
+		setClob(nameToIndex(parameterName), reader);
+	}
+	@Override
+	public void setClob(String parameterName, Reader reader, long length) throws SQLException {
+		setClob(nameToIndex(parameterName), reader, length);
+	}
+	@Override
+	public void setDate(String parameterName, Date x) throws SQLException {
+		setDate(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setDate(String parameterName, Date x, Calendar cal) throws SQLException {
+		setDate(nameToIndex(parameterName), x, cal);
+	}
+	@Override
+	public void setDouble(String parameterName, double x) throws SQLException {
+		setDouble(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setFloat(String parameterName, float x) throws SQLException {
+		setFloat(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setInt(String parameterName, int x) throws SQLException {
+		setInt(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setLong(String parameterName, long x) throws SQLException {
+		setLong(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setNCharacterStream(String parameterName, Reader value) throws SQLException {
+		setNCharacterStream(nameToIndex(parameterName), value);
+	}
+	@Override
+	public void setNCharacterStream(String parameterName, Reader value, long length) throws SQLException {
+		setNCharacterStream(nameToIndex(parameterName), value, length);
+	}
+	@Override
+	public void setNClob(String parameterName, NClob value) throws SQLException {
+		setNClob(nameToIndex(parameterName), value);
+	}
+	@Override
+	public void setNClob(String parameterName, Reader reader) throws SQLException {
+		setNClob(nameToIndex(parameterName), reader);
+	}
+	@Override
+	public void setNClob(String parameterName, Reader reader, long length) throws SQLException {
+		setNClob(nameToIndex(parameterName), reader, length);
+	}
+	@Override
+	public void setNString(String parameterName, String value) throws SQLException {
+		setNString(nameToIndex(parameterName), value);
+	}
+	@Override
+	public void setNull(String parameterName, int sqlType) throws SQLException {
+		setNull(nameToIndex(parameterName), sqlType);
+	}
+	@Override
+	public void setNull(String parameterName, int sqlType, String typeName) throws SQLException {
+		setNull(nameToIndex(parameterName), sqlType, typeName);
+	}
+	@Override
+	public void setObject(String parameterName, Object x) throws SQLException {
+		setObject(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setObject(String parameterName, Object x, int targetSqlType) throws SQLException {
+		setObject(nameToIndex(parameterName), x, targetSqlType);
+	}
+	@Override
+	public void setObject(String parameterName, Object x, int targetSqlType, int scale) throws SQLException {
+		setObject(nameToIndex(parameterName), x, targetSqlType, scale);
+	}
+	@Override
+	public void setRowId(String parameterName, RowId x) throws SQLException {
+		setRowId(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setShort(String parameterName, short x) throws SQLException {
+		setShort(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setSQLXML(String parameterName, SQLXML xmlObject) throws SQLException {
+		setSQLXML(nameToIndex(parameterName), xmlObject);
+	}
+	@Override
+	public void setString(String parameterName, String x) throws SQLException {
+		setString(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setTime(String parameterName, Time x) throws SQLException {
+		setTime(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setTime(String parameterName, Time x, Calendar cal) throws SQLException {
+		setTime(nameToIndex(parameterName), x, cal);
+	}
+	@Override
+	public void setTimestamp(String parameterName, Timestamp x) throws SQLException {
+		setTimestamp(nameToIndex(parameterName), x);
+	}
+	@Override
+	public void setTimestamp(String parameterName, Timestamp x, Calendar cal) throws SQLException {
+		setTimestamp(nameToIndex(parameterName), x, cal);
+	}
+	@Override
+	public void setURL(String parameterName, URL val) throws SQLException {
+		setURL(nameToIndex(parameterName), val);
+	}
+
+	/* Retrieves whether the last OUT parameter read had the value of SQL NULL. */
+	@Override
+	public boolean wasNull() throws SQLException {
+		// wasNull() method is NOT supported
+		// because output parameters in stored procedures are not supported by MonetDB
+		throw newSQLFeatureNotSupportedException("wasNull");
+	}
+
+	// end methods interface CallableStatement
+}
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetConnection.java
+++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetConnection.java
@@ -156,7 +156,7 @@ public class MonetConnection
 	private boolean queriedCommentsTable = false;
 	private boolean hasCommentsTable = false;
 
-	/** The last set query timeout on the server as used by Statement and PreparedStatement (and CallableStatement in future) */
+	/** The last set query timeout on the server as used by Statement, PreparedStatement and CallableStatement */
 	protected int lastSetQueryTimeout = 0;	// 0 means no timeout, which is the default on the server
 
 
@@ -502,8 +502,7 @@ public class MonetConnection
 		} catch (IllegalArgumentException e) {
 			throw new SQLException(e.toString(), "M0M03");
 		}
-		// we don't have to catch SQLException because that is declared to
-		// be thrown
+		// we don't have to catch SQLException because that is declared to be thrown
 	}
 
 	/**
@@ -726,8 +725,21 @@ public class MonetConnection
 	public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability)
 		throws SQLException
 	{
-		throw newSQLFeatureNotSupportedException("prepareCall");
-		/* a request to implement prepareCall() has already been logged, see https://www.monetdb.org/bugzilla/show_bug.cgi?id=6402 */
+		try {
+			CallableStatement ret = new MonetCallableStatement(
+				this,
+				resultSetType,
+				resultSetConcurrency,
+				resultSetHoldability,
+				sql
+			);
+			// store it in the map for when we close...
+			statements.put(ret, null);
+			return ret;
+		} catch (IllegalArgumentException e) {
+			throw new SQLException(e.toString(), "M0M03");
+		}
+		// we don't have to catch SQLException because that is declared to be thrown
 	}
 
 	/**
new file mode 100644
--- /dev/null
+++ b/tests/Test_CallableStmt.java
@@ -0,0 +1,182 @@
+/*
+ * 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 - 2019 MonetDB B.V.
+ */
+
+import java.sql.*;
+
+public class Test_CallableStmt {
+	public static void main(String[] args) throws Exception {
+		Connection con = DriverManager.getConnection(args[0]);
+		Statement stmt = null;
+		CallableStatement cstmt = null;
+		try {
+			String tbl_nm = "tbl6402";
+			String proc_nm = "proc6402";
+
+			stmt = con.createStatement();
+
+			// create a test table.
+			stmt.executeUpdate("CREATE TABLE IF NOT EXISTS " + tbl_nm + " (tint int, tdouble double, tbool boolean, tvarchar varchar(15), tclob clob, turl url, tclen int);");
+			System.out.println("Created table: " + tbl_nm);
+
+			// create a procedure with multiple different IN parameters which inserts a row into a table of which one column is computed.
+			stmt.executeUpdate("CREATE PROCEDURE " + proc_nm + " (myint int, mydouble double, mybool boolean, myvarchar varchar(15), myclob clob, myurl url) BEGIN" +
+				" INSERT INTO " + tbl_nm + " (tint, tdouble, tbool, tvarchar, tclob, turl, tclen) VALUES (myint, mydouble, mybool, myvarchar, myclob, myurl, LENGTH(myvarchar) + LENGTH(myclob)); " +
+				"END;");
+			System.out.println("Created procedure: " + proc_nm);
+
+			// make sure we can call the procedure the old way (as string)
+			stmt.executeUpdate("call " + proc_nm + "(1, 1.1, true,'one','ONE', 'www.monetdb.org');");
+			System.out.println("Called procedure (1): " + proc_nm);
+			showContents(con, tbl_nm);
+
+
+			// now use a CallableStament object
+			cstmt = con.prepareCall(" { call " + proc_nm + " (?,?, ?, ? , ?,?) } ;");
+			System.out.println("Prepared Callable procedure: " + proc_nm);
+
+			// specify first set of params
+			cstmt.setInt(1, 2);
+			cstmt.setDouble(2, 2.02);
+			cstmt.setBoolean(3, true);
+			cstmt.setString(4, "Two");
+			Clob myclob = con.createClob();
+			myclob.setString(1, "TWOs");
+			cstmt.setClob(5, myclob);
+			cstmt.setString(6, "http://www.monetdb.org/");
+			cstmt.execute();
+			System.out.println("Called Prepared procedure (1): " + proc_nm);
+			showParams(cstmt);
+			showContents(con, tbl_nm);
+
+			myclob.setString(1, "TREEs");
+			// specify second set of params (some (1 and 3 and 5) are left the same)
+			cstmt.setDouble(2, 3.02);
+			cstmt.setString(4, "Tree");
+			cstmt.setURL(6, new java.net.URL("https://www.monetdb.org/"));
+			cstmt.execute();
+			System.out.println("Called Prepared procedure (2): " + proc_nm);
+			// showParams(cstmt);
+			showContents(con, tbl_nm);
+
+			// specify third set of params (some (1 and 2) are left the same)
+			cstmt.setInt(1, 4);
+			cstmt.setBoolean(3, false);
+			cstmt.setString(4, "Four");
+			cstmt.executeUpdate();
+			System.out.println("Called Prepared procedure (3): " + proc_nm);
+			showContents(con, tbl_nm);
+
+			// test setNull() also
+			cstmt.setNull(3, Types.BOOLEAN);
+			cstmt.setNull(5, Types.CLOB);
+			cstmt.setNull(2, Types.DOUBLE);
+			cstmt.setNull(4, Types.VARCHAR);
+			cstmt.setNull(1, Types.INTEGER);
+			cstmt.executeUpdate();
+			System.out.println("Called Prepared procedure (with NULLs): " + proc_nm);
+			showContents(con, tbl_nm);
+
+
+			System.out.println("Test completed. Cleanup procedure and table.");
+			stmt.execute("DROP PROCEDURE IF EXISTS " + proc_nm + ";");
+			stmt.execute("DROP TABLE     IF EXISTS " + tbl_nm + ";");
+
+		} catch (SQLException e) {
+			System.out.println("main failed: " + e.getMessage());
+			System.out.println("ABORTING TEST");
+		} finally {
+			try {
+				if (cstmt != null)
+					cstmt.close();
+				if (stmt != null)
+					stmt.close();
+			} catch (SQLException e) { /* ignore */ }
+		}
+
+		con.close();
+	}
+
+
+	// some utility methods for showing table content and params meta data
+	static void showContents(Connection con, String tblnm) {
+		Statement stmt = null;
+		ResultSet rs = null;
+		try {
+			stmt = con.createStatement();
+			rs = stmt.executeQuery("SELECT * FROM " + tblnm);
+			if (rs != null) {
+				ResultSetMetaData rsmd = rs.getMetaData();
+				System.out.println("Table " + tblnm + " has " + rsmd.getColumnCount() + " columns:");
+				for (int col = 1; col <= rsmd.getColumnCount(); col++) {
+					System.out.print("\t" + rsmd.getColumnLabel(col));
+				}
+				System.out.println();
+				while (rs.next()) {
+					for (int col = 1; col <= rsmd.getColumnCount(); col++) {
+						System.out.print("\t" + rs.getString(col));
+					}
+					System.out.println();
+				}
+			} else
+				System.out.println("failed to execute query: SELECT * FROM " + tblnm);
+		} catch (SQLException e) {
+			System.out.println("showContents failed: " + e.getMessage());
+		} finally {
+			try {
+				if (rs != null)
+					rs.close();
+				if (stmt != null)
+					stmt.close();
+			} catch (SQLException e) { /* ignore */ }
+		}
+	}
+
+	static void showParams(PreparedStatement stmt) {
+		try {
+			ParameterMetaData pmd = stmt.getParameterMetaData();
+			System.out.println(pmd.getParameterCount() + " parameters reported:");
+			for (int parm = 1; parm <= pmd.getParameterCount(); parm++) {
+				System.out.print(parm + ".");
+				int nullable = pmd.isNullable(parm);
+				System.out.println("\tnullable  " + nullable + " (" + paramNullableName(nullable) + ")");
+				System.out.println("\tsigned    " + pmd.isSigned(parm));
+				System.out.println("\tprecision " + pmd.getPrecision(parm));
+				System.out.println("\tscale     " + pmd.getScale(parm));
+				System.out.println("\ttype      " + pmd.getParameterType(parm));
+				System.out.println("\ttypename  " + pmd.getParameterTypeName(parm));
+				System.out.println("\tclassname " + pmd.getParameterClassName(parm));
+				int mode = pmd.getParameterMode(parm);
+				System.out.println("\tmode      " + mode + " (" + paramModeName(mode) + ")");
+			}
+		} catch (SQLException e) {
+			System.out.println("showParams failed: " + e.getMessage());
+		}
+	}
+
+	static String paramNullableName(int nullable) {
+		if (nullable == ParameterMetaData.parameterNoNulls)
+			return "NO";
+		if (nullable == ParameterMetaData.parameterNullable)
+			return "YA";
+		if (nullable == ParameterMetaData.parameterNullableUnknown)
+			return "UNKNOWN";
+		return "INVALID" + nullable;
+	}
+
+	static String paramModeName(int mode) {
+		if (mode == ParameterMetaData.parameterModeIn)
+			return "IN";
+		if (mode == ParameterMetaData.parameterModeInOut)
+			return "INOUT";
+		if (mode == ParameterMetaData.parameterModeOut)
+			return "OUT";
+		if (mode == ParameterMetaData.parameterModeUnknown)
+			return "UNKNOWN";
+		return "INVALID" + mode;
+	}
+}
--- a/tests/build.xml
+++ b/tests/build.xml
@@ -121,6 +121,7 @@ Copyright 1997 - July 2008 CWI, August 2
     <antcall target="Test_PStimedate" />
     <antcall target="Test_PStimezone" />
     <antcall target="Test_PStypes" />
+    <antcall target="Test_CallableStmt" />
     <antcall target="Test_Rbooleans" />
     <antcall target="Test_Rmetadata" />
     <antcall target="Test_Rpositioning" />
@@ -160,6 +161,12 @@ Copyright 1997 - July 2008 CWI, August 2
     </antcall>
   </target>
 
+  <target name="Test_CallableStmt">
+    <antcall target="test_class">
+      <param name="test.class" value="Test_CallableStmt" />
+    </antcall>
+  </target>
+
   <target name="Test_Cautocommit">
     <antcall target="test_class">
       <param name="test.class" value="Test_Cautocommit" />