From 585785c7bc5a6b97a138d5086d0bbf4c76dc9f6b Mon Sep 17 00:00:00 2001 From: Kristian Nielsen Date: Wed, 23 Jul 2025 00:19:30 +0200 Subject: [PATCH] Binlog-in-engine: Handle mixing transactional and non-transactional tables When updating non-transactional tables inside a multi-statement transaction, and binlog_direct_non_transactional_updates=1, then the non-transactional updates are binlogged directly through the statement cache while the transaction cache is still being added to in the main transaction. Thus, move the engine_binlog_info out from binlog_cache_mngr and into the individual stmt/trx binlog_cache_data, so that we can have separate engine_binlog_info active for the statement and the transaction cache. Signed-off-by: Kristian Nielsen --- .../suite/binlog_in_engine/nontrans.result | 415 ++++++++++++++++++ .../suite/binlog_in_engine/nontrans.test | 67 +++ .../suite/binlog_in_engine/savepoint.result | 23 +- .../suite/binlog_in_engine/savepoint.test | 3 +- sql/handler.h | 4 +- sql/log.cc | 145 +++--- sql/log_cache.h | 14 + storage/innobase/handler/innodb_binlog.cc | 8 +- storage/innobase/include/innodb_binlog.h | 4 +- 9 files changed, 612 insertions(+), 71 deletions(-) create mode 100644 mysql-test/suite/binlog_in_engine/nontrans.result create mode 100644 mysql-test/suite/binlog_in_engine/nontrans.test diff --git a/mysql-test/suite/binlog_in_engine/nontrans.result b/mysql-test/suite/binlog_in_engine/nontrans.result new file mode 100644 index 00000000000..f7917290d9a --- /dev/null +++ b/mysql-test/suite/binlog_in_engine/nontrans.result @@ -0,0 +1,415 @@ +include/master-slave.inc +[connection master] +CREATE TABLE t1(a INT PRIMARY KEY, b INT, c LONGTEXT) ENGINE=InnoDB; +CREATE TABLE t2(a INT PRIMARY KEY, b INT, c LONGTEXT) ENGINE=Aria; +SET @c= REPEAT('*', 20); +SET SESSION binlog_format=statement; +SET SESSION binlog_direct_non_transactional_updates= 0; +INSERT INTO t1 VALUES (1 + 0, 0, @c), (2 + 0, 0, @c), (3 + 0, 0, @c); +INSERT INTO t2 VALUES (1 + 0, 1, @c), (2 + 0, 1, @c), (3 + 0, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+0; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+0; +Warnings: +Note 1592 Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it accesses a non-transactional table after accessing a transactional table within the same transaction +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+0; +INSERT INTO t2 VALUES (4 + 0, 2, @c); +Warnings: +Note 1592 Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it accesses a non-transactional table after accessing a transactional table within the same transaction +COMMIT; +SET SESSION binlog_direct_non_transactional_updates= 1; +INSERT INTO t1 VALUES (1 + 10, 0, @c), (2 + 10, 0, @c), (3 + 10, 0, @c); +INSERT INTO t2 VALUES (1 + 10, 1, @c), (2 + 10, 1, @c), (3 + 10, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+10; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+10; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+10; +INSERT INTO t2 VALUES (4 + 10, 2, @c); +COMMIT; +SET SESSION binlog_format=row; +SET SESSION binlog_direct_non_transactional_updates= 0; +INSERT INTO t1 VALUES (1 + 100, 0, @c), (2 + 100, 0, @c), (3 + 100, 0, @c); +INSERT INTO t2 VALUES (1 + 100, 1, @c), (2 + 100, 1, @c), (3 + 100, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+100; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+100; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+100; +INSERT INTO t2 VALUES (4 + 100, 2, @c); +COMMIT; +SET SESSION binlog_direct_non_transactional_updates= 1; +INSERT INTO t1 VALUES (1 + 110, 0, @c), (2 + 110, 0, @c), (3 + 110, 0, @c); +INSERT INTO t2 VALUES (1 + 110, 1, @c), (2 + 110, 1, @c), (3 + 110, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+110; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+110; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+110; +INSERT INTO t2 VALUES (4 + 110, 2, @c); +COMMIT; +SET @c= REPEAT('%', 1024); +SET SESSION binlog_format=statement; +SET SESSION binlog_direct_non_transactional_updates= 0; +INSERT INTO t1 VALUES (1 + 1000, 0, @c), (2 + 1000, 0, @c), (3 + 1000, 0, @c); +INSERT INTO t2 VALUES (1 + 1000, 1, @c), (2 + 1000, 1, @c), (3 + 1000, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+1000; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+1000; +Warnings: +Note 1592 Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it accesses a non-transactional table after accessing a transactional table within the same transaction +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+1000; +INSERT INTO t2 VALUES (4 + 1000, 2, @c); +Warnings: +Note 1592 Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it accesses a non-transactional table after accessing a transactional table within the same transaction +COMMIT; +SET SESSION binlog_direct_non_transactional_updates= 1; +INSERT INTO t1 VALUES (1 + 1010, 0, @c), (2 + 1010, 0, @c), (3 + 1010, 0, @c); +INSERT INTO t2 VALUES (1 + 1010, 1, @c), (2 + 1010, 1, @c), (3 + 1010, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+1010; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+1010; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+1010; +INSERT INTO t2 VALUES (4 + 1010, 2, @c); +COMMIT; +SET SESSION binlog_format=row; +SET SESSION binlog_direct_non_transactional_updates= 0; +INSERT INTO t1 VALUES (1 + 1100, 0, @c), (2 + 1100, 0, @c), (3 + 1100, 0, @c); +INSERT INTO t2 VALUES (1 + 1100, 1, @c), (2 + 1100, 1, @c), (3 + 1100, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+1100; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+1100; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+1100; +INSERT INTO t2 VALUES (4 + 1100, 2, @c); +COMMIT; +SET SESSION binlog_direct_non_transactional_updates= 1; +INSERT INTO t1 VALUES (1 + 1110, 0, @c), (2 + 1110, 0, @c), (3 + 1110, 0, @c); +INSERT INTO t2 VALUES (1 + 1110, 1, @c), (2 + 1110, 1, @c), (3 + 1110, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+1110; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+1110; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+1110; +INSERT INTO t2 VALUES (4 + 1110, 2, @c); +COMMIT; +SET @c= REPEAT('.', 18000); +SET SESSION binlog_format=statement; +SET SESSION binlog_direct_non_transactional_updates= 0; +INSERT INTO t1 VALUES (1 + 2000, 0, @c), (2 + 2000, 0, @c), (3 + 2000, 0, @c); +INSERT INTO t2 VALUES (1 + 2000, 1, @c), (2 + 2000, 1, @c), (3 + 2000, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+2000; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+2000; +Warnings: +Note 1592 Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it accesses a non-transactional table after accessing a transactional table within the same transaction +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+2000; +INSERT INTO t2 VALUES (4 + 2000, 2, @c); +Warnings: +Note 1592 Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it accesses a non-transactional table after accessing a transactional table within the same transaction +COMMIT; +SET SESSION binlog_direct_non_transactional_updates= 1; +INSERT INTO t1 VALUES (1 + 2010, 0, @c), (2 + 2010, 0, @c), (3 + 2010, 0, @c); +INSERT INTO t2 VALUES (1 + 2010, 1, @c), (2 + 2010, 1, @c), (3 + 2010, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+2010; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+2010; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+2010; +INSERT INTO t2 VALUES (4 + 2010, 2, @c); +COMMIT; +SET SESSION binlog_format=row; +SET SESSION binlog_direct_non_transactional_updates= 0; +INSERT INTO t1 VALUES (1 + 2100, 0, @c), (2 + 2100, 0, @c), (3 + 2100, 0, @c); +INSERT INTO t2 VALUES (1 + 2100, 1, @c), (2 + 2100, 1, @c), (3 + 2100, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+2100; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+2100; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+2100; +INSERT INTO t2 VALUES (4 + 2100, 2, @c); +COMMIT; +SET SESSION binlog_direct_non_transactional_updates= 1; +INSERT INTO t1 VALUES (1 + 2110, 0, @c), (2 + 2110, 0, @c), (3 + 2110, 0, @c); +INSERT INTO t2 VALUES (1 + 2110, 1, @c), (2 + 2110, 1, @c), (3 + 2110, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+2110; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+2110; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+2110; +INSERT INTO t2 VALUES (4 + 2110, 2, @c); +COMMIT; +SET @c= REPEAT('.', 40000); +SET SESSION binlog_format=statement; +SET SESSION binlog_direct_non_transactional_updates= 0; +INSERT INTO t1 VALUES (1 + 3000, 0, @c), (2 + 3000, 0, @c), (3 + 3000, 0, @c); +INSERT INTO t2 VALUES (1 + 3000, 1, @c), (2 + 3000, 1, @c), (3 + 3000, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+3000; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+3000; +Warnings: +Note 1592 Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it accesses a non-transactional table after accessing a transactional table within the same transaction +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+3000; +INSERT INTO t2 VALUES (4 + 3000, 2, @c); +Warnings: +Note 1592 Unsafe statement written to the binary log using statement format since BINLOG_FORMAT = STATEMENT. Statement is unsafe because it accesses a non-transactional table after accessing a transactional table within the same transaction +COMMIT; +SET SESSION binlog_direct_non_transactional_updates= 1; +INSERT INTO t1 VALUES (1 + 3010, 0, @c), (2 + 3010, 0, @c), (3 + 3010, 0, @c); +INSERT INTO t2 VALUES (1 + 3010, 1, @c), (2 + 3010, 1, @c), (3 + 3010, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+3010; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+3010; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+3010; +INSERT INTO t2 VALUES (4 + 3010, 2, @c); +COMMIT; +SET SESSION binlog_format=row; +SET SESSION binlog_direct_non_transactional_updates= 0; +INSERT INTO t1 VALUES (1 + 3100, 0, @c), (2 + 3100, 0, @c), (3 + 3100, 0, @c); +INSERT INTO t2 VALUES (1 + 3100, 1, @c), (2 + 3100, 1, @c), (3 + 3100, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+3100; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+3100; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+3100; +INSERT INTO t2 VALUES (4 + 3100, 2, @c); +COMMIT; +SET SESSION binlog_direct_non_transactional_updates= 1; +INSERT INTO t1 VALUES (1 + 3110, 0, @c), (2 + 3110, 0, @c), (3 + 3110, 0, @c); +INSERT INTO t2 VALUES (1 + 3110, 1, @c), (2 + 3110, 1, @c), (3 + 3110, 1, @c); +BEGIN; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+3110; +UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+3110; +UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+3110; +INSERT INTO t2 VALUES (4 + 3110, 2, @c); +COMMIT; +SELECT a, b, length(c) FROM t1 ORDER BY a; +a b length(c) +1 2 21 +2 0 20 +3 2 21 +11 2 21 +12 0 20 +13 2 21 +101 2 21 +102 0 20 +103 2 21 +111 2 21 +112 0 20 +113 2 21 +1001 2 1025 +1002 0 1024 +1003 2 1025 +1011 2 1025 +1012 0 1024 +1013 2 1025 +1101 2 1025 +1102 0 1024 +1103 2 1025 +1111 2 1025 +1112 0 1024 +1113 2 1025 +2001 2 18001 +2002 0 18000 +2003 2 18001 +2011 2 18001 +2012 0 18000 +2013 2 18001 +2101 2 18001 +2102 0 18000 +2103 2 18001 +2111 2 18001 +2112 0 18000 +2113 2 18001 +3001 2 40001 +3002 0 40000 +3003 2 40001 +3011 2 40001 +3012 0 40000 +3013 2 40001 +3101 2 40001 +3102 0 40000 +3103 2 40001 +3111 2 40001 +3112 0 40000 +3113 2 40001 +SELECT a, b, length(c) FROM t2 ORDER BY a; +a b length(c) +1 1 20 +2 2 21 +3 1 20 +4 2 20 +11 1 20 +12 2 21 +13 1 20 +14 2 20 +101 1 20 +102 2 21 +103 1 20 +104 2 20 +111 1 20 +112 2 21 +113 1 20 +114 2 20 +1001 1 1024 +1002 2 1025 +1003 1 1024 +1004 2 1024 +1011 1 1024 +1012 2 1025 +1013 1 1024 +1014 2 1024 +1101 1 1024 +1102 2 1025 +1103 1 1024 +1104 2 1024 +1111 1 1024 +1112 2 1025 +1113 1 1024 +1114 2 1024 +2001 1 18000 +2002 2 18001 +2003 1 18000 +2004 2 18000 +2011 1 18000 +2012 2 18001 +2013 1 18000 +2014 2 18000 +2101 1 18000 +2102 2 18001 +2103 1 18000 +2104 2 18000 +2111 1 18000 +2112 2 18001 +2113 1 18000 +2114 2 18000 +3001 1 40000 +3002 2 40001 +3003 1 40000 +3004 2 40000 +3011 1 40000 +3012 2 40001 +3013 1 40000 +3014 2 40000 +3101 1 40000 +3102 2 40001 +3103 1 40000 +3104 2 40000 +3111 1 40000 +3112 2 40001 +3113 1 40000 +3114 2 40000 +include/save_master_gtid.inc +connection slave; +include/sync_with_master_gtid.inc +SELECT a, b, length(c) FROM t1 ORDER BY a; +a b length(c) +1 2 21 +2 0 20 +3 2 21 +11 2 21 +12 0 20 +13 2 21 +101 2 21 +102 0 20 +103 2 21 +111 2 21 +112 0 20 +113 2 21 +1001 2 1025 +1002 0 1024 +1003 2 1025 +1011 2 1025 +1012 0 1024 +1013 2 1025 +1101 2 1025 +1102 0 1024 +1103 2 1025 +1111 2 1025 +1112 0 1024 +1113 2 1025 +2001 2 18001 +2002 0 18000 +2003 2 18001 +2011 2 18001 +2012 0 18000 +2013 2 18001 +2101 2 18001 +2102 0 18000 +2103 2 18001 +2111 2 18001 +2112 0 18000 +2113 2 18001 +3001 2 40001 +3002 0 40000 +3003 2 40001 +3011 2 40001 +3012 0 40000 +3013 2 40001 +3101 2 40001 +3102 0 40000 +3103 2 40001 +3111 2 40001 +3112 0 40000 +3113 2 40001 +SELECT a, b, length(c) FROM t2 ORDER BY a; +a b length(c) +1 1 20 +2 2 21 +3 1 20 +4 2 20 +11 1 20 +12 2 21 +13 1 20 +14 2 20 +101 1 20 +102 2 21 +103 1 20 +104 2 20 +111 1 20 +112 2 21 +113 1 20 +114 2 20 +1001 1 1024 +1002 2 1025 +1003 1 1024 +1004 2 1024 +1011 1 1024 +1012 2 1025 +1013 1 1024 +1014 2 1024 +1101 1 1024 +1102 2 1025 +1103 1 1024 +1104 2 1024 +1111 1 1024 +1112 2 1025 +1113 1 1024 +1114 2 1024 +2001 1 18000 +2002 2 18001 +2003 1 18000 +2004 2 18000 +2011 1 18000 +2012 2 18001 +2013 1 18000 +2014 2 18000 +2101 1 18000 +2102 2 18001 +2103 1 18000 +2104 2 18000 +2111 1 18000 +2112 2 18001 +2113 1 18000 +2114 2 18000 +3001 1 40000 +3002 2 40001 +3003 1 40000 +3004 2 40000 +3011 1 40000 +3012 2 40001 +3013 1 40000 +3014 2 40000 +3101 1 40000 +3102 2 40001 +3103 1 40000 +3104 2 40000 +3111 1 40000 +3112 2 40001 +3113 1 40000 +3114 2 40000 +connection master; +DROP TABLE t1, t2; +CALL mtr.add_suppression('Statement is unsafe because it accesses a non-transactional table after accessing a transactional table'); +include/rpl_end.inc diff --git a/mysql-test/suite/binlog_in_engine/nontrans.test b/mysql-test/suite/binlog_in_engine/nontrans.test new file mode 100644 index 00000000000..5506d3e9ab1 --- /dev/null +++ b/mysql-test/suite/binlog_in_engine/nontrans.test @@ -0,0 +1,67 @@ +--source include/master-slave.inc +--source include/have_binlog_format_mixed.inc +--source include/have_innodb_binlog.inc + +CREATE TABLE t1(a INT PRIMARY KEY, b INT, c LONGTEXT) ENGINE=InnoDB; +CREATE TABLE t2(a INT PRIMARY KEY, b INT, c LONGTEXT) ENGINE=Aria; + +--let $i= 0 +while ($i <= 3) { + if ($i == 0) { + SET @c= REPEAT('*', 20); + } + if ($i == 1) { + SET @c= REPEAT('%', 1024); + } + if ($i == 2) { + SET @c= REPEAT('.', 18000); + } + if ($i == 3) { + SET @c= REPEAT('.', 40000); + } + + --let $f= 0 + while ($f <= 1) { + if ($f == 0) { + SET SESSION binlog_format=statement; + } + if ($f == 1) { + SET SESSION binlog_format=row; + } + + --let $s= 0 + while ($s <= 1) { + --let $k = `SELECT $i*1000 + $f*100 + $s*10` + eval SET SESSION binlog_direct_non_transactional_updates= $s; + + eval INSERT INTO t1 VALUES (1 + $k, 0, @c), (2 + $k, 0, @c), (3 + $k, 0, @c); + eval INSERT INTO t2 VALUES (1 + $k, 1, @c), (2 + $k, 1, @c), (3 + $k, 1, @c); + + BEGIN; + eval UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=1+$k; + eval UPDATE t2 SET b=2, c=CONCAT('!', c) WHERE a=2+$k; + eval UPDATE t1 SET b=2, c=CONCAT('!', c) WHERE a=3+$k; + eval INSERT INTO t2 VALUES (4 + $k, 2, @c); + COMMIT; + inc $s; + } + inc $f; + } + inc $i; +} + +SELECT a, b, length(c) FROM t1 ORDER BY a; +SELECT a, b, length(c) FROM t2 ORDER BY a; + +--source include/save_master_gtid.inc +--connection slave +--source include/sync_with_master_gtid.inc +SELECT a, b, length(c) FROM t1 ORDER BY a; +SELECT a, b, length(c) FROM t2 ORDER BY a; + +--connection master +DROP TABLE t1, t2; + +CALL mtr.add_suppression('Statement is unsafe because it accesses a non-transactional table after accessing a transactional table'); + +--source include/rpl_end.inc diff --git a/mysql-test/suite/binlog_in_engine/savepoint.result b/mysql-test/suite/binlog_in_engine/savepoint.result index 180e68a07ce..da735d8b5b0 100644 --- a/mysql-test/suite/binlog_in_engine/savepoint.result +++ b/mysql-test/suite/binlog_in_engine/savepoint.result @@ -1,7 +1,7 @@ include/master-slave.inc [connection master] CREATE TABLE t1 (i INT, a INT, b TEXT, PRIMARY KEY(i, a)) ENGINE=InnoDB; -CREATE TABLE t2 (i INT, a INT, b TEXT, PRIMARY KEY(i, a)) ENGINE=InnoDB; +CREATE TABLE t2 (i INT, a INT, b TEXT, PRIMARY KEY(i, a)) ENGINE=MyISAM; SET @b= REPEAT('$', 0); BEGIN; INSERT INTO t1 VALUES (0, 1, @b); @@ -34,12 +34,15 @@ SAVEPOINT s10; INSERT INTO t1 VALUES (0, 11, @b); INSERT INTO t2 VALUES (0, 12, @b); ROLLBACK TO s10; +Warnings: +Warning 1196 Some non-transactional changed tables couldn't be rolled back COMMIT; SELECT a, length(b) FROM t1 WHERE i=0 AND a>=10 ORDER BY a; a length(b) 10 0 SELECT a, length(b) FROM t2 WHERE i=0 ORDER BY a; a length(b) +12 0 BEGIN; UPDATE t1 SET a=a+1000 WHERE i=0; UPDATE t1 SET b='x' WHERE i=0; @@ -90,12 +93,15 @@ SAVEPOINT s10; INSERT INTO t1 VALUES (1, 11, @b); INSERT INTO t2 VALUES (1, 12, @b); ROLLBACK TO s10; +Warnings: +Warning 1196 Some non-transactional changed tables couldn't be rolled back COMMIT; SELECT a, length(b) FROM t1 WHERE i=1 AND a>=10 ORDER BY a; a length(b) 10 10 SELECT a, length(b) FROM t2 WHERE i=1 ORDER BY a; a length(b) +12 10 BEGIN; UPDATE t1 SET a=a+1000 WHERE i=1; UPDATE t1 SET b='x' WHERE i=1; @@ -146,12 +152,15 @@ SAVEPOINT s10; INSERT INTO t1 VALUES (2, 11, @b); INSERT INTO t2 VALUES (2, 12, @b); ROLLBACK TO s10; +Warnings: +Warning 1196 Some non-transactional changed tables couldn't be rolled back COMMIT; SELECT a, length(b) FROM t1 WHERE i=2 AND a>=10 ORDER BY a; a length(b) 10 100 SELECT a, length(b) FROM t2 WHERE i=2 ORDER BY a; a length(b) +12 100 BEGIN; UPDATE t1 SET a=a+1000 WHERE i=2; UPDATE t1 SET b='x' WHERE i=2; @@ -202,12 +211,15 @@ SAVEPOINT s10; INSERT INTO t1 VALUES (3, 11, @b); INSERT INTO t2 VALUES (3, 12, @b); ROLLBACK TO s10; +Warnings: +Warning 1196 Some non-transactional changed tables couldn't be rolled back COMMIT; SELECT a, length(b) FROM t1 WHERE i=3 AND a>=10 ORDER BY a; a length(b) 10 642 SELECT a, length(b) FROM t2 WHERE i=3 ORDER BY a; a length(b) +12 642 BEGIN; UPDATE t1 SET a=a+1000 WHERE i=3; UPDATE t1 SET b='x' WHERE i=3; @@ -258,12 +270,15 @@ SAVEPOINT s10; INSERT INTO t1 VALUES (4, 11, @b); INSERT INTO t2 VALUES (4, 12, @b); ROLLBACK TO s10; +Warnings: +Warning 1196 Some non-transactional changed tables couldn't be rolled back COMMIT; SELECT a, length(b) FROM t1 WHERE i=4 AND a>=10 ORDER BY a; a length(b) 10 3930 SELECT a, length(b) FROM t2 WHERE i=4 ORDER BY a; a length(b) +12 3930 BEGIN; UPDATE t1 SET a=a+1000 WHERE i=4; UPDATE t1 SET b='x' WHERE i=4; @@ -314,12 +329,15 @@ SAVEPOINT s10; INSERT INTO t1 VALUES (5, 11, @b); INSERT INTO t2 VALUES (5, 12, @b); ROLLBACK TO s10; +Warnings: +Warning 1196 Some non-transactional changed tables couldn't be rolled back COMMIT; SELECT a, length(b) FROM t1 WHERE i=5 AND a>=10 ORDER BY a; a length(b) 10 16000 SELECT a, length(b) FROM t2 WHERE i=5 ORDER BY a; a length(b) +12 16000 BEGIN; UPDATE t1 SET a=a+1000 WHERE i=5; UPDATE t1 SET b='x' WHERE i=5; @@ -370,12 +388,15 @@ SAVEPOINT s10; INSERT INTO t1 VALUES (6, 11, @b); INSERT INTO t2 VALUES (6, 12, @b); ROLLBACK TO s10; +Warnings: +Warning 1196 Some non-transactional changed tables couldn't be rolled back COMMIT; SELECT a, length(b) FROM t1 WHERE i=6 AND a>=10 ORDER BY a; a length(b) 10 40000 SELECT a, length(b) FROM t2 WHERE i=6 ORDER BY a; a length(b) +12 40000 BEGIN; UPDATE t1 SET a=a+1000 WHERE i=6; UPDATE t1 SET b='x' WHERE i=6; diff --git a/mysql-test/suite/binlog_in_engine/savepoint.test b/mysql-test/suite/binlog_in_engine/savepoint.test index b9077236846..8836cb5124a 100644 --- a/mysql-test/suite/binlog_in_engine/savepoint.test +++ b/mysql-test/suite/binlog_in_engine/savepoint.test @@ -3,8 +3,7 @@ --source include/have_innodb_binlog.inc CREATE TABLE t1 (i INT, a INT, b TEXT, PRIMARY KEY(i, a)) ENGINE=InnoDB; -# ToDo CREATE TABLE t2 (i INT, a INT, b TEXT, PRIMARY KEY(i, a)) ENGINE=MyISAM; -CREATE TABLE t2 (i INT, a INT, b TEXT, PRIMARY KEY(i, a)) ENGINE=InnoDB; +CREATE TABLE t2 (i INT, a INT, b TEXT, PRIMARY KEY(i, a)) ENGINE=MyISAM; # Add different amounts of data, to test various cases where event # groups fit or do not fit in case, are binlogged / not binlogged as diff --git a/sql/handler.h b/sql/handler.h index 2de9a1c16cd..786ac9d95bc 100644 --- a/sql/handler.h +++ b/sql/handler.h @@ -1599,9 +1599,9 @@ struct handlerton binlog_oob_data(). Can also change the pointer to point to different data (or set it to NULL). */ - void (*binlog_oob_reset)(THD *thd, void **engine_data); + void (*binlog_oob_reset)(void **engine_data); /* Call to allow engine to release the engine_data from binlog_oob_data(). */ - void (*binlog_oob_free)(THD *thd, void *engine_data); + void (*binlog_oob_free)(void *engine_data); /* Obtain an object to allow reading from the binlog. The boolean argument wait_durable is set to true to require that diff --git a/sql/log.cc b/sql/log.cc index c7956853bbb..ccc0cf50cc9 100644 --- a/sql/log.cc +++ b/sql/log.cc @@ -394,7 +394,6 @@ public: stmt_start_engine_ptr(nullptr), cache_savepoint_list(nullptr), cache_savepoint_next_ptr(&cache_savepoint_list), - engine_binlog_info {0, 0, 0}, using_xa(FALSE), xa_xid(0) { stmt_cache.set_binlog_cache_info(param_max_binlog_stmt_cache_size, @@ -410,24 +409,37 @@ public: } ~binlog_cache_mngr() { - if (engine_binlog_info.engine_ptr) - (*opt_binlog_engine_hton->binlog_oob_free) - (thd, engine_binlog_info.engine_ptr); } void reset(bool do_stmt, bool do_trx) { - if (engine_binlog_info.engine_ptr) - (*opt_binlog_engine_hton->binlog_oob_reset) - (thd, &engine_binlog_info.engine_ptr); if (do_stmt) - stmt_cache.reset(); + { + if (opt_binlog_engine_hton) + { + stmt_cache.reset_for_engine_binlog(); + /* + Use a custom write_function to spill to the engine-implemented binlog. + And re-use the IO_CACHE::append_read_pos as a handle for our + write_function; it is unused when the cache is not SEQ_READ_APPEND. + */ + stmt_cache.cache_log.write_function= binlog_spill_to_engine; + stmt_cache.cache_log.append_read_pos= (uchar *)this; + } + else + stmt_cache.reset(); + } if (do_trx) { if (opt_binlog_engine_hton) { trx_cache.reset_for_engine_binlog(); + trx_cache.cache_log.write_function= binlog_spill_to_engine; + trx_cache.cache_log.append_read_pos= (uchar *)this; last_commit_pos_file.engine_file_no= ~(uint64_t)0; + stmt_start_engine_ptr= nullptr; + cache_savepoint_list= nullptr; + cache_savepoint_next_ptr= &cache_savepoint_list; } else { @@ -437,23 +449,6 @@ public: last_commit_pos_offset= 0; using_xa= FALSE; } - if (likely(opt_binlog_engine_hton) && - likely(opt_binlog_engine_hton->binlog_oob_data)) - { - stmt_start_engine_ptr= nullptr; - cache_savepoint_list= nullptr; - cache_savepoint_next_ptr= &cache_savepoint_list; - /* - Use a custom write_function to spill to the engine-implemented binlog. - And re-use the IO_CACHE::append_read_pos as a handle for our - write_function; it is unused when the cache is not SEQ_READ_APPEND. - */ - trx_cache.cache_log.write_function= binlog_spill_to_engine; - trx_cache.cache_log.append_read_pos= (uchar *)this; - engine_binlog_info.out_of_band_offset= 0; - engine_binlog_info.gtid_offset= 0; - /* Preserve the engine_ptr for the engine to re-use, was reset above. */ - } } binlog_cache_data* get_binlog_cache_data(bool is_transactional) @@ -496,8 +491,6 @@ public: */ binlog_savepoint_info *cache_savepoint_list; binlog_savepoint_info **cache_savepoint_next_ptr; - /* Context for engine-implemented binlogging. */ - handler_binlog_event_group_info engine_binlog_info; /* Flag set true if this transaction is committed with log_xid() as part of @@ -1810,9 +1803,10 @@ binlog_trans_log_truncate(THD *thd, binlog_savepoint_info *sv) /* No pending savepoints in-cache anymore. */ cache_mngr->cache_savepoint_next_ptr= &cache_mngr->cache_savepoint_list; cache_mngr->cache_savepoint_list= nullptr; - cache_mngr->engine_binlog_info.out_of_band_offset= sv->cache_offset; + cache_mngr->trx_cache.engine_binlog_info.out_of_band_offset= sv->cache_offset; (*opt_binlog_engine_hton->binlog_savepoint_rollback) - (thd, &cache_mngr->engine_binlog_info.engine_ptr, nullptr, &sv->engine_ptr); + (thd, &cache_mngr->trx_cache.engine_binlog_info.engine_ptr, + nullptr, &sv->engine_ptr); DBUG_VOID_RETURN; } @@ -1950,7 +1944,7 @@ binlog_flush_cache(THD *thd, binlog_cache_mngr *cache_mngr, if (mysql_bin_log.write_event(end_ev, cache_data, &cache_data->cache_log)) DBUG_RETURN(1); - if (cache_mngr->engine_binlog_info.out_of_band_offset) + if (cache_data->engine_binlog_info.out_of_band_offset) { /* This is a "large" transaction, where parts of the transaction were @@ -1960,7 +1954,7 @@ binlog_flush_cache(THD *thd, binlog_cache_mngr *cache_mngr, group is consecutive out-of-band data and the commit record will only contain the GTID event (depending on engine implementation). */ - if (my_b_flush_io_cache(&cache_mngr->trx_cache.cache_log, 0)) + if (my_b_flush_io_cache(&cache_data->cache_log, 0)) DBUG_RETURN(1); } } @@ -2018,9 +2012,10 @@ binlog_get_cache(THD *thd, uint64_t file_no, uint64_t offset, { cache_mngr->last_commit_pos_file.engine_file_no= file_no; cache_mngr->last_commit_pos_offset= offset; - context= &cache_mngr->engine_binlog_info; - cache= !cache_mngr->trx_cache.empty() ? - &cache_mngr->trx_cache.cache_log : &cache_mngr->stmt_cache.cache_log; + binlog_cache_data *cache_data= !cache_mngr->trx_cache.empty() ? + &cache_mngr->trx_cache : &cache_mngr->stmt_cache; + context= &cache_data->engine_binlog_info; + cache= &cache_data->cache_log; gtid= thd->get_last_commit_gtid(); } *out_cache= cache; @@ -2227,9 +2222,9 @@ binlog_truncate_trx_cache(THD *thd, binlog_cache_mngr *cache_mngr, bool all) trx_cache.set_prev_position(cache->pos_in_file); trx_cache.restore_prev_position(); trx_cache.reset_cache_for_engine(stmt_pos, binlog_spill_to_engine); - cache_mngr->engine_binlog_info.out_of_band_offset= stmt_pos; + cache_mngr->trx_cache.engine_binlog_info.out_of_band_offset= stmt_pos; (*opt_binlog_engine_hton->binlog_savepoint_rollback) - (thd, &cache_mngr->engine_binlog_info.engine_ptr, + (thd, &cache_mngr->trx_cache.engine_binlog_info.engine_ptr, &cache_mngr->stmt_start_engine_ptr, nullptr); } } @@ -6695,7 +6690,9 @@ binlog_spill_to_engine(struct st_io_cache *cache, const uchar *data, size_t len) } binlog_cache_mngr *mngr= (binlog_cache_mngr *)cache->append_read_pos; - void **engine_ptr= &mngr->engine_binlog_info.engine_ptr; + binlog_cache_data *cache_data= unlikely(cache==&mngr->stmt_cache.cache_log) ? + &mngr->stmt_cache : &mngr->trx_cache; + void **engine_ptr= &cache_data->engine_binlog_info.engine_ptr; mysql_mutex_assert_not_owner(&LOCK_commit_ordered); size_t max_len= std::min(binlog_max_spill_size, (size_t)binlog_cache_size); @@ -6826,7 +6823,7 @@ binlog_spill_to_engine(struct st_io_cache *cache, const uchar *data, size_t len) sofar+= part_len; } while (sofar < len); - mngr->engine_binlog_info.out_of_band_offset+= len; + cache_data->engine_binlog_info.out_of_band_offset+= len; cache->pos_in_file= spill_end; return false; @@ -6843,17 +6840,11 @@ static binlog_cache_mngr *binlog_setup_cache_mngr(THD *thd) auto *cache_mngr= (binlog_cache_mngr*) my_malloc(key_memory_binlog_cache_mngr, sizeof(binlog_cache_mngr), MYF(MY_ZEROFILL)); - if (!cache_mngr || - open_cached_file(&cache_mngr->stmt_cache.cache_log, mysql_tmpdir, - LOG_PREFIX, (size_t)binlog_stmt_cache_size, MYF(MY_WME))) - { - my_free(cache_mngr); + if (!cache_mngr) return NULL; - } - IO_CACHE *trx_cache= &cache_mngr->trx_cache.cache_log; + IO_CACHE *stmt_cache= &cache_mngr->stmt_cache.cache_log; my_bool res; - if (likely(opt_binlog_engine_hton) && - likely(opt_binlog_engine_hton->binlog_oob_data)) + if (opt_binlog_engine_hton) { /* With binlog implementation in engine, we do not need to spill large @@ -6861,13 +6852,31 @@ static binlog_cache_mngr *binlog_setup_cache_mngr(THD *thd) through the binlog as the transaction runs. Setting the file to INT_MIN makes IO_CACHE not attempt to create the temporary file. */ - res= init_io_cache(trx_cache, (File)INT_MIN, (size_t)binlog_cache_size, + res= init_io_cache(stmt_cache, (File)INT_MIN, + (size_t)binlog_stmt_cache_size, WRITE_CACHE, 0L, 0, MYF(MY_WME | MY_NABP)); /* Use a custom write_function to spill to the engine-implemented binlog. And re-use the IO_CACHE::append_read_pos as a handle for our write_function; it is unused when the cache is not SEQ_READ_APPEND. */ + stmt_cache->write_function= binlog_spill_to_engine; + stmt_cache->append_read_pos= (uchar *)cache_mngr; + } + else + res= open_cached_file(&cache_mngr->stmt_cache.cache_log, mysql_tmpdir, + LOG_PREFIX, (size_t)binlog_stmt_cache_size, + MYF(MY_WME)); + if (unlikely(res)) + { + my_free(cache_mngr); + return NULL; + } + IO_CACHE *trx_cache= &cache_mngr->trx_cache.cache_log; + if (opt_binlog_engine_hton) + { + res= init_io_cache(trx_cache, (File)INT_MIN, (size_t)binlog_cache_size, + WRITE_CACHE, 0L, 0, MYF(MY_WME | MY_NABP)); trx_cache->write_function= binlog_spill_to_engine; trx_cache->append_read_pos= (uchar *)cache_mngr; } @@ -8111,7 +8120,7 @@ err: if (opt_binlog_engine_hton) { handler_binlog_event_group_info *engine_context= - &cache_mngr->engine_binlog_info; + &cache_data->engine_binlog_info; engine_context->gtid_offset= my_b_tell(file); my_off_t binlog_total_bytes; MDL_request mdl_request; @@ -9239,6 +9248,14 @@ MYSQL_BIN_LOG::write_transaction_to_binlog(THD *thd, else break; } + /* + If not going through log_and_order(), we are not going to go through + commit_ordered(), and the engine will not binlog for us as part of its + own internal transaction commit. So we will need to binlog explicitly. + (This occurs when mixing transactional and non-transactional DML in the + same event group). + */ + entry.auto_binlog= entry.auto_binlog && cache_mngr->using_xa; if (cache_mngr->stmt_cache.has_incident() || cache_mngr->trx_cache.has_incident()) @@ -10083,10 +10100,11 @@ MYSQL_BIN_LOG::trx_group_commit_leader(group_commit_entry *leader) { set_current_thd(current->thd); binlog_cache_mngr *cache_mngr= current->cache_mngr; - IO_CACHE *file= - cache_mngr->get_binlog_cache_log(current->using_trx_cache); + binlog_cache_data *cache_data= + cache_mngr->get_binlog_cache_data(current->using_trx_cache); + IO_CACHE *file= &cache_data->cache_log; handler_binlog_event_group_info *engine_context= - &cache_mngr->engine_binlog_info; + &cache_data->engine_binlog_info; if (likely(!current->error)) current->error= (*opt_binlog_engine_hton->binlog_write_direct_ordered) (file, engine_context, current->thd->get_last_commit_gtid()); @@ -10165,10 +10183,11 @@ MYSQL_BIN_LOG::trx_group_commit_leader(group_commit_entry *leader) { set_current_thd(current->thd); binlog_cache_mngr *cache_mngr= current->cache_mngr; - IO_CACHE *file= - cache_mngr->get_binlog_cache_log(current->using_trx_cache); + binlog_cache_data *cache_data= + cache_mngr->get_binlog_cache_data(current->using_trx_cache); + IO_CACHE *file= &cache_data->cache_log; handler_binlog_event_group_info *engine_context= - &cache_mngr->engine_binlog_info; + &cache_data->engine_binlog_info; if (likely(!current->error)) current->error= (*opt_binlog_engine_hton->binlog_write_direct) (file, engine_context, current->thd->get_last_commit_gtid()); @@ -10176,8 +10195,11 @@ MYSQL_BIN_LOG::trx_group_commit_leader(group_commit_entry *leader) set_current_thd(leader->thd); } + binlog_cache_data *cache_data= + last_in_queue->cache_mngr->get_binlog_cache_data + (last_in_queue->using_trx_cache); (*opt_binlog_engine_hton->binlog_group_commit_ordered) - (last_in_queue->thd, &last_in_queue->cache_mngr->engine_binlog_info); + (last_in_queue->thd, &cache_data->engine_binlog_info); } else if (check_purge) checkpoint_and_purge(binlog_id); @@ -10281,16 +10303,17 @@ MYSQL_BIN_LOG::write_transaction_or_stmt(group_commit_entry *entry, DBUG_ASSERT((entry->using_stmt_cache && !mngr->stmt_cache.empty()) || (entry->using_trx_cache && !mngr->trx_cache.empty()) /* Assert that empty transaction is handled elsewhere. */); - IO_CACHE *cache= (entry->using_trx_cache && !mngr->trx_cache.empty()) ? - &mngr->trx_cache.cache_log : &mngr->stmt_cache.cache_log; + binlog_cache_data *cache_data= + (entry->using_trx_cache && !mngr->trx_cache.empty()) ? + &mngr->trx_cache : &mngr->stmt_cache; /* The GTID event cannot go first since we only allocate the GTID at binlog time. So write the GTID at the very end, and record its offset so that the engine can pick it out and binlog it at the start. */ - mngr->engine_binlog_info.gtid_offset= my_b_tell(cache); - if (write_gtid_event(entry->thd, cache, is_prepared_xa(entry->thd), - false, + cache_data->engine_binlog_info.gtid_offset= my_b_tell(&cache_data->cache_log); + if (write_gtid_event(entry->thd, &cache_data->cache_log, + is_prepared_xa(entry->thd), false, entry->using_trx_cache, commit_id, has_xid, entry->ro_1pc)) DBUG_RETURN(ER_ERROR_ON_WRITE); diff --git a/sql/log_cache.h b/sql/log_cache.h index 1e9e0270c08..b9a27273a14 100644 --- a/sql/log_cache.h +++ b/sql/log_cache.h @@ -27,6 +27,7 @@ class binlog_cache_data { public: binlog_cache_data(bool precompute_checksums): + engine_binlog_info {0, 0, 0}, before_stmt_pos(MY_OFF_T_UNDEF), m_pending(0), status(0), incident(FALSE), precompute_checksums(precompute_checksums), saved_max_binlog_cache_size(0), ptr_binlog_cache_use(0), @@ -45,6 +46,9 @@ public: ~binlog_cache_data() { DBUG_ASSERT(empty()); + if (engine_binlog_info.engine_ptr) + (*opt_binlog_engine_hton->binlog_oob_free) + (engine_binlog_info.engine_ptr); close_cached_file(&cache_log); } @@ -108,6 +112,14 @@ public: void reset_for_engine_binlog() { bool cache_was_empty= empty(); + + if (engine_binlog_info.engine_ptr) + (*opt_binlog_engine_hton->binlog_oob_reset) + (&engine_binlog_info.engine_ptr); + engine_binlog_info.out_of_band_offset= 0; + engine_binlog_info.gtid_offset= 0; + /* Preserve the engine_ptr for the engine to re-use, was reset above. */ + truncate(cache_log.pos_in_file); cache_log.pos_in_file= 0; cache_log.request_pos= cache_log.write_pos= cache_log.buffer; @@ -198,6 +210,8 @@ public: Cache to store data before copying it to the binary log. */ IO_CACHE cache_log; + /* Context for engine-implemented binlogging. */ + handler_binlog_event_group_info engine_binlog_info; protected: /* diff --git a/storage/innobase/handler/innodb_binlog.cc b/storage/innobase/handler/innodb_binlog.cc index bb04816062e..4f445c2ead7 100644 --- a/storage/innobase/handler/innodb_binlog.cc +++ b/storage/innobase/handler/innodb_binlog.cc @@ -291,6 +291,7 @@ public: struct chunk_data_cache : public chunk_data_base { IO_CACHE *cache; binlog_oob_context *oob_ctx; + my_off_t main_start; size_t main_remain; size_t gtid_remain; uint32_t header_remain; @@ -300,6 +301,7 @@ struct chunk_data_cache : public chunk_data_base { chunk_data_cache(IO_CACHE *cache_arg, handler_binlog_event_group_info *binlog_info) : cache(cache_arg), + main_start(binlog_info->out_of_band_offset), main_remain((size_t)(binlog_info->gtid_offset - binlog_info->out_of_band_offset)), header_sofar(0) @@ -381,7 +383,7 @@ struct chunk_data_cache : public chunk_data_base { ut_a(!res2 /* ToDo: Error handling */); gtid_remain-= size2; if (gtid_remain == 0) - my_b_seek(cache, 0); /* Move to read the rest of the events. */ + my_b_seek(cache, main_start); /* Move to read the rest of the events. */ max_len-= size2; size+= size2; if (max_len == 0) @@ -2578,7 +2580,7 @@ ibb_savepoint_rollback(THD *thd, void **engine_data, void -innodb_reset_oob(THD *thd, void **engine_data) +innodb_reset_oob(void **engine_data) { binlog_oob_context *c= (binlog_oob_context *)*engine_data; if (c) @@ -2587,7 +2589,7 @@ innodb_reset_oob(THD *thd, void **engine_data) void -innodb_free_oob(THD *thd, void *engine_data) +innodb_free_oob(void *engine_data) { free_oob_context((binlog_oob_context *)engine_data); } diff --git a/storage/innobase/include/innodb_binlog.h b/storage/innobase/include/innodb_binlog.h index c120c67701d..6dfd09494f7 100644 --- a/storage/innobase/include/innodb_binlog.h +++ b/storage/innobase/include/innodb_binlog.h @@ -243,8 +243,8 @@ extern bool innodb_binlog_oob(THD *thd, const unsigned char *data, size_t data_len, void **engine_data); void ibb_savepoint_rollback(THD *thd, void **engine_data, void **stmt_start_data, void **savepoint_data); -extern void innodb_reset_oob(THD *thd, void **engine_data); -extern void innodb_free_oob(THD *thd, void *engine_data); +extern void innodb_reset_oob(void **engine_data); +extern void innodb_free_oob(void *engine_data); extern handler_binlog_reader *innodb_get_binlog_reader(bool wait_durable); extern void ibb_wait_durable_offset(uint64_t file_no, uint64_t wait_offset); extern void ibb_get_filename(char name[FN_REFLEN], uint64_t file_no);