Mercurial > hg > monetdb-java
changeset 64:bb0d66ad7dc6 embedded
More done
line wrap: on
line diff
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetConnection.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetConnection.java @@ -1,2698 +1,1287 @@ -/* - * 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 - 2016 MonetDB B.V. - */ - package nl.cwi.monetdb.jdbc; -import java.io.File; -import java.io.IOException; -import java.net.SocketException; -import java.net.SocketTimeoutException; -import java.sql.Array; -import java.sql.Blob; -import java.sql.CallableStatement; -import java.sql.Clob; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.NClob; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; -import java.sql.SQLWarning; -import java.sql.SQLXML; -import java.sql.Savepoint; -import java.sql.Statement; -import java.sql.Struct; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Properties; -import java.util.WeakHashMap; -import java.util.concurrent.Executor; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - import nl.cwi.monetdb.jdbc.types.INET; import nl.cwi.monetdb.jdbc.types.URL; -import nl.cwi.monetdb.mcl.MCLException; -import nl.cwi.monetdb.mcl.io.AbstractMCLReader; -import nl.cwi.monetdb.mcl.io.AbstractMCLWriter; -import nl.cwi.monetdb.mcl.connection.EmbeddedMonetDB; -import nl.cwi.monetdb.mcl.connection.DeleteMe; -import nl.cwi.monetdb.mcl.connection.AbstractMonetDBConnection; -import nl.cwi.monetdb.mcl.parser.HeaderLineParser; -import nl.cwi.monetdb.mcl.parser.StartOfHeaderParser; +import nl.cwi.monetdb.mcl.connection.MCLException; +import nl.cwi.monetdb.mcl.connection.Debugger; +import nl.cwi.monetdb.mcl.connection.MonetDBLanguage; import nl.cwi.monetdb.mcl.parser.MCLParseException; +import nl.cwi.monetdb.responses.ResponseList; +import nl.cwi.monetdb.responses.SendThread; + +import java.io.*; +import java.net.SocketException; +import java.net.SocketTimeoutException; +import java.sql.*; +import java.util.*; +import java.util.concurrent.Executor; /** * A {@link Connection} suitable for the MonetDB database. - * + * * This connection represents a connection (session) to a MonetDB * database. SQL statements are executed and results are returned within * the context of a connection. This Connection object holds a physical * connection to the MonetDB database. - * + * * A Connection object's database should able to provide information * describing its tables, its supported SQL grammar, its stored * procedures, the capabilities of this connection, and so on. This * information is obtained with the getMetaData method. - * + * * Note: By default a Connection object is in auto-commit mode, which * means that it automatically commits changes after executing each * statement. If auto-commit mode has been disabled, the method commit * must be called explicitly in order to commit changes; otherwise, * database changes will not be saved. - * + * * The current state of this connection is that it nearly implements the * whole Connection interface. * * @author Fabian Groffen * @version 1.2 */ -public class MonetConnection extends MonetWrapper implements Connection { - - /** A connection to mserver5 either through MAPI with TCP or embedded */ - private final AbstractMonetDBConnection server; - /** The Reader from the server */ - private final AbstractMCLReader in; - /** The Writer to the server */ - private final AbstractMCLWriter out; - /** A StartOfHeaderParser declared for reuse. */ - private final StartOfHeaderParser sohp; - - /** Whether this Connection is closed (and cannot be used anymore) */ - private boolean closed; +public abstract class MonetConnection extends MonetWrapper implements Connection { - /** Whether this Connection is in autocommit mode */ - private boolean autoCommit = true; - - /** The stack of warnings for this Connection object */ - private SQLWarning warnings = null; - /** The Connection specific mapping of user defined types to Java - * types */ - private Map<String,Class<?>> typeMap = new HashMap<String,Class<?>>() {/** - * - */ - private static final long serialVersionUID = 1L; - { - put("inet", INET.class); - put("url", URL.class); - } - }; + /** the successful processed input properties */ + private final Properties conn_props; - // See javadoc for documentation about WeakHashMap if you don't know what - // it does !!!NOW!!! (only when you deal with it of course) - /** A Map containing all (active) Statements created from this Connection */ - private Map<Statement,?> statements = new WeakHashMap<Statement, Object>(); - - /** The number of results we receive from the server at once */ - private int curReplySize = -1; // the server by default uses -1 (all) - - /** Whether or not BLOB is mapped to BINARY within the driver */ - private final boolean blobIsBinary; + /** The language to connect with */ + protected MonetDBLanguage currentMonetDBLanguage = MonetDBLanguage.LANG_SQL; + /** The database to connect to */ + protected String database; + /** Authentication hash method */ + protected final String hash; + /** An optional thread that is used for sending large queries */ + private SendThread sendThread; + /** Whether this Connection is closed (and cannot be used anymore) */ + private boolean closed; + /** Whether this Connection is in autocommit mode */ + private boolean autoCommit = true; + /** The stack of warnings for this Connection object */ + private SQLWarning warnings; + /** The Connection specific mapping of user defined types to Java types */ + private Map<String,Class<?>> typeMap = new HashMap<String,Class<?>>() { + private static final long serialVersionUID = 1L; { + put("inet", INET.class); + put("url", URL.class); + } + }; - /** - * Constructor of a Connection for MonetDB. At this moment the - * current implementation limits itself to storing the given host, - * database, username and password for later use by the - * createStatement() call. This constructor is only accessible to - * classes from the jdbc package. - * - * @param props a Property hashtable holding the properties needed for - * connecting - * @throws SQLException if a database error occurs - * @throws IllegalArgumentException is one of the arguments is null or empty - */ - MonetConnection(Properties props) - throws SQLException, IllegalArgumentException - { - String database = props.getProperty("database"); - if (database == null || database.trim().isEmpty()) - throw new IllegalArgumentException("database should not be null or empty"); - boolean isEmbedded = Boolean.parseBoolean(props.getProperty("embedded")); - String username = props.getProperty("user"); - String password = props.getProperty("password"); - boolean debug = Boolean.valueOf(props.getProperty("debug")); - blobIsBinary = Boolean.valueOf(props.getProperty("treat_blob_as_binary")); + // See javadoc for documentation about WeakHashMap if you don't know what + // it does !!!NOW!!! (only when you deal with it of course) + /** A Map containing all (active) Statements created from this Connection */ + private Map<Statement,?> statements = new WeakHashMap<Statement, Object>(); - if(isEmbedded) { - String directory = props.getProperty("directory"); - if (directory == null || directory.trim().isEmpty()) - throw new IllegalArgumentException("directory should not be null or empty"); + /** The number of results we receive from the server at once */ + private int curReplySize = -1; // the server by default uses -1 (all) - server = new EmbeddedMonetDB("localhost", -1, database, username, debug, "sql", null, directory); - } else { - String hostname = props.getProperty("host"); - String hash = props.getProperty("hash"); - String language = props.getProperty("language"); - int port = 0; - int sockTimeout = 0; + /** Whether or not BLOB is mapped to BINARY within the driver */ + private final boolean blobIsBinary; - try { - port = Integer.parseInt(props.getProperty("port")); - } catch (NumberFormatException e) { - } - try { - sockTimeout = Integer.parseInt(props.getProperty("so_timeout")); - } catch (NumberFormatException e) { - } + protected boolean isDebugging; + + protected Debugger ourSavior; - // check input arguments - if (hostname == null || hostname.trim().isEmpty()) - throw new IllegalArgumentException("hostname should not be null or empty"); - if (port == 0) - throw new IllegalArgumentException("port should not be 0"); - if (username == null || username.trim().isEmpty()) - throw new IllegalArgumentException("user should not be null or empty"); - if (password == null || password.trim().isEmpty()) - throw new IllegalArgumentException("password should not be null or empty"); - if (language == null || language.trim().isEmpty()) { - language = "sql"; - addWarning("No language given, defaulting to 'sql'", "M1M05"); - } - server = new DeleteMe(hostname, port, database, username, debug, language, hash); - try { - server.setSoTimeout(sockTimeout); - } catch (SocketException e) { - addWarning("The socket timeout could not be set", "M1M05"); - } - server.setLanguage(language); - } + /** + * Constructor of a Connection for MonetDB. At this moment the + * current implementation limits itself to storing the given host, + * database, username and password for later use by the + * createStatement() call. This constructor is only accessible to + * classes from the jdbc package. + * + * @throws IOException if an error occurs + */ + public MonetConnection(Properties props, String database, String hash, String language, boolean blobIsBinary, boolean isDebugging) throws IOException { + this.conn_props = props; + this.database = database; + this.hash = hash; + this.currentMonetDBLanguage = MonetDBLanguage.GetLanguageFromString(language); + this.blobIsBinary = blobIsBinary; + this.isDebugging = isDebugging; + } - // we're debugging here... uhm, should be off in real life - if (debug) { - try { - String fname = props.getProperty("logfile", "monet_" + - System.currentTimeMillis() + ".log"); - File f = new File(fname); - int ext = fname.lastIndexOf('.'); - if (ext < 0) ext = fname.length(); - String pre = fname.substring(0, ext); - String suf = fname.substring(ext); - - for (int i = 1; f.exists(); i++) { - f = new File(pre + "-" + i + suf); - } + public MonetDBLanguage getCurrentMonetDBLanguage() { + return currentMonetDBLanguage; + } - server.debug(f.getAbsolutePath()); - } catch (IOException ex) { - throw new SQLException("Opening logfile failed: " + ex.getMessage(), "08M01"); - } - } + public void setCurrentMonetDBLanguage(MonetDBLanguage currentMonetDBLanguage) { + this.currentMonetDBLanguage = currentMonetDBLanguage; + } - try { - List<String> warnings = server.connect(username, password); - if(warnings != null) { - for (String warning : warnings) { - addWarning(warning, "01M02"); - } - } - - in = server.getReader(); - out = server.getWriter(); - String error = in.waitForPrompt(); - if (error != null) - throw new SQLException(error.substring(6), "08001"); + public void setDebugging(String filename) throws IOException { + ourSavior = new Debugger(filename); + } - sohp = server.getStartOfHeaderParser(); - } catch (IOException e) { - throw new SQLException("Unable to connect (" + server.getHostname() + ":" + server.getPort() + "): " + e.getMessage(), "08006"); - } catch (MCLParseException e) { - throw new SQLException(e.getMessage(), "08001"); - } catch (MCLException e) { - String[] connex = e.getMessage().split("\n"); - SQLException sqle = new SQLException(connex[0], "08001", e); - for (int i = 1; i < connex.length; i++) { - sqle.setNextException(new SQLException(connex[1], "08001")); - } - throw sqle; - } - - // the following initialisers are only valid when the language - // is SQL... - if (server.getLang() == AbstractMonetDBConnection.LANG_SQL) { - // enable auto commit - setAutoCommit(true); - // set our time zone on the server - Calendar cal = Calendar.getInstance(); - int offset = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET); - offset /= (60 * 1000); // milliseconds to minutes - String tz = offset < 0 ? "-" : "+"; - tz += (Math.abs(offset) / 60 < 10 ? "0" : "") + (Math.abs(offset) / 60) + ":"; - offset -= (offset / 60) * 60; - tz += (offset < 10 ? "0" : "") + offset; - sendIndependentCommand("SET TIME ZONE INTERVAL '" + tz + "' HOUR TO MINUTE"); - } - } - - protected AbstractMonetDBConnection getServer() { - return server; - } - - //== methods of interface Connection + /** + * Connects to the given host and port, logging in as the given + * user. If followRedirect is false, a RedirectionException is + * thrown when a redirect is encountered. + * + * @return A List with informational (warning) messages. If this + * list is empty; then there are no warnings. + * @throws IOException if an I/O error occurs when creating the + * socket + * @throws MCLParseException if bogus data is received + * @throws MCLException if an MCL related error occurs + */ + public abstract List<String> connect(String user, String pass) throws IOException, MCLParseException, MCLException; - /** - * Clears all warnings reported for this Connection object. After a - * call to this method, the method getWarnings returns null until a - * new warning is reported for this Connection object. - */ - @Override - public void clearWarnings() { - warnings = null; - } + public abstract int getBlockSize(); + + public abstract int getSoTimeout() throws SocketException; - /** - * Releases this Connection object's database and JDBC resources - * immediately instead of waiting for them to be automatically - * released. All Statements created from this Connection will be - * closed when this method is called. - * - * Calling the method close on a Connection object that is already - * closed is a no-op. - */ - @Override - public void close() { - synchronized (server) { - for (Statement st : statements.keySet()) { - try { - st.close(); - } catch (SQLException e) { - // better luck next time! - } - } - // close the socket - server.close(); - // close active SendThread if any - if (sendThread != null) { - sendThread.shutdown(); - sendThread = null; - } - // report ourselves as closed - closed = true; - } - } + public abstract void setSoTimeout(int s) throws SocketException; + + public abstract void closeUnderlyingConnection() throws IOException; + + public abstract String getJDBCURL(); - /** - * Makes all changes made since the previous commit/rollback - * permanent and releases any database locks currently held by this - * Connection object. This method should be used only when - * auto-commit mode has been disabled. - * - * @throws SQLException if a database access error occurs or this - * Connection object is in auto-commit mode - * @see #setAutoCommit(boolean) - */ - @Override - public void commit() throws SQLException { - // note: can't use sendIndependentCommand here because we need - // to process the auto_commit state the server gives - - // create a container for the result - ResponseList l = new ResponseList( - 0, - 0, - ResultSet.FETCH_FORWARD, - ResultSet.CONCUR_READ_ONLY - ); - // send commit to the server - try { - l.processQuery("COMMIT"); - } finally { - l.close(); - } - } + /** + * Releases this Connection object's database and JDBC resources + * immediately instead of waiting for them to be automatically + * released. All Statements created from this Connection will be + * closed when this method is called. + * + * Calling the method close on a Connection object that is already + * closed is a no-op. + */ + @Override + public synchronized void close() { + for (Statement st : statements.keySet()) { + try { + st.close(); + } catch (SQLException e) { + // better luck next time! + } + } + //close the debugger + try { + if (ourSavior != null) { + ourSavior.close(); + } + } catch (IOException e) { + // ignore it + } + // close the socket or the embedded server + try { + this.closeUnderlyingConnection(); + } catch (IOException e) { + // ignore it + } + // close active SendThread if any + if (sendThread != null) { + sendThread.shutdown(); + sendThread = null; + } + // report ourselves as closed + closed = true; + } - /** - * Factory method for creating Array objects. - * - * Note: When createArrayOf is used to create an array object that - * maps to a primitive data type, then it is implementation-defined - * whether the Array object is an array of that primitive data type - * or an array of Object. - * - * Note: The JDBC driver is responsible for mapping the elements - * Object array to the default JDBC SQL type defined in - * java.sql.Types for the given class of Object. The default mapping - * is specified in Appendix B of the JDBC specification. If the - * resulting JDBC type is not the appropriate type for the given - * typeName then it is implementation defined whether an - * SQLException is thrown or the driver supports the resulting - * conversion. - * - * @param typeName the SQL name of the type the elements of the - * array map to. The typeName is a database-specific name - * which may be the name of a built-in type, a user-defined - * type or a standard SQL type supported by this database. - * This is the value returned by Array.getBaseTypeName - * @return an Array object whose elements map to the specified SQL - * type - * @throws SQLException if a database error occurs, the JDBC type - * is not appropriate for the typeName and the conversion is - * not supported, the typeName is null or this method is - * called on a closed connection - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this data type - */ - @Override - public Array createArrayOf(String typeName, Object[] elements) - throws SQLException - { - throw new SQLFeatureNotSupportedException("createArrayOf(String, Object[]) not supported", "0A000"); - } - - /** - * Creates a Statement object for sending SQL statements to the - * database. SQL statements without parameters are normally - * executed using Statement objects. If the same SQL statement is - * executed many times, it may be more efficient to use a - * PreparedStatement object. - * - * Result sets created using the returned Statement object will by - * default be type TYPE_FORWARD_ONLY and have a concurrency level of - * CONCUR_READ_ONLY. - * - * @return a new default Statement object - * @throws SQLException if a database access error occurs - */ - @Override - public Statement createStatement() throws SQLException { - return createStatement( - ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY, - ResultSet.HOLD_CURSORS_OVER_COMMIT); - } + /** + * Destructor called by garbage collector before destroying this + * object tries to disconnect the MonetDB connection if it has not + * been disconnected already. + */ + @Override + protected void finalize() throws Throwable { + this.close(); + super.finalize(); + } - /** - * Creates a Statement object that will generate ResultSet objects - * with the given type and concurrency. This method is the same as - * the createStatement method above, but it allows the default - * result set type and concurrency to be overridden. - * - * @param resultSetType a result set type; one of - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, - * or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of - * ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE - * @return a new Statement object that will generate ResultSet objects with - * the given type and concurrency - * @throws SQLException if a database access error occurs - */ - @Override - public Statement createStatement( - int resultSetType, - int resultSetConcurrency) - throws SQLException - { - return createStatement( - resultSetType, - resultSetConcurrency, - ResultSet.HOLD_CURSORS_OVER_COMMIT); - } + //== methods of interface Connection + + /** + * Clears all warnings reported for this Connection object. After a + * call to this method, the method getWarnings returns null until a + * new warning is reported for this Connection object. + */ + @Override + public void clearWarnings() { + warnings = null; + } - /** - * Creates a Statement object that will generate ResultSet objects - * with the given type, concurrency, and holdability. This method - * is the same as the createStatement method above, but it allows - * the default result set type, concurrency, and holdability to be - * overridden. - * - * @param resultSetType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, - * or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency one of the following ResultSet - * constants: ResultSet.CONCUR_READ_ONLY or - * ResultSet.CONCUR_UPDATABLE - * @param resultSetHoldability one of the following ResultSet - * constants: ResultSet.HOLD_CURSORS_OVER_COMMIT or - * ResultSet.CLOSE_CURSORS_AT_COMMIT - * - * @return a new Statement object that will generate ResultSet - * objects with the given type, concurrency, and holdability - * @throws SQLException if a database access error occurs or the - * given parameters are not ResultSet constants indicating type, - * concurrency, and holdability - */ - @Override - public Statement createStatement( - int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) - throws SQLException - { - try { - Statement ret = - new MonetStatement( - this, - resultSetType, - resultSetConcurrency, - resultSetHoldability - ); - // 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 - } + /** + * Makes all changes made since the previous commit/rollback + * permanent and releases any database locks currently held by this + * Connection object. This method should be used only when + * auto-commit mode has been disabled. + * + * @throws SQLException if a database access error occurs or this + * Connection object is in auto-commit mode + * @see #setAutoCommit(boolean) + */ + @Override + public void commit() throws SQLException { + // note: can't use sendIndependentCommand here because we need + // to process the auto_commit state the server gives + + // create a container for the result + ResponseList l = new ResponseList(0, 0, ResultSet.FETCH_FORWARD, ResultSet.CONCUR_READ_ONLY); + // send commit to the server + try { + l.processQuery("COMMIT"); + } finally { + l.close(); + } + } - /** - * Constructs an object that implements the Clob interface. The - * object returned initially contains no data. The setAsciiStream, - * setCharacterStream and setString methods of the Clob interface - * may be used to add data to the Clob. - * - * @return a MonetClob instance - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support MonetClob objects that can be filled in - */ - @Override - public Clob createClob() throws SQLException { - throw new SQLFeatureNotSupportedException("createClob() not supported", "0A000"); - } - - /** - * Constructs an object that implements the Blob interface. The - * object returned initially contains no data. The setBinaryStream - * and setBytes methods of the Blob interface may be used to add - * data to the Blob. - * - * @return a MonetBlob instance - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support MonetBlob objects that can be filled in - */ - @Override - public Blob createBlob() throws SQLException { - throw new SQLFeatureNotSupportedException("createBlob() not supported", "0A000"); - } - - /** - * Constructs an object that implements the NClob interface. The - * object returned initially contains no data. The setAsciiStream, - * setCharacterStream and setString methods of the NClob interface - * may be used to add data to the NClob. - * - * @return an NClob instance - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support MonetClob objects that can be filled in - */ - @Override - public NClob createNClob() throws SQLException { - throw new SQLFeatureNotSupportedException("createNClob() not supported", "0A000"); - } - - /** - * Factory method for creating Struct objects. - * - * @param typeName the SQL type name of the SQL structured type that - * this Struct object maps to. The typeName is the name of a - * user-defined type that has been defined for this database. - * It is the value returned by Struct.getSQLTypeName. - * @param attributes the attributes that populate the returned - * object - * @return a Struct object that maps to the given SQL type and is - * populated with the given attributes - * @throws SQLException if a database error occurs, the typeName - * is null or this method is called on a closed connection - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this data type - */ - @Override - public Struct createStruct(String typeName, Object[] attributes) - throws SQLException - { - throw new SQLFeatureNotSupportedException("createStruct() not supported", "0A000"); - } - - /** - * Constructs an object that implements the SQLXML interface. The - * object returned initially contains no data. The - * createXmlStreamWriter object and setString method of the SQLXML - * interface may be used to add data to the SQLXML object. - * - * @return An object that implements the SQLXML interface - * @throws SQLFeatureNotSupportedException the JDBC driver does - * not support this data type - */ - @Override - public SQLXML createSQLXML() throws SQLException { - throw new SQLFeatureNotSupportedException("createSQLXML() not supported", "0A000"); - } + /** + * Factory method for creating Array objects. + * + * Note: When createArrayOf is used to create an array object that + * maps to a primitive data type, then it is implementation-defined + * whether the Array object is an array of that primitive data type + * or an array of Object. + * + * Note: The JDBC driver is responsible for mapping the elements + * Object array to the default JDBC SQL type defined in + * java.sql.Types for the given class of Object. The default mapping + * is specified in Appendix B of the JDBC specification. If the + * resulting JDBC type is not the appropriate type for the given + * typeName then it is implementation defined whether an + * SQLException is thrown or the driver supports the resulting + * conversion. + * + * @param typeName the SQL name of the type the elements of the + * array map to. The typeName is a database-specific name + * which may be the name of a built-in type, a user-defined + * type or a standard SQL type supported by this database. + * This is the value returned by Array.getBaseTypeName + * @return an Array object whose elements map to the specified SQL + * type + * @throws SQLException if a database error occurs, the JDBC type + * is not appropriate for the typeName and the conversion is + * not supported, the typeName is null or this method is + * called on a closed connection + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support this data type + * + * @since 1.6 + */ + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + throw new SQLFeatureNotSupportedException("createArrayOf(String, Object[]) not supported", "0A000"); + } - /** - * Retrieves the current auto-commit mode for this Connection - * object. - * - * @return the current state of this Connection object's auto-commit - * mode - * @see #setAutoCommit(boolean) - */ - @Override - public boolean getAutoCommit() throws SQLException { - return autoCommit; - } - - /** - * Retrieves this Connection object's current catalog name. - * - * @return the current catalog name or null if there is none - * @throws SQLException if a database access error occurs or the - * current language is not SQL - */ - @Override - public String getCatalog() throws SQLException { - // MonetDB does NOT support catalogs - return null; - } - - /** - * Not implemented by MonetDB's JDBC driver. - * - * @param name The name of the client info property to retrieve - * @return The value of the client info property specified - */ - @Override - public String getClientInfo(String name) { - // This method will also return null if the specified client - // info property name is not supported by the driver. - return null; - } - - /** - * Not implemented by MonetDB's JDBC driver. - * - * @return A Properties object that contains the name and current - * value of each of the client info properties supported by - * the driver. - */ - @Override - public Properties getClientInfo() { - return new Properties(); - } - - /** - * Retrieves the current holdability of ResultSet objects created - * using this Connection object. - * - * @return the holdability, one of - * ResultSet.HOLD_CURSORS_OVER_COMMIT or - * ResultSet.CLOSE_CURSORS_AT_COMMIT - */ - @Override - public int getHoldability() { - // TODO: perhaps it is better to have the server implement - // CLOSE_CURSORS_AT_COMMIT - return ResultSet.HOLD_CURSORS_OVER_COMMIT; - } - - /** - * Retrieves a DatabaseMetaData object that contains metadata about - * the database to which this Connection object represents a - * connection. The metadata includes information about the - * database's tables, its supported SQL grammar, its stored - * procedures, the capabilities of this connection, and so on. - * - * @throws SQLException if the current language is not SQL - * @return a DatabaseMetaData object for this Connection object - */ - @Override - public DatabaseMetaData getMetaData() throws SQLException { - if (server.getLang() != AbstractMonetDBConnection.LANG_SQL) - throw new SQLException("This method is only supported in SQL mode", "M0M04"); - - return new MonetDatabaseMetaData(this); - } + /** + * Creates a Statement object for sending SQL statements to the + * database. SQL statements without parameters are normally + * executed using Statement objects. If the same SQL statement is + * executed many times, it may be more efficient to use a + * PreparedStatement object. + * + * Result sets created using the returned Statement object will by + * default be type TYPE_FORWARD_ONLY and have a concurrency level of + * CONCUR_READ_ONLY. + * + * @return a new default Statement object + * @throws SQLException if a database access error occurs + */ + @Override + public Statement createStatement() throws SQLException { + return createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + } - /** - * Retrieves this Connection object's current transaction isolation - * level. - * - * @return the current transaction isolation level, which will be - * Connection.TRANSACTION_SERIALIZABLE - */ - @Override - public int getTransactionIsolation() { - return TRANSACTION_SERIALIZABLE; - } - - /** - * Retrieves the Map object associated with this Connection object. - * Unless the application has added an entry, the type map returned - * will be empty. - * - * @return the java.util.Map object associated with this Connection - * object - */ - @Override - public Map<String,Class<?>> getTypeMap() { - return typeMap; - } + /** + * Creates a Statement object that will generate ResultSet objects + * with the given type and concurrency. This method is the same as + * the createStatement method above, but it allows the default + * result set type and concurrency to be overridden. + * + * @param resultSetType a result set type; one of + * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, + * or ResultSet.TYPE_SCROLL_SENSITIVE + * @param resultSetConcurrency a concurrency type; one of + * ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE + * @return a new Statement object that will generate ResultSet objects with + * the given type and concurrency + * @throws SQLException if a database access error occurs + */ + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + return createStatement(resultSetType, resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT); + } - /** - * Retrieves the first warning reported by calls on this Connection - * object. If there is more than one warning, subsequent warnings - * will be chained to the first one and can be retrieved by calling - * the method SQLWarning.getNextWarning on the warning that was - * retrieved previously. - * - * This method may not be called on a closed connection; doing so - * will cause an SQLException to be thrown. - * - * Note: Subsequent warnings will be chained to this SQLWarning. - * - * @return the first SQLWarning object or null if there are none - * @throws SQLException if a database access error occurs or this method is - * called on a closed connection - */ - @Override - public SQLWarning getWarnings() throws SQLException { - if (closed) - throw new SQLException("Cannot call on closed Connection", "M1M20"); - - // if there are no warnings, this will be null, which fits with the - // specification. - return warnings; - } - - /** - * Retrieves whether this Connection object has been closed. A - * connection is closed if the method close has been called on it or - * if certain fatal errors have occurred. This method is guaranteed - * to return true only when it is called after the method - * Connection.close has been called. - * - * This method generally cannot be called to determine whether a - * connection to a database is valid or invalid. A typical client - * can determine that a connection is invalid by catching any - * exceptions that might be thrown when an operation is attempted. - * - * @return true if this Connection object is closed; false if it is - * still open - */ - @Override - public boolean isClosed() { - return closed; - } - - /** - * Retrieves whether this Connection object is in read-only mode. - * MonetDB currently doesn't support updateable result sets, but - * updates are possible. Hence the Connection object is never in - * read-only mode. - * - * @return true if this Connection object is read-only; false otherwise - */ - @Override - public boolean isReadOnly() { - return false; - } + /** + * Creates a Statement object that will generate ResultSet objects + * with the given type, concurrency, and holdability. This method + * is the same as the createStatement method above, but it allows + * the default result set type, concurrency, and holdability to be + * overridden. + * + * @param resultSetType one of the following ResultSet constants: + * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, + * or ResultSet.TYPE_SCROLL_SENSITIVE + * @param resultSetConcurrency one of the following ResultSet + * constants: ResultSet.CONCUR_READ_ONLY or + * ResultSet.CONCUR_UPDATABLE + * @param resultSetHoldability one of the following ResultSet + * constants: ResultSet.HOLD_CURSORS_OVER_COMMIT or + * ResultSet.CLOSE_CURSORS_AT_COMMIT + * + * @return a new Statement object that will generate ResultSet + * objects with the given type, concurrency, and holdability + * @throws SQLException if a database access error occurs or the + * given parameters are not ResultSet constants indicating type, + * concurrency, and holdability + */ + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + try { + Statement ret = new MonetStatement(this, resultSetType, resultSetConcurrency, resultSetHoldability); + // 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 + } - /** - * Returns true if the connection has not been closed and is still - * valid. The driver shall submit a query on the connection or use - * some other mechanism that positively verifies the connection is - * still valid when this method is called. - * - * The query submitted by the driver to validate the connection - * shall be executed in the context of the current transaction. - * - * @param timeout The time in seconds to wait for the database - * operation used to validate the connection to complete. If - * the timeout period expires before the operation completes, - * this method returns false. A value of 0 indicates a - * timeout is not applied to the database operation. - * @return true if the connection is valid, false otherwise - * @throws SQLException if the value supplied for timeout is less - * than 0 - */ - @Override - public boolean isValid(int timeout) throws SQLException { - if (timeout < 0) - throw new SQLException("timeout is less than 0", "M1M05"); - if (closed) - return false; - // ping db using select 1; - Statement stmt = null; - try { - stmt = createStatement(); - // the timeout parameter is ignored here, since - // MonetStatement.setQueryTimeout(timeout) is not supported. - stmt.executeQuery("SELECT 1"); - stmt.close(); - return true; - } catch (Exception e) { - if (stmt != null) { - try { - stmt.close(); - } catch (Exception e2) {} - } - } - return false; - } + /** + * Constructs an object that implements the Clob interface. The + * object returned initially contains no data. The setAsciiStream, + * setCharacterStream and setString methods of the Clob interface + * may be used to add data to the Clob. + * + * @return a MonetClob instance + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support MonetClob objects that can be filled in + * @since 1.6 + */ + @Override + public Clob createClob() throws SQLException { + return new MonetClob(""); + } - @Override - public String nativeSQL(String sql) {return sql;} - @Override - public CallableStatement prepareCall(String sql) {return null;} - @Override - public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) {return null;} - @Override - public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) {return null;} + /** + * Constructs an object that implements the Blob interface. The + * object returned initially contains no data. The setBinaryStream + * and setBytes methods of the Blob interface may be used to add + * data to the Blob. + * + * @return a MonetBlob instance + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support MonetBlob objects that can be filled in + */ + @Override + public Blob createBlob() throws SQLException { + throw new SQLFeatureNotSupportedException("createBlob() not supported", "0A000"); + } - /** - * Creates a PreparedStatement object for sending parameterized SQL - * statements to the database. - * - * A SQL statement with or without IN parameters can be pre-compiled - * and stored in a PreparedStatement object. This object can then be - * used to efficiently execute this statement multiple times. - * - * Note: This method is optimized for handling parametric SQL - * statements that benefit from precompilation. If the driver - * supports precompilation, the method prepareStatement will send - * the statement to the database for precompilation. Some drivers - * may not support precompilation. In this case, the statement may - * not be sent to the database until the PreparedStatement object is - * executed. This has no direct effect on users; however, it does - * affect which methods throw certain SQLException objects. - * - * Result sets created using the returned PreparedStatement object - * will by default be type TYPE_FORWARD_ONLY and have a concurrency - * level of CONCUR_READ_ONLY. - * - * @param sql an SQL statement that may contain one or more '?' IN - * parameter placeholders - * @return a new default PreparedStatement object containing the - * pre-compiled SQL statement - * @throws SQLException if a database access error occurs - */ - @Override - public PreparedStatement prepareStatement(String sql) throws SQLException { - return prepareStatement( - sql, - ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY, - ResultSet.HOLD_CURSORS_OVER_COMMIT - ); - } + /** + * Constructs an object that implements the NClob interface. The + * object returned initially contains no data. The setAsciiStream, + * setCharacterStream and setString methods of the NClob interface + * may be used to add data to the NClob. + * + * @return an NClob instance + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support MonetClob objects that can be filled in + */ + @Override + public NClob createNClob() throws SQLException { + throw new SQLFeatureNotSupportedException("createNClob() not supported", "0A000"); + } - /** - * Creates a PreparedStatement object that will generate ResultSet - * objects with the given type and concurrency. This method is the - * same as the prepareStatement method above, but it allows the - * default result set type and concurrency to be overridden. - * - * @param sql a String object that is the SQL statement to be sent to the - * database; may contain one or more ? IN parameters - * @param resultSetType a result set type; one of - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, - * or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency a concurrency type; one of - * ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE - * @return a new PreparedStatement object containing the pre-compiled SQL - * statement that will produce ResultSet objects with the given - * type and concurrency - * @throws SQLException if a database access error occurs or the given - * parameters are not ResultSet constants indicating - * type and concurrency - */ - @Override - public PreparedStatement prepareStatement( - String sql, - int resultSetType, - int resultSetConcurrency) - throws SQLException - { - return prepareStatement( - sql, - resultSetType, - resultSetConcurrency, - ResultSet.HOLD_CURSORS_OVER_COMMIT - ); - } + /** + * Factory method for creating Struct objects. + * + * @param typeName the SQL type name of the SQL structured type that + * this Struct object maps to. The typeName is the name of a + * user-defined type that has been defined for this database. + * It is the value returned by Struct.getSQLTypeName. + * @param attributes the attributes that populate the returned + * object + * @return a Struct object that maps to the given SQL type and is + * populated with the given attributes + * @throws SQLException if a database error occurs, the typeName + * is null or this method is called on a closed connection + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support this data type + */ + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + throw new SQLFeatureNotSupportedException("createStruct() not supported", "0A000"); + } - /** - * Creates a PreparedStatement object that will generate ResultSet - * objects with the given type, concurrency, and holdability. - * - * This method is the same as the prepareStatement method above, but - * it allows the default result set type, concurrency, and - * holdability to be overridden. - * - * @param sql a String object that is the SQL statement to be sent - * to the database; may contain one or more ? IN parameters - * @param resultSetType one of the following ResultSet constants: - * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, - * or ResultSet.TYPE_SCROLL_SENSITIVE - * @param resultSetConcurrency one of the following ResultSet - * constants: ResultSet.CONCUR_READ_ONLY or - * ResultSet.CONCUR_UPDATABLE - * @param resultSetHoldability one of the following ResultSet - * constants: ResultSet.HOLD_CURSORS_OVER_COMMIT or - * ResultSet.CLOSE_CURSORS_AT_COMMIT - * @return a new PreparedStatement object, containing the - * pre-compiled SQL statement, that will generate ResultSet objects - * with the given type, concurrency, and holdability - * @throws SQLException if a database access error occurs or the - * given parameters are not ResultSet constants indicating type, - * concurrency, and holdability - */ - @Override - public PreparedStatement prepareStatement( - String sql, - int resultSetType, - int resultSetConcurrency, - int resultSetHoldability) - throws SQLException - { - try { - PreparedStatement ret = new MonetPreparedStatement( - 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 - } + /** + * Constructs an object that implements the SQLXML interface. The + * object returned initially contains no data. The + * createXmlStreamWriter object and setString method of the SQLXML + * interface may be used to add data to the SQLXML object. + * + * @return An object that implements the SQLXML interface + * @throws SQLFeatureNotSupportedException the JDBC driver does + * not support this data type + */ + @Override + public SQLXML createSQLXML() throws SQLException { + throw new SQLFeatureNotSupportedException("createSQLXML() not supported", "0A000"); + } + + /** + * Retrieves the current auto-commit mode for this Connection + * object. + * + * @return the current state of this Connection object's auto-commit + * mode + * @see #setAutoCommit(boolean) + */ + @Override + public boolean getAutoCommit() throws SQLException { + return autoCommit; + } - /** - * Creates a default PreparedStatement object that has the - * capability to retrieve auto-generated keys. The given constant - * tells the driver whether it should make auto-generated keys - * available for retrieval. This parameter is ignored if the SQL - * statement is not an INSERT statement. - * - * Note: This method is optimized for handling parametric SQL - * statements that benefit from precompilation. If the driver - * supports precompilation, the method prepareStatement will send - * the statement to the database for precompilation. Some drivers - * may not support precompilation. In this case, the statement may - * not be sent to the database until the PreparedStatement object is - * executed. This has no direct effect on users; however, it does - * affect which methods throw certain SQLExceptions. - * - * Result sets created using the returned PreparedStatement object - * will by default be type TYPE_FORWARD_ONLY and have a concurrency - * level of CONCUR_READ_ONLY. - * - * @param sql an SQL statement that may contain one or more '?' IN - * parameter placeholders - * @param autoGeneratedKeys a flag indicating whether auto-generated - * keys should be returned; one of - * Statement.RETURN_GENERATED_KEYS or - * Statement.NO_GENERATED_KEYS - * @return a new PreparedStatement object, containing the - * pre-compiled SQL statement, that will have the capability - * of returning auto-generated keys - * @throws SQLException - if a database access error occurs or the - * given parameter is not a Statement constant indicating - * whether auto-generated keys should be returned - */ - @Override - public PreparedStatement prepareStatement( - String sql, - int autoGeneratedKeys) - throws SQLException - { - if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && - autoGeneratedKeys != Statement.NO_GENERATED_KEYS) - throw new SQLException("Invalid argument, expected RETURN_GENERATED_KEYS or NO_GENERATED_KEYS", "M1M05"); - - /* MonetDB has no way to disable this, so just do the normal - * thing ;) */ - return prepareStatement( - sql, - ResultSet.TYPE_FORWARD_ONLY, - ResultSet.CONCUR_READ_ONLY - ); - } + /** + * Retrieves this Connection object's current catalog name. + * + * @return the current catalog name or null if there is none + * @throws SQLException if a database access error occurs or the + * current language is not SQL + */ + @Override + public String getCatalog() throws SQLException { + // MonetDB does NOT support catalogs + return null; + } - @Override - public PreparedStatement prepareStatement(String sql, int[] columnIndexes) {return null;} - @Override - public PreparedStatement prepareStatement(String sql, String[] columnNames) {return null;} + /** + * Not implemented by MonetDB's JDBC driver. + * + * @param name The name of the client info property to retrieve + * @return The value of the client info property specified + */ + @Override + public String getClientInfo(String name) { + // This method will also return null if the specified client + // info property name is not supported by the driver. + return null; + } - /** - * Removes the given Savepoint object from the current transaction. - * Any reference to the savepoint after it have been removed will - * cause an SQLException to be thrown. - * - * @param savepoint the Savepoint object to be removed - * @throws SQLException if a database access error occurs or the given - * Savepoint object is not a valid savepoint in the current - * transaction - */ - @Override - public void releaseSavepoint(Savepoint savepoint) throws SQLException { - if (!(savepoint instanceof MonetSavepoint)) throw - new SQLException("This driver can only handle savepoints it created itself", "M0M06"); - - MonetSavepoint sp = (MonetSavepoint)savepoint; - - // note: can't use sendIndependentCommand here because we need - // to process the auto_commit state the server gives - - // create a container for the result - ResponseList l = new ResponseList( - 0, - 0, - ResultSet.FETCH_FORWARD, - ResultSet.CONCUR_READ_ONLY - ); - // send the appropriate query string to the database - try { - l.processQuery("RELEASE SAVEPOINT " + sp.getName()); - } finally { - l.close(); - } - } + /** + * Not implemented by MonetDB's JDBC driver. + * + * @return A Properties object that contains the name and current + * value of each of the client info properties supported by + * the driver. + */ + @Override + public Properties getClientInfo() { + return new Properties(); + } - /** - * Undoes all changes made in the current transaction and releases - * any database locks currently held by this Connection object. This - * method should be used only when auto-commit mode has been - * disabled. - * - * @throws SQLException if a database access error occurs or this - * Connection object is in auto-commit mode - * @see #setAutoCommit(boolean) - */ - @Override - public void rollback() throws SQLException { - // note: can't use sendIndependentCommand here because we need - // to process the auto_commit state the server gives - - // create a container for the result - ResponseList l = new ResponseList( - 0, - 0, - ResultSet.FETCH_FORWARD, - ResultSet.CONCUR_READ_ONLY - ); - // send rollback to the server - try { - l.processQuery("ROLLBACK"); - } finally { - l.close(); - } - } - - /** - * Undoes all changes made after the given Savepoint object was set. - * - * This method should be used only when auto-commit has been - * disabled. - * - * @param savepoint the Savepoint object to roll back to - * @throws SQLException if a database access error occurs, the - * Savepoint object is no longer valid, or this Connection - * object is currently in auto-commit mode - */ - @Override - public void rollback(Savepoint savepoint) throws SQLException { - if (!(savepoint instanceof MonetSavepoint)) throw - new SQLException("This driver can only handle savepoints it created itself", "M0M06"); - - MonetSavepoint sp = (MonetSavepoint)savepoint; - - // note: can't use sendIndependentCommand here because we need - // to process the auto_commit state the server gives + /** + * Retrieves the current holdability of ResultSet objects created + * using this Connection object. + * + * @return the holdability, one of + * ResultSet.HOLD_CURSORS_OVER_COMMIT or + * ResultSet.CLOSE_CURSORS_AT_COMMIT + */ + @Override + public int getHoldability() { + // TODO: perhaps it is better to have the server implement + // CLOSE_CURSORS_AT_COMMIT + return ResultSet.HOLD_CURSORS_OVER_COMMIT; + } - // create a container for the result - ResponseList l = new ResponseList( - 0, - 0, - ResultSet.FETCH_FORWARD, - ResultSet.CONCUR_READ_ONLY - ); - // send the appropriate query string to the database - try { - l.processQuery("ROLLBACK TO SAVEPOINT " + sp.getName()); - } finally { - l.close(); - } - } + /** + * Retrieves a DatabaseMetaData object that contains metadata about + * the database to which this Connection object represents a + * connection. The metadata includes information about the + * database's tables, its supported SQL grammar, its stored + * procedures, the capabilities of this connection, and so on. + * + * @throws SQLException if the current language is not SQL + * @return a DatabaseMetaData object for this Connection object + */ + @Override + public DatabaseMetaData getMetaData() throws SQLException { + if (this.currentMonetDBLanguage != MonetDBLanguage.LANG_SQL) { + throw new SQLException("This method is only supported in SQL mode", "M0M04"); + } + return new MonetDatabaseMetaData(this); + } - /** - * Sets this connection's auto-commit mode to the given state. If a - * connection is in auto-commit mode, then all its SQL statements - * will be executed and committed as individual transactions. - * Otherwise, its SQL statements are grouped into transactions that - * are terminated by a call to either the method commit or the - * method rollback. By default, new connections are in auto-commit - * mode. - * - * The commit occurs when the statement completes or the next - * execute occurs, whichever comes first. In the case of statements - * returning a ResultSet object, the statement completes when the - * last row of the ResultSet object has been retrieved or the - * ResultSet object has been closed. In advanced cases, a single - * statement may return multiple results as well as output parameter - * values. In these cases, the commit occurs when all results and - * output parameter values have been retrieved. - * - * NOTE: If this method is called during a transaction, the - * transaction is committed. - * - * @param autoCommit true to enable auto-commit mode; false to disable it - * @throws SQLException if a database access error occurs - * @see #getAutoCommit() - */ - @Override - public void setAutoCommit(boolean autoCommit) throws SQLException { - if (this.autoCommit != autoCommit) { - sendControlCommand("auto_commit " + (autoCommit ? "1" : "0")); - this.autoCommit = autoCommit; - } - } - - /** - * Sets the given catalog name in order to select a subspace of this - * Connection object's database in which to work. If the driver - * does not support catalogs, it will silently ignore this request. - */ - @Override - public void setCatalog(String catalog) throws SQLException { - // silently ignore this request as MonetDB does not support catalogs - } - - /** - * Not implemented by MonetDB's JDBC driver. - * - * @param name The name of the client info property to set - * @param value The value to set the client info property to. If the - * value is null, the current value of the specified property - * is cleared. - */ - @Override - public void setClientInfo(String name, String value) { - addWarning("clientInfo: " + name + "is not a recognised property", "01M07"); - } - - /** - * Not implemented by MonetDB's JDBC driver. - * - * @param props The list of client info properties to set - */ - @Override - public void setClientInfo(Properties props) { - for (Entry<Object, Object> entry : props.entrySet()) { - setClientInfo(entry.getKey().toString(), - entry.getValue().toString()); - } - } + /** + * Retrieves this Connection object's current transaction isolation + * level. + * + * @return the current transaction isolation level, which will be + * Connection.TRANSACTION_SERIALIZABLE + */ + @Override + public int getTransactionIsolation() { + return TRANSACTION_SERIALIZABLE; + } - @Override - public void setHoldability(int holdability) {} - - /** - * Puts this connection in read-only mode as a hint to the driver to - * enable database optimizations. MonetDB doesn't support any mode - * here, hence an SQLWarning is generated if attempted to set - * to true here. - * - * @param readOnly true enables read-only mode; false disables it - * @throws SQLException if a database access error occurs or this - * method is called during a transaction. - */ - @Override - public void setReadOnly(boolean readOnly) throws SQLException { - if (readOnly) - addWarning("cannot setReadOnly(true): read-only Connection mode not supported", "01M08"); - } - - /** - * Creates an unnamed savepoint in the current transaction and - * returns the new Savepoint object that represents it. - * - * @return the new Savepoint object - * @throws SQLException if a database access error occurs or this Connection - * object is currently in auto-commit mode - */ - @Override - public Savepoint setSavepoint() throws SQLException { - // create a new Savepoint object - MonetSavepoint sp = new MonetSavepoint(); - - // note: can't use sendIndependentCommand here because we need - // to process the auto_commit state the server gives + /** + * Retrieves the Map object associated with this Connection object. + * Unless the application has added an entry, the type map returned + * will be empty. + * + * @return the java.util.Map object associated with this Connection + * object + */ + @Override + public Map<String,Class<?>> getTypeMap() { + return typeMap; + } - // create a container for the result - ResponseList l = new ResponseList( - 0, - 0, - ResultSet.FETCH_FORWARD, - ResultSet.CONCUR_READ_ONLY - ); - // send the appropriate query string to the database - try { - l.processQuery("SAVEPOINT " + sp.getName()); - } finally { - l.close(); - } - - return sp; - } - - /** - * Creates a savepoint with the given name in the current - * transaction and returns the new Savepoint object that represents - * it. - * - * @param name a String containing the name of the savepoint - * @return the new Savepoint object - * @throws SQLException if a database access error occurs or this Connection - * object is currently in auto-commit mode - */ - @Override - public Savepoint setSavepoint(String name) throws SQLException { - // create a new Savepoint object - MonetSavepoint sp; - try { - sp = new MonetSavepoint(name); - } catch (IllegalArgumentException e) { - throw new SQLException(e.getMessage(), "M0M03"); - } - - // note: can't use sendIndependentCommand here because we need - // to process the auto_commit state the server gives - - // create a container for the result - ResponseList l = new ResponseList( - 0, - 0, - ResultSet.FETCH_FORWARD, - ResultSet.CONCUR_READ_ONLY - ); - // send the appropriate query string to the database - try { - l.processQuery("SAVEPOINT " + sp.getName()); - } finally { - l.close(); - } + /** + * Retrieves the first warning reported by calls on this Connection + * object. If there is more than one warning, subsequent warnings + * will be chained to the first one and can be retrieved by calling + * the method SQLWarning.getNextWarning on the warning that was + * retrieved previously. + * + * This method may not be called on a closed connection; doing so + * will cause an SQLException to be thrown. + * + * Note: Subsequent warnings will be chained to this SQLWarning. + * + * @return the first SQLWarning object or null if there are none + * @throws SQLException if a database access error occurs or this method is + * called on a closed connection + */ + @Override + public SQLWarning getWarnings() throws SQLException { + if (closed) { + throw new SQLException("Cannot call on closed Connection", "M1M20"); + } + // if there are no warnings, this will be null, which fits with the + // specification. + return warnings; + } - return sp; - } - - /** - * Attempts to change the transaction isolation level for this - * Connection object to the one given. The constants defined in the - * interface Connection are the possible transaction isolation - * levels. - * - * @param level one of the following Connection constants: - * Connection.TRANSACTION_READ_UNCOMMITTED, - * Connection.TRANSACTION_READ_COMMITTED, - * Connection.TRANSACTION_REPEATABLE_READ, or - * Connection.TRANSACTION_SERIALIZABLE. - */ - @Override - public void setTransactionIsolation(int level) { - if (level != TRANSACTION_SERIALIZABLE) { - addWarning("MonetDB only supports fully serializable " + - "transactions, continuing with transaction level " + - "raised to TRANSACTION_SERIALIZABLE", "01M09"); - } - } - - /** - * Installs the given TypeMap object as the type map for this - * Connection object. The type map will be used for the custom - * mapping of SQL structured types and distinct types. - * - * @param map the java.util.Map object to install as the replacement for - * this Connection object's default type map - */ - @Override - public void setTypeMap(Map<String, Class<?>> map) { - typeMap = map; - } + /** + * Retrieves whether this Connection object has been closed. A + * connection is closed if the method close has been called on it or + * if certain fatal errors have occurred. This method is guaranteed + * to return true only when it is called after the method + * Connection.close has been called. + * + * This method generally cannot be called to determine whether a + * connection to a database is valid or invalid. A typical client + * can determine that a connection is invalid by catching any + * exceptions that might be thrown when an operation is attempted. + * + * @return true if this Connection object is closed; false if it is + * still open + */ + @Override + public boolean isClosed() { + return closed; + } - /** - * Returns a string identifying this Connection to the MonetDB - * server. - * - * @return a String representing this Object - */ - @Override - public String toString() { - return "MonetDB Connection (" + getJDBCURL() + ") " + - (closed ? "connected" : "disconnected"); - } - - //== 1.7 methods (JDBC 4.1) - - /** - * Sets the given schema name to access. - * - * @param schema the name of a schema in which to work - * @throws SQLException if a database access error occurs or this - * method is called on a closed connection - */ - @Override - public void setSchema(String schema) throws SQLException { - if (closed) - throw new SQLException("Cannot call on closed Connection", "M1M20"); - if (schema != null) - createStatement().execute("SET SCHEMA \"" + schema + "\""); - } - - /** - * Retrieves this Connection object's current schema name. - * - * @return the current schema name or null if there is none - * @throws SQLException if a database access error occurs or this - * method is called on a closed connection - */ - @Override - public String getSchema() throws SQLException { - if (closed) - throw new SQLException("Cannot call on closed Connection", "M1M20"); + /** + * Retrieves whether this Connection object is in read-only mode. + * MonetDB currently doesn't support updateable result sets, but + * updates are possible. Hence the Connection object is never in + * read-only mode. + * + * @return true if this Connection object is read-only; false otherwise + */ + @Override + public boolean isReadOnly() { + return false; + } - String cur_schema; - Statement st = createStatement(); - ResultSet rs = null; - try { - rs = st.executeQuery("SELECT CURRENT_SCHEMA"); - if (!rs.next()) - throw new SQLException("Row expected", "02000"); - cur_schema = rs.getString(1); - } finally { - if (rs != null) - rs.close(); - st.close(); - } - return cur_schema; - } - - /** - * Terminates an open connection. Calling abort results in: - * * The connection marked as closed - * * Closes any physical connection to the database - * * Releases resources used by the connection - * * Insures that any thread that is currently accessing the - * connection will either progress to completion or throw an - * SQLException. - * Calling abort marks the connection closed and releases any - * resources. Calling abort on a closed connection is a no-op. - * - * @param executor The Executor implementation which will be used by - * abort - * @throws SQLException if a database access error occurs or the - * executor is null - * @throws SecurityException if a security manager exists and - * its checkPermission method denies calling abort - */ - @Override - public void abort(Executor executor) throws SQLException { - if (closed) - return; - if (executor == null) - throw new SQLException("executor is null", "M1M05"); - // this is really the simplest thing to do, it destroys - // everything (in particular the server connection) - close(); - } + /** + * Returns true if the connection has not been closed and is still + * valid. The driver shall submit a query on the connection or use + * some other mechanism that positively verifies the connection is + * still valid when this method is called. + * + * The query submitted by the driver to validate the connection + * shall be executed in the context of the current transaction. + * + * @param timeout The time in seconds to wait for the database + * operation used to validate the connection to complete. If + * the timeout period expires before the operation completes, + * this method returns false. A value of 0 indicates a + * timeout is not applied to the database operation. + * @return true if the connection is valid, false otherwise + * @throws SQLException if the value supplied for timeout is less + * than 0 + */ + @Override + public boolean isValid(int timeout) throws SQLException { + if (timeout < 0) + throw new SQLException("timeout is less than 0", "M1M05"); + if (closed) + return false; + // ping db using select 1; + Statement stmt = null; + try { + stmt = createStatement(); + // the timeout parameter is ignored here, since + // MonetStatement.setQueryTimeout(timeout) is not supported. + stmt.executeQuery("SELECT 1"); + stmt.close(); + return true; + } catch (Exception e) { + if (stmt != null) { + try { + stmt.close(); + } catch (Exception e2) {} + } + } + return false; + } - /** - * Sets the maximum period a Connection or objects created from the - * Connection will wait for the database to reply to any one - * request. If any request remains unanswered, the waiting method - * will return with a SQLException, and the Connection or objects - * created from the Connection will be marked as closed. Any - * subsequent use of the objects, with the exception of the close, - * isClosed or Connection.isValid methods, will result in a - * SQLException. - * - * @param executor The Executor implementation which will be used by - * setNetworkTimeout - * @param millis The time in milliseconds to wait for the - * database operation to complete - * @throws SQLException if a database access error occurs, this - * method is called on a closed connection, the executor is - * null, or the value specified for seconds is less than 0. - */ - @Override - public void setNetworkTimeout(Executor executor, int millis) - throws SQLException - { - if (closed) - throw new SQLException("Cannot call on closed Connection", "M1M20"); - if (executor == null) - throw new SQLException("executor is null", "M1M05"); - if (millis < 0) - throw new SQLException("milliseconds is less than zero", "M1M05"); + @Override + public String nativeSQL(String sql) {return sql;} + + @Override + public CallableStatement prepareCall(String sql) {return null;} - try { - server.setSoTimeout(millis); - } catch (SocketException e) { - throw new SQLException(e.getMessage(), "08000"); - } - } + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) {return null;} - /** - * Retrieves the number of milliseconds the driver will wait for a - * database request to complete. If the limit is exceeded, a - * SQLException is thrown. - * - * @return the current timeout limit in milliseconds; zero means - * there is no limit - * @throws SQLException if a database access error occurs or - * this method is called on a closed Connection - */ - @Override - public int getNetworkTimeout() throws SQLException { - if (closed) - throw new SQLException("Cannot call on closed Connection", "M1M20"); + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) {return null;} - try { - return server.getSoTimeout(); - } catch (SocketException e) { - throw new SQLException(e.getMessage(), "08000"); - } - } - - //== end methods of interface Connection - - public String getJDBCURL() { - return server.getJDBCURL(); - } - - /** - * Returns whether the BLOB type should be mapped to BINARY type. - */ - boolean getBlobAsBinary() { - return blobIsBinary; - } - - /** - * Sends the given string to MonetDB as regular statement, making - * sure there is a prompt after the command is sent. All possible - * returned information is discarded. Encountered errors are - * reported. - * - * @param command the exact string to send to MonetDB - * @throws SQLException if an IO exception or a database error occurs - */ - void sendIndependentCommand(String command) throws SQLException { - synchronized (server) { - try { - out.writeLine(server.getQueryTemplateHeader(0) + command + server.getQueryTemplateHeader(1)); - String error = in.waitForPrompt(); - if (error != null) - throw new SQLException(error.substring(6), - error.substring(0, 5)); - } catch (SocketTimeoutException e) { - close(); // JDBC 4.1 semantics: abort() - throw new SQLException("connection timed out", "08M33"); - } catch (IOException e) { - throw new SQLException(e.getMessage(), "08000"); - } - } - } + /** + * Creates a PreparedStatement object for sending parameterized SQL + * statements to the database. + * + * A SQL statement with or without IN parameters can be pre-compiled + * and stored in a PreparedStatement object. This object can then be + * used to efficiently execute this statement multiple times. + * + * Note: This method is optimized for handling parametric SQL + * statements that benefit from precompilation. If the driver + * supports precompilation, the method prepareStatement will send + * the statement to the database for precompilation. Some drivers + * may not support precompilation. In this case, the statement may + * not be sent to the database until the PreparedStatement object is + * executed. This has no direct effect on users; however, it does + * affect which methods throw certain SQLException objects. + * + * Result sets created using the returned PreparedStatement object + * will by default be type TYPE_FORWARD_ONLY and have a concurrency + * level of CONCUR_READ_ONLY. + * + * @param sql an SQL statement that may contain one or more '?' IN + * parameter placeholders + * @return a new default PreparedStatement object containing the + * pre-compiled SQL statement + * @throws SQLException if a database access error occurs + */ + @Override + public PreparedStatement prepareStatement(String sql) throws SQLException { + return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, ResultSet.HOLD_CURSORS_OVER_COMMIT); + } - /** - * Sends the given string to MonetDB as control statement, making - * sure there is a prompt after the command is sent. All possible - * returned information is discarded. Encountered errors are - * reported. - * - * @param command the exact string to send to MonetDB - * @throws SQLException if an IO exception or a database error occurs - */ - void sendControlCommand(String command) throws SQLException { - // send X command - synchronized (server) { - try { - out.writeLine(server.getCommandTemplateHeader(0) + command + server.getCommandTemplateHeader(1)); - String error = in.waitForPrompt(); - if (error != null) - throw new SQLException(error.substring(6), - error.substring(0, 5)); - } catch (SocketTimeoutException e) { - close(); // JDBC 4.1 semantics, abort() - throw new SQLException("connection timed out", "08M33"); - } catch (IOException e) { - throw new SQLException(e.getMessage(), "08000"); - } - } - } - - /** - * Adds a warning to the pile of warnings this Connection object - * has. If there were no warnings (or clearWarnings was called) - * this warning will be the first, otherwise this warning will get - * appended to the current warning. - * - * @param reason the warning message - */ - void addWarning(String reason, String sqlstate) { - if (warnings == null) { - warnings = new SQLWarning(reason, sqlstate); - } else { - warnings.setNextWarning(new SQLWarning(reason, sqlstate)); - } - } - - /** the default number of rows that are (attempted to) read at once */ - private final static int DEF_FETCHSIZE = 250; - /** The sequence counter */ - private static int seqCounter = 0; - - /** An optional thread that is used for sending large queries */ - private SendThread sendThread = null; - - /** - * A Response is a message sent by the server to indicate some - * action has taken place, and possible results of that action. - */ - // {{{ interface Response - interface Response { - /** - * Adds a line to the underlying Response implementation. - * - * @param line the header line as String - * @param linetype the line type according to the MAPI protocol - * @return a non-null String if the line is invalid, - * or additional lines are not allowed. - */ - String addLine(String line, int linetype); - - /** - * Returns whether this Reponse expects more lines to be added - * to it. - * - * @return true if a next line should be added, false otherwise - */ - boolean wantsMore(); - - /** - * Indicates that no more header lines will be added to this - * Response implementation. - * - * @throws SQLException if the contents of the Response is not - * consistent or sufficient. - */ - void complete() throws SQLException; - - /** - * Instructs the Response implementation to close and do the - * necessary clean up procedures. - * - */ - void close(); - } - // }}} + /** + * Creates a PreparedStatement object that will generate ResultSet + * objects with the given type and concurrency. This method is the + * same as the prepareStatement method above, but it allows the + * default result set type and concurrency to be overridden. + * + * @param sql a String object that is the SQL statement to be sent to the + * database; may contain one or more ? IN parameters + * @param resultSetType a result set type; one of + * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, + * or ResultSet.TYPE_SCROLL_SENSITIVE + * @param resultSetConcurrency a concurrency type; one of + * ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE + * @return a new PreparedStatement object containing the pre-compiled SQL + * statement that will produce ResultSet objects with the given + * type and concurrency + * @throws SQLException if a database access error occurs or the given + * parameters are not ResultSet constants indicating + * type and concurrency + */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + return prepareStatement(sql, resultSetType, resultSetConcurrency, ResultSet.HOLD_CURSORS_OVER_COMMIT); + } - /** - * The ResultSetResponse represents a tabular result sent by the - * server. This is typically an SQL table. The MAPI headers of the - * Response look like: - * <pre> - * &1 1 28 2 10 - * # name, value # name - * # varchar, varchar # type - * </pre> - * there the first line consists out of<br /> - * <tt>&"qt" "id" "tc" "cc" "rc"</tt>. - */ - // {{{ ResultSetResponse class implementation - class ResultSetResponse implements Response { - /** The number of columns in this result */ - public final int columncount; - /** The total number of rows this result set has */ - public final int tuplecount; - /** The numbers of rows to retrieve per DataBlockResponse */ - private int cacheSize; - /** The table ID of this result */ - public final int id; - /** The names of the columns in this result */ - private String[] name; - /** The types of the columns in this result */ - private String[] type; - /** The max string length for each column in this result */ - private int[] columnLengths; - /** The table for each column in this result */ - private String[] tableNames; - /** The query sequence number */ - private final int seqnr; - /** A List of result blocks (chunks of size fetchSize/cacheSize) */ - private DataBlockResponse[] resultBlocks; - - /** A bitmap telling whether the headers are set or not */ - private boolean[] isSet; - /** Whether this Response is closed */ - private boolean closed; - - /** The Connection that we should use when requesting a new block */ - private MonetConnection.ResponseList parent; - /** Whether the fetchSize was explitly set by the user */ - private boolean cacheSizeSetExplicitly = false; - /** Whether we should send an Xclose command to the server - * if we close this Response */ - private boolean destroyOnClose; - /** the offset to be used on Xexport queries */ - private int blockOffset = 0; - - /** A parser for header lines */ - HeaderLineParser hlp; - - private final static int NAMES = 0; - private final static int TYPES = 1; - private final static int TABLES = 2; - private final static int LENS = 3; - + /** + * Creates a PreparedStatement object that will generate ResultSet + * objects with the given type, concurrency, and holdability. + * + * This method is the same as the prepareStatement method above, but + * it allows the default result set type, concurrency, and + * holdability to be overridden. + * + * @param sql a String object that is the SQL statement to be sent + * to the database; may contain one or more ? IN parameters + * @param resultSetType one of the following ResultSet constants: + * ResultSet.TYPE_FORWARD_ONLY, ResultSet.TYPE_SCROLL_INSENSITIVE, + * or ResultSet.TYPE_SCROLL_SENSITIVE + * @param resultSetConcurrency one of the following ResultSet + * constants: ResultSet.CONCUR_READ_ONLY or + * ResultSet.CONCUR_UPDATABLE + * @param resultSetHoldability one of the following ResultSet + * constants: ResultSet.HOLD_CURSORS_OVER_COMMIT or + * ResultSet.CLOSE_CURSORS_AT_COMMIT + * @return a new PreparedStatement object, containing the + * pre-compiled SQL statement, that will generate ResultSet objects + * with the given type, concurrency, and holdability + * @throws SQLException if a database access error occurs or the + * given parameters are not ResultSet constants indicating type, + * concurrency, and holdability + */ + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + try { + PreparedStatement ret = new MonetPreparedStatement(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 + } - /** - * Sole constructor, which requires a MonetConnection parent to - * be given. - * - * @param id the ID of the result set - * @param tuplecount the total number of tuples in the result set - * @param columncount the number of columns in the result set - * @param rowcount the number of rows in the current block - * @param parent the parent that created this Response and will - * supply new result blocks when necessary - * @param seq the query sequence number - */ - ResultSetResponse( - int id, - int tuplecount, - int columncount, - int rowcount, - MonetConnection.ResponseList parent, - int seq) - throws SQLException - { - isSet = new boolean[7]; - this.parent = parent; - if (parent.cachesize == 0) { - /* Below we have to calculate how many "chunks" we need - * to allocate to store the entire result. However, if - * the user didn't set a cache size, as in this case, we - * need to stick to our defaults. */ - cacheSize = MonetConnection.DEF_FETCHSIZE; - cacheSizeSetExplicitly = false; - } else { - cacheSize = parent.cachesize; - cacheSizeSetExplicitly = true; - } - /* So far, so good. Now the problem with EXPLAIN, DOT, etc - * queries is, that they don't support any block fetching, - * so we need to always fetch everything at once. For that - * reason, the cache size is here set to the rowcount if - * it's larger, such that we do a full fetch at once. - * (Because we always set a reply_size, we can only get a - * larger rowcount from the server if it doesn't paginate, - * because it's a pseudo SQL result.) */ - if (rowcount > cacheSize) - cacheSize = rowcount; - seqnr = seq; - closed = false; - destroyOnClose = id > 0 && tuplecount > rowcount; - - this.id = id; - this.tuplecount = tuplecount; - this.columncount = columncount; - this.resultBlocks = - new DataBlockResponse[(tuplecount / cacheSize) + 1]; - - hlp = server.getHeaderLineParser(columncount); + /** + * Creates a default PreparedStatement object that has the + * capability to retrieve auto-generated keys. The given constant + * tells the driver whether it should make auto-generated keys + * available for retrieval. This parameter is ignored if the SQL + * statement is not an INSERT statement. + * + * Note: This method is optimized for handling parametric SQL + * statements that benefit from precompilation. If the driver + * supports precompilation, the method prepareStatement will send + * the statement to the database for precompilation. Some drivers + * may not support precompilation. In this case, the statement may + * not be sent to the database until the PreparedStatement object is + * executed. This has no direct effect on users; however, it does + * affect which methods throw certain SQLExceptions. + * + * Result sets created using the returned PreparedStatement object + * will by default be type TYPE_FORWARD_ONLY and have a concurrency + * level of CONCUR_READ_ONLY. + * + * @param sql an SQL statement that may contain one or more '?' IN + * parameter placeholders + * @param autoGeneratedKeys a flag indicating whether auto-generated + * keys should be returned; one of + * Statement.RETURN_GENERATED_KEYS or + * Statement.NO_GENERATED_KEYS + * @return a new PreparedStatement object, containing the + * pre-compiled SQL statement, that will have the capability + * of returning auto-generated keys + * @throws SQLException - if a database access error occurs or the + * given parameter is not a Statement constant indicating + * whether auto-generated keys should be returned + */ + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { + throw new SQLException("Invalid argument, expected RETURN_GENERATED_KEYS or NO_GENERATED_KEYS", "M1M05"); + } + /* MonetDB has no way to disable this, so just do the normal thing ;) */ + return prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + } - resultBlocks[0] = new DataBlockResponse( - rowcount, - parent.rstype == ResultSet.TYPE_FORWARD_ONLY - ); - } - - /** - * Parses the given string and changes the value of the matching - * header appropriately, or passes it on to the underlying - * DataResponse. - * - * @param tmpLine the string that contains the header - * @return a non-null String if the header cannot be parsed or - * is unknown - */ - // {{{ addLine - @Override - public String addLine(String tmpLine, int linetype) { - if (isSet[LENS] && isSet[TYPES] && isSet[TABLES] && isSet[NAMES]) { - return resultBlocks[0].addLine(tmpLine, linetype); - } - - if (linetype != AbstractMCLReader.HEADER) - return "header expected, got: " + tmpLine; + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) {return null;} - // depending on the name of the header, we continue - try { - switch (hlp.parse(tmpLine)) { - case HeaderLineParser.NAME: - name = hlp.values.clone(); - isSet[NAMES] = true; - break; - case HeaderLineParser.LENGTH: - columnLengths = hlp.intValues.clone(); - isSet[LENS] = true; - break; - case HeaderLineParser.TYPE: - type = hlp.values.clone(); - isSet[TYPES] = true; - break; - case HeaderLineParser.TABLE: - tableNames = hlp.values.clone(); - isSet[TABLES] = true; - break; - } - } catch (MCLParseException e) { - return e.getMessage(); - } - - // all is well - return null; - } - // }}} - - /** - * Returns whether this ResultSetResponse needs more lines. - * This method returns true if not all headers are set, or the - * first DataBlockResponse reports to want more. - */ - @Override - public boolean wantsMore() { - return !(isSet[LENS] && isSet[TYPES] && isSet[TABLES] && isSet[NAMES]) || resultBlocks[0].wantsMore(); - } + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) {return null;} - /** - * Returns an array of Strings containing the values between - * ',\t' separators. - * - * @param chrLine a character array holding the input data - * @param start where the relevant data starts - * @param stop where the relevant data stops - * @return an array of Strings - */ - final private String[] getValues(char[] chrLine, int start, int stop) { - int elem = 0; - String[] values = new String[columncount]; - - for (int i = start; i < stop; i++) { - if (chrLine[i] == '\t' && chrLine[i - 1] == ',') { - values[elem++] = - new String(chrLine, start, i - 1 - start); - start = i + 1; - } - } - // at the left over part - values[elem++] = new String(chrLine, start, stop - start); - - return values; - } - - /** - * Adds the given DataBlockResponse to this ResultSetResponse at - * the given block position. - * - * @param offset the offset number of rows for this block - * @param rr the DataBlockResponse to add - */ - void addDataBlockResponse(int offset, DataBlockResponse rr) { - int block = (offset - blockOffset) / cacheSize; - resultBlocks[block] = rr; - } - - /** - * Marks this Response as being completed. A complete Response - * needs to be consistent with regard to its internal data. - * - * @throws SQLException if the data currently in this Response is not - * sufficient to be consistant - */ - @Override - public void complete() throws SQLException { - String error = ""; - if (!isSet[NAMES]) error += "name header missing\n"; - if (!isSet[TYPES]) error += "type header missing\n"; - if (!isSet[TABLES]) error += "table name header missing\n"; - if (!isSet[LENS]) error += "column width header missing\n"; - if (!error.equals("")) throw new SQLException(error, "M0M10"); - } - - /** - * Returns the names of the columns - * - * @return the names of the columns - */ - String[] getNames() { - return name; - } - - /** - * Returns the types of the columns - * - * @return the types of the columns - */ - String[] getTypes() { - return type; - } + /** + * Removes the given Savepoint object from the current transaction. + * Any reference to the savepoint after it have been removed will + * cause an SQLException to be thrown. + * + * @param savepoint the Savepoint object to be removed + * @throws SQLException if a database access error occurs or the given + * Savepoint object is not a valid savepoint in the current + * transaction + */ + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + if (!(savepoint instanceof MonetSavepoint)) { + throw new SQLException("This driver can only handle savepoints it created itself", "M0M06"); + } + MonetSavepoint sp = (MonetSavepoint) savepoint; + // note: can't use sendIndependentCommand here because we need + // to process the auto_commit state the server gives + // create a container for the result + ResponseList l = new ResponseList(0, 0, ResultSet.FETCH_FORWARD, ResultSet.CONCUR_READ_ONLY); + // send the appropriate query string to the database + try { + l.processQuery("RELEASE SAVEPOINT " + sp.getName()); + } finally { + l.close(); + } + } - /** - * Returns the tables of the columns - * - * @return the tables of the columns - */ - String[] getTableNames() { - return tableNames; - } - - /** - * Returns the lengths of the columns - * - * @return the lengths of the columns - */ - int[] getColumnLengths() { - return columnLengths; - } - - /** - * Returns the cache size used within this Response - * - * @return the cache size - */ - int getCacheSize() { - return cacheSize; - } - - /** - * Returns the current block offset - * - * @return the current block offset - */ - int getBlockOffset() { - return blockOffset; - } + /** + * Undoes all changes made in the current transaction and releases + * any database locks currently held by this Connection object. This + * method should be used only when auto-commit mode has been + * disabled. + * + * @throws SQLException if a database access error occurs or this + * Connection object is in auto-commit mode + * @see #setAutoCommit(boolean) + */ + @Override + public void rollback() throws SQLException { + // note: can't use sendIndependentCommand here because we need + // to process the auto_commit state the server gives + // create a container for the result + ResponseList l = new ResponseList(0, 0, ResultSet.FETCH_FORWARD, ResultSet.CONCUR_READ_ONLY); + // send rollback to the server + try { + l.processQuery("ROLLBACK"); + } finally { + l.close(); + } + } - /** - * Returns the ResultSet type, FORWARD_ONLY or not. - * - * @return the ResultSet type - */ - int getRSType() { - return parent.rstype; - } - - /** - * Returns the concurrency of the ResultSet. - * - * @return the ResultSet concurrency - */ - int getRSConcur() { - return parent.rsconcur; - } + /** + * Undoes all changes made after the given Savepoint object was set. + * + * This method should be used only when auto-commit has been + * disabled. + * + * @param savepoint the Savepoint object to roll back to + * @throws SQLException if a database access error occurs, the + * Savepoint object is no longer valid, or this Connection + * object is currently in auto-commit mode + */ + @Override + public void rollback(Savepoint savepoint) throws SQLException { + if (!(savepoint instanceof MonetSavepoint)) { + throw new SQLException("This driver can only handle savepoints it created itself", "M0M06"); + } - /** - * Returns a line from the cache. If the line is already present in the - * cache, it is returned, if not apropriate actions are taken to make - * sure the right block is being fetched and as soon as the requested - * line is fetched it is returned. - * - * @param row the row in the result set to return - * @return the exact row read as requested or null if the requested row - * is out of the scope of the result set - * @throws SQLException if an database error occurs - */ - String getLine(int row) throws SQLException { - if (row >= tuplecount || row < 0) - return null; - - int block = (row - blockOffset) / cacheSize; - int blockLine = (row - blockOffset) % cacheSize; - - // do we have the right block loaded? (optimistic try) - DataBlockResponse rawr; - // load block if appropriate - if ((rawr = resultBlocks[block]) == null) { - /// TODO: ponder about a maximum number of blocks to keep - /// in memory when dealing with random access to - /// reduce memory blow-up - - // if we're running forward only, we can discard the oldmapi - // block loaded - if (parent.rstype == ResultSet.TYPE_FORWARD_ONLY) { - for (int i = 0; i < block; i++) - resultBlocks[i] = null; + MonetSavepoint sp = (MonetSavepoint)savepoint; + // note: can't use sendIndependentCommand here because we need + // to process the auto_commit state the server gives + // create a container for the result + ResponseList l = new ResponseList(0, 0, ResultSet.FETCH_FORWARD, ResultSet.CONCUR_READ_ONLY); + // send the appropriate query string to the database + try { + l.processQuery("ROLLBACK TO SAVEPOINT " + sp.getName()); + } finally { + l.close(); + } + } - if (MonetConnection.seqCounter - 1 == seqnr && - !cacheSizeSetExplicitly && - tuplecount - row > cacheSize && - cacheSize < MonetConnection.DEF_FETCHSIZE * 10) - { - // there has no query been issued after this - // one, so we can consider this an uninterrupted - // continuation request. Let's once increase - // the cacheSize as it was not explicitly set, - // since the chances are high that we won't - // bother anyone else by doing so, and just - // gaining some performance. - - // store the previous position in the - // blockOffset variable - blockOffset += cacheSize; - - // increase the cache size (a lot) - cacheSize *= 10; - - // by changing the cacheSize, we also - // change the block measures. Luckily - // we don't care about previous blocks - // because we have a forward running - // pointer only. However, we do have - // to recalculate the block number, to - // ensure the next call to find this - // new block. - block = (row - blockOffset) / cacheSize; - blockLine = (row - blockOffset) % cacheSize; - } - } - - // ok, need to fetch cache block first - parent.executeQuery( - server.getCommandHeaderTemplates(), - "export " + id + " " + ((block * cacheSize) + blockOffset) + " " + cacheSize - ); - rawr = resultBlocks[block]; - if (rawr == null) throw - new AssertionError("block " + block + " should have been fetched by now :("); - } - - return rawr.getRow(blockLine); - } - - /** - * Closes this Response by sending an Xclose to the server indicating - * that the result can be closed at the server side as well. - */ - @Override - public void close() { - if (closed) return; - // send command to server indicating we're done with this - // result only if we had an ID in the header and this result - // was larger than the reply size - try { - if (destroyOnClose) sendControlCommand("close " + id); - } catch (SQLException e) { - // probably a connection error... - } - - // close the data block associated with us - for (int i = 1; i < resultBlocks.length; i++) { - DataBlockResponse r = resultBlocks[i]; - if (r != null) r.close(); - } - - closed = true; - } + /** + * Sets this connection's auto-commit mode to the given state. If a + * connection is in auto-commit mode, then all its SQL statements + * will be executed and committed as individual transactions. + * Otherwise, its SQL statements are grouped into transactions that + * are terminated by a call to either the method commit or the + * method rollback. By default, new connections are in auto-commit + * mode. + * + * The commit occurs when the statement completes or the next + * execute occurs, whichever comes first. In the case of statements + * returning a ResultSet object, the statement completes when the + * last row of the ResultSet object has been retrieved or the + * ResultSet object has been closed. In advanced cases, a single + * statement may return multiple results as well as output parameter + * values. In these cases, the commit occurs when all results and + * output parameter values have been retrieved. + * + * NOTE: If this method is called during a transaction, the + * transaction is committed. + * + * @param autoCommit true to enable auto-commit mode; false to disable it + * @throws SQLException if a database access error occurs + * @see #getAutoCommit() + */ + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + if (this.autoCommit != autoCommit) { + sendControlCommand("auto_commit " + (autoCommit ? "1" : "0")); + this.autoCommit = autoCommit; + } + } - /** - * Returns whether this Response is closed - * - * @return whether this Response is closed - */ - boolean isClosed() { - return closed; - } - } - // }}} - - /** - * The DataBlockResponse is tabular data belonging to a - * ResultSetResponse. Tabular data from the server typically looks - * like: - * <pre> - * [ "value", 56 ] - * </pre> - * where each column is separated by ",\t" and each tuple surrounded - * by brackets ("[" and "]"). A DataBlockResponse object holds the - * raw data as read from the server, in a parsed manner, ready for - * easy retrieval. - * - * This object is not intended to be queried by multiple threads - * synchronously. It is designed to work for one thread retrieving - * rows from it. When multiple threads will retrieve rows from this - * object, it is possible for threads to get the same data. - */ - // {{{ DataBlockResponse class implementation - static class DataBlockResponse implements Response { - /** The String array to keep the data in */ - private final String[] data; + /** + * Sets the given catalog name in order to select a subspace of this + * Connection object's database in which to work. If the driver + * does not support catalogs, it will silently ignore this request. + */ + @Override + public void setCatalog(String catalog) throws SQLException { + // silently ignore this request as MonetDB does not support catalogs + } - /** The counter which keeps the current position in the data array */ - private int pos; - /** Whether we can discard lines as soon as we have read them */ - private boolean forwardOnly; - - /** - * Constructs a DataBlockResponse object - * @param size the size of the data array to create - * @param forward whether this is a forward only result - */ - DataBlockResponse(int size, boolean forward) { - pos = -1; - data = new String[size]; - forwardOnly = forward; - } + /** + * Not implemented by MonetDB's JDBC driver. + * + * @param name The name of the client info property to set + * @param value The value to set the client info property to. If the + * value is null, the current value of the specified property + * is cleared. + */ + @Override + public void setClientInfo(String name, String value) { + addWarning("clientInfo: " + name + "is not a recognised property", "01M07"); + } - /** - * addLine adds a String of data to this object's data array. - * Note that an IndexOutOfBoundsException can be thrown when an - * attempt is made to add more than the original construction size - * specified. - * - * @param line the header line as String - * @param linetype the line type according to the MAPI protocol - * @return a non-null String if the line is invalid, - * or additional lines are not allowed. - */ - @Override - public String addLine(String line, int linetype) { - if (linetype != AbstractMCLReader.RESULT) - return "protocol violation: unexpected line in data block: " + line; - // add to the backing array - data[++pos] = line; - - // all is well - return null; - } + /** + * Not implemented by MonetDB's JDBC driver. + * + * @param props The list of client info properties to set + */ + @Override + public void setClientInfo(Properties props) { + for (Map.Entry<Object, Object> entry : props.entrySet()) { + setClientInfo(entry.getKey().toString(), entry.getValue().toString()); + } + } - /** - * Returns whether this Reponse expects more lines to be added - * to it. - * - * @return true if a next line should be added, false otherwise - */ - @Override - public boolean wantsMore() { - // remember: pos is the value already stored - return pos + 1 < data.length; - } - - /** - * Indicates that no more header lines will be added to this - * Response implementation. In most cases this is a redundant - * operation because the data array is full. However... it can - * happen that this is NOT the case! - * - * @throws SQLException if not all rows are filled - */ - @Override - public void complete() throws SQLException { - if ((pos + 1) != data.length) throw - new SQLException("Inconsistent state detected! Current block capacity: " + data.length + ", block usage: " + (pos + 1) + ". Did MonetDB send what it promised to?", "M0M10"); - } - - /** - * Instructs the Response implementation to close and do the - * necessary clean up procedures. - * - */ - @Override - public void close() { - // feed all rows to the garbage collector - for (int i = 0; i < data.length; i++) data[i] = null; - } + /** + * Changes the default holdability of ResultSet objects created using this + * Connection object to the given holdability. The default holdability of + * ResultSet objects can be be determined by invoking DatabaseMetaData.getResultSetHoldability(). + * + * @param holdability - a ResultSet holdability constant; one of + * ResultSet.HOLD_CURSORS_OVER_COMMIT or + * ResultSet.CLOSE_CURSORS_AT_COMMIT + * @see #getHoldability() + */ + @Override + public void setHoldability(int holdability) throws SQLException { + // we only support ResultSet.HOLD_CURSORS_OVER_COMMIT + if (holdability != ResultSet.HOLD_CURSORS_OVER_COMMIT) + throw new SQLFeatureNotSupportedException("setHoldability(CLOSE_CURSORS_AT_COMMIT) not supported", "0A000"); + } - /** - * Retrieves the required row. Warning: if the requested rows - * is out of bounds, an IndexOutOfBoundsException will be - * thrown. - * - * @param line the row to retrieve - * @return the requested row as String - */ - String getRow(int line) { - if (forwardOnly) { - String ret = data[line]; - data[line] = null; - return ret; - } else { - return data[line]; - } - } - } - // }}} - - /** - * The UpdateResponse represents an update statement response. It - * is issued on an UPDATE, INSERT or DELETE SQL statement. This - * response keeps a count field that represents the affected rows - * and a field that contains the last inserted auto-generated ID, or - * -1 if not applicable.<br /> - * <tt>&2 0 -1</tt> - */ - // {{{ UpdateResponse class implementation - static class UpdateResponse implements Response { - public final int count; - public final String lastid; - - public UpdateResponse(int cnt, String id) { - // fill the blank finals - this.count = cnt; - this.lastid = id; - } - - @Override - public String addLine(String line, int linetype) { - return "Header lines are not supported for an UpdateResponse"; - } - - @Override - public boolean wantsMore() { - return false; - } - - @Override - public void complete() { - // empty, because there is nothing to check - } - - @Override - public void close() { - // nothing to do here... - } - } - // }}} + /** + * Puts this connection in read-only mode as a hint to the driver to + * enable database optimizations. MonetDB doesn't support any mode + * here, hence an SQLWarning is generated if attempted to set + * to true here. + * + * @param readOnly true enables read-only mode; false disables it + * @throws SQLException if a database access error occurs or this + * method is called during a transaction. + */ + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + if (readOnly) { + addWarning("cannot setReadOnly(true): read-only Connection mode not supported", "01M08"); + } + } - /** - * The SchemaResponse represents an schema modification response. - * It is issued on statements like CREATE, DROP or ALTER TABLE. - * This response keeps a field that represents the success state, as - * defined by JDBC, which is currently in MonetDB's case alwats - * SUCCESS_NO_INFO. Note that this state is not sent by the - * server.<br /> - * <tt>&3</tt> - */ - // {{{ SchemaResponse class implementation - class SchemaResponse implements Response { - public final int state = Statement.SUCCESS_NO_INFO; - - @Override - public String addLine(String line, int linetype) { - return "Header lines are not supported for a SchemaResponse"; - } - - @Override - public boolean wantsMore() { - return false; - } - - @Override - public void complete() { - // empty, because there is nothing to check - } - - @Override - public void close() { - // nothing to do here... - } - } - // }}} - - /** - * The AutoCommitResponse represents a transaction message. It - * stores (a change in) the server side auto commit mode.<br /> - * <tt>&4 (t|f)</tt> - */ - // {{{ AutoCommitResponse class implementation - class AutoCommitResponse extends SchemaResponse { - public final boolean autocommit; - - public AutoCommitResponse(boolean ac) { - // fill the blank final - this.autocommit = ac; - } - } - // }}} - - /** - * A list of Response objects. Responses are added to this list. - * Methods of this class are not synchronized. This is left as - * responsibility to the caller to prevent concurrent access. - */ - // {{{ ResponseList class implementation - class ResponseList { - /** The cache size (number of rows in a DataBlockResponse object) */ - final int cachesize; - /** The maximum number of results for this query */ - final int maxrows; - /** The ResultSet type to produce */ - final int rstype; - /** The ResultSet concurrency to produce */ - final int rsconcur; - /** The sequence number of this ResponseList */ - final int seqnr; - /** A list of the Responses associated with the query, - * in the right order */ - private List<Response> responses; - /** A map of ResultSetResponses, used for additional - * DataBlockResponse mapping */ - private Map<Integer, ResultSetResponse> rsresponses; - - /** The current header returned by getNextResponse() */ - private int curResponse; + /** + * Creates an unnamed savepoint in the current transaction and + * returns the new Savepoint object that represents it. + * + * @return the new Savepoint object + * @throws SQLException if a database access error occurs or this Connection + * object is currently in auto-commit mode + */ + @Override + public Savepoint setSavepoint() throws SQLException { + // create a new Savepoint object + MonetSavepoint sp = new MonetSavepoint(); + // note: can't use sendIndependentCommand here because we need + // to process the auto_commit state the server gives + // create a container for the result + ResponseList l = new ResponseList(0, 0, ResultSet.FETCH_FORWARD, ResultSet.CONCUR_READ_ONLY); + // send the appropriate query string to the database + try { + l.processQuery("SAVEPOINT " + sp.getName()); + } finally { + l.close(); + } + return sp; + } - /** - * Main constructor. The query argument can either be a String - * or List. An SQLException is thrown if another object - * instance is supplied. - * - * @param cachesize overall cachesize to use - * @param maxrows maximum number of rows to allow in the set - * @param rstype the type of result sets to produce - * @param rsconcur the concurrency of result sets to produce - */ - ResponseList( - int cachesize, - int maxrows, - int rstype, - int rsconcur - ) throws SQLException { - this.cachesize = cachesize; - this.maxrows = maxrows; - this.rstype = rstype; - this.rsconcur = rsconcur; - responses = new ArrayList<>(); - curResponse = -1; - seqnr = MonetConnection.seqCounter++; - } - - /** - * Retrieves the next available response, or null if there are - * no more responses. - * - * @return the next Response available or null - */ - Response getNextResponse() throws SQLException { - if (rstype == ResultSet.TYPE_FORWARD_ONLY) { - // free resources if we're running forward only - if (curResponse >= 0 && curResponse < responses.size()) { - Response tmp = responses.get(curResponse); - if (tmp != null) tmp.close(); - responses.set(curResponse, null); - } - } - curResponse++; - if (curResponse >= responses.size()) { - // ResponseList is obviously completed so, there are no - // more responses - return null; - } else { - // return this response - return responses.get(curResponse); - } - } - - /** - * Closes the Reponse at index i, if not null. - * - * @param i the index position of the header to close - */ - void closeResponse(int i) { - if (i < 0 || i >= responses.size()) return; - Response tmp = responses.set(i, null); - if (tmp != null) - tmp.close(); - } - - /** - * Closes the current response. - */ - void closeCurrentResponse() { - closeResponse(curResponse); - } - - /** - * Closes the current and previous responses. - */ - void closeCurOldResponses() { - for (int i = curResponse; i >= 0; i--) { - closeResponse(i); - } - } + /** + * Creates a savepoint with the given name in the current + * transaction and returns the new Savepoint object that represents + * it. + * + * @param name a String containing the name of the savepoint + * @return the new Savepoint object + * @throws SQLException if a database access error occurs or this Connection + * object is currently in auto-commit mode + */ + @Override + public Savepoint setSavepoint(String name) throws SQLException { + // create a new Savepoint object + MonetSavepoint sp; + try { + sp = new MonetSavepoint(name); + } catch (IllegalArgumentException e) { + throw new SQLException(e.getMessage(), "M0M03"); + } + // note: can't use sendIndependentCommand here because we need + // to process the auto_commit state the server gives + // create a container for the result + ResponseList l = new ResponseList(0, 0, ResultSet.FETCH_FORWARD, ResultSet.CONCUR_READ_ONLY); + // send the appropriate query string to the database + try { + l.processQuery("SAVEPOINT " + sp.getName()); + } finally { + l.close(); + } + return sp; + } - /** - * Closes this ResponseList by closing all the Responses in this - * ResponseList. - */ - void close() { - for (int i = 0; i < responses.size(); i++) { - closeResponse(i); - } - } - - /** - * Returns whether this ResponseList has still unclosed - * Responses. - */ - boolean hasUnclosedResponses() { - for (Response r : responses) { - if (r != null) - return true; - } - return false; - } - - /** - * Executes the query contained in this ResponseList, and - * stores the Responses resulting from this query in this - * ResponseList. - * - * @throws SQLException if a database error occurs - */ - void processQuery(String query) throws SQLException { - executeQuery(server.getQueryHeaderTemplates(), query); - } - - /** - * Internal executor of queries. - * - * @param templ the template to fill in - * @param query the query to execute - * @throws SQLException if a database error occurs - */ - @SuppressWarnings("fallthrough") - void executeQuery(String[] templ, String query) - throws SQLException - { - boolean sendThreadInUse = false; - String error = null; + /** + * Attempts to change the transaction isolation level for this + * Connection object to the one given. The constants defined in the + * interface Connection are the possible transaction isolation + * levels. + * + * @param level one of the following Connection constants: + * Connection.TRANSACTION_READ_UNCOMMITTED, + * Connection.TRANSACTION_READ_COMMITTED, + * Connection.TRANSACTION_REPEATABLE_READ, or + * Connection.TRANSACTION_SERIALIZABLE. + */ + @Override + public void setTransactionIsolation(int level) { + if (level != TRANSACTION_SERIALIZABLE) { + addWarning("MonetDB only supports fully serializable " + "transactions, continuing with transaction level " + + "raised to TRANSACTION_SERIALIZABLE", "01M09"); + } + } - try { - synchronized (server) { - // make sure we're ready to send query; read data till we - // have the prompt it is possible (and most likely) that we - // already have the prompt and do not have to skip any - // lines. Ignore errors from previous result sets. - in.waitForPrompt(); - - // {{{ set reply size - /** - * Change the reply size of the server. If the given - * value is the same as the current value known to use, - * then ignore this call. If it is set to 0 we get a - * prompt after the server sent it's header. - */ - int size = cachesize == 0 ? DEF_FETCHSIZE : cachesize; - size = maxrows != 0 ? Math.min(maxrows, size) : size; - // don't do work if it's not needed - if (server.getLang() == AbstractMonetDBConnection.LANG_SQL && size != curReplySize && templ != server.getCommandHeaderTemplates()) { - sendControlCommand("reply_size " + size); + /** + * Installs the given TypeMap object as the type map for this + * Connection object. The type map will be used for the custom + * mapping of SQL structured types and distinct types. + * + * @param map the java.util.Map object to install as the replacement for + * this Connection object's default type map + */ + @Override + public void setTypeMap(Map<String, Class<?>> map) { + typeMap = map; + } - // store the reply size after a successful change - curReplySize = size; - } - // }}} set reply size + /** + * Returns a string identifying this Connection to the MonetDB + * server. + * + * @return a String representing this Object + */ + @Override + public String toString() { + return "MonetDB Connection (" + this.getJDBCURL() + ") " + (closed ? "connected" : "disconnected"); + } + + //== 1.7 methods (JDBC 4.1) - // If the query is larger than the TCP buffer size, use a - // special send thread to avoid deadlock with the server due - // to blocking behaviour when the buffer is full. Because - // the server will be writing back results to us, it will - // eventually block as well when its TCP buffer gets full, - // as we are blocking an not consuming from it. The result - // is a state where both client and server want to write, - // but block. - if (query.length() > server.getBlockSize()) { - // get a reference to the send thread - if (sendThread == null) - sendThread = new SendThread(out); - // tell it to do some work! - sendThread.runQuery(templ, query); - sendThreadInUse = true; - } else { - // this is a simple call, which is a lot cheaper and will - // always succeed for small queries. - out.writeLine((templ[0] == null ? "" : templ[0] + query + templ[1] == null ? "" : templ[1])); - } + /** + * Sets the given schema name to access. + * + * @param schema the name of a schema in which to work + * @throws SQLException if a database access error occurs or this + * method is called on a closed connection + */ + @Override + public void setSchema(String schema) throws SQLException { + if (closed) { + throw new SQLException("Cannot call on closed Connection", "M1M20"); + } + if (schema != null) { + createStatement().execute("SET SCHEMA \"" + schema + "\""); + } + } - // go for new results - String tmpLine = in.readLine(); - int linetype = in.getLineType(); - Response res = null; - while (linetype != AbstractMCLReader.PROMPT) { - // each response should start with a start of header - // (or error) - switch (linetype) { - case AbstractMCLReader.SOHEADER: - // make the response object, and fill it - try { - switch (sohp.parse(tmpLine)) { - case StartOfHeaderParser.Q_PARSE: - throw new MCLParseException("Q_PARSE header not allowed here", 1); - case StartOfHeaderParser.Q_TABLE: - case StartOfHeaderParser.Q_PREPARE: { - int id = sohp.getNextAsInt(); - int tuplecount = sohp.getNextAsInt(); - int columncount = sohp.getNextAsInt(); - int rowcount = sohp.getNextAsInt(); - // enforce the maxrows setting - if (maxrows != 0 && tuplecount > maxrows) - tuplecount = maxrows; - res = new ResultSetResponse( - id, - tuplecount, - columncount, - rowcount, - this, - seqnr - ); - // only add this resultset to - // the hashmap if it can possibly - // have an additional datablock - if (rowcount < tuplecount) { - if (rsresponses == null) - rsresponses = new HashMap<>(); - rsresponses.put( - id, - (ResultSetResponse) res - ); - } - } break; - case StartOfHeaderParser.Q_UPDATE: - res = new UpdateResponse( - sohp.getNextAsInt(), // count - sohp.getNextAsString() // key-id - ); - break; - case StartOfHeaderParser.Q_SCHEMA: - res = new SchemaResponse(); - break; - case StartOfHeaderParser.Q_TRANS: - boolean ac = sohp.getNextAsString().equals("t"); - if (autoCommit && ac) { - addWarning("Server enabled auto commit " + - "mode while local state " + - "already was auto commit.", "01M11" - ); - } - autoCommit = ac; - res = new AutoCommitResponse(ac); - break; - case StartOfHeaderParser.Q_BLOCK: { - // a new block of results for a - // response... - int id = sohp.getNextAsInt(); - sohp.getNextAsInt(); // columncount - int rowcount = sohp.getNextAsInt(); - int offset = sohp.getNextAsInt(); - ResultSetResponse t = - rsresponses.get(id); - if (t == null) { - error = "M0M12!no ResultSetResponse with id " + id + " found"; - break; - } - - DataBlockResponse r = - new DataBlockResponse( - rowcount, // rowcount - t.getRSType() == ResultSet.TYPE_FORWARD_ONLY - ); + /** + * Retrieves this Connection object's current schema name. + * + * @return the current schema name or null if there is none + * @throws SQLException if a database access error occurs or this + * method is called on a closed connection + */ + @Override + public String getSchema() throws SQLException { + if (closed) { + throw new SQLException("Cannot call on closed Connection", "M1M20"); + } + String cur_schema; + Statement st = createStatement(); + ResultSet rs = null; + try { + rs = st.executeQuery("SELECT CURRENT_SCHEMA"); + if (!rs.next()) + throw new SQLException("Row expected", "02000"); + cur_schema = rs.getString(1); + } finally { + if (rs != null) + rs.close(); + st.close(); + } + return cur_schema; + } - t.addDataBlockResponse(offset, r); - res = r; - } break; - } - } catch (MCLParseException e) { - error = "M0M10!error while parsing start of header:\n" + - e.getMessage() + - " found: '" + tmpLine.charAt(e.getErrorOffset()) + "'" + - " in: \"" + tmpLine + "\"" + - " at pos: " + e.getErrorOffset(); - // flush all the rest - in.waitForPrompt(); - linetype = in.getLineType(); - break; - } - - // immediately handle errors after parsing - // the header (res may be null) - if (error != null) { - in.waitForPrompt(); - linetype = in.getLineType(); - break; - } + /** + * Terminates an open connection. Calling abort results in: + * * The connection marked as closed + * * Closes any physical connection to the database + * * Releases resources used by the connection + * * Insures that any thread that is currently accessing the + * connection will either progress to completion or throw an + * SQLException. + * Calling abort marks the connection closed and releases any + * resources. Calling abort on a closed connection is a no-op. + * + * @param executor The Executor implementation which will be used by + * abort + * @throws SQLException if a database access error occurs or the + * executor is null + * @throws SecurityException if a security manager exists and + * its checkPermission method denies calling abort + */ + @Override + public void abort(Executor executor) throws SQLException { + if (closed) + return; + if (executor == null) + throw new SQLException("executor is null", "M1M05"); + // this is really the simplest thing to do, it destroys + // everything (in particular the server connection) + close(); + } - // here we have a res object, which - // we can start filling - while (res.wantsMore()) { - error = res.addLine( - in.readLine(), - in.getLineType() - ); - if (error != null) { - // right, some protocol violation, - // skip the rest of the result - error = "M0M10!" + error; - in.waitForPrompt(); - linetype = in.getLineType(); - break; - } - } - if (error != null) - break; - // it is of no use to store - // DataBlockReponses, you never want to - // retrieve them directly anyway - if (!(res instanceof DataBlockResponse)) - responses.add(res); - - // read the next line (can be prompt, new - // result, error, etc.) before we start the - // loop over - tmpLine = in.readLine(); - linetype = in.getLineType(); - break; - case AbstractMCLReader.INFO: - addWarning(tmpLine.substring(1), "01000"); - - // read the next line (can be prompt, new - // result, error, etc.) before we start the - // loop over - tmpLine = in.readLine(); - linetype = in.getLineType(); - break; - default: // Yeah... in Java this is correct! - // we have something we don't - // expect/understand, let's make it an error - // message - tmpLine = "!M0M10!protocol violation, unexpected line: " + tmpLine; - // don't break; fall through... - case AbstractMCLReader.ERROR: - // read everything till the prompt (should be - // error) we don't know if we ignore some - // garbage here... but the log should reveal - // that - error = in.waitForPrompt(); - linetype = in.getLineType(); - if (error != null) { - error = tmpLine.substring(1) + "\n" + error; - } else { - error = tmpLine.substring(1); - } - break; - } - } - } + /** + * Sets the maximum period a Connection or objects created from the + * Connection will wait for the database to reply to any one + * request. If any request remains unanswered, the waiting method + * will return with a SQLException, and the Connection or objects + * created from the Connection will be marked as closed. Any + * subsequent use of the objects, with the exception of the close, + * isClosed or Connection.isValid methods, will result in a + * SQLException. + * + * @param executor The Executor implementation which will be used by + * setNetworkTimeout + * @param millis The time in milliseconds to wait for the + * database operation to complete + * @throws SQLException if a database access error occurs, this + * method is called on a closed connection, the executor is + * null, or the value specified for seconds is less than 0. + */ + @Override + public void setNetworkTimeout(Executor executor, int millis) throws SQLException { + if (closed) { + throw new SQLException("Cannot call on closed Connection", "M1M20"); + } + if (executor == null) + throw new SQLException("executor is null", "M1M05"); + if (millis < 0) + throw new SQLException("milliseconds is less than zero", "M1M05"); + try { + this.setSoTimeout(millis); + } catch (SocketException e) { + throw new SQLException(e.getMessage(), "08000"); + } + } - // if we used the sendThread, make sure it has finished - if (sendThreadInUse) { - String tmp = sendThread.getErrors(); - if (tmp != null) { - if (error == null) { - error = "08000!" + tmp; - } else { - error += "\n08000!" + tmp; - } - } - } - if (error != null) { - SQLException ret = null; - String[] errors = error.split("\n"); - for (String error1 : errors) { - if (ret == null) { - ret = new SQLException(error1.substring(6), - error1.substring(0, 5)); - } else { - ret.setNextException(new SQLException( - error1.substring(6), - error1.substring(0, 5))); - } - } - throw ret; - } - } catch (SocketTimeoutException e) { - close(); // JDBC 4.1 semantics, abort() - throw new SQLException("connection timed out", "08M33"); - } catch (IOException e) { - closed = true; - throw new SQLException(e.getMessage() + " (mserver still alive?)", "08000"); - } - } - } - // }}} + /** + * Retrieves the number of milliseconds the driver will wait for a + * database request to complete. If the limit is exceeded, a + * SQLException is thrown. + * + * @return the current timeout limit in milliseconds; zero means + * there is no limit + * @throws SQLException if a database access error occurs or + * this method is called on a closed Connection + */ + @Override + public int getNetworkTimeout() throws SQLException { + if (closed) { + throw new SQLException("Cannot call on closed Connection", "M1M20"); + } + try { + return this.getSoTimeout(); + } catch (SocketException e) { + throw new SQLException(e.getMessage(), "08000"); + } + } - /** - * A thread to send a query to the server. When sending large - * amounts of data to a server, the output buffer of the underlying - * communication socket may overflow. In such case the sending - * process blocks. In order to prevent deadlock, it might be - * desirable that the driver as a whole does not block. This thread - * facilitates the prevention of such 'full block', because this - * separate thread only will block.<br /> - * This thread is designed for reuse, as thread creation costs are - * high. - */ - // {{{ SendThread class implementation - static class SendThread extends Thread { - /** The state WAIT represents this thread to be waiting for - * something to do */ - private final static int WAIT = 0; - /** The state QUERY represents this thread to be executing a query */ - private final static int QUERY = 1; - /** The state SHUTDOWN is the final state that ends this thread */ - private final static int SHUTDOWN = -1; + //== end methods of interface Connection - private String[] templ; - private String query; - private AbstractMCLWriter out; - private String error; - private int state = WAIT; - - final Lock sendLock = new ReentrantLock(); - final Condition queryAvailable = sendLock.newCondition(); - final Condition waiting = sendLock.newCondition(); - - /** - * Constructor which immediately starts this thread and sets it - * into daemon mode. - * - * @param out the socket to write to - */ - public SendThread(AbstractMCLWriter out) { - super("SendThread"); - setDaemon(true); - this.out = out; - start(); - } + /** + * Returns whether the BLOB type should be mapped to BINARY type. + */ + public boolean getBlobAsBinary() { + return blobIsBinary; + } - @Override - public void run() { - sendLock.lock(); - try { - while (true) { - while (state == WAIT) { - try { - queryAvailable.await(); - } catch (InterruptedException e) { - // woken up, eh? - } - } - if (state == SHUTDOWN) - break; - - // state is QUERY here - try { - out.writeLine((templ[0] == null ? "" : templ[0]) + query + (templ[1] == null ? "" : templ[1])); - } catch (IOException e) { - error = e.getMessage(); - } - - // update our state, and notify, maybe someone is waiting - // for us in throwErrors - state = WAIT; - waiting.signal(); - } - } finally { - sendLock.unlock(); - } - } - - /** - * Starts sending the given query over the given socket. Beware - * that the thread should be finished (can be assured by calling - * throwErrors()) before this method is called! - * - * @param templ the query template - * @param query the query itself - * @throws SQLException if this SendThread is already in use - */ - public void runQuery(String[] templ, String query) throws SQLException { - sendLock.lock(); - try { - if (state != WAIT) - throw new SQLException("SendThread already in use or shutting down!", "M0M03"); + /** + * Sends the given string to MonetDB as regular statement, making + * sure there is a prompt after the command is sent. All possible + * returned information is discarded. Encountered errors are + * reported. + * + * @param command the exact string to send to MonetDB + * @throws SQLException if an IO exception or a database error occurs + */ + public void sendIndependentCommand(String command) throws SQLException { + synchronized (this) { + try { + out.writeLine(server.getQueryTemplateHeader(0) + command + server.getQueryTemplateHeader(1)); + String error = in.waitForPrompt(); + if (error != null) + throw new SQLException(error.substring(6), + error.substring(0, 5)); + } catch (SocketTimeoutException e) { + close(); // JDBC 4.1 semantics: abort() + throw new SQLException("connection timed out", "08M33"); + } catch (IOException e) { + throw new SQLException(e.getMessage(), "08000"); + } + } + } - this.templ = templ; - this.query = query; - - // let the thread know there is some work to do - state = QUERY; - queryAvailable.signal(); - } finally { - sendLock.unlock(); - } - } + /** + * Sends the given string to MonetDB as control statement, making + * sure there is a prompt after the command is sent. All possible + * returned information is discarded. Encountered errors are + * reported. + * + * @param command the exact string to send to MonetDB + * @throws SQLException if an IO exception or a database error occurs + */ + public void sendControlCommand(String command) throws SQLException { + // send X command + synchronized (this) { + try { + out.writeLine(server.getCommandTemplateHeader(0) + command + server.getCommandTemplateHeader(1)); + String error = in.waitForPrompt(); + if (error != null) + throw new SQLException(error.substring(6), + error.substring(0, 5)); + } catch (SocketTimeoutException e) { + close(); // JDBC 4.1 semantics, abort() + throw new SQLException("connection timed out", "08M33"); + } catch (IOException e) { + throw new SQLException(e.getMessage(), "08000"); + } + } + } - /** - * Returns errors encountered during the sending process. - * - * @return the errors or null if none - */ - public String getErrors() { - sendLock.lock(); - try { - // make sure the thread is in WAIT state, not QUERY - while (state == QUERY) { - try { - waiting.await(); - } catch (InterruptedException e) { - // just try again - } - } - if (state == SHUTDOWN) - error = "SendThread is shutting down"; - } finally { - sendLock.unlock(); - } - return error; - } - - /** - * Requests this SendThread to stop. - */ - public void shutdown() { - sendLock.lock(); - state = SHUTDOWN; - sendLock.unlock(); - this.interrupt(); // break any wait conditions - } - } - // }}} + /** + * Adds a warning to the pile of warnings this Connection object + * has. If there were no warnings (or clearWarnings was called) + * this warning will be the first, otherwise this warning will get + * appended to the current warning. + * + * @param reason the warning message + */ + public void addWarning(String reason, String sqlstate) { + if (warnings == null) { + warnings = new SQLWarning(reason, sqlstate); + } else { + warnings.setNextWarning(new SQLWarning(reason, sqlstate)); + } + } } -
--- a/src/main/java/nl/cwi/monetdb/jdbc/MonetStatement.java +++ b/src/main/java/nl/cwi/monetdb/jdbc/MonetStatement.java @@ -8,7 +8,6 @@ package nl.cwi.monetdb.jdbc; -import nl.cwi.monetdb.mcl.connection.AbstractMonetDBConnection; import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.Statement; @@ -202,7 +201,7 @@ public class MonetStatement extends Mone boolean first = true; boolean error = false; - AbstractMonetDBConnection server = connection.getServer(); + MonetConnection server = connection.getServer(); BatchUpdateException e = new BatchUpdateException("Error(s) occurred while executing the batch, see next SQLExceptions for details", "22000", counts); StringBuilder tmpBatch = new StringBuilder(server.getBlockSize());
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/connection/AbstractMonetDBConnection.java +++ /dev/null @@ -1,204 +0,0 @@ -package nl.cwi.monetdb.mcl.connection; - -import nl.cwi.monetdb.mcl.MCLException; -import nl.cwi.monetdb.mcl.parser.MCLParseException; -import nl.cwi.monetdb.mcl.protocol.AbstractProtocolParser; - -import java.io.*; -import java.net.SocketException; -import java.util.List; - -/** - * Created by ferreira on 11/23/16. - */ -public abstract class AbstractMonetDBConnection { - - /** The language to connect with */ - protected MonetDBLanguage currentMonetDBLanguage = MonetDBLanguage.LANG_SQL; - /** The database to connect to */ - protected final String database; - /** Authentication hash method */ - protected final String hash; - /** Whether we are debugging or not */ - protected boolean debug; - /** The Writer for the debug log-file */ - protected Writer log; - - public AbstractMonetDBConnection(String database, String hash, boolean debug, MonetDBLanguage lang) throws IOException { - this.database = database; - this.hash = hash; - this.debug = debug; - this.currentMonetDBLanguage = lang; - } - - public String getDatabase() { - return database; - } - - public String getHash() { - return hash; - } - - public boolean isDebug() { - return debug; - } - - /** - * Enables/disables debug - * - * @param debug Value to set - */ - public void setDebug(boolean debug) { - this.debug = debug; - } - - public MonetDBLanguage getCurrentMonetDBLanguage() { - return currentMonetDBLanguage; - } - - public void setCurrentMonetDBLanguage(MonetDBLanguage currentMonetDBLanguage) { - this.currentMonetDBLanguage = currentMonetDBLanguage; - } - - /** - * Connects to the given host and port, logging in as the given - * user. If followRedirect is false, a RedirectionException is - * thrown when a redirect is encountered. - * - * @return A List with informational (warning) messages. If this - * list is empty; then there are no warnings. - * @throws IOException if an I/O error occurs when creating the - * socket - * @throws MCLParseException if bogus data is received - * @throws MCLException if an MCL related error occurs - */ - public abstract List<String> connect(String user, String pass) throws IOException, MCLParseException, MCLException; - - /** - * Enables logging to a file what is read and written from and to - * the server. Logging can be enabled at any time. However, it is - * encouraged to start debugging before actually connecting the - * socket. - * - * @param filename the name of the file to write to - * @throws IOException if the file could not be opened for writing - */ - public void debug(String filename) throws IOException { - debug(new FileWriter(filename)); - } - - /** - * Enables logging to a stream what is read and written from and to - * the server. Logging can be enabled at any time. However, it is - * encouraged to start debugging before actually connecting the - * socket. - * - * @param out to write the log to - * @throws IOException if the file could not be opened for writing - */ - public void debug(PrintStream out) throws IOException { - debug(new PrintWriter(out)); - } - - /** - * Enables logging to a stream what is read and written from and to - * the server. Logging can be enabled at any time. However, it is - * encouraged to start debugging before actually connecting the - * socket. - * - * @param out to write the log to - * @throws IOException if the file could not be opened for writing - */ - public void debug(Writer out) throws IOException { - log = out; - debug = true; - } - - /** - * Writes a logline tagged with a timestamp using the given string. - * Used for debugging purposes only and represents a message that is - * connected to writing to the socket. A logline might look like: - * TX 152545124: Hello MonetDB! - * - * @param message the message to log - * @throws IOException if an IO error occurs while writing to the logfile - */ - private void logTx(String message) throws IOException { - log.write("TX " + System.currentTimeMillis() + - ": " + message + "\n"); - } - - /** - * Writes a logline tagged with a timestamp using the given string. - * Lines written using this log method are tagged as "added - * metadata" which is not strictly part of the data sent. - * - * @param message the message to log - * @throws IOException if an IO error occurs while writing to the logfile - */ - private void logTd(String message) throws IOException { - log.write("TD " + System.currentTimeMillis() + - ": " + message + "\n"); - } - - /** - * Writes a logline tagged with a timestamp using the given string, - * and flushes afterwards. Used for debugging purposes only and - * represents a message that is connected to reading from the - * socket. The log is flushed after writing the line. A logline - * might look like: - * RX 152545124: Hi JDBC! - * - * @param message the message to log - * @throws IOException if an IO error occurs while writing to the logfile - */ - private void logRx(String message) throws IOException { - log.write("RX " + System.currentTimeMillis() + - ": " + message + "\n"); - log.flush(); - } - - /** - * Writes a logline tagged with a timestamp using the given string, - * and flushes afterwards. Lines written using this log method are - * tagged as "added metadata" which is not strictly part of the data - * received. - * - * @param message the message to log - * @throws IOException if an IO error occurs while writing to the logfile - */ - private void logRd(String message) throws IOException { - log.write("RD " + System.currentTimeMillis() + - ": " + message + "\n"); - log.flush(); - } - - public synchronized void close() { - try { - if (debug && log instanceof FileWriter) log.close(); - } catch (IOException e) { - // ignore it - } - } - - /** - * Destructor called by garbage collector before destroying this - * object tries to disconnect the MonetDB connection if it has not - * been disconnected already. - */ - @Override - protected void finalize() throws Throwable { - this.close(); - super.finalize(); - } - - public abstract String getJDBCURL(); - - public abstract int getBlockSize(); - - public abstract int getSoTimeout() throws SocketException; - - public abstract void setSoTimeout(int s) throws SocketException; - - public abstract AbstractProtocolParser getUnderlyingProtocol(); -}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/Debugger.java @@ -0,0 +1,111 @@ +package nl.cwi.monetdb.mcl.connection; + +import java.io.*; + +/** + * Created by ferreira on 12/1/16. + */ +public class Debugger implements Closeable { + + /** The Writer for the debug log-file */ + private final Writer log; + + /** + * Enables logging to a stream what is read and written from and to + * the server. Logging can be enabled at any time. However, it is + * encouraged to start debugging before actually connecting the + * socket. + * + * @param log to write the log to + */ + public Debugger(Writer log) { + this.log = log; + } + + /** + * Enables logging to a file what is read and written from and to + * the server. Logging can be enabled at any time. However, it is + * encouraged to start debugging before actually connecting the + * socket. + * + * @param filename the name of the file to write to + * @throws IOException if the file could not be opened for writing + */ + public Debugger(String filename) throws IOException { + this.log = new FileWriter(filename); + } + + /** + * Enables logging to a stream what is read and written from and to + * the server. Logging can be enabled at any time. However, it is + * encouraged to start debugging before actually connecting the + * socket. + * + * @param out to write the log to + */ + public Debugger(PrintStream out) { + this.log = new PrintWriter(out); + } + + /** + * Writes a logline tagged with a timestamp using the given string. + * Used for debugging purposes only and represents a message that is + * connected to writing to the socket. A logline might look like: + * TX 152545124: Hello MonetDB! + * + * @param message the message to log + * @throws IOException if an IO error occurs while writing to the logfile + */ + private void logTx(String message) throws IOException { + log.write("TX " + System.currentTimeMillis() + ": " + message + "\n"); + } + + /** + * Writes a logline tagged with a timestamp using the given string. + * Lines written using this log method are tagged as "added + * metadata" which is not strictly part of the data sent. + * + * @param message the message to log + * @throws IOException if an IO error occurs while writing to the logfile + */ + private void logTd(String message) throws IOException { + log.write("TD " + System.currentTimeMillis() + ": " + message + "\n"); + } + + /** + * Writes a logline tagged with a timestamp using the given string, + * and flushes afterwards. Used for debugging purposes only and + * represents a message that is connected to reading from the + * socket. The log is flushed after writing the line. A logline + * might look like: + * RX 152545124: Hi JDBC! + * + * @param message the message to log + * @throws IOException if an IO error occurs while writing to the logfile + */ + private void logRx(String message) throws IOException { + log.write("RX " + System.currentTimeMillis() + ": " + message + "\n"); + log.flush(); + } + + /** + * Writes a logline tagged with a timestamp using the given string, + * and flushes afterwards. Lines written using this log method are + * tagged as "added metadata" which is not strictly part of the data + * received. + * + * @param message the message to log + * @throws IOException if an IO error occurs while writing to the logfile + */ + private void logRd(String message) throws IOException { + log.write("RD " + System.currentTimeMillis() + ": " + message + "\n"); + log.flush(); + } + + @Override + public void close() throws IOException { + if (log instanceof FileWriter) { + log.close(); + } + } +}
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/connection/DeleteMe.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - * - * Copyright 1997 - July 2008 CWI, August 2008 - 2016 MonetDB B.V. - */ - -package nl.cwi.monetdb.mcl.connection; - -import java.io.IOException; - - -public final class DeleteMe extends MapiConnection { - - - public DeleteMe(String database, boolean debug, MonetDBLanguage lang, String hostname, int port) throws IOException { - super(database, debug, lang, hostname, port, 9); - } - -}
--- a/src/main/java/nl/cwi/monetdb/mcl/connection/EmbeddedMonetDB.java +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/EmbeddedMonetDB.java @@ -2,7 +2,7 @@ package nl.cwi.monetdb.mcl.connection; import nl.cwi.monetdb.embedded.env.MonetDBEmbeddedDatabase; import nl.cwi.monetdb.embedded.env.MonetDBEmbeddedException; -import nl.cwi.monetdb.mcl.MCLException; +import nl.cwi.monetdb.jdbc.MonetConnection; import nl.cwi.monetdb.mcl.io.*; import nl.cwi.monetdb.mcl.parser.MCLParseException; @@ -13,21 +13,17 @@ import java.util.List; /** * Created by ferreira on 11/23/16. */ -public final class EmbeddedMonetDB extends AbstractMonetDBConnection { +public final class EmbeddedMonetDB extends MonetConnection { private final String directory; private InternalConnection connection; - public EmbeddedMonetDB(String database, String hash, boolean debug, MonetDBLanguage lang, String directory) throws IOException { - super(database, hash, debug, lang); + public EmbeddedMonetDB(String database, String hash, String language, boolean blobIsBinary, boolean isDebugging, String directory) throws IOException { + super(database, hash, language, blobIsBinary, isDebugging); this.directory = directory; } - public String getDirectory() { - return directory; - } - @Override public List<String> connect(String user, String pass) throws IOException, MCLParseException, MCLException { try { @@ -44,18 +40,8 @@ public final class EmbeddedMonetDB exten } @Override - public synchronized void close() { - super.close(); - try { - MonetDBEmbeddedDatabase.StopDatabase(); - } catch (MonetDBEmbeddedException e) { - // ignore it - } - } - - @Override public String getJDBCURL() { - return "jdbc:monetdb://localhost@" + this.getDirectory() + "/" + this.getDatabase(); + return "jdbc:monetdb://localhost@" + this.directory + "/" + this.database; } @Override @@ -65,11 +51,20 @@ public final class EmbeddedMonetDB exten @Override public int getSoTimeout() throws SocketException { - throw new IllegalArgumentException("Cannot get a timeout on a embedded connection!"); + throw new SocketException("Cannot get a timeout on a embedded connection!"); } @Override public void setSoTimeout(int s) throws SocketException { - throw new IllegalArgumentException("Cannot set a timeout on a embedded connection!"); + throw new SocketException("Cannot set a timeout on a embedded connection!"); + } + + @Override + public void closeUnderlyingConnection() throws IOException { + try { + MonetDBEmbeddedDatabase.StopDatabase(); + } catch (MonetDBEmbeddedException e) { + // ignore it + } } }
rename from src/main/java/nl/cwi/monetdb/mcl/MCLException.java rename to src/main/java/nl/cwi/monetdb/mcl/connection/MCLException.java --- a/src/main/java/nl/cwi/monetdb/mcl/MCLException.java +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/MCLException.java @@ -6,7 +6,7 @@ * Copyright 1997 - July 2008 CWI, August 2008 - 2016 MonetDB B.V. */ -package nl.cwi.monetdb.mcl; +package nl.cwi.monetdb.mcl.connection; /** * A general purpose Exception class for MCL related problems. This
--- a/src/main/java/nl/cwi/monetdb/mcl/connection/MapiConnection.java +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/MapiConnection.java @@ -1,22 +1,18 @@ package nl.cwi.monetdb.mcl.connection; -import nl.cwi.monetdb.mcl.MCLException; +import nl.cwi.monetdb.jdbc.MonetConnection; import nl.cwi.monetdb.mcl.io.BufferedMCLReader; import nl.cwi.monetdb.mcl.io.BufferedMCLWriter; import nl.cwi.monetdb.mcl.io.SocketConnection; -import nl.cwi.monetdb.mcl.io.SocketIOHandler; import nl.cwi.monetdb.mcl.parser.MCLParseException; -import nl.cwi.monetdb.mcl.protocol.AbstractProtocolParser; import nl.cwi.monetdb.mcl.protocol.ServerResponses; import nl.cwi.monetdb.mcl.protocol.oldmapi.OldMapiProtocol; +import nl.cwi.monetdb.util.ChannelSecurity; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.*; /** @@ -69,26 +65,7 @@ import java.util.*; * @see BufferedMCLReader * @see BufferedMCLWriter */ -public class MapiConnection extends AbstractMonetDBConnection { - - private static char hexChar(int n) { return (n > 9) ? (char) ('a' + (n - 10)) : (char) ('0' + n); } - - /** - * Small helper method to convert a byte string to a hexadecimal - * string representation. - * - * @param digest the byte array to convert - * @return the byte array as hexadecimal string - */ - private static String toHex(byte[] digest) { - char[] result = new char[digest.length * 2]; - int pos = 0; - for (byte aDigest : digest) { - result[pos++] = hexChar((aDigest & 0xf0) >> 4); - result[pos++] = hexChar(aDigest & 0x0f); - } - return new String(result); - } +public class MapiConnection extends MonetConnection { /** The hostname to connect to */ protected final String hostname; @@ -103,20 +80,12 @@ public class MapiConnection extends Abst protected OldMapiProtocol protocol; - public MapiConnection(String database, String hash, boolean debug, MonetDBLanguage lang, String hostname, int port) throws IOException { - super(database, hash, debug, lang); + public MapiConnection(String database, String hash, String language, boolean blobIsBinary, boolean isDebugging, String hostname, int port) throws IOException { + super(database, hash, language, blobIsBinary, isDebugging); this.hostname = hostname; this.port = port; } - public String getHostname() { - return hostname; - } - - public int getPort() { - return port; - } - /** * Sets whether MCL redirections should be followed or not. If set * to false, an MCLException will be thrown when a redirect is @@ -156,76 +125,82 @@ public class MapiConnection extends Abst } @Override + public int getBlockSize() { + return protocol.getConnection().getBlockSize(); + } + + @Override + public int getSoTimeout() throws SocketException { + return protocol.getConnection().getSoTimeout(); + } + + @Override + public void setSoTimeout(int s) throws SocketException { + protocol.getConnection().setSoTimeout(s); + } + + @Override + public void closeUnderlyingConnection() throws IOException { + protocol.getConnection().close(); + } + + @Override public String getJDBCURL() { String language = ""; if (this.getCurrentMonetDBLanguage() == MonetDBLanguage.LANG_MAL) language = "?language=mal"; - return "jdbc:monetdb://" + this.getHostname() + ":" + this.getPort() + "/" + this.getDatabase() + language; + return "jdbc:monetdb://" + this.hostname + ":" + this.port + "/" + this.database + language; } @Override - public void close() { - super.close(); - try { - protocol.getHandler().getConnection().close(); - } catch (IOException e) { - // ignore it - } - } - - public int getBlockSize() { - return protocol.getHandler().getConnection().getBlockSize(); + public List<String> connect(String user, String pass) throws IOException, MCLParseException, MCLException { + // Wrap around the internal connect that needs to know if it + // should really make a TCP connection or not. + List<String> res = connect(this.hostname, this.port, user, pass, true); + // apply NetworkTimeout value from legacy (pre 4.1) driver + // so_timeout calls + this.setSoTimeout(this.getSoTimeout()); + return res; } - public int getSoTimeout() throws SocketException { - return protocol.getHandler().getConnection().getSoTimeout(); - } - - public void setSoTimeout(int s) throws SocketException { - protocol.getHandler().getConnection().setSoTimeout(s); - } - - @Override - public AbstractProtocolParser getUnderlyingProtocol() { - return protocol; - } - - private List<String> connect(String user, String pass, boolean makeConnection) throws IOException, MCLParseException, MCLException { + private List<String> connect(String host, int port, String user, String pass, boolean makeConnection) throws IOException, MCLParseException, MCLException { if (ttl-- <= 0) throw new MCLException("Maximum number of redirects reached, aborting connection attempt. Sorry."); if (makeConnection) { this.protocol = new OldMapiProtocol(new SocketConnection(this.hostname, this.port)); + this.protocol.getConnection().setTcpNoDelay(true); + // set nodelay, as it greatly speeds up small messages (like we // often do) - this.protocol.getHandler().getConnection().setTcpNoDelay(true); - //TODO writer.registerReader(reader); + //TODO writer.registerReader(reader); ?? } - ServerResponses nextResponse; - - String test = getChallengeResponse(user, pass, language, database, hash); + this.protocol.getNextResponseHeader(); + String test = this.getChallengeResponse(this.protocol.getEntireResponseLine(), user, pass, + this.currentMonetDBLanguage.getRepresentation(), this.database, this.hash); + this.protocol.writeNextLine(test.getBytes()); - writer.writeLine(); - - // read monet response till prompt List<String> redirects = new ArrayList<>(); List<String> warns = new ArrayList<>(); - String err = "", tmp; - int lineType; + String err = ""; + ServerResponses next; + do { - if ((tmp = reader.readLine()) == null) - throw new IOException("Read from " + this.getHostname() + ":" + this.getPort() + ": End of stream reached"); - if ((lineType = reader.getLineType()) == BufferedMCLReader.ERROR) { - err += "\n" + tmp.substring(7); - } else if (lineType == BufferedMCLReader.INFO) { - warns.add(tmp.substring(1)); - } else if (lineType == BufferedMCLReader.REDIRECT) { - redirects.add(tmp.substring(1)); + next = this.protocol.getNextResponseHeader(); + switch (next) { + case ERROR: + err += "\n" + this.protocol.getRemainingResponseLine(0); + break; + case INFO: + warns.add(this.protocol.getRemainingResponseLine(0)); + case REDIRECT: + redirects.add(this.protocol.getRemainingResponseLine(0)); } - } while (lineType != BufferedMCLReader.PROMPT); + } while (next != ServerResponses.PROMPT); + if (!err.equals("")) { - close(); + this.close(); throw new MCLException(err.trim()); } if (!redirects.isEmpty()) { @@ -250,7 +225,7 @@ public class MapiConnection extends Abst throw new MCLParseException(e.toString()); } - tmp = u.getQuery(); + String tmp = u.getQuery(); if (tmp != null) { String args[] = tmp.split("&"); for (String arg : args) { @@ -261,15 +236,14 @@ public class MapiConnection extends Abst case "database": tmp = arg.substring(pos + 1); if (!tmp.equals(database)) { - warns.add("redirect points to different " + - "database: " + tmp); - setDatabase(tmp); + warns.add("redirect points to different " + "database: " + tmp); + this.database = tmp; } break; case "language": tmp = arg.substring(pos + 1); warns.add("redirect specifies use of different language: " + tmp); - setLanguage(tmp); + this.currentMonetDBLanguage = MonetDBLanguage.GetLanguageFromString(tmp); break; case "user": tmp = arg.substring(pos + 1); @@ -296,20 +270,19 @@ public class MapiConnection extends Abst // this is a redirect to another (monetdb) server, // which means a full reconnect // avoid the debug log being closed - if (debug) { - debug = false; - close(); - debug = true; + if (this.isDebugging) { + this.isDebugging = false; + this.close(); + this.isDebugging = true; } else { - close(); + this.close(); } tmp = u.getPath(); if (tmp != null && tmp.length() != 0) { tmp = tmp.substring(1).trim(); if (!tmp.isEmpty() && !tmp.equals(database)) { - warns.add("redirect points to different " + - "database: " + tmp); - setDatabase(tmp); + warns.add("redirect points to different " + "database: " + tmp); + this.database = tmp; } } int p = u.getPort(); @@ -335,17 +308,6 @@ public class MapiConnection extends Abst return warns; } - @Override - public List<String> connect(String user, String pass) throws IOException, MCLParseException, MCLException { - // Wrap around the internal connect that needs to know if it - // should really make a TCP connection or not. - List<String> res = connect(user, pass, true); - // apply NetworkTimeout value from legacy (pre 4.1) driver - // so_timeout calls - this.setSoTimeout(this.getSoTimeout()); - return res; - } - /** * A little helper function that processes a challenge string, and * returns a response string for the server. If the challenge @@ -359,7 +321,7 @@ public class MapiConnection extends Abst * @param hash the hash method(s) to use, or NULL for all supported * hashes */ - private String getChallengeResponse(String username, String password, String language, String database, String hash) + private String getChallengeResponse(String chalstr, String username, String password, String language, String database, String hash) throws MCLParseException, MCLException, IOException { String response; String algo; @@ -409,14 +371,7 @@ public class MapiConnection extends Abst throw new MCLException("Unsupported password hash: " + chaltok[5]); } - try { - MessageDigest md = MessageDigest.getInstance(algo); - md.update(password.getBytes("UTF-8")); - byte[] digest = md.digest(); - password = toHex(digest); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { - throw new AssertionError("internal error: " + e.toString()); - } + password = ChannelSecurity.DigestStrings(algo, password); // proto 7 (finally) used the challenge and works with a // password hash. The supported implementations come @@ -454,17 +409,11 @@ public class MapiConnection extends Abst algo = "MD5"; pwhash = "{MD5}"; } else { - throw new MCLException("no supported password hashes in " + hashes); + throw new MCLException("No supported password hashes in " + hashes); } - try { - MessageDigest md = MessageDigest.getInstance(algo); - md.update(password.getBytes("UTF-8")); - md.update(challenge.getBytes("UTF-8")); - byte[] digest = md.digest(); - pwhash += toHex(digest); - } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { - throw new AssertionError("internal error: " + e.toString()); - } + + pwhash += ChannelSecurity.DigestStrings(algo, password, challenge); + // TODO: some day when we need this, we should store // this switch (chaltok[4]) { @@ -486,5 +435,4 @@ public class MapiConnection extends Abst return response; } } - }
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/MonetDBConnectionFactory.java @@ -0,0 +1,152 @@ +package nl.cwi.monetdb.mcl.connection; + +import nl.cwi.monetdb.jdbc.MonetConnection; +import nl.cwi.monetdb.mcl.parser.MCLParseException; + +import java.io.File; +import java.io.IOException; +import java.net.SocketException; +import java.sql.SQLException; +import java.util.Calendar; +import java.util.List; +import java.util.Properties; + +/** + * Created by ferreira on 12/1/16. + */ +public final class MonetDBConnectionFactory { + + public static MonetConnection CreateMonetDBJDBCConnection(Properties props) throws SQLException, IllegalArgumentException { + MonetConnection res; + + boolean isEmbedded = Boolean.parseBoolean(props.getProperty("embedded", "false")); + boolean debug = Boolean.valueOf(props.getProperty("debug", "false")); + boolean blobIsBinary = Boolean.valueOf(props.getProperty("treat_blob_as_binary", "false")); + String language = props.getProperty("language", "sql"); + + String username = props.getProperty("user", null); + String password = props.getProperty("password", null); + String database = props.getProperty("database"); + if (database == null || database.trim().isEmpty()) + throw new IllegalArgumentException("database should not be null or empty"); + String hash = props.getProperty("hash"); + int sockTimeout = 0; + + //instantiate the connection + if(isEmbedded) { + String directory = props.getProperty("directory"); + if (directory == null || directory.trim().isEmpty()) + throw new IllegalArgumentException("directory should not be null or empty"); + try { + res = new EmbeddedMonetDB(database, hash, language, blobIsBinary, debug, directory); + } catch (IOException e) { + throw new SQLException(e); + } + } else { + String hostname = props.getProperty("host"); + if (hostname == null || hostname.trim().isEmpty()) + throw new IllegalArgumentException("hostname should not be null or empty"); + int port = 0; + try { + port = Integer.parseInt(props.getProperty("port")); + } catch (NumberFormatException e) { + } + if (port <= 0) + throw new IllegalArgumentException("port should not be 0 or less"); + if (username == null || username.trim().isEmpty()) + throw new IllegalArgumentException("user should not be null or empty"); + if (password == null || password.trim().isEmpty()) + throw new IllegalArgumentException("password should not be null or empty"); + + try { + res = new MapiConnection(database, hash, language, blobIsBinary, debug, hostname, port); + } catch (IOException e) { + throw new SQLException(e); + } + String timout = props.getProperty("so_timeout", "0"); + try { + sockTimeout = Integer.parseInt(timout); + } catch (NumberFormatException e) { + res.addWarning("Unable to parse socket timeout number from: " + timout, "M1M05"); + } + if (sockTimeout < 0) { + res.addWarning("Negative socket timeout not allowed. Value ignored", "M1M05"); + sockTimeout = 0; + } + try { + res.setSoTimeout(sockTimeout); + } catch (SocketException e) { + res.addWarning("The socket timeout could not be set", "M1M05"); + } + } + + //initialize the debugging stuff if so + if (debug) { + try { + String fname = props.getProperty("logfile", "monet_" + System.currentTimeMillis() + ".log"); + File f = new File(fname); + int ext = fname.lastIndexOf('.'); + if (ext < 0) ext = fname.length(); + String pre = fname.substring(0, ext); + String suf = fname.substring(ext); + for (int i = 1; f.exists(); i++) { + f = new File(pre + "-" + i + suf); + } + res.setDebugging(f.getAbsolutePath()); + } catch (IOException ex) { + throw new SQLException("Opening logfile failed: " + ex.getMessage(), "08M01"); + } + } + + try { + List<String> warnings = res.connect(username, password); + for (String warning : warnings) { + res.addWarning(warning, "01M02"); + } + + // apply NetworkTimeout value from legacy (pre 4.1) driver + // so_timeout calls + if(!isEmbedded) { + res.setSoTimeout(sockTimeout); + } + + + in = server.getReader(); + out = server.getWriter(); + + String error = in.waitForPrompt(); + if (error != null) + throw new SQLException((error.length() > 6) ? error.substring(6) : error, "08001"); + } catch (IOException e) { + throw new SQLException("Unable to connect (" + hostname + ":" + port + "): " + e.getMessage(), "08006"); + } catch (MCLParseException e) { + throw new SQLException(e.getMessage(), "08001"); + } catch (MCLException e) { + String[] connex = e.getMessage().split("\n"); + SQLException sqle = new SQLException(connex[0], "08001", e); + for (int i = 1; i < connex.length; i++) { + sqle.setNextException(new SQLException(connex[1], "08001")); + } + throw sqle; + } + + if (res.getCurrentMonetDBLanguage() == MonetDBLanguage.LANG_SQL) { + // enable auto commit + res.setAutoCommit(true); + + // set our time zone on the server + Calendar cal = Calendar.getInstance(); + int offset = cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET); + offset /= (60 * 1000); // milliseconds to minutes + String tz = offset < 0 ? "-" : "+"; + tz += (Math.abs(offset) / 60 < 10 ? "0" : "") + (Math.abs(offset) / 60) + ":"; + offset -= (offset / 60) * 60; + tz += (offset < 10 ? "0" : "") + offset; + + // TODO check this + res.sendIndependentCommand("SET TIME ZONE INTERVAL '" + tz + "' HOUR TO MINUTE"); + } + + return res; + } +}
--- a/src/main/java/nl/cwi/monetdb/mcl/connection/MonetDBLanguage.java +++ b/src/main/java/nl/cwi/monetdb/mcl/connection/MonetDBLanguage.java @@ -6,26 +6,44 @@ package nl.cwi.monetdb.mcl.connection; public enum MonetDBLanguage { /** the SQL language */ - LANG_SQL(new byte[][]{"s".getBytes(), "\n;".getBytes(), "\n;\n".getBytes()}, new byte[][]{"X".getBytes(), null, "\nX".getBytes()}), + LANG_SQL(new byte[][]{"s".getBytes(), "\n;".getBytes(), "\n;\n".getBytes()}, new byte[][]{"X".getBytes(), null, "\nX".getBytes()}, "sql"), /** the MAL language (officially *NOT* supported) */ - LANG_MAL(new byte[][]{null, ";\n".getBytes(), ";\n".getBytes()}, new byte[][]{null, null, null}), + LANG_MAL(new byte[][]{null, ";\n".getBytes(), ";\n".getBytes()}, new byte[][]{null, null, null}, "mal"), /** an unknown language */ - LANG_UNKNOWN(null, null); + LANG_UNKNOWN(null, null, "unknown"); - MonetDBLanguage(byte[][] queryTemplate, byte[][] commandTemplate) { - this.queryTemplate = queryTemplate; - this.commandTemplate = commandTemplate; + MonetDBLanguage(byte[][] queryTemplates, byte[][] commandTemplates, String representation) { + this.queryTemplates = queryTemplates; + this.commandTemplates = commandTemplates; + this.representation = representation; } - private final byte[][] queryTemplate; + private final byte[][] queryTemplates; - private final byte[][] commandTemplate; + private final byte[][] commandTemplates; + + private final String representation; public byte[] getQueryTemplateIndex(int index) { - return queryTemplate[index]; + return queryTemplates[index]; } public byte[] getCommandTemplateIndex(int index) { - return commandTemplate[index]; + return commandTemplates[index]; + } + + public String getRepresentation() { + return representation; + } + + public static MonetDBLanguage GetLanguageFromString(String language) { + switch (language) { + case "sql": + return LANG_SQL; + case "mal": + return LANG_MAL; + default: + return LANG_UNKNOWN; + } } }
--- a/src/main/java/nl/cwi/monetdb/mcl/io/SocketConnection.java +++ b/src/main/java/nl/cwi/monetdb/mcl/io/SocketConnection.java @@ -15,14 +15,38 @@ public class SocketConnection implements /** The blocksize (hardcoded in compliance with stream.mx) */ private static final int BLOCK = 8 * 1024 - 2; + private static final int CHAR_SIZE = Character.SIZE / Byte.SIZE; + + private static final int SHORT_SIZE = Short.SIZE / Byte.SIZE; + + private static final int INTEGER_SIZE = Integer.SIZE / Byte.SIZE; + + private static final int LONG_SIZE = Long.SIZE / Byte.SIZE; + + private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE; + + private static final int DOUBLE_SIZE = Double.SIZE / Byte.SIZE; + + private static final int INTERMEDIATE_BUFFER_SIZE = 1024; + + /* Local variables */ + private boolean hasFinished; + + /** Bytebuffers */ + private final ByteBuffer bufferIn = ByteBuffer.allocateDirect(INTERMEDIATE_BUFFER_SIZE); + + private final ByteBuffer bufferOut = ByteBuffer.allocateDirect(INTERMEDIATE_BUFFER_SIZE); + /** The socket channel */ - private SocketChannel connection; + private final SocketChannel connection; public SocketConnection(String hostname, int port) throws IOException { this.connection = SocketChannel.open(new InetSocketAddress(hostname, port)); this.connection.configureBlocking(true); } + /* Socket Channel methods */ + public int getSoTimeout() throws SocketException { return connection.socket().getSoTimeout(); } @@ -49,6 +73,121 @@ public class SocketConnection implements @Override public void close() throws IOException { - connection.close(); + this.hasFinished = true; + this.connection.close(); + } + + /* Byte buffer methods */ + + private void refillBufferIn() throws IOException { + bufferIn.compact(); + if(!hasFinished) { + try { + connection.read(this.bufferIn); + bufferIn.flip(); + } catch (IOException ex) { + hasFinished = true; + } + } else { + throw new IOException("Done!"); + } + } + + + public byte readNextByte() throws IOException { + if(this.bufferIn.remaining() < Byte.SIZE) { + this.refillBufferIn(); + } + return this.bufferIn.get(); + } + + public char readNextChar() throws IOException { + if(this.bufferIn.remaining() < CHAR_SIZE) { + this.refillBufferIn(); + } + return this.bufferIn.getChar(); + } + + public short readNextShort() throws IOException { + if(this.bufferIn.remaining() < SHORT_SIZE) { + this.refillBufferIn(); + } + return this.bufferIn.getShort(); + } + + public int readNextInt() throws IOException { + if(this.bufferIn.remaining() < INTEGER_SIZE) { + this.refillBufferIn(); + } + return this.bufferIn.getInt(); + } + + public long readNextLong() throws IOException { + if(this.bufferIn.remaining() < LONG_SIZE) { + this.refillBufferIn(); + } + return this.bufferIn.getLong(); + } + + public float readNextFloat() throws IOException { + if(this.bufferIn.remaining() < FLOAT_SIZE) { + this.refillBufferIn(); + } + return this.bufferIn.getFloat(); + } + + public double readNextDouble() throws IOException { + if(this.bufferIn.remaining() < DOUBLE_SIZE) { + this.refillBufferIn(); + } + return this.bufferIn.getDouble(); + } + + public int readUntilChar(StringBuilder builder, char limit) throws IOException { + builder.setLength(0); + boolean found = false; + + while(!found) { + if (this.bufferIn.remaining() < CHAR_SIZE) { + this.refillBufferIn(); + } + char next = this.bufferIn.getChar(); + builder.append(next); + if(next == limit) { + found = true; + } + } + return builder.length(); + } + + public void writeNextLine(byte[] line) throws IOException { + bufferOut.clear(); + this.writeNextBlock(line); + if (bufferOut.hasRemaining()) { + bufferOut.flip(); + connection.write(this.bufferOut); + } + } + + public void writeNextLine(byte[] prefix, String line, byte[] suffix) throws IOException { + bufferOut.clear(); + this.writeNextBlock(prefix); + this.writeNextBlock(line.getBytes()); + this.writeNextBlock(suffix); + if (bufferOut.hasRemaining()) { + bufferOut.flip(); + connection.write(this.bufferOut); + } + } + + private void writeNextBlock(byte[] block) throws IOException { + for (byte aBlock : block) { + if (!bufferOut.hasRemaining()) { + bufferOut.flip(); + connection.write(this.bufferOut); + bufferOut.clear(); + } + bufferOut.put(aBlock); + } } }
deleted file mode 100644 --- a/src/main/java/nl/cwi/monetdb/mcl/io/SocketIOHandler.java +++ /dev/null @@ -1,141 +0,0 @@ -package nl.cwi.monetdb.mcl.io; - -import java.io.IOException; -import java.nio.ByteBuffer; - -/** - * Created by ferreira on 11/29/16. - */ -public class SocketIOHandler { - - private static final int CHAR_SIZE = Character.SIZE / Byte.SIZE; - - private static final int SHORT_SIZE = Short.SIZE / Byte.SIZE; - - private static final int INTEGER_SIZE = Integer.SIZE / Byte.SIZE; - - private static final int LONG_SIZE = Long.SIZE / Byte.SIZE; - - private static final int FLOAT_SIZE = Float.SIZE / Byte.SIZE; - - private static final int DOUBLE_SIZE = Double.SIZE / Byte.SIZE; - - private static final int INTERMEDIATE_BUFFER_SIZE = 1024; - - private boolean hasFinished; - - private ByteBuffer bufferIn = ByteBuffer.allocateDirect(INTERMEDIATE_BUFFER_SIZE); - - private ByteBuffer bufferOut = ByteBuffer.allocateDirect(INTERMEDIATE_BUFFER_SIZE); - - private final SocketConnection connection; - - public SocketIOHandler(SocketConnection connection) { - this.connection = connection; - } - - public SocketConnection getConnection() { - return connection; - } - - public byte readNextByte() throws IOException { - if(this.bufferIn.remaining() < Byte.SIZE) { - this.refillBufferIn(); - } - return this.bufferIn.get(); - } - - public char readNextChar() throws IOException { - if(this.bufferIn.remaining() < CHAR_SIZE) { - this.refillBufferIn(); - } - return this.bufferIn.getChar(); - } - - public short readNextShort() throws IOException { - if(this.bufferIn.remaining() < SHORT_SIZE) { - this.refillBufferIn(); - } - return this.bufferIn.getShort(); - } - - public int readNextInt() throws IOException { - if(this.bufferIn.remaining() < INTEGER_SIZE) { - this.refillBufferIn(); - } - return this.bufferIn.getInt(); - } - - public long readNextLong() throws IOException { - if(this.bufferIn.remaining() < LONG_SIZE) { - this.refillBufferIn(); - } - return this.bufferIn.getLong(); - } - - public float readNextFloat() throws IOException { - if(this.bufferIn.remaining() < FLOAT_SIZE) { - this.refillBufferIn(); - } - return this.bufferIn.getFloat(); - } - - public double readNextDouble() throws IOException { - if(this.bufferIn.remaining() < DOUBLE_SIZE) { - this.refillBufferIn(); - } - return this.bufferIn.getDouble(); - } - - public void readUntilChar(StringBuilder builder, char limit) throws IOException { - builder.setLength(0); - boolean found = false; - - while(!found) { - if (this.bufferIn.remaining() < CHAR_SIZE) { - this.refillBufferIn(); - } - char next = this.bufferIn.getChar(); - builder.append(next); - if(next == limit) { - found = true; - } - } - } - - private void refillBufferIn() throws IOException { - bufferIn.compact(); - if(!hasFinished) { - try { - connection.readMore(this.bufferIn); - bufferIn.flip(); - } catch (IOException ex) { - hasFinished = true; - } - } else { - throw new IOException("Done!"); - } - } - - public void writeNextLine(byte[] prefix, String line, byte[] suffix) throws IOException { - bufferOut.clear(); - this.writeNextBlock(prefix); - this.writeNextBlock(line.getBytes()); - this.writeNextBlock(suffix); - if (bufferOut.hasRemaining()) { - bufferOut.flip(); - connection.writeMore(this.bufferOut); - } - } - - private void writeNextBlock(byte[] block) throws IOException { - for (byte aBlock : block) { - if (!bufferOut.hasRemaining()) { - bufferOut.flip(); - connection.writeMore(this.bufferOut); - bufferOut.clear(); - } - bufferOut.put(aBlock); - } - } -}
--- a/src/main/java/nl/cwi/monetdb/mcl/protocol/AbstractProtocolParser.java +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/AbstractProtocolParser.java @@ -5,11 +5,11 @@ package nl.cwi.monetdb.mcl.protocol; */ public abstract class AbstractProtocolParser { - private ServerResponses currentServerResponseHeader = ServerResponses.UNKNOWN; + protected ServerResponses currentServerResponseHeader = ServerResponses.UNKNOWN; - private StarterHeaders currentStarterHeader = StarterHeaders.Q_UNKNOWN; + protected StarterHeaders currentStarterHeader = StarterHeaders.Q_UNKNOWN; - private TableResultHeaders currentTableResultSetHeader = TableResultHeaders.UNKNOWN; + protected TableResultHeaders currentTableResultSetHeader = TableResultHeaders.UNKNOWN; public ServerResponses getCurrentServerResponseHeader() { return currentServerResponseHeader;
--- a/src/main/java/nl/cwi/monetdb/mcl/protocol/newmapi/NewMapiProtocol.java +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/newmapi/NewMapiProtocol.java @@ -1,7 +1,6 @@ package nl.cwi.monetdb.mcl.protocol.newmapi; import nl.cwi.monetdb.mcl.io.SocketConnection; -import nl.cwi.monetdb.mcl.io.SocketIOHandler; import nl.cwi.monetdb.mcl.protocol.AbstractProtocolParser; import nl.cwi.monetdb.mcl.protocol.ServerResponses; import nl.cwi.monetdb.mcl.protocol.StarterHeaders; @@ -12,10 +11,10 @@ import nl.cwi.monetdb.mcl.protocol.Table */ public class NewMapiProtocol extends AbstractProtocolParser { - private final SocketIOHandler handler; + private final SocketConnection connection; public NewMapiProtocol(SocketConnection con) { - this.handler = new SocketIOHandler(con); + this.connection = con; } @Override
--- a/src/main/java/nl/cwi/monetdb/mcl/protocol/oldmapi/OldMapiProtocol.java +++ b/src/main/java/nl/cwi/monetdb/mcl/protocol/oldmapi/OldMapiProtocol.java @@ -1,7 +1,6 @@ package nl.cwi.monetdb.mcl.protocol.oldmapi; import nl.cwi.monetdb.mcl.io.SocketConnection; -import nl.cwi.monetdb.mcl.io.SocketIOHandler; import nl.cwi.monetdb.mcl.protocol.AbstractProtocolParser; import nl.cwi.monetdb.mcl.protocol.ServerResponses; import nl.cwi.monetdb.mcl.protocol.StarterHeaders; @@ -14,30 +13,59 @@ import java.io.IOException; */ public class OldMapiProtocol extends AbstractProtocolParser { - private final SocketIOHandler handler; + private static final int STRING_BUILDER_INITIAL_SIZE = 128; + + private final SocketConnection connection; + + private int builderPointer; + + private final StringBuilder builder = new StringBuilder(STRING_BUILDER_INITIAL_SIZE); public OldMapiProtocol(SocketConnection con) { - this.handler = new SocketIOHandler(con); + this.connection = con; } - public SocketIOHandler getHandler() { - return handler; + public SocketConnection getConnection() { + return connection; } @Override public ServerResponses getNextResponseHeaderImplementation() { + ServerResponses res = ServerResponses.UNKNOWN; try { - char nextToken = handler.readNextChar(); - return OldMapiConverter.GetNextResponseOnOldMapi(nextToken); + while(res != ServerResponses.PROMPT) { + connection.readUntilChar(this.builder, '\n'); + res = OldMapiConverter.GetNextResponseOnOldMapi(this.builder.charAt(0)); + if(res == ServerResponses.ERROR && !this.builder.toString().matches("^![0-9A-Z]{5}!.+")) { + this.builder.insert(1, "!22000!"); + } + } + this.builderPointer = 1; } catch (IOException e) { - return ServerResponses.ERROR; + res = ServerResponses.ERROR; + this.builder.setLength(0); + this.builderPointer = 0; + this.builder.append("!22000!").append(e.getMessage()); } + return res; + } + + public String getEntireResponseLine() { + String res = this.builder.toString(); + this.builderPointer = this.builder.length(); + return res; + } + + public String getRemainingResponseLine(int startIndex) { + String res = this.builder.substring(this.builderPointer + startIndex); + this.builderPointer = this.builder.length(); + return res; } @Override public StarterHeaders getNextStarterHeaderImplementation() { try { - char nextToken = handler.readNextChar(); + char nextToken = connection.readNextChar(); return OldMapiConverter.GetNextStartHeaderOnOldMapi(nextToken); } catch (IOException e) { return StarterHeaders.Q_UNKNOWN; @@ -48,4 +76,9 @@ public class OldMapiProtocol extends Abs public TableResultHeaders getNextTableHeaderImplementation() { return null; } + + + public void writeNextLine(byte[] line) throws IOException { + connection.writeNextLine(line); + } }
--- a/src/main/java/nl/cwi/monetdb/merovingian/Control.java +++ b/src/main/java/nl/cwi/monetdb/merovingian/Control.java @@ -11,7 +11,7 @@ package nl.cwi.monetdb.merovingian; import nl.cwi.monetdb.mcl.io.AbstractMCLReader; import nl.cwi.monetdb.mcl.io.AbstractMCLWriter; import nl.cwi.monetdb.mcl.connection.DeleteMe; -import nl.cwi.monetdb.mcl.MCLException; +import nl.cwi.monetdb.mcl.connection.MCLException; import nl.cwi.monetdb.mcl.parser.MCLParseException; import java.io.BufferedReader;
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/responses/AutoCommitResponse.java @@ -0,0 +1,15 @@ +package nl.cwi.monetdb.responses; + +/** + * The AutoCommitResponse represents a transaction message. It + * stores (a change in) the server side auto commit mode.<br /> + * <tt>&4 (t|f)</tt> + */ +public class AutoCommitResponse extends SchemaResponse { + public final boolean autocommit; + + public AutoCommitResponse(boolean ac) { + // fill the blank final + this.autocommit = ac; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/responses/DataBlockResponse.java @@ -0,0 +1,123 @@ +package nl.cwi.monetdb.responses; + +import nl.cwi.monetdb.mcl.io.AbstractMCLReader; + +import java.sql.SQLException; + +/** + * The DataBlockResponse is tabular data belonging to a + * ResultSetResponse. Tabular data from the server typically looks + * like: + * <pre> + * [ "value", 56 ] + * </pre> + * where each column is separated by ",\t" and each tuple surrounded + * by brackets ("[" and "]"). A DataBlockResponse object holds the + * raw data as read from the server, in a parsed manner, ready for + * easy retrieval. + * + * This object is not intended to be queried by multiple threads + * synchronously. It is designed to work for one thread retrieving + * rows from it. When multiple threads will retrieve rows from this + * object, it is possible for threads to get the same data. + */ +public class DataBlockResponse implements IResponse { + + /** The String array to keep the data in */ + private final String[] data; + /** The counter which keeps the current position in the data array */ + private int pos; + /** Whether we can discard lines as soon as we have read them */ + private boolean forwardOnly; + + /** + * Constructs a DataBlockResponse object + * @param size the size of the data array to create + * @param forward whether this is a forward only result + */ + DataBlockResponse(int size, boolean forward) { + this.pos = -1; + this.data = new String[size]; + this.forwardOnly = forward; + } + + /** + * addLine adds a String of data to this object's data array. + * Note that an IndexOutOfBoundsException can be thrown when an + * attempt is made to add more than the original construction size + * specified. + * + * @param line the header line as String + * @param linetype the line type according to the MAPI protocol + * @return a non-null String if the line is invalid, + * or additional lines are not allowed. + */ + @Override + public String addLine(String line, int linetype) { + if (linetype != AbstractMCLReader.RESULT) + return "protocol violation: unexpected line in data block: " + line; + // add to the backing array + data[++pos] = line; + + // all is well + return null; + } + + /** + * Returns whether this Reponse expects more lines to be added + * to it. + * + * @return true if a next line should be added, false otherwise + */ + @Override + public boolean wantsMore() { + // remember: pos is the value already stored + return pos + 1 < data.length; + } + + /** + * Indicates that no more header lines will be added to this + * Response implementation. In most cases this is a redundant + * operation because the data array is full. However... it can + * happen that this is NOT the case! + * + * @throws SQLException if not all rows are filled + */ + @Override + public void complete() throws SQLException { + if ((pos + 1) != data.length) + throw new SQLException("Inconsistent state detected! Current block capacity: " + data.length + + ", block usage: " + (pos + 1) + ". Did MonetDB send what it promised to?", "M0M10"); + } + + /** + * Instructs the Response implementation to close and do the + * necessary clean up procedures. + * + */ + @Override + public void close() { + // feed all rows to the garbage collector + for (int i = 0; i < data.length; i++) { + data[i] = null; + } + } + + /** + * Retrieves the required row. Warning: if the requested rows + * is out of bounds, an IndexOutOfBoundsException will be + * thrown. + * + * @param line the row to retrieve + * @return the requested row as String + */ + String getRow(int line) { + if (forwardOnly) { + String ret = data[line]; + data[line] = null; + return ret; + } else { + return data[line]; + } + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/responses/IResponse.java @@ -0,0 +1,43 @@ +package nl.cwi.monetdb.responses; + +import java.sql.SQLException; + +/** + * A Response is a message sent by the server to indicate some + * action has taken place, and possible results of that action. + */ +public interface IResponse { + + /** + * Adds a line to the underlying Response implementation. + * + * @param line the header line as String + * @param linetype the line type according to the MAPI protocol + * @return a non-null String if the line is invalid, + * or additional lines are not allowed. + */ + String addLine(String line, int linetype); + + /** + * Returns whether this Response expects more lines to be added + * to it. + * + * @return true if a next line should be added, false otherwise + */ + boolean wantsMore(); + + /** + * Indicates that no more header lines will be added to this + * Response implementation. + * + * @throws SQLException if the contents of the Response is not + * consistent or sufficient. + */ + void complete() throws SQLException; + + /** + * Instructs the Response implementation to close and do the + * necessary clean up procedures. + */ + void close(); +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/responses/ResponseList.java @@ -0,0 +1,405 @@ +package nl.cwi.monetdb.responses; + +import nl.cwi.monetdb.jdbc.MonetConnection; +import nl.cwi.monetdb.mcl.io.AbstractMCLReader; +import nl.cwi.monetdb.mcl.parser.MCLParseException; +import nl.cwi.monetdb.mcl.parser.StartOfHeaderParser; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A list of Response objects. Responses are added to this list. + * Methods of this class are not synchronized. This is left as + * responsibility to the caller to prevent concurrent access. + */ +public class ResponseList { + + /** the default number of rows that are (attempted to) read at once */ + protected final static int DEF_FETCHSIZE = 250; + /** The sequence counter */ + protected static int SeqCounter = 0; + + /** The cache size (number of rows in a DataBlockResponse object) */ + private final int cachesize; + /** The maximum number of results for this query */ + private final int maxrows; + /** The ResultSet type to produce */ + final int rstype; + /** The ResultSet concurrency to produce */ + final int rsconcur; + /** The sequence number of this ResponseList */ + private final int seqnr; + /** A list of the Responses associated with the query, + * in the right order */ + private List<IResponse> responses; + /** A map of ResultSetResponses, used for additional + * DataBlockResponse mapping */ + private Map<Integer, ResultSetResponse> rsresponses; + + /** The current header returned by getNextResponse() */ + private int curResponse; + + /** + * Main constructor. The query argument can either be a String + * or List. An SQLException is thrown if another object + * instance is supplied. + * + * @param cachesize overall cachesize to use + * @param maxrows maximum number of rows to allow in the set + * @param rstype the type of result sets to produce + * @param rsconcur the concurrency of result sets to produce + */ + public ResponseList(int cachesize, int maxrows, int rstype, int rsconcur) throws SQLException { + this.cachesize = cachesize; + this.maxrows = maxrows; + this.rstype = rstype; + this.rsconcur = rsconcur; + responses = new ArrayList<>(); + curResponse = -1; + seqnr = SeqCounter++; + } + + /** + * Retrieves the next available response, or null if there are + * no more responses. + * + * @return the next Response available or null + */ + public IResponse getNextResponse() throws SQLException { + if (rstype == ResultSet.TYPE_FORWARD_ONLY) { + // free resources if we're running forward only + if (curResponse >= 0 && curResponse < responses.size()) { + IResponse tmp = responses.get(curResponse); + if (tmp != null) tmp.close(); + responses.set(curResponse, null); + } + } + curResponse++; + if (curResponse >= responses.size()) { + // ResponseList is obviously completed so, there are no + // more responses + return null; + } else { + // return this response + return responses.get(curResponse); + } + } + + /** + * Closes the Response at index i, if not null. + * + * @param i the index position of the header to close + */ + public void closeResponse(int i) { + if (i < 0 || i >= responses.size()) return; + IResponse tmp = responses.set(i, null); + if (tmp != null) + tmp.close(); + } + + /** + * Closes the current response. + */ + void closeCurrentResponse() { + closeResponse(curResponse); + } + + /** + * Closes the current and previous responses. + */ + void closeCurOldResponses() { + for (int i = curResponse; i >= 0; i--) { + closeResponse(i); + } + } + + /** + * Closes this ResponseList by closing all the Responses in this + * ResponseList. + */ + public void close() { + for (int i = 0; i < responses.size(); i++) { + closeResponse(i); + } + } + + /** + * Returns whether this ResponseList has still unclosed + * Responses. + */ + boolean hasUnclosedResponses() { + for (IResponse r : responses) { + if (r != null) + return true; + } + return false; + } + + /** + * Executes the query contained in this ResponseList, and + * stores the Responses resulting from this query in this + * ResponseList. + * + * @throws SQLException if a database error occurs + */ + public void processQuery(String query) throws SQLException { + executeQuery(server.getQueryHeaderTemplates(), query); + } + + /** + * Internal executor of queries. + * + * @param templ the template to fill in + * @param query the query to execute + * @throws SQLException if a database error occurs + */ + @SuppressWarnings("fallthrough") + public void executeQuery(String[] templ, String query) throws SQLException { + boolean sendThreadInUse = false; + String error = null; + + try { + synchronized (server) { + // make sure we're ready to send query; read data till we + // have the prompt it is possible (and most likely) that we + // already have the prompt and do not have to skip any + // lines. Ignore errors from previous result sets. + in.waitForPrompt(); + + // {{{ set reply size + /** + * Change the reply size of the server. If the given + * value is the same as the current value known to use, + * then ignore this call. If it is set to 0 we get a + * prompt after the server sent it's header. + */ + int size = cachesize == 0 ? DEF_FETCHSIZE : cachesize; + size = maxrows != 0 ? Math.min(maxrows, size) : size; + // don't do work if it's not needed + if (server.getLang() == MonetConnection.LANG_SQL && size != curReplySize && templ != server.getCommandHeaderTemplates()) { + sendControlCommand("reply_size " + size); + + // store the reply size after a successful change + curReplySize = size; + } + // }}} set reply size + + // If the query is larger than the TCP buffer size, use a + // special send thread to avoid deadlock with the server due + // to blocking behaviour when the buffer is full. Because + // the server will be writing back results to us, it will + // eventually block as well when its TCP buffer gets full, + // as we are blocking an not consuming from it. The result + // is a state where both client and server want to write, + // but block. + if (query.length() > server.getBlockSize()) { + // get a reference to the send thread + if (sendThread == null) + sendThread = new SendThread(out); + // tell it to do some work! + sendThread.runQuery(templ, query); + sendThreadInUse = true; + } else { + // this is a simple call, which is a lot cheaper and will + // always succeed for small queries. + out.writeLine((templ[0] == null ? "" : templ[0] + query + templ[1] == null ? "" : templ[1])); + } + + // go for new results + String tmpLine = in.readLine(); + int linetype = in.getLineType(); + IResponse res = null; + while (linetype != AbstractMCLReader.PROMPT) { + // each response should start with a start of header + // (or error) + switch (linetype) { + case AbstractMCLReader.SOHEADER: + // make the response object, and fill it + try { + switch (sohp.parse(tmpLine)) { + case StartOfHeaderParser.Q_PARSE: + throw new MCLParseException("Q_PARSE header not allowed here", 1); + case StartOfHeaderParser.Q_TABLE: + case StartOfHeaderParser.Q_PREPARE: { + int id = sohp.getNextAsInt(); + int tuplecount = sohp.getNextAsInt(); + int columncount = sohp.getNextAsInt(); + int rowcount = sohp.getNextAsInt(); + // enforce the maxrows setting + if (maxrows != 0 && tuplecount > maxrows) + tuplecount = maxrows; + res = new ResultSetResponse(id, tuplecount, columncount, rowcount, + this, seqnr); + // only add this resultset to + // the hashmap if it can possibly + // have an additional datablock + if (rowcount < tuplecount) { + if (rsresponses == null) + rsresponses = new HashMap<>(); + rsresponses.put(id, (ResultSetResponse) res); + } + } break; + case StartOfHeaderParser.Q_UPDATE: + res = new UpdateResponse( + sohp.getNextAsInt(), // count + sohp.getNextAsString() // key-id + ); + break; + case StartOfHeaderParser.Q_SCHEMA: + res = new SchemaResponse(); + break; + case StartOfHeaderParser.Q_TRANS: + boolean ac = sohp.getNextAsString().equals("t"); + if (autoCommit && ac) { + addWarning("Server enabled auto commit " + + "mode while local state " + + "already was auto commit.", "01M11" + ); + } + autoCommit = ac; + res = new AutoCommitResponse(ac); + break; + case StartOfHeaderParser.Q_BLOCK: { + // a new block of results for a + // response... + int id = sohp.getNextAsInt(); + sohp.getNextAsInt(); // columncount + int rowcount = sohp.getNextAsInt(); + int offset = sohp.getNextAsInt(); + ResultSetResponse t = rsresponses.get(id); + if (t == null) { + error = "M0M12!no ResultSetResponse with id " + id + " found"; + break; + } + + DataBlockResponse r = new DataBlockResponse(rowcount, + t.getRSType() == ResultSet.TYPE_FORWARD_ONLY); + + t.addDataBlockResponse(offset, r); + res = r; + } break; + } + } catch (MCLParseException e) { + error = "M0M10!error while parsing start of header:\n" + + e.getMessage() + + " found: '" + tmpLine.charAt(e.getErrorOffset()) + "'" + + " in: \"" + tmpLine + "\"" + + " at pos: " + e.getErrorOffset(); + // flush all the rest + in.waitForPrompt(); + linetype = in.getLineType(); + break; + } + + // immediately handle errors after parsing + // the header (res may be null) + if (error != null) { + in.waitForPrompt(); + linetype = in.getLineType(); + break; + } + + // here we have a res object, which + // we can start filling + while (res.wantsMore()) { + error = res.addLine( + in.readLine(), + in.getLineType() + ); + if (error != null) { + // right, some protocol violation, + // skip the rest of the result + error = "M0M10!" + error; + in.waitForPrompt(); + linetype = in.getLineType(); + break; + } + } + if (error != null) + break; + // it is of no use to store + // DataBlockReponses, you never want to + // retrieve them directly anyway + if (!(res instanceof DataBlockResponse)) + responses.add(res); + + // read the next line (can be prompt, new + // result, error, etc.) before we start the + // loop over + tmpLine = in.readLine(); + linetype = in.getLineType(); + break; + case AbstractMCLReader.INFO: + addWarning(tmpLine.substring(1), "01000"); + + // read the next line (can be prompt, new + // result, error, etc.) before we start the + // loop over + tmpLine = in.readLine(); + linetype = in.getLineType(); + break; + default: // Yeah... in Java this is correct! + // we have something we don't + // expect/understand, let's make it an error + // message + tmpLine = "!M0M10!protocol violation, unexpected line: " + tmpLine; + // don't break; fall through... + case AbstractMCLReader.ERROR: + // read everything till the prompt (should be + // error) we don't know if we ignore some + // garbage here... but the log should reveal + // that + error = in.waitForPrompt(); + linetype = in.getLineType(); + if (error != null) { + error = tmpLine.substring(1) + "\n" + error; + } else { + error = tmpLine.substring(1); + } + break; + } + } + } + + // if we used the sendThread, make sure it has finished + if (sendThreadInUse) { + String tmp = sendThread.getErrors(); + if (tmp != null) { + if (error == null) { + error = "08000!" + tmp; + } else { + error += "\n08000!" + tmp; + } + } + } + if (error != null) { + SQLException ret = null; + String[] errors = error.split("\n"); + for (String error1 : errors) { + if (ret == null) { + ret = new SQLException(error1.substring(6), + error1.substring(0, 5)); + } else { + ret.setNextException(new SQLException( + error1.substring(6), + error1.substring(0, 5))); + } + } + throw ret; + } + } catch (SocketTimeoutException e) { + close(); // JDBC 4.1 semantics, abort() + throw new SQLException("connection timed out", "08M33"); + } catch (IOException e) { + closed = true; + throw new SQLException(e.getMessage() + " (mserver still alive?)", "08000"); + } + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/responses/ResultSetResponse.java @@ -0,0 +1,409 @@ +package nl.cwi.monetdb.responses; + +import nl.cwi.monetdb.jdbc.MonetConnection; +import nl.cwi.monetdb.mcl.io.AbstractMCLReader; +import nl.cwi.monetdb.mcl.parser.HeaderLineParser; +import nl.cwi.monetdb.mcl.parser.MCLParseException; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * The ResultSetResponse represents a tabular result sent by the + * server. This is typically an SQL table. The MAPI headers of the + * Response look like: + * <pre> + * &1 1 28 2 10 + * # name, value # name + * # varchar, varchar # type + * </pre> + * there the first line consists out of<br /> + * <tt>&"qt" "id" "tc" "cc" "rc"</tt>. + */ +public class ResultSetResponse implements IResponse { + /** The number of columns in this result */ + public final int columncount; + /** The total number of rows this result set has */ + public final int tuplecount; + /** The numbers of rows to retrieve per DataBlockResponse */ + private int cacheSize; + /** The table ID of this result */ + public final int id; + /** The names of the columns in this result */ + private String[] name; + /** The types of the columns in this result */ + private String[] type; + /** The max string length for each column in this result */ + private int[] columnLengths; + /** The table for each column in this result */ + private String[] tableNames; + /** The query sequence number */ + private final int seqnr; + /** A List of result blocks (chunks of size fetchSize/cacheSize) */ + private DataBlockResponse[] resultBlocks; + + /** A bitmap telling whether the headers are set or not */ + private boolean[] isSet; + /** Whether this Response is closed */ + private boolean closed; + + /** The Connection that we should use when requesting a new block */ + private ResponseList parent; + /** Whether the fetchSize was explitly set by the user */ + private boolean cacheSizeSetExplicitly = false; + /** Whether we should send an Xclose command to the server + * if we close this Response */ + private boolean destroyOnClose; + /** the offset to be used on Xexport queries */ + private int blockOffset = 0; + + /** A parser for header lines */ + HeaderLineParser hlp; + + private final static int NAMES = 0; + private final static int TYPES = 1; + private final static int TABLES = 2; + private final static int LENS = 3; + + /** + * Sole constructor, which requires a MonetConnection parent to + * be given. + * + * @param id the ID of the result set + * @param tuplecount the total number of tuples in the result set + * @param columncount the number of columns in the result set + * @param rowcount the number of rows in the current block + * @param parent the parent that created this Response and will + * supply new result blocks when necessary + * @param seq the query sequence number + */ + ResultSetResponse(int id, int tuplecount, int columncount, int rowcount, ResponseList parent, int seq) throws SQLException { + isSet = new boolean[7]; + this.parent = parent; + if (parent.cachesize == 0) { + /* Below we have to calculate how many "chunks" we need + * to allocate to store the entire result. However, if + * the user didn't set a cache size, as in this case, we + * need to stick to our defaults. */ + cacheSize = ResponseList.DEF_FETCHSIZE; + cacheSizeSetExplicitly = false; + } else { + cacheSize = parent.cachesize; + cacheSizeSetExplicitly = true; + } + /* So far, so good. Now the problem with EXPLAIN, DOT, etc + * queries is, that they don't support any block fetching, + * so we need to always fetch everything at once. For that + * reason, the cache size is here set to the rowcount if + * it's larger, such that we do a full fetch at once. + * (Because we always set a reply_size, we can only get a + * larger rowcount from the server if it doesn't paginate, + * because it's a pseudo SQL result.) */ + if (rowcount > cacheSize) + cacheSize = rowcount; + seqnr = seq; + closed = false; + destroyOnClose = id > 0 && tuplecount > rowcount; + + this.id = id; + this.tuplecount = tuplecount; + this.columncount = columncount; + this.resultBlocks = new DataBlockResponse[(tuplecount / cacheSize) + 1]; + + hlp = server.getHeaderLineParser(columncount); + + resultBlocks[0] = new DataBlockResponse(rowcount, parent.rstype == ResultSet.TYPE_FORWARD_ONLY); + } + + /** + * Parses the given string and changes the value of the matching + * header appropriately, or passes it on to the underlying + * DataResponse. + * + * @param tmpLine the string that contains the header + * @return a non-null String if the header cannot be parsed or + * is unknown + */ + // {{{ addLine + @Override + public String addLine(String tmpLine, int linetype) { + if (isSet[LENS] && isSet[TYPES] && isSet[TABLES] && isSet[NAMES]) { + return resultBlocks[0].addLine(tmpLine, linetype); + } + + if (linetype != AbstractMCLReader.HEADER) + return "header expected, got: " + tmpLine; + + // depending on the name of the header, we continue + try { + switch (hlp.parse(tmpLine)) { + case HeaderLineParser.NAME: + name = hlp.values.clone(); + isSet[NAMES] = true; + break; + case HeaderLineParser.LENGTH: + columnLengths = hlp.intValues.clone(); + isSet[LENS] = true; + break; + case HeaderLineParser.TYPE: + type = hlp.values.clone(); + isSet[TYPES] = true; + break; + case HeaderLineParser.TABLE: + tableNames = hlp.values.clone(); + isSet[TABLES] = true; + break; + } + } catch (MCLParseException e) { + return e.getMessage(); + } + + // all is well + return null; + } + // }}} + + /** + * Returns whether this ResultSetResponse needs more lines. + * This method returns true if not all headers are set, or the + * first DataBlockResponse reports to want more. + */ + @Override + public boolean wantsMore() { + return !(isSet[LENS] && isSet[TYPES] && isSet[TABLES] && isSet[NAMES]) || resultBlocks[0].wantsMore(); + } + + /** + * Returns an array of Strings containing the values between + * ',\t' separators. + * + * @param chrLine a character array holding the input data + * @param start where the relevant data starts + * @param stop where the relevant data stops + * @return an array of Strings + */ + final private String[] getValues(char[] chrLine, int start, int stop) { + int elem = 0; + String[] values = new String[columncount]; + + for (int i = start; i < stop; i++) { + if (chrLine[i] == '\t' && chrLine[i - 1] == ',') { + values[elem++] = + new String(chrLine, start, i - 1 - start); + start = i + 1; + } + } + // at the left over part + values[elem++] = new String(chrLine, start, stop - start); + + return values; + } + + /** + * Adds the given DataBlockResponse to this ResultSetResponse at + * the given block position. + * + * @param offset the offset number of rows for this block + * @param rr the DataBlockResponse to add + */ + void addDataBlockResponse(int offset, DataBlockResponse rr) { + int block = (offset - blockOffset) / cacheSize; + resultBlocks[block] = rr; + } + + /** + * Marks this Response as being completed. A complete Response + * needs to be consistent with regard to its internal data. + * + * @throws SQLException if the data currently in this Response is not + * sufficient to be consistant + */ + @Override + public void complete() throws SQLException { + String error = ""; + if (!isSet[NAMES]) error += "name header missing\n"; + if (!isSet[TYPES]) error += "type header missing\n"; + if (!isSet[TABLES]) error += "table name header missing\n"; + if (!isSet[LENS]) error += "column width header missing\n"; + if (!error.equals("")) throw new SQLException(error, "M0M10"); + } + + /** + * Returns the names of the columns + * + * @return the names of the columns + */ + String[] getNames() { + return name; + } + + /** + * Returns the types of the columns + * + * @return the types of the columns + */ + String[] getTypes() { + return type; + } + + /** + * Returns the tables of the columns + * + * @return the tables of the columns + */ + String[] getTableNames() { + return tableNames; + } + + /** + * Returns the lengths of the columns + * + * @return the lengths of the columns + */ + int[] getColumnLengths() { + return columnLengths; + } + + /** + * Returns the cache size used within this Response + * + * @return the cache size + */ + int getCacheSize() { + return cacheSize; + } + + /** + * Returns the current block offset + * + * @return the current block offset + */ + int getBlockOffset() { + return blockOffset; + } + + /** + * Returns the ResultSet type, FORWARD_ONLY or not. + * + * @return the ResultSet type + */ + int getRSType() { + return parent.rstype; + } + + /** + * Returns the concurrency of the ResultSet. + * + * @return the ResultSet concurrency + */ + int getRSConcur() { + return parent.rsconcur; + } + + /** + * Returns a line from the cache. If the line is already present in the + * cache, it is returned, if not apropriate actions are taken to make + * sure the right block is being fetched and as soon as the requested + * line is fetched it is returned. + * + * @param row the row in the result set to return + * @return the exact row read as requested or null if the requested row + * is out of the scope of the result set + * @throws SQLException if an database error occurs + */ + String getLine(int row) throws SQLException { + if (row >= tuplecount || row < 0) + return null; + + int block = (row - blockOffset) / cacheSize; + int blockLine = (row - blockOffset) % cacheSize; + + // do we have the right block loaded? (optimistic try) + DataBlockResponse rawr; + // load block if appropriate + if ((rawr = resultBlocks[block]) == null) { + /// TODO: ponder about a maximum number of blocks to keep + /// in memory when dealing with random access to + /// reduce memory blow-up + + // if we're running forward only, we can discard the oldmapi + // block loaded + if (parent.rstype == ResultSet.TYPE_FORWARD_ONLY) { + for (int i = 0; i < block; i++) + resultBlocks[i] = null; + + if (ResponseList.SeqCounter - 1 == seqnr && !cacheSizeSetExplicitly && + tuplecount - row > cacheSize && cacheSize < ResponseList.DEF_FETCHSIZE * 10) { + // there has no query been issued after this + // one, so we can consider this an uninterrupted + // continuation request. Let's once increase + // the cacheSize as it was not explicitly set, + // since the chances are high that we won't + // bother anyone else by doing so, and just + // gaining some performance. + + // store the previous position in the + // blockOffset variable + blockOffset += cacheSize; + + // increase the cache size (a lot) + cacheSize *= 10; + + // by changing the cacheSize, we also + // change the block measures. Luckily + // we don't care about previous blocks + // because we have a forward running + // pointer only. However, we do have + // to recalculate the block number, to + // ensure the next call to find this + // new block. + block = (row - blockOffset) / cacheSize; + blockLine = (row - blockOffset) % cacheSize; + } + } + + // ok, need to fetch cache block first + parent.executeQuery(server.getCommandHeaderTemplates(), + "export " + id + " " + ((block * cacheSize) + blockOffset) + " " + cacheSize); + rawr = resultBlocks[block]; + if (rawr == null) throw + new AssertionError("block " + block + " should have been fetched by now :("); + } + + return rawr.getRow(blockLine); + } + + /** + * Closes this Response by sending an Xclose to the server indicating + * that the result can be closed at the server side as well. + */ + @Override + public void close() { + if (closed) return; + // send command to server indicating we're done with this + // result only if we had an ID in the header and this result + // was larger than the reply size + try { + if (destroyOnClose) sendControlCommand("close " + id); + } catch (SQLException e) { + // probably a connection error... + } + + // close the data block associated with us + for (int i = 1; i < resultBlocks.length; i++) { + DataBlockResponse r = resultBlocks[i]; + if (r != null) r.close(); + } + + closed = true; + } + + /** + * Returns whether this Response is closed + * + * @return whether this Response is closed + */ + boolean isClosed() { + return closed; + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/responses/SchemaResponse.java @@ -0,0 +1,37 @@ +package nl.cwi.monetdb.responses; + +import java.sql.Statement; + +/** + * The SchemaResponse represents an schema modification response. + * It is issued on statements like CREATE, DROP or ALTER TABLE. + * This response keeps a field that represents the success state, as + * defined by JDBC, which is currently in MonetDB's case always + * SUCCESS_NO_INFO. Note that this state is not sent by the + * server.<br /> + * <tt>&3</tt> + */ +public class SchemaResponse implements IResponse { + + public final int state = Statement.SUCCESS_NO_INFO; + + @Override + public String addLine(String line, int linetype) { + return "Header lines are not supported for a SchemaResponse"; + } + + @Override + public boolean wantsMore() { + return false; + } + + @Override + public void complete() { + // empty, because there is nothing to check + } + + @Override + public void close() { + // nothing to do here... + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/responses/SendThread.java @@ -0,0 +1,145 @@ +package nl.cwi.monetdb.responses; + +import nl.cwi.monetdb.mcl.io.AbstractMCLWriter; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A thread to send a query to the server. When sending large + * amounts of data to a server, the output buffer of the underlying + * communication socket may overflow. In such case the sending + * process blocks. In order to prevent deadlock, it might be + * desirable that the driver as a whole does not block. This thread + * facilitates the prevention of such 'full block', because this + * separate thread only will block.<br /> + * This thread is designed for reuse, as thread creation costs are + * high. + */ +public class SendThread extends Thread { + /** The state WAIT represents this thread to be waiting for + * something to do */ + private final static int WAIT = 0; + /** The state QUERY represents this thread to be executing a query */ + private final static int QUERY = 1; + /** The state SHUTDOWN is the final state that ends this thread */ + private final static int SHUTDOWN = -1; + + private String[] templ; + private String query; + private AbstractMCLWriter out; + private String error; + private int state = WAIT; + + private final Lock sendLock = new ReentrantLock(); + private final Condition queryAvailable = sendLock.newCondition(); + private final Condition waiting = sendLock.newCondition(); + + /** + * Constructor which immediately starts this thread and sets it + * into daemon mode. + * + * @param out the socket to write to + */ + public SendThread(AbstractMCLWriter out) { + super("SendThread"); + this.setDaemon(true); + this.out = out; + this.start(); + } + + @Override + public void run() { + sendLock.lock(); + try { + while (true) { + while (state == WAIT) { + try { + queryAvailable.await(); + } catch (InterruptedException e) { + // woken up, eh? + } + } + if (state == SHUTDOWN) + break; + + // state is QUERY here + try { + out.writeLine((templ[0] == null ? "" : templ[0]) + query + (templ[1] == null ? "" : templ[1])); + } catch (IOException e) { + error = e.getMessage(); + } + + // update our state, and notify, maybe someone is waiting + // for us in throwErrors + state = WAIT; + waiting.signal(); + } + } finally { + sendLock.unlock(); + } + } + + /** + * Starts sending the given query over the given socket. Beware + * that the thread should be finished (can be assured by calling + * throwErrors()) before this method is called! + * + * @param templ the query template + * @param query the query itself + * @throws SQLException if this SendThread is already in use + */ + public void runQuery(String[] templ, String query) throws SQLException { + sendLock.lock(); + try { + if (state != WAIT) + throw new SQLException("SendThread already in use or shutting down!", "M0M03"); + + this.templ = templ; + this.query = query; + + // let the thread know there is some work to do + state = QUERY; + queryAvailable.signal(); + } finally { + sendLock.unlock(); + } + } + + /** + * Returns errors encountered during the sending process. + * + * @return the errors or null if none + */ + public String getErrors() { + sendLock.lock(); + try { + // make sure the thread is in WAIT state, not QUERY + while (state == QUERY) { + try { + waiting.await(); + } catch (InterruptedException e) { + // just try again + } + } + if (state == SHUTDOWN) + error = "SendThread is shutting down"; + } finally { + sendLock.unlock(); + } + return error; + } + + /** + * Requests this SendThread to stop. + */ + public void shutdown() { + sendLock.lock(); + state = SHUTDOWN; + sendLock.unlock(); + this.interrupt(); // break any wait conditions + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/responses/UpdateResponse.java @@ -0,0 +1,40 @@ +package nl.cwi.monetdb.responses; + +/** + * The UpdateResponse represents an update statement response. It + * is issued on an UPDATE, INSERT or DELETE SQL statement. This + * response keeps a count field that represents the affected rows + * and a field that contains the last inserted auto-generated ID, or + * -1 if not applicable.<br /> + * <tt>&2 0 -1</tt> + */ +public class UpdateResponse implements IResponse { + public final int count; + public final String lastid; + + public UpdateResponse(int cnt, String id) { + // fill the blank finals + this.count = cnt; + this.lastid = id; + } + + @Override + public String addLine(String line, int linetype) { + return "Header lines are not supported for an UpdateResponse"; + } + + @Override + public boolean wantsMore() { + return false; + } + + @Override + public void complete() { + // empty, because there is nothing to check + } + + @Override + public void close() { + // nothing to do here... + } +}
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/util/ChannelSecurity.java @@ -0,0 +1,43 @@ +package nl.cwi.monetdb.util; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Created by ferreira on 12/1/16. + */ +public class ChannelSecurity { + + private static char HexChar(int n) { return (n > 9) ? (char) ('a' + (n - 10)) : (char) ('0' + n); } + + /** + * Small helper method to convert a byte string to a hexadecimal + * string representation. + * + * @param digest the byte array to convert + * @return the byte array as hexadecimal string + */ + private static String ToHex(byte[] digest) { + char[] result = new char[digest.length * 2]; + int pos = 0; + for (byte aDigest : digest) { + result[pos++] = HexChar((aDigest & 0xf0) >> 4); + result[pos++] = HexChar(aDigest & 0x0f); + } + return new String(result); + } + + public static String DigestStrings(String algorithm, String... toDigests) { + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + for (String str : toDigests) { + md.update(str.getBytes("UTF-8")); + } + byte[] digest = md.digest(); + return ToHex(digest); + } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) { + throw new AssertionError("internal error: " + e.toString()); + } + } +}
--- a/src/main/java/nl/cwi/monetdb/util/SQLRestore.java +++ b/src/main/java/nl/cwi/monetdb/util/SQLRestore.java @@ -14,7 +14,7 @@ import java.io.FileReader; import java.io.IOException; import java.util.concurrent.atomic.AtomicBoolean; -import nl.cwi.monetdb.mcl.MCLException; +import nl.cwi.monetdb.mcl.connection.MCLException; import nl.cwi.monetdb.mcl.io.AbstractMCLReader; import nl.cwi.monetdb.mcl.io.AbstractMCLWriter; import nl.cwi.monetdb.mcl.connection.DeleteMe;