Index: java/testing/org/apache/derbyTesting/functionTests/tests/jdbcapi/LargeDataLocksTest.java =================================================================== --- java/testing/org/apache/derbyTesting/functionTests/tests/jdbcapi/LargeDataLocksTest.java (revision 574730) +++ java/testing/org/apache/derbyTesting/functionTests/tests/jdbcapi/LargeDataLocksTest.java (working copy) @@ -196,8 +196,7 @@ public static Test suite() { TestSuite suite = new TestSuite("LargeDataLocksTest"); suite.addTest(baseSuite("LargeDataLocksTest:embedded")); - // Disable for client until DERBY-2892 is fixed - //suite.addTest(TestConfiguration.clientServerDecorator(baseSuite("LargeDataLocksTest:client"))); + suite.addTest(TestConfiguration.clientServerDecorator(baseSuite("LargeDataLocksTest:client"))); return suite; } Index: java/testing/org/apache/derbyTesting/functionTests/tests/derbynet/_Suite.java =================================================================== --- java/testing/org/apache/derbyTesting/functionTests/tests/derbynet/_Suite.java (revision 574730) +++ java/testing/org/apache/derbyTesting/functionTests/tests/derbynet/_Suite.java (working copy) @@ -55,6 +55,7 @@ suite.addTest(SecureServerTest.suite()); suite.addTest(SSLTest.suite()); suite.addTest(NetIjTest.suite()); + suite.addTest(LOBLocatorReleaseTest.suite()); // Disabled due to "java.sql.SQLSyntaxErrorException: The class // 'org.apache.derbyTesting.functionTests.tests.derbynet.checkSecMgr' Index: java/client/org/apache/derby/client/net/NetConnection.java =================================================================== --- java/client/org/apache/derby/client/net/NetConnection.java (revision 574730) +++ java/client/org/apache/derby/client/net/NetConnection.java (working copy) @@ -1782,6 +1782,17 @@ /** + * Checks whether the server supports locators for large objects. + * + * @return {@code true} if LOB locators are supported. + */ + protected final boolean serverSupportsLocators() { + // Support for locators was added in the same version as layer B + // streaming. + return serverSupportsLayerBStreaming(); + } + + /** * Returns if a transaction is in process * @return open */ Index: java/client/org/apache/derby/client/net/NetCursor.java =================================================================== --- java/client/org/apache/derby/client/net/NetCursor.java (revision 574730) +++ java/client/org/apache/derby/client/net/NetCursor.java (working copy) @@ -1052,11 +1052,14 @@ /** * Get locator for LOB of the designated column + *

+ * Note that this method cannot be invoked on a LOB column that is NULL. + * * @param column column number, starts at 1 * @return locator value, Lob.INVALID_LOCATOR if LOB * value was sent instead of locator */ - private int locator(int column) + protected int locator(int column) { int locator = get_INTEGER(column); // If Lob value was sent instead of locator, highest bit will be set @@ -1068,8 +1071,15 @@ } } - public Blob getBlobColumn_(int column, Agent agent) throws SqlException - { + /** + * @see org.apache.derby.client.am.Cursor#getBlobColumn_ + */ + public Blob getBlobColumn_(int column, Agent agent, boolean toBePublished) + throws SqlException { + // Only inform the tracker if the Blob is published to the user. + if (toBePublished) { + netResultSet_.markLOBAsAccessed(column); + } // Check for locator int locator = locator(column); if (locator > 0) { // Create locator-based LOB object @@ -1103,7 +1113,15 @@ } - public Clob getClobColumn_(int column, Agent agent) throws SqlException { + /** + * @see org.apache.derby.client.am.Cursor#getClobColumn_ + */ + public Clob getClobColumn_(int column, Agent agent, boolean toBePublished) + throws SqlException { + // Only inform the tracker if the Clob is published to the user. + if (toBePublished) { + netResultSet_.markLOBAsAccessed(column); + } // Check for locator int locator = locator(column); if (locator > 0) { // Create locator-based LOB object Index: java/client/org/apache/derby/client/am/Connection.java =================================================================== --- java/client/org/apache/derby/client/am/Connection.java (revision 574730) +++ java/client/org/apache/derby/client/am/Connection.java (working copy) @@ -986,6 +986,13 @@ } } + /** + * Checks whether the server supports locators for large objects. + * + * @return {@code true} if LOB locators are supported. + */ + protected abstract boolean serverSupportsLocators(); + public int getTransactionIsolation() throws SQLException { // Store the current auto-commit value and use it to restore Index: java/client/org/apache/derby/client/am/Statement.java =================================================================== --- java/client/org/apache/derby/client/am/Statement.java (revision 574730) +++ java/client/org/apache/derby/client/am/Statement.java (working copy) @@ -1390,6 +1390,8 @@ } resultSet.resultSetMetaData_ = resultSetMetaData_; resultSet.resultSetMetaData_.resultSetConcurrency_ = resultSet.resultSetConcurrency_; + // Create tracker for LOB locator columns. + resultSet.createLOBColumnTracker(); // only cache the Cursor object for a PreparedStatement and if a Cursor object is // not already cached. @@ -1422,6 +1424,8 @@ resultSet.completeSqlca(sqlca); // For CallableStatements we can't just clobber the resultSet_ here, must use setResultSetEvent() separately resultSet.resultSetMetaData_ = resultSetMetaData; + // Create tracker for LOB locator columns. + resultSet.createLOBColumnTracker(); // The following two assignments should have already happened via prepareEvent(), // but are included here for safety for the time being. Index: java/client/org/apache/derby/client/am/Cursor.java =================================================================== --- java/client/org/apache/derby/client/am/Cursor.java (revision 574730) +++ java/client/org/apache/derby/client/am/Cursor.java (working copy) @@ -23,6 +23,8 @@ import org.apache.derby.shared.common.reference.SQLState; import java.sql.SQLException; +import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.UnsupportedEncodingException; // When we calculate column offsets make sure we calculate the correct offsets for double byte charactr5er data @@ -661,10 +663,55 @@ return recyclableCalendar_; } - abstract public Blob getBlobColumn_(int column, Agent agent) throws SqlException; + /** + * Returns a reference to the locator procedures. + *

+ * These procedures are used to operate on large objects referenced on the + * server by locators. + * + * @return The locator procedures object. + */ + CallableLocatorProcedures getLocatorProcedures() { + return agent_.connection_.locatorProcedureCall(); + } - abstract public Clob getClobColumn_(int column, Agent agent) throws SqlException; + /** + * Obtains the locator for the specified LOB column. + *

+ * Note that this method cannot be invoked on a LOB column that is NULL. + * + * @param column 1-based column index + * @return A positive integer locator if valid, {@link Lob#INVALID_LOCATOR} + * otherwise. + */ + protected abstract int locator(int column); + /** + * Returns a {@code Blob} object. + * + * @param column 1-based column index + * @param agent associated agent + * @param toBePublished whether the Blob will be published to the user + * @return A {@linkplain java.sql.Blob Blob} object. + * @throws SqlException if getting the {@code Blob} fails + */ + public abstract Blob getBlobColumn_(int column, Agent agent, + boolean toBePublished) + throws SqlException; + + /** + * Returns a {@code Clob} object. + * + * @param column 1-based column index + * @param agent associated agent + * @param toBePublished whether the Clob will be published to the user + * @return A {@linkplain java.sql.Clob Clob} object. + * @throws SqlException if getting the {@code Clob} fails + */ + public abstract Clob getClobColumn_(int column, Agent agent, + boolean toBePublished) + throws SqlException; + // get the raw clob bytes, without translation. dataOffset must be int[1] abstract public byte[] getClobBytes_(int column, int[] dataOffset /*output*/) throws SqlException; @@ -979,11 +1026,14 @@ return (maxFieldSize_ == 0) ? tempString : tempString.substring(0, java.lang.Math.min(maxFieldSize_, tempString.length())); case java.sql.Types.BLOB: - Blob b = (Blob) getBlobColumn_(column, agent_); - return agent_.crossConverters_.getStringFromBytes(b.getBytes(1, (int) b.length())); + Blob b = getBlobColumn_(column, agent_, false); + tempString = agent_.crossConverters_. + getStringFromBytes(b.getBytes(1, (int) b.length())); + return tempString; case java.sql.Types.CLOB: - Clob c = getClobColumn_(column, agent_); - return c.getSubString(1, (int) c.length()); + Clob c = getClobColumn_(column, agent_, false); + tempString = c.getSubString(1, (int) c.length()); + return tempString; default: throw new ColumnTypeConversionException(agent_.logWriter_, "java.sql.Types " + jdbcTypes_[column -1], "String"); @@ -1002,8 +1052,9 @@ case java.sql.Types.LONGVARBINARY: return get_VARCHAR_FOR_BIT_DATA(column); case java.sql.Types.BLOB: - Blob b = (Blob) getBlobColumn_(column, agent_); - return b.getBytes(1, (int) b.length()); + Blob b = getBlobColumn_(column, agent_, false); + byte[] bytes = b.getBytes(1, (int) b.length()); + return bytes; default: throw new ColumnTypeConversionException(agent_.logWriter_, "java.sql.Types " + jdbcTypes_[column -1], "byte[]"); @@ -1013,32 +1064,43 @@ } } - public final java.io.InputStream getBinaryStream(int column) throws SqlException { - try { - switch (jdbcTypes_[column - 1]) { + public final java.io.InputStream getBinaryStream(int column) + throws SqlException + { + switch (jdbcTypes_[column - 1]) { case java.sql.Types.BINARY: return new java.io.ByteArrayInputStream(get_CHAR_FOR_BIT_DATA(column)); case java.sql.Types.VARBINARY: case java.sql.Types.LONGVARBINARY: return new java.io.ByteArrayInputStream(get_VARCHAR_FOR_BIT_DATA(column)); case java.sql.Types.BLOB: - Blob b = (Blob) getBlobColumn_(column, agent_); - return b.getBinaryStream(); + Blob b = getBlobColumn_(column, agent_, false); + if (b.isLocator()) { + BlobLocatorInputStream is + = new BlobLocatorInputStream(agent_.connection_, b); + return new BufferedInputStream(is); + } else { + return b.getBinaryStreamX(); + } default: throw new ColumnTypeConversionException(agent_.logWriter_, "java.sql.Types " + jdbcTypes_[column -1], "java.io.InputStream"); - } - } catch ( SQLException se ) { - throw new SqlException(se); } } - public final java.io.InputStream getAsciiStream(int column) throws SqlException { - try { - switch (jdbcTypes_[column - 1]) { + public final java.io.InputStream getAsciiStream(int column) + throws SqlException + { + switch (jdbcTypes_[column - 1]) { case java.sql.Types.CLOB: - Clob c = getClobColumn_(column, agent_); - return c.getAsciiStream(); + Clob c = getClobColumn_(column, agent_, false); + if (c.isLocator()) { + ClobLocatorInputStream is + = new ClobLocatorInputStream(agent_.connection_, c); + return new BufferedInputStream(is); + } else { + return c.getAsciiStreamX(); + } case java.sql.Types.CHAR: try { return new java.io.ByteArrayInputStream(getCHAR(column).getBytes("ISO-8859-1")); @@ -1062,24 +1124,19 @@ case java.sql.Types.LONGVARBINARY: return new java.io.ByteArrayInputStream(get_VARCHAR_FOR_BIT_DATA(column)); case java.sql.Types.BLOB: - Blob b = (Blob) getBlobColumn_(column, agent_); - return b.getBinaryStream(); + return getBinaryStream(column); default: throw new ColumnTypeConversionException(agent_.logWriter_, "java.sql.Types " + jdbcTypes_[column -1], "java.io.InputStream"); - } } - catch ( SQLException se ) { - throw new SqlException(se); - } } - + public final java.io.InputStream getUnicodeStream(int column) throws SqlException { try { switch (jdbcTypes_[column - 1]) { case java.sql.Types.CLOB: { - Clob c = getClobColumn_(column, agent_); + Clob c = getClobColumn_(column, agent_, false); String s = c.getSubString(1L, (int) c.length()); try { return new java.io.ByteArrayInputStream(s.getBytes("UTF-8")); @@ -1114,8 +1171,7 @@ case java.sql.Types.LONGVARBINARY: return new java.io.ByteArrayInputStream(get_VARCHAR_FOR_BIT_DATA(column)); case java.sql.Types.BLOB: - Blob b = (Blob) getBlobColumn_(column, agent_); - return b.getBinaryStream(); + return getBinaryStream(column); default: throw new ColumnTypeConversionException(agent_.logWriter_, "java.sql.Types " + jdbcTypes_[column -1], "UnicodeStream"); @@ -1125,12 +1181,19 @@ } } - public final java.io.Reader getCharacterStream(int column) throws SqlException { - try { - switch (jdbcTypes_[column - 1]) { + public final java.io.Reader getCharacterStream(int column) + throws SqlException + { + switch (jdbcTypes_[column - 1]) { case java.sql.Types.CLOB: - Clob c = getClobColumn_(column, agent_); - return c.getCharacterStream(); + Clob c = getClobColumn_(column, agent_, false); + if (c.isLocator()) { + ClobLocatorReader reader + = new ClobLocatorReader(agent_.connection_, c); + return new BufferedReader(reader); + } else { + return c.getCharacterStreamX(); + } case java.sql.Types.CHAR: return new java.io.StringReader(getCHAR(column)); case java.sql.Types.VARCHAR: @@ -1155,8 +1218,8 @@ } case java.sql.Types.BLOB: try { - Blob b = (Blob) getBlobColumn_(column, agent_); - return new java.io.InputStreamReader(b.getBinaryStream(), "UTF-16BE"); + return new java.io.InputStreamReader(getBinaryStream(column), + "UTF-16BE"); } catch (java.io.UnsupportedEncodingException e) { throw new SqlException(agent_.logWriter_, new ClientMessageId (SQLState.UNSUPPORTED_ENCODING), @@ -1166,15 +1229,12 @@ throw new ColumnTypeConversionException(agent_.logWriter_, "java.sql.Types " + jdbcTypes_[column -1], "java.io.Reader"); } - } catch ( SQLException se ) { - throw new SqlException(se); - } } public final java.sql.Blob getBlob(int column) throws SqlException { switch (jdbcTypes_[column - 1]) { case Types.BLOB: - return getBlobColumn_(column, agent_); + return getBlobColumn_(column, agent_, true); default: throw new ColumnTypeConversionException(agent_.logWriter_, "java.sql.Types " + jdbcTypes_[column -1], "java.sql.Blob"); @@ -1184,7 +1244,7 @@ public final java.sql.Clob getClob(int column) throws SqlException { switch (jdbcTypes_[column - 1]) { case Types.CLOB: - return getClobColumn_(column, agent_); + return getClobColumn_(column, agent_, true); default: throw new ColumnTypeConversionException(agent_.logWriter_, "java.sql.Types " + jdbcTypes_[column -1], "java.sql.Clob"); @@ -1233,9 +1293,9 @@ case java.sql.Types.LONGVARBINARY: return get_VARCHAR_FOR_BIT_DATA(column); case java.sql.Types.BLOB: - return getBlobColumn_(column, agent_); + return getBlobColumn_(column, agent_, true); case java.sql.Types.CLOB: - return getClobColumn_(column, agent_); + return getClobColumn_(column, agent_, true); default: throw new ColumnTypeConversionException(agent_.logWriter_, "java.sql.Types " + jdbcTypes_[column -1], "Object"); Index: java/client/org/apache/derby/client/am/Blob.java =================================================================== --- java/client/org/apache/derby/client/am/Blob.java (revision 574730) +++ java/client/org/apache/derby/client/am/Blob.java (working copy) @@ -21,7 +21,6 @@ package org.apache.derby.client.am; -import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -260,7 +259,7 @@ } } - private java.io.InputStream getBinaryStreamX() throws SqlException { + java.io.InputStream getBinaryStreamX() throws SqlException { checkForClosedConnection(); if (isBinaryStream()) // this Lob is used for input @@ -651,8 +650,6 @@ * @throws SQLException if pos is less than 1 or if pos is greater than * the number of bytes in the Blob or if pos + length is * greater than the number of bytes in the Blob - * - * @throws SQLException. */ public InputStream getBinaryStream(long pos, long length) throws SQLException { Index: java/client/org/apache/derby/client/am/Clob.java =================================================================== --- java/client/org/apache/derby/client/am/Clob.java (revision 574730) +++ java/client/org/apache/derby/client/am/Clob.java (working copy) @@ -363,7 +363,7 @@ } } - private java.io.Reader getCharacterStreamX() throws SqlException { + java.io.Reader getCharacterStreamX() throws SqlException { checkForClosedConnection(); //check is this Lob is locator enabled @@ -409,7 +409,7 @@ } } - private java.io.InputStream getAsciiStreamX() throws SqlException { + java.io.InputStream getAsciiStreamX() throws SqlException { checkForClosedConnection(); if (isAsciiStream()) // this Lob is used for input Index: java/client/org/apache/derby/client/am/ResultSet.java =================================================================== --- java/client/org/apache/derby/client/am/ResultSet.java (revision 574730) +++ java/client/org/apache/derby/client/am/ResultSet.java (working copy) @@ -28,6 +28,7 @@ import org.apache.derby.client.am.SQLExceptionFactory; import org.apache.derby.shared.common.reference.SQLState; import org.apache.derby.shared.common.i18n.MessageUtil; +import org.apache.derby.shared.common.sanity.SanityManager; public abstract class ResultSet implements java.sql.ResultSet, ResultSetCallbackInterface { @@ -37,6 +38,8 @@ public ColumnMetaData resultSetMetaData_; // As obtained from the SQLDA private SqlWarning warnings_; public Cursor cursor_; + /** Tracker object for LOB state, used to free locators on the server. */ + private LOBStateTracker lobState = null; protected Agent agent_; public Section generatedSection_ = null; @@ -426,6 +429,13 @@ if (!openOnClient_) { return; } + closeCloseFilterInputStream(); + // See if there are open locators on the current row, if valid. + if (isValidCursorPosition_ && !isOnInsertRow_) { + lobState.checkCurrentRow(cursor_); + } + // NOTE: The preClose_ method must also check for locators if + // prefetching of data is enabled for result sets containing LOBs. preClose_(); try { if (openOnServer_) { @@ -3753,6 +3763,12 @@ } } + /** + * Moves off the insert row if positioned there, and checks the current row + * for releasable LOB locators if positioned on a valid data row. + * + * @throws SqlException if releasing a LOB locator fails + */ private void moveToCurrentRowX() throws SqlException { if (isOnInsertRow_) { resetUpdatedColumns(); @@ -3763,6 +3779,14 @@ } isValidCursorPosition_ = true; } + if (isValidCursorPosition_) { + // isOnInsertRow must be false here. + if (SanityManager.DEBUG) { + SanityManager.ASSERT(!isOnInsertRow_, + "Cannot check current row if positioned on insert row"); + } + lobState.checkCurrentRow(cursor_); + } } /** @@ -4329,6 +4353,7 @@ public void completeLocalCommit(java.util.Iterator listenerIterator) { cursorUnpositionedOnServer_ = true; + lobState.discardState(); // Locators released on server side. markAutoCommitted(); if (!cursorHold_) { // only non-held cursors need to be closed at commit @@ -4341,6 +4366,7 @@ } public void completeLocalRollback(java.util.Iterator listenerIterator) { + lobState.discardState(); // Locators released on server side. markAutoCommitted(); // all cursors need to be closed at rollback markClosed(); @@ -6164,4 +6190,52 @@ throw se.getSQLException(); } } + + /** + * Marks the LOB at the specified column as accessed. + *

+ * When a LOB is marked as accessed, the release mechanism will not be + * invoked by the result set. It is expected that the code accessing the + * LOB releases the locator when it is done with the LOB. + * + * @param index 1-based column index + */ + public final void markLOBAsAccessed(int index) { + this.lobState.markAccessed(index); + } + + /** + * Initializes the LOB state tracker. + *

+ * The state tracker is used to free LOB locators on the server. + */ + final void createLOBColumnTracker() { + if (SanityManager.DEBUG) { + SanityManager.ASSERT(this.lobState == null, + "LOB state tracker already initialized."); + } + if (this.resultSetMetaData_.hasLobColumns()) { + final int columnCount = this.resultSetMetaData_.columns_; + int lobCount = 0; + int[] tmpIndexes = new int[columnCount]; + boolean[] tmpIsBlob = new boolean[columnCount]; + for (int i=0; i < columnCount; i++) { + int type = this.resultSetMetaData_.types_[i]; + if (type == Types.BLOB || type == Types.CLOB) { + tmpIndexes[lobCount] = i +1; // Convert to 1-based index. + tmpIsBlob[lobCount++] = (type == Types.BLOB); + } + } + // Create a tracker for the LOB columns found. + int[] lobIndexes = new int[lobCount]; + boolean[] isBlob = new boolean[lobCount]; + System.arraycopy(tmpIndexes, 0, lobIndexes, 0, lobCount); + System.arraycopy(tmpIsBlob, 0, isBlob, 0, lobCount); + this.lobState = new LOBStateTracker(lobIndexes, isBlob, + this.connection_.serverSupportsLocators()); + } else { + // Use a no-op state tracker to simplify code expecting a tracker. + this.lobState = LOBStateTracker.NO_OP_TRACKER; + } + } } Index: java/client/org/apache/derby/client/am/UpdateSensitiveLOBLocatorInputStream.java =================================================================== --- java/client/org/apache/derby/client/am/UpdateSensitiveLOBLocatorInputStream.java (revision 574730) +++ java/client/org/apache/derby/client/am/UpdateSensitiveLOBLocatorInputStream.java (working copy) @@ -160,6 +160,13 @@ currentPos += ret; return ret; } + + public void close() throws IOException + { + if (is != null) { + is.close(); + } + } /** * Verifies whether the current updateCount matches