Skip to content

insert_many: date-times, Decimals, bytes, numpy scalars and pandas NA stay on the bulk path; an uncarriable value moves its own row, not the chunk - #82

Merged
tae898 merged 5 commits into
mainfrom
mixed-chunk-bulk
Oct 26, 2026
Merged

tae898 merged 5 commits into
mainfrom
mixed-chunk-bulk

Conversation

@tae898

@tae898 tae898 commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Moved from #50: the personal fork was retired; same commits.

This is the port of humemai/arcadedb-embedded-python#528 (package arcadedb, no "embedded" names).

insert_many sends each 10,000-row chunk to the engine as one JSON text. Until now json.dumps raised on a datetime, date, Decimal, bytes, a numpy scalar or a pandas NA/NaT, and one such value anywhere in a chunk sent the whole chunk down the per-row path (new_document + set per property + save, about 10 JPype crossings per row). Real tables nearly always carry a timestamp or a decimal, so most loads never reached the bulk path (backlog rows N-04, J-04, J-05).

What changes

  • A value that sits directly in a row now crosses inside the JSON as a tagged value {"\^A": [kind, payload]}: T (datetime, as the UTC wall clock the per-row path stores), D (date), n (Decimal), b (bytes, base64). DocumentBatcher.fill decodes the tag to the object convert_python_to_java builds for the per-row path (LocalDateTime, LocalDate, BigDecimal, byte[]). Numpy scalars cross as plain numbers (np.bool_ as a bool), pandas.NA/NaT/np.datetime64('NaT') as null, pandas.Timestamp and np.datetime64 as a naive datetime (microseconds, as the per-row path keeps them).
  • A value the bulk path still cannot carry sends its own row down the per-row path, not the 10,000 rows around it: NaN and Infinity, an integer beyond 64 bits, a datetime/Decimal inside a list or dict, a non-str dict key, a lone surrogate, a datetime.time, a numpy longdouble or timedelta64, a non-finite Decimal. Runs of carriable rows still go in bulk, and every row is inserted in the order given. In parallel=True the uncarriable rows are written synchronously after the writers drain (the writers never kept order).
  • json_bulk_dumps (used by GraphBatch) is unchanged; only insert_many uses the new json_bulk_dumps_tagged, because only DocumentBatcher.fill decodes the tags. A user dict keyed by the tag marker is refused by the bulk path, so it stays a map.
  • Not in this PR: insert_dataframe/insert_arrow/insert_columns with a null in a numeric column (the other half of J-05) still builds an Object[] element by element. That is a separate Java change (a validity mask), so it gets its own PR.

Stored values

tests/test_insert_many_mixed_values.py writes the same rows through the per-row path and through insert_many, into a declared type (DATETIME, DATE, DECIMAL, BINARY, LONG, INTEGER, DOUBLE, FLOAT, BOOLEAN properties) and a schemaless one, and compares the stored value and its Python type for 17 value kinds (naive and UTC+9 datetime, date, Decimal with and without exponent, bytes, bytearray, np.int64/int8/uint32/float64/float32/bool_, None). It counts Database.new_document calls to prove which path ran, and also covers pandas.Timestamp (naive with nanoseconds, aware), pandas.NA/NaT and np.datetime64 (the per-row path raises for these; the bulk path stores null / a naive datetime), a 10,000-row chunk with one datetime, an uncarriable row in the middle of 1,000 (exactly one new_document call, stored rows equal the per-row path's), insertion order around per-row runs, commit_every across chunks, rollback on a failing row, parallel=True, and the tag lookalike. The suite failed 15 of 20 tests before the change.

Untimed 1M-row figure

One run each (not through ab.py, so no claim), fresh process, 1,000,000 rows of six properties with a Python datetime per row, row building inside the timed call, JVM -Xmx4g, through quiet_build: insert_many took 31.2 s before (1,000,000 new_document calls, the whole load on the per-row path) and 7.4 s after (0 new_document calls). The same rows without the ts column take 4.1 s, so the datetime itself now costs about 3.3 s per million rows (JSON text plus LocalDateTime.parse) instead of 27 s. Measured on the humemai fork (humemai/arcadedb-embedded-python#528) with the same change.

Local checks

  • tests/test_insert_many_mixed_values.py 20 passed, plus test_bulk_insert.py, test_bulk_json_paths.py, test_float_vector_bulk.py, test_numpy_support.py, test_conversion_fixes.py, test_graph_batch.py, test_insert_columns.py, test_async_executor.py, test_core.py, test_bridge_fixes.py, test_type_conversion.py, test_docs_examples.py, test_check_test_skips.py (all pass) against the engine jars and JRE of the humemai main CI wheel with this branch's Python files and the bridge classes compiled into a copy of the bridge jar (171 passed for the five suites listed first)
  • black, isort and the repository pre-commit hooks pass, bandit reports nothing at medium or above
  • The full suite and the wheel build are left to CI

Docs changed in this PR: the insert_many docstring (what is carried, what still goes per row). This repository keeps its API reference in Antora modules that do not describe the insert_many fallback, so there is no other page to update.

🤖 Generated with Claude Code

tae898 and others added 2 commits October 25, 2026 17:38
…o longer moves its 10,000-row chunk to the per-row path

A datetime, date, Decimal, bytes, numpy scalar, pandas NA or NaT sitting directly in a row now crosses inside the JSON text as a tagged value ({"\u0001": [kind, payload]}) that DocumentBatcher.fill decodes to the objects the per-row path builds (LocalDateTime, LocalDate, BigDecimal, byte[]). A value the text still cannot carry (NaN, Infinity, an integer beyond 64 bits, a date-time inside a list or map, a lone surrogate, a datetime.time) sends only its own row down the per-row path, in order, and the rows around it stay on the bulk path. The parallel mode does the same. Port of humemai/arcadedb-embedded-python#528.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: DocumentBatcher.java header (kept both paragraphs) and core.py (insert_many fallback: the per-row helper creates vertices for a vertex type, as main's fallback does). Same tree as the rebased branch that the tests ran on.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@tae898
tae898 marked this pull request as ready for review October 26, 2026 15:45
tae898 and others added 2 commits October 27, 2026 00:55
…re strict xfail on native

The native parity job failed 8 of the 14 new tests: it has its own per-row path that never calls Database.new_document, so the three non-zero counts
(only the uncarriable row goes per row) now run on the JVM wheel only, with the stored-value comparison on both. The native build refuses pandas NA/NaT
and numpy datetime64 as parameters (TypeError), recorded as strict xfail naming the backlog row so it turns red when native supports them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ead of a strict xfail

The native parity gate counts an xfail as an uncounted skip (parity_wheel.py counts every <skipped>), so the two strict xfail rows made it fail with skipped=2 although all 1520 tests passed.
On native the two tests now assert the TypeError the native build raises today and return; they turn red the day native accepts these values.

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