Repository: jdbi/jdbi
Pull request #2791: Test for Issue #2790
Author: login: hgschmie
Created: 2025-05-02T22:17:08Z | Merged: 2025-05-03T16:41:55Z

Tests that string templating works in Kotlin.

Adds a test for Kotlin value classes.



Diff:
diff --git a/kotlin-sqlobject/pom.xml b/kotlin-sqlobject/pom.xml
index 0f08f21c5d..c2a7f83979 100644
--- a/kotlin-sqlobject/pom.xml
+++ b/kotlin-sqlobject/pom.xml
@@ -84,6 +84,12 @@
             <scope>test</scope>
         </dependency>
 
+        <dependency>
+            <groupId>org.jdbi</groupId>
+            <artifactId>jdbi3-stringtemplate4</artifactId>
+            <scope>test</scope>
+        </dependency>
+
         <dependency>
             <groupId>org.jetbrains.kotlinx</groupId>
             <artifactId>kotlinx-coroutines-core-jvm</artifactId>
diff --git a/kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator.kt b/kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator.kt
new file mode 100644
index 0000000000..77943397e2
--- /dev/null
+++ b/kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator.kt
@@ -0,0 +1,147 @@
+/*
+ * 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
+ *
+ * http://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.jdbi.v3.sqlobject.kotlin
+
+import org.assertj.core.api.Assertions.assertThat
+import org.jdbi.v3.core.Handle
+import org.jdbi.v3.core.Something
+import org.jdbi.v3.core.kotlin.mapTo
+import org.jdbi.v3.core.mapper.RowMapper
+import org.jdbi.v3.core.statement.StatementContext
+import org.jdbi.v3.sqlobject.SqlObjectPlugin
+import org.jdbi.v3.sqlobject.config.RegisterRowMapper
+import org.jdbi.v3.sqlobject.customizer.Bind
+import org.jdbi.v3.sqlobject.customizer.BindBean
+import org.jdbi.v3.sqlobject.customizer.Define
+import org.jdbi.v3.sqlobject.statement.SqlBatch
+import org.jdbi.v3.sqlobject.statement.SqlQuery
+import org.jdbi.v3.sqlobject.statement.SqlUpdate
+import org.jdbi.v3.stringtemplate4.UseStringTemplateSqlLocator
+import org.jdbi.v3.testing.junit5.JdbiExtension
+import org.jdbi.v3.testing.junit5.internal.TestingInitializers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import java.sql.ResultSet
+import java.sql.SQLException
+
+// port from the stringtemplate4 class to kotlin
+class TestStringTemplateSqlLocator {
+    @RegisterExtension
+    var h2Extension: JdbiExtension = JdbiExtension.h2().withInitializer(TestingInitializers.something()).withPlugin(SqlObjectPlugin())
+
+    private lateinit var handle: Handle
+    private lateinit var wombat: Wombat
+
+    @BeforeEach
+    fun setUp() {
+        handle = h2Extension.getSharedHandle()
+        wombat = handle.attach(Wombat::class)
+    }
+
+    @Test
+    fun testBaz() {
+        wombat.insert(Something(7, "Henning"))
+
+        val name = handle.createQuery("select name from something where id = 7")
+            .mapTo(String::class)
+            .one()
+
+        assertThat(name).isEqualTo("Henning")
+    }
+
+    @Test
+    fun testBam() {
+        handle.execute("insert into something (id, name) values (6, 'Martin')")
+
+        val s = wombat.findById(6L)
+        assertThat(s.name).isEqualTo("Martin")
+    }
+
+    @Test
+    fun testBap() {
+        handle.execute("insert into something (id, name) values (2, 'Bean')")
+        assertThat(wombat.findNameFor(2)).isEqualTo("Bean")
+    }
+
+    @Test
+    fun testDefines() {
+        wombat.weirdInsert("something", "id", "name", 5, "Bouncer")
+        wombat.weirdInsert("something", "id", "name", 6, "Bean")
+        val name = handle.createQuery("select name from something where id = 5")
+            .mapTo(String::class.java)
+            .one()
+
+        assertThat(name).isEqualTo("Bouncer")
+    }
+
+    @Test
+    fun testConditionalExecutionWithNullValue() {
+        wombat.insert(Something(6, "Jack"))
+        wombat.insert(Something(7, "Wolf"))
+
+        val somethings = wombat.findByIdOrUptoLimit(6, null)
+        assertThat(somethings).hasSize(1)
+    }
+
+    @Test
+    fun testConditionalExecutionWithNonNullValue() {
+        wombat.insert(Something(6, "Jack"))
+        wombat.insert(Something(7, "Wolf"))
+
+        val somethings = wombat.findByIdOrUptoLimit(null, 8)
+        assertThat(somethings).hasSize(2)
+    }
+
+    @Test
+    fun testBatching() {
+        wombat.insertBunches(Something(1, "Jeff"), Something(2, "Brian"))
+
+        assertThat(wombat.findById(1L)).isEqualTo(Something(1, "Jeff"))
+        assertThat(wombat.findById(2L)).isEqualTo(Something(2, "Brian"))
+    }
+
+    @UseStringTemplateSqlLocator
+    @RegisterRowMapper(SomethingMapper::class)
+    interface Wombat {
+        @SqlUpdate
+        fun insert(@BindBean s: Something?)
+
+        @SqlQuery
+        fun findById(@Bind("id") id: Long?): Something
+
+        @SqlQuery
+        fun findByIdOrUptoLimit(@Bind("id") id: Int?, @Define("idLimit") @Bind("idLimit") idLimit: Int?): MutableList<Something?>?
+
+        @SqlQuery
+        fun findNameFor(@Bind("id") id: Int): String?
+
+        @SqlUpdate
+        fun weirdInsert(
+            @Define("table") table: String?,
+            @Define("id_column") idColumn: String?,
+            @Define("value_column") valueColumn: String?,
+            @Bind("id") id: Int,
+            @Bind("value") name: String?
+        )
+
+        @SqlBatch
+        fun insertBunches(@BindBean vararg somethings: Something?)
+    }
+
+    class SomethingMapper : RowMapper<Something?> {
+        @Throws(SQLException::class)
+        override fun map(r: ResultSet, ctx: StatementContext?): Something = Something(r.getInt("id"), r.getString("name"))
+    }
+}
diff --git a/kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestValueClasses.kt b/kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestValueClasses.kt
new file mode 100644
index 0000000000..5ad3abd981
--- /dev/null
+++ b/kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestValueClasses.kt
@@ -0,0 +1,148 @@
+/*
+ * 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
+ *
+ * http://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.jdbi.v3.sqlobject.kotlin
+
+import org.assertj.core.api.Assertions.assertThat
+import org.jdbi.v3.core.Handle
+import org.jdbi.v3.core.argument.AbstractArgumentFactory
+import org.jdbi.v3.core.argument.Argument
+import org.jdbi.v3.core.config.ConfigRegistry
+import org.jdbi.v3.core.kotlin.KotlinMapper
+import org.jdbi.v3.core.mapper.ColumnMapper
+import org.jdbi.v3.core.mapper.RowMapper
+import org.jdbi.v3.core.statement.StatementContext
+import org.jdbi.v3.sqlobject.SqlObjectPlugin
+import org.jdbi.v3.sqlobject.customizer.Bind
+import org.jdbi.v3.sqlobject.statement.SqlQuery
+import org.jdbi.v3.stringtemplate4.UseStringTemplateSqlLocator
+import org.jdbi.v3.testing.junit5.JdbiExtension
+import org.jdbi.v3.testing.junit5.internal.TestingInitializers
+import org.junit.jupiter.api.AfterEach
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import java.sql.PreparedStatement
+import java.sql.ResultSet
+import java.sql.SQLException
+import java.sql.Types
+
+/**
+ * Test a value class as a parameter for a sqlobject method - see https://github.com/jdbi/jdbi/issues/2790
+ */
+class TestValueClasses {
+    @RegisterExtension
+    var h2Extension: JdbiExtension = JdbiExtension.h2().withInitializer(TestingInitializers.something()).withPlugin(SqlObjectPlugin())
+
+    private lateinit var handle: Handle
+    private lateinit var dao: Dao
+
+    @BeforeEach
+    fun setUp() {
+        handle = h2Extension.jdbi.open()
+        handle.registerArgument(ValueIdArgumentFactory())
+        handle.registerColumnMapper(ValueId::class.java, ValueIdColumnMapper())
+        dao = handle.attach(Dao::class)
+    }
+
+    @AfterEach
+    fun tearDown() {
+        handle.close()
+    }
+
+    @JvmInline
+    value class ValueId(val value: Int)
+
+    @Test
+    fun testValueClassParameters() {
+        handle.execute("insert into something (id, name) values (2, 'Bean')")
+        assertThat(dao.findNameById(ValueId(2))).isEqualTo("Bean")
+    }
+
+    @Test
+    fun testValueClassBean() {
+        handle.registerRowMapper(Something::class.java, SomethingMapper())
+        handle.execute("insert into something (id, name) values (6, 'Martin')")
+        // can't use the dao; it does not have the row mapper registered
+        val dao2 = handle.attach(Dao::class)
+        val s = dao2.retrieveById(ValueId(6))
+        assertThat(s.name).isEqualTo("Martin")
+    }
+
+    @Test
+    @Disabled("Sadly the Kotlin Mapper does not work with Value types")
+    fun testValueClassKotlinMapper() {
+        handle.registerRowMapper(Something::class.java, KotlinMapper(Something::class))
+        handle.execute("insert into something (id, name) values (6, 'Martin')")
+        // can't use the dao; it does not have the row mapper registered
+        val dao2 = handle.attach(Dao::class)
+        val s = dao2.retrieveById(ValueId(6))
+        assertThat(s.name).isEqualTo("Martin")
+    }
+
+    @Test
+    fun testValueClassColumn() {
+        handle.execute("insert into something (id, name) values (6, 'Martin')")
+
+        val s = dao.findIdByName("Martin")
+        assertThat(s).isEqualTo(ValueId(6))
+
+        val t = dao.findIdByName("Brian")
+        assertThat(t).isNull()
+    }
+
+    @UseStringTemplateSqlLocator
+    interface Dao {
+        // A value class as a parameter requires an explicit value for the SQL query; Kotlin mangles the method name (findNameById-<xxxx>) otherwise.
+        @SqlQuery("findNameById")
+        fun findNameById(@Bind("id") id: ValueId): String?
+
+        @SqlQuery("retrieveById")
+        fun retrieveById(@Bind("id") id: ValueId): Something
+
+        @SqlQuery("findIdByName")
+        fun findIdByName(@Bind("name") name: String?): ValueId?
+    }
+
+    class SomethingMapper : RowMapper<Something> {
+        @Throws(SQLException::class)
+        override fun map(r: ResultSet, ctx: StatementContext): Something = Something(ValueId(r.getInt("id")), r.getString("name"))
+    }
+
+    data class Something(val id: ValueId, val name: String)
+
+    class ValueIdArgumentFactory : AbstractArgumentFactory<ValueId>(Types.INTEGER) {
+        override fun build(value: ValueId?, config: ConfigRegistry): Argument? =
+            Argument { position: Int, statement: PreparedStatement, ctx: StatementContext ->
+                if (value != null) {
+                    statement.setInt(position, value.value)
+                } else {
+                    statement.setNull(position, Types.INTEGER)
+                }
+            }
+    }
+
+    // value type as a column
+
+    class ValueIdColumnMapper : ColumnMapper<ValueId?> {
+        override fun map(rs: ResultSet, columnNumber: Int, ctx: StatementContext): ValueId? {
+            val value = rs.getInt(columnNumber)
+            return if (rs.wasNull()) {
+                null
+            } else {
+                ValueId(value)
+            }
+        }
+    }
+}
diff --git a/kotlin-sqlobject/src/test/resources/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator$Wombat.sql.stg b/kotlin-sqlobject/src/test/resources/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator$Wombat.sql.stg
new file mode 100644
index 0000000000..ca54d21574
--- /dev/null
+++ b/kotlin-sqlobject/src/test/resources/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator$Wombat.sql.stg
@@ -0,0 +1,29 @@
+//
+// 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
+//
+// http://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.
+//
+
+group Wombat;
+
+kangaroo(name) ::= << hello <name> >>
+insert() ::= << insert into something (id, name) values (:id, :name) >>
+findById() ::= << select id, name from something where id = :id >>
+findByIdOrUptoLimit(idLimit) ::= << select id, name from something <if(idLimit)> where id \< :idLimit <else> where id = :id <endif> >>
+findNameFor() ::= << select name from something where id = :id >>
+findNameForWeirdId() ::= << select name from something where id = :id >>
+findByName(name) ::= << select id from something where name = :name >>
+weirdInsert(table, id_column, value_column) ::= <<
+    insert into <table> (<id_column>, <value_column>) values (:id, :value)
+>>
+insertBunches() ::= <<
+    insert into something (id, name) values (:id, :name)
+>>
diff --git a/kotlin-sqlobject/src/test/resources/org/jdbi/v3/sqlobject/kotlin/TestValueClasses$Dao.sql.stg b/kotlin-sqlobject/src/test/resources/org/jdbi/v3/sqlobject/kotlin/TestValueClasses$Dao.sql.stg
new file mode 100644
index 0000000000..9c3d9ca8bd
--- /dev/null
+++ b/kotlin-sqlobject/src/test/resources/org/jdbi/v3/sqlobject/kotlin/TestValueClasses$Dao.sql.stg
@@ -0,0 +1,19 @@
+//
+// 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
+//
+// http://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.
+//
+
+group Dao;
+
+findNameById(id) ::= << select name from something where id = :id >>
+retrieveById(id) ::= << select id, name from something where id = :id >>
+findIdByName(name) ::= << select id from something where name = :name >>
diff --git a/stringtemplate4/src/test/java/org/jdbi/v3/stringtemplate4/TestStringTemplateSqlLocator.java b/stringtemplate4/src/test/java/org/jdbi/v3/stringtemplate4/TestStringTemplateSqlLocator.java
index 5ff11d584b..f282f39f22 100644
--- a/stringtemplate4/src/test/java/org/jdbi/v3/stringtemplate4/TestStringTemplateSqlLocator.java
+++ b/stringtemplate4/src/test/java/org/jdbi/v3/stringtemplate4/TestStringTemplateSqlLocator.java
@@ -43,20 +43,21 @@ public class TestStringTemplateSqlLocator {
     public JdbiExtension h2Extension = JdbiExtension.h2().withInitializer(TestingInitializers.something()).withPlugin(new SqlObjectPlugin());
 
     private Handle handle;
+    private Wombat wombat;
 
     @BeforeEach
     public void setUp() {
         handle = h2Extension.getSharedHandle();
+        wombat = handle.attach(Wombat.class);
     }
 
     @Test
     public void testBaz() {
-        Wombat wombat = handle.attach(Wombat.class);
         wombat.insert(new Something(7, "Henning"));
 
         String name = handle.createQuery("select name from something where id = 7")
-                            .mapTo(String.class)
-                            .one();
+                .mapTo(String.class)
+                .one();
 
         assertThat(name).isEqualTo("Henning");
     }
@@ -65,53 +66,51 @@ public void testBaz() {
     public void testBam() {
         handle.execute("insert into something (id, name) values (6, 'Martin')");
 
-        Something s = handle.attach(Wombat.class).findById(6L);
+        Something s = wombat.findById(6L);
         assertThat(s.getName()).isEqualTo("Martin");
     }
 
     @Test
     public void testBap() {
         handle.execute("insert into something (id, name) values (2, 'Bean')");
-        Wombat w = handle.attach(Wombat.class);
-        assertThat(w.findNameFor(2)).isEqualTo("Bean");
+        assertThat(wombat.findNameFor(2)).isEqualTo("Bean");
     }
 
     @Test
     public void testDefines() {
-        handle.attach(Wombat.class).weirdInsert("something", "id", "name", 5, "Bouncer");
-        handle.attach(Wombat.class).weirdInsert("something", "id", "name", 6, "Bean");
+        wombat.weirdInsert("something", "id", "name", 5, "Bouncer");
+        wombat.weirdInsert("something", "id", "name", 6, "Bean");
         String name = handle.createQuery("select name from something where id = 5")
-                            .mapTo(String.class)
-                            .one();
+                .mapTo(String.class)
+                .one();
 
         assertThat(name).isEqualTo("Bouncer");
     }
 
     @Test
     public void testConditionalExecutionWithNullValue() {
-        handle.attach(Wombat.class).insert(new Something(6, "Jack"));
-        handle.attach(Wombat.class).insert(new Something(7, "Wolf"));
+        wombat.insert(new Something(6, "Jack"));
+        wombat.insert(new Something(7, "Wolf"));
 
-        List<Something> somethings = handle.attach(Wombat.class).findByIdOrUptoLimit(6, null);
+        List<Something> somethings = wombat.findByIdOrUptoLimit(6, null);
         assertThat(somethings).hasSize(1);
     }
 
     @Test
     public void testConditionalExecutionWithNonNullValue() {
-        handle.attach(Wombat.class).insert(new Something(6, "Jack"));
-        handle.attach(Wombat.class).insert(new Something(7, "Wolf"));
+        wombat.insert(new Something(6, "Jack"));
+        wombat.insert(new Something(7, "Wolf"));
 
-        List<Something> somethings = handle.attach(Wombat.class).findByIdOrUptoLimit(null, 8);
+        List<Something> somethings = wombat.findByIdOrUptoLimit(null, 8);
         assertThat(somethings).hasSize(2);
     }
 
     @Test
     public void testBatching() {
-        Wombat roo = handle.attach(Wombat.class);
-        roo.insertBunches(new Something(1, "Jeff"), new Something(2, "Brian"));
+        wombat.insertBunches(new Something(1, "Jeff"), new Something(2, "Brian"));
 
-        assertThat(roo.findById(1L)).isEqualTo(new Something(1, "Jeff"));
-        assertThat(roo.findById(2L)).isEqualTo(new Something(2, "Brian"));
+        assertThat(wombat.findById(1L)).isEqualTo(new Something(1, "Jeff"));
+        assertThat(wombat.findById(2L)).isEqualTo(new Something(2, "Brian"));
     }
 
     @UseStringTemplateSqlLocator
@@ -131,10 +130,10 @@ public interface Wombat {
 
         @SqlUpdate
         void weirdInsert(@Define("table") String table,
-                         @Define("id_column") String idColumn,
-                         @Define("value_column") String valueColumn,
-                         @Bind("id") int id,
-                         @Bind("value") String name);
+                @Define("id_column") String idColumn,
+                @Define("value_column") String valueColumn,
+                @Bind("id") int id,
+                @Bind("value") String name);
 
         @SqlBatch
         void insertBunches(@BindBean Something... somethings);


Reviews:
user: login: stevenschlansker
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-02T22:58:00Z
user: login: stevenschlansker
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T22:58:58Z
user: login: hgschmie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T01:15:19Z
user: login: hgschmie
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-03T01:25:16Z

Review comments:
diff_hunk: @@ -0,0 +1,177 @@
+/*
+ * 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
+ *
+ * http://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.jdbi.v3.sqlobject.kotlin
+
+import org.assertj.core.api.Assertions
+import org.jdbi.v3.core.Handle
+import org.jdbi.v3.core.Something
+import org.jdbi.v3.core.kotlin.mapTo
+import org.jdbi.v3.core.mapper.RowMapper
+import org.jdbi.v3.core.statement.StatementContext
+import org.jdbi.v3.sqlobject.SqlObjectPlugin
+import org.jdbi.v3.sqlobject.config.RegisterRowMapper
+import org.jdbi.v3.sqlobject.customizer.Bind
+import org.jdbi.v3.sqlobject.customizer.BindBean
+import org.jdbi.v3.sqlobject.customizer.Define
+import org.jdbi.v3.sqlobject.statement.SqlBatch
+import org.jdbi.v3.sqlobject.statement.SqlQuery
+import org.jdbi.v3.sqlobject.statement.SqlUpdate
+import org.jdbi.v3.stringtemplate4.UseStringTemplateSqlLocator
+import org.jdbi.v3.testing.junit5.JdbiExtension
+import org.jdbi.v3.testing.junit5.internal.TestingInitializers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import java.sql.ResultSet
+import java.sql.SQLException
+
+// port from the stringtemplate4 class to kotlin
+class TestStringTemplateSqlLocator {
+    @RegisterExtension
+    var h2Extension: JdbiExtension = JdbiExtension.h2().withInitializer(TestingInitializers.something()).withPlugin(SqlObjectPlugin())
+
+    private var handle: Handle? = null
+
+    @BeforeEach
+    fun setUp() {
+        handle = h2Extension.getSharedHandle()
+    }
+
+    @Test
+    fun testBaz() {
+        val wombat =
+            handle!!.attach(Wombat::class)
+        wombat.insert(Something(7, "Henning"))
+
+        val name = handle!!.createQuery("select name from something where id = 7")
+            .mapTo(String::class)
+            .one()
+
+        Assertions.assertThat(name).isEqualTo("Henning")
+    }
+
+    @Test
+    fun testBam() {
+        handle!!.execute("insert into something (id, name) values (6, 'Martin')")
path: kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator.kt
user: login: stevenschlansker
body: if we will end up attaching a sqlobject anyway, could make a `Wombat.insert(id, name)` method to avoid repeating the insert sql
created_at: 2025-05-02T22:54:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+/*
+ * 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
+ *
+ * http://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.jdbi.v3.sqlobject.kotlin
+
+import org.assertj.core.api.Assertions
+import org.jdbi.v3.core.Handle
+import org.jdbi.v3.core.Something
+import org.jdbi.v3.core.kotlin.mapTo
+import org.jdbi.v3.core.mapper.RowMapper
+import org.jdbi.v3.core.statement.StatementContext
+import org.jdbi.v3.sqlobject.SqlObjectPlugin
+import org.jdbi.v3.sqlobject.config.RegisterRowMapper
+import org.jdbi.v3.sqlobject.customizer.Bind
+import org.jdbi.v3.sqlobject.customizer.BindBean
+import org.jdbi.v3.sqlobject.customizer.Define
+import org.jdbi.v3.sqlobject.statement.SqlBatch
+import org.jdbi.v3.sqlobject.statement.SqlQuery
+import org.jdbi.v3.sqlobject.statement.SqlUpdate
+import org.jdbi.v3.stringtemplate4.UseStringTemplateSqlLocator
+import org.jdbi.v3.testing.junit5.JdbiExtension
+import org.jdbi.v3.testing.junit5.internal.TestingInitializers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import java.sql.ResultSet
+import java.sql.SQLException
+
+// port from the stringtemplate4 class to kotlin
+class TestStringTemplateSqlLocator {
+    @RegisterExtension
+    var h2Extension: JdbiExtension = JdbiExtension.h2().withInitializer(TestingInitializers.something()).withPlugin(SqlObjectPlugin())
+
+    private var handle: Handle? = null
+
+    @BeforeEach
+    fun setUp() {
+        handle = h2Extension.getSharedHandle()
+    }
+
+    @Test
+    fun testBaz() {
+        val wombat =
+            handle!!.attach(Wombat::class)
path: kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator.kt
user: login: stevenschlansker
body: could attach your wombat in the setup method, since this line is repeated in every test
created_at: 2025-05-02T22:55:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+/*
+ * 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
+ *
+ * http://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.jdbi.v3.sqlobject.kotlin
+
+import org.assertj.core.api.Assertions
+import org.jdbi.v3.core.Handle
+import org.jdbi.v3.core.Something
+import org.jdbi.v3.core.kotlin.mapTo
+import org.jdbi.v3.core.mapper.RowMapper
+import org.jdbi.v3.core.statement.StatementContext
+import org.jdbi.v3.sqlobject.SqlObjectPlugin
+import org.jdbi.v3.sqlobject.config.RegisterRowMapper
+import org.jdbi.v3.sqlobject.customizer.Bind
+import org.jdbi.v3.sqlobject.customizer.BindBean
+import org.jdbi.v3.sqlobject.customizer.Define
+import org.jdbi.v3.sqlobject.statement.SqlBatch
+import org.jdbi.v3.sqlobject.statement.SqlQuery
+import org.jdbi.v3.sqlobject.statement.SqlUpdate
+import org.jdbi.v3.stringtemplate4.UseStringTemplateSqlLocator
+import org.jdbi.v3.testing.junit5.JdbiExtension
+import org.jdbi.v3.testing.junit5.internal.TestingInitializers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import java.sql.ResultSet
+import java.sql.SQLException
+
+// port from the stringtemplate4 class to kotlin
+class TestStringTemplateSqlLocator {
+    @RegisterExtension
+    var h2Extension: JdbiExtension = JdbiExtension.h2().withInitializer(TestingInitializers.something()).withPlugin(SqlObjectPlugin())
+
+    private var handle: Handle? = null
+
+    @BeforeEach
+    fun setUp() {
+        handle = h2Extension.getSharedHandle()
+    }
+
+    @Test
+    fun testBaz() {
+        val wombat =
+            handle!!.attach(Wombat::class)
+        wombat.insert(Something(7, "Henning"))
+
+        val name = handle!!.createQuery("select name from something where id = 7")
+            .mapTo(String::class)
+            .one()
+
+        Assertions.assertThat(name).isEqualTo("Henning")
+    }
+
+    @Test
+    fun testBam() {
+        handle!!.execute("insert into something (id, name) values (6, 'Martin')")
+
+        val s = handle!!.attach(Wombat::class).findById(6L)
+        Assertions.assertThat(s.getName()).isEqualTo("Martin")
+    }
+
+    @Test
path: kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator.kt
user: login: stevenschlansker
body: A number of these tests don't seem to have anything to do with the stated issue - doesn't use the value type, doesn't use a statement locator. If they are just examples to follow, that's fine, but if we keep them some better method names would be nice.
created_at: 2025-05-02T22:57:54Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+/*
+ * 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
+ *
+ * http://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.jdbi.v3.sqlobject.kotlin
+
+import org.assertj.core.api.Assertions
+import org.jdbi.v3.core.Handle
+import org.jdbi.v3.core.Something
+import org.jdbi.v3.core.kotlin.mapTo
+import org.jdbi.v3.core.mapper.RowMapper
+import org.jdbi.v3.core.statement.StatementContext
+import org.jdbi.v3.sqlobject.SqlObjectPlugin
+import org.jdbi.v3.sqlobject.config.RegisterRowMapper
+import org.jdbi.v3.sqlobject.customizer.Bind
+import org.jdbi.v3.sqlobject.customizer.BindBean
+import org.jdbi.v3.sqlobject.customizer.Define
+import org.jdbi.v3.sqlobject.statement.SqlBatch
+import org.jdbi.v3.sqlobject.statement.SqlQuery
+import org.jdbi.v3.sqlobject.statement.SqlUpdate
+import org.jdbi.v3.stringtemplate4.UseStringTemplateSqlLocator
+import org.jdbi.v3.testing.junit5.JdbiExtension
+import org.jdbi.v3.testing.junit5.internal.TestingInitializers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import java.sql.ResultSet
+import java.sql.SQLException
+
+// port from the stringtemplate4 class to kotlin
+class TestStringTemplateSqlLocator {
+    @RegisterExtension
+    var h2Extension: JdbiExtension = JdbiExtension.h2().withInitializer(TestingInitializers.something()).withPlugin(SqlObjectPlugin())
+
+    private var handle: Handle? = null
+
+    @BeforeEach
+    fun setUp() {
+        handle = h2Extension.getSharedHandle()
+    }
+
+    @Test
+    fun testBaz() {
+        val wombat =
+            handle!!.attach(Wombat::class)
+        wombat.insert(Something(7, "Henning"))
+
+        val name = handle!!.createQuery("select name from something where id = 7")
+            .mapTo(String::class)
+            .one()
+
+        Assertions.assertThat(name).isEqualTo("Henning")
+    }
+
+    @Test
+    fun testBam() {
+        handle!!.execute("insert into something (id, name) values (6, 'Martin')")
+
+        val s = handle!!.attach(Wombat::class).findById(6L)
+        Assertions.assertThat(s.getName()).isEqualTo("Martin")
+    }
+
+    @Test
path: kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator.kt
user: login: stevenschlansker
body: Oh, I suppose the `findForName` uses statement locator. That's fine then. I still think a better method name would be nice.
created_at: 2025-05-02T22:58:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,177 @@
+/*
+ * 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
+ *
+ * http://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.jdbi.v3.sqlobject.kotlin
+
+import org.assertj.core.api.Assertions
+import org.jdbi.v3.core.Handle
+import org.jdbi.v3.core.Something
+import org.jdbi.v3.core.kotlin.mapTo
+import org.jdbi.v3.core.mapper.RowMapper
+import org.jdbi.v3.core.statement.StatementContext
+import org.jdbi.v3.sqlobject.SqlObjectPlugin
+import org.jdbi.v3.sqlobject.config.RegisterRowMapper
+import org.jdbi.v3.sqlobject.customizer.Bind
+import org.jdbi.v3.sqlobject.customizer.BindBean
+import org.jdbi.v3.sqlobject.customizer.Define
+import org.jdbi.v3.sqlobject.statement.SqlBatch
+import org.jdbi.v3.sqlobject.statement.SqlQuery
+import org.jdbi.v3.sqlobject.statement.SqlUpdate
+import org.jdbi.v3.stringtemplate4.UseStringTemplateSqlLocator
+import org.jdbi.v3.testing.junit5.JdbiExtension
+import org.jdbi.v3.testing.junit5.internal.TestingInitializers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import java.sql.ResultSet
+import java.sql.SQLException
+
+// port from the stringtemplate4 class to kotlin
+class TestStringTemplateSqlLocator {
+    @RegisterExtension
+    var h2Extension: JdbiExtension = JdbiExtension.h2().withInitializer(TestingInitializers.something()).withPlugin(SqlObjectPlugin())
+
+    private var handle: Handle? = null
+
+    @BeforeEach
+    fun setUp() {
+        handle = h2Extension.getSharedHandle()
+    }
+
+    @Test
+    fun testBaz() {
+        val wombat =
+            handle!!.attach(Wombat::class)
+        wombat.insert(Something(7, "Henning"))
+
+        val name = handle!!.createQuery("select name from something where id = 7")
+            .mapTo(String::class)
+            .one()
+
+        Assertions.assertThat(name).isEqualTo("Henning")
+    }
+
+    @Test
+    fun testBam() {
+        handle!!.execute("insert into something (id, name) values (6, 'Martin')")
path: kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator.kt
user: login: hgschmie
body: this is a literal 1:1 copy of the test in the string template module. They may not be the greatest piece of code but they do the job and I spent minimal time to copy it over so that I could add the test for the value class. :-) 
created_at: 2025-05-03T01:15:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,177 @@
+/*
+ * 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
+ *
+ * http://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.jdbi.v3.sqlobject.kotlin
+
+import org.assertj.core.api.Assertions
+import org.jdbi.v3.core.Handle
+import org.jdbi.v3.core.Something
+import org.jdbi.v3.core.kotlin.mapTo
+import org.jdbi.v3.core.mapper.RowMapper
+import org.jdbi.v3.core.statement.StatementContext
+import org.jdbi.v3.sqlobject.SqlObjectPlugin
+import org.jdbi.v3.sqlobject.config.RegisterRowMapper
+import org.jdbi.v3.sqlobject.customizer.Bind
+import org.jdbi.v3.sqlobject.customizer.BindBean
+import org.jdbi.v3.sqlobject.customizer.Define
+import org.jdbi.v3.sqlobject.statement.SqlBatch
+import org.jdbi.v3.sqlobject.statement.SqlQuery
+import org.jdbi.v3.sqlobject.statement.SqlUpdate
+import org.jdbi.v3.stringtemplate4.UseStringTemplateSqlLocator
+import org.jdbi.v3.testing.junit5.JdbiExtension
+import org.jdbi.v3.testing.junit5.internal.TestingInitializers
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.RegisterExtension
+import java.sql.ResultSet
+import java.sql.SQLException
+
+// port from the stringtemplate4 class to kotlin
+class TestStringTemplateSqlLocator {
+    @RegisterExtension
+    var h2Extension: JdbiExtension = JdbiExtension.h2().withInitializer(TestingInitializers.something()).withPlugin(SqlObjectPlugin())
+
+    private var handle: Handle? = null
+
+    @BeforeEach
+    fun setUp() {
+        handle = h2Extension.getSharedHandle()
+    }
+
+    @Test
+    fun testBaz() {
+        val wombat =
+            handle!!.attach(Wombat::class)
+        wombat.insert(Something(7, "Henning"))
+
+        val name = handle!!.createQuery("select name from something where id = 7")
+            .mapTo(String::class)
+            .one()
+
+        Assertions.assertThat(name).isEqualTo("Henning")
+    }
+
+    @Test
+    fun testBam() {
+        handle!!.execute("insert into something (id, name) values (6, 'Martin')")
+
+        val s = handle!!.attach(Wombat::class).findById(6L)
+        Assertions.assertThat(s.getName()).isEqualTo("Martin")
+    }
+
+    @Test
path: kotlin-sqlobject/src/test/kotlin/org/jdbi/v3/sqlobject/kotlin/TestStringTemplateSqlLocator.kt
user: login: hgschmie
body: as I said, this class is a literal copy of the class from stringtemplate and converted to Kotlin (by Claude Sonnet BTW).
created_at: 2025-05-03T01:25:16Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-03T05:36:02Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=jdbi_jdbi&pullRequest=2791) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=jdbi_jdbi&pullRequest=2791&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=jdbi_jdbi&pullRequest=2791&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=jdbi_jdbi&pullRequest=2791&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=jdbi_jdbi&pullRequest=2791&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=jdbi_jdbi&pullRequest=2791&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=jdbi_jdbi&pullRequest=2791)


author_association: NONE