Skip to content

insert_many and create_vertices: check the rows in the one json.dumps pass, not in a loop before it (refs #318) - #330

Merged
tae898 merged 4 commits into
release/26.11.1from
perf/318-single-pass-validation
Nov 10, 2026
Merged

tae898 merged 4 commits into
release/26.11.1from
perf/318-single-pass-validation

Conversation

@tae898

@tae898 tae898 commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

For users nothing changes in what is stored or in which error a bad row raises. insert_many and create_vertices now check their rows during the one json.dumps pass instead of in a Python loop over every value before it, and the first bad row of a batch still gives the same exception type and message as before. One small difference on the JVM wheel, below.

Refs #318, part of #315. This targets release/26.11.1 (the base of the wheels-only 26.11.1.post1); the engine, jars and image are untouched.

What changed:

  • type_conversion.py: json_bulk_dumps and json_bulk_dumps_tagged write the rows once (_dumps_once). A json.dumps hook cannot see a str or an int (the encoder writes both itself), so the checks come from the text and the types: a surrogate escape or an integer token of 2**63 and above in the text, and a census of the key and value types done in C (set(map(type, chain.from_iterable(...)))). Nothing is copied, so peak memory does not change.
  • The old per-row loop is kept as _check_rows, the exact reference. It runs only when a value may be bad (a hit in the text, a type the census cannot vouch for) and when json.dumps itself refuses the rows. So the exception type and message are the old loop's for the first bad row in row order, and a false alarm costs the walk and nothing else. The old messages name no row index; the tests tell the first bad row from a later one by the value in the message.
  • JVM wheel, GraphBatch.create_vertices: classify_vertex_rows decides the shapes by a type census instead of calling json_bulk_scalar_ok on every value. The rule for a bad scalar (a NaN, an integer beyond 64 bits, a lone surrogate, a subnormal) is the one the code had before: it sends the WHOLE input through the property-matrix path, so the stored type of a property never depends on which chunk held a bad value (the matrix path stores a Python int as a Long and the JSON text as an Integer, and a first version of this PR let one call store both; a CI test caught it). An input of one chunk (at most 100,000 rows) is decided by its own single dumps pass before anything is sent. An input of several chunks first screens its columns of exact scalars in C over the whole input, in blocks of 100,000 rows (type census, the joined text of a str column, min and max of an int column, one float64 array for a float column), and a column that may hold a bad value is judged value by value as the old loop did. So only inputs of several chunks pay a pass besides the dumps, and that pass is not the old per-value loop. The untagged path also refuses a subnormal now, as the tagged path did (the engine reads the text of a subnormal as a DECIMAL). A tagged value that fails inside a chunk (a NaN in a list, a date-time inside a map) still sends only its own chunk the matrix way, as it did before this PR.
  • Traceback: when json.dumps itself refuses the rows (a NaN in a later row, say) and the walk then finds an earlier bad row, the walk's error is raised outside the except block, so it carries no json.dumps error as __context__ (a test checks that). The traceback of the first bad row's error is the one the old loop gave, plus the frames of _dumps_once.
  • Native wheel: the dead NativeGraphBatch._create_vertices_json_bulk is deleted, and the row type check in create_vertices is a C census. insert_many on both wheels gets the change through json_bulk_dumps_tagged.

Not touched: new_edges (its own per-value loop), the columnar and bulk.py paths, and records.py.

Tests, which run in CI only:

  • tests/test_bulk_single_pass_validation.py (both wheels) copies the old loop as a private reference and compares text or exception type and message over 52 shaped cases and 6000 random rows, tagged and untagged. It checks the first bad row of a 20,000-row batch for seven kinds of bad value, the boundaries of the 64-bit pattern, and spies on _check_rows to show that scalar and tagged rows, with the lookalikes (19-digit ints below 2**63, floats with long digit runs, Hangul, CJK, NUL), are not walked while a value that may be bad is. It also stores a 25,000-row scalar batch through insert_many and create_vertices.
  • tests/test_graph_batch_single_pass.py (JVM wheel) shows that a scalar table is not judged value by value, that a name or type the text does not carry still sends the whole input the matrix way, and that a subnormal, NaN, big integer or lone surrogate sends only its own chunk.
  • tests/test_graph_batch_single_pass.py also loads 250,000 rows with an undeclared int property and a NaN in another property of one row in the first, the middle and the last chunk, checks that the whole input takes the matrix path once, and compares the stored type (k.javaType()) and value of that int property across the chunks; a clean input of three chunks is checked too.
  • test_bulk_json_paths.py and test_graph_batch_tagged_shapes.py are adjusted where they asserted the old whole-input behaviour or spied on the old call site.
  • Before opening this I ran the old and new modules against each other on 60,000 fuzzed row sets, tagged and untagged: no difference in text, exception type or message.

I made no speed claim here. The measurement is the untimed profile study (queued) and a later laptop A/B; I will report there whether the gain exists.

🤖 Generated with Claude Code

tae898 and others added 2 commits November 9, 2026 18:58
… pass, not in a loop before it (refs #318)

json_bulk_dumps and json_bulk_dumps_tagged walked every value of every row in Python before json.dumps walked the rows again. The rows are now written once and the checks come from what that pass shows: the text (a surrogate escape, an integer token of 2**63 and above) and a census of the types of the keys and values in C. The exact per-row walk (_check_rows) is the reference and runs only when a value may be bad, and when json.dumps itself refuses the rows, so the exception type and message for the first bad row in row order are the old loop's. Fuzzed against the old code: 60000 cases, no difference.

JVM GraphBatch.create_vertices: classify_vertex_rows decides the shapes by type census instead of a json_bulk_scalar_ok call per value; a scalar the text would change (NaN, integer beyond 64 bits, lone surrogate, subnormal) now sends its own chunk through the property-matrix path, as a tagged chunk already did. The untagged path now also refuses a subnormal, as the tagged one did.

NativeGraphBatch: the dead _create_vertices_json_bulk is deleted, and the row type check is a C census.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed query (refs #318)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
tae898 and others added 2 commits November 9, 2026 21:23
…(no json.dumps __context__); test the stored type across chunks (refs #318)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… it has several chunks (refs #318)

A bad scalar in a later chunk used to send only that chunk through the property-matrix path, which stores a Python int as a Long while the JSON chunks store an Integer: one undeclared property got two Java types in one call. An input of more than one chunk now has its columns of exact scalars screened in C over the whole input first (type census, joined text of a str column, min and max of an int column, one float64 array for a float column, in blocks of 100,000 rows); a column that may hold a bad value is judged value by value as before, so a bad scalar anywhere sends the whole input the matrix way, as before #330. One chunk is decided by its own dumps before anything is sent.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@tae898 tae898 self-assigned this Nov 9, 2026
@tae898
tae898 merged commit 983768c into release/26.11.1 Nov 10, 2026
19 checks passed
@tae898
tae898 deleted the perf/318-single-pass-validation branch November 10, 2026 02:23
@github-actions github-actions Bot added this to the 26.12.1 milestone Nov 10, 2026
abhirajadhikary06 pushed a commit to abhirajadhikary06/arcadedb-python that referenced this pull request Nov 10, 2026
…ows checked in the one json.dumps pass, whole input decided before chunks)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
abhirajadhikary06 pushed a commit to abhirajadhikary06/arcadedb-python that referenced this pull request Nov 10, 2026
… float[] and float32 arrays cross as one matrix per property)

Conflicts, two:
- tests/conftest.py: both sides added a native-only registration at the same spot; both kept, each once.
- src/arcadedb/type_conversion.py classify_vertex_rows, the per-column shortcut: ArcadeData#330 (7e81170) screens the exact-scalar columns of an input of several chunks in C (screen_scalars, _scalar_screen) and judges a suspect column value by value; ArcadeData#339 lets a declared column of vectors (now also Java float[] and double[]) and a column of float32 arrays or Java float[] skip the walk. Merged: an exact-scalar column continues unless screen_scalars and the screen flags it; a column with other types skips the walk when it is a declared vector column (_is_vector_kind) or a column of float arrays (_is_float_array_kind), in both modes, so a multi-chunk create_vertices with float32 columns keeps the one-matrix-per-property path.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
abhirajadhikary06 pushed a commit to abhirajadhikary06/arcadedb-python that referenced this pull request Nov 10, 2026
…cc08cf: column screen for create_vertices, vector blocks)

Conflict: src/arcadedb/type_conversion.py _classify_rows, the per-column shortcut. Candidate 3 had my merge of ArcadeData#330 7e81170 (screen_scalars) and the first ArcadeData#339 (vector shortcuts); the new ArcadeData#339 head contains ArcadeData#330 itself and replaces the scalar screen by _screen_key (one list and one census per block, with the suspect flag) and tracks 'vetted'. Took the ArcadeData#339 side: it keeps both behaviours (a flagged scalar column is judged by value, declared vector columns and float-array columns skip the walk in both modes). type_conversion.py and graph_batch.py equal the ArcadeData#339 head after the merge.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.

1 participant