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>
@tae898
tae898 merged commit 7711179 into main Oct 26, 2026
36 checks passed
@tae898
tae898 deleted the mixed-chunk-bulk branch October 26, 2026 17:11
tae898 added a commit that referenced this pull request Nov 4, 2026
…path

Native insert_many now sends date-times, Decimals, bytes, numpy scalars and pandas NA/NaT inside the bulk text (as the JVM build has since #82); rows the text cannot carry go in with their own statement, in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tae898 added a commit that referenced this pull request Nov 4, 2026
…as NA stays on the JSON bulk entry; an uncarriable value moves its own row, not the chunk

Port of humemai/arcadedb-embedded-python#532, which was closed unmerged in the move to this repo. The JVM build has done this since #82.

_insert_chunk called the untagged json_bulk_dumps, which raises on a datetime, date, Decimal, bytes, numpy scalar, pandas NA or NaT, so the whole 10,000-row chunk went through INSERT INTO t CONTENT :row for each row (arcadedb_n_exec_many). Json.java already decodes the tagged values, so the rows now cross inside the arcadedb_n_insert_rows text via json_bulk_dumps_tagged. A row with a value the text still cannot carry (NaN or Infinity, an integer beyond 64 bits, a date-time or Decimal inside a list or dict, a non-str key, a lone surrogate, a datetime.time) goes to the statement on its own; runs of carriable rows keep the bulk entry (columnar first), and insert order is kept. Python only, no native-image change.

tests/test_native_mixed_chunk.py stores 17 value kinds through the statement path and through insert_many, declared and schemaless, and compares the stored value and its Python type; a spy on lib.call proves the entry. test_native_columnar_insert.py's datetime case is split in two: a datetime stays on the JSON entry, a datetime inside a list is still a statement run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tae898 added a commit that referenced this pull request Nov 4, 2026
…path

Native insert_many now sends date-times, Decimals, bytes, numpy scalars and pandas NA/NaT inside the bulk text (as the JVM build has since #82); rows the text cannot carry go in with their own statement, in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tae898 added a commit that referenced this pull request Nov 4, 2026
…hrough the JSON bulk path (#243)

create_vertices and new_edges refused a whole call when one property was a list, dict, date-time, date, Decimal or bytes, and went per element
through JPype. They now use json_bulk_dumps_tagged (insert_many's format from #82) and VertexBatcher/EdgeBatcher decode with
DocumentBatcher.untag after toMap(). A chunk (vertices) or call (edges) holding a value the text cannot carry still takes the old path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tae898 added a commit that referenced this pull request Nov 5, 2026
…ndas NA stay on the JSON bulk path; an uncarriable value moves its own row, not the chunk (#218)

* Native insert_many: a date-time, Decimal, bytes, numpy scalar or pandas NA stays on the JSON bulk entry; an uncarriable value moves its own row, not the chunk

Port of humemai/arcadedb-embedded-python#532, which was closed unmerged in the move to this repo. The JVM build has done this since #82.

_insert_chunk called the untagged json_bulk_dumps, which raises on a datetime, date, Decimal, bytes, numpy scalar, pandas NA or NaT, so the whole 10,000-row chunk went through INSERT INTO t CONTENT :row for each row (arcadedb_n_exec_many). Json.java already decodes the tagged values, so the rows now cross inside the arcadedb_n_insert_rows text via json_bulk_dumps_tagged. A row with a value the text still cannot carry (NaN or Infinity, an integer beyond 64 bits, a date-time or Decimal inside a list or dict, a non-str key, a lone surrogate, a datetime.time) goes to the statement on its own; runs of carriable rows keep the bulk entry (columnar first), and insert order is kept. Python only, no native-image change.

tests/test_native_mixed_chunk.py stores 17 value kinds through the statement path and through insert_many, declared and schemaless, and compares the stored value and its Python type; a spy on lib.call proves the entry. test_native_columnar_insert.py's datetime case is split in two: a datetime stays on the JSON entry, a datetime inside a list is still a statement run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tests: pandas NA/NaT and numpy datetime64 are accepted by the native insert_many bulk path now, so both builds assert the same stored values

test_insert_many_mixed_values.py asserted that the native build refuses pd.NA, pd.NaT and np.datetime64 with a TypeError, so that the two tests would turn red the day native accepted them. With json_bulk_dumps_tagged on the native path they do (null, null, naive datetime), as the JVM build stores them. Both tests now run the JVM assertions on both builds. Found by the dispatched native parity run 41660808672 (passed=2015 failed=2, exactly these two).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: bulk-loads page lists the values insert_many keeps on the bulk path

Native insert_many now sends date-times, Decimals, bytes, numpy scalars and pandas NA/NaT inside the bulk text (as the JVM build has since #82); rows the text cannot carry go in with their own statement, in order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
tae898 added a commit that referenced this pull request Nov 5, 2026
…bulk path (#244)

* JVM GraphBatch: carry list, dict, date-time and other tagged shapes through the JSON bulk path (#243)

create_vertices and new_edges refused a whole call when one property was a list, dict, date-time, date, Decimal or bytes, and went per element
through JPype. They now use json_bulk_dumps_tagged (insert_many's format from #82) and VertexBatcher/EdgeBatcher decode with
DocumentBatcher.untag after toMap(). A chunk (vertices) or call (edges) holding a value the text cannot carry still takes the old path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* JVM GraphBatch: numeric lists and all-scalar inputs keep main's path (#243)

Step-0 probe (untimed): a list of 128 doubles cost 117 us per row through the text against 31 us on the property matrix (one double[]), and a scalar row 6.18 against 5.22 us. A list or tuple of only int/float now keeps main's path, and an input whose values are all scalars runs main's untagged json.dumps and gate (no shape test, no tagged writer); Java reads scalars as before (DocumentBatcher.value adds two type tests).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* TA-6 test: the chunk-fallback row is ["x", nan], so read element 1 as the NaN (CI run 41731925082)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* TA-6 docs: which property shapes a JVM graph batch loads in bulk, the fallback rule, the laptop A/B figures (#243)

Docs and docstring only. Figures copied by script from the study ab.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 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