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
Teemu Ollakka
Merge remote-tracking branch 'origin/MGL-337' into 4.ee-MGL-337
Rex Johnston
MDEV-39492 PQ: charge a parallel plan for every row the workers ship

The cost of a parallel plan was the workers' share of the join plus the
setup, and nothing for the result: the manager receives every row the
workers ship, by itself, and a plan whose output is a large share of
what it reads handed it as many rows as the join produced at no cost.
A cross join of 1000 x 2000 rows was costed cheaper in the workers and
ran there 15 times slower than serially, the time going into writing
two million rows into the workers' containers and draining them.

The join-order search now adds the cost of shipping to a parallel plan
when the plan is complete, which is the first point its row count is
known: at the leaf of best_extension_by_limited_search() and at the end
of cost_straight_join(), as parallel_ship_cost(). A partial plan carries
no estimate of it, which is a lower bound, so the pruning by cost above
the leaf is unchanged.

What is shipped depends on the query's shape, decided once per
optimization by parallel_estimate_shipped_groups() into
JOIN::parallel_groups. A query that does not aggregate ships every
joined row. One that aggregates over everything it reads ships a partial
per worker, which is charged nothing. A grouped one ships a partial per
group per worker, and the number of groups is estimated as the product
of the GROUP BY columns' distinct values from the engine-independent
statistics, capped by the joined rows; without statistics the estimate
is one group, the optimistic side, which leaves such a query costed as
before rather than charged for every joined row.

A row costs optimizer_parallel_row_cost, a new session variable, while
the worker's container is in memory: 0.03 us by default, measured on a
1,000,000-row scan shipping two INT columns. A container is a temporary
table that starts as a HEAP table of at most min(tmp_memory_table_size,
max_heap_table_size) bytes and is converted to Aria when that is
exceeded, after which every row goes through the page cache: the same
scan shipping a VARCHAR(30) as well took 630 ms in the workers against
65 ms serially, 0.6 us a row. So the rows past what the containers hold
in memory, the busiest worker holding rows / parallel_divisor of them
and each container one HEAP table's worth, cost DISK_TEMPTABLE_LOOKUP_COST
more. The row width is the plan's tables' used field lengths at their
declared length, which is how HEAP stores a VARCHAR, not capped at the
engine's mean row length as the join buffer's estimate is. Set to 0 the
variable switches the charge off, the disk tier included.

On the scratch server, with four workers: the two-INT-column scan of a
million rows stays divided (43 ms against 61), the VARCHAR one is read
serially (93 ms, where the divided reading took 630), and the TPC-H
plans are unchanged. The estimate is the optimizer's: a filter it
cannot see through, such as b % 10 = 0, is costed as shipping every row.

Tests. main.parallel_query_costing: a join that ships every joined row
is divided at the default charge and serial at a large one, and a
grouped query at the same charge stays divided, with the trace's
parallel_rows_shipped for each. main.parallel_query_join, _sort and
_spill switch the charge off beside the setup cost, as what they ship
and spill is what they test, not what it costs. The variable is added
to the sysvars, optimizer_costs, opt_context_replay and mysqld--help
results.

This commit was prepared with Claude Code (Fable 5.1), which measured
the per-row shipping cost in memory and after a spill and found the
spill to be the ten-fold difference, wrote the estimate and the cost,
adapted the tests, rebuilt the worktree and ran the parallel_query tests
in both protocols and the main suite.
Teemu Ollakka
Merge remote-tracking branch 'origin/4.x' into 4.ee-MDEV-38920-tests
Rex Johnston
MDEV-39492 PQ: key lookups under a divided driver are credited with
the square root of the division

TPC-DS Q68's grouped derived table, with eight workers, was costed
cheaper with store_sales divided among them and date_dim, store,
customer_address and household_demographics looked up by eq_ref under
it -- 395 against 514 for the serial plan that starts from the 72
matching dates -- and ran in 0.70 s where the serial plan runs in
0.24 s.

The join-order search divided every position under a parallel driver
by the driver's parallel_divisor, as if W workers did W times the work
of one. A scan does: each worker reads its own pages at the serial
rate, store_sales at 0.14 us a row per worker against 0.12 serially.
Key lookups do not. The 2,880,404 eq_ref probes of date_dim took
0.35 us each serially and 1.6 us each per worker with eight workers
probing the same index at once, and the plan as a whole ran 1.5 times
faster with two workers, 2.1 with four and 2.3 with eight.

position_cost_in_plan() now divides a ref, eq_ref or ref_or_null
position by the square root of the divisor. That fits the two- and
four-worker measurements and is still generous at eight; it is also
enough to make Q68's divided plan lose at eight workers, and the query
runs the serial plan in 0.32 s. A BNL or hashed join buffer scans the
inner table and divides like a scan, so it is left at the full
division.

What else it moves was checked: TPC-DS Q29 and Q66 keep their divided
plans at four and eight workers, and TPC-H Q1, Q3, Q5, Q10 and Q12 keep
theirs at four with their times unchanged (Q7 and Q11 serial as
before). The dimension lookups under a divided fact table are few next
to the rows scanned, and the discount on them does not reach the
decision.

main.parallel_query_costing: the grouped case's trace lists one complete
parallel plan fewer, the search having pruned the other sooner.

This commit was prepared with Claude Code (Fable 5.1), which measured
the forced plan's scaling by worker count and the per-probe cost from
ANALYZE, wrote the rule, and re-ran the TPC-DS and TPC-H queries, the
parallel_query tests in both protocols and the main suite.
Rex Johnston
MDEV-39492 PQ: spread the driving rows over no more workers than the pieces they fill

The rows that survive the driving table's WHERE were spread, in the
costing, over as many workers as the engine could cut the table into.
Where in the table they lie was not asked, and for the tables an
analytic query filters most often it is not spread at all: the 366
rows of date_dim for one year are four of its 671 pages, next to each
other in the primary key, and the 122 returned dates of TPC-DS Q29 are
two. The partitioner hands out a chunk per page, so those rows reach
four workers of eight, or two, and the join under them runs at the
pace of the busiest while the costing credited an eighth of it to each
worker.

parallel_scan_cost() now takes the surviving rows to lie together:
R rows of a T-row table of P pieces fill ceil(R * P / T) of them, and
the rows reach no more workers than that. For rows spread evenly
through the table this is pessimistic, and it only bites when the
survivors are fewer than N pieces' worth; for rows that do lie
together it is what the workers will find, and a plan chosen on a
division the workers cannot make costs more than a division not made.
The busiest worker's share then follows as before, R / ceil(R / W).

On TPC-DS Q66 the divisor for date_dim drops from 8 to 4, which is
what the workers achieve, and the plan is still chosen at cost 114
against the serial plan's 168. It still loses, 457 ms against 315,
because the worker holding the fourth quarter does half the work:
sales peak before the holidays, and how much of the join each driving
row carries is not something the statistics describe. Q29's divided
plan is unchanged at divisor 2 and runs as before, and the TPC-H plans
and times are as the costing commit measured them.

The cap moved a second kind of plan into the wrong pruning family. A
large fact table of which one percent survives, scanned in parallel
with a dimension looked up per surviving row, now has a divisor of 1
-- its 200 rows fill one page -- and the pruning heuristic compared it
with the serial prefixes, where the five-row dimension beats it on
rows and on cost and drops it, although its scan alone, divided four
ways, repays the setup several times over. The parallel family is
therefore for a parallel candidate with something to gain by the
division, either in the join under it (parallel_divisor > 1) or in the
scan itself (the new POSITION::parallel_scan_pays, the divided reading
costing less than the serial one); a candidate with neither is one of
the serial prefixes but for the setup, as before.

