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:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user