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

MDEV-40012 PQ: refuse DEFAULT() in an expression a worker evaluates

DEFAULT(col) was evaluated against the worker's row. Item_default_value is
an Item_field whose field is a private copy made by make_default_field(),
reading the share's default_values but carrying the column's own table.
Its deep_copy() shares that field, and Pwt_field_rebinder, seeing the
manager's table, repointed it at the worker's real column. DEFAULT(col)
then read the current row's value, and for an expression default
calculate() called set_default() and wrote the default into the row --
wrong results in a release build, a marked_for_write assertion in a debug
one. It shows only when the expression also names the column some other
way; alone, DEFAULT(col) is table-independent and evaluated at optimize
time.

The gate now refuses any expression holding a DEFAULT(), found with
check_func_default_processor, which only Item_default_value answers.

Found by running the main suite with --parallel-worker-threads=4, where
type_temporal_innodb lost a "Truncated incorrect BOOLEAN value" warning
because the worker's WHERE evaluated to NULL. parallel_query_clone covers
a lost row, a gained row and the expression default that crashed, and
checks that none of them ran in the workers.

This commit was prepared with Claude Code (Opus 5.5), which traced the
wrong result from a main-suite sweep with workers enabled to the rebinder,
wrote the gate refusal and the test, confirmed the test fails without the
refusal, and ran the parallel_query tests in normal and --ps-protocol
modes.
This commit is contained in:
Rex Johnston
2026-10-20 06:22:42 +13:00
parent 007b40adaf
commit b6a15dbbe9
3 changed files with 80 additions and 1 deletions

View File

@@ -79,4 +79,38 @@ d
2012-01-03
2012-01-04
DROP TABLE t6;
#
# DEFAULT() reads the column's default, never the row a worker is on
#
CREATE TABLE t7 (f TIMESTAMP DEFAULT 0, a INT, b INT DEFAULT (a*100))
ENGINE=InnoDB;
INSERT INTO t7 VALUES ('2024-02-29', 1, 5), ('2025-01-01', 2, 6);
SET SESSION parallel_worker_threads=0;
SELECT a FROM t7 WHERE CONCAT(DEFAULT(f), '|', f) =
'0000-00-00 00:00:00|2024-02-29 00:00:00';
a
1
SELECT a FROM t7 WHERE SUBSTR(1 FROM BIT_LENGTH(f) FOR DEFAULT(f)) IS NULL;
a
SELECT a, b FROM t7 WHERE DEFAULT(b) + b > 0;
a b
1 5
2 6
# same answers with workers enabled, and none of them ran in the workers
SET SESSION parallel_worker_threads=4;
FLUSH STATUS;
SELECT a FROM t7 WHERE CONCAT(DEFAULT(f), '|', f) =
'0000-00-00 00:00:00|2024-02-29 00:00:00';
a
1
SELECT a FROM t7 WHERE SUBSTR(1 FROM BIT_LENGTH(f) FOR DEFAULT(f)) IS NULL;
a
SELECT a, b FROM t7 WHERE DEFAULT(b) + b > 0;
a b
1 5
2 6
SHOW SESSION STATUS LIKE 'Parallel_queries_executed';
Variable_name Value
Parallel_queries_executed 0
DROP TABLE t7;
DROP TABLE t1,t2,t3,t4,t5;

View File

@@ -65,4 +65,27 @@ SET SESSION parallel_worker_threads=4;
SELECT * FROM t6 WHERE LEAST( UTC_TIME(), d ) AND d < DATE'2012-01-05';
DROP TABLE t6;
--echo #
--echo # DEFAULT() reads the column's default, never the row a worker is on
--echo #
CREATE TABLE t7 (f TIMESTAMP DEFAULT 0, a INT, b INT DEFAULT (a*100))
ENGINE=InnoDB;
INSERT INTO t7 VALUES ('2024-02-29', 1, 5), ('2025-01-01', 2, 6);
SET SESSION parallel_worker_threads=0;
SELECT a FROM t7 WHERE CONCAT(DEFAULT(f), '|', f) =
'0000-00-00 00:00:00|2024-02-29 00:00:00';
SELECT a FROM t7 WHERE SUBSTR(1 FROM BIT_LENGTH(f) FOR DEFAULT(f)) IS NULL;
--sorted_result
SELECT a, b FROM t7 WHERE DEFAULT(b) + b > 0;
--echo # same answers with workers enabled, and none of them ran in the workers
SET SESSION parallel_worker_threads=4;
FLUSH STATUS;
SELECT a FROM t7 WHERE CONCAT(DEFAULT(f), '|', f) =
'0000-00-00 00:00:00|2024-02-29 00:00:00';
SELECT a FROM t7 WHERE SUBSTR(1 FROM BIT_LENGTH(f) FOR DEFAULT(f)) IS NULL;
--sorted_result
SELECT a, b FROM t7 WHERE DEFAULT(b) + b > 0;
SHOW SESSION STATUS LIKE 'Parallel_queries_executed';
DROP TABLE t7;
DROP TABLE t1,t2,t3,t4,t5;

View File

@@ -125,10 +125,32 @@ static bool pwt_item_reads_a_foreign_table(JOIN *join, Item *item)
}
/**
@brief
Whether 'item' holds a DEFAULT(col).
@description
Item_default_value is an Item_field whose field is a private copy built by
make_default_field(), reading the share's default_values, but with the
column's own table. Its copy shares that field, and the rebinder, seeing the
manager's table, repoints it at the worker's column. DEFAULT(col) then reads
the row the worker is on, and for an expression default calculate() goes
further and set_default() writes the default into that row.
Only reachable when the condition also names the column some other way:
alone, DEFAULT(col) depends on no table and is evaluated at optimize time.
*/
static bool pwt_item_has_default_value(Item *item)
{
return item->walk(&Item::check_func_default_processor, nullptr, 0);
}
static bool pwt_item_is_worker_safe(JOIN *join, Item *item)
{
return pwt_item_is_clonable(join->thd, item) &&
!pwt_item_reads_a_foreign_table(join, item);
!pwt_item_reads_a_foreign_table(join, item) &&
!pwt_item_has_default_value(item);
}