main.parallel_query_costing's 2000-row dimension becomes a 20000-row
one whose first 2000 rows match, four pages that four workers can
divide, and the sections it supports read the same.

This commit was prepared with Claude Code (Fable 5.1), which measured
the per-worker row counts of Q66 and Q29 against the divisor the
costing had assumed, implemented the cap, adapted the test, and ran
the parallel-query tests in both protocols and the main suite.
Andrei Elkin
MDEV-42004 STOP SLAVE waits for slave_net_timeout when master is unreachable

STOP SLAVE could take up to slave_net_timeout to stop the slave IO thread
when the master accepts the connection but does not answer, e.g. when its
host is frozen or its packets are dropped.
The IO thread was blocked in a reconnect
  try_to_reconnect() -> safe_reconnect() -> connect_to_master() ->
  mysql_reconnect() -> mysql_real_connect()
waiting for the greeting of the master, and nothing could wake it up in the
base of this patch, 11.4 at c5272f132d3. The stall is not specific to
semi-sync: an asynchronous slave waits the same, and a semi-sync one waits
in addition for rpl_semi_sync_slave_kill_conn_timeout at the IO thread exit,
which this commit does not change.

The wait was interruptible before MDEV-32567 removed the pthread_kill() of
the slave thread in terminate_slave_thread() (commit 013fc02a23e) in order to
rely on the THD::awake_no_mutex() -> vio_shutdown(active_vio) mechanism.
This bug exposes that it does not cover slave reconnect, as the slave's IO
thread does not bind its THD::active_vio to a new socket vio at the time of
the reconnect attempt.

This commit makes that "tentative" vio reachable for awake_no_mutex().
For that mysql_real_connect() now publishes the vio to the slave thread
right after it is created.
See the new hook slave_io_thread_attach_vio() in the TCP branch (the Unix one
is irrelevant for slave) that pairs an existing slave_io_thread_detach_vio().
So now in the patch when STOP SLAVE shuts the vio down, which wakes up
the poll().
The detach hook is called when the bound to THD::active_vio is about
to be destroyed.

connect_to_master() reports the interrupt to the error log when
log_warnings > 2 and the slave has been stopped after the failed connect:

  Slave I/O thread: reconnect to master '[email protected]:19000' was
  interrupted by the slave stop: Lost connection to server at 'reading
  initial communication packet', system error: 0 ...

Tested with rpl.rpl_stop_slave_unreachable_master.

The test simulates an unresponsive master: it is not shut down but frozen
with SIGSTOP, so the kernel still completes the TCP handshake of a new
connection, while no greeting is sent, and the IO thread gets into its
reconnect. The test waits for that to most probably happen and then
runs STOP SLAVE, which normally (not on an overloaded server) must not wait
for slave_net_timeout.
A shut-down master would not serve for that: its port is closed, so the
connect fails at once with "Connection refused", and the IO thread, which
then sleeps connect_retry in slave_sleep() (it checks io_slave_killed()),
stops promptly without this fix.
The test is not run on Windows, as it needs SIGSTOP.

With --let $debug_test= 1 the test prints how long STOP SLAVE took. Measured
this way, it returned after 3.5 s before the fix and 1.0 s after, with
slave_net_timeout=3 and rpl_semi_sync_slave_kill_conn_timeout=1; the same
test with a plain asynchronous slave took 2.5 s and 0.0 s.
For a reliable run on BuildBot the time is not checked. The fix is proven
by the error text in the note above. A reconnect that waits for the greeting
until the timeout is out fails with "waiting for initial communication
packet ... Connection timed out", while the one whose vio is shut down fails
with "reading initial communication packet". The test greps the slave's
error log for the latter with search_pattern_in_file.inc, which reports

  FOUND 1 /Slave I/O thread: reconnect to master .* was interrupted by the slave stop: .*reading initial communication packet/ in mysqld.2.err
Jan Lindström
Merge branch '4.x-MDEV-42140' into 4.ee-MDEV-42140
Alexey Yurchenko
Merge pull request #1136 from mariadb-corporation/4.ee-MDEV-38920-tests

MDEV-38920 add gcomm unit tests for the EVS install timeout defect
Jan Lindström
MDEV-42140 fix uncaught exception from allowlist check in IST receiver

galera::ist::Receiver::run() called acceptor_->accept() outside its own
try block. AsioAcceptorReact::accept() throws gu::Exception when the
allowlist check rejects the peer, and run() is invoked straight from
the raw pthread entry point run_receiver_thread(), which has no
exception handler of its own, so the exception reached
std::terminate() and aborted the whole process (SIGABRT) instead of
being reported through ist_end().

In galera.galera_wsrep_schema_detached this happened when a node
rejoined the cluster while mysql.wsrep_allowlist still held a stale
entry: the IST receiver's incoming connection got rejected and crashed
the process instead of failing the state transfer cleanly.

Move the accept() call inside the existing try block so the exception
is caught like any other IST failure. The acceptor must also be closed
on this error path, not only after a successful accept: otherwise a
later finished()/interrupt() reconnects to the still-listening
acceptor and blocks forever waiting for a handshake nobody sends.

Add galera/tests/ist_check.cpp:test_ist_allowlist_rejected_v7, which
reproduces the crash deterministically via a mock allowlist service
that rejects every connection, confirmed to abort with the same
uncaught gu::Exception on pre-fix code and pass on the fix.

Assisted-by: Claude:claude-4.6-sonnet
Jan Lindström
Merge branch '4.x-26.4.29' into 4.ee-26.4.29
Jan Lindström
MDEV-42000 Interrupt IST receiver's SST wait on shutdown

ist::Receiver::run() waits for ready() to be called once SST/IST
negotiation completes. If the server starts closing before that
happens, nothing signals the wait, and the receiver thread, and
ReplicatorSMM::close() with it, block forever.

Add an interrupted_ flag the wait loop also checks, and a cancel_wait()
method that sets it and signals the condition variable safely from any
thread. Call it from start_closing() and from finished(). Add
test_ist_interrupted_before_ready_v7 as a regression test.

Assisted-by: Claude:claude-4.6-sonnet
Rex Johnston
MDEV-39492 PQ: a GROUP BY column the WHERE binds to a constant is a group column

TPC-DS Q66 ran three times slower with eight workers than without, and
the workers never ran. Each UNION branch groups by the warehouse
columns and d_year under d_year = 2000. The gate asked before the plan
(pwt_query_shape_allows_workers) admitted the branch, so the search
costed date_dim as a parallel driver and chose it, at cost 26 against
the serial plan's. The gate asked after the plan refused the same
branch: "a select list item reads a column outside an aggregate that
GROUP BY does not name". The plan then ran serially in the shape
chosen for the workers, date_dim first with the fact table looked up
per date, 1015 ms against 321.

The two gates disagreed because they read different group lists. The
first sees the GROUP BY as written, with d_year in it. By the time the
second asks, remove_const() has dropped d_year from join->group_list
and from the aggregation table's key as having one value everywhere,
and the select list's d_year is no longer named by either. The column
is a group column all the same: bound to a constant by the WHERE, it
has the one value in every group, which is what the check exists to
establish.

Pwt_group_checker now accepts a column that the join's multiple
equalities bind to a constant, looked up through the COND_EQUAL both
gates are given, whether or not the group list names it. The two gates
agree again, and the branch runs divided. A query cannot be refused on
this ground after the plan if it was admitted before it, which is the
property the pre-plan gate was introduced to provide: a parallel plan
the workers will not run must never be chosen.

main.parallel_query_why gains the shape: a GROUP BY column bound to a
constant, read by the select list, with no refusal and the driving
table divided.

