fix(#5968): CSV/JSON importer skip-and-log mode instead of aborting on first bad row - #5974
Conversation
…n first bad row CSVImporterFormat.loadDocuments/loadVertices and JSONImporterFormat.parseRecords had no per-row error handling, so a single malformed/out-of-range property value (surfaced more often since #5967 rejects out-of-range numeric narrowing) aborted the whole bulk import. Add an opt-in -onRowError skip|abort setting (default abort, preserving current behavior) that logs and skips the offending row/record instead, reusing the existing ImporterContext.errors counter for the summary.
|
Tick the box to add this pull request to the merge queue (same as
|
Up to standards ✅🟢 Issues
|
Review of #5974 (onRowError skip|abort for CSV/JSON importer)Nice fix, and the approach (opt-in setting, default-preserving, reusing 1.
|
…dge-path divergence, log accuracy - CSVImporterFormat.loadVertices: vertices are persisted via database.async(), so a failure that only surfaces at persist time (mandatory property, unique index, ...) was invisible to the per-row try/catch and silently swallowed by the pre-existing onError log-only callback, even in default "abort" mode. Now the callback counts it into context.errors, and after waitCompletion() the import aborts in "abort" mode by rethrowing the first captured failure. - CSVImporterFormat.loadEdges: added a comment explaining why edges keep their own pre-existing unconditional skip-and-log (unresolved from/to references are expected during graph import), independent from -onRowError. - JSONImporterFormat.parseRecords: log the top-level array index instead of context.parsed (which also counts nested embedded objects/arrays), so the "skipping record #N" message lines up with the actual array position. - ImporterSettings: normalize -onRowError to lowercase on set. - New regression tests for the async vertex persist-time failure in both abort and skip mode.
|
Addressed in bf0b516:
All existing importer tests ( |
ReviewSolid fix for a real pain point (aborting a large bulk import on the first bad row). The abort-by-default behavior preserves backward compatibility, the opt-in flag is validated eagerly, and the new tests do a good job distinguishing the synchronous validation path from the async persist-time path for vertices. A few things worth a look before merge. Correctness: "skip" mode can silently drop valid vertices, not just the bad one
So in Minor / non-blocking
Things verified while reviewing
Nice, well-tested change overall; the batch-rollback caveat above is the main thing I'd want addressed or at least explicitly documented before merge. |
…on't take siblings down - CSVImporterFormat.loadVertices: in -onRowError skip mode, temporarily force database.async().setCommitEvery(1) for the duration of the vertex import (restored afterward). A persist-time failure on the async worker thread rolls back its whole in-flight batch, not just the failing vertex; with a batch size of 1 that blast radius is always exactly the failing row, so "skip" no longer risks silently dropping valid vertices queued in the same uncommitted batch. - CSVImporterFormat.loadDocuments: removed a vestigial database.async() onError registration and waitCompletion() call left over from before this path became fully synchronous; it registered/waited on nothing. - Log the full exception at FINE for skipped rows/records (CSV documents, CSV vertices, JSON) in addition to the WARNING summary line, so root-causing a skipped row after the fact doesn't require reproducing it. - Documented the batch-size mitigation on ImporterSettings.isSkipOnRowError(). - Added a CSV document -onRowError skip test for symmetry with the vertex case, and tightened the vertex async-failure test to assert the exact surviving count now that the blast radius is bounded to one row.
|
Addressed in e539e9b: Main point - "skip" can silently drop valid vertices in the same async batch - fixed properly this time, not just documented. Minor points:
All existing importer tests plus the now-9-test |
Review of #5974 - importer skip-on-row-errorOverall this is a well-scoped, well-tested follow-up to #5967. The abort/skip split, the async-persist-time-failure handling for vertices, and the edge-loop comment explaining why edges are excluded are all good calls. A couple of things worth a look before merge: 1. JSON skip mode can desync the reader on nested/embedded objects (likely bug)
When control returns to So for a JSON import whose mapping has nested embedded objects/arrays with schema'd properties, 2.
|
…tating shared async state - JSONImporterFormat: a Type/schema conversion error thrown from a NESTED mapped object/array (parsed via recursive parseRecord()/parseArray() calls) used to unwind past the enclosing object's own reader.endObject(), leaving the JsonReader mid-object. The outer parseRecords() loop would then either silently drop the rest of the array or abort with a confusing structural exception unrelated to the actual bad value. Every nested BEGIN_OBJECT/ BEGIN_ARRAY recursion site now catches RuntimeException locally (safe, since each recursive call's own tokens are already balanced by the time its own createRecord() can throw) and substitutes null for just that property/ item in skip mode, so a failure at any nesting depth can never desync the reader for sibling records. Added a regression test with a nested mapped object to prove it. - CSVImporterFormat.loadVertices: replaced the previous setCommitEvery(1) mitigation (which mutated database.async()'s batch size for the whole Database instance, affecting any concurrent unrelated async caller) with synchronous per-vertex save() in skip mode, matching how loadDocuments already avoids the async-batch-rollback blast radius entirely - no shared state touched. Abort mode is unchanged (still async, still aborts on the first async persist-time failure).
|
Addressed in 9c42e5f: 1. JSON reader desync on nested objects (likely bug) - confirmed and fixed. You were right: the invariant only held for flat top-level records. Every nested 2. 3. Minor points - Full |
|
Review in progress - posting full findings shortly. |
|
Review: -onRowError skip|abort importer setting (issue 5968) Overall this is a solid, well-tested feature: the opt-in default-abort design, the reader-desync analysis for nested JSON, and the abort-mode async persist-time-failure fix (previously silently swallowed even in the default abort mode) are all good. There is one real correctness gap around transaction handling in skip mode, plus a couple of minor nits. 1. Skip mode can commit a partially-written ghost record when the failure happens after the bucket write (e.g. duplicate unique key) [Confirmed] CSVImporterFormat.loadDocuments/loadVertices open a single transaction before the row loop and only call database.commit() once, at the very end. The per-row catch (RuntimeException e) (CSVImporterFormat.java around line 140-148 for documents, 275-283 for vertices) logs and continues the loop, but unlike JSONImporterFormat.parseRecords, which explicitly calls database.rollback() before continuing, it never rolls back. That matters because LocalDatabase.createRecordNoLock writes the record into the transaction (bucket.createRecord(...), transaction.updateRecordInCache(...), transaction.updateBucketRecordDelta(...)) BEFORE indexer.createDocument(...) runs. If the failure is a unique-index violation (DuplicatedKeyException, itself an ArcadeDBException/RuntimeException), the bucket slot has already been allocated by the time the exception is thrown, and since an explicit transaction is active (not the implicitTransaction case), there is no automatic rollback of that partial write. Concretely: -vertices dirty.csv -typeIdProperty Id -onRowError skip (the exact pattern used in this PR's own tests) auto-creates a unique index on Id. If two rows share the same Id, the second row's v.save() throws DuplicatedKeyException inside indexer.createDocument(), after bucket.createRecord() already ran. The catch block increments context.errors and moves on, but the half-created vertex (RID assigned, present in the bucket, bucket-count delta already bumped) is never rolled back, and rides along into the single database.commit() at the end of the loop. The result is a vertex that exists in the bucket (shows up in db.countType()/full scans) but has no reachable index entry, so it can never be found via lookupByKey, can never be resolved as an edge endpoint by loadEdges, and cannot be cleaned up through normal key-based operations. That is worse than the abort behavior this feature is meant to improve on, and it quietly breaks the PR's own stated guarantee that skip only ever loses the bad row; here the bad row is not fully lost, a corrupted fragment of it survives. The same gap exists in JSONImporterFormat's nested object/array recursion catches (parseRecord's BEGIN_OBJECT/BEGIN_ARRAY cases, parseArray's BEGIN_OBJECT/BEGIN_ARRAY cases): those set attributeValue/entryValue = null and continue without rolling back. Only the top-level parseRecords() catch rolls back correctly, because that's the only place where the whole per-record transaction is scoped and discarded. The out-of-range/mandatory-property cases this PR's tests exercise do not hit this, because Type.convert() failures happen in MutableDocument.set() (pure in-memory) and mandatory-property failures happen in DocumentValidator.validate(), both of which run before bucket.createRecord(). It is specifically index-time failures (duplicate keys being the realistic one, since a unique index on the id column is exactly what this PR's own vertex-import codepath creates) that fall through the gap. Might be worth rolling back per-row in the CSV paths too (mirroring what JSON already does at the top level), or documenting the limitation explicitly if a full per-row transaction wrap is judged too costly. 2. Skip mode drops periodic commits for vertices (minor/performance) Pre-PR, vertex import always went through database.async() with its own internal batching/commit cadence. In skip mode, vertices are now saved synchronously one-by-one inside a single transaction that isn't committed until the entire file has been read. settings.commitEvery is effectively ignored for the skip path. For very large CSVs this means the whole import is held in one uncommitted transaction, a different memory/WAL footprint than the batched-async default. It might be worth committing every N rows (still per-row save(), just periodic commit()/begin()) to bound this, especially since large/dirty files are exactly the use case -onRowError skip targets. 3. Minor: stale test doc/name Issue5968ImporterSkipOnRowErrorTest.csvVertexImportCountsAsyncPersistTimeFailureWhenOptedIn (and its inline comment) still describes the force setCommitEvery(1) on database.async() mitigation from an earlier commit in this PR. The final implementation (commit 4) replaced that with a fully synchronous v.save() for skip mode, so skip mode no longer touches the async executor at all; the comment (and arguably the test name) is now stale/inaccurate. Nit: broad catch (RuntimeException e) All the new catch sites catch RuntimeException broadly rather than the specific data-related exceptions (ValidationException, IllegalArgumentException, DuplicatedKeyException, ...). In skip mode, an unrelated bug elsewhere (e.g. an NPE from a code defect) would now be silently logged as a skipped row instead of surfacing as a real failure. This may be an intentional robustness trade-off for a bulk importer, just flagging it as something to be aware of. Test coverage is otherwise thorough (abort/skip pairs for CSV documents, CSV vertices, JSON documents, JSON nested objects, plus the async persist-time-failure regression and eager -onRowError validation). Finding 1 is not covered because none of the fixtures trigger an index-time failure under skip mode; a test with duplicate -typeIdProperty values in skip mode would likely reproduce it. |
…N nested-failure propagation) - CSVImporterFormat.loadDocuments/loadVertices: in -onRowError skip mode, each row now commits (or rolls back) its own transaction instead of sharing one transaction for the whole file. LocalDatabase#createRecordNoLock writes the bucket entry before the indexer runs, so a failure that only surfaces at index time (a duplicate key, the realistic case since loadVertices itself auto-creates a unique index on -typeIdProperty) used to leave a "ghost" record - present in the bucket, inflating countType(), but never indexed and unreachable via lookupByKey - riding along into the single end-of-file commit. Rolling back just the failing row's own transaction can never touch a previously committed row, closing the gap. Regression tests reproduce the duplicate-key scenario for both loadDocuments and loadVertices. - JSONImporterFormat: the same ghost-record risk existed for nested mapped objects/arrays, whose recursive parseRecord()/parseArray() catches previously just substituted null and kept going. A nested record's own bucket write can only be undone by rolling back its enclosing top-level record's transaction (real nested begin()/commit() would each commit independently and durably, which is worse - a nested failure now sets a recordFailed flag threaded down through parseRecord()/parseArray(), and parseRecords() discards the whole top-level record via its existing, already-correct rollback path when the flag comes back set. This changes nested-failure granularity from "drop just the bad property" to "drop the whole record", trading a larger blast radius for the same ghost-record-free guarantee flat top-level failures already had. - Fixed a stale test name/comment left over from the abandoned setCommitEvery(1) approach (superseded by synchronous per-row save()).
|
Addressed in 5fb47df: 1. Ghost record on index-time failure (Confirmed) - fixed for both CSV paths. You were exactly right: Also confirmed and fixed the same gap in JSON's nested 2. Skip mode dropping periodic commits (minor/performance) - superseded by the fix above: skip mode now commits every row by necessity (that's what makes the rollback safe), so there's no separate batching knob to reintroduce; 3. Stale test doc/name - fixed: renamed Nit (broad Side note, not part of this PR: chasing the duplicate-key repro surfaced a preexisting, unrelated quirk - 14 tests in |
Review: fix(#5968) CSV/JSON importer skip-and-log modeNice work here — the commit history shows this went through several rounds of self-review already (ghost records, JSON reader desync, shared-state mutation), and the final design holds up well under a close read. A few things worth a look before merge: Performance consideration (the one I'd weigh most)In
Not a blocker, but given Minor / polish
What looks solid
Overall this is in good shape; the performance trade-off is the main thing I'd want surfaced/acknowledged explicitly before merge, everything else is minor. |
…w round 6 - ImporterSettings: onRowError field now documents that it governs document/vertex rows only (edges always skip-and-log unconditionally), and isSkipOnRowError()'s javadoc spells out the per-row-transaction throughput trade-off explicitly (effectively commitEvery=1 for the whole run while skip mode is on, not just for the failing row). - CSVImporterFormat: added a comment explaining why an IOException from csvParser.parseNext() can never coincide with an open, dirty per-row transaction in skip mode (the only other throwing call in the row loop is the row-processing logic itself, already caught; parseNext() only runs at the top of the next iteration, right after the prior row's transaction was already committed or rolled back and a fresh empty one begun). - New sqlImportDatabaseSupportsOnRowErrorSkipSetting test locking in the IMPORT DATABASE ... WITH onRowError=skip entry point (reflection-based settings forwarding), previously only covered via the CLI-arg constructor. - Tightened jsonNestedObjectImportSkipsWholeRecordAndKeepsProcessingSiblingsWhenOptedIn's errors assertion from isNotNull() to isEqualTo(1L), matching every sibling test in the file and guarding against future double-counting in the nested-failure path.
|
Addressed in a1215f7: Performance trade-off - not changing the mechanism (per-row commit is what makes the correctness guarantee from round 4 hold), but made it much more visible: Minor 1 ( Minor 2 (SQL Minor 3 (loose Minor 4 (IOException mid-loop leaving a dirty transaction) - traced through this and it's a non-issue by construction, so no code change: the only other throwing call in the row loop is 15 tests in |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
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.
Review:
|
…rolled back - CSVImporterFormat.loadDocuments/loadVertices: in skip mode, createdDocuments/ createdVertices were incremented right after save(), but a duplicate-key violation is only detected later, at commit() (TransactionIndexContext checks uniqueness against already-committed data there, not in save()). A row whose commit() then failed and got rolled back was still counted as created. Moved the increment to after commit() succeeds. - JSONImporterFormat: the same class of bug, worse - createRecord() counted a new document/vertex/edge as soon as it was allocated, before its properties were even set (let alone saved or committed), so a record that failed and triggered the round-5 whole-record rollback was still counted. Rather than threading counts through the whole recursive parse/create chain, snapshot the context counters before attempting a top-level record in parseRecords() and reset them back to the snapshot on any rollback (caught RuntimeException or a nested recordFailed) - correctly undoes nested increments too, however deep they occurred. - Added createdDocuments/createdVertices assertions to the duplicate-key ghost-record tests and the nested-JSON test, which are exactly the scenarios that were silently overcounting. - Removed "see #5974 review"/"round N" references from comments and test Javadoc per CLAUDE.md guidance (that context belongs in the PR, not in code that outlives it); rephrased to describe the invariant itself.
|
Addressed in 0867a43: Counter overcount bug (real, confirmed) - fixed in both CSV and JSON paths.
Style nit (comments referencing "#5974 review"/"round N") - removed all of them from source comments and test Javadoc, rephrased to describe the invariant itself per CLAUDE.md's guidance that fix/task/review context belongs in the PR, not in code. 15 tests in |
ReviewOverall this is a well-engineered fix: the CSV/JSON per-row skip logic correctly reasons through transaction boundaries (per-row commit/rollback to avoid "ghost" bucket-only records on duplicate-key/commit-time failures), A few things worth a second look before merge: 1. Behavior change in the default (
This is arguably a correctness fix, and it's covered by a test, but it's a behavior change for the default mode that isn't mentioned in the PR summary (which frames the change as purely additive/opt-in and backward-compatible). Any existing pipeline that was unknowingly relying on "vertex import completes even if a few rows fail validation" will now see those imports abort. Worth calling out explicitly as an intentional fix in the PR description so it doesn't surprise anyone bisecting a newly-failing import job. 2. Skip mode collapses CSV vertex import to synchronous, batch-size-1 commits Well documented in the code/Javadoc, but worth flagging explicitly in review: enabling 3. In 4. Minor: The new onRowError-skip increments reuse the existing 5. Style: very dense, repetitive ALL-CAPS block comments The file already used all-caps inline comments before this PR, so the convention itself is consistent, but several of the new comments (e.g. the near-duplicate reasoning blocks in Things that look solid:
Nice test coverage overall - the abort/skip pairing per scenario plus the two ghost-record regression tests give good confidence in the transaction-boundary reasoning. |
…comments - JSONImporterFormat.load(): log a one-time WARNING when -onRowError skip is set but the source is a single top-level JSON object (no -mapping), where the flag has nothing to recover into and any error still aborts - instead of silently doing nothing. - CSVImporterFormat: condensed the near-duplicate per-row transaction/ghost- record reasoning repeated across loadDocuments/loadVertices into short pointers at ImporterSettings.isSkipOnRowError()'s Javadoc (which already carries the full explanation), keeping only what's specific to each method.
|
Addressed in 0277dfb: 1. Behavior change in the default 2. Skip mode throughput trade-off - already documented in 3. 4. 5. Repetitive ALL-CAPS comments - condensed the near-duplicate per-row transaction/ghost-record reasoning in 15 tests in |
ReviewI read through the diff in Potential bug:
|
…on edge case - CSVImporterFormat.loadDocuments/loadVertices: documented (with an empirical test result) why csvParser.parseNext() is deliberately left outside the per-row try/catch, unlike the row-processing logic: a univocity TextParsingException (maxCharsPerColumn/maxColumns exceeded, malformed quoted field, ...) leaves the parser's own position tracking compromised. Verified empirically that catching it and calling parseNext() again does not cleanly resume at the next line - it can return a truncated/wrong row and silently drop the following one entirely, with no error raised for the lost row. That would be worse than aborting, so a CSV-syntax-level parse failure still aborts the import even in skip mode; only row-content validation failures (out-of-range values, missing mandatory properties, duplicate keys) are skippable. - CSVImporterFormat.loadVertices: moved the skip-mode database.begin() to after the source file is successfully opened, so an IOException from opening/beginParsing() can no longer leave an (empty, but untracked) transaction for the caller to reconcile.
|
Addressed in 585166b: CSV-syntax-level parse errors not caught (the main point) - investigated this empirically rather than guessing, since the fix here hinges entirely on whether univocity-parsers can safely resume after a Given that, I intentionally left
Minor points - 15 tests in |
ReviewReviewed the diff and the current state of Overall this is a well-scoped, carefully iterated change (9 commits progressively closing gaps: ghost records, overcounting, reader desync, leaked transactions). The pre-existing bug fix (async vertex persist-time failures being silently swallowed even in default "abort" mode) is valuable independent of the opt-in feature. A few things worth a look before merge: 1. Skip mode's per-row
|
|
Addressed the one concrete gap in 97a7e2f; the other three are either recurring themes or explicitly framed as non-urgent. 2. JSON's weaker abort-mode atomicity missing from release notes (fixed) - confirmed and agreed: unlike CSV documents' single whole-file transaction, JSON's per-record commit (unconditional, not gated on skip mode) means a later record's failure doesn't roll back earlier successfully-imported ones, even in default `abort` mode. This was already locked in by `jsonDocumentImportAbortsWithoutDiscardingCallersPendingWorkInExternallyManagedTransaction` but never called out in the docs alongside the equivalent CSV vertex caveat. Added a paragraph mirroring it. 1. Comment density - same recurring theme from many earlier rounds, already weighed repeatedly; not doing another pass. 3. `context.errors` overloading - explicitly framed as "worth considering as a fast-follow if `-onRowError skip` sees real usage," i.e. future work outside this PR's scope, not something to act on now. 4. `stopParsingQuietly` swallowing on the success path too - explicitly "probably fine, just flagging for awareness," no concrete ask. Verified: no code changes this round (docs-only), so the previous verification (36 tests, 3x clean runs, broader importer suites, full compile check) still stands unchanged. |
ReviewReviewed the diff ( Strengths
Possible issue: stale
|
…ces completes database.async().onError() replaces the previous handler rather than stacking, and DatabaseAsyncExecutor exposes no getter to save/restore whatever was registered before this call. For an externally-managed Database (the Importer(Database, String) embedding constructor, or IMPORT DATABASE reusing the caller's own Database), the handler registered by loadVertices() in abort mode would otherwise stay live after the method returns and keep routing the caller's own later, unrelated database.async() failures into this call's own stale ImporterContext/firstAsyncError. Added a handlerActive AtomicBoolean: the handler closure returns immediately once it's false, and the finally block flips it false after the method's own async work has already been drained via waitCompletion() on every code path that reaches it - so this can only suppress reactions to later, unrelated work, never miss one of this import's own errors. Added a regression test that runs a vertex import to completion, then issues a separate, unrelated async failure directly against the same Database and confirms it is not attributed to the finished import's error count. Verified non-vacuous: fails with the guard removed (expected: 1L but was: 2L).
ReviewI read through the full diff (both parent commits of the merge are checked out locally in this environment) and traced the underlying OverallThis is an unusually well-engineered change. The transaction-ownership reasoning is subtle (self-managed vs. externally-managed
I did not find a correctness bug in the core logic. The test suite ( Minor / non-blocking notes
PerformanceSkip mode's cost (per-row transaction, synchronous vertex saves losing SecurityNo concerns - Nice work on the test coverage and on documenting the non-obvious invariants inline, this would have been very easy to get subtly wrong. |
…erFormat CLAUDE.md's guidance is to default to no comments and add one only where the WHY is non-obvious; several methods here carried paragraph-length comments on nearly every statement, which had started to compete with the code for attention. Condensed the verbose blocks in loadDocuments/ loadVertices/loadEdges (CSVImporterFormat) and load/parseRecords/ parseRecordsArray/parseRecord (JSONImporterFormat) to their load-bearing invariants, dropping restated context that's already covered elsewhere (cross-references to sibling methods, redundant "why" repeated across symmetric branches). Left ImporterSettings#isSkipOnRowError() and ImporterContext#callerTransactionActiveOnEntry as-is: both are already the single consolidated Javadoc that the trimmed inline comments point back to, which is exactly where this kind of design narrative belongs. No behavior change. Verified via full test-compile, 3x runs of Issue5968ImporterSkipOnRowErrorTest (37/37), CSVImporterIT/JSONImporterIT/ GraphImporterCSVTest/GraphImporterTypedPropsTest, and a clean module install.
|
Addressed in 710b913 and 5eee805. Stale Comment density (fixed, 5eee805) - valid per CLAUDE.md's default-to-no-comments guidance. Trimmed the paragraph-length comments in
Dedicated 37 tests in PR: #5974 |
…rowable identity Throwable doesn't override equals(), so the .equals() checks in attachConcurrentAsyncError are really reference-identity comparisons, done this way instead of == to satisfy ErrorProne's Throwable-reference-equality lint. A reviewer flagged that without a comment, a future reader could mistake this for a same-message equality check and "simplify" it into one. No behavior change. Verified via full test-compile, 3x runs of Issue5968ImporterSkipOnRowErrorTest (37/37), the broader importer suites, and a clean module install.
|
Addressed in b352a89. This review found no correctness bugs and called the change "unusually well-engineered" - the four notes were all explicitly minor/non-blocking.
Complexity concentration in Two transaction-ownership strategies (CSV reuses, JSON nests) for the same conceptual problem - explicitly called "a deliberate, explained choice rather than an oversight" via the contrasting class-level Javadocs already in place. No action requested. Default- 37 tests in PR: #5974 |
ReviewThis is a well-scoped fix for #5968, and the transaction-ownership reasoning is unusually well documented (both in the release notes and in-code Javadoc/comments). I read through Correctness
Design / behavior-change risk
Performance
Test coverage
Nit
Nice work on the exclusive-transaction-ownership guard for |
ReviewI read through the full diff ( OverallThis is a very carefully engineered change. The transaction-ownership problem (per-row commit/rollback vs. not stepping on a caller-managed transaction) is genuinely tricky, and the PR handles it consistently across all four paths (CSV documents, CSV vertices, CSV edges, JSON records), with CorrectnessI couldn't find a scenario that breaks the invariants documented in the Javadoc/comments. A few things I specifically traced and confirmed hold up:
Minor / nit-level observations
Test coverageExcellent - genuinely one of the more thorough regression suites for a change like this. It covers both formats, both modes, the ghost-record scenario (bucket write surviving without its index entry), multi-batch async survival semantics, nested JSON object/array failures without desyncing the reader, the HTTP-atomic-transaction rejection path via Docs / behavior-change communicationThe release notes callout for the vertex "abort" default-path behavior change is exactly the kind of thing that should be flagged loudly, and it is - good execution here. The distinction drawn between the document path's whole-file atomicity and JSON's per-record atomicity in "abort" mode is also correctly caveated rather than glossed over. No security or dependency concerns (no new dependencies; all changes are internal to the importer's transaction/error-handling flow). |
…mode vertex schema-survival gap Two follow-ups from review: 1. context.createdDocuments can undercount relative to what actually persisted when a mid-loop row failure happens in default "abort" mode with ownsTransaction == false (an externally-managed Database with a caller-owned transaction already active): rows saved before the failing one are left staged, uncommitted, in the caller's own still-open transaction, but this import's own createdDocuments never gets credited for them since the throw skips the post-loop addAndGet() entirely. This is deliberate, not a bug: whether those rows ultimately become durable is the caller's own commit/rollback decision on a transaction this import never controlled, not something a FAILED import can vouch for in its own summary. Documented the reasoning in loadDocuments() and locked it in with an assertion in csvDocumentImportAbortsWithoutDiscardingCallersPendingWorkInExternallyManagedTransaction. 2. Added csvVertexImportAbortsOnFirstRowFailureWhenSchemaAutoCreatedViaEmbeddingConstructor, symmetric to the existing document-side and "skip"-mode vertex-side coverage of the same scenario: default "abort" mode, embedding constructor, lazily-created vertex type, first-row synchronous v.set() type-conversion failure. Confirms the auto-created Node type/Id index survive the failure and no vertex is durably counted. No behavior change. Verified via full test-compile, 3x runs of Issue5968ImporterSkipOnRowErrorTest (38/38, up from 37), the broader importer suites, and a clean module install.
|
Addressed in fe98d64. No correctness bugs found in this review either; two concrete, actionable points and several already-settled/non-blocking ones.
Missing "abort mode + vertices + schema-auto-create + first-row synchronous failure" test (added) - added
Default abort-path behavior change worth confirming for the target release - already flagged to the maintainer in the previous round's reply; no new action.
38 tests in PR: #5974 |
|
No code changes this round - all three points are explicitly minor/non-blocking, and none warrant a change:
Comment density in
No test/behavior changes, so the previous round's verification (38 tests, 3x clean runs, broader importer suites, clean install) still stands unchanged. PR: #5974 |
ReviewThis is a thorough, well-engineered PR. The transaction-ownership reasoning ( Potential issue:
|
…k() throws mid-array parseRecordsArray()'s while (reader.peek() == BEGIN_OBJECT) evaluated reader.peek() outside the per-record try/catch, so a RuntimeException from it (Gson's JsonReader declares peek() as throwing only IOException, but CSVImporterFormat's equivalent parseNext()-in-the-loop-condition already defends against exactly this for the same reason) would leave that iteration's transaction dangling, uncommitted and unrolled-back. A first attempt at fixing this added an unconditional rollback in parseRecords()'s own outer RuntimeException catch, on the reasoning that this method always begins a fresh nested transaction and never touches a caller's. That reasoning doesn't hold: by the time a RuntimeException reaches that outer catch, the per-record catch inside parseRecordsArray() may have *already* correctly rolled back its own level and popped the transaction stack down to an externally-managed Database's own, still-active caller transaction - rolling back again there discards the caller's unrelated pending work instead of anything of this import's own. Caught by re-running the full regression suite before pushing: jsonDocumentImportAbortsWithoutDiscardingCallersPendingWorkInExternallyManagedTransaction failed exactly this way. Fixed at the actual source instead: reader.peek() now runs inside its own small try/catch at the top of the loop, where the active transaction is unambiguously this loop's own level (just begun, untouched since) - safe to roll back unconditionally, with no risk of the ambiguity above. The existing per-record try/catch is untouched. No dedicated regression test added for this path: Gson's JsonReader only throws IOException from peek() in practice, so this is defense-in-depth against a scenario not reachable through the public API today, not a currently-triggerable bug. Also fixed a stale test count in the PR description (27 -> 38). Verified via full test-compile, 3x runs of Issue5968ImporterSkipOnRowErrorTest (38/38, including the test that caught the first, incorrect attempt), the broader importer suites, and a clean module install.
|
Addressed in ebe6f53. Good catch on the dangling-transaction gap - worth disclosing how it went, since my first attempt was wrong.
Fixed at the actual source instead: No dedicated regression test added for this specific path: as the review itself notes, Gson's
PR description said 27 tests, actually 38 - fixed directly in the PR description. 38 tests in PR: #5974 |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 0 |
🟢 Coverage 85.07% diff coverage
Metric Results Coverage variation Report missing for f80265f1 Diff coverage ✅ 85.07% diff coverage Coverage variation details
Coverable lines Covered lines Coverage Common ancestor commit (f80265f) Report Missing Report Missing Report Missing Head commit (c40c466) 185989 132526 71.25% 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 (#5974) 221 188 85.07% 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%1 Codacy didn't receive coverage data for the commit, or there was an error processing the received data. Check your integration for errors and validate that your coverage setup is correct.
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.
ReviewWent through Possible issue: async error handler is not restored for externally-managed databases In Practical effect for the embedding constructor ( Everything else checked out
Not verified in this environment I was not able to run Nice work on the documentation-in-comments here - the invariants around transaction ownership are genuinely subtle and future readers (including future PRs touching this code) will benefit from them being spelled out. |
… caller's async error handler Narrow, explicitly non-blocking edge case from review: a CSV vertex import in default "abort" mode registers its own database.async().onError(...) handler for the duration of loadVertices(), and handlerActive only makes it inert afterward rather than restoring whatever the caller had registered before - DatabaseAsyncExecutor has no getter to read that back. Only matters for a long-lived externally-managed Database with a pre-existing custom async handler that keeps using database.async() after the import returns, and it's arguably a pre-existing anti-pattern this PR deepens (pre-PR the overwrite at least kept logging SEVERE) rather than a fresh regression - so documenting it on the embedding constructor rather than building out DatabaseAsyncExecutor save/restore machinery for it. No behavior change. Verified via full test-compile, 3x runs of Issue5968ImporterSkipOnRowErrorTest (38/38), the broader importer suites, and a clean module install.
|
Addressed in d538066. This review found no correctness bugs ("everything else checked out" across CSV/JSON transaction handling, async draining, style, test coverage) - only one narrow, explicitly non-blocking point. Embedding constructor doesn't restore a caller's own async error handler (documented) - agreed this is real but narrow: only matters for a long-lived externally-managed 38 tests in PR: #5974 |
Review: bulk importer skip-and-log mode (#5968)I did a close read of the transaction-ownership logic in What I checked specifically
I didn't find a functional bug in this logic. A couple of minor, non-blocking observations:
Test coverageThe 38-test suite is thorough: default-abort regression coverage (including the async persist-time-failure fix), skip-mode happy path with counts, ghost-record regressions for both CSV documents and vertices, nested JSON failure isolation, the SQL Behavior-change disclosureThe PR description and release note are upfront about the two behavior changes in the default Nice work on the transaction reasoning and the regression tests that pin down some genuinely subtle interactions (ghost records, reader desync, dangling schema transactions). |
… Javadoc scope for JSON edges Two readability nits from review, both non-blocking: 1. `if (ownsTransaction && database.isTransactionActive()) database.rollback();` appeared identically in six places across loadDocuments/loadVertices. Extracted a private rollbackIfOwned(database, ownsTransaction) helper, mirroring the existing beginRowTransaction()/computeTransactionOwnership() pattern already in this file. Pure deduplication, no behavior change. 2. ImporterSettings.onRowError's field Javadoc said "edges always skip-and-log unconditionally" as a general statement, but that's only true for CSVImporterFormat.loadEdges. JSON edges go through the same per-record convertMap()/createRecord() path as JSON documents/vertices and are subject to the same onRowError handling - pre-existing behavior, not introduced by this PR, but worth clarifying so a reader doesn't assume the CSV-specific rule also covers JSON. Verified via full test-compile, 3x runs of Issue5968ImporterSkipOnRowErrorTest (38/38), the broader importer suites, and a clean module install.
|
Addressed in c40c466. No functional bug found in this review either - two readability nits, both implemented.
Repeated Performance tradeoff (skip mode's throughput cliff) - already disclosed prominently in the settings Javadoc and release notes; no action needed, just noted as worth the maintainer's attention. 38 tests in PR: #5974 |
ReviewThis is a well-engineered fix with an unusually thorough test suite (38 tests, Verified correct
Minor, non-blocking observations
Test coverageExcellent - covers both CSV and JSON, documents/vertices/edges, default vs. skip mode, embedding constructor vs. CLI vs. SQL Not runI wasn't able to execute Overall: solid fix for a real pain point (aborting multi-hour imports on the first bad row), with unusually careful attention to not regressing the embedding API's transaction contract. No blocking issues found. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #5974 +/- ##
==========================================
+ Coverage 68.22% 69.07% +0.84%
==========================================
Files 1797 1797
Lines 153395 153646 +251
Branches 32481 32509 +28
==========================================
+ Hits 104658 106134 +1476
+ Misses 35338 34116 -1222
+ Partials 13399 13396 -3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Summary
Follow-up from #5967 (fix for #5905/#5906:
Type.convert()now rejects an out-of-range numeric value being narrowed toINTEGER/SHORT/BYTEinstead of silently wrapping it).CSVImporterFormat.loadDocuments/loadVerticesandJSONImporterFormat.parseRecordshad no per-row/per-record error handling: a single malformed or out-of-range property value aborted the entire bulk import job, which is expensive to iterate on for large imports (fix one bad row, restart, hit the next one, repeat).Behavior change in the default (
abort) path — please readWhile fixing this,
CSVImporterFormat.loadVerticesturned out to have a pre-existing bug that this PR also fixes, even without opting into-onRowError skip: vertices are persisted viadatabase.async(), and a persist-time failure (e.g. a missing mandatory property, only caught on the async worker thread) was previously only logged atSEVERE— the import completed "successfully" even though some vertices silently failed to persist. That handler now captures the failure and the import aborts with anImportExceptioninstead of silently succeeding. Note this still isn't full atomicity for vertices: they persist incommitEvery-sizeddatabase.async()batches, and a persist-time failure only rolls back the batch containing the bad record - any earlier batch already committed stays durably persisted (seecsvVertexImportAbortsOnOutOfRangeValueByDefaultButPriorBatchesSurvive, which pins this down). "abort" means "fail loudly" for vertices, not "nothing is imported" the way it does for documents. This is arguably a correctness fix and is covered bycsvVertexImportAbortsOnAsyncPersistTimeFailureByDefault, but it is a behavior change in the default path: an existing pipeline that was unknowingly relying on "vertex import completes even if a few rows fail validation" will now see that import abort instead of silently dropping rows.Changes
ImporterSettings: new opt-in-onRowError skip|abortsetting (also usable fromIMPORT DATABASE ... WITH onRowError=skip), defaulting toabortto preserve today's behavior for backward compatibility (aside from the fix above). Invalid values are rejected eagerly. Governs document/vertex rows only — edges already skip-and-log unconditionally, since an unresolved from/to reference is expected during graph import, not a data error.CSVImporterFormat.loadDocuments/loadVertices: per-row try/catch around the build+save. In skip mode, each row commits (or rolls back) its own transaction instead of sharing one transaction for the whole file — a failure that only surfaces at index time (a duplicate key) already has a bucket write by then, and only rolling back that row's own transaction can undo it without discarding an unrelated, already-committed row ("ghost record" — present in the bucket, never indexed).loadVerticesadditionally switches to a synchronous per-vertexsave()in skip mode instead ofdatabase.async(), for the same reason (an async batch rollback would otherwise take down every other vertex queued in the same uncommitted batch, not just the failing one). This trades import throughput — effectively a batch size of 1 for the whole run while skip mode is on, not just the failing row — for the guarantee that skip mode can never lose more than the one bad row, nor leave a ghost record behind.JSONImporterFormat: aType/schema conversion error is caught at every nestedBEGIN_OBJECT/BEGIN_ARRAYrecursion site inparseRecord()/parseArray(), not just at the top level — a failure buried in a nested mapped object could otherwise unwind past the enclosing object's ownreader.endObject(), desyncing theJsonReaderfor the rest of the array. A nested failure discards the whole enclosing top-level record (not just the bad nested field) via the existing top-level rollback path, since a nested record can already have a bucket write that only a full transaction rollback can safely undo.createdDocuments/createdVertices/createdEdgescounters are snapshotted before each top-level record and reset on rollback, so a record that gets allocated-then-discarded is never counted. A genuinely malformed JSON structure (anIOException, not a data error) is not caught anywhere in this chain and still aborts the import, even in skip mode.ImporterContext.errorscounter (already used elsewhere inJSONImporterFormatfor other non-fatal skip conditions) rather than adding a new one; it's surfaced inImporter.load()'s returned summary map ("errors").Test plan
Issue5968ImporterSkipOnRowErrorTest(inintegration, 38 tests) covers, for both CSV and JSON:abortbehavior reproduces the original bug (ImportExceptionwith the expected root cause), including the async persist-time-failure fix above.-onRowError skipimports the valid rows, skips the bad one, and reports the expectederrors/createdDocuments/createdVerticescounts.IMPORT DATABASE ... WITH onRowError=skip(the SQL entry point) is covered separately from the CLI-arg constructor.-onRowErrorvalue is rejected eagerly at settings-parsing time.Ran
CSVImporterIT,JSONImporterIT,GraphImporterCSVTest,GraphImporterTypedPropsTestalongside the new tests — all green, no regressions.mvn -pl integration -am install(compile + full module test run) passes. Full suite repeated 3x to rule out flakiness in the async/sync transaction paths.🤖 Generated with Claude Code