diff --git a/mysql-test/main/delete_use_source.result b/mysql-test/main/delete_use_source.result index 608d20b383d0a..e4d0c554ccd91 100644 --- a/mysql-test/main/delete_use_source.result +++ b/mysql-test/main/delete_use_source.result @@ -350,7 +350,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY t2 ALL NULL NULL NULL NULL 8 100.00 Using where 1 PRIMARY t2 ref a a 5 test.t2.c1 1 100.00 Using index; FirstMatch(t2) Warnings: -Note 1003 delete from `test`.`t1` `t2` using (`test`.`t2`) where `test`.`t2`.`a` = `test`.`t2`.`c1` +Note 1003 delete from `t2` using (`test`.`t2`) where `test`.`t2`.`a` = `test`.`t2`.`c1` delete from t1 t2 where t2.c1 in (select a from t2); select * from t1; c1 c2 c3 @@ -368,7 +368,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY t_x ALL NULL NULL NULL NULL 7 100.00 Using where 1 PRIMARY t_y ref a a 5 test.t_x.c2 1 100.00 Using index; FirstMatch(t_x) Warnings: -Note 1003 delete from `test`.`t1` `t_x` using (`test`.`t2` `t_y`) where `test`.`t_y`.`a` = `test`.`t_x`.`c2` +Note 1003 delete from `t_x` using (`test`.`t2` `t_y`) where `test`.`t_y`.`a` = `test`.`t_x`.`c2` delete from t1 t_x where t_x.c2 IN (select a from t2 as t_y); select * from t1; c1 c2 c3 @@ -383,7 +383,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY t_x ALL NULL NULL NULL NULL 4 100.00 Using where 1 PRIMARY t_x ref a a 5 test.t_x.c3 1 100.00 Using index; FirstMatch(t_x) Warnings: -Note 1003 delete from `test`.`t1` `t_x` using (`test`.`t2` `t_x`) where `test`.`t_x`.`a` = `test`.`t_x`.`c3` +Note 1003 delete from `t_x` using (`test`.`t2` `t_x`) where `test`.`t_x`.`a` = `test`.`t_x`.`c3` delete from t1 as t_x where t_x.c3 IN (select a from t2 t_x); select * from t1; c1 c2 c3 @@ -397,7 +397,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY t2 ALL NULL NULL NULL NULL 3 100.00 1 PRIMARY t1 index NULL a 5 NULL 3 33.33 Using where; Using index; FirstMatch(t2) Warnings: -Note 1003 delete from `test`.`t1` `t2` using (`test`.`t2` `t1`) where `test`.`t2`.`c1` = `test`.`t1`.`a` - 1 +Note 1003 delete from `t2` using (`test`.`t2` `t1`) where `test`.`t2`.`c1` = `test`.`t1`.`a` - 1 delete from t1 t2 where t2.c1 IN (select a -1 from t2 as t1); select * from t1; c1 c2 c3 diff --git a/mysql-test/main/opt_hints_join_order.result b/mysql-test/main/opt_hints_join_order.result index 70df5ca241434..e2924f02d893f 100644 --- a/mysql-test/main/opt_hints_join_order.result +++ b/mysql-test/main/opt_hints_join_order.result @@ -893,7 +893,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY ta1 eq_ref PRIMARY PRIMARY 4 test.t2.f1 1 100.00 Using where 1 PRIMARY ta2 ALL NULL NULL NULL NULL 3 100.00 Warnings: -Note 1003 delete from `test`.`t1` `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 +Note 1003 delete from `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 # Applicable hint EXPLAIN EXTENDED DELETE /*+ JOIN_PREFIX(t2, t3, ta2) */ FROM ta1.* USING t1 AS ta1 JOIN t1 AS ta2 ON 1 @@ -906,7 +906,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY ta1 eq_ref PRIMARY PRIMARY 4 test.t2.f1 1 100.00 2 MATERIALIZED t3 ALL NULL NULL NULL NULL 3 33.33 Using where Warnings: -Note 1003 delete /*+ JOIN_PREFIX(@`select#1` `t2`,`t3`,`ta2`) */ from `test`.`t1` `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 +Note 1003 delete /*+ JOIN_PREFIX(@`select#1` `t2`,`t3`,`ta2`) */ from `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 # Applicable hint EXPLAIN EXTENDED DELETE /*+ JOIN_PREFIX(t2, t3, ta1, ta2) */ FROM ta1.* USING t1 AS ta1 JOIN t1 AS ta2 ON 1 @@ -919,7 +919,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY ta2 ALL NULL NULL NULL NULL 3 100.00 2 MATERIALIZED t3 ALL NULL NULL NULL NULL 3 33.33 Using where Warnings: -Note 1003 delete /*+ JOIN_PREFIX(@`select#1` `t2`,`t3`,`ta1`,`ta2`) */ from `test`.`t1` `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 +Note 1003 delete /*+ JOIN_PREFIX(@`select#1` `t2`,`t3`,`ta1`,`ta2`) */ from `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 # Applicable hint EXPLAIN EXTENDED DELETE /*+ JOIN_PREFIX(t2, t3, ta2, ta1) */ FROM ta1.* USING t1 AS ta1 JOIN t1 AS ta2 ON 1 @@ -932,7 +932,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY ta1 eq_ref PRIMARY PRIMARY 4 test.t2.f1 1 100.00 2 MATERIALIZED t3 ALL NULL NULL NULL NULL 3 33.33 Using where Warnings: -Note 1003 delete /*+ JOIN_PREFIX(@`select#1` `t2`,`t3`,`ta2`,`ta1`) */ from `test`.`t1` `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 +Note 1003 delete /*+ JOIN_PREFIX(@`select#1` `t2`,`t3`,`ta2`,`ta1`) */ from `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 # Hint should be ignored EXPLAIN EXTENDED DELETE /*+ JOIN_SUFFIX(ta2, t3, ta1) */ FROM ta1.* USING t1 AS ta1 JOIN t1 AS ta2 ON 1 @@ -945,7 +945,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra 1 PRIMARY ta2 ALL NULL NULL NULL NULL 3 100.00 Warnings: Warning 4219 Hint JOIN_SUFFIX(`ta2`,`t3`,`ta1`) is ignored as conflicting/duplicated -Note 1003 delete from `test`.`t1` `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 +Note 1003 delete from `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 # Both hints are ignored EXPLAIN EXTENDED DELETE /*+ JOIN_PREFIX(ta1, t2, t3) JOIN_SUFFIX(t3, ta2) */ FROM ta1.* USING t1 AS ta1 JOIN t1 AS ta2 ON 1 @@ -959,7 +959,7 @@ id select_type table type possible_keys key key_len ref rows filtered Extra Warnings: Warning 4219 Hint JOIN_PREFIX(`ta1`,`t2`,`t3`) is ignored as conflicting/duplicated Warning 4219 Hint JOIN_SUFFIX(`t3`,`ta2`) is ignored as conflicting/duplicated -Note 1003 delete from `test`.`t1` `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 +Note 1003 delete from `ta1` using `test`.`t2` semi join (`test`.`t3`) left join (`test`.`t1` `ta1` join `test`.`t1` `ta2`) on(`test`.`ta1`.`f1` = `test`.`t2`.`f1` and 1 and `test`.`t2`.`f1` is not null) where `test`.`t3`.`f1` = 9 DROP TABLE t1, t2, t3; # Const table behavior, table order is not changed, hint is applicable. # Note: Const tables are excluded from the process of dependency setting diff --git a/mysql-test/suite/federated/federatedx_create_handlers.result b/mysql-test/suite/federated/federatedx_create_handlers.result index 77d080147c440..4ff9814840a39 100644 --- a/mysql-test/suite/federated/federatedx_create_handlers.result +++ b/mysql-test/suite/federated/federatedx_create_handlers.result @@ -1401,43 +1401,9 @@ EXPLAIN { "query_block": { "select_id": 1, - "cost": 1000.010933, - "nested_loop": [ - { - "table": { - "table_name": "p", - "access_type": "ALL", - "loops": 1, - "rows": 2, - "cost": 1000.010388, - "filtered": 100, - "attached_condition": "p.product_id is not null" - } - }, - { - "table": { - "table_name": "", - "access_type": "ref", - "possible_keys": ["key0"], - "key": "key0", - "key_length": "9", - "used_key_parts": ["product_id"], - "ref": ["federated.p.product_id"], - "loops": 2, - "rows": 1, - "cost": 0.000545388, - "filtered": 100, - "materialized": { - "query_block": { - "select_id": 2, - "table": { - "message": "Pushed derived" - } - } - } - } - } - ] + "table": { + "message": "Pushed update" + } } } update @@ -1480,31 +1446,9 @@ EXPLAIN { "query_block": { "select_id": 1, - "cost": 2000.022975, - "nested_loop": [ - { - "table": { - "table_name": "t2", - "access_type": "ALL", - "loops": 1, - "rows": 4, - "cost": 1000.010655, - "filtered": 100, - "attached_condition": "t2.`id` in (1,2)" - } - }, - { - "table": { - "table_name": "t1", - "access_type": "ALL", - "loops": 4, - "rows": 4, - "cost": 1000.012319, - "filtered": 100, - "attached_condition": "t1.`id` = t2.`id`" - } - } - ] + "table": { + "message": "Pushed update" + } } } update federated.t2 inner join federated.t1 on t2.id = t1.id @@ -1534,31 +1478,9 @@ EXPLAIN { "query_block": { "select_id": 1, - "cost": 2000.023025, - "nested_loop": [ - { - "table": { - "table_name": "t2", - "access_type": "ALL", - "loops": 1, - "rows": 4, - "cost": 1000.010705, - "filtered": 100, - "attached_condition": "t2.op = 'updated'" - } - }, - { - "table": { - "table_name": "t1", - "access_type": "ALL", - "loops": 4, - "rows": 4, - "cost": 1000.012319, - "filtered": 100, - "attached_condition": "t1.`id` = t2.`id`" - } - } - ] + "table": { + "message": "Pushed delete" + } } } delete federated.t2 from federated.t2 inner join federated.t1 on t2.id = t1.id and t2.op = 'updated'; diff --git a/mysql-test/suite/federated/federatedx_pushdown_upd_del.result b/mysql-test/suite/federated/federatedx_pushdown_upd_del.result new file mode 100644 index 0000000000000..7c2a077f2c6b1 --- /dev/null +++ b/mysql-test/suite/federated/federatedx_pushdown_upd_del.result @@ -0,0 +1,874 @@ +connect master,127.0.0.1,root,,test,$MASTER_MYPORT,; +connect slave,127.0.0.1,root,,test,$SLAVE_MYPORT,; +connection master; +CREATE DATABASE federated; +connection slave; +CREATE DATABASE federated; +connection default; +ALTER DATABASE federated CHARACTER SET latin1; +set global federated_pushdown=1; +connection slave; +DROP TABLE IF EXISTS federated.t1, federated.t2, federated.t3; +Warnings: +Note 1051 Unknown table 'federated.t1,federated.t2,federated.t3' +CREATE TABLE federated.t1 (a int, b varchar(16)) DEFAULT CHARSET=latin1; +CREATE TABLE federated.t2 (a int, c varchar(16)) DEFAULT CHARSET=latin1; +CREATE TABLE federated.t3 (a int, b int, UNIQUE KEY(b)); +SET @old_log_output= @@global.log_output; +SET @old_general_log= @@global.general_log; +SET global log_output='TABLE'; +connection master; +DROP TABLE IF EXISTS federated.t1, federated.t2, federated.t3; +Warnings: +Note 1051 Unknown table 'federated.t1,federated.t2,federated.t3' +CREATE TABLE federated.t1 (a int, b varchar(16)) +ENGINE="FEDERATED" DEFAULT CHARSET=latin1 +CONNECTION='mysql://root@127.0.0.1:SLAVE_PORT/federated/t1'; +CREATE TABLE federated.t2 (a int, c varchar(16)) +ENGINE="FEDERATED" DEFAULT CHARSET=latin1 +CONNECTION='mysql://root@127.0.0.1:SLAVE_PORT/federated/t2'; +CREATE TABLE federated.t3 (a int, b int, UNIQUE KEY(b)) +ENGINE="FEDERATED" +CONNECTION='mysql://root@127.0.0.1:SLAVE_PORT/federated/t3'; +USE federated; +CREATE PROCEDURE fill_tables() +BEGIN +DELETE FROM t1; +DELETE FROM t2; +DELETE FROM t3; +INSERT INTO t1 VALUES (1,'one'),(2,'two'),(3,'three'),(4,'four'); +INSERT INTO t2 VALUES (1,'I'),(2,'II'),(3,'III'); +INSERT INTO t3 VALUES (1,10),(2,20),(3,30); +END| +# +# ================================================================== +# The statement the remote server receives +# ================================================================== +# +# A pushed down statement is printed back by SELECT_LEX::print() with +# QT_SELECT_ONLY cleared, which produces +# update set where +# for an UPDATE and +# delete from using where +# for a DELETE. Both are understood by the remote server. +# +connection master; +CALL fill_tables(); +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; +connection master; +UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; +UPDATE t1 JOIN t2 ON t1.a= t2.a SET t1.b= CONCAT(t2.c,'!') WHERE t2.a > 2; +UPDATE t1 AS x JOIN t2 AS y ON x.a= y.a SET x.b= y.c WHERE y.a= 1; +UPDATE t1, t2 SET t1.b= 'both', t2.c= 'both' WHERE t1.a= t2.a; +UPDATE t1, t2 SET t1.b= 'sub' WHERE t1.a= t2.a AND +t1.a IN (SELECT a FROM t2 WHERE c= 'both'); +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 3; +DELETE FROM x USING t1 AS x JOIN t2 AS y ON x.a= y.a WHERE y.a= 2; +DELETE t1, t2 FROM t1 JOIN t2 ON t1.a= t2.a; +connection slave; +SET global general_log=0; +SELECT argument FROM mysql.general_log +WHERE argument LIKE 'update %' OR argument LIKE 'delete %'; +argument +update `federated`.`t1` join `federated`.`t2` set `federated`.`t1`.`b` = 'updated' where `federated`.`t1`.`a` = `federated`.`t2`.`a` and `federated`.`t1`.`a` < 3 +update (`federated`.`t1` join `federated`.`t2` on(`federated`.`t1`.`a` = `federated`.`t2`.`a`)) set `federated`.`t1`.`b` = concat(`federated`.`t2`.`c`,'!') where `federated`.`t2`.`a` > 2 +update (`federated`.`t1` `x` join `federated`.`t2` `y` on(`federated`.`x`.`a` = `federated`.`y`.`a`)) set `federated`.`x`.`b` = `federated`.`y`.`c` where `federated`.`y`.`a` = 1 +update `federated`.`t1` join `federated`.`t2` set `federated`.`t1`.`b` = 'both',`federated`.`t2`.`c` = 'both' where `federated`.`t1`.`a` = `federated`.`t2`.`a` +update `federated`.`t1` join `federated`.`t2` set `federated`.`t1`.`b` = 'sub' where `federated`.`t1`.`a` = `federated`.`t2`.`a` and `federated`.`t1`.`a` in (select `federated`.`t2`.`a` from `federated`.`t2` where `federated`.`t2`.`c` = 'both') +delete from `federated`.`t1` using (`federated`.`t1` join `federated`.`t2` on(`federated`.`t1`.`a` = `federated`.`t2`.`a`)) where `federated`.`t1`.`a` = 3 +delete from `x` using (`federated`.`t1` `x` join `federated`.`t2` `y` on(`federated`.`x`.`a` = `federated`.`y`.`a`)) where `federated`.`y`.`a` = 2 +delete from `federated`.`t1`,`federated`.`t2` using (`federated`.`t1` join `federated`.`t2` on(`federated`.`t1`.`a` = `federated`.`t2`.`a`)) +# +# ================================================================== +# Row counts reported by batch_update_delete() +# ================================================================== +# +connection master; +# All matched rows are changed +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; +affected rows: 2 +info: Rows matched: 2 Changed: 2 Warnings: 0 +EXPLAIN UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL +affected rows: 1 +ANALYZE UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; +id select_type table type possible_keys key key_len ref rows r_rows filtered r_filtered Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL +affected rows: 1 +EXPLAIN +WITH cte1 AS (SELECT * FROM t2 WHERE t2.a < 5) +UPDATE t1, cte1 SET t1.b= 'updated' WHERE t1.a= cte1.a AND t1.a < 3; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL +affected rows: 1 +WITH cte1 AS (SELECT * FROM t2 WHERE t2.a < 5) +UPDATE t1, cte1 SET t1.b= 'updated' WHERE t1.a= cte1.a AND t1.a < 3; +affected rows: 0 +info: Rows matched: 2 Changed: 0 Warnings: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 updated +2 updated +3 three +4 four +affected rows: 4 +# explain plans for single table +CALL fill_tables(); +EXPLAIN SELECT * FROM t1 WHERE t1.a < 3; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED SELECT NULL NULL NULL NULL NULL NULL NULL NULL +affected rows: 1 +EXPLAIN UPDATE t1 SET t1.b= 'updated' WHERE t1.a < 3; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 4 Using where +affected rows: 1 +ANALYZE UPDATE t1 SET t1.b= 'updated' WHERE t1.a < 3; +id select_type table type possible_keys key key_len ref rows r_rows filtered r_filtered Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 4 4.00 100.00 50.00 Using where +affected rows: 1 +UPDATE t1 SET t1.b= 'updated' WHERE t1.a < 3; +affected rows: 0 +info: Rows matched: 2 Changed: 0 Warnings: 0 +SELECT * FROM t1; +a b +1 updated +2 updated +3 three +4 four +affected rows: 4 +EXPLAIN +WITH cte1 AS (SELECT * FROM t2 WHERE t2.a < 5) +UPDATE cte1 SET cte1.a = cte1.a * 2 ; +ERROR HY000: The target table cte1 of the UPDATE is not updatable +WITH cte1 AS (SELECT * FROM t2 WHERE t2.a < 5) +UPDATE cte1 SET cte1.a = cte1.a * 2 ; +ERROR HY000: The target table cte1 of the UPDATE is not updatable +SELECT * FROM t2; +a c +1 I +2 II +3 III +affected rows: 3 +# A matched row that keeps its value is counted as matched but not as +# changed. The count comes from federatedx_io_mysql::matched_rows(), +# which parses it out of the remote server's info string +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'two' WHERE t1.a= t2.a AND t1.a < 3; +affected rows: 1 +info: Rows matched: 2 Changed: 1 Warnings: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 two +2 two +3 three +4 four +# Two tables updated at once +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'upd1', t2.c= 'upd2' WHERE t1.a= t2.a; +affected rows: 6 +info: Rows matched: 6 Changed: 6 Warnings: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 upd1 +2 upd1 +3 upd1 +4 four +SELECT * FROM t2 ORDER BY a; +a c +1 upd2 +2 upd2 +3 upd2 +# No row matches +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'nope' WHERE t1.a= t2.a AND t1.a > 100; +affected rows: 0 +info: Rows matched: 0 Changed: 0 Warnings: 0 +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a > 100; +affected rows: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 one +2 two +3 three +4 four +# For a DELETE the remote server reports no matched count, so +# matched_rows() falls back to the number of deleted rows +CALL fill_tables(); +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t2.c <> 'II'; +affected rows: 2 +SELECT * FROM t1 ORDER BY a; +a b +2 two +4 four +# Two tables deleted from at once +CALL fill_tables(); +DELETE t1, t2 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 2; +affected rows: 2 +SELECT * FROM t1 ORDER BY a; +a b +1 one +3 three +4 four +SELECT * FROM t2 ORDER BY a; +a c +1 I +3 III +# Aliased delete target +CALL fill_tables(); +DELETE FROM x USING t1 AS x JOIN t2 AS y ON x.a= y.a WHERE y.a= 2; +affected rows: 1 +SELECT * FROM t1 ORDER BY a; +a b +1 one +3 three +4 four +# +# ================================================================== +# An error on the remote server is reported to the client +# ================================================================== +# +# The remote server executed the whole statement, so the error it +# reports is the error of this statement. Its error code and SQLSTATE +# are passed through rather than hidden behind the generic +# ER_QUERY_ON_FOREIGN_DATA_SOURCE, so the client sees the same error it +# would have seen had the statement been executed locally +# +CALL fill_tables(); +# A division by zero in the WHERE clause. Compare with the same error +# raised locally by a single-table UPDATE, which is never pushed down +# through the select handler +UPDATE t1 SET t1.b= 'x' WHERE t1.a < 3/0; +ERROR 22012: Division by 0 +UPDATE t1, t2 SET t1.b= 'x' WHERE t1.a= t2.a AND t1.a < 3/0; +ERROR 22012: Division by 0 +# A division by zero in the SET clause +UPDATE t1, t2 SET t1.a= 1/0 WHERE t1.a= t2.a; +ERROR 22012: Division by 0 +# A duplicate key +UPDATE t3, t2 SET t3.b= 30 WHERE t3.a= t2.a AND t3.a < 3; +ERROR 23000: Duplicate entry '30' for key 'b' +# An error raised by a trigger of the remote server, reported for a +# DELETE the same way +connection slave; +CREATE TRIGGER federated.t1_bd BEFORE DELETE ON federated.t1 +FOR EACH ROW SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT= 'deleting is not allowed here'; +connection master; +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 1; +ERROR HY000: deleting is not allowed here +connection slave; +DROP TRIGGER federated.t1_bd; +connection master; +# Nothing was modified by any of the failed statements +SELECT * FROM t1 ORDER BY a; +a b +1 one +2 two +3 three +4 four +SELECT * FROM t3 ORDER BY a; +a b +1 10 +2 20 +3 30 +# +# ================================================================== +# A warning on the remote server does not reach the client +# ================================================================== +# +# Unlike an error, a warning raised by the remote server is lost. +# batch_update_delete() reports only the two row counts to the SQL layer +# and FederatedX does not read the remote warnings, so nothing carries a +# condition of the remote statement back to the client: the statement +# succeeds, the row counts and the data are the ones the remote server +# produced, but SHOW WARNINGS is empty and the "Warnings:" of the OK +# packet stays 0 (it is thd->cuted_fields, i.e. the local count). +# +# This is not specific to the pushdown: FederatedX has no code that +# reads the diagnostics of the remote statement, so the row-by-row +# execution loses a warning of the remote server just the same, see the +# end of this section. +# +# To get warnings where the previous section got errors, the strict mode +# is turned off on the remote server, keeping ERROR_FOR_DIVISION_BY_ZERO +# (which on its own raises a warning, not an error). FederatedX caches +# its connections in FEDERATEDX_SERVER, and an open connection keeps +# the sql_mode it inherited from the remote global one when it was +# opened, so the tables have to be closed (which closes the connections +# with them) for the next statement to open a connection that runs in +# the new mode +# +connection slave; +SET @old_slave_sql_mode= @@global.sql_mode; +SET global sql_mode='ERROR_FOR_DIVISION_BY_ZERO'; +connection master; +SET @old_local_sql_mode= @@session.sql_mode; +FLUSH TABLES; +# A division by zero in the WHERE clause: the remote server warns and +# evaluates the condition to NULL, so no row matches +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.b= 'x' WHERE t1.a= t2.a AND t1.a < 3/0; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL +UPDATE t1, t2 SET t1.b= 'x' WHERE t1.a= t2.a AND t1.a < 3/0; +affected rows: 0 +info: Rows matched: 0 Changed: 0 Warnings: 0 +SHOW WARNINGS; +Level Code Message +SELECT * FROM t1 ORDER BY a; +a b +1 one +2 two +3 three +4 four +# A division by zero in the SET clause: the remote server warns and +# stores NULL +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.a= 1/0 WHERE t1.a= t2.a; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL +UPDATE t1, t2 SET t1.a= 1/0 WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 0 +SHOW WARNINGS; +Level Code Message +SELECT * FROM t1 ORDER BY a; +a b +NULL one +NULL two +NULL three +4 four +# The warnings the remote server raised are visible only there: the same +# statement executed on the remote server directly reports them. The +# session of this connection was opened before the mode was changed, so +# it is set explicitly here, the way a new connection would inherit it +CALL fill_tables(); +connection slave; +SET @old_slave_session_sql_mode= @@session.sql_mode; +SET session sql_mode= @@global.sql_mode; +UPDATE federated.t1, federated.t2 SET t1.a= 1/0 WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 0 +Warnings: +Warning 1365 Division by 0 +Warning 1365 Division by 0 +Warning 1365 Division by 0 +SHOW WARNINGS; +Level Code Message +Warning 1365 Division by 0 +Warning 1365 Division by 0 +Warning 1365 Division by 0 +SET session sql_mode= @old_slave_session_sql_mode; +connection master; +# A value too long for the column: the remote server warns and truncates +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.b= 'much too long a value' WHERE t1.a= t2.a; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL +UPDATE t1, t2 SET t1.b= 'much too long a value' WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 0 +SHOW WARNINGS; +Level Code Message +SELECT * FROM t1 ORDER BY a; +a b +1 much too long a +2 much too long a +3 much too long a +4 four +# A value of a wrong type: the remote server warns and stores 0 +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.a= 'not a number' WHERE t1.a= t2.a; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL +UPDATE t1, t2 SET t1.a= 'not a number' WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 0 +SHOW WARNINGS; +Level Code Message +SELECT * FROM t1 ORDER BY a; +a b +0 one +0 two +0 three +4 four +# A warning raised by a trigger of the remote server, for an UPDATE and +# for a DELETE. Compare with the trigger of the previous section, which +# signals SQLSTATE 45000 and whose error does reach the client +connection slave; +CREATE TRIGGER federated.t1_bu BEFORE UPDATE ON federated.t1 +FOR EACH ROW SIGNAL SQLSTATE '01000' + SET MESSAGE_TEXT= 'updating here is deprecated'; +CREATE TRIGGER federated.t1_bd BEFORE DELETE ON federated.t1 +FOR EACH ROW SIGNAL SQLSTATE '01000' + SET MESSAGE_TEXT= 'deleting here is deprecated'; +connection master; +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.b= 'trigger' WHERE t1.a= t2.a; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL +UPDATE t1, t2 SET t1.b= 'trigger' WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 0 +SHOW WARNINGS; +Level Code Message +SELECT * FROM t1 ORDER BY a; +a b +1 trigger +2 trigger +3 trigger +4 four +EXPLAIN DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a < 3; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED DELETE NULL NULL NULL NULL NULL NULL NULL NULL +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a < 3; +affected rows: 2 +SHOW WARNINGS; +Level Code Message +SELECT * FROM t1 ORDER BY a; +a b +3 trigger +4 four +connection slave; +DROP TRIGGER federated.t1_bu; +DROP TRIGGER federated.t1_bd; +connection master; +# A duplicate key stays an error even in a non-strict mode, it is not +# one of the errors sql_mode downgrades to a warning. Only IGNORE would +# make the remote server skip the row, and an IGNORE statement is not +# pushed down at all, see the section on the statements that stay local +CALL fill_tables(); +UPDATE t3, t2 SET t3.b= 30 WHERE t3.a= t2.a AND t3.a < 3; +ERROR 23000: Duplicate entry '30' for key 'b' +SELECT * FROM t3 ORDER BY a; +a b +1 10 +2 20 +3 30 +# Without the pushdown the SET expressions are evaluated by the local +# server, so the same statements are decided by the local sql_mode and +# their warnings are the local ones and are reported. A warning of the +# remote server is still not reported: the trigger below raises three of +# them and none is seen, exactly as in the pushed down case above +connection default; +set global federated_pushdown=0; +connection master; +SET session sql_mode='ERROR_FOR_DIVISION_BY_ZERO'; +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'x' WHERE t1.a= t2.a AND t1.a < 3/0; +affected rows: 0 +info: Rows matched: 0 Changed: 0 Warnings: 0 +Warnings: +Warning 1365 Division by 0 +SHOW WARNINGS; +Level Code Message +Warning 1365 Division by 0 +SELECT * FROM t1 ORDER BY a; +a b +1 one +2 two +3 three +4 four +CALL fill_tables(); +UPDATE t1, t2 SET t1.a= 1/0 WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 0 +Warnings: +Warning 1365 Division by 0 +Warning 1365 Division by 0 +Warning 1365 Division by 0 +SHOW WARNINGS; +Level Code Message +Warning 1365 Division by 0 +Warning 1365 Division by 0 +Warning 1365 Division by 0 +SELECT * FROM t1 ORDER BY a; +a b +NULL one +NULL two +NULL three +4 four +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'much too long a value' WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 3 +Warnings: +Warning 1265 Data truncated for column 'b' at row 1 +Warning 1265 Data truncated for column 'b' at row 2 +Warning 1265 Data truncated for column 'b' at row 3 +SHOW WARNINGS; +Level Code Message +Warning 1265 Data truncated for column 'b' at row 1 +Warning 1265 Data truncated for column 'b' at row 2 +Warning 1265 Data truncated for column 'b' at row 3 +SELECT * FROM t1 ORDER BY a; +a b +1 much too long a +2 much too long a +3 much too long a +4 four +CALL fill_tables(); +UPDATE t1, t2 SET t1.a= 'not a number' WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 0 +Warnings: +Warning 1366 Incorrect integer value: 'not a number' for column ``.`(temporary)`.`a` at row 1 +Warning 1366 Incorrect integer value: 'not a number' for column ``.`(temporary)`.`a` at row 2 +Warning 1366 Incorrect integer value: 'not a number' for column ``.`(temporary)`.`a` at row 3 +SHOW WARNINGS; +Level Code Message +Warning 1366 Incorrect integer value: 'not a number' for column ``.`(temporary)`.`a` at row 1 +Warning 1366 Incorrect integer value: 'not a number' for column ``.`(temporary)`.`a` at row 2 +Warning 1366 Incorrect integer value: 'not a number' for column ``.`(temporary)`.`a` at row 3 +SELECT * FROM t1 ORDER BY a; +a b +0 one +0 two +0 three +4 four +connection slave; +CREATE TRIGGER federated.t1_bu BEFORE UPDATE ON federated.t1 +FOR EACH ROW SIGNAL SQLSTATE '01000' + SET MESSAGE_TEXT= 'updating here is deprecated'; +connection master; +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'trigger' WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 0 +SHOW WARNINGS; +Level Code Message +SELECT * FROM t1 ORDER BY a; +a b +1 trigger +2 trigger +3 trigger +4 four +connection slave; +DROP TRIGGER federated.t1_bu; +# Restore the strict mode on both servers and reopen the connections +SET global sql_mode= @old_slave_sql_mode; +connection default; +set global federated_pushdown=1; +connection master; +SET session sql_mode= @old_local_sql_mode; +FLUSH TABLES; +# +# ================================================================== +# Inside a transaction the statement is not auto-committed by itself +# ================================================================== +# +CALL fill_tables(); +BEGIN; +UPDATE t1, t2 SET t1.b= 'in trx' WHERE t1.a= t2.a; +affected rows: 3 +info: Rows matched: 3 Changed: 3 Warnings: 0 +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 3; +affected rows: 1 +COMMIT; +SELECT * FROM t1 ORDER BY a; +a b +1 in trx +2 in trx +4 four +# +# ================================================================== +# EXPLAIN and ANALYZE report the pushdown +# ================================================================== +# +# A statement taken over by the select handler has no local query plan, +# it is reported as PUSHED UPDATE / PUSHED DELETE, the way a +# pushed down SELECT is reported as PUSHED SELECT +# +# EXPLAIN does not execute the statement +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.b= 'explained' WHERE t1.a= t2.a; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL +EXPLAIN FORMAT=JSON UPDATE t1, t2 SET t1.b= 'explained' WHERE t1.a= t2.a; +EXPLAIN +{ + "query_block": { + "select_id": 1, + "table": { + "message": "Pushed update" + } + } +} +EXPLAIN DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a; +id select_type table type possible_keys key key_len ref rows Extra +1 PUSHED DELETE NULL NULL NULL NULL NULL NULL NULL NULL +SELECT * FROM t1 ORDER BY a; +a b +1 one +2 two +3 three +4 four +# ANALYZE does execute it, and the whole statement still goes to the +# remote server in one piece +CALL fill_tables(); +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; +connection master; +ANALYZE UPDATE t1, t2 SET t1.b= 'analyzed' WHERE t1.a= t2.a AND t1.a= 1; +id select_type table type possible_keys key key_len ref rows r_rows filtered r_filtered Extra +1 PUSHED UPDATE NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL NULL +connection slave; +SET global general_log=0; +SELECT argument FROM mysql.general_log WHERE argument LIKE 'update %'; +argument +update `federated`.`t1` join `federated`.`t2` set `federated`.`t1`.`b` = 'analyzed' where `federated`.`t1`.`a` = `federated`.`t2`.`a` and `federated`.`t1`.`a` = 1 +connection master; +SELECT * FROM t1 ORDER BY a; +a b +1 analyzed +2 two +3 three +4 four +# +# ================================================================== +# Statements that are not pushed down +# ================================================================== +# +# EXPLAIN shows the local query plan for them, so it tells a pushed +# down statement from one that is executed locally +# +# IGNORE is not printed by SELECT_LEX::print(), so an IGNORE statement +# is executed locally and keeps ignoring the duplicate key errors +CALL fill_tables(); +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; +connection master; +EXPLAIN UPDATE IGNORE t3, t2 SET t3.b= 30 WHERE t3.a= t2.a AND t3.a < 3; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t3 ALL NULL NULL NULL NULL 3 Using where +1 SIMPLE t2 ALL NULL NULL NULL NULL 3 Using where +UPDATE IGNORE t3, t2 SET t3.b= 30 WHERE t3.a= t2.a AND t3.a < 3; +SELECT * FROM t3 ORDER BY a; +a b +1 10 +2 20 +3 30 +connection slave; +SET global general_log=0; +SELECT argument FROM mysql.general_log WHERE argument LIKE 'update %'; +argument +UPDATE IGNORE `t3` SET `b` = 30 WHERE `a` = 1 AND `b` = 10 LIMIT 1 +UPDATE IGNORE `t3` SET `b` = 30 WHERE `a` = 2 AND `b` = 20 LIMIT 1 +# A statement that mixes engines has no FederatedX-only table list +connection master; +CREATE TABLE t4 (a int, d varchar(16)) ENGINE=MyISAM; +INSERT INTO t4 VALUES (1,'x'),(2,'y'); +CALL fill_tables(); +EXPLAIN UPDATE t1, t4 SET t1.b= t4.d WHERE t1.a= t4.a; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t4 ALL NULL NULL NULL NULL 2 +1 SIMPLE t1 ALL NULL NULL NULL NULL 4 Using where +UPDATE t1, t4 SET t1.b= t4.d WHERE t1.a= t4.a; +affected rows: 2 +info: Rows matched: 2 Changed: 2 Warnings: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 x +2 y +3 three +4 four +DROP TABLE t4; +# A merged VIEW is not pushed down. get_fed_table_for_pushdown() walks +# SELECT_LEX::join->tables_list, where a merged VIEW is still present as +# its own TABLE_LIST carrying the view's internal temporary table. That +# table's handler is not FederatedX, so the check that every table +# belongs to the same FederatedX server fails and the whole statement is +# executed locally, row by row. +CREATE VIEW v1 AS SELECT * FROM t1; +CREATE VIEW v2 AS SELECT * FROM v1; +CALL fill_tables(); +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; +connection master; +# VIEW as the UPDATE/DELETE target +EXPLAIN UPDATE v1, t2 SET v1.b= 'view' WHERE v1.a= t2.a AND v1.a < 3; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 ALL NULL NULL NULL NULL 3 Using where +1 SIMPLE t1 ALL NULL NULL NULL NULL 4 Using where +UPDATE v1, t2 SET v1.b= 'view' WHERE v1.a= t2.a AND v1.a < 3; +affected rows: 2 +info: Rows matched: 2 Changed: 2 Warnings: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 view +2 view +3 three +4 four +# VIEW only as a source, base table as the target +EXPLAIN UPDATE t1, v1 SET t1.b= 'src' WHERE t1.a= v1.a AND v1.a= 4; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 4 Using where +1 SIMPLE t1 ALL NULL NULL NULL NULL 4 Using where +UPDATE t1, v1 SET t1.b= 'src' WHERE t1.a= v1.a AND v1.a= 4; +affected rows: 1 +info: Rows matched: 1 Changed: 1 Warnings: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 view +2 view +3 three +4 src +# Nested VIEW as the DELETE target +EXPLAIN DELETE v2 FROM v2, t2 WHERE v2.a= t2.a AND v2.a= 1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t2 ALL NULL NULL NULL NULL 3 Using where +1 SIMPLE t1 ALL NULL NULL NULL NULL 4 Using where +DELETE v2 FROM v2, t2 WHERE v2.a= t2.a AND v2.a= 1; +affected rows: 1 +SELECT * FROM t1 ORDER BY a; +a b +2 view +3 three +4 src +connection slave; +SET global general_log=0; +# Row-by-row execution: several statements, not one pushed down join +SELECT argument FROM mysql.general_log +WHERE argument LIKE 'UPDATE %' OR argument LIKE 'DELETE %'; +argument +UPDATE `t1` SET `b` = 'view' WHERE `a` = 1 AND `b` = 'one' LIMIT 1 +UPDATE `t1` SET `b` = 'view' WHERE `a` = 2 AND `b` = 'two' LIMIT 1 +UPDATE `t1` SET `b` = 'src' WHERE `a` = 4 AND `b` = 'four' LIMIT 1 +DELETE FROM `t1` WHERE `a` = 1 AND `b` = 'view' LIMIT 1 +connection master; +DROP VIEW v1, v2; +# A single-table UPDATE/DELETE does not go through the select handler +# at all, it is pushed down by direct_update_rows()/direct_delete_rows(), +# a decision taken at execution time. So its plan is the ordinary one +CALL fill_tables(); +EXPLAIN UPDATE t1 SET b= 'single' WHERE a= 1; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 4 Using where +EXPLAIN DELETE FROM t1 WHERE a= 4; +id select_type table type possible_keys key key_len ref rows Extra +1 SIMPLE t1 ALL NULL NULL NULL NULL 4 Using where +UPDATE t1 SET b= 'single' WHERE a= 1; +affected rows: 1 +info: Rows matched: 1 Changed: 1 Warnings: 0 +DELETE FROM t1 WHERE a= 4; +affected rows: 1 +SELECT * FROM t1 ORDER BY a; +a b +1 single +2 two +3 three +# +# ================================================================== +# Without pushdown the same statements give the same results +# ================================================================== +# +connection default; +set global federated_pushdown=0; +connection master; +CALL fill_tables(); +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; +connection master; +UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; +affected rows: 2 +info: Rows matched: 2 Changed: 2 Warnings: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 updated +2 updated +3 three +4 four +connection slave; +SET global general_log=0; +SELECT argument FROM mysql.general_log WHERE argument LIKE 'update %'; +argument +UPDATE `t1` SET `b` = 'updated' WHERE `a` = 1 AND `b` = 'one' LIMIT 1 +UPDATE `t1` SET `b` = 'updated' WHERE `a` = 2 AND `b` = 'two' LIMIT 1 +connection master; +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'two' WHERE t1.a= t2.a AND t1.a < 3; +affected rows: 1 +info: Rows matched: 2 Changed: 1 Warnings: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 two +2 two +3 three +4 four +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'upd1', t2.c= 'upd2' WHERE t1.a= t2.a; +affected rows: 6 +info: Rows matched: 6 Changed: 6 Warnings: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 upd1 +2 upd1 +3 upd1 +4 four +SELECT * FROM t2 ORDER BY a; +a c +1 upd2 +2 upd2 +3 upd2 +CALL fill_tables(); +UPDATE t1, t2 SET t1.b= 'nope' WHERE t1.a= t2.a AND t1.a > 100; +affected rows: 0 +info: Rows matched: 0 Changed: 0 Warnings: 0 +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a > 100; +affected rows: 0 +SELECT * FROM t1 ORDER BY a; +a b +1 one +2 two +3 three +4 four +CALL fill_tables(); +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t2.c <> 'II'; +affected rows: 2 +SELECT * FROM t1 ORDER BY a; +a b +2 two +4 four +CALL fill_tables(); +DELETE t1, t2 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 2; +affected rows: 2 +SELECT * FROM t1 ORDER BY a; +a b +1 one +3 three +4 four +SELECT * FROM t2 ORDER BY a; +a c +1 I +3 III +CALL fill_tables(); +DELETE FROM x USING t1 AS x JOIN t2 AS y ON x.a= y.a WHERE y.a= 2; +affected rows: 1 +SELECT * FROM t1 ORDER BY a; +a b +1 one +3 three +4 four +DROP PROCEDURE fill_tables; +DROP TABLE federated.t1, federated.t2, federated.t3; +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log= @old_general_log; +SET global log_output= @old_log_output; +DROP TABLE federated.t1, federated.t2, federated.t3; +connection default; +connection master; +DROP TABLE IF EXISTS federated.t1; +DROP DATABASE IF EXISTS federated; +connection slave; +DROP TABLE IF EXISTS federated.t1; +DROP DATABASE IF EXISTS federated; +# End of 13.2 tests diff --git a/mysql-test/suite/federated/federatedx_pushdown_upd_del.test b/mysql-test/suite/federated/federatedx_pushdown_upd_del.test new file mode 100644 index 0000000000000..f522093832d45 --- /dev/null +++ b/mysql-test/suite/federated/federatedx_pushdown_upd_del.test @@ -0,0 +1,662 @@ +# +# Pushdown of multi-table UPDATE/DELETE into the FederatedX engine. +# +# There is no "primary" handler object for a multi-table UPDATE/DELETE, so +# handler::direct_update_rows_init()/direct_delete_rows_init() cannot be used +# for them. Instead the whole statement is pushed down through the +# select_handler interface: create_federatedx_select_handler() builds a +# handler of type select_handler_type::UPDATE_DELETE, which prints the whole +# statement back and executes it on the remote server in +# ha_federatedx_select_handler::batch_update_delete(). +# +# The remote server's general log is used throughout to tell a pushed down +# statement (a single statement, printed by SELECT_LEX::print(), so lowercase) +# from the ordinary row-by-row execution (several "UPDATE ... LIMIT 1" +# statements sent by the ha_federatedx handler methods). +# +--source have_federatedx.inc +--source include/federated.inc +--source include/no_valgrind_without_big.inc + +connection default; +ALTER DATABASE federated CHARACTER SET latin1; +set global federated_pushdown=1; + +connection slave; +DROP TABLE IF EXISTS federated.t1, federated.t2, federated.t3; +CREATE TABLE federated.t1 (a int, b varchar(16)) DEFAULT CHARSET=latin1; +CREATE TABLE federated.t2 (a int, c varchar(16)) DEFAULT CHARSET=latin1; +CREATE TABLE federated.t3 (a int, b int, UNIQUE KEY(b)); +SET @old_log_output= @@global.log_output; +SET @old_general_log= @@global.general_log; +SET global log_output='TABLE'; + +connection master; +DROP TABLE IF EXISTS federated.t1, federated.t2, federated.t3; +--replace_result $SLAVE_MYPORT SLAVE_PORT +eval +CREATE TABLE federated.t1 (a int, b varchar(16)) +ENGINE="FEDERATED" DEFAULT CHARSET=latin1 +CONNECTION='mysql://root@127.0.0.1:$SLAVE_MYPORT/federated/t1'; + +--replace_result $SLAVE_MYPORT SLAVE_PORT +eval +CREATE TABLE federated.t2 (a int, c varchar(16)) +ENGINE="FEDERATED" DEFAULT CHARSET=latin1 +CONNECTION='mysql://root@127.0.0.1:$SLAVE_MYPORT/federated/t2'; + +--replace_result $SLAVE_MYPORT SLAVE_PORT +eval +CREATE TABLE federated.t3 (a int, b int, UNIQUE KEY(b)) +ENGINE="FEDERATED" +CONNECTION='mysql://root@127.0.0.1:$SLAVE_MYPORT/federated/t3'; + +USE federated; + +delimiter |; +CREATE PROCEDURE fill_tables() +BEGIN + DELETE FROM t1; + DELETE FROM t2; + DELETE FROM t3; + INSERT INTO t1 VALUES (1,'one'),(2,'two'),(3,'three'),(4,'four'); + INSERT INTO t2 VALUES (1,'I'),(2,'II'),(3,'III'); + INSERT INTO t3 VALUES (1,10),(2,20),(3,30); +END| +delimiter ;| + +--echo # +--echo # ================================================================== +--echo # The statement the remote server receives +--echo # ================================================================== +--echo # +--echo # A pushed down statement is printed back by SELECT_LEX::print() with +--echo # QT_SELECT_ONLY cleared, which produces +--echo # update set where +--echo # for an UPDATE and +--echo # delete from using where +--echo # for a DELETE. Both are understood by the remote server. +--echo # + +connection master; +CALL fill_tables(); + +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; + +connection master; +UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; +UPDATE t1 JOIN t2 ON t1.a= t2.a SET t1.b= CONCAT(t2.c,'!') WHERE t2.a > 2; +UPDATE t1 AS x JOIN t2 AS y ON x.a= y.a SET x.b= y.c WHERE y.a= 1; +UPDATE t1, t2 SET t1.b= 'both', t2.c= 'both' WHERE t1.a= t2.a; +UPDATE t1, t2 SET t1.b= 'sub' WHERE t1.a= t2.a AND + t1.a IN (SELECT a FROM t2 WHERE c= 'both'); +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 3; +DELETE FROM x USING t1 AS x JOIN t2 AS y ON x.a= y.a WHERE y.a= 2; +DELETE t1, t2 FROM t1 JOIN t2 ON t1.a= t2.a; + +connection slave; +SET global general_log=0; +SELECT argument FROM mysql.general_log + WHERE argument LIKE 'update %' OR argument LIKE 'delete %'; + +--echo # +--echo # ================================================================== +--echo # Row counts reported by batch_update_delete() +--echo # ================================================================== +--echo # + +connection master; +--echo # All matched rows are changed +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; +EXPLAIN UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; +ANALYZE UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; + +EXPLAIN +WITH cte1 AS (SELECT * FROM t2 WHERE t2.a < 5) +UPDATE t1, cte1 SET t1.b= 'updated' WHERE t1.a= cte1.a AND t1.a < 3; + +WITH cte1 AS (SELECT * FROM t2 WHERE t2.a < 5) +UPDATE t1, cte1 SET t1.b= 'updated' WHERE t1.a= cte1.a AND t1.a < 3; + +SELECT * FROM t1 ORDER BY a; +--disable_info + +--echo # explain plans for single table +CALL fill_tables(); +--enable_info +EXPLAIN SELECT * FROM t1 WHERE t1.a < 3; + +EXPLAIN UPDATE t1 SET t1.b= 'updated' WHERE t1.a < 3; +ANALYZE UPDATE t1 SET t1.b= 'updated' WHERE t1.a < 3; +UPDATE t1 SET t1.b= 'updated' WHERE t1.a < 3; + +SELECT * FROM t1; + +# A CTE is not an updatable target, this has nothing to do with the pushdown +--error ER_NON_UPDATABLE_TABLE +EXPLAIN +WITH cte1 AS (SELECT * FROM t2 WHERE t2.a < 5) +UPDATE cte1 SET cte1.a = cte1.a * 2 ; + +--error ER_NON_UPDATABLE_TABLE +WITH cte1 AS (SELECT * FROM t2 WHERE t2.a < 5) +UPDATE cte1 SET cte1.a = cte1.a * 2 ; + +SELECT * FROM t2; +--disable_info + +--echo # A matched row that keeps its value is counted as matched but not as +--echo # changed. The count comes from federatedx_io_mysql::matched_rows(), +--echo # which parses it out of the remote server's info string +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'two' WHERE t1.a= t2.a AND t1.a < 3; +--disable_info +SELECT * FROM t1 ORDER BY a; + +--echo # Two tables updated at once +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'upd1', t2.c= 'upd2' WHERE t1.a= t2.a; +--disable_info +SELECT * FROM t1 ORDER BY a; +SELECT * FROM t2 ORDER BY a; + +--echo # No row matches +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'nope' WHERE t1.a= t2.a AND t1.a > 100; +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a > 100; +--disable_info +SELECT * FROM t1 ORDER BY a; + +--echo # For a DELETE the remote server reports no matched count, so +--echo # matched_rows() falls back to the number of deleted rows +CALL fill_tables(); +--enable_info +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t2.c <> 'II'; +--disable_info +SELECT * FROM t1 ORDER BY a; + +--echo # Two tables deleted from at once +CALL fill_tables(); +--enable_info +DELETE t1, t2 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 2; +--disable_info +SELECT * FROM t1 ORDER BY a; +SELECT * FROM t2 ORDER BY a; + +--echo # Aliased delete target +CALL fill_tables(); +--enable_info +DELETE FROM x USING t1 AS x JOIN t2 AS y ON x.a= y.a WHERE y.a= 2; +--disable_info +SELECT * FROM t1 ORDER BY a; + +--echo # +--echo # ================================================================== +--echo # An error on the remote server is reported to the client +--echo # ================================================================== +--echo # +--echo # The remote server executed the whole statement, so the error it +--echo # reports is the error of this statement. Its error code and SQLSTATE +--echo # are passed through rather than hidden behind the generic +--echo # ER_QUERY_ON_FOREIGN_DATA_SOURCE, so the client sees the same error it +--echo # would have seen had the statement been executed locally +--echo # +CALL fill_tables(); + +--echo # A division by zero in the WHERE clause. Compare with the same error +--echo # raised locally by a single-table UPDATE, which is never pushed down +--echo # through the select handler +--error ER_DIVISION_BY_ZERO +UPDATE t1 SET t1.b= 'x' WHERE t1.a < 3/0; +--error ER_DIVISION_BY_ZERO +UPDATE t1, t2 SET t1.b= 'x' WHERE t1.a= t2.a AND t1.a < 3/0; + +--echo # A division by zero in the SET clause +--error ER_DIVISION_BY_ZERO +UPDATE t1, t2 SET t1.a= 1/0 WHERE t1.a= t2.a; + +--echo # A duplicate key +--error ER_DUP_ENTRY +UPDATE t3, t2 SET t3.b= 30 WHERE t3.a= t2.a AND t3.a < 3; + +--echo # An error raised by a trigger of the remote server, reported for a +--echo # DELETE the same way +connection slave; +CREATE TRIGGER federated.t1_bd BEFORE DELETE ON federated.t1 + FOR EACH ROW SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT= 'deleting is not allowed here'; + +connection master; +--error ER_SIGNAL_EXCEPTION +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 1; + +connection slave; +DROP TRIGGER federated.t1_bd; + +connection master; +--echo # Nothing was modified by any of the failed statements +SELECT * FROM t1 ORDER BY a; +SELECT * FROM t3 ORDER BY a; + +--echo # +--echo # ================================================================== +--echo # A warning on the remote server does not reach the client +--echo # ================================================================== +--echo # +--echo # Unlike an error, a warning raised by the remote server is lost. +--echo # batch_update_delete() reports only the two row counts to the SQL layer +--echo # and FederatedX does not read the remote warnings, so nothing carries a +--echo # condition of the remote statement back to the client: the statement +--echo # succeeds, the row counts and the data are the ones the remote server +--echo # produced, but SHOW WARNINGS is empty and the "Warnings:" of the OK +--echo # packet stays 0 (it is thd->cuted_fields, i.e. the local count). +--echo # +--echo # This is not specific to the pushdown: FederatedX has no code that +--echo # reads the diagnostics of the remote statement, so the row-by-row +--echo # execution loses a warning of the remote server just the same, see the +--echo # end of this section. +--echo # +--echo # To get warnings where the previous section got errors, the strict mode +--echo # is turned off on the remote server, keeping ERROR_FOR_DIVISION_BY_ZERO +--echo # (which on its own raises a warning, not an error). FederatedX caches +--echo # its connections in FEDERATEDX_SERVER, and an open connection keeps +--echo # the sql_mode it inherited from the remote global one when it was +--echo # opened, so the tables have to be closed (which closes the connections +--echo # with them) for the next statement to open a connection that runs in +--echo # the new mode +--echo # +connection slave; +SET @old_slave_sql_mode= @@global.sql_mode; +SET global sql_mode='ERROR_FOR_DIVISION_BY_ZERO'; + +connection master; +SET @old_local_sql_mode= @@session.sql_mode; +FLUSH TABLES; + +--echo # A division by zero in the WHERE clause: the remote server warns and +--echo # evaluates the condition to NULL, so no row matches +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.b= 'x' WHERE t1.a= t2.a AND t1.a < 3/0; +--enable_info +UPDATE t1, t2 SET t1.b= 'x' WHERE t1.a= t2.a AND t1.a < 3/0; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +--echo # A division by zero in the SET clause: the remote server warns and +--echo # stores NULL +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.a= 1/0 WHERE t1.a= t2.a; +--enable_info +UPDATE t1, t2 SET t1.a= 1/0 WHERE t1.a= t2.a; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +--echo # The warnings the remote server raised are visible only there: the same +--echo # statement executed on the remote server directly reports them. The +--echo # session of this connection was opened before the mode was changed, so +--echo # it is set explicitly here, the way a new connection would inherit it +CALL fill_tables(); + +connection slave; +SET @old_slave_session_sql_mode= @@session.sql_mode; +SET session sql_mode= @@global.sql_mode; +--enable_info +UPDATE federated.t1, federated.t2 SET t1.a= 1/0 WHERE t1.a= t2.a; +--disable_info +SHOW WARNINGS; +SET session sql_mode= @old_slave_session_sql_mode; + +connection master; +--echo # A value too long for the column: the remote server warns and truncates +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.b= 'much too long a value' WHERE t1.a= t2.a; +--enable_info +UPDATE t1, t2 SET t1.b= 'much too long a value' WHERE t1.a= t2.a; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +--echo # A value of a wrong type: the remote server warns and stores 0 +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.a= 'not a number' WHERE t1.a= t2.a; +--enable_info +UPDATE t1, t2 SET t1.a= 'not a number' WHERE t1.a= t2.a; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +--echo # A warning raised by a trigger of the remote server, for an UPDATE and +--echo # for a DELETE. Compare with the trigger of the previous section, which +--echo # signals SQLSTATE 45000 and whose error does reach the client +connection slave; +CREATE TRIGGER federated.t1_bu BEFORE UPDATE ON federated.t1 + FOR EACH ROW SIGNAL SQLSTATE '01000' + SET MESSAGE_TEXT= 'updating here is deprecated'; +CREATE TRIGGER federated.t1_bd BEFORE DELETE ON federated.t1 + FOR EACH ROW SIGNAL SQLSTATE '01000' + SET MESSAGE_TEXT= 'deleting here is deprecated'; + +connection master; +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.b= 'trigger' WHERE t1.a= t2.a; +--enable_info +UPDATE t1, t2 SET t1.b= 'trigger' WHERE t1.a= t2.a; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +EXPLAIN DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a < 3; +--enable_info +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a < 3; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +connection slave; +DROP TRIGGER federated.t1_bu; +DROP TRIGGER federated.t1_bd; + +connection master; +--echo # A duplicate key stays an error even in a non-strict mode, it is not +--echo # one of the errors sql_mode downgrades to a warning. Only IGNORE would +--echo # make the remote server skip the row, and an IGNORE statement is not +--echo # pushed down at all, see the section on the statements that stay local +CALL fill_tables(); +--error ER_DUP_ENTRY +UPDATE t3, t2 SET t3.b= 30 WHERE t3.a= t2.a AND t3.a < 3; +SELECT * FROM t3 ORDER BY a; + +--echo # Without the pushdown the SET expressions are evaluated by the local +--echo # server, so the same statements are decided by the local sql_mode and +--echo # their warnings are the local ones and are reported. A warning of the +--echo # remote server is still not reported: the trigger below raises three of +--echo # them and none is seen, exactly as in the pushed down case above +connection default; +set global federated_pushdown=0; + +connection master; +SET session sql_mode='ERROR_FOR_DIVISION_BY_ZERO'; +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'x' WHERE t1.a= t2.a AND t1.a < 3/0; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.a= 1/0 WHERE t1.a= t2.a; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'much too long a value' WHERE t1.a= t2.a; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.a= 'not a number' WHERE t1.a= t2.a; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +connection slave; +CREATE TRIGGER federated.t1_bu BEFORE UPDATE ON federated.t1 + FOR EACH ROW SIGNAL SQLSTATE '01000' + SET MESSAGE_TEXT= 'updating here is deprecated'; + +connection master; +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'trigger' WHERE t1.a= t2.a; +--disable_info +SHOW WARNINGS; +SELECT * FROM t1 ORDER BY a; + +connection slave; +DROP TRIGGER federated.t1_bu; + +--echo # Restore the strict mode on both servers and reopen the connections +SET global sql_mode= @old_slave_sql_mode; + +connection default; +set global federated_pushdown=1; + +connection master; +SET session sql_mode= @old_local_sql_mode; +FLUSH TABLES; + +--echo # +--echo # ================================================================== +--echo # Inside a transaction the statement is not auto-committed by itself +--echo # ================================================================== +--echo # +CALL fill_tables(); +BEGIN; +--enable_info +UPDATE t1, t2 SET t1.b= 'in trx' WHERE t1.a= t2.a; +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 3; +--disable_info +COMMIT; +SELECT * FROM t1 ORDER BY a; + +--echo # +--echo # ================================================================== +--echo # EXPLAIN and ANALYZE report the pushdown +--echo # ================================================================== +--echo # +--echo # A statement taken over by the select handler has no local query plan, +--echo # it is reported as PUSHED UPDATE / PUSHED DELETE, the way a +--echo # pushed down SELECT is reported as PUSHED SELECT +--echo # +--echo # EXPLAIN does not execute the statement +CALL fill_tables(); +EXPLAIN UPDATE t1, t2 SET t1.b= 'explained' WHERE t1.a= t2.a; +EXPLAIN FORMAT=JSON UPDATE t1, t2 SET t1.b= 'explained' WHERE t1.a= t2.a; +EXPLAIN DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a; +SELECT * FROM t1 ORDER BY a; + +--echo # ANALYZE does execute it, and the whole statement still goes to the +--echo # remote server in one piece +CALL fill_tables(); + +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; + +connection master; +ANALYZE UPDATE t1, t2 SET t1.b= 'analyzed' WHERE t1.a= t2.a AND t1.a= 1; + +connection slave; +SET global general_log=0; +SELECT argument FROM mysql.general_log WHERE argument LIKE 'update %'; + +connection master; +SELECT * FROM t1 ORDER BY a; + +--echo # +--echo # ================================================================== +--echo # Statements that are not pushed down +--echo # ================================================================== +--echo # +--echo # EXPLAIN shows the local query plan for them, so it tells a pushed +--echo # down statement from one that is executed locally +--echo # + +--echo # IGNORE is not printed by SELECT_LEX::print(), so an IGNORE statement +--echo # is executed locally and keeps ignoring the duplicate key errors +CALL fill_tables(); + +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; + +connection master; +EXPLAIN UPDATE IGNORE t3, t2 SET t3.b= 30 WHERE t3.a= t2.a AND t3.a < 3; +UPDATE IGNORE t3, t2 SET t3.b= 30 WHERE t3.a= t2.a AND t3.a < 3; +SELECT * FROM t3 ORDER BY a; + +connection slave; +SET global general_log=0; +SELECT argument FROM mysql.general_log WHERE argument LIKE 'update %'; + +--echo # A statement that mixes engines has no FederatedX-only table list +connection master; +CREATE TABLE t4 (a int, d varchar(16)) ENGINE=MyISAM; +INSERT INTO t4 VALUES (1,'x'),(2,'y'); +CALL fill_tables(); +EXPLAIN UPDATE t1, t4 SET t1.b= t4.d WHERE t1.a= t4.a; +--enable_info +UPDATE t1, t4 SET t1.b= t4.d WHERE t1.a= t4.a; +--disable_info +SELECT * FROM t1 ORDER BY a; +DROP TABLE t4; + +--echo # A merged VIEW is not pushed down. get_fed_table_for_pushdown() walks +--echo # SELECT_LEX::join->tables_list, where a merged VIEW is still present as +--echo # its own TABLE_LIST carrying the view's internal temporary table. That +--echo # table's handler is not FederatedX, so the check that every table +--echo # belongs to the same FederatedX server fails and the whole statement is +--echo # executed locally, row by row. +CREATE VIEW v1 AS SELECT * FROM t1; +CREATE VIEW v2 AS SELECT * FROM v1; +CALL fill_tables(); + +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; + +connection master; +--echo # VIEW as the UPDATE/DELETE target +EXPLAIN UPDATE v1, t2 SET v1.b= 'view' WHERE v1.a= t2.a AND v1.a < 3; +--enable_info +UPDATE v1, t2 SET v1.b= 'view' WHERE v1.a= t2.a AND v1.a < 3; +--disable_info +SELECT * FROM t1 ORDER BY a; + +--echo # VIEW only as a source, base table as the target +EXPLAIN UPDATE t1, v1 SET t1.b= 'src' WHERE t1.a= v1.a AND v1.a= 4; +--enable_info +UPDATE t1, v1 SET t1.b= 'src' WHERE t1.a= v1.a AND v1.a= 4; +--disable_info +SELECT * FROM t1 ORDER BY a; + +--echo # Nested VIEW as the DELETE target +EXPLAIN DELETE v2 FROM v2, t2 WHERE v2.a= t2.a AND v2.a= 1; +--enable_info +DELETE v2 FROM v2, t2 WHERE v2.a= t2.a AND v2.a= 1; +--disable_info +SELECT * FROM t1 ORDER BY a; + +connection slave; +SET global general_log=0; +--echo # Row-by-row execution: several statements, not one pushed down join +SELECT argument FROM mysql.general_log + WHERE argument LIKE 'UPDATE %' OR argument LIKE 'DELETE %'; + +connection master; +DROP VIEW v1, v2; + +--echo # A single-table UPDATE/DELETE does not go through the select handler +--echo # at all, it is pushed down by direct_update_rows()/direct_delete_rows(), +--echo # a decision taken at execution time. So its plan is the ordinary one +CALL fill_tables(); +EXPLAIN UPDATE t1 SET b= 'single' WHERE a= 1; +EXPLAIN DELETE FROM t1 WHERE a= 4; +--enable_info +UPDATE t1 SET b= 'single' WHERE a= 1; +DELETE FROM t1 WHERE a= 4; +--disable_info +SELECT * FROM t1 ORDER BY a; + +--echo # +--echo # ================================================================== +--echo # Without pushdown the same statements give the same results +--echo # ================================================================== +--echo # +connection default; +set global federated_pushdown=0; + +connection master; +CALL fill_tables(); + +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log=1; + +connection master; +--enable_info +UPDATE t1, t2 SET t1.b= 'updated' WHERE t1.a= t2.a AND t1.a < 3; +--disable_info +SELECT * FROM t1 ORDER BY a; + +connection slave; +SET global general_log=0; +SELECT argument FROM mysql.general_log WHERE argument LIKE 'update %'; + +connection master; +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'two' WHERE t1.a= t2.a AND t1.a < 3; +--disable_info +SELECT * FROM t1 ORDER BY a; + +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'upd1', t2.c= 'upd2' WHERE t1.a= t2.a; +--disable_info +SELECT * FROM t1 ORDER BY a; +SELECT * FROM t2 ORDER BY a; + +CALL fill_tables(); +--enable_info +UPDATE t1, t2 SET t1.b= 'nope' WHERE t1.a= t2.a AND t1.a > 100; +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a > 100; +--disable_info +SELECT * FROM t1 ORDER BY a; + +CALL fill_tables(); +--enable_info +DELETE t1 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t2.c <> 'II'; +--disable_info +SELECT * FROM t1 ORDER BY a; + +CALL fill_tables(); +--enable_info +DELETE t1, t2 FROM t1 JOIN t2 ON t1.a= t2.a WHERE t1.a= 2; +--disable_info +SELECT * FROM t1 ORDER BY a; +SELECT * FROM t2 ORDER BY a; + +CALL fill_tables(); +--enable_info +DELETE FROM x USING t1 AS x JOIN t2 AS y ON x.a= y.a WHERE y.a= 2; +--disable_info +SELECT * FROM t1 ORDER BY a; + +DROP PROCEDURE fill_tables; +DROP TABLE federated.t1, federated.t2, federated.t3; + +connection slave; +TRUNCATE TABLE mysql.general_log; +SET global general_log= @old_general_log; +SET global log_output= @old_log_output; +DROP TABLE federated.t1, federated.t2, federated.t3; + +connection default; +source include/federated_cleanup.inc; + +--echo # End of 13.2 tests diff --git a/sql/handler.h b/sql/handler.h index fa9196f189a8f..68c3d194cccef 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1268,7 +1268,9 @@ class handler; class group_by_handler; class derived_handler; class select_handler; +class multi_upddel_handler; struct Query; +struct LEX; typedef class st_select_lex SELECT_LEX; typedef class st_select_lex_unit SELECT_LEX_UNIT; typedef struct st_order ORDER; @@ -1774,6 +1776,13 @@ struct handlerton : public transaction_participant */ select_handler *(*create_unit)(THD *thd, SELECT_LEX_UNIT *select_unit); + /* + Create and return a multi_upddel_handler that performs a whole + multi-table UPDATE/DELETE. If the storage engine cannot execute the + statement, return NULL + */ + multi_upddel_handler *(*create_multi_upddel)(THD *thd, LEX *lex); + /********************************************************************* Table discovery API. It allows the server to "discover" tables that exist in the storage diff --git a/sql/select_handler.cc b/sql/select_handler.cc index ab2a6850a6e70..5b4fa0a453ed2 100644 --- a/sql/select_handler.cc +++ b/sql/select_handler.cc @@ -18,44 +18,213 @@ #include "sql_priv.h" #include "sql_select.h" #include "sql_cte.h" +#include "sql_explain.h" #include "select_handler.h" /** - The methods of the select_handler class. - - The objects of this class are used for pushdown of the select queries - into engines. The main method of the class is select_handler::execute() - that initiates execution of a select query by a foreign engine, receives the - rows of the result set, put it in a buffer of a temporary table and send - them from the buffer directly into output. - - The method uses the functions of the select_handle interface to do this. - It also employes plus some helper functions to create the needed temporary - table and to send rows from the temporary table into output. - The constructor of the class gets the select_handler interface as a parameter. + The methods of the pushdown_handler class and of the two classes derived + from it, select_handler and multi_upddel_handler. + + The objects of these classes are used for pushdown of whole statements + into engines. + + For a select query, the main method is select_handler::execute() that + initiates execution of the query by a foreign engine, receives the rows of + the result set, put it in a buffer of a temporary table and send them from + the buffer directly into output. The method uses the functions of the + select_handler interface to do this. It also employes plus some helper + functions to create the needed temporary table and to send rows from the + temporary table into output. + + For a multi-table UPDATE/DELETE, multi_upddel_handler::execute() hands the + whole statement over to the engine with a single call and reports the row + counts the engine returns. */ +/* + Walk the tables of a statement starting at first_tbl and return the first + non-NULL pushdown handler that create() builds for the handlerton of one of + them, or NULL if no engine offers to take the statement over. create() is + expected to return NULL for a handlerton that does not provide the requested + interface. +*/ +template +static auto scan_tables_for_pushdown(TABLE_LIST *first_tbl, Create create) + -> decltype(create((handlerton *) nullptr)) +{ + for (TABLE_LIST *tbl= first_tbl; tbl; tbl= tbl->next_global) + { + if (!tbl->table) + continue; + if (auto handler= create(tbl->table->file->partition_ht())) + return handler; + } + return nullptr; +} + + +/** + @brief + Look for provision of the select_handler interface by a foreign engine. + Must not be called directly, use find_single_select_handler() or + find_partial_select_handler() instead. + + @param + thd The thread handler + select_lex SELECT_LEX object, must be passed in the cases of: + - single select pushdown + - partial pushdown (part of a UNION/EXCEPT/INTERSECT) + Must be NULL in case of entire unit pushdown + select_lex_unit SELECT_LEX_UNIT object, must be passed in the cases of: + - entire unit pushdown + - partial pushdown (part of a UNION/EXCEPT/INTERSECT) + Must be NULL in case of single select pushdown + + @details + The function checks that this is an upper level select and if so looks + through its tables searching for one whose handlerton owns a + create_select call-back function. If the call of this function returns + a select_handler interface object then the server will push the select + query into this engine. + This function does not check if the select has tables from + different engines. Such a check must be done inside each engine's + create_select function. + Also the engine's create_select function must perform other checks + to make sure the engine can execute the query. + + @retval the found select_handler if the search is successful + 0 otherwise +*/ +static select_handler *find_select_handler_inner(THD *thd, + SELECT_LEX *select_lex, + SELECT_LEX_UNIT *select_lex_unit) +{ + // Pushdown is not supported for non-top-level SELECTs + if (select_lex->master_unit()->outer_select()) + return 0; + + TABLE_LIST *tbl= nullptr; + // For SQLCOM_INSERT_SELECT the server takes TABLE_LIST + // from thd->lex->query_tables and skips its first table + // b/c it is the target table for the INSERT..SELECT. + if (thd->lex->sql_command != SQLCOM_INSERT_SELECT) + { + tbl= select_lex->join->tables_list; + } + else if (thd->lex->query_tables && + thd->lex->query_tables->next_global) + { + tbl= thd->lex->query_tables->next_global; + } + else + return 0; + + return scan_tables_for_pushdown(tbl, [&](handlerton *ht) -> select_handler * + { + return ht->create_select ? + ht->create_select(thd, select_lex, select_lex_unit) : nullptr; + }); +} + + +/** + Wrapper for find_select_handler_inner() for the case of single select + pushdown. See more comments at the description of + find_select_handler_inner() + +*/ +select_handler *find_single_select_handler(THD *thd, SELECT_LEX *select_lex) +{ + return find_select_handler_inner(thd, select_lex, nullptr); +} + + +/** + Wrapper for find_select_handler_inner() for the case of partial select + pushdown. Partial pushdown means that a unit (i.e. multiple selects combined + with UNION/EXCEPT/INTERSECT operators) cannot be pushed down to + the storage engine as a whole but some particular selects of this unit can. + For example, + SELECT a FROM federated.t1 -- can be pushed down to Federated + UNION + SELECT b FROM local.t2 -- cannot be pushed down, executed locally + + See more comments at the description of find_select_handler_inner() + +*/ +select_handler * +find_partial_select_handler(THD *thd, SELECT_LEX *select_lex, + SELECT_LEX_UNIT *select_lex_unit) +{ + return find_select_handler_inner(thd, select_lex, select_lex_unit); +} + + +/** + @brief + Look for provision of the multi_upddel_handler interface by a foreign + engine, i.e. for an engine that can perform the whole multi-table + UPDATE/DELETE this statement is. + + @details + Works the same way as find_select_handler_inner() does for a select, see + the comments there. An engine's create_multi_upddel function must make + sure the engine can execute the statement, in particular that all its + tables belong to this engine. + + @retval the found multi_upddel_handler if the search is successful + 0 otherwise +*/ + +multi_upddel_handler *find_multi_upddel_handler(THD *thd, LEX *lex) +{ + return scan_tables_for_pushdown(lex->first_select_lex()->join->tables_list, + [&](handlerton *ht) -> multi_upddel_handler * + { + return ht->create_multi_upddel ? + ht->create_multi_upddel(thd, lex) : nullptr; + }); +} + +pushdown_handler::pushdown_handler(THD *thd_arg, handlerton *ht_arg, + SELECT_LEX *sel_lex, + select_result *result_arg) + : select_lex(sel_lex), thd(thd_arg), ht(ht_arg), result(result_arg), + is_analyze(thd_arg->lex->analyze_stmt) +{} + + +void pushdown_handler::print_error(int error, myf errflag) +{ + my_error(ER_GET_ERRNO, MYF(0), error, hton_name(ht)->str); +} + + +bool pushdown_handler::send_eof() +{ + DBUG_ENTER("pushdown_handler::send_eof"); + DBUG_RETURN(result->send_eof()); +} + + select_handler::select_handler(THD *thd_arg, handlerton *ht_arg, SELECT_LEX *sel_lex) - : select_lex(sel_lex), lex_unit(nullptr), table(nullptr), - thd(thd_arg), ht(ht_arg), result(sel_lex->join->result), - is_analyze(thd_arg->lex->analyze_stmt) + : pushdown_handler(thd_arg, ht_arg, sel_lex, sel_lex->join->result), + lex_unit(nullptr), table(nullptr) {} select_handler::select_handler(THD *thd_arg, handlerton *ht_arg, SELECT_LEX_UNIT *sel_unit) - : select_lex(nullptr), lex_unit(sel_unit), table(nullptr), - thd(thd_arg), ht(ht_arg), result(sel_unit->result), - is_analyze(thd_arg->lex->analyze_stmt) + : pushdown_handler(thd_arg, ht_arg, nullptr, sel_unit->result), + lex_unit(sel_unit), table(nullptr) {} select_handler::select_handler(THD *thd_arg, handlerton *ht_arg, SELECT_LEX *sel_lex, SELECT_LEX_UNIT *sel_unit) - : select_lex(sel_lex), lex_unit(sel_unit), table(nullptr), thd(thd_arg), - ht(ht_arg), result(sel_lex->join->result), - is_analyze(thd_arg->lex->analyze_stmt) + : pushdown_handler(thd_arg, ht_arg, sel_lex, sel_lex->join->result), + lex_unit(sel_unit), table(nullptr) {} select_handler::~select_handler() @@ -144,13 +313,6 @@ bool select_handler::send_data() } -bool select_handler::send_eof() -{ - DBUG_ENTER("select_handler::send_eof"); - DBUG_RETURN(result->send_eof()); -} - - int select_handler::execute() { int err; @@ -196,9 +358,9 @@ int select_handler::execute() DBUG_RETURN(-1); // Error not sent to client } -void select_handler::print_error(int error, myf errflag) +const char *select_handler::explain_type() const { - my_error(ER_GET_ERRNO, MYF(0), error, hton_name(ht)->str); + return pushed_select_text; } select_pushdown_type select_handler::get_pushdown_type() @@ -215,3 +377,54 @@ select_pushdown_type select_handler::get_pushdown_type() return select_lex ? select_pushdown_type::PART_OF_UNIT : select_pushdown_type::WHOLE_UNIT; } + + +multi_upddel_handler::multi_upddel_handler(THD *thd_arg, handlerton *ht_arg, + LEX *lex_arg) + : pushdown_handler(thd_arg, ht_arg, lex_arg->first_select_lex(), + lex_arg->first_select_lex()->join->result), + lex(lex_arg) +{} + + +/* + Execute a multi-table UPDATE/DELETE that was pushed down into the engine. + + The engine does the whole job and only reports the row counts. The SQL + layer still has to do the final part of the statement: invalidate the + query cache, write the statement to the binary log and send the OK packet + to the client. All of that is done by multi_update::send_eof() / + multi_delete::send_eof(), so the counters are passed to the result object + and send_eof() is called as if the rows had been updated locally. +*/ + +int multi_upddel_handler::execute() +{ + int err; + ha_rows found_rows= 0, affected_rows= 0; + + DBUG_ENTER("multi_upddel_handler::execute"); + + if ((err= batch_update_delete(&found_rows, &affected_rows))) + { + if (!thd->is_error()) + print_error(err, MYF(0)); + DBUG_RETURN(-1); + } + + result->direct_update_delete_done(found_rows, affected_rows); + + if (send_eof()) + DBUG_RETURN(-1); + + DBUG_RETURN(0); +} + + +const char *multi_upddel_handler::explain_type() const +{ + enum_sql_command sql_command= lex->sql_command; + return (sql_command == SQLCOM_DELETE || + sql_command == SQLCOM_DELETE_MULTI) ? pushed_delete_text : + pushed_update_text; +} diff --git a/sql/select_handler.h b/sql/select_handler.h index 207af43f56ca0..16f8da0a21557 100644 --- a/sql/select_handler.h +++ b/sql/select_handler.h @@ -20,12 +20,81 @@ #include "mariadb.h" #include "sql_priv.h" +struct LEX; + enum class select_pushdown_type { SINGLE_SELECT, PART_OF_UNIT, WHOLE_UNIT }; + +/** + @class pushdown_handler + + A statement that a foreign engine has taken over from the SQL layer. + + The SQL layer builds no query plan for such a statement: the optimizer + stops after prepare() and JOIN::exec_inner() calls execute() instead of + running the join. What "execute" means depends on the kind of statement, + which is what the two derived classes stand for: + + - select_handler the engine produces a result set, the SQL layer + retrieves the rows and sends them to the client + - multi_upddel_handler the engine performs a whole multi-table + UPDATE/DELETE and only reports the row counts + + An engine provides them through the handlerton::create_select / + create_unit / create_multi_upddel call-back functions. +*/ + +class pushdown_handler +{ +public: + pushdown_handler(THD *thd_arg, handlerton *ht_arg, SELECT_LEX *sel_lex, + select_result *result_arg); + + virtual ~pushdown_handler() = default; + + /* + Get ready for the execution. Called instead of the optimization of + the statement. Returns true in case of error. + */ + virtual bool prepare() { return false; } + + /* + Execute the statement. + + @retval 0 ok + @retval -1 error, it has been sent to the client already + */ + virtual int execute() = 0; + + /* + The name this statement is reported with in the select_type column of + EXPLAIN, one of the pushed_*_text constants of sql_explain.h + */ + virtual const char *explain_type() const = 0; + + /* Single select to be executed. NULL for a whole unit, see select_handler */ + SELECT_LEX *select_lex; + +protected: + + /* Report errors */ + virtual void print_error(int error, myf errflag); + + bool send_eof(); + + THD *thd; + handlerton *ht; + + select_result *result; // Object receiving the retrieved data + + bool is_analyze; +}; + + /** @class select_handler @@ -33,7 +102,7 @@ enum class select_pushdown_type { by foreign engines */ -class select_handler +class select_handler : public pushdown_handler { public: // Constructor for a single SELECT_LEX (not a part of a unit) @@ -49,11 +118,13 @@ class select_handler select_handler(THD *thd_arg, handlerton *ht_arg, SELECT_LEX *sel_lex, SELECT_LEX_UNIT *sel_unit); - virtual ~select_handler(); + ~select_handler() override; + + int execute() override; - int execute(); + bool prepare() override; - virtual bool prepare(); + const char *explain_type() const override; /* Select_handler processes these cases: @@ -66,7 +137,6 @@ class select_handler in the case of partial pushdown both select_lex and lex_unit are initialized */ - SELECT_LEX *select_lex; // Single select/part of a unit to be executed SELECT_LEX_UNIT *lex_unit; // Unit to be executed /* @@ -96,24 +166,71 @@ class select_handler /* Finish scanning */ virtual int end_scan() = 0; - /* Report errors */ - virtual void print_error(int error, myf errflag); - bool send_result_set_metadata(); bool send_data(); - bool send_eof(); TABLE *create_tmp_table(THD *thd); select_pushdown_type get_pushdown_type(); - THD *thd; - handlerton *ht; - - select_result *result; // Object receiving the retrieved data List result_columns; +}; - bool is_analyze; + +/** + @class multi_upddel_handler + + This interface class is to be used for execution of multi-table UPDATE + and DELETE statements by foreign engines. + + A multi-table UPDATE/DELETE has no "primary" ha_something object to call + handler::direct_update_rows_init()/direct_delete_rows_init() on, so the + whole statement is handed over to the engine at once, and the engine + reports only how many rows it has matched and changed. + + The whole statement being the unit of the pushdown, the handler is built + from the LEX of the statement rather than from a single SELECT_LEX. +*/ + +class multi_upddel_handler : public pushdown_handler +{ +public: + multi_upddel_handler(THD *thd_arg, handlerton *ht_arg, LEX *lex_arg); + + int execute() override; + + const char *explain_type() const override; + +protected: + + /* The statement to be executed */ + LEX *lex; + + /* + Perform the whole statement and report how many rows matched the WHERE + clause (*found_rows) and how many rows were really changed or deleted + (*affected_rows). + + This is called from multi_upddel_handler::execute(). + + @retval 0 ok + @retval != 0 error code + */ + virtual int batch_update_delete(ha_rows *found_rows, + ha_rows *affected_rows) = 0; }; + +/* + Look through the tables of a statement for an engine that offers to take + the statement over. Defined in select_handler.cc together with the classes + they return. +*/ +select_handler *find_single_select_handler(THD *thd, SELECT_LEX *select_lex); + +select_handler *find_partial_select_handler(THD *thd, SELECT_LEX *select_lex, + SELECT_LEX_UNIT *select_lex_unit); + +multi_upddel_handler *find_multi_upddel_handler(THD *thd, LEX *lex); + #endif /* SELECT_HANDLER_INCLUDED */ diff --git a/sql/select_result.h b/sql/select_result.h index 4bc1050388570..1533541062fe4 100644 --- a/sql/select_result.h +++ b/sql/select_result.h @@ -106,6 +106,19 @@ class select_result :public select_result_sink @retval TRUE error, an error message is set */ virtual bool check_simple_select() const; + /* + A multi-table UPDATE/DELETE was performed by the storage engine itself, + see select_handler_type::UPDATE_DELETE. Tell the result object how many + rows matched the WHERE clause and how many rows were really changed, so + that the following send_eof() reports them instead of running the + SQL-layer update/delete loop. + Only multi_update and multi_delete can receive this call; any other result + object returns true so that the caller reports an error instead of + silently losing the row counts. + */ + virtual void direct_update_delete_done(ha_rows found_rows, + ha_rows affected_rows) + { DBUG_ASSERT(0); } virtual void abort_result_set() {} virtual void reset_for_next_ps_execution(); void set_thd(THD *thd_arg) { thd= thd_arg; } diff --git a/sql/sql_class.h b/sql/sql_class.h index 07046ff0f3714..ff84145d069c4 100644 --- a/sql/sql_class.h +++ b/sql/sql_class.h @@ -7849,6 +7849,8 @@ class multi_delete :public select_result_interceptor so that afterward abort_result_set() needs to find out that. */ bool error_handled; + /* True if the engine has performed the whole DELETE, see select_handler */ + bool direct_dml_done; public: // Methods used by ColumnStore @@ -7866,6 +7868,8 @@ class multi_delete :public select_result_interceptor int do_table_deletes(TABLE *table, SORT_INFO *sort_info, bool ignore); bool send_eof() override; inline ha_rows num_deleted() const { return deleted; } + void direct_update_delete_done(ha_rows found_rows, + ha_rows affected_rows) override; void abort_result_set() override; void prepare_to_read_rows() override; }; @@ -7900,7 +7904,9 @@ class multi_update :public select_result_interceptor so that afterward abort_result_set() needs to find out that. */ bool error_handled; - + /* True if the engine has performed the whole UPDATE, see select_handler */ + bool direct_dml_done; + /* Need this to protect against multiple prepare() calls */ bool prepared; @@ -7927,6 +7933,8 @@ class multi_update :public select_result_interceptor inline ha_rows num_updated() const { return updated; } inline void set_found (ha_rows n) { found= n; } inline void set_updated (ha_rows n) { updated= n; } + void direct_update_delete_done(ha_rows found_rows, + ha_rows affected_rows) override; virtual void abort_result_set() override; void update_used_tables() override; void prepare_to_read_rows() override; diff --git a/sql/sql_delete.cc b/sql/sql_delete.cc index b9e2c583a6dca..539ad7b478f3d 100644 --- a/sql/sql_delete.cc +++ b/sql/sql_delete.cc @@ -1175,7 +1175,8 @@ multi_delete::multi_delete(THD *thd_arg, do_delete(0), transactional_tables(0), normal_tables(0), - error_handled(0) + error_handled(0), + direct_dml_done(0) { tmp_tables = thd->calloc(table_count); tmp_table_param = thd->calloc(table_count); @@ -1523,7 +1524,7 @@ void multi_delete::abort_result_set() The same if all tables are transactional, regardless of where we are. In all other cases do attempt deletes ... */ - if (do_delete && normal_tables && + if (do_delete && normal_tables && !direct_dml_done && (table_being_deleted != delete_tables || !table_being_deleted->table->file->has_transactions_and_rollback())) { @@ -1735,6 +1736,46 @@ int multi_delete::rowid_table_deletes(TABLE *table, bool ignore) } +/* + The engine has performed the whole multi-table DELETE on its own, the + statement was pushed down through the select_handler interface. Remember + the row counts reported by the engine and check whether modified tables + were transactional. send_eof() will need this info to binlog the statement + correctly. +*/ + +void multi_delete::direct_update_delete_done(ha_rows found_rows, + ha_rows affected_rows) +{ + DBUG_ENTER("multi_delete::direct_update_delete_done"); + + found= found_rows; + deleted= affected_rows; + direct_dml_done= true; + + /* + Do the same as multi_delete's initialize_tables() and send_data(): + walk the tables that we delete from and check if they are transactional + */ + for (TABLE_LIST *walk= delete_tables; walk; walk= walk->next_local) + { + TABLE_LIST *tbl= walk->table ? walk : + walk->correspondent_table->find_table_for_update(); + if (!tbl || !tbl->table) + continue; + if (tbl->table->file->has_transactions()) + transactional_tables= 1; + else + { + normal_tables= 1; + if (deleted) + thd->transaction->stmt.modified_non_trans_table= TRUE; + } + } + DBUG_VOID_RETURN; +} + + /* Send ok to the client @@ -1748,7 +1789,7 @@ bool multi_delete::send_eof() THD_STAGE_INFO(thd, stage_deleting_from_reference_tables); /* Does deletes for the last n - 1 tables, returns 0 if ok */ - int local_error= do_deletes(); // returns 0 if success + int local_error= direct_dml_done ? 0 : do_deletes(); // returns 0 if success /* compute a total error to know if something failed */ local_error= local_error || error; @@ -1788,7 +1829,14 @@ bool multi_delete::send_eof() else errcode= query_error_code(thd, killed_status == NOT_KILLED); thd->used|= THD::THREAD_SPECIFIC_USED; - StatementBinlog stmt_binlog(thd, thd->binlog_need_stmt_format(transactional_tables)); + /* + When the engine has performed the whole DELETE on its own no row + events were produced, so the statement must be binlogged in statement + format regardless of binlog_format, otherwise the change would not be + replicated. + */ + StatementBinlog stmt_binlog(thd, direct_dml_done || + thd->binlog_need_stmt_format(transactional_tables)); if (unlikely(thd->binlog_query(THD::ROW_QUERY_TYPE, thd->query(), thd->query_length(), transactional_tables, FALSE, FALSE, diff --git a/sql/sql_explain.cc b/sql/sql_explain.cc index 2e894deae5fce..d538ec0765a9b 100644 --- a/sql/sql_explain.cc +++ b/sql/sql_explain.cc @@ -38,8 +38,32 @@ const char *pushed_unit_operation_text[4]= "PUSHED UNIT", "PUSHED UNION", "PUSHED INTERSECT", "PUSHED EXCEPT" }; -const char *pushed_derived_text= "PUSHED DERIVED"; -const char *pushed_select_text= "PUSHED SELECT"; +const char * const pushed_derived_text= "PUSHED DERIVED"; +const char * const pushed_select_text= "PUSHED SELECT"; +const char * const pushed_update_text= "PUSHED UPDATE"; +const char * const pushed_delete_text= "PUSHED DELETE"; + +/* + A statement that was taken over by an engine has no local query plan, it is + reported as a single line carrying just the select type. Below are the + select types this applies to and the messages they are shown with in the + JSON output. +*/ + +static const char *get_pushed_down_select_text(const char *select_type) +{ + if (select_type == pushed_derived_text) + return "Pushed derived"; + if (select_type == pushed_select_text) + return "Pushed select"; + if (select_type == pushed_update_text) + return "Pushed update"; + if (select_type == pushed_delete_text) + return "Pushed delete"; + return nullptr; +} + + /* See enum ha_parititon::partition_index_scan_method */ const char *partitions_index_scan_method_str[]= {NULL, "merge_ordered_scans", "iterate_over_partitions", "both"}; @@ -1020,8 +1044,9 @@ int Explain_select::print_explain(Explain_query *query, { THD *thd= output->thd; MEM_ROOT *mem_root= thd->mem_root; + const char *pushed_down_msg; - if (select_type == pushed_derived_text || select_type == pushed_select_text) + if ((pushed_down_msg= get_pushed_down_select_text(select_type))) { print_explain_message_line(output, explain_flags, is_analyze, select_id /*select number*/, @@ -1153,21 +1178,16 @@ void Explain_select::print_explain_json(Explain_query *query, bool started_cache= print_explain_json_cache(writer, is_analyze); bool started_subq_mat= print_explain_json_subq_materialization(writer, is_analyze); - - if (message || - select_type == pushed_derived_text || - select_type == pushed_select_text) + const char *pushed_down_msg= get_pushed_down_select_text(select_type); + if (message || pushed_down_msg) { writer->add_member("query_block").start_object(); writer->add_member("select_id").add_ll(select_id); add_linkage(writer); writer->add_member("table").start_object(); - writer->add_member("message").add_str(select_type == pushed_derived_text ? - "Pushed derived" : - select_type == pushed_select_text ? - "Pushed select" : - message); + writer->add_member("message"). + add_str(pushed_down_msg ? pushed_down_msg : message); writer->end_object(); print_explain_json_for_children(query, writer, is_analyze); diff --git a/sql/sql_explain.h b/sql/sql_explain.h index 9b07721973b45..f2d4d54669942 100644 --- a/sql/sql_explain.h +++ b/sql/sql_explain.h @@ -345,8 +345,10 @@ class Explain_aggr_window_funcs : public Explain_aggr_node extern const char *unit_operation_text[4]; extern const char *pushed_unit_operation_text[4]; -extern const char *pushed_derived_text; -extern const char *pushed_select_text; +extern const char * const pushed_derived_text; +extern const char * const pushed_select_text; +extern const char * const pushed_update_text; +extern const char * const pushed_delete_text; /* Explain structure for a UNION [ALL]. diff --git a/sql/sql_lex.cc b/sql/sql_lex.cc index 6e1c6df4715b7..4971d5b7de99d 100644 --- a/sql/sql_lex.cc +++ b/sql/sql_lex.cc @@ -5821,7 +5821,8 @@ void st_select_lex::set_explain_type(bool on_the_fly) if (pushdown_select) { - type= pushed_select_text; + /* The engine has taken over the whole statement, it says what it is */ + type= pushdown_select->explain_type(); return; } diff --git a/sql/sql_lex.h b/sql/sql_lex.h index 5e0258ee42bf3..44935bd80bf48 100644 --- a/sql/sql_lex.h +++ b/sql/sql_lex.h @@ -290,6 +290,7 @@ class Item_window_func; struct sql_digest_state; class With_clause; class my_var; +class pushdown_handler; class select_handler; class Pushdown_select; class Opt_hints_global; @@ -1181,7 +1182,7 @@ class st_select_lex: public st_select_lex_node table_value_constr *tvc; /* The object used to organize execution of the query by a foreign engine */ - select_handler *pushdown_select; + pushdown_handler *pushdown_select; List *join_list; /* list for the currently parsed join */ st_select_lex *merged_into; /* select which this select is merged into */ /* (not 0 only for views/derived tables) */ diff --git a/sql/sql_select.cc b/sql/sql_select.cc index dc9e7b6113333..5924781a4e646 100644 --- a/sql/sql_select.cc +++ b/sql/sql_select.cc @@ -5200,112 +5200,6 @@ void JOIN::cleanup_item_list(List &items) const } -/** - @brief - Look for provision of the select_handler interface by a foreign engine. - Must not be called directly, use find_single_select_handler() or - find_partial_select_handler() instead. - - @param - thd The thread handler - select_lex SELECT_LEX object, must be passed in the cases of: - - single select pushdown - - partial pushdown (part of a UNION/EXCEPT/INTERSECT) - Must be NULL in case of entire unit pushdown - select_lex_unit SELECT_LEX_UNIT object, must be passed in the cases of: - - entire unit pushdown - - partial pushdown (part of a UNION/EXCEPT/INTERSECT) - Must be NULL in case of single select pushdown - - @details - The function checks that this is an upper level select and if so looks - through its tables searching for one whose handlerton owns a - create_select call-back function. If the call of this function returns - a select_handler interface object then the server will push the select - query into this engine. - This function does not check if the select has tables from - different engines. Such a check must be done inside each engine's - create_select function. - Also the engine's create_select function must perform other checks - to make sure the engine can execute the query. - - @retval the found select_handler if the search is successful - 0 otherwise -*/ - -static -select_handler *find_select_handler_inner(THD *thd, - SELECT_LEX *select_lex, - SELECT_LEX_UNIT *select_lex_unit) -{ - // Pushdown is not supported for non-top-level SELECTs - if (select_lex->master_unit()->outer_select()) - return 0; - - TABLE_LIST *tbl= nullptr; - // For SQLCOM_INSERT_SELECT the server takes TABLE_LIST - // from thd->lex->query_tables and skips its first table - // b/c it is the target table for the INSERT..SELECT. - if (thd->lex->sql_command != SQLCOM_INSERT_SELECT) - { - tbl= select_lex->join->tables_list; - } - else if (thd->lex->query_tables && - thd->lex->query_tables->next_global) - { - tbl= thd->lex->query_tables->next_global; - } - else - return 0; - - for (;tbl; tbl= tbl->next_global) - { - if (!tbl->table) - continue; - handlerton *ht= tbl->table->file->partition_ht(); - if (!ht->create_select) - continue; - select_handler *sh= ht->create_select(thd, select_lex, select_lex_unit); - if (sh) - return sh; - } - return 0; -} - - -/** - Wrapper for find_select_handler_inner() for the case of single select - pushdown. See more comments at the description of - find_select_handler_inner() - -*/ -select_handler *find_single_select_handler(THD *thd, SELECT_LEX *select_lex) -{ - return find_select_handler_inner(thd, select_lex, nullptr); -} - - -/** - Wrapper for find_select_handler_inner() for the case of partial select - pushdown. Partial pushdown means that a unit (i.e. multiple selects combined - with UNION/EXCEPT/INTERSECT operators) cannot be pushed down to - the storage engine as a whole but some particular selects of this unit can. - For example, - SELECT a FROM federated.t1 -- can be pushed down to Federated - UNION - SELECT b FROM local.t2 -- cannot be pushed down, executed locally - - See more comments at the description of find_select_handler_inner() - -*/ -select_handler * -find_partial_select_handler(THD *thd, SELECT_LEX *select_lex, - SELECT_LEX_UNIT *select_lex_unit) -{ - return find_select_handler_inner(thd, select_lex, select_lex_unit); -} - - /** An entry point to single-unit select (a select without UNION). @@ -32387,6 +32281,16 @@ enum explainable_cmd_type get_explainable_cmd_type(THD *thd) } +/* + Print a table of the target list of a multi-table DELETE, i.e. of the list + of the tables the rows are deleted from. + + The grammar accepts only a (possibly qualified) table name or an alias + there, and not a table reference with an alias like the USING clause does. + So a table that is given an alias in the USING clause has to be referred to + by this alias alone, otherwise the printed statement cannot be parsed back. +*/ + void TABLE_LIST::print_leaf_tables(THD *thd, String *str, enum_query_type query_type) { @@ -32394,9 +32298,17 @@ void TABLE_LIST::print_leaf_tables(THD *thd, String *str, { for (TABLE_LIST *tbl= merge_underlying_list; tbl; tbl= tbl->next_local) tbl->print_leaf_tables(thd, str, query_type); + return; } - else - print(thd, 0, str, query_type); + + const Lex_ident_table name= view_name.str ? Lex_ident_table(view_name) + : table_name; + if (!name.streq(alias)) + { + append_identifier_opt_casedn(thd, str, alias, lower_case_table_names == 1); + return; + } + print(thd, 0, str, query_type); } @@ -35118,8 +35030,21 @@ bool Sql_cmd_dml::execute_inner(THD *thd) SELECT_LEX *select_lex= unit->first_select(); JOIN *join= select_lex->join; - // look for select_handler provided by engines - select_lex->pushdown_select= find_single_select_handler(thd, select_lex); + /* + Look for a handler provided by engines. This is the execution path of a + multi-table UPDATE/DELETE, for which an engine may take over the whole + statement through the multi_upddel_handler interface. + */ + switch (thd->lex->sql_command) { + case SQLCOM_UPDATE: + case SQLCOM_UPDATE_MULTI: + case SQLCOM_DELETE: + case SQLCOM_DELETE_MULTI: + select_lex->pushdown_select= find_multi_upddel_handler(thd, lex); + break; + default: + select_lex->pushdown_select= find_single_select_handler(thd, select_lex); + } if (join->optimize()) goto err; diff --git a/sql/sql_union.cc b/sql/sql_union.cc index 8bf48bc4487ba..466f60e10af00 100644 --- a/sql/sql_union.cc +++ b/sql/sql_union.cc @@ -32,9 +32,6 @@ #include "sql_cte.h" #include "item_windowfunc.h" -select_handler *find_partial_select_handler(THD *thd, SELECT_LEX *select_lex, - SELECT_LEX_UNIT *lex_unit); - bool mysql_union(THD *thd, LEX *lex, select_result *result, SELECT_LEX_UNIT *unit, ulonglong setup_tables_done_option) { diff --git a/sql/sql_update.cc b/sql/sql_update.cc index 847d073b48d78..c8cc1513fffe2 100644 --- a/sql/sql_update.cc +++ b/sql/sql_update.cc @@ -1846,6 +1846,7 @@ multi_update::multi_update(THD *thd_arg, TABLE_LIST *table_list, transactional_tables(0), ignore(ignore_arg), error_handled(0), + direct_dml_done(0), prepared(0), updated_sys_ver(0), tables_to_update(get_table_map(fields)) @@ -2614,7 +2615,7 @@ void multi_update::abort_result_set() if (! trans_safe) { DBUG_ASSERT(thd->transaction->stmt.modified_non_trans_table); - if (do_update && table_count > 1) + if (do_update && table_count > 1 && !direct_dml_done) { /* Add warning here */ (void) do_updates(); @@ -2930,6 +2931,45 @@ int multi_update::do_updates() } +/* + The engine has performed the whole multi-table UPDATE on its own, the + statement was pushed down through the select_handler interface. Remember + the row counts reported by the engine and check whether modified tables + were transactional. send_eof() will need this info to binlog the statement + correctly. +*/ + +void multi_update::direct_update_delete_done(ha_rows found_rows, + ha_rows affected_rows) +{ + DBUG_ENTER("multi_update::direct_update_delete_done"); + + found= found_rows; + updated= affected_rows; + direct_dml_done= true; + + /* + Do the same as multi_update::do_updates() does: + walk the tables that were updated and check if they were transactional + */ + for (TABLE_LIST *cur_table= update_tables; cur_table; + cur_table= cur_table->next_local) + { + TABLE *table= cur_table->table; + if (!table) + continue; + if (table->file->has_transactions_and_rollback()) + transactional_tables= TRUE; + else if (updated) + { + trans_safe= FALSE; + thd->transaction->stmt.modified_non_trans_table= TRUE; + } + } + DBUG_VOID_RETURN; +} + + /* out: 1 if error, 0 if success */ bool multi_update::send_eof() @@ -2940,13 +2980,13 @@ bool multi_update::send_eof() DBUG_ENTER("multi_update::send_eof"); THD_STAGE_INFO(thd, stage_updating_reference_tables); - /* + /* Does updates for the last n - 1 tables, returns 0 if ok; error takes into account killed status gained in do_updates() */ int local_error= thd->is_error(); if (likely(!local_error)) - local_error = (table_count) ? do_updates() : 0; + local_error= (table_count && !direct_dml_done) ? do_updates() : 0; /* if local_error is not set ON until after do_updates() then later carried out killing should not affect binlogging. @@ -2994,7 +3034,14 @@ bool multi_update::send_eof() else errcode= query_error_code(thd, killed_status == NOT_KILLED); - bool force_stmt= thd->binlog_need_stmt_format(transactional_tables); + /* + When the engine has performed the whole UPDATE on its own no row + events were produced, so the statement must be binlogged in statement + format regardless of binlog_format, otherwise the change would not be + replicated. + */ + bool force_stmt= direct_dml_done || + thd->binlog_need_stmt_format(transactional_tables); if (!force_stmt) for (TABLE *table= all_tables->table; table; table= table->next) { diff --git a/storage/federatedx/federatedx_io_mysql.cc b/storage/federatedx/federatedx_io_mysql.cc index a7f1e5135b82c..d46a00d4de8e7 100644 --- a/storage/federatedx/federatedx_io_mysql.cc +++ b/storage/federatedx/federatedx_io_mysql.cc @@ -78,6 +78,7 @@ class federatedx_io_mysql :public federatedx_io size_t max_query_size() const override; my_ulonglong affected_rows() const override; + my_ulonglong matched_rows() const override; my_ulonglong last_insert_id() const override; int error_code() override; @@ -468,6 +469,32 @@ my_ulonglong federatedx_io_mysql::affected_rows() const } +my_ulonglong federatedx_io_mysql::matched_rows() const +{ + /* + For an UPDATE the server returns an info string of the form + "Rows matched: N Changed: M Warnings: W" + where N is the matched count. The text around the numbers varies depending + on the remote server's locale, but the order of the numbers is fixed, so + read the first number rather than search for an English label. For + statements that do not report matched rows (e.g. DELETE) mysql_info() is + NULL, fall back to the number of affected rows. + */ + if (mysql.info) + { + const char *p= mysql.info; + while (*p && (*p < '0' || *p > '9')) + p++; + if (*p) + { + int error= 0; + return (my_ulonglong) my_strtoll10(p, NULL, &error); + } + } + return mysql.affected_rows; +} + + my_ulonglong federatedx_io_mysql::last_insert_id() const { return mysql.insert_id; diff --git a/storage/federatedx/federatedx_io_null.cc b/storage/federatedx/federatedx_io_null.cc index 5c62ea6ab5271..f15c16a757dcf 100644 --- a/storage/federatedx/federatedx_io_null.cc +++ b/storage/federatedx/federatedx_io_null.cc @@ -59,6 +59,7 @@ class federatedx_io_null :public federatedx_io size_t max_query_size() const override; my_ulonglong affected_rows() const override; + my_ulonglong matched_rows() const override; my_ulonglong last_insert_id() const override; int error_code() override; @@ -190,6 +191,12 @@ my_ulonglong federatedx_io_null::affected_rows() const } +my_ulonglong federatedx_io_null::matched_rows() const +{ + return 0; +} + + my_ulonglong federatedx_io_null::last_insert_id() const { return 0; diff --git a/storage/federatedx/federatedx_pushdown.cc b/storage/federatedx/federatedx_pushdown.cc index 3fc7423eee9a0..0b1f531b7243a 100644 --- a/storage/federatedx/federatedx_pushdown.cc +++ b/storage/federatedx/federatedx_pushdown.cc @@ -15,15 +15,17 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ /* !!! For inclusion into ha_federatedx.cc */ - +/* For CR_MIN_ERROR/CR_MAX_ERROR, the error code range of the client library */ +#include /* This is a quick a dirty implemention of the derived_handler and select_handler interfaces to be used to push select queries and the queries specifying derived tables into FEDERATEDX engine. The functions - create_federatedx_derived_handler and - create_federatedx_select_handler + create_federatedx_derived_handler, + create_federatedx_select_handler, and + create_federatedx_multi_upddel_handler that return the corresponding interfaces for pushdown capabilities do not check a lot of things. In particular they do not check that the tables of the pushed queries belong to the same foreign server. @@ -79,14 +81,45 @@ bool local_and_remote_names_mismatch(const TABLE_SHARE *tbl_share, /* - Check that all tables in the sel_lex use the FederatedX storage engine - and return one of them + Whether two FederatedX tables live on the same remote server. + + The whole statement is sent to a single remote connection, so all the + tables it touches must be reachable through the same one. A connection is + identified by its scheme, host, port, socket, and user; the remote + database may differ from table to table (one connection can address several + databases), so it is not compared here. +*/ +static bool same_remote_server(const FEDERATEDX_SHARE *a, + const FEDERATEDX_SHARE *b) +{ + auto str_eq= [](const char *x, const char *y) + { return (!x || !y) ? x == y : !strcmp(x, y); }; + + return a->port == b->port && + str_eq(a->scheme, b->scheme) && + str_eq(a->hostname, b->hostname) && + str_eq(a->socket, b->socket) && + str_eq(a->username, b->username); +} + + +/* + Check that all tables in the sel_lex use the FederatedX storage engine and + live on the same remote server, and return one of them. + + @param sel_lex the select to check + @param ref_share in/out: the share of the first FederatedX table seen so + far across the whole statement, every other table is + required to match its remote server. Must point to a + nullptr on the top-level call. + @return One of the tables from sel_lex */ -static TABLE *get_fed_table_for_pushdown(SELECT_LEX *sel_lex) +static TABLE *get_fed_table_for_pushdown(SELECT_LEX *sel_lex, + const FEDERATEDX_SHARE **ref_share) { - TABLE *table= nullptr; + TABLE *pushdown_table= nullptr; if (!sel_lex->join) return nullptr; for (TABLE_LIST *tbl= sel_lex->join->tables_list; tbl; tbl= tbl->next_local) @@ -103,6 +136,7 @@ static TABLE *get_fed_table_for_pushdown(SELECT_LEX *sel_lex) } /* + Check that all tables are FederatedX tables. We intentionally don't support partitioned federatedx tables here, so use file->ht and not file->partition_ht(). */ @@ -110,11 +144,21 @@ static TABLE *get_fed_table_for_pushdown(SELECT_LEX *sel_lex) return nullptr; const FEDERATEDX_SHARE *fshare= ((ha_federatedx *) tbl->table->file)->get_federatedx_share(); + /* + We print the local (frontend) query and run it on the remote server. + This only works if the table name on the remote server is the same. + */ if (local_and_remote_names_mismatch(tbl->table->s, fshare)) return nullptr; - if (!table) - table= tbl->table; + /* All the tables of the statement must be on the same remote server */ + if (!*ref_share) + *ref_share= fshare; + else if (!same_remote_server(*ref_share, fshare)) + return nullptr; + + if (!pushdown_table) + pushdown_table= tbl->table; } for (SELECT_LEX_UNIT *un= sel_lex->first_inner_unit(); un; @@ -122,14 +166,24 @@ static TABLE *get_fed_table_for_pushdown(SELECT_LEX *sel_lex) { for (SELECT_LEX *sl= un->first_select(); sl; sl= sl->next_select()) { - auto inner_tbl= get_fed_table_for_pushdown(sl); + auto inner_tbl= get_fed_table_for_pushdown(sl, ref_share); if (!inner_tbl) return nullptr; - if (!table) - table= inner_tbl; + if (!pushdown_table) + pushdown_table= inner_tbl; } } - return table; + return pushdown_table; +} + + +/* + A wrapper for the top-level call, see get_fed_table_for_pushdown() above +*/ +static TABLE *get_fed_table_for_pushdown(SELECT_LEX *sel_lex) +{ + const FEDERATEDX_SHARE *ref_share= nullptr; + return get_fed_table_for_pushdown(sel_lex, &ref_share); } @@ -143,10 +197,11 @@ static TABLE *get_fed_table_for_pushdown(SELECT_LEX *sel_lex) static TABLE *get_fed_table_for_unit_pushdown(SELECT_LEX_UNIT *lex_unit) { TABLE *table= nullptr; + const FEDERATEDX_SHARE *ref_share= nullptr; for (auto sel_lex= lex_unit->first_select(); sel_lex; sel_lex= sel_lex->next_select()) { - auto next_tbl= get_fed_table_for_pushdown(sel_lex); + auto next_tbl= get_fed_table_for_pushdown(sel_lex, &ref_share); if (!next_tbl) return nullptr; if (!table) @@ -238,6 +293,47 @@ create_federatedx_select_handler(THD *thd, SELECT_LEX *sel_lex, return new ha_federatedx_select_handler(thd, sel_lex, lex_unit, tbl); } + +/* + Create FederatedX handler for processing a whole multi-table UPDATE/DELETE +*/ +static multi_upddel_handler * +create_federatedx_multi_upddel_handler(THD *thd, LEX *lex) +{ + DBUG_ASSERT(lex->sql_command == SQLCOM_UPDATE || + lex->sql_command == SQLCOM_UPDATE_MULTI || + lex->sql_command == SQLCOM_DELETE || + lex->sql_command == SQLCOM_DELETE_MULTI); + + /* Is pushdown enabled by @@federatedx_use_pushdown? */ + if (!use_pushdown) + return nullptr; + + /* + SELECT_LEX::print() reproduces neither the IGNORE modifier nor the + LOW_PRIORITY/QUICK ones. Of these only IGNORE changes the outcome of the + statement: a pushed down UPDATE IGNORE/DELETE IGNORE would turn an error + the remote server ignores into a real one, so such statements are executed + locally. LOW_PRIORITY and QUICK only affect local locking and index + housekeeping; they change neither the affected rows nor the errors raised, + and the row-by-row path does not forward them to the remote server either, + so dropping them here is harmless and needs no guard. + */ + if (lex->ignore) + return nullptr; + + SELECT_LEX *sel_lex= lex->first_select_lex(); + + auto tbl= get_fed_table_for_pushdown(sel_lex); + if (!tbl) + return nullptr; + + if (sel_lex->uncacheable & UNCACHEABLE_SIDEEFFECT) + return nullptr; + + return new ha_federatedx_multi_upddel_handler(thd, lex, tbl); +} + /* Create FederatedX select handler for processing a unit as a whole. Term "unit" stands for multiple SELECTs combined with @@ -315,6 +411,99 @@ ha_federatedx_select_handler::ha_federatedx_select_handler( } } +/* + Implementation class of the multi_upddel_handler interface for FEDERATEDX: + class implementation +*/ + +ha_federatedx_multi_upddel_handler::ha_federatedx_multi_upddel_handler( + THD *thd, LEX *lex_arg, TABLE *tbl) + : multi_upddel_handler(thd, federatedx_hton, lex_arg), + federatedx_handler_base(thd, tbl) +{ + query.length(0); + /* + Print the whole statement back. SELECT_LEX::print() produces + update set where + for an UPDATE and + delete from using where + for a DELETE, both of which the remote server understands. + + Must go through SELECT_LEX_UNIT::print() rather than call + SELECT_LEX::print() directly, because a possible WITH clause is stored at + SELECT_LEX_UNIT::with_clause and is printed only by the former. + */ + lex->unit.print(&query, FEDERATEDX_PRINT_UPD_DEL_QUERY_TYPE); +} + + +/* + Execute a multi-table UPDATE/DELETE on the remote server and report + how many rows it has changed +*/ + +int ha_federatedx_multi_upddel_handler::batch_update_delete( + ha_rows *found_rows, ha_rows *affected_rows) +{ + THD *thd= query_table->in_use; + int rc; + DBUG_ENTER("ha_federatedx_multi_upddel_handler::batch_update_delete"); + + ha_federatedx *h= (ha_federatedx *) query_table->file; + iop= &h->io; + share= get_share(query_table->s->table_name.str, query_table, + h->option_struct); + txn= h->get_txn(thd); + + /* no need for savepoint in autocommit mode */ + if (!(thd->variables.option_bits & (OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN))) + txn->stmt_autocommit(); + + if ((rc= txn->acquire(share, thd, FALSE, iop))) + { + free_share(txn, share); + share= NULL; + DBUG_RETURN(rc); + } + + if ((*iop)->query(query.ptr(), query.length())) + { + /* + The whole statement was executed by the remote server, so an error it + reports is an error of this statement. Pass its error code through + instead of hiding it behind ER_QUERY_ON_FOREIGN_DATA_SOURCE, so that the + client sees the same error it would have seen had the statement been + executed locally, e.g. ER_DIVISION_BY_ZERO or ER_DUP_ENTRY. + + Errors of the client library (a lost connection and the like) are not + server error codes and are reported as a foreign data source failure. + */ + const int remote_errno= (*iop)->error_code(); + if (remote_errno < CR_MIN_ERROR || remote_errno > CR_MAX_ERROR) + my_message(remote_errno, (*iop)->error_str(), MYF(0)); + else + my_error(ER_QUERY_ON_FOREIGN_DATA_SOURCE, MYF(0), (*iop)->error_str()); + rc= HA_FEDERATEDX_ERROR_WITH_REMOTE_SYSTEM; + } + else + { + /* + For an UPDATE the remote server reports both the number of matched and + of changed rows (matched_rows() parses it out of the info string), for a + DELETE matched_rows() returns the number of deleted rows just like + affected_rows() does. + */ + *affected_rows= (ha_rows) (*iop)->affected_rows(); + *found_rows= (ha_rows) (*iop)->matched_rows(); + rc= 0; + } + + free_share(txn, share); + share= NULL; + DBUG_RETURN(rc); +} + + int federatedx_handler_base::init_scan_() { THD *thd= query_table->in_use; diff --git a/storage/federatedx/federatedx_pushdown.h b/storage/federatedx/federatedx_pushdown.h index cea1192b9193b..76a1f4134d8aa 100644 --- a/storage/federatedx/federatedx_pushdown.h +++ b/storage/federatedx/federatedx_pushdown.h @@ -54,6 +54,20 @@ class ha_federatedx_derived_handler: public derived_handler, public federatedx_h }; +/* + The query type a statement is printed back with before it is sent to the + remote server. QT_SELECT_ONLY makes SELECT_LEX::print() print only the + SELECT part of a statement, so it is set for a select and cleared for a + whole UPDATE/DELETE. +*/ +static constexpr auto FEDERATEDX_PRINT_QUERY_TYPE= + enum_query_type(QT_VIEW_INTERNAL | QT_SELECT_ONLY | + QT_ITEM_ORIGINAL_FUNC_NULLIF | QT_PARSABLE); + +static constexpr auto FEDERATEDX_PRINT_UPD_DEL_QUERY_TYPE= + enum_query_type(FEDERATEDX_PRINT_QUERY_TYPE & ~QT_SELECT_ONLY); + + /* Implementation class of the select_handler interface for FEDERATEDX: class declaration @@ -72,7 +86,25 @@ class ha_federatedx_select_handler: public select_handler, public federatedx_han int end_scan() override; private: - static constexpr auto PRINT_QUERY_TYPE= - enum_query_type(QT_VIEW_INTERNAL | QT_SELECT_ONLY | - QT_ITEM_ORIGINAL_FUNC_NULLIF | QT_PARSABLE); + static constexpr auto PRINT_QUERY_TYPE= FEDERATEDX_PRINT_QUERY_TYPE; }; + + +/* + Implementation class of the multi_upddel_handler interface for FEDERATEDX: + class declaration + + The whole multi-table UPDATE/DELETE is printed back and executed by the + remote server with a single query, see batch_update_delete() +*/ + +class ha_federatedx_multi_upddel_handler: public multi_upddel_handler, + public federatedx_handler_base +{ +public: + ha_federatedx_multi_upddel_handler(THD *thd_arg, LEX *lex_arg, TABLE *tbl); + + int batch_update_delete(ha_rows *found_rows, + ha_rows *affected_rows) override; +}; + diff --git a/storage/federatedx/ha_federatedx.cc b/storage/federatedx/ha_federatedx.cc index ff0ca86abb733..043665d42620d 100644 --- a/storage/federatedx/ha_federatedx.cc +++ b/storage/federatedx/ha_federatedx.cc @@ -421,6 +421,9 @@ create_federatedx_select_handler(THD *thd, SELECT_LEX *sel_lex, static select_handler * create_federatedx_unit_handler(THD *thd, SELECT_LEX_UNIT *sel_unit); +static multi_upddel_handler * +create_federatedx_multi_upddel_handler(THD *thd, LEX *lex); + /* Federated doesn't need costs.disk_read_ratio as everything is one a remote server and nothing is cached locally @@ -470,6 +473,8 @@ int federatedx_db_init(void *p) federatedx_hton->create_select= create_federatedx_select_handler; federatedx_hton->update_optimizer_costs= federatedx_update_optimizer_costs; federatedx_hton->create_unit= create_federatedx_unit_handler; + federatedx_hton->create_multi_upddel= + create_federatedx_multi_upddel_handler; if (mysql_mutex_init(fe_key_mutex_federatedx, &federatedx_mutex, MY_MUTEX_INIT_FAST)) diff --git a/storage/federatedx/ha_federatedx.h b/storage/federatedx/ha_federatedx.h index 7831f394a26b7..d0ff0fb9e9503 100644 --- a/storage/federatedx/ha_federatedx.h +++ b/storage/federatedx/ha_federatedx.h @@ -177,6 +177,13 @@ class federatedx_io virtual size_t max_query_size() const=0; virtual my_ulonglong affected_rows() const=0; + /* + Number of rows that matched the WHERE clause of the last statement. + For an UPDATE this can be larger than affected_rows() (rows that matched + but were left unchanged). When the server does not report it (e.g. for a + DELETE) this returns the same value as affected_rows(). + */ + virtual my_ulonglong matched_rows() const=0; virtual my_ulonglong last_insert_id() const=0; virtual int error_code()=0; @@ -462,6 +469,7 @@ class ha_federatedx final : public handler const FEDERATEDX_SHARE *get_federatedx_share() const { return share; } friend class ha_federatedx_derived_handler; friend class ha_federatedx_select_handler; + friend class ha_federatedx_multi_upddel_handler; friend class federatedx_handler_base; };