Index: java/client/org/apache/derby/client/net/NetConnection.java
===================================================================
--- java/client/org/apache/derby/client/net/NetConnection.java	(revision 642908)
+++ java/client/org/apache/derby/client/net/NetConnection.java	(working copy)
@@ -1768,6 +1768,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 642908)
+++ java/client/org/apache/derby/client/net/NetCursor.java	(working copy)
@@ -1054,11 +1054,14 @@
     
     /**
      * Get locator for LOB of the designated column
+     * <p>
+     * Note that this method cannot be invoked on a LOB column that is NULL.
+     *
      * @param column column number, starts at 1
      * @return locator value, <code>Lob.INVALID_LOCATOR</code> 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, the value will be
@@ -1078,6 +1081,7 @@
         // Check for locator
         int locator = locator(column);
         if (locator > 0) { // Create locator-based LOB object
+            netResultSet_.markLOBAsAccessed(column);
             return new Blob(agent, locator);
         }
         
@@ -1112,6 +1116,7 @@
         // Check for locator
         int locator = locator(column);
         if (locator > 0) { // Create locator-based LOB object
+            netResultSet_.markLOBAsAccessed(column);
             return new Clob(agent, locator);
         }
         
Index: java/client/org/apache/derby/client/am/Connection.java
===================================================================
--- java/client/org/apache/derby/client/am/Connection.java	(revision 642908)
+++ java/client/org/apache/derby/client/am/Connection.java	(working copy)
@@ -1000,6 +1000,13 @@
      */
     protected abstract boolean supportsSessionDataCaching();
 
+    /**
+     * 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 642908)
+++ java/client/org/apache/derby/client/am/Statement.java	(working copy)
@@ -1489,6 +1489,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.
@@ -1521,6 +1523,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 642908)
+++ java/client/org/apache/derby/client/am/Cursor.java	(working copy)
@@ -661,6 +661,29 @@
         return recyclableCalendar_;
     }
 
+    /**
+     * Returns a reference to the locator procedures.
+     * <p>
+     * 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();
+    }
+
+    /**
+     * Obtains the locator for the specified LOB column.
+     * <p>
+     * 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);
+
     abstract public Blob getBlobColumn_(int column, Agent agent) throws SqlException;
 
     abstract public Clob getClobColumn_(int column, Agent agent) throws SqlException;
Index: java/client/org/apache/derby/client/am/NoOpLOBTracker.java
===================================================================
--- java/client/org/apache/derby/client/am/NoOpLOBTracker.java	(revision 0)
+++ java/client/org/apache/derby/client/am/NoOpLOBTracker.java	(revision 0)
@@ -0,0 +1,70 @@
+/*
+
+   Derby - Class org.apache.derby.client.am.NoOpLOBTracker
+
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+*/
+package org.apache.derby.client.am;
+
+import org.apache.derby.shared.common.sanity.SanityManager;
+
+/**
+ * A tracker implementation that does nothing, intended to be used when
+ * locators are not supported by the server or when there are no LOBs in the
+ * result set.
+ */
+//@Immutable
+final class NoOpLOBTracker
+    implements LOBStateTracker {
+
+    /** Singleton instance. */
+    public static final LOBStateTracker NO_OP_TRACKER =
+            new NoOpLOBTracker();
+
+    /** Cached empty integer array. */
+    private static final int[] EMPTY_INT_ARRAY = new int[0];
+
+    /**
+     * Private constructor to enforce singleton pattern.
+     *
+     * @see #NO_OP_TRACKER
+     */
+    private NoOpLOBTracker() {}
+
+    public void checkCurrentRow(Cursor ignore) {
+        // Does nothing.
+    }
+
+    public void noRelease(int ignore) {
+        // Does nothing.
+        if (SanityManager.DEBUG) {
+            // Indicates a programming error if called when locators are not
+            // supported or there are no LOBs in the result set.
+            SanityManager.THROWASSERT("NoOpLOBTracker.noRelease should " +
+                    "not be called when locators are not supported.");
+        }
+    }
+
+    public void discardState() {
+        // Does nothing.
+    }
+
+    public int[] locatorsForRelease() {
+        // Always return an array of length zero.
+        return EMPTY_INT_ARRAY;
+    }
+}

Property changes on: java/client/org/apache/derby/client/am/NoOpLOBTracker.java
___________________________________________________________________
Name: svn:eol-style
   + native