This commit was prepared with Claude Code (Fable 5.1), which traced the
Q66 refusal to the trimmed group list, implemented the check through
the multiple equalities, wrote the test case and ran the parallel-query
tests in both protocols and the main suite.
Vladislav Vaintroub
MDEV-42004 STOP SLAVE waits for slave_net_timeout when the primary is
unreachable

STOP SLAVE could take up to slave_net_timeout to stop the slave IO
thread when the primary accepts the connection but does not answer,
e.g. when its host is frozen or its packets are dropped. The IO thread
is blocked in mysql_real_connect(), waiting for the server greeting,
and THD::awake() could not interrupt it: until the connect succeeds,
the vio is not THD::active_vio.

Let the client tell the owner of the connection about its vio. A new
MYSQL_OPT_VIO_CALLBACK (and MYSQL_OPT_VIO_CALLBACK_ARG, its first
argument) is called with the new vio as soon as it is created, and with
NULL right before it is deleted. It is kept in options.extension, so
mysql_reconnect() carries it over to its temporary MYSQL object.

The slave IO thread sets it up in connect_to_master(). The callback
makes the vio THD::active_vio, so STOP SLAVE shuts the socket down with
vio_shutdown(), which wakes up the blocked wait. If STOP SLAVE has
already set abort_slave, the vio is shut down right away. This works on
all platforms, no signals are needed.

The test does not need SIGSTOP and runs on Windows too. The "primary"
is a socket that listens and never accepts or answers. It fails without
the fix (STOP SLAVE waits for most of slave_net_timeout), and passes in
about a second with it.

The idea of the test, to freeze the primary and check that STOP SLAVE
does not wait for slave_net_timeout, is reused from Andrei Elkin's
original patch for this bug.

Co-authored-by: Andrei Elkin <[email protected]>
Assisted-by: Claude:claude-sonnet-5-5
Rex Johnston
MDEV-39492 PQ: each worker runs a join buffer of its own

