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 @@ -242,6 +242,9 @@
<action dev="jeffjensen" type="update" issue="512" system="github" due-to="slandelle">
Reduce FlatXmlProducer memory footprint: column names built from SAX attribute names, previously a fresh String per occurrence even when identical to one already seen elsewhere in the document, now flow through a per-parse cache so repeated column names across rows and tables share one String instance instead of each Column retaining its own duplicate copy. Attribute values are left untouched since they are far less likely to repeat.
</action>
<action dev="jeffjensen" type="fix" issue="496" system="github" due-to="zigarn">
Fix a flat XML/DTD table declared in the DTD but never appearing as a row element (a genuinely empty fixture table) being silently absent from the produced IDataSet: FlatXmlProducer only ever registered a table inside startElement()'s new-table handling, so a table with zero rows in a given fixture never entered the dataset at all, letting CLEAN_INSERT/DELETE_ALL skip it and risk a foreign-key violation against data a prior test left behind. FlatXmlProducer now cross-references every table name reported by the available metadata source (the parsed DTD, or an explicitly-supplied metadata IDataSet) against the tables actually encountered in the XML body once parsing finishes, and reports any still missing as an empty table using that source's column metadata. No-op, so behavior is unchanged, when no DTD or metadata dataset is available.
</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
35 changes: 35 additions & 0 deletions src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,38 @@ private boolean isNewTable(String tableName)
return !_orderedTableNameMap.isLastTable(tableName);
}

/**
* Notifies the consumer of every table declared in {@link #_metaDataSet} (DTD or
* explicit metadata dataset) that never appeared as a row element in the XML body,
* as an empty table using that source's column metadata. Without this, a table with
* zero rows in a given fixture is silently absent from the produced dataset, which
* can make operations like {@code CLEAN_INSERT}/{@code DELETE_ALL} skip it entirely
* even though the DTD declares it. No-op when no DTD/metadata dataset is available,
* so behavior is unchanged for plain flat XML.
*
* @throws DataSetException if the consumer cannot be notified.
*/
private void addMissingDtdTables() throws DataSetException
{
if (_metaDataSet == null)
{
return;
}

String[] dtdTableNames = _metaDataSet.getTableNames();
for (int i = 0; i < dtdTableNames.length; i++)
{
String dtdTableName = dtdTableNames[i];
if (!_orderedTableNameMap.containsTable(dtdTableName))
{
ITableMetaData metaData = _metaDataSet.getTableMetaData(dtdTableName);
_orderedTableNameMap.add(metaData.getTableName(), metaData);
_consumer.startTable(metaData);
_consumer.endTable();
}
}
}

