Repository navigation
Conversation
…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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (14)
Included review availability: Your plan provides up to 4 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe 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. ChangesParsed counter phase scope
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
Suggested reviewers: Merge Risk: ⚪ Minimal · up to 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)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
|
Tick the box to add this pull request to the merge queue (same as
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
🟢 Coverage 90.48% diff coverage · -6.39% coverage variation
Metric Results Coverage variation ✅ -6.39% coverage variation Diff coverage ✅ 90.48% diff coverage 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.
ReviewI read through the diff ( Correctness
Minor nits (non-blocking)
Test coverage
Performance / securityNo concerns. The added work per phase transition is a couple of SummaryI 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
ReviewI read through the diff and the linked issue history, checked every call site of Correctness
Test coverageGood. The four new tests in Minor / non-blocking
Security / performanceNo concerns. No new dependencies, no new synchronization beyond existing 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
ReviewRead through the diff ( Correctness
Design / API
Test coverage
Style
Nothing else stood outNo security concerns (no new I/O, no new external input parsing), no obvious performance regression (the extra 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. |
CI triage on
|
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
Addendum - the two long lanes finished, both also pre-existing
The regression test of the already-closed #6534, in
Summary across all five red lanes on
Zero failures in |
|
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. |
Closes #7342
One
ImporterContextserves all fourloadFromSource()phases of anImporter.load()(
Importer.java:79-82, dispatch at:139), socontext.parsedis a running total unless the phasezeroes it. Nine of the ten
FormatImporterimplementations zeroed it themselves, one at a time;JSONImporterFormatnever did, so a run ending in a JSON phase reported aparsedRecordsthat mixedthat 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 beforeformat.load(), which is the oneplace 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
totalParsedaccumulator rather than discarded, so the run can still report what it parsed as a whole.
What changed
ImporterContexttotalParsedaccumulator;beginParsingPhase()folds the finished phase in and zeroesparsedandlastParsed;totalParsedRecords();toMap()publishestotalParsedRecordsalongsideparsedRecordsand documents what each meansImporter.loadFromSource()context.beginParsingPhase()immediately beforeformat.load(...)- the hoistCSV,RDF,Jsonl,XML,Neo4j,OrientDB,GloVe,Word2Vec,Word2VecLSM)context.parsed.set(0)->context.beginParsingPhase()JSONImporterFormatOrientDBImporter:410'sparsed.set(0)is not a phase boundaryThe two things the issue asked to settle
1. What
parsedRecordsmeans. It stays the last phase's row count, now uniform across all tenformats, and the whole-run figure is published alongside it as a new
totalParsedRecordskeyrather than by redefining the existing one:
parsedRecordsis a published key of a public report map -Importer.load()'s return value, the SQLresult 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.theTwoPhaseCliRouteScopesTheCounterToTheJsonlPhaseassertsparsedRecords == 2Lfor a two-phase run that parsed five rows. Redefining the key means editing thatassertion, 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.
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#printProgressprints(context.parsed.get() - context.lastParsed) / deltaInSecs, andlastParsedwas never reset alongsideparsed, so the first progress line after a phase boundary subtracted the previous phase's total fromthis phase's handful of rows. Reproduced literally by reverting the fix:
beginParsingPhase()zeroes both.lastLapOnis deliberately not reset with them, and the code sayswhy:
printProgressdivides every delta on the line by the same window, and thecreatedDocuments/Vertices/Edgescounters it also prints are import-wide and never reset - restartingthe 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) callformat.load()directly with a pre-loaded counter andassert the format itself zeroes it, so the reset is part of
FormatImporter.load()'s tested contract fordirect callers, and
FormatImporteris a public interface in a public package. The duplication the issueobjected 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.
JSONImporterFormatis deliberately left with no reset of its own. It is the one format whose phaseis zeroed by the hoist alone, which is what makes
theHoistedResetScopesTheCounterToTheJsonPhasea testof the hoist rather than of a tenth copy of the same line - remove the
beginParsingPhase()call fromloadFromSource()and that test goes red.OrientDBImporter.parseRecords():410'sparsed.set(0)is untouched:run()callsparseInputFile()upto three times (
:230ANALYZE,:248CREATE_SCHEMA,:255CREATE_EDGES) and every pass reachesparseRecords()at:373and re-counts the same records, so that one is a deliberate discard, not aphase boundary. Folding it into the accumulator would report every OrientDB record two or three times.
Coverage
theHoistedResetScopesTheCounterToTheJsonPhasebeginParsingPhase()printProgress()first line after a phase boundarylastParsedzeroed withparsedaPhaseBoundaryNeverPrintsANegativeParseRateparsedRecords/totalParsedRecordsmeaningtheImportWideTotalCountsEveryPhase,beginParsingPhaseRollsTheFinishedPhaseIntoTheImportWideTotalFormatImporter.load()on the nine resetting formatsServerControlPlane/ SQLIMPORT DATABASEsingle--urlrunsIssue7346RdfShapeDetectionTest,SQLLocalImporterITArgued, not fixed, with evidence:
format.load()onJSONImporterFormat- deliberate, see above; a reset there would make thehoist unobservable.
OrientDBImporter:410- not a phase boundary, see above.ServerControlPlane.runImport()andImportDatabaseStatementbuild the importer throughImporter(Database, String), which setssettings.urlonly (Importer.java:40); the other threeloadFromSource()calls return immediately on a null url (Importer.java:113-115), so exactly one phaseruns unless the caller also passes
documents/vertices/edgesthroughsetSettings- which only theSQL
WITHclause and the CLI can do, and both are covered by the rows above.Known gaps
ServerControlPlane.scheduleImportCounters()resolvesImporterContext.parsedonce andsamples it every second into the SSE
parsedfield, so a multi-phaseIMPORT DATABASE ... WITH documents=..., vertices=...makes a client's progress bar run backwards ateach phase boundary.
totalParsedadded here is the monotonic field that reader wants, but rewiring theserver's reflective lookup is a server-module change with its own fallback story for older importer jars.
FormatImporter#printProgress's printedParsed %,dtotal has the same shape off the same counter and isnoted on that issue, so both surfaces get decided together rather than one being fixed in isolation.
Test plan
Issue7342ParsedCounterPhaseScopeTest- 4 new tests, and they fail without the fix: with bothhalves reverted (
beginParsingPhase()removed fromloadFromSource(),lastParsed = 0Lremoved frombeginParsingPhase()), 3 of 4 go red withexpected: 2L but was: 5L,expected: 5L but was: -45Landexpected: 0L but was: 3L.mvn -o -pl integration test -DexcludedGroups=benchmark,slow,vector-> 370 tests, 0 failuresmvn -o -pl integration verify -DskipITs=false -DexcludedGroups=benchmark,slow,vector-> 133 ITs,0 failures, covering every affected format end to end:
CSVImporterIT,JSONImporterIT,JsonLImporterIT,XMLImporterIT,Neo4jImporterIT,OrientDBImporterIT,GloVeImporterIT,Word2VecImporterIT,Word2VecImporterLSMIT,SQLLocalImporterIT,SQLRemoteImporterITformat.load()classes are green unchanged,including their two-phase CLI assertions on
parsedRecords- the compatibility claim abovemvn -o -DskipTests install(full reactor) -> BUILD SUCCESSTo verify by hand:
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.
ef60853toMap()'s over-long javadoc line; said in the code whytoMap()inlines the sum instead of callingtotalParsedRecords(); rewordedbeginParsingPhase()'sgetAndSetrationale, 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 inRDFImporterFormatFormatImporter.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 threads173ff01beginParsingPhase()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; documentedtotalParsedRecords(), which had no production caller and read as dead code next to atoMap()that inlines the same arithmetic - the javadoc now names both whytoMap()does not call it and the reader it exists for (#7483)37d0694toMap()andtotalParsedRecords()ever drift, nothing enforces they compute the same thing") was raised as "Not asking for a change". It is in fact enforced:beginParsingPhaseRollsTheFinishedPhaseIntoTheImportWideTotalassertstotalParsedRecords()andtoMap()'stotalParsedRecordsentry against the same5Lin the same test, so a drift between them fails itAdversarial pass: the
Tasktool was unavailable in this environment, so the independent-subagent passcould 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 totaltwice (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
OrientDBImporterparses 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 nostall can break.
🤖 Generated with Claude Code
https://claude.ai/code/session_01B1vgWQXZoDXYq7TqAwLYXY
Summary by CodeRabbit
Bug Fixes
Tests