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 @@ -221,6 +221,9 @@
<action dev="jeffjensen" type="fix" issue="672" system="github" due-to="lufecir">
Fix DefaultPrepAndExpectedTestCase#verifyData false-failing tables whose generated/identity column is the first column and excluded from comparison: it always sorted the actual table by all of its native columns - identity column included - while the expected table, which never declares that column, sorted by data content only; excludeColumns/includeColumns were applied to the comparison but never to the sort. When production code does not guarantee row insertion order (e.g. Hibernate reordering a batch insert), the database's generated-ID assignment order then diverges from the data-content order, misaligning same-data rows and failing the comparison despite both sides holding identical data. Add VerifyTableDefinition#sortOnFilteredColumnsOnly (default false, preserving prior behavior), implementing the opt-in toggle proposed in issue 676: when true, both tables sort by only their excludeColumns/includeColumns-filtered columns instead of by all native columns.
</action>
<action dev="jeffjensen" type="fix" issue="708" system="github" due-to="roel-tjin">
Fix NoSuchColumnException when two same-named tables merged by CompositeDataSet (e.g. from two separate flat-XML datasets both inserting into the same table) disagree on columns. A row belonging to a part that never declared an optional column crashed instead of being treated as not supplied, hitting InsertOperation's core insert path via equalsIgnoreMapping/getIgnoreMapping. InsertOperation.getIgnoreMapping/equalsIgnoreMapping now resolve such a column to ITable.NO_VALUE, the same sentinel already used to omit a column from a generated statement, instead of letting the row's NoSuchColumnException propagate. This is deliberately scoped to InsertOperation alone rather than CompositeTable itself: UpdateOperation and DeleteOperation bind every requested column's value directly with no equivalent ignore-mapping, so a genuinely missing column there must keep throwing instead of silently binding NULL into a WHERE clause.
</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
46 changes: 44 additions & 2 deletions src/main/java/org/dbunit/operation/InsertOperation.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ITableMetaData;
import org.dbunit.dataset.NoSuchColumnException;

import java.util.BitSet;

