From b1927c7bf70f71ebb3d9c8928d425bf7bd806959 Mon Sep 17 00:00:00 2001 From: Jeff Jensen Date: Tue, 11 Aug 2026 17:34:18 -0500 Subject: [PATCH] fix(postgresql): PostgreSQLOidDataType doesn't read non-large-object oid A PostgreSQL oid column is a generic object identifier, not necessarily a large object reference (e.g. a table's own oid via 'table'::regclass). PostgreSQLOidDataType#getSqlValue() previously had a commented-out swallow-everything attempt for this, explicitly noted as wrong since it would hide real errors - so it just let the SQLException propagate and fail the whole read instead. * Distinguish "not a large object" from every other failure via the driver's own SQLState (42704/undefined_object, exposed as org.postgresql.util.PSQLState.UNDEFINED_OBJECT), but only for the LargeObjectManager#open() call itself: a failure reading, sizing, or closing an object that did open is a real error and always propagates, since the oid is already proven to be a large object by that point. * Open the large object under a savepoint: PostgreSQL aborts the entire enclosing transaction on any failed command, so rolling back to the savepoint on failure clears the abort without discarding any other work already done in that transaction, keeping the connection usable for the rest of the read. A large object that did open is closed on the success path only; recovering via the savepoint rollback already reclaims it server-side, and attempting to close it after a failed read would itself throw since the transaction is still aborted at that point. * Add Mockito-based PostgreSQLOidDataTypeTest coverage: zero-oid, ambient auto-commit already disabled, a genuine large object, a 42704 open failure, a non-42704 open failure, and post-open size/read and close failures. * Extend PostgresSQLOidIT with a live-container case inserting two distinct real catalog oids via raw SQL (dbUnit's own setSqlValue() always creates a genuine large object, so it can't produce this scenario itself); reading the second row also proves the connection recovers, since advancing past row 1 requires a real resultSet.next() round-trip. * Document the new behavior in postgresql.adoc, including a Known Quirks note that a non-large-object oid reads identically to a genuine SQL NULL. Refs: 693 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01XBjSJ2bgVtwqyxGv2nvwP9 --- src/changes/changes.xml | 3 + .../ext/postgresql/PostgreSQLOidDataType.java | 83 +++++-- src/site/asciidoc/databases/postgresql.adoc | 28 +++ .../postgresql/PostgreSQLOidDataTypeTest.java | 232 +++++++++++++++++- .../ext/postgresql/PostgresSQLOidIT.java | 38 +++ 5 files changed, 362 insertions(+), 22 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 3e8a25fc5..d98cb7088 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -273,6 +273,9 @@ Fix GenericEnumType.typeCast() throwing NullPointerException instead of returning null for a null value (issue 677: AbstractDataType.compare() calls typeCast() directly whenever comparing a null value against a non-null one - reached by any Assertion/DbUnitAssert comparison involving a null-valued native-enum column). Tracing the same defect shape through the rest of org.dbunit.ext.postgresql's reflection-based PGobject types (issue 930) found the identical gap in UuidType, InetType, and CitextType: none of the four null-checked in typeCast(), and their setSqlValue() overrides bypass typeCast() entirely to call a private PGobject-building helper (getEnum()/getUUID()/getInet()/getCitext()) that also dereferences the value unconditionally, so a direct setSqlValue(null, ...) call would separately NPE too. All four now return null from typeCast() for a null input and bind sql NULL from setSqlValue() before ever reaching the PGobject-building helper, matching the pattern JsonType (issue 574) already established. Added PostgresqlNullableOtherTypesIT, round-tripping a null uuid/inet/citext row through CLEAN_INSERT against a live PostgreSQL 16 container; GenericEnumType's fix is proven at the unit level only - writing the live round-trip test surfaced a separate, pre-existing defect (issue 933) that leaves GenericEnumType unreachable for a real table column via CLEAN_INSERT regardless of null handling. + + Fix PostgreSQLOidDataType#getSqlValue() failing an entire read whenever an `oid` column's value doesn't reference an actual large object (issue 693: a PostgreSQL `oid` column is a generic object identifier, not necessarily a large object reference - e.g. a real catalog object's own oid, such as a table's oid via `'sometable'::regclass`, the original report's own example). PostgreSQL reports this case as SQLState 42704 (`undefined_object`, exposed by the driver as `PSQLState.UNDEFINED_OBJECT`); getSqlValue() now distinguishes it from every other failure and returns null instead, while any other SQLException (a genuine access/permission error, for example) still propagates exactly as before - the method previously had this exact swallow-everything attempt commented out, with a note that doing so unconditionally would hide real errors, which is why a SQLState-based check was needed rather than a blanket catch. PostgreSQL aborts the whole enclosing transaction on any failed command, so the large-object open now runs under a savepoint: on failure, rolling back to it clears the abort without discarding any other work already done in that transaction, keeping the connection usable for the rest of the read (proven by PostgresSQLOidIT reading two distinct non-large-object oids back to back). Added Mockito-based PostgreSQLOidDataTypeTest coverage for the zero-oid, genuine-large-object, not-a-large-object, and other-SQLException-still-propagates cases. + diff --git a/src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java b/src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java index f8fbe4c60..304055fbc 100644 --- a/src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java +++ b/src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java @@ -5,6 +5,7 @@ import org.postgresql.PGConnection; import org.postgresql.largeobject.LargeObject; import org.postgresql.largeobject.LargeObjectManager; +import org.postgresql.util.PSQLState; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -13,6 +14,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Savepoint; import java.sql.Statement; import java.sql.Types; @@ -36,6 +38,16 @@ public PostgreSQLOidDataType() { super("OID", Types.BIGINT); } + /** + * Reads the large object referenced by the column's oid. + * + * @param column the column index to read, starting at 1. + * @param resultSet the result set to read the column value from. + * @return the large object's bytes, or null if the oid is zero (a SQL + * NULL) or does not refer to a large object. + * @throws SQLException if a database access error occurs. + * @throws TypeCastException never thrown by this implementation. + */ @Override public Object getSqlValue(final int column, final ResultSet resultSet) throws SQLException, @@ -58,28 +70,63 @@ public Object getSqlValue(final int column, final ResultSet resultSet) logger.debug("'oid' is zero, the data is NULL."); return null; } - LargeObject obj = lobj.open(oid, LargeObjectManager.READ); - // If lobj.open() throws an exception, it means something wrong with the OID / table. - // So to be accurate, we don't catch this exception but let it propagate. - // Swallowing the exception silently indeed hides the problem, which is the wrong behavior -// try { -// obj = lobj.open(oid, LargeObjectManager.READ); -// } catch (SQLException ex) { -// logger.error("Failed to open oid {} for Large Object reading.", oid); -// logger.error("Exception: {}", ex.getMessage()); -// logger.error("Returning null instead of bailing out"); -// return null; -// } - - // Read the data + + return readLargeObject(connection, lobj, oid); + } finally { + connection.setAutoCommit(autoCommit); + } + } + + /** + * Reads the large object referenced by the given oid, or returns null if the + * oid does not refer to a large object at all - a PostgreSQL oid column is a + * generic object identifier, not necessarily a large object reference (e.g. + * 'table'::regclass), so {@link LargeObjectManager#open(long, int)} failing with + * SQLState 42704 (undefined_object) is an expected outcome, not a real error. + *

