diff --git a/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseLobValuesTest.java b/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseLobValuesTest.java index 8bc24fb5af..c4274e159b 100644 --- a/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseLobValuesTest.java +++ b/agents/drivers/oceanbase-oracle/src/test/java/com/dbx/agent/oceanbaseoracle/OceanBaseLobValuesTest.java @@ -14,6 +14,32 @@ import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; class OceanBaseLobValuesTest { + @Test + void idleAndAbsoluteExpiryFreeLocatorsAndRestorePayloadCapacity() throws Exception { + OceanBaseLobValues values = new OceanBaseLobValues((connection, locator, offset, limit) -> chunk("x".repeat(1000), offset, limit), ignored -> 40L * 1024 * 1024); + for (String fieldName : List.of("accessed", "created")) { + Fixture fixture = new Fixture(); + var preview = (OceanBaseLobValues.Preview) values.preview(fixture.resultSet, 1, Types.CLOB, "CLOB"); + var entriesField = OceanBaseLobValues.class.getDeclaredField("entries"); + entriesField.setAccessible(true); + Object entry = ((java.util.Map) entriesField.get(values)).get(preview.ref()); + var ageField = entry.getClass().getDeclaredField(fieldName); + ageField.setAccessible(true); + ageField.setLong(entry, System.currentTimeMillis() - (fieldName.equals("accessed") ? 5 : 30) * 60_000L - 1); + assertEquals("expired", values.fetch(fixture.connection, preview.ref(), 0, 1).status()); + assertTrue(fixture.freed.get()); + assertFalse(values.hasValues()); + } + } + + @Test + void nclobRetainsTheExistingCompleteReaderWithoutCapturingAPreview() throws Exception { + Fixture fixture = new Fixture(); + OceanBaseLobValues values = new OceanBaseLobValues((connection, locator, offset, limit) -> { fail("NCLOB must use its existing complete reader"); return null; }); + assertNull(values.preview(fixture.resultSet, 1, Types.NCLOB, "NCLOB")); + assertFalse(values.hasValues()); + assertFalse(fixture.freed.get()); + } @Test void refusesNewLocatorsWhenRetainedPayloadBudgetIsFullAndRestoresCapacityOnRelease() throws Exception { OceanBaseLobValues values = new OceanBaseLobValues((connection, locator, offset, limit) -> chunk("x".repeat(1000), offset, limit), ignored -> 40L * 1024 * 1024); diff --git a/apps/desktop/src/lib/__tests__/dataGrid/largeValueSnapshot.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/largeValueSnapshot.spec.ts index 379f324f17..e5730686a8 100644 --- a/apps/desktop/src/lib/__tests__/dataGrid/largeValueSnapshot.spec.ts +++ b/apps/desktop/src/lib/__tests__/dataGrid/largeValueSnapshot.spec.ts @@ -8,6 +8,12 @@ const response = (data: string, next_offset: number, eof = false) => ({ status: beforeEach(() => mocks.readLargeValueChunk.mockReset()); describe("original-result LOB snapshot consumption", () => { + it.each([{ eof: "false" }, { value_kind: "unknown" }, { data: null }])("rejects malformed payload fields before consuming a chunk: %j", async (invalid) => { + mocks.readLargeValueChunk.mockResolvedValue({ ...response("text", 4, true), ...invalid }); + const consume = vi.fn(); + await expect(readLargeValueSnapshot(request, () => true, consume)).rejects.toThrow("Invalid LOB chunk payload"); + expect(consume).not.toHaveBeenCalled(); + }); it("uses server character offsets and preserves Chinese and emoji across chunks", async () => { mocks.readLargeValueChunk.mockResolvedValueOnce(response("δΈ­ζ–‡πŸ˜€", 3)).mockResolvedValueOnce(response("ζœ«ε°ΎπŸ˜€", 6, true)); expect(await materializeLargeValueSnapshot(request, () => true)).toBe("δΈ­ζ–‡πŸ˜€ζœ«ε°ΎπŸ˜€"); diff --git a/apps/desktop/src/lib/dataGrid/largeValueSnapshot.ts b/apps/desktop/src/lib/dataGrid/largeValueSnapshot.ts index 5b0b3bb295..cf3705a786 100644 --- a/apps/desktop/src/lib/dataGrid/largeValueSnapshot.ts +++ b/apps/desktop/src/lib/dataGrid/largeValueSnapshot.ts @@ -16,6 +16,7 @@ export async function readLargeValueSnapshot( const chunk = await api.readLargeValueChunk({ ...request, offset, limit: 4096 }); if (!isCurrent()) throw new Error("LOB result context changed"); if (chunk.status !== "ok") throw new Error(`LOB snapshot ${chunk.status}; execute the query again`); + if (typeof chunk.data !== "string" || typeof chunk.eof !== "boolean" || (chunk.value_kind !== "text" && chunk.value_kind !== "binary")) throw new Error("Invalid LOB chunk payload"); if (!Number.isSafeInteger(chunk.next_offset) || chunk.next_offset < offset || (!chunk.eof && chunk.next_offset === offset)) { throw new Error("Invalid LOB chunk offset"); }