diff --git a/src/query/storages/fuse/src/retry/commit.rs b/src/query/storages/fuse/src/retry/commit.rs index ff1c8e4d5974d..5834d42a02afa 100644 --- a/src/query/storages/fuse/src/retry/commit.rs +++ b/src/query/storages/fuse/src/retry/commit.rs @@ -153,48 +153,50 @@ async fn try_rebuild_req( if let Some(txn_begin_timestamp) = txn_mgr.get_table_txn_begin_timestamp(latest_table.get_id()) { - let Some(latest_snapshot_timestamp) = latest_snapshot.timestamp() else { - return Err(ErrorCode::UnresolvableConflict(format!( + if let Some(latest_snapshot) = latest_snapshot.as_ref() { + let Some(latest_snapshot_timestamp) = latest_snapshot.timestamp else { + return Err(ErrorCode::UnresolvableConflict(format!( "Table {} snapshot lacks required timestamp. This table was created with a significantly outdated version that is no longer directly supported by the current version and requires migration. Please contact us at https://www.databend.com/contact-us/ or email hi@databend.com", tid ))); - }; - - // By enforcing txn_begin_timestamp >= latest_snapshot_timestamp, we ensure that - // vacuum operations won't remove table data (segment, blocks, etc.) that newly - // created in the current active transaction. - - // In the current transaction, all the newly created table data (segments, blocks, etc.) - // has timestamps that are greater than or equal to txn_begin_timestamp, but the - // final snapshot which contains those data (and is yet to be committed) may have a timestamp - // that is larger than txn_begin_timestamp. - - // To maintain vacuum safety, we must ensure that if the latest snapshot's timestamp - // (latest_snapshot_timestamp) is larger than txn_begin_timestamp, we abort the transaction - // to prevent potential data loss during vacuum operations. - - // Example: - // session1: session2: session3: - // begin; - // -- newly created table data - // -- timestamped as A - // insert into t values (1); - // -- new snapshot S's ts is B - // insert into t values (2); - // -- using S as gc root - // -- if B > A, then newly created table data - // -- in session1 will be purged - // call fuse_vacuum2('db', 't'); - // -- while merging with S - // -- if A < B, this txn should abort - // commit; - - if txn_begin_timestamp < latest_snapshot_timestamp { - return Err(ErrorCode::UnresolvableConflict(format!( + }; + + // By enforcing txn_begin_timestamp >= latest_snapshot_timestamp, we ensure that + // vacuum operations won't remove table data (segment, blocks, etc.) that newly + // created in the current active transaction. + + // In the current transaction, all the newly created table data (segments, blocks, etc.) + // has timestamps that are greater than or equal to txn_begin_timestamp, but the + // final snapshot which contains those data (and is yet to be committed) may have a timestamp + // that is larger than txn_begin_timestamp. + + // To maintain vacuum safety, we must ensure that if the latest snapshot's timestamp + // (latest_snapshot_timestamp) is larger than txn_begin_timestamp, we abort the transaction + // to prevent potential data loss during vacuum operations. + + // Example: + // session1: session2: session3: + // begin; + // -- newly created table data + // -- timestamped as A + // insert into t values (1); + // -- new snapshot S's ts is B + // insert into t values (2); + // -- using S as gc root + // -- if B > A, then newly created table data + // -- in session1 will be purged + // call fuse_vacuum2('db', 't'); + // -- while merging with S + // -- if A < B, this txn should abort + // commit; + + if txn_begin_timestamp < latest_snapshot_timestamp { + return Err(ErrorCode::UnresolvableConflict(format!( "Unresolvable conflict detected for table {} while resolving conflicts: txn started with logical timestamp {}, which is less than the latest table timestamp {}. Transaction must be aborted.", tid, txn_begin_timestamp, latest_snapshot_timestamp ))); + } } } } diff --git a/tests/suites/0_stateless/01_transaction/01_03_issue_18705.py b/tests/suites/0_stateless/01_transaction/01_03_issue_18705.py new file mode 100755 index 0000000000000..c7f8bcf08606f --- /dev/null +++ b/tests/suites/0_stateless/01_transaction/01_03_issue_18705.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +import mysql.connector + +# https://github.com/databendlabs/databend/issues/18705 +if __name__ == "__main__": + # Session 1: + # Insert into empty table + mdb = mysql.connector.connect(host="127.0.0.1", user="root", passwd="root", port="3307") + mycursor = mdb.cursor() + mycursor.execute("create or replace table t_18705(c int)") + mycursor.fetchall() + mycursor.execute("begin") + mycursor.fetchall() + mycursor.execute("insert into t_18705 values (1)") + mycursor.fetchall() + + # Session 2: + # Alter table in another session, so that the new table after alter operation will still be empty + mydb_alter_tbl = mysql.connector.connect(host="127.0.0.1", user="root", passwd="root", port="3307") + mycursor_alter_tbl = mydb_alter_tbl.cursor() + mycursor_alter_tbl.execute("alter table t_18705 SET OPTIONS (block_per_segment = 500)") + mycursor_alter_tbl.fetchall() + + # Session 1: + # Try commit the txn in session one + mycursor.execute("commit") + mycursor.fetchall() + + # Will not reach here, if `commit` failed + print("Looks good") diff --git a/tests/suites/0_stateless/01_transaction/01_03_issue_18705.result b/tests/suites/0_stateless/01_transaction/01_03_issue_18705.result new file mode 100644 index 0000000000000..ba637add17b3e --- /dev/null +++ b/tests/suites/0_stateless/01_transaction/01_03_issue_18705.result @@ -0,0 +1 @@ +Looks good