This is an automated email from the ASF dual-hosted git repository.

matrei pushed a commit to branch database-cleanup-feature-feedback
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 588eab185a2439216bef146e70719cd088afa0eb
Author: Mattias Reichel <[email protected]>
AuthorDate: Wed Feb 25 15:14:30 2026 +0100

    style: groovy cleanup
---
 .../cleanup/core/ApplicationContextResolver.groovy |   1 -
 .../testing/cleanup/core/DatabaseCleaner.groovy    |  35 ++++-
 .../testing/cleanup/core/DatabaseCleanup.groovy    |   3 +-
 .../cleanup/core/DatabaseCleanupContext.groovy     | 149 +++++++++++++--------
 .../cleanup/core/DatabaseCleanupExtension.groovy   |  82 ++++++------
 .../cleanup/core/DatabaseCleanupInterceptor.groovy |  73 ++++++----
 .../cleanup/core/DatabaseCleanupStats.groovy       |  16 +--
 .../cleanup/core/DatasourceCleanupMapping.groovy   |  68 +++++-----
 .../core/DefaultApplicationContextResolver.groovy  |  11 +-
 .../cleanup/core/TestContextHolderListener.groovy  |   1 -
 .../testing/cleanup/h2/H2DatabaseCleaner.groovy    |  60 +++------
 .../cleanup/h2/H2DatabaseCleanupHelper.groovy      |  97 +++++---------
 .../postgresql/PostgresDatabaseCleaner.groovy      |  90 ++++---------
 .../PostgresDatabaseCleanupHelper.groovy           |  74 ++++------
 14 files changed, 363 insertions(+), 397 deletions(-)

diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/ApplicationContextResolver.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/ApplicationContextResolver.groovy
index d367da4e2c..45fff9d4a0 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/ApplicationContextResolver.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/ApplicationContextResolver.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
 import groovy.transform.CompileStatic
diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleaner.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleaner.groovy
index 3b35214b24..5329039153 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleaner.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleaner.groovy
@@ -16,14 +16,15 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
 import javax.sql.DataSource
 
 import groovy.transform.CompileStatic
+import org.codehaus.groovy.runtime.InvokerHelper
 
 import org.springframework.context.ApplicationContext
+import org.springframework.util.ClassUtils
 
 /**
  * SPI interface for database-specific cleanup implementations.
@@ -37,16 +38,16 @@ import org.springframework.context.ApplicationContext
  * {@code 
META-INF/services/org.apache.grails.testing.cleanup.core.DatabaseCleaner}.</p>
  */
 @CompileStatic
