Skip to content

fix: protocol-level prepared select ? + ? fails at prepare and panics at execute (#25423)#25452

Draft
ck89119 wants to merge 5 commits into
matrixorigin:mainfrom
ck89119:issue-25423-main
Draft

fix: protocol-level prepared select ? + ? fails at prepare and panics at execute (#25423)#25452
ck89119 wants to merge 5 commits into
matrixorigin:mainfrom
ck89119:issue-25423-main

Conversation

@ck89119

@ck89119 ck89119 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

  • BUG

Which issue(s) this PR fixes:

issue #25423

What this PR does / why we need it:

Protocol-level prepared statements such as select ? + ? (e.g. via
mysql-connector-python's cursor(prepared=True)) failed during
COM_STMT_PREPARE with:

invalid argument operator +, bad value [TEXT TEXT]

Root cause

baseBindParam bound every unknown prepared-statement parameter as T_text.
For ? + ? the + resolver therefore saw two concrete TEXT operands and
rejected the arithmetic at PREPARE time, before the real values arrive with
COM_STMT_EXECUTE. The function type system already knows how to deduce
T_any + T_any into a numeric type, and string-context binders (LIKE, etc.)
already handle T_any explicitly.

Fix (two parts)

  1. pkg/sql/plan/base_binder.go: bind an unresolved parameter as T_any
    instead of T_text, so the existing type deduction promotes ? + ? to a
    numeric type (int64).

  2. pkg/sql/colexec/evalExpression.go: T_any alone is not enough. Parameter
    values are always stored by the frontend as text bytes, and
    ResetPreparePlan keeps the ParamRef in the compiled plan instead of
    substituting typed constants. At runtime ParamExpressionExecutor
    materializes the value via NewConstBytes(paramRef.Typ, textBytes); a
    T_any type has element size 0 and panics in NewConstBytes /
    SetConstBytes at COM_STMT_EXECUTE (and cast(T_any -> int64) would
    otherwise be treated as scalar NULL). We materialize an unresolved T_any
    parameter as T_text, so the const has a real element size and the
    planner-inserted cast converts the text bytes to the wanted type. This keeps
    the pre-existing "params materialize as text" behavior while decoupling the
    planning-time type from the runtime materialization type.

Real TEXT columns keep their existing semantics: text_col + text_col still
errors with [TEXT TEXT]; this PR does not relax arithmetic for real text
values.

Verification

  • End-to-end with mysql-connector-python 9.7.0 cursor(prepared=True)
    select ? + ? with (1, 2) returns 3 on both the C-extension and pure
    Python paths.
  • Text-protocol PREPARE ... EXECUTE USING returns 3.
  • Regression spot-checks unchanged: > ? int comparison, = ? string
    equality, LIKE ?, UPDATE ... = ?, INSERT (?,?,?).
  • New unit tests: TestPrepareArithmeticParams (plan) and
    TestParamExpressionExecutorAnyType (colexec). pkg/sql/plan and
    pkg/sql/colexec pass; go vet and git diff --check clean.

Known boundary (pre-existing limitation, out of scope): because params are
stored as text and any + any deduces to int64 at PREPARE, passing a
non-integer such as 2.5 to ? + ? errors at cast to int. Before this fix
the statement could not even be prepared, so this is a strict improvement.

PR Checklist

  • Added/updated unit tests
  • Verified end-to-end against a running mo-service

…atrixorigin#25423)

Protocol-level prepared statements such as `select ? + ?` failed during
COM_STMT_PREPARE with `invalid argument operator +, bad value [TEXT TEXT]`.

`baseBindParam` unconditionally bound every unknown parameter as `T_text`, so
the `+` resolver saw two concrete TEXT operands and rejected the arithmetic
before the real values arrive with COM_STMT_EXECUTE. Bind the parameter as
`T_any` instead, letting the existing function type deduction promote both
unknown params to a numeric type (`? + ?` -> int64).

`T_any` alone is not enough: parameter values are always stored by the frontend
as text bytes, and `ResetPreparePlan` keeps the `ParamRef` in the compiled plan
rather than substituting typed constants. At runtime `ParamExpressionExecutor`
materializes the value via `NewConstBytes(paramRef.Typ, textBytes)`; a `T_any`
type has element size 0 and panics in NewConstBytes/SetConstBytes (and
`cast(T_any -> int64)` would otherwise be treated as scalar NULL). Materialize
an unresolved `T_any` parameter as `T_text` so the const has a real element
size and the planner-inserted cast converts the text to the wanted type.

Real text columns keep their existing semantics: `text_col + text_col` still
errors with `[TEXT TEXT]`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ck89119 ck89119 requested review from aunjgr and ouyuanning as code owners July 4, 2026 08:45
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

…ID cast (matrixorigin#25423)

Three CI failures when prepared-statement params are T_any instead of T_text:

1. "?"/"%" with T_any + BIGINT (/ "%"): fixedTypeCastRule2 had a single-T_any
   special case that overrode the initFixed2 table. For `10/?`, the override
   returned {BIGINT, BIGINT} instead of the correct {FLOAT64, FLOAT64} that
   the table specifies, and divOperatorSupports rejects BIGINT. Remove the
   override; let the comprehensive initFixed2 rules handle the cast.

2. "0.0 + ?" with a large parameter: initFixed1 mapped {DECIMAL64, ANY} to
   {DECIMAL64, DECIMAL64} whereas {DECIMAL64, TEXT} mapped to {FLOAT64, FLOAT64}.
   The narrower decimal type caused overflow at EXECUTE. Align the T_any
   rules with the T_text rules (to float64) so that any parameter can carry
   values beyond the decimal range.

3. "meta_scan(?)": the cast from T_any to UUID was missing from
   supportedTypeCast[T_any], though T_text had it. Add T_uuid to the
   supported targets for T_any.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…matrixorigin#25423)

Replace the T_any approach (which conflated unresolved params with SQL NULL)
with a targeted binder rewrite: when both arguments of an arithmetic operator
(+, -, *, /, %) are T_text prepared-statement parameter references (Expr_P),
promote each to float64 via an implicit cast before the function type check.

This avoids three P1 regressions that the T_any approach introduced:
- ? = ? remained text comparison ("01" != "1") rather than silently switching
  to integer comparison.
- DECIMAL + NULL kept its output schema (DECIMAL) since T_any is unchanged.
- PREPARE metadata stayed T_text/MYSQL_TYPE_VARCHAR, matching EXECUTE data.

Real TEXT column arithmetic is untouched (column refs are not Expr_P, so the
rewrite does not fire), preserving the existing TEXT + TEXT error semantics.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses SCA ineffassign lint finding — the error from casting the
second prepared-statement parameter to float64 was not immediately
checked, though it was caught by the enclosing err check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/bug Something isn't working size/M Denotes a PR that changes [100,499] lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants