Home - Waterfall Grid T-Grid Console Builders Recent Builds Buildslaves Changesources - JSON API - About

Console View


Categories: connectors experimental galera main
Legend:   Passed Failed Warnings Failed Again Running Exception Offline No data

connectors experimental galera main
Lawrin Novitsky
CONCPP-164 Savepoint names in setSavepoint were not quoted and escaped

They are not quoted and escaped also in release and rollabck methods. It
could cause incorrect work and created theoretical opportunity for SQL
injection.

The testcase has been added and savepoint suite added to ctest tests.
Lawrin Novitsky
CONCCP-165 Making PS more resilient to forged metadata

PS here means server side prepared statements. With forged metadata
driver could read past the allocated buffer.
The fix make the read area is not longer than buffer_length. Atm I can't
see any possibility for a testcase. We need to be able to run "evil
server" for that.
Khaled Riyad
MDEV-40551 Copy/Paste friendly output format for MariaDB Command Line Client

Copy/paste friendly output was only reachable by starting the client with
--silent --skip-column-names, which cannot be done from a running
interactive session.

Add \S, a statement terminator which prints the result of one statement in
the tab separated format without column names.

com_silent() sets output_plain, opt_silent and column_names around
com_go(), then restores them, the same way com_ego() handles vertical.
output_plain selects print_tab_data() ahead of the vertical and table
branches, so \S gives the same output whether the session was started
plainly or with --table, --vertical or --silent. --html and --xml still
win, matching \G.
Rex Johnston
PQ: changes to manage mtr output

1) undo cost discount -> revert mtr plan changes
2) worker thread env fixup -> wrong result during evalution of item
3) add --sorted-result in a few spots
4) add an include/not_parallel_execution.inc, mainly for
    main/innodb_mrr_cpk.test, so far.
5) add in missing parallel_query_index test
Monty
Trivial optimziations for group_concat

- Remove some if
- Reorder code
- More code comments
Alexey (Holyfoot) Botchkov
MDEV-40394 XML schema fails on self-referencing type.

Copy XMLSchema_user_type information when the type is used recursively.
Copies that were once created are stored in m_c_free and then reused
later to avoid memory issues.
XMLSchema_item::is_validate_done() replaced with ::end_validation().
XMLSchema_group_def::check_type() now checks for circular groups.
Lawrin Novitsky
CONCPP-167 Changing of auto_increment_increment with crashed the driver

This could happen only if rewriteBatchedStatements on - only in this
case auto_increment_increment was tracked. If that actually happened, the bug in
the session tracking processing caused unhandled exception that aborted the program.

The test has been added.
Vladislav Vaintroub
MDEV-40878 avoid repeated parsing of PROXY headers

Do not parse multiple proxy headers. The protocol spec says the header is
at the beginning of a connection, so there could only be one.

According to example in the spec, in multi-layer scenarios, where several
proxies are involved between client and backend, the intermediate proxy is supposed
to be configured to replace the existing header. Thus multiple headers
are not expected.
Marko Mäkelä
MDEV-40932 SET GLOBAL innodb_log_archive=OFF may still break recovery

log_t::set_archive(archive=false): Ensure that both the latest checkpoint
and the latest log record (which has possibly not been written out yet)
will carry the log_sys.get_sequence_bit(lsn)==1, to guarantee a successful
recovery after the switch to the innodb_log_archive=OFF format.

Tested by: Matthias Leich
Reviewed by: Thirunarayanan Balathandayuthapani
Vladislav Vaintroub
MDEV-40878 avoid repeated parsing of PROXY headers

Do not parse multiple proxy headers. The protocol spec says the header is
at the beginning of a connection, so there could only be one.

According to example in the spec, in multi-layer scenarios, where several
proxies are involved between client and backend, the intermediate proxy
is supposed to be configured to replace the existing header. Thus multiple
headers are not expected.
Oleksandr Byelkin
fix result
Arcadiy Ivanov
MDEV-41007 Warn when GROUP_CONCAT(DISTINCT) loses rows silently

