Visitar URL original
fix(#7342): hoist the context.parsed reset into Importer.loadFromSource() so a format cannot forget it by robfrank · Pull Request #7491 · ArcadeData/arcadedb · GitHub
Skip to content

fix(#7342): hoist the context.parsed reset into Importer.loadFromSource() so a format cannot forget it - #7491

Closed
robfrank wants to merge 3 commits into
mainfrom
fix/7342-hoist-context-parsed-reset
Closed

robfrank wants to merge 3 commits into
mainfrom
fix/7342-hoist-context-parsed-reset

Conversation

@robfrank

@robfrank robfrank commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #7342

One ImporterContext serves all four loadFromSource() phases of an Importer.load()
(Importer.java:79-82, dispatch at :139), so context.parsed is a running total unless the phase
zeroes it. Nine of the ten FormatImporter implementations zeroed it themselves, one at a time;
JSONImporterFormat never did, so a run ending in a JSON phase reported a parsedRecords that mixed
that phase's rows with the previous phase's while a run ending in any other format reported only the
last phase's - two adjacent invocations of the same CLI reporting the same quantity two different ways.
The reset now lives in Importer.loadFromSource(), immediately before format.load(), which is the one
place every phase of every import passes through, so a format cannot forget it and a format added later
inherits it; the rows the finished phase parsed are rolled into a new import-wide totalParsed
accumulator rather than discarded, so the run can still report what it parsed as a whole.

What changed

File Change
ImporterContext new totalParsed accumulator; beginParsingPhase() folds the finished phase in and zeroes parsed and lastParsed; totalParsedRecords(); toMap() publishes totalParsedRecords alongside parsedRecords and documents what each means
Importer.loadFromSource() context.beginParsingPhase() immediately before format.load(...) - the hoist
9 formats (CSV, RDF, Jsonl, XML, Neo4j, OrientDB, GloVe, Word2Vec, Word2VecLSM) context.parsed.set(0) -> context.beginParsingPhase()
JSONImporterFormat javadoc only: why it deliberately has no reset of its own
OrientDBImporter comment only: why :410's parsed.set(0) is not a phase boundary

The two things the issue asked to settle

1. What parsedRecords means. It stays the last phase's row count, now uniform across all ten
formats, and the whole-run figure is published alongside it as a new totalParsedRecords key
rather than by redefining the existing one:

  • parsedRecords is a published key of a public report map - Importer.load()'s return value, the SQL
    result row (ImportDatabaseStatement:116 setPropertiesFromMap) and the HTTP/gRPC import report
    (ServerControlPlane:1503). Silently changing what an existing key counts breaks readers with no signal.
  • JsonlImporterFormatStaleParsedCounterTest.theTwoPhaseCliRouteScopesTheCounterToTheJsonlPhase asserts
    parsedRecords == 2L for a two-phase run that parsed five rows. Redefining the key means editing that
    assertion, and the assertion is not wrong - it is the only observable proof JsonlImporterFormat and XMLImporterFormat take a decision off context.parsed without resetting it, so an earlier import phase shifts the boundary #7313's reset happens on the
    live CLI path.
  • Additive is strictly more informative: the report now answers both "how many rows did the last phase
    parse" and "how many rows did this import parse", and the second is the one the issue said did not exist.

2. The negative progress rate. FormatImporter#printProgress prints
(context.parsed.get() - context.lastParsed) / deltaInSecs, and lastParsed was never reset alongside
parsed, so the first progress line after a phase boundary subtracted the previous phase's total from
this phase's handful of rows. Reproduced literally by reverting the fix:

- Parsed 10 (-45/sec) 0 documents (0/sec) 0 vertices (0/sec) 0 edges ...

beginParsingPhase() zeroes both. lastLapOn is deliberately not reset with them, and the code says
why: printProgress divides every delta on the line by the same window, and the
createdDocuments/Vertices/Edges counters it also prints are import-wide and never reset - restarting
the clock would leave their deltas measured over a window shorter than the rows they cover and overstate
those rates. Understating the parse rate on the first line after a boundary by the same small amount is
the cheaper of the two errors.

Deviation from the issue's suggested fix

The issue sketched "hoist the reset and drop the ten per-format resets". Dropping them is not safe:
three existing tests (RDFImporterFormatCommitCadenceTest, JsonlImporterFormatStaleParsedCounterTest,
XMLImporterFormatStaleParsedCounterTest) call format.load() directly with a pre-loaded counter and
assert the format itself zeroes it, so the reset is part of FormatImporter.load()'s tested contract for
direct callers, and FormatImporter is a public interface in a public package. The duplication the issue
objected to is removed a different way: all nine now call the same ImporterContext#beginParsingPhase(),
so there is exactly one definition of what a phase reset is and they cannot drift.

JSONImporterFormat is deliberately left with no reset of its own. It is the one format whose phase
is zeroed by the hoist alone, which is what makes theHoistedResetScopesTheCounterToTheJsonPhase a test
of the hoist rather than of a tenth copy of the same line - remove the beginParsingPhase() call from
loadFromSource() and that test goes red.

OrientDBImporter.parseRecords():410's parsed.set(0) is untouched: run() calls parseInputFile() up
to three times (:230 ANALYZE, :248 CREATE_SCHEMA, :255 CREATE_EDGES) and every pass reaches
parseRecords() at :373 and re-counts the same records, so that one is a deliberate discard, not a
phase boundary. Folding it into the accumulator would report every OrientDB record two or three times.

Coverage

Entry point Fixed Tested
CLI/SQL multi-phase run, JSON phase last (the format that never reset) yes - the hoist theHoistedResetScopesTheCounterToTheJsonPhase
CLI multi-phase run, any of the nine formats that already reset yes - reset routed through beginParsingPhase() existing jsonl/XML/RDF two-phase CLI tests, green unchanged
printProgress() first line after a phase boundary yes - lastParsed zeroed with parsed aPhaseBoundaryNeverPrintsANegativeParseRate
parsedRecords / totalParsedRecords meaning yes - documented + accumulator theImportWideTotalCountsEveryPhase, beginParsingPhaseRollsTheFinishedPhaseIntoTheImportWideTotal
Direct FormatImporter.load() on the nine resetting formats yes - each keeps its own reset the three existing direct-call tests
ServerControlPlane / SQL IMPORT DATABASE single--url runs yes - one phase, unchanged Issue7346RdfShapeDetectionTest, SQLLocalImporterIT

Argued, not fixed, with evidence:

  • Direct format.load() on JSONImporterFormat - deliberate, see above; a reset there would make the
    hoist unobservable.
  • OrientDBImporter:410 - not a phase boundary, see above.
  • ServerControlPlane.runImport() and ImportDatabaseStatement build the importer through
    Importer(Database, String), which sets settings.url only (Importer.java:40); the other three
    loadFromSource() calls return immediately on a null url (Importer.java:113-115), so exactly one phase
    runs unless the caller also passes documents/vertices/edges through setSettings - which only the
    SQL WITH clause and the CLI can do, and both are covered by the rows above.

Known gaps

  • The server's live import progress counter jumps backwards at every phase boundary #7483 - ServerControlPlane.scheduleImportCounters() resolves ImporterContext.parsed once and
    samples it every second into the SSE parsed field, so a multi-phase
    IMPORT DATABASE ... WITH documents=..., vertices=... makes a client's progress bar run backwards at
    each phase boundary. totalParsed added here is the monotonic field that reader wants, but rewiring the
    server's reflective lookup is a server-module change with its own fallback story for older importer jars.
    FormatImporter#printProgress's printed Parsed %,d total has the same shape off the same counter and is
    noted on that issue, so both surfaces get decided together rather than one being fixed in isolation.

Test plan

To verify by hand:

# two JSON phases, five records total
echo '{"Rows":[{"id":"1"},{"id":"2"},{"id":"3"}]}' > /tmp/p1.json
echo '{"Rows":[{"id":"4"},{"id":"5"}]}'            > /tmp/p2.json
java -cp ... com.arcadedb.integration.importer.Importer \
  -url file:///tmp/p1.json -documents file:///tmp/p2.json \
  -database /tmp/db7342 -forceDatabaseCreate true \
  -mapping '{"Rows":[{"@cat":"d","@type":"Row","@id":"id","@idType":"string"}]}'
# before: parsedRecords=5   after: parsedRecords=2, totalParsedRecords=5

Review log

Three review cycles ran. Cycles 1 and 2 were comment/javadoc only - no behaviour changed after the first
commit, and the full integration suite (370 unit + 133 IT) was re-run green on each.

Cycle Head Applied Skipped, with reason
1 ef60853 Rewrapped toMap()'s over-long javadoc line; said in the code why toMap() inlines the sum instead of calling totalParsedRecords(); reworded beginParsingPhase()'s getAndSet rationale, which argued from "nothing races this today" and so undersold a form that is correct whichever thread increments; fixed a comment that wrapped mid-identifier in RDFImporterFormat CodeRabbit's Docstring Coverage 45.83% pre-merge check. The 24 functions it counted are mostly the ten FormatImporter.load() overrides, which inherit their contract from the interface; adding a javadoc block to each to clear a generic threshold is the boilerplate CLAUDE.md's comment guidance exists to avoid. CodeRabbit's own verdict on the same commit was Merge Risk: Minimal, no actionable merge risk remains, with zero inline threads
2 173ff01 Collapsed the rationale comment that had been pasted verbatim above beginParsingPhase() in six formats into a two-line pointer at that method's javadoc, and trimmed the duplicated tail from the three that carry issue-specific rationale of their own; documented totalParsedRecords(), which had no production caller and read as dead code next to a toMap() that inlines the same arithmetic - the javadoc now names both why toMap() does not call it and the reader it exists for (#7483) -
3 37d0694 Nothing - clean The one remaining nit ("if toMap() and totalParsedRecords() ever drift, nothing enforces they compute the same thing") was raised as "Not asking for a change". It is in fact enforced: beginParsingPhaseRollsTheFinishedPhaseIntoTheImportWideTotal asserts totalParsedRecords() and toMap()'s totalParsedRecords entry against the same 5L in the same test, so a drift between them fails it

Adversarial pass: the Task tool was unavailable in this environment, so the independent-subagent pass
could not be spawned and the review was done against the diff directly - weaker, because the reviewer had
already been convinced. It still caught four things before the PR opened: toMap() read the run total
twice (tested against one value, published with another); two comments asserted things that turned out to
be false (an async incrementer that does not exist, and "parses its input twice" where OrientDBImporter
parses it up to three times); a format count of "eleven" that is ten; and an equality assertion on a rate
derived from raw wall clock, which is now isNotNegative() - the claim the test actually makes, and one no
stall can break.

🤖 Generated with Claude Code

https://claude.ai/code/session_01B1vgWQXZoDXYq7TqAwLYXY

Summary by CodeRabbit

  • Bug Fixes

    • Import progress now reports records parsed in the current phase separately from the total across all phases.
    • Multi-phase imports now maintain accurate cumulative record counts.
    • Progress rates no longer display negative values when transitioning between import phases.
  • Tests

    • Added coverage for multi-phase imports, cumulative totals, phase resets, and progress reporting.

…ce()

One ImporterContext serves all four loadFromSource() phases of an Importer.load(), so context.parsed
is a running total unless the phase zeroes it. Nine of the ten FormatImporter implementations zeroed
it themselves, one at a time; JSONImporterFormat never did, so a run ending in a JSON phase reported
a parsedRecords that mixed that phase's rows with the previous phase's while a run ending in any
other format reported only the last phase's.

The reset now lives in loadFromSource(), immediately before format.load() - the one place every phase
of every import passes through - so a format cannot forget it and a format added later inherits it.
JSONImporterFormat is deliberately left with no reset of its own: it is the one format whose phase is
zeroed by the hoist alone, which is what makes the new CLI test a test of the hoist rather than of a
tenth copy of the same line.

The nine formats keep theirs, because three existing tests call format.load() directly with a
pre-loaded counter and FormatImporter is public API - but they now call
ImporterContext#beginParsingPhase() instead of assigning the field, so there is a single definition
of what a phase reset is and the two cannot drift.

Two things the issue asked to settle as part of it:

- FormatImporter#printProgress prints (parsed - lastParsed) / deltaInSecs, and lastParsed was never
  reset alongside parsed, so the first progress line after a phase boundary printed a negative rate
  ("- Parsed 10 (-45/sec) ..."). beginParsingPhase() zeroes both. lastLapOn is deliberately left
  alone: the createdDocuments/Vertices/Edges deltas on the same line are import-wide and restarting
  the clock would overstate their rates.

- parsedRecords stays the last phase's count - it is a published key of Importer.load()'s report, of
  the SQL IMPORT DATABASE result row and of the HTTP/gRPC import report - and the whole-run figure is
  published alongside it as a new totalParsedRecords, backed by a totalParsed accumulator that each
  phase boundary rolls the finished phase into.

OrientDBImporter.parseRecords()'s own parsed.set(0) is untouched: run() calls parseInputFile() up to
three times and every pass re-counts the same records, so that one is a deliberate discard, not a
phase boundary.

Follow-up #7483: the server's live import progress counter (and the CLI's printed Parsed total) reads
the per-phase counter, so it jumps backwards at a phase boundary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B1vgWQXZoDXYq7TqAwLYXY
@coderabbitai

coderabbitai Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 41f6732b-27a7-4065-84d2-110612d4840c

📥 Commits

Reviewing files that changed from the base of the PR and between d3d9a48 and ef60853.

📒 Files selected for processing (14)
  • integration/src/main/java/com/arcadedb/integration/importer/Importer.java
  • integration/src/main/java/com/arcadedb/integration/importer/ImporterContext.java
  • integration/src/main/java/com/arcadedb/integration/importer/OrientDBImporter.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/CSVImporterFormat.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/GloVeImporterFormat.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/JSONImporterFormat.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/JsonlImporterFormat.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/Neo4jImporterFormat.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/OrientDBImporterFormat.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/RDFImporterFormat.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/Word2VecImporterFormat.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/Word2VecImporterFormatLSM.java
  • integration/src/main/java/com/arcadedb/integration/importer/format/XMLImporterFormat.java
  • integration/src/test/java/com/arcadedb/integration/importer/Issue7342ParsedCounterPhaseScopeTest.java

Included review availability: Your plan provides up to 4 included reviews per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The importer now resets parsed counts at each source phase, accumulates completed and active phase totals, and exposes both values in reports. Format loaders use the shared phase-reset method. Integration tests cover multi-phase imports and progress rates.

Changes

Parsed counter phase scope

Layer / File(s) Summary
Phase accounting and totals
integration/src/main/java/com/arcadedb/integration/importer/ImporterContext.java, integration/src/main/java/com/arcadedb/integration/importer/Importer.java, integration/src/main/java/com/arcadedb/integration/importer/OrientDBImporter.java
ImporterContext tracks current-phase and import-wide parsed records. Importer.loadFromSource starts each phase through beginParsingPhase().
Format phase reset wiring
integration/src/main/java/com/arcadedb/integration/importer/format/*
Format loaders use beginParsingPhase() for direct loading. JSON documents the centralized reset path.
Counter scope validation
integration/src/test/java/com/arcadedb/integration/importer/Issue7342ParsedCounterPhaseScopeTest.java
Tests verify phase-scoped counts, cumulative totals, phase-boundary rates, and context reporting.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant Importer
  participant ImporterContext
  participant FormatLoader
  Importer->>ImporterContext: beginParsingPhase()
  ImporterContext->>ImporterContext: move parsed to totalParsed
  Importer->>FormatLoader: load(...)
  FormatLoader->>ImporterContext: update parsed
  ImporterContext-->>Importer: parsedRecords and totalParsedRecords
Loading

Suggested reviewers: lvca

Merge Risk: ⚪ Minimal · up to ef608

Phase-scoped and import-wide parsed counts are consistently handled, with coverage for multi-phase imports and progress reporting. No actionable merge risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.83% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 14 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #7342. Importer.loadFromSource() calls context.beginParsingPhase() immediately before format.load(). beginParsingPhase() moves completed rows to totalParsed, resets…
Out of Scope Changes check ✅ Passed The reviewed changes stay within issue #7342. The implementation, format updates, OrientDB clarification, and tests all support phase counter scope, import-wide totals, direct format loading, or progr…
Title check ✅ Passed The title clearly identifies the main change: moving the parsed-counter reset into Importer.loadFromSource() for issue #7342. It is specific and related to the changeset.
Description check ✅ Passed The description is detailed and on-topic. It explains the motivation, implementation, linked issue, compatibility decisions, known gaps, coverage, tests, and build results. Although it does not use ev…
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7342-hoist-context-parsed-reset

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.

@mergify

mergify Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@codacy-production

codacy-production Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 0 complexity

Metric Results
Complexity 0

View in Codacy

🟢 Coverage 90.48% diff coverage · -6.39% coverage variation

Metric Results
Coverage variation ✅ -6.39% coverage variation
Diff coverage ✅ 90.48% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (d3d9a48) 178988 147639 82.49%
Head commit (37d0694) 211401 (+32413) 160870 (+13231) 76.10% (-6.39%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#7491) 21 19 90.48%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review

I read through the diff (Importer.java, ImporterContext.java, OrientDBImporter.java, all ten FormatImporter implementations, and the new test class) and traced the counter arithmetic by hand. Overall this is a well-scoped, well-tested fix - it addresses the root cause rather than patching the symptom, and it is careful about backward compatibility.

Correctness

  • The hoist is placed correctly: Importer.loadFromSource() calls context.beginParsingPhase() right before format.load(...), and only on the paths that actually reach a format (it returns early for a null url, probeOnly, or an undetermined schema, all of which never call format.load() either - so there's no accidental phase-counting for a no-op call).
  • beginParsingPhase() uses parsed.getAndSet(0L) plus totalParsed.addAndGet(...), so the fold is atomic regardless of which thread is incrementing parsed at the time - correct even though the comment frames it as "nothing races this call as the code stands," which undersells it a bit: the atomic form is correct even if something did race it, not just cheap insurance.
  • Verified all eleven context.parsed increment sites (CSV x3, JSON x1, RDF x1, Jsonl x1, XML x1, Neo4jImporter x2, OrientDBImporter x2) - the "eleven sites" claim in the comment checks out.
  • The nine formats calling beginParsingPhase() themselves and Importer.loadFromSource() calling it immediately beforehand means it runs twice per phase on the CLI path. That's harmless (the second call folds zero) and is documented, but it's easy for a future reader to mistake for a bug at first glance - worth being aware of if this code gets touched again.
  • The OrientDBImporter.parseRecords() case (raw parsed.set(0), not routed through beginParsingPhase()) is correctly reasoned: run() re-parses the same input up to three times (ANALYZE/CREATE_SCHEMA/CREATE_EDGES), so folding each pass into totalParsed would triple-count. Good catch, and good that it's called out with a comment instead of left as a silent inconsistency with the other nine formats.
  • parsedRecords semantics are preserved exactly (still "last phase's count"), verified against the three pre-existing tests that assert on it directly (JsonlImporterFormatStaleParsedCounterTest, XMLImporterFormatStaleParsedCounterTest, RDFImporterFormatCommitCadenceTest) - none of them needed to change. totalParsedRecords is purely additive (grep confirms no other code reads it yet), so no existing consumer can be broken by this change.
  • printProgress's negative-rate bug is fixed correctly by zeroing lastParsed alongside parsed, while deliberately leaving lastLapOn alone - the reasoning (the other printed counters are import-wide and never reset, so restarting the clock would overstate their rates) is sound and well-explained in the comment.

Minor nits (non-blocking)

  • ImporterContext.toMap()'s javadoc (around line 149) has an unwrapped long line, inconsistent with the ~120-col wrapping used everywhere else in the file.
  • toMap() re-derives totalParsed.get() + phaseParsed instead of calling the existing totalParsedRecords() helper - looks like near-duplication at first glance, but it's actually deliberate (reusing the already-read phaseParsed local avoids a second, potentially inconsistent, read of parsed). Might be worth a one-line comment noting why it doesn't just call totalParsedRecords(), since that method sits right above it.
  • The comment density in this PR is high (multiple paragraphs per call site) - consistent with the existing style in this file/package (e.g. the pre-existing RDFImporterFormat and ImporterContext comments are similarly verbose), so I'm not flagging it as inconsistent, just noting it as a style choice.

Test coverage

Issue7342ParsedCounterPhaseScopeTest is solid:

  • theHoistedResetScopesTheCounterToTheJsonPhase exercises the actual reported bug end-to-end through the CLI/Importer.load() path with the one format that never reset itself.
  • aPhaseBoundaryNeverPrintsANegativeParseRate reproduces the printProgress regression directly and asserts on the rate rather than a brittle exact string.
  • theImportWideTotalCountsEveryPhase and beginParsingPhaseRollsTheFinishedPhaseIntoTheImportWideTotal cover the new accumulator both end-to-end and in isolation.
  • The PR description states these fail without the fix (with specific expected/was values), which is exactly the kind of evidence that's convincing for a counter-reset bug like this.

Performance / security

No concerns. The added work per phase transition is a couple of AtomicLong operations, called at most a handful of times per import - not measurable. No new dependencies, no I/O/security surface touched.

Summary

I don't see any bugs in the logic. The OrientDB carve-out and the "why JSON gets no reset of its own" reasoning are the two places this could easily have gone wrong, and both are handled correctly and explained clearly enough that a future maintainer won't "fix" them into a regression. Nice work.

…talParsedRecords()

Cycle 1 of the review loop. Comments only, no behaviour change - 370 integration unit tests still green.

- ImporterContext.toMap()'s javadoc had one line past the file's ~120-col wrapping. Rewrapped.
- toMap() sums totalParsed.get() + phaseParsed instead of calling the totalParsedRecords() helper that sits
  right above it, which reads as near-duplication. The comment now says why: the two published keys have to be
  built from the same reading of `parsed`, so re-reading it through the helper is the thing being avoided.
- beginParsingPhase()'s getAndSet comment argued from "nothing races this call as the code stands", which
  undersells it - the atomic form is correct whichever thread increments, not merely cheap insurance. Reworded.
- RDFImporterFormat's comment wrapped mid-identifier ("FormatImporter" / ".load()"). Rewrapped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B1vgWQXZoDXYq7TqAwLYXY
@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review

I read through the diff and the linked issue history, checked every call site of context.parsed / parsedRecords / totalParsedRecords in the tree, and traced the three specific claims the PR description makes (JSONImporterFormat is never invoked directly outside loadFromSource, OrientDBImporter's inner parsed.set(0) is not a phase boundary, no production reader of the parsedRecords map key exists outside the ones the PR already accounts for). All three check out against the current source.

Correctness

  • The hoist is placed correctly: Importer.loadFromSource() (Importer.java:144) calls context.beginParsingPhase() only after the early returns for url == null, probeOnly, and a null sourceSchema, so a skipped phase never wastes a reset or double-counts.
  • beginParsingPhase()'s totalParsed.addAndGet(parsed.getAndSet(0L)) is correctly ordered: getAndSet captures the finished phase's count atomically before zeroing, so a concurrent incrementer can't be lost between the two ops, and calling it twice (once from loadFromSource, once from the nine formats' own load()) is genuinely harmless since the second call folds zero, as documented.
  • lastParsed reset alongside parsed is exactly the right fix for the negative-rate bug in FormatImporter#printProgress (FormatImporter.java:55), and the test (aPhaseBoundaryNeverPrintsANegativeParseRate) reproduces the actual defect shape (asserts isNotNegative() rather than an exact value, correctly avoiding a wall-clock flake) rather than just asserting the implementation.
  • The new totalParsedRecords map key is additive only. I checked ImportDatabaseStatement.java:116 (result.setPropertiesFromMap(statistics)) and the equivalent server control-plane path: both copy the returned map generically, so adding a key doesn't require changes there and can't break existing readers keyed on parsedRecords.
  • Leaving OrientDBImporter.parseRecords()'s own parsed.set(0) (not routed through beginParsingPhase()) is correct given run() calls parseInputFile() up to three times over the same records; routing it through the accumulator would triple-count a single OrientDB import.

Test coverage

Good. The four new tests in Issue7342ParsedCounterPhaseScopeTest each isolate one of the four things called out in the description (cross-phase leakage into a JSON-last run, the import-wide total, the negative-rate regression, and the accumulator arithmetic in isolation), and the PR notes they were verified to fail against a revert of the fix, which is the right bar for a regression test.

Minor / non-blocking

  1. Duplicated rationale comment across 6 formats. CSVImporterFormat, GloVeImporterFormat, Neo4jImporterFormat, OrientDBImporterFormat, Word2VecImporterFormat, and Word2VecImporterFormatLSM all got the identical multi-line comment above context.beginParsingPhase():

    // Redundant with Importer.loadFromSource(), which zeroes the phase counter for every format before dispatching
    // here; kept because FormatImporter.load() is public API and is called directly too. Routed through
    // ImporterContext so there is one definition of what a phase reset is (issue #7342).
    

    beginParsingPhase()'s own javadoc already carries this explanation in full. Six verbatim copies is copy-paste drift risk if the rationale ever needs to change (e.g. if FormatImporter.load()'s "public API, called directly" premise changes). Worth trimming these six to a one-liner like // Kept for direct FormatImporter.load() callers; see ImporterContext#beginParsingPhase().

  2. totalParsedRecords() has no production caller. toMap() deliberately inlines the same arithmetic instead of calling it (with a comment explaining why, reading parsed once for both keys). That leaves the method used only by the new test. Not wrong, since it reads as public API for whatever eventually consumes ImporterContext directly (the PR's own "known gaps" section on The server's live import progress counter jumps backwards at every phase boundary #7483 gestures at this), but worth confirming that's the intent rather than dead code the reviewer should ask about.

  3. Nit: a couple of the redundant-reset comments read a little awkwardly in the second sentence ("...before dispatching here; kept because..., Routed through ImporterContext so..." reads as three fused clauses across a semicolon and a comma-splice). Not a functional issue, just a readability pass if you're touching these lines again.

Security / performance

No concerns. No new dependencies, no new synchronization beyond existing AtomicLong usage, and the added work per phase boundary is O(1).

Nice root-cause fix, and the write-up makes the reasoning easy to verify against the code rather than having to take it on faith.

…ference

Cycle 2 of the review loop. Comments and javadoc only, no behaviour change - 370 integration unit tests
and 133 ITs still green.

- Six formats (CSV, GloVe, Neo4j, OrientDB, Word2Vec, Word2VecLSM) carried the same three-line rationale
  above context.beginParsingPhase(), verbatim. beginParsingPhase()'s own javadoc already holds the full
  explanation, so six copies were drift risk with nothing to gain: each is now a two-line pointer at it.
  The three that carry issue-specific rationale of their own (RDF's commit boundary, jsonl's commit
  boundary, XML's -parsingLimitEntries) keep that and lose only the duplicated tail.

- totalParsedRecords() had no production caller, which reads as dead code next to a toMap() that inlines
  the same arithmetic. The javadoc now names both halves of that: why toMap() does not call it (both
  published keys have to come from one reading of `parsed`), and the reader it exists for - a progress
  counter that must not jump backwards at a phase boundary, which is #7483.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B1vgWQXZoDXYq7TqAwLYXY
@claude

claude Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review

Read through the diff (Importer.java, ImporterContext.java, OrientDBImporter.java, the nine FormatImporter implementations, and the new Issue7342ParsedCounterPhaseScopeTest) and traced the control flow by hand. mvn wasn't available in this session, so this is a static review, not a build-verified one.

Correctness

  • The core fix is sound. Importer.loadFromSource() calls context.beginParsingPhase() right before format.load(...), which is the one chokepoint every phase (url/documents/vertices/edges) passes through, so JSONImporterFormat (the format that never reset the counter) is now covered without needing a reset of its own.
  • Double-reset is handled correctly: on the live CLI path, beginParsingPhase() now runs both in loadFromSource() and again inside the nine formats that keep their own call (kept because FormatImporter.load() is public API exercised directly by three existing tests). Since the second call folds an already-zeroed parsed into totalParsed (adding zero) and re-zeroes an already-zero lastParsed, it's a correct no-op rather than merely "probably harmless."
  • lastParsed being zeroed alongside parsed genuinely fixes the negative-rate bug in printProgress ((parsed - lastParsed) / deltaInSecs); checked against FormatImporter.printProgress and the reasoning in the PR description matches the code.
  • OrientDBImporter.parseRecords() (3 passes: ANALYZE/CREATE_SCHEMA/CREATE_EDGES) is correctly left with a plain parsed.set(0) rather than beginParsingPhase(), since folding each pass into totalParsed would triple-count the same records. Good catch, well commented.
  • Checked the three existing tests that call format.load() directly with a pre-seeded context.parsed (JsonlImporterFormatStaleParsedCounterTest, XMLImporterFormatStaleParsedCounterTest, RDFImporterFormatCommitCadenceTest): swapping parsed.set(0) for beginParsingPhase() doesn't change what they assert on context.parsed, so they should stay green.

Design / API

  • Making totalParsedRecords additive rather than redefining parsedRecords is the right call given parsedRecords is a published key read by the SQL IMPORT DATABASE result row and the HTTP/gRPC report - redefining it would be a silent breaking change for existing consumers.
  • Minor nit: toMap() deliberately avoids calling the totalParsedRecords() helper it sits right next to, to avoid a second read of parsed. The comment explains why, but since this is single-threaded per-import bookkeeping, the justification reads as more defensive than strictly necessary. Not asking for a change, just noting that if toMap() and totalParsedRecords() ever drift, nothing enforces they compute the same thing.

Test coverage

  • Good coverage of the actual bug: theHoistedResetScopesTheCounterToTheJsonPhase exercises the literal reported scenario (two-phase JSON import) end to end through the real CLI/Importer.load() path, not just unit-level context manipulation.
  • aPhaseBoundaryNeverPrintsANegativeParseRate correctly avoids asserting exact wall-clock rate values (uses isNotNegative(), with a comment explaining why an equality assertion would be flaky), matching the repo's stall-aware testing guidance.
  • The PR description states these 4 tests fail without the fix and gives the exact failure diffs, good discipline for a regression test.

Style

  • Consistent with the existing (already very verbose) javadoc style in this file. Good "why" rationale throughout, with issue cross-references, per CLAUDE.md's comment guidance. No stray System.out, no new dependencies, final used consistently.

Nothing else stood out

No security concerns (no new I/O, no new external input parsing), no obvious performance regression (the extra beginParsingPhase() call per phase is O(1) atomic ops), and the known follow-up gap (#7483, the server's SSE progress counter still reading the non-monotonic parsed field) is explicitly called out rather than silently left behind.

Thorough writeup connecting the fix back to the original issue and both things it asked to settle. Looks ready to merge from a correctness standpoint, modulo whatever CI/CodeRabbit still has open.

@robfrank

Copy link
Copy Markdown
Collaborator Author

CI triage on 37d0694 - none of the three red lanes is this PR

Three lanes are red. I pulled the surefire/failsafe XML for each and checked every failing test against
main. None of them touches the importer, and none of them is caused by this diff.

The decisive check first. integration-tests passed on this PR's first commit ef60853 (run
34655859791) and is red on 37d0694.
Everything between those two commits is comments and javadoc - the review-cycle commits changed no
executable line:

$ git diff ef60853077..37d0694e61 | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' \
    | grep -vE '^[+-]\s*(//|\*|/\*)' | grep -vE '^[+-]\s*$'
(no output)

Identical bytecode, green then red. Whatever moved, it was not this branch.

unit-tests - broken on main, same four tests

com.arcadedb.server.ha.raft.ArcadeStateMachinePerDatabaseHaltTest.perDatabaseApplyErrorDoesNotTripNodeWideHalt
com.arcadedb.server.ha.raft.ArcadeStateMachinePerDatabaseHaltTest.otherDatabasesKeepApplyingAfterOneDatabaseFails
com.arcadedb.engine.timeseries.MultiColumnAggregationResultTest.emptySumAndCountStayZeroNotNaN
com.arcadedb.engine.timeseries.Issue7089NaNTransparentSumAvgTest.oneNaNSampleNoLongerPoisonsTheBucketOnAnyPath

Byte-for-byte the same four names in main's own latest run
(34653826386, artifact
unit-test-reports), and unit-tests is red on all six of the last main runs. Two Raft WAL tests
and two TimeSeries NaN tests, in engine and server; this PR changes neither module. Worth someone
filing separately - main has been red on this lane for six runs.

vector-unit-tests - the known convoy flake

com.arcadedb.index.vector.Issue7357BulkLoadSuspendsRebuildTest.closingABatchWhileTheTimerFiresStillLeavesARebuildComing
  [nothing is left stranded in the buffer after eight rounds of racing the boundary] expected: 0L but was: 1L

An LSM vector rebuild race in engine. Every @Tag("vector") class lives in
engine/src/test/java/com/arcadedb/index/vector/, and none of them mentions ImporterContext,
context.parsed, parsedRecords or lastParsed. The lane is red in 2 of the last 6 main runs on its
own, which is the REBUILD_SEMAPHORE convoy CLAUDE.md documents.

integration-tests - server port/state cascade

13 failures, all in server: ten in ServerProfilingIT (Cannot create type 'Vertex1' because already exists, Cannot create bucket 'Bucket1' because already exists, reloadWhileUsingDatabase expected: 29 but was: 2 - leaked schema state between methods) and three gRPC ITs failing at Error starting plugin: GrpcServerPlugin, which is the fixed-port conflict CLAUDE.md warns about. Zero failures in
com.arcadedb.integration.*. The same lane failed on an unrelated concurrent PR
(fix/7461-import-database-result-ok-on-fail, run
34657385125) in the same window.

Locally, on this exact tree

mvn -o -pl integration test   -DexcludedGroups=benchmark,slow,vector   ->  370 tests, 0 failures
mvn -o -pl integration verify -DskipITs=false -DexcludedGroups=...     ->  133 ITs,   0 failures
mvn -o -DskipTests install                                             ->  BUILD SUCCESS (full reactor)

Re-running the three failed jobs on this commit should be enough to clear them; I have not done so, since
the merge - and the rerun - is the developer's call.

@codecov

codecov Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.47619% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.00%. Comparing base (d3d9a48) to head (37d0694).
⚠️ Report is 20 commits behind head on main.

Files with missing lines Patch % Lines
...tegration/importer/format/Neo4jImporterFormat.java 0.00% 1 Missing ⚠️
...ion/importer/format/Word2VecImporterFormatLSM.java 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7491      +/-   ##
==========================================
- Coverage   74.00%   74.00%   -0.01%     
==========================================
  Files        1957     1957              
  Lines      178988   178998      +10     
  Branches    37784    37785       +1     
==========================================
+ Hits       132469   132471       +2     
- Misses      31386    31396      +10     
+ Partials    15133    15131       -2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@robfrank

Copy link
Copy Markdown
Collaborator Author

Addendum - the two long lanes finished, both also pre-existing

ha-integration-tests and slow-unit-tests have now completed, both red. Same picture: neither touches
the importer, and both are red on main without this branch.

slow-unit-tests - one failure:

com.arcadedb.database.async.Issue6534QuiesceResizeRaceTest.aGrowCannotPublishWhileAQuiescenceIsStillWaitingOnABusyWorker
  [a grow must not publish a new pool size while a quiescence is still waiting on a busy worker to park]
  expected: 1 but was: 3

The regression test of the already-closed #6534, in engine's async pool code. It is red on main HEAD
itself, so it reddens this lane on every PR - it is not specific to this branch and, as far as I can see,
has no issue tracking it yet.

ha-integration-tests - one failure:

com.arcadedb.server.ha.raft.Issue5569SlotMergeDeleteRaftIT.mergedDeletesReplicateIntact

server HA/Raft replication. This lane is red on 3 of main's last 5 runs, which matches the long-running
intermittency tracked in #5668.

Summary across all five red lanes on 37d0694:

Lane Failing tests Module Red on main?
unit-tests 2 Raft WAL + 2 TimeSeries NaN server, engine yes - 6 of last 6, same four names
integration-tests 10 ServerProfilingIT + 3 gRPC plugin-start server also red on an unrelated concurrent PR in the same window
vector-unit-tests Issue7357BulkLoadSuspendsRebuildTest engine yes - 2 of last 6
slow-unit-tests Issue6534QuiesceResizeRaceTest engine yes - red on main HEAD
ha-integration-tests Issue5569SlotMergeDeleteRaftIT server yes - 3 of last 5 (#5668)

Zero failures in com.arcadedb.integration.* anywhere, which is the only package this PR changes.

@robfrank

Copy link
Copy Markdown
Collaborator Author

Superseded by #7497, which implemented all five issues (#7341, #7342, #7345, #7347, #7461) independently and merged first, and by #7498, which carries the four follow-ups this branch only filed (#7487, #7488, #7490, #7494). Main is ahead of this branch, so merging it would regress that work.

Closing rather than resolving the conflicts. The regression tests on this branch are NOT superseded - they cover considerably more than what landed - and porting them onto the merged implementation's API is tracked separately.

@robfrank robfrank closed this Sep 12, 2026
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.

Hoist the context.parsed reset into Importer.loadFromSource() so a format cannot forget it, and settle what parsedRecords reports

1 participant