1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00

MDEV-41238 Bound parameters prevent condition pushdown into a grouped view, while CAST enables it

Item_param::deep_copy() returned NULL, and every rewrite that copies a
condition gives up on what it cannot copy. A conjunct holding a
prepared statement parameter was therefore left out of:
- pushdown into derived tables and views, including UNION branches
  and conditions on a derived table column whose expression holds a
  parameter;
- pushdown from HAVING to WHERE, for the part extracted from an OR;
- pushdown into a materialized IN subquery;
- split materialization, which then found no split key;
- the UPPER(col) IN (...) rewrite, which has skipped such conjuncts
  since MDEV-40006.
When the derived table groups by an expression with a parameter, the
condition was pushed into its HAVING instead of its WHERE.

CAST(? AS ...) worked around this only by accident. Before the
pushdown, constant propagation can replace a constant expression on
one side of = with a literal. A bare parameter already counts as a
basic constant, so propagation leaves it as it is. A client whose
charset differs from the column's got the pushdown by a similar
accident: the charset conversion replaces the parameter with a
literal.

Item_param::shallow_copy() now copies a parameter that holds a value,
NULL included, and deep_copy() calls it, as in other leaf items. Every
such copy is made while optimizing during EXECUTE, and the next
EXECUTE makes a new one, so a copy always carries the value bound for
the EXECUTE that made it. A copy that outlived its EXECUTE would keep
an old value and point into a string buffer that the next binding may
free, so shallow_copy() asserts that it does not allocate on the
statement's arena.

The copy is not added to m_clones. m_clones keeps the parameter's
copies in CTE definitions updated with each new value for the whole
life of the statement, while this copy lives for one execution.

An unbound parameter, DEFAULT and IGNORE still have no copy.

mysqltest gets a PS_bind_long command. It binds like PS_bind, but
sends every value with mysql_stmt_send_long_data() before each
PS_execute, so a test can reach a parameter in LONG_DATA_VALUE state.
This commit is contained in:
Nikita Malyavin
2026-10-19 22:51:21 +02:00
parent 412d8fa525
commit 2a76621e0d
7 changed files with 2028 additions and 9 deletions

View File

