We have successfully discovered 49 bugs (26 fixed, 39 confirmed and 10 open reported) from real-world production-level DBMSs, including 5 bugs in MySQL, 2 bugs in PostgreSQL, 20 bugs in TiDB, 3 bugs in OpenGauss, 3 bugs in Oceanbase, and 16 bugs in DB-X, a commercial DBMS whose name is anonymized at the request of our collaborator.
We are thankful to the DBMS developers for responding to our bug reports and fixing the bugs that we found. Because the nondeterministic interleavings among operations challenges the reproducibility of the isolation-related bugs, there are 10 bugs that can not be reproduced but are open-reported. In the future, we will aim to the research question about reducing bug cases such that the bug can be easily reproduced.
We have anonymized the corresponding issue and other personal information due to the double-blind reviewing constraint.
[TOC]
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Mode Setting | Set global tidb_txn_mode = 'pessimistic'; | Success |
| Schema Creation | Create table table_7_2(a int primary key, b int, c double); | Success |
| Database Initialization | Insert into table_7_2 values(676,-5012153, 2240641.4); | Success |
| 739 | Begin transaction; | Success |
| 739 | Update table_7_2 set b=-5012153, c=2240641.4 where a=676 | Success |
| 723 | Begin transaction; | Success |
| 723 | Update table_7_2 set b=852150 where a=676 | Success X |
| 739 | Commit | Success |
Bug Description
It first set transaction mode as pessimistic. Transaction 739 updates a record 676 in table_ 7_2 and holds an exclusive lock on the record 676. Before transaction 739 releases the exclusive lock on record 676, transaction 723 also successfully acquires an exclusive lock on record 676 and updates it, which results dirty write anomalies should be avoided in pessimistic transaction mode as described in TiDB official website.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | Create table table_7_2(primarykey int primary key, attribute1 double,attribute6 double); | Success |
| Database Initialization | Insert into table_7_2 values(3873, 0.213, 0.234); | Success |
| 904 | Begin transaction; | Success |
| 904 | Update table_8_2 set attribute6=-0.386 where primarykey=3873 | Success |
| 904 | Commit | Success |
| 914 | Set @@global.tx_isolation='REPEATABLE-READ'; | Success |
| 907 | Begin transaction; | Success |
| 907 | Update table_8_2 set attribute6=0.484 where primarykey=3873 | Success |
| 907 | Commit | Success |
| 914 | Select attribute6 from table_8_2 where primarykey=3873 | Success(attribute6=-0.368) X |
Bug Description
There are two historical versions on attribute6 of record 3873 in table_ 8_2. The first one is -0.386 created by transaction 904; the second one is 0.484 created by transaction 907. Since the select operation of transaction 914 happens after the committing of transaction 907, transaction 914 should see the second one, i.e., 0.484, instead of the first one, i.e., -0.368. However, the select operation of transaction 914 returns the first version -0.368, which indicates there is a defect about the implementation of consistent read in TiDB.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t1(a int primary key, b int); create table t2(a int primary key, b int, constraint fk1 foreign key(b) references t1(a)); create view view0(t2_a,t2_b,t1_b) as select t2.a,t2.b,t1.b from t2,t1 where t2.b=t1.a; |
Success |
| Database Initialization | insert into t1 values(1,2); insert into t1 values(2,3); insert into t1 values(3,4); insert into t1 values(4,5); insert into t1 values(5,6); insert into t2 values(1,2); insert into t2 values(2,3); insert into t2 values(3,4); insert into t2 values(4,5); insert into t2 values(5,1); |
Success |
| Operation ID | Session1 | Session2 | State |
|---|---|---|---|
| 1 | Begin transaction; | Success | |
| 2 | Begin transaction; | Success | |
| 3 | update t1 set b=12 where a=1; | Success | |
| 4 | select * from view0 where t2_a>3 for update; | Success Query Result(5,1,2)(4,5,6) |
|
| 5 | Commit;(Success) | Success | |
| 6 | Commit;(Success) | Success |
Bug Description
Operation 3 holds an exclusive lock on a record 1 in table t1 until operation 6 releases the lock. Due to the nature of exclusive locks, operation 4 attempts to acquire an exclusive lock on record 1 in table t1, which should be blocked. However, TiDB grants operation 4 an exclusive lock on record 1 in table t1, which indicates a locking violation.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | Create table t(a int primary key, b int); | Success |
| Database Initialization | Success |
| Operation ID | Session1 | Session2 | State |
|---|---|---|---|
| 1 | Begin transaction; | Success | |
| 2 | Begin transaction; | Success | |
| 3 | Update t set b=314 where a=1; | Success with row count = 0 | |
| 4 | Insert into t values(1,3); | blocking X | |
| 5 | Commit; | Success | |
| 6 | Insert into t values(1,3); | Success with row count = 1 | |
| 7 | Commit; | Success |
Bug Description
After investigating TiDB official website, the write operations of TiDB only locks the record that satisfies the conditions. Notice that the read operation of TiDB can avoid the phantom by the way of MVCC.
However, as shown in above test case, the update operation (Operation ID=3) locks a non-existing record that dose not satisfy its where condition. Additionally, the update operation (Operation ID=3) blocks the insertion operation of another transaction (Operation ID=6), which may lead to some performance issues about locking.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Mode Setting | Set global tidb_txn_mode = 'pessimistic'; | Success |
| Schema Creation | create table t (c1 varchar(10), c2 int, c3 char(20), primary key (c1, c2)); | Success |
| Database Initialization | insert into t values ('tag', 10, 't'), ('cat', 20, 'c'); | Success |
| 2 | Begin transaction; | Success |
| 2 | update t set c1=reverse(c1) where c1='tag'; | Success |
| 4 | Begin transaction; | Success |
| 4 | insert into t values('dress',40,'d'),('tag', 10, 't'); | Success |
| 2 | Commit | Success |
| 4 | select * from t use index(primary) order by c1,c2; |
When primary key is clustered index, query returns
| C1 | C2 | C3 |
|---|---|---|
| cat | 20 | c |
| dress | 40 | d |
| tag | 10 | t |
When primary key is nonclustered index, query returns
| C1 | C2 | C3 |
|---|---|---|
| cat | 20 | c |
| dress | 40 | d |
| tag | 10 | t |
| tag | 10 | t |
Bug Description
The above test case runs on pessimistic transaction mode in TiDB. Session 1 initializes a table with two record. One of them is ('tag','10',t). Session 2 acquires a exclusive lock to update the record ('tag','10',t) as ('gat','10',t). Before update committed, session 4 successfully acquires a exclusive lock to insert a new record ('tag','10',t), which violates the principle of long duration lock.
After that, session 4 issues a query to see all record in table t. When primary key is clustered index, query return the newly inserted record ('tag','10',t). However, when primary key is nonclustered index, query return two identical records ('tag','10',t), which indicates a bug hidden in TiDB. We have reported such a test case in TiDB community. TiDB developers have acknowledged it and fixed it.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table table0 (pkId integer, pkAttr0 integer, pkAttr1 integer, pkAttr2 integer, coAttr0_0 integer, primary key(pkAttr0, pkAttr1, pkAttr2)); | Success |
| Database Initialization | insert into t values (412,409,258,17702); | Success |
| 281 | SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ | Success |
| 282 | SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE | Success |
| 282 | Begin transaction; | Success |
| 282 | update table0 set coAttr0_0 = 40569 where ( pkAttr0 = 412 ) and ( pkAttr1 = 409 ) and ( pkAttr2 = 258 ); | Success |
| 282 | commit; | Success |
| 281 | Begin transaction; | Success |
| 281 | select pkAttr0, pkAttr1, pkAttr2, coAttr0_0 from table0 where ( coAttr0_0 = 89665 ) | Success |
| 281 | update table0 set coAttr0_0 = 40569 where ( pkAttr0 = 412 ) and ( pkAttr1 = 409 ) and ( pkAttr2 = 258 ) | Success |
| 281 | select pkAttr0, pkAttr1, pkAttr2, coAttr0_0 from table0 where ( coAttr0_0 = 17702 ) | Return (412,409,258,17702) |
| 281 | commit | Success |
Bug Description
Though Session-282 update the only row into (412,409,258, 40569). The last select query still returns a stale version of data for Key<412,409,258>, i.e. (412,409,258,17702).
See Transaction can't read its own update in repeatable read. · Issue #42487 · pingcap/tidb (github.com)
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table table1 (pkId integer, pkAttr0 integer, pkAttr1 integer, pkAttr2 integer, coAttr0_0 integer, primary key(pkAttr0, pkAttr1, pkAttr2) NONCLUSTERED); | Success |
| Database Initialization | insert into table1 values (1,1,1,1,1); | Success |
| 1 | SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ | Success |
| 2 | SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ | Success |
| 2 | Begin transaction; | Success |
| 1 | Begin transaction; | Success |
| 2 | update table1 set coAttr0_0 = 2 where pkAttr0=1 and pkAttr1=1 and pkAttr2=1; | Success |
| 2 | commit | Success |
| 1 | update table1 set coAttr0_0 = 2 where pkAttr0=1 and pkAttr1=1 and pkAttr2=1; | Success |
| 1 | select * from table1 where pkAttr0=1 and pkAttr1=1 and pkAttr2=1; | Return (1,1,1,1,2) instead of (1,1,1,1,1) |
Bug Description
The last select query of session1 cannot get its own update.
When we update a different value in session2, the last query can get correct value, i.e., when we execute the transaction below:
It seems that some opt(skipping update) when two transactions update a same value may go wrong because the select and update hold different snapshot.
See DROP DATABASE is not blocked while executing transaction · Issue #46943 · pingcap/tidb (github.com)
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t (a int); | Success |
| 2 | Begin transaction; | Success |
| 2 | insert into t values(1); | Success |
| 1 | drop database test; | No block |
Bug Description
DROP DATABASE can be executed without blocking. And the idling transaction can also be committed without warning/error.
We tested DROP TABLE in TiDB and found this DDL will be blocked. And we also test DROP DATABASE in MySQL, which is also be blocked. So it seems obviously that DROP DATABASE should also be blocked.
Maybe locking all tables while altering database can help.
const c = await mysql.createConnection({ host: TIDB_HOST, port: 4000, user: "root", database: 'test' });
try {
await c.query("set @@tidb_general_log=1");
await c.query("drop table if exists t1");
await c.query("create table t1 (a int, b int)");
await c.query("insert into t1 values (3, 2), (2, 3)");
log("show config", await pp(c.query("show config where name like '%max-txn-ttl'")));
await c.query("begin");
await c.query("update t1 set a=4 where a=3");
log("select(1):", await pp(c.query("select * from t1")));
log("wait 45 seconds");
await sleep(45*1000);
log("continue");
log("select(2):", await pp(c.query("select * from t1")));
log("rollback(bin):", await pp(c.execute("rollback")));
log("commit(bin): ", await pp(c.execute("commit")));
log("commit(txt): ", await pp(c.query("commit")));
} finally {
await close(c);
}Bug Description
Both commit and rollback fail.
2023-12-04T10:49:43.457Z show config [{"Type":"tidb","Instance":"127.0.0.1:4000","Name":"performance.max-txn-ttl","Value":"30000"}]
2023-12-04T10:49:43.467Z select(1): [{"a":4,"b":2},{"a":2,"b":3}]
2023-12-04T10:49:43.467Z wait 45 seconds
2023-12-04T10:50:28.476Z continue
2023-12-04T10:50:28.478Z select(2): TTL manager has timed out, pessimistic locks may expire, please commit or rollback this transaction
2023-12-04T10:50:28.481Z rollback(bin): TTL manager has timed out, pessimistic locks may expire, please commit or rollback this transaction
2023-12-04T10:50:28.482Z commit(bin): TTL manager has timed out, pessimistic locks may expire, please commit or rollback this transaction
2023-12-04T10:50:28.486Z commit(txt): {"fieldCount":0,"affectedRows":0,"insertId":0,"info":"","serverStatus":2,"warningStatus":0}It's because binary protocal cannot finish an aborted transaction.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Drop db0.table_1_2 | Success | |
| 723 | Update db1.table_5_1 set attribute2=8132130 where primarykey=6123 | Exception:Information schema is changed X |
Bug Description
During verifying the read committed isolation level recently developed by TiDB team, we find there is a checking schema problem. Specifically, the first line in above table modifies db0's schema information, while the second line in transaction 723 modifies db1's table with exception "information schema is changed". However, there is no modification on db1's schema information, which indicates a bug hidden in checking schema version.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| 232 | Select * from table_2_1 where primarykey=4323 | Stall(never respond) |
Bug Description
As definition, read committed isolation level sees a consistent view of database as of the beginning of an operation. Thus, each read operation in read committed isolation level should acquire a timestamp. Under the read committed isolation level recently developed by TiDB team, in order to optimize the performance of timestamp acquisition, asynchronous timestamp acquisition mechanism is adopted, but there are internal problems in this mechanism, as shown in the above table. Specifically, the read operation in above table never be responded from timestamp sever. We have help their developers fix this bugs.
Test Case
| Operation ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t(pk0 int, pk1 int, pk2 int, attr3 blob); | Success |
| Database Initialization | insert into t values(1,1,1,null); | |
| 1 | Update t set attr3=FILE("./data_case/obj/12obj_file.obj") where pk0=1 and pk1=1 and pk2= 1 | Success |
| 2 | Update t set attr3=FILE("./data_case/obj/12obj_file.obj") and other column where pk0 = 1 and pk1 = 1 and pk2 = 1 | Success |
| 3 | Select attr3 from t where pk0 = 1 and pk1 = 1 and pk2 = 1 for update | Success Return attr3 = NULL X |
Bug Description
For BLOB data type, when the new value and the old value written by the update operation are for the same binary file, the value actually written is null and success is returned, which indicates a BLOB-related bug hidden in TiDB.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| 1 | Start transaction read only with consistent snapshot | fail |
Bug Description
As for the start transaction statement, the TiDB official websiate shows that it supports the keywords "with consistent snapshot" and "read only". However, in fact, we found that TiDB cannot support these two keywords at the same time.
Bug#14 JDBC ResultSetMetaData.getColumnName for view query returns the attribute name defined in the table instead of the one defined in the view
Test Case
Just query a view with renamed attribute and print ResultSetMetaData.getColumnName.
import lombok.Cleanup;
import java.sql.*;
public class Main {
public static void main(String[] args) throws SQLException {
String url = "jdbc:mysql://localhost:3306?serverTimezone=UTC&useServerPrepStmts=true&cachePrepStmts=true";
String username = "root";
String password = "root";
String dropDatabase = "drop database if exists RSMDTestDB;";
String createDatabase = "create database RSMDTestDB;";
String useDatabase = "use RSMDTestDB;";
String createTable0 = "create table table0(t0c0 int, t0c1 int, t0c2 int);";
String createView0 = "create view view0(v0c0, v0c1, v0c2) as select t0c0, t0c1, t0c2 from table0;";
String selectView0 = "select * from view0;";
String selectView0WithAlias = "select v0c0 as alias_v0c0, v0c1 as alias_v0c1, v0c2 as alias_v0c2 from view0;";
@Cleanup
Connection conn = DriverManager.getConnection(url, username, password);
Statement stat = conn.createStatement();
stat.execute(dropDatabase);
stat.execute(createDatabase);
stat.execute(useDatabase);
stat.execute(createTable0);
stat.execute(createView0);
// print DBMS info
DatabaseMetaData dbMD = conn.getMetaData();
System.out.println(dbMD.getDatabaseProductVersion());
// select from view without alias
ResultSet rs = stat.executeQuery(selectView0);
ResultSetMetaData md = rs.getMetaData();
System.out.println("SELECT WITHOUT ALIAS");
System.out.printf("%-10s\t%-10s\t%-10s\n", "Index", "ColName", "ColLabel");
for(int ind=1; ind <= md.getColumnCount(); ind++) {
System.out.printf("%-10d\t%-10s\t%-10s\n", ind, md.getColumnName(ind), md.getColumnLabel(ind));
}
rs.close();
// select from view with alias
rs = stat.executeQuery(selectView0WithAlias);
md = rs.getMetaData();
System.out.println("SELECT WITH ALIAS");
System.out.printf("%-10s\t%-10s\t%-10s\n", "Index", "ColName", "ColLabel");
for(int ind=1; ind <= md.getColumnCount(); ind++) {
System.out.printf("%-10d\t%-10s\t%-10s\n", ind, md.getColumnName(ind), md.getColumnLabel(ind));
}
rs.close();
// drop database
stat.execute(dropDatabase);
}
}Bug Description
For MySQL, ResultSetMetaData.getColumnName returns the name of a column defined in the view, and ResultSetMetaData.getColumnLabel returns the alias given in the query.
However, in TiDB, ResultSetMetaData.getColumnName returns the name of a column defined in the table instead of in the view, while ResultSetMetaData.getColumnLabel returns the alias given in the query.
However, considering that when users query a view, they treat it as an abstract table, and do not care whether it is a table or a view. Perhaps it is more reasonable to return the name of the attribute listed in the view definition. This is the behavior of MySQL.
Moreover, if the user uses the as keyword to alias a attribute when querying the view, it seems that the attribute name defined in the view cannot be obtained from ResultSetMetaData.
See Query Error in information_schema.slow_query · Issue #28069 · pingcap/tidb (github.com)
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | Use information_schema | Success |
| 1 | select Txn_start_ts,time from slow_query where time > '2021-08-13 16:18:37.313976' limit 10; | Success returns 1 rows |
| 1 | select Txn_start_ts,time from slow_query where time > '2021-08-10 16:18:37.313976' limit 10; | Empty set |
Bug Description
Since the last query select a larger scale(>2021-08-10) tha the first one (2021-08-13), it should not return less result.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | CREATE TABLE IF NOT EXISTS test_table ( a INT NOT NULL, b INT NOT NULL, c INT NOT NULL, PRIMARY KEY (a,b,c)) PARTITION BY HASH(a) PARTITIONS 5; | Success |
| Database Initialization | Insert 10k tuples.For any tuple, column a and b have same value, generated from [1-10], and column c is increasing by 1. | Success |
| 1 | select min(a) from test_table; | Success(1min 54 sec) |
| 1 | select max(a) from test_table; | Success(2min 3 sec) |
| 1 | select distinct(a) from test_table order by a limit 1; | Success(21ms) |
| 1 | select min(b) from test_table; | Success(14ms) |
Bug Description
Expect to see that four queries should take a similar amount of time.
But first two queries took a very long time to execute(1 min 54.51 sec, 2 min 3.46 sec). While the last two act quickly(21.3ms, 14.4ms). There is a 5000x performance degradation between them.
And we found that the first two queries used Limit but no TopN on tikv, which may lead to a large data transfer from tikv to root. This may make query slow(even on the same machine).
See Create index is blocked when no /tmp/tidb/tmp_ddl-4000 · Issue #45624 · pingcap/tidb (github.com)
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | Create table t(a int) | Success |
| Schema Creation | Create index t1 on (a) | block |
Bug Description
The ddl is blocked. When kill this ddl, return error mes cannot get disk capacity at /tmp/tidb/tmp_ddl-4000: no such file or directory. And there is no tidb under /tmp. Index can be created after making dir /tmp/tidb/tmp_ddl-4000 manually.
It is because TiDB does not build such dir automatically after checking.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t2(a blob); create table t3(a blob); |
Success |
| Database Initialization | insert into t2 values(0xC2A0); insert into t3 values(0xC2); |
block |
| 1 | select * from t2,t3 where t2.a like concat("%",t3.a,"%"); |
In MySQL(v8.0.33), the last query returns one row.
mysql> select * from t2,t3 where t2.a like concat("%",t3.a,"%");
+------------+------------+
| a | a |
+------------+------------+
| 0xC2A0 | 0xC2 |
+------------+------------+
1 row in set (0.01 sec)However, TiDB returns an empty set.
> select * from t2,t3 where t2.a like concat("%",t3.a,"%");
Empty set (0.00 sec)It looks weird since in the case below, TiDB works well.
create table t2(a blob);
create table t3(a blob);
insert into t2 values(0xC2A020);
insert into t3 values(0xC2A0);
select * from t2,t3 where t2.a like concat("%",t3.a,"%");In this case, two DBMSs both returns one row. The charsets of TiDB and MySQL are the same(utf8mb4).
This bug may be relative to how match operation(like) compare two objects. When taking two bytes(as java's char) as a compare unit, the second object in the first case are different from the first one instead of contained by the first object. However, in the second case, the second object is contained by the first one. Moreover, when taking one byte as a compare unit(as c's char), the second object is contained by the first one in both cases.
See MySQL Bugs: #108015: select under repeatable read isolation level returns stale version of data
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table table0 (pkId integer, pkAttr0 integer, pkAttr1 integer, pkAttr2 integer, coAttr0_0 integer, primary key(pkAttr0, pkAttr1, pkAttr2)); | Success |
| Database Initialization | insert into t values (412,409,258,17702); | Success |
| 281 | SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ | Success |
| 282 | SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE | Success |
| 282 | Begin transaction; | Success |
| 282 | update table0 set coAttr0_0 = 40569 where ( pkAttr0 = 412 ) and ( pkAttr1 = 409 ) and ( pkAttr2 = 258 ); | Success |
| 282 | commit; | Success |
| 281 | Begin transaction; | Success |
| 281 | select pkAttr0, pkAttr1, pkAttr2, coAttr0_0 from table0 where ( coAttr0_0 = 89665 ) | Success |
| 281 | update table0 set coAttr0_0 = 40569 where ( pkAttr0 = 412 ) and ( pkAttr1 = 409 ) and ( pkAttr2 = 258 ) | Success |
| 281 | select pkAttr0, pkAttr1, pkAttr2, coAttr0_0 from table0 where ( coAttr0_0 = 17702 ) | Return (412,409,258,17702) |
| 281 | commit | Success |
Bug Description
Though Session-282 update the only row into (412,409,258, 40569). The last select query still returns a stale version of data for Key<412,409,258>, i.e. (412,409,258,17702).
See MySQL Bugs: #103891: Two parallel threads trigger error code '1032 Can't find record in 'table'
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t(pkattr0 integer, coattr1 varchar(100), coattr2 varchar(100), coattr3 varchar(100), coattr4 integer, coattr5 varchar(100), coattr6 varchar(100), primary key(pkattr0)); | Success |
| Database Initialization | delimiter $ create procedure p() begin declare done int default false; declare v_pkattr0 integer; declare v_coattr1 varchar(100); declare v_coattr2 varchar(100); declare v_coattr3 varchar(100); declare v_coattr4 varchar(100); declare v_coattr5 varchar(100); declare v_coattr6 varchar(100); declare cur1 cursor for select pkattr0, coattr1, coattr2, coattr3, coattr4, coattr5, coattr6 from t order by pkattr0, coattr1, coattr2, coattr3, coattr4, coattr5, coattr6; -- declare continue handler for sqlexception begin end; declare continue handler for not found set done = true; repeat set session transaction isolation level read uncommitted; if rand()<0.5 then start transaction; end if; open cur1; read_loop: loop fetch cur1 into v_pkattr0, v_coattr1, v_coattr2, v_coattr3, v_coattr4, v_coattr5, v_coattr6; if done then leave read_loop; end if; end loop; close cur1; if rand()<0.5 then commit; end if; if rand()<0.5 then start transaction; end if; if rand()<0.5 then delete from t where pkattr0 = 1; end if; if rand()<0.5 then commit; end if;set session transaction isolation level read uncommitted; if rand()<0.5 then start transaction; end if; if rand()<0.5 then replace into t(pkattr0, coattr1, coattr2, coattr3, coattr4, coattr5, coattr6) values("1", "varchar1", "varchar2", "varchar3", "2021", "varchar5", "varchar6"); end if; if rand()<0.5 then commit; end if; until 1=2 end repeat; end $ delimiter ; |
Success |
| 1 | call p(); | Success |
| 2 | call p(); | Success |
| 3 | call p(); | ERROR 1032 (HY000): Can't find record in 't' |
Bug Description
The expected result should not throw '1032' error.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table table0(pkId integer,pkAttr0 integer,pkAttr1 integer,pkAttr2 integer,pkAttr3 integer,pkAttr4 integer,coAttr0_0 integer,coAttr0_1 decimal(10, 0),coAttr0_2 varchar(100),primary key (pkAttr0, pkAttr1, pkAttr2, pkAttr3, pkAttr4)); | Success |
| Schema Creation | alter table table0 add index index_pk(pkAttr0, pkAttr1, pkAttr2, pkAttr3, pkAttr4); | Success |
| Schema Creation | create table table1(pkId integer,pkAttr0 integer,pkAttr1 integer,coAttr0_0 varchar(100),coAttr0_1 integer,coAttr0_2 varchar(100),primary key (pkAttr0, pkAttr1)); | Success |
| Schema Creation | alter table table1 add index index_pk(pkAttr0, pkAttr1); | Success |
| Schema Creation | alter table table1 add index index_commAttr0(coAttr0_0, coAttr0_1, coAttr0_2); | Success |
| Schema Creation | create table table2(pkId integer,pkAttr0 integer,pkAttr1 integer,pkAttr2 integer,pkAttr3 integer,pkAttr4 integer,pkAttr5 integer,pkAttr6 integer,coAttr0_0 decimal(10, 0),coAttr0_1 varchar(100),coAttr0_2 varchar(100),fkAttr0_0 integer,fkAttr0_1 integer,fkAttr0_2 integer,fkAttr0_3 integer,fkAttr0_4 integer,fkAttr1_0 integer,fkAttr1_1 integer,primary key (pkAttr0, pkAttr1, pkAttr2, pkAttr3, pkAttr4, pkAttr5, pkAttr6),foreign key (fkAttr0_0, fkAttr0_1, fkAttr0_2, fkAttr0_3, fkAttr0_4) references table0 (pkAttr0, pkAttr1, pkAttr2, pkAttr3, pkAttr4),foreign key (fkAttr1_0, fkAttr1_1) references table1 (pkAttr0, pkAttr1)); | Success |
| Schema Creation | alter table table2 add index index_pk(pkAttr0, pkAttr1, pkAttr2, pkAttr3, pkAttr4, pkAttr5, pkAttr6); | Success |
| Schema Creation | alter table table2 add index index_fk0(fkAttr0_0, fkAttr0_1, fkAttr0_2, fkAttr0_3, fkAttr0_4); | Success |
| Schema Creation | alter table table2 add index index_fk1(fkAttr1_0, fkAttr1_1); | Success |
| Schema Creation | create view view2 (pkAttr0, pkAttr1, pkAttr2, pkAttr3, pkAttr4, pkAttr5, pkAttr6, fkAttr0_0, fkAttr0_1, fkAttr0_2, fkAttr0_3, fkAttr0_4, fkAttr1_0, fkAttr1_1, coAttr0_0, coAttr0_1, coAttr0_2, coAttr1_0, coAttr1_1, coAttr1_2)asselect table2.pkAttr0,table2.pkAttr1,table2.pkAttr2,table2.pkAttr3,table2.pkAttr4,table2.pkAttr5,table2.pkAttr6,table2.fkAttr0_0,table2.fkAttr0_1,table2.fkAttr0_2,table2.fkAttr0_3,table2.fkAttr0_4,table2.fkAttr1_0,table2.fkAttr1_1,table2.coAttr0_0,table2.coAttr0_1,table2.coAttr0_2,table0.coAttr0_0,table0.coAttr0_1,table0.coAttr0_2from table2,table0where table2.fkAttr0_0 = table0.pkAttr0and table2.fkAttr0_1 = table0.pkAttr1and table2.fkAttr0_2 = table0.pkAttr2and table2.fkAttr0_3 = table0.pkAttr3and table2.fkAttr0_4 = table0.pkAttr4 | Error: duplicate column name |
Bug Description
When creating a correct view defined above, an error that cannot be imported may occur, and the error message "duplicate column name" will be reported. However, after careful inspection, there are no duplicate column names in the statement, and the same DDL statement can run normally on MySQL 5.7, which indicates a schema-related bug hidden in Oceanbase.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t0 (a decimal(5,3));create table t1 (a double); | Success |
| Database Initialization | insert into t0 values(0.01);insert into t1 values(0.1); | Success |
| 281 | select * from t0,t1 where t0.a = t1.a - t1.a + t0.a; | Returns (0.01,0.1) |
| 282 | select * from t0,t1 where t0.a = t0.a + t1.a - t1.a; | Returns Empty set |
Bug Description
The two queries are expected to have the same result. The different results are caused by the float deviation in different calculation order.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t (a int); | Success |
| Database Initialization | insert into t values(1) | Success |
| 281(jdbc) | begin; | Success |
| 282(jdbc) | update t set a = 2; | Success |
| 281(jdbc) | select * from t; | Returns (1) |
Bug Description
The last query is expected to return (2), as that does in CLI, but it returns (1).
It is because OpenGauss is designed to get snapshot at the first non-transaction-control statement of each transaction, but jdbc requires a snapshot at begin.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | CREATE TABLE tbl_1 (col_1_1 TINYINT, col_1_2 INT, col_1_3 INT, col_1_4 TIMESTAMP, pkid_1 INT, version_1 VARCHAR(200), PRIMARY KEY idx_1_1 (col_1_2)) PARTITION BY LIST (col_1_2) (PARTITION p_low VALUES IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), PARTITION p_high VALUES IN (12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22)); CREATE TABLE tbl_2 (col_2_1 INT, col_2_2 MEDIUMINT, col_2_3 INT, col_2_4 VARCHAR(100), pkid_2 INT, version_2 VARCHAR(200), PRIMARY KEY idx_2_1 (col_2_3)); |
Success |
| Database Initialization | INSERT INTO tbl_1 VALUES ('', 1, 20, 1, 8, {ts '2025-12-23 19:08:33.0'}); INSERT INTO tbl_2 VALUES (8, 1, 'str_002', '', 19, 1); INSERT INTO tbl_1 VALUES ('', 2, 2, 1, 10, {ts '2025-12-23 19:08:33.0'}); INSERT INTO tbl_2 VALUES (10, 1, 'str_002', '', 22, 1); |
Success |
| Session 1 | BEGIN; | Success |
| Session 1 | SELECT tbl_2.col_2_3, tbl_2.pkid_2, tbl_2.version_2 FROM tbl_2 WHERE tbl_2.col_2_4 = 'str_003' FOR SHARE; | Success |
| Session 2 | BEGIN; | Success |
| Session 2 | DELETE FROM tbl_2 WHERE tbl_2.col_2_4 = 'str_003'; | Blocked |
| Session 1 | UPDATE tbl_1 SET tbl_1.col_1_2 = 7, tbl_1.version_1 = CONCAT(tbl_1.version_1, 'T0') WHERE tbl_1.col_1_4 = {ts '2025-12-23 19:08:33.0'}; | ERROR 2013: Lost connection to MySQL server during query X |
Bug Description
In this test case, Session 1 first acquires a shared lock (FOR SHARE) on tbl_2 for a non-existent value 'str_003'. Session 2 attempts to delete records with the same condition 'str_003' from tbl_2 and gets blocked, waiting for the lock held by Session 1 (likely due to gap locking).
Subsequently, Session 1 attempts to update tbl_1. Note that tbl_1 is a partitioned table, and the update operation modifies col_1_2, which is the partition key (changing the value from 20 to 7). This update requires moving the row from partition p_high to p_low. Instead of completing the transaction or detecting a deadlock, the server crashes, and the client loses the connection with ERROR 2013. This indicates a critical stability issue when handling partition key updates under specific locking conditions.
See MySQL Bugs: #119521: Deadlock with SELECT FOR SHARE but not FOR UPDATE
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | drop table if exists table1; create table table1 (pkId int primary key, colA int); |
Success |
| Database Initialization | insert into table1 values (5, 100), (15, 200), (30, 100); | Success |
| Session 1 | begin; | Success |
| Session 2 | begin; | Success |
| Session 1 | select pkId, colA from table1 where pkId between 10 and 20 LOCK IN SHARE MODE; | Success |
| Session 2 | update table1 set colA = 50 where pkId = 30; | Success |
| Session 3 | ALTER TABLE table1 ADD UNIQUE (colA); | Blocked |
| Session 1 | DELETE FROM table1 WHERE pkId = 30; | ERROR 1213 (40001): Deadlock found when trying to get lock X |
Bug Description
In this scenario, Session 1 holds a shared lock on a range, Session 2 updates a row outside that range, and Session 3 attempts an ALTER TABLE to add a unique index (which waits for metadata locks). When Session 1 subsequently attempts to delete the row updated by Session 2, a deadlock error occurs immediately.
However, if the operation in Session 1 is changed from LOCK IN SHARE MODE (Shared Lock) to FOR UPDATE (Exclusive Lock), the deadlock does not occur. Instead, the DELETE operation in Session 1 is merely blocked, waiting for locks to be released. Once Session 2 commits, Session 1 proceeds successfully, and finally Session 3 completes. The fact that a weaker lock (SHARE) triggers a deadlock while a stronger lock (UPDATE) does not indicates an anomaly in lock dependency handling or metadata locking logic in MySQL 8.4.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table table0 (pkId integer, pkAttr0 decimal(10, 0), primary key(pkAttr0)) PARTITION BY key() PARTITIONS 43; alter table table0 add index table0index_pk(pkAttr0); |
Success |
| Database Initialization | insert into table0 (pkId,pkAttr0) values ("0","3.0"); insert into table0 (pkId,pkAttr0) values ("1","8.0"); ... (Insert total 30 rows) ... insert into table0 (pkId,pkAttr0) values ("29","145.0"); |
Success |
| JDBC Session | conn.setAutoCommit(false); | Success |
| JDBC Session | PreparedStatement ps = conn.prepareStatement("SELECT * FROM table0 WHERE pkAttr0 IN (?, ?)"); ps.setBigDecimal(1, new BigDecimal("3.0")); ps.setBigDecimal(2, new BigDecimal("8.0")); ps.executeQuery(); |
Connection Lost / Server Crash X |
Bug Description
When using a JDBC connection to execute a transaction, if a PreparedStatement is used to query a table defined with partitions (specifically PARTITION BY key()), and the query condition involves an IN clause with at least two parameters on the partition key (e.g., pkAttr0 IN (?, ?)), the database connection is unexpectedly dropped. This behavior suggests a potential server crash triggered by the specific combination of prepared statements, IN clause evaluation, and partitioning logic.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t (k integer primary key, v integer); | Success |
| Database Initialization | insert into t values(1,1),(2,2); | Success |
| Session 1 | BEGIN; | Success |
| Session 2 | BEGIN; | Success |
| Session 1 | update t set v=v+10 where k=1; | Success |
| Session 2 | update t set v=v+10 where k=2; | Success |
| Session 1 | update t set v=v+10 where k=2; | Blocked (Waiting for Session 2) |
| Session 2 | update t set v=v+10 where k=1; | Deadlock found, transaction rolled back (Exception thrown) |
| Session 2 | rollback() OR close() | SQLNonTransientConnectionException: Communications link failure X |
Bug Description
This bug relates to how DB-X handles JDBC connections after a deadlock occurs. The test case simulates a standard deadlock scenario using two parallel sessions updating records in reverse order.
In DB-X v2.0 and v3.0, when the deadlock is detected and an exception is thrown for Session 2, the underlying JDBC connection remains valid, allowing the application to execute a rollback() or proceed with other operations.
However, in DB-X v2.5, triggering the deadlock causes the JDBC connection to break entirely. Any subsequent operation on that connection object, including standard error handling routines like rollback() or close(), results in a SQLNonTransientConnectionException or Communications link failure. This prevents applications from gracefully recovering from deadlock exceptions.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Global Setting | set global DB-X_no_range_lock_on_rc = true; | Success |
| Schema Creation | create table t (a int , b int,c int, primary key(a,b)); alter table t add index ic(c); |
Success |
| Database Initialization | insert into t values(1,1,null),(1,2,null),(3,3,null); | Success |
| Session 1 | BEGIN; | Success |
| Session 2 | BEGIN; | Success |
| Session 1 | update t set a=2 where b=3; | Success |
| Session 2 | update t set a=4 where c is null; | Blocked |
| Session 1 | Commit; | Success |
| Session 2 | ... (Unblocked) -> Commit | Success (Rows matched: 2, Changed: 2) |
| Session 2 | select * from t; | Returns <2,3,null>,<4,1,null>,<4,2,null> (Expected: <4,3,null>,<4,1,null>,<4,2,null>) |
Bug Description
In Read Committed (RC) isolation level, a transaction may fail to update a row if that row's primary key was concurrently modified by another transaction, even if the row still satisfies the update condition.
In the test case above, Session 2 intends to update all rows where c is null (which includes all 3 rows). It first scans the data but gets blocked on the row <3,3,null> because Session 1 holds a lock on it. Session 1 modifies the primary key of this row from <3,3> to <2,3> and commits.
When Session 1 commits, Session 2 resumes. However, because the primary key of the blocked row has changed, Session 2 likely fails to locate or re-fetch the row using the old primary key it originally scanned, resulting in that row being skipped.
DB-X updates only 2 rows, leaving the row modified by Session 1 as <2,3,null> (with a=2 from Session 1, not a=4 from Session 2). In contrast, MySQL (in this specific scenario involving a secondary index) correctly updates all 3 rows (Rows matched: 3).
Note: A similar anomaly exists in MySQL when no secondary index is used (MySQL Bug #118923), but the behavior described here for DB-X occurs even when secondary indexes are present, deviating from standard MySQL behavior.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | DROP DATABASE IF EXISTS database43; CREATE DATABASE database43; USE database43; CREATE TABLE IF NOT EXISTS t0(c0 MEDIUMINT(70) ZEROFILL COLUMN_FORMAT DEFAULT COMMENT 'asdf' UNIQUE KEY PRIMARY KEY NOT NULL STORAGE DISK); |
Success |
| Data Manipulation | UPDATE t0 SET c0=INET_ATON((("e") = (NULL)) && (( EXISTS (SELECT 1 WHERE FALSE)) NOT IN (t0.c0))) WHERE (((t0.c0) IS NOT FALSE) <= ((')i') NOT IN (t0.c0, t0.c0, 1341945352))) < ((+ (('h?F*pCHH') IN (0.8245236965404992)))); | Success |
| Data Manipulation | REPLACE INTO t0(c0) VALUES(1341945352); REPLACE LOW_PRIORITY INTO t0(c0) VALUES(1580004396); INSERT HIGH_PRIORITY INTO t0(c0) VALUES(-1958477645); INSERT INTO t0(c0) VALUES(1341945352); REPLACE INTO t0(c0) VALUES(-781030407), (-1170603897), (2142965007), (-1), (-993046491); REPLACE DELAYED INTO t0(c0) VALUES(233032385), (233032385), (-929048787); REPLACE LOW_PRIORITY INTO t0(c0) VALUES(1184887975); REPLACE DELAYED INTO t0(c0) VALUES(NULL); REPLACE DELAYED INTO t0(c0) VALUES(NULL); INSERT LOW_PRIORITY IGNORE INTO t0(c0) VALUES(-348976803); INSERT INTO t0(c0) VALUES(-993046491); REPLACE LOW_PRIORITY INTO t0(c0) VALUES(1702262481); INSERT HIGH_PRIORITY IGNORE INTO t0(c0) VALUES(-1604196711); |
Success |
| Session 1 | INSERT IGNORE INTO t0(c0) VALUES(-1183122001), (2017470206), (1063044896); | Core Dump / Crash X |
Bug Description
Executing a specific sequence of SQL statements involving MEDIUMINT ZEROFILL columns, complex conditional UPDATE statements, and various types of write operations (REPLACE, INSERT, DELAYED, LOW_PRIORITY, IGNORE) may cause the DB-X server to core dump (crash).
The crash typically triggers on the final INSERT IGNORE statement in the sequence provided above. It is noted that this bug is difficult to reproduce consistently, suggesting it may involve a race condition or a specific memory corruption state triggered by the combination of ZEROFILL attributes, storage engine properties, and mixed DML operations.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | DROP TABLE IF EXISTS t0; CREATE TABLE t0(c1 INT PRIMARY KEY , c0 DATE) ; CREATE INDEX i0 ON t0(c1, c0 ); |
Success |
| Database Initialization | INSERT IGNORE INTO t0(c1, c0) VALUES(2, '2025-01-21'), (2, '1742-08-30'), (2, '2026-05-28'); | Success (2 rows affected, 1 warning) X (Expected: 1 row affected) |
| Session 1 | set htap_routing_strategy='default'; | Success |
| Session 1 | select * from t0; | Returns 2 rows: (2025-01-21, 2), (2026-05-28, 2) X |
| Session 1 | select count(*) from t0; | Returns 1 X |
| Session 1 | set htap_routing_strategy='vector_engine'; | Success |
| Session 1 | select * from t0; | Returns 1 row: (2026-05-28, 2) X |
Bug Description
This bug highlights a violation of the Primary Key uniqueness constraint and inconsistent read results across different execution engines in DB-X.
The table t0 has a Primary Key on column c1. When executing INSERT IGNORE with three records containing the same duplicate key c1=2, the database should strictly insert only the first record and discard the others (Standard MySQL behavior results in 1 row: 2025-01-21).
However, DB-X exhibits the following anomalies:
- Constraint Violation: The
INSERT IGNOREstatement successfully inserts 2 rows, violating the primary key constraint. - Inconsistent Reads (Default Engine): A simple
SELECT *scan returns 2 rows (showing duplicate PKs), whereasSELECT COUNT(*)returns only 1 row. - Engine Discrepancy (Vector Engine): When switching to the Vector Engine,
SELECT *returns 1 row, but it returns the last inserted value (2026-05-28) instead of the first one (2025-01-21).
This issue appears to be specific to tables containing DATE and INT columns with a secondary covering index (i0).
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table table0 (pkId integer, pkAttr0 varchar(10), pkAttr1 varchar(10), coAttr0_0 integer, primary key(pkAttr0, pkAttr1) ) ; | Success |
| Database Initialization | insert into table0 values (0,'vc0','vc2',479),(1,'vc7','vc8',646)...; | Success |
| Session 1 | BEGIN; | Success |
| Session 2 | BEGIN; | Success |
| Session 1 | update table0 set coAttr0_0 = 347 where ( pkAttr0 = 'vc7' ); | Success (Locks Row A) |
| Session 2 | insert into table0 (..., pkId=9, pkAttr0='vc45'...) values(...); | Success (Creates Row B) |
| Session 2 | update table0 set coAttr0_0 = 348 where ( pkAttr0 = 'vc7' ); | Blocked (Waiting for Lock A held by S1) |
| Session 1 | (JDBC setQueryTimeout = 2s) select ... from table0 where ( pkId > 5 ) for update; |
Blocked (Waiting for Lock B held by S2) |
| Session 1 | ... (After 2 seconds) | MySQLTimeoutException: Statement cancelled due to timeout |
| Session 1 | Commit; | Success |
| Session 1 | select coAttr0_0 ... where pkAttr0 = 'vc7'; | Returns 646 (Old Value) Expected: 347 (Updated Value) |
Bug Description
This bug occurs when using the JDBC setQueryTimeout method in a concurrency scenario that effectively forms a deadlock.
- Session 1 updates a row (Row A).
- Session 2 inserts a new row (Row B) and then attempts to update Row A, getting blocked by Session 1.
- Session 1 attempts to read Row B with
FOR UPDATE. This forms a circular dependency (Deadlock): Session 1 holds A, wants B; Session 2 holds B, wants A.
If Session 1 has a short query timeout (e.g., 2 seconds) set via JDBC:
- MySQL Behavior: It typically detects the deadlock immediately and rolls back the victim transaction (usually Session 2), or if the timeout triggers first, it cancels the statement.
- DB-X Behavior: Session 1 throws a
MySQLTimeoutException(Statement cancelled). The application catches this and executesCOMMIT. However, the data updated by Session 1 earlier (Row A = 347) is lost.
This implies that although DB-X threw a "Timeout" exception to the client, it internally performed a full transaction rollback (likely due to deadlock detection logic interfering with the timeout logic). Since the client received a Timeout exception rather than a Deadlock exception, it assumed the transaction was still valid and committed, leading to an unexpected loss of written data.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| JDBC Configuration | URL: jdbc:mysql://...?useServerPrepStmts=true&cachePrepStmts=true |
Success |
| Schema Creation | drop table if exists table0; create table table0 (k integer, v integer); |
Success |
| JDBC Session | PreparedStatement ps = conn.prepareStatement("select * from table0 where ( v = ( select ? ) )"); |
Success |
| JDBC Session | ps.setInt(1, 10); ps.executeQuery(); |
Communications link failure (Server Crash / Core Dump) X |
Bug Description
When using JDBC with server-side prepared statements (useServerPrepStmts=true), executing a query where a parameter placeholder (?) appears in the projection of a subquery (e.g., ( select ? )) causes the DB-X server to crash (Core Dump).
The client receives a Communications link failure exception. The server-side stack trace indicates a segmentation fault in Protocol_classic::send_field_metadata -> __strlen_sse2_pminub. This suggests that during the COM_STMT_PREPARE phase, when the server attempts to send result set metadata back to the client, it encounters a NULL pointer when trying to determine the column name or metadata for the un-typed ? in the subquery.
This behavior differs from MySQL 8.4.3, which handles this syntax correctly without crashing.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Global Setting | set global DB-X_no_range_lock_on_rc = true; | Success |
| Schema Creation | create table t (a double primary key, b double, c double unique, d double); alter table t add index i(d); |
Success |
| Database Initialization | insert into t values(1,1,1,1),(3,3,3,3); | Success |
| Session 1 | BEGIN; | Success |
| Session 2 | BEGIN; | Success |
| Session 1 | update t set c=2 where a=1; | Success |
| Session 2 | update t set c=2 where d > 2; | Success (Affected 1 row) X (Expected: Blocked / Error) |
| Session 1 | Commit; | Success |
| Session 2 | Commit; | Success |
| Session 2 | select * from t; | Returns <1,1,2,1>, <3,3,2,3> X (Duplicate entry '2' for unique column 'c') |
Bug Description
In Read Committed (RC) isolation level (with range locks disabled), DB-X fails to enforce unique constraints under specific concurrent update scenarios involving different access paths (Primary Key vs. Secondary Index).
Transaction 1 updates a row via the primary key, setting the unique column c to 2. Concurrently, Transaction 2 updates a different row (found via secondary index d) setting the same unique column c to 2.
In MySQL, Transaction 2 would be blocked waiting for the lock on the unique index entry 2, and upon Transaction 1's commit, Transaction 2 would fail with ERROR 1062 (Duplicate entry). However, DB-X allows both transactions to succeed and commit. This results in a corrupted state where two different rows hold the value 2 in column c, violating the UNIQUE constraint.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation (Case A) | create table test (a int,b int, primary key(a),unique(b)); | Success |
| Schema Creation (Case B) | create table test (a int,b int, primary key(a),key(b)); | Success |
| Database Initialization | insert into test values(1,1); | Success |
| Session 1 | BEGIN; | Success |
| Session 1 | select * from test; | Success |
| Session 2 | update test set a = 2; (and Commit) | Success |
| Session 1 | update test set a=3 where a=2; | Success |
| Session 1 (Case A) | select * from test where b = 1; | Returns <1,1> (1 row) |
| Session 1 (Case A) | select * from test where b >= 1; | Returns <1,1>, <3,1> (2 rows) X |
| Session 1 (Case B) | select * from test where b = 1; | Returns <1,1>, <3,1> (2 rows) |
| Session 1 (Case B) | select * from test where b >= 1; | Returns <1,1>, <3,1> (2 rows) |
Bug Description
This bug reveals an inconsistency in query results depending on whether a secondary index is defined as UNIQUE or not, specifically after a concurrent transaction modifies the Primary Key.
The scenario involves Transaction 1 starting, then Transaction 2 updates the Primary Key of a row from 1 to 2. Subsequently, Transaction 1 updates that same row (using the new PK 2) to a new PK 3.
-
Case A (Unique Index on
b):- A point query (
b=1) returns only the old version<1,1>. - A range query (
b>=1) returns both the old version<1,1>and the newly updated version<3,1>. This internal inconsistency (Point scan vs. Range scan) suggests an issue with how unique index lookups handle visibility in the write-set of the current transaction combined with history versions.
- A point query (
-
Case B (Non-Unique Index on
b):- Both point and range queries return duplicate versions
<1,1>and<3,1>.
- Both point and range queries return duplicate versions
While returning both versions (Ghost Rows) might be an anomaly in itself (violating standard snapshot visibility where one should arguably see the latest state of the transaction), the fact that Unique Index behaves inconsistently between point and range selects makes this a distinct bug.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | CREATE TABLE tbl_1 (col_1_1 INT, col_1_4 INT, pkid_1 INT) PARTITION BY RANGE (col_1_1) SUBPARTITION BY HASH (col_1_1) (PARTITION p0 VALUES LESS THAN (2) (SUBPARTITION s0, SUBPARTITION s1), PARTITION p1 VALUES LESS THAN (4) (SUBPARTITION s2, SUBPARTITION s3)); | Success |
| Database Initialization | INSERT INTO tbl_1 (col_1_4, pkid_1, col_1_1) VALUES (1, 2, 1); INSERT INTO tbl_1 (col_1_4, pkid_1, col_1_1) VALUES (1, 3, 0); |
Success |
| Transaction 1 | BEGIN; | Success |
| Transaction 1 | DELETE FROM tbl_1 WHERE (tbl_1.col_1_4 < 1 ); | Success (0 rows affected) |
| Transaction 1 | UPDATE tbl_1 SET tbl_1.col_1_4 = 0, tbl_1.col_1_1 = 0 WHERE tbl_1.col_1_1 > 0; | Success (Updates Row 1: col_1_1 changes 1->0, col_1_4 changes 1->0) |
| Transaction 1 | SELECT tbl_1.col_1_4, tbl_1.pkid_1 FROM tbl_1 WHERE tbl_1.col_1_4 = 1; | Returns (1,3), (1,2) X (Row (1,2) should not match because col_1_4 became 0) |
| Transaction 1 | ROLLBACK; | Success |
| Transaction 2 | BEGIN; | Success |
| Transaction 2 | DELETE FROM tbl_1 WHERE (tbl_1.col_1_4 < 1 ); | Success |
| Transaction 2 | UPDATE tbl_1 SET tbl_1.col_1_4 = 0, tbl_1.col_1_1 = 0 WHERE tbl_1.col_1_1 > 0; | Success |
| Transaction 2 | SELECT tbl_1.col_1_4, tbl_1.pkid_1 FROM tbl_1 WHERE tbl_1.col_1_4 = 1; | Returns (1,3) (Correct result) |
| Transaction 2 | ROLLBACK; | Success |
Bug Description
This bug demonstrates inconsistent read behavior when executing the exact same transaction logic twice, separated by a rollback.
The transaction performs an UPDATE that modifies the Partition Key (col_1_1) and another column (col_1_4) of a row.
- First Execution: The subsequent
SELECTquery incorrectly returns the old version of the updated row (1,2), even thoughcol_1_4was updated to 0 (which should result in the row being filtered out). This suggests the query in the first transaction failed to see the effects of its own partition-key update. - Second Execution: After rolling back and retrying the exact same operations, the
SELECTquery correctly returns only the non-updated row (1,3).
Since the first transaction was rolled back, the database state should be identical for the start of the second transaction. The difference in results indicates a state contamination or caching issue related to Partition Key updates within the engine.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | DROP TABLE IF EXISTS tbl_1; CREATE TABLE tbl_1 (col_1_1 INT, col_1_2 INT, pkid_1 INT, PRIMARY KEY idx_1_1 (col_1_1, col_1_2)) PARTITION BY HASH (col_1_2) PARTITIONS 4; |
Success |
| Database Initialization | INSERT INTO tbl_1 (col_1_1, col_1_2, pkid_1) VALUES (0, 0, 0); | Success |
| Session 1 | BEGIN; | Success |
| Session 1 | DELETE FROM tbl_1 WHERE ((tbl_1.col_1_2 = -1)); | Success (0 rows affected) |
| Session 1 | UPDATE tbl_1 SET tbl_1.col_1_1 = -1, tbl_1.col_1_2 = -1 WHERE tbl_1.col_1_2 = 0; | Success (Updates partition key) |
| Session 1 | SELECT * FROM tbl_1; | Returns 2 rows: (0,0,0) and (-1,-1,0) X (Expected: Only (-1,-1,0)) |
| Session 1 | COMMIT; | Success |
| Session 1 | SELECT * FROM tbl_1; | Returns 1 row: (-1,-1,0) |
Bug Description
In a partitioned table, updating a row's partition key (which involves moving data across partitions or logical delete+insert) causes a subsequent query within the same transaction to return duplicate versions of the same row: the old version (before update) and the new version (after update).
This anomaly is triggered only if the UPDATE is preceded by a DELETE statement (even if that delete affects 0 rows). This suggests a defect in how the transaction write-set or cursor visibility is handled during partition key updates when combined with previous write intentions in the same transaction. The issue disappears after the transaction is committed.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t (id int, a int); | Success |
| Database Initialization | insert into t values(1,2),(2,2),(3,3); | Success |
| Session 1 | BEGIN; | Success |
| Session 1 | select * from t; | Returns (1,2), (2,2), (3,3) |
| Session 2 | update t set a=a+1; | Success (Rows: 3, Changed: 3) Data becomes (1,3), (2,3), (3,4) |
| Session 1 | update t set a=3; | Success (Rows matched: 3, Changed: 1) Row 1 & 2 are already 3, so optimization skips them. Row 3 changes 4->3. |
| Session 1 | select * from t; | Returns (1,3), (2,2), (3,3) X |
| Session 1 | Commit; | Success |
| Session 1 | select * from t; | Returns (1,3), (2,3), (3,3) |
Bug Description
This bug highlights an inconsistency in how "no-op" updates (updates where the new value equals the old value) affect data visibility within a Repeatable Read transaction.
- Context: Session 1 starts and views a snapshot. Session 2 updates all rows to
a+1(Row 1:2->3, Row 2:2->3, Row 3:3->4). - The Trigger: Session 1 attempts to update
a=3.- For Row 1 (
ais 3) and Row 2 (ais 3), the value matches the target. The engine optimizes this by not performing a physical update (Changed: 0for these rows). - For Row 3 (
ais 4), the value changes to 3 (Changed: 1).
- For Row 1 (
- The Anomaly: When Session 1 selects the data again:
- MySQL Behavior: It returns the snapshot version for rows that were not physically changed by the transaction (Row 1:
2, Row 2:2) and the new version for the row that was changed (Row 3:3). - DB-X Behavior: It acts inconsistently. It returns the new version for Row 1 (
3), but the snapshot version for Row 2 (2). Both rows were treated identically by the logic (skipped updates), yet they exhibit different visibility rules. This randomness indicates a bug in how the read view interacts with skipped update optimizations.
- MySQL Behavior: It returns the snapshot version for rows that were not physically changed by the transaction (Row 1:
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t1(a int); alter table t1 add index i(a); |
Success |
| Database Initialization | (Table t1 is empty) | Success |
| Session 1 | BEGIN; | Success |
| Session 2 | BEGIN; | Success |
| Session 1 | insert into t1 values(1); | Success |
| Session 2 | select * from t1 where a = null; | Success (Empty set) (Trigger: Query optimization detects "Impossible WHERE" and skips execution/snapshot creation) |
| Session 1 | Commit; | Success |
| Session 2 | select * from t1; | Returns (1) X (Expected: Empty set, due to Repeatable Read) |
Bug Description
In Repeatable Read isolation level, the first read operation in a transaction should establish a consistent snapshot. However, if that first read query contains a condition that is statically determined to be impossible (e.g., a = null which is always False in SQL, vs a is null), the database engine optimizes the execution by directly returning an empty set without actually accessing the storage engine or creating a transaction read view (snapshot).
Consequently, when the transaction executes a second query later, it creates the snapshot at that moment. If another transaction (Session 1) has committed data in the interim, this delayed snapshot creation causes the current transaction (Session 2) to see data committed after its logical start, violating the expected Repeatable Read behavior.
This issue requires an index on the column used in the WHERE clause to trigger the specific optimization path. While present in MySQL 8.0/8.4, it is also observed in DB-X, differing from systems like TiDB which handle this correctly.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t(a int, b int); | Success |
| Database Initialization | insert into t values(1,1); | Success |
| 1 | begin; | Success |
| 2 | update t set b=2 where a=1; | Success |
| 1 | update t set b=3 where b=(select b from t where b=2); | Success, affects 0 row |
| 1 | commit | Success |
| 1 | select * from t; | Returns (1,2) instead of (1,3) |
Bug Description
Update is designed to read the last committed data. Thus the update in session 1 should read the data version (1,2). However, the sub query in update is executed as a single query and uses snapshot read, which read a earlier version(1,1).The snapshot read by sub query is different from that read by update and breaks the consistency in this single update SQL(with sub query), because a single SQL reads two snapshots.We test the same case in MySQL(v8.0.33), whose sub query read consistent snapshot as update.
Thus this execution is incompatible with MySQL.
See MySQL Bugs: #105988: The problem about predicate lock in Serializable isolation level
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | Create table table0 (pkId integer, pkAttr0 integer, coAttr0_0 integer, primary key(pkAttr0)); | Success |
| Mode Setting | Set autocommit = 0; | Success |
| 69264 | SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; | Success |
| 69269 | SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; | Success |
| 69264 | START TRANSACTION; | Success |
| 69264 | Insert into table0(pkId, pkAttr0, coAttr0_0) values(225, 225, 35704); |
Success |
| 69269 | START TRANSACTION; | Success |
| 69269 | Select pkAttr0, coAttr0_0 from table0 where ( pkAttr0 = 225 ); |
Success X |
| 69264 | rollback | Success |
Bug Description
We disable autocomit and set both the above two transactions as serializable isolation level, as shown in the second to third lines.
As definition, InnoDB implicitly converts all plain SELECT statements to SELECT ... FOR SHARE if autocommit is disabled. Therefore, the seventh line should acquire a range-level shared lock to protect all records whose pkAttr0 is 225.
However, the fifth line has acquired a record-level exclusive lock on a record whose pkAttr0 is 225. The exclusive lock acquired by the fifth line is imcompatible with the shared lock acquired by the seventh line, which indicates a bug hidden in range-level lock.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | set global innodb_deadlock_detect=off;create table t(a int primary key, b int); | Success |
| Database Initialization | insert into t values(1,2);insert into t values(2,4); | Success |
| 1 | begin; | Success |
| 2 | begin; | Success |
| 2 | set session transaction isolation level read uncommitted; | Success |
| 3 | begin; | Success |
| 3 | set session transaction isolation level read uncommitted; | Success |
| 2 | delete from t where a=1; | Success |
| 3 | update t set b=321 where a=2; | Success |
| 2 | update t set b=1421 where a=2; | Success |
| 3 | insert into t value(1,1231); | Rollback |
| 1 | select * from t where a=1; | Returns (1,2) instead of empty set |
Bug Description
Transaction 2 writes new versions on records 1 and 2 successively, while Transaction 3 writes new versions on records 2 and 1 successively. So there is a deadlock situation between transaction 2 and 3. Before the deadlock between transaction 2 and 3 timeouts, another read uncommitted transaction 1 launch a query to read the record 1 that has been modified by transaction 2 and 3 successively. Since the second write operation of transaction 3 are failed due to deadlock, we should not see its write results. Therefore, as expected, the query result of transaction 1 should be the write result of transaction 2. However, the query result of transaction 1 is the write result before transaction 2, which is weird. We think there may be a subtle bug hidden in the current version of MySQL.
Test Case
| Operation ID | Operation Detail | State |
|---|---|---|
| Schema Creation | create table t(pk0 int, pk1 int, pk2 int, attr3 blob); | Success |
| Database Initialization | insert into t values(1,1,1,null); | |
| 1 | Update t set attr3=FILE("./data_case/obj/12obj_file.obj") where pk0=1 and pk1=1 and pk2= 1 | Success |
| 2 | Update t set attr3=FILE("./data_case/obj/12obj_file.obj") and other column where pk0 = 1 and pk1 = 1 and pk2 = 1 | Success |
| 3 | Select attr3 from t where pk0 = 1 and pk1 = 1 and pk2 = 1 for update | Success Return attr3 = NULL X |
Bug Description
For BLOB data type, when the new value and the old value written by the update operation are for the same binary file, the value actually written is null and success is returned, which indicates a BLOB-related bug hidden in MySQL.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| 206 | Select attribute1 from table_7_1 where primarykey= 832 | Success |
| 204 | Select attribute1 from table_7_4 where primarykey= 1460 | Success |
| 206 | Update table_7_4 set attribute where primarykey=1460 | Success |
| 204 | Update table_7_1 set attribute1 = -635092 where primarykey= 832 | Success |
| 204 | Commit | Success |
| 206 | Commit | Success |
Bug Description
Transaction 206 reads a record 832 in table_ 7_ 1,then transaction 204 writes a new record to cover it, so transactions 206 to 204 have a RW dependency. Similarly, transaction 204 reads the record 1460 in table_ 7_ 4, then transaction 206 writes a new record to cover it, so transactions 204 to 206 have a RW dependency. Finally, transactions 204 to 206 generate a circular dependency, that is, write skew anomalies that should be avoided in Snapshot Isolation Level of PostgreSQL.
See PostgreSQL: BUG #17017: Two versions of the same row of records are returned in one query
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | Create Table t(a int primary key, b int); | Success |
| Database Initialization | Insert into t values(1,2);Insert into t values(2,3); | Success |
| 1 | begin; | Success |
| 1 | set transaction isolation level repeatable read; | Success |
| 1 | Select * from t where a=1; | Success |
| 2 | Begin; | Success |
| 2 | set transaction isolation level read committed; | Success |
| 2 | Delete from t where a=2; | Success |
| 2 | Commit; | Success |
| 1 | Insert into t values(2,4); | Success |
| 1 | Select * from t where a=2; | Returns (2,4)(2,3) instead of (2,4) |
Bug Description
According to the definition of snapshot isolation, a query in a transaction should always see a consistent view of the database. That is, it should see a consistent version for each record in the database. However, bug#20 shows that two different versions of the same record are returned to a query under the snapshot isolation of PostgreSQL. This certainly violates the definition of snapshot isolation. Thus, we report it to the PostgreSQL community.
Test Case
| Transaction ID | Session1 | Session2 | State |
|---|---|---|---|
| 2 | Begin; | Success | |
| 2 | set session transaction isolation level repeatable read; | Success | |
| 2 | update "table0" set "coAttr31_0" = 1048.0 where ( "pkAttr0" = 280 ) and ( "pkAttr1" = 241 ) and ( "pkAttr2" = ‘vc204’ ) and ( "pkAttr3" = ‘vc361’ ) and ( "pkAttr4" = 363 );--row count=1 | Success | |
| 1 | Begin; | Success | |
| 1 | set session transaction isolation level repeatable read; | Success | |
| 1 | select "pkAttr0", "pkAttr1", "pkAttr2", "pkAttr3", "pkAttr4", "pkAttr5", "pkAttr6", "pkAttr7", "fkAttr0_0", "fkAttr0_1", "fkAttr0_2", "fkAttr0_3", "fkAttr0_4" from "view0" where ( "fkAttr0_0" = 94 ) and ( "fkAttr0_1" = 239 ) or ( "fkAttr0_2" < 'vc119' ) and ( "fkAttr0_3" > 'vc81u' ) and ( "fkAttr0_4" = 278 ) ; | Success | |
| 2 | commit | Success | |
| 1 | delete from "table0" where ( "pkAttr0" = 280 ) and ( "pkAttr1" = 241 ) and ( "pkAttr2" = 'vc204' ) and ( "pkAttr3" = 'vc361' ) and ( "pkAttr4" = 363 ); | Success --row count=1 |
Bug Description
Transaction 1 starts before transaction 2 commit, and both transaction 1 and 2 write a new version on a record (280, 241,'vc204' , 'vc361' ,363 ). Therefore, transaction 1 and 2 are a pair of concurrent transaction, which should be avoided by first updater wins mechanism in OpenGauss.
Test Case
Create table table2 (primarykey int primary key, coAttr25_0 int);
Insert into table2 values(6,0);
Insert into table2 values(7,0);
| Transaction ID | Session1 | Session2 | State |
|---|---|---|---|
| 1 | Begin; | Success | |
| 1 | set session transaction isolation level repeatable read; | Success | |
| 1 | update "table2" set "coAttr25_0" = 78354, where "primaryKey" = 7; | Success | |
| 2 | Begin; | Success | |
| 2 | set session transaction isolation level repeatable read; | Success | |
| 2 | "update "table2" set " coAttr25_0" = 14 where "primaryKey" = 6; | Success | |
| 2 | Commit | Success | |
| 1 | Select "primaryKey", "fkAttr0_0", "coAttr25_0" from "table2"; | Success returns"primaryKey":"6", "coAttr25_0": "14" |
Bug Description
Transaction 1 launch a update operation while fetches a consistent snapshot. According to the rule of repeatable read isolation level, any operation in transaction 1 should sees a same snapshot., so transaction 1 should not see the write result created by transaction 2. However, transaction 1 sees the write result created by transaction 2, which indicates a consistency read violation.
Test Case
| Transaction ID | Session1 | Session2 | State |
|---|---|---|---|
| 1 | set session transaction isolation level repeatable read; | Success | |
| 2 | set session transaction isolation level repeatable read; | Success | |
| 1 | START TRANSACTION READ ONLY,WITH CONSISTENT SNAPSHOT; | Success | |
| 2 | START TRANSACTION; | Success | |
| 2 | update table0 set coAttr17 = 19635, coAttr18 = 1244, coAttr19 = 92947 where ( pkAttr0 = 'vc239' ) and ( pkAttr1 = 'vc234' ) and ( pkAttr2 = 'vc233' ); | Success, affects 1 row | |
| 2 | COMMIT | Success | |
| 1 | select pkAttr0, pkAttr1, pkAttr2, coAttr17, coAttr18, coAttr19 from table0 order by pkAttr0 ; | Success returns (vc239, vc234, vc233, 19635, 1244, 92947) |
Bug Description
After confirmation with developer, the repeatable read isolation level of Oceanbase is consistent with snapshot isolation as defined in the paper "A Critique of ANSI SQL Isolation Levels". Specifically, snapshot isolation is define as:
- A read operation sees a snapshot as of the start of the transaction.
- No transaction modify the record that has been modified by another concurrent transaction.
However, Oceanbase violates the first rule of snapshot isolation. Specifically, after transaction 1 obtains the consistency snapshot, another parallel transaction 2 issues a write operation. Transaction 1 should not see the write result created by transaction 2. In practice, transaction 1 sees it.
Test Case
| Transaction ID | Operation Detail | State |
|---|---|---|
| Schema Creation | drop table if exists table0; create table table0 (pkId integer, pkAttr0 integer, commonAttr0_0 integer, commonAttr1_0 varchar(10), commonAttr2_0 double(10, 2), commonAttr3_0 varchar(10), commonAttr4_0 varchar(10), primary key(pkAttr0)) ROW_FORMAT = DYNAMIC PARTITION BY KEY(pkAttr0) PARTITIONS 4 ; |
Success |
| Session 1 | BEGIN; | Success |
| Session 1 | select pkId, commonAttr4_0, pkAttr0 from table0 where ( pkAttr0 = 77 ) ; |
Success |
| Session 2 | alter table table0 coalesce partition 1; | Blocked (Waiting for metadata lock held by S1) |
| Session 1 | update table0 set commonAttr1_0 = "CLf"... where (pkAttr0 = 132 ); |
ERROR 1213 (40001): Deadlock found when trying to get lock X |
Bug Description
This bug manifests as a deadlock when a DML transaction runs concurrently with specific partition-related DDL operations on the same table.
- Session 1 starts a transaction and performs a read operation (
SELECT), acquiring a shared metadata lock (MDL) on the table. - Session 2 attempts a partition DDL (e.g.,
COALESCE PARTITION,REORGANIZE PARTITION, orADD PARTITION). This requires an exclusive lock and is blocked by Session 1. - Session 1 proceeds to perform an
UPDATEon the same table.
Instead of simply being blocked or successfully executing (depending on the exact lock compatibility), Session 1 immediately fails with a Deadlock error. This suggests a conflict in the wait graph between the DML locks and the pending Partition DDL locks. This behavior is also reproducible in MySQL (Bug #117735), but in DB-X, the deadlock cycle is difficult to trace in the transaction logs.