Give a warning when `GROUP_CONCAT(DISTINCT x)` or
`JSON_ARRAYAGG(DISTINCT x)` returns only part of a group, or nothing at
all, because the walk of the duplicate filter failed. The result is
wrong rather than deliberately cut, and nothing was said about it.

Both build their result in `val_str()` by walking `unique_filter`, and
threw the walk's return value away. `Unique::walk()` reports its own
failures through it, from allocating the merge buffer to reading back
the chunks it merged, so a failure gave a short result, or an empty
one, in silence.

The return value cannot be used on its own. `dump_leaf_key()` also
stops the walk, for two reasons that are not failures: it cuts the
result at `group_concat_max_len`, which it already reports by setting
`result_cut`, and it stops without losing anything once the `LIMIT` is
used up. Reporting every non-zero return as a cut warns about
`GROUP_CONCAT(DISTINCT a LIMIT 5)` returning exactly the five rows
that were asked for.

`dump_leaf_key()` now records that it was the one that stopped the
walk, so `val_str()` asks for the cut value warning only when the walk
itself failed.

Not every failure is silent either. The merge buffer is allocated with
`MY_WME` and the spill file is opened with `MY_WME`, so running out of
memory or failing to read raises an error of its own. Only the guard at
the top of `merge_walk()`, which refuses a merge buffer too small to
hold one key per chunk, returns without saying anything. Warn only when
no error was raised: where one was, the user has been told and the
statement is failing, so describing the length of a result nobody will
see adds nothing.

The debug keyword `unique_walk_merge_fail` fails the merging walk
quietly and `unique_walk_merge_error` fails it with an error raised.
`main.gconcat_distinct_walk_fail` uses both. The `LIMIT` case needs no
debug build and is checked in `main.gconcat_distinct_spill`.
Sergei Golubchik
windows
bsrikanth-mariadb
MDEV-39368: Code cleanup and make it more maintainable

1. Remove hard codings and instead use MACROS
2. Introduce pre and post query hooks
3. Remove duplicate code and instead use functions
Lawrin Novitsky
CONCPP-161, CONCPP-162 Fix and test of the wrong DB metadata methods parameters escaping

There were 2 separate issues - some hepler functions had hardcoded parameter for
"BACKSLASH_NO_ESCAPE", and 2nd - other helper functions that actually
escaped quots, due to the bug could process own output and in this way
multiply quots/backslashes.
Monty
fixup! e5d01cc1ae5ed3e97fdb0e329a9fd967c4c72856
ParadoxV5
MDEV-40996 Support `--sync_with_master 0, $variable` in mysqltest

`--sync_with_master` uses `get_string()`,
which has `$variable` support, but it only uses the read buffer,
which is written with the unexpanded string and not the variable value.

Reviewed-by: KhaledR57 <[email protected]>
Lawrin Novitsky
CONCPP-163 Introducing allowMultiQueries option to control if multistatement queries are allowed

Use of multistatements was harcoded - it wasn't meant to be this way. Now the option
`allowMultiQueries` controls it and the default is off.

Also, useAffectedOptions is added. Again, the code was there but it was not used/documented. The
default for it is changed to be true, unlike JDBC wants it, to preserve the current default.
Setting to false will make the driver to return for upserts number of found rows rather than affected

In save way the option interactiveClient has been brought to life - the code was there, but the
option was not used. Default is false. True will make the driver to use interactive client wait timeout.

The test has been added.
Alexey (Holyfoot) Botchkov
MDEV-40394 XML schema fails on self-referencing type.

Copy XMLSchema_user_type information when the type is used recursively.
Copies that were once created are stored in m_c_free and then reused
later to avoid memory issues.
XMLSchema_item::is_validate_done() replaced with ::end_validation().
XMLSchema_group_def::check_type() now checks for circular groups.
Yuchen Pei
MDEV-40168 [wip] Add multi valued index over fulltext

TODOs on top of those in the patch diff:

- EXPLAIN output should not say fulltext
- check type match to avoid false negative / positive bugs in mysql
- transcode the value into the index charset in mvi_encode_key
Monty
fixup! e61cabc0a2ae1a58c401fc24bb9f25caab6dc9b5
Arcadiy Ivanov
MDEV-40920 Give a note when a value is cut while a group is built

