diff --git a/pom.xml b/pom.xml
index a6dc167a03..1ba673449d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.data
spring-data-commons
- 4.0.0-SNAPSHOT
+ 4.0.x-GH-3270-SNAPSHOT
Spring Data Core
Core Spring concepts underpinning every Spring Data module.
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
index 0dd0806637..fc4fb6a6eb 100644
--- a/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
+++ b/src/main/java/org/springframework/data/repository/aot/generate/AotQueryMethodGenerationContext.java
@@ -24,11 +24,11 @@
import javax.lang.model.element.Modifier;
import org.jspecify.annotations.Nullable;
-
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotationSelectors;
import org.springframework.core.annotation.MergedAnnotations;
+import org.springframework.data.repository.aot.generate.VariableNameFactory.VariableName;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.QueryMethod;
@@ -54,6 +54,7 @@ public class AotQueryMethodGenerationContext {
private final AotRepositoryFragmentMetadata targetTypeMetadata;
private final MethodMetadata targetMethodMetadata;
private final CodeBlocks codeBlocks;
+ private final VariableNameFactory variableNameFactory;
AotQueryMethodGenerationContext(RepositoryInformation repositoryInformation, Method method, QueryMethod queryMethod,
AotRepositoryFragmentMetadata targetTypeMetadata) {
@@ -65,6 +66,7 @@ public class AotQueryMethodGenerationContext {
this.targetTypeMetadata = targetTypeMetadata;
this.targetMethodMetadata = new MethodMetadata(repositoryInformation, method);
this.codeBlocks = new CodeBlocks(targetTypeMetadata);
+ this.variableNameFactory = LocalVariableNameFactory.forMethod(targetMethodMetadata);
}
AotRepositoryFragmentMetadata getTargetTypeMetadata() {
@@ -127,6 +129,16 @@ public TypeName getReturnTypeName() {
return TypeName.get(getReturnType().getType());
}
+ /**
+ * Suggests naming clash free variant for the given intended variable name within the local method context.
+ *
+ * @param variableName the intended variable name.
+ * @return the suggested VariableName
+ */
+ public VariableName suggestLocalVariableName(String variableName) {
+ return variableNameFactory.generateName(variableName);
+ }
+
/**
* Returns the required parameter name for the {@link Parameter#isBindable() bindable parameter} at the given
* {@code parameterIndex} or throws {@link IllegalArgumentException} if the parameter cannot be determined by its
@@ -274,8 +286,7 @@ public String getParameterNameOf(Class> type) {
return null;
}
- List> entries = new ArrayList<>(
- targetMethodMetadata.getMethodArguments().entrySet());
+ List> entries = new ArrayList<>(targetMethodMetadata.getMethodArguments().entrySet());
if (position < entries.size()) {
return entries.get(position).getKey();
}
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactory.java b/src/main/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactory.java
new file mode 100644
index 0000000000..63b56d1c28
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactory.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.repository.aot.generate;
+
+import java.util.Set;
+
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+/**
+ * {@link VariableNameFactory} implementation keeping track of defined names resolving name clashes using internal
+ * counter appending {@code _%d} to a suggested name in case of a clash.
+ *
+ * @author Christoph Strobl
+ * @since 4.0
+ */
+class LocalVariableNameFactory implements VariableNameFactory {
+
+ private final MultiValueMap variables;
+
+ static LocalVariableNameFactory forMethod(MethodMetadata methodMetadata) {
+ return of(methodMetadata.getMethodArguments().keySet());
+ }
+
+ static LocalVariableNameFactory empty() {
+ return of(Set.of());
+ }
+
+ static LocalVariableNameFactory of(Set variables) {
+ return new LocalVariableNameFactory(variables);
+ }
+
+ LocalVariableNameFactory(Iterable predefinedVariableNames) {
+
+ variables = new LinkedMultiValueMap<>();
+ for (String parameterName : predefinedVariableNames) {
+ variables.add(parameterName, new VariableName(parameterName));
+ }
+ }
+
+ @Override
+ public VariableName generateName(String suggestedName) {
+
+ if (!variables.containsKey(suggestedName)) {
+ VariableName variableName = new VariableName(suggestedName);
+ variables.add(suggestedName, variableName);
+ return variableName;
+ }
+
+ String targetName = suggestTargetName(suggestedName);
+ VariableName variableName = new VariableName(suggestedName, targetName);
+ variables.add(suggestedName, variableName);
+ variables.add(targetName, variableName);
+ return variableName;
+ }
+
+ String suggestTargetName(String suggested) {
+ return suggestTargetName(suggested, 1);
+ }
+
+ String suggestTargetName(String suggested, int counter) {
+
+ String targetName = "%s_%s".formatted(suggested, counter);
+ if (!variables.containsKey(targetName)) {
+ return targetName;
+ }
+ return suggestTargetName(suggested, counter + 1);
+ }
+}
diff --git a/src/main/java/org/springframework/data/repository/aot/generate/VariableNameFactory.java b/src/main/java/org/springframework/data/repository/aot/generate/VariableNameFactory.java
new file mode 100644
index 0000000000..1111474b04
--- /dev/null
+++ b/src/main/java/org/springframework/data/repository/aot/generate/VariableNameFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.repository.aot.generate;
+
+/**
+ * @author Christoph Strobl
+ * @since 4.0
+ */
+public interface VariableNameFactory {
+
+ VariableName generateName(String suggestedName);
+
+ record VariableName(String source, String target) {
+
+ public VariableName(String source) {
+ this(source, source);
+ }
+
+ public String name() {
+ return target;
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactoryUnitTests.java b/src/test/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactoryUnitTests.java
new file mode 100644
index 0000000000..63cfc110a1
--- /dev/null
+++ b/src/test/java/org/springframework/data/repository/aot/generate/LocalVariableNameFactoryUnitTests.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.repository.aot.generate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Christoph Strobl
+ */
+class LocalVariableNameFactoryUnitTests {
+
+ LocalVariableNameFactory variableNameFactory;
+
+ @BeforeEach
+ void beforeEach() {
+ variableNameFactory = LocalVariableNameFactory.of(Set.of("firstname", "lastname", "sort"));
+ }
+
+ @Test // GH-3270
+ void resolvesNameClashesInNames() {
+
+ assertThat(variableNameFactory.generateName("name").name()).isEqualTo("name");
+ assertThat(variableNameFactory.generateName("name").name()).isEqualTo("name_1");
+ assertThat(variableNameFactory.generateName("name").name()).isEqualTo("name_2");
+ assertThat(variableNameFactory.generateName("name1").name()).isEqualTo("name1");
+ assertThat(variableNameFactory.generateName("name3").name()).isEqualTo("name3");
+ assertThat(variableNameFactory.generateName("name3").name()).isEqualTo("name3_1");
+ assertThat(variableNameFactory.generateName("name4_1").name()).isEqualTo("name4_1");
+ assertThat(variableNameFactory.generateName("name4").name()).isEqualTo("name4");
+ assertThat(variableNameFactory.generateName("name4_1_1").name()).isEqualTo("name4_1_1");
+ assertThat(variableNameFactory.generateName("name4_1").name()).isEqualTo("name4_1_2");
+ assertThat(variableNameFactory.generateName("name4_1").name()).isEqualTo("name4_1_3");
+ }
+
+ @Test // GH-3270
+ void considersPredefinedNames() {
+ assertThat(variableNameFactory.generateName("firstname").name()).isEqualTo("firstname_1");
+ }
+
+ @Test // GH-3270
+ void considersCase() {
+ assertThat(variableNameFactory.generateName("firstName").name()).isEqualTo("firstName");
+ }
+}