Copilot commented on code in PR #8050: URL: https://github.com/apache/incubator-seata/pull/8050#discussion_r3091884958
########## changes/zh-cn/2.x.md: ########## @@ -30,8 +30,10 @@ - [[#8002](https://github.com/apache/incubator-seata/pull/8002)] 为namingserver指标增加Grafana dashboard JSON - [[#8020](https://github.com/apache/incubator-seata/pull/8020)] 新增 UnregisterRM 协议,在客户端销毁时通知服务端 - [[#8044](https://github.com/apache/incubator-seata/pull/8044)] 为 UnregisterRM 协议添加 protobuf 序列化支持 +- [[#8041](https://github.com/apache/incubator-seata/pull/8041)] 新增SQL Server 多主键支持 - [[#8046](https://github.com/apache/incubator-seata/pull/8046)] 添加了 fastjson2 和 jackson3 + Review Comment: There is an extra blank line introduced after the new #8041 entry, which leaves unnecessary vertical spacing in the rendered changelog. Consider removing the additional empty line to keep formatting consistent with surrounding entries. ```suggestion ``` ########## rm-datasource/src/test/java/org/apache/seata/rm/datasource/exec/SqlServerInsertExecutorTest.java: ########## @@ -209,4 +209,101 @@ private void mockStatementInsertRows() { rows.add(Arrays.asList(Null.get(), "xx", "xx", "xx")); when(sqlInsertRecognizer.getInsertRows(pkIndexMap.values())).thenReturn(rows); } + + @Test + public void testGetPkValues_compositePrimaryKey_withAllPkInInsert() throws Exception { + // Mock composite primary key: id + user_id + List<String> compositePkList = Arrays.asList(ID_COLUMN, USER_ID_COLUMN); + when(tableMeta.getPrimaryKeyOnlyName()).thenReturn(compositePkList); + + doReturn(tableMeta).when(insertExecutor).getTableMeta(); + doReturn(true).when(insertExecutor).containsPK(); // All primary keys are in INSERT statement + + // Mock getPkValuesByColumn to return expected values directly + Map<String, List<Object>> expectedPkValues = new HashMap<>(); + expectedPkValues.put(ID_COLUMN, Arrays.asList(1, 2)); + expectedPkValues.put(USER_ID_COLUMN, Arrays.asList("user1", "user2")); + doReturn(expectedPkValues).when(insertExecutor).getPkValuesByColumn(); + + Map<String, List<Object>> pkValues = insertExecutor.getPkValues(); + + // Verify composite primary key values are correctly retrieved + Assertions.assertNotNull(pkValues); + Assertions.assertEquals(expectedPkValues, pkValues); + + // Verify that getPkValuesByColumn was called, confirming the code path for composite keys with manual values + verify(insertExecutor).getPkValuesByColumn(); + } + + @Test + public void testGetPkValues_compositePrimaryKey_withAutoIncrement() throws Exception { + // Mock composite primary key with auto-increment columns + List<String> compositePkList = Arrays.asList(ID_COLUMN, USER_ID_COLUMN); + when(tableMeta.getPrimaryKeyOnlyName()).thenReturn(compositePkList); + + Map<String, ColumnMeta> pkMap = new HashMap<>(); + ColumnMeta idMeta = mock(ColumnMeta.class); + when(idMeta.isAutoincrement()).thenReturn(true); // Auto-increment + pkMap.put(ID_COLUMN, idMeta); + + ColumnMeta userIdMeta = mock(ColumnMeta.class); + when(userIdMeta.isAutoincrement()).thenReturn(true); // Auto-increment + pkMap.put(USER_ID_COLUMN, userIdMeta); + when(tableMeta.getPrimaryKeyMap()).thenReturn(pkMap); + + doReturn(tableMeta).when(insertExecutor).getTableMeta(); + doReturn(false).when(insertExecutor).containsPK(); // Primary keys not in INSERT + doReturn(Arrays.asList(PK_VALUE)).when(insertExecutor).getGeneratedKeys(); + + Map<String, List<Object>> pkValues = insertExecutor.getPkValues(); + + // Verify auto-increment column gets value from generated keys + Assertions.assertEquals(Arrays.asList(PK_VALUE), pkValues.get(ID_COLUMN)); + Assertions.assertEquals(Arrays.asList(PK_VALUE), pkValues.get(USER_ID_COLUMN)); + } + + @Test Review Comment: The new composite-PK tests cover (1) all PK columns provided and (2) all PK columns marked autoincrement, but they don't cover the common mixed case where some PK columns are provided in the INSERT and the remaining PK column is identity/auto-generated. Adding a test for that scenario would catch the current behavior where `getPkValues()` throws instead of merging manual + generated PK values. ```suggestion @Test public void testGetPkValues_compositePrimaryKey_withPartialPkInInsertAndAutoIncrement() throws Exception { // Mock composite primary key where one PK is provided in INSERT and the other is auto-increment List<String> compositePkList = Arrays.asList(ID_COLUMN, USER_ID_COLUMN); when(tableMeta.getPrimaryKeyOnlyName()).thenReturn(compositePkList); Map<String, ColumnMeta> pkMap = new HashMap<>(); ColumnMeta idMeta = mock(ColumnMeta.class); when(idMeta.isAutoincrement()).thenReturn(false); // Provided in INSERT pkMap.put(ID_COLUMN, idMeta); ColumnMeta userIdMeta = mock(ColumnMeta.class); when(userIdMeta.isAutoincrement()).thenReturn(true); // Generated by database pkMap.put(USER_ID_COLUMN, userIdMeta); when(tableMeta.getPrimaryKeyMap()).thenReturn(pkMap); mockParametersForCompositePk(); doReturn(tableMeta).when(insertExecutor).getTableMeta(); doReturn(true).when(insertExecutor).containsPK(); // One primary key column is present in INSERT doReturn(Arrays.asList(PK_VALUE)).when(insertExecutor).getGeneratedKeys(); Map<String, List<Object>> pkValues = insertExecutor.getPkValues(); // Verify manual and generated PK values are merged correctly Assertions.assertEquals(Arrays.asList(1), pkValues.get(ID_COLUMN)); Assertions.assertEquals(Arrays.asList(PK_VALUE), pkValues.get(USER_ID_COLUMN)); } @Test ``` ########## rm-datasource/src/main/java/org/apache/seata/rm/datasource/SqlGenerateUtils.java: ########## @@ -78,6 +79,11 @@ public static List<WhereSql> buildWhereConditionListByPKs(List<String> pkNameLis */ public static List<WhereSql> buildWhereConditionListByPKs( List<String> pkNameList, int rowSize, String dbType, int maxInSize) { + // SQL Server does not support tuple IN syntax: (col1,col2) IN ((?,?),(?,?)) + // Use AND/OR syntax instead + if (JdbcConstants.SQLSERVER.equalsIgnoreCase(dbType)) { Review Comment: The SQL Server special-case in `buildWhereConditionListByPKs` triggers for all PK sizes, including single-column PKs. This changes SQL Server WHERE generation from the existing tuple-`IN` form to an `OR` chain (e.g. `(id=?) OR (id=?)`), which is longer and can be less efficient for large batches. If the limitation only applies to composite PKs, gate this branch on `pkNameList.size() > 1` and let the existing implementation handle single-column PKs. ```suggestion // Use AND/OR syntax instead for composite primary keys if (JdbcConstants.SQLSERVER.equalsIgnoreCase(dbType) && pkNameList.size() > 1) { ``` ########## rm-datasource/src/test/java/org/apache/seata/rm/datasource/exec/SqlServerInsertExecutorTest.java: ########## @@ -209,4 +209,101 @@ private void mockStatementInsertRows() { rows.add(Arrays.asList(Null.get(), "xx", "xx", "xx")); when(sqlInsertRecognizer.getInsertRows(pkIndexMap.values())).thenReturn(rows); } + + @Test + public void testGetPkValues_compositePrimaryKey_withAllPkInInsert() throws Exception { + // Mock composite primary key: id + user_id + List<String> compositePkList = Arrays.asList(ID_COLUMN, USER_ID_COLUMN); + when(tableMeta.getPrimaryKeyOnlyName()).thenReturn(compositePkList); + + doReturn(tableMeta).when(insertExecutor).getTableMeta(); + doReturn(true).when(insertExecutor).containsPK(); // All primary keys are in INSERT statement + + // Mock getPkValuesByColumn to return expected values directly + Map<String, List<Object>> expectedPkValues = new HashMap<>(); + expectedPkValues.put(ID_COLUMN, Arrays.asList(1, 2)); + expectedPkValues.put(USER_ID_COLUMN, Arrays.asList("user1", "user2")); + doReturn(expectedPkValues).when(insertExecutor).getPkValuesByColumn(); + + Map<String, List<Object>> pkValues = insertExecutor.getPkValues(); + + // Verify composite primary key values are correctly retrieved + Assertions.assertNotNull(pkValues); + Assertions.assertEquals(expectedPkValues, pkValues); + + // Verify that getPkValuesByColumn was called, confirming the code path for composite keys with manual values + verify(insertExecutor).getPkValuesByColumn(); + } + + @Test + public void testGetPkValues_compositePrimaryKey_withAutoIncrement() throws Exception { + // Mock composite primary key with auto-increment columns + List<String> compositePkList = Arrays.asList(ID_COLUMN, USER_ID_COLUMN); + when(tableMeta.getPrimaryKeyOnlyName()).thenReturn(compositePkList); + + Map<String, ColumnMeta> pkMap = new HashMap<>(); + ColumnMeta idMeta = mock(ColumnMeta.class); + when(idMeta.isAutoincrement()).thenReturn(true); // Auto-increment + pkMap.put(ID_COLUMN, idMeta); + + ColumnMeta userIdMeta = mock(ColumnMeta.class); + when(userIdMeta.isAutoincrement()).thenReturn(true); // Auto-increment + pkMap.put(USER_ID_COLUMN, userIdMeta); + when(tableMeta.getPrimaryKeyMap()).thenReturn(pkMap); + + doReturn(tableMeta).when(insertExecutor).getTableMeta(); + doReturn(false).when(insertExecutor).containsPK(); // Primary keys not in INSERT + doReturn(Arrays.asList(PK_VALUE)).when(insertExecutor).getGeneratedKeys(); + + Map<String, List<Object>> pkValues = insertExecutor.getPkValues(); + + // Verify auto-increment column gets value from generated keys + Assertions.assertEquals(Arrays.asList(PK_VALUE), pkValues.get(ID_COLUMN)); + Assertions.assertEquals(Arrays.asList(PK_VALUE), pkValues.get(USER_ID_COLUMN)); + } + + @Test + public void testGetPkValues_compositePrimaryKey_nonAutoIncrementThrowsException() throws Exception { + // Mock composite primary key with non-auto-increment column not in INSERT + List<String> compositePkList = Arrays.asList(ID_COLUMN, USER_ID_COLUMN); + when(tableMeta.getPrimaryKeyOnlyName()).thenReturn(compositePkList); + + Map<String, ColumnMeta> pkMap = new HashMap<>(); + ColumnMeta idMeta = mock(ColumnMeta.class); + when(idMeta.isAutoincrement()).thenReturn(false); // Not auto-increment + pkMap.put(ID_COLUMN, idMeta); + + ColumnMeta userIdMeta = mock(ColumnMeta.class); + when(userIdMeta.isAutoincrement()).thenReturn(false); + pkMap.put(USER_ID_COLUMN, userIdMeta); + when(tableMeta.getPrimaryKeyMap()).thenReturn(pkMap); + + doReturn(tableMeta).when(insertExecutor).getTableMeta(); + doReturn(false).when(insertExecutor).containsPK(); // Primary keys not in INSERT + + // Should throw exception for non-auto-increment composite primary key + Assertions.assertThrows(NotSupportYetException.class, () -> insertExecutor.getPkValues()); + } + + private void mockParametersForCompositePk() { + Map<Integer, ArrayList<Object>> parameters = new HashMap<>(4); + ArrayList<Object> arrayList0 = new ArrayList<>(); + arrayList0.add(1); // id value + ArrayList<Object> arrayList1 = new ArrayList<>(); + arrayList1.add("userId1"); + ArrayList<Object> arrayList2 = new ArrayList<>(); + arrayList2.add("userName1"); + ArrayList<Object> arrayList3 = new ArrayList<>(); + arrayList3.add("userStatus1"); + parameters.put(1, arrayList0); + parameters.put(2, arrayList1); + parameters.put(3, arrayList2); + parameters.put(4, arrayList3); + PreparedStatementProxy psp = (PreparedStatementProxy) this.statementProxy; + when(psp.getParameters()).thenReturn(parameters); + + List<List<Object>> rows = new ArrayList<>(); + rows.add(Arrays.asList("?", "?", "?", "?")); + when(sqlInsertRecognizer.getInsertRows(pkIndexMap.values())).thenReturn(rows); + } Review Comment: `mockParametersForCompositePk()` is added but never called in this test class. Unused helpers make the test harder to maintain; either use it in the composite PK tests or remove it. ```suggestion ``` ########## rm-datasource/src/main/java/org/apache/seata/rm/datasource/exec/sqlserver/SqlServerInsertExecutor.java: ########## @@ -84,8 +86,29 @@ public Map<String, List<Object>> getPkValues() throws SQLException { pkValuesMap = getPkValuesWithNoColumn(); } } else { - // when there is a composite primary key - throw new NotSupportYetException("composite primary key is not supported in sqlserver"); + // when there is a composite primary key - Fix: Support SQL Server composite primary keys. + if (isContainsPk) { + // All primary key columns are manually assigned in the INSERT statement. + pkValuesMap = getPkValuesByColumn(); + } else { + // Some or all primary key columns are auto-generated. + // Get metadata for all primary key columns. + Map<String, ColumnMeta> primaryKeyMap = getTableMeta().getPrimaryKeyMap(); + + // Iterate over each primary key column. + for (String pkColumnName : pkColumnNameList) { + ColumnMeta pkMeta = primaryKeyMap.get(pkColumnName); + if (pkMeta.isAutoincrement()) { + // Auto-increment column: get from generated keys. + pkValuesMap.put(pkColumnName, getGeneratedKeys()); + } else { + // Non-auto-increment column: requires manual assignment, throw exception if not supported. + throw new NotSupportYetException( + "composite primary key with non-autoincrement column is not supported in sqlserver: " + + pkColumnName); + } + } Review Comment: `getPkValues()` for SQL Server composite PKs incorrectly assumes that when `containsPK()` is false, *none* of the PK columns are provided. For a common composite PK case where one PK column is provided in the INSERT and another is identity/auto-generated, this branch will throw for the provided non-autoincrement column instead of merging manual values with generated keys. Consider following the MySQLInsertExecutor pattern: start from `getPkValuesByColumn()` (for any PKs present) and then fill any missing PK columns that are autoincrement from `getGeneratedKeys()`; only throw if a required PK column is still missing and not autoincrement. ########## changes/en-us/2.x.md: ########## @@ -29,8 +29,10 @@ Add changes here for all PR submitted to the 2.x branch. - [[#8002](https://github.com/apache/incubator-seata/pull/8002)] add Grafana dashboard JSON for NamingServer metrics - [[#8020](https://github.com/apache/incubator-seata/pull/8020)] add UnregisterRM protocol to notify server on client destroy - [[#8044](https://github.com/apache/incubator-seata/pull/8044)] add protobuf serialization support for UnregisterRM protocol +- [[#8041](https://github.com/apache/incubator-seata/pull/8041)] add SQL Server composite primary keys - [[#8046](https://github.com/apache/incubator-seata/pull/8046)] add fastjson2 and jackson3 + Review Comment: There is an extra blank line introduced after the new #8041 entry, which leaves an empty list item spacing in the rendered changelog. Consider removing the additional empty line to keep formatting consistent with surrounding entries. ```suggestion ``` ########## rm-datasource/src/main/java/org/apache/seata/rm/datasource/AbstractConnectionProxy.java: ########## @@ -121,7 +122,14 @@ public PreparedStatement prepareStatement(String sql) throws SQLException { String[] pkNameArray = new String[tableMeta.getPrimaryKeyOnlyName().size()]; tableMeta.getPrimaryKeyOnlyName().toArray(pkNameArray); - targetPreparedStatement = getTargetConnection().prepareStatement(sql, pkNameArray); + // Fix: SQL Server does not support array of column names for getGeneratedKeys, use + // RETURN_GENERATED_KEYS instead. + if (JdbcConstants.SQLSERVER.equalsIgnoreCase(dbType)) { + targetPreparedStatement = + getTargetConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); + } else { + targetPreparedStatement = getTargetConnection().prepareStatement(sql, pkNameArray); + } Review Comment: `pkNameArray` is built unconditionally, but for SQL Server the code path now ignores it and uses `Statement.RETURN_GENERATED_KEYS`. Consider moving the `pkNameArray` allocation/population into the non-SQLServer branch to avoid unnecessary work on SQL Server inserts. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
