Reject overlapping declared and generated key columns in SimpleJdbcInsert

When a column was declared via usingColumns() and also listed in
usingGeneratedKeyColumns(), TableMetaDataContext.reconcileColumnsToUse
accepted the declared list as-is: the generated key column was rendered
into the INSERT statement and counted against the parameter values,
even though the database is expected to generate its value.

Such an overlap is a configuration error, so it is now rejected at
compile time with an InvalidDataAccessApiUsageException naming the
offending columns in their declared spelling, consistent with the
existing validation in AbstractJdbcInsert.compile(). Matching is
case-insensitive, mirroring the normalization used for auto-discovered
columns; the auto-discovery path itself is unchanged and continues to
exclude generated key columns silently.

The tests cover the rejection, its message, a case-insensitive variant,
and the untouched non-overlapping declared path.

Closes gh-37014

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
This commit is contained in:
김준형
2026-09-07 10:34:38 +02:00
committed by GitHub
parent 99f2ccc2ec
commit e06482ad51
2 changed files with 73 additions and 3 deletions
@@ -205,13 +205,23 @@ public class TableMetaDataContext {
if (generatedKeyNames.length > 0) {
this.generatedKeyColumnsUsed = true;
}
if (!declaredColumns.isEmpty()) {
return new ArrayList<>(declaredColumns);
}
Set<String> keys = CollectionUtils.newLinkedHashSet(generatedKeyNames.length);
for (String key : generatedKeyNames) {
keys.add(key.toUpperCase(Locale.ROOT));
}
if (!declaredColumns.isEmpty()) {
List<String> overlapping = new ArrayList<>();
for (String column : declaredColumns) {
if (keys.contains(column.toUpperCase(Locale.ROOT))) {
overlapping.add(column);
}
}
if (!overlapping.isEmpty()) {
throw new InvalidDataAccessApiUsageException(
"Declared columns " + overlapping + " must not overlap with generated key columns");
}
return new ArrayList<>(declaredColumns);
}
List<String> columns = new ArrayList<>();
for (TableParameterMetaData meta : obtainMetaDataProvider().getTableParameterMetaData()) {
if (!keys.contains(meta.getParameterName().toUpperCase(Locale.ROOT))) {
@@ -29,11 +29,13 @@ import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.jdbc.core.SqlParameterValue;
import org.springframework.jdbc.core.metadata.TableMetaDataContext;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
@@ -153,4 +155,62 @@ class TableMetaDataContextTests {
verify(columnsResultSet).close();
}
@Test
void overlappingDeclaredAndGeneratedKeyColumnsAreRejected() throws Exception {
initializeTwoColumnCustomersTable();
context.setTableName("customers");
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
.isThrownBy(() -> context.processMetaData(
dataSource, List.of("id", "name"), new String[] { "id" }))
.withMessage("Declared columns [id] must not overlap with generated key columns");
}
@Test
void overlappingDeclaredAndGeneratedKeyColumnsAreRejectedRegardlessOfCase() throws Exception {
initializeTwoColumnCustomersTable();
context.setTableName("customers");
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
.isThrownBy(() -> context.processMetaData(
dataSource, List.of("ID", "name"), new String[] { "id" }))
.withMessage("Declared columns [ID] must not overlap with generated key columns");
}
@Test
void declaredColumnsWithoutOverlapAreUsedAsIs() throws Exception {
initializeTwoColumnCustomersTable();
MapSqlParameterSource map = new MapSqlParameterSource();
map.addValue("name", "Sven");
String[] keyCols = new String[] { "id" };
context.setTableName("customers");
context.processMetaData(dataSource, List.of("name"), keyCols);
List<Object> values = context.matchInParameterValuesWithInsertColumns(map);
String insertString = context.createInsertString(keyCols);
assertThat(insertString).isEqualTo("INSERT INTO customers (name) VALUES(?)");
assertThat(values).containsExactly("Sven");
}
private void initializeTwoColumnCustomersTable() throws Exception {
ResultSet metaDataResultSet = mock();
given(metaDataResultSet.next()).willReturn(true, false);
given(metaDataResultSet.getString("TABLE_SCHEM")).willReturn("me");
given(metaDataResultSet.getString("TABLE_NAME")).willReturn("customers");
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("me");
given(databaseMetaData.storesLowerCaseIdentifiers()).willReturn(true);
given(databaseMetaData.getTables(null, null, "customers", null)).willReturn(metaDataResultSet);
given(databaseMetaData.getColumns(null, "me", "customers", null)).willReturn(columnsResultSet);
}
}