MDEV-35747: Wrong result from prepared TVC with parameter markers - #5072
MDEV-35747: Wrong result from prepared TVC with parameter markers#5072DaveGosselin-MariaDB wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request fixes MDEV-35747, which caused prepared statements with Table Value Constructors (TVC) in CTEs to return incorrect results. The implementation reuses Type_holder buffers across executions and refreshes type metadata on each call. Review feedback identifies a critical bug where type_holders is assigned before full initialization, potentially leading to crashes on OOM. Additionally, the review suggests checking for other 'sticky' attributes that might cause metadata corruption and points out a redundant call to set_maybe_null.
f9fe725 to
c44ddd9
Compare
|
claude review Code Review: MDEV-35747 — Wrong result from prepared TVC with parameter markersAuthor: Dave Gosselin · Files: OverviewA table value constructor used in a prepared statement (e.g. The fix correctly splits the work:
Correctness — verified
Issues / risks
Style
Test coverage
Security
VerdictApprove. Correct, well-reasoned fix that properly separates per-statement from per-execute work and eliminates a real wrong-results bug, with a nice defensive improvement around partial initialization. Only minor follow-ups worth considering: refresh |
| DBUG_RETURN(true); | ||
|
|
||
| /* | ||
| Should be nullptr only during first PREPARE at which time it's |
There was a problem hiding this comment.
first PREPARE
I guess should be "PREPARE or first EXECUTE", or are there scenarios of multiple PREPAREs where it is not nullptr during the second and subsequent PREPAREs?
There was a problem hiding this comment.
Thanks, right, I reworded the comment; I can't see how it would be otherwise.
There was a problem hiding this comment.
It still says "first PREPARE". Are there cases of multiple PREPAREs where it is not nullptr during the second and subsequent PREPAREs?
| */ | ||
| if (!holders) | ||
| { | ||
| DBUG_ASSERT(thd->stmt_arena->is_stmt_prepare_or_first_stmt_execute() || |
There was a problem hiding this comment.
I tried to narrow the "is_stmt_prepare_or_first_stmt_execute()" to "is_stmt_prepare()" and I get assertion failures in SP testcases as well as PS cases such as "SELECT * FROM t1 WHERE c IN ('10','20')" as the prepared statement. So for some reason the it could be the first EXECUTE too, not just PREPARE. Can you update the commit message to reflect that? Currently it refers to this branch as PREPARE only.
There was a problem hiding this comment.
Added another paragraph to the commit message to clarify.
There was a problem hiding this comment.
Thanks. The new paragraph says "For a prepared statement it happens at PREPARE". But there are (edge?) cases where it can happen at the first EXECUTE, such as when a TVC is constructed due to the "IN predicates into subqueries" optimisation e.g. this block in table_value_constr.test:
--echo #
--echo # MDEV-23182: Server crashes in
--echo # Item::fix_fields_if_needed / table_value_constr::prepare upon 2nd execution of PS
--echo #
SET @save_in_predicate_conversion_threshold=@@in_predicate_conversion_threshold;
SET in_predicate_conversion_threshold=2;
CREATE TABLE t1 (c VARCHAR(10)) DEFAULT CHARSET=utf8;
PREPARE stmt FROM "SELECT * FROM t1 WHERE c IN ('10','20')";
EXECUTE stmt;
--echo # Without the patch second execution of the prepared statement 'stmt'
--echo # results in crash.
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
DROP TABLE t1;
c44ddd9 to
bc07edd
Compare
|
claude review comment: Code Review: MDEV-35747 — Wrong result from prepared TVC with parameter markers (revised)Author: Dave Gosselin · Files: OverviewSame core fix as the prior revision: What changed since the previous revision — and it's better
Correctness — verified (carried over + re-checked)
Remaining minor points (mostly unchanged from prior review)
Test coverage
Security / performance
VerdictApprove. This revision keeps the correct once-vs-per-execute split and improves on the previous version: the shared |
| DBUG_RETURN(true); | ||
|
|
||
| /* | ||
| Should be nullptr only during first PREPARE at which time it's |
There was a problem hiding this comment.
It still says "first PREPARE". Are there cases of multiple PREPAREs where it is not nullptr during the second and subsequent PREPAREs?
| */ | ||
| if (!holders) | ||
| { | ||
| DBUG_ASSERT(thd->stmt_arena->is_stmt_prepare_or_first_stmt_execute() || |
There was a problem hiding this comment.
Thanks. The new paragraph says "For a prepared statement it happens at PREPARE". But there are (edge?) cases where it can happen at the first EXECUTE, such as when a TVC is constructed due to the "IN predicates into subqueries" optimisation e.g. this block in table_value_constr.test:
--echo #
--echo # MDEV-23182: Server crashes in
--echo # Item::fix_fields_if_needed / table_value_constr::prepare upon 2nd execution of PS
--echo #
SET @save_in_predicate_conversion_threshold=@@in_predicate_conversion_threshold;
SET in_predicate_conversion_threshold=2;
CREATE TABLE t1 (c VARCHAR(10)) DEFAULT CHARSET=utf8;
PREPARE stmt FROM "SELECT * FROM t1 WHERE c IN ('10','20')";
EXECUTE stmt;
--echo # Without the patch second execution of the prepared statement 'stmt'
--echo # results in crash.
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
DROP TABLE t1;
| If the counts don't match, then allocate some more | ||
| Item_type_holder instances. These will, on subsequent EXECUTEs, | ||
| be mutated in place (see the 'else' case following). | ||
| */ |
There was a problem hiding this comment.
The comment needs update too now that the condition has been simplified. Suggest something like "Populate the item_list with the Item_type_holders just constructed"
| DBUG_ASSERT(thd->stmt_arena->is_stmt_prepare_or_first_stmt_execute() || | ||
| thd->stmt_arena->is_conventional()); |
There was a problem hiding this comment.
Given this assert also appears in the "if (!holder)" branch above, I wonder if this "if" branch is entered if and only if that "if" branch is. If that is the case, perhaps it would be a good idea to make this connection clear, with a comment or a boolean set to "!holder" above
The setup of column type information in table_value_constr::prepare() was wrapped in an "if (!holders)" guard so that it runs only once per statement. However, the guard was too wide because it bound the allocation of item holders (which should happen only once) to the collection of type information (which should happen on each execution). This leaves the TVC stuck with whatever placeholder type the parameter had when the holders were first built, which may not match the type of the next substitution. A parameter marker has no type of its own until a value is bound at EXECUTE time. So both the TVC types and the corresponding Item_type_holder instance in the SELECT item list must be computed again on every EXECUTE. Type holder allocation happens on the first call to the prepare() function but that doesn't always coincide with a PREPARE. It does for a prepared statement whose table value constructor comes from the parser. For a statement of a stored procedure, and for a table value constructor that the conversion of an IN predicate into an IN subquery creates, allocation happens instead on the first execution. The corresponding assertion allows the first execution and conventional execution as well as PREPARE. This patch separates the work done once per statement from the work done on every execution as described above.
bc07edd to
db0d0d4
Compare
There was a problem hiding this comment.
Pull request overview
Fixes MDEV-35747 where prepared statements using TVCs (VALUES (...) / WITH ... AS (VALUES ...)) could return wrong results because placeholder-derived column type metadata was incorrectly kept from PREPARE time instead of being recomputed on each EXECUTE.
Changes:
- Split TVC preparation work into “once per statement” (allocate
Type_holder+ args buffers) vs “each execution” (re-collect type handlers/attributes). - Add
Item_type_holder::refresh()and use it to update the cached SELECT-list type holders in place on every execution. - Add regression tests covering type changes across EXECUTEs (including metadata output and NULL behavior).
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| sql/sql_tvc.cc | Separates one-time holder allocation from per-execute type/attribute collection; refreshes cached Item_type_holder instances on later executions. |
| sql/sql_class.h | Documents current “sticky” maybe_null accumulation behavior in Type_holder::aggregate_attributes(). |
| sql/item.h | Introduces Item_type_holder::refresh() and adjusts construction to support in-place refresh. |
| mysql-test/main/table_value_constr.test | Adds MDEV-35747 regression tests for prepared TVCs across multiple EXECUTEs and metadata checks. |
| mysql-test/main/table_value_constr.result | Expected output for the new tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (first_call) | ||
| { | ||
| DBUG_ASSERT(sl->item_list.is_empty()); | ||
| List_iterator_fast<Item> it(*first_elem); |
The setup of column type information in table_value_constr::prepare() was wrapped in an "if (!holders)" guard so that it runs only once per prepared statement. However, the guard was too wide because it bound the allocation of item holders (which should happen only once) to the collection of type information (which should happen on each execution).
This leaves the TVC stuck with whatever placeholder type the parameter had at PREPARE time which likely won't match the type of the next substitution (because a type holder has no actual type at PREPARE time). Its type only becomes known when a value is bound at EXECUTE time. So both the TVC types and the corresponding Item_type_holder instance in the SELECT item list must be computed again on every EXECUTE.
This patch does just that, and separates the work done once per prepared statement from the work done on every execution as implied above.