From 986fc81ba752d0a17cf551b2ca1a9a466710f7bd Mon Sep 17 00:00:00 2001 From: will <349071347@qq.com> Date: Tue, 5 Dec 2023 21:48:35 +0800 Subject: [PATCH] bugfix: fix could not rollback when insert the table with multiple key (#6077) --- changes/en-us/2.x.md | 2 + changes/zh-cn/2.x.md | 2 + .../exec/BaseTransactionalExecutor.java | 73 ++++---- .../datasource/exec/DeleteExecutorTest.java | 142 +++++++++++++++- .../exec/MariadbInsertExecutorTest.java | 48 ++++++ .../exec/MySQLInsertExecutorTest.java | 141 ++++++++++++++-- .../exec/PolarDBXInsertExecutorTest.java | 47 ++++++ .../datasource/exec/UpdateExecutorTest.java | 156 +++++++++++++++--- 8 files changed, 540 insertions(+), 71 deletions(-) diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md index 5280fedf118..7a1a4ce22e7 100644 --- a/changes/en-us/2.x.md +++ b/changes/en-us/2.x.md @@ -10,6 +10,7 @@ Add changes here for all PR submitted to the 2.x branch. - [[#6075](https://github.com/seata/seata/pull/6075)] fix missing table alias for on update column of image SQL - [[#6086](https://github.com/seata/seata/pull/6086)] fix oracle column alias cannot find - [[#6085](https://github.com/seata/seata/pull/6085)] fix jdk9+ compile error +- [[#6077](https://github.com/seata/seata/pull/6077)] fix could not rollback when table with multiple primary ### optimize: - [[#6061](https://github.com/seata/seata/pull/6061)] merge the rpcMergeMessageSend threads of rm and tm and increase the thread hibernation duration @@ -42,5 +43,6 @@ Thanks to these contributors for their code commits. Please report an unintended - [wangliang181230](https://github.com/wangliang181230) - [ggbocoder](https://github.com/ggbocoder) - [leezongjie](https://github.com/leezongjie) +- [l81893521](https://github.com/l81893521) Also, we receive many valuable issues, questions and advices from our community. Thanks for you all. diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md index a4bb7cfb570..cc6675ca9ed 100644 --- a/changes/zh-cn/2.x.md +++ b/changes/zh-cn/2.x.md @@ -10,6 +10,7 @@ - [[#6075](https://github.com/seata/seata/pull/6075)] 修复镜像SQL对于on update列没有添加表别名的问题 - [[#6086](https://github.com/seata/seata/pull/6086)] 修复oracle alias 解析异常 - [[#6085](https://github.com/seata/seata/pull/6085)] 修复jdk9+版本编译后,引入后ByteBuffer#flip NoSuchMethodError的问题 +- [[#6077](https://github.com/seata/seata/pull/6077)] 修复表存在复合主键索引导致无法回滚问题 ### optimize: - [[#6061](https://github.com/seata/seata/pull/6061)] 合并rm和tm的rpcMergeMessageSend线程,增加线程休眠时长 @@ -41,5 +42,6 @@ - [wangliang181230](https://github.com/wangliang181230) - [ggbocoder](https://github.com/ggbocoder) - [leezongjie](https://github.com/leezongjie) +- [l81893521](https://github.com/l81893521) 同时,我们收到了社区反馈的很多有价值的issue和建议,非常感谢大家。 diff --git a/rm-datasource/src/main/java/io/seata/rm/datasource/exec/BaseTransactionalExecutor.java b/rm-datasource/src/main/java/io/seata/rm/datasource/exec/BaseTransactionalExecutor.java index ffd809dcf56..3b2e1f746f0 100644 --- a/rm-datasource/src/main/java/io/seata/rm/datasource/exec/BaseTransactionalExecutor.java +++ b/rm-datasource/src/main/java/io/seata/rm/datasource/exec/BaseTransactionalExecutor.java @@ -22,7 +22,9 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.StringJoiner; +import java.util.TreeSet; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -193,18 +195,6 @@ protected String buildLimitCondition(WhereRecognizer recognizer, ArrayList getColumnNamesWithTablePrefixList(String table,String tab /** * Gets several column name in sql. * + * @param table the table + * @param tableAlias the table alias * @param columnNameList the column name * @return the column name in sql */ - protected String getColumnNamesInSQL(List columnNameList) { + protected String getColumnNamesWithTablePrefix(String table,String tableAlias, List columnNameList) { if (CollectionUtils.isEmpty(columnNameList)) { return null; } @@ -248,20 +240,43 @@ protected String getColumnNamesInSQL(List columnNameList) { if (i > 0) { columnNamesStr.append(" , "); } - columnNamesStr.append(getColumnNameInSQL(columnNameList.get(i))); + columnNamesStr.append(getColumnNameWithTablePrefix(table, tableAlias, columnNameList.get(i))); } return columnNamesStr.toString(); } + /** + * Gets column name in sql. + * + * @param columnName the column name + * @return the column name in sql + */ + protected String getColumnNameInSQL(String columnName) { + String tableAlias = sqlRecognizer.getTableAlias(); + return tableAlias == null ? columnName : tableAlias + "." + columnName; + } + + /** + * Gets column names in sql. + * + * @param columnNames the column names + * @return + */ + protected List getColumnNamesInSQLList(List columnNames) { + List columnNameWithTableAlias = new ArrayList<>(); + for (String columnName : columnNames) { + columnNameWithTableAlias.add(this.getColumnNameInSQL(columnName)); + } + return columnNameWithTableAlias; + } + /** * Gets several column name in sql. * - * @param table the table - * @param tableAlias the table alias * @param columnNameList the column name * @return the column name in sql */ - protected String getColumnNamesWithTablePrefix(String table,String tableAlias, List columnNameList) { + protected String getColumnNamesInSQL(List columnNameList) { if (CollectionUtils.isEmpty(columnNameList)) { return null; } @@ -270,7 +285,7 @@ protected String getColumnNamesWithTablePrefix(String table,String tableAlias, L if (i > 0) { columnNamesStr.append(" , "); } - columnNamesStr.append(getColumnNameWithTablePrefix(table,tableAlias, columnNameList.get(i))); + columnNamesStr.append(getColumnNameInSQL(columnNameList.get(i))); } return columnNamesStr.toString(); } @@ -517,22 +532,24 @@ protected TableRecords buildTableRecords(Map> pkValuesMap) } protected List getNeedColumns(String table, String tableAlias, List unescapeColumns) { - List needUpdateColumns = new ArrayList<>(); + Set needUpdateColumns = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); TableMeta tableMeta = getTableMeta(table); if (ONLY_CARE_UPDATE_COLUMNS && CollectionUtils.isNotEmpty(unescapeColumns)) { if (!containsPK(table, unescapeColumns)) { List pkNameList = tableMeta.getEscapePkNameList(getDbType()); if (CollectionUtils.isNotEmpty(pkNameList)) { if (StringUtils.isNotBlank(tableAlias)) { - needUpdateColumns.add(getColumnNamesWithTablePrefix(table, tableAlias, pkNameList)); + needUpdateColumns.addAll( + ColumnUtils.delEscape(getColumnNamesWithTablePrefixList(table, tableAlias, pkNameList), getDbType()) + ); } else { - needUpdateColumns.add(getColumnNamesInSQL(pkNameList)); + needUpdateColumns.addAll( + ColumnUtils.delEscape(getColumnNamesInSQLList(pkNameList), getDbType()) + ); } } } - needUpdateColumns.addAll(unescapeColumns.stream() - .map(unescapeUpdateColumn -> ColumnUtils.addEscape(unescapeUpdateColumn, getDbType(), tableMeta)).collect( - Collectors.toList())); + needUpdateColumns.addAll(unescapeColumns); // The on update xxx columns will be auto update by db, so it's also the actually updated columns List onUpdateColumns = tableMeta.getOnUpdateColumnsOnlyName(); @@ -541,19 +558,15 @@ protected List getNeedColumns(String table, String tableAlias, List getColumnNameWithTablePrefix(table, tableAlias, onUpdateColumn)) .collect(Collectors.toList()); } - onUpdateColumns.removeAll(unescapeColumns); - needUpdateColumns.addAll(onUpdateColumns.stream() - .map(onUpdateColumn -> ColumnUtils.addEscape(onUpdateColumn, getDbType(), tableMeta)) - .collect(Collectors.toList())); + needUpdateColumns.addAll(onUpdateColumns); } else { Stream allColumns = tableMeta.getAllColumns().keySet().stream(); if (StringUtils.isNotBlank(tableAlias)) { allColumns = allColumns.map(columnName -> getColumnNameWithTablePrefix(table, tableAlias, columnName)); } - allColumns = allColumns.map(columnName -> ColumnUtils.addEscape(columnName, getDbType(), tableMeta)); allColumns.forEach(needUpdateColumns::add); } - return needUpdateColumns; + return needUpdateColumns.stream().map(column -> ColumnUtils.addEscape(column, getDbType(), tableMeta)).collect(Collectors.toList()); } /** diff --git a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/DeleteExecutorTest.java b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/DeleteExecutorTest.java index b3b5ed10554..21ea5851fcf 100644 --- a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/DeleteExecutorTest.java +++ b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/DeleteExecutorTest.java @@ -87,7 +87,7 @@ public static void init() { } catch (Exception e) { throw new RuntimeException("init failed"); } - String sql = "delete from t where id = 1"; + String sql = "delete from table_delete_executor_test where id = 1"; List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> { @@ -96,20 +96,146 @@ public static void init() { } @Test - public void testBeforeImage() throws SQLException { - Assertions.assertNotNull(deleteExecutor.beforeImage()); + public void testBeforeAndAfterImage() throws SQLException { + String sql = "delete from table_delete_executor_test"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableAlias() throws SQLException { + String sql = "delete from table_delete_executor_test t where t.id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableSchema() throws SQLException { + String sql = "delete from seata.table_delete_executor_test where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); - String sql = "delete from t"; + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableSchemaAndTableAlias() throws SQLException { + String sql = "delete from seata.table_delete_executor_test t where t.id = 1"; List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); - Assertions.assertNotNull(deleteExecutor.beforeImage()); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); } @Test - public void testAfterImage() throws SQLException { - TableRecords tableRecords = deleteExecutor.beforeImage(); - Assertions.assertEquals(0, deleteExecutor.afterImage(tableRecords).size()); + public void testBeforeAndAfterImageWithTableSchemaQuote() throws SQLException { + String sql = "delete from `seata`.table_delete_executor_test where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableSchemaAndTableNameQuote() throws SQLException { + String sql = "delete from seata.`table_delete_executor_test` where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableSchemaQuoteAndTableNameQuote() throws SQLException { + String sql = "delete from `seata`.`table_delete_executor_test` where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithColumnQuote() throws SQLException { + String sql = "delete from table_delete_executor_test where `id` = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithUpperColumn() throws SQLException { + String sql = "delete from table_delete_executor_test where ID = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableAliasAndUpperColumn() throws SQLException { + String sql = "delete from table_delete_executor_test t where t.ID = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithKeyword() throws SQLException { + String sql = "delete from table_delete_executor_test where `or` = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLDeleteRecognizer recognizer = new MySQLDeleteRecognizer(sql, asts.get(0)); + deleteExecutor = new DeleteExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = deleteExecutor.beforeImage(); + TableRecords afterImage = deleteExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); } } diff --git a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MariadbInsertExecutorTest.java b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MariadbInsertExecutorTest.java index a83f2efd55c..36fc4fbb736 100644 --- a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MariadbInsertExecutorTest.java +++ b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MariadbInsertExecutorTest.java @@ -15,13 +15,24 @@ */ package io.seata.rm.datasource.exec; +import java.lang.reflect.Field; import java.sql.SQLException; +import java.sql.Types; import java.util.Arrays; import java.util.HashMap; +import java.util.List; + +import com.alibaba.druid.mock.MockStatement; +import com.alibaba.druid.mock.MockStatementBase; +import com.alibaba.druid.pool.DruidDataSource; +import com.google.common.collect.Lists; import io.seata.rm.datasource.ConnectionProxy; import io.seata.rm.datasource.DataSourceProxy; +import io.seata.rm.datasource.DataSourceProxyTest; import io.seata.rm.datasource.PreparedStatementProxy; +import io.seata.rm.datasource.StatementProxy; import io.seata.rm.datasource.exec.mysql.MySQLInsertExecutor; +import io.seata.rm.datasource.mock.MockDriver; import io.seata.rm.datasource.mock.MockMariadbDataSource; import io.seata.rm.datasource.mock.MockResultSet; import io.seata.sqlparser.struct.TableMeta; @@ -66,5 +77,42 @@ public void init() throws SQLException { put(ID_COLUMN, pkIndex); } }; + + // new test init property + List returnValueColumnLabels = Lists.newArrayList("id", "user_id", "name", "sex", "update_time"); + Object[][] returnValue = new Object[][] { + new Object[] {1, 1, "will", 1, 0}, + }; + Object[][] columnMetas = new Object[][] { + new Object[] {"", "", "table_insert_executor_test", "id", Types.INTEGER, "INTEGER", 64, 0, 10, 1, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "user_id", Types.INTEGER, "INTEGER", 64, 0, 10, 1, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "name", Types.VARCHAR, "VARCHAR", 64, 0, 10, 0, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "sex", Types.INTEGER, "INTEGER", 64, 0, 10, 0, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "update_time", Types.INTEGER, "INTEGER", 64, 0, 10, 0, "", "", 0, 0, 64, 2, "YES", "NO"}, + }; + Object[][] indexMetas = new Object[][] { + new Object[] {"PRIMARY", "id", false, "", 3, 1, "A", 34}, + new Object[] {"PRIMARY", "user_id", false, "", 3, 1, "A", 34}, + }; + Object[][] onUpdateColumnsReturnValue = new Object[][] { + new Object[]{0, "update_time", Types.INTEGER, "INTEGER", 64, 10, 0, 0} + }; + + MockDriver mockDriver = new MockDriver(returnValueColumnLabels, returnValue, columnMetas, indexMetas, null, onUpdateColumnsReturnValue); + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setUrl("jdbc:mock:xxx"); + dataSource.setDriver(mockDriver); + + DataSourceProxy newDataSourceProxy = DataSourceProxyTest.getDataSourceProxy(dataSource); + try { + Field field = dataSourceProxy.getClass().getDeclaredField("dbType"); + field.setAccessible(true); + field.set(newDataSourceProxy, "mysql"); + ConnectionProxy newConnectionProxy = new ConnectionProxy(newDataSourceProxy, dataSource.getConnection().getConnection()); + MockStatementBase mockStatement = new MockStatement(dataSource.getConnection().getConnection()); + newStatementProxy = new StatementProxy(newConnectionProxy, mockStatement); + } catch (Exception e) { + throw new RuntimeException("init failed"); + } } } diff --git a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MySQLInsertExecutorTest.java b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MySQLInsertExecutorTest.java index b7ba7ccfdb7..7010f71acda 100644 --- a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MySQLInsertExecutorTest.java +++ b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/MySQLInsertExecutorTest.java @@ -15,27 +15,38 @@ */ package io.seata.rm.datasource.exec; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.math.BigDecimal; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; + +import com.alibaba.druid.mock.MockStatement; +import com.alibaba.druid.mock.MockStatementBase; +import com.alibaba.druid.pool.DruidDataSource; +import com.alibaba.druid.sql.SQLUtils; +import com.alibaba.druid.sql.ast.SQLStatement; +import com.google.common.collect.Lists; import io.seata.common.exception.ShouldNeverHappenException; import io.seata.rm.datasource.ConnectionProxy; import io.seata.rm.datasource.DataSourceProxy; +import io.seata.rm.datasource.DataSourceProxyTest; import io.seata.rm.datasource.PreparedStatementProxy; import io.seata.rm.datasource.StatementProxy; import io.seata.rm.datasource.exec.mysql.MySQLInsertExecutor; import io.seata.rm.datasource.mock.MockDataSource; +import io.seata.rm.datasource.mock.MockDriver; import io.seata.rm.datasource.mock.MockResultSet; +import io.seata.sqlparser.druid.mysql.MySQLInsertRecognizer; import io.seata.sqlparser.struct.ColumnMeta; -import io.seata.rm.datasource.sql.struct.Row; import io.seata.sqlparser.struct.TableMeta; import io.seata.rm.datasource.sql.struct.TableRecords; import io.seata.sqlparser.SQLInsertRecognizer; @@ -69,12 +80,16 @@ public class MySQLInsertExecutorTest { protected StatementProxy statementProxy; + protected StatementProxy newStatementProxy; + protected SQLInsertRecognizer sqlInsertRecognizer; protected TableMeta tableMeta; protected MySQLInsertExecutor insertExecutor; + protected MySQLInsertExecutor newInsertExecutor; + protected final int pkIndex = 0; protected HashMap pkIndexMap; @@ -103,25 +118,123 @@ public void init() throws SQLException { put(ID_COLUMN, pkIndex); } }; - } - @Test - public void testBeforeImage() throws SQLException { - doReturn(tableMeta).when(insertExecutor).getTableMeta(); - TableRecords tableRecords = insertExecutor.beforeImage(); - Assertions.assertEquals(tableRecords.size(), 0); - try { - tableRecords.add(new Row()); - } catch (Exception e) { - Assertions.assertTrue(e instanceof UnsupportedOperationException); - } + // new test init property + List returnValueColumnLabels = Lists.newArrayList("id", "user_id", "name", "sex", "update_time"); + Object[][] returnValue = new Object[][] { + new Object[] {1, 1, "will", 1, 0}, + }; + Object[][] columnMetas = new Object[][] { + new Object[] {"", "", "table_insert_executor_test", "id", Types.INTEGER, "INTEGER", 64, 0, 10, 1, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "user_id", Types.INTEGER, "INTEGER", 64, 0, 10, 1, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "name", Types.VARCHAR, "VARCHAR", 64, 0, 10, 0, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "sex", Types.INTEGER, "INTEGER", 64, 0, 10, 0, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "update_time", Types.INTEGER, "INTEGER", 64, 0, 10, 0, "", "", 0, 0, 64, 2, "YES", "NO"}, + }; + Object[][] indexMetas = new Object[][] { + new Object[] {"PRIMARY", "id", false, "", 3, 1, "A", 34}, + new Object[] {"PRIMARY", "user_id", false, "", 3, 1, "A", 34}, + }; + Object[][] onUpdateColumnsReturnValue = new Object[][] { + new Object[]{0, "update_time", Types.INTEGER, "INTEGER", 64, 10, 0, 0} + }; + + MockDriver mockDriver = new MockDriver(returnValueColumnLabels, returnValue, columnMetas, indexMetas, null, onUpdateColumnsReturnValue); + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setUrl("jdbc:mock:xxx"); + dataSource.setDriver(mockDriver); + + DataSourceProxy newDataSourceProxy = DataSourceProxyTest.getDataSourceProxy(dataSource); try { - tableRecords.getTableMeta(); + Field field = dataSourceProxy.getClass().getDeclaredField("dbType"); + field.setAccessible(true); + field.set(newDataSourceProxy, "mysql"); + ConnectionProxy newConnectionProxy = new ConnectionProxy(newDataSourceProxy, dataSource.getConnection().getConnection()); + MockStatementBase mockStatement = new MockStatement(dataSource.getConnection().getConnection()); + newStatementProxy = new StatementProxy(newConnectionProxy, mockStatement); } catch (Exception e) { - Assertions.assertTrue(e instanceof UnsupportedOperationException); + throw new RuntimeException("init failed"); } } + @Test + public void testBeforeAndAfterImage() throws SQLException { + String sql = "insert into table_insert_executor_test(id, user_id, name, sex) values (1, 1, 'will', 1)"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLInsertRecognizer recognizer = new MySQLInsertRecognizer(sql, asts.get(0)); + newInsertExecutor = new MySQLInsertExecutor(newStatementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = newInsertExecutor.beforeImage(); + TableRecords afterImage = newInsertExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageTableSchemaAndTableName() throws SQLException { + String sql = "insert into seata.table_insert_executor_test(id, user_id, name, sex) values (1, 1, 'will', 1)"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLInsertRecognizer recognizer = new MySQLInsertRecognizer(sql, asts.get(0)); + newInsertExecutor = new MySQLInsertExecutor(newStatementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = newInsertExecutor.beforeImage(); + TableRecords afterImage = newInsertExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageTableSchemaWithQuoteAndTableName() throws SQLException { + String sql = "insert into `seata`.table_insert_executor_test(id, user_id, name, sex) values (1, 1, 'will', 1)"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLInsertRecognizer recognizer = new MySQLInsertRecognizer(sql, asts.get(0)); + newInsertExecutor = new MySQLInsertExecutor(newStatementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = newInsertExecutor.beforeImage(); + TableRecords afterImage = newInsertExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageTableSchemaWithQuoteAndTableNameWithQuote() throws SQLException { + String sql = "insert into `seata`.`table_insert_executor_test`(id, user_id, name, sex) values (1, 1, 'will', 1)"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLInsertRecognizer recognizer = new MySQLInsertRecognizer(sql, asts.get(0)); + newInsertExecutor = new MySQLInsertExecutor(newStatementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = newInsertExecutor.beforeImage(); + TableRecords afterImage = newInsertExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageColumnWithQuote() throws SQLException { + String sql = "insert into table_insert_executor_test(`id`, `user_id`, `name`, `sex`) values (1, 1, 'will', 1)"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLInsertRecognizer recognizer = new MySQLInsertRecognizer(sql, asts.get(0)); + newInsertExecutor = new MySQLInsertExecutor(newStatementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = newInsertExecutor.beforeImage(); + TableRecords afterImage = newInsertExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageUpperColumn() throws SQLException { + String sql = "insert into table_insert_executor_test(ID, USER_ID, NMAE, SEX) values (1, 1, 'will', 1)"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLInsertRecognizer recognizer = new MySQLInsertRecognizer(sql, asts.get(0)); + newInsertExecutor = new MySQLInsertExecutor(newStatementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = newInsertExecutor.beforeImage(); + TableRecords afterImage = newInsertExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + @Test public void testAfterImage_ByColumn() throws SQLException { doReturn(true).when(insertExecutor).containsPK(); diff --git a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/PolarDBXInsertExecutorTest.java b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/PolarDBXInsertExecutorTest.java index 18d9dfec76d..9a594204d95 100644 --- a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/PolarDBXInsertExecutorTest.java +++ b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/PolarDBXInsertExecutorTest.java @@ -15,14 +15,24 @@ */ package io.seata.rm.datasource.exec; +import java.lang.reflect.Field; import java.sql.SQLException; +import java.sql.Types; import java.util.Arrays; import java.util.HashMap; +import java.util.List; +import com.alibaba.druid.mock.MockStatement; +import com.alibaba.druid.mock.MockStatementBase; +import com.alibaba.druid.pool.DruidDataSource; +import com.google.common.collect.Lists; import io.seata.rm.datasource.ConnectionProxy; import io.seata.rm.datasource.DataSourceProxy; +import io.seata.rm.datasource.DataSourceProxyTest; import io.seata.rm.datasource.PreparedStatementProxy; +import io.seata.rm.datasource.StatementProxy; import io.seata.rm.datasource.exec.polardbx.PolarDBXInsertExecutor; +import io.seata.rm.datasource.mock.MockDriver; import io.seata.rm.datasource.mock.MockResultSet; import io.seata.sqlparser.SQLInsertRecognizer; import io.seata.sqlparser.struct.TableMeta; @@ -69,5 +79,42 @@ public void init() throws SQLException { put(ID_COLUMN, pkIndex); } }; + + // new test init property + List returnValueColumnLabels = Lists.newArrayList("id", "user_id", "name", "sex", "update_time"); + Object[][] returnValue = new Object[][] { + new Object[] {1, 1, "will", 1, 0}, + }; + Object[][] columnMetas = new Object[][] { + new Object[] {"", "", "table_insert_executor_test", "id", Types.INTEGER, "INTEGER", 64, 0, 10, 1, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "user_id", Types.INTEGER, "INTEGER", 64, 0, 10, 1, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "name", Types.VARCHAR, "VARCHAR", 64, 0, 10, 0, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "sex", Types.INTEGER, "INTEGER", 64, 0, 10, 0, "", "", 0, 0, 64, 2, "NO", "NO"}, + new Object[] {"", "", "table_insert_executor_test", "update_time", Types.INTEGER, "INTEGER", 64, 0, 10, 0, "", "", 0, 0, 64, 2, "YES", "NO"}, + }; + Object[][] indexMetas = new Object[][] { + new Object[] {"PRIMARY", "id", false, "", 3, 1, "A", 34}, + new Object[] {"PRIMARY", "user_id", false, "", 3, 1, "A", 34}, + }; + Object[][] onUpdateColumnsReturnValue = new Object[][] { + new Object[]{0, "update_time", Types.INTEGER, "INTEGER", 64, 10, 0, 0} + }; + + MockDriver mockDriver = new MockDriver(returnValueColumnLabels, returnValue, columnMetas, indexMetas, null, onUpdateColumnsReturnValue); + DruidDataSource dataSource = new DruidDataSource(); + dataSource.setUrl("jdbc:mock:xxx"); + dataSource.setDriver(mockDriver); + + DataSourceProxy newDataSourceProxy = DataSourceProxyTest.getDataSourceProxy(dataSource); + try { + Field field = dataSourceProxy.getClass().getDeclaredField("dbType"); + field.setAccessible(true); + field.set(newDataSourceProxy, "mysql"); + ConnectionProxy newConnectionProxy = new ConnectionProxy(newDataSourceProxy, dataSource.getConnection().getConnection()); + MockStatementBase mockStatement = new MockStatement(dataSource.getConnection().getConnection()); + newStatementProxy = new StatementProxy(newConnectionProxy, mockStatement); + } catch (Exception e) { + throw new RuntimeException("init failed"); + } } } diff --git a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/UpdateExecutorTest.java b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/UpdateExecutorTest.java index a25fdfa7fa1..482ce56afb7 100644 --- a/rm-datasource/src/test/java/io/seata/rm/datasource/exec/UpdateExecutorTest.java +++ b/rm-datasource/src/test/java/io/seata/rm/datasource/exec/UpdateExecutorTest.java @@ -35,18 +35,10 @@ import io.seata.rm.datasource.StatementProxy; import io.seata.rm.datasource.mock.MockDriver; import io.seata.rm.datasource.sql.struct.TableRecords; -import io.seata.rm.datasource.undo.UndoLogManagerFactory; -import io.seata.rm.datasource.undo.mysql.MySQLUndoLogManager; import io.seata.sqlparser.druid.mysql.MySQLUpdateRecognizer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.mockito.MockedStatic; -import org.mockito.Mockito; - -import javax.sql.DataSource; - -import static org.mockito.ArgumentMatchers.anyString; /** * @author will @@ -103,49 +95,175 @@ public static void init() { } @Test - public void testBeforeImage() throws SQLException { + public void testBeforeAndAfterImage() throws SQLException { Assertions.assertNotNull(updateExecutor.beforeImage()); String sql = "update table_update_executor_test set name = 'WILL' where id = 1"; List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); - Assertions.assertNotNull(updateExecutor.beforeImage()); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); } @Test - public void testBeforeImageWithTableAlias() throws SQLException { + public void testBeforeAndAfterImageWithTableAlias() throws SQLException { Assertions.assertNotNull(updateExecutor.beforeImage()); String sql = "update table_update_executor_test t set t.name = 'WILL' where t.id = 1"; List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); - String builtSql = updateExecutor.buildBeforeImageSQL(updateExecutor.getTableMeta(), new ArrayList<>()); - Assertions.assertTrue(builtSql.contains("t.updated")); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableSchema() throws SQLException { + String sql = "update seata.table_update_executor_test set name = 'WILL' where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableSchemaAndTableAlias() throws SQLException { + String sql = "update seata.table_update_executor_test t set t.name = 'WILL' where t.id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableSchemaQuote() throws SQLException { + String sql = "update `seata`.table_update_executor_test set name = 'WILL' where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableSchemaAndTableNameQuote() throws SQLException { + String sql = "update seata.`table_update_executor_test` set name = 'WILL' where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithTableSchemaQuoteAndTableNameQuote() throws SQLException { + String sql = "update `seata`.`table_update_executor_test` set name = 'WILL' where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithColumnQuote() throws SQLException { + String sql = "update table_update_executor_test set `name` = 'WILL' where `id` = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithUpperColumn() throws SQLException { + String sql = "update table_update_executor_test set NAME = 'WILL', UPDATED = `567` where ID = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); + Assertions.assertNotNull(afterImage); } @Test - public void testKeyword() throws SQLException { - String sql = "update table_update_executor_test set `all` = '1234' where id = 1"; + public void testBeforeAndAfterImageWithTableAliasAndUpperColumn() throws SQLException { + String sql = "update table_update_executor_test t set t.NAME = 'WILL', t.UPDATED = `567` where t.ID = 1"; List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); Assertions.assertNotNull(beforeImage); - Assertions.assertNotNull(updateExecutor.afterImage(beforeImage)); + Assertions.assertNotNull(afterImage); } @Test - public void testAfterImage() throws SQLException { + public void testBeforeAndAfterImageWithKeywordQuote() throws SQLException { + String sql = "update table_update_executor_test set `all` = '1234', `updated` = `567` where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + TableRecords beforeImage = updateExecutor.beforeImage(); TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); Assertions.assertNotNull(afterImage); + } - afterImage = updateExecutor.afterImage(new TableRecords()); + @Test + public void testBeforeAndAfterImageWithOnUpdateColumn() throws SQLException { + String sql = "update table_update_executor_test set updated = 1 where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); + + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); Assertions.assertNotNull(afterImage); + } + + @Test + public void testBeforeAndAfterImageWithOnUpdateUpperColumn() throws SQLException { + String sql = "update table_update_executor_test set UPDATED = 1 where id = 1"; + List asts = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL); + MySQLUpdateRecognizer recognizer = new MySQLUpdateRecognizer(sql, asts.get(0)); + updateExecutor = new UpdateExecutor(statementProxy, (statement, args) -> null, recognizer); - afterImage = updateExecutor.afterImage(null); + TableRecords beforeImage = updateExecutor.beforeImage(); + TableRecords afterImage = updateExecutor.afterImage(beforeImage); + Assertions.assertNotNull(beforeImage); Assertions.assertNotNull(afterImage); } }