A plan that read a table through a join buffer was refused by the gate
(101a4ccc724), and the join-order search costed the tables under a
parallel driver without one so as not to choose such a plan. That kept
the workers out of every plan where a buffer is the right join method:
a cross join, a small dimension scanned under the divided driver
(TPC-DS Q66's ship_mode, which the gate refused on that ground), any
join without a usable index. Before 101a4ccc724 the worker stripped the buffer off its copy of
the tab and joined a row at a time, scanning the inner table once per
driving row instead of once per buffer-full, which is where the order of
magnitude went on

  SELECT * FROM s1 JOIN s2 ON (1)    -- 1000 x 2000 rows

Now a worker runs a buffer of its own. The saving a buffer gives is fewer
scans of the inner table, and a buffer per worker gets the same saving: a
worker with a quarter of the driving rows fills it a quarter as often, so
the inner table is scanned about as often in total as serially, at most
one extra partial-buffer scan per worker, and those scans run
concurrently. There is nothing to share between the workers.

setup_worker_jointabs() builds, for a tab the manager reads through a
BNL or BNLH cache, a new JOIN_CACHE of the same kind over the worker's
own JOIN and tabs, chained to the previous tab's the way the manager's
is, and calls init() on it. init() walks the worker's tabs up to this one
and takes the fields of the worker's table copies, and it sizes and
allocates the buffer through the worker's THD, which carries the
session's join_buffer_size, so each worker gets a buffer of the session's
size. The tab before it is pointed at sub_select_cache(), which fills
the buffer and flushes it when full and on the end-of-records call the
worker's driver already makes. The buffer is freed with the worker's
tables (pwt_worker::close_tables()).

A buffer evaluates two conditions the nested loop does not, and both
needed a home on the worker's tab. The scan filter in cache_select is
applied to each row of the inner table as the buffer scans it
(JOIN_TAB_SCAN::next()), and remove_redundant_bnl_scan_conds() has taken
its conjuncts out of select_cond, so the worker's tab gets an SQL_SELECT
holding a clone of it, rebound to the worker's tables. The remainder is
evaluated by JOIN_CACHE::check_match() through select->cond rather than
select_cond, so a cached tab gets a second SQL_SELECT over the cloned
select_cond; neither carries a quick. The gate checks the scan filter is
worker-safe whole: it is applied before any prefix row is matched, so
the manager cannot take a conjunct of it the way pwt_split_table_cond()
takes one of a table condition.

A hashed buffer (BNLH) keys the buffered rows by the tab's ref, which
for a hash join is keyed by a KEY create_hj_key_for_table() built for
the manager's table, with key parts naming the manager's fields.
pwt_clone_hj_key() makes the same descriptor over the worker's fields,
clone_table_ref() takes the KEY rather than deriving it from the table,
and the gate and the inert assertions admit JT_HASH.

BKA and BKAH are not run: they read the inner table through multi-range
read on the handler. The gate refuses them, and so that the costing never
chooses a plan the gate refuses, check_join_cache_usage_for_tables()
caps a plan chosen for the workers at join_cache_level 4, where a level
that serially means BKA means BNLH. best_access_path() prices a ref
access the same with or without a BKA buffer, so the plan runs in the
shape it was costed in either way.

The join-order search costs the tables under a parallel driver with join
buffers and hash joins (parallel_chain in best_access_path() now
withholds only the rowid filter).

The worker's hand-built JOIN gets the manager's tables_list, read only:
next_linear_tab() ends its walk at exec_join_tab_cnt(), which is zero
for a JOIN without one, and JOIN_CACHE::calc_record_fields() walked past
the end of the worker's array.

ANALYZE: the worker gets its own three buffer counters, summed into the
manager's tab trackers by quiesce_workers() like the table counters.
Exec_time_tracker gains add() and reset_counts() for that.

Measured on the cross join above with four workers: ANALYZE shows s2
scanned twice, once per worker that got rows, where the stripped worker
scanned it a thousand times. The join is no longer the cost of that
query in the workers; what remains is every joined row being written to
the worker's sink and drained by the manager, which the costing does not
price and which no buffer can help. TPC-H SF1 plans and times (Q3, Q5,
Q10, Q12) are as before this commit.

Tests. main.parallel_query_join: the BNL case now runs in the workers,
plus a BNLH case, the level-6 case (BKA serially, BNLH in the workers)
and the cross join above. main.parallel_query_why: a buffered inner
table is no longer a reason. main.parallel_query_costing gains a small
table scanned under the divided driver, which takes its BNL buffer in
the serial and the parallel plan alike and runs in the workers.

This commit was prepared with Claude Code (Fable 5.1), which traced what
a JOIN_CACHE reads from its JOIN and tabs, wrote the worker-side cache
construction, condition and hash-key cloning, the gate and costing
changes and the tracker merge, updated the tests, rebuilt the worktree,
ran the parallel_query tests in both protocols and the main suite, and
timed the cross join and TPC-H on a scratch server.
Jan Lindström
Merge branch '4.x-MDEV-42139' into 4.ee-MDEV-42139
Jan Lindström
Merge remote-tracking branch 'upstream/4.ee' into galera-26.4.29
Rex Johnston
MDEV-42165 Selectivity of a multiple equality with an earlier table is never applied

During optimization, when a table is joined onto the tables already in the
plan, table_multi_eq_cond_selectivity() is meant to account for the
multiple equalities tying its columns to theirs that the chosen access
method does not already use: for each such equality, the largest
avg_frequency/rows over its fields on the earlier tables, multiplied
together across equalities.

It never did. The accumulator starts at 1.0 and is only ever raised:

      double eq_fld_sel= 1.0;
      ...
        if (curr_eq_fld_sel < 1.0)
          set_if_bigger(eq_fld_sel, curr_eq_fld_sel);

Every candidate is below 1.0, so eq_fld_sel stays 1.0 and the function
returns 1.0 for every table in every plan. This has been so since the
function was added (fc1c8ffdadf, the MWL#253 pilot patch).

The effect is visible whenever a table is joined on a column without an
index, so that the join runs through a join buffer: the rows the join
condition rejects are counted as surviving, and every table joined after
it is costed for that many loops. In the test, one of 20 dimension rows
survives its filter and one twentieth of the fact rows joins it, but the
fact table is estimated at filtered 100 and the lookup table under it at
2000 loops where 100 happen. TPC-DS Q93 shows the same shape at scale:
store_returns joined to reason on an unindexed sr_reason_sk, 287514
store_sales lookups estimated where 3710 happen, and a serial plan costed
at 360 that runs in 43 ms.

Make the accumulator do what the comment describes: the largest selectivity
among the fields of the earlier tables, 1.0 only when there is none. The
fact table is then estimated at filtered 5 and the lookup table at 100
loops.

Two things the no-op had been hiding had to change with it.

A column without statistics has an average frequency of 0, which the
accumulator would have taken as "no row matches" and the plan as a
zero-row join. Such a field says nothing and is skipped.

table_after_join_selectivity() multiplied the factor into pos->records_out,
the smallest row count any access method considered for the table had
estimated. That estimate often comes from a ref access on the very
equality the factor accounts for -- a scan whose row count was capped by
the estimate of a ref on the join column, or a ref on one key capped by a
ref on another -- so multiplying counted the equality twice: supplier
after nation in the dbt3 Q9 plan came out at 0.44% where 4% is right. The
factor is now applied to the access method's own estimate (records_init
with the ref discounts for a ref access, records_read for a scan) and the
smaller of that and pos->records_out is kept. This also makes the function
idempotent, which optimize_wo_join_buffering() relies on when it re-costs
a semi-join range from positions that already carry the factor.

Eleven result files change: filtered values where an equality with an
earlier table now counts, and a few join orders in tests on tiny tables
where the estimates moved.

This commit was prepared with Claude Code (Fable 5.1), which found the
no-op while tracing why the parallel plan for Q93 was preferred, confirmed
it with the test here, found the two consequences above from the main
suite's result changes, and wrote the fix, the result updates and this
message.
Aleksey Midenkov
MDEV-41849 Killed query reports ER_NOT_FORM_FILE for a table with virtual column expressions

Since MDEV-37938 the lexer stops on kill: it sends the kill message and
returns END_OF_INPUT. parse_sql() fails only if the grammar rejects the
text read so far.

When a query is killed while a table is opened, the parse of a vcol
expression or partitioning clause from the .frm fails, and
open_table_from_share() reports ER_NOT_FORM_FILE after the kill error,
as if the .frm were corrupted. The grammar also adds ER_EMPTY_QUERY or
ER_PARSE_ERROR. A killed parallel slave worker shows these errors in
Last_SQL_Error. Table discovery replaces the kill error with
ER_SQL_DISCOVER_ERROR. The event scheduler logs an error during
compilation of the event. A kill while trigger bodies are parsed marks
the triggers as broken, and DML on the table fails until FLUSH TABLES.

If the kill comes after a part that is valid on its own (a in a + 1),
the parse succeeds and the cut-short text is used. This applies to any
text parsed by parse_sql().

The fix makes parse_sql() fail when the lexer stopped on kill and
reports only the kill error: the grammar adds no errors,
open_table_from_share() does not report ER_NOT_FORM_FILE, discovery
keeps the kill error, the event scheduler logs only the kill error, and
triggers are not marked as broken.

Lex_input_stream gets ignore_kill, stopped_on_kill and
fail_if_stopped_on_kill(), used in this order:

  1. The caller sets ignore_kill before the parse.
  2. The lexer checks ignore_kill. If it is not set and the thread is
    killed with an error, the lexer raises the kill error, stops and
    sets stopped_on_kill.
  3. After the parse, fail_if_stopped_on_kill() reads stopped_on_kill
    and fails the parse.

ignore_kill is set when triggers are loaded for their names only
(DROP TABLE, RENAME TABLE, I_S.TRIGGERS): they need every trigger name,
e.g. for the .TRN files, and a parse stopped by the kill loses the name.

A CTE referenced twice is parsed again by THD::sql_parser() inside the
outer parse. That parser takes ignore_kill from the outer one and passes
stopped_on_kill back to it, so the outer parse also fails on the kill.

ABORT_QUERY and KILL_BAD_DATA have no kill error, so the lexer does not
stop on them and reads the whole text, as before MDEV-37938.

Table_triggers_list::check_n_load() deletes the triggers loaded so far
on every error, which also fixes their leak on out-of-memory errors.
Teemu Ollakka
MDEV-38920 add gcomm unit tests for the EVS install timeout defect

Cover the install timeout ordering defect and the configuration
sanitizing which fixes it. Simulated time makes both tests
deterministic, and a run takes a fraction of a second.

test_mdev_38920_install_timer_expired (check_pc.cpp) builds a primary
component of five nodes with install_timeout below suspect_timeout.
Messages from node 5 to node 1 are then dropped for 60 seconds, after
which the link is restored. That asymmetry is what
asymmetry_elimination() exists to resolve, and an install timeout
below the suspect timeout disables it. A fully silent node does not
reproduce the defect, because check_inactive() evicts a suspected node
which has no join message at once. The fault is therefore injected as
one directional message loss rather than as a process freeze.

The verdict is that no node may log "install timer expired". The other
assertions record the expected outcome, but neither discriminates the
fix on its own, because a provider which does not sanitize the install
timeout still recovers from a fault of this length.

test_mdev_38920_config_warn (check_evs2.cpp) asserts that the
constructor clamps both install_timeout and inactive_check_period,
warns about each, and leaves a conforming configuration alone.

Both verdicts rest on what the provider logged, so add
gcomm::LogCapture to check_trace. It collects log messages while
started, and raises the log level to info when lower, so that a test
which asserts that a message was absent cannot pass merely because the
message was filtered out.

create_dummy_node() gains an optional install timeout. It defaults to
the inactive timeout, as before.
Teemu Ollakka
MGL-337 Bounds-check GCS state, UUID, vote and action messages

gcs_state_msg_read() parsed a state message received from the network
without checking its length in release builds. The fixed header was
read without a length check, name and inc_addr were measured with
strlen(), and the version-dependent fields were guarded only by
assert(). A truncated or unterminated state message from a cluster
member caused an out-of-bounds read and could crash the node.

Track the read offset and check the message length before each field.
Find the ends of name and inc_addr with memchr() within the buffer.
Form field pointers only within the message. Messages of version 5 and
later always contain the v5 fields, also when gcs protocol is below 2,
so require them. Return NULL for a malformed message. The caller
already logs and ignores a message which cannot be parsed.

Other GCS message handlers had the same missing bounds checks:

- gcs_group_handle_uuid_msg() checked the message size only with
  assert() and read a full UUID from a shorter message. Drop a state
  UUID message of a wrong size.
- gcs_group_handle_vote_msg() logged the optional vote message text as
  a C string, without a NUL terminator check within the message, and
  computed a pointer past the code message for shorter messages. Build
  the text only for failed votes and limit it to the message size.
- gcs_act_proto_read() read the protocol version byte before the
  message length check. Check the length first, and set proto_ver to
  -1 for a message shorter than the header.

Add gcs_state_msg_test_truncated, which places truncated and
unterminated messages before a PROT_NONE page so that any read past
the end of the message faults. The buffer is 8-byte aligned, as
gcs_state_msg_read() requires. Run it with gcs protocol 1 and later.
Move GuardedBuf to gcs_test_utils and add a test for each handler.
Extend the vote message tests to check the recorded vote, the logged
text, a vote without text, and the size check before gcs protocol 1.

Assisted-by: Claude:claude-opus-5-5
Jan Lindström
Bump Galera version to 26.4.29
Alexey Yurchenko
Merge pull request #1135 from mariadb-corporation/MDEV-38920-tests

MDEV-38920 add gcomm unit tests for the EVS install timeout defect
Alessandro Vetere
MDEV-21725 Log page reorganization as the ranges by which records moved

btr_page_reorganize_low() logged the payload as one WRITE from the first
to the last changed byte. When the reorganization removes a gap near the
start of the page, it moves all the later records, and this WRITE covers
almost the whole page.

btr_page_reorganize_log_payload(): Log the payload with MEMMOVE for the
runs of records that only moved, and WRITE for the rest. The function
first copies the runs that moved towards the end of the page, in
descending order of destination. Then it copies the runs that moved
towards the start of the page, in ascending order of destination. In
each order, no copy changes the source of a later copy. If a copy of the
first order changed a part of a source, the function does not copy that
part. Instead, it logs with WRITE the bytes at the destination of that
part that differ from the old page. The function does not copy a run
that is shorter than 8 bytes. It also does not copy the shortest runs
that moved towards the end of the page when the working memory is full.
It logs the changed bytes of these runs with WRITE. The log format does
not change.

btr_page_reorganize_low(): Reserve a second scratch frame for the
working memory of btr_page_reorganize_log_payload().

page_copy_rec_list_end_no_locks(): Add the output parameter extra.

In all versions since the 10.5 redo log format, the recovery parser has
an error. For a same_page record that follows a MEMSET or MEMMOVE
record, it calculates an offset that is too low. It can then reject a
valid MEMMOVE whose source is below its destination as "Malformed log
record". Older servers and backup tools must still recover the log that
the new payload logging writes.

mtr_t::memmove(): Write the page identifier when the parser would
calculate a source below 8 (FIL_PAGE_PREV), which it rejects.
mtr_t::m_last_lag is the difference between the correct offset and the
offset that the parser calculates.

mtr_t::memset(), mtr_t::log_write(): Update mtr_t::m_last_lag.

recv_sys_t::parse_tail(): Add a FIXME for the parser error.

No new test: innodb.alter_copy and innodb.instant_alter_crash recover
page reorganizations. Without the mtr_t::memmove() change,
innodb.alter_copy, innodb.autoinc_persist, innodb.innodb-alter-tempfile,
innodb_fts.crash_recovery and innodb_zip.wl5522_debug_zip fail in
recovery.

innodb.index_lock_upgrade,on with 4k pages (Debug) writes 2,815,439
bytes of redo log instead of 3,293,565 (shutdown LSN minus startup LSN).

Assisted-by: Claude:claude-opus-5-5
Vladislav Vaintroub
MDEV-42004 STOP SLAVE waits for slave_net_timeout when the primary is
unreachable

STOP SLAVE could take up to slave_net_timeout to stop the slave IO
thread when the primary accepts the connection but does not answer,
e.g. when its host is frozen or its packets are dropped. The IO thread
is blocked in mysql_real_connect(), waiting for the server greeting,
and THD::awake() could not interrupt it: until the connect succeeds,
the vio is not THD::active_vio.

Let the client tell the owner of the connection about its vio. A new
MYSQL_OPT_VIO_CALLBACK (and MYSQL_OPT_VIO_CALLBACK_ARG, its first
argument) is called with the new vio as soon as it is created, and with
NULL right before it is deleted. It is kept in options.extension, so
mysql_reconnect() carries it over to its temporary MYSQL object.

The slave IO thread sets it up in connect_to_master(). The callback
makes the vio THD::active_vio, so STOP SLAVE shuts the socket down with
vio_shutdown(), which wakes up the blocked wait. If STOP SLAVE has
already set abort_slave, the vio is shut down right away. This works on
all platforms, no signals are needed.

The test does not need SIGSTOP and runs on Windows too. The "primary"
is a socket that listens and never accepts or answers. It fails without
the fix (STOP SLAVE waits for most of slave_net_timeout), and passes in
about a second with it.

The idea of the test, to freeze the primary and check that STOP SLAVE
does not wait for slave_net_timeout, is reused from Andrei Elkin's
original patch for this bug.

Co-authored-by: Andrei Elkin <[email protected]>
Assisted-by: Claude:claude-sonnet-5-5
Thirunarayanan Balathandayuthapani
MDEV-39061 mariadb-backup compatible wrapper for BACKUP SERVER

This adds a shell script that lets users keep using their existing
mariadb-backup commands while the real work is done by the new
server-side BACKUP SERVER command. The goal is "drop-in": users
should not have to change their backup scripts.

mariadb-backup-server.sh : Understands the usual mariadb-backup
modes and translates each one.

mbstream-server.sh : lets streamed backups be unpacked by
pipelines that expect the mbstream CLI

mariadb-backup-pipe.sh is the stream sink that
BACKUP SERVER itself runs.

All three are documented in README.md.

--backup
========
Connects with the mariadb client and runs
"BACKUP SERVER TO '<dir>'".

Connection options (--user, --host, --port, --socket,
--defaults-file, ssl, ...) are passed through to the client.

--parallel=N becomes "<N+1> CONCURRENT": mariadb-backup
runs a dedicated log_copying_thread() besides its N data-copy
threads, while CONCURRENT counts every BACKUP SERVER worker.
The result is clamped to the 1..256 the parser accepts,
and to a minimum of 2.

After the backup it writes backup-prepare.cnf into the backup
directory, recording what --prepare needs later:
  - where mariadbd lives
  - InnoDB parameters (page size, data file path, undo tablespaces,
                      checksum algorithm, log file size)
  - If the server is encrypted then how to reload the
    encryption key plugin (the file_key_management variables),
    so an encrypted backup can be prepared without extra input.
  - every active provider_* compression plugin, with plugin-dir.

The providers matter because --prepare runs "mariadbd --bootstrap
--defaults-file=backup-prepare.cnf", which suppresses the
/etc/my.cnf.d snippet that normally enables a provider. Recovery has
to decompress a PAGE_COMPRESSED page before it can apply redo to it,
so without the provider it aborts with "Unable to apply log to
corrupted page". mariadb-backup carries the same plugins through
backup-my.cnf (encryption_plugin.cc QUERY_PLUGIN collects the
ENCRYPTION plugin and the provider_* DAEMON plugins together), so
this is parity with it rather than a new capability.

--backup --stream
=================
Runs "BACKUP SERVER WITH 'pipe'".
The server only accepts a bare command name, which it resolves
n the PATH of the mariadbd process after prepending mariadb-backup-,
so uses the installed mariadb-backup-pipe instead.

mariadb-backup-pipe writes the tar into
.mariadb-backup-pipe-<stream>.tar, a named pipe the wrapper
created in the server @@datadir and is already draining to
its own stdout.

The backup therefore reaches the consumer as it is produced and
never lands on local disk. The wrapper appends backup-prepare.cnf
as a final tar afterwards; the server's tar carries no
end-of-archive marker, so that trailing archive supplies the
only one and the whole stream extracts with a plain "tar -x".
The trailing archive is written with "tar -b 1" so that nothing
follows its end-of-archive marker. At the default blocking factor tar
pads the archive to 10240 bytes; the reader stops at the marker and
exits with the padding still unwritten, which on a small pipe is a
short write. tar treats a short write as fatal, so a backup that had
in fact completed was reported as failed.

--parallel is ignored here, with a warning.

Four properties follow from how BACKUP SERVER streams,
all differing from mariadb-backup:
- local: the stream command runs inside the server,
so the wrapper must share its filesystem;
- fifo lives in @@datadir, because mariadb-backup-pipe resolves it
relative to the working directory of mariadbd, so --stream needs
write access there;
- tar only: any --stream=<format> yields tar;
- single-threaded: one worker, so no parallel read either.
--target-dir is optional in stream mode;

mbstream-server.sh maps the mbstream CLI onto a plain
"tar -x"/"tar -c", so existing "mbstream -x"/"-c" pipelines
keep working on the wrapper's stream.

mbstream-only flags (-p/--parallel, ...) are accepted and
ignored; any other unknown option is rejected.

Environment overrides:
MARIADB (client),
MARIADBD (the --prepare bootstrap server) and
TAR (the tar implementation, e.g. TAR=bsdtar) can each be overridden.

To run the bootstrap under rr, put it in MARIADBD and
let rr's own _RR_TRACE_DIR choose the trace location, e.g.
  _RR_TRACE_DIR=/dev/shm/rr MARIADBD='rr record mariadbd'

--prepare
=========
Starts "mariadbd --bootstrap" on the backup directory using
backup-prepare.cnf as its defaults file, replays the archived redo
log between the start and target LSN read from backup.cnf,
then builds a fresh ib_logfile0 so a normal server can start
on the directory.

mariadbd is taken from the path recorded in backup-prepare.cnf
if that binary exists, otherwise by searching
/libexec, /sbin, /bin and the configured install directories.

PATH is not searched; set MARIADBD to point elsewhere.
User --defaults-file/-extra-file and encryption options are
layered onto the bootstrap.

--copy-back / --move-back
=========================
Copy or move a prepared backup into the datadir. The datadir
is created if missing, a non-empty datadir is refused unless
--force-non-empty-directories is given, and a chown
reminder is printed.

If --aria-log-dir-path is given, the Aria logs (aria_log_control,
aria_log.*) are relocated into that directory.

Packaging
=========
The wrapper is not installed by default and never replaces the
real mariadb-backup / mbstream binaries.
1. cmake -DWITH_MARIABACKUP_WRAPPER=ON (default OFF) controls it.
2. When ON, the scripts install as /usr/bin/mariadb-backup-server,
/usr/bin/mbstream-server and /usr/bin/mariadb-backup-pipe, tagged
COMPONENT Backup so they ship in the mariadb-backup package.
mariadb-backup-pipe must end up in the PATH of the mariadbd
process, not merely in the PATH of whoever runs the wrapper.
  mariadb-backup-pipe is generated unconditionally, because the
  mariabackup test suite needs it on the server's PATH even when the
  wrapper is not installed; only its install rule is gated.
3. RPM: nothing extra to do. the component handles it.
4. DEB: not wired. debian/rules uses --fail-missing and does not
enable the option, so the -server binaries are not listed.
To ship via DEB, make a paired change: add
-DWITH_MARIABACKUP_WRAPPER=ON in debian/rules and list all three of
usr/bin/mariadb-backup-server, usr/bin/mbstream-server and
usr/bin/mariadb-backup-pipe in debian/mariadb-backup.install together.
5. The real mariadb-backup/mbstream binaries and the
mariabackup symlink are left untouched; opt in via an alias or a
symlink early in PATH.

Limitations (not supported yet)
===============================
1) Incremental backup & prepare (--incremental-basedir,
  --incremental-dir, --apply-log-only)
