Skip to content

JVM insert_columns: a column of str and None crosses as one UTF-8 blob plus int32 offsets (refs #324) - #334

Merged
tae898 merged 2 commits into
release/26.11.1from
perf/324-jvm-string-blob
Nov 10, 2026
Merged

tae898 merged 2 commits into
release/26.11.1from
perf/324-jvm-string-blob

Conversation

@tae898

@tae898 tae898 commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

For users nothing changes in what is stored: a string column given to insert_columns (and through it to insert_dataframe and insert_arrow) on the JVM wheel stores the same values as before, and the same as the native wheel. How the column crosses JPype changes.

Refs #324, part of #315. This targets release/26.11.1 (the base of the wheels-only 26.11.1.post1); it changes the bridge jar, so it ships with the rebuilt wheel, and the engine commit, jars and image are untouched.

What changed:

  • A column of nothing but str and None is now sent as one byte[] (the UTF-8 text of all rows back to back), one int[] of row + 1 offsets and, when the column has a null, one boolean[] of the null rows. They are wrapped in a new bridge class, StringColumn, and DocumentBatcher.insertColumns and insertColumnsParallel decode each row when they build its document. Before, _column_to_java called convert_python_to_java for every value and built an Object[] of Java Strings element by element.
  • The transfer is a copy: the blob goes through bytes into jpype.JArray(jpype.JByte) and the offsets and mask through numpy into JArray(JInt) and JArray(JBoolean), as the numeric columns already do. I did not use a memoryview of Java or Python memory, since the JPype guide does not say whether it pins or copies.
  • The type census, the lengths and the join run in C (set(map(type, ...)), np.fromiter(map(len, ...)), "".join).
  • Kept on the per-element path, exactly as before: a mixed column (an int, bytes, a date-time, a list), an empty column, a column with a lone surrogate (no UTF-8 form: the blob builder gives up and never raises, and the old path refuses the column with the UnicodeEncodeError JPype raises, before anything is written, as it does for every bulk entry point), and a column whose text exceeds an int offset (2 GiB).
  • Not touched: numeric columns, dataframe.py (it reaches the change through insert_columns), and the native wheel.

Memory, not measured: the blob holds the UTF-8 bytes plus 4 bytes per row (plus 1 per row for a mask) in Java at once, where the old path held an Object[] of references plus one Java String per distinct value (UTF-16) for the whole column, so a high-cardinality column holds less and a column of a few distinct values holds a little more. On the Python side the ASCII case builds the joined text and its encoded copy once (about the column's byte size each), and the non-ASCII case a list of the encoded rows (about 45 bytes per row plus the text) that is freed before the transfer. The Java Strings now live only for the row being built. The untimed profile study (queued) and the laptop A/B are what measure it; I make no speed or memory claim here.

Tests, which run in CI only:

  • tests/test_insert_columns_string_blob.py (JVM wheel) spies on convert_python_to_java (no call for a string column, which fails on the old code), checks which columns become a StringColumn and which keep the per-element path, compares the stored table with the one the per-element path stores for the same columns (non-BMP characters, NUL, an empty string, a null, a 5,000-character string, in the synchronous and the parallel mode), and checks that the Java side refuses a malformed column.
  • tests/test_insert_columns_strings.py (both wheels) checks that those values come back as given from a list, a numpy string array and an object array, in a mixed column, with a single row, and in the parallel mode.
  • A local pytest run was not possible here, so I compiled and ran pyflakes on the Python files, ran the blob and offsets logic against a decoder on edge cases and random columns outside the JVM, and read the Java by eye: the CI build of the bridge jar is its first compile.

🤖 Generated with Claude Code

tae898 and others added 2 commits November 9, 2026 19:04
…b plus int32 offsets (refs #324)

A string column used to cross JPype one element at a time: convert_python_to_java per value and an Object[] of Java Strings built element by element, all alive at once. A column of nothing but str and None is now one byte[] (the UTF-8 text of all rows), one int[] of row + 1 offsets and, when there is a null, one boolean[], wrapped in the new bridge class StringColumn; DocumentBatcher.insertColumns and insertColumnsParallel decode each row when they build its document, so the Java Strings are short-lived. The blob crosses through bytes into JArray(JByte), a copy; nothing relies on a JPype view of Python memory.

Kept as before: a mixed column (an int, bytes, a date-time) and an empty column convert per element; a column with a lone surrogate (no UTF-8 form) and one beyond an int offset keep the per-element path. insert_dataframe and insert_arrow reach the change through insert_columns. Stored values are the same on both wheels (tests compare with the per-element path and with the values given).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…by the per-element path as before (refs #324)

The blob builder returns None for a column with a lone surrogate; the UnicodeEncodeError of the first CI run came from JPype in the per-element path that ran before, which is the refusal every bulk entry point has. The test now checks the builder at the first, middle and last row, the refusal of _column_to_java and insert_columns with nothing written, and that a non-BMP character still takes the blob.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@tae898 tae898 self-assigned this Nov 9, 2026
@tae898
tae898 merged commit cbebd91 into release/26.11.1 Nov 10, 2026
19 checks passed
@tae898
tae898 deleted the perf/324-jvm-string-blob branch November 10, 2026 02:23
@github-actions github-actions Bot added this to the 26.12.1 milestone Nov 10, 2026
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