Index: java/client/org/apache/derby/client/am/LOBStateTracker.java
===================================================================
--- java/client/org/apache/derby/client/am/LOBStateTracker.java	(revision 0)
+++ java/client/org/apache/derby/client/am/LOBStateTracker.java	(revision 0)
@@ -0,0 +1,65 @@
+/*
+
+   Derby - Class org.apache.derby.client.am.LOBStateTracker
+
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+*/
+package org.apache.derby.client.am;
+
+/**
+ * Interface defining methods of an object that tracks the state of large
+ * objects (LOBs) in a result set and is capable of releasing LOB locators on
+ * the server.
+ */
+public interface LOBStateTracker {
+
+    /**
+     * Checks the current row, updating state and releasing locators on the
+     * server if required.
+     *
+     * @param cursor the cursor object to use for releasing the locators
+     * @throws SqlException if releasing the locators on the server fails
+     */
+    void checkCurrentRow(Cursor cursor) throws SqlException;
+
+    /**
+     * Discards all recorded dynamic state about LOBs.
+     * <p>
+     * Typically called after connection commit or rollback, as those operations
+     * will release all locators on the server automatically. There is no need
+     * to release them from the client side in this case.
+     */
+    void discardState();
+
+    /**
+     * Marks the specified column of the current row as non-releasable.
+     * <p>
+     * Columns must be marked as non-releasable when a LOB object is created on
+     * the client, to avoid releasing the corresponding locator too early.
+     *
+     * @param index 1-based column index
+     */
+    void noRelease(int index);
+
+    /**
+     * Returns a list of LOB locators scheduled for release and resets the
+     * dynamic LOB state in the tracker instance.
+     *
+     * @return A list of LOB locators, or an empty list if there are none.
+     */
+    int[] locatorsForRelease();
+}

Property changes on: java/client/org/apache/derby/client/am/LOBStateTracker.java
___________________________________________________________________
Name: svn:eol-style
   + native

Index: java/client/org/apache/derby/client/am/ResultSet.java
===================================================================
--- java/client/org/apache/derby/client/am/ResultSet.java	(revision 642908)
+++ 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,7 @@
     public ColumnMetaData resultSetMetaData_; // As obtained from the SQLDA
     private SqlWarning warnings_;
     public Cursor cursor_;
+    private LOBStateTracker lobState = null;
     protected Agent agent_;
 
     public Section generatedSection_ = null;
@@ -426,6 +428,12 @@
         if (!openOnClient_) {
             return;
         }
