Fix None rendering as \N inside Array/Tuple/Map bind parameters#883
Open
polyglotAI-bot wants to merge 1 commit into
Open
Fix None rendering as \N inside Array/Tuple/Map bind parameters#883polyglotAI-bot wants to merge 1 commit into
polyglotAI-bot wants to merge 1 commit into
Conversation
format_bind_value emitted the escaped-text sentinel \N for None at every
nesting level. \N is only valid for a top-level scalar Nullable bind
parameter; inside an Array/Tuple/Map literal the server parses a SQL
expression and expects the NULL keyword, so a parameter such as
{tup:Tuple(String, Nullable(String))} bound to ("user_1", None) was
rejected with Code: 26 CANNOT_PARSE_QUOTED_STRING. Emit NULL when nested,
keep \N at top level.
Fixes: #879
polyglotAI-bot
requested review from
joe-clickhouse and
peter-leonov-ch
as code owners
July 21, 2026 13:44
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes server-side typed bind parameters ({name:Type}) so that Python None nested inside container literals is rendered as the SQL NULL keyword (instead of the top-level \N sentinel), resolving ClickHouse parse failures for Array, Tuple, and Map bind values.
Changes:
- Update
format_bind_valueto emit"NULL"for nestedNonevalues while preserving top-level scalarNone -> "\\N"behavior. - Add unit tests covering nested
Noneformatting (including thedict_parameter_format="map"path) and a contrast case for top-levelNone. - Add an integration regression test that round-trips
NoneinsideTuple(...),Array(Nullable(...)), andArray(Tuple(...))using the shared sync/async fixtures, and document the user-visible fix inCHANGELOG.md.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
clickhouse_connect/driver/binding.py |
Fix nested-None rendering for typed bind parameters by emitting NULL inside container literals while keeping top-level \N. |
tests/unit_tests/test_driver/test_params.py |
Add unit coverage for nested None formatting, including dict_parameter_format="map" behavior and top-level None preservation. |
tests/integration_tests/test_params.py |
Add sync/async integration regression coverage for nested None in Tuple and Array typed bind parameters. |
CHANGELOG.md |
Document the bug fix and its impact, referencing the reported issue. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes #879.
format_bind_value(clickhouse_connect/driver/binding.py) formats Python values for server-side{name:Type}bind parameters. ItsNonebranch unconditionally returned the escaped-text sentinel\N.\Nis the correct way to pass a NULL for a top-level scalar Nullable parameter, but inside anArray,Tuple, orMapliteral the server parses a SQL expression and expects the keywordNULL. So{tup:Tuple(String, Nullable(String))}bound to("user_1", None)produced('user_1', \N)and the server rejected it with:The fix emits
NULLwhen nested (top_level=False, the levelrecurse()already uses for container elements) and keeps\Nat top level. This mirrors how the sibling client-side pathformat_query_valuealready rendersNoneasNULL, and matches the recent nested-value fix for UUID/IP in the same function (#792).Changes
clickhouse_connect/driver/binding.py: informat_bind_value, theNonebranch now returns"\\N" if top_level else "NULL". This resolvesArray,Tuple, and nested containers on the default path, andMapvalues whendict_parameter_formatismap. The defaultjsondict path is unaffected (it serializesNoneas JSONnullviaany_to_json, never through this branch).Test
tests/integration_tests/test_params.py::test_null_in_containers(runs sync and async via the sharedparam_client/callfixtures): round-tripsNoneinsideTuple(String, Nullable(String), Int32),Array(Nullable(String)), andArray(Tuple(String, Nullable(String)))against a live server and asserts the returned values. Fails onmainwithCANNOT_PARSE_QUOTED_STRING; passes with the fix.tests/unit_tests/test_driver/test_params.py: new parametrizedtest_format_bind_valuecases for nestedNone(array, tuple with the null mid-row and a trailing field, array-of-tuple) plus an explicit top-level(None, "\\N")contrast case that pins the unchanged scalar behavior. Newtest_format_bind_value_map_nullcovers thedict_parameter_format="map"path.Expected values were confirmed against a local ClickHouse server (26.5.1.882).
Pre-PR validation gate
main, passes on branch)Nonebehavior preservedNotes
Out of scope for this fix: passing a
dictto a{name:Map(...)}parameter with the defaultdict_parameter_format="json"renders JSON (double-quoted) which the server also rejects for a Map literal. That is a separate, pre-existing issue independent of the\N-vs-NULLdefect and is not addressed here.