+ * PostgreSQL aborts the entire enclosing transaction on any failed command, so the open runs + * under a savepoint: on failure, rolling back to it clears the abort without discarding any + * other work already done in that transaction. Only a failure from the open itself is treated + * as "not a large object" - a failure reading or closing an object that did open is a real + * error and is rethrown rather than masked, since by then the oid is proven to be a large + * object. + * + * @param connection the connection to read from, already in a non-autocommit transaction. + * @param lobj the large object API to read the oid through. + * @param oid the oid value read from the result set. + * @return the large object's bytes, or null if oid is not a large + * object. + * @throws SQLException on any failure other than the oid not referring to a large object. + */ + private byte[] readLargeObject(final Connection connection, final LargeObjectManager lobj, final long oid) + throws SQLException + { + Savepoint savepoint = connection.setSavepoint(); + final LargeObject obj; + try { + obj = lobj.open(oid, LargeObjectManager.READ); + } catch (SQLException ex) { + connection.rollback(savepoint); + if (PSQLState.UNDEFINED_OBJECT.getState().equals(ex.getSQLState())) { + logger.debug("oid {} is not a large object (SQLState={}), returning null.", oid, + ex.getSQLState()); + return null; + } + throw ex; + } + + try { byte buf[] = new byte[obj.size()]; obj.read(buf, 0, obj.size()); - // Close the object obj.close(); - return buf; - } finally { - connection.setAutoCommit(autoCommit); + } catch (SQLException ex) { + // The transaction is aborted again here, same as an open() failure, so recover the + // same way - but rethrow unconditionally: the oid is proven to be a large object by + // now, and attempting obj.close() in a finally block would itself throw (the + // transaction is aborted until the rollback below runs), masking this exception. + connection.rollback(savepoint); + throw ex; } } diff --git a/src/site/asciidoc/databases/postgresql.adoc b/src/site/asciidoc/databases/postgresql.adoc index c7c81338f..82e206b21 100644 --- a/src/site/asciidoc/databases/postgresql.adoc +++ b/src/site/asciidoc/databases/postgresql.adoc @@ -51,6 +51,29 @@ connection.getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, |link:/dbunit/apidocs/org/dbunit/ext/postgresql/ArrayType.html[ArrayType] |Array columns (e.g. `integer[]`, `text[]`), read/written as their PostgreSQL literal text representation. |=== +=== PostgreSQLOidDataType + +link:/dbunit/apidocs/org/dbunit/ext/postgresql/PostgreSQLOidDataType.html[PostgreSQLOidDataType] +reads and writes `oid` columns through the PostgreSQL JDBC driver's +large-object API (`LargeObjectManager`). Writing always creates a new large +object and binds its oid; reading opens the large object the column's oid +refers to and returns its bytes, or `null` when the oid is zero (dbUnit's +convention for a SQL `NULL` in this column). + +A PostgreSQL `oid` column is a generic object identifier, not necessarily a +large object reference — see the PostgreSQL +https://www.postgresql.org/docs/current/datatype-oid.html[`oid` type +documentation]. A column holding some other catalog object's oid (for +example a table's own oid via `'sometable'::regclass`) now reads as `null` +instead of failing the whole read: PostgreSQL reports this specific case as +SQLState `42704` (`undefined_object`), which is distinguished from every +other failure — a real access/permission error still propagates as an +exception rather than being swallowed. Opening the large object runs under a +savepoint, since PostgreSQL aborts the entire enclosing transaction on any +failed command; rolling back to the savepoint on failure clears the abort +without discarding any other work already done in that transaction, so the +connection stays usable for the rest of the read. + === GenericEnumType link:/dbunit/apidocs/org/dbunit/ext/postgresql/GenericEnumType.html[GenericEnumType] @@ -141,3 +164,8 @@ against PostgreSQL's own canonical output format (no extra whitespace, elements quoted only where required) — see ArrayType above. Writing a multi-dimensional array from a literal string is not supported; reading one back for comparison or export works fine. + +An `oid` column reads as `null` both for a genuine SQL `NULL` and for an oid +that doesn't reference a large object — see PostgreSQLOidDataType above. +dbUnit cannot distinguish the two cases from the read value alone, since +neither has any large-object content to return. diff --git a/src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java b/src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java index 32f4a18c8..db24cde23 100644 --- a/src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java +++ b/src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java @@ -21,22 +21,82 @@ package org.dbunit.ext.postgresql; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Savepoint; +import java.sql.Statement; import java.sql.Types; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.postgresql.PGConnection; +import org.postgresql.largeobject.LargeObject; +import org.postgresql.largeobject.LargeObjectManager; +import org.postgresql.util.PSQLState; /** * Unit tests for {@link PostgreSQLOidDataType}. * * @author DbUnit.org */ +@ExtendWith(MockitoExtension.class) class PostgreSQLOidDataTypeTest { + private static final int COLUMN = 1; + + @Mock + private ResultSet resultSet; + + @Mock + private Statement statement; + + @Mock + private Connection connection; + + @Mock + private PGConnection pgConnection; + + @Mock + private LargeObjectManager largeObjectManager; + + @Mock + private LargeObject largeObject; + + @Mock + private Savepoint savepoint; + + private final PostgreSQLOidDataType type = new PostgreSQLOidDataType(); + + private void mockConnectionChain() throws SQLException + { + mockConnectionChain(true); + } + + private void mockConnectionChain(final boolean ambientAutoCommit) throws SQLException + { + when(resultSet.getStatement()).thenReturn(statement); + when(statement.getConnection()).thenReturn(connection); + when(connection.getAutoCommit()).thenReturn(ambientAutoCommit); + when(connection.unwrap(PGConnection.class)).thenReturn(pgConnection); + when(pgConnection.getLargeObjectAPI()).thenReturn(largeObjectManager); + } + @Test void testGetSqlType_onNewInstance_returnsTypesBigint() { - final PostgreSQLOidDataType type = new PostgreSQLOidDataType(); assertThat(type.getSqlType()) .as("getSqlType() should return Types.BIGINT for OID type.") .isEqualTo(Types.BIGINT); @@ -45,7 +105,6 @@ void testGetSqlType_onNewInstance_returnsTypesBigint() @Test void testIsNumber_onNewInstance_returnsFalse() { - final PostgreSQLOidDataType type = new PostgreSQLOidDataType(); assertThat(type.isNumber()) .as("isNumber() should return false for OID type.") .isFalse(); @@ -54,7 +113,6 @@ void testIsNumber_onNewInstance_returnsFalse() @Test void testGetTypeClass_onNewInstance_returnsByteArrayClass() { - final PostgreSQLOidDataType type = new PostgreSQLOidDataType(); assertThat(type.getTypeClass()) .as("getTypeClass() should return byte[].class for OID type.") .isEqualTo(byte[].class); @@ -63,9 +121,175 @@ void testGetTypeClass_onNewInstance_returnsByteArrayClass() @Test void testInstantiation_withNoArgs_createsNonNullInstance() { - final PostgreSQLOidDataType type = new PostgreSQLOidDataType(); assertThat(type) .as("PostgreSQLOidDataType should be instantiable with no arguments.") .isNotNull(); } + + @Test + void testGetSqlValue_withZeroOid_returnsNullWithoutOpeningLargeObject() throws Exception + { + mockConnectionChain(); + when(resultSet.getLong(COLUMN)).thenReturn(0L); + + final Object result = type.getSqlValue(COLUMN, resultSet); + + assertThat(result) + .as("getSqlValue() should return null for a zero oid.") + .isNull(); + verify(largeObjectManager, never()).open(anyLong(), anyInt()); + verify(connection, never()).setSavepoint(); + verify(connection).setAutoCommit(false); + verify(connection).setAutoCommit(true); + } + + /** + * Issue 693: a live large object should still round-trip exactly as before. + */ + @Test + void testGetSqlValue_whenOidIsALargeObject_returnsItsBytes() throws Exception + { + mockConnectionChain(); + final byte[] data = {1, 2, 3, 4, 5}; + when(resultSet.getLong(COLUMN)).thenReturn(42L); + when(connection.setSavepoint()).thenReturn(savepoint); + when(largeObjectManager.open(42L, LargeObjectManager.READ)).thenReturn(largeObject); + when(largeObject.size()).thenReturn(data.length); + when(largeObject.read(any(byte[].class), eq(0), eq(data.length))).thenAnswer(invocation -> { + final byte[] buf = invocation.getArgument(0); + System.arraycopy(data, 0, buf, 0, data.length); + return data.length; + }); + + final Object result = type.getSqlValue(COLUMN, resultSet); + + assertThat(result) + .as("getSqlValue() should return the large object's bytes.") + .isEqualTo(data); + verify(connection, never()).rollback(any(Savepoint.class)); + verify(largeObject).close(); + } + + /** + * Issue 693: an oid that is not a large object (e.g. a row oid from + * 'table'::regclass) must not fail the whole read - PostgreSQL reports this as + * SQLState 42704 (undefined_object). + */ + @Test + void testGetSqlValue_whenOidIsNotALargeObject_returnsNullAndRecoversTransaction() throws Exception + { + mockConnectionChain(); + when(resultSet.getLong(COLUMN)).thenReturn(16548L); + when(connection.setSavepoint()).thenReturn(savepoint); + final SQLException notALargeObject = new SQLException("large object 16548 does not exist", + PSQLState.UNDEFINED_OBJECT.getState()); + when(largeObjectManager.open(16548L, LargeObjectManager.READ)).thenThrow(notALargeObject); + + final Object result = type.getSqlValue(COLUMN, resultSet); + + assertThat(result) + .as("getSqlValue() should return null when the oid is not a large object.") + .isNull(); + verify(connection).rollback(savepoint); + verify(connection).setAutoCommit(true); + } + + /** + * Issue 693: only the "not a large object" SQLState is swallowed - any other failure (e.g. a + * permission error) must still propagate, matching the original bug report's requirement that + * hiding real errors would be the wrong fix. + */ + @Test + void testGetSqlValue_whenOpenFailsForAnotherReason_rethrowsAndRecoversTransaction() throws Exception + { + mockConnectionChain(); + when(resultSet.getLong(COLUMN)).thenReturn(16548L); + when(connection.setSavepoint()).thenReturn(savepoint); + final SQLException permissionDenied = + new SQLException("permission denied for large object 16548", "42501"); + when(largeObjectManager.open(16548L, LargeObjectManager.READ)).thenThrow(permissionDenied); + + assertThatThrownBy(() -> type.getSqlValue(COLUMN, resultSet)) + .as("getSqlValue() should propagate a SQLException unrelated to a missing large object.") + .isSameAs(permissionDenied); + verify(connection).rollback(savepoint); + verify(connection).setAutoCommit(true); + } + + /** + * Issue 693: a failure reading a large object that did open (e.g. its catalog row is deleted + * mid-transaction) is not "not a large object" and must not be swallowed as null, even + * though PostgreSQL aborts the transaction the same way an open() failure does. + */ + @Test + void testGetSqlValue_whenOpenSucceedsButSizeFails_rethrowsAndRecoversTransaction() throws Exception + { + mockConnectionChain(); + when(resultSet.getLong(COLUMN)).thenReturn(42L); + when(connection.setSavepoint()).thenReturn(savepoint); + when(largeObjectManager.open(42L, LargeObjectManager.READ)).thenReturn(largeObject); + final SQLException undefinedObjectAfterOpen = new SQLException("large object 42 does not exist", + PSQLState.UNDEFINED_OBJECT.getState()); + when(largeObject.size()).thenThrow(undefinedObjectAfterOpen); + + assertThatThrownBy(() -> type.getSqlValue(COLUMN, resultSet)) + .as("getSqlValue() must not treat a post-open failure as \"not a large object\", " + + "even with the same SQLState an open() failure would have.") + .isSameAs(undefinedObjectAfterOpen); + verify(connection).rollback(savepoint); + verify(largeObject, never()).close(); + } + + /** + * Issue 693: same as the size()-failure case, but failing on close() after a successful read + * - the bytes were already read successfully, so the failure must still propagate rather + * than silently discarding a value that was in fact read. + */ + @Test + void testGetSqlValue_whenReadSucceedsButCloseFails_rethrowsAndRecoversTransaction() throws Exception + { + mockConnectionChain(); + final byte[] data = {1, 2, 3}; + when(resultSet.getLong(COLUMN)).thenReturn(42L); + when(connection.setSavepoint()).thenReturn(savepoint); + when(largeObjectManager.open(42L, LargeObjectManager.READ)).thenReturn(largeObject); + when(largeObject.size()).thenReturn(data.length); + when(largeObject.read(any(byte[].class), eq(0), eq(data.length))).thenAnswer(invocation -> { + final byte[] buf = invocation.getArgument(0); + System.arraycopy(data, 0, buf, 0, data.length); + return data.length; + }); + final SQLException closeFailed = new SQLException("connection reset", "08006"); + doThrow(closeFailed).when(largeObject).close(); + + assertThatThrownBy(() -> type.getSqlValue(COLUMN, resultSet)) + .as("getSqlValue() must propagate a close() failure instead of masking it.") + .isSameAs(closeFailed); + verify(connection).rollback(savepoint); + } + + /** + * Issue 693: when the caller already has auto-commit disabled (e.g. an externally managed + * transaction), getSqlValue() must not enable it, and the savepoint/rollback recovery must + * still work identically to the auto-commit-enabled case. + */ + @Test + void testGetSqlValue_whenAmbientAutoCommitIsAlreadyFalse_leavesItFalseAndStillRecovers() throws Exception + { + mockConnectionChain(false); + when(resultSet.getLong(COLUMN)).thenReturn(16548L); + when(connection.setSavepoint()).thenReturn(savepoint); + final SQLException notALargeObject = new SQLException("large object 16548 does not exist", + PSQLState.UNDEFINED_OBJECT.getState()); + when(largeObjectManager.open(16548L, LargeObjectManager.READ)).thenThrow(notALargeObject); + + final Object result = type.getSqlValue(COLUMN, resultSet); + + assertThat(result) + .as("getSqlValue() should still return null for a non-large-object oid when " + + "auto-commit was already disabled.") + .isNull(); + verify(connection, never()).setAutoCommit(true); + verify(connection).rollback(savepoint); + } } diff --git a/src/test/java/org/dbunit/ext/postgresql/PostgresSQLOidIT.java b/src/test/java/org/dbunit/ext/postgresql/PostgresSQLOidIT.java index 1017a59ba..d8eadfcae 100644 --- a/src/test/java/org/dbunit/ext/postgresql/PostgresSQLOidIT.java +++ b/src/test/java/org/dbunit/ext/postgresql/PostgresSQLOidIT.java @@ -102,4 +102,42 @@ void testOidDataType_withNullAndBinaryValues_roundTripsThroughDatabase() throws assertThat("\\[text UTF-8](Anything)".getBytes()) .isEqualTo(it.getValue(1, "DATA")); } + + /** + * Issue 693: a PostgreSQL oid column is a generic object identifier, not necessarily a large + * object reference (see https://www.postgresql.org/docs/current/datatype-oid.html), e.g. a + * real catalog object's own oid such as a table's oid via 'table'::regclass, + * the original bug report's own example. Reading such a row must not fail the whole read, + * and the connection must remain usable for subsequent rows afterward. + */ + @Test + void testOidDataType_withOidNotReferencingALargeObject_readsAsNullInsteadOfFailing() + throws Exception + { + assertThat(_connection).as("didn't get a connection").isNotNull(); + final DatabaseConfig config = _connection.getConfig(); + config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, + new PostgresqlDataTypeFactory()); + + // dbUnit's own write path (setSqlValue()) always creates a genuine large object, so use + // raw SQL - two distinct real catalog oids - to put non-large-object values into the + // column. + try (Statement stat = _connection.getConnection().createStatement()) + { + stat.execute("INSERT INTO " + testTable + "(DATA) VALUES ('pg_class'::regclass::oid)"); + stat.execute("INSERT INTO " + testTable + "(DATA) VALUES ('pg_proc'::regclass::oid)"); + } + + final IDataSet ids = _connection.createDataSet(); + final ITable it = ids.getTable(testTable); + + assertThat(it.getRowCount()).isEqualTo(2); + assertThat(it.getValue(0, "DATA")) + .as("a non-large-object oid should read as null instead of throwing.") + .isNull(); + assertThat(it.getValue(1, "DATA")) + .as("reading a second, distinct non-large-object oid afterward should still " + + "work, proving the connection recovered from the first failed read.") + .isNull(); + } }