Skip to content

Bindings: more positional values than placeholders is a TypeError, not a silently dropped value - #52

Merged
tae898 merged 1 commit into
mainfrom
port/positional-count
Oct 25, 2026
Merged

tae898 merged 1 commit into
mainfrom
port/positional-count

Conversation

@tae898

@tae898 tae898 commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

What

db.query("sql", "SELECT FROM V WHERE @rid IN ?", [rid_a, rid_b]) reads the lone list as the positional-parameter list (the documented form, one element per ?). The statement has one ?, so the engine binds rid_a, drops rid_b without a word, and the query returns one row where two were meant. The engine does not count positional values: it also ignores a second value for a ? and binds null for a missing one (ArcadeData/arcadedb#10721). This is the client-side guard until the engine checks it.

Port of humemai/arcadedb-embedded-python#536.

Behaviour change

A db.query() or db.command() call with more positional values than the SQL statement can bind raises TypeError before the statement runs (not wrapped as ArcadeDBError, because it is a wrong call and not a failed statement). The message names both ways out: wrap the list, [[rid_a, rid_b]], or bind it by name, IN :ids with {"ids": [rid_a, rid_b]}. The documented API is unchanged: a lone list or tuple is still the positional list, and a call with one value per placeholder binds as before. The only calls refused are those that dropped a value.

The count is an upper bound taken from the ?, $ and : characters of the text, with sql and sqlscript checked and other languages left alone. It is not an exact count: the parsed statement's placeholder count is no public engine call, and named parameters carry positional numbers in the parser, so an exact count from the text would be a SQL tokenizer. The bound never refuses a call that binds (a ? inside a string literal counts as a placeholder, so such a statement is checked more loosely). Fewer values than placeholders are not checked. The async executor is not checked either; this change touches query() and command() only. The check is a new module, src/arcadedb/_params.py, called once from each of the two methods.

Tests

  • New tests/test_positional_count.py (20 tests): list, tuple and several arguments for one ? are refused for query() and command(), a refused command() changes nothing, the error is a TypeError and not Query failed, and the forms that bind keep binding (one value per ?, a wrapped list, a named list, $n, SQLScript, a ? in a literal, openCypher).
  • Red before, green after: 9 of the 20 failed on the source from origin/main (none raised) and all 20 pass with the change.
  • One existing test changed: test_each_parameter_shape_reaches_one_java_overload passed values to SELECT 1 (no placeholder); it now uses SELECT ?, ?, since it only checks overload resolution.

Docs

This repository has one page on parameters, how-to/databases-and-queries.adoc, and it gets the new paragraph and the two forms that bind a list. #536 also edited an API reference page and a test table in the old docs site; neither exists here.

Checked locally

  • tests/test_positional_count.py and tests/test_parameter_binding.py: 48 passed (red run before the change: 9 failed, 39 passed).
  • Full suite on this branch's source over the jars of the installed JVM wheel (arcadedb_embedded 26.11.1.dev0, engine 26.11.1-SNAPSHOT): 1529 passed, 21 failed, 4 skipped (neo4j is not in my environment). 20 of the failures are the same 20 that fail with the source from origin/main (checked by running those six files on it): this wheel's bridge jar predates GraphCalls.ridSet, FloatVectors and the vertex-type insert_many, and its jars fail the jar-closure and server-packaging checks. The 21st, test_python_function_interrupt.py::test_sigint_during_a_query_that_calls_python_is_a_keyboard_interrupt, counted no interrupt during the full run (a timing test, with another job running on the machine) and passes 3 of 3 alone on this branch. None of the 21 mention positional values.
  • pre-commit on the touched files: black and isort pass, the YAML, shfmt and actionlint hooks have no files, and scripts/check_no_embedded_name.py passes.
  • Bandit (-c pyproject.toml --severity-level low --confidence-level low) on the touched Python files: no issues.
  • Not run: the Antora docs build, and any timing. The check costs one isinstance and a length compare on the one-value paths and a cached str.count on calls with two or more values; no timing claim is made.

🤖 Generated with Claude Code

query("sql", "... WHERE @Rid IN ?", [rid_a, rid_b]) reads the lone list as the positional-parameter list, one element per ?. The statement has one ?, so the engine binds rid_a, drops rid_b without a word, and returns one row where two were meant. The engine does not count positional values (ArcadeData/arcadedb#10721), so Database.query() and command() now raise TypeError before the statement runs when a SQL or SQLScript call has more positional values than the text can bind. The count is an upper bound taken from the ?, $ and : characters of the text, so a call that binds is never refused.

The message names both ways out: wrap the list, [[a, b]], or bind it by name, IN :ids. test_parameter_binding.py passed values to SELECT 1 (no placeholder) and now uses SELECT ?, ?. The how-to page documents the behaviour.

Port of humemai/arcadedb-embedded-python#536.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@tae898
tae898 merged commit 896ed0e into main Oct 25, 2026
20 checks passed
@tae898
tae898 deleted the port/positional-count branch October 25, 2026 09:51
tae898 added a commit that referenced this pull request Oct 25, 2026
…on never replays a partly committed block

Three Python-only ports from humemai/arcadedb-embedded-python native-preview (#537, #531, #527):

- NativeDatabase._exec calls refuse_surplus_positionals (already shared with the JVM build,
  PR #52): more positional values than the SQL text can hold is a TypeError, not a silently
  dropped value.
- The native RID class offers the Java RID surface (ordering, getBucketId, getPosition,
  toString, isValid, equals, compareTo) and Result.get_rid() reads a projected @Rid.
- run_in_transaction: the guard that refuses to replay a block whose BATCH boundary or commit()
  already made part of its work durable sat inside a try whose own except swallowed the raise.
  The raise moves outside it. New test tests/test_run_in_transaction_partial_commit.py runs on
  both builds.

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