/**
* Rebuilds {@link #_activeColumnNamesUpperCase} from the given metadata's columns.
* Must be called whenever the active table's metadata changes.
Expand Down Expand Up @@ -653,6 +685,9 @@ public void endElement(String uri, String localName, String qName) throws SAXExc
_consumer.endTable();
}

// Notify consumer of DTD/metadata-declared tables no row element referenced
addMissingDtdTables();

// Notify end of dataset to consumer
_consumer.endDataSet();
}
Expand Down
102 changes: 102 additions & 0 deletions src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,47 @@ void testProduceMetaDataSet_withMetaDataSetProvided_usesMetaDataSetColumnsForEmp
consumer.verify();
}

@Test
void testProduceMetaDataSet_withTableAbsentFromXmlBody_addsEmptyTableFromMetaDataSet() throws Exception
{
// Setup consumer
final String presentTable = "PRESENT_TABLE";
final String missingTable = "MISSING_TABLE";
// Deliberately different shapes (name and column count) per table, so a producer
// bug that mixed up which table's metadata to use would make this test fail
// instead of passing by coincidence.
final Column[] presentColumns = new Column[] {
new Column("PRESENT_COL", DataType.UNKNOWN, Column.NULLABLE)};
final Column[] missingColumns = new Column[] {
new Column("MISSING_COL0", DataType.UNKNOWN, Column.NULLABLE),
new Column("MISSING_COL1", DataType.UNKNOWN, Column.NULLABLE)};
final MockDataSetConsumer consumer = new MockDataSetConsumer();
consumer.addExpectedStartDataSet();
consumer.addExpectedEmptyTable(presentTable, presentColumns);
// MISSING_TABLE is declared in the supplied metaDataSet but never appears as a
// row element in the XML body; it must still be reported, with zero rows and its
// own column metadata, or a CLEAN_INSERT/DELETE_ALL relying on the produced
// dataset's table list would silently skip it (issue #496).
consumer.addExpectedEmptyTable(missingTable, missingColumns);
consumer.addExpectedEndDataSet();

// Setup producer
final String content = "<?xml version=\"1.0\"?>"
+ "<!DOCTYPE dataset SYSTEM \"urn:/dummy.dtd\">" + "<dataset>"
+ "<PRESENT_TABLE/>" + "</dataset>";
final InputSource source = new InputSource(new StringReader(content));
final DefaultDataSet metaDataSet = new DefaultDataSet();
metaDataSet.addTable(new DefaultTable(presentTable, presentColumns));
metaDataSet.addTable(new DefaultTable(missingTable, missingColumns));
final IDataSetProducer producer =
new FlatXmlProducer(source, metaDataSet);
producer.setConsumer(consumer);

// Produce and verify consumer
producer.produce();
consumer.verify();
}

@Test
void testProduceCustomEntityResolver_withCustomEntityResolver_usesResolverToLoadDtd() throws Exception
{
Expand Down Expand Up @@ -199,6 +240,67 @@ public InputSource resolveEntity(final String s,
consumer.verify();
}

@Test
void testProduce_withDtdTablesAbsentFromXmlBody_addsEmptyTablesInDtdOrder() throws Exception
{
// Setup consumer
final String presentTable = "PRESENT_TABLE";
final String missingTableA = "MISSING_TABLE_A";
final String missingTableB = "MISSING_TABLE_B";
// Deliberately different shapes (name and column count) per table, so a producer
// bug that mixed up which table's metadata to use would make this test fail
// instead of passing by coincidence.
final Column[] presentColumns = new Column[] {
new Column("PRESENT_COL", DataType.UNKNOWN, Column.NULLABLE)};
final Column[] missingAColumns = new Column[] {
new Column("MISSING_A_COL", DataType.UNKNOWN, Column.NULLABLE)};
final Column[] missingBColumns = new Column[] {
new Column("MISSING_B_COL0", DataType.UNKNOWN, Column.NULLABLE),
new Column("MISSING_B_COL1", DataType.UNKNOWN, Column.NULLABLE)};
final MockDataSetConsumer consumer = new MockDataSetConsumer();
consumer.addExpectedStartDataSet();
consumer.addExpectedStartTable(presentTable, presentColumns);
consumer.addExpectedRow(presentTable, new Object[] {"value0"});
consumer.addExpectedEndTable(presentTable);
// MISSING_TABLE_A/B are declared in the DTD but never appear as row elements;
// they must still be reported, with zero rows and their own DTD-sourced column
// metadata, in DTD declaration order, or a CLEAN_INSERT/DELETE_ALL relying on the
// produced dataset's table list would silently skip them (issue #496).
consumer.addExpectedEmptyTable(missingTableA, missingAColumns);
consumer.addExpectedEmptyTable(missingTableB, missingBColumns);
consumer.addExpectedEndDataSet();

// Setup producer
final String dtdContent =
"<!ELEMENT dataset (PRESENT_TABLE*,MISSING_TABLE_A*,MISSING_TABLE_B*)>"
+ "<!ATTLIST PRESENT_TABLE PRESENT_COL CDATA #IMPLIED>"
+ "<!ATTLIST MISSING_TABLE_A MISSING_A_COL CDATA #IMPLIED>"
+ "<!ATTLIST MISSING_TABLE_B MISSING_B_COL0 CDATA #IMPLIED MISSING_B_COL1 CDATA #IMPLIED>";
final InputSource dtdSource =
new InputSource(new StringReader(dtdContent));

final String xmlContent = "<?xml version=\"1.0\"?>"
+ "<!DOCTYPE dataset SYSTEM \"urn:/dummy.dtd\">" + "<dataset>"
+ "<PRESENT_TABLE PRESENT_COL=\"value0\"/>" + "</dataset>";
final InputSource xmlSource =
new InputSource(new StringReader(xmlContent));
final IDataSetProducer producer =
new FlatXmlProducer(xmlSource, new EntityResolver()
{
@Override
public InputSource resolveEntity(final String s,
final String s1) throws SAXException, IOException
{
return dtdSource;
}
});
producer.setConsumer(consumer);

// Produce and verify consumer
producer.produce();
consumer.verify();
}

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