Skip to content

Commit 9b8129a

Browse files
committed
feat: add DuckDB as a supported database
Add DuckDB as an embedded database alongside SQLite. Includes a workaround for the JDBC driver's TIME type timezone offset issue by using string serialization.
1 parent 51081ab commit 9b8129a

7 files changed

Lines changed: 32 additions & 5 deletions

File tree

.github/workflows/build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
- build
8181
strategy:
8282
matrix:
83-
db: [ POSTGRESQL, MYSQL, SQLITE, MSSQLSERVER, ORACLE ]
83+
db: [ POSTGRESQL, MYSQL, SQLITE, MSSQLSERVER, ORACLE, DUCKDB ]
8484
steps:
8585
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
8686
with:

core/src/integrationTest/kotlin/net/samyn/kapper/AbstractDbTests.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ abstract class AbstractDbTests {
3434
companion object {
3535
init {
3636
Class.forName("org.sqlite.JDBC")
37+
Class.forName("org.duckdb.DuckDBDriver")
3738
}
3839

3940
private val postgresql by lazy {
@@ -74,13 +75,14 @@ abstract class AbstractDbTests {
7475
DbFlavour.POSTGRESQL to { getConnection(postgresql) },
7576
DbFlavour.MYSQL to { getConnection(mysql) },
7677
DbFlavour.SQLITE to { DriverManager.getConnection("jdbc:sqlite::memory:") },
78+
DbFlavour.DUCKDB to { DriverManager.getConnection("jdbc:duckdb:") },
7779
DbFlavour.MSSQLSERVER to { getConnection(msSqlServer) },
7880
DbFlavour.ORACLE to { getConnection(oracle) },
7981
).filter {
8082
// by default run against SQLite and PG only
8183
// this allows parallel runs for different int tests.
8284
when (System.getProperty("db", "").uppercase()) {
83-
"" -> it.key == DbFlavour.SQLITE || it.key == DbFlavour.POSTGRESQL
85+
"" -> it.key == DbFlavour.SQLITE || it.key == DbFlavour.POSTGRESQL || it.key == DbFlavour.DUCKDB
8486
"ALL" -> true
8587
else -> it.key == DbFlavour.valueOf(System.getProperty("db").uppercase())
8688
}

core/src/integrationTest/kotlin/net/samyn/kapper/DbTypeConverter.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,19 @@ private val specialTypes =
1515
DbFlavour.MYSQL to "TEXT",
1616
DbFlavour.POSTGRESQL to "TEXT",
1717
DbFlavour.MSSQLSERVER to "NVARCHAR(MAX)",
18+
DbFlavour.DUCKDB to "VARCHAR",
1819
),
1920
"BINARY" to
2021
mapOf(
2122
DbFlavour.POSTGRESQL to "BYTEA",
2223
DbFlavour.ORACLE to "RAW(128)",
24+
DbFlavour.DUCKDB to "BLOB",
2325
),
2426
"VARBINARY" to
2527
mapOf(
2628
DbFlavour.POSTGRESQL to "BYTEA",
2729
DbFlavour.ORACLE to "RAW(128)",
30+
DbFlavour.DUCKDB to "BLOB",
2831
),
2932
"BLOB" to
3033
mapOf(

core/src/main/kotlin/net/samyn/kapper/DbFlavour.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ enum class DbFlavour {
66
SQLITE,
77
ORACLE,
88
MSSQLSERVER,
9+
DUCKDB,
910
UNKNOWN,
1011
}

core/src/main/kotlin/net/samyn/kapper/internal/DbFlavourFunc.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ fun Connection.getDbFlavour(): DbFlavour {
1515
productName.contains("oracle", ignoreCase = true) -> DbFlavour.ORACLE
1616
productName.contains("sql server", ignoreCase = true) ||
1717
productName.contains("mssql", ignoreCase = true) -> DbFlavour.MSSQLSERVER
18+
productName.contains("duckdb", ignoreCase = true) -> DbFlavour.DUCKDB
1819
else -> DbFlavour.UNKNOWN
1920
}
2021
}

core/src/main/kotlin/net/samyn/kapper/internal/automapper/SQLTypesConverter.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ val sqlTypesConverter =
6767
JDBCType.JAVA_OBJECT,
6868
-> resultSet.getObject(field.columnIndex)
6969

70-
in TIME_TYPES -> resultSet.getTime(field.columnIndex)?.toLocalTime()
70+
in TIME_TYPES -> convertTime(resultSet, field.columnIndex, field.dbFlavour)
7171

7272
in TIMESTAMP_TYPES -> convertTimestamp(resultSet, field.columnIndex, field.typeName)
7373

@@ -163,6 +163,18 @@ fun ResultSet.getNullableDouble(columnIndex: Int): Double? {
163163
return dbValue
164164
}
165165

166+
fun convertTime(
167+
resultSet: ResultSet,
168+
fieldIndex: Int,
169+
dbFlavour: DbFlavour,
170+
): LocalTime? =
171+
when (dbFlavour) {
172+
// DuckDB's JDBC driver stores TIME in UTC; using getTime() with the default
173+
// calendar applies a timezone offset. Reading as String avoids this.
174+
DbFlavour.DUCKDB -> resultSet.getString(fieldIndex)?.let { LocalTime.parse(it) }
175+
else -> resultSet.getTime(fieldIndex)?.toLocalTime()
176+
}
177+
166178
fun convertDecimal(
167179
resultSet: ResultSet,
168180
fieldIndex: Int,
@@ -217,7 +229,13 @@ fun PreparedStatement.setParameter(
217229
is Date -> setDate(index, java.sql.Date(value.time))
218230
is LocalDate -> setDate(index, java.sql.Date.valueOf(value))
219231
is LocalDateTime -> setTimestamp(index, Timestamp.from(value.atZone(java.time.ZoneOffset.systemDefault()).toInstant()))
220-
is LocalTime -> setTime(index, java.sql.Time.valueOf(value))
232+
is LocalTime ->
233+
when (dbFlavour) {
234+
// DuckDB's JDBC driver applies a timezone offset with setTime/getTime;
235+
// using setString avoids the conversion and preserves the intended value.
236+
DbFlavour.DUCKDB -> setString(index, value.toString())
237+
else -> setTime(index, java.sql.Time.valueOf(value))
238+
}
221239
else -> setObject(index, value)
222240
}
223241
}

gradle/libs.versions.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mysql-driver = "9.6.0"
1313
oracle-driver = "23.26.0.0.0"
1414
postgresql-driver = "42.7.8"
1515
slf4j = "2.0.17"
16+
duckdb = "1.2.2.0"
1617
sqlite = "3.51.2.0"
1718
test-containers = "1.21.4"
1819

@@ -39,6 +40,7 @@ oracle-driver = { module = "com.oracle.database.jdbc:ojdbc11", version.ref = "or
3940
postgresql-driver = { module = "org.postgresql:postgresql", version.ref = "postgresql-driver" }
4041
slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
4142
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
43+
duckdb-jdbc = { module = "org.duckdb:duckdb_jdbc", version.ref = "duckdb" }
4244
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite" }
4345
test-containers = { module = "org.testcontainers:testcontainers", version.ref = "test-containers" }
4446
test-containers-junit = { module = "org.testcontainers:junit-jupiter", version.ref = "test-containers" }
@@ -79,7 +81,7 @@ test-containers = [
7981
"test-containers-mssqlserver",
8082
"test-containers-oracle"
8183
]
82-
test-dbs = ["mysql-driver", "postgresql-driver", "sqlite-jdbc", "mssql-server-driver", "oracle-driver"]
84+
test-dbs = ["mysql-driver", "postgresql-driver", "sqlite-jdbc", "duckdb-jdbc", "mssql-server-driver", "oracle-driver"]
8385

8486
# Example bundles
8587
hibernate = ["hibernate-core", "hibernate-validator", "glassfish-jakarta"]

0 commit comments

Comments
 (0)