2) --rollback-xa
3) Partial backup (--databases, --tables, --tables-file)
4) Output compression and encryption (--compress, --encrypt)
5) --export is accepted but only warns and runs a plain recovery
6) --extra-lsndir is ignored
7) --parallel is ignored with --stream
8) Windows: POSIX sh only, not installed on Windows

Behaviour differences from native mariadb-backup
================================================
- The wrapper needs the mariadb client on PATH for --backup;
--prepare needs mariadbd recorded in backup-prepare.cnf, in a
standard install directory, or named by MARIADBD
- BACKUP SERVER refuses an already-existing target directory
- BACKUP SERVER does copy the data file as raw pages without
checksum validation, so a corrupted table is not detected
at backup time
- --prepare only works on a wrapper-made backup: it
needs backup-prepare.cnf
- --stream is tar, not xbstream, local-only and single-threaded,
and needs write access to @@datadir for the fifo

Tests
=====
include/have_mariabackup_wrapper.inc redirects $XTRABACKUP to
mariadb-backup-server.sh and $XBSTREAM to mbstream-server.sh,
skipping when a wrapper or the mariadb client is unavailable.

include/have_mariabackup_combination.inc runs a test under both the
[CLIENT] mariadb-backup binary and the [SERVER] wrapper.

suite/mariabackup/suite.pm prepends the directory holding
mariadb-backup-pipe to PATH. BACKUP SERVER resolves the command in the
PATH of the mariadbd process, which mtr starts from the suite's
environment, so it has to be set there rather than with a --let at
--exec time. have_mariabackup_wrapper.inc skips the [SERVER]
combination when the helper is not on PATH.
Alexey Yurchenko
Merge pull request #1147 from mariadb-corporation/4.ee-MDEV-42140