-interface DatabaseCleaner {
+trait DatabaseCleaner {
 
     /**
      * Returns the database type that this cleaner supports (e.g., {@code 
"h2"}, {@code "mysql"},
-     * {@code "postgresql"}). This value is used to match cleaners to 
datasources at runtime and
+     * {@code "postgresql"}). This value is used to match cleaners to data 
sources at runtime and
      * must be unique across all {@link DatabaseCleaner} implementations on 
the classpath.
      *
      * @return a non-null, non-empty string identifying the database type
      */
-    String databaseType()
+    abstract String databaseType()
 
     /**
      * Returns {@code true} if this cleaner supports the given {@link 
DataSource}.
@@ -55,7 +56,7 @@ interface DatabaseCleaner {
      * @param dataSource the datasource to check
      * @return {@code true} if this cleaner can handle the given datasource
      */
-    boolean supports(DataSource dataSource)
+    abstract boolean supports(DataSource dataSource)
 
     /**
      * Performs the cleanup (truncation) of all tables in the given datasource.
@@ -64,5 +65,27 @@ interface DatabaseCleaner {
      * @param dataSource the datasource to clean
      * @return statistics about what was cleaned
      */
-    DatabaseCleanupStats cleanup(ApplicationContext applicationContext, 
DataSource dataSource)
+    abstract DatabaseCleanupStats cleanup(ApplicationContext 
applicationContext, DataSource dataSource)
+
+    /**
+     * Optional callback to evict the Hibernate second-level cache after 
database cleanup.
+     * This is necessary for tests that rely on Hibernate's caching behavior 
and want to
+     * ensure a clean state after truncation. Implementations can check for 
the presence
+     * of Hibernate and evict the cache if applicable.
+     *
+     * @param applicationContext the application context
+     */
+    void cleanupCacheLayer(ApplicationContext applicationContext) {
+        // Clear the 2nd layer cache if it exists
+        if (ClassUtils.isPresent('org.hibernate.SessionFactory', 
this.getClass().classLoader)) {
+            def sessionFactory = applicationContext.getBean(
+                    'sessionFactory',
+                    Class.forName('org.hibernate.SessionFactory')
+            )
+            def cache = InvokerHelper.getProperty(sessionFactory, 'cache')
+            if (cache) {
+                InvokerHelper.invokeMethod(cache, 'evictAllRegions', null)
+            }
+        }
+    }
 }
diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanup.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanup.groovy
index ee59e42d11..3163daac31 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanup.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanup.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
 import java.lang.annotation.ElementType
@@ -85,7 +84,7 @@ import java.lang.annotation.Target
     /**
      * The datasource entries to clean up. Each entry can be a plain 
datasource bean name
      * (e.g., {@code "dataSource"}) or a datasource-to-type mapping
-     * (e.g., {@code "dataSource:h2"}). If empty (the default), all 
datasources found
+     * (e.g., {@code "dataSource:h2"}). If empty (the default), all data 
sources found
      * in the application context will be cleaned using auto-discovery.
      *
      * @return an array of datasource entries
diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupContext.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupContext.groovy
index 13aa4933e6..10fdba73dd 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupContext.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupContext.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
 import javax.sql.DataSource
@@ -28,7 +27,7 @@ import org.springframework.context.ApplicationContext
 
 /**
  * Context that holds the discovered {@link DatabaseCleaner} implementations 
and
- * provides access to the application's datasources for cleanup operations.
+ * provides access to the application's data sources for cleanup operations.
  *
  * <p>Multiple cleaners can be registered (one per database type). When 
performing cleanup,
  * the context matches each datasource to an appropriate cleaner using either 
an explicit
@@ -41,31 +40,39 @@ import org.springframework.context.ApplicationContext
 class DatabaseCleanupContext {
 
     private ApplicationContext applicationContext
-    final Map<String, DatabaseCleaner> cleanersByType
+    private final Map<String, DatabaseCleaner> cleanersByType
 
     DatabaseCleanupContext(List<DatabaseCleaner> cleaners) {
         cleanersByType = createCleanersMap(cleaners)
     }
 
     private static Map<String, DatabaseCleaner> 
createCleanersMap(List<DatabaseCleaner> cleaners) {
-        Map<String, DatabaseCleaner> typeMap = [:]
-        for (DatabaseCleaner cleaner : cleaners) {
-            def type = cleaner.databaseType()
-            if (!type || type.trim().isEmpty()) {
+        def typeMap = [:] as Map<String, DatabaseCleaner>
+        cleaners.each {
+            def type = it.databaseType()?.trim()
+            if (!type) {
                 throw new IllegalStateException(
-                        "DatabaseCleaner implementation ${cleaner.class.name} 
returned a null or empty databaseType()" as String)
+                        "DatabaseCleaner implementation $it.class.name " +
+                        'returned a null or empty databaseType()'
+                )
             }
-
-            DatabaseCleaner existing = typeMap.get(type)
+            def existing = typeMap[type]
             if (existing) {
                 throw new IllegalStateException(
-                        "Duplicate databaseType '${type}' declared by both 
${existing.class.name} and ${cleaner.class.name}. Each DatabaseCleaner must 
declare a unique databaseType." as String)
+                        "Duplicate databaseType '$type' declared by both " +
+                        "$existing.class.name and $it.class.name. " +
+                        'Each DatabaseCleaner must declare a unique 
databaseType.'
+                )
             }
 
-            typeMap.put(type, cleaner)
-            log.debug('Discovered DatabaseCleaner implementation: {} (type: 
{})', cleaner.class.name, type)
+            typeMap[type] = it
+            log.debug(
+                    'Discovered DatabaseCleaner implementation: {} (type: {})',
+                    it.class.name,
+                    type
+            )
         }
-        Collections.unmodifiableMap(typeMap)
+        typeMap.asImmutable()
     }
 
     /**
@@ -84,14 +91,14 @@ class DatabaseCleanupContext {
     }
 
     /**
-     * Performs cleanup on datasources found in the application context using 
the
+     * Performs cleanup on data sources found in the application context using 
the
      * provided mapping from the {@link DatabaseCleanup} annotation.
      *
-     * <p>If the mapping specifies explicit database types for datasources, 
those types
+     * <p>If the mapping specifies explicit database types for data sources, 
those types
      * are used to look up the cleaner directly. Otherwise, auto-discovery via
      * {@link DatabaseCleaner#supports(DataSource)} is used.</p>
      *
-     * @param mapping the parsed annotation value describing which datasources 
to clean
+     * @param mapping the parsed annotation value describing which data 
sources to clean
      *        and optionally which cleaner types to use
      * @return a list of {@link DatabaseCleanupStats}, one per datasource 
cleaned
      * @throws IllegalStateException if a datasource marked for cleanup has no
@@ -101,63 +108,100 @@ class DatabaseCleanupContext {
      */
     List<DatabaseCleanupStats> performCleanup(DatasourceCleanupMapping 
mapping) {
         if (!applicationContext) {
-            throw new IllegalStateException('Cannot perform database cleanup: 
ApplicationContext is not available')
+            throw new IllegalStateException(
+                    'Cannot perform database cleanup: ApplicationContext is 
not available'
+            )
         }
         if (!cleanersByType) {
-            throw new IllegalStateException('Cannot perform database cleanup: 
no DatabaseCleaner implementations found')
+            throw new IllegalStateException(
+                    'Cannot perform database cleanup: no DatabaseCleaner 
implementations found'
+            )
         }
 
-        Map<String, DataSource> allDataSources = 
applicationContext.getBeansOfType(DataSource)
-        List<DatabaseCleanupStats> allStats = []
+        def allDataSources = applicationContext.getBeansOfType(DataSource)
+        def allStats = [] as List<DatabaseCleanupStats>
 
         if (mapping.cleanAll) {
-            // Clean all datasources using auto-discovery
-            allDataSources.each { String beanName, DataSource dataSource ->
-                DatabaseCleaner cleaner = findCleanerFor(dataSource)
+            // Clean all data sources using auto-discovery
+            allDataSources.each { beanName, dataSource ->
+                def cleaner = findCleanerFor(dataSource)
                 if (!cleaner) {
                     throw new IllegalStateException(
-                            "No DatabaseCleaner implementation found that 
supports datasource '${beanName}'. Ensure that a database-specific cleanup 
library (e.g., grails-testing-support-cleanup-h2) is on the classpath for each 
database type used in your tests. Available cleaners: 
${cleanersByType.values().collect { it.databaseType() }}" as String)
+                            'No DatabaseCleaner implementation found that 
supports ' +
+                            "datasource '$beanName'. Ensure that a 
database-specific " +
+                            'cleanup library (e.g., 
grails-testing-support-cleanup-h2) ' +
+                            'is on the classpath for each database type used 
in your tests. ' +
+                            "Available cleaners: 
${cleanersByType.values()*.databaseType()}"
+                    )
                 }
 
-                log.debug('Cleaning up datasource: {} (using {} cleaner)', 
beanName, cleaner.databaseType())
-                DatabaseCleanupStats stats = 
cleaner.cleanup(applicationContext, dataSource)
+                log.debug(
+                        'Cleaning up datasource: {} (using {} cleaner)',
+                        beanName,
+                        cleaner.databaseType()
+                )
+                def stats = cleaner.cleanup(applicationContext, dataSource)
                 stats.datasourceName = beanName
                 if (stats.tableRowCounts) {
-                    log.debug('Cleaned {} tables from datasource {}', 
stats.tableRowCounts.size(), beanName)
+                    log.debug(
+                            'Cleaned {} tables from datasource {}',
+                            stats.tableRowCounts.size(),
+                            beanName
+                    )
                 }
-                allStats.add(stats)
+                allStats << stats
             }
         } else {
-            // Clean specific datasources per the mapping entries
-            for (DatasourceCleanupMapping.Entry entry : mapping.entries) {
-                DataSource dataSource = 
allDataSources.get(entry.datasourceName)
+            // Clean specific data sources per the mapping entries
+            mapping.entries.each {
+                def dsName = it.datasourceName
+                def dataSource = allDataSources[dsName]
                 if (!dataSource) {
                     throw new IllegalStateException(
-                            "Datasource '${entry.datasourceName}' specified in 
@DatabaseCleanup was not found in the application context. Available 
datasources: ${allDataSources.keySet()}" as String)
+                            "Datasource '$dsName' specified in 
@DatabaseCleanup " +
+                            'was not found in the application context. ' +
+                            "Available datasources: ${allDataSources.keySet()}"
+                    )
                 }
 
-                DatabaseCleaner cleaner
-                if (entry.hasExplicitType()) {
-                    cleaner = cleanersByType.get(entry.databaseType)
-                    if (!cleaner) {
-                        throw new IllegalStateException(
-                                "No DatabaseCleaner found for database type 
'${entry.databaseType}' specified in @DatabaseCleanup for datasource 
'${entry.datasourceName}'. Available cleaner types: ${cleanersByType.keySet()}" 
as String)
-                    }
-                } else {
-                    cleaner = findCleanerFor(dataSource)
-                    if (!cleaner) {
+                def cleaner = it.hasExplicitType() ?
+                        cleanersByType[it.databaseType] :
+                        findCleanerFor(dataSource)
+
+                if (!cleaner) {
+                    if (it.hasExplicitType()) {
                         throw new IllegalStateException(
-                                "No DatabaseCleaner implementation found that 
supports datasource '${entry.datasourceName}'. Ensure that a database-specific 
cleanup library (e.g., grails-testing-support-cleanup-h2) is on the classpath, 
or specify the database type explicitly: '${entry.datasourceName}:type'. 
Available cleaners: ${cleanersByType.values().collect { it.databaseType() }}" 
as String)
+                                "No DatabaseCleaner found for database type 
'$it.databaseType' " +
+                                "specified in @DatabaseCleanup for datasource 
'$dsName'. " +
+                                "Available cleaner types: 
${cleanersByType.keySet()}"
+                        )
                     }
+                    throw new IllegalStateException(
+                            "No DatabaseCleaner implementation found that 
supports datasource '$dsName'. " +
+                            'Ensure that a database-specific cleanup library ' 
+
+                            '(e.g., grails-testing-support-cleanup-h2) is on 
the classpath, ' +
+                            "or specify the database type explicitly: 
'$dsName:type'. " +
+                            "Available cleaners: 
${cleanersByType.values()*.databaseType()}"
+                    )
                 }
 
-                log.debug('Cleaning up datasource: {} (using {} cleaner)', 
entry.datasourceName, cleaner.databaseType())
-                DatabaseCleanupStats stats = 
cleaner.cleanup(applicationContext, dataSource)
-                stats.datasourceName = entry.datasourceName
+                log.debug(
+                        'Cleaning up datasource: {} (using {} cleaner)',
+                        dsName,
+                        cleaner.databaseType()
+                )
+                def stats = cleaner.cleanup(applicationContext, dataSource)
+                stats.datasourceName = dsName
+
                 if (stats.tableRowCounts) {
-                    log.debug('Cleaned {} tables from datasource {}', 
stats.tableRowCounts.size(), entry.datasourceName)
+                    log.debug(
+                            'Cleaned {} tables from datasource {}',
+                            stats.tableRowCounts.size(),
+                            dsName
+                    )
                 }
-                allStats.add(stats)
+
+                allStats << stats
             }
         }
 
@@ -171,11 +215,6 @@ class DatabaseCleanupContext {
      * @return the matching cleaner, or {@code null} if none supports it
      */
     private DatabaseCleaner findCleanerFor(DataSource dataSource) {
-        for (DatabaseCleaner cleaner : cleanersByType.values()) {
-            if (cleaner.supports(dataSource)) {
-                return cleaner
-            }
-        }
-        null
+        cleanersByType.values().find { it.supports(dataSource) }
     }
 }
diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtension.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtension.groovy
index 375709dacb..b50524f8d9 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtension.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupExtension.groovy
@@ -16,20 +16,16 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
-import java.lang.reflect.Method
-
 import groovy.transform.CompileStatic
 import groovy.util.logging.Slf4j
 
-import org.springframework.boot.test.context.SpringBootTest
-
 import org.spockframework.runtime.extension.IGlobalExtension
-import org.spockframework.runtime.model.FeatureInfo
 import org.spockframework.runtime.model.SpecInfo
 
+import org.springframework.boot.test.context.SpringBootTest
+
 /**
  * Spock global extension that detects the {@link DatabaseCleanup} annotation 
on test classes
  * and methods, and registers the {@link DatabaseCleanupInterceptor} to 
perform database
@@ -74,8 +70,8 @@ class DatabaseCleanupExtension implements IGlobalExtension {
 
     @Override
     void start() {
-        ServiceLoader<DatabaseCleaner> cleanerLoader = 
ServiceLoader.load(DatabaseCleaner)
-        List<DatabaseCleaner> cleaners = cleanerLoader.toList()
+        def cleanerLoader = ServiceLoader.load(DatabaseCleaner)
+        def cleaners = cleanerLoader.toList()
 
         if (cleaners.isEmpty()) {
             log.debug('No DatabaseCleaner implementations found on classpath')
@@ -92,21 +88,28 @@ class DatabaseCleanupExtension implements IGlobalExtension {
         }
 
         // Without an application context, there can be no database
-        boolean integrationEnvironment =  
spec.isAnnotationPresent(SpringBootTest) && 
spec.getAnnotation(SpringBootTest).webEnvironment() in 
[SpringBootTest.WebEnvironment.DEFINED_PORT, 
SpringBootTest.WebEnvironment.RANDOM_PORT]
+        boolean integrationEnvironment =
+                spec.isAnnotationPresent(SpringBootTest)
+                        && spec.getAnnotation(SpringBootTest).webEnvironment() 
in [
+                            SpringBootTest.WebEnvironment.DEFINED_PORT,
+                            SpringBootTest.WebEnvironment.RANDOM_PORT
+                        ]
         if (!integrationEnvironment) {
             if (hasDatabaseCleanupAnnotation(spec)) {
                 throw new IllegalStateException(
-                    "@DatabaseCleanup requires an environment with an 
ApplicationContext. Add @Integration or define the web environment on 
@SpringBootTest. Spec: ${spec.name}" as String)
+                    '@DatabaseCleanup requires an environment with an 
ApplicationContext. ' +
+                    'Add @Integration or define the web environment on 
@SpringBootTest. ' +
+                    "Spec: $spec.name")
             }
             return
         }
 
         boolean classAnnotated = spec.isAnnotationPresent(DatabaseCleanup)
         if (classAnnotated) {
-            DatabaseCleanup annotation = spec.getAnnotation(DatabaseCleanup)
-            DatasourceCleanupMapping mapping = 
DatasourceCleanupMapping.parse(annotation.value())
-            ApplicationContextResolver resolver = 
createResolver(annotation.resolver())
-            DatabaseCleanupInterceptor interceptor = new 
DatabaseCleanupInterceptor(context, true, mapping, resolver)
+            def annotation = spec.getAnnotation(DatabaseCleanup)
+            def mapping = DatasourceCleanupMapping.parse(annotation.value())
+            def resolver = createResolver(annotation.resolver())
+            def interceptor = new DatabaseCleanupInterceptor(context, true, 
mapping, resolver)
             spec.addSetupInterceptor(interceptor)
             spec.addCleanupInterceptor(interceptor)
             log.debug('Registered DatabaseCleanupInterceptor for spec: {} 
(class-level)', spec.name)
@@ -120,21 +123,17 @@ class DatabaseCleanupExtension implements 
IGlobalExtension {
         }
 
         // Check for method-level annotations on feature methods
-        boolean hasMethodAnnotation = false
-        for (FeatureInfo feature : spec.features) {
-            if (feature.featureMethod.isAnnotationPresent(DatabaseCleanup)) {
-                hasMethodAnnotation = true
-                break
-            }
+        boolean hasMethodAnnotation = spec.features.any {
+            it.featureMethod.isAnnotationPresent(DatabaseCleanup)
         }
 
         if (hasMethodAnnotation) {
             // For method-level, pass a clean-all mapping as default; the 
interceptor reads
             // each method's own annotation at runtime.
             // Use the default resolver; the interceptor will read the 
method-level resolver at runtime.
-            DatasourceCleanupMapping defaultMapping = 
DatasourceCleanupMapping.parse(new String[0])
-            ApplicationContextResolver defaultResolver = new 
DefaultApplicationContextResolver()
-            DatabaseCleanupInterceptor interceptor = new 
DatabaseCleanupInterceptor(context, false, defaultMapping, defaultResolver)
+            def defaultMapping = DatasourceCleanupMapping.parse(new String[0])
+            def defaultResolver = new DefaultApplicationContextResolver()
+            def interceptor = new DatabaseCleanupInterceptor(context, false, 
defaultMapping, defaultResolver)
             spec.addSetupInterceptor(interceptor)
             spec.addCleanupInterceptor(interceptor)
             log.debug('Registered DatabaseCleanupInterceptor for spec: {} 
(method-level)', spec.name)
@@ -153,7 +152,10 @@ class DatabaseCleanupExtension implements IGlobalExtension 
{
         }
         catch (Exception e) {
             throw new IllegalStateException(
-                "Failed to instantiate ApplicationContextResolver: 
${resolverClass.name}. Ensure it has a no-arg constructor." as String, e)
+                    'Failed to instantiate ApplicationContextResolver: ' +
+                    "$resolverClass.name. Ensure it has a no-arg constructor.",
+                    e
+            )
         }
     }
 
@@ -171,19 +173,22 @@ class DatabaseCleanupExtension implements 
IGlobalExtension {
      */
     private static void validateNoAnnotationOnNonFeatureMethods(SpecInfo spec) 
{
         // Collect all feature method names for comparison
-        Set<String> featureMethodNames = [] as Set
-        for (FeatureInfo feature : spec.features) {
-            featureMethodNames.add(feature.featureMethod.name)
-        }
+        def featureMethodNames = spec.features*.name as Set<String>
 
         // Check all declared methods in the spec class for misplaced 
annotations
         // This necessarily uses spec.reflection since we need to scan raw 
Java methods
         // that are not exposed as Spock features
-        for (Method method : spec.reflection.declaredMethods) {
-            if (method.isAnnotationPresent(DatabaseCleanup) && 
!featureMethodNames.contains(method.name)) {
-                throw new IllegalStateException(
-                    "@DatabaseCleanup annotation on method '${method.name}' in 
${spec.reflection.name} is not valid. @DatabaseCleanup can only be applied to 
Spock feature methods (test methods) or at the class level. It cannot be 
applied to setup(), cleanup(), setupSpec(), or cleanupSpec() methods." as 
String)
-            }
+        def invalid = spec.reflection.declaredMethods.find {
+            it.isAnnotationPresent(DatabaseCleanup) && 
!featureMethodNames.contains(it.name)
+        }
+        if (invalid) {
+            throw new IllegalStateException(
+                    "@DatabaseCleanup annotation on method '$invalid.name' " +
+                    "in $spec.reflection.name is not valid. @DatabaseCleanup " 
+
+                    'can only be applied to Spock feature methods (test 
methods) ' +
+                    'or at the class level. It cannot be applied to setup(), ' 
+
+                    'cleanup(), setupSpec(), or cleanupSpec() methods.'
+            )
         }
     }
 
@@ -192,14 +197,7 @@ class DatabaseCleanupExtension implements IGlobalExtension 
{
      * class level or on any feature method.
      */
     private static boolean hasDatabaseCleanupAnnotation(SpecInfo spec) {
-        if (spec.isAnnotationPresent(DatabaseCleanup)) {
-            return true
-        }
-        for (FeatureInfo feature : spec.features) {
-            if (feature.featureMethod.isAnnotationPresent(DatabaseCleanup)) {
-                return true
-            }
-        }
-        false
+        spec.isAnnotationPresent(DatabaseCleanup) ||
+                spec.features.any { 
it.featureMethod.isAnnotationPresent(DatabaseCleanup) }
     }
 }
diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptor.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptor.groovy
index e726b60fc7..d1e5921665 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptor.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupInterceptor.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
 import groovy.transform.CompileStatic
@@ -27,8 +26,6 @@ import org.spockframework.runtime.extension.IMethodInvocation
 
 import org.springframework.context.ApplicationContext
 
-import java.text.SimpleDateFormat
-
 /**
  * Spock method interceptor that performs database cleanup after tests 
annotated
  * with {@link DatabaseCleanup}. Supports both class-level and method-level 
annotations.
@@ -38,8 +35,8 @@ import java.text.SimpleDateFormat
  * {@link DatabaseCleanupContext}. After cleanup completes, the ThreadLocal is 
cleared.</p>
  *
  * <p>When datasource entries are specified in the annotation (with optional 
database type
- * mappings), only those datasources are cleaned using the specified or 
auto-discovered
- * cleaners. Otherwise, all datasources are cleaned.</p>
+ * mappings), only those data sources are cleaned using the specified or 
auto-discovered
+ * cleaners. Otherwise, all datas ources are cleaned.</p>
  */
 @Slf4j
 @CompileStatic
@@ -58,8 +55,12 @@ class DatabaseCleanupInterceptor extends 
AbstractMethodInterceptor {
      *        for method-level cleanup, the method's own annotation values are 
parsed at runtime
      * @param resolver the strategy for resolving the ApplicationContext from 
test instances
      */
-    DatabaseCleanupInterceptor(DatabaseCleanupContext context, boolean 
classLevelCleanup,
-                               DatasourceCleanupMapping mapping, 
ApplicationContextResolver resolver) {
+    DatabaseCleanupInterceptor(
+            DatabaseCleanupContext context,
+            boolean classLevelCleanup,
+            DatasourceCleanupMapping mapping,
+            ApplicationContextResolver resolver
+    ) {
         this.context = context
         this.classLevelCleanup = classLevelCleanup
         this.mapping = mapping
@@ -83,15 +84,22 @@ class DatabaseCleanupInterceptor extends 
AbstractMethodInterceptor {
         }
         finally {
             try {
-                def methodMapping = 
invocation.feature?.featureMethod?.isAnnotationPresent(DatabaseCleanup)
+                def methodMapping = invocation.
+                        feature?.
+                        featureMethod?.
+                        isAnnotationPresent(DatabaseCleanup)
                 if (!classLevelCleanup && !methodMapping) {
                     return
                 }
-
-                log.debug('Performing database cleanup after test method: {}', 
invocation.feature?.name ?: 'unknown')
-                DatasourceCleanupMapping selectedMapping = methodMapping ? 
getMethodMapping(invocation) : mapping
+                log.debug(
+                        'Performing database cleanup after test method: {}',
+                        invocation.feature?.name ?: 'unknown'
+                )
+                def selectedMapping = methodMapping ?
+                        getMethodMapping(invocation)
+                        : mapping
                 long startTime = System.currentTimeMillis()
-                List<DatabaseCleanupStats> stats = 
context.performCleanup(selectedMapping)
+                def stats = context.performCleanup(selectedMapping)
                 logStats(stats, startTime)
             }
             finally {
@@ -104,8 +112,13 @@ class DatabaseCleanupInterceptor extends 
AbstractMethodInterceptor {
      * Gets the parsed mapping from the method-level @DatabaseCleanup 
annotation.
      */
     private static DatasourceCleanupMapping getMethodMapping(IMethodInvocation 
invocation) {
-        DatabaseCleanup annotation = 
invocation.feature?.featureMethod?.getAnnotation(DatabaseCleanup)
-        annotation ? DatasourceCleanupMapping.parse(annotation.value()) : 
DatasourceCleanupMapping.parse(new String[0])
+        def annotation = invocation.
+                feature?.
+                featureMethod?.
+                getAnnotation(DatabaseCleanup)
+        annotation ?
+                DatasourceCleanupMapping.parse(annotation.value()) :
+                DatasourceCleanupMapping.parse(new String[0])
     }
 
     /**
@@ -119,20 +132,22 @@ class DatabaseCleanupInterceptor extends 
AbstractMethodInterceptor {
             return
         }
 
-        ApplicationContext appCtx = resolver.resolve(invocation)
+        def appCtx = resolver.resolve(invocation)
         if (appCtx) {
             context.applicationContext = appCtx
         }
         else {
             throw new IllegalStateException(
-                'Could not resolve ApplicationContext from test instance. 
Ensure the spec is annotated with @Integration.')
+                'Could not resolve ApplicationContext from test instance. ' +
+                'Ensure the spec is annotated with @Integration.'
+            )
         }
     }
 
     /**
      * Logs cleanup statistics and overall timing information for the cleanup 
operation.
      *
-     * @param statsList the list of cleanup statistics from individual 
datasources
+     * @param statsList the list of cleanup statistics from individual data 
sources
      * @param overallStartTime the overall start time of the cleanup operation
      */
     private static void logStats(List<DatabaseCleanupStats> statsList, long 
overallStartTime) {
@@ -140,20 +155,20 @@ class DatabaseCleanupInterceptor extends 
AbstractMethodInterceptor {
             long overallEndTime = System.currentTimeMillis()
             long overallDuration = overallEndTime - overallStartTime
 
-            String separator = 
'=========================================================='
-            String startTimeFormatted = 
DatabaseCleanupStats.formatTime(overallStartTime)
-            String endTimeFormatted = 
DatabaseCleanupStats.formatTime(overallEndTime)
+            def separator = 
'=========================================================='
+            def startTimeFormatted = 
DatabaseCleanupStats.formatTime(overallStartTime)
+            def endTimeFormatted = 
DatabaseCleanupStats.formatTime(overallEndTime)
 
-            System.out.println(separator)
-            System.out.println('Overall Cleanup Timing')
-            System.out.println("Start Time: ${startTimeFormatted}")
-            System.out.println("End Time:   ${endTimeFormatted}")
-            System.out.println("Duration:   ${overallDuration} ms")
-            System.out.println(separator)
+            println(separator)
+            println('Overall Cleanup Timing')
+            println("Start Time: $startTimeFormatted")
+            println("End Time:   $endTimeFormatted")
+            println("Duration:   $overallDuration ms")
+            println(separator)
 
-            for (DatabaseCleanupStats stats : statsList) {
-                if (stats.tableRowCounts) {
-                    System.out.println(stats.toFormattedReport())
+            statsList.each {
+                if (it.tableRowCounts) {
+                    println(it.toFormattedReport())
                 }
             }
         }
diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupStats.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupStats.groovy
index 68c2eed9d3..c7d3e2d0bc 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupStats.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatabaseCleanupStats.groovy
@@ -16,12 +16,12 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
-import groovy.transform.CompileStatic
 import java.text.SimpleDateFormat
 
+import groovy.transform.CompileStatic
+
 /**
  * Captures statistics from a single datasource cleanup operation, including 
which
  * tables were truncated and how many rows were in each table before 
truncation.
@@ -147,10 +147,10 @@ class DatabaseCleanupStats {
         String separator = 
'=========================================================='
         String divider = 
'----------------------------------------------------------'
 
-        StringBuilder sb = new StringBuilder()
+        def sb = new StringBuilder()
         sb.append(separator).append('\n')
         if (datasourceName) {
-            sb.append("Database Cleanup Stats (datasource: 
${datasourceName})").append('\n')
+            sb.append("Database Cleanup Stats (datasource: 
$datasourceName)").append('\n')
         }
         else {
             sb.append('Database Cleanup Stats').append('\n')
@@ -165,7 +165,7 @@ class DatabaseCleanupStats {
                 sb.append("End Time:   
${formatTime(endTimeMillis)}").append('\n')
             }
             if (startTimeMillis > 0L && endTimeMillis > 0L) {
-                sb.append("Duration:   ${durationMillis} ms").append('\n')
+                sb.append("Duration:   $durationMillis ms").append('\n')
             }
         }
 
@@ -193,8 +193,8 @@ class DatabaseCleanupStats {
      * @return the formatted time string
      */
     static String formatTime(long timeMillis) {
-        SimpleDateFormat sdf = new 
SimpleDateFormat('yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'')
-        sdf.setTimeZone(TimeZone.getTimeZone('UTC'))
-        sdf.format(new Date(timeMillis))
+        new SimpleDateFormat(/yyyy-MM-dd'T'HH:mm:ss.SSS'Z'/).tap {
+            timeZone = TimeZone.getTimeZone('UTC')
+        }.format(new Date(timeMillis))
     }
 }
diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatasourceCleanupMapping.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatasourceCleanupMapping.groovy
index b628bfb82a..e1f724edbb 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatasourceCleanupMapping.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DatasourceCleanupMapping.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
 import groovy.transform.CompileStatic
@@ -33,18 +32,18 @@ import groovy.transform.CompileStatic
  *       cleaner that declares the specified {@link 
DatabaseCleaner#databaseType()}</li>
  * </ul>
  *
- * <p>If the annotation value is empty (the default), all datasources in the 
application
+ * <p>If the annotation value is empty (the default), all data sources in the 
application
  * context are cleaned using auto-discovery.</p>
  *
  * <p>Examples:</p>
  * <pre>
- * // Clean all datasources (auto-discover cleaners)
+ * // Clean all data sources (auto-discover cleaners)
  * &#64;DatabaseCleanup
  *
- * // Clean specific datasources (auto-discover cleaners)
+ * // Clean specific data sources (auto-discover cleaners)
  * &#64;DatabaseCleanup(['dataSource', 'dataSource_secondary'])
  *
- * // Clean specific datasources with explicit cleaner types
+ * // Clean specific data sources with explicit cleaner types
  * &#64;DatabaseCleanup(['dataSource:h2', 'dataSource_pg:postgresql'])
  *
  * // Mixed: some explicit, some auto-discovered
@@ -71,12 +70,14 @@ class DatasourceCleanupMapping {
          * @return {@code true} if this entry has an explicit database type 
mapping
          */
         boolean hasExplicitType() {
-            databaseType as boolean
+            databaseType
         }
 
         @Override
         String toString() {
-            hasExplicitType() ? "${datasourceName}:${databaseType}" : 
datasourceName
+            hasExplicitType() ?
+                    "$datasourceName:$databaseType" :
+                    datasourceName
         }
     }
 
@@ -84,7 +85,7 @@ class DatasourceCleanupMapping {
     private final boolean cleanAll
 
     private DatasourceCleanupMapping(List<Entry> entries, boolean cleanAll) {
-        this.entries = Collections.unmodifiableList(new ArrayList<>(entries))
+        this.entries = entries.asImmutable()
         this.cleanAll = cleanAll
     }
 
@@ -96,8 +97,8 @@ class DatasourceCleanupMapping {
     }
 
     /**
-     * @return {@code true} if no specific datasources were specified, meaning 
all
-     *         datasources in the application context should be cleaned
+     * @return {@code true} if no specific data sources were specified, 
meaning all
+     *         datas ources in the application context should be cleaned
      */
     boolean isCleanAll() {
         cleanAll
@@ -114,37 +115,40 @@ class DatasourceCleanupMapping {
      * @throws IllegalArgumentException if an entry has an empty datasource 
name or database type
      */
     static DatasourceCleanupMapping parse(String[] annotationValues) {
-        if (!annotationValues || annotationValues.length == 0) {
+        if (!annotationValues) {
             return new DatasourceCleanupMapping([], true)
         }
 
-        List<Entry> entries = []
-        for (String value : annotationValues) {
-            if (!value || value.trim().isEmpty()) {
+        def entries = annotationValues.collect {
+            def v = it?.trim()
+            if (!v) {
                 throw new IllegalArgumentException(
-                    '@DatabaseCleanup contains a null or empty entry')
+                        '@DatabaseCleanup contains a null or empty entry'
+                )
             }
 
-            int colonIdx = value.indexOf(':')
-            if (colonIdx < 0) {
+            def parts = v.split(':', 2)*.trim()
+            if (parts.size() == 1) {
                 // No colon — datasource name only, auto-discover cleaner
-                entries.add(new Entry(value.trim(), null))
+                return new Entry(parts[0], null)
+            }
+
+            def name = parts[0]
+            def type = parts[1]
+            if (!name) {
+                throw new IllegalArgumentException(
+                        "Invalid @DatabaseCleanup entry '$it': " +
+                        'datasource name cannot be empty'
+                )
             }
-            else {
-                String name = value.substring(0, colonIdx).trim()
-                String type = value.substring(colonIdx + 1).trim()
-
-                if (name.isEmpty()) {
-                    throw new IllegalArgumentException(
-                        "Invalid @DatabaseCleanup entry '${value}': datasource 
name cannot be empty")
-                }
-                if (type.isEmpty()) {
-                    throw new IllegalArgumentException(
-                        "Invalid @DatabaseCleanup entry '${value}': database 
type cannot be empty after ':'")
-                }
-
-                entries.add(new Entry(name, type))
+            if (!type) {
+                throw new IllegalArgumentException(
+                        "Invalid @DatabaseCleanup entry '$it': " +
+                        "database type cannot be empty after ':'"
+                )
             }
+
+            new Entry(name, type)
         }
 
         new DatasourceCleanupMapping(entries, false)
diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DefaultApplicationContextResolver.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DefaultApplicationContextResolver.groovy
index 617b38afda..ba0368f132 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DefaultApplicationContextResolver.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/DefaultApplicationContextResolver.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
 import groovy.transform.CompileStatic
@@ -49,9 +48,9 @@ class DefaultApplicationContextResolver implements 
ApplicationContextResolver {
 
     @Override
     ApplicationContext resolve(IMethodInvocation invocation) {
-        TestContext testContext = TestContextHolderListener.CURRENT.get()
+        def testContext = TestContextHolderListener.CURRENT.get()
         if (testContext) {
-            ApplicationContext ctx = testContext.applicationContext
+            def ctx = testContext.applicationContext
             if (ctx) {
                 log.debug('Resolved ApplicationContext via 
TestContextHolderListener')
                 return ctx
@@ -59,6 +58,10 @@ class DefaultApplicationContextResolver implements 
ApplicationContextResolver {
         }
 
         throw new IllegalStateException(
-            'Could not resolve ApplicationContext. Ensure the spec is 
annotated with @Integration and that TestContextHolderListener is registered as 
a TestExecutionListener.')
+                'Could not resolve ApplicationContext. ' +
+                'Ensure the spec is annotated with @Integration ' +
+                'and that TestContextHolderListener is registered ' +
+                'as a TestExecutionListener.'
+        )
     }
 }
diff --git 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/TestContextHolderListener.groovy
 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/TestContextHolderListener.groovy
index 73304209ca..5241dec615 100644
--- 
a/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/TestContextHolderListener.groovy
+++ 
b/grails-testing-support-cleanup-core/src/main/groovy/org/apache/grails/testing/cleanup/core/TestContextHolderListener.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.core
 
 import groovy.transform.CompileStatic
diff --git 
a/grails-testing-support-cleanup-h2/src/main/groovy/org/apache/grails/testing/cleanup/h2/H2DatabaseCleaner.groovy
 
b/grails-testing-support-cleanup-h2/src/main/groovy/org/apache/grails/testing/cleanup/h2/H2DatabaseCleaner.groovy
index b1a71a0f1a..c3cce96176 100644
--- 
a/grails-testing-support-cleanup-h2/src/main/groovy/org/apache/grails/testing/cleanup/h2/H2DatabaseCleaner.groovy
+++ 
b/grails-testing-support-cleanup-h2/src/main/groovy/org/apache/grails/testing/cleanup/h2/H2DatabaseCleaner.groovy
@@ -16,22 +16,15 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.h2
 
-import java.sql.Connection
-import java.sql.DatabaseMetaData
-
 import javax.sql.DataSource
 
-import groovy.sql.GroovyResultSet
 import groovy.sql.Sql
-import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
 import groovy.util.logging.Slf4j
 
 import org.springframework.context.ApplicationContext
-import org.springframework.util.ClassUtils
 
 import org.apache.grails.testing.cleanup.core.DatabaseCleaner
 import org.apache.grails.testing.cleanup.core.DatabaseCleanupStats
@@ -49,6 +42,11 @@ import 
org.apache.grails.testing.cleanup.core.DatabaseCleanupStats
 class H2DatabaseCleaner implements DatabaseCleaner {
 
     private static final String DATABASE_TYPE = 'h2'
+    private static final String QUERY = '''
+            SELECT table_name, row_count_estimate
+            FROM information_schema.tables
+            WHERE table_schema = ? AND row_count_estimate > 0
+    '''
 
     @Override
     String databaseType() {
@@ -57,53 +55,36 @@ class H2DatabaseCleaner implements DatabaseCleaner {
 
     @Override
     boolean supports(DataSource dataSource) {
-        Connection connection = null
-        try {
-            connection = dataSource.getConnection()
-            DatabaseMetaData metaData = connection.getMetaData()
-            String url = metaData.getURL()
-            return url && url.startsWith('jdbc:h2:')
-        }
-        catch (Exception e) {
+        try (def con = dataSource.connection) {
+            return con.metaData?.URL?.startsWith('jdbc:h2:')
+        } catch (Exception e) {
             log.debug('Could not determine if datasource is H2', e)
             return false
         }
-        finally {
-            if (connection) {
-                try {
-                    connection.close()
-                }
-                catch (Exception ignored) {
-                    // ignore
-                }
-            }
-        }
     }
 
-    @SuppressWarnings('SqlNoDataSourceInspection')
     @Override
+    @SuppressWarnings('SqlNoDataSourceInspection')
     DatabaseCleanupStats cleanup(ApplicationContext applicationContext, 
DataSource dataSource) {
-        DatabaseCleanupStats stats = new DatabaseCleanupStats()
+        def stats = new DatabaseCleanupStats()
         stats.start()
 
-        String schemaName = 
H2DatabaseCleanupHelper.resolveSchemaName(dataSource)
+        def schemaName = H2DatabaseCleanupHelper.resolveSchemaName(dataSource)
         if (!schemaName) {
             log.warn('Could not resolve schema name for datasource, skipping 
cleanup')
             stats.stop()
             return stats
         }
 
-        Sql sql = new Sql(dataSource)
+        def sql = new Sql(dataSource)
         try {
             sql.execute('SET REFERENTIAL_INTEGRITY FALSE')
-            String query = "SELECT table_name, row_count_estimate FROM 
information_schema.tables WHERE table_schema = '${schemaName}' AND 
row_count_estimate > 0" as String
-            sql.eachRow(query) { GroovyResultSet row ->
-                String tableName = row['table_name'] as String
-                stats.addTableRowCount(tableName, row['row_count_estimate'] as 
Long)
+            sql.eachRow(QUERY, [schemaName] as List<Object>) { row ->
+                def tableName = row.getString('table_name')
+                stats.addTableRowCount(tableName, 
row.getLong('row_count_estimate'))
                 log.debug('Truncating table: {}', tableName)
-                sql.executeUpdate("TRUNCATE TABLE \"${tableName}\"" as String)
+                sql.executeUpdate(/TRUNCATE TABLE "$tableName"/)
             }
-
             cleanupCacheLayer(applicationContext)
         }
         finally {
@@ -119,13 +100,4 @@ class H2DatabaseCleaner implements DatabaseCleaner {
 
         stats
     }
-
-    @CompileDynamic
-    private void cleanupCacheLayer(ApplicationContext applicationContext) {
-        // Clear the 2nd layer cache if it exists
-        if (ClassUtils.isPresent('org.hibernate.SessionFactory', 
this.class.classLoader)) {
-            def sessionFactory = applicationContext.getBean('sessionFactory', 
Class.forName('org.hibernate.SessionFactory'))
-            sessionFactory?.cache?.evictAllRegions()
-        }
-    }
 }
diff --git 
a/grails-testing-support-cleanup-h2/src/main/groovy/org/apache/grails/testing/cleanup/h2/H2DatabaseCleanupHelper.groovy
 
b/grails-testing-support-cleanup-h2/src/main/groovy/org/apache/grails/testing/cleanup/h2/H2DatabaseCleanupHelper.groovy
index 6811b0df06..8b8480c19f 100644
--- 
a/grails-testing-support-cleanup-h2/src/main/groovy/org/apache/grails/testing/cleanup/h2/H2DatabaseCleanupHelper.groovy
+++ 
b/grails-testing-support-cleanup-h2/src/main/groovy/org/apache/grails/testing/cleanup/h2/H2DatabaseCleanupHelper.groovy
@@ -16,12 +16,8 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.h2
 
-import java.sql.Connection
-import java.sql.DatabaseMetaData
-
 import javax.sql.DataSource
 
 import groovy.transform.CompileStatic
@@ -45,37 +41,21 @@ class H2DatabaseCleanupHelper {
      * @return the schema name, or {@code null} if it cannot be determined
      */
     static String resolveSchemaName(DataSource dataSource) {
-        Connection connection = null
-        try {
-            connection = dataSource.getConnection()
-            String schema = connection.getSchema()
+        try (def con = dataSource.getConnection()) {
+            def schema = con.getSchema()
             if (schema) {
                 log.debug('Resolved schema name from connection: {}', schema)
                 return schema
             }
-
-            // Fallback: try to get the schema from the database metadata URL
-            DatabaseMetaData metaData = connection.getMetaData()
-            String url = metaData.getURL()
+            def url = con.metaData.URL
             if (url) {
-                schema = extractSchemaFromUrl(url)
-                log.debug('Resolved schema name from URL {}: {}', url, schema)
-                return schema
+                def extracted = extractSchemaFromUrl(url)
+                log.debug('Resolved schema name from URL {}: {}', url, 
extracted)
+                return extracted
             }
-        }
-        catch (Exception e) {
+        } catch (Exception e) {
             log.warn('Failed to resolve schema name from datasource', e)
         }
-        finally {
-            if (connection) {
-                try {
-                    connection.close()
-                }
-                catch (Exception ignored) {
-                    // ignore
-                }
-            }
-        }
         null
     }
 
@@ -94,53 +74,48 @@ class H2DatabaseCleanupHelper {
      * @return the uppercase schema name, or {@code null} if the URL format is 
not recognized
      */
     static String extractSchemaFromUrl(String url) {
-        if (!url) {
+        if (!url?.startsWith('jdbc:h2:')) {
             return null
         }
 
         // Handle H2 in-memory URLs: jdbc:h2:mem:dbName or 
jdbc:h2:mem:dbName;params
         if (url.startsWith('jdbc:h2:mem:')) {
-            String remainder = url.substring('jdbc:h2:mem:'.length())
-            // Remove any trailing parameters after ';'
-            int semicolonIdx = remainder.indexOf(';')
-            if (semicolonIdx >= 0) {
-                remainder = remainder.substring(0, semicolonIdx)
-            }
-            return remainder ? remainder.toUpperCase() : null
+            def name = stripParams(url.substring('jdbc:h2:mem:'.length()))
+            return toSchema(name)
         }
 
-        // Handle H2 file-based URLs: jdbc:h2:./dbName, 
jdbc:h2:file:./path/dbName, jdbc:h2:tcp://host/dbName
-        if (url.startsWith('jdbc:h2:')) {
-            String remainder = url.substring('jdbc:h2:'.length())
-
-            // Remove protocol prefixes (tcp://, ssl://)
-            if (remainder.startsWith('tcp://') || 
remainder.startsWith('ssl://')) {
-                int slashIdx = remainder.indexOf('/', remainder.indexOf('//') 
+ 2)
-                if (slashIdx >= 0) {
-                    remainder = remainder.substring(slashIdx + 1)
-                }
-            }
+        def remainder = url.substring('jdbc:h2:'.length())
 
-            // Remove 'file:' prefix
-            if (remainder.startsWith('file:')) {
-                remainder = remainder.substring('file:'.length())
+        // Remove protocol prefixes (tcp://, ssl://)
+        if (remainder.startsWith('tcp://') || remainder.startsWith('ssl://')) {
+            int slashIdx = remainder.indexOf('/', remainder.indexOf('//') + 2)
+            if (slashIdx >= 0) {
+                remainder = remainder.substring(slashIdx + 1)
             }
+        }
 
-            // Remove any trailing parameters after ';'
-            int semicolonIdx = remainder.indexOf(';')
-            if (semicolonIdx >= 0) {
-                remainder = remainder.substring(0, semicolonIdx)
-            }
+        // Remove 'file:' prefix
+        if (remainder.startsWith('file:')) {
+            remainder = remainder.substring('file:'.length())
+        }
 
-            // Get the last path segment as the database name
-            int lastSlash = remainder.lastIndexOf('/')
-            if (lastSlash >= 0) {
-                remainder = remainder.substring(lastSlash + 1)
-            }
+        remainder = stripParams(remainder)
 
-            return remainder ? remainder.toUpperCase() : null
+        // last path segment
+        int lastSlash = remainder.lastIndexOf('/')
+        if (lastSlash >= 0) {
+            remainder = remainder.substring(lastSlash + 1)
         }
 
-        null
+        return toSchema(remainder)
+    }
+
+    private static String stripParams(String s) {
+        int idx = s.indexOf(';')
+        idx >= 0 ? s.substring(0, idx) : s
+    }
+
+    private static String toSchema(String s) {
+        s ? s.toUpperCase() : null
     }
 }
diff --git 
a/grails-testing-support-cleanup-postgresql/src/main/groovy/org/apache/grails/testing/cleanup/postgresql/PostgresDatabaseCleaner.groovy
 
b/grails-testing-support-cleanup-postgresql/src/main/groovy/org/apache/grails/testing/cleanup/postgresql/PostgresDatabaseCleaner.groovy
index 5ed1756602..089bdafbaa 100644
--- 
a/grails-testing-support-cleanup-postgresql/src/main/groovy/org/apache/grails/testing/cleanup/postgresql/PostgresDatabaseCleaner.groovy
+++ 
b/grails-testing-support-cleanup-postgresql/src/main/groovy/org/apache/grails/testing/cleanup/postgresql/PostgresDatabaseCleaner.groovy
@@ -16,22 +16,15 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.postgresql
 
-import java.sql.Connection
-import java.sql.DatabaseMetaData
-
 import javax.sql.DataSource
 
-import groovy.sql.GroovyResultSet
 import groovy.sql.Sql
-import groovy.transform.CompileDynamic
 import groovy.transform.CompileStatic
 import groovy.util.logging.Slf4j
 
 import org.springframework.context.ApplicationContext
-import org.springframework.util.ClassUtils
 
 import org.apache.grails.testing.cleanup.core.DatabaseCleaner
 import org.apache.grails.testing.cleanup.core.DatabaseCleanupStats
@@ -57,6 +50,16 @@ import 
org.apache.grails.testing.cleanup.core.DatabaseCleanupStats
 class PostgresDatabaseCleaner implements DatabaseCleaner {
 
     private static final String DATABASE_TYPE = 'postgresql'
+    private static final String QUERY = '''
+            SELECT n.nspname AS schemaname, 
+                c.relname AS table_name,
+                c.reltuples::bigint AS row_count_estimate
+            FROM pg_class c
+            JOIN pg_namespace n ON n.oid = c.relnamespace
+            WHERE c.relkind = 'r'
+            AND n.nspname = ?
+            ORDER BY row_count_estimate DESC;
+        '''
 
     @Override
     String databaseType() {
@@ -65,45 +68,27 @@ class PostgresDatabaseCleaner implements DatabaseCleaner {
 
     @Override
     boolean supports(DataSource dataSource) {
-        Connection connection = null
-        try {
-            connection = dataSource.getConnection()
-            DatabaseMetaData metaData = connection.getMetaData()
-            String url = metaData.getURL()
-            return url && url.startsWith('jdbc:postgresql:')
-        }
-        catch (Exception e) {
+        try (def con = dataSource.connection) {
+            return con.metaData?.URL?.startsWith('jdbc:postgresql:')
+        } catch (Exception e) {
             log.debug('Could not determine if datasource is PostgreSQL', e)
             return false
         }
-        finally {
-            if (connection) {
-                try {
-                    connection.close()
-                }
-                catch (Exception ignored) {
-                    // ignore
-                }
-            }
-        }
     }
 
-    @SuppressWarnings('SqlNoDataSourceInspection')
     @Override
+    @SuppressWarnings('SqlNoDataSourceInspection')
     DatabaseCleanupStats cleanup(ApplicationContext applicationContext, 
DataSource dataSource) {
-        DatabaseCleanupStats stats = new DatabaseCleanupStats()
-        stats.start()
 
-        Sql sql = new Sql(dataSource)
+        def stats = new DatabaseCleanupStats().tap { start() }
+        def sql = new Sql(dataSource)
         try {
             // Disable all triggers and referential integrity checks for this 
session
             // This is more efficient than using CASCADE on each truncate
             sql.execute('SET session_replication_role = replica')
-
-            String currentSchema = 
PostgresDatabaseCleanupHelper.resolveCurrentSchema(dataSource)
+            def currentSchema = 
PostgresDatabaseCleanupHelper.resolveCurrentSchema(dataSource)
             log.debug('Cleaning schema: {}', currentSchema)
             cleanupSchema(sql, currentSchema, stats)
-
             cleanupCacheLayer(applicationContext)
         }
         finally {
@@ -117,32 +102,19 @@ class PostgresDatabaseCleaner implements DatabaseCleaner {
             }
             stats.stop()
         }
-
         stats
     }
 
     @SuppressWarnings('SqlNoDataSourceInspection')
-    private void cleanupSchema(Sql sql, String schemaName, 
DatabaseCleanupStats stats) {
-        String query = """
-            SELECT n.nspname AS schemaname, 
-                c.relname AS table_name,
-                c.reltuples::bigint AS row_count_estimate
-            FROM pg_class c
-            JOIN pg_namespace n ON n.oid = c.relnamespace
-            WHERE c.relkind = 'r'
-            AND n.nspname = '${schemaName}'
-            ORDER BY row_count_estimate DESC;
-        """ as String
-
-        List<String> tablesInSchema = []
-        sql.eachRow(query) { GroovyResultSet row ->
-            String tableName = row['table_name'] as String
+    private static void cleanupSchema(Sql sql, String schemaName, 
DatabaseCleanupStats stats) {
+        def tablesInSchema = [] as List<String>
+        sql.eachRow(QUERY, [schemaName] as List<Object>) { row ->
+            def tableName = row.getString('table_name')
             tablesInSchema << tableName
-
-            def rowCount = row['row_count_estimate'] as Long
+            def rowCount = row.getLong('row_count_estimate')
             if (stats.detailedStatCollection) {
                 if (rowCount == -1) {
-                    String countQuery = "SELECT COUNT(*) AS cnt FROM 
${tableName}" as String
+                    def countQuery = "SELECT COUNT(*) AS cnt FROM $tableName" 
as String
                     rowCount = sql.firstRow(countQuery)?.cnt as Long ?: 0L
                 }
             }
@@ -150,17 +122,11 @@ class PostgresDatabaseCleaner implements DatabaseCleaner {
         }
 
         if (tablesInSchema) {
-            log.debug('Truncating tables: {}', tablesInSchema.join(','))
-            sql.executeUpdate("TRUNCATE TABLE ${tablesInSchema.collect { 
"\"$it\"" }.join(',')} RESTART IDENTITY CASCADE" as String)
-        }
-    }
-
-    @CompileDynamic
-    private void cleanupCacheLayer(ApplicationContext applicationContext) {
-        // Clear the 2nd layer cache if it exists
-        if (ClassUtils.isPresent('org.hibernate.SessionFactory', 
this.class.classLoader)) {
-            def sessionFactory = applicationContext.getBean('sessionFactory', 
Class.forName('org.hibernate.SessionFactory'))
-            sessionFactory?.cache?.evictAllRegions()
+            log.debug('Truncating tables: {}', tablesInSchema.join(', '))
+            def tables = tablesInSchema.collect { "\"$it\"" }.join(',')
+            sql.executeUpdate(
+                    "TRUNCATE TABLE $tables RESTART IDENTITY CASCADE" as String
+            )
         }
     }
 }
diff --git 
a/grails-testing-support-cleanup-postgresql/src/main/groovy/org/apache/grails/testing/cleanup/postgresql/PostgresDatabaseCleanupHelper.groovy
 
b/grails-testing-support-cleanup-postgresql/src/main/groovy/org/apache/grails/testing/cleanup/postgresql/PostgresDatabaseCleanupHelper.groovy
index a79e65d60a..a6816b13db 100644
--- 
a/grails-testing-support-cleanup-postgresql/src/main/groovy/org/apache/grails/testing/cleanup/postgresql/PostgresDatabaseCleanupHelper.groovy
+++ 
b/grails-testing-support-cleanup-postgresql/src/main/groovy/org/apache/grails/testing/cleanup/postgresql/PostgresDatabaseCleanupHelper.groovy
@@ -16,67 +16,52 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package org.apache.grails.testing.cleanup.postgresql
 
-import java.sql.Connection
-import java.sql.DatabaseMetaData
-
 import javax.sql.DataSource
 
 import groovy.transform.CompileStatic
 import groovy.util.logging.Slf4j
 
 /**
- * Helper utility for PostgreSQL database cleanup operations. Provides 
PostgreSQL-specific logic such as
- * resolving the current schema from a {@link DataSource} by inspecting JDBC 
connection metadata
- * and parsing PostgreSQL JDBC URLs.
+ * Helper utility for PostgreSQL database cleanup operations.
+ * Provides PostgreSQL-specific logic such as resolving the
+ * current schema from a {@link DataSource} by inspecting
+ * JDBC connection metadata and parsing PostgreSQL JDBC URLs.
  */
 @Slf4j
 @CompileStatic
 class PostgresDatabaseCleanupHelper {
 
     /**
-     * Resolves the current schema for the given PostgreSQL datasource by 
inspecting the JDBC connection metadata.
-     * If the JDBC URL contains a `currentSchema` parameter, returns that 
schema.
+     * Resolves the current schema for the given PostgreSQL datasource
+     * by inspecting the JDBC connection metadata. If the JDBC URL
+     * contains a `currentSchema` parameter, returns that schema.
      * Otherwise, returns the connection's current schema.
      *
      * @param dataSource the datasource to resolve the schema for
      * @return the schema name, or {@code null} if it cannot be determined
      */
     static String resolveCurrentSchema(DataSource dataSource) {
-        Connection connection = null
-        try {
-            connection = dataSource.getConnection()
-            String schema = connection.getSchema()
+        try (def con = dataSource.connection) {
+            def schema = con.getSchema()
             if (schema) {
                 log.debug('Resolved current schema from connection: {}', 
schema)
                 return schema
             }
 
-            // Fallback: try to get the schema from the database metadata URL
-            DatabaseMetaData metaData = connection.getMetaData()
-            String url = metaData.getURL()
+            def url = con.metaData.URL
             if (url) {
                 schema = extractCurrentSchemaFromUrl(url)
-                if (schema) {
-                    log.debug('Resolved current schema from URL {}: {}', url, 
schema)
-                    return schema
-                }
-            }
-        }
-        finally {
-            if (connection) {
-                try {
-                    connection.close()
-                }
-                catch (Exception ignored) {
-                    // ignore
-                }
+                log.debug('Resolved current schema from URL {}: {}', url, 
schema)
+                return schema
             }
         }
-
-        throw new IllegalStateException("Because postgres defaults to the 
search_path when currentSchema isn't defined, a schema should always be found")
+        throw new IllegalStateException(
+                'Because postgres defaults to the search_path ' +
+                'when currentSchema isn\'t defined, a schema ' +
+                'should always be found'
+        )
     }
 
     /**
@@ -98,29 +83,18 @@ class PostgresDatabaseCleanupHelper {
             return null
         }
 
-        // Look for currentSchema parameter in query string
-        int questionIdx = url.indexOf('?')
-        if (questionIdx < 0) {
+        int q = url.indexOf('?')
+        if (q < 0) {
             return null
         }
 
-        String queryString = url.substring(questionIdx + 1)
-        String[] params = queryString.split('&')
-        for (String param : params) {
+        def query = url.substring(q + 1)
+        for (def param : query.split('&')) {
             if (param.startsWith('currentSchema=')) {
-                String schema = param.substring('currentSchema='.length())
-                // URL decode if needed (handle common cases)
-                if (schema) {
-                    // Remove any trailing parameters if present
-                    int ampIdx = schema.indexOf('&')
-                    if (ampIdx >= 0) {
-                        schema = schema.substring(0, ampIdx)
-                    }
-                    return schema ?: null
-                }
+                def schema = param.substring('currentSchema='.length())
+                return schema ?: null
             }
         }
-
-        null
+        return null
     }
 }

Reply via email to