Expand Down Expand Up @@ -120,7 +121,7 @@ protected BitSet getIgnoreMapping(ITable table, int row) throws DataSetException
for (int i = 0; i < columns.length; i++)
{
Column column = columns[i];
Object value = table.getValue(row, column.getColumnName());
Object value = getValueOrNoValueIfMissing(table, row, column);
if (wouldIgnore(column, value))
{
ignoreMapping.set(i);
Expand All @@ -143,7 +144,7 @@ protected boolean equalsIgnoreMapping(BitSet ignoreMapping, ITable table,
for (int i = 0; i < columns.length; i++)
{
Column column = columns[i];
Object value = table.getValue(row, column.getColumnName());
Object value = getValueOrNoValueIfMissing(table, row, column);
if (wouldIgnore(column, value) != ignoreMapping.get(i))
{
return false;
Expand All @@ -153,6 +154,47 @@ protected boolean equalsIgnoreMapping(BitSet ignoreMapping, ITable table,
return true;
}

/**
* Reads the row's value for the given column, treating a column the row's
* underlying table does not itself declare as not supplied rather than an
* error. This tolerates a {@code CompositeDataSet} merging same-named
* tables whose column sets differ -- e.g. two flat-XML files feeding the
* same table where only one declares an optional column -- since such a
* table's own metadata (searched by {@link #getIgnoreMapping} and
* {@link #equalsIgnoreMapping} to build {@code columns}) can list a column
* that a specific row's backing part never itself had.
* <p>
* Deliberately local to {@code InsertOperation}: a missing column is only
* safe to treat as "not supplied" here because {@link #wouldIgnore} then
* omits it from the generated insert statement entirely. Other operations
* (update, delete) bind every requested column's value directly with no
* equivalent ignore-mapping, so a genuinely missing column there must keep
* throwing {@link NoSuchColumnException} instead of silently binding
* {@code NULL} into a {@code WHERE} clause.
*
* @param table
* The table being read.
* @param row
* The row index.
* @param column
* The column to read.
* @return The row's value for the column, or {@link ITable#NO_VALUE} if
* the row's underlying table does not have this column at all.
* @throws DataSetException
* if the value cannot be retrieved for any other reason.
*/
private static Object getValueOrNoValueIfMissing(final ITable table,
final int row, final Column column) throws DataSetException
{
try
{
return table.getValue(row, column.getColumnName());
} catch (final NoSuchColumnException e)
{
return ITable.NO_VALUE;
}
}

/**
* Determines whether a column's value would be omitted from the insert
* statement: either because no value was supplied at all, or because the
Expand Down
43 changes: 43 additions & 0 deletions src/test/java/org/dbunit/dataset/CompositeTableTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
package org.dbunit.dataset;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.dbunit.dataset.datatype.DataType;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -85,4 +86,46 @@ void testConstructor_withNewName_preservesRowData() throws Exception
assertThat(renamed.getRowCount()).as("row count preserved.").isEqualTo(1);
assertThat(renamed.getValue(0, "VAL")).as("row data preserved.").isEqualTo("hello");
}

// -------------------------------------------------------------------------
// getValue(int, String) across parts with divergent columns (issue #708)
// -------------------------------------------------------------------------

@Test
void testGetValue_whenColumnMissingFromOnePartOwnMetaData_stillThrowsNoSuchColumnException()
throws Exception
{
// CompositeTable itself is deliberately left strict: a column exposed by
// the composite's own metadata but absent from a specific part's own
// metadata (e.g. two flat-XML files merged into the same table where
// only one declares an optional column) still throws here. Tolerating a
// missing column is InsertOperation's job (see InsertOperationTest and
// DeleteOperationTest for why that leniency must not live here: DELETE
// and UPDATE bind every requested column directly, with no ignore-mapping
// to keep a substituted NO_VALUE from silently becoming a NULL bind).
final Column[] columnsWithOptional = new Column[] {
new Column("COL1", DataType.INTEGER),
new Column("OPTIONAL_COL", DataType.VARCHAR)};
final DefaultTable partWithOptional =
new DefaultTable("TABLE_1", columnsWithOptional);
partWithOptional.addRow(new Object[] {1, "val2"});
Comment thread
sourcery-ai[bot] marked this conversation as resolved.

final Column[] columnsWithoutOptional =
new Column[] {new Column("COL1", DataType.INTEGER)};
final DefaultTable partWithoutOptional =
new DefaultTable("TABLE_1", columnsWithoutOptional);
partWithoutOptional.addRow(new Object[] {1});

final CompositeTable combined = new CompositeTable(
partWithOptional.getTableMetaData(),
new ITable[] {partWithOptional, partWithoutOptional});

assertThat(combined.getValue(0, "OPTIONAL_COL"))
.as("value read from the part that declares the column.")
.isEqualTo("val2");
assertThatThrownBy(() -> combined.getValue(1, "OPTIONAL_COL"))
.as("column absent from the second part's own metadata must still"
+ " throw when read directly through CompositeTable.")
.isInstanceOf(NoSuchColumnException.class);
}
}
57 changes: 57 additions & 0 deletions src/test/java/org/dbunit/operation/DeleteOperationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,20 @@

package org.dbunit.operation;

import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.MockDatabaseConnection;
import org.dbunit.database.statement.MockBatchStatement;
import org.dbunit.database.statement.MockStatementFactory;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.CompositeTable;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.DefaultTable;
import org.dbunit.dataset.DefaultTableMetaData;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.NoSuchColumnException;
import org.dbunit.dataset.datatype.DataType;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -167,4 +171,57 @@ void testExecute_withEmptyTable_noStatementCreated() throws Exception
factory.verify();
connection.verify();
}

@Test
void testExecute_withCompositeTablePartMissingPrimaryKeyColumn_throwsNoSuchColumnException()
throws Exception
{
// A CompositeTable part missing a column is tolerated by InsertOperation
// (issue #708, an optional data column), but DeleteOperation has no
// equivalent ignore-mapping: every primary key column is bound directly
// into the WHERE clause, so a part missing the key itself must still
// throw rather than silently bind SQL NULL and delete zero rows.
final String tableName = "TABLE_1";
final Column[] columnsWithKey = new Column[] {
new Column("ID", DataType.INTEGER),
new Column("NAME", DataType.VARCHAR)};
final String[] primaryKeys = {"ID"};
final DefaultTable partWithKey = new DefaultTable(
new DefaultTableMetaData(tableName, columnsWithKey, primaryKeys));
partWithKey.addRow(new Object[] {1, "first"});

final Column[] columnsWithoutKey =
new Column[] {new Column("NAME", DataType.VARCHAR)};
final DefaultTable partWithoutKey =
new DefaultTable(tableName, columnsWithoutKey);
partWithoutKey.addRow(new Object[] {"second"});

final CompositeTable combinedTable = new CompositeTable(
partWithKey.getTableMetaData(),
new ITable[] {partWithKey, partWithoutKey});
final IDataSet dataSet = new DefaultDataSet(combinedTable);

final MockBatchStatement statement = new MockBatchStatement();
statement.setExpectedExecuteBatchCalls(0);
statement.setExpectedClearBatchCalls(0);
statement.setExpectedCloseCalls(1);

final MockStatementFactory factory = new MockStatementFactory();
factory.setExpectedCreatePreparedStatementCalls(1);
factory.setupStatement(statement);

final MockDatabaseConnection connection = new MockDatabaseConnection();
connection.setupDataSet(dataSet);
connection.setupStatementFactory(factory);
connection.setExpectedCloseCalls(0);

assertThatThrownBy(() -> new DeleteOperation().execute(connection, dataSet))
.as("a primary key column missing from a CompositeTable part must"
+ " not be silently treated as not-supplied.")
.isInstanceOf(NoSuchColumnException.class);

statement.verify();
factory.verify();
connection.verify();
}
}
51 changes: 51 additions & 0 deletions src/test/java/org/dbunit/operation/InsertOperationIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import java.io.FileReader;
import java.io.Reader;
import java.io.StringReader;
import java.sql.SQLException;

import org.dbunit.AbstractDatabaseIT;
Expand All @@ -32,6 +33,7 @@
import org.dbunit.TestFeature;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.CompositeDataSet;
import org.dbunit.dataset.DataSetUtils;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.DefaultTable;
Expand Down Expand Up @@ -334,6 +336,55 @@ void testExecute_emptyStringWithAllowEmptyFields_insertedAsEmptyString() throws
assertThat(actual.getValue(0, "COLUMN0")).as("COLUMN0.").isEqualTo("hasValue");
}

@Test
void testExecute_withCompositeDataSetFromTwoFlatXmlDataSetsAndOneMissingAnOptionalColumn_insertsSuccessfully()
throws Exception
{
// Reproduces GitHub issue #708: two flat-XML datasets both insert into the
// same table, but only the first declares the optional column.
final String tableName = "EMPTY_TABLE";
final IDataSet dataSetWithOptionalColumn =
new FlatXmlDataSetBuilder().build(new StringReader("<dataset><"
+ tableName
+ " COLUMN0=\"row1\" COLUMN1=\"optionalValue\"/></dataset>"));
final IDataSet dataSetWithoutOptionalColumn =
new FlatXmlDataSetBuilder().build(new StringReader(
"<dataset><" + tableName + " COLUMN0=\"row2\"/></dataset>"));
final IDataSet dataSet = new CompositeDataSet(dataSetWithOptionalColumn,
dataSetWithoutOptionalColumn);

assertThat(_connection.getRowCount(tableName)).as("count before.")
.isZero();

DatabaseOperation.INSERT.execute(_connection, dataSet);

final ITable actual = _connection.createDataSet().getTable(tableName);
assertThat(actual.getRowCount()).as("count after.").isEqualTo(2);

// Read back without an ORDER BY, so match rows by their COLUMN0 identifier
// instead of assuming a particular physical row order.
Object column1ForRow1 = null;
Object column1ForRow2 = null;
for (int i = 0; i < actual.getRowCount(); i++)
{
final Object column0Value = actual.getValue(i, "COLUMN0");
if ("row1".equals(column0Value))
{
column1ForRow1 = actual.getValue(i, "COLUMN1");
} else if ("row2".equals(column0Value))
{
column1ForRow2 = actual.getValue(i, "COLUMN1");
}
}

assertThat(column1ForRow1).as("row1 COLUMN1.").isEqualTo("optionalValue");
assertThat(column1ForRow2)
.as("row2 COLUMN1 - the second dataset never declared this"
+ " column, so it is omitted from the insert and the"
+ " database's own NULL applies.")
.isNull();
}

@Test
@EnabledIfSystemProperty(named = "dbunit.profile", matches = "hsqldb")
void testExecute_withDefaultValueNotNullColumnTurningNull_appliesDefaultOnSecondRow()
Expand Down
59 changes: 59 additions & 0 deletions src/test/java/org/dbunit/operation/InsertOperationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import org.dbunit.database.statement.MockBatchStatement;
import org.dbunit.database.statement.MockStatementFactory;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.CompositeTable;
import org.dbunit.dataset.DefaultDataSet;
import org.dbunit.dataset.DefaultTable;
import org.dbunit.dataset.DefaultTableMetaData;
Expand Down Expand Up @@ -331,6 +332,64 @@ void testExecute_withNoValueFields_omitsNoValueColumnsFromInsertSql() throws Exc
connection.verify();
}

@Test
void testExecute_withTwoDataSetsMergedOnSameTableAndOneMissingAnOptionalColumn_omitsColumnForThatPart()
throws Exception
{
// Reproduces GitHub issue #708: two flat-XML datasets both insert into
// TABLE_1, but only the first declares the optional column. Combining them
// (e.g. via CompositeDataSet) merges both parts under the first part's
// metadata, so InsertOperation sees OPTIONAL_COL as a column of the table
// even while reading rows that belong to the part that never declared it.
final String schemaName = "schema";
final String tableName = "TABLE_1";
final String[] expected = {
"insert into schema.TABLE_1 (COL1, OPTIONAL_COL) values (1, 'val2')",
"insert into schema.TABLE_1 (COL1) values (1)",};

final Column[] columnsWithOptional = new Column[] {
new Column("COL1", DataType.INTEGER),
new Column("OPTIONAL_COL", DataType.VARCHAR)};
final DefaultTable partWithOptional =
new DefaultTable(tableName, columnsWithOptional);
partWithOptional.addRow(new Object[] {"1", "val2"});

final Column[] columnsWithoutOptional =
new Column[] {new Column("COL1", DataType.INTEGER)};
final DefaultTable partWithoutOptional =
new DefaultTable(tableName, columnsWithoutOptional);
partWithoutOptional.addRow(new Object[] {"1"});

final CompositeTable combinedTable = new CompositeTable(
partWithOptional.getTableMetaData(),
new ITable[] {partWithOptional, partWithoutOptional});
final IDataSet dataSet = new DefaultDataSet(combinedTable);

// setup mock objects
final MockBatchStatement statement = new MockBatchStatement();
statement.addExpectedBatchStrings(expected);
statement.setExpectedExecuteBatchCalls(2);
statement.setExpectedClearBatchCalls(2);
statement.setExpectedCloseCalls(2);

final MockStatementFactory factory = new MockStatementFactory();
factory.setExpectedCreatePreparedStatementCalls(2);
factory.setupStatement(statement);

final MockDatabaseConnection connection = new MockDatabaseConnection();
connection.setupDataSet(dataSet);
connection.setupSchema(schemaName);
connection.setupStatementFactory(factory);
connection.setExpectedCloseCalls(0);

// execute operation
new InsertOperation().execute(connection, dataSet);

statement.verify();
factory.verify();
connection.verify();
}

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