4.ee mdev 42140
Aleksey Midenkov
MDEV-41849 Killed query reports ER_NOT_FORM_FILE for a table with virtual column expressions

Since MDEV-37938 the lexer stops on kill: it sends the kill message and
returns END_OF_INPUT. parse_sql() fails only if the grammar rejects the
text read so far.

When a query is killed while a table is opened, the parse of a vcol
expression or partitioning clause from the .frm fails, and
open_table_from_share() reports ER_NOT_FORM_FILE after the kill error,
as if the .frm were corrupted. The grammar also adds ER_EMPTY_QUERY or
ER_PARSE_ERROR. A killed parallel slave worker shows these errors in
Last_SQL_Error. Table discovery replaces the kill error with
ER_SQL_DISCOVER_ERROR. The event scheduler logs an error during
compilation of the event. A kill while trigger bodies are parsed marks
the triggers as broken, and DML on the table fails until FLUSH TABLES.

If the kill comes after a part that is valid on its own (a in a + 1),
the parse succeeds and the cut-short text is used. This applies to any
text parsed by parse_sql().

The fix makes parse_sql() fail when the lexer stopped on kill and
reports only the kill error: the grammar adds no errors,
open_table_from_share() does not report ER_NOT_FORM_FILE, discovery
keeps the kill error, the event scheduler logs only the kill error, and
triggers are not marked as broken.

Lex_input_stream gets ignore_kill, stopped_on_kill and
fail_if_stopped_on_kill(), used in this order:

  1. The caller sets ignore_kill before the parse.
  2. The lexer checks ignore_kill. If it is not set and the thread is
    killed with an error, the lexer raises the kill error, stops and
    sets stopped_on_kill.
  3. After the parse, fail_if_stopped_on_kill() reads stopped_on_kill
    and fails the parse.

ignore_kill is set when triggers are loaded for their names only
(DROP TABLE, RENAME TABLE, I_S.TRIGGERS): they need every trigger name,
e.g. for the .TRN files, and a parse stopped by the kill loses the name.

A CTE referenced twice is parsed again by THD::sql_parser() inside the
outer parse. That parser takes ignore_kill from the outer one and passes
stopped_on_kill back to it, so the outer parse also fails on the kill.

ABORT_QUERY and KILL_BAD_DATA have no kill error, so the lexer does not
stop on them and reads the whole text, as before MDEV-37938.

Table_triggers_list::check_n_load() deletes the triggers loaded so far
on every error, which also fixes their leak on out-of-memory errors.
Jan Lindström
MDEV-42139 fix spurious wakeup in gu_debug_sync_wait()

