+ * 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
diff --git a/src/test/java/org/dbunit/dataset/CompositeTableTest.java b/src/test/java/org/dbunit/dataset/CompositeTableTest.java
index d82f952cb..c05ea8e43 100644
--- a/src/test/java/org/dbunit/dataset/CompositeTableTest.java
+++ b/src/test/java/org/dbunit/dataset/CompositeTableTest.java
@@ -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;
@@ -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"});
+
+ 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);
+ }
}
diff --git a/src/test/java/org/dbunit/operation/DeleteOperationTest.java b/src/test/java/org/dbunit/operation/DeleteOperationTest.java
index 56b33348f..c4878dd42 100644
--- a/src/test/java/org/dbunit/operation/DeleteOperationTest.java
+++ b/src/test/java/org/dbunit/operation/DeleteOperationTest.java
@@ -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;
@@ -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();
+ }
}
diff --git a/src/test/java/org/dbunit/operation/InsertOperationIT.java b/src/test/java/org/dbunit/operation/InsertOperationIT.java
index 77b385048..bde139188 100644
--- a/src/test/java/org/dbunit/operation/InsertOperationIT.java
+++ b/src/test/java/org/dbunit/operation/InsertOperationIT.java
@@ -24,6 +24,7 @@
import java.io.FileReader;
import java.io.Reader;
+import java.io.StringReader;
import java.sql.SQLException;
import org.dbunit.AbstractDatabaseIT;
@@ -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;
@@ -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("