Repository navigation
Native columnar insert: date-time, date, decimal and bytes columns stay columnar (refs #316) - #333
Conversation
…325) A parallel call skipped the columnar fast path and went through one dict per row and one JSON text per chunk of 10,000 rows. The columnar entry arcadedb_n_insert_colp now takes "parallel": true in its header: the image builds the records from the column buffers and hands them to the async writers (db.async().createRecord with a failure token), as DocumentBatcher.insertColumnsParallel does on the JVM wheel. The Python side waits for the writers and reads the failures exactly as insert_many(parallel=True) does (_finish_parallel, shared). Rows the buffers cannot carry still take the row path. The native library is rebuilt from the unchanged engine commit. Tests (native only): a spy proves one colp crossing and no insert_parallel chunk; the stored records equal those of insert_many(parallel=True); nulls, vertex types, rejected records, an edge type, a missing type, the fall back and the off switch. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… columnar entry (refs #325) A document load with parallel=True left the Arrow columns as Python values and went to insert_many(parallel=True). It now keeps the columns as they are for the columnar entry, which hands the records to the async writers; a batch the buffers cannot carry still takes the row path. A test spies on the entry points and compares the stored records with the row path of the old code. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
CI evidence for 🤖 Generated with Claude Code |
… after black wrapped it Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…loss (review of #328) The failure token is now made by the caller (arcadedb_n_async "token") and its id travels in the colp header, so the caller holds the id whatever happens to the crossing. An interrupt in it (or between the chunks of the row path) waits for the writers and reads the tokens before it propagates; a wait that raises still reads every token (the entries of an unread token lived forever); the error of a failed handover says how many rows were handed over and that some may have been stored. insert_table(parallel=True) no longer says the earlier batches were written in the caller's transaction (the writers commit them, a rollback does not remove them). Tests: duplicate keys (stored + reported >= n, stored == distinct), the writers' batch boundary, nothing lands after the call, an interrupt in the crossing and in the row path, a failing wait, the in-transaction message. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the crossing (review of #333) A date64 past int32 days wrapped in the int32 cast (the row path raised) and a timestamp[s] past the years of java.time threw DateTimeException in the middle of a batch (BulkOps T kind, LocalDateTime.ofEpochSecond). Both now make pack() return None, so the call takes the row path and raises what it raised before, with nothing stored. Only a timestamp[s] can pass the java.time range: an int64 of milliseconds is at most 9.2e15 seconds against 3.15e16. Tests: the pack-level limits, the sync call (nothing stored, no transaction left open), the parallel call (may have been stored), and a date-time chunk with NaN stores the NaN that INSERT ... CONTENT :row stores (both end in MutableDocument.putProperty). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
0acd741 to
b1001e2
Compare
|
I applied an independent review to this PR (new head |
…'s open batch (CI of the first candidate) The first candidate run showed stored 1000 of 2950 distinct keys: a writer that meets a duplicate key rolls back its whole open batch (up to commit_every rows), so the valid rows of that batch are abandoned and reported too. stored == distinct was the wrong invariant. Assert stored + reported >= n, stored <= distinct, only sent ids, none stored twice. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ay columnar (refs #316) One column of a date-time, date, Decimal or bytes value used to make columnar.pack return None, so every column of the batch went through one dict per row and a JSON text with tagged values. BulkOps.insertColumns gets four kinds that read the same objects Json.decodeTag builds for those tagged values, so the stored value is the same: T (int64 ticks of the UTC wall clock since the epoch, dim = ticks per second so an Arrow timestamp keeps its unit, LocalDateTime), D (int32 epoch days, LocalDate), n (decimal text in the string layout, BigDecimal) and x (bytes in the string layout, byte[]). columnar.py lays out lists of datetime (also pandas.Timestamp), date, Decimal, bytes and bytearray and Arrow timestamp, date32, date64, decimal and binary arrays; a time-zone-aware value is its UTC wall clock and a naive one is taken as it is, the rule of _tag_datetime. The DataFrame and Arrow loader keeps those Arrow columns for the entry and turns them into tagged values only when a batch falls back to the row path. The native library is rebuilt from the unchanged engine commit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the crossing (review of #333) A date64 past int32 days wrapped in the int32 cast (the row path raised) and a timestamp[s] past the years of java.time threw DateTimeException in the middle of a batch (BulkOps T kind, LocalDateTime.ofEpochSecond). Both now make pack() return None, so the call takes the row path and raises what it raised before, with nothing stored. Only a timestamp[s] can pass the java.time range: an int64 of milliseconds is at most 9.2e15 seconds against 3.15e16. Tests: the pack-level limits, the sync call (nothing stored, no transaction left open), the parallel call (may have been stored), and a date-time chunk with NaN stores the NaN that INSERT ... CONTENT :row stores (both end in MutableDocument.putProperty). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ds changed or that were mis-built - review fixes: the date64 case built an int64 beyond its own range (DAY_MS * 2**40); use 2**33, still past int32 days. A non-finite Decimal (NaN, -NaN, sNaN, Infinity, -Infinity) is refused in lists, with None, and in object columns. - nan-decimal fall back: the NaN row goes to the statement as before; the finite row around it is a run of carriable rows and crosses on the columnar entry now (a finite Decimal is carried), so one colp call is expected, not none. - pack refusals: bytes alone are carried (kind x) now; refuse bytes with text and bytes with bytearray instead. - temporal lists: a BINARY property reads back as the signed values of a Java byte[] or as bytes; compare after & 0xFF. - mixed chunk: the rows around an uncarriable row cross on the columnar entry (the date-time column is carried), one colp call per run. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
b1001e2 to
c4d41e5
Compare
…th its stack ArcadeData#328 and ArcadeData#333) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Docstrings that still described the 26.11.1 routes after the combined PRs: - core.py Database.insert_arrow: a vertex type no longer loads through GraphBatch (ArcadeData#335); it loads through the columnar insert like a document type. - _native/database.py Database.insert_dataframe: batches go through the columnar insert (insert_many only when a column cannot cross as memory) for a document and a vertex type alike (ArcadeData#333, ArcadeData#335). - dataframe.py: module docstring dropped 'No Java was added' (this candidate changes BulkOps.java and DocumentBatcher.java for this path) and lists the kinds that now cross as memory (ArcadeData#333); _insert_documents_native docstring covers vertices and the temporal, decimal and bytes columns. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
For users nothing changes in the stored values: on the native wheel a batch with a date-time, date, Decimal or bytes column now stays on the columnar entry instead of turning every column of the batch into a dict per row and a JSON text with tagged values, and the same objects are stored. Part of #316 (all four kinds are in; I could prove each conversion from the code, see below). Base is
release/26.11.1, for the wheels-only26.11.1.post1.This PR is stacked on #328 (it needs the same
BulkOps.javaanddataframe.pycode, and both change the value switch), so its diff shows that PR's two commits until it merges; the change of this PR is the last commit. Merge #328 first.What changes:
BulkOps.insertColumnsgets four kinds, each read as the object thatJson.decodeTagbuilds for the tagged value the row path sends.Tis a date-time: int64 ticks of the UTC wall clock since 1970-01-01T00:00:00 withdimticks per second (1, 1000, 10^6 or 10^9, so an Arrow timestamp keeps its unit and a nanosecond value keeps its nanoseconds, as_temporal_to_taggeddid), read withfloorDivandfloorModas aLocalDateTime(the objectLocalDateTime.parseof the tag gave, so pre-1970 values are exact).Dis a date: int32 epoch days, aLocalDate.nis a decimal:str(Decimal)in the string layout read withnew BigDecimal(text), which is what thentag does with the same text, so the scale is kept (1E+3stays1E+3).xis bytes in the string layout read as thebyte[]thebtag decodes to.columnar.pylays out lists ofdatetime(alsopandas.Timestamp, microseconds as_tag_datetimekeeps them),date,Decimal,bytesandbytearray, and Arrow timestamp, date32, date64, decimal and binary arrays. The time zone rule is not changed: a time-zone-aware value is its UTC wall clock and a naive one is taken as it is, the rule of_tag_datetime, and nothing in the column decides a zone. Still refused (row path as before): numpy datetime64 arrays and elements, a column that mixes types (adatetimeand adate, bytes and text,bytesandbytearray), a non-finite Decimal, and pandasNaTandNAcolumns.insert_arrowandinsert_dataframekeep those Arrow columns for the entry and turn them into tagged values only when a batch falls back to the row path (another column, such as a list of strings, cannot cross), so a fall back stores what it stored before.This is Java glue: the native library is rebuilt from the unchanged engine commit, no engine file is touched. Memory: the batch no longer builds the per-row dicts and the JSON text, but a list of Python date-times is converted to an int64 array first (one 8-byte value per row); I did not measure the peak.
Tests:
tests/test_native_columnar_temporal.py(native only) loads the same rows and Arrow tables through the row path (the columnar path switched off, the private reference of the old behaviour) and through the columns, compares every stored value and its Python type, and spies on the crossings (the old code makes noarcadedb_n_insert_colpcall there, so these fail on it); it covers declared and undeclared properties, nulls, pre-1970 values, aware and naive date-times, pandas Timestamps, Arrow unitss,ms,us,nswith and without a zone, date32, decimal128, binary, the fall back with a list column, a parallel load, and the image's refusal of a wrong tick unit.tests/test_columnar_temporal_lists.pyruns on both wheels with literal expected values (naive, aware to UTC, pre-1970, null, date, Decimal, bytes), so the same stored values on the JVM wheel and the native wheel are a CI result; the existingtests/test_dataframe_ingest.pyprecision, zone and date tests run through the new path on the native wheel.tests/test_native_columnar_pack.pypins the layouts without a library, and three existing tests that asserted the old fall back (a date-time column, a Timestamp column, an Arrow timestamp) now assert the columnar entry. The golden testtests/test_cross_wheel_golden.pyis not touched.No speed claim is made here. The measurement is the untimed profile study of the ingest shapes and a later laptop A/B on this change. I could not run any test locally (CI only); the CI runs are linked in the first comment.
Changes after an independent review of this PR. A
date64value beyond int32 days no longer wraps in the int32 cast (it did, silently; the row path raised):columnar.packnow returns None for it, so the batch takes the row path and raises what it raised before, with nothing stored. Atimestamp[s]beyond the years ofjava.time(about 3.15e16 seconds) madeLocalDateTime.ofEpochSecondthrow in the middle of a batch;packnow checks the range in Python and returns None, so the row path raises its usual error before anything is stored (the synchronous call leaves no transaction open; a parallel call says that rows may have been stored, as it can have handed rows over before the failure). The review namedtimestamp[s]andtimestamp[ms]: onlyscan pass the range, because the largest int64 of milliseconds is 9.2e15 seconds against 3.15e16, so milliseconds, microseconds and nanoseconds need no check (a test loads the int64 limits in those units and expects them to stay columnar).One behaviour change to read: an
insert_manychunk that mixes a date-time, date, Decimal or bytes column with a float column holding NaN now goes columnar, and the NaN is stored asDouble.NaN; before, the temporal column forced the JSON path, where a NaN row goes throughINSERT INTO t CONTENT :rowon its own. I read the engine ata3a5d12457:CONTENTisUpdateContentStep, which callsdoc.fromMap, andfromMapanddoc.setboth end inMutableDocument.putPropertywith no special case for NaN, so the stored value is the sameDouble.NaNon both routes (anINSERTof a NaN into a declared DOUBLE keeps it). That is read from code;tests/test_native_columnar_temporal_review_fixes.pycompares both routes by value and type, and the CI run is the proof.Review tests (native only,
tests/test_native_columnar_temporal_review_fixes.py): the pack-level limits ofdate64andtimestamp[s](one value past the limit is enough to refuse the column, the limit itself stays columnar and exact);pa.array([0, 2**62], pa.timestamp("s"))and adate64past int32 days through the synchronous call (nothing stored, no transaction left open) and the parallel call (may have been stored); and the NaN comparison above.A chunk of
insert_manyrows in which one row holds a value the JSON text cannot carry still sends that row to the statement on its own, and the runs of rows around it stay on the bulk entry; since the columnar entry now carries date-time, date, Decimal and bytes columns, such a run crosses onarcadedb_n_insert_colpwhere it used to cross on the JSON entry (arcadedb_n_insert_rows). A Decimal column that holds a non-finite value (NaN,sNaN,Infinity) is refused as a whole, in lists, with nulls and in object arrays (new BigDecimal("NaN")would throw in the middle of a batch), so the non-finite row takes the statement path as before.🤖 Generated with Claude Code