1
0
mirror of https://github.com/MariaDB/server.git synced 2026-10-23 18:49:29 +03:00
Files
mariadb/sql/select_handler.cc
bsrikanth-mariadb aa78c6a89c MDEV-39226: Push whole multi-table update/delete down into engines
Give storage engines a way to take over an entire multi-table
UPDATE/DELETE, the way they can already take over a SELECT. Without it the
join, the row matching and every modification run in the SQL layer even
when an engine could do the whole statement itself in one step; a
single-table UPDATE/DELETE already avoids this via
direct_update_rows()/direct_delete_rows(), but a multi-table statement has
no primary handler object to drive that path.

This adds a generic, engine-agnostic pushdown interface: the SQL layer
offers the statement to the engine, and if the engine accepts it, it
performs the whole thing and reports only the row counts.

- Split select_handler into a pushdown_handler base with select_handler
  (result set) and a new multi_upddel_handler (runs a whole UPDATE/DELETE,
  reports row counts, reported as PUSHED UPDATE/PUSHED DELETE); add
  handlerton::create_multi_upddel, looked up in Sql_cmd_dml::execute_inner().
- multi_update/multi_delete gain direct_update_delete_done(), which records
  the engine's counts so send_eof() binlogs and replies without the
  SQL-layer loop; it forces statement-format binlogging so the change still
  replicates under binlog_format=ROW, and errors out instead of silently
  dropping counts for an unsupported result object.
- FederatedX implements the interface as the reference engine used to test
  correctness: it prints the statement back and runs it remotely, passes
  the engine's error code/SQLSTATE through, reads the matched count from the
  remote info string, executes IGNORE locally, and only pushes down when all
  tables share one remote server (same as SELECT/derived/unit pushdown).

Test: federated.federatedx_pushdown_upd_del.
2026-09-16 16:23:12 +02:00

431 lines
13 KiB
C++

