Skip to content

perf(dataset): Reduce FlatXmlProducer memory footprint - #915

Merged
jeffjensen merged 1 commit into
mainfrom
512-flatxmlproducer-column-name-cache
Aug 7, 2026
Merged

jeffjensen merged 1 commit into
mainfrom
512-flatxmlproducer-column-name-cache

Conversation

@jeffjensen

@jeffjensen jeffjensen commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary

  • Add a per-parse column-name cache to FlatXmlProducer (_columnNameCache, reset at the start of each produce() call) so a column name repeated across rows and across different tables shares one String instance instead of each Column retaining its own duplicate copy.
  • Wired through both Column-construction sites via a new internColumnName() helper: createTableMetaData() (metadata built from the first row's attributes) and handleMissingColumns() (columns discovered later via column sensing).
  • Scoped to column names only, matching the analysis in the tracking doc: attribute values (e.g. FK values) are left untouched since they're less likely to repeat.

Test plan

  • ./mvnw clean test -Dtest=FlatXmlProducerTest — 12 tests, 0 failures, including new testProduce_sameColumnNameAcrossTables_reusesColumnNameStringInstance (AssertJ isSameAs, proving actual String-instance reuse rather than just value-equality)
  • ./mvnw clean test — full unit suite, 2024 tests, 0 failures/errors

Fixes #512

Summary by Sourcery

Introduce per-parse column-name interning in FlatXmlProducer to reduce memory usage while preserving existing behavior.

Enhancements:

  • Add a per-parse column-name cache so repeated SAX attribute names reuse a single String instance across rows and tables.
  • Apply column-name interning to both initial table metadata construction and late-discovered columns from column sensing.

Documentation:

Tests:

  • Add a regression test verifying that identical column names across different tables share the same String instance in produced metadata.

Summary by CodeRabbit

  • Performance

    • Reduced memory usage when parsing Flat XML datasets by reusing identical column names across rows and tables.
    • Attribute values remain unchanged.
  • Bug Fixes

    • Added coverage to ensure repeated column names are consistently reused during parsing.

@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a per-parse column-name String cache to FlatXmlProducer to reduce memory usage by interning column names used in Column construction, verifies instance reuse with a new test, and documents the change in the project changelog.

File-Level Changes

Change Details Files
Introduce a per-parse column-name cache in FlatXmlProducer and route all Column constructions through it to reuse String instances for repeated column names.
  • Add a _columnNameCache Map field with documentation explaining its per-produce lifecycle and purpose.
  • Initialize the column-name cache at the start of produce() so each parse has a fresh cache.
  • Add a private internColumnName helper that returns a canonical String from the cache, inserting new names as needed.
  • Update createTableMetaData to obtain column names via internColumnName instead of using attributes.getQName(i) directly.
  • Update handleMissingColumns to use internColumnName for newly discovered columns before constructing Column instances.
src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java
Add a regression test to prove column-name String instance reuse across tables and wire in AssertJ for identity checks.
  • Import AssertJ assertThat and supporting collection/metadata types required by the new test.
  • Create testProduce_sameColumnNameAcrossTables_reusesColumnNameStringInstance to capture ITableMetaData from a DefaultConsumer and assert two tables share the same column-name String instance using isSameAs.
  • Ensure the producer uses FlatXmlProducer on a small inline XML dataset with shared column names across two tables.
src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java
Document the FlatXmlProducer memory footprint improvement in the changelog, referencing the GitHub issue. src/changes/changes.xml

Assessment against linked issues

Issue Objective Addressed Explanation
#512 Reduce FlatXmlProducer memory footprint by caching/reusing String instances for column names built from SAX attribute names so repeated column names do not allocate new Strings for each occurrence.
#512 Reduce FlatXmlProducer memory footprint by caching/reusing String instances for attribute values (e.g., foreign key values) built from SAX attributes so repeated values do not allocate new Strings for each occurrence. The PR explicitly scopes the cache to column names only, via a per-parse _columnNameCache and internColumnName(), and leaves attribute values untouched based on the author’s analysis that they are less likely to repeat. No caching or interning is added for attribute values.

Possibly linked issues

  • QuerySet docs #167: PR implements the issue’s requested FlatXmlProducer memory reduction by caching and reusing repeated column-name String instances.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jeffjensen, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 44 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5124ea6d-641d-4881-810c-53801cec6831

📥 Commits

Reviewing files that changed from the base of the PR and between f72437a and 3bbdbe4.

📒 Files selected for processing (2)
  • src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java
  • src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java
📝 Walkthrough

Walkthrough

FlatXmlProducer now caches repeated column-name String instances within each produce() run. Metadata creation and column sensing use the cache. A regression test verifies reuse across tables, and the changelog records the change.

Changes

FlatXmlProducer column-name caching

Layer / File(s) Summary
Per-parse column-name canonicalization
src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java, src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java, src/changes/changes.xml
FlatXmlProducer initializes a per-parse cache and reuses cached names during metadata creation and column sensing. The test verifies identical column-name instances across tables. The changelog records the optimization.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The change reduces memory use by caching column names, but issue #512 also identifies repeated attribute values as a target. Also cache repeated attribute values, or update issue #512 to define column-name caching as the complete scope.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: reducing the FlatXmlProducer memory footprint.
Out of Scope Changes check ✅ Passed The implementation, regression test, and changelog entry directly support the memory optimization described in issue #512.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 512-flatxmlproducer-column-name-cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • Consider parameterizing _columnNameCache as Map<String, String> (and using the generic HashMap<String, String>) to improve type safety and avoid unchecked casts in internColumnName.
  • If FlatXmlProducer instances are long-lived, you may want to clear or null out _columnNameCache at the end of produce() to avoid retaining the per-parse cache and its entries longer than necessary.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider parameterizing `_columnNameCache` as `Map<String, String>` (and using the generic `HashMap<String, String>`) to improve type safety and avoid unchecked casts in `internColumnName`.
- If `FlatXmlProducer` instances are long-lived, you may want to clear or null out `_columnNameCache` at the end of `produce()` to avoid retaining the per-parse cache and its entries longer than necessary.

## Individual Comments

### Comment 1
<location path="src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java" line_range="314" />
<code_context>
     }

