Skip to content

fix(postgresql): Fix PostgreSQLOidDataType failing to read an oid tha… - #935

Merged
jeffjensen merged 1 commit into
mainfrom
693-postgresql-oid-not-large-object
Aug 11, 2026
Merged

jeffjensen merged 1 commit into
mainfrom
693-postgresql-oid-not-large-object

Conversation

@jeffjensen

@jeffjensen jeffjensen commented Aug 11, 2026

Copy link
Copy Markdown
Member

…t isn't a large object

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, the issue's own example). 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) instead of a blanket catch; any other SQLException still propagates unchanged.
  • 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.
  • Add Mockito-based PostgreSQLOidDataTypeTest coverage: zero-oid, a genuine large object, a 42704 failure, and a non-42704 SQLException still propagating.
  • 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

Claude-Session: https://claude.ai/code/session_01XBjSJ2bgVtwqyxGv2nvwP9

Summary by Sourcery

Handle PostgreSQL OID values that do not reference large objects without failing the entire read, and ensure transactions recover cleanly after large-object access errors.

Bug Fixes:

  • Treat non-large-object OID values as NULL instead of throwing, based on PostgreSQL’s undefined_object SQLState, while still propagating other SQLExceptions.
  • Wrap large-object reads in a savepoint so failed large-object access does not abort the surrounding transaction or break subsequent result set processing.

Enhancements:

  • Add unit and integration tests covering zero OID, valid large objects, non-large-object OIDs, and other failure modes for PostgreSQLOidDataType.
  • Document the revised OID handling semantics and known quirks for PostgreSQL in the PostgreSQL database guide.

Documentation:

  • Update postgresql.adoc to describe how OID columns are handled, including that non-large-object OIDs are read as NULL and noted as a known quirk.

Tests:

  • Introduce Mockito-based PostgreSQLOidDataTypeTest for fine-grained behavior verification of getSqlValue() under various OID scenarios.
  • Extend PostgresSQLOidIT to exercise real catalog OIDs that are not large objects and verify the connection remains usable across multiple rows.

Chores:

  • Record the fix for issue 693 in changes.xml, describing the corrected behavior for PostgreSQLOidDataType#getSqlValue().

Summary by CodeRabbit

  • Bug Fixes

    • Improved PostgreSQL oid handling for missing or invalid large-object references.
    • Preserved transaction usability after failed large-object reads.
    • Continued propagating unexpected database errors.
  • Documentation

    • Documented PostgreSQL oid read/write behavior, error handling, recovery, and known NULL value limitations.

@jeffjensen jeffjensen linked an issue Aug 11, 2026 that may be closed by this pull request
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sourcery-ai

sourcery-ai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adjust PostgreSQLOidDataType#getSqlValue() to treat non–large-object oids as nulls while preserving real error propagation and transaction usability, and add tests/docs/changelog entries to cover the new behavior and regression scenarios (issue 693).

Sequence diagram for PostgreSQLOidDataType#getSqlValue handling non_large_object oids

sequenceDiagram
    actor Caller
    participant PostgreSQLOidDataType
    participant Connection
    participant LargeObjectManager as LOBManager
    participant LargeObject

    Caller->>PostgreSQLOidDataType: getSqlValue(column, resultSet)
    PostgreSQLOidDataType->>Connection: getAutoCommit()
    PostgreSQLOidDataType->>Connection: setAutoCommit(false)
    PostgreSQLOidDataType->>Connection: unwrap(PGConnection)
    PostgreSQLOidDataType->>LOBManager: getLargeObjectAPI()
    PostgreSQLOidDataType->>PostgreSQLOidDataType: readLargeObject(connection, lobj, oid)

    activate PostgreSQLOidDataType
    PostgreSQLOidDataType->>Connection: setSavepoint()
    PostgreSQLOidDataType->>LOBManager: open(oid, LargeObjectManager.READ)

    alt [large object exists]
        LOBManager-->>PostgreSQLOidDataType: LargeObject
        PostgreSQLOidDataType->>LargeObject: size()
        PostgreSQLOidDataType->>LargeObject: read(buf, 0, size)
        PostgreSQLOidDataType->>LargeObject: close()
        PostgreSQLOidDataType-->>Caller: byte[]
    else [SQLException]
        LOBManager-->>PostgreSQLOidDataType: SQLException
        PostgreSQLOidDataType->>Connection: rollback(savepoint)
        alt [SQLState 42704]
            PostgreSQLOidDataType-->>Caller: null
        else [other SQLState]
            PostgreSQLOidDataType-->>Caller: SQLException (propagated)
        end
    end
    deactivate PostgreSQLOidDataType

    PostgreSQLOidDataType->>Connection: setAutoCommit(autoCommit)
Loading

File-Level Changes

