Skip to content

Fix inline sequence rendering after IN (#629) - #1013

Merged
cathleeny merged 10 commits into
mainfrom
cathleeny/fix-inline-in-list-rendering
Nov 9, 2026
Merged

cathleeny merged 10 commits into
mainfrom
cathleeny/fix-inline-in-list-rendering

Conversation

@cathleeny

@cathleeny cathleeny commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Description

BUG FIX: Since 4.0.4 (#559), inline parameters (use_inline_params=True) render every Python sequence as an ARRAY(...) literal. The WHERE x IN %(ids)s pattern documented in docs/parameters.md therefore renders as x IN ARRAY(1,2,3), which Spark rejects with PARSE_SYNTAX_ERROR. That blocks users moving from 3.x.

Change: A sequence at a marker that directly follows IN or NOT IN renders as a parenthesised list, (1,2,3), again. Everywhere else it stays an ARRAY(...) literal, including IN (%(p)s) where the caller wrote the parentheses, so the inline writes to ARRAY columns added in #559 keep working. Inside the list, a tuple renders as a row and a list as an ARRAY(...) literal, so a multi-column (a, b) IN %(pairs)s with [(1, 2), (3, 4)] renders as (a, b) IN ((1,2),(3,4)), as on 3.x.

Scope:

  • Rendering happens in Cursor._prepare_inline_parameters before backend dispatch, so the fix covers the Thrift, SEA and kernel backends.
  • When no parameter is a sequence, rendering skips the extra pass and runs the same code as main.
  • Spark doesn't widen types inside a row, so each value must have its column's type: the literal 1 is an INT and doesn't match a BIGINT column. docs/parameters.md says so.
  • Unchanged:
    • An empty sequence renders as IN (), which Spark rejects with INVALID_SQL_SYNTAX.EMPTY_IN_PREDICATE, the same as on 3.x.
    • A marker separated from IN by a comment keeps the ARRAY rendering.
  • An earlier community PR (Fix inline list parameter rendering for IN clauses #773) rendered every top-level sequence as (...). That would break the inline ARRAY inserts covered by test_primitive_single[INLINE-ARRAYS] and test_inline_recursive_complex_type, so this change only targets the IN position.

How is this tested?

  • Unit:

    • The full unit suite passes (-m "not realkernel"), and black --check src and mypy src are clean.
  • E2E:

    Case main This PR
    id IN %(ids)s with [1, 3] PARSE_SYNTAX_ERROR rows 1, 3
    id NOT IN %(ids)s with (1, 3) PARSE_SYNTAX_ERROR rows 0, 2, 4
    single element [2] PARSE_SYNTAX_ERROR row 2
    strings containing ' and \ PARSE_SYNTAX_ERROR both match
    same name after IN and in array_contains PARSE_SYNTAX_ERROR rows 1, 3
    IN %(ids)s ... LIMIT %(n)d PARSE_SYNTAX_ERROR rows 1, 3
    PIVOT (... FOR k IN %(ks)s) PARSE_SYNTAX_ERROR a=1, b=2
    position('b' IN %(s)s) with ["abc"] DATATYPE_MISMATCH 2
    nested arrays [[1, 2], [3]] PARSE_SYNTAX_ERROR rows [1,2], [3]
    a date and NULL PARSE_SYNTAX_ERROR row 2024-01-02
    Decimal("1.50") PARSE_SYNTAX_ERROR row 1.50
    empty list PARSE_SYNTAX_ERROR EMPTY_IN_PREDICATE, as on 3.x
    (id, tag) IN %(pairs)s with [(1, "a"), (2, "x"), (3, "c")] PARSE_SYNTAX_ERROR rows 1, 3
    (a, b) NOT IN %s with ((1, 2), (3, 4)) PARSE_SYNTAX_ERROR row (5, 6)
    Row objects from fetchall() PARSE_SYNTAX_ERROR row (3, 4)
    rows of INT literals against BIGINT columns PARSE_SYNTAX_ERROR DATATYPE_MISMATCH (Spark doesn't widen row types)

    The unchanged renderings, array_contains(%(p)s, 2) and arr IN (%(p)s), still return the expected results.

Related Tickets & Documents

Fixes #629

Since 4.0.4, inline parameters render every Python sequence as an ARRAY
literal, so the documented `x IN %(ids)s` pattern produces
`x IN ARRAY(1,2,3)`, which Spark rejects with PARSE_SYNTAX_ERROR.

A sequence whose marker directly follows IN (or NOT IN) now renders as a
parenthesised list again. Everywhere else, including `IN (%(p)s)` where
the caller wrote the parentheses, it stays an ARRAY literal, so inline
writes to ARRAY columns keep working. Because `x IN ARRAY(...)` never
parses, no query that runs today renders differently.

Co-authored-by: Isaac <[email protected]>
Signed-off-by: cathleen.yan <[email protected]>
Follow-ups from review of the inline IN fix:

- Decide per marker instead of per parameter name. A name used both
  directly after IN and elsewhere now keeps its ARRAY rendering at the
  other markers. Before, every occurrence became `(...)`, which could
  break a query that runs on main, e.g. one with a commented-out filter:
  `arr = %(tags)s -- AND tag IN %(tags)s`.
- Probe only sequence values and give every other marker its real
  escaped value, so a `%d` or `%(n)d` marker elsewhere in the query no
  longer turns the fix off. Probe tokens carry a random nonce so that a
  string value cannot pass for one.
- Leave escape_args exactly as it is on main and move the logic into
  ParamEscaper.render_inline, which Cursor._prepare_inline_parameters
  now calls.

Co-authored-by: Isaac <[email protected]>
Signed-off-by: Cathleen Yan <[email protected]>
@cathleeny
cathleeny marked this pull request as ready for review November 6, 2026 01:27

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No issues identified by the review bot.

Comment thread src/databricks/sql/utils.py Resolved
Comment thread src/databricks/sql/utils.py Resolved
Comment thread src/databricks/sql/utils.py Resolved
Follow-ups from review:

- Inside an IN list a tuple now renders as a row, so a multi-column
  `(a, b) IN %(pairs)s` with `[(1, 2), (3, 4)]` renders as
  `(a, b) IN ((1,2),(3,4))`, as it did in 3.x. Lists still render as
  ARRAY literals so ARRAY columns can be matched. Spark does not widen
  the field types of a row, so each value must have its column's type;
  docs/parameters.md says so.
- When no parameter is a sequence, render_inline skips the probe pass
  and renders exactly as escape_args and inject_parameters do on main.

Co-authored-by: Isaac <[email protected]>
Signed-off-by: Cathleen Yan <[email protected]>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good overall, with one low-severity edge case in IN detection. The probe-based per-marker approach is correct for named, positional, %% and %d markers, and it keeps the code path main uses when no parameter is a sequence. Nit (not posted inline): the new render_inline / _sequence_markers_following_in signatures leave parameters/escaped untyped, unlike the typed helpers next to them.

Comment thread src/databricks/sql/utils.py Resolved
Review bot follow-up: a `--` comment line that ends with the word IN
made the marker on the next line render as a list. For example

    WHERE arr =  -- tags we filter IN
      %(tags)s

rendered as `arr = ('a','b')` instead of `arr = ARRAY('a','b')`, which
breaks a query that runs on main. A match is now ignored when a `--`
comes before its IN on the same line and the marker is on a later line.

The probe pass also replaces escaped strings with '' (numbers stay, so
%d still formats), so a value containing `--` cannot hide a real IN.

Co-authored-by: Isaac <[email protected]>
Signed-off-by: Cathleen Yan <[email protected]>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Nit

Looks good. I found no correctness issues. The probe-based detection uses the same %-formatting as the real substitution, so marker numbering lines up for named, positional and %% cases. Errors still surface the same way as before, and queries without a sequence parameter render exactly as they do on main. The only note is a nit: a CHANGELOG entry is missing.

Other findings

  • ⚪ Nit — Consider adding an entry under # Unreleased in CHANGELOG.md. This is a user-visible fix for a 4.0.4 regression (#629) that blocks users migrating from 3.x.

Up to 4.0.3 the inline escaper rendered any iterable as `(...)`, so a
set after IN worked. 4.0.4 switched to Sequence for ARRAY rendering and
sets started raising ProgrammingError.

A set, frozenset or dict key view is accepted again when every marker
it fills directly follows IN. Its items are sorted so the SQL text is
stable, and a set of tuples renders as rows. Anywhere else a set raises
the same ProgrammingError as before, since a set has no order to keep
in an ARRAY.

Also adds a CHANGELOG entry for the inline IN fix.

Co-authored-by: Isaac <[email protected]>
Signed-off-by: Cathleen Yan <[email protected]>
Brings in #1015, which scopes the GetColumns e2e test to the current catalog and bumps oauthlib to 4.0.1.

Co-authored-by: Isaac <[email protected]>
Signed-off-by: Cathleen Yan <[email protected]>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

The change looks correct and is well tested, with 1 low concern. The probe-based detection of markers after IN keeps %% and positional numbering consistent and keeps value text from affecting detection. It falls back to the main behaviour whenever no parameter is a sequence or set, and its limits as a text heuristic (string literals, block comments) are documented in the code and in docs/parameters.md.

Comment thread src/databricks/sql/utils.py Resolved
Review bot follow-up: bytes and bytearray are sequences of ints, so
`x IN %(v)s` with b"ab" rendered as `x IN (97,98)`. That is valid SQL
and silently compares x with the byte values, where main renders
`x IN ARRAY(97,98)` and Spark rejects it. bytes and bytearray now stay
out of the IN-list rendering, so they keep main's behaviour.

Co-authored-by: Isaac <[email protected]>
Signed-off-by: Cathleen Yan <[email protected]>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 2 Low

Looks good: two low findings. The per-marker probe design is careful: the probe pass and the real pass use the same %-formatting, string values can't affect IN detection, and errors still match escape_args. One docstring line breaks the 100-char limit, and one test can't fail even if the nonce check it targets is removed.

Comment thread src/databricks/sql/utils.py Resolved
Comment thread tests/unit/test_param_escaper.py Resolved
Review bot follow-ups:

- Rewrap the _markers_following_in docstring, where an edit had joined
  two lines into one of about 140 characters.
- test_probe_shaped_string_value_is_not_a_probe could not fail without
  the nonce check, because string values are blanked during the probe
  pass. The test now also puts probe-shaped text in the operation
  itself, which reaches the probe pass unchanged, so only the nonce
  check keeps it from counting.

Co-authored-by: Isaac <[email protected]>
Signed-off-by: Cathleen Yan <[email protected]>

@peco-review-bot peco-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: 1 Low

Looks good: one low test-coverage gap. The probe-based per-marker detection keeps %%, positional/named numbering and the error parity with escape_args intact, and sets and bytes are handled carefully, with thorough tests.

Comment thread src/databricks/sql/utils.py Resolved
@cathleeny
cathleeny added this pull request to the merge queue Nov 9, 2026
Merged via the queue into main with commit 559f1d3 Nov 9, 2026
52 checks passed

This branch was previously deployed

1 inactive deployment
azure-prod — be3094a1 Deployed Nov 9, 2026 by peco-review-bot[bot] via followup #1439
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.

Compiled sql for an input list parameter results in incorrect syntax in 4.0.4

3 participants