+    @Test
+    void testProduce_sameColumnNameAcrossTables_reusesColumnNameStringInstance() throws Exception
+    {
+        // Two distinct tables sharing a column name, and no DTD/metaDataSet, so both
</code_context>
<issue_to_address>
**suggestion (testing):** Add a test that exercises column-name interning via the column-sensing path (handleMissingColumns).

The current test only exercises interning via `createTableMetaData`, where metadata comes from the first row’s attributes. Since `internColumnName()` is now also used in `handleMissingColumns` for column sensing (later rows introducing new columns), please add a test where:

- The first row omits a column that a later row adds, ensuring `handleMissingColumns` runs.
- The same column name appears in another table.
- The `Column` instances created via column sensing are verified to reuse the same `String` instance as the corresponding columns in the first table.

This will ensure the interning behavior is covered for both construction paths and guard against regressions in the column-sensing logic.

Suggested implementation:

```java
import org.dbunit.dataset.datatype.DataType;
import org.dbunit.dataset.stream.AbstractProducerTest;
import org.dbunit.dataset.stream.DefaultConsumer;
import org.dbunit.dataset.stream.IDataSetProducer;
import org.dbunit.dataset.stream.MockDataSetConsumer;
import org.dbunit.testutil.TestUtils;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.ITableMetaData;
import org.dbunit.dataset.xml.FlatXmlProducer;
import org.xml.sax.InputSource;

import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertSame;

```

To implement the requested test that exercises column-name interning via the column-sensing path (`handleMissingColumns`), you’ll need to add a new `@Test` method to `FlatXmlProducerTest` (near the existing `testProduce_sameColumnNameAcrossTables_reusesColumnNameStringInstance`), along these lines:

```java
    @Test
    void testProduce_columnSensing_reusesColumnNameStringInstanceAcrossTables() throws Exception
    {
        // TABLE_B: column metadata comes from the first row's attributes (createTableMetaData path).
        // TABLE_A: first row omits COL_SHARED; second row introduces COL_SHARED so handleMissingColumns runs.
        // Both tables share the same column name COL_SHARED, and we verify that the Column instances
        // reuse the same String instance for the column name across tables.
        final String content = "<?xml version=\"1.0\"?>"
                + "<dataset>"
                + "<TABLE_B COL_SHARED=\"b0\"/>"
                + "<TABLE_A COL0=\"a0\"/>"
                + "<TABLE_A COL0=\"a1\" COL_SHARED=\"a1\"/>"
                + "</dataset>";

        final InputSource source = new InputSource(new StringReader(content));
        final IDataSetProducer producer = new FlatXmlProducer(source);

        final List<ITableMetaData> capturedMetaData = new ArrayList<>();
        producer.setConsumer(new DefaultConsumer()
        {
            @Override
            public void startTable(ITableMetaData metaData)
            {
                capturedMetaData.add(metaData);
                super.startTable(metaData);
            }
        });

        producer.produce();

        ITableMetaData tableA = null;
        ITableMetaData tableB = null;
        for (ITableMetaData metaData : capturedMetaData)
        {
            if ("TABLE_A".equals(metaData.getTableName()))
            {
                tableA = metaData;
            }
            else if ("TABLE_B".equals(metaData.getTableName()))
            {
                tableB = metaData;
            }
        }

        // Basic sanity checks to ensure tables were captured
        org.junit.jupiter.api.Assertions.assertNotNull(tableA, "TABLE_A metadata should be captured");
        org.junit.jupiter.api.Assertions.assertNotNull(tableB, "TABLE_B metadata should be captured");

        Column colSharedA = tableA.getColumn("COL_SHARED");
        Column colSharedB = tableB.getColumn("COL_SHARED");

        org.junit.jupiter.api.Assertions.assertNotNull(colSharedA, "TABLE_A should have sensed COL_SHARED");
        org.junit.jupiter.api.Assertions.assertNotNull(colSharedB, "TABLE_B should have COL_SHARED from first row");

        // The Column instances created via column sensing (TABLE_A) must reuse the same String instance
        // as the corresponding columns in TABLE_B (created via createTableMetaData).
        assertSame(colSharedB.getColumnName(), colSharedA.getColumnName(),
                "Column name String instances should be interned and reused across tables");
    }
```

You should place this method inside the `FlatXmlProducerTest` class, alongside the other `@Test` methods. If the existing `testProduce_sameColumnNameAcrossTables_reusesColumnNameStringInstance` already captures metadata in a slightly different way (e.g., using a shared `DefaultConsumer` or helper), you can adapt the above to use the same pattern, as long as:

1. The first row of `TABLE_A` omits `COL_SHARED` and a later row adds it (triggering `handleMissingColumns`).
2. `TABLE_B` has `COL_SHARED` present in its first row (metadata from `createTableMetaData`).
3. You assert `assertSame` on the `String` instances returned by `getColumnName()` for `COL_SHARED` in both tables.

This will ensure column-name interning is covered for both construction paths and guard against regressions in the column-sensing logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java (1)

313-347: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the column-sensing cache path.

This test verifies reuse through createTableMetaData. The changed handleMissingColumns path at Lines 379-380 is exercised by the existing column-sensing test, but no test verifies that a sensed column name is reused.

Add a case that senses COL1 in one table, then creates metadata for COL1 in another table, and assert isSameAs on both column names.

As per coding guidelines, changes under src/test/** must be covered by unit tests and tests must be added or updated as needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java` around lines
313 - 347, Extend the FlatXmlProducer tests with a column-sensing scenario that
first discovers COL1 in one table, then creates metadata for COL1 in another
table through the handleMissingColumns path. Capture both ITableMetaData results
and assert their COL1 column-name references with isSameAs, covering reuse in
the sensed-column cache rather than only createTableMetaData.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java`:
- Around line 379-380: Update the attribute-processing logic around
internColumnName and the _columnSensing check so ignored columns use
attributes.getQName(i) directly when sensing is disabled, avoiding
_columnNameCache growth. Only call internColumnName when _columnSensing is
enabled and the column will be retained.

In `@src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java`:
- Around line 327-331: Add JavaDoc to the overridden public startTable method in
the anonymous DefaultConsumer subclass, including a topic sentence plus complete
descriptions for the metaData parameter and DataSetException. Keep the existing
callback behavior unchanged.

---

Nitpick comments:
In `@src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java`:
- Around line 313-347: Extend the FlatXmlProducer tests with a column-sensing
scenario that first discovers COL1 in one table, then creates metadata for COL1
in another table through the handleMissingColumns path. Capture both
ITableMetaData results and assert their COL1 column-name references with
isSameAs, covering reuse in the sensed-column cache rather than only
createTableMetaData.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 28d32d5f-6d46-4028-a8ba-f95022520446

📥 Commits

Reviewing files that changed from the base of the PR and between 35bd741 and f72437a.

📒 Files selected for processing (3)
  • src/changes/changes.xml
  • src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java
  • src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java

Comment thread src/main/java/org/dbunit/dataset/xml/FlatXmlProducer.java Outdated
Comment thread src/test/java/org/dbunit/dataset/xml/FlatXmlProducerTest.java
…and tables

FlatXmlProducer built a new Column from each SAX attribute name it saw,
even though column names commonly repeat across many rows of a table and
across different tables in the same document. Add a per-parse String
cache (created at the start of each produce() call, cleared at its end)
and route both Column construction sites through it: the initial
metadata built from the first row's attributes, and columns discovered
later via column sensing when they will actually be retained. Columns
that would just be logged as an ignored/extra-column warning (column
sensing off) skip the cache instead of growing it for nothing. Attribute
values are left untouched since they are far less likely to repeat.

Refs: 512
@jeffjensen

Copy link
Copy Markdown
Member Author

Addressed this round of feedback, all amended into the single commit (now 3bbdbe44):

  • CodeRabbit (major): handleMissingColumns no longer caches a column name that will just be discarded when column sensing is off - applied their suggested guard.
  • Sourcery (overall comment): _columnNameCache is now Map<String, String> / HashMap<String, String> instead of raw types, dropping the unchecked cast in internColumnName.
  • Sourcery (overall comment): _columnNameCache is now cleared in a finally block at the end of produce() instead of living as long as the producer instance does.
  • Sourcery + CodeRabbit (test coverage, same finding): added testProduce_columnSensedNameMatchesLaterTable_reusesColumnNameStringInstance, covering the handleMissingColumns/column-sensing interning path the existing test didn't reach.
  • CodeRabbit (minor, JavaDoc on the anonymous startTable override): declined - replied inline citing CLAUDE.md's src/main-only JavaDoc scope.

Full unit suite green (2025 tests, 0 failures/errors). Not pushed yet - will push once after this round is fully processed.

@jeffjensen
jeffjensen force-pushed the 512-flatxmlproducer-column-name-cache branch from f72437a to 3bbdbe4 Compare August 7, 2026 16:01
@jeffjensen
jeffjensen merged commit 560b7ac into main Aug 7, 2026
29 checks passed
@jeffjensen
jeffjensen deleted the 512-flatxmlproducer-column-name-cache branch August 7, 2026 20:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Reduce FlatXmlProducer memory footprint

1 participant