Change Details Files
Change PostgreSQLOidDataType#getSqlValue() to delegate large-object handling to a new helper that uses a savepoint and inspects SQLState to distinguish "undefined_object" (non-large-object oid) from real failures, returning null only in the former case.
  • Introduce readLargeObject(Connection, LargeObjectManager, long) to encapsulate large-object opening and reading logic.
  • Wrap LargeObjectManager.open() in a savepoint and rollback on SQLException to clear PostgreSQL transaction aborts without losing other work.
  • Check SQLException SQLState against PSQLState.UNDEFINED_OBJECT and return null (with debug logging) only when the oid is not a large object.
  • Preserve previous behavior for other SQLExceptions by rethrowing them unchanged.
  • Ensure connection auto-commit mode is restored after getSqlValue() completes.
src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java
Add focused unit tests for PostgreSQLOidDataType using Mockito to validate zero-oid handling, successful large-object reads, non-large-object oid behavior, and propagation of other errors with transaction recovery.
  • Annotate PostgreSQLOidDataTypeTest with MockitoExtension and add mocks for JDBC and PostgreSQL driver interfaces (ResultSet, Statement, Connection, PGConnection, LargeObjectManager, LargeObject, Savepoint).
  • Factor out a mockConnectionChain() helper to set up the ResultSet→Statement→Connection→PGConnection→LargeObjectManager chain and auto-commit behavior.
  • Add tests for zero oid returning null and avoiding large-object/open/savepoint calls while toggling auto-commit.
  • Add tests for a real large object round-tripping bytes, including close() verification and absence of rollback.
  • Add tests verifying that a 42704/UNDEFINED_OBJECT error causes getSqlValue() to return null, roll back to the savepoint, and restore auto-commit.
  • Add tests asserting that non-42704 SQLExceptions are rethrown but still roll back to the savepoint and restore auto-commit.
src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java
Extend the PostgreSQL integration test suite to cover reading non–large-object oids written via raw SQL and to demonstrate that the connection stays usable for subsequent rows.
  • Configure the DatabaseConnection with PostgresqlDataTypeFactory to ensure oid handling uses PostgreSQLOidDataType.
  • Insert two catalog oids (pg_class, pg_proc) directly into the test table using raw SQL, since dbUnit’s setSqlValue() always creates large objects.
  • Read back the dataset and assert that both rows’ DATA column values are null rather than causing failures.
  • Assert that the second row can still be read, proving the connection recovered from the first failed large-object open.
src/test/java/org/dbunit/ext/postgresql/PostgresSQLOidIT.java
Document and register the fix for issue 693 in the project’s changelog and PostgreSQL database documentation.
  • Add a changes.xml entry under the 3.4.0 release describing the new behavior for PostgreSQLOidDataType#getSqlValue(), including SQLState-based handling and transaction savepoint usage.
  • Update PostgreSQL Asciidoc documentation to describe how oid columns that don’t reference large objects are read (as null) and note this as a known quirk.
  • Clarify that genuine SQL errors (e.g., permission issues) still propagate instead of being swallowed.
src/changes/changes.xml
src/site/asciidoc/databases/postgresql.adoc

Assessment against linked issues

Issue Objective Addressed Explanation
#693 Change PostgreSQLOidDataType#getSqlValue() so that reading an oid value that does not reference a LargeObject no longer fails the read but instead treats it as a non-error (e.g., returns null), while still propagating genuine errors.
#693 Add automated tests that reproduce the scenario where an oid does not reference a LargeObject and verify the new, non-failing behavior.
#693 Document the new behavior and its implications for PostgreSQL oid handling in the project documentation/change log.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jeffjensen, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0a839fb9-425e-446f-af94-f3d862ed981a

📥 Commits

Reviewing files that changed from the base of the PR and between aff2570 and b1927c7.

📒 Files selected for processing (2)
  • src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java
  • src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java
📝 Walkthrough

Walkthrough

PostgreSQL OID reads now return null for undefined large-object references, propagate other SQL errors, and roll back failed opens to a savepoint. Unit tests, integration tests, documentation, and the changelog cover the new behavior.

Changes

PostgreSQL OID handling

