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 @@ -264,6 +264,9 @@
<action dev="jeffjensen" type="fix" issue="656" system="github" due-to="jeffjensen">
Fix DatabaseDataSet#getTable()/getTableMetaData() throwing NoSuchTableException for every table in a schema the very first time that schema is accessed under a different case than the database itself stored it in, when FEATURE_QUALIFIED_TABLE_NAMES is enabled (issue 656: a multi-schema PostgreSQL FlatXmlDataSet row like "CORE.USER" against a live "core" schema - PostgreSQL folds an unquoted schema name to lower case at creation time). DatabaseDataSet#initialize() passed the requested schema straight into IMetadataHandler#getTables(), whose underlying DatabaseMetaData#getTables() matches the schema pattern case-sensitively against the live catalog regardless of dbUnit's own FEATURE_CASE_SENSITIVE_TABLE_NAMES setting, so a wrongly-cased first request found zero tables - and DatabaseDataSet's own per-schema initialization cache then permanently remembered that schema as already-initialized-but-empty, so even a later, correctly-cased retry kept failing too. This also explains the original report's second observation, that later rows loaded fine once some earlier row happened to use the schema's actual case: that merely primed the cache under a casing OrderedTableNameMap's existing case-insensitive table lookup could then match. DatabaseDataSet now resolves a requested schema name against DatabaseMetaData#getSchemas()'s actual reported names, case-insensitively, before querying for tables, whenever FEATURE_CASE_SENSITIVE_TABLE_NAMES is off (the default); a schema that doesn't exist under any casing still resolves to zero tables exactly as before.
</action>
<action dev="jeffjensen" type="add" issue="574" system="github" due-to="hieunv15">
Add JsonType, recognized by PostgresqlDataTypeFactory for `json` and `jsonb` columns (reported as sql type OTHER), read/written as their raw text representation via the same reflection-based PGobject approach as UuidType/InetType/CitextType, so a bound parameter's PGobject type name always matches its target column exactly - PostgreSQL has no implicit cast between `json` and `jsonb`. Unlike the reporter's original 2015 patch attachment, setSqlValue() explicitly handles a null value (binding sql NULL) instead of dereferencing it, and typeCast() returns null for a null input.
</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
151 changes: 151 additions & 0 deletions src/main/java/org/dbunit/ext/postgresql/JsonType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
*
* The DbUnit Database Testing Framework
* Copyright (C)2002-2026, DbUnit.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package org.dbunit.ext.postgresql;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Objects;