A TEXT value longer than `group_concat_max_len` is cut on its way into
`blob_storage`, in `Field_blob::handle_group_concat()`. That happens
while the group is being built, not when the answer is put together,
so it need not have changed the answer at all: the result may well have
been cut in the same place anyway. It was reported as a cut value
warning, which says that the answer lost something the user asked for.

`ER_CUT_VALUES_WHILE_PROCESSING` says instead that a value was cut
while the query was processed and names `group_concat_max_len`, which
is the limit `handle_group_concat()` uses. It is a note, and one note
per aggregate is enough for a statement however many groups had a
value cut. `cleanup()` clears the mark, so a statement that is run
again gets its own note.

What the result lost is still a warning, and is untouched: the result
cut at `gconcat_max_len()`, the rows a repack cannot keep, and the
failures MDEV-41007 reports. A group can hit both, and then both are
given, the warning first. Keeping the warning also keeps a strict
`sql_mode` aborting on a cut result, which it does because
`THD::raise_condition()` promotes a warning and never promotes a note.

`ST_COLLECT` is not affected. It reports `ER_CUT_VALUE_GROUP_CONCAT`
itself, against `group_collect_max_len`.

`main.gconcat_cut_note` covers the granularity: one note for three cut
values in three groups, one note per aggregate when a statement has
two of them, a fresh note when the statement runs again, and silence
when nothing is cut. It also shows the two diagnostics together, as
`JSON_ARRAYAGG()` loses data on the same rows where `GROUP_CONCAT()`
does not: its brackets take the result past the limit.

Note that `blob_storage` only exists when the aggregate has an
`ORDER BY` or a `DISTINCT` and a blob field, so this is the only shape
in which a value is cut this way.
Sergei Petrunia
Add comments.
Oleksandr Byelkin
Merge branch 'br-13.0-merge' into bb-13.1-release
Arcadiy Ivanov
MDEV-40692 GROUP_CONCAT replays a group when an OFFSET skips every row

Nothing says how many times a statement asks for the result of a group,
and the answer must not depend on it. A `HAVING` clause on the alias is
the shortest statement that asks twice, and it returns a different value
than the same aggregate asked once:

    SELECT GROUP_CONCAT(a ORDER BY a LIMIT 2 OFFSET 4) v FROM t1;
    -> (empty)
    SELECT GROUP_CONCAT(a ORDER BY a LIMIT 2 OFFSET 4) v FROM t1
      HAVING v LIKE '%';
    -> a,b

`val_str()` walks only while `result_finalized` is false, and
`dump_leaf_key()` raises that flag for the first row it writes. A row
that falls inside the offset is skipped by an earlier return, which
decrements the offset counter and leaves the flag alone. The row-limit
arm immediately above it does raise the flag before its own early
return, so two adjacent early returns behave differently.

A walk in which every row was skipped therefore writes nothing and
records nothing. The next caller walks again with the offset already
spent, and the rows skipped the first time are appended to a result
buffer that was handed over once already.

Once the duplicate filter has spilled to disk the second walk is worse
than wrong. `Unique::reset()` documents the contract:

    Clear the tree and the file.
    You must call reset() if you want to reuse Unique after walk().

The first walk flushed the tree and emptied it, so the second flushes an
empty tree, appending a chunk that holds no rows. `merge_walk()` reads
nothing back from it and fails `DBUG_ASSERT(bytes_read)`. A build
without assertions goes on to take keys from that chunk.

Set `result_finalized` where the walk block ends, so that it records
every path that has consumed the filter rather than only the paths that
wrote a row.

On the release branches only the form without `ORDER BY` reaches the
duplicate filter. Since MDEV-21879 the `DISTINCT ... ORDER BY`
combination builds its result the same way, so both forms can reach the
assertion here.
Marko Mäkelä
fixup! a003970e816b75d0b8d30b95f7543a6d7f01d4d7
Monty
MDEV-40454 UBSAN: maria.aria_pack_mdev invalid-shift-exponent

Shifting with 64 is no-op in the the code (no ill effects).

