diff --git a/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc b/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc index 9d41722b..e4d529b0 100644 --- a/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc +++ b/docs/modules/ROOT/pages/how-to/databases-and-queries.adoc @@ -210,7 +210,7 @@ Bind values as parameters instead of pasting them into the statement text. Ident * Named: `:name` in the statement and a dict as the one extra argument. A named parameter can hold a collection, as in `age IN :ages`. * `None` binds as null. -A list or tuple on its own is the positional list, so it never binds as one parameter. `db.query("sql", "SELECT FROM V WHERE @rid IN ?", [rid_a, rid_b])` has two values for one `?`: the engine would bind `rid_a`, drop `rid_b` without a word and return one row where two were meant. The bindings raise `TypeError` before the statement runs when a SQL or SQLScript call has more positional values than the statement can bind, because the engine does not count them (https://github.com/ArcadeData/arcadedb/issues/10721[ArcadeData/arcadedb#10721]). To bind a list as one parameter, bind it by name or wrap it so it is the one positional value: +A list or tuple on its own is the positional list, so it never binds as one parameter. `db.query("sql", "SELECT FROM V WHERE @rid IN ?", [rid_a, rid_b])` has two values for one `?`. The bindings raise `TypeError` before the statement runs when a SQL or SQLScript call has more positional values than the statement can bind, and since 26.11.1 the engine refuses a mismatched count too (https://github.com/ArcadeData/arcadedb/issues/10721[ArcadeData/arcadedb#10721]). Older engines bound `rid_a`, dropped `rid_b` without a word and returned one row where two were meant. To bind a list as one parameter, bind it by name or wrap it so it is the one positional value: [source,python] ---- @@ -218,7 +218,7 @@ db.query("sql", "SELECT FROM V WHERE @rid IN :ids", {"ids": [rid_a, rid_b]}) db.query("sql", "SELECT FROM V WHERE @rid IN ?", [[rid_a, rid_b]]) ---- -The check counts the `?`, `$` and `:` characters of the SQL text, so the count is an upper bound: a `?` inside a string literal counts as a placeholder, and a call that binds is never refused. Fewer values than placeholders are not checked, and the engine binds null for the missing ones. `db.query` and `db.command` run the check; the async executor does not. +The bindings' check counts the `?`, `$` and `:` characters of the SQL text, so its count is an upper bound: a `?` inside a string literal counts as a placeholder, and a call that binds is never refused. The engine counts exactly: from 26.11.1 it also refuses fewer values than placeholders, with an error that names both counts (an `ArcadeDBError` wrapping the engine's `CommandSQLParsingException`), where older engines bound null for the missing ones. Pass `None` for a placeholder you mean as null. `db.query` and `db.command` run the bindings' check; the async executor does not, so there the engine's error reaches the error callback. === Results diff --git a/src/arcadedb/_params.py b/src/arcadedb/_params.py index 90940c92..b0399ed6 100644 --- a/src/arcadedb/_params.py +++ b/src/arcadedb/_params.py @@ -1,9 +1,11 @@ """More positional values than a statement has placeholders is a wrong call, refused before it runs. ``query("sql", "... WHERE @rid IN ?", [rid_a, rid_b])`` reads the lone list as the positional-parameter list (the documented form, -one element per ``?``). The statement holds one ``?``, the engine binds the first element, ignores the second without a word, and -the query returns fewer rows than the caller meant. The engine does not count (it binds null for a missing value too; -ArcadeData/arcadedb#10721), and the bindings cannot ask it: the parsed statement's placeholder count is no public engine call. +one element per ``?``). The statement holds one ``?``. Engines before 26.11.1 bound the first element, ignored the second without a +word, and returned fewer rows than the caller meant (and bound null for a missing value). Since 26.11.1 the engine refuses a +mismatched count itself (ArcadeData/arcadedb#10721); this check still runs because it fails earlier, without an engine round trip, +with a TypeError that names the fix, and because on a build linked to an older engine nothing else stops it. The bindings cannot ask +the engine for the count: the parsed statement's placeholder count is no public engine call. So the check is the one that never refuses a call that binds: an UPPER BOUND on the placeholders, the characters ``?``, ``$`` and ``:`` anywhere in the SQL text (a ``?`` in a string literal counts, so such a statement is checked more loosely, never refused wrongly). A call with more values than that bound has a value no placeholder can take. diff --git a/tests/test_positional_count.py b/tests/test_positional_count.py index 83f5c55c..4f90fe04 100644 --- a/tests/test_positional_count.py +++ b/tests/test_positional_count.py @@ -1,10 +1,10 @@ """More positional values than the statement has placeholders is refused, not bound silently. ``query("sql", "... 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 bound the first element, dropped the second without a -word, and returned fewer rows than the caller meant. The engine does not check the count (it also lets a call with fewer values -than placeholders bind null; ArcadeData/arcadedb#10721), so the bindings refuse the form that cannot be right: more values than -the statement can hold. +form: one element per ``?``). The statement has one ``?``, so engines before 26.11.1 bound the first element, dropped the second +without a word, and returned fewer rows than the caller meant (and bound null for a missing value). Since 26.11.1 the engine +refuses a mismatched count itself (ArcadeData/arcadedb#10721); the bindings still refuse the form that cannot be right, more values +than the statement can hold, before the call, with a TypeError that names the fix. The check is an upper bound (placeholder characters counted anywhere in the text), so it never refuses a call that binds. """