import org.dbunit.dataset.datatype.AbstractDataType;
import org.dbunit.dataset.datatype.TypeCastException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Adapter to handle conversion between PostgreSQL native {@code json}/
* {@code jsonb} types and Strings.
*
* <p>PostgreSQL requires a bound parameter's declared type to match its
* target {@code json}/{@code jsonb} column exactly - it has no implicit cast
* between the two - so one instance handles only the single sql type name it
* was constructed with. {@link PostgresqlDataTypeFactory} constructs the
* matching instance for each column automatically; there is normally no need
* to instantiate this class directly.
*
* <p>This class only shuttles the column's raw text between the driver and
* the dataset; it does not compare JSON values semantically. Use
* {@link org.dbunit.assertion.comparer.value.IsActualEqualToExpectedJsonValueComparer}
* to compare json/jsonb column values by their parsed document structure
* instead of raw text.
*
* @author Jeff Jensen
* @since 3.4.1
*/
public class JsonType extends AbstractDataType
{
private static final Logger logger =
LoggerFactory.getLogger(JsonType.class);

private final String sqlTypeName;

/**
* Creates a data type adapter for the given PostgreSQL JSON sql type
* name.
*
* @param sqlTypeName
* The sql type name, {@code "json"} or {@code "jsonb"},
* needed to invoke the {@code setType()} method on the
* PGobject class.
*/
public JsonType(final String sqlTypeName)
{
super(Objects.requireNonNull(sqlTypeName,
"The parameter 'sqlTypeName' must not be null"), Types.OTHER,
String.class, false);

this.sqlTypeName = sqlTypeName;
}

@Override
public Object getSqlValue(final int column, final ResultSet resultSet)
throws SQLException, TypeCastException
{
return resultSet.getString(column);
}

@Override
public void setSqlValue(final Object value, final int column,
final PreparedStatement statement)
throws SQLException, TypeCastException
{
if (value == null)
{
statement.setNull(column, Types.OTHER);
return;
}

statement.setObject(column,
getJson(value, statement.getConnection()));
}

@Override
public Object typeCast(final Object value) throws TypeCastException
{
return value == null ? null : value.toString();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat missing JSON cells as null

When a flat-XML (or other sparse) expected row omits a json/jsonb attribute after the table metadata already includes that column, ITable.getValue() returns ITable.NO_VALUE. Default assertions and SortedTable compare through DataType.compare(), which reaches this typeCast() method; converting the sentinel with toString() makes an omitted expected JSON cell compare as a random object string instead of matching a database NULL, unlike the string-like datatypes this adapter replaces. Please handle ITable.NO_VALUE the same as null here.

Useful? React with 👍 / 👎.

}

/**
* Returns the sql type name this instance handles.
*
* @return {@code "json"} or {@code "jsonb"}, whichever sql type name this
* instance was constructed with.
*/
public String getSqlTypeName()
{
return sqlTypeName;
}

private Object getJson(final Object value, final Connection connection)
throws TypeCastException
{
logger.debug("getJson(value={}, connection={}) - start", value,
connection);

Object tempJson = null;

try
{
final Class<?> aPGObjectClass = super.loadClass(
"org.postgresql.util.PGobject", connection);
final Constructor<?> ct = aPGObjectClass.getConstructor();
tempJson = ct.newInstance();

final Method setTypeMethod =
aPGObjectClass.getMethod("setType", String.class);
setTypeMethod.invoke(tempJson, sqlTypeName);

final Method setValueMethod =
aPGObjectClass.getMethod("setValue", String.class);
setValueMethod.invoke(tempJson, value.toString());

} catch (final ReflectiveOperationException e)
{
throw new TypeCastException(value, this, e);
}

return tempJson;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public DataType createDataType(final int sqlType, final String sqlTypeName)
} else if ("citext".equals(sqlTypeName))
{
return new CitextType();
} else if ("json".equals(sqlTypeName) || "jsonb".equals(sqlTypeName))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid ordering raw json columns

For a PostgreSQL table without a primary key when DatabaseConfig.FEATURE_SORT_ALL_COLUMNS_WHEN_NO_PRIMARY_KEY is enabled, DatabaseDataSet.getSelectStatement() orders by every non-LOB column. The json half of this branch now makes those columns known non-LOB columns instead of being ignored as unknown, so reading such a table generates ORDER BY on a json column, which PostgreSQL rejects because json has no ordering operator. Please exclude json columns from that fallback ordering or otherwise avoid adding them to the generated ORDER BY.

Useful? React with 👍 / 👎.

{
return new JsonType(sqlTypeName);
} else
{
// Finally check whether the user defined a custom datatype
Expand Down
34 changes: 31 additions & 3 deletions src/site/asciidoc/databases/postgresql.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ equivalent.
== IDataTypeFactory

link:/dbunit/apidocs/org/dbunit/ext/postgresql/PostgresqlDataTypeFactory.html[PostgresqlDataTypeFactory]
recognizes `uuid`, `interval`, `inet`, `geometry`, `citext`, `oid`, and
(via an overridable hook) custom enum types, mapping each to the dedicated
classes below; everything else delegates to `DefaultDataTypeFactory`.
recognizes `uuid`, `interval`, `inet`, `geometry`, `citext`, `json`, `jsonb`,
`oid` when reported as JDBC `BIGINT`, and (via an overridable hook) custom
enum types, mapping each to the dedicated classes below; everything else
delegates to `DefaultDataTypeFactory`.
Register it via `DatabaseConfig.PROPERTY_DATATYPE_FACTORY` — see
link:../properties.html#typefactory[Properties] and
link:../connections.html[Connections & Configuration].
Expand Down Expand Up @@ -45,6 +46,7 @@ connection.getConfig().setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY,
|link:/dbunit/apidocs/org/dbunit/ext/postgresql/InetType.html[InetType] |`inet` (IP address/network) values.
|link:/dbunit/apidocs/org/dbunit/ext/postgresql/UuidType.html[UuidType] |`uuid` values.
|link:/dbunit/apidocs/org/dbunit/ext/postgresql/GeometryType.html[GeometryType] |`geometry` (PostGIS) values, read/written as their string representation.
|link:/dbunit/apidocs/org/dbunit/ext/postgresql/JsonType.html[JsonType] |`json` and `jsonb` values, read/written as their raw text representation.
|===

=== GenericEnumType
Expand All @@ -69,7 +71,33 @@ See the FAQ for more:
link:../faq.html#postgresqlEnumTypes[Are Postgresql enum types supported by
dbunit?]

=== JsonType

link:/dbunit/apidocs/org/dbunit/ext/postgresql/JsonType.html[JsonType]
adapts between PostgreSQL's native `json`/`jsonb` types and Strings, using
the same reflection-based `PGobject.setType()` approach as the other types
above. PostgreSQL has no implicit cast between `json` and `jsonb`, so a
bound parameter's PGobject type must match its target column exactly;
`PostgresqlDataTypeFactory` handles this automatically by constructing a
`JsonType` for the specific sql type name (`json` or `jsonb`) each column
reports.

This class only shuttles a column's raw text between the driver and the
dataset; it does not compare JSON values semantically. PostgreSQL
reformats `jsonb` text on storage (for example, inserting whitespace or
reordering object keys), so a literal string comparison against an
expected dataset value can spuriously fail even when the JSON is
semantically identical. Use
link:/dbunit/apidocs/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparer.html[IsActualEqualToExpectedJsonValueComparer]
(see link:../datacomparisons/valuecomparer.html[Value Comparers]) to compare
`json`/`jsonb` column values by their parsed document structure instead.

== Known Quirks

Enum type support requires the `isEnumType()` override above — see
GenericEnumType.

`json`/`jsonb` column comparisons in assertions are literal string
comparisons unless you use
link:/dbunit/apidocs/org/dbunit/assertion/comparer/value/IsActualEqualToExpectedJsonValueComparer.html[IsActualEqualToExpectedJsonValueComparer]
— see JsonType above.
123 changes: 123 additions & 0 deletions src/test/java/org/dbunit/ext/postgresql/JsonTypeTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
*
* The DbUnit Database Testing Framework
* Copyright (C)2002-2004, DbUnit.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*/
package org.dbunit.ext.postgresql;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;

import java.sql.PreparedStatement;
import java.sql.Types;

import org.dbunit.dataset.datatype.AbstractDataType;
import org.dbunit.dataset.datatype.TypeCastException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

/**
* Unit tests for {@link JsonType}.
*
* @author Jeff Jensen
* @since 3.4.1
*/
@ExtendWith(MockitoExtension.class)
class JsonTypeTest extends AbstractPostgresqlStringDataTypeTest
{
@Mock
private PreparedStatement statement;

@Override
protected AbstractDataType createType()
{
return new JsonType("json");
}

@Test
void testConstructor_withValidSqlTypeName_storesSqlTypeName()
{
final JsonType type = new JsonType("jsonb");
assertThat(type.getSqlTypeName())
.as("getSqlTypeName() should return the name passed to the constructor.")
.isEqualTo("jsonb");
}

@Test
void testConstructor_withNullSqlTypeName_throwsNullPointerException()
{
assertThatThrownBy(() -> new JsonType(null))
.as("Constructor should throw NullPointerException when sqlTypeName is null.")
.isInstanceOf(NullPointerException.class);
}

@Test
void testTypeCast_withJsonObjectString_returnsStringRepresentation()
throws TypeCastException
{
final JsonType type = new JsonType("json");
final String value = "{\"a\":1}";
final Object result = type.typeCast(value);
assertThat(result)
.as("typeCast() should return the string representation of the value.")
.isEqualTo("{\"a\":1}");
}

@Test
void testTypeCast_withJsonArrayString_returnsStringRepresentation()
throws TypeCastException
{
final JsonType type = new JsonType("jsonb");
final String value = "[1,2,3]";
final Object result = type.typeCast(value);
assertThat(result)
.as("typeCast() should return the string representation of the value.")
.isEqualTo("[1,2,3]");
}

@Test
void testTypeCast_withNullValue_returnsNull() throws TypeCastException
{
final JsonType type = new JsonType("json");
final Object result = type.typeCast(null);
assertThat(result)
.as("typeCast() should return null when given null.")
.isNull();
}

/**
* Issue 574: the reporter's original patch threw NPE from setSqlValue()
* when the column value was null.
*/
@Test
void testSetSqlValue_withNullValue_doesNotThrowAndSetsSqlNull()
throws Exception
{
final JsonType type = new JsonType("jsonb");

assertThatCode(() -> type.setSqlValue(null, 1, statement))
.as("setSqlValue() should not throw NullPointerException when value is null.")
.doesNotThrowAnyException();

verify(statement).setNull(1, Types.OTHER);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,40 @@ void testCreateCitextType_withCitextTypeName_returnsCitextTypeInstance() throws
assertThat(result).isInstanceOf(CitextType.class);
}

@Test
void testCreateJsonType_withJsonTypeName_returnsJsonTypeInstance() throws Exception
{
final PostgresqlDataTypeFactory instance =
new PostgresqlDataTypeFactory();

final int sqlType = Types.OTHER;
final String sqlTypeName = "json";

final DataType result = instance.createDataType(sqlType, sqlTypeName);
assertThat(result).as("createDataType() should return a JsonType for json.")
.isInstanceOf(JsonType.class);
assertThat(((JsonType) result).getSqlTypeName())
.as("The JsonType should keep the json sql type name.")
.isEqualTo("json");
}

@Test
void testCreateJsonType_withJsonbTypeName_returnsJsonTypeInstance() throws Exception
{
final PostgresqlDataTypeFactory instance =
new PostgresqlDataTypeFactory();

final int sqlType = Types.OTHER;
final String sqlTypeName = "jsonb";

final DataType result = instance.createDataType(sqlType, sqlTypeName);
assertThat(result).as("createDataType() should return a JsonType for jsonb.")
.isInstanceOf(JsonType.class);
assertThat(((JsonType) result).getSqlTypeName())
.as("The JsonType should keep the jsonb sql type name.")
.isEqualTo("jsonb");
}

@Test
void testCreateEnumType_withCustomEnumTypeName_returnsGenericEnumTypeInstance() throws Exception
{
Expand Down
Loading
Loading