Added a test to not do anything if shift with 64 would happen.
Tested with ma_test_all that test aria_pack.
Oleksandr Byelkin
fixed maturity
Arcadiy Ivanov
MDEV-21879 GROUP_CONCAT(DISTINCT ORDER BY) is wrong when Unique spills

`Item_func_group_concat::add()` decided whether a row was a duplicate
by checking whether `Unique::elements_in_tree()` had grown after
`unique_add()`:

    uint count= unique_filter->elements_in_tree();
    unique_filter->unique_add(get_record_pointer());
    if (count == unique_filter->elements_in_tree())
      row_eligible= FALSE;

`Unique` flushes its whole in-memory tree to disk when it runs out of
memory, and `elements_in_tree()` only counts what is still in memory.
After the first flush the test says nothing about the rows that were
already spilled.

**MDEV-11563** made this harmless for `GROUP_CONCAT(DISTINCT x)` by
building the result in `val_str()` from `unique_filter->walk()`, which
merges the spilled parts back in. It left the `ORDER BY` case alone.
There the result comes from the sort tree, which `add()` fills gated by
`row_eligible`, so the defect is still fully live.

Both directions of the failure are reachable, depending on how often
the filter flushes relative to the insert:

1. Duplicates reach the result. 100 rows holding 50 distinct values
  give all 100 values back.
2. Rows are lost. 30 distinct rows of 2000 bytes give one value back.

`JSON_ARRAYAGG(DISTINCT x ORDER BY y)` fails in the same way.

Fixed by not filling the sort tree from `add()` when `DISTINCT` is
used. `val_str()` now walks the merged `unique_filter` into the sort
tree and then walks the sort tree, so the rows are sorted after the
duplicate filtering is complete instead of during it.

`Unique::walk()` merges everything it flushed, so the sort tree can be
handed more rows than fit in memory. `insert_to_order_tree()` repacks
it on the same memory budget `add()` used, and a walk that runs out of
memory sets `result_cut`, so the user gets a cut value warning rather
than a silently short result.

