You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@shardingsphere.apache.org by zh...@apache.org on 2026/08/16 09:11:57 UTC

(shardingsphere) branch master updated: Refactor Firebird result row building and BLOB reads (#39484)

This is an automated email from the ASF dual-hosted git repository.

terrymanu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shardingsphere.git


The following commit(s) were added to refs/heads/master by this push:
     new 506aaf424be Refactor Firebird result row building and BLOB reads (#39484)
506aaf424be is described below

commit 506aaf424be4e84f888d1cb08831ec4f7fcea638
Author: Liang Zhang <zh...@apache.org>
AuthorDate: Sun Aug 16 17:11:50 2026 +0800

    Refactor Firebird result row building and BLOB reads (#39484)
---
 .../FirebirdGetBlobSegmentCommandExecutor.java     |  21 +---
 .../query/blob/cache/FirebirdBlobReadCache.java    |  78 +++++++++---
 .../query/info/FirebirdBlobInfoExecutor.java       |   8 +-
 .../query/statement/FirebirdBinaryRowBuilder.java  |  50 ++++++++
 .../FirebirdExecuteStatementCommandExecutor.java   |  14 +--
 .../FirebirdFetchStatementCommandExecutor.java     |  16 +--
 .../FirebirdGetBlobSegmentCommandExecutorTest.java |  43 +++++--
 .../blob/cache/FirebirdBlobReadCacheTest.java      | 139 +++++++++++++++++++++
 .../query/info/FirebirdBlobInfoExecutorTest.java   |  64 ++++++++--
 .../statement/FirebirdBinaryRowBuilderTest.java    |  54 ++++++++
 10 files changed, 407 insertions(+), 80 deletions(-)

diff --git a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/FirebirdGetBlobSegmentCommandExecutor.java b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/FirebirdGetBlobSegmentCommandExecutor.java
index ad2ccc0203d..bd646e2d905 100644
--- a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/FirebirdGetBlobSegmentCommandExecutor.java
+++ b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/FirebirdGetBlobSegmentCommandExecutor.java
@@ -25,9 +25,9 @@ import org.apache.shardingsphere.database.protocol.packet.DatabasePacket;
 import org.apache.shardingsphere.proxy.backend.session.ConnectionSession;
 import org.apache.shardingsphere.proxy.frontend.command.executor.CommandExecutor;
 import org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.cache.FirebirdBlobReadCache;
+import org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.cache.FirebirdBlobReadCache.BlobSegment;
 import org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.generator.FirebirdBlobHandleGenerator;
 
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Optional;
@@ -60,22 +60,13 @@ public final class FirebirdGetBlobSegmentCommandExecutor implements CommandExecu
     public Collection<DatabasePacket> execute() {
         int connectionId = connectionSession.getConnectionId();
         int blobHandle = FirebirdBlobHandleGenerator.getInstance().resolveBlobHandle(connectionId, packet.getBlobHandle());
-        Optional<byte[]> remaining = FirebirdBlobReadCache.getInstance().getSegment(connectionId, blobHandle);
-        if (!remaining.isPresent() || 0 == remaining.get().length) {
+        Optional<BlobSegment> blobSegment = FirebirdBlobReadCache.getInstance().readSegment(connectionId, blobHandle, getMaxSegmentDataLength());
+        if (!blobSegment.isPresent()) {
             return Collections.singleton(new FirebirdGenericResponsePacket().setHandle(SEGMENT_STATE_EOF));
         }
-        byte[] segment = remaining.get();
-        int segmentLength = Math.min(getMaxSegmentDataLength(), segment.length);
-        byte[] payloadSegment = Arrays.copyOf(segment, segmentLength);
-        int segmentState;
-        if (segmentLength >= segment.length) {
-            FirebirdBlobReadCache.getInstance().removeBlob(connectionId, blobHandle);
-            segmentState = SEGMENT_STATE_COMPLETE;
-        } else {
-            FirebirdBlobReadCache.getInstance().setSegment(connectionId, blobHandle, Arrays.copyOfRange(segment, segmentLength, segment.length));
-            segmentState = SEGMENT_STATE_PARTIAL;
-        }
-        FirebirdGetBlobSegmentResponsePacket responsePacket = new FirebirdGetBlobSegmentResponsePacket(payloadSegment);
+        BlobSegment segment = blobSegment.get();
+        int segmentState = segment.isComplete() ? SEGMENT_STATE_COMPLETE : SEGMENT_STATE_PARTIAL;
+        FirebirdGetBlobSegmentResponsePacket responsePacket = new FirebirdGetBlobSegmentResponsePacket(segment.getData());
         return Collections.singleton(new FirebirdGenericResponsePacket().setHandle(segmentState).setData(responsePacket));
     }
     
diff --git a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/cache/FirebirdBlobReadCache.java b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/cache/FirebirdBlobReadCache.java
index 166e7cd4dc2..b401f9db3cb 100644
--- a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/cache/FirebirdBlobReadCache.java
+++ b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/cache/FirebirdBlobReadCache.java
@@ -18,17 +18,21 @@
 package org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.cache;
 
 import lombok.AccessLevel;
+import lombok.Getter;
 import lombok.NoArgsConstructor;
+import lombok.RequiredArgsConstructor;
 
+import java.util.Arrays;
 import java.util.Map;
 import java.util.Optional;
+import java.util.OptionalInt;
 import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * Registry for Firebird BLOB reads buffered by the proxy.
+ * Cache for Firebird BLOB reads buffered by the proxy.
  *
  * <p>Read direction counterpart of the write cache: open_blob puts the whole BLOB content here,
- * and get_segment hands it out to the client chunk by chunk, replacing the stored value with the remainder.
+ * and get_segment hands it out to the client chunk by chunk using a cursor over the original content.
  * Entries are keyed by connection id and blob handle, so concurrent connections and BLOBs do not interfere.</p>
  */
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
@@ -36,7 +40,7 @@ public final class FirebirdBlobReadCache {
     
     private static final FirebirdBlobReadCache INSTANCE = new FirebirdBlobReadCache();
     
-    private final Map<Integer, Map<Integer, byte[]>> remainingSegments = new ConcurrentHashMap<>(16);
+    private final Map<Integer, Map<Integer, BlobReadCursor>> cursors = new ConcurrentHashMap<>(16);
     
     public static FirebirdBlobReadCache getInstance() {
         return INSTANCE;
@@ -48,7 +52,7 @@ public final class FirebirdBlobReadCache {
      * @param connectionId connection id
      */
     public void registerConnection(final int connectionId) {
-        remainingSegments.put(connectionId, new ConcurrentHashMap<>(4));
+        cursors.put(connectionId, new ConcurrentHashMap<>(4));
     }
     
     /**
@@ -57,7 +61,7 @@ public final class FirebirdBlobReadCache {
      * @param connectionId connection id
      */
     public void unregisterConnection(final int connectionId) {
-        remainingSegments.remove(connectionId);
+        cursors.remove(connectionId);
     }
     
     /**
@@ -68,29 +72,43 @@ public final class FirebirdBlobReadCache {
      * @param content blob content
      */
     public void registerBlob(final int connectionId, final int blobHandle, final byte[] content) {
-        getSegmentMap(connectionId).put(blobHandle, content);
+        getCursorMap(connectionId).put(blobHandle, new BlobReadCursor(content));
     }
     
     /**
-     * Get remaining segment data by handle.
+     * Read segment data by handle.
      *
      * @param connectionId connection id
      * @param blobHandle blob handle
-     * @return optional remaining segment data
+     * @param maximumLength maximum segment length
+     * @return optional segment data
      */
-    public Optional<byte[]> getSegment(final int connectionId, final int blobHandle) {
-        return Optional.ofNullable(getSegmentMap(connectionId).get(blobHandle));
+    public Optional<BlobSegment> readSegment(final int connectionId, final int blobHandle, final int maximumLength) {
+        Map<Integer, BlobReadCursor> cursorMap = getCursorMap(connectionId);
+        BlobReadCursor cursor = cursorMap.get(blobHandle);
+        if (null == cursor || 0 == cursor.getRemainingSize()) {
+            return Optional.empty();
+        }
+        int segmentLength = Math.min(maximumLength, cursor.getRemainingSize());
+        byte[] data = Arrays.copyOfRange(cursor.content, cursor.offset, cursor.offset + segmentLength);
+        cursor.offset += segmentLength;
+        boolean complete = 0 == cursor.getRemainingSize();
+        if (complete) {
+            cursorMap.remove(blobHandle);
+        }
+        return Optional.of(new BlobSegment(data, complete));
     }
     
     /**
-     * Replace remaining segment data by handle.
+     * Get remaining BLOB size by handle.
      *
      * @param connectionId connection id
      * @param blobHandle blob handle
-     * @param segment remaining segment data
+     * @return optional remaining BLOB size
      */
-    public void setSegment(final int connectionId, final int blobHandle, final byte[] segment) {
-        getSegmentMap(connectionId).put(blobHandle, segment);
+    public OptionalInt getRemainingSize(final int connectionId, final int blobHandle) {
+        BlobReadCursor cursor = getCursorMap(connectionId).get(blobHandle);
+        return null == cursor ? OptionalInt.empty() : OptionalInt.of(cursor.getRemainingSize());
     }
     
     /**
@@ -100,11 +118,35 @@ public final class FirebirdBlobReadCache {
      * @param blobHandle blob handle
      */
     public void removeBlob(final int connectionId, final int blobHandle) {
-        getSegmentMap(connectionId).remove(blobHandle);
+        getCursorMap(connectionId).remove(blobHandle);
     }
     
-    private Map<Integer, byte[]> getSegmentMap(final int connectionId) {
-        Map<Integer, byte[]> result = remainingSegments.get(connectionId);
-        return null == result ? remainingSegments.computeIfAbsent(connectionId, key -> new ConcurrentHashMap<>(4)) : result;
+    private Map<Integer, BlobReadCursor> getCursorMap(final int connectionId) {
+        Map<Integer, BlobReadCursor> result = cursors.get(connectionId);
+        return null == result ? cursors.computeIfAbsent(connectionId, key -> new ConcurrentHashMap<>(4)) : result;
+    }
+    
+    @RequiredArgsConstructor
+    private static final class BlobReadCursor {
+        
+        private final byte[] content;
+        
+        private int offset;
+        
+        private int getRemainingSize() {
+            return content.length - offset;
+        }
+    }
+    
+    /**
+     * Firebird BLOB segment.
+     */
+    @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+    @Getter
+    public static final class BlobSegment {
+        
+        private final byte[] data;
+        
+        private final boolean complete;
     }
 }
diff --git a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/info/FirebirdBlobInfoExecutor.java b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/info/FirebirdBlobInfoExecutor.java
index 364a2816a25..e4e87455096 100644
--- a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/info/FirebirdBlobInfoExecutor.java
+++ b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/info/FirebirdBlobInfoExecutor.java
@@ -30,7 +30,7 @@ import org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.gene
 
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Optional;
+import java.util.OptionalInt;
 
 /**
  * Blob info command executor for Firebird.
@@ -50,9 +50,9 @@ public final class FirebirdBlobInfoExecutor implements CommandExecutor {
     private int getBlobLength() {
         int connectionId = connectionSession.getConnectionId();
         int blobHandle = FirebirdBlobHandleGenerator.getInstance().resolveBlobHandle(connectionId, packet.getHandle());
-        Optional<byte[]> readSegment = FirebirdBlobReadCache.getInstance().getSegment(connectionId, blobHandle);
-        if (readSegment.isPresent()) {
-            return readSegment.get().length;
+        OptionalInt remainingSize = FirebirdBlobReadCache.getInstance().getRemainingSize(connectionId, blobHandle);
+        if (remainingSize.isPresent()) {
+            return remainingSize.getAsInt();
         }
         return FirebirdBlobWriteCache.getInstance().getBlobSizeByHandle(connectionId, blobHandle).orElse(0);
     }
diff --git a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/FirebirdBinaryRowBuilder.java b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/FirebirdBinaryRowBuilder.java
new file mode 100644
index 00000000000..9b16a8cabc2
--- /dev/null
+++ b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/FirebirdBinaryRowBuilder.java
@@ -0,0 +1,50 @@
+/*
+ * 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.shardingsphere.proxy.frontend.firebird.command.query.statement;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.shardingsphere.database.protocol.binary.BinaryCell;
+import org.apache.shardingsphere.database.protocol.binary.BinaryRow;
+import org.apache.shardingsphere.database.protocol.firebird.packet.command.query.FirebirdBinaryColumnType;
+import org.apache.shardingsphere.proxy.backend.response.data.QueryResponseCell;
+import org.apache.shardingsphere.proxy.backend.response.data.QueryResponseRow;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Firebird binary row builder.
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class FirebirdBinaryRowBuilder {
+    
+    /**
+     * Build binary row.
+     *
+     * @param row query response row
+     * @return binary row
+     */
+    public static BinaryRow build(final QueryResponseRow row) {
+        List<BinaryCell> result = new ArrayList<>(row.getCells().size());
+        for (QueryResponseCell each : row.getCells()) {
+            result.add(new BinaryCell(FirebirdBinaryColumnType.valueOfJDBCType(each.getJdbcType()), each.getData()));
+        }
+        return new BinaryRow(result);
+    }
+}
diff --git a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/execute/FirebirdExecuteStatementCommandExecutor.java b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/execute/FirebirdExecuteStatementCommandExecutor.java
index fdb0db60819..f10abfa61d0 100644
--- a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/execute/FirebirdExecuteStatementCommandExecutor.java
+++ b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/execute/FirebirdExecuteStatementCommandExecutor.java
@@ -22,7 +22,6 @@ import lombok.RequiredArgsConstructor;
 import org.apache.shardingsphere.database.connector.core.type.DatabaseType;
 import org.apache.shardingsphere.database.exception.firebird.exception.protocol.InvalidStatementHandleException;
 import org.apache.shardingsphere.database.exception.firebird.exception.protocol.InvalidTransactionHandleException;
-import org.apache.shardingsphere.database.protocol.binary.BinaryCell;
 import org.apache.shardingsphere.database.protocol.binary.BinaryRow;
 import org.apache.shardingsphere.database.protocol.firebird.packet.command.query.FirebirdBinaryColumnType;
 import org.apache.shardingsphere.database.protocol.firebird.packet.command.query.statement.execute.FirebirdExecuteStatementPacket;
@@ -38,7 +37,6 @@ import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader;
 import org.apache.shardingsphere.proxy.backend.context.ProxyContext;
 import org.apache.shardingsphere.proxy.backend.handler.ProxyBackendHandler;
 import org.apache.shardingsphere.proxy.backend.handler.ProxyBackendHandlerFactory;
-import org.apache.shardingsphere.proxy.backend.response.data.QueryResponseCell;
 import org.apache.shardingsphere.proxy.backend.response.data.QueryResponseRow;
 import org.apache.shardingsphere.proxy.backend.response.header.ResponseHeader;
 import org.apache.shardingsphere.proxy.backend.response.header.query.QueryResponseHeader;
@@ -48,12 +46,12 @@ import org.apache.shardingsphere.proxy.frontend.command.executor.CommandExecutor
 import org.apache.shardingsphere.proxy.frontend.command.executor.ResponseType;
 import org.apache.shardingsphere.proxy.frontend.firebird.command.query.FirebirdServerPreparedStatement;
 import org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.cache.FirebirdBlobWriteCache;
+import org.apache.shardingsphere.proxy.frontend.firebird.command.query.statement.FirebirdBinaryRowBuilder;
 import org.apache.shardingsphere.proxy.frontend.firebird.command.query.statement.FirebirdStatementResourceCleaner;
 import org.apache.shardingsphere.proxy.frontend.firebird.command.query.statement.fetch.FirebirdFetchStatementCache;
 import org.apache.shardingsphere.proxy.frontend.firebird.command.query.transaction.FirebirdTransactionIdGenerator;
 
 import java.sql.SQLException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedList;
 import java.util.List;
@@ -166,15 +164,7 @@ public final class FirebirdExecuteStatementCommandExecutor implements CommandExe
     
     private FirebirdSQLResponsePacket getSQLResponse() throws SQLException {
         QueryResponseRow queryResponseRow = proxyBackendHandler.getRowData();
-        BinaryRow row = createBinaryRow(queryResponseRow);
+        BinaryRow row = FirebirdBinaryRowBuilder.build(queryResponseRow);
         return new FirebirdSQLResponsePacket(row);
     }
-    
-    private BinaryRow createBinaryRow(final QueryResponseRow queryResponseRow) {
-        List<BinaryCell> result = new ArrayList<>(queryResponseRow.getCells().size());
-        for (QueryResponseCell each : queryResponseRow.getCells()) {
-            result.add(new BinaryCell(FirebirdBinaryColumnType.valueOfJDBCType(each.getJdbcType()), each.getData()));
-        }
-        return new BinaryRow(result);
-    }
 }
diff --git a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/fetch/FirebirdFetchStatementCommandExecutor.java b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/fetch/FirebirdFetchStatementCommandExecutor.java
index 153194c1563..5808855ae7e 100644
--- a/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/fetch/FirebirdFetchStatementCommandExecutor.java
+++ b/proxy/frontend/dialect/firebird/src/main/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/fetch/FirebirdFetchStatementCommandExecutor.java
@@ -17,24 +17,19 @@
 
 package org.apache.shardingsphere.proxy.frontend.firebird.command.query.statement.fetch;
 
-import org.apache.shardingsphere.database.protocol.binary.BinaryCell;
 import org.apache.shardingsphere.database.protocol.binary.BinaryRow;
-import org.apache.shardingsphere.database.protocol.firebird.packet.command.query.FirebirdBinaryColumnType;
 import org.apache.shardingsphere.database.protocol.firebird.packet.command.query.statement.FirebirdFetchStatementPacket;
 import org.apache.shardingsphere.database.protocol.firebird.packet.generic.FirebirdFetchResponsePacket;
 import org.apache.shardingsphere.database.protocol.packet.DatabasePacket;
 import org.apache.shardingsphere.proxy.backend.handler.ProxyBackendHandler;
-import org.apache.shardingsphere.proxy.backend.response.data.QueryResponseCell;
-import org.apache.shardingsphere.proxy.backend.response.data.QueryResponseRow;
 import org.apache.shardingsphere.proxy.backend.session.ConnectionSession;
 import org.apache.shardingsphere.proxy.frontend.command.executor.QueryCommandExecutor;
 import org.apache.shardingsphere.proxy.frontend.command.executor.ResponseType;
+import org.apache.shardingsphere.proxy.frontend.firebird.command.query.statement.FirebirdBinaryRowBuilder;
 
 import java.sql.SQLException;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.List;
 
 /**
  * Firebird fetch statement command executor.
@@ -77,7 +72,7 @@ public final class FirebirdFetchStatementCommandExecutor implements QueryCommand
         fetchCount--;
         if (0 <= fetchCount) {
             if (proxyBackendHandler.next()) {
-                BinaryRow row = createBinaryRow(proxyBackendHandler.getRowData());
+                BinaryRow row = FirebirdBinaryRowBuilder.build(proxyBackendHandler.getRowData());
                 return FirebirdFetchResponsePacket.getFetchRowPacket(row);
             } else {
                 connectionSession.getDatabaseConnectionManager().unmarkResourceInUse(proxyBackendHandler);
@@ -88,11 +83,4 @@ public final class FirebirdFetchStatementCommandExecutor implements QueryCommand
         return FirebirdFetchResponsePacket.getFetchEndPacket();
     }
     
-    private BinaryRow createBinaryRow(final QueryResponseRow queryResponseRow) {
-        List<BinaryCell> result = new ArrayList<>(queryResponseRow.getCells().size());
-        for (QueryResponseCell each : queryResponseRow.getCells()) {
-            result.add(new BinaryCell(FirebirdBinaryColumnType.valueOfJDBCType(each.getJdbcType()), each.getData()));
-        }
-        return new BinaryRow(result);
-    }
 }
diff --git a/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/FirebirdGetBlobSegmentCommandExecutorTest.java b/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/FirebirdGetBlobSegmentCommandExecutorTest.java
index 36a67a45c24..e21a8fedd6a 100644
--- a/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/FirebirdGetBlobSegmentCommandExecutorTest.java
+++ b/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/FirebirdGetBlobSegmentCommandExecutorTest.java
@@ -94,16 +94,25 @@ class FirebirdGetBlobSegmentCommandExecutorTest {
     }
     
     @Test
-    void assertExecuteWithZeroRequestedLengthExpectsPartial() {
+    void assertExecuteWithEmptyBlobExpectsEof() {
+        FirebirdBlobReadCache.getInstance().registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[0]);
+        FirebirdGetBlobSegmentCommandExecutor executor = new FirebirdGetBlobSegmentCommandExecutor(packet, connectionSession);
+        Collection<DatabasePacket> actualPackets = executor.execute();
+        FirebirdGenericResponsePacket actualGenericPacket = (FirebirdGenericResponsePacket) actualPackets.iterator().next();
+        assertThat(actualGenericPacket.getHandle(), is(2));
+        assertNull(actualGenericPacket.getData());
+    }
+    
+    @Test
+    void assertExecuteWithRequestedLengthBelowPrefixExpectsPartial() {
         FirebirdBlobReadCache.getInstance().registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{9, 8});
-        when(packet.getSegmentLength()).thenReturn(0);
+        when(packet.getSegmentLength()).thenReturn(1);
         FirebirdGetBlobSegmentCommandExecutor executor = new FirebirdGetBlobSegmentCommandExecutor(packet, connectionSession);
         Collection<DatabasePacket> actualPackets = executor.execute();
         FirebirdGenericResponsePacket actualGenericPacket = (FirebirdGenericResponsePacket) actualPackets.iterator().next();
         assertThat(actualGenericPacket.getHandle(), is(1));
         assertThat(getResponseSegment(actualGenericPacket).length, is(0));
-        byte[] actualRemainingSegment = FirebirdBlobReadCache.getInstance().getSegment(CONNECTION_ID, BLOB_HANDLE).orElse(new byte[0]);
-        assertThat(actualRemainingSegment, is(new byte[]{9, 8}));
+        assertThat(FirebirdBlobReadCache.getInstance().getRemainingSize(CONNECTION_ID, BLOB_HANDLE).getAsInt(), is(2));
     }
     
     @Test
@@ -116,8 +125,7 @@ class FirebirdGetBlobSegmentCommandExecutorTest {
         assertThat(actualGenericPacket.getHandle(), is(1));
         assertThat(actualGenericPacket.getData(), isA(FirebirdGetBlobSegmentResponsePacket.class));
         assertThat(getResponseSegment(actualGenericPacket), is(new byte[]{1, 2}));
-        byte[] actualRemainingSegment = FirebirdBlobReadCache.getInstance().getSegment(CONNECTION_ID, BLOB_HANDLE).orElse(new byte[0]);
-        assertThat(actualRemainingSegment, is(new byte[]{3}));
+        assertThat(FirebirdBlobReadCache.getInstance().getRemainingSize(CONNECTION_ID, BLOB_HANDLE).getAsInt(), is(1));
     }
     
     @Test
@@ -129,7 +137,7 @@ class FirebirdGetBlobSegmentCommandExecutorTest {
         FirebirdGenericResponsePacket actualGenericPacket = (FirebirdGenericResponsePacket) actualPackets.iterator().next();
         assertThat(actualGenericPacket.getHandle(), is(0));
         assertThat(getResponseSegment(actualGenericPacket), is(new byte[]{4, 5}));
-        assertFalse(FirebirdBlobReadCache.getInstance().getSegment(CONNECTION_ID, BLOB_HANDLE).isPresent());
+        assertFalse(FirebirdBlobReadCache.getInstance().getRemainingSize(CONNECTION_ID, BLOB_HANDLE).isPresent());
     }
     
     @Test
@@ -143,8 +151,23 @@ class FirebirdGetBlobSegmentCommandExecutorTest {
         FirebirdGenericResponsePacket actualGenericPacket = (FirebirdGenericResponsePacket) actualPackets.iterator().next();
         assertThat(actualGenericPacket.getHandle(), is(1));
         assertThat(getResponseSegment(actualGenericPacket).length, is(0xFFFF));
-        byte[] actualRemainingSegment = FirebirdBlobReadCache.getInstance().getSegment(CONNECTION_ID, BLOB_HANDLE).orElse(new byte[0]);
-        assertThat(actualRemainingSegment, is(new byte[]{42}));
+        assertThat(FirebirdBlobReadCache.getInstance().getRemainingSize(CONNECTION_ID, BLOB_HANDLE).getAsInt(), is(1));
+    }
+    
+    @Test
+    void assertExecuteConsecutivelyReturnsAllDataOnce() {
+        FirebirdBlobReadCache.getInstance().registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{1, 2, 3, 4, 5});
+        when(packet.getSegmentLength()).thenReturn(4);
+        FirebirdGetBlobSegmentCommandExecutor executor = new FirebirdGetBlobSegmentCommandExecutor(packet, connectionSession);
+        FirebirdGenericResponsePacket firstPacket = (FirebirdGenericResponsePacket) executor.execute().iterator().next();
+        assertThat(firstPacket.getHandle(), is(1));
+        assertThat(getResponseSegment(firstPacket), is(new byte[]{1, 2}));
+        FirebirdGenericResponsePacket secondPacket = (FirebirdGenericResponsePacket) executor.execute().iterator().next();
+        assertThat(secondPacket.getHandle(), is(1));
+        assertThat(getResponseSegment(secondPacket), is(new byte[]{3, 4}));
+        FirebirdGenericResponsePacket lastPacket = (FirebirdGenericResponsePacket) executor.execute().iterator().next();
+        assertThat(lastPacket.getHandle(), is(0));
+        assertThat(getResponseSegment(lastPacket), is(new byte[]{5}));
     }
     
     @Test
@@ -155,7 +178,7 @@ class FirebirdGetBlobSegmentCommandExecutorTest {
         Collection<DatabasePacket> actualPackets = executor.execute();
         FirebirdGenericResponsePacket actualGenericPacket = (FirebirdGenericResponsePacket) actualPackets.iterator().next();
         assertThat(actualGenericPacket.getHandle(), is(2));
-        assertThat(FirebirdBlobReadCache.getInstance().getSegment(2, BLOB_HANDLE).orElse(new byte[0]), is(new byte[]{1, 2, 3}));
+        assertThat(FirebirdBlobReadCache.getInstance().getRemainingSize(2, BLOB_HANDLE).getAsInt(), is(3));
         FirebirdBlobReadCache.getInstance().unregisterConnection(2);
     }
     
diff --git a/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/cache/FirebirdBlobReadCacheTest.java b/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/cache/FirebirdBlobReadCacheTest.java
new file mode 100644
index 00000000000..db6a74f02a9
--- /dev/null
+++ b/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/blob/cache/FirebirdBlobReadCacheTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.shardingsphere.proxy.frontend.firebird.command.query.blob.cache;
+
+import org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.cache.FirebirdBlobReadCache.BlobSegment;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+import java.util.OptionalInt;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class FirebirdBlobReadCacheTest {
+    
+    private static final FirebirdBlobReadCache CACHE = FirebirdBlobReadCache.getInstance();
+    
+    private static final int CONNECTION_ID = 1;
+    
+    private static final int OTHER_CONNECTION_ID = 2;
+    
+    private static final int BLOB_HANDLE = 3;
+    
+    @AfterEach
+    void tearDown() {
+        CACHE.unregisterConnection(CONNECTION_ID);
+        CACHE.unregisterConnection(OTHER_CONNECTION_ID);
+    }
+    
+    @Test
+    void assertUnregisterConnectionRemovesBlobs() {
+        CACHE.registerConnection(CONNECTION_ID);
+        CACHE.registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{1});
+        CACHE.unregisterConnection(CONNECTION_ID);
+        assertFalse(CACHE.getRemainingSize(CONNECTION_ID, BLOB_HANDLE).isPresent());
+    }
+    
+    @Test
+    void assertRegisterBlobKeepsContentReference() {
+        CACHE.registerConnection(CONNECTION_ID);
+        byte[] content = new byte[]{1, 2};
+        CACHE.registerBlob(CONNECTION_ID, BLOB_HANDLE, content);
+        content[0] = 9;
+        Optional<BlobSegment> actual = CACHE.readSegment(CONNECTION_ID, BLOB_HANDLE, content.length);
+        assertTrue(actual.isPresent());
+        assertThat(actual.get().getData(), is(new byte[]{9, 2}));
+        assertThat(actual.get().getData(), not(sameInstance(content)));
+    }
+    
+    @Test
+    void assertReadMissingBlob() {
+        CACHE.registerConnection(CONNECTION_ID);
+        assertFalse(CACHE.readSegment(CONNECTION_ID, BLOB_HANDLE, 1).isPresent());
+    }
+    
+    @Test
+    void assertReadEmptyBlob() {
+        CACHE.registerConnection(CONNECTION_ID);
+        CACHE.registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[0]);
+        assertFalse(CACHE.readSegment(CONNECTION_ID, BLOB_HANDLE, 1).isPresent());
+        OptionalInt remainingSize = CACHE.getRemainingSize(CONNECTION_ID, BLOB_HANDLE);
+        assertTrue(remainingSize.isPresent());
+        assertThat(remainingSize.getAsInt(), is(0));
+    }
+    
+    @Test
+    void assertReadPartialSegmentAdvancesCursor() {
+        CACHE.registerConnection(CONNECTION_ID);
+        CACHE.registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{1, 2, 3});
+        Optional<BlobSegment> actual = CACHE.readSegment(CONNECTION_ID, BLOB_HANDLE, 2);
+        assertTrue(actual.isPresent());
+        assertThat(actual.get().getData(), is(new byte[]{1, 2}));
+        assertFalse(actual.get().isComplete());
+        assertThat(CACHE.getRemainingSize(CONNECTION_ID, BLOB_HANDLE).getAsInt(), is(1));
+    }
+    
+    @Test
+    void assertReadCompleteSegmentRemovesCursor() {
+        CACHE.registerConnection(CONNECTION_ID);
+        CACHE.registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{1, 2});
+        Optional<BlobSegment> actual = CACHE.readSegment(CONNECTION_ID, BLOB_HANDLE, 2);
+        assertTrue(actual.isPresent());
+        assertThat(actual.get().getData(), is(new byte[]{1, 2}));
+        assertTrue(actual.get().isComplete());
+        assertFalse(CACHE.getRemainingSize(CONNECTION_ID, BLOB_HANDLE).isPresent());
+    }
+    
+    @Test
+    void assertReadZeroLengthDoesNotAdvanceCursor() {
+        CACHE.registerConnection(CONNECTION_ID);
+        CACHE.registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{1, 2});
+        Optional<BlobSegment> actual = CACHE.readSegment(CONNECTION_ID, BLOB_HANDLE, 0);
+        assertTrue(actual.isPresent());
+        assertThat(actual.get().getData(), is(new byte[0]));
+        assertFalse(actual.get().isComplete());
+        assertThat(CACHE.getRemainingSize(CONNECTION_ID, BLOB_HANDLE).getAsInt(), is(2));
+    }
+    
+    @Test
+    void assertRemoveBlob() {
+        CACHE.registerConnection(CONNECTION_ID);
+        CACHE.registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{1});
+        CACHE.removeBlob(CONNECTION_ID, BLOB_HANDLE);
+        assertFalse(CACHE.getRemainingSize(CONNECTION_ID, BLOB_HANDLE).isPresent());
+    }
+    
+    @Test
+    void assertConnectionsAndHandlesAreIsolated() {
+        CACHE.registerConnection(CONNECTION_ID);
+        CACHE.registerConnection(OTHER_CONNECTION_ID);
+        CACHE.registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{1});
+        CACHE.registerBlob(CONNECTION_ID, BLOB_HANDLE + 1, new byte[]{2, 3});
+        CACHE.registerBlob(OTHER_CONNECTION_ID, BLOB_HANDLE, new byte[]{4, 5, 6});
+        CACHE.readSegment(CONNECTION_ID, BLOB_HANDLE, 1);
+        assertFalse(CACHE.getRemainingSize(CONNECTION_ID, BLOB_HANDLE).isPresent());
+        assertThat(CACHE.getRemainingSize(CONNECTION_ID, BLOB_HANDLE + 1).getAsInt(), is(2));
+        assertThat(CACHE.getRemainingSize(OTHER_CONNECTION_ID, BLOB_HANDLE).getAsInt(), is(3));
+    }
+}
diff --git a/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/info/FirebirdBlobInfoExecutorTest.java b/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/info/FirebirdBlobInfoExecutorTest.java
index c7333b2e1ea..95692641a87 100644
--- a/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/info/FirebirdBlobInfoExecutorTest.java
+++ b/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/info/FirebirdBlobInfoExecutorTest.java
@@ -22,6 +22,11 @@ import org.apache.shardingsphere.database.protocol.firebird.packet.command.query
 import org.apache.shardingsphere.database.protocol.firebird.packet.generic.FirebirdGenericResponsePacket;
 import org.apache.shardingsphere.database.protocol.packet.DatabasePacket;
 import org.apache.shardingsphere.proxy.backend.session.ConnectionSession;
+import org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.cache.FirebirdBlobReadCache;
+import org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.cache.FirebirdBlobWriteCache;
+import org.apache.shardingsphere.proxy.frontend.firebird.command.query.blob.generator.FirebirdBlobHandleGenerator;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.Mock;
@@ -31,25 +36,70 @@ import java.util.Collection;
 import java.util.Collections;
 
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.isA;
+import static org.hamcrest.Matchers.is;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
 class FirebirdBlobInfoExecutorTest {
     
+    private static final int CONNECTION_ID = 1;
+    
+    private static final int BLOB_HANDLE = 2;
+    
     @Mock
     private FirebirdInfoPacket packet;
     
     @Mock
     private ConnectionSession connectionSession;
     
-    @Test
-    void assertExecute() {
+    @BeforeEach
+    void setUp() {
+        FirebirdBlobHandleGenerator.getInstance().registerConnection(CONNECTION_ID);
+        FirebirdBlobReadCache.getInstance().registerConnection(CONNECTION_ID);
+        FirebirdBlobWriteCache.getInstance().registerConnection(CONNECTION_ID);
+        when(connectionSession.getConnectionId()).thenReturn(CONNECTION_ID);
+        when(packet.getHandle()).thenReturn(BLOB_HANDLE);
         when(packet.getInfoItems()).thenReturn(Collections.emptyList());
-        FirebirdBlobInfoExecutor executor = new FirebirdBlobInfoExecutor(packet, connectionSession);
-        Collection<DatabasePacket> actual = executor.execute();
-        assertThat(actual.iterator().next(), isA(FirebirdGenericResponsePacket.class));
+    }
+    
+    @AfterEach
+    void tearDown() {
+        FirebirdBlobHandleGenerator.getInstance().unregisterConnection(CONNECTION_ID);
+        FirebirdBlobReadCache.getInstance().unregisterConnection(CONNECTION_ID);
+        FirebirdBlobWriteCache.getInstance().unregisterConnection(CONNECTION_ID);
+    }
+    
+    @Test
+    void assertExecuteWithReadBlob() {
+        FirebirdBlobReadCache.getInstance().registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{1, 2, 3});
+        Collection<DatabasePacket> actual = new FirebirdBlobInfoExecutor(packet, connectionSession).execute();
+        assertThat(getBlobLength(actual), is(3));
+    }
+    
+    @Test
+    void assertExecuteWithPartiallyReadBlob() {
+        FirebirdBlobReadCache.getInstance().registerBlob(CONNECTION_ID, BLOB_HANDLE, new byte[]{1, 2, 3});
+        FirebirdBlobReadCache.getInstance().readSegment(CONNECTION_ID, BLOB_HANDLE, 1);
+        Collection<DatabasePacket> actual = new FirebirdBlobInfoExecutor(packet, connectionSession).execute();
+        assertThat(getBlobLength(actual), is(2));
+    }
+    
+    @Test
+    void assertExecuteWithWriteBlob() {
+        FirebirdBlobWriteCache.getInstance().registerBlob(CONNECTION_ID, BLOB_HANDLE, 3L);
+        FirebirdBlobWriteCache.getInstance().appendSegment(CONNECTION_ID, BLOB_HANDLE, new byte[]{1, 2, 3, 4});
+        Collection<DatabasePacket> actual = new FirebirdBlobInfoExecutor(packet, connectionSession).execute();
+        assertThat(getBlobLength(actual), is(4));
+    }
+    
+    @Test
+    void assertExecuteWithMissingBlob() {
+        Collection<DatabasePacket> actual = new FirebirdBlobInfoExecutor(packet, connectionSession).execute();
+        assertThat(getBlobLength(actual), is(0));
+    }
+    
+    private int getBlobLength(final Collection<DatabasePacket> actual) {
         FirebirdGenericResponsePacket actualResponsePacket = (FirebirdGenericResponsePacket) actual.iterator().next();
-        assertThat(actualResponsePacket.getData(), isA(FirebirdBlobInfoReturnPacket.class));
+        return ((FirebirdBlobInfoReturnPacket) actualResponsePacket.getData()).getBlobLength();
     }
 }
diff --git a/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/FirebirdBinaryRowBuilderTest.java b/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/FirebirdBinaryRowBuilderTest.java
new file mode 100644
index 00000000000..12dc6ae8ff7
--- /dev/null
+++ b/proxy/frontend/dialect/firebird/src/test/java/org/apache/shardingsphere/proxy/frontend/firebird/command/query/statement/FirebirdBinaryRowBuilderTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.shardingsphere.proxy.frontend.firebird.command.query.statement;
+
+import org.apache.shardingsphere.database.protocol.binary.BinaryCell;
+import org.apache.shardingsphere.database.protocol.binary.BinaryRow;
+import org.apache.shardingsphere.database.protocol.firebird.packet.command.query.FirebirdBinaryColumnType;
+import org.apache.shardingsphere.proxy.backend.response.data.QueryResponseCell;
+import org.apache.shardingsphere.proxy.backend.response.data.QueryResponseRow;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Types;
+import java.util.Arrays;
+import java.util.Iterator;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class FirebirdBinaryRowBuilderTest {
+    
+    @Test
+    void assertBuildPreservesCellOrderAndData() {
+        byte[] expectedData = new byte[]{1, 2};
+        QueryResponseRow row = new QueryResponseRow(Arrays.asList(new QueryResponseCell(Types.BLOB, expectedData), new QueryResponseCell(Types.INTEGER, null)));
+        BinaryRow actual = FirebirdBinaryRowBuilder.build(row);
+        assertThat(actual.getCells().size(), is(2));
+        Iterator<BinaryCell> iterator = actual.getCells().iterator();
+        BinaryCell firstCell = iterator.next();
+        assertThat(firstCell.getColumnType(), is(FirebirdBinaryColumnType.BLOB));
+        assertThat(firstCell.getData(), sameInstance(expectedData));
+        BinaryCell secondCell = iterator.next();
+        assertThat(secondCell.getColumnType(), is(FirebirdBinaryColumnType.LONG));
+        assertNull(secondCell.getData());
+        assertFalse(iterator.hasNext());
+    }
+}