gu_debug_sync_wait() blocked on a single, unlooped condition variable
wait. pthread_cond_wait() may return without any matching signal (a
spurious wakeup), and nothing here re-checked the real predicate, so a
spurious wakeup was treated the same as a real gu_debug_sync_signal()
call. Every other condition variable wait in galerautils (gu_to.c,
gu_fifo.c) already loops on its predicate; this was the one place that
did not.

In galera_3nodes.galera_join_with_cc_B this let a waiter on the
"process_primary_configuration" sync point disappear from
wsrep_debug_sync_waiters between a wait_condition.inc poll and the
test's own follow-up SELECT, producing an empty result instead of the
point name.

Add a per-waiter 'signalled' flag and re-check it in a loop around the
wait call. Add galerautils/tests/gu_debug_sync_test.cpp, which
reproduces the defect deterministically via a test-only hook that
injects a simulated spurious wakeup, and wire GALERA_GU_DBUG_ON through
to galerautilsxx so the test can build.

Assisted-by: Claude:claude-4.6-sonnet
Jan Lindström
MDEV-42000 Interrupt IST receiver's SST wait on shutdown

ist::Receiver::run() waits for ready() to be called once SST/IST
negotiation completes. If the server starts closing before that
happens, nothing signals the wait, and the receiver thread, and
ReplicatorSMM::close() with it, block forever.

Add an interrupted_ flag the wait loop also checks, and an interrupt_wait()
method that sets it and signals the condition variable safely from any
thread. Call it from start_closing() and from finished(). Add
test_ist_interrupted_before_ready_v7 as a regression test.

Assisted-by: Claude:claude-4.6-sonnet
Oleksandr Byelkin
fix main.mdev_31342_kill with view and cursor protocol
Rex Johnston
MDEV-39492 PQ: choose the divided driving table as a plan, not by its scan alone

TPC-DS Q29 ran at twice its serial time with eight workers. Serially the
optimizer starts at date_dim d2, whose WHERE keeps 122 of 73049 rows,
and reaches store_returns through the returned-date index: 16700
lookups into store_sales. With workers it started at store_returns
instead, divided its 287514 rows eight ways, and applied the d2 filter
only after the store_sales lookup, so each worker did 36000 of those
lookups. Dividing d2 and keeping the serial order under it costs 24
against the chosen plan's 58 and the serial plan's 61, and the search
never saw it.

best_access_path() priced the driving table's scan both ways and kept
the cheaper reading on the spot. For d2 the parallel reading is the
dearer: one eighth of a 12.5 scan plus 2 ms of setup for each of eight
tables comes to 17.6, so the serial reading was kept and the plan with
d2 divided was never extended. What that comparison leaves out is the
join under the driving table, which the division cuts eight ways too
and which is where the cost is. The scan of a table like d2 can never
repay the setup on its own; the join under it repays it many times
over.

The search now offers the driving position every candidate twice. After
get_costs_for_tables() has costed each table with its scan read
serially, add_parallel_driver_candidates() costs it again with
JOIN::parallel_driver_costing set, under which best_access_path()
prices a full or range scan as one worker's share plus the setup
(parallel_scan_cost() no longer judges the reading), and appends the
result as a further candidate when the parallel scan is what
best_access_path() chose. Both readings are extended, and the complete
plans compete on cost. optimize_straight_join() costs the fixed order
twice in the same way and keeps the cheaper. The sort space in
make_join_statistics() grows by two POSITIONs per table for the extra
candidates.

Two more things had to change for the search to be honest about it.

The pruning heuristic keeps parallel prefixes apart from serial ones so
that a divided big table is not dropped next to a tiny driver. With
every table now offered as a parallel candidate, the tiny driver's
parallel reading landed in the parallel family too -- one row, next to
no cost -- and dropped every prefix that does spread its rows, which
is what the family exists to protect. A candidate whose divisor is 1
spreads nothing over the workers and is compared with the serial
prefixes, where it loses to its serial twin by the setup it is charged.

And the divisor itself credited a table with a division the workers
cannot make. With the local comparison gone, TPC-H Q5 chose NATION as
its driving table: 25 rows, 5 surviving, costed as spreading the whole
join under it over four workers at cost 230 against the ORDERS-first
plan's 297. NATION is a single InnoDB page, which the partitioner
cannot cut, so one worker ran the whole join at serial speed. The
handler now says how many pieces a scan of its table can be cut into
(parallel_scan_pieces(): for InnoDB the leaf pages of the clustered
index, which bounds the chunks the partitioner can make), scaled by
the share of the table a range reads, and the rows that survive the
driving table's WHERE are spread over no more workers than that. They
are also spread as the busiest worker sees them: R rows over W workers
put ceil(R/W) on one of them, so the divisor is R / ceil(R/W), which
makes five rows over four workers a division by 2.5 and one row a
division by 1. The scan's own cost is still divided by N: a scan the
engine cannot cut is at most a couple of pages, worth nothing next to
the setup, and dividing it is what lets the parallel-query tests, whose
tables are that small, run the workers with the setup cost at 0.

Measured on TPC-DS SF1 with eight workers, Q29: serial 34-46 ms, before
78-90 ms with store_returns divided, after 33-35 ms with d2 divided.
The two workers that receive the 122 qualifying dates, contiguous in
the primary key, do most of the join, so the gain over serial is
small; the loss is gone. Q93 and Q32 stay serial, as they should. On
TPC-H SF1 with four workers the plans of Q1, Q3, Q5, Q7, Q10, Q11 and
Q12 and their times are as the costing commit measured them, Q5 at
165 ms with ORDERS divided.

main.parallel_query_costing gains a 2000-row dimension with 200 rows
matching and 100 fact rows under each: its scan alone cannot repay the
default setup, and it drives the join divided all the same, with and
without STRAIGHT_JOIN.

This commit was prepared with Claude Code (Fable 5.1), which found the
local comparison in the optimizer trace of Q29, implemented the second
candidate, the divisor-keyed pruning family, the pieces estimate and
the busiest-worker divisor, found the Q5 regression and the pruning
regression by measuring TPC-H and re-running the costing test, wrote
the new test section, and ran the parallel-query tests in both
protocols and the main suite.
Rex Johnston
MDEV-39492 PQ: a plan chosen for the workers must reach them, through
the split-materialized reoptimization and a reduced GROUP BY

TPC-DS Q68 with eight workers chose, for its grouped derived table, the
plan with store_sales divided among the workers and every other table
looked up by eq_ref, and then ran that plan serially: a full scan of
2,880,404 rows with four lookups each, 1.7 s against 0.3 s for the
serial plan that starts from the 72 matching dates. Two gate defects,
each enough on its own, turned the chosen parallel plan into a serial
one in the shape the workers would have run.

The first is in the post-plan check that every select-list column
outside an aggregate is a group column. The query groups by
ss_ticket_number, ss_customer_sk, ss_addr_sk and ca_city, and selects
ca_city. The plan reads customer_address by eq_ref on ss_addr_sk, a
grouped column, so remove_const() drops ca_city from the plan's GROUP
BY (eq_ref_table(): every row of a group reads the one same
customer_address row), and the aggregation table is keyed by what is
left. The check, asked with that key, found ca_city unnamed and
refused. And the pre-plan form of the same check, which is asked again
by the full gate, read the query's GROUP BY from join->group_list,
which remove_const() had reduced in place -- relinking the select's
own ORDER nodes, so select_lex->group_list was reduced with it -- and
refused for the same reason.