**Behaviour change.** `ORDER BY` does not order rows that tie on the
ordering expression, and which of them comes first changes here. It
used to follow the order the rows were read in; it now follows the
order the duplicate filter keeps them in. Unlike the old order, the new
one depends on neither the memory available nor the physical row order.
`main.gconcat_distinct_spill` checks that, and `main.func_gconcat`
records one such tie.
Thirunarayanan Balathandayuthapani
MDEV-26057 Assertion `!vcol->v_indexes.empty() in trx_undo_log_v_idx

Problem:
========
-  Rollback of an INPLACE ALTER TABLE is executed while holding only a
shared metadata lock on the table, so DML can run concurrently.
rollback_inplace_alter_table() resets dict_col_t::ord_part in a
critical section of its own, after row_merge_drop_indexes() already
removed the aborted indexes from the dictionary cache and emptied
dict_v_col_t::v_indexes. During this time, DML statement can see a
virtual column with ord_part set and an empty v_indexes, which
makes assert failure in trx_undo_report_insert_virtual().

Solution:
========
row_merge_reset_ord_part(): Added a function to reset
dict_col_t::ord_part for the columns that are no longer a field of
any index remaining in the dictionary cache.
For virtual columns the decision is based on dict_v_col_t::v_indexes
being empty, and no element is ever removed from that list.

row_merge_drop_indexes(): Added a call to row_merge_reset_ord_part()
in the branch that removes the indexes from the cache, in the same
dict_sys.latch critical section. That branch is taken only when
MDL_EXCLUSIVE is held or when this is the only handle to the table,
so no concurrent DML can observe the intermediate state.
In the lazy drop branch the indexes and their v_indexes entries
stay in the cache and nothing is reset; that is done later,
when the indexes are dropped while holding MDL_EXCLUSIVE.

check_col_exists_in_indexes(): Removed the only_committed parameter,
which no longer has any caller.
Lawrin Novitsky
CONCPP-160 Binary RS metadata was re-read before mysql_stmt_store_result

That prevented the driver to have info about max field length. That made
calculation of that length by C/C useless + made it allocate larger
buffers for row and RS cache.

The test does not cover that. I can not think of one. But leaving it in
plcace - won't hurt.
Rex Johnston
PQ: scan-only prototype, for testing the scan and the transport

Not for the shipping tree. Every hunk is bracketed START PROTOTYPE / END
PROTOTYPE so it can be lifted out again in one pass.

Behind debug_dbug='+d,pwt_scan_only' the workers do nothing but read their
chunk of the driving table and ship the rows. There are no worker JOIN_TABs, no
cloned conditions or select list, and no join in a worker. This thread runs the
plan it would have run serially, and the only difference is that the driving
table's JOIN_TAB reads its rows from the transport rather than from the
handler: do_select() stops diverting, start_scan_only() displaces
read_first_record, and the executor above it is untouched.

Why bother. The gate admits a few per cent of the queries in the test suite,
because almost every refusal it makes is about evaluating cloned Items in
another thread rather than about dividing a scan. Take the evaluation away and
nearly all of them go, so the chunked scan and the transport -- the two layers
underneath -- can be run over the whole suite instead of over that sliver.

How to run it. The scan-only flag on its own does nothing; the workers still
have to be asked for:

  ./mtr --parallel=16 --force --max-test-fail=0 --ignore-parallel-diff \
        --mysqld=--parallel-worker-threads=4 \
        --mysqld=--debug-dbug=+d,pwt_scan_only \
        --suite=main

--ignore-parallel-diff is what makes the run readable: without it a third of
the failures are nothing but the _parallel suffix EXPLAIN adds to the access
type. It does not cover FORMAT=JSON, where the value is quoted, so a handful of
json tests still differ on access_type alone. Never combine it with --record.

For one test, or from inside a test, the flag can be set on a live server:

  SET @sd=@@global.debug_dbug;
  SET GLOBAL debug_dbug='+d,pwt_scan_only';
  ...
  SET GLOBAL debug_dbug=@sd;

Reading the results. main/parallel_query_* fail by construction -- they measure
that the workers ran the join, and here they do not -- as do the environmental
five this tree always fails. What is worth reading is anything with a row
difference. As it stands the whole main suite gives 1358 passes and 39
failures, of which none is a wrong answer: 10 are trace, counter or JSON
EXPLAIN content, 7 are the parallel_query tests, 5 environmental, 2 EXPLAIN
only, 2 an unordered select whose rows arrive in another order, and one an
unordered LIMIT 2 that picks a different pair. No crashes.

What it cannot tell you, which is the more important half. Everything a worker
does with an Item is gone, and that is where the defects have actually been: a
worker THD that did not carry the session's time zone, a condition left only in
a Filesort, a reader that wanted a SQL_SELECT the worker copy did not have, a
materialized subquery re-opened per worker. None of those are reachable here;
most cannot even exist in this shape. Passing in this mode says nothing about
the path that ships. Its value is as a lower layer's test, and as a bisection
tool: a query that answers wrongly in both modes is wrong in the scan or the
transport, and one that answers wrongly only in the full mode is wrong in the
worker join.

Two things the mode needs that the full path gets for free. The displaced
reader is put back by finalize_parallel_workers(), because a JOIN_TAB outlives
one execution -- a prepared statement, a correlated subquery, a routine loop
all run the same plan again -- while the manager does not. And a tab whose
condition was partly pushed into the index is given the whole condition back
for the length of the scan: the pushed half lives in the manager handler's
pushed_idx_cond, which is no longer the handler producing the rows, so it would
otherwise be applied nowhere.

This commit was prepared with Claude Code: it wrote the mode, and found three
defects in it by running the suite -- a flush skipped because the scan loop
ends on HA_ERR_END_OF_FILE, the reader left installed across a re-execution,
and the index-condition pushdown above, which is the same trap the full path
hit from the opposite direction.
ParadoxV5
MDEV-39880: Reïmplement MDEV-37146 to include MDEV-39519

MDEV-39519 added MySQL 8 compatibility to `mariadb-dump --dump-slave`
by attempting `SHOW REPLICA STATUS` first and then `SHOW SLAVE STATUS`.
This conflicted with MDEV-37146, where `mariadb-dump --dump-slave`
queries either `SELECT … FROM information_schema.SLAVE_STATUS`
or `SHOW ALL SLAVES STATUS` depending on the server version.

This commit merges MDEV-37146 and MDEV-39519:
* Use MDEV-39519’s strategy based on syntax error handling.
* Use MDEV-37146’s preference order:
  1. `SELECT … FROM information_schema.SLAVE_STATUS`
  2. `SHOW ALL SLAVES STATUS`
  3. `SHOW REPLICA STATUS` (for MySQL compatibility _only_)
* Send `STOP`/`START REPLICA SQL_THREAD FOR CHANNEL '…'`
  commands for both MariaDB 10.7+ and MySQL.
* Refactor column indices to variables set when a query succeeds.
* Partially revert MDEV-37146’s removal of `--dump-slave`’s support for
  pre-GTID & pre-multi-source, but tailored for MySQL compatibility;
  coverage for MariaDB pre-10.0 is not fully restored.

Reviewed-by: Brandon Nesterenko <[email protected]>
Sergei Golubchik
and again, debian
Lawrin Novitsky
CONCPP-165 the driver made more resilient to unexpected data in date/time fields

The functions parsing Date and Timestamp fields(in text protocol) expected data
in normal format only. Evil server could make them write to unallocated
vector members and with specially crafted strings could control offset
and whad driver writes there
Oleg Smirnov
PQ: add mtr --ignore-parallel-diff

Parallel query execution appends _parallel to the 'type' column of
tabular EXPLAIN/ANALYZE, so an existing test whose plan gets
parallelized fails on that one column alone:

  -17    DERIVED r1      ALL            NULL NULL NULL NULL 2
  +17    DERIVED r1      ALL_parallel    NULL NULL NULL NULL 2

Running the whole suite with --parallel-worker-threads > 0 then buries
the failures that are worth looking at.

Add --ignore-parallel-diff, which makes the result comparison ignore
that suffix: once the two files are known to differ, strip the
suffix from both and compare again.

The text has to be exactly one of ALL, range or index followed
by _parallel, and it has to fill the span between two field
delimiters ('\t', '\n', or end of file).
FORMAT=JSON does not match this criteria, so is not affected.
Alexander Barkov
MDEV-39518 Allow prepared statements in stored functions in assignment right hand

Allowing prepared statements in stored functions when
a stored function is used in an assignment right hand.

Both DEFAULT clause of a variable initialization and
the right side of the SET statement are supported:

  CREATE PROCEDURE p1()
  BEGIN
    -- case 1: DEFAULT clause
    DECLARE spvar1 INT DEFAULT f1_with_ps(); -- OK

    -- case 2: SP variable assignment statement
    DECLARE spvar2 INT;
    SET spvar2= f1_with_ps(); -- OK
  END;

- Only assignments to SP variables works for now:
  * SET spvar= func_with_ps(); -- OK
  * SET @uvar= func_with_ps(); -- Error

- Only bare function calls are supported for now. Using a function in
  an expression does not make it PS-safe yet:
    SET v= f1()+0;

- The parser now does not reject PS statements in stored functions.
  PS applicability in stored functions is now detected at run time.
  Note, PS statements in triggers are still prohibited by the parser.

- Functions with PS do not acquire MDL locks on tables, and no MDL is
  taken on the routines themselves either. They work like procedures in
  terms of table opening and routine locking: a concurrent DROP FUNCTION
  can complete while such a function is executing.

- Functions with PS are not replicated as a single `SELECT f1()` call.
  They are replicated per-statement, like procedures.

Helper changes:
- Changing the return result for LEX::sp_variable_declarations_init()
  from void to bool to catch errors in the caller properly.

Misc:
- This patch incorporates fixes for the following bugs found during debugging:
  MDEV-39518,MDEV-40224,MDEV-40225,MDEV-40226,MDEV-40227,MDEV-40240,
  MDEV-40285,MDEV-40288,MDEV-40315,MDEV-40318,MDEV-40890,MDEV-40900,
  MDEV-40901,MDEV-40913,MDEV-40914,MDEV-41013,MDEV-41015,MDEV-41019

Assisted-by: Claude - reviews and minor clean-ups
Lawrin Novitsky
Moved C/C to what is supposed to be 3.3.21
bsrikanth-mariadb
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.