/*
Copyright (c) 2018, 2020, MariaDB
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA */
#include "mariadb.h"
#include "sql_priv.h"
#include "sql_select.h"
#include "sql_cte.h"
#include "sql_explain.h"
#include "select_handler.h"
/**
The methods of the pushdown_handler class and of the two classes derived
from it, select_handler and multi_upddel_handler.
The objects of these classes are used for pushdown of whole statements
into engines.
For a select query, the main method is select_handler::execute() that
initiates execution of the query by a foreign engine, receives the rows of
the result set, put it in a buffer of a temporary table and send them from
the buffer directly into output. The method uses the functions of the
select_handler interface to do this. It also employes plus some helper
functions to create the needed temporary table and to send rows from the
temporary table into output.
For a multi-table UPDATE/DELETE, multi_upddel_handler::execute() hands the
whole statement over to the engine with a single call and reports the row
counts the engine returns.
*/
/*
Walk the tables of a statement starting at first_tbl and return the first
non-NULL pushdown handler that create() builds for the handlerton of one of
them, or NULL if no engine offers to take the statement over. create() is
expected to return NULL for a handlerton that does not provide the requested
interface.
*/
template <typename Create>
static auto scan_tables_for_pushdown(TABLE_LIST *first_tbl, Create create)
-> decltype(create((handlerton *) nullptr))
{
for (TABLE_LIST *tbl= first_tbl; tbl; tbl= tbl->next_global)
{
if (!tbl->table)
continue;
if (auto handler= create(tbl->table->file->partition_ht()))
return handler;
}
return nullptr;
}
/**
@brief
Look for provision of the select_handler interface by a foreign engine.
Must not be called directly, use find_single_select_handler() or
find_partial_select_handler() instead.
@param
thd The thread handler
select_lex SELECT_LEX object, must be passed in the cases of:
- single select pushdown
- partial pushdown (part of a UNION/EXCEPT/INTERSECT)
Must be NULL in case of entire unit pushdown
select_lex_unit SELECT_LEX_UNIT object, must be passed in the cases of:
- entire unit pushdown
- partial pushdown (part of a UNION/EXCEPT/INTERSECT)
Must be NULL in case of single select pushdown
@details
The function checks that this is an upper level select and if so looks
through its tables searching for one whose handlerton owns a
create_select call-back function. If the call of this function returns
a select_handler interface object then the server will push the select
query into this engine.
This function does not check if the select has tables from
different engines. Such a check must be done inside each engine's
create_select function.
Also the engine's create_select function must perform other checks
to make sure the engine can execute the query.
@retval the found select_handler if the search is successful
0 otherwise
*/
static select_handler *find_select_handler_inner(THD *thd,
SELECT_LEX *select_lex,
SELECT_LEX_UNIT *select_lex_unit)
{
// Pushdown is not supported for non-top-level SELECTs
if (select_lex->master_unit()->outer_select())
return 0;
TABLE_LIST *tbl= nullptr;
// For SQLCOM_INSERT_SELECT the server takes TABLE_LIST
// from thd->lex->query_tables and skips its first table
// b/c it is the target table for the INSERT..SELECT.
if (thd->lex->sql_command != SQLCOM_INSERT_SELECT)
{
tbl= select_lex->join->tables_list;
}
else if (thd->lex->query_tables &&
thd->lex->query_tables->next_global)
{
tbl= thd->lex->query_tables->next_global;
}
else
return 0;
return scan_tables_for_pushdown(tbl, [&](handlerton *ht) -> select_handler *
{
return ht->create_select ?
ht->create_select(thd, select_lex, select_lex_unit) : nullptr;
});
}
/**
Wrapper for find_select_handler_inner() for the case of single select
pushdown. See more comments at the description of
find_select_handler_inner()
*/
select_handler *find_single_select_handler(THD *thd, SELECT_LEX *select_lex)
{
return find_select_handler_inner(thd, select_lex, nullptr);
}
/**
Wrapper for find_select_handler_inner() for the case of partial select
pushdown. Partial pushdown means that a unit (i.e. multiple selects combined
with UNION/EXCEPT/INTERSECT operators) cannot be pushed down to
the storage engine as a whole but some particular selects of this unit can.
For example,
SELECT a FROM federated.t1 -- can be pushed down to Federated
UNION
SELECT b FROM local.t2 -- cannot be pushed down, executed locally
See more comments at the description of find_select_handler_inner()
*/
select_handler *
find_partial_select_handler(THD *thd, SELECT_LEX *select_lex,
SELECT_LEX_UNIT *select_lex_unit)
{
return find_select_handler_inner(thd, select_lex, select_lex_unit);
}
/**
@brief
Look for provision of the multi_upddel_handler interface by a foreign
engine, i.e. for an engine that can perform the whole multi-table
UPDATE/DELETE this statement is.
@details
Works the same way as find_select_handler_inner() does for a select, see
the comments there. An engine's create_multi_upddel function must make
sure the engine can execute the statement, in particular that all its
tables belong to this engine.
@retval the found multi_upddel_handler if the search is successful
0 otherwise
*/
multi_upddel_handler *find_multi_upddel_handler(THD *thd, LEX *lex)
{
return scan_tables_for_pushdown(lex->first_select_lex()->join->tables_list,
[&](handlerton *ht) -> multi_upddel_handler *
{
return ht->create_multi_upddel ?
ht->create_multi_upddel(thd, lex) : nullptr;
});
}
pushdown_handler::pushdown_handler(THD *thd_arg, handlerton *ht_arg,
SELECT_LEX *sel_lex,
select_result *result_arg)
: select_lex(sel_lex), thd(thd_arg), ht(ht_arg), result(result_arg),
is_analyze(thd_arg->lex->analyze_stmt)
{}
void pushdown_handler::print_error(int error, myf errflag)
{
my_error(ER_GET_ERRNO, MYF(0), error, hton_name(ht)->str);
}
bool pushdown_handler::send_eof()
{
DBUG_ENTER("pushdown_handler::send_eof");
DBUG_RETURN(result->send_eof());
}
select_handler::select_handler(THD *thd_arg, handlerton *ht_arg,
SELECT_LEX *sel_lex)
: pushdown_handler(thd_arg, ht_arg, sel_lex, sel_lex->join->result),
lex_unit(nullptr), table(nullptr)
{}
select_handler::select_handler(THD *thd_arg, handlerton *ht_arg,
SELECT_LEX_UNIT *sel_unit)
: pushdown_handler(thd_arg, ht_arg, nullptr, sel_unit->result),
lex_unit(sel_unit), table(nullptr)
{}
select_handler::select_handler(THD *thd_arg, handlerton *ht_arg,
SELECT_LEX *sel_lex, SELECT_LEX_UNIT *sel_unit)
: pushdown_handler(thd_arg, ht_arg, sel_lex, sel_lex->join->result),
lex_unit(sel_unit), table(nullptr)
{}
select_handler::~select_handler()
{
if (table)
free_tmp_table(thd, table);
}
TABLE *select_handler::create_tmp_table(THD *thd)
{
List<Item> types;
TMP_TABLE_PARAM tmp_table_param;
DBUG_ENTER("select_handler::create_tmp_table");
SELECT_LEX_UNIT *unit= nullptr;
uint unit_parts_count= 0;
if (lex_unit)
{
unit= lex_unit;
SELECT_LEX *sl= unit->first_select();
while (sl)
{
unit_parts_count++;
sl= sl->next_select();
}
}
else
{
unit= select_lex->master_unit();
unit_parts_count= 1;
}
if (unit->join_union_item_types(thd, types, unit_parts_count))
DBUG_RETURN(NULL);
tmp_table_param.init();
tmp_table_param.field_count= tmp_table_param.func_count= types.elements;
TABLE *table= ::create_tmp_table(thd, &tmp_table_param, types,
(ORDER *) 0, false, 0,
TMP_TABLE_ALL_COLUMNS, 1,
&empty_clex_str, true, false);
DBUG_RETURN(table);
}
bool select_handler::prepare()
{
DBUG_ENTER("select_handler::prepare");
/*
Some engines (e.g. XPand) initialize "table" on their own.
So we need to create a temporary table only if "table" is NULL.
*/
if (!table && !(table= create_tmp_table(thd)))
DBUG_RETURN(true);
DBUG_RETURN(table->fill_item_list(&result_columns));
}
bool select_handler::send_result_set_metadata()
{
DBUG_ENTER("select_handler::send_result_set_metadata");
#ifdef WITH_WSREP
if (WSREP(thd) && thd->wsrep_retry_query)
{
WSREP_DEBUG("skipping select metadata");
DBUG_RETURN(false);
}
#endif /* WITH_WSREP */
DBUG_RETURN(result->send_result_set_metadata(
result_columns, Protocol::SEND_NUM_ROWS | Protocol::SEND_EOF));
}
bool select_handler::send_data()
{
DBUG_ENTER("select_handler::send_data");
int res= result->send_data(result_columns);
// "-1" means "duplicate when executing UNION"
if (res && res != -1)
DBUG_RETURN(true);
DBUG_RETURN(false);
}
int select_handler::execute()
{
int err;
DBUG_ENTER("select_handler::execute");
if ((err= init_scan()))
goto error;
if (is_analyze)
{
end_scan();
DBUG_RETURN(0);
}
if (send_result_set_metadata())
DBUG_RETURN(-1);
while (!(err= next_row()))
{
if (thd->check_killed() || send_data())
{
end_scan();
DBUG_RETURN(-1);
}
}
if (err != 0 && err != HA_ERR_END_OF_FILE)
goto error;
if ((err= end_scan()))
goto error_2;
if (send_eof())
DBUG_RETURN(-1);
DBUG_RETURN(0);
error:
end_scan();
error_2:
print_error(err, MYF(0));
DBUG_RETURN(-1); // Error not sent to client
}
const char *select_handler::explain_type() const
{
return pushed_select_text;
}
select_pushdown_type select_handler::get_pushdown_type()
{
/*
In the case of single SELECT select_lex is initialized and lex_unit==NULL,
in the case of whole UNIT select_lex == NULL and lex_unit is initialized,
in the case of partial pushdown both select_lex and lex_unit
are initialized
*/
if(!lex_unit)
return select_pushdown_type::SINGLE_SELECT;
return select_lex ? select_pushdown_type::PART_OF_UNIT :
select_pushdown_type::WHOLE_UNIT;
}
multi_upddel_handler::multi_upddel_handler(THD *thd_arg, handlerton *ht_arg,
LEX *lex_arg)
: pushdown_handler(thd_arg, ht_arg, lex_arg->first_select_lex(),
lex_arg->first_select_lex()->join->result),
lex(lex_arg)
{}
/*
Execute a multi-table UPDATE/DELETE that was pushed down into the engine.
The engine does the whole job and only reports the row counts. The SQL
layer still has to do the final part of the statement: invalidate the
query cache, write the statement to the binary log and send the OK packet
to the client. All of that is done by multi_update::send_eof() /
multi_delete::send_eof(), so the counters are passed to the result object
and send_eof() is called as if the rows had been updated locally.
*/
int multi_upddel_handler::execute()
{
int err;
ha_rows found_rows= 0, affected_rows= 0;
DBUG_ENTER("multi_upddel_handler::execute");
if ((err= batch_update_delete(&found_rows, &affected_rows)))
{
if (!thd->is_error())
print_error(err, MYF(0));
DBUG_RETURN(-1);
}
result->direct_update_delete_done(found_rows, affected_rows);
if (send_eof())
DBUG_RETURN(-1);
DBUG_RETURN(0);
}
const char *multi_upddel_handler::explain_type() const
{
enum_sql_command sql_command= lex->sql_command;
return (sql_command == SQLCOM_DELETE ||
sql_command == SQLCOM_DELETE_MULTI) ? pushed_delete_text :
pushed_update_text;
}