diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 69f8a0e78..821f22c9e 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -242,6 +242,9 @@ 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. + + 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. + diff --git a/src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java b/src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java index 2e61307ce..6ac03b576 100644 --- a/src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java +++ b/src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java @@ -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. @@ -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(); } diff --git a/src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java b/src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java index 53d568910..44bfd086d 100644 --- a/src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java +++ b/src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java @@ -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 = "" + + "" + "" + + "" + ""; + 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 { @@ -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 = + "" + + "" + + "" + + ""; + final InputSource dtdSource = + new InputSource(new StringReader(dtdContent)); + + final String xmlContent = "" + + "" + "" + + "" + ""; + 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 {