Skip to content

Native: a created or updated record reads its row in the statement's crossing (closes #317) - #329

Merged
tae898 merged 4 commits into
release/26.11.1from
perf/317-native-one-row-crossings
Nov 10, 2026
Merged

tae898 merged 4 commits into
release/26.11.1from
perf/317-native-one-row-crossings

Conversation

@tae898

@tae898 tae898 commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #317. Part of #315 and the glue changes of the wheels-only 26.11.1.post1. Nothing changes for users: the records, their values and the error messages are those of the lazy form, and the test below compares them with it.

What changes: on the native wheel, save() of a new record and Vertex.new_edge run their statement with the lazy command form (arcadedb_n_cmd returns a cursor id) and then read the one row they always read with a second crossing (arcadedb_n_next_bin). They now call a private NativeDatabase._command_first_row, which is command through the existing form of arcadedb_n_exec whose reply already carries the first rows (the lazy bit of its flags left off), so a create is one crossing instead of two. save() of a saved record uses the same form for both of its UPDATE statements (the exact one and the RETURN AFTER one), so an update is three crossings instead of four. Public command is unchanged and keeps the lazy form for every other caller. Both forms run the same Java code to pull the rows; error timing is the same on both forms: an error the engine raises while it runs the statement (an INSERT or UPDATE, which save() sends) is raised by the call, as by command, and one met while rows are computed lazily (a SELECT run as a command) comes in the reply as rerr and is raised when the row is read, as before. Public command is unchanged.

The stale-record check is not read from the update's own result, and I kept it as it was. The engine's reply to an UPDATE comes after the write, and save() promises that it writes nothing when the stored record changed. A before-image from RETURN BEFORE would only show the conflict after the caller's transaction already holds the write, so it would change what a caller that catches the error sees. The SELECT and the is_transaction_active crossing stay.

Peak memory does not rise: a create drops a cursor and a call, the reply holds the same one row.

Tests (tests/test_native_one_row_crossings.py, native backend only): spies on the ctypes doors (cmd, call, call_bin, exec_query and the batched cursor close) count the crossings of one save() (create: one, also with a float array, which takes arcadedb_n_exec_bin), one new_edge with and without properties, and one update (three, for the exact and for the read-back form). They fail on the old code, which makes two and four. Oracles on the old behaviour: every created record (ints, text, non-ASCII, date and time, decimal, list and map, float array, nothing) is compared with the record the lazy statement INSERT INTO D CONTENT :row returns and with a lookup by RID; a unique-key violation raises the same message and class as the lazy form; a stale record is still refused with ConcurrentModificationException and writes nothing; an update outside a transaction still raises before any read. Added after review: a before_create veto on a new record (no error, no record id, nothing stored, one crossing, and the same object saves once the veto is lifted), a before_update veto on both UPDATE forms (nothing written, three crossings, and the next save is no conflict), a statement error (a division by zero in an INSERT and an UPDATE: same timing, message and class as the lazy form, raised by the call in both, as a CI run showed) and a row error (a SELECT run as a command: raised when the row is read, by both forms, with the Reading the result set failed: message), and a before_create callback that raises. tests/test_native_statement_texts.py and tests/test_native_save_baseline.py (statement texts and the number of copies) are untouched and must pass, and so must tests/test_cross_wheel_golden.py. I could not run any of it here (CI only); the runs are listed below.

No speed claim: the measurement is a later laptop A/B, and the untimed profile studies queued for tonight. I did not touch lookup_by_rid, which keeps the record id in its SQL text (#229).

🤖 Generated with Claude Code

tae898 and others added 4 commits November 9, 2026 18:54
…ing of the statement (#317)

The create path read the row it always reads with a second crossing
(arcadedb_n_next_bin after arcadedb_n_cmd). It now uses the form of
arcadedb_n_exec whose reply carries the first rows, and so do the two UPDATE forms of
save(). The stale-record check stays: an UPDATE's own reply comes after the write.

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

A before_create veto on a new record, a before_update veto on both UPDATE forms,
and an error met while the first row is computed (rerr), each compared with the lazy
form of the same statement.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… form (#317)

A CI run showed that an INSERT or UPDATE error is raised by the call in both forms, not
when the row is read. The test pins that the two forms agree, and a SELECT run as a
command pins the read-time case for both. The helper's docstring states the timing.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@tae898 tae898 self-assigned this Nov 9, 2026
@tae898
tae898 merged commit 9c1772a into release/26.11.1 Nov 10, 2026
25 of 41 checks passed
@tae898
tae898 deleted the perf/317-native-one-row-crossings 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