@@ -342,6 +342,8 @@ struct st_connection
size_t name_len;
MYSQL_STMT* stmt;
MYSQL_BIND *ps_params;
/* PS_bind_long: send ps_params as long data before each PS_execute */
my_bool ps_long_data;
/* Set after send to disallow other queries before reap */
my_bool pending;
@@ -421,6 +423,7 @@ enum enum_commands {
Q_OPTIMIZER_TRACE,
Q_PS_PREPARE,
Q_PS_BIND,
Q_PS_BIND_LONG,
Q_PS_EXECUTE,
Q_PS_CLOSE,
Q_UNKNOWN, /* Unknown command. */
@@ -546,6 +549,7 @@ const char *command_names[]=
"optimizer_trace",
"PS_prepare",
"PS_bind",
"PS_bind_long",
"PS_execute",
"PS_close",
0
@@ -11236,7 +11240,8 @@ static void handle_no_active_connection(struct st_command *command,
void run_prepare_stmt(struct st_connection *cn, struct st_command *command, const char *query,
size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings);
void run_bind_stmt(struct st_connection *cn, struct st_command *command, const char *query,
size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings);
size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings,
my_bool long_data);
void run_execute_stmt(struct st_connection *cn, struct st_command *command, const char *query,
size_t query_len, DYNAMIC_STRING *ds, DYNAMIC_STRING *ds_warnings);
void run_close_stmt(struct st_connection *cn, struct st_command *command, const char *query,
@@ -11280,7 +11285,9 @@ void run_query_normal(struct st_connection *cn, struct st_command *command,
goto end;
break;
case Q_PS_BIND:
run_bind_stmt(cn, command, query, query_len, ds, ds_warnings);
case Q_PS_BIND_LONG:
run_bind_stmt(cn, command, query, query_len, ds, ds_warnings,
command->type == Q_PS_BIND_LONG);
flags &= ~QUERY_SEND_FLAG;
goto end;
break;
@@ -12085,15 +12092,15 @@ void run_prepare_stmt(struct st_connection *cn, struct st_command *command,
query - query string to execute
query_len - length query string to execute
ds - output buffer where to store result form query
long_data - send the values as long data before each execution
RETURN VALUE
error - function will not return
*/
void run_bind_stmt(struct st_connection *cn, struct st_command *command,
const char *query, size_t query_len, DYNAMIC_STRING *ds,
DYNAMIC_STRING *ds_warnings
)
const char *query, size_t query_len, DYNAMIC_STRING *ds,
DYNAMIC_STRING *ds_warnings, my_bool long_data)
{
MYSQL_STMT *stmt= cn->stmt;
DBUG_ENTER("run_bind_stmt");
@@ -12116,6 +12123,8 @@ void run_bind_stmt(struct st_connection *cn, struct st_command *command,
stmt->param_count,
MYF(MY_WME|MY_FAE));
bzero((char *) ps_params, sizeof(MYSQL_BIND) * stmt->param_count);
/* Long data can only be a string, so every parameter is bound as one */
cn->ps_long_data= long_data;
int i=0;
char *c;
@@ -12126,7 +12135,7 @@ void run_bind_stmt(struct st_connection *cn, struct st_command *command,
while (p != nullptr)
{
(void)strtol(p, &c, 10);
if (!*c)
if (!long_data && !*c)
{
ps_params[i].buffer_type= MYSQL_TYPE_LONG;
l= (long*)my_malloc(PSI_NOT_INSTRUMENTED, sizeof(long),
@@ -12138,7 +12147,7 @@ void run_bind_stmt(struct st_connection *cn, struct st_command *command,
else
{
(void)strtod(p, &c);
if (!*c)
if (!long_data && !*c)
{
ps_params[i].buffer_type= MYSQL_TYPE_DECIMAL;
d= (double*)my_malloc(PSI_NOT_INSTRUMENTED, sizeof(double),
@@ -12215,6 +12224,16 @@ void run_execute_stmt(struct st_connection *cn, struct st_command *command,
}
#endif
if (cn->ps_params && cn->ps_long_data)
{
for (ulong i= 0; i < stmt->param_count; i++)
if (mysql_stmt_send_long_data(stmt, (uint) i,
(const char *) cn->ps_params[i].buffer,
cn->ps_params[i].buffer_length))
die("mysql_stmt_send_long_data() failed': %d %s",
mysql_stmt_errno(stmt), mysql_stmt_error(stmt));
}
/*
Execute the query
*/
@@ -13482,6 +13501,7 @@ int main(int argc, char **argv)
case Q_REAP:
case Q_PS_PREPARE:
case Q_PS_BIND:
case Q_PS_BIND_LONG:
case Q_PS_EXECUTE:
case Q_PS_CLOSE:
{

File diff suppressed because it is too large Load Diff

View File

@@ -4572,3 +4572,306 @@ select * from (select id from t1 union select id from t2) dt1,
drop table t1,t2,t3;
--echo # End of 10.11 tests
--echo #
--echo # MDEV-41238 Bound parameters prevent condition pushdown into a grouped
--echo # view, while CAST enables it
--echo #
create table t0 (x varchar(32) not null, y varchar(32) not null,
key ix_xy (x, y));
insert into t0 select concat('x', seq mod 10), concat('y', seq mod 5)
from seq_1_to_200;
create view v1 as select x, y, count(*) as total from t0 group by x, y;
create table t1 (i int not null, s varchar(32) not null,
dc decimal(10,2) not null, d date not null,
dtm datetime not null, b int not null);
insert into t1 select seq mod 10, concat('s', seq mod 10), (seq mod 10) + 0.5,
date'2020-01-01' + interval (seq mod 10) day,
timestamp'2020-01-01 00:00:00' + interval (seq mod 10) day,
seq
from seq_1_to_100;
create view vg as select i, s, dc, d, dtm, count(*) as cnt, max(b) as mb
from t1 group by i, s, dc, d, dtm;
create view vu as select i, s, b from t1 where b <= 50
union all
select i, s, b from t1 where b > 50;
create table t2 (i int not null, b int not null);
insert into t2 select seq mod 10, seq from seq_1_to_50;
--echo # Both conjuncts are pushed into the view, with each execution's values
prepare s from 'select * from v1 where x = ? and y = ?';
execute s using 'x7', 'y2';
execute s using 'x3', 'y3';
prepare s from 'explain format=json select * from v1 where x = ? and y = ?';
--echo # Check that x = 'x7' and y = 'y2' is pushed down into the materialized view v1
--source include/explain-no-costs.inc
execute s using 'x7', 'y2';
--echo # One parameter and one literal
prepare s from 'select * from v1 where x = ? and y = ''y2''';
execute s using 'x7';
execute s using 'x2';
prepare s from 'explain format=json select * from v1 where x = ? and y = ''y2''';
--echo # Check that x = 'x7' and y = 'y2' is pushed down into the materialized view v1
--source include/explain-no-costs.inc
execute s using 'x7';
--echo # The same with the value sent with mysql_stmt_send_long_data()
# The PS_ commands are sent as SQL when these protocols are on
--disable_cursor_protocol
--disable_ps_protocol
--PS_prepare select * from v1 where x = ? and y = 'y2'
--PS_bind_long x7
--PS_execute
--PS_bind_long x2
--PS_execute
--PS_close
--PS_prepare explain format=json select * from v1 where x = ? and y = 'y2'
--PS_bind_long x7
--echo # Check that x = 'x7' and y = 'y2' is pushed down into the materialized view v1
--source include/explain-no-costs.inc
--PS_execute
--PS_close
--enable_ps_protocol
--enable_cursor_protocol
--echo # A parameter inside an OR, a comparison and an IN list
prepare s from 'select i, cnt from vg where (i = ? or i = 8) and s <> ''zz''';
execute s using 7;
execute s using 1;
prepare s from
'explain format=json select i, cnt from vg where (i = ? or i = 8) and s <> ''zz''';
--echo # Check that (i = 7 or i = 8) and s <> 'zz' is pushed down into vg
--source include/explain-no-costs.inc
execute s using 7;
prepare s from 'select i, cnt from vg where i > ?';
execute s using 7;
execute s using 8;
prepare s from 'explain format=json select i, cnt from vg where i > ?';
--echo # Check that i > 7 is pushed down into vg
--source include/explain-no-costs.inc
execute s using 7;
prepare s from 'select i, cnt from vg where s in (?, ?)';
execute s using 's7', 's8';
execute s using 's1', 's2';
prepare s from 'explain format=json select i, cnt from vg where s in (?, ?)';
--echo # Check that s in ('s7','s8') is pushed down into vg
--source include/explain-no-costs.inc
execute s using 's7', 's8';
--echo # Equalities on columns of each type, and NULL
prepare s from 'select i, cnt from vg where i = ?';
execute s using 7;
execute s using NULL;
prepare s from 'explain format=json select i, cnt from vg where i = ?';
--echo # Check that i = 7 is pushed down into vg
--source include/explain-no-costs.inc
execute s using 7;
--echo # Check that i = NULL is pushed down into vg
--source include/explain-no-costs.inc
execute s using NULL;
prepare s from 'select i, cnt from vg where dc = ?';
execute s using 7.5;
execute s using 2.50;
prepare s from 'explain format=json select i, cnt from vg where dc = ?';
--echo # Check that dc = 7.5 is pushed down into vg
--source include/explain-no-costs.inc
execute s using 7.5;
prepare s from 'select i, cnt from vg where d = ?';
execute s using date'2020-01-08';
execute s using date'2020-01-03';
prepare s from 'explain format=json select i, cnt from vg where d = ?';
--echo # Check that d = DATE'2020-01-08' is pushed down into vg
--source include/explain-no-costs.inc
execute s using date'2020-01-08';
prepare s from 'select i, cnt from vg where dtm = ?';
execute s using timestamp'2020-01-08 00:00:00';
execute s using date'2020-01-03';
prepare s from 'explain format=json select i, cnt from vg where dtm = ?';
--echo # Check that dtm = TIMESTAMP'2020-01-08 00:00:00' is pushed down into vg
--source include/explain-no-costs.inc
execute s using timestamp'2020-01-08 00:00:00';
--echo # A UNION ALL view, and a parameter in one branch's select list
prepare s from 'select * from vu where i = ? and b > ?';
execute s using 7, 40;
execute s using 3, 80;
prepare s from 'explain format=json select * from vu where i = ? and b > ?';
--echo # Check that i = 7 and b > 40 is pushed down into both branches of vu
--source include/explain-no-costs.inc
execute s using 7, 40;
prepare s from
'select * from (select i, b as bb from t1 where b <= 50
union all
select i, b + ? from t1 where b > 50) dt
where dt.bb > 95';
execute s using 0;
execute s using 10;
prepare s from
'explain format=json
select * from (select i, b as bb from t1 where b <= 50
union all
select i, b + ? from t1 where b > 50) dt
where dt.bb > 95';
--echo # Check that bb > 95 is pushed down into both branches, as b + 0 > 95 into the second
--source include/explain-no-costs.inc
execute s using 0;
--echo # A derived table that is not grouped, and a parameter in its select list
set optimizer_switch='derived_merge=off';
prepare s from 'select * from (select i, s, b from t1) dt where dt.i = ? and dt.b > ?';
execute s using 7, 90;
execute s using 3, 50;
prepare s from
'explain format=json
select * from (select i, s, b from t1) dt where dt.i = ? and dt.b > ?';
--echo # Check that i = 7 and b > 90 is pushed down into dt
--source include/explain-no-costs.inc
execute s using 7, 90;
prepare s from 'select * from (select i, b + ? as bb from t1) dt where dt.bb > 99';
execute s using 0;
execute s using 5;
prepare s from
'explain format=json
select * from (select i, b + ? as bb from t1) dt where dt.bb > 99';
--echo # Check that bb > 99 is pushed down into dt as b + 0 > 99
--source include/explain-no-costs.inc
execute s using 0;
set optimizer_switch=default;
--echo # A parameter in the GROUP BY expression: pushed into WHERE, not HAVING
prepare s from
'select * from (select i + ? as ii, count(*) as c from t1 group by ii) dt
where dt.ii = 5';
execute s using 0;
execute s using 2;
prepare s from
'explain format=json
select * from (select i + ? as ii, count(*) as c from t1 group by ii) dt
where dt.ii = 5';
--echo # Check that ii = 5 is pushed down into the WHERE of dt as i + 0 = 5
--source include/explain-no-costs.inc
execute s using 0;
--echo # HAVING to WHERE: the part of an OR that is extracted
prepare s from
'select i, max(b) from t1 group by i
having (i = ? and max(b) > 0) or (i = 8 and max(b) > 0)';
execute s using 7;
execute s using 1;
prepare s from
'explain format=json select i, max(b) from t1 group by i
having (i = ? and max(b) > 0) or (i = 8 and max(b) > 0)';
--echo # Check that i = 7 or i = 8 is moved from HAVING to WHERE
--source include/explain-no-costs.inc
execute s using 7;
prepare s from
'select i, max(b) from t1 group by i
having count(*) > 0 and ((i > ? and max(b) > 0) or (i < 3 and max(b) > 0))';
execute s using 7;
execute s using 8;
prepare s from
'explain format=json select i, max(b) from t1 group by i
having count(*) > 0 and ((i > ? and max(b) > 0) or (i < 3 and max(b) > 0))';
--echo # Check that i > 7 or i < 3 is moved from HAVING to WHERE
--source include/explain-no-costs.inc
execute s using 7;
--echo # Pushdown into a materialized IN subquery
prepare s from
'select * from t1
where (t1.i, t1.b) in (select t2.i, max(t2.b) from t2 group by t2.i)
and t1.i = ? and t1.b < ?';
execute s using 7, 50;
execute s using 3, 50;
prepare s from
'explain format=json select * from t1
where (t1.i, t1.b) in (select t2.i, max(t2.b) from t2 group by t2.i)
and t1.i = ? and t1.b < ?';
--echo # Check that i = 7 is pushed down into the materialized subquery as t2.i = 7
--source include/explain-no-costs.inc
execute s using 7, 50;
prepare s from
'select * from t1
where t1.i in (select t2.i + cast(? as signed) as x from t2 having x >= 0)
and t1.i > 7';
execute s using 0;
execute s using 1;
prepare s from
'explain format=json select * from t1
where t1.i in (select t2.i + cast(? as signed) as x from t2 having x >= 0)
and t1.i > 7';
--echo # Check that i > 7 is pushed down into the materialized subquery as
--echo # t2.i + cast(0 as signed) > 7
--source include/explain-no-costs.inc
execute s using 0;
--echo # A CTE referenced twice: the second reference has clones of the
--echo # parameters, and both references get the copies
prepare s from
'with c as (select i, max(b) as mb from t1 group by i
having (i = ? and max(b) > 0) or (i = 8 and max(b) > 0))
select * from c c1 join c c2 on c1.i = c2.i order by c1.i';
execute s using 7;
execute s using 1;
prepare s from
'explain format=json
with c as (select i, max(b) as mb from t1 group by i
having (i = ? and max(b) > 0) or (i = 8 and max(b) > 0))
select * from c c1 join c c2 on c1.i = c2.i';
--echo # Check that i = 7 or i = 8 is moved to WHERE in both references to c
--source include/explain-no-costs.inc
execute s using 7;
prepare s from
'with c as (select i + ? as ii, count(*) as cnt from t1 group by ii)
select * from c c1 join c c2 on c1.ii = c2.ii where c1.ii = 5';
execute s using 0;
execute s using 2;
prepare s from
'explain format=json
with c as (select i + ? as ii, count(*) as cnt from t1 group by ii)
select * from c c1 join c c2 on c1.ii = c2.ii where c1.ii = 5';
--echo # Check that ii = 5 is pushed down into both references to c as i + 0 = 5
--source include/explain-no-costs.inc
execute s using 0;
drop view v1, vg, vu;
drop table t0, t1, t2;
--echo # Split materialization on an equality with a parameter
create table t3 (a int not null, b int not null, key ix_a (a));
insert into t3 select seq mod 2000, seq from seq_1_to_20000;
create table t4 (k int not null);
insert into t4 values (10), (20), (30), (40), (50);
analyze table t3, t4 persistent for all;
prepare s from
'select * from t4, (select a, count(*) as c, max(b) as mb from t3 group by a) dt
where dt.a = t4.k + ?';
execute s using 0;
execute s using 1;
prepare s from
'explain format=json
select * from t4, (select a, count(*) as c, max(b) as mb from t3 group by a) dt
where dt.a = t4.k + ?';
--echo # Check that dt is split: it is read by ref on key0, and t3 by ref on ix_a
--source include/explain-no-costs.inc
execute s using 0;
drop table t3, t4;
--echo # The casefold rewrite of MDEV-40006 applies to a parameter list
set names utf8mb4;
create table t1 (id int, cs varchar(10), key(cs)) collate=utf8mb4_general_ci;
insert into t1 (id, cs) values (1, 'AA1AA'),(2, 'BB2BB'),(3, 'CC3CC');
prepare s from 'select id, cs from t1 where upper(cs) in (?, ?)';
execute s using 'AA1AA', 'BB2BB';
execute s using 'aa1aa', 'CC3CC';
prepare s from 'explain select id, cs from t1 where upper(cs) in (?, ?)';
--echo # Check that the plan reads t1 by range on cs
execute s using 'AA1AA', 'BB2BB';
deallocate prepare s;
drop table t1;
set names default;
--echo # End of 11.4 tests

View File

@@ -1044,3 +1044,34 @@ SELECT 1 /* doesn't throw error */;
#
# caught error and exited my_copy()
# End of 10.11 tests
#
# PS_bind_long binds like PS_bind, but sends every value as a string
# with mysql_stmt_send_long_data() before each PS_execute
#
PS_prepare select concat('[', ?, ']') as a, concat('[', ?, ']') as b;
# PS_bind: 010 is bound as an integer, no long data is sent
PS_bind 010 abc;
PS_execute;
a b
[10] [abc]
# long data packets: 0
PS_close;
PS_prepare select concat('[', ?, ']') as a, concat('[', ?, ']') as b;
# PS_bind_long: 010 stays a string, both values are sent as long data
# for each of the two executions
PS_bind_long 010 abc;
PS_execute;
a b
[010] [abc]
PS_execute;
a b
[010] [abc]
# long data packets: 4
# A later PS_bind sends no long data
PS_bind 7 xyz;
PS_execute;
a b
[7] [xyz]
# long data packets: 0
PS_close;
# End of 11.4 tests

View File

@@ -3084,6 +3084,47 @@ SELECT 1 /* doesn't throw error */;
--echo # End of 10.11 tests
--echo #
--echo # PS_bind_long binds like PS_bind, but sends every value as a string
--echo # with mysql_stmt_send_long_data() before each PS_execute
--echo #
# The PS_ commands are sent as SQL when these protocols are on
--disable_cursor_protocol
--disable_ps_protocol
--PS_prepare select concat('[', ?, ']') as a, concat('[', ?, ']') as b
--echo # PS_bind: 010 is bound as an integer, no long data is sent
--let $c0= query_get_value(show session status like 'Com_stmt_send_long_data', Value, 1)
--PS_bind 010 abc
--PS_execute
--let $c1= query_get_value(show session status like 'Com_stmt_send_long_data', Value, 1)
--let $d= $($c1 - $c0)
--echo # long data packets: $d
--PS_close
# Prepare again: the server cannot take long data for a parameter that the
# previous execution bound as an integer
--PS_prepare select concat('[', ?, ']') as a, concat('[', ?, ']') as b
--echo # PS_bind_long: 010 stays a string, both values are sent as long data
--echo # for each of the two executions
--PS_bind_long 010 abc
--PS_execute
--PS_execute
--let $c2= query_get_value(show session status like 'Com_stmt_send_long_data', Value, 1)
--let $d= $($c2 - $c1)
--echo # long data packets: $d
--echo # A later PS_bind sends no long data
--PS_bind 7 xyz
--PS_execute
--let $c3= query_get_value(show session status like 'Com_stmt_send_long_data', Value, 1)
--let $d= $($c3 - $c2)
--echo # long data packets: $d
--PS_close
--enable_ps_protocol
--enable_cursor_protocol
--echo # End of 11.4 tests
# Wait till we reached the initial number of concurrent sessions
--source include/wait_until_count_sessions.inc

View File

@@ -4187,6 +4187,51 @@ Item_param::Item_param(THD *thd, const LEX_CSTRING *name_arg,
}
/*
Binding sets only the statement's parameters and their m_clones, so the copy
keeps its value. Its strings point into the original's buffer without owning
it, which is safe because copies are dropped before the next EXECUTE.
*/
Item_param::Item_param(THD *thd, const Item_param &item):
Item_basic_value(item),
Settable_routine_parameter(item),
Rewritable_query_parameter(item),
Type_handler_hybrid_field_type(item),
state(item.state),
m_empty_string_is_null(item.m_empty_string_is_null),
value(item.value),
indicator(item.indicator),
m_out_param_info(item.m_out_param_info),
m_is_settable_routine_parameter(item.m_is_settable_routine_parameter),
m_clones(thd->mem_root),
m_associated_field(item.m_associated_field),
m_default_field(item.m_default_field)
{}
Item *Item_param::shallow_copy(THD *thd) const
{
switch (state) {
case NULL_VALUE:
case SHORT_DATA_VALUE:
case LONG_DATA_VALUE:
{
/* We can clone only if item has the single query lifetime */
DBUG_ASSERT(thd->mem_root != thd->stmt_arena->mem_root);
Item *copy= new (thd->mem_root) Item_param(thd, *this);
if (likely(copy))
copy->register_in(thd);
return copy;
}
case NO_VALUE:
case DEFAULT_VALUE:
case IGNORE_VALUE:
break;
}
return nullptr;
}
/* Add reference to Item_param used in a copy of CTE to its master as a clone */
bool Item_param::add_as_clone(THD *thd)

View File

@@ -4297,6 +4297,7 @@ public:
Item_param(THD *thd, const LEX_CSTRING *name_arg,
uint pos_in_query_arg, uint len_in_query_arg);
Item_param(THD *thd, const Item_param &item);
void cleanup() override
{
@@ -4572,8 +4573,9 @@ private:
Field *m_default_field;
protected:
Item *shallow_copy(THD *thd) const override { return nullptr; }
Item *deep_copy(THD *thd) const override { return nullptr; }
Item *shallow_copy(THD *thd) const override;
Item *deep_copy(THD *thd) const override
{ return shallow_copy_with_checks(thd); }
};