Pwt_group_checker now also accepts a column of a table the plan reads
by eq_ref through columns that are themselves grouped, constant or
determined the same way, which is remove_const()'s own reasoning and
leaves the column with one value per group; it is asked only of a
finished plan, with the plan to read. And pwt_query_shape_allows_workers()
copies the query's GROUP BY the first time it is asked, before the plan
(choose_plan()), into JOIN::parallel_query_group, and answers from the
copy afterwards.

The second is in what survives a reoptimization. The outer query joins
the derived table by a key on ss_customer_sk, so opt_split.cc tries
splitting it: it saves the plan, runs choose_plan() again with the outer
equality pushed in, and restores the saved plan when the split loses, as
it does here with 100,000 customers to run it for. choose_plan() sets
JOIN::parallel_plan_chosen for the plan it finds, so the restored plan
-- the divided one -- came back with the flag the losing split had left,
false, and parallel_join_check() declined it as "costed cheaper read
serially". The flag is a property of the plan and now travels with it:
Join_plan_state carries it, save_query_plan() takes it and
restore_query_plan() puts it back.

With both, the derived table runs in the workers: 0.76 s with eight,
Parallel_queries_executed counting it. That is still slower than the
0.30 s serial plan, which the costing values at 514 against the divided
plan's 395. The gap is not in the gate. The forced store_sales-first
plan takes 1.61 s serially, 1.10 s with two workers, 0.78 s with four,
0.70 s with eight and 0.73 s with twelve, and ANALYZE puts the time in
the 2,880,404 eq_ref lookups of date_dim: 0.35 us each serially, 1.6 us
each in a worker when eight of them look up the same 73,049-row table
at once, while the scan of store_sales divides cleanly. The costing
divides lookups by the number of workers as it divides the scan, and
for lookups into a small hot table the engine does not. That is a
separate matter, left for its own change.

Tests. main.parallel_query_why: a GROUP BY column of a table read by
eq_ref through a grouped column, dropped from the plan's key, no longer
a reason. main.parallel_query_costing: a grouped derived table joined by
a key on its group column from an outer query too wide for the split to
win, which runs in the workers.

This commit was prepared with Claude Code (Fable 5.1), which traced the
two refusals on a copy of the TPC-DS data, found the reduced GROUP BY
and the flag lost across the restored plan, wrote the fixes and the
tests, rebuilt the worktree, measured the forced plan's scaling by
worker count, and ran the parallel_query tests in both protocols and
the main suite.
Sergei Petrunia
MDEV-42044: I_S.OPTIMIZER_CONTEXT exposes rows, DDL and statistics ...

Optimizer Context has DDLs of used tables and VIEWs, table statistics,
and even captured table rows which include all, even the columns that
the query doesn't read.

The user running the query might not have access to some of this
information. Making it available in the optimizer context would create
an opportunity to bypass the security controls.

There is a similar issue in the Optimizer Trace. It is solved by not
producing optimizer trace if it would expose data that's not available
otherwise.

Piggy-back on the Optimizer Trace security checks and do the same:
disallow saving Optimizer Context if it can contain information that
the caller would not have access to.

In addition to Optimizer Trace's check, add
opt_ctx_can_read_all_columns() and check that when we're about to
dump a full table row into the optimizer context.
bsrikanth-mariadb
MDEV-42044: Fix Optimizer Context privilege checks

Follow-up to the previous MDEV-42044 commit:

* opt_trace_disable_if_no_security_context_access() returned early when
  the optimizer trace was not started, so a context recorded through
  DEFINER routines, triggers or views was never marked as restricted.
  Mark the recorder before that return.
* Check that the invoker can read every table whose DDL and statistics
  are dumped, including tables locked only for stored functions and
  triggers. Previously only dumped rows were checked.
* Skip the access checks when built with NO_EMBEDDED_ACCESS_CHECKS.
* Factor the repeated "missing privilege" code in opt_trace.cc into
  helpers.

Tests: assert the "Insufficient privileges" output, and add cases for
column-level grants, an unexecuted function, a DEFINER trigger,
optimizer_trace=ON, and a privileged user who still gets a full context.
Aleksey Midenkov
MDEV-41849 Killed query reports ER_NOT_FORM_FILE for a table with virtual column expressions

Since MDEV-37938 the lexer stops on kill: it sends the kill message and
returns END_OF_INPUT. parse_sql() fails only if the grammar rejects the
text read so far.

When a query is killed while a table is opened, the parse of a vcol
expression or partitioning clause from the .frm fails, and
open_table_from_share() reports ER_NOT_FORM_FILE after the kill error,
as if the .frm were corrupted. The grammar also adds ER_EMPTY_QUERY or
ER_PARSE_ERROR. A killed parallel slave worker shows these errors in
Last_SQL_Error. Table discovery replaces the kill error with
ER_SQL_DISCOVER_ERROR. The event scheduler logs an error during
compilation of the event. A kill while trigger bodies are parsed marks
the triggers as broken, and DML on the table fails until FLUSH TABLES.

If the kill comes after a part that is valid on its own (a in a + 1),
the parse succeeds and the cut-short text is used. This applies to any
text parsed by parse_sql().

The fix makes parse_sql() fail when the lexer stopped on kill and
reports only the kill error: the grammar adds no errors,
open_table_from_share() does not report ER_NOT_FORM_FILE, discovery
keeps the kill error, the event scheduler logs only the kill error, and
triggers are not marked as broken.

Lex_input_stream gets ignore_kill, stopped_on_kill and
fail_if_stopped_on_kill(), used in this order:

  1. The caller sets ignore_kill before the parse.
  2. The lexer checks ignore_kill. If it is not set and the thread is
    killed with an error, the lexer raises the kill error, stops and
    sets stopped_on_kill.
  3. After the parse, fail_if_stopped_on_kill() reads stopped_on_kill
    and fails the parse.

ignore_kill is set when triggers are loaded for their names only
(DROP TABLE, RENAME TABLE, I_S.TRIGGERS): they need every trigger name,
e.g. for the .TRN files, and a parse stopped by the kill loses the name.

A CTE referenced twice is parsed again by THD::sql_parser() inside the
outer parse. That parser takes ignore_kill from the outer one and passes
stopped_on_kill back to it, so the outer parse also fails on the kill.

ABORT_QUERY and KILL_BAD_DATA have no kill error, so the lexer does not
stop on them and reads the whole text, as before MDEV-37938.

Table_triggers_list::check_n_load() deletes the triggers loaded so far
on every error, which also fixes their leak on out-of-memory errors.
Jan Lindström
Fix mdev_31342 test case by disabling test on protocol tests
bsrikanth-mariadb
MDEV-42044: Fix Optimizer Context privilege checks

Follow-up to the previous MDEV-42044 commit:

* opt_trace_disable_if_no_security_context_access() returned early when
  the optimizer trace was not started, so a context recorded through
  DEFINER routines, triggers or views was never marked as restricted.
  Mark the recorder before that return.
* Check that the invoker can read every table whose DDL and statistics
  are dumped, including tables locked only for stored functions and
  triggers. Previously only dumped rows were checked.
* Skip the access checks when built with NO_EMBEDDED_ACCESS_CHECKS.
* Factor the repeated "missing privilege" code in opt_trace.cc into
  helpers.

Tests: assert the "Insufficient privileges" output, and add cases for
column-level grants, an unexecuted function, a DEFINER trigger,
optimizer_trace=ON, and a privileged user who still gets a full context.
Jan Lindström
Merge branch '4.x-MDEV-42000' into 4.ee-MDEV-42000
Alexey Yurchenko
Merge pull request #1146 from mariadb-corporation/4.x-MDEV-42140

MDEV-42140 fix uncaught exception from allowlist check in IST receiver