Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@
<action dev="jeffjensen" type="fix" issue="677" system="github" due-to="arrom">
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.
</action>
<action dev="jeffjensen" type="fix" issue="693" system="github" due-to="coiouhkc">
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.
</action>
</release>
<release version="3.4.0" date="Jul 28, 2026" description="Test-suite hardening (un-skip and strengthen dozens of disabled/no-op tests); add CachingConnectionProvider and reduce DefaultPrepAndExpectedTestCase's per-test connection churn; pin identifier case-folding to Locale.ENGLISH for Turkish-locale correctness; and a broad set of correctness fixes across export formats (XML, YAML, CSV, XLS, Ant), TimestampDataType timezone handling, InsertOperation/TransactionOperation, and resource-leak cleanups">
<action dev="jeffjensen" type="fix" issue="797" system="github" due-to="jeffjensen">
Expand Down
83 changes: 65 additions & 18 deletions src/main/java/org/dbunit/ext/postgresql/PostgreSQLOidDataType.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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 <code>null</code> if the oid is zero (a SQL
* <code>NULL</code>) 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,
Expand All @@ -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 <code>null</code> if the
* oid does not refer to a large object at all - a PostgreSQL <code>oid</code> column is a
* generic object identifier, not necessarily a large object reference (e.g.
* <code>'table'::regclass</code>), so {@link LargeObjectManager#open(long, int)} failing with
* SQLState <code>42704</code> (undefined_object) is an expected outcome, not a real error.
* <p>
* 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 <code>null</code> if <code>oid</code> is not a large
* object.
* @throws SQLException on any failure other than the oid not referring to a large object.
*/
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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;
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/site/asciidoc/databases/postgresql.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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.
Loading
Loading