From 15d9e57760bd20de6559957c9d9625d30bb8eca7 Mon Sep 17 00:00:00 2001 From: junhyeong9812 Date: Wed, 8 Jul 2026 08:21:05 +0900 Subject: [PATCH] Exclude generated key columns from declared insert columns TableMetaDataContext.reconcileColumnsToUse() filtered generated key columns out of the auto-discovered column list, but returned declared columns verbatim when usingColumns() was used together with usingGeneratedKeyColumns() for the same column. This left the insert SQL (which filters generated keys separately in createInsertString()) out of sync with the bound values and types arrays (which iterate the unfiltered declared column list), causing a parameter count mismatch at execution time. Signed-off-by: junhyeong9812 --- .../core/metadata/TableMetaDataContext.java | 12 +- .../simple/TableMetaDataContextTests.java | 122 ++++++++++++++++++ 2 files changed, 131 insertions(+), 3 deletions(-) diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java index 7c741ef9fb05..dcb115c8e92d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java @@ -205,13 +205,19 @@ protected List reconcileColumnsToUse(List declaredColumns, Strin if (generatedKeyNames.length > 0) { this.generatedKeyColumnsUsed = true; } - if (!declaredColumns.isEmpty()) { - return new ArrayList<>(declaredColumns); - } Set keys = CollectionUtils.newLinkedHashSet(generatedKeyNames.length); for (String key : generatedKeyNames) { keys.add(key.toUpperCase(Locale.ROOT)); } + if (!declaredColumns.isEmpty()) { + List columns = new ArrayList<>(); + for (String column : declaredColumns) { + if (!keys.contains(column.toUpperCase(Locale.ROOT))) { + columns.add(column); + } + } + return columns; + } List columns = new ArrayList<>(); for (TableParameterMetaData meta : obtainMetaDataProvider().getTableParameterMetaData()) { if (!keys.contains(meta.getParameterName().toUpperCase(Locale.ROOT))) { diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/TableMetaDataContextTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/TableMetaDataContextTests.java index f2b06097ae7b..d52656ac6dc7 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/TableMetaDataContextTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/TableMetaDataContextTests.java @@ -153,4 +153,126 @@ void tableWithSingleColumnGeneratedKey() throws Exception { verify(columnsResultSet).close(); } + @Test + void declaredColumnsIncludingGeneratedKeyAreExcluded() throws Exception { + final String TABLE = "customers"; + final String USER = "me"; + + ResultSet metaDataResultSet = mock(); + given(metaDataResultSet.next()).willReturn(true, false); + given(metaDataResultSet.getString("TABLE_SCHEM")).willReturn(USER); + given(metaDataResultSet.getString("TABLE_NAME")).willReturn(TABLE); + given(metaDataResultSet.getString("TABLE_TYPE")).willReturn("TABLE"); + + ResultSet columnsResultSet = mock(); + given(columnsResultSet.next()).willReturn(true, true, false); + given(columnsResultSet.getString("COLUMN_NAME")).willReturn("id", "name"); + given(columnsResultSet.getInt("DATA_TYPE")).willReturn(Types.INTEGER, Types.VARCHAR); + given(columnsResultSet.getBoolean("NULLABLE")).willReturn(false, true); + + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getDatabaseProductVersion()).willReturn("1.0"); + given(databaseMetaData.getUserName()).willReturn(USER); + given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true); + given(databaseMetaData.getTables(null, null, TABLE, null)).willReturn(metaDataResultSet); + given(databaseMetaData.getColumns(null, USER, TABLE, null)).willReturn(columnsResultSet); + + MapSqlParameterSource map = new MapSqlParameterSource(); + map.addValue("id", 1); + map.addValue("name", "Sven"); + String[] keyCols = new String[] { "id" }; + context.setTableName(TABLE); + context.processMetaData(dataSource, List.of("id", "name"), keyCols); + List values = context.matchInParameterValuesWithInsertColumns(map); + String insertString = context.createInsertString(keyCols); + int[] insertTypes = context.createInsertTypes(); + + assertThat(insertString).as("insert string should exclude the declared generated key column") + .isEqualTo("INSERT INTO customers (name) VALUES(?)"); + assertThat(values).as("values should exclude the declared generated key column").containsExactly("Sven"); + assertThat(insertTypes.length).as("types array must match the number of placeholders").isEqualTo(1); + verify(metaDataResultSet, atLeastOnce()).next(); + verify(columnsResultSet, atLeastOnce()).next(); + verify(metaDataResultSet).close(); + verify(columnsResultSet).close(); + } + + @Test + void declaredColumnsMatchingGeneratedKeysProduceEmptyInsert() throws Exception { + final String TABLE = "customers"; + final String USER = "me"; + + ResultSet metaDataResultSet = mock(); + given(metaDataResultSet.next()).willReturn(true, false); + given(metaDataResultSet.getString("TABLE_SCHEM")).willReturn(USER); + given(metaDataResultSet.getString("TABLE_NAME")).willReturn(TABLE); + given(metaDataResultSet.getString("TABLE_TYPE")).willReturn("TABLE"); + + ResultSet columnsResultSet = mock(); + given(columnsResultSet.next()).willReturn(true, false); + given(columnsResultSet.getString("COLUMN_NAME")).willReturn("id"); + given(columnsResultSet.getInt("DATA_TYPE")).willReturn(Types.INTEGER); + given(columnsResultSet.getBoolean("NULLABLE")).willReturn(false); + + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getDatabaseProductVersion()).willReturn("1.0"); + given(databaseMetaData.getUserName()).willReturn(USER); + given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true); + given(databaseMetaData.getTables(null, null, TABLE, null)).willReturn(metaDataResultSet); + given(databaseMetaData.getColumns(null, USER, TABLE, null)).willReturn(columnsResultSet); + + MapSqlParameterSource map = new MapSqlParameterSource(); + map.addValue("id", 1); + String[] keyCols = new String[] { "id" }; + context.setTableName(TABLE); + context.processMetaData(dataSource, List.of("id"), keyCols); + List values = context.matchInParameterValuesWithInsertColumns(map); + String insertString = context.createInsertString(keyCols); + int[] insertTypes = context.createInsertTypes(); + + assertThat(insertString).as("empty insert not generated correctly") + .isEqualTo("INSERT INTO customers () VALUES()"); + assertThat(values).as("no values should remain once the only declared column is the generated key").isEmpty(); + assertThat(insertTypes).as("no types should remain once the only declared column is the generated key").isEmpty(); + } + + @Test + void declaredColumnsExcludeGeneratedKeyRegardlessOfCase() throws Exception { + final String TABLE = "customers"; + final String USER = "me"; + + ResultSet metaDataResultSet = mock(); + given(metaDataResultSet.next()).willReturn(true, false); + given(metaDataResultSet.getString("TABLE_SCHEM")).willReturn(USER); + given(metaDataResultSet.getString("TABLE_NAME")).willReturn(TABLE); + given(metaDataResultSet.getString("TABLE_TYPE")).willReturn("TABLE"); + + ResultSet columnsResultSet = mock(); + given(columnsResultSet.next()).willReturn(true, true, false); + given(columnsResultSet.getString("COLUMN_NAME")).willReturn("id", "name"); + given(columnsResultSet.getInt("DATA_TYPE")).willReturn(Types.INTEGER, Types.VARCHAR); + given(columnsResultSet.getBoolean("NULLABLE")).willReturn(false, true); + + given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB"); + given(databaseMetaData.getDatabaseProductVersion()).willReturn("1.0"); + given(databaseMetaData.getUserName()).willReturn(USER); + given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true); + given(databaseMetaData.getTables(null, null, TABLE, null)).willReturn(metaDataResultSet); + given(databaseMetaData.getColumns(null, USER, TABLE, null)).willReturn(columnsResultSet); + + MapSqlParameterSource map = new MapSqlParameterSource(); + map.addValue("ID", 1); + map.addValue("name", "Sven"); + String[] keyCols = new String[] { "id" }; + context.setTableName(TABLE); + context.processMetaData(dataSource, List.of("ID", "name"), keyCols); + List values = context.matchInParameterValuesWithInsertColumns(map); + String insertString = context.createInsertString(keyCols); + + assertThat(insertString).as("insert string should exclude the declared generated key column regardless of case") + .isEqualTo("INSERT INTO customers (name) VALUES(?)"); + assertThat(values).as("values should exclude the declared generated key column regardless of case") + .containsExactly("Sven"); + } + }