Layer / File(s) Summary
OID read and transaction recovery
src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java
getSqlValue uses savepoint-protected large-object reads. SQLState 42704 returns null; other SQL exceptions propagate.
OID behavior validation and documentation
src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java, src/test/java/org/dbunit/ext/postgresql/PostgresSQLOidIT.java, src/site/asciidoc/databases/postgresql.adoc, src/changes/changes.xml
Tests cover zero OIDs, valid large objects, invalid references, recovery, and propagated errors. Documentation and the changelog describe the behavior and its null ambiguity.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant getSqlValue
  participant readLargeObject
  participant PostgreSQLConnection
  participant LargeObject
  getSqlValue->>readLargeObject: read OID
  readLargeObject->>PostgreSQLConnection: create savepoint
  readLargeObject->>LargeObject: open OID
  LargeObject-->>readLargeObject: bytes or SQL exception
  readLargeObject->>PostgreSQLConnection: rollback failed open
  readLargeObject-->>getSqlValue: bytes, null, or propagated exception
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the PostgreSQL OID read failure being fixed, which matches the main change in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 693-postgresql-oid-not-large-object

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java" line_range="82-86" />
<code_context>
+
+    private final PostgreSQLOidDataType type = new PostgreSQLOidDataType();
+
+    private void mockConnectionChain() throws SQLException
+    {
+        when(resultSet.getStatement()).thenReturn(statement);
+        when(statement.getConnection()).thenReturn(connection);
+        when(connection.getAutoCommit()).thenReturn(true);
+        when(connection.unwrap(PGConnection.class)).thenReturn(pgConnection);
+        when(pgConnection.getLargeObjectAPI()).thenReturn(largeObjectManager);
</code_context>
<issue_to_address>
**suggestion (testing):** Add coverage for connections that already have auto-commit disabled

Right now all tests stub `connection.getAutoCommit()` as `true`, so they only cover the path where we disable and then restore auto-commit. Please also add a test where `getAutoCommit()` initially returns `false` to verify we don’t accidentally enable auto-commit and that savepoint/rollback still behave correctly. You could parameterize `mockConnectionChain` or add a separate helper for the `false` case.

Suggested implementation:

```java
    private final PostgreSQLOidDataType type = new PostgreSQLOidDataType();

    private void mockConnectionChain() throws SQLException
    {
        mockConnectionChain(true);
    }

    private void mockConnectionChain(boolean autoCommit) throws SQLException
    {
        when(resultSet.getStatement()).thenReturn(statement);
        when(statement.getConnection()).thenReturn(connection);
        when(connection.getAutoCommit()).thenReturn(autoCommit);
        when(connection.unwrap(PGConnection.class)).thenReturn(pgConnection);
        when(pgConnection.getLargeObjectAPI()).thenReturn(largeObjectManager);
    }

```

```java
    void testGetSqlType_onNewInstance_returnsTypesBigint()
    {
        assertThat(type.getSqlType())
                .as("getSqlType() should return Types.BIGINT for OID type.")
                .isEqualTo(Types.BIGINT);
    }

    @Test
    void testSetSqlValue_whenAutoCommitDisabled_doesNotEnableAutoCommitAndUsesSavepoint() throws Exception
    {
        mockConnectionChain(false);

        long oid = 123L;
        when(largeObjectManager.createLO()).thenReturn(oid);
        when(largeObjectManager.open(eq(oid), anyInt())).thenReturn(largeObject);

        type.setSqlValue(oid, statement, 1);

        verify(connection, never()).setAutoCommit(true);
        verify(connection, never()).setAutoCommit(false);

        verify(connection).setSavepoint();
        verify(connection).rollback(any(Savepoint.class));
    }

    @Test

```

1. Ensure the following static imports exist (or equivalent qualified calls are used) in the test file:
   - `import static org.mockito.ArgumentMatchers.any;`
   - `import static org.mockito.ArgumentMatchers.anyInt;`
   - `import static org.mockito.ArgumentMatchers.eq;`
   - `import static org.mockito.Mockito.never;`
   - `import static org.mockito.Mockito.verify;`
2. If the production implementation does not use a savepoint/rollback when auto-commit is disabled, adjust the `verify(connection).setSavepoint();` and `verify(connection).rollback(any(Savepoint.class));` expectations to match the actual behavior (or remove them if not applicable).
3. The new test assumes `type.setSqlValue(Object, PreparedStatement, int)` is the method under test and that it uses `LargeObjectManager` and `Savepoint`; if the method signature or behavior differ, update the test accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java`:
- Around line 64-87: Complete the JavaDoc in PostgreSQLOidDataType by adding
documentation for the public getSqlValue method, including complete descriptions
of its parameters, return value, and declared exceptions as applicable. Update
the readLargeObject JavaDoc so each `@param` and `@return` description begins with a
capital letter and ends with a period, while preserving the existing behavior
and meaning.
- Around line 91-105: Limit SQLState 42704-to-null handling to the lobj.open
call in the PostgreSQLOidDataType read flow. Handle open failures separately,
ensure the LargeObject is closed during cleanup after a successful open, and
rethrow exceptions from size, read, or close instead of masking them. Add tests
covering open, size/read, and close failure paths.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: da8f5db4-bab0-4fde-a6a5-7aa39ae4d13c

📥 Commits

Reviewing files that changed from the base of the PR and between 387e4db and aff2570.

📒 Files selected for processing (5)
  • src/changes/changes.xml
  • src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java
  • src/site/asciidoc/databases/postgresql.adoc
  • src/test/java/org/dbunit/ext/postgresql/PostgreSQLOidDataTypeTest.java
  • src/test/java/org/dbunit/ext/postgresql/PostgresSQLOidIT.java

Comment thread src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java
Comment thread src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBjSJ2bgVtwqyxGv2nvwP9
@jeffjensen
jeffjensen force-pushed the 693-postgresql-oid-not-large-object branch from aff2570 to b1927c7 Compare August 11, 2026 23:28
@jeffjensen
jeffjensen merged commit c1685b5 into main Aug 11, 2026
29 checks passed
@jeffjensen
jeffjensen deleted the 693-postgresql-oid-not-large-object branch August 11, 2026 23:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PostgreSQLOidDataType should not fail if OID is not a LargeObject

1 participant