-
Notifications
You must be signed in to change notification settings - Fork 4
feat(postgresql): Add json/jsonb type support to PostgresqlDataTypeFa… #931
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
| } | ||
|
|
||
| /** | ||
| * 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 |
|---|---|---|
|
|
@@ -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)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a PostgreSQL table without a primary key when Useful? React with 👍 / 👎. |
||
| { | ||
| return new JsonType(sqlTypeName); | ||
| } else | ||
| { | ||
| // Finally check whether the user defined a custom datatype | ||
|
|
||
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a flat-XML (or other sparse) expected row omits a
json/jsonbattribute after the table metadata already includes that column,ITable.getValue()returnsITable.NO_VALUE. Default assertions andSortedTablecompare throughDataType.compare(), which reaches thistypeCast()method; converting the sentinel withtoString()makes an omitted expected JSON cell compare as a random object string instead of matching a databaseNULL, unlike the string-like datatypes this adapter replaces. Please handleITable.NO_VALUEthe same asnullhere.Useful? React with 👍 / 👎.