+        // 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_) {
@@ -3767,6 +3775,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_);
+        }
     }
 
     /**
@@ -4338,6 +4354,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
@@ -4350,6 +4367,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();
@@ -6173,4 +6191,53 @@
             throw se.getSQLException();
         }
     }
+
+    /**
+     * Marks the LOB at the specified column as accessed.
+     * <p>
+     * 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.noRelease(index);
+    }
+
+    /**
+     * Initializes the LOB state tracker.
+     * <p>
+     * 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.connection_.serverSupportsLocators() &&
+                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 SingleReleaseLOBTracker(lobIndexes, isBlob);
+        } else {
+            // Use a no-op state tracker to simply code expecting a tracker.
+            this.lobState = NoOpLOBTracker.NO_OP_TRACKER;
+
+        }
+    }
 }
Index: java/client/org/apache/derby/client/am/SingleReleaseLOBTracker.java
===================================================================
--- java/client/org/apache/derby/client/am/SingleReleaseLOBTracker.java	(revision 0)
+++ java/client/org/apache/derby/client/am/SingleReleaseLOBTracker.java	(revision 0)
@@ -0,0 +1,165 @@
+/*
+
+   Derby - Class org.apache.derby.client.am.SingleReleaseLOBTracker
+
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF licenses this file to You under the Apache License, Version 2.0
+   (the "License"); you may not use this file except in compliance with
+   the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+
+*/
+
+package org.apache.derby.client.am;
+
+import java.util.Arrays;
+import org.apache.derby.shared.common.sanity.SanityManager;
+
+/**
+ * Tracks the state of large objects (LOBs) represented by locators in a
+ * result set.
+ * <p>
+ * The state of LOBs must be tracked to ensure they are released on the server
+ * in a timely manner. If too many locators are kept upen, the assoicated
+ * resources on the server will consume too much memory. There are also other
+ * possible side effects, like holding on to locks for too long.
+ * <p>
+ * Note that it only makes sense to use this class if locators are used.
+ * <p>
+ * This class should only be used against Derby server versions earlier than
+ * 10.4, as the release mechanism is ineffective. In 10.4, a more effective
+ * mechanism was implemented.
+ */
+//@NotThreadSafe
+class SingleReleaseLOBTracker implements LOBStateTracker {
+
+    /** Cached empty integer array. */
+    private static final int[] EMPTY_INT_ARRAY = new int[0];
+
+    /** The 1-based column indexes of the LOB columns. */
+    private final int[] columns;
+    /** Tells whether a LOB is a Blob or a Clob. */
+    private final boolean[] isBlob;
+    /** Which of the LOB columns we should track for the current row. */
+    private final boolean[] trackColumn;
+
+    /**
+     * Creates a LOB locator state tracker for the specified LOB columns.
+     *
+     * @param lobColumns the 1-based column indexes of the LOB columns
+     * @param isBlob whether the LOB is a Blob or not
+     */
+    SingleReleaseLOBTracker(int[] lobColumns, boolean[] isBlob) {
+        this.columns = lobColumns;
+        this.isBlob = isBlob;
+        this.trackColumn = new boolean[lobColumns.length];
+        if (lobColumns.length != isBlob.length) {
+            throw new IllegalArgumentException("Index array must have same " +
+                    "length as type array: " + lobColumns.length + " != " +
+                    isBlob.length);
+        }
+    }
+
+    /**
+     * Marks the specified column of the current row as non-releasable.
+     * <p>
+     * Columns must be marked as non-releasable when a LOB object is created on
+     * the client, to avoid releasing the corresponding locator too early.
+     *
+     * @param index 1-based column index
+     */
+    public void noRelease(int index) {
+        // Locate the 1-based column index in the internal array and update
+        // the corresponding state entry.
+        boolean indexFound = false; // For sanity check only.
+        for (int i=0; i < this.columns.length; i++) {
+            if (this.columns[i] == index) {
+                this.trackColumn[i] = false;
+                indexFound = true; // For sanity check only.
+                break;
+            }
+        }
+        if (SanityManager.DEBUG) {
+            SanityManager.ASSERT(indexFound,
+                    "Failed to locate 1-based index " + index);
+        }
+    }
+
+    /**
+     * Checks the current row, updating state and releasing locators on the
+     * server if required.
+     * <p>
+     * This method must only be called when the cursor is positioned on a valid
+     * row. The row must also be the current row. Failing to follow these
+     * requirements might cause the wrong locators to be released, or causing
+     * the cursor call to obtain the locator to fail.
+     *
+     * @param cursor the cursor object to use for releasing the locators
+     * @throws SqlException if releasing the locators on the server fails
+     */
+    public void checkCurrentRow(Cursor cursor)
+            throws SqlException {
+        final CallableLocatorProcedures procs = cursor.getLocatorProcedures();
+        // IMPLEMENTATION NOTE / OPTIMALIZATION:
+        // The block of code below is very naive, and can cause a round trip
+        // for each LOB for each rs.next (or other navigational method).
+        // There are two possible optimizations:
+        //  (a) Implement a new stored procedure freeing a list of locators,
+        //      and free locators when a threshold is reached.
+        //  (b) Piggy-back list of locators on other requests.
+        //
+        // Optimization (a) is easy and will most likely be implemented right
+        // away. (b) is more complex, but frees resources optimally at almost no
+        // extra cost.
+        for (int i=0; i < this.columns.length; i++) {
+            // See if we have to check the column and if it is not NULL.
+            // NULL LOBs do not have a locator.
+            if (this.trackColumn[i] &&
+                    !cursor.isNull_[this.columns[i] -1]) {
+                // Fetch locator so we can free it.
+                int locator = cursor.locator(this.columns[i]);
+                if (locator != Lob.INVALID_LOCATOR) {
+                    if (this.isBlob[i]) {
+                        procs.blobReleaseLocator(locator);
+                    } else {
+                        procs.clobReleaseLocator(locator);
+                    }
+                }
+            } else {
+                // Reset state so we track the column for the next row.
+                this.trackColumn[i] = true;
+            }
+        }
+    }
+
+    /**
+     * Discards all recorded dynamic state about LOBs.
+     * <p>
+     * Typically called on connection commit or rollback, as this will release
+     * all locators on the server automatically. There is no need to release
+     * them from the client side.
+     */
+    public void discardState() {
+        Arrays.fill(this.trackColumn, false);
+    }
+
+    /**
+     * Returns an empty integer array, as this tracker object handles all
+     * release itself.
+     *
+     * @return An empty integer array.
+     */
+    public int[] locatorsForRelease() {
+        // We want to handle all release out self, so just return an empty list.
+        return EMPTY_INT_ARRAY;
+    }
+}

Property changes on: java/client/org/apache/derby/client/am/SingleReleaseLOBTracker.java
___________________________________________________________________
Name: svn:eol-style
   + native

