diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 64c2228..f60088c 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -11,3 +11,8 @@ dependencyResolutionManagement { } rootProject.name = "build-logic" + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 461dc19..4344c20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,26 +1,19 @@ [versions] -grpc-java = "1.64.0" -grpc-kotlin = "1.4.1" -kotlin-core = "2.0.0" +kotlin-core = "1.9.24" +kotlin-coroutines = "1.8.1" ktlint = "1.2.1" -protobuf = "3.25.3" +spring-data = "3.3.0" [libraries] assertk = { module = "com.willowtreeapps.assertk:assertk-jvm", version = "0.28.1" } -grpc-java-protobuf = { module = "io.grpc:grpc-protobuf", version.ref = "grpc-java" } -grpc-java-stub = { module = "io.grpc:grpc-stub", version.ref = "grpc-java" } -grpc-kotlin-stub = { module = "io.grpc:grpc-kotlin-stub", version.ref = "grpc-kotlin" } junit-bom = { module = "org.junit:junit-bom", version = "5.10.2" } -kotlinpoet = { module = "com.squareup:kotlinpoet", version = "1.17.0" } +kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" } +kotlin-coroutines-reactor = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactor", version.ref = "kotlin-coroutines" } mockk-core = { module = "io.mockk:mockk", version = "1.13.11" } -protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } -protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" } +spring-data-jdbc = { module = "org.springframework.data:spring-data-jdbc", version.ref = "spring-data" } +spring-data-r2dbc = { module = "org.springframework.data:spring-data-r2dbc", version.ref = "spring-data" } # gradle plugins for build-logic gradle-plugin-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version = "1.23.6" } gradle-plugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin-core" } gradle-plugin-ktlint = { module = "org.jlleitschuh.gradle:ktlint-gradle", version = "12.1.1" } - -[plugins] -protobuf = { id = "com.google.protobuf", version = "0.9.4" } -sonatype-central-upload = { id = "cl.franciscosolis.sonatype-central-upload", version = "1.0.3" } diff --git a/kuery-client-core/build.gradle.kts b/kuery-client-core/build.gradle.kts index 7410ed2..b072ab6 100644 --- a/kuery-client-core/build.gradle.kts +++ b/kuery-client-core/build.gradle.kts @@ -3,3 +3,7 @@ plugins { id("conventions.ktlint") id("conventions.detekt") } + +dependencies { + compileOnly(libs.kotlin.coroutines.core) +} diff --git a/kuery-client-core/src/main/kotlin/dev/hsbrysk/kuery/core/KueryClient.kt b/kuery-client-core/src/main/kotlin/dev/hsbrysk/kuery/core/KueryClient.kt new file mode 100644 index 0000000..7f65f43 --- /dev/null +++ b/kuery-client-core/src/main/kotlin/dev/hsbrysk/kuery/core/KueryClient.kt @@ -0,0 +1,22 @@ +package dev.hsbrysk.kuery.core + +import kotlinx.coroutines.flow.Flow +import kotlin.reflect.KClass + +interface KueryClient { + fun sql(block: SqlDsl.() -> Unit): KueryFetchSpec +} + +interface KueryFetchSpec { + suspend fun single(returnType: KClass): T + + suspend fun singleOrNull(returnType: KClass): T? + + suspend fun list(returnType: KClass): List + + fun flow(returnType: KClass): Flow + + suspend fun rowsUpdated(): Long + + suspend fun generatedValues(vararg columns: String): Map +} diff --git a/kuery-client-core/src/main/kotlin/dev/hsbrysk/kuery/core/SqlDsl.kt b/kuery-client-core/src/main/kotlin/dev/hsbrysk/kuery/core/SqlDsl.kt index 52813c7..d501e5b 100644 --- a/kuery-client-core/src/main/kotlin/dev/hsbrysk/kuery/core/SqlDsl.kt +++ b/kuery-client-core/src/main/kotlin/dev/hsbrysk/kuery/core/SqlDsl.kt @@ -27,3 +27,14 @@ interface SqlDsl { inline fun SqlDsl.bind(value: T?): String { return bind(value, T::class) } + +private val NUMBER_REGEX = "^[0-9]+$".toRegex() + +fun (SqlDsl.() -> Unit).id(): String { + val parts = this.javaClass.name.split("$").filterNot { it.matches(NUMBER_REGEX) } + return if (parts.isEmpty()) { + "UNKNOWN" + } else { + parts.joinToString(".") + } +} diff --git a/kuery-client-spring-data-jdbc/build.gradle.kts b/kuery-client-spring-data-jdbc/build.gradle.kts new file mode 100644 index 0000000..82a80af --- /dev/null +++ b/kuery-client-spring-data-jdbc/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("conventions.kotlin") + id("conventions.ktlint") + id("conventions.detekt") +} + +dependencies { + implementation(libs.spring.data.jdbc) +} diff --git a/kuery-client-spring-data-r2dbc/build.gradle.kts b/kuery-client-spring-data-r2dbc/build.gradle.kts new file mode 100644 index 0000000..c3aa2ae --- /dev/null +++ b/kuery-client-spring-data-r2dbc/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("conventions.kotlin") + id("conventions.ktlint") + id("conventions.detekt") +} + +dependencies { + implementation(projects.kueryClientCore) + implementation(libs.spring.data.r2dbc) + implementation(libs.kotlin.coroutines.core) + implementation(libs.kotlin.coroutines.reactor) +} diff --git a/kuery-client-spring-data-r2dbc/src/main/kotlin/dev/hsbrysk/kuery/spring/r2dbc/DatabaseClientExtensions.kt b/kuery-client-spring-data-r2dbc/src/main/kotlin/dev/hsbrysk/kuery/spring/r2dbc/DatabaseClientExtensions.kt new file mode 100644 index 0000000..d37ea1a --- /dev/null +++ b/kuery-client-spring-data-r2dbc/src/main/kotlin/dev/hsbrysk/kuery/spring/r2dbc/DatabaseClientExtensions.kt @@ -0,0 +1,17 @@ +package dev.hsbrysk.kuery.spring.r2dbc + +import dev.hsbrysk.kuery.core.Sql +import dev.hsbrysk.kuery.core.SqlDsl +import org.springframework.r2dbc.core.DatabaseClient + +fun DatabaseClient.sql(block: SqlDsl.() -> Unit): DatabaseClient.GenericExecuteSpec { + val sql = Sql.create(block) + @Suppress("SqlSourceToSinkFlow") + return sql.parameters.fold(this.sql(sql.body)) { acc, parameter -> + if (parameter.value != null) { + acc.bindNull(parameter.name, parameter.kClass.java) + } else { + acc.bind(parameter.name, checkNotNull(parameter.value)) + } + } +} diff --git a/kuery-client-spring-data-r2dbc/src/main/kotlin/dev/hsbrysk/kuery/spring/r2dbc/SpringR2dbcKueryClient.kt b/kuery-client-spring-data-r2dbc/src/main/kotlin/dev/hsbrysk/kuery/spring/r2dbc/SpringR2dbcKueryClient.kt new file mode 100644 index 0000000..2a654b3 --- /dev/null +++ b/kuery-client-spring-data-r2dbc/src/main/kotlin/dev/hsbrysk/kuery/spring/r2dbc/SpringR2dbcKueryClient.kt @@ -0,0 +1,73 @@ +package dev.hsbrysk.kuery.spring.r2dbc + +import dev.hsbrysk.kuery.core.KueryClient +import dev.hsbrysk.kuery.core.KueryFetchSpec +import dev.hsbrysk.kuery.core.SqlDsl +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.reactor.awaitSingle +import org.springframework.data.r2dbc.convert.EntityRowMapper +import org.springframework.data.r2dbc.core.R2dbcEntityOperations +import org.springframework.r2dbc.core.DatabaseClient.GenericExecuteSpec +import org.springframework.r2dbc.core.RowsFetchSpec +import org.springframework.r2dbc.core.awaitOne +import org.springframework.r2dbc.core.awaitOneOrNull +import org.springframework.r2dbc.core.awaitRowsUpdated +import org.springframework.r2dbc.core.flow +import java.util.function.Function +import kotlin.reflect.KClass + +class SpringR2dbcKueryClient( + private val operations: R2dbcEntityOperations, +) : KueryClient { + override fun sql(block: SqlDsl.() -> Unit): KueryFetchSpec { + return SpringR2dbcKueryFetchSpec(operations, block) + } +} + +class SpringR2dbcKueryFetchSpec( + private val operations: R2dbcEntityOperations, + private val block: SqlDsl.() -> Unit, +) : KueryFetchSpec { + override suspend fun single(returnType: KClass): T { + return operations.databaseClient.sql(block) + .map(returnType) + .awaitOne() + } + + override suspend fun singleOrNull(returnType: KClass): T? { + return operations.databaseClient.sql(block) + .map(returnType) + .awaitOneOrNull() + } + + override suspend fun list(returnType: KClass): List { + return operations.databaseClient.sql(block) + .map(returnType) + .all() + .collectList() + .awaitSingle() + } + + override fun flow(returnType: KClass): Flow { + return operations.databaseClient.sql(block) + .map(returnType) + .flow() + } + + override suspend fun rowsUpdated(): Long { + return operations.databaseClient.sql(block) + .fetch() + .awaitRowsUpdated() + } + + override suspend fun generatedValues(vararg columns: String): Map { + return operations.databaseClient.sql(block) + .filter(Function { it.returnGeneratedValues(*columns) }) + .fetch() + .awaitOne() + } + + private fun GenericExecuteSpec.map(returnType: KClass): RowsFetchSpec { + return this.map(EntityRowMapper(returnType.java, operations.converter)) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7195a2f..ead0dfb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,13 @@ dependencyResolutionManagement { rootProject.name = "kuery-client" -include("kuery-client-core") +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} -include("tmp") +include("kuery-client-core") +include("kuery-client-spring-data-jdbc") +include("kuery-client-spring-data-r2dbc") diff --git a/tmp/build.gradle.kts b/tmp/build.gradle.kts deleted file mode 100644 index 8b01cb6..0000000 --- a/tmp/build.gradle.kts +++ /dev/null @@ -1,10 +0,0 @@ -plugins { - id("conventions.kotlin") - id("conventions.ktlint") - id("conventions.detekt") -} - -dependencies { - implementation("org.springframework.data:spring-data-r2dbc:3.3.0") - implementation("org.springframework.data:spring-data-jdbc:3.3.0") -} diff --git a/tmp/src/main/kotlin/Tmp.kt b/tmp/src/main/kotlin/Tmp.kt deleted file mode 100644 index 9635e34..0000000 --- a/tmp/src/main/kotlin/Tmp.kt +++ /dev/null @@ -1,13 +0,0 @@ -@file:Suppress("UNREACHABLE_CODE") - -import org.springframework.jdbc.core.simple.JdbcClient -import org.springframework.r2dbc.core.DatabaseClient -import java.util.function.Function - -fun main() { - val r2dbcClient: DatabaseClient = checkNotNull(null) - r2dbcClient.sql("").filter(Function { it.returnGeneratedValues() }) - - val jdbcClient: JdbcClient = checkNotNull(null) - jdbcClient.sql("").param("", null) -}