diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 83bafda5..21501fb8 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -39,7 +39,7 @@ jobs:
- name: Build & Test with Gradle
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- run: ./gradlew build sonar
+ run: ./gradlew build sonar -Ddb=all
- name: Upload Test Report
uses: actions/upload-artifact@v4
diff --git a/.run/Integration Tests (ALL).run.xml b/.run/Integration Tests (ALL).run.xml
new file mode 100644
index 00000000..803b7136
--- /dev/null
+++ b/.run/Integration Tests (ALL).run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+ false
+ true
+ false
+ true
+
+
+
\ No newline at end of file
diff --git a/.run/Integration Tests (MS SQL Server).run.xml b/.run/Integration Tests (MS SQL Server).run.xml
new file mode 100644
index 00000000..ab74d743
--- /dev/null
+++ b/.run/Integration Tests (MS SQL Server).run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ true
+ false
+ true
+
+
+
\ No newline at end of file
diff --git a/.run/Integration Tests (Oracle).run.xml b/.run/Integration Tests (Oracle).run.xml
new file mode 100644
index 00000000..01e2de95
--- /dev/null
+++ b/.run/Integration Tests (Oracle).run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ true
+ false
+ true
+
+
+
\ No newline at end of file
diff --git a/.run/Integration Tests.run.xml b/.run/Integration Tests.run.xml
new file mode 100644
index 00000000..7469c60a
--- /dev/null
+++ b/.run/Integration Tests.run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ true
+ false
+ true
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 0f040f52..b2817d51 100644
--- a/README.md
+++ b/README.md
@@ -372,7 +372,7 @@ If you need support for another DB, and/or find an issue with a particular DB, f
Kapper is in its early stages of development.
The following will be worked on in the next few releases:
-- [ ] Create a benchmark suite to validate performance.
+- [x] Create a benchmark suite to validate performance.
- [x] Add co-routine support.
- [x] Add flow support.
- [ ] Improve and additional support for date/time conversion.
@@ -381,7 +381,7 @@ The following will be worked on in the next few releases:
- [ ] Cache query parsing.
- [ ] Custom SQL type conversion.
- [ ] Bulk operations support
-- [ ] Add MS SQL Server and Oracle integration tests.
+- [x] Add MS SQL Server, Oracle and SQLite integration tests.
- [ ] Tests & examples in other JVM languages.
- [x] Create transaction syntax sugar.
- [ ] Support DTO argument for `execute`.
diff --git a/buildSrc/src/main/kotlin/kapper.library-conventions.gradle.kts b/buildSrc/src/main/kotlin/kapper.library-conventions.gradle.kts
index 2b03d30d..463dc868 100644
--- a/buildSrc/src/main/kotlin/kapper.library-conventions.gradle.kts
+++ b/buildSrc/src/main/kotlin/kapper.library-conventions.gradle.kts
@@ -89,6 +89,8 @@ tasks.register("integrationTest") {
systemProperties(
"junit.jupiter.execution.parallel.enabled" to "false",
)
+
+ systemProperty("db", System.getProperty("db", ""))
}
tasks.named("test") {
diff --git a/core/src/integrationTest/kotlin/net/samyn/kapper/AbstractDbTests.kt b/core/src/integrationTest/kotlin/net/samyn/kapper/AbstractDbTests.kt
index 4c3599be..75500481 100644
--- a/core/src/integrationTest/kotlin/net/samyn/kapper/AbstractDbTests.kt
+++ b/core/src/integrationTest/kotlin/net/samyn/kapper/AbstractDbTests.kt
@@ -1,159 +1,147 @@
package net.samyn.kapper
-import io.kotest.matchers.booleans.shouldBeTrue
+import net.samyn.kapper.internal.DbFlavour
+import net.samyn.kapper.internal.getDbFlavour
+import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Named.named
-import org.junit.jupiter.api.Order
import org.junit.jupiter.api.TestInstance
-import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.Arguments.arguments
-import org.junit.jupiter.params.provider.MethodSource
import org.testcontainers.containers.JdbcDatabaseContainer
+import org.testcontainers.containers.MSSQLServerContainer
import org.testcontainers.containers.MySQLContainer
import org.testcontainers.containers.PostgreSQLContainer
-import org.testcontainers.junit.jupiter.Container
-import org.testcontainers.junit.jupiter.Testcontainers
+import org.testcontainers.oracle.OracleContainer
+import java.sql.Connection
import java.sql.DriverManager
+import java.time.Duration
import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.logging.Level
+import java.util.logging.Logger
+import kotlin.random.Random
import kotlin.use
-@Testcontainers
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
abstract class AbstractDbTests {
companion object {
- val specialTypes =
- mapOf(
- "UUID" to
- mapOf(
- MySQLContainer::class to "VARCHAR(36)",
- ),
- "CLOB" to
- mapOf(
- MySQLContainer::class to "TEXT",
- PostgreSQLContainer::class to "TEXT",
- ),
- "BINARY" to
- mapOf(
- PostgreSQLContainer::class to "BYTEA",
- ),
- "VARBINARY" to
- mapOf(
- PostgreSQLContainer::class to "BYTEA",
- ),
- "BLOB" to
- mapOf(
- PostgreSQLContainer::class to "BYTEA",
- ),
- "FLOAT" to
- mapOf(
- PostgreSQLContainer::class to "NUMERIC",
- ),
- "REAL" to
- mapOf(
- MySQLContainer::class to "FLOAT",
- ),
- )
+ init {
+ Class.forName("org.sqlite.JDBC")
+ }
+
+ private val postgresql by lazy {
+ PostgreSQLContainer("postgres:16").also { it.start() }
+ }
+
+ private val mysql by lazy {
+ MySQLContainer("mysql:8.4").also { it.start() }
+ }
- @Container
- val postgresql = PostgreSQLContainer("postgres:16")
+ private val oracle by lazy {
+ OracleContainer("gvenzl/oracle-free:23-slim-faststart")
+ // pfff, this container is sloooow
+ .withStartupTimeout(Duration.ofMinutes(2))
+ .also { it.start() }
+ }
+
+ private val msSqlServer by lazy {
+ MSSQLServerContainer("mcr.microsoft.com/mssql/server:2017-CU12")
+ .acceptLicense()
+ .also {
+ // pfff, this container is noisy
+ val logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerConnection")
+ logger.level = Level.SEVERE
+ it.start()
+ }
+ }
- @Container
- val mysql = MySQLContainer("mysql:8.4")
+ private val connections = ConcurrentHashMap()
- val allContainers =
+ private fun getConnection(container: JdbcDatabaseContainer<*>): Connection {
+ Class.forName(container.driverClassName)
+ return DriverManager.getConnection(container.jdbcUrl, container.username, container.password)
+ }
+
+ val dbs =
mapOf(
- "PostgreSQL" to postgresql,
- "MySQL" to mysql,
- )
+ DbFlavour.POSTGRESQL to { getConnection(postgresql) },
+ DbFlavour.MYSQL to { getConnection(mysql) },
+ DbFlavour.SQLITE to { DriverManager.getConnection("jdbc:sqlite::memory:") },
+ DbFlavour.MSSQLSERVER to { getConnection(msSqlServer) },
+ DbFlavour.ORACLE to { getConnection(oracle) },
+ ).filter {
+ // by default run against SQLite and PG only
+ // this allows parallel runs for different int tests.
+ when (System.getProperty("db", "").uppercase()) {
+ "" -> it.key == DbFlavour.SQLITE || it.key == DbFlavour.POSTGRESQL
+ "ALL" -> true
+ else -> it.key == DbFlavour.valueOf(System.getProperty("db").uppercase())
+ }
+ }
@JvmStatic
- fun databaseContainers() = allContainers.map { arguments(named(it.key, it.value)) }
+ fun databaseContainers(): List {
+ println("--------------------------------")
+ println("Running tests against:")
+ val connections =
+ connections
+ .map {
+ println(" ${it.key}")
+ arguments(named(it.key.toString(), it.value))
+ }
+ println("--------------------------------")
+ return connections
+ }
}
- private fun convertDbColumnType(
- name: String,
- container: JdbcDatabaseContainer<*>,
- suffix: String = "",
- ) = specialTypes[name]?.get(container::class) ?: (name + suffix)
-
+ val testId = randomUpperCaseString(10) // pfff, Oracle has a limit of 30 chars for table names
val superman = SuperHero(UUID.randomUUID(), "Superman", "superman@dc.com", 86)
val batman = SuperHero(UUID.randomUUID(), "Batman", "batman@dc.com", 85)
val spiderMan = SuperHero(UUID.randomUUID(), "Spider-man", "spider@marvel.com", 62)
@BeforeAll
fun setup() {
- allContainers.values.forEach { container ->
- setupDatabase(container)
+ dbs.forEach { container ->
+ setupDatabase(connections.computeIfAbsent(container.key) { container.value() })
}
}
- protected open fun setupDatabase(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- connection.createStatement().use { statement ->
- statement.execute(
- """
- CREATE TABLE super_heroes (
- id ${convertDbColumnType("UUID", container)} PRIMARY KEY,
- name VARCHAR(100),
- email VARCHAR(100),
- age INT
- );
- """.trimIndent().also {
- println(it)
- },
- )
- statement.execute(
- """
- INSERT INTO super_heroes (id, name, email, age) VALUES
- ('${superman.id}', '${superman.name}', '${superman.email}', ${superman.age}),
- ('${batman.id}', '${batman.name}', '${batman.email}', ${batman.age}),
- ('${spiderMan.id}', '${spiderMan.name}', '${spiderMan.email}', ${spiderMan.age});
- """.trimIndent().also {
- println(it)
- },
- )
- statement.execute(
- """
- CREATE TABLE types_test (
- t_uuid ${convertDbColumnType("UUID", container)},
- t_char CHAR,
- t_varchar VARCHAR(120),
- t_clob ${convertDbColumnType("CLOB", container)},
- t_binary ${convertDbColumnType("BINARY", container, "(16)")},
- t_varbinary ${convertDbColumnType("VARBINARY", container, "(128)")},
- t_large_binary ${convertDbColumnType("BLOB", container)},
- t_numeric NUMERIC(12,6),
- t_decimal DECIMAL(12,6),
- t_smallint SMALLINT,
- t_int INT,
- t_bigint BIGINT,
- t_float ${convertDbColumnType("FLOAT", container, "(8)")},
- t_real ${convertDbColumnType("REAL", container)},
- t_double DOUBLE PRECISION,
- t_date DATE,
- t_local_date DATE,
- t_local_time TIME,
- t_timestamp TIMESTAMP,
- t_boolean BOOLEAN
- )
- """.trimIndent().also {
- println(it)
- },
- )
- }
+ @AfterAll
+ fun tearDown() {
+ connections.forEach { (_, connection) ->
+ connection.close()
}
+ connections.clear()
}
- @ParameterizedTest()
- @MethodSource("databaseContainers")
- @Order(1)
- fun `database should be running`(container: JdbcDatabaseContainer<*>) {
- container.isRunning.shouldBeTrue()
+ protected open fun setupDatabase(connection: Connection) {
+ val dbFlavour = connection.getDbFlavour()
+ connection.createStatement().use { statement ->
+ statement.execute(
+ """
+ CREATE TABLE super_heroes_$testId (
+ id ${convertDbColumnType("UUID", dbFlavour)} PRIMARY KEY,
+ name VARCHAR(100),
+ email VARCHAR(100),
+ age ${convertDbColumnType("INT", dbFlavour)}
+ )
+ """.trimIndent().also {
+ println("------------ $dbFlavour --------------")
+ println(it)
+ },
+ )
+ statement.execute(
+ """
+ INSERT INTO super_heroes_$testId (id, name, email, age) VALUES
+ (${convertUUIDString(superman.id, dbFlavour)}, '${superman.name}', '${superman.email}', ${superman.age}),
+ (${convertUUIDString(batman.id, dbFlavour)}, '${batman.name}', '${batman.email}', ${batman.age}),
+ (${convertUUIDString(spiderMan.id, dbFlavour)}, '${spiderMan.name}', '${spiderMan.email}', ${spiderMan.age});
+ """,
+ )
+ }
}
- protected fun createConnection(container: JdbcDatabaseContainer<*>) =
- DriverManager.getConnection(container.jdbcUrl, container.username, container.password)
-
data class SuperHero(val id: UUID, val name: String, val email: String? = null, val age: Int? = null)
class Villain {
@@ -161,3 +149,11 @@ abstract class AbstractDbTests {
var name: String? = null
}
}
+
+fun randomUpperCaseString(size: Int): String {
+ val charPool = ('A'..'Z') + ('0'..'9')
+ return (1..size)
+ .map { Random.nextInt(0, charPool.size) }
+ .map(charPool::get)
+ .joinToString("")
+}
diff --git a/core/src/integrationTest/kotlin/net/samyn/kapper/DbTypeConverter.kt b/core/src/integrationTest/kotlin/net/samyn/kapper/DbTypeConverter.kt
new file mode 100644
index 00000000..683c56ef
--- /dev/null
+++ b/core/src/integrationTest/kotlin/net/samyn/kapper/DbTypeConverter.kt
@@ -0,0 +1,92 @@
+package net.samyn.kapper
+
+import net.samyn.kapper.internal.DbFlavour
+import java.util.UUID
+
+private val specialTypes =
+ mapOf(
+ "UUID" to
+ mapOf(
+ DbFlavour.MYSQL to "VARCHAR(36)",
+ DbFlavour.MSSQLSERVER to "UNIQUEIDENTIFIER",
+ DbFlavour.ORACLE to "RAW(16)",
+ ),
+ "CLOB" to
+ mapOf(
+ DbFlavour.MYSQL to "TEXT",
+ DbFlavour.POSTGRESQL to "TEXT",
+ DbFlavour.MSSQLSERVER to "NVARCHAR(MAX)",
+ ),
+ "BINARY" to
+ mapOf(
+ DbFlavour.POSTGRESQL to "BYTEA",
+ DbFlavour.ORACLE to "RAW(128)",
+ ),
+ "VARBINARY" to
+ mapOf(
+ DbFlavour.POSTGRESQL to "BYTEA",
+ DbFlavour.ORACLE to "RAW(128)",
+ ),
+ "BLOB" to
+ mapOf(
+ DbFlavour.POSTGRESQL to "BYTEA",
+ DbFlavour.MSSQLSERVER to "VARBINARY(MAX)",
+ ),
+ "FLOAT" to
+ mapOf(
+ DbFlavour.POSTGRESQL to "NUMERIC",
+ DbFlavour.ORACLE to "FLOAT",
+ ),
+ "REAL" to
+ mapOf(
+ DbFlavour.MYSQL to "FLOAT",
+ DbFlavour.ORACLE to "BINARY_FLOAT",
+ ),
+ "BOOLEAN" to
+ mapOf(
+ DbFlavour.MSSQLSERVER to "BIT",
+ DbFlavour.ORACLE to "NUMBER(1)",
+ ),
+ "TIMESTAMP" to
+ mapOf(
+ DbFlavour.MSSQLSERVER to "DATETIME",
+ ),
+ "INT" to
+ mapOf(
+ DbFlavour.ORACLE to "NUMBER(10)",
+ ),
+ "BIGINT" to
+ mapOf(
+ DbFlavour.ORACLE to "NUMBER(19)",
+ ),
+ "DOUBLE PRECISION" to
+ mapOf(
+ DbFlavour.ORACLE to "BINARY_DOUBLE",
+ ),
+ "SMALLINT" to
+ mapOf(
+ DbFlavour.ORACLE to "NUMBER(5)",
+ ),
+ "TIME" to
+ mapOf(
+ DbFlavour.ORACLE to "TIMESTAMP",
+ ),
+ "VARCHAR" to
+ mapOf(
+ DbFlavour.ORACLE to "VARCHAR2(120)",
+ ),
+ )
+
+fun convertDbColumnType(
+ name: String,
+ flavour: DbFlavour,
+ suffix: String = "",
+) = specialTypes[name]?.get(flavour) ?: (name + suffix)
+
+fun convertUUIDString(
+ id: UUID,
+ flavour: DbFlavour,
+) = when (flavour) {
+ DbFlavour.ORACLE -> "HEXTORAW('${id.toString().replace("-", "")}')"
+ else -> "'$id'"
+}
diff --git a/core/src/integrationTest/kotlin/net/samyn/kapper/ExecuteTests.kt b/core/src/integrationTest/kotlin/net/samyn/kapper/ExecuteTests.kt
index d7d6ac09..b12a9583 100644
--- a/core/src/integrationTest/kotlin/net/samyn/kapper/ExecuteTests.kt
+++ b/core/src/integrationTest/kotlin/net/samyn/kapper/ExecuteTests.kt
@@ -4,218 +4,207 @@ import io.kotest.assertions.assertSoftly
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.shouldBe
+import net.samyn.kapper.internal.getDbFlavour
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
-import org.testcontainers.containers.JdbcDatabaseContainer
-import org.testcontainers.containers.MySQLContainer
+import java.sql.Connection
import java.sql.SQLException
import java.util.UUID
-import kotlin.uuid.ExperimentalUuidApi
-import kotlin.uuid.toKotlinUuid
class ExecuteTests : AbstractDbTests() {
- @OptIn(ExperimentalUuidApi::class)
- val table = "super_heros_${UUID.randomUUID().toKotlinUuid().toHexString()}"
-
- override fun setupDatabase(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- connection.createStatement().use { statement ->
- statement.execute(
- """
- CREATE TABLE $table (
- id ${if (container is MySQLContainer) "VARCHAR(36)" else "UUID"} PRIMARY KEY,
- name VARCHAR(100),
- email VARCHAR(100),
- age INT
- );
- """.trimIndent().also {
- println(it)
- },
- )
- }
- }
- }
-
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `SQL Insert single`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val results =
- connection.execute(
- """
- INSERT INTO $table(id, name, email, age) VALUES(:id, :name, :email, :age);
- """.trimIndent(),
- "id" to superman.id,
- "name" to superman.name,
- "email" to superman.email,
- "age" to superman.age,
- )
-
- results.shouldBe(1)
-
- connection.prepareStatement("SELECT name, email, age FROM $table WHERE id = '${superman.id}'").use { stmt ->
+ fun `SQL Insert single`(connection: Connection) {
+ val supermanClone = superman.copy(id = UUID.randomUUID())
+ val results =
+ connection.execute(
+ """
+ INSERT INTO super_heroes_$testId(id, name, email, age) VALUES(:id, :name, :email, :age)
+ """.trimIndent(),
+ "id" to supermanClone.id,
+ "name" to supermanClone.name,
+ "email" to supermanClone.email,
+ "age" to supermanClone.age,
+ )
+
+ results.shouldBe(1)
+
+ connection.prepareStatement(
+ "SELECT name, email, age FROM super_heroes_$testId " +
+ "WHERE id = ${convertUUIDString(supermanClone.id, connection.getDbFlavour())}",
+ )
+ .use { stmt ->
val resultSet = stmt.executeQuery()
+ resultSet.next().shouldBe(true)
assertSoftly(resultSet) {
- next().shouldBe(true)
- getString(1).shouldBe(superman.name)
- getString(2).shouldBe(superman.email)
- getInt(3).shouldBe(superman.age)
+ getString(1).shouldBe(supermanClone.name)
+ getString(2).shouldBe(supermanClone.email)
+ getInt(3).shouldBe(supermanClone.age)
}
}
- }
}
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `SQL Update single`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- connection.createStatement().use { stmt ->
- stmt.execute("INSERT INTO $table(id, name) VALUES('${batman.id}', 'foo');")
- }
-
- val results =
- connection.execute(
- "UPDATE $table SET name = :name, email = :email, age = :age WHERE id = :id;",
- "id" to batman.id,
- "name" to batman.name,
- "email" to batman.email,
- "age" to batman.age,
- )
-
- results.shouldBe(1)
+ fun `SQL Update single`(connection: Connection) {
+ val batmanClone = batman.copy(id = UUID.randomUUID())
+ connection.createStatement().use { stmt ->
+ stmt.execute(
+ "INSERT INTO super_heroes_$testId(id, name) " +
+ "VALUES(${convertUUIDString(batmanClone.id, connection.getDbFlavour())}, 'foo')",
+ )
+ }
- connection.prepareStatement("SELECT name, email, age FROM $table WHERE id = '${batman.id}'").use { stmt ->
+ val results =
+ connection.execute(
+ "UPDATE super_heroes_$testId SET name = :name, email = :email, age = :age WHERE id = :id",
+ "id" to batmanClone.id,
+ "name" to batmanClone.name,
+ "email" to batmanClone.email,
+ "age" to batmanClone.age,
+ )
+
+ results.shouldBe(1)
+
+ connection.prepareStatement(
+ "SELECT name, email, age FROM super_heroes_$testId " +
+ "WHERE id = ${convertUUIDString(batmanClone.id, connection.getDbFlavour())}",
+ )
+ .use { stmt ->
val resultSet = stmt.executeQuery()
assertSoftly(resultSet) {
next().shouldBe(true)
- getString(1).shouldBe(batman.name)
- getString(2).shouldBe(batman.email)
- getInt(3).shouldBe(batman.age)
+ getString(1).shouldBe(batmanClone.name)
+ getString(2).shouldBe(batmanClone.email)
+ getInt(3).shouldBe(batmanClone.age)
}
}
- }
}
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `SQL Update multiple`(container: JdbcDatabaseContainer<*>) {
+ fun `SQL Update multiple`(connection: Connection) {
val ids = listOf(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())
val name = "foo-${UUID.randomUUID()}"
- createConnection(container).use { connection ->
- connection.createStatement().use { stmt ->
- ids.forEach {
- stmt.execute("INSERT INTO $table(id, name) VALUES('$it', '$name');")
- }
+ connection.createStatement().use { stmt ->
+ ids.forEach {
+ stmt.execute(
+ "INSERT INTO super_heroes_$testId(id, name) " +
+ "VALUES(${convertUUIDString(it, connection.getDbFlavour())}, '$name')",
+ )
}
+ }
- val results =
- connection.execute(
- "UPDATE $table SET name = :newName WHERE name = :name;",
- "name" to name,
- "newName" to "bar",
- )
+ val results =
+ connection.execute(
+ "UPDATE super_heroes_$testId SET name = :newName WHERE name = :name",
+ "name" to name,
+ "newName" to "bar",
+ )
- results.shouldBe(ids.size)
- }
+ results.shouldBe(ids.size)
}
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `SQL Delete single`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- connection.createStatement().use { stmt ->
- stmt.execute("INSERT INTO $table(id, name) VALUES('${spiderMan.id}', 'foo');")
- }
+ fun `SQL Delete single`(connection: Connection) {
+ val spidermanClone = spiderMan.copy(id = UUID.randomUUID())
+ connection.createStatement().use { stmt ->
+ stmt.execute(
+ "INSERT INTO super_heroes_$testId(id, name) " +
+ "VALUES(${convertUUIDString(spidermanClone.id, connection.getDbFlavour())}, 'foo')",
+ )
+ }
- val results =
- connection.execute(
- "DELETE FROM $table WHERE id = :id;",
- "id" to spiderMan.id,
- )
+ val results =
+ connection.execute(
+ "DELETE FROM super_heroes_$testId WHERE id = :id",
+ "id" to spidermanClone.id,
+ )
- results.shouldBe(1)
+ results.shouldBe(1)
- connection.prepareStatement("SELECT * FROM $table WHERE id = '${batman.id}'").use { stmt ->
+ connection.prepareStatement(
+ "SELECT * FROM super_heroes_$testId " +
+ "WHERE id = ${convertUUIDString(spidermanClone.id, connection.getDbFlavour())}",
+ )
+ .use { stmt ->
val resultSet = stmt.executeQuery()
assertSoftly(resultSet) {
next().shouldBe(false)
}
}
- }
}
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `SQL Delete multiple`(container: JdbcDatabaseContainer<*>) {
+ fun `SQL Delete multiple`(connection: Connection) {
val ids = listOf(UUID.randomUUID(), UUID.randomUUID(), UUID.randomUUID())
val name = "bar-${UUID.randomUUID()}"
- createConnection(container).use { connection ->
- connection.createStatement().use { stmt ->
- ids.forEach {
- stmt.execute("INSERT INTO $table(id, name) VALUES('$it', '$name');")
- }
+ connection.createStatement().use { stmt ->
+ ids.forEach {
+ stmt.execute(
+ "INSERT INTO super_heroes_$testId(id, name) " +
+ "VALUES(${convertUUIDString(it, connection.getDbFlavour())}, '$name')",
+ )
}
+ }
- val results =
- connection.execute(
- "DELETE FROM $table WHERE name = :name;",
- "name" to name,
- )
+ val results =
+ connection.execute(
+ "DELETE FROM super_heroes_$testId WHERE name = :name",
+ "name" to name,
+ )
- results.shouldBe(ids.size)
- }
+ results.shouldBe(ids.size)
}
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `with TX completes`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- connection.withTransaction {
- val results =
+ fun `with TX completes`(connection: Connection) {
+ connection.withTransaction {
+ val results =
+ connection.execute(
+ """
+ INSERT INTO super_heroes_$testId(id, name, email) VALUES(:id, :name, :email)
+ """.trimIndent(),
+ "id" to UUID.randomUUID(),
+ "name" to "thor",
+ "email" to "thor@world.com",
+ ) +
connection.execute(
"""
- INSERT INTO $table(id, name, email) VALUES(:id, :name, :email);
+ INSERT INTO super_heroes_$testId(id, name, email) VALUES(:id, :name, :email)
""".trimIndent(),
"id" to UUID.randomUUID(),
"name" to "thor",
"email" to "thor@world.com",
- ) +
- connection.execute(
- """
- INSERT INTO $table(id, name, email) VALUES(:id, :name, :email);
- """.trimIndent(),
- "id" to UUID.randomUUID(),
- "name" to "thor",
- "email" to "thor@world.com",
- )
- results shouldBe 2
- }
+ )
+ results shouldBe 2
}
}
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `with TX rolls back`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val id = UUID.randomUUID()
- shouldThrow {
- connection.withTransaction {
- repeat(2) {
- connection.execute(
- """
- INSERT INTO $table(id, name, email) VALUES(:id, :name, :email);
- """.trimIndent(),
- "id" to id,
- "name" to "thor",
- "email" to "thor@world.com",
- )
- }
+ fun `with TX rolls back`(connection: Connection) {
+ val id = UUID.randomUUID()
+ shouldThrow {
+ connection.withTransaction {
+ repeat(2) {
+ connection.execute(
+ """
+ INSERT INTO super_heroes_$testId(id, name, email) VALUES(:id, :name, :email)
+ """.trimIndent(),
+ "id" to id,
+ "name" to "thor",
+ "email" to "thor@world.com",
+ )
}
}
- connection.query(
- "SELECT * FROM $table WHERE id = :id",
- "id" to id,
- ).shouldBeEmpty()
}
+ connection.query(
+ "SELECT * FROM super_heroes_$testId WHERE id = :id",
+ "id" to id,
+ ).shouldBeEmpty()
}
}
diff --git a/core/src/integrationTest/kotlin/net/samyn/kapper/QuerySingleTests.kt b/core/src/integrationTest/kotlin/net/samyn/kapper/QuerySingleTests.kt
index 86420b8d..6034d65c 100644
--- a/core/src/integrationTest/kotlin/net/samyn/kapper/QuerySingleTests.kt
+++ b/core/src/integrationTest/kotlin/net/samyn/kapper/QuerySingleTests.kt
@@ -5,98 +5,94 @@ import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
+import net.samyn.kapper.internal.DbFlavour
+import net.samyn.kapper.internal.getDbFlavour
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
-import org.testcontainers.containers.JdbcDatabaseContainer
+import java.sql.Connection
import java.sql.ResultSet
class QuerySingleTests : AbstractDbTests() {
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `should query 1 heros`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val hero = connection.querySingle("SELECT * FROM super_heroes ORDER BY age DESC LIMIT 1")
- hero.shouldBe(superman)
- }
+ fun `should query 1 heros`(connection: Connection) {
+ val sql =
+ if (DbFlavour.MSSQLSERVER == connection.getDbFlavour()) {
+ "SELECT TOP 1 * FROM super_heroes_$testId ORDER BY age DESC"
+ } else if (DbFlavour.ORACLE == connection.getDbFlavour()) {
+ "SELECT * FROM super_heroes_$testId ORDER BY age DESC FETCH FIRST 1 ROWS ONLY"
+ } else {
+ "SELECT * FROM super_heroes_$testId ORDER BY age DESC LIMIT 1"
+ }
+ val hero = connection.querySingle(sql)
+ hero.shouldBe(superman)
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `should query hero with condition`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val hero = connection.querySingle("SELECT * FROM super_heroes WHERE age > :age", "age" to 85)
- hero.shouldBe(superman)
- }
+ fun `should query hero with condition`(connection: Connection) {
+ val hero = connection.querySingle("SELECT * FROM super_heroes_$testId WHERE age > :age", "age" to 85)
+ hero.shouldBe(superman)
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `should query specific columns`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val hero =
- connection.querySingle(
- "SELECT id, name FROM super_heroes WHERE name = :name",
- "name" to superman.name,
- )
- hero.shouldBe(SuperHero(superman.id, superman.name))
- }
+ fun `should query specific columns`(connection: Connection) {
+ val hero =
+ connection.querySingle(
+ "SELECT id, name FROM super_heroes_$testId WHERE name = :name",
+ "name" to superman.name,
+ )
+ hero.shouldBe(SuperHero(superman.id, superman.name))
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `should handle empty result set`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val hero =
- connection.querySingle(
- "SELECT * FROM super_heroes WHERE name = :name",
- "name" to "joker",
- )
- hero.shouldBeNull()
- }
+ fun `should handle empty result set`(connection: Connection) {
+ val hero =
+ connection.querySingle(
+ "SELECT * FROM super_heroes_$testId WHERE name = :name",
+ "name" to "joker",
+ )
+ hero.shouldBeNull()
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `should throw when larger than one result`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val ex =
- shouldThrow {
- connection.querySingle(
- "SELECT * FROM super_heroes",
- )
- }
- ex.message.shouldContain("3")
- }
+ fun `should throw when larger than one result`(connection: Connection) {
+ val ex =
+ shouldThrow {
+ connection.querySingle(
+ "SELECT * FROM super_heroes_$testId",
+ )
+ }
+ ex.message.shouldContain("3")
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `should query with multiple conditions`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val hero =
- connection.querySingle(
- "SELECT * FROM super_heroes WHERE age BETWEEN :fromAge AND :toAge",
- "fromAge" to 86,
- "toAge" to 89,
- )
- hero.shouldBe(superman)
- }
+ fun `should query with multiple conditions`(connection: Connection) {
+ val hero =
+ connection.querySingle(
+ "SELECT * FROM super_heroes_$testId WHERE age BETWEEN :fromAge AND :toAge",
+ "fromAge" to 86,
+ "toAge" to 89,
+ )
+ hero.shouldBe(superman)
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `can use custom mapper`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val villain =
- connection.querySingle(
- "SELECT id, name FROM super_heroes WHERE name = :name",
- ::createVillain,
- "name" to superman.name,
- )
- villain.shouldNotBeNull()
- villain.id.shouldBe(superman.id.toString())
- villain.name.shouldBe(superman.name)
- }
+ fun `can use custom mapper`(connection: Connection) {
+ val villain =
+ connection.querySingle(
+ "SELECT id, name FROM super_heroes_$testId WHERE name = :name",
+ ::createVillain,
+ "name" to superman.name,
+ )
+ villain.shouldNotBeNull()
+ villain.id!!.shouldBe(superman.id.toString().lowercase().replace("-", ""))
+ villain.name.shouldBe(superman.name)
}
private fun createVillain(
@@ -104,7 +100,7 @@ class QuerySingleTests : AbstractDbTests() {
fields: Map,
): Villain =
Villain().also {
- it.id = resultSet.getString("id")
+ it.id = resultSet.getString("id").lowercase().replace("-", "")
it.name = resultSet.getString("name")
}
}
diff --git a/core/src/integrationTest/kotlin/net/samyn/kapper/QueryTests.kt b/core/src/integrationTest/kotlin/net/samyn/kapper/QueryTests.kt
index 28151ab7..d5a58dab 100644
--- a/core/src/integrationTest/kotlin/net/samyn/kapper/QueryTests.kt
+++ b/core/src/integrationTest/kotlin/net/samyn/kapper/QueryTests.kt
@@ -6,111 +6,97 @@ import io.kotest.matchers.collections.shouldContainOnly
import io.kotest.matchers.shouldBe
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
-import org.testcontainers.containers.JdbcDatabaseContainer
+import java.sql.Connection
import java.sql.ResultSet
class QueryTests : AbstractDbTests() {
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `should query all heros`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val heroes = connection.query("SELECT * FROM super_heroes")
- heroes.shouldContainExactlyInAnyOrder(
- superman,
- batman,
- spiderMan,
- )
- }
+ fun `should query all heros`(connection: Connection) {
+ val heroes = connection.query("SELECT * FROM super_heroes_$testId")
+ heroes.shouldContainExactlyInAnyOrder(
+ superman,
+ batman,
+ spiderMan,
+ )
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `should query heros with condition`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val heroes = connection.query("SELECT * FROM super_heroes WHERE age > :age", "age" to 80)
- heroes.shouldContainExactlyInAnyOrder(
- superman,
- batman,
- )
- }
+ fun `should query heros with condition`(connection: Connection) {
+ val heroes = connection.query("SELECT * FROM super_heroes_$testId WHERE age > :age", "age" to 80)
+ heroes.shouldContainExactlyInAnyOrder(
+ superman,
+ batman,
+ )
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `should query specific columns`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val heroes =
- connection.query(
- "SELECT id, name FROM super_heroes WHERE name = :name",
- "name" to superman.name,
- )
- heroes.shouldContainExactlyInAnyOrder(
- SuperHero(superman.id, superman.name),
+ fun `should query specific columns`(connection: Connection) {
+ val heroes =
+ connection.query(
+ "SELECT id, name FROM super_heroes_$testId WHERE name = :name",
+ "name" to superman.name,
)
- }
+ heroes.shouldContainExactlyInAnyOrder(
+ SuperHero(superman.id, superman.name),
+ )
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `should handle empty result set`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val heroes =
- connection.query(
- "SELECT * FROM super_heroes WHERE name = :name",
- "name" to "joker",
- )
- heroes.shouldBeEmpty()
- }
+ fun `should handle empty result set`(connection: Connection) {
+ val heroes =
+ connection.query(
+ "SELECT * FROM super_heroes_$testId WHERE name = :name",
+ "name" to "joker",
+ )
+ heroes.shouldBeEmpty()
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `should query with multiple conditions`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val heroes =
- connection.query(
- "SELECT * FROM super_heroes WHERE age BETWEEN :fromAge AND :toAge",
- "fromAge" to 80,
- "toAge" to 89,
- )
- heroes.shouldContainExactlyInAnyOrder(
- superman,
- batman,
+ fun `should query with multiple conditions`(connection: Connection) {
+ val heroes =
+ connection.query(
+ "SELECT * FROM super_heroes_$testId WHERE age BETWEEN :fromAge AND :toAge",
+ "fromAge" to 80,
+ "toAge" to 89,
)
- }
+ heroes.shouldContainExactlyInAnyOrder(
+ superman,
+ batman,
+ )
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `support field labels`(container: JdbcDatabaseContainer<*>) {
+ fun `support field labels`(connection: Connection) {
data class SimpleClass(val superHeroName: String)
- createConnection(container).use { connection ->
- val hero =
- connection.query(
- "SELECT name as superHeroName FROM super_heroes WHERE id = :id",
- "id" to superman.id,
- )
- hero.shouldContainOnly(
- SimpleClass(superman.name),
+ val hero =
+ connection.query(
+ "SELECT name as superHeroName FROM super_heroes_$testId WHERE id = :id",
+ "id" to superman.id,
)
- }
+ hero.shouldContainOnly(
+ SimpleClass(superman.name),
+ )
}
@ParameterizedTest
@MethodSource("databaseContainers")
- fun `can use custom mapper`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- val villain =
- connection.query(
- "SELECT id, name FROM super_heroes WHERE name = :name",
- ::createVillain,
- "name" to superman.name,
- )
+ fun `can use custom mapper`(connection: Connection) {
+ val villain =
+ connection.query(
+ "SELECT id, name FROM super_heroes_$testId WHERE name = :name",
+ ::createVillain,
+ "name" to superman.name,
+ )
- villain.size.shouldBe(1)
- villain.first().id.shouldBe(superman.id.toString())
- villain.first().name.shouldBe(superman.name)
- }
+ villain.size.shouldBe(1)
+ villain.first().id!!.shouldBe(superman.id.toString().lowercase().replace("-", ""))
+ villain.first().name.shouldBe(superman.name)
}
private fun createVillain(
@@ -118,7 +104,7 @@ class QueryTests : AbstractDbTests() {
fields: Map,
): Villain =
Villain().also {
- it.id = resultSet.getString("id")
+ it.id = resultSet.getString("id").lowercase().replace("-", "")
it.name = resultSet.getString("name")
}
}
diff --git a/core/src/integrationTest/kotlin/net/samyn/kapper/SqlInjectionTest.kt b/core/src/integrationTest/kotlin/net/samyn/kapper/SqlInjectionTest.kt
index 773d1c55..69af678e 100644
--- a/core/src/integrationTest/kotlin/net/samyn/kapper/SqlInjectionTest.kt
+++ b/core/src/integrationTest/kotlin/net/samyn/kapper/SqlInjectionTest.kt
@@ -3,19 +3,17 @@ package net.samyn.kapper
import io.kotest.matchers.collections.shouldNotBeEmpty
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
-import org.testcontainers.containers.JdbcDatabaseContainer
+import java.sql.Connection
class SqlInjectionTest : AbstractDbTests() {
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `cannot inject SQL with parameter`(container: JdbcDatabaseContainer<*>) {
- createConnection(container).use { connection ->
- connection.execute(
- "UPDATE super_heroes SET name = 'foo' WHERE name = :name;",
- "name" to "bar; DROP TABLE super_heroes;",
- )
+ fun `cannot inject SQL with parameter`(connection: Connection) {
+ connection.execute(
+ "UPDATE super_heroes_$testId SET name = 'foo' WHERE name = :name",
+ "name" to "bar; DROP TABLE super_heroes",
+ )
- connection.query("SELECT * FROM super_heroes").shouldNotBeEmpty()
- }
+ connection.query("SELECT * FROM super_heroes_$testId").shouldNotBeEmpty()
}
}
diff --git a/core/src/integrationTest/kotlin/net/samyn/kapper/TypeCastTest.kt b/core/src/integrationTest/kotlin/net/samyn/kapper/TypeCastTest.kt
index 9bad678b..eb1ea6a1 100644
--- a/core/src/integrationTest/kotlin/net/samyn/kapper/TypeCastTest.kt
+++ b/core/src/integrationTest/kotlin/net/samyn/kapper/TypeCastTest.kt
@@ -4,15 +4,26 @@ import io.kotest.matchers.should
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import org.junit.jupiter.api.Test
+import org.testcontainers.containers.PostgreSQLContainer
+import org.testcontainers.junit.jupiter.Container
+import org.testcontainers.junit.jupiter.Testcontainers
+import java.sql.DriverManager
import java.time.Instant
-class TypeCastTest : AbstractDbTests() {
+@Testcontainers
+class TypeCastTest {
+ @Container
+ val postgresql = PostgreSQLContainer("postgres:16")
+
+ private fun createConnection() = DriverManager.getConnection(postgresql.jdbcUrl, postgresql.username, postgresql.password)
+
@Test
fun `postgresql uuid can cast to string`() {
+ postgresql.start()
// replicate isssue https://github.com/driessamyn/kapper/issues/77
// create table
- createConnection(postgresql).use { connection ->
+ createConnection().use { connection ->
connection.execute(
"""
CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Ensures the UUID extension is available
@@ -28,7 +39,7 @@ class TypeCastTest : AbstractDbTests() {
}
// insert
- createConnection(postgresql).use { connection ->
+ createConnection().use { connection ->
connection.execute(
"""
INSERT INTO blogs (title, content) VALUES ('First blog', 'This is the first blog');
@@ -40,7 +51,7 @@ class TypeCastTest : AbstractDbTests() {
data class Blog(val id: String, val title: String, val createdAt: Instant, val content: String)
val result =
- createConnection(postgresql).use { connection ->
+ createConnection().use { connection ->
connection.query("SELECT id::text, title, created_at as createdAt, content FROM blogs;")
}
diff --git a/core/src/integrationTest/kotlin/net/samyn/kapper/TypesTest.kt b/core/src/integrationTest/kotlin/net/samyn/kapper/TypesTest.kt
index 84f81820..1daa0a2d 100644
--- a/core/src/integrationTest/kotlin/net/samyn/kapper/TypesTest.kt
+++ b/core/src/integrationTest/kotlin/net/samyn/kapper/TypesTest.kt
@@ -1,9 +1,10 @@
package net.samyn.kapper
import io.kotest.matchers.shouldBe
+import net.samyn.kapper.internal.getDbFlavour
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
-import org.testcontainers.containers.JdbcDatabaseContainer
+import java.sql.Connection
import java.time.Instant
import java.time.LocalDate
import java.time.LocalTime
@@ -14,89 +15,121 @@ import java.util.UUID
import kotlin.random.Random
class TypesTest : AbstractDbTests() {
+ override fun setupDatabase(connection: Connection) {
+ val dbFlavour = connection.getDbFlavour()
+ connection.createStatement().use { statement ->
+ statement.execute(
+ """
+ CREATE TABLE types_test_$testId (
+ t_uuid ${convertDbColumnType("UUID", dbFlavour)},
+ t_char CHAR,
+ t_varchar ${convertDbColumnType("VARCHAR", dbFlavour, "(120)")},
+ t_clob ${convertDbColumnType("CLOB", dbFlavour)},
+ t_binary ${convertDbColumnType("BINARY", dbFlavour, "(16)")},
+ t_varbinary ${convertDbColumnType("VARBINARY", dbFlavour, "(128)")},
+ t_large_binary ${convertDbColumnType("BLOB", dbFlavour)},
+ t_numeric NUMERIC(12,6),
+ t_decimal DECIMAL(12,6),
+ t_smallint ${convertDbColumnType("SMALLINT", dbFlavour)},
+ t_int ${convertDbColumnType("INT", dbFlavour)},
+ t_bigint ${convertDbColumnType("BIGINT", dbFlavour)},
+ t_float ${convertDbColumnType("FLOAT", dbFlavour, "(8)")},
+ t_real ${convertDbColumnType("REAL", dbFlavour)},
+ t_double ${convertDbColumnType("DOUBLE PRECISION", dbFlavour)},
+ t_date DATE,
+ t_local_date DATE,
+ t_local_time ${convertDbColumnType("TIME", dbFlavour)},
+ t_timestamp ${convertDbColumnType("TIMESTAMP", dbFlavour)},
+ t_boolean ${convertDbColumnType("BOOLEAN", dbFlavour)}
+ )
+ """.trimIndent().also {
+ println(it)
+ },
+ )
+ }
+ }
+
@ParameterizedTest()
@MethodSource("databaseContainers")
- fun `can insert and retreive the same types`(container: JdbcDatabaseContainer<*>) {
+ fun `can insert and retreive the same types`(connection: Connection) {
val testData = createTestObject()
- createConnection(container).use { connection ->
- val result =
- connection.execute(
- """
- INSERT INTO types_test (
- t_uuid,
- t_char,
- t_varchar,
- t_clob,
- t_binary,
- t_varbinary,
- t_large_binary,
- t_numeric,
- t_decimal,
- t_smallint,
- t_int,
- t_bigint,
- t_float,
- t_real,
- t_double,
- t_date,
- t_local_date,
- t_local_time,
- t_timestamp,
- t_boolean
- ) VALUES (
- :uuid,
- :char,
- :varchar,
- :clob,
- :binary,
- :varbinary,
- :large_binary,
- :numeric,
- :decimal,
- :smallint,
- :int,
- :bigint,
- :float,
- :real,
- :double,
- :date,
- :local_date,
- :local_time,
- :timestamp,
- :boolean
- );
- """.trimIndent(),
- "uuid" to testData.t_uuid,
- "char" to testData.t_char,
- "varchar" to testData.t_varchar,
- "clob" to testData.t_clob,
- "binary" to testData.t_binary,
- "varbinary" to testData.t_varbinary,
- "large_binary" to testData.t_large_binary,
- "numeric" to testData.t_numeric,
- "decimal" to testData.t_decimal,
- "smallint" to testData.t_smallint,
- "int" to testData.t_int,
- "bigint" to testData.t_bigint,
- "float" to testData.t_float,
- "real" to testData.t_real,
- "double" to testData.t_double,
- "date" to testData.t_date,
- "local_date" to testData.t_local_date,
- "local_time" to testData.t_local_time,
- "timestamp" to testData.t_timestamp,
- "boolean" to testData.t_boolean,
- )
+ val result =
+ connection.execute(
+ """
+ INSERT INTO types_test_$testId (
+ t_uuid,
+ t_char,
+ t_varchar,
+ t_clob,
+ t_binary,
+ t_varbinary,
+ t_large_binary,
+ t_numeric,
+ t_decimal,
+ t_smallint,
+ t_int,
+ t_bigint,
+ t_float,
+ t_real,
+ t_double,
+ t_date,
+ t_local_date,
+ t_local_time,
+ t_timestamp,
+ t_boolean
+ ) VALUES (
+ :uuid,
+ :char,
+ :varchar,
+ :clob,
+ :binary,
+ :varbinary,
+ :large_binary,
+ :numeric,
+ :decimal,
+ :smallint,
+ :int,
+ :bigint,
+ :float,
+ :real,
+ :double,
+ :date,
+ :local_date,
+ :local_time,
+ :timestamp,
+ :boolean
+ )
+ """.trimIndent(),
+ "uuid" to testData.t_uuid,
+ "char" to testData.t_char,
+ "varchar" to testData.t_varchar,
+ "clob" to testData.t_clob,
+ "binary" to testData.t_binary,
+ "varbinary" to testData.t_varbinary,
+ "large_binary" to testData.t_large_binary,
+ "numeric" to testData.t_numeric,
+ "decimal" to testData.t_decimal,
+ "smallint" to testData.t_smallint,
+ "int" to testData.t_int,
+ "bigint" to testData.t_bigint,
+ "float" to testData.t_float,
+ "real" to testData.t_real,
+ "double" to testData.t_double,
+ "date" to testData.t_date,
+ "local_date" to testData.t_local_date,
+ "local_time" to testData.t_local_time,
+ "timestamp" to testData.t_timestamp,
+ "boolean" to testData.t_boolean,
+ )
- result.shouldBe(1)
+ result.shouldBe(1)
- val selectResult =
- connection.querySingle(
- "SELECT * FROM types_test where t_uuid = :uuid",
- "uuid" to testData.t_uuid,
- )
- selectResult.shouldBe(testData)
- }
+ val selectResult =
+ connection.querySingle(
+ "SELECT * FROM types_test_$testId where t_uuid = :uuid",
+ "uuid" to testData.t_uuid,
+ )
+ selectResult.shouldBe(testData)
}
data class TypeTest(
diff --git a/core/src/main/kotlin/net/samyn/kapper/Field.kt b/core/src/main/kotlin/net/samyn/kapper/Field.kt
index 935f1082..aa4fc3a0 100644
--- a/core/src/main/kotlin/net/samyn/kapper/Field.kt
+++ b/core/src/main/kotlin/net/samyn/kapper/Field.kt
@@ -1,5 +1,6 @@
package net.samyn.kapper
+import net.samyn.kapper.internal.DbFlavour
import java.sql.JDBCType
/**
@@ -7,5 +8,6 @@ import java.sql.JDBCType
* @param columnIndex index of the column.
* @param type JDBCType
* @param typeName of the DB type as returned by the JDBC driver.
+ * @param dbFlavour the database flavour.
*/
-data class Field(val columnIndex: Int, val type: JDBCType, val typeName: String)
+data class Field(val columnIndex: Int, val type: JDBCType, val typeName: String, val dbFlavour: DbFlavour)
diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/AutoConverter.kt b/core/src/main/kotlin/net/samyn/kapper/internal/AutoConverter.kt
index 7055d2c0..19c0e26d 100644
--- a/core/src/main/kotlin/net/samyn/kapper/internal/AutoConverter.kt
+++ b/core/src/main/kotlin/net/samyn/kapper/internal/AutoConverter.kt
@@ -7,6 +7,7 @@ import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
+import java.util.Date
import java.util.UUID
import kotlin.reflect.KClass
@@ -20,6 +21,11 @@ internal class AutoConverter(
LocalDateTime::class to ::convertLocalDateTime,
LocalTime::class to ::convertLocalTime,
Instant::class to ::convertInstant,
+ Char::class to ::convertChar,
+ Int::class to ::convertInt,
+ Long::class to ::convertLong,
+ Date::class to ::convertDate,
+ Boolean::class to ::convertBoolean,
),
) {
fun convert(
diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/Converters.kt b/core/src/main/kotlin/net/samyn/kapper/internal/Converters.kt
index 4308b232..41ea020f 100644
--- a/core/src/main/kotlin/net/samyn/kapper/internal/Converters.kt
+++ b/core/src/main/kotlin/net/samyn/kapper/internal/Converters.kt
@@ -20,7 +20,7 @@ internal fun convertInstant(value: Any): Instant =
}
is LocalDateTime -> {
- value.atZone(java.time.ZoneOffset.UTC).toInstant()
+ value.atZone(java.time.ZoneOffset.systemDefault()).toInstant()
}
else -> {
@@ -31,32 +31,85 @@ internal fun convertInstant(value: Any): Instant =
}
internal fun convertLocalTime(value: Any): LocalTime =
- if (value is Instant) {
- LocalTime.ofInstant(value, java.time.ZoneOffset.UTC)
- } else {
- throw KapperUnsupportedOperationException(
- "Cannot auto-convert from ${value.javaClass} to LocalTime",
- )
+ when (value) {
+ is Instant -> {
+ LocalTime.ofInstant(value, java.time.ZoneOffset.systemDefault())
+ }
+
+ is String -> {
+ LocalTime.parse(value)
+ }
+
+ is Long -> {
+ LocalTime.ofInstant(Instant.ofEpochMilli(value), java.time.ZoneOffset.systemDefault())
+ }
+
+ is Int -> {
+ LocalTime.ofInstant(Instant.ofEpochMilli(value.toLong()), java.time.ZoneOffset.systemDefault())
+ }
+
+ else -> {
+ throw KapperUnsupportedOperationException(
+ "Cannot auto-convert value '$value' from ${value.javaClass} to LocalTime",
+ )
+ }
}
internal fun convertLocalDateTime(value: Any): LocalDateTime =
- if (value is Instant) {
- LocalDateTime.ofInstant(value, java.time.ZoneOffset.UTC)
- } else {
- throw KapperUnsupportedOperationException(
- "Cannot auto-convert from ${value.javaClass} to LocalDateTime",
- )
+ when (value) {
+ is Instant -> {
+ LocalDateTime.ofInstant(value, java.time.ZoneOffset.systemDefault())
+ }
+
+ is String -> {
+ LocalDateTime.parse(value)
+ }
+
+ else -> {
+ throw KapperUnsupportedOperationException(
+ "Cannot auto-convert from ${value.javaClass} to LocalDateTime",
+ )
+ }
}
internal fun convertLocalDate(value: Any): LocalDate =
- if (value is Date) {
- val cal = Calendar.getInstance()
- cal.time = value
- LocalDate.of(cal[Calendar.YEAR], cal[Calendar.MONTH] + 1, cal[Calendar.DAY_OF_MONTH])
- } else {
- throw KapperUnsupportedOperationException(
- "Cannot auto-convert from ${value.javaClass} to LocalDate",
- )
+ when (value) {
+ is Date -> {
+ val cal = Calendar.getInstance()
+ cal.time = value
+ LocalDate.of(cal[Calendar.YEAR], cal[Calendar.MONTH] + 1, cal[Calendar.DAY_OF_MONTH])
+ }
+ is LocalDateTime -> {
+ value.toLocalDate()
+ }
+ is String -> {
+ LocalDate.parse(value)
+ }
+ is Instant -> {
+ LocalDate.ofInstant(value, java.time.ZoneOffset.systemDefault())
+ }
+ else -> {
+ throw KapperUnsupportedOperationException(
+ "Cannot auto-convert from ${value.javaClass} to LocalDate",
+ )
+ }
+ }
+
+internal fun convertDate(value: Any): Date =
+ when (value) {
+ is Instant -> {
+ Date.from(value)
+ }
+
+ is LocalDateTime -> {
+ Date.from(value.atZone(java.time.ZoneOffset.systemDefault()).toInstant())
+ }
+
+ else -> {
+ throw KapperUnsupportedOperationException(
+ "Cannot auto-convert from ${value.javaClass} to Date",
+ )
+ }
}
internal fun convertUUID(value: Any): UUID =
@@ -72,6 +125,17 @@ internal fun convertUUID(value: Any): UUID =
}
}
+ is CharArray -> {
+ try {
+ UUID.fromString(String(value))
+ } catch (e: Exception) {
+ throw KapperParseException(
+ "Cannot parse $value to UUID",
+ e,
+ )
+ }
+ }
+
is ByteArray -> {
value.asUUID()
}
@@ -83,6 +147,86 @@ internal fun convertUUID(value: Any): UUID =
}
}
+internal fun convertChar(value: Any): Char =
+ if (value is String) {
+ if (value.length != 1) {
+ throw KapperParseException(
+ "Cannot parse $value to Char (length != 1)",
+ )
+ }
+ value[0]
+ } else if (value is CharArray) {
+ if (value.size != 1) {
+ throw KapperParseException(
+ "Cannot parse $value to Char (size != 1)",
+ )
+ }
+ value[0]
+ } else {
+ throw KapperUnsupportedOperationException(
+ "Cannot auto-convert from ${value.javaClass} to Char",
+ )
+ }
+
+internal fun convertInt(value: Any): Int =
+ when (value) {
+ is Float -> {
+ value.toInt()
+ }
+
+ else -> {
+ throw KapperUnsupportedOperationException(
+ "Cannot auto-convert from ${value.javaClass} to Int",
+ )
+ }
+ }
+
+internal fun convertLong(value: Any): Long =
+ when (value) {
+ is Float -> {
+ value.toLong()
+ }
+
+ else -> {
+ throw KapperUnsupportedOperationException(
+ "Cannot auto-convert from ${value.javaClass} to Long",
+ )
+ }
+ }
+
+internal fun convertBoolean(value: Any): Boolean =
+ when (value) {
+ is String -> {
+ value == "1" || value.toBoolean()
+ }
+
+ is Int -> {
+ value != 0
+ }
+
+ is Byte -> {
+ value != 0.toByte()
+ }
+
+ is Short -> {
+ value != 0.toShort()
+ }
+
+ is Long -> {
+ value != 0L
+ }
+
+ is Float -> {
+ value != 0.0f
+ }
+
+ else -> {
+ throw KapperUnsupportedOperationException(
+ "Cannot auto-convert from ${value.javaClass} to Boolean",
+ )
+ }
+ }
+
fun ByteArray.asUUID(): UUID {
val b = ByteBuffer.wrap(this)
return UUID(b.getLong(), b.getLong())
diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/DbFlavour.kt b/core/src/main/kotlin/net/samyn/kapper/internal/DbFlavour.kt
index 209a5753..a343cb37 100644
--- a/core/src/main/kotlin/net/samyn/kapper/internal/DbFlavour.kt
+++ b/core/src/main/kotlin/net/samyn/kapper/internal/DbFlavour.kt
@@ -12,6 +12,10 @@ fun Connection.getDbFlavour(): DbFlavour {
DbFlavour.MYSQL
} else if (productName.contains("sqlite")) {
DbFlavour.SQLITE
+ } else if (productName.contains("oracle")) {
+ DbFlavour.ORACLE
+ } else if (productName.contains("sql server") || productName.contains("mssql")) {
+ DbFlavour.MSSQLSERVER
} else {
DbFlavour.UNKNOWN
}
@@ -21,5 +25,7 @@ enum class DbFlavour {
POSTGRESQL,
MYSQL,
SQLITE,
+ ORACLE,
+ MSSQLSERVER,
UNKNOWN,
}
diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/KapperImpl.kt b/core/src/main/kotlin/net/samyn/kapper/internal/KapperImpl.kt
index 0bf3a918..821b91a2 100644
--- a/core/src/main/kotlin/net/samyn/kapper/internal/KapperImpl.kt
+++ b/core/src/main/kotlin/net/samyn/kapper/internal/KapperImpl.kt
@@ -31,7 +31,7 @@ internal class KapperImpl(
return buildList {
connection.executeQuery(queryFactory(sql), args).use { rs ->
try {
- val fields = rs.extractFields()
+ val fields = rs.extractFields(connection.getDbFlavour())
while (rs.next()) {
add(mapper(rs, fields))
}
diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/KotlinDataClassMapper.kt b/core/src/main/kotlin/net/samyn/kapper/internal/KotlinDataClassMapper.kt
index f9f29b8b..8a182fa3 100644
--- a/core/src/main/kotlin/net/samyn/kapper/internal/KotlinDataClassMapper.kt
+++ b/core/src/main/kotlin/net/samyn/kapper/internal/KotlinDataClassMapper.kt
@@ -14,7 +14,7 @@ import kotlin.reflect.full.primaryConstructor
class KotlinDataClassMapper(
private val clazz: Class,
val autoConverter: (Any, KClass<*>) -> Any = AutoConverter()::convert,
- val sqlTypesConverter: (JDBCType, String, ResultSet, Int) -> Any? = SQLTypesConverter::convertSQLType,
+ val sqlTypesConverter: (JDBCType, String, ResultSet, Int, DbFlavour) -> Any? = SQLTypesConverter::convertSQLType,
) : Mapper {
private val constructor: KFunction =
clazz.kotlin.primaryConstructor
@@ -29,8 +29,8 @@ class KotlinDataClassMapper(
"Constructor for ${clazz.name} only has: ${properties.keys}",
)
} else if (columns.size < properties.size) {
- val all = columns.map { it.name }
- val missing = properties.filter { !it.value.isOptional && !all.contains(it.value.name) }
+ val all = columns.map { it.name.lowercase() }
+ val missing = properties.filter { !it.value.isOptional && !all.contains(it.key) }
if (missing.isNotEmpty()) {
throw KapperMappingException("The following properties are non-optional and missing: ${missing.keys}")
}
@@ -60,7 +60,13 @@ class KotlinDataClassMapper(
fields.map { field ->
ColumnValue(
field.key,
- sqlTypesConverter(field.value.type, field.value.typeName, resultSet, field.value.columnIndex),
+ sqlTypesConverter(
+ field.value.type,
+ field.value.typeName,
+ resultSet,
+ field.value.columnIndex,
+ field.value.dbFlavour,
+ ),
)
},
)
diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/Metadata.kt b/core/src/main/kotlin/net/samyn/kapper/internal/Metadata.kt
index ae2cdf01..526522bc 100644
--- a/core/src/main/kotlin/net/samyn/kapper/internal/Metadata.kt
+++ b/core/src/main/kotlin/net/samyn/kapper/internal/Metadata.kt
@@ -6,12 +6,17 @@ import net.samyn.kapper.Field
import java.sql.JDBCType
import java.sql.ResultSet
-fun ResultSet.extractFields(): Map =
+fun ResultSet.extractFields(dbFlavour: DbFlavour): Map =
(1..this.metaData.columnCount).associate {
this.metaData.getColumnLabel(it) to
Field(
it,
- JDBCType.valueOf(this.metaData.getColumnType(it)),
+ this.metaData.getColumnType(it).jdbcType(),
this.metaData.getColumnTypeName(it),
+ dbFlavour,
)
}
+
+internal fun Int.jdbcType() =
+ JDBCType.entries.firstOrNull { it.vendorTypeNumber == this }
+ ?: JDBCType.OTHER
diff --git a/core/src/main/kotlin/net/samyn/kapper/internal/SQLTypesConverter.kt b/core/src/main/kotlin/net/samyn/kapper/internal/SQLTypesConverter.kt
index 04b5feb5..efc016c2 100644
--- a/core/src/main/kotlin/net/samyn/kapper/internal/SQLTypesConverter.kt
+++ b/core/src/main/kotlin/net/samyn/kapper/internal/SQLTypesConverter.kt
@@ -3,23 +3,28 @@
package net.samyn.kapper.internal
import net.samyn.kapper.KapperUnsupportedOperationException
+import java.nio.ByteBuffer
import java.sql.JDBCType
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.Timestamp
+import java.text.SimpleDateFormat
import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
import java.util.Date
import java.util.UUID
// TODO: this could be more sophisticated by allowing type conversion hints.
-// TODO: check what hibernate does for these conversions.
internal object SQLTypesConverter {
fun convertSQLType(
sqlType: JDBCType,
sqlTypeName: String,
resultSet: ResultSet,
fieldIndex: Int,
- ): Any {
+ dbFlavour: DbFlavour,
+ ): Any? {
val result =
when (sqlType) {
JDBCType.ARRAY -> resultSet.getArray(fieldIndex)
@@ -29,7 +34,7 @@ internal object SQLTypesConverter {
in listOf(JDBCType.BIT, JDBCType.BOOLEAN) -> resultSet.getBoolean(fieldIndex)
in
listOf(JDBCType.CHAR),
- -> resultSet.getString(fieldIndex).toCharArray()[0]
+ -> resultSet.getString(fieldIndex)?.toCharArray()
in
listOf(
JDBCType.CLOB, JDBCType.LONGNVARCHAR, JDBCType.LONGVARCHAR,
@@ -37,7 +42,7 @@ internal object SQLTypesConverter {
),
-> resultSet.getString(fieldIndex)
in listOf(JDBCType.DATE),
- -> Date(resultSet.getDate(fieldIndex).time)
+ -> convertDate(resultSet, fieldIndex, dbFlavour)
in listOf(JDBCType.DECIMAL, JDBCType.FLOAT, JDBCType.NUMERIC, JDBCType.REAL),
-> resultSet.getFloat(fieldIndex)
JDBCType.DOUBLE ->
@@ -51,19 +56,22 @@ internal object SQLTypesConverter {
JDBCType.TIME,
JDBCType.TIME_WITH_TIMEZONE,
),
- -> resultSet.getTime(fieldIndex).toLocalTime()
+ -> resultSet.getTime(fieldIndex)?.toLocalTime()
in
listOf(
JDBCType.TIMESTAMP,
JDBCType.TIMESTAMP_WITH_TIMEZONE,
),
- -> resultSet.getTimestamp(fieldIndex).toInstant()
+ -> convertTimestamp(resultSet, fieldIndex, sqlTypeName)
// includes: DATALINK, DISTINCT, OTHER, REF, REF_CURSOR, STRUCT, NULL
else -> {
// use name if type is
when (sqlTypeName.lowercase()) {
- "uuid" -> UUID.fromString(resultSet.getString(fieldIndex))
+ "uuid" -> resultSet.getString(fieldIndex)?.let { UUID.fromString(it) }
+ // oracle types
+ "binary_float" -> resultSet.getFloat(fieldIndex)
+ "binary_double" -> resultSet.getDouble(fieldIndex)
else ->
throw KapperUnsupportedOperationException("Conversion from type $sqlType is not supported")
}
@@ -72,6 +80,18 @@ internal object SQLTypesConverter {
return result
}
+ private fun convertTimestamp(
+ resultSet: ResultSet,
+ fieldIndex: Int,
+ sqlTypeName: String,
+ ): Any? =
+ when (sqlTypeName.uppercase()) {
+ "DATE" -> {
+ resultSet.getTimestamp(fieldIndex)?.toLocalDateTime()
+ }
+ else -> resultSet.getTimestamp(fieldIndex)?.toInstant()
+ }
+
fun PreparedStatement.setParameter(
index: Int,
value: Any?,
@@ -88,16 +108,100 @@ internal object SQLTypesConverter {
is String -> setString(index, value)
is ByteArray -> setBytes(index, value)
is Boolean -> setBoolean(index, value)
- is UUID -> {
- if (DbFlavour.MYSQL == dbFlavour) {
- setString(index, value.toString())
- } else {
- setObject(index, value)
+ is UUID ->
+ when (dbFlavour) {
+ DbFlavour.MYSQL -> setString(index, value.toString())
+ DbFlavour.ORACLE -> setBytes(index, value.toBytes())
+ else -> setObject(index, value)
}
- }
is Instant -> setTimestamp(index, Timestamp.from(value))
is Date -> setDate(index, java.sql.Date(value.time))
+ is LocalDate -> setDate(index, java.sql.Date.valueOf(value))
+ is LocalDateTime -> setTimestamp(index, Timestamp.from(value.atZone(java.time.ZoneOffset.systemDefault()).toInstant()))
+ is LocalTime -> setTime(index, java.sql.Time.valueOf(value))
else -> setObject(index, value)
}
}
}
+
+fun UUID.toBytes(): ByteArray {
+ val buffer = ByteBuffer.wrap(ByteArray(16))
+ buffer.putLong(this.mostSignificantBits)
+ buffer.putLong(this.leastSignificantBits)
+ return buffer.array()
+}
+
+val formatters =
+ mapOf(
+ "yyyy-MM-dd" to SimpleDateFormat("yyyy-MM-dd"),
+ "yyyy-MM-dd HH:mm" to SimpleDateFormat("yyyy-MM-dd HH:mm"),
+ "yyyy-MM-dd HH:mm:ss" to SimpleDateFormat("yyyy-MM-dd HH:mm:ss"),
+ "yyyy-MM-dd HH:mm:ss.SSS" to SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"),
+ "yyyy-MM-dd'T'HH:mm" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm"),
+ "yyyy-MM-dd'T'HH:mm:ss" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"),
+ "yyyy-MM-dd'T'HH:mm:ss.SSS" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS"),
+ "HH:mm" to SimpleDateFormat("HH:mm"),
+ "HH:mm:ss" to SimpleDateFormat("HH:mm:ss"),
+ "HH:mm:ss.SSS" to SimpleDateFormat("HH:mm:ss.SSS"),
+ "HH:mm'Z'" to SimpleDateFormat("HH:mm'Z'"),
+ "HH:mm:ss'Z'" to SimpleDateFormat("HH:mm:ss'Z'"),
+ "HH:mm:ss.SSS'Z'" to SimpleDateFormat("HH:mm:ss.SSS'Z'"),
+ "yyyy-MM-dd HH:mm'Z'" to SimpleDateFormat("yyyy-MM-dd HH:mm'Z'"),
+ "yyyy-MM-dd HH:mm:ss'Z'" to SimpleDateFormat("yyyy-MM-dd HH:mm:ss'Z'"),
+ "yyyy-MM-dd HH:mm:ss.SSS'Z'" to SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS'Z'"),
+ "yyyy-MM-dd'T'HH:mm'Z'" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"),
+ "yyyy-MM-dd'T'HH:mm:ss'Z'" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"),
+ "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
+ )
+
+fun convertDate(
+ resultSet: ResultSet,
+ fieldIndex: Int,
+ dbFlavour: DbFlavour,
+): Date? =
+ when (dbFlavour) {
+ DbFlavour.SQLITE -> {
+ val date = resultSet.getObject(fieldIndex) ?: return null
+ when (date) {
+ is Long -> Date(date)
+ is String -> convertSQliteDate(date)
+ else -> throw KapperUnsupportedOperationException("Conversion from type ${date.javaClass} to Date is not supported")
+ }
+ }
+ else -> resultSet.getDate(fieldIndex)?.let { Date(it.time) }
+ }
+
+private fun convertSQliteDate(date: String): Date? =
+ when {
+ date[2] == ':' ->
+ when (date.length) {
+ 5 -> formatters["HH:mm"]!!.parse(date)
+ 6 -> formatters["HH:mm'Z'"]!!.parse(date)
+ 8 -> formatters["HH:mm:ss"]!!.parse(date)
+ 9 -> formatters["HH:mm:ss'Z'"]!!.parse(date)
+ 12 -> formatters["HH:mm:ss.SSS"]!!.parse(date)
+ 13 -> formatters["HH:mm:ss.SSS'Z'"]!!.parse(date)
+ else -> null
+ }
+ date.length > 10 && date[10] == 'T' ->
+ when (date.length) {
+ 16 -> formatters["yyyy-MM-dd'T'HH:mm"]!!.parse(date)
+ 17 -> formatters["yyyy-MM-dd'T'HH:mm'Z'"]!!.parse(date)
+ 19 -> formatters["yyyy-MM-dd'T'HH:mm:ss"]!!.parse(date)
+ 20 -> formatters["yyyy-MM-dd'T'HH:mm:ss'Z'"]!!.parse(date)
+ 23 -> formatters["yyyy-MM-dd'T'HH:mm:ss.SSS"]!!.parse(date)
+ 24 -> formatters["yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"]!!.parse(date)
+ else -> null
+ }
+ else ->
+ when (date.length) {
+ 10 -> formatters["yyyy-MM-dd"]!!.parse(date)
+ 16 -> formatters["yyyy-MM-dd HH:mm"]!!.parse(date)
+ 17 -> formatters["yyyy-MM-dd HH:mm'Z'"]!!.parse(date)
+ 19 -> formatters["yyyy-MM-dd HH:mm:ss"]!!.parse(date)
+ 20 -> formatters["yyyy-MM-dd HH:mm:ss'Z'"]!!.parse(date)
+ 23 -> formatters["yyyy-MM-dd HH:mm:ss.SSS"]!!.parse(date)
+ 24 -> formatters["yyyy-MM-dd HH:mm:ss.SSS'Z'"]!!.parse(date)
+ else -> null
+ }
+ } ?: throw KapperUnsupportedOperationException("Cannot convert $date to Date")
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/ConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/ConverterTest.kt
deleted file mode 100644
index 2efd2590..00000000
--- a/core/src/test/kotlin/net/samyn/kapper/internal/ConverterTest.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-package net.samyn.kapper.internal
-
-import io.kotest.assertions.throwables.shouldThrow
-import io.kotest.matchers.shouldBe
-import net.samyn.kapper.KapperParseException
-import net.samyn.kapper.KapperUnsupportedOperationException
-import org.junit.jupiter.api.Nested
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.ValueSource
-import java.nio.ByteBuffer
-import java.time.Instant
-import java.time.LocalDate
-import java.time.LocalDateTime
-import java.time.LocalTime
-import java.util.Date
-import java.util.UUID
-
-class ConverterTest {
- @Nested
- inner class UUIDConverter {
- @Test
- fun `when string UUID convert`() {
- val uuid = "123e4567-e89b-12d3-a456-426614174000"
- val autoConvertedUuid = convertUUID(uuid)
- autoConvertedUuid.shouldBe(UUID.fromString(uuid))
- }
-
- @Test
- fun `when binary UUID convert`() {
- val uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000")
- val autoConvertedUuid = convertUUID(uuid.asBytes())
- autoConvertedUuid.shouldBe(uuid)
- }
-
- @Test
- fun `when int cannot convert to UUID`() {
- shouldThrow {
- convertUUID(123)
- }
- }
-
- @ParameterizedTest
- @ValueSource(strings = ["", "123e4567-e89b-12d3-a456-426614ZZZZZZ"])
- fun `when invalid string cannot convert to UUID`(input: String) {
- shouldThrow {
- convertUUID(input)
- }
- }
- }
-
- @Nested
- inner class LocalDateConverter {
- @Test
- fun `convert valid Date to LocalDate`() {
- val date = Date.from(Instant.parse("2023-10-01T00:00:00Z"))
- val localDate = convertLocalDate(date)
- localDate.shouldBe(LocalDate.of(2023, 10, 1))
- }
-
- @Test
- fun `throw exception when invalid type is converted to LocalDate`() {
- shouldThrow {
- convertLocalDate("2023-01-01") // Invalid input type
- }
- }
- }
-
- @Nested
- inner class InstantConverter {
- @Test
- fun `convert valid LocalTime to Instant`() {
- val time = LocalTime.of(12, 30) // 12:30 PM
- val instant = convertInstant(time) as Instant
- val expectedInstant = LocalDate.now().atTime(12, 30).toInstant(java.time.ZoneOffset.UTC)
- instant.shouldBe(expectedInstant)
- }
-
- @Test
- fun `convert LocalTime at midnight to Instant`() {
- val midnight = LocalTime.MIDNIGHT
- val instant = convertInstant(midnight) as Instant
- val expectedInstant = LocalDate.now().atTime(0, 0).toInstant(java.time.ZoneOffset.UTC)
- instant.shouldBe(expectedInstant)
- }
-
- @Test
- fun `convert valid LocalDateTime to Instant`() {
- val now = LocalDateTime.now()
- val instant = convertInstant(now) as Instant
- val expectedInstant = now.toInstant(java.time.ZoneOffset.UTC)
- instant.shouldBe(expectedInstant)
- }
-
- @Test
- fun `throw exception when invalid type is converted to Instant`() {
- shouldThrow {
- convertInstant(Date()) // Invalid input type
- }
- }
- }
-
- @Nested
- inner class LocalDateTimeConverter {
- @Test
- fun `convert valid Instant to LocalDateTime`() {
- val now = Instant.now()
- val localDateTime = convertLocalDateTime(now) as LocalDateTime
- val expectedDt = LocalDateTime.ofInstant(now, java.time.ZoneOffset.UTC)
- localDateTime.shouldBe(expectedDt)
- }
-
- @Test
- fun `throw exception when invalid type is converted to LocalDateTime`() {
- shouldThrow {
- convertLocalDateTime(Date()) // Invalid input type
- }
- }
- }
-
- @Nested
- inner class LocalTimeConverter {
- @Test
- fun `convert valid Instant to LocalTime`() {
- val now = Instant.now()
- val locaTime = convertLocalTime(now) as LocalTime
- val expectedTime = LocalTime.ofInstant(now, java.time.ZoneOffset.UTC)
- locaTime.shouldBe(expectedTime)
- }
-
- @Test
- fun `throw exception when invalid type is converted to LocalTime`() {
- shouldThrow {
- convertLocalTime(Date()) // Invalid input type
- }
- }
- }
-
- private fun UUID.asBytes(): ByteArray {
- val b = ByteBuffer.wrap(ByteArray(16))
- b.putLong(mostSignificantBits)
- b.putLong(leastSignificantBits)
- return b.array()
- }
-}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/DbFlavourTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/DbFlavourTest.kt
index 0fc5370b..e8769af3 100644
--- a/core/src/test/kotlin/net/samyn/kapper/internal/DbFlavourTest.kt
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/DbFlavourTest.kt
@@ -66,13 +66,33 @@ class DbFlavourTest {
"Oracle Database 18c",
"Oracle Database 19c",
"Oracle Database 21c",
+ ],
+ )
+ fun `when databaseProductName is oracle then getDbFlavour returns ORACLE`(value: String) {
+ every { connection.metaData.databaseProductName } returns value
+ connection.getDbFlavour() shouldBe DbFlavour.ORACLE
+ }
+ @ParameterizedTest
+ @ValueSource(
+ strings =
+ [
// Microsoft SQL Server variants
"Microsoft SQL Server",
"SQL Server",
"MS SQL Server",
"MSSQL",
+ ],
+ )
+ fun `when databaseProductName is sql server then getDbFlavour returns MSSQLSERVER`(value: String) {
+ every { connection.metaData.databaseProductName } returns value
+ connection.getDbFlavour() shouldBe DbFlavour.MSSQLSERVER
+ }
+ @ParameterizedTest
+ @ValueSource(
+ strings =
+ [
// IBM DB2 variants
"DB2",
"DB2/NT",
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/KapperImplQueryTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/KapperImplQueryTest.kt
index 276c685d..0992aca3 100644
--- a/core/src/test/kotlin/net/samyn/kapper/internal/KapperImplQueryTest.kt
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/KapperImplQueryTest.kt
@@ -35,7 +35,7 @@ class KapperImplQueryTest {
private val mockMapper = mockk<(ResultSet, Map) -> TestEntity>(relaxed = true)
private val testFieldMeta =
mapOf(
- "id" to Field(1, JDBCType.INTEGER, "INTEGER"),
+ "id" to Field(1, JDBCType.INTEGER, "INTEGER", DbFlavour.UNKNOWN),
)
companion object {
@@ -52,7 +52,7 @@ class KapperImplQueryTest {
mockkStatic(ResultSet::extractFields)
every { mockConnection.executeQuery(any(), any()) } returns mockResultSet
every { mockConnection.getDbFlavour() } returns DbFlavour.UNKNOWN
- every { mockResultSet.extractFields() } returns testFieldMeta
+ every { mockResultSet.extractFields(any()) } returns testFieldMeta
every { mockMapper.invoke(any(), any()) } returns TestEntity(1, "test")
every { mockQueryBuilder(mockSqlTemplate) } returns mockQuery
}
@@ -120,6 +120,20 @@ class KapperImplQueryTest {
}.cause shouldBe ex
}
+ @Test
+ fun `when query blank throw`() {
+ shouldThrow {
+ kapper
+ .query(
+ TestEntity::class.java,
+ mockConnection,
+ "",
+ mockMapper,
+ mapOf("id" to 3),
+ )
+ }
+ }
+
@Test
fun `querySingle execute query`() {
every { mockResultSet.next() } returns true andThen false
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/KotlinDataClassMapperTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/KotlinDataClassMapperTest.kt
index 63c0b76e..8ae2191b 100644
--- a/core/src/test/kotlin/net/samyn/kapper/internal/KotlinDataClassMapperTest.kt
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/KotlinDataClassMapperTest.kt
@@ -19,7 +19,7 @@ import kotlin.reflect.KClass
class KotlinDataClassMapperTest {
private val autoMapperMock = mockk<(Any, KClass<*>) -> Any>(relaxed = true)
private val resultSet = mockk(relaxed = true)
- val sqlTypeConverterMock = mockk<(JDBCType, String, ResultSet, Int) -> Any?>(relaxed = true)
+ val sqlTypeConverterMock = mockk<(JDBCType, String, ResultSet, Int, DbFlavour) -> Any?>(relaxed = true)
@ParameterizedTest
@ValueSource(strings = ["email", "EMAIL", "eMail"])
@@ -27,18 +27,18 @@ class KotlinDataClassMapperTest {
val batman = SuperHero(UUID.randomUUID(), "Batman", "batman@dc.com", 85)
val fields =
mapOf(
- "id" to Field(1, JDBCType.BIT, "SomeType"),
- "name" to Field(2, JDBCType.BIT, "SomeType"),
- emailParam to Field(3, JDBCType.BIT, "SomeType"),
- "age" to Field(4, JDBCType.BIT, "SomeType"),
+ "id" to Field(1, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "name" to Field(2, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ emailParam to Field(3, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "age" to Field(4, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
)
- every { sqlTypeConverterMock(any(), any(), any(), eq(1)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(1), any()) } returns
batman.id
- every { sqlTypeConverterMock(any(), any(), any(), eq(2)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(2), any()) } returns
batman.name
- every { sqlTypeConverterMock(any(), any(), any(), eq(3)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(3), any()) } returns
batman.email!!
- every { sqlTypeConverterMock(any(), any(), any(), eq(4)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(4), any()) } returns
batman.age!!
val kotlinDataClassMapper = KotlinDataClassMapper(SuperHero::class.java, autoMapperMock, sqlTypeConverterMock)
@@ -52,12 +52,12 @@ class KotlinDataClassMapperTest {
val batman = SuperHero(UUID.randomUUID(), "Batman", "batman@dc.com", 85)
val fields =
mapOf(
- "id" to Field(1, JDBCType.BIT, "SomeType"),
- "name" to Field(2, JDBCType.BIT, "SomeType"),
+ "id" to Field(1, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "name" to Field(2, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
)
- every { sqlTypeConverterMock(any(), any(), any(), eq(1)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(1), any()) } returns
batman.id
- every { sqlTypeConverterMock(any(), any(), any(), eq(2)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(2), any()) } returns
batman.name
val kotlinDataClassMapper = KotlinDataClassMapper(SuperHero::class.java, autoMapperMock, sqlTypeConverterMock)
@@ -70,21 +70,21 @@ class KotlinDataClassMapperTest {
fun `should throw when too many columns`() {
val fields =
mapOf(
- "id" to Field(1, JDBCType.BIT, "SomeType"),
- "name" to Field(2, JDBCType.BIT, "SomeType"),
- "email" to Field(3, JDBCType.BIT, "SomeType"),
- "age" to Field(4, JDBCType.BIT, "SomeType"),
- "foo" to Field(5, JDBCType.BIT, "SomeType"),
+ "id" to Field(1, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "name" to Field(2, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "email" to Field(3, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "age" to Field(4, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "foo" to Field(5, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
)
- every { sqlTypeConverterMock(any(), any(), any(), eq(1)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(1), any()) } returns
UUID.randomUUID()
- every { sqlTypeConverterMock(any(), any(), any(), eq(2)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(2), any()) } returns
"joker"
- every { sqlTypeConverterMock(any(), any(), any(), eq(3)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(3), any()) } returns
"joker@dc.com"
- every { sqlTypeConverterMock(any(), any(), any(), eq(4)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(4), any()) } returns
85
- every { sqlTypeConverterMock(any(), any(), any(), eq(5)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(5), any()) } returns
"bar"
val kotlinDataClassMapper = KotlinDataClassMapper(SuperHero::class.java, autoMapperMock, sqlTypeConverterMock)
@@ -100,15 +100,15 @@ class KotlinDataClassMapperTest {
val batman = SuperHero(UUID.randomUUID(), "Batman", "batman@dc.com", 85)
val fields =
mapOf(
- "name" to Field(1, JDBCType.BIT, "SomeType"),
- "email" to Field(2, JDBCType.BIT, "SomeType"),
- "age" to Field(3, JDBCType.BIT, "SomeType"),
+ "name" to Field(1, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "email" to Field(2, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "age" to Field(3, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
)
- every { sqlTypeConverterMock(any(), any(), any(), eq(1)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(1), any()) } returns
batman.name
- every { sqlTypeConverterMock(any(), any(), any(), eq(2)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(2), any()) } returns
batman.email!!
- every { sqlTypeConverterMock(any(), any(), any(), eq(3)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(3), any()) } returns
batman.age!!
val kotlinDataClassMapper = KotlinDataClassMapper(SuperHero::class.java, autoMapperMock, sqlTypeConverterMock)
@@ -124,12 +124,12 @@ class KotlinDataClassMapperTest {
every { autoMapperMock(any(), any>()) } returns "Foo"
val fields =
mapOf(
- "id" to Field(1, JDBCType.BIT, "SomeType"),
- "name" to Field(2, JDBCType.BIT, "SomeType"),
+ "id" to Field(1, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "name" to Field(2, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
)
- every { sqlTypeConverterMock(any(), any(), any(), eq(1)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(1), any()) } returns
UUID.randomUUID()
- every { sqlTypeConverterMock(any(), any(), any(), eq(2)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(2), any()) } returns
123
val kotlinDataClassMapper = KotlinDataClassMapper(SuperHero::class.java, autoMapperMock, sqlTypeConverterMock)
@@ -142,15 +142,15 @@ class KotlinDataClassMapperTest {
val batman = SuperHero(UUID.randomUUID(), "Batman", "batman@dc.com", 85)
val fields =
mapOf(
- "id" to Field(1, JDBCType.BIT, "SomeType"),
- "name" to Field(2, JDBCType.BIT, "SomeType"),
- "email" to Field(3, JDBCType.BIT, "SomeType"),
+ "id" to Field(1, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "name" to Field(2, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "email" to Field(3, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
)
- every { sqlTypeConverterMock(any(), any(), any(), eq(1)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(1), any()) } returns
batman.id
- every { sqlTypeConverterMock(any(), any(), any(), eq(2)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(2), any()) } returns
batman.name
- every { sqlTypeConverterMock(any(), any(), any(), eq(3)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(3), any()) } returns
null
val kotlinDataClassMapper = KotlinDataClassMapper(SuperHero::class.java, autoMapperMock, sqlTypeConverterMock)
@@ -175,11 +175,11 @@ class KotlinDataClassMapperTest {
fun `should throw when no property found`() {
val fields =
mapOf(
- "id" to Field(1, JDBCType.BIT, "SomeType"),
- "name" to Field(2, JDBCType.BIT, "SomeType"),
- "foo" to Field(1, JDBCType.BIT, "SomeType"),
+ "id" to Field(1, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "name" to Field(2, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
+ "foo" to Field(1, JDBCType.BIT, "SomeType", DbFlavour.UNKNOWN),
)
- every { sqlTypeConverterMock(any(), any(), any(), eq(1)) } returns
+ every { sqlTypeConverterMock(any(), any(), any(), eq(1), any()) } returns
UUID.randomUUID()
val kotlinDataClassMapper = KotlinDataClassMapper(SuperHero::class.java, autoMapperMock, sqlTypeConverterMock)
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/MetadataTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/MetadataTest.kt
index d6e884e3..733ac00f 100644
--- a/core/src/test/kotlin/net/samyn/kapper/internal/MetadataTest.kt
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/MetadataTest.kt
@@ -1,6 +1,7 @@
package net.samyn.kapper.internal
import io.kotest.matchers.maps.shouldContainExactly
+import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import net.samyn.kapper.Field
@@ -28,12 +29,26 @@ class MetadataTest {
every { metaData } returns mockMetadata
}
- val fields = mockResultSet.extractFields()
+ val fields = mockResultSet.extractFields(DbFlavour.UNKNOWN)
fields.shouldContainExactly(
mapOf(
- "id" to Field(1, JDBCType.INTEGER, "INTEGER"),
- "name" to Field(2, JDBCType.VARCHAR, "VARCHAR"),
+ "id" to Field(1, JDBCType.INTEGER, "INTEGER", DbFlavour.UNKNOWN),
+ "name" to Field(2, JDBCType.VARCHAR, "VARCHAR", DbFlavour.UNKNOWN),
),
)
}
+
+ @Test
+ fun `when jdbcType returns JDBCType`() {
+ val jdbcType = JDBCType.INTEGER.vendorTypeNumber
+ val result = jdbcType.jdbcType()
+ result shouldBe JDBCType.INTEGER
+ }
+
+ @Test
+ fun `when jdbcType not known returns OTHER`() {
+ val jdbcType = 999
+ val result = jdbcType.jdbcType()
+ result shouldBe JDBCType.OTHER
+ }
}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/SQLTypesConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/SQLTypesConverterTest.kt
index fdbb0ab2..0e493333 100644
--- a/core/src/test/kotlin/net/samyn/kapper/internal/SQLTypesConverterTest.kt
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/SQLTypesConverterTest.kt
@@ -1,5 +1,6 @@
package net.samyn.kapper.internal
+import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
@@ -12,13 +13,19 @@ import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments.arguments
import org.junit.jupiter.params.provider.MethodSource
+import org.junit.jupiter.params.provider.ValueSource
import java.sql.JDBCType
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.sql.Time
import java.sql.Timestamp
+import java.text.SimpleDateFormat
import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
import java.time.LocalTime
+import java.time.ZoneId
+import java.time.ZoneOffset
import java.time.temporal.ChronoUnit
import java.util.Date
import java.util.UUID
@@ -52,6 +59,23 @@ class SQLTypesConverterTest {
named("DATE", Date.from(Instant.now())),
{ i: Int, v: Date -> statement.setDate(i, java.sql.Date(v.time)) },
),
+ arguments(
+ named("LOCALDATE", LocalDate.now()),
+ { i: Int, v: LocalDate -> statement.setDate(i, java.sql.Date.valueOf(v)) },
+ ),
+ arguments(
+ named("LOCALDATETIME", LocalDateTime.now()),
+ {
+ i: Int,
+ v: LocalDateTime,
+ ->
+ statement.setTimestamp(i, Timestamp.from(v.atZone(ZoneOffset.systemDefault()).toInstant()))
+ },
+ ),
+ arguments(
+ named("LOCALTIME", LocalTime.now()),
+ { i: Int, v: LocalTime -> statement.setTime(i, Time.valueOf(v)) },
+ ),
)
private val uuid = UUID.randomUUID()
@@ -66,6 +90,7 @@ class SQLTypesConverterTest {
DbFlavour.POSTGRESQL,
),
arguments(named("UUID", uuid), statement::setString, uuid.toString(), DbFlavour.MYSQL),
+ arguments(named("UUID", uuid), statement::setBytes, uuid.toBytes(), DbFlavour.ORACLE),
)
@JvmStatic
@@ -180,7 +205,7 @@ class SQLTypesConverterTest {
sqlTypeName: String,
expectedGetter: (Int) -> Any,
) {
- SQLTypesConverter.convertSQLType(jdbcType, sqlTypeName, resultSet, 1)
+ SQLTypesConverter.convertSQLType(jdbcType, sqlTypeName, resultSet, 1, DbFlavour.UNKNOWN)
verify { expectedGetter(1) }
}
@@ -188,60 +213,176 @@ class SQLTypesConverterTest {
@MethodSource("convertSQLTypeUnsupportedTests")
fun `unsupported SQL types throws`(jdbcType: JDBCType) {
assertThrows {
- SQLTypesConverter.convertSQLType(jdbcType, jdbcType.toString(), resultSet, 1)
+ SQLTypesConverter.convertSQLType(jdbcType, jdbcType.toString(), resultSet, 1, DbFlavour.UNKNOWN)
}
}
@Test
- fun `char needs truncating`() {
+ fun `char needs converting`() {
every { resultSet.getString(1) } returns "example"
- val result = SQLTypesConverter.convertSQLType(JDBCType.CHAR, "CHAR", resultSet, 1)
+ val result = SQLTypesConverter.convertSQLType(JDBCType.CHAR, "CHAR", resultSet, 1, DbFlavour.UNKNOWN)
- result.shouldBe('e')
+ result.shouldBe("example".toCharArray())
+ }
+
+ @Test
+ fun `char can return null`() {
+ every { resultSet.getString(1) } returns null
+ val result = SQLTypesConverter.convertSQLType(JDBCType.CHAR, "CHAR", resultSet, 1, DbFlavour.UNKNOWN)
+
+ result.shouldBe(null)
}
@Test
fun `uuid needs parsing`() {
val id = UUID.randomUUID()
every { resultSet.getString(2) } returns id.toString()
- val result = SQLTypesConverter.convertSQLType(JDBCType.OTHER, "UUID", resultSet, 2)
+ val result = SQLTypesConverter.convertSQLType(JDBCType.OTHER, "UUID", resultSet, 2, DbFlavour.UNKNOWN)
result.shouldBe(id)
}
+ @Test
+ fun `uuid supports null`() {
+ every { resultSet.getString(2) } returns null
+ val result = SQLTypesConverter.convertSQLType(JDBCType.OTHER, "UUID", resultSet, 2, DbFlavour.UNKNOWN)
+
+ result.shouldBe(null)
+ }
+
+ @Test
+ fun `binary_float needs parsing`() {
+ val f = 123.45F
+ every { resultSet.getFloat(2) } returns f
+ val result = SQLTypesConverter.convertSQLType(JDBCType.OTHER, "binary_float", resultSet, 2, DbFlavour.UNKNOWN)
+
+ result.shouldBe(f)
+ }
+
+ @Test
+ fun `binary_double needs parsing`() {
+ val d = 123.45
+ every { resultSet.getDouble(2) } returns d
+ val result = SQLTypesConverter.convertSQLType(JDBCType.OTHER, "binary_double", resultSet, 2, DbFlavour.UNKNOWN)
+
+ result.shouldBe(d)
+ }
+
@Test
fun `time needs converting`() {
val time = LocalTime.now().truncatedTo(ChronoUnit.SECONDS)
every { resultSet.getTime(3) } returns Time.valueOf(time)
- val result = SQLTypesConverter.convertSQLType(JDBCType.TIME, "time", resultSet, 3)
+ val result = SQLTypesConverter.convertSQLType(JDBCType.TIME, "time", resultSet, 3, DbFlavour.UNKNOWN)
result.shouldBe(time)
}
+ @Test
+ fun `time supports null`() {
+ every { resultSet.getTime(3) } returns null
+ val result = SQLTypesConverter.convertSQLType(JDBCType.TIME, "time", resultSet, 3, DbFlavour.UNKNOWN)
+
+ result.shouldBe(null)
+ }
+
@Test
fun `time with zone needs converting`() {
val time = LocalTime.now().truncatedTo(ChronoUnit.SECONDS)
every { resultSet.getTime(4) } returns Time.valueOf(time)
- val result = SQLTypesConverter.convertSQLType(JDBCType.TIME_WITH_TIMEZONE, "time", resultSet, 4)
+ val result = SQLTypesConverter.convertSQLType(JDBCType.TIME_WITH_TIMEZONE, "time", resultSet, 4, DbFlavour.UNKNOWN)
result.shouldBe(time)
}
@Test
- fun `timestamp needs converting`() {
- val timestamp = Instant.now()
- every { resultSet.getTimestamp(5) } returns Timestamp.from(timestamp)
- val result = SQLTypesConverter.convertSQLType(JDBCType.TIMESTAMP, "timestamp", resultSet, 5)
+ fun `time with zone supports null`() {
+ every { resultSet.getTime(4) } returns null
+ val result = SQLTypesConverter.convertSQLType(JDBCType.TIME_WITH_TIMEZONE, "time", resultSet, 4, DbFlavour.UNKNOWN)
- result.shouldBe(timestamp)
+ result.shouldBe(null)
}
@Test
- fun `timestamp with zone needs converting`() {
+ fun `timestamp as DATE needs converting`() {
val timestamp = Instant.now()
every { resultSet.getTimestamp(6) } returns Timestamp.from(timestamp)
- val result = SQLTypesConverter.convertSQLType(JDBCType.TIMESTAMP_WITH_TIMEZONE, "timestamp", resultSet, 6)
+ val result = SQLTypesConverter.convertSQLType(JDBCType.TIMESTAMP_WITH_TIMEZONE, "DATE", resultSet, 6, DbFlavour.UNKNOWN)
+
+ result.shouldBe(LocalDateTime.ofInstant(timestamp, ZoneId.systemDefault()))
+ }
+
+ @Test
+ fun `timestamp as DATE supports null`() {
+ every { resultSet.getTimestamp(6) } returns null
+ val result = SQLTypesConverter.convertSQLType(JDBCType.TIMESTAMP_WITH_TIMEZONE, "DATE", resultSet, 6, DbFlavour.UNKNOWN)
+
+ result.shouldBe(null)
+ }
+
+ @Test
+ fun `sqlite long date needs converting`() {
+ val date = Instant.now().toEpochMilli()
+ every { resultSet.getObject(7) } returns date
+ val result = SQLTypesConverter.convertSQLType(JDBCType.DATE, "DATE", resultSet, 7, DbFlavour.SQLITE)
+
+ result.shouldBe(Date(date))
+ }
+
+ @Test
+ fun `sqlite long date supports null`() {
+ every { resultSet.getObject(7) } returns null
+ val result = SQLTypesConverter.convertSQLType(JDBCType.DATE, "DATE", resultSet, 7, DbFlavour.SQLITE)
- result.shouldBe(timestamp)
+ result.shouldBe(null)
+ }
+
+ @ParameterizedTest
+ // https://sqlite.org/lang_datefunc.html#tmval
+ @ValueSource(
+ strings = [
+ "yyyy-MM-dd",
+ "yyyy-MM-dd HH:mm",
+ "yyyy-MM-dd HH:mm:ss",
+ "yyyy-MM-dd HH:mm:ss.SSS",
+ "yyyy-MM-dd'T'HH:mm",
+ "yyyy-MM-dd'T'HH:mm:ss",
+ "yyyy-MM-dd'T'HH:mm:ss.SSS",
+ "HH:mm",
+ "HH:mm:ss",
+ "HH:mm:ss.SSS",
+ "yyyy-MM-dd HH:mm'Z'",
+ "yyyy-MM-dd HH:mm:ss'Z'",
+ "yyyy-MM-dd HH:mm:ss.SSS'Z'",
+ "yyyy-MM-dd'T'HH:mm'Z'",
+ "yyyy-MM-dd'T'HH:mm:ss'Z'",
+ "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
+ "HH:mm'Z'",
+ "HH:mm:ss'Z'",
+ "HH:mm:ss.SSS'Z'",
+ ],
+ )
+ fun `sqlite string date needs converting`(format: String) {
+ val df = SimpleDateFormat(format)
+ val date = df.format(Date.from(Instant.now()))
+ every { resultSet.getObject(7) } returns date
+ val result = SQLTypesConverter.convertSQLType(JDBCType.DATE, "DATE", resultSet, 7, DbFlavour.SQLITE)
+
+ df.format(result).shouldBe(date)
+ }
+
+ @Test
+ fun `sqlite invalid date throws`() {
+ every { resultSet.getObject(7) } returns "Thu Aug 08 17:26:32 GMT+08:00 2013"
+ shouldThrow {
+ SQLTypesConverter.convertSQLType(JDBCType.DATE, "DATE", resultSet, 7, DbFlavour.SQLITE)
+ }
+ }
+
+ @Test
+ fun `sqlite invalid date type throws`() {
+ every { resultSet.getObject(7) } returns 123
+ shouldThrow {
+ SQLTypesConverter.convertSQLType(JDBCType.DATE, "DATE", resultSet, 7, DbFlavour.SQLITE)
+ }
}
}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/BooleanConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/BooleanConverterTest.kt
new file mode 100644
index 00000000..a56cf0f7
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/BooleanConverterTest.kt
@@ -0,0 +1,61 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertBoolean
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import java.util.Date
+import java.util.stream.Stream
+
+class BooleanConverterTest {
+ companion object {
+ @JvmStatic
+ fun booleanTrueValues(): Stream =
+ Stream.of(
+ Arguments.of(1),
+ Arguments.of(1F),
+ Arguments.of(1L),
+ Arguments.of(1.toByte()),
+ Arguments.of(1.toShort()),
+ Arguments.of("true"),
+ Arguments.of("1"),
+ )
+
+ @JvmStatic
+ fun booleanFalseValues(): Stream =
+ Stream.of(
+ Arguments.of(0),
+ Arguments.of(0F),
+ Arguments.of(0L),
+ Arguments.of(0.toByte()),
+ Arguments.of(0.toShort()),
+ Arguments.of("false"),
+ Arguments.of("0"),
+ )
+ }
+
+ @ParameterizedTest
+ @MethodSource("booleanTrueValues")
+ fun `convert valid true to Bool`(value: Any) {
+ val bool = convertBoolean(value)
+ bool.shouldBe(true)
+ }
+
+ @ParameterizedTest
+ @MethodSource("booleanFalseValues")
+ fun `convert valid false to Bool`(value: Any) {
+ val bool = convertBoolean(value)
+ bool.shouldBe(false)
+ }
+
+ @Test
+ fun `throw exception when invalid type is converted to Bool`() {
+ shouldThrow {
+ convertBoolean(Date()) // Invalid input type
+ }
+ }
+}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/CharConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/CharConverterTest.kt
new file mode 100644
index 00000000..03d694bc
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/CharConverterTest.kt
@@ -0,0 +1,63 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperParseException
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertChar
+import org.junit.jupiter.api.Test
+
+class CharConverterTest {
+ @Test
+ fun `convert valid String to Char`() {
+ val charString = "A"
+ val char = convertChar(charString)
+ char.shouldBe('A')
+ }
+
+ @Test
+ fun `String too short for Char`() {
+ val charString = ""
+ shouldThrow {
+ convertChar(charString)
+ }
+ }
+
+ @Test
+ fun `String too long for Char`() {
+ val charString = "AB"
+ shouldThrow {
+ convertChar(charString)
+ }
+ }
+
+ @Test
+ fun `convert valid CharArray to Char`() {
+ val charArray = charArrayOf('A')
+ val char = convertChar(charArray)
+ char.shouldBe('A')
+ }
+
+ @Test
+ fun `CharArray too short for Char`() {
+ val charArray = charArrayOf()
+ shouldThrow {
+ convertChar(charArray)
+ }
+ }
+
+ @Test
+ fun `CharArray too long for Char`() {
+ val charArray = charArrayOf('A', 'B')
+ shouldThrow {
+ convertChar(charArray)
+ }
+ }
+
+ @Test
+ fun `throw exception when invalid type is converted to Char`() {
+ shouldThrow {
+ convertChar(123) // Invalid input type
+ }
+ }
+}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/DateConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/DateConverterTest.kt
new file mode 100644
index 00000000..02433418
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/DateConverterTest.kt
@@ -0,0 +1,33 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertDate
+import org.junit.jupiter.api.Test
+import java.time.Instant
+import java.time.LocalDateTime
+import java.util.Date
+
+class DateConverterTest {
+ @Test
+ fun `convert valid Instant to Long`() {
+ val instant = Instant.now()
+ val dateValue = convertDate(instant)
+ dateValue.shouldBe(Date.from(instant))
+ }
+
+ @Test
+ fun `convert valid LocalDateTime to Long`() {
+ val instant = LocalDateTime.now()
+ val dateValue = convertDate(instant)
+ dateValue.shouldBe(Date.from(instant.atZone(java.time.ZoneOffset.systemDefault()).toInstant()))
+ }
+
+ @Test
+ fun `throw exception when invalid type is converted to Date`() {
+ shouldThrow {
+ convertDate("") // Invalid input type
+ }
+ }
+}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/InstantConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/InstantConverterTest.kt
new file mode 100644
index 00000000..16a3dac9
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/InstantConverterTest.kt
@@ -0,0 +1,45 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertInstant
+import org.junit.jupiter.api.Test
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.LocalTime
+import java.time.ZoneOffset
+import java.util.Date
+
+class InstantConverterTest {
+ @Test
+ fun `convert valid LocalTime to Instant`() {
+ val time = LocalTime.of(12, 30) // 12:30 PM
+ val instant = convertInstant(time) as Instant
+ val expectedInstant = LocalDate.now().atTime(12, 30).toInstant(ZoneOffset.UTC)
+ instant.shouldBe(expectedInstant)
+ }
+
+ @Test
+ fun `convert LocalTime at midnight to Instant`() {
+ val midnight = LocalTime.MIDNIGHT
+ val instant = convertInstant(midnight) as Instant
+ val expectedInstant = LocalDate.now().atTime(0, 0).toInstant(ZoneOffset.UTC)
+ instant.shouldBe(expectedInstant)
+ }
+
+ @Test
+ fun `convert valid LocalDateTime to Instant`() {
+ val now = Instant.now()
+ val instant = convertInstant(LocalDateTime.ofInstant(now, ZoneOffset.systemDefault())) as Instant
+ instant.shouldBe(now)
+ }
+
+ @Test
+ fun `throw exception when invalid type is converted to Instant`() {
+ shouldThrow {
+ convertInstant(Date()) // Invalid input type
+ }
+ }
+}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/IntConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/IntConverterTest.kt
new file mode 100644
index 00000000..51ec5b2c
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/IntConverterTest.kt
@@ -0,0 +1,24 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertInt
+import org.junit.jupiter.api.Test
+import java.util.Date
+
+class IntConverterTest {
+ @Test
+ fun `convert valid Float to Int`() {
+ val float = 123F
+ val intValue = convertInt(float)
+ intValue.shouldBe(123)
+ }
+
+ @Test
+ fun `throw exception when invalid type is converted to Int`() {
+ shouldThrow {
+ convertInt(Date()) // Invalid input type
+ }
+ }
+}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/LocalDateConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/LocalDateConverterTest.kt
new file mode 100644
index 00000000..7e87516c
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/LocalDateConverterTest.kt
@@ -0,0 +1,48 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertLocalDate
+import org.junit.jupiter.api.Test
+import java.time.Instant
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.util.Date
+
+class LocalDateConverterTest {
+ @Test
+ fun `convert valid Date to LocalDate`() {
+ val date = Date.from(Instant.parse("2023-10-01T00:00:00Z"))
+ val localDate = convertLocalDate(date)
+ localDate.shouldBe(LocalDate.of(2023, 10, 1))
+ }
+
+ @Test
+ fun `convert valid LocalDateTime to LocalDate`() {
+ val date = LocalDateTime.of(2023, 10, 1, 0, 0)
+ val localDate = convertLocalDate(date)
+ localDate.shouldBe(LocalDate.of(2023, 10, 1))
+ }
+
+ @Test
+ fun `convert valid Instant to LocalDate`() {
+ val date = Instant.parse("2023-10-01T00:00:00Z")
+ val localDate = convertLocalDate(date)
+ localDate.shouldBe(LocalDate.of(2023, 10, 1))
+ }
+
+ @Test
+ fun `convert valid string to LocalDate`() {
+ val date = "2023-10-01"
+ val localDate = convertLocalDate(date)
+ localDate.shouldBe(LocalDate.of(2023, 10, 1))
+ }
+
+ @Test
+ fun `throw exception when invalid type is converted to LocalDate`() {
+ shouldThrow {
+ convertLocalDate(2023) // Invalid input type
+ }
+ }
+}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/LocalDateTimeConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/LocalDateTimeConverterTest.kt
new file mode 100644
index 00000000..3fe0591b
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/LocalDateTimeConverterTest.kt
@@ -0,0 +1,35 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertLocalDateTime
+import org.junit.jupiter.api.Test
+import java.time.Instant
+import java.time.LocalDateTime
+import java.util.Date
+
+class LocalDateTimeConverterTest {
+ @Test
+ fun `convert valid Instant to LocalDateTime`() {
+ val now = Instant.now()
+ val localDateTime = convertLocalDateTime(now) as LocalDateTime
+ val expectedDt = LocalDateTime.ofInstant(now, java.time.ZoneOffset.systemDefault())
+ localDateTime.shouldBe(expectedDt)
+ }
+
+ @Test
+ fun `convert valid String to LocalDateTime`() {
+ val dateTimeString = "2023-10-01T12:30:00"
+ val localDateTime = convertLocalDateTime(dateTimeString) as LocalDateTime
+ val expectedDt = LocalDateTime.parse(dateTimeString)
+ localDateTime.shouldBe(expectedDt)
+ }
+
+ @Test
+ fun `throw exception when invalid type is converted to LocalDateTime`() {
+ shouldThrow {
+ convertLocalDateTime(Date()) // Invalid input type
+ }
+ }
+}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/LocalTimeConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/LocalTimeConverterTest.kt
new file mode 100644
index 00000000..ae9d7a4d
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/LocalTimeConverterTest.kt
@@ -0,0 +1,51 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertLocalTime
+import org.junit.jupiter.api.Test
+import java.time.Instant
+import java.time.LocalTime
+import java.time.ZoneOffset
+import java.util.Date
+
+class LocalTimeConverterTest {
+ @Test
+ fun `convert valid Instant to LocalTime`() {
+ val now = Instant.now()
+ val localTime = convertLocalTime(now)
+ val expectedTime = LocalTime.ofInstant(now, ZoneOffset.systemDefault())
+ localTime.shouldBe(expectedTime)
+ }
+
+ @Test
+ fun `convert valid String to LocalTime`() {
+ val timeString = "12:30:00"
+ val localTime = convertLocalTime(timeString)
+ val expectedTime = LocalTime.parse(timeString)
+ localTime.shouldBe(expectedTime)
+ }
+
+ @Test
+ fun `convert valid Long to LocalTime`() {
+ val now = Instant.now().truncatedTo(java.time.temporal.ChronoUnit.MILLIS)
+ val localTime = convertLocalTime(now.toEpochMilli())
+ val expectedTime = LocalTime.ofInstant(now, ZoneOffset.systemDefault())
+ localTime.shouldBe(expectedTime)
+ }
+
+ @Test
+ fun `convert valid Int to LocalTime`() {
+ val twoOclockPmUTC = 14 * 60 * 60 * 1000 // 14:00:00 - SQLite stores time as ms from 1970
+ val localTime = convertLocalTime(twoOclockPmUTC)
+ localTime.shouldBe(Instant.ofEpochMilli(twoOclockPmUTC.toLong()).atZone(ZoneOffset.systemDefault()).toLocalTime())
+ }
+
+ @Test
+ fun `throw exception when invalid type is converted to LocalTime`() {
+ shouldThrow {
+ convertLocalTime(Date()) // Invalid input type
+ }
+ }
+}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/LongConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/LongConverterTest.kt
new file mode 100644
index 00000000..def0d8d3
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/LongConverterTest.kt
@@ -0,0 +1,23 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertLong
+import org.junit.jupiter.api.Test
+
+class LongConverterTest {
+ @Test
+ fun `convert valid Float to Long`() {
+ val float = 123F
+ val longValue = convertLong(float)
+ longValue.shouldBe(123)
+ }
+
+ @Test
+ fun `throw exception when invalid type is converted to Long`() {
+ shouldThrow {
+ convertLong("") // Invalid input type
+ }
+ }
+}
diff --git a/core/src/test/kotlin/net/samyn/kapper/internal/converter/UUIDConverterTest.kt b/core/src/test/kotlin/net/samyn/kapper/internal/converter/UUIDConverterTest.kt
new file mode 100644
index 00000000..dc6bb148
--- /dev/null
+++ b/core/src/test/kotlin/net/samyn/kapper/internal/converter/UUIDConverterTest.kt
@@ -0,0 +1,57 @@
+package net.samyn.kapper.internal.converter
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import net.samyn.kapper.KapperParseException
+import net.samyn.kapper.KapperUnsupportedOperationException
+import net.samyn.kapper.internal.convertUUID
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import java.nio.ByteBuffer
+import java.util.UUID
+
+class UUIDConverterTest {
+ @Test
+ fun `when string UUID convert`() {
+ val uuid = "123e4567-e89b-12d3-a456-426614174000"
+ val autoConvertedUuid = convertUUID(uuid)
+ autoConvertedUuid.shouldBe(UUID.fromString(uuid))
+ }
+
+ @Test
+ fun `when char array UUID convert`() {
+ val uuid = "123e4567-e89b-12d3-a456-426614174000"
+ val autoConvertedUuid = convertUUID(uuid.toCharArray())
+ autoConvertedUuid.shouldBe(UUID.fromString(uuid))
+ }
+
+ @Test
+ fun `when binary UUID convert`() {
+ val uuid = UUID.fromString("123e4567-e89b-12d3-a456-426614174000")
+ val autoConvertedUuid = convertUUID(uuid.asBytes())
+ autoConvertedUuid.shouldBe(uuid)
+ }
+
+ @Test
+ fun `when int cannot convert to UUID`() {
+ shouldThrow {
+ convertUUID(123)
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["", "123e4567-e89b-12d3-a456-426614ZZZZZZ"])
+ fun `when invalid string cannot convert to UUID`(input: String) {
+ shouldThrow {
+ convertUUID(input)
+ }
+ }
+
+ private fun UUID.asBytes(): ByteArray {
+ val b = ByteBuffer.wrap(ByteArray(16))
+ b.putLong(mostSignificantBits)
+ b.putLong(leastSignificantBits)
+ return b.array()
+ }
+}
diff --git a/coroutines/src/main/kotlin/net/samyn/kapper/coroutines/KapperKotlinFlowQueryFun.kt b/coroutines/src/main/kotlin/net/samyn/kapper/coroutines/KapperKotlinFlowQueryFun.kt
index 4e530e26..2e714f3c 100644
--- a/coroutines/src/main/kotlin/net/samyn/kapper/coroutines/KapperKotlinFlowQueryFun.kt
+++ b/coroutines/src/main/kotlin/net/samyn/kapper/coroutines/KapperKotlinFlowQueryFun.kt
@@ -6,9 +6,11 @@ import kotlinx.coroutines.flow.flow
import net.samyn.kapper.Field
import net.samyn.kapper.KapperQueryException
import net.samyn.kapper.createMapper
+import net.samyn.kapper.internal.DbFlavour
import net.samyn.kapper.internal.Query
import net.samyn.kapper.internal.executeQuery
import net.samyn.kapper.internal.extractFields
+import net.samyn.kapper.internal.getDbFlavour
import net.samyn.kapper.internal.logger
import java.sql.Connection
import java.sql.ResultSet
@@ -82,7 +84,7 @@ inline fun Connection.queryAsFlow(
): Flow {
require(sql.isNotBlank()) { "SQL query cannot be empty or blank" }
this.executeQuery(Query(sql), args.toMap(), fetchSize).let { rs ->
- return queryFlow(rs, mapper, sql)
+ return queryFlow(rs, mapper, sql, this.getDbFlavour())
}
}
@@ -93,8 +95,9 @@ fun queryFlow(
rs: ResultSet,
mapper: (ResultSet, Map) -> T,
sql: String,
+ dbFlavour: DbFlavour,
): Flow {
- val fields = rs.extractFields()
+ val fields = rs.extractFields(dbFlavour)
return flow {
try {
while (rs.next()) {
diff --git a/coroutines/src/test/kotlin/net/samyn/kapper/coroutines/FlowQueryTest.kt b/coroutines/src/test/kotlin/net/samyn/kapper/coroutines/FlowQueryTest.kt
index 5cca524b..ae556af4 100644
--- a/coroutines/src/test/kotlin/net/samyn/kapper/coroutines/FlowQueryTest.kt
+++ b/coroutines/src/test/kotlin/net/samyn/kapper/coroutines/FlowQueryTest.kt
@@ -13,6 +13,7 @@ import kotlinx.coroutines.runBlocking
import net.samyn.kapper.Field
import net.samyn.kapper.KapperQueryException
import net.samyn.kapper.createMapper
+import net.samyn.kapper.internal.DbFlavour
import net.samyn.kapper.internal.Query
import net.samyn.kapper.internal.executeQuery
import net.samyn.kapper.internal.extractFields
@@ -24,8 +25,8 @@ import java.sql.SQLException
class FlowQueryTest {
private val fields =
mapOf(
- "id" to Field(1, java.sql.JDBCType.INTEGER, "id"),
- "name" to Field(1, java.sql.JDBCType.VARCHAR, "name"),
+ "id" to Field(1, java.sql.JDBCType.INTEGER, "id", DbFlavour.UNKNOWN),
+ "name" to Field(1, java.sql.JDBCType.VARCHAR, "name", DbFlavour.UNKNOWN),
)
private val result = Hero(1, "Superman")
private val resultSet =
@@ -41,7 +42,7 @@ class FlowQueryTest {
init {
mockkStatic(Connection::executeQuery)
mockkStatic(ResultSet::extractFields)
- every { resultSet.extractFields() } returns fields
+ every { resultSet.extractFields(any()) } returns fields
every { connection.executeQuery(any(), any(), any()) } returns resultSet
every { mapper.invoke(resultSet, fields) } returns result
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index febb826f..1ef64cbb 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,7 +8,9 @@ kotlin = "2.1.20"
kotlinx-coroutines = "1.10.2"
mockito = "5.17.0"
mockk = "1.14.0"
+mssql-server-driver = "12.10.0.jre11"
mysql-driver = "9.2.0"
+oracle-driver = "23.7.0.25.01"
postgresql-driver = "42.7.5"
slf4j = "2.0.17"
sqlite = "3.49.1.0"
@@ -25,14 +27,18 @@ kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-t
# mockito only to be used from Java code (API usability test)
mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
+mssql-server-driver = { module = "com.microsoft.sqlserver:mssql-jdbc", version.ref = "mssql-server-driver" }
mysql-driver = { module = "com.mysql:mysql-connector-j", version.ref = "mysql-driver" }
+oracle-driver = { module = "com.oracle.database.jdbc:ojdbc11", version.ref = "oracle-driver" }
postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql-driver" }
slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
test-containers = { module = "org.testcontainers:testcontainers", version.ref = "test-containers" }
test-containers-junit = { module = "org.testcontainers:junit-jupiter", version.ref = "test-containers" }
+test-containers-mssqlserver = { module = "org.testcontainers:mssqlserver", version.ref = "test-containers" }
test-containers-mysql = { module = "org.testcontainers:mysql", version.ref = "test-containers" }
+test-containers-oracle = { module = "org.testcontainers:oracle-free", version.ref = "test-containers" }
test-containers-postgresql = { module = "org.testcontainers:postgresql", version.ref = "test-containers" }
# PLUGINS
@@ -47,8 +53,14 @@ kover-plugin = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version =
[bundles]
jmh = ["jmh-core", "jmh-generator-annprocess"]
test = ["junit-jupiter", "mockk", "kotest-assertions-core"]
-test-containers = ["test-containers-junit", "test-containers-mysql", "test-containers-postgresql"]
-test-dbs = ["mysql-driver", "postgresql-driver"]
+test-containers = [
+ "test-containers-junit",
+ "test-containers-mysql",
+ "test-containers-postgresql",
+ "test-containers-mssqlserver",
+ "test-containers-oracle"
+]
+test-dbs = ["mysql-driver", "postgresql-driver", "sqlite-jdbc", "mssql-server-driver", "oracle-driver"]
[plugins]
sonar = { id = "org.sonarqube", version = "6.1.0.5360" }