├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── kotysa-core ├── src │ ├── test │ │ ├── resources │ │ │ ├── junit-platform.properties │ │ │ └── logback.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── pullvert │ │ │ └── kotysa │ │ │ └── sample │ │ │ ├── h2Tables.kt │ │ │ ├── sqLiteTables.kt │ │ │ └── postgresqlTables.kt │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── pullvert │ │ │ └── kotysa │ │ │ ├── Nullable.kt │ │ │ ├── KotysaMarker.kt │ │ │ ├── DbType.kt │ │ │ ├── Exceptions.kt │ │ │ ├── PrimaryKey.kt │ │ │ ├── ForeignKey.kt │ │ │ ├── Reflection.kt │ │ │ ├── JoinClause.kt │ │ │ ├── JoinDsl.kt │ │ │ ├── SelectDslApi.kt │ │ │ ├── WhereClause.kt │ │ │ ├── SqlType.kt │ │ │ ├── postgresql │ │ │ ├── PostgresqlDefaultSqlClientDeleteOrUpdate.kt │ │ │ ├── PostgresqlTablesDsl.kt │ │ │ ├── PostgresqlTableDsl.kt │ │ │ └── PostgresqlColumnDsl.kt │ │ │ ├── h2 │ │ │ ├── H2TablesDsl.kt │ │ │ ├── H2TableDsl.kt │ │ │ ├── H2DefaultSqlClientDeleteOrUpdate.kt │ │ │ ├── H2Column.kt │ │ │ ├── H2ColumnBuilder.kt │ │ │ └── H2ColumnDsl.kt │ │ │ ├── sqlite │ │ │ ├── SqLiteTablesDsl.kt │ │ │ ├── SqLiteTableDsl.kt │ │ │ ├── SqLiteDefaultSqlClient.kt │ │ │ └── SqLiteColumnDsl.kt │ │ │ ├── AliasedTable.kt │ │ │ ├── Table.kt │ │ │ ├── FieldSetter.kt │ │ │ ├── DbTypeChoice.kt │ │ │ ├── TableDsl.kt │ │ │ ├── TableColumnPropertyProvider.kt │ │ │ ├── ValueProvider.kt │ │ │ ├── TablesDsl.kt │ │ │ ├── FieldAccess.kt │ │ │ ├── ColumnDsl.kt │ │ │ ├── UpdateSetDsl.kt │ │ │ └── AbstractRow.kt │ └── testFixtures │ │ ├── java │ │ └── com │ │ │ └── pullvert │ │ │ └── kotysa │ │ │ └── test │ │ │ ├── Entity.java │ │ │ └── JavaUser.java │ │ └── kotlin │ │ └── com │ │ └── pullvert │ │ └── kotysa │ │ └── test │ │ └── CommonTestEntities.kt └── build.gradle ├── kotysa-spring-data-r2dbc ├── src │ ├── test │ │ ├── resources │ │ │ ├── application.properties │ │ │ ├── junit-platform.properties │ │ │ └── logback.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── pullvert │ │ │ └── kotysa │ │ │ └── r2dbc │ │ │ ├── Repository.kt │ │ │ ├── h2 │ │ │ ├── R2DbcSelectAndH2Test.kt │ │ │ ├── R2DbcSelectOrH2Test.kt │ │ │ ├── AbstractR2dbcH2Test.kt │ │ │ ├── R2DbcSelectBooleanH2Test.kt │ │ │ ├── AbstractUserRepositoryH2.kt │ │ │ ├── R2DbcSelectH2Test.kt │ │ │ ├── R2DbcInheritanceH2Test.kt │ │ │ └── R2DbcSelectUuidH2Test.kt │ │ │ ├── postgresql │ │ │ ├── R2DbcSelectOrPostgresqlTest.kt │ │ │ ├── R2DbcSelectBooleanPostgresqlTest.kt │ │ │ ├── AbstractUserRepositoryPostgresql.kt │ │ │ ├── AbstractR2dbcPostgresqlTest.kt │ │ │ ├── R2DbcSelectPostgresqlTest.kt │ │ │ └── R2DbcInheritancePostgresqlTest.kt │ │ │ └── sample │ │ │ ├── coroutinesSqlClientR2dbc.kt │ │ │ └── sqlClientR2dbc.kt │ └── main │ │ └── kotlin │ │ └── com │ │ └── pullvert │ │ └── kotysa │ │ └── r2dbc │ │ ├── TransactionStatus.kt │ │ ├── ReactorTransactionalOperation.kt │ │ ├── TransactionStatusR2dbc.kt │ │ ├── TransactionalOperationR2dbc.kt │ │ ├── AbstractSqlClientDeleteR2dbc.kt │ │ ├── AbstractSqlClientSelectR2dbc.kt │ │ ├── AbstractSqlClientR2dbc.kt │ │ ├── AbstractSqlClientUpdateR2dbc.kt │ │ ├── CoroutinesSqlClientR2dbc.kt │ │ ├── SqlClientR2dbc.kt │ │ ├── SqlClientSelectR2dbc.kt │ │ └── SqlClientDeleteR2dbc.kt ├── README.md └── build.gradle ├── samples ├── kotysa-coroutines-r2dbc │ ├── src │ │ ├── test │ │ │ ├── resources │ │ │ │ ├── junit-platform.properties │ │ │ │ └── logback.xml │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── sample │ │ │ │ ├── UserRepositoryTests.kt │ │ │ │ └── IntegrationTests.kt │ │ └── main │ │ │ └── kotlin │ │ │ └── com │ │ │ └── sample │ │ │ ├── Routes.kt │ │ │ ├── Application.kt │ │ │ ├── Handlers.kt │ │ │ ├── Model.kt │ │ │ ├── Repositories.kt │ │ │ └── Configurations.kt │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ └── gradlew.bat ├── kotysa-reactive-r2dbc │ ├── src │ │ ├── test │ │ │ ├── resources │ │ │ │ ├── junit-platform.properties │ │ │ │ └── logback.xml │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── sample │ │ │ │ ├── UserRepositoryTests.kt │ │ │ │ └── IntegrationTests.kt │ │ └── main │ │ │ └── kotlin │ │ │ └── com │ │ │ └── sample │ │ │ ├── Routes.kt │ │ │ ├── Application.kt │ │ │ ├── Model.kt │ │ │ ├── Handlers.kt │ │ │ ├── Repositories.kt │ │ │ └── Configurations.kt │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ └── gradlew.bat └── README.md ├── kotysa-android ├── src │ ├── test │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── pullvert │ │ │ │ └── kotysa │ │ │ │ └── android │ │ │ │ ├── Repository.kt │ │ │ │ ├── DbHelper.kt │ │ │ │ ├── AbstractSqLiteTest.kt │ │ │ │ ├── SqLiteSelectOrTest.kt │ │ │ │ ├── SqLiteSelectBooleanTest.kt │ │ │ │ ├── AbstractUserRepository.kt │ │ │ │ ├── SqLiteSelectTest.kt │ │ │ │ ├── sample │ │ │ │ └── sqlClientSqLite.kt │ │ │ │ └── SqLiteUpdateDeleteTest.kt │ │ └── resources │ │ │ └── logback.xml │ └── main │ │ └── kotlin │ │ └── com │ │ └── pullvert │ │ └── kotysa │ │ └── android │ │ └── SqlClientSqLite.kt ├── README.md └── build.gradle ├── RELEASE.md ├── .gitignore ├── settings.gradle ├── gradle.properties ├── kotysa-platform └── build.gradle ├── UNLICENSE ├── docs └── sql-queries.md ├── README.md └── gradlew.bat /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pull-vert/kotysa/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /kotysa-core/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.testinstance.lifecycle.default=per_class 2 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | #logging.level.org.springframework.data.r2dbc=DEBUG 2 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.testinstance.lifecycle.default=per_class 2 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.testinstance.lifecycle.default = per_class -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.testinstance.lifecycle.default = per_class -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pull-vert/kotysa/HEAD/samples/kotysa-reactive-r2dbc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pull-vert/kotysa/HEAD/samples/kotysa-coroutines-r2dbc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven { url "https://repo.spring.io/milestone" } 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven { url "https://repo.spring.io/milestone" } 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/Nullable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | 8 | public enum class Nullable { 9 | TRUE 10 | } 11 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/KotysaMarker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | 8 | @DslMarker 9 | internal annotation class KotysaMarker 10 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/main/kotlin/com/sample/Routes.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import org.springframework.web.reactive.function.server.router 4 | 5 | fun routes(userHandler: UserHandler) = router { 6 | GET("/api/user", userHandler::listApi) 7 | GET("/api/user/{id}", userHandler::userApi) 8 | } 9 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/main/kotlin/com/sample/Routes.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import org.springframework.web.reactive.function.server.coRouter 4 | 5 | fun routes(userHandler: UserHandler) = coRouter { 6 | GET("/api/user", userHandler::listApi) 7 | GET("/api/user/{id}", userHandler::userApi) 8 | } 9 | -------------------------------------------------------------------------------- /kotysa-android/src/test/kotlin/com/pullvert/kotysa/android/Repository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android 6 | 7 | interface Repository { 8 | fun init() 9 | fun delete() 10 | } 11 | -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # Samples 2 | 3 | ## kotysa-reactive-r2dbc 4 | This is a sample project for a Spring Boot Reactive web application with Kofu and a R2DBC backend accessed via Kotysa. 5 | 6 | ## kotysa-corutines-r2dbc 7 | This is a sample project for a Spring Boot Coroutines web application with Kofu and a R2DBC backend accessed via Kotysa. 8 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | * verify current version is not already released in gradle.properties 2 | * verify you are using SSH with GIT 3 | * do **publish** task 4 | * verify artifacts are available on bintray, Publish them 5 | * do **release** task (for minor release, press Enter for suggested versions : release version = current, new version = current + 1) 6 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/DbType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | /** 8 | * Supported Databases 9 | */ 10 | public enum class DbType { 11 | H2, SQLITE, POSTGRESQL 12 | } 13 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/Repository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | 8 | interface Repository { 9 | fun init() 10 | 11 | fun delete() 12 | } 13 | -------------------------------------------------------------------------------- /kotysa-core/src/testFixtures/java/com/pullvert/kotysa/test/Entity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.test; 6 | 7 | /** 8 | * Basic Entity Superclass 9 | */ 10 | public interface Entity { 11 | 12 | ID getId(); 13 | } 14 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/main/kotlin/com/sample/Application.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import org.springframework.boot.WebApplicationType 4 | import org.springframework.fu.kofu.application 5 | 6 | val app = application(WebApplicationType.REACTIVE) { 7 | enable(dataConfig) 8 | enable(webConfig) 9 | } 10 | 11 | fun main() { 12 | app.run() 13 | } 14 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/main/kotlin/com/sample/Application.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import org.springframework.boot.WebApplicationType 4 | import org.springframework.fu.kofu.application 5 | 6 | val app = application(WebApplicationType.REACTIVE) { 7 | enable(dataConfig) 8 | enable(webConfig) 9 | } 10 | 11 | fun main() { 12 | app.run() 13 | } 14 | -------------------------------------------------------------------------------- /kotysa-android/README.md: -------------------------------------------------------------------------------- 1 | # Kotysa for SqLite on Android 2 | 3 | ## Dependency 4 | 5 | Kotysa is a single dependency to add to your Android project. 6 | 7 | ```groovy 8 | repositories { 9 | jcenter() 10 | } 11 | 12 | dependencies { 13 | implementation 'com.pullvert:kotysa-android:0.1.1' 14 | } 15 | ``` 16 | 17 | See [SqLite supported types](../docs/table-modelling.md#SqLite) 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | out 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | 28 | ### VS Code ### 29 | .vscode/ 30 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/Exceptions.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | 8 | public class NonUniqueResultException : RuntimeException("Multiple results, query expected a single result") 9 | 10 | 11 | public class NoResultException : RuntimeException("No result, query expected a result") 12 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | plugins { 3 | id 'org.jetbrains.kotlin.jvm' version "$kotlin_plugin_version" 4 | id 'org.jetbrains.dokka' version "$dokka_plugin_version" 5 | id 'net.researchgate.release' version "$release_plugin_version" 6 | } 7 | } 8 | 9 | rootProject.name = 'kotysa' 10 | 11 | include 'kotysa-core' 12 | 13 | include 'kotysa-android' 14 | include 'kotysa-spring-data-r2dbc' 15 | 16 | include 'kotysa-platform' 17 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/PrimaryKey.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | 8 | public interface PrimaryKey { 9 | public val name: String? 10 | } 11 | 12 | 13 | internal class SinglePrimaryKey internal constructor( 14 | override val name: String?, 15 | internal val column: Column 16 | ): PrimaryKey 17 | -------------------------------------------------------------------------------- /kotysa-android/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /kotysa-core/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/ForeignKey.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | 8 | public interface ForeignKey { 9 | public val name: String? 10 | } 11 | 12 | 13 | internal data class SingleForeignKey internal constructor( 14 | override val name: String?, 15 | internal val column: Column, 16 | internal var referencedColumn: Column<*, *> 17 | ) : ForeignKey 18 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/TransactionStatus.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | 8 | // todo put in kotysa-core 9 | public interface TransactionStatus { 10 | 11 | public fun isNewTransaction(): Boolean 12 | 13 | public fun setRollbackOnly() 14 | 15 | public fun isRollbackOnly(): Boolean 16 | 17 | public fun isCompleted(): Boolean 18 | } 19 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/Reflection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import kotlin.reflect.KCallable 8 | import kotlin.reflect.KFunction 9 | import kotlin.reflect.KProperty1 10 | 11 | 12 | public fun ((T) -> Any?).toCallable(): KCallable = 13 | when (this) { 14 | is KProperty1 -> this 15 | is KFunction<*> -> this 16 | else -> throw RuntimeException("Wrong type for $this, support only KProperty1 and KFunction") 17 | } 18 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/JoinClause.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | 8 | public class JoinClause internal constructor( 9 | internal val table: AliasedTable<*>, 10 | internal val field: ColumnField<*, *>, 11 | internal val type: JoinType 12 | ) 13 | 14 | 15 | public enum class JoinType(internal val sql: String) { 16 | INNER("INNER JOIN"), 17 | LEFT_OUTER("LEFT OUTER JOIN"), 18 | RIGHT_OUTER("RIGHT OUTER JOIN"), 19 | FULL_OUTER("OUTER JOIN"), 20 | CROSS("CROSSS JOIN") 21 | } 22 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/JoinDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | 8 | @KotysaMarker 9 | public class JoinDsl internal constructor( 10 | private val init: (FieldProvider) -> ColumnField<*, *>, 11 | private val table: AliasedTable<*>, 12 | private val type: JoinType, 13 | availableColumns: Map Any?, Column<*, *>>, 14 | dbType: DbType 15 | ) : SimpleFieldProvider(availableColumns, dbType) { 16 | 17 | internal fun initialize() = JoinClause(table, init(this), type) 18 | } 19 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/ReactorTransactionalOperation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import reactor.core.publisher.Flux 8 | import reactor.core.publisher.Mono 9 | 10 | /** 11 | * @see org.springframework.transaction.reactive.TransactionalOperator 12 | */ 13 | public interface ReactorTransactionalOperation { 14 | 15 | public fun transactional(flux: Flux): Flux = execute { flux } 16 | 17 | public fun transactional(mono: Mono): Mono 18 | 19 | public fun execute(block: (TransactionStatus) -> Flux): Flux 20 | } 21 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/SelectDslApi.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import kotlin.reflect.KClass 8 | 9 | 10 | public abstract class SelectDslApi protected constructor(){ 11 | @PublishedApi 12 | internal abstract fun count(resultClass: KClass, dsl: ((FieldProvider) -> ColumnField)? = null, 13 | alias: String? = null): Long 14 | } 15 | 16 | 17 | public inline fun SelectDslApi.count( 18 | noinline dsl: ((FieldProvider) -> ColumnField 19 | )? = null): Long = count(T::class, dsl) 20 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/main/kotlin/com/sample/Handlers.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import org.springframework.http.MediaType 4 | import org.springframework.web.reactive.function.server.* 5 | 6 | @Suppress("UNUSED_PARAMETER") 7 | class UserHandler(private val repository: UserRepository) { 8 | 9 | suspend fun listApi(request: ServerRequest) = ServerResponse 10 | .ok() 11 | .contentType(MediaType.APPLICATION_JSON) 12 | .bodyAndAwait(repository.findAll()) 13 | 14 | suspend fun userApi(request: ServerRequest) = ServerResponse 15 | .ok() 16 | .contentType(MediaType.APPLICATION_JSON) 17 | .bodyValueAndAwait(repository.findOne(request.pathVariable("id").toInt())) 18 | 19 | } -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/main/kotlin/com/sample/Model.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import java.time.LocalDateTime 4 | import java.util.* 5 | 6 | data class Role( 7 | val label: String, 8 | val id: UUID = UUID.randomUUID() 9 | ) 10 | 11 | data class User( 12 | val firstname: String, 13 | val lastname: String, 14 | val isAdmin: Boolean, 15 | val roleId: UUID, 16 | val alias: String? = null, 17 | val creationTime: LocalDateTime = LocalDateTime.now(), 18 | val id: Int? = null 19 | ) 20 | 21 | /** 22 | * Not an entity 23 | * name = first_name and last_name 24 | */ 25 | data class UserDto( 26 | val name: String, 27 | val alias: String?, 28 | val role: String 29 | ) 30 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/main/kotlin/com/sample/Model.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import java.time.LocalDateTime 4 | import java.util.* 5 | 6 | data class Role( 7 | val label: String, 8 | val id: UUID = UUID.randomUUID() 9 | ) 10 | 11 | data class User( 12 | val firstname: String, 13 | val lastname: String, 14 | val isAdmin: Boolean, 15 | val roleId: UUID, 16 | val alias: String? = null, 17 | val creationTime: LocalDateTime = LocalDateTime.now(), 18 | val id: Int? = null 19 | ) 20 | 21 | /** 22 | * Not an entity 23 | * name = first_name and last_name 24 | */ 25 | data class UserDto( 26 | val name: String, 27 | val alias: String?, 28 | val role: String 29 | ) 30 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/WhereClause.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | 8 | public class WhereClause internal constructor( 9 | internal val field: Field, 10 | internal val operation: Operation, 11 | public val value: Any? 12 | ) 13 | 14 | internal enum class Operation { 15 | EQ, NOT_EQ, CONTAINS, STARTS_WITH, ENDS_WITH, SUP, INF, SUP_OR_EQ, INF_OR_EQ, IS 16 | } 17 | 18 | public class TypedWhereClause internal constructor( 19 | public val whereClause: WhereClause, 20 | internal val type: WhereClauseType 21 | ) 22 | 23 | internal enum class WhereClauseType { 24 | WHERE, AND, OR 25 | } 26 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/SqlType.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | /** 8 | * All supported SQL types 9 | */ 10 | public enum class SqlType(internal val fullType: String) { 11 | // text 12 | VARCHAR("VARCHAR"), 13 | TEXT("TEXT"), 14 | 15 | // numbers 16 | INTEGER("INTEGER"), 17 | SERIAL("SERIAL"), 18 | 19 | // date 20 | TIMESTAMP("TIMESTAMP"), 21 | DATE("DATE"), 22 | DATE_TIME("DATETIME"), 23 | TIME("TIME"), 24 | TIMESTAMP_WITH_TIME_ZONE("TIMESTAMP WITH TIME ZONE"), 25 | TIME9("TIME(9)"), // time9 with fractional seconds precision to match with java.time.LocalTime's value 26 | 27 | BOOLEAN("BOOLEAN"), 28 | 29 | UUID("UUID") 30 | } 31 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/main/kotlin/com/sample/Handlers.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import org.springframework.http.MediaType 4 | import org.springframework.web.reactive.function.server.ServerRequest 5 | import org.springframework.web.reactive.function.server.ServerResponse 6 | import org.springframework.web.reactive.function.server.body 7 | 8 | @Suppress("UNUSED_PARAMETER") 9 | class UserHandler(private val repository: UserRepository) { 10 | 11 | fun listApi(request: ServerRequest) = ServerResponse 12 | .ok() 13 | .contentType(MediaType.APPLICATION_JSON) 14 | .body(repository.findAll()) 15 | 16 | fun userApi(request: ServerRequest) = ServerResponse 17 | .ok() 18 | .contentType(MediaType.APPLICATION_JSON) 19 | .body(repository.findOne(request.pathVariable("id").toInt())) 20 | 21 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | group=com.pullvert 2 | version=0.1.2 3 | 4 | # plugins 5 | kotlin_plugin_version=1.3.72 6 | dokka_plugin_version=0.10.1 7 | release_plugin_version=2.8.1 8 | 9 | # main 10 | kotlin_inline_logger_version=1.0.2 11 | kotlinx_coroutines_bom_version=1.3.7 12 | reactor_kotlin_extension_version=1.0.2.RELEASE 13 | spring_data_r2dbc_version=1.1.0.RELEASE 14 | 15 | # tests 16 | assertj_version=3.16.1 17 | jsr305_version=3.0.2 18 | junit_bom_version=5.6.2 19 | logback_version=1.2.3 20 | r2dbc_bom_version=Arabba-SR3 21 | testcontainers_bom_version=1.14.2 22 | spring_core_version=5.2.6.RELEASE 23 | spring_fu_version=0.3.0.M3 24 | 25 | # miscellaneous 26 | org.gradle.warning.mode=all 27 | # Workaround for Bintray treating .sha512 files as artifacts 28 | # https://github.com/gradle/gradle/issues/11412 29 | systemProp.org.gradle.internal.publish.checksums.insecure=true 30 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/TransactionStatusR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import org.springframework.transaction.ReactiveTransaction 8 | 9 | /** 10 | * @see org.springframework.transaction.TransactionExecution 11 | */ 12 | public class TransactionStatusR2dbc(private val reactiveTransaction: ReactiveTransaction) : TransactionStatus { 13 | 14 | override fun isNewTransaction(): Boolean = reactiveTransaction.isNewTransaction 15 | 16 | override fun setRollbackOnly() { 17 | reactiveTransaction.setRollbackOnly() 18 | } 19 | 20 | override fun isRollbackOnly(): Boolean = reactiveTransaction.isRollbackOnly 21 | 22 | override fun isCompleted(): Boolean = reactiveTransaction.isCompleted 23 | } 24 | -------------------------------------------------------------------------------- /kotysa-android/build.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | google() 3 | } 4 | 5 | dependencies { 6 | api platform(project(':kotysa-platform')) 7 | api project(':kotysa-core') 8 | 9 | compileOnly 'com.google.android:android:4.1.1.4' 10 | 11 | testImplementation testFixtures(project(':kotysa-core')) 12 | 13 | testImplementation 'junit:junit:4.12' 14 | testImplementation 'org.robolectric:robolectric:4.0.2' 15 | testImplementation "org.assertj:assertj-core:$assertj_version" 16 | testImplementation "ch.qos.logback:logback-classic:$logback_version" 17 | testImplementation 'com.google.android:android:4.1.1.4' 18 | 19 | testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' 20 | } 21 | 22 | //dokka { 23 | // configuration { 24 | // externalDocumentationLink { 25 | // url = new URL("https://developer.android.com/reference/") 26 | // } 27 | // } 28 | //} 29 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/postgresql/PostgresqlDefaultSqlClientDeleteOrUpdate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.postgresql 6 | 7 | import com.github.michaelbull.logging.InlineLogger 8 | import com.pullvert.kotysa.DefaultSqlClientDeleteOrUpdate 9 | 10 | 11 | internal fun DefaultSqlClientDeleteOrUpdate.Return<*>.postgresqlUpdateTableSql(logger: InlineLogger) = with(properties) { 12 | val updateSql = "UPDATE ${table.name}" 13 | var index = 1 14 | val setSql = setValues.keys.joinToString(prefix = "SET ") { column -> "${column.name} = $${index++}" } 15 | val joinsAndWheres = joinsWithExistsAndWheres(offset = index) 16 | logger.debug { "Exec SQL (${tables.dbType.name}) : $updateSql $setSql $joinsAndWheres" } 17 | 18 | "$updateSql $setSql $joinsAndWheres" 19 | } 20 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/h2/H2TablesDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.h2 6 | 7 | import com.pullvert.kotysa.Table 8 | import com.pullvert.kotysa.TablesDsl 9 | import kotlin.reflect.KClass 10 | 11 | /** 12 | * @sample com.pullvert.kotysa.sample.h2Tables 13 | */ 14 | public class H2TablesDsl(init: H2TablesDsl.() -> Unit) : TablesDsl>(init) { 15 | 16 | override fun initializeTable(tableClass: KClass, dsl: H2TableDsl<*>.() -> Unit): Table<*> { 17 | val tableDsl = H2TableDsl(dsl, tableClass) 18 | return tableDsl.initialize(tableDsl) 19 | } 20 | 21 | @Suppress("UNCHECKED_CAST") 22 | public inline fun table(noinline dsl: H2TableDsl.() -> Unit) { 23 | table(T::class, dsl as H2TableDsl<*>.() -> Unit) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/h2/H2TableDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.h2 6 | 7 | import com.pullvert.kotysa.ColumnBuilder 8 | import com.pullvert.kotysa.TableColumnPropertyProvider 9 | import com.pullvert.kotysa.TableDsl 10 | import kotlin.reflect.KClass 11 | 12 | 13 | public class H2TableDsl( 14 | init: H2TableDsl.() -> Unit, 15 | tableClass: KClass 16 | ) : TableDsl>(init, tableClass) { 17 | 18 | /** 19 | * Declare a Column, supported types follow : [H2 Data types](http://h2database.com/html/datatypes.html) 20 | */ 21 | public fun column(dsl: H2ColumnDsl.(TableColumnPropertyProvider) -> ColumnBuilder<*, T, *>) { 22 | val columnDsl = H2ColumnDsl(dsl) 23 | val column = columnDsl.initialize(columnDsl) 24 | addColumn(column) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/sqlite/SqLiteTablesDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.sqlite 6 | 7 | import com.pullvert.kotysa.Table 8 | import com.pullvert.kotysa.TablesDsl 9 | import kotlin.reflect.KClass 10 | 11 | /** 12 | * @sample com.pullvert.kotysa.sample.sqLiteTables 13 | */ 14 | public class SqLiteTablesDsl(init: SqLiteTablesDsl.() -> Unit) : TablesDsl>(init) { 15 | 16 | override fun initializeTable(tableClass: KClass, dsl: SqLiteTableDsl<*>.() -> Unit): Table<*> { 17 | val tableDsl = SqLiteTableDsl(dsl, tableClass) 18 | return tableDsl.initialize(tableDsl) 19 | } 20 | 21 | @Suppress("UNCHECKED_CAST") 22 | public inline fun table(noinline dsl: SqLiteTableDsl.() -> Unit) { 23 | table(T::class, dsl as SqLiteTableDsl<*>.() -> Unit) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kotysa-core/src/test/kotlin/com/pullvert/kotysa/sample/h2Tables.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.sample 6 | 7 | import com.pullvert.kotysa.tables 8 | import java.util.* 9 | 10 | fun h2Tables() = 11 | tables().h2 { // choose database type 12 | table { 13 | name = "users" 14 | column { it[H2User::id].uuid().primaryKey() } 15 | column { it[H2User::firstname].varchar().name("fname") } 16 | column { it[H2User::lastname].varchar().name("lname") } 17 | column { it[H2User::isAdmin].boolean() } 18 | column { it[H2User::alias].varchar() } 19 | } 20 | } 21 | 22 | data class H2User( 23 | val firstname: String, 24 | val lastname: String, 25 | val isAdmin: Boolean, 26 | val alias: String? = null, 27 | val id: UUID 28 | ) 29 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/sqlite/SqLiteTableDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.sqlite 6 | 7 | import com.pullvert.kotysa.ColumnBuilder 8 | import com.pullvert.kotysa.TableColumnPropertyProvider 9 | import com.pullvert.kotysa.TableDsl 10 | import kotlin.reflect.KClass 11 | 12 | 13 | public class SqLiteTableDsl( 14 | init: SqLiteTableDsl.() -> Unit, 15 | tableClass: KClass 16 | ) : TableDsl>(init, tableClass) { 17 | 18 | /** 19 | * Declare a Column, supported types follow : [SqLite Data types](https://www.sqlite.org/datatype3.html) 20 | */ 21 | public fun column(dsl: SqLiteColumnDsl.(TableColumnPropertyProvider) -> ColumnBuilder<*, T, *>) { 22 | val columnDsl = SqLiteColumnDsl(dsl) 23 | val column = columnDsl.initialize(columnDsl) 24 | addColumn(column) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /kotysa-core/src/test/kotlin/com/pullvert/kotysa/sample/sqLiteTables.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.sample 6 | 7 | import com.pullvert.kotysa.tables 8 | 9 | fun sqLiteTables() = 10 | tables().sqlite { // choose database type 11 | table { 12 | name = "users" 13 | column { it[SqLiteUser::id].text().primaryKey() } 14 | column { it[SqLiteUser::firstname].text().name("fname") } 15 | column { it[SqLiteUser::lastname].text().name("lname") } 16 | column { it[SqLiteUser::isAdmin].integer() } 17 | column { it[SqLiteUser::alias].text() } 18 | } 19 | } 20 | 21 | data class SqLiteUser( 22 | val firstname: String, 23 | val lastname: String, 24 | val isAdmin: Boolean, 25 | val alias: String? = null, 26 | val id: String 27 | ) 28 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/postgresql/PostgresqlTablesDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.postgresql 6 | 7 | import com.pullvert.kotysa.Table 8 | import com.pullvert.kotysa.TablesDsl 9 | import kotlin.reflect.KClass 10 | 11 | /** 12 | * @sample com.pullvert.kotysa.sample.postgresqlTables 13 | */ 14 | public class PostgresqlTablesDsl(init: PostgresqlTablesDsl.() -> Unit) : TablesDsl>(init) { 15 | 16 | override fun initializeTable(tableClass: KClass, dsl: PostgresqlTableDsl<*>.() -> Unit): Table<*> { 17 | val tableDsl = PostgresqlTableDsl(dsl, tableClass) 18 | return tableDsl.initialize(tableDsl) 19 | } 20 | 21 | @Suppress("UNCHECKED_CAST") 22 | public inline fun table(noinline dsl: PostgresqlTableDsl.() -> Unit) { 23 | table(T::class, dsl as PostgresqlTableDsl<*>.() -> Unit) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/postgresql/PostgresqlTableDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.postgresql 6 | 7 | import com.pullvert.kotysa.ColumnBuilder 8 | import com.pullvert.kotysa.TableColumnPropertyProvider 9 | import com.pullvert.kotysa.TableDsl 10 | import kotlin.reflect.KClass 11 | 12 | 13 | public class PostgresqlTableDsl( 14 | init: PostgresqlTableDsl.() -> Unit, 15 | tableClass: KClass 16 | ) : TableDsl>(init, tableClass) { 17 | 18 | /** 19 | * Declare a Column, supported types follow : [Postgres Data types](https://www.postgresql.org/docs/11/datatype.html) 20 | */ 21 | public fun column(dsl: PostgresqlColumnDsl.(TableColumnPropertyProvider) -> ColumnBuilder<*, T, *>) { 22 | val columnDsl = PostgresqlColumnDsl(dsl) 23 | val column = columnDsl.initialize(columnDsl) 24 | addColumn(column) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /kotysa-core/src/test/kotlin/com/pullvert/kotysa/sample/postgresqlTables.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.sample 6 | 7 | import com.pullvert.kotysa.tables 8 | import java.util.* 9 | 10 | fun postgresqlTables() = 11 | tables().postgresql { // choose database type 12 | table { 13 | name = "users" 14 | column { it[PostgresUser::id].uuid().primaryKey() } 15 | column { it[PostgresUser::firstname].varchar().name("fname") } 16 | column { it[PostgresUser::lastname].varchar().name("lname") } 17 | column { it[PostgresUser::isAdmin].boolean() } 18 | column { it[PostgresUser::alias].varchar() } 19 | } 20 | } 21 | 22 | data class PostgresUser( 23 | val firstname: String, 24 | val lastname: String, 25 | val isAdmin: Boolean, 26 | val alias: String? = null, 27 | val id: UUID 28 | ) 29 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/TransactionalOperationR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import org.springframework.transaction.reactive.TransactionalOperator 8 | import reactor.core.publisher.Flux 9 | import reactor.core.publisher.Mono 10 | 11 | public class TransactionalOperationR2dbc(private val operator: TransactionalOperator) : ReactorTransactionalOperation { 12 | override fun transactional(mono: Mono): Mono = operator.transactional(mono) 13 | 14 | override fun execute(block: (TransactionStatus) -> Flux): Flux = 15 | operator.execute { reactiveTransaction -> block.invoke(TransactionStatusR2dbc(reactiveTransaction)) } 16 | } 17 | 18 | /** 19 | * Create a [ReactorTransactionalOperation] from a Reactive [TransactionalOperator] 20 | * 21 | * @sample com.pullvert.kotysa.r2dbc.sample.UserRepositoryR2dbc 22 | */ 23 | public fun TransactionalOperator.transactionalOperation(): ReactorTransactionalOperation = TransactionalOperationR2dbc(this) 24 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/AbstractSqlClientDeleteR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import com.pullvert.kotysa.DefaultSqlClientDeleteOrUpdate 8 | import org.springframework.data.r2dbc.core.DatabaseClient 9 | import org.springframework.data.r2dbc.core.FetchSpec 10 | 11 | 12 | internal abstract class AbstractSqlClientDeleteR2dbc protected constructor() : DefaultSqlClientDeleteOrUpdate() { 13 | 14 | protected interface Return : DefaultSqlClientDeleteOrUpdate.Return { 15 | val client: DatabaseClient 16 | 17 | fun fetch(): FetchSpec> = with(properties) { 18 | var executeSpec = client.execute(deleteFromTableSql()) 19 | 20 | whereClauses 21 | .mapNotNull { typedWhereClause -> typedWhereClause.whereClause.value } 22 | .forEachIndexed { index, value -> 23 | executeSpec = executeSpec.bind(index, value) 24 | } 25 | 26 | executeSpec.fetch() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/h2/H2DefaultSqlClientDeleteOrUpdate.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.h2 6 | 7 | import com.github.michaelbull.logging.InlineLogger 8 | import com.pullvert.kotysa.DefaultSqlClientDeleteOrUpdate 9 | 10 | 11 | internal fun DefaultSqlClientDeleteOrUpdate.Return<*>.h2DeleteFromTableSql(logger: InlineLogger) = with(properties) { 12 | val deleteSql = "DELETE FROM ${table.name}" 13 | val joinsAndWheres = joinsWithExistsAndWheres() 14 | logger.debug { "Exec SQL (${tables.dbType.name}) : $deleteSql $joinsAndWheres" } 15 | 16 | "$deleteSql $joinsAndWheres" 17 | } 18 | 19 | 20 | internal fun DefaultSqlClientDeleteOrUpdate.Return<*>.h2UpdateTableSql(logger: InlineLogger) = with(properties) { 21 | val updateSql = "UPDATE ${table.name}" 22 | val setSql = setValues.keys.joinToString(prefix = "SET ") { column -> "${column.name} = ?" } 23 | val joinsAndWheres = joinsWithExistsAndWheres() 24 | logger.debug { "Exec SQL (${tables.dbType.name}) : $updateSql $setSql $joinsAndWheres" } 25 | 26 | "$updateSql $setSql $joinsAndWheres" 27 | } 28 | -------------------------------------------------------------------------------- /kotysa-core/build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.config.KotlinCompilerVersion 2 | 3 | plugins { 4 | id 'java-test-fixtures' 5 | } 6 | 7 | println("Using Kotlin compiler version: $KotlinCompilerVersion.VERSION") 8 | 9 | dependencies { 10 | api platform(project(':kotysa-platform')) 11 | 12 | implementation 'org.jetbrains.kotlin:kotlin-reflect' 13 | implementation "com.michael-bull.kotlin-inline-logger:kotlin-inline-logger-jvm" 14 | 15 | compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-core' 16 | 17 | testFixturesImplementation "com.google.code.findbugs:jsr305:$jsr305_version" 18 | testFixturesImplementation "org.springframework:spring-core:$spring_core_version" 19 | 20 | testImplementation 'org.junit.jupiter:junit-jupiter-api' 21 | testImplementation "ch.qos.logback:logback-classic:$logback_version" 22 | testImplementation "org.assertj:assertj-core:$assertj_version" 23 | 24 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' 25 | } 26 | 27 | compileTestFixturesKotlin { 28 | kotlinOptions { 29 | freeCompilerArgs = ['-Xjvm-default=enable'] 30 | jvmTarget = '1.8' 31 | } 32 | } 33 | 34 | dokka { 35 | configuration { 36 | includes = ['../README.md'] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /kotysa-platform/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-platform' 3 | id 'maven-publish' 4 | } 5 | 6 | dependencies { 7 | // The platform declares constraints on all components that require alignment 8 | constraints { 9 | api project(':kotysa-core') 10 | api project(':kotysa-android') 11 | api project(':kotysa-spring-data-r2dbc') 12 | 13 | api "com.michael-bull.kotlin-inline-logger:kotlin-inline-logger-jvm:$kotlin_inline_logger_version" 14 | } 15 | } 16 | 17 | publishing { 18 | repositories { 19 | maven { 20 | def user = 'pull-vert' 21 | def repo = 'kotysa' 22 | def name = 'kotysa' 23 | url = "https://api.bintray.com/maven/$user/$repo/$name/;publish=0" 24 | 25 | credentials { 26 | username = project.hasProperty('bintray_user') ? project.property('bintray_user') : System.getenv('BINTRAY_USER') 27 | password = project.hasProperty('bintray_api_key') ? project.property('bintray_api_key') : System.getenv('BINTRAY_API_KEY') 28 | } 29 | } 30 | } 31 | 32 | publications { 33 | myPlatform(MavenPublication) { 34 | from components.javaPlatform 35 | } 36 | } 37 | } 38 | 39 | 40 | -------------------------------------------------------------------------------- /kotysa-android/src/test/kotlin/com/pullvert/kotysa/android/DbHelper.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android 6 | 7 | import android.content.Context 8 | import android.database.sqlite.SQLiteDatabase 9 | import android.database.sqlite.SQLiteOpenHelper 10 | import android.util.Log 11 | 12 | /** 13 | * see [Android SQLite database unit testing](https://medium.com/@elye.project/android-sqlite-database-unit-testing-is-easy-a09994701162#.s44tity8x) 14 | */ 15 | class DbHelper internal constructor( 16 | context: Context 17 | ) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { 18 | 19 | companion object { 20 | private val TAG = DbHelper::class.java.simpleName 21 | 22 | private const val DATABASE_NAME = "simpledatabase.sqlite" 23 | private const val DATABASE_VERSION = 1 24 | } 25 | 26 | override fun onCreate(db: SQLiteDatabase) { 27 | // no op 28 | } 29 | 30 | override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { 31 | Log.w(TAG, "Upgrade from version $oldVersion to $newVersion") 32 | Log.w(TAG, "This is version 1, no DB to update") 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/h2/R2DbcSelectAndH2Test.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.h2 6 | 7 | import com.pullvert.kotysa.test.* 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.junit.jupiter.api.Test 10 | import org.springframework.data.r2dbc.core.DatabaseClient 11 | 12 | 13 | class R2DbcSelectAndH2Test : AbstractR2dbcH2Test() { 14 | override val context = startContext() 15 | 16 | override val repository = getContextRepository() 17 | 18 | @Test 19 | fun `Verify selectRolesByLabels finds h2God`() { 20 | assertThat(repository.selectRolesByLabels("d", "g").toIterable()) 21 | .hasSize(1) 22 | .containsExactly(h2God) 23 | } 24 | } 25 | 26 | 27 | class UserRepositoryH2SelectAnd(dbClient: DatabaseClient) : AbstractUserRepositoryH2(dbClient) { 28 | 29 | fun selectRolesByLabels(label1: String, label2: String) = sqlClient.select() 30 | .where { it[H2Role::label] contains label1 } 31 | .and { it[H2Role::label] contains label2 } 32 | .fetchAll() 33 | } 34 | -------------------------------------------------------------------------------- /kotysa-android/src/test/kotlin/com/pullvert/kotysa/android/AbstractSqLiteTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android 6 | 7 | import com.pullvert.kotysa.Tables 8 | import com.pullvert.kotysa.test.sqLiteTables 9 | import org.junit.After 10 | import org.junit.Before 11 | import org.junit.runner.RunWith 12 | import org.robolectric.RobolectricTestRunner 13 | import org.robolectric.RuntimeEnvironment 14 | import org.robolectric.annotation.Config 15 | 16 | /** 17 | * Android SDK 5.0 (API = 21) is the minimal that works 18 | */ 19 | @RunWith(RobolectricTestRunner::class) 20 | @Config(manifest = Config.NONE, sdk = [21]) 21 | abstract class AbstractSqLiteTest { 22 | 23 | private lateinit var dbHelper: DbHelper 24 | protected lateinit var repository: T 25 | 26 | @Suppress("DEPRECATION") 27 | @Before 28 | fun setup() { 29 | dbHelper = DbHelper(RuntimeEnvironment.application) 30 | repository = getRepository(dbHelper, sqLiteTables) 31 | repository.init() 32 | } 33 | 34 | @After 35 | fun afterAll() { 36 | repository.delete() 37 | } 38 | 39 | protected abstract fun getRepository(dbHelper: DbHelper, sqLiteTables: Tables): T 40 | } 41 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/test/kotlin/com/sample/UserRepositoryTests.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import org.assertj.core.api.Assertions.assertThat 4 | import org.junit.jupiter.api.AfterAll 5 | import org.junit.jupiter.api.BeforeAll 6 | import org.junit.jupiter.api.Test 7 | import org.springframework.beans.factory.getBean 8 | import org.springframework.boot.WebApplicationType 9 | import org.springframework.context.ConfigurableApplicationContext 10 | import org.springframework.fu.kofu.application 11 | 12 | class UserRepositoryTests { 13 | 14 | private val dataApp = application(WebApplicationType.NONE) { 15 | enable(dataConfig) 16 | } 17 | 18 | private lateinit var context: ConfigurableApplicationContext 19 | private lateinit var repository: UserRepository 20 | 21 | @BeforeAll 22 | fun beforeAll() { 23 | context = dataApp.run(profiles = "test") 24 | repository = context.getBean() 25 | } 26 | 27 | @Test 28 | fun count() { 29 | assertThat(repository.count().block()) 30 | .isEqualTo(2) 31 | } 32 | 33 | @Test 34 | fun selectWithJoin() { 35 | assertThat(repository.selectWithJoin().log().toIterable()) 36 | .hasSize(2) 37 | } 38 | 39 | @AfterAll 40 | fun afterAll() { 41 | context.close() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/h2/R2DbcSelectOrH2Test.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.h2 6 | 7 | import com.pullvert.kotysa.test.* 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.junit.jupiter.api.Test 10 | import org.springframework.data.r2dbc.core.DatabaseClient 11 | 12 | 13 | class R2DbcSelectOrH2Test : AbstractR2dbcH2Test() { 14 | override val context = startContext() 15 | 16 | override val repository = getContextRepository() 17 | 18 | @Test 19 | fun `Verify selectRolesByLabels finds h2Admin and h2God`() { 20 | assertThat(repository.selectRolesByLabels(h2Admin.label, h2God.label).toIterable()) 21 | .hasSize(2) 22 | .containsExactlyInAnyOrder(h2Admin, h2God) 23 | } 24 | } 25 | 26 | 27 | class UserRepositoryH2SelectOr(dbClient: DatabaseClient) : AbstractUserRepositoryH2(dbClient) { 28 | 29 | fun selectRolesByLabels(label1: String, label2: String) = sqlClient.select() 30 | .where { it[H2Role::label] eq label1 } 31 | .or { it[H2Role::label] eq label2 } 32 | .fetchAll() 33 | } 34 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/AliasedTable.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | /** 8 | * A table with an alias (that may be null) 9 | */ 10 | internal class AliasedTable internal constructor( 11 | internal val table: Table, 12 | internal val alias: String? = null 13 | ) : Table by table { 14 | 15 | /** 16 | * The prefix : alias if exists, or table name 17 | */ 18 | internal val prefix = alias ?: name 19 | 20 | /** 21 | * Declaration in queries : "tableName AS alias" if alias exists, or "tableName" 22 | */ 23 | internal val declaration = if (alias != null) { 24 | "$name AS $alias" 25 | } else { 26 | name 27 | } 28 | 29 | override fun equals(other: Any?): Boolean { 30 | if (this === other) return true 31 | if (javaClass != other?.javaClass) return false 32 | 33 | other as AliasedTable<*> 34 | 35 | if (alias != other.alias) return false 36 | if (name != other.name) return false 37 | 38 | return true 39 | } 40 | 41 | override fun hashCode(): Int { 42 | var result = name.hashCode() 43 | result = 31 * result + (alias?.hashCode() ?: 0) 44 | return result 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/test/kotlin/com/sample/UserRepositoryTests.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import kotlinx.coroutines.flow.toList 4 | import kotlinx.coroutines.runBlocking 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.junit.jupiter.api.AfterAll 7 | import org.junit.jupiter.api.BeforeAll 8 | import org.junit.jupiter.api.Test 9 | import org.springframework.beans.factory.getBean 10 | import org.springframework.boot.WebApplicationType 11 | import org.springframework.context.ConfigurableApplicationContext 12 | import org.springframework.fu.kofu.application 13 | 14 | class UserRepositoryTests { 15 | 16 | private val dataApp = application(WebApplicationType.NONE) { 17 | enable(dataConfig) 18 | } 19 | 20 | private lateinit var context: ConfigurableApplicationContext 21 | private lateinit var repository: UserRepository 22 | 23 | @BeforeAll 24 | fun beforeAll() { 25 | context = dataApp.run(profiles = "test") 26 | repository = context.getBean() 27 | } 28 | 29 | @Test 30 | fun count() = runBlocking { 31 | assertThat(repository.count()) 32 | .isEqualTo(2) 33 | } 34 | 35 | @Test 36 | fun selectWithJoin() = runBlocking { 37 | assertThat(repository.selectWithJoin().toList()) 38 | .hasSize(2) 39 | } 40 | 41 | @AfterAll 42 | fun afterAll() { 43 | context.close() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /kotysa-android/src/test/kotlin/com/pullvert/kotysa/android/SqLiteSelectOrTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android 6 | 7 | import android.database.sqlite.SQLiteOpenHelper 8 | import com.pullvert.kotysa.Tables 9 | import com.pullvert.kotysa.test.SqLiteRole 10 | import com.pullvert.kotysa.test.sqLiteAdmin 11 | import com.pullvert.kotysa.test.sqLiteGod 12 | import org.assertj.core.api.Assertions.assertThat 13 | import org.junit.Test 14 | 15 | class SqLiteSelectOrTest : AbstractSqLiteTest() { 16 | 17 | override fun getRepository(dbHelper: DbHelper, sqLiteTables: Tables) = 18 | UserRepositorySelectOr(dbHelper, sqLiteTables) 19 | 20 | @Test 21 | fun `Verify selectRolesByLabels finds BigBoss`() { 22 | assertThat(repository.selectRolesByLabels(sqLiteAdmin.label, sqLiteGod.label)) 23 | .hasSize(2) 24 | .containsExactlyInAnyOrder(sqLiteAdmin, sqLiteGod) 25 | } 26 | } 27 | 28 | class UserRepositorySelectOr(sqLiteOpenHelper: SQLiteOpenHelper, tables: Tables) : AbstractUserRepository(sqLiteOpenHelper, tables) { 29 | 30 | fun selectRolesByLabels(label1: String, label2: String) = sqlClient.select() 31 | .where { it[SqLiteRole::label] eq label1 } 32 | .or { it[SqLiteRole::label] eq label2 } 33 | .fetchAll() 34 | } 35 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/postgresql/R2DbcSelectOrPostgresqlTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.postgresql 6 | 7 | import com.pullvert.kotysa.test.* 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.junit.jupiter.api.Test 10 | import org.springframework.data.r2dbc.core.DatabaseClient 11 | 12 | 13 | class R2DbcSelectOrPostgresqlTest : AbstractR2dbcPostgresqlTest() { 14 | override val context = startContext() 15 | 16 | override val repository = getContextRepository() 17 | 18 | @Test 19 | fun `Verify selectRolesByLabels finds postgresqlAdmin and postgresqlGod`() { 20 | assertThat(repository.selectRolesByLabels(postgresqlAdmin.label, postgresqlGod.label).toIterable()) 21 | .hasSize(2) 22 | .containsExactlyInAnyOrder(postgresqlAdmin, postgresqlGod) 23 | } 24 | } 25 | 26 | 27 | class UserRepositoryPostgresqlSelectOr(dbClient: DatabaseClient) : AbstractUserRepositoryPostgresql(dbClient) { 28 | 29 | fun selectRolesByLabels(label1: String, label2: String) = sqlClient.select() 30 | .where { it[PostgresqlRole::label] eq label1 } 31 | .or { it[PostgresqlRole::label] eq label2 } 32 | .fetchAll() 33 | } 34 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/h2/AbstractR2dbcH2Test.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.h2 6 | 7 | import com.pullvert.kotysa.r2dbc.Repository 8 | import org.junit.jupiter.api.AfterAll 9 | import org.springframework.beans.factory.getBean 10 | import org.springframework.boot.WebApplicationType 11 | import org.springframework.boot.context.event.ApplicationReadyEvent 12 | import org.springframework.context.ConfigurableApplicationContext 13 | import org.springframework.fu.kofu.application 14 | import org.springframework.fu.kofu.r2dbc.r2dbcH2 15 | 16 | 17 | abstract class AbstractR2dbcH2Test { 18 | 19 | protected abstract val repository: T 20 | 21 | protected inline fun startContext() = 22 | application(WebApplicationType.NONE) { 23 | beans { 24 | bean() 25 | } 26 | listener { 27 | ref().init() 28 | } 29 | r2dbcH2() 30 | }.run() 31 | 32 | protected abstract val context: ConfigurableApplicationContext 33 | 34 | protected inline fun getContextRepository() = context.getBean() 35 | 36 | @AfterAll 37 | fun afterAll() { 38 | repository.delete() 39 | context.close() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/sqlite/SqLiteDefaultSqlClient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.sqlite 6 | 7 | import com.github.michaelbull.logging.InlineLogger 8 | import com.pullvert.kotysa.DefaultSqlClientDeleteOrUpdate 9 | 10 | 11 | internal fun DefaultSqlClientDeleteOrUpdate.Return<*>.sqLiteDeleteFromTableSql(logger: InlineLogger) = with(properties) { 12 | val joinsAndWheres = joinsWithExistsAndWheres(false) 13 | logger.debug { 14 | val joinsAndWheresDebug = if (joinsAndWheres.isNotEmpty()) { 15 | "WHERE $joinsAndWheres" 16 | } else { 17 | "" 18 | } 19 | "Exec SQL (${tables.dbType.name}) : DELETE FROM ${table.name} $joinsAndWheresDebug" 20 | } 21 | joinsAndWheres 22 | } 23 | 24 | 25 | internal fun DefaultSqlClientDeleteOrUpdate.Return<*>.sqLiteUpdateTableSql(logger: InlineLogger) = with(properties) { 26 | val joinsAndWheres = joinsWithExistsAndWheres(false) 27 | logger.debug { 28 | val setSqlDebug = setValues.keys.joinToString(prefix = "SET ") { column -> "${column.name} = ?" } 29 | val joinsAndWheresDebug = if (joinsAndWheres.isNotEmpty()) { 30 | "WHERE $joinsAndWheres" 31 | } else { 32 | "" 33 | } 34 | "Exec SQL (${tables.dbType.name}) : UPDATE ${table.name} $setSqlDebug $joinsAndWheresDebug" 35 | } 36 | joinsAndWheres 37 | } 38 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/h2/R2DbcSelectBooleanH2Test.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.h2 6 | 7 | import com.pullvert.kotysa.test.H2User 8 | import com.pullvert.kotysa.test.h2Bboss 9 | import com.pullvert.kotysa.test.h2Jdoe 10 | import org.assertj.core.api.Assertions.assertThat 11 | import org.junit.jupiter.api.Test 12 | import org.springframework.data.r2dbc.core.DatabaseClient 13 | 14 | 15 | class R2DbcSelectBooleanH2Test : AbstractR2dbcH2Test() { 16 | override val context = startContext() 17 | 18 | override val repository = getContextRepository() 19 | 20 | @Test 21 | fun `Verify selectAllByIsAdminEq true finds Big Boss`() { 22 | assertThat(repository.selectAllByIsAdminEq(true).toIterable()) 23 | .hasSize(1) 24 | .containsExactly(h2Bboss) 25 | } 26 | 27 | @Test 28 | fun `Verify selectAllByIsAdminEq false finds John`() { 29 | assertThat(repository.selectAllByIsAdminEq(false).toIterable()) 30 | .hasSize(1) 31 | .containsExactly(h2Jdoe) 32 | } 33 | } 34 | 35 | 36 | class UserRepositoryH2SelectBoolean(dbClient: DatabaseClient) : AbstractUserRepositoryH2(dbClient) { 37 | 38 | fun selectAllByIsAdminEq(value: Boolean) = sqlClient.select() 39 | .where { it[H2User::isAdmin] eq value } 40 | .fetchAll() 41 | } 42 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.3.72' 3 | id 'io.spring.dependency-management' version '1.0.9.RELEASE' 4 | id 'org.springframework.boot' version '2.3.0.RC1' 5 | } 6 | 7 | repositories { 8 | jcenter() 9 | maven { url 'https://repo.spring.io/milestone' } 10 | } 11 | 12 | dependencies { 13 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 14 | implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' 15 | implementation 'org.springframework.data:spring-data-r2dbc' 16 | implementation 'io.r2dbc:r2dbc-postgresql' 17 | 18 | implementation 'com.pullvert:kotysa-spring-data-r2dbc:0.1.1' 19 | implementation 'org.springframework.fu:spring-fu-kofu:0.3.0.M3' 20 | implementation 'org.testcontainers:postgresql:1.14.1' 21 | 22 | testImplementation 'io.r2dbc:r2dbc-h2' 23 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 24 | } 25 | 26 | compileKotlin { 27 | kotlinOptions { 28 | freeCompilerArgs = ['-Xjsr305=strict', '-Xjvm-default=enable'] 29 | jvmTarget = '1.8' 30 | } 31 | } 32 | 33 | compileTestKotlin { 34 | kotlinOptions { 35 | freeCompilerArgs = ['-Xjsr305=strict', '-Xjvm-default=enable'] 36 | jvmTarget = '1.8' 37 | } 38 | } 39 | 40 | test { 41 | useJUnitPlatform() 42 | testLogging { 43 | events 'passed', 'failed', 'skipped' 44 | showStandardStreams = true 45 | } 46 | } 47 | 48 | wrapper { 49 | gradleVersion = '6.3' 50 | distributionType = Wrapper.DistributionType.ALL 51 | } 52 | -------------------------------------------------------------------------------- /kotysa-android/src/test/kotlin/com/pullvert/kotysa/android/SqLiteSelectBooleanTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android 6 | 7 | import android.database.sqlite.SQLiteOpenHelper 8 | import com.pullvert.kotysa.Tables 9 | import com.pullvert.kotysa.test.SqLiteUser 10 | import com.pullvert.kotysa.test.sqLiteBboss 11 | import com.pullvert.kotysa.test.sqLiteJdoe 12 | import org.assertj.core.api.Assertions.assertThat 13 | import org.junit.Test 14 | 15 | class SqLiteSelectBooleanTest : AbstractSqLiteTest() { 16 | 17 | override fun getRepository(dbHelper: DbHelper, sqLiteTables: Tables) = 18 | UserRepositoryBooleanSelect(dbHelper, sqLiteTables) 19 | 20 | @Test 21 | fun `Verify selectAllByIsAdminEq true finds Big Boss`() { 22 | assertThat(repository.selectAllByIsAdminEq(true)) 23 | .hasSize(1) 24 | .containsExactly(sqLiteBboss) 25 | } 26 | 27 | @Test 28 | fun `Verify selectAllByIsAdminEq false finds John`() { 29 | assertThat(repository.selectAllByIsAdminEq(false)) 30 | .hasSize(1) 31 | .containsExactly(sqLiteJdoe) 32 | } 33 | } 34 | 35 | class UserRepositoryBooleanSelect(sqLiteOpenHelper: SQLiteOpenHelper, tables: Tables) : AbstractUserRepository(sqLiteOpenHelper, tables) { 36 | 37 | fun selectAllByIsAdminEq(value: Boolean) = sqlClient.select() 38 | .where { it[SqLiteUser::isAdmin] eq value } 39 | .fetchAll() 40 | } 41 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/postgresql/R2DbcSelectBooleanPostgresqlTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.postgresql 6 | 7 | import com.pullvert.kotysa.test.* 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.junit.jupiter.api.Test 10 | import org.springframework.data.r2dbc.core.DatabaseClient 11 | 12 | 13 | class R2DbcSelectBooleanH2Test : AbstractR2dbcPostgresqlTest() { 14 | override val context = startContext() 15 | 16 | override val repository = getContextRepository() 17 | 18 | @Test 19 | fun `Verify selectAllByIsAdminEq true finds Big Boss`() { 20 | assertThat(repository.selectAllByIsAdminEq(true).toIterable()) 21 | .hasSize(1) 22 | .containsExactly(postgresqlBboss) 23 | } 24 | 25 | @Test 26 | fun `Verify selectAllByIsAdminEq false finds John`() { 27 | assertThat(repository.selectAllByIsAdminEq(false).toIterable()) 28 | .hasSize(1) 29 | .containsExactly(postgresqlJdoe) 30 | } 31 | } 32 | 33 | 34 | class UserRepositoryPostgresqlSelectBoolean(dbClient: DatabaseClient) : AbstractUserRepositoryPostgresql(dbClient) { 35 | 36 | fun selectAllByIsAdminEq(value: Boolean) = sqlClient.select() 37 | .where { it[PostgresqlUser::isAdmin] eq value } 38 | .fetchAll() 39 | } 40 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/test/kotlin/com/sample/IntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import org.junit.jupiter.api.AfterAll 4 | import org.junit.jupiter.api.BeforeAll 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.context.ConfigurableApplicationContext 7 | import org.springframework.http.MediaType 8 | import org.springframework.test.web.reactive.server.WebTestClient 9 | import org.springframework.test.web.reactive.server.expectBody 10 | import org.springframework.test.web.reactive.server.expectBodyList 11 | 12 | class IntegrationTests { 13 | 14 | private val client = WebTestClient.bindToServer().baseUrl("http://localhost:8181").build() 15 | 16 | private lateinit var context: ConfigurableApplicationContext 17 | 18 | @BeforeAll 19 | fun beforeAll() { 20 | context = app.run(profiles = "dev") 21 | } 22 | 23 | @Test 24 | fun `Request HTTP API endpoint for listing all users`() { 25 | client.get().uri("/api/user").exchange() 26 | .expectStatus().is2xxSuccessful 27 | .expectHeader().contentType(MediaType.APPLICATION_JSON_VALUE) 28 | .expectBodyList() 29 | .hasSize(2) 30 | } 31 | 32 | @Test 33 | fun `Request HTTP API endpoint for getting one specified user`() { 34 | client.get().uri("/api/user/123").exchange() 35 | .expectStatus().is2xxSuccessful 36 | .expectHeader().contentType(MediaType.APPLICATION_JSON_VALUE) 37 | .expectBody() 38 | } 39 | 40 | @AfterAll 41 | fun afterAll() { 42 | context.close() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/test/kotlin/com/sample/IntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import org.junit.jupiter.api.AfterAll 4 | import org.junit.jupiter.api.BeforeAll 5 | import org.junit.jupiter.api.Test 6 | import org.springframework.context.ConfigurableApplicationContext 7 | import org.springframework.http.MediaType 8 | import org.springframework.test.web.reactive.server.WebTestClient 9 | import org.springframework.test.web.reactive.server.expectBody 10 | import org.springframework.test.web.reactive.server.expectBodyList 11 | 12 | class IntegrationTests { 13 | 14 | private val client = WebTestClient.bindToServer().baseUrl("http://localhost:8181").build() 15 | 16 | private lateinit var context: ConfigurableApplicationContext 17 | 18 | @BeforeAll 19 | fun beforeAll() { 20 | context = app.run(profiles = "dev") 21 | } 22 | 23 | @Test 24 | fun `Request HTTP API endpoint for listing all users`() { 25 | client.get().uri("/api/user").exchange() 26 | .expectStatus().is2xxSuccessful 27 | .expectHeader().contentType(MediaType.APPLICATION_JSON_VALUE) 28 | .expectBodyList() 29 | .hasSize(2) 30 | } 31 | 32 | @Test 33 | fun `Request HTTP API endpoint for getting one specified user`() { 34 | client.get().uri("/api/user/123").exchange() 35 | .expectStatus().is2xxSuccessful 36 | .expectHeader().contentType(MediaType.APPLICATION_JSON_VALUE) 37 | .expectBody() 38 | } 39 | 40 | @AfterAll 41 | fun afterAll() { 42 | context.close() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /kotysa-android/src/test/kotlin/com/pullvert/kotysa/android/AbstractUserRepository.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android 6 | 7 | import android.database.sqlite.SQLiteOpenHelper 8 | import com.pullvert.kotysa.Tables 9 | import com.pullvert.kotysa.test.* 10 | 11 | abstract class AbstractUserRepository( 12 | sqLiteOpenHelper: SQLiteOpenHelper, 13 | tables: Tables 14 | ) : Repository { 15 | 16 | protected val sqlClient = sqLiteOpenHelper.sqlClient(tables) 17 | 18 | override fun init() { 19 | createTable() 20 | insertRoles() 21 | insertUsers() 22 | } 23 | 24 | override fun delete() { 25 | deleteAllFromUsers() 26 | deleteAllFromRoles() 27 | } 28 | 29 | private fun createTable() { 30 | sqlClient.createTable() 31 | sqlClient.createTable() 32 | } 33 | 34 | private fun insertRoles() = sqlClient.insert(sqLiteUser, sqLiteAdmin, sqLiteGod) 35 | 36 | fun insertUsers() = sqlClient.insert(sqLiteJdoe, sqLiteBboss) 37 | 38 | fun deleteAllFromUsers() = sqlClient.deleteAllFromTable() 39 | 40 | private fun deleteAllFromRoles() = sqlClient.deleteAllFromTable() 41 | 42 | fun insertJDoe() = sqlClient.insert(sqLiteJdoe) 43 | 44 | fun selectAll() = sqlClient.selectAll() 45 | 46 | fun selectFirstByFirstame(firstname: String) = sqlClient.select() 47 | .where { it[SqLiteUser::firstname] eq firstname } 48 | .fetchFirstOrNull() 49 | } 50 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/README.md: -------------------------------------------------------------------------------- 1 | # Kotysa for Spring data R2DBC 2 | 3 | ## Dependency 4 | 5 | Kotysa is an additional dependency you can add to your Spring project. 6 | It is an extension to Spring Data R2DBC, and does not replace it. 7 | 8 | ```groovy 9 | repositories { 10 | jcenter() 11 | } 12 | 13 | dependencies { 14 | implementation 'com.pullvert:kotysa-spring-data-r2dbc:0.1.1' 15 | 16 | implementation "org.springframework.data:spring-data-r2dbc" 17 | } 18 | ``` 19 | 20 | ## Reactive support 21 | 22 | **SqlClient** has one reactive implementation on top of R2DBC using spring-data-r2dbc's ```DatabaseClient```, it can be obtained via an Extension function directly on ```DatabaseClient```. 23 | 24 | It provides a SQL client API using Reactor ```Mono``` and ```Flux```. 25 | 26 | ```kotlin 27 | class UserRepository(dbClient: DatabaseClient, tables: Tables) { 28 | 29 | private val sqlClient = dbClient.sqlClient(tables) 30 | 31 | // enjoy sqlClient with Reactor :) 32 | } 33 | ``` 34 | 35 | ## Coroutines first class support 36 | 37 | **SqlClient** has one Coroutines implementation on top of R2DBC using spring-data-r2dbc's ```DatabaseClient```, it can be obtained via an Extension function directly on ```DatabaseClient```. 38 | 39 | It provides a SQL client API using ```suspend``` functions and kotlinx-coroutines ```Flow```. 40 | 41 | ```kotlin 42 | class UserRepository(dbClient: DatabaseClient, tables: Tables) { 43 | 44 | private val sqlClient = dbClient.coSqlClient(tables) 45 | 46 | // enjoy sqlClient use with coroutines :) 47 | } 48 | ``` 49 | 50 | ## Supported databases 51 | 52 | * [H2](../docs/table-modelling.md#H2) 53 | * [PostgreSQL](../docs/table-modelling.md#PostgreSQL) 54 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/Table.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import kotlin.reflect.KClass 8 | 9 | /** 10 | * A database Table model mapped by entity class [tableClass] 11 | */ 12 | public interface Table { 13 | public val tableClass: KClass 14 | /** 15 | * Real name of this table in the database 16 | */ 17 | public val name: String 18 | public val columns: Map<(T) -> Any?, Column> 19 | public val primaryKey: PrimaryKey 20 | public var foreignKeys: Set 21 | } 22 | 23 | 24 | internal class TableImpl internal constructor( 25 | override val tableClass: KClass, 26 | override val name: String, 27 | override val columns: Map<(T) -> Any?, Column>, 28 | override val primaryKey: PrimaryKey 29 | ) : Table { 30 | 31 | override lateinit var foreignKeys: Set 32 | 33 | override fun equals(other: Any?): Boolean { 34 | if (this === other) return true 35 | if (javaClass != other?.javaClass) return false 36 | 37 | other as TableImpl<*> 38 | 39 | if (name != other.name) return false 40 | 41 | return true 42 | } 43 | 44 | override fun hashCode(): Int { 45 | return name.hashCode() 46 | } 47 | } 48 | 49 | /** 50 | * All Mapped Tables 51 | */ 52 | public class Tables internal constructor( 53 | public val allTables: Map, Table<*>>, 54 | internal val allColumns: Map Any?, Column<*, *>>, 55 | internal val dbType: DbType 56 | ) 57 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/FieldSetter.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.LocalTime 10 | import java.time.OffsetDateTime 11 | import java.util.* 12 | 13 | 14 | public interface FieldSetter { 15 | 16 | public operator fun set(getter: (T) -> String, value: String) 17 | 18 | public operator fun set(getter: (T) -> String?, value: String?): Nullable 19 | 20 | public operator fun set(getter: (T) -> LocalDateTime, value: LocalDateTime) 21 | 22 | public operator fun set(getter: (T) -> LocalDateTime?, value: LocalDateTime?): Nullable 23 | 24 | public operator fun set(getter: (T) -> LocalDate, value: LocalDate) 25 | 26 | public operator fun set(getter: (T) -> LocalDate?, value: LocalDate?): Nullable 27 | 28 | public operator fun set(getter: (T) -> OffsetDateTime, value: OffsetDateTime) 29 | 30 | public operator fun set(getter: (T) -> OffsetDateTime?, value: OffsetDateTime?): Nullable 31 | 32 | public operator fun set(getter: (T) -> LocalTime, value: LocalTime) 33 | 34 | public operator fun set(getter: (T) -> LocalTime?, value: LocalTime?): Nullable 35 | 36 | public operator fun set(getter: (T) -> Boolean, value: Boolean) 37 | 38 | public operator fun set(getter: (T) -> UUID, value: UUID) 39 | 40 | public operator fun set(getter: (T) -> UUID?, value: UUID?): Nullable 41 | 42 | public operator fun set(getter: (T) -> Int, value: Int) 43 | 44 | public operator fun set(getter: (T) -> Int?, value: Int?): Nullable 45 | } 46 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/h2/AbstractUserRepositoryH2.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.h2 6 | 7 | import com.pullvert.kotysa.r2dbc.Repository 8 | import com.pullvert.kotysa.r2dbc.sqlClient 9 | import com.pullvert.kotysa.test.* 10 | import org.springframework.data.r2dbc.core.DatabaseClient 11 | 12 | 13 | abstract class AbstractUserRepositoryH2(dbClient: DatabaseClient) : Repository { 14 | 15 | protected val sqlClient = dbClient.sqlClient(h2Tables) 16 | 17 | override fun init() { 18 | createTables() 19 | .then(insertRoles()) 20 | .then(insertUsers()) 21 | .block() 22 | } 23 | 24 | override fun delete() { 25 | deleteAllFromUsers() 26 | .then(deleteAllFromRole()) 27 | .block() 28 | } 29 | 30 | private fun createTables() = 31 | sqlClient.createTable() 32 | .then(sqlClient.createTable()) 33 | 34 | private fun insertRoles() = sqlClient.insert(h2User, h2Admin, h2God) 35 | 36 | fun insertUsers() = sqlClient.insert(h2Jdoe, h2Bboss) 37 | 38 | fun insertJDoe() = sqlClient.insert(h2Jdoe) 39 | 40 | private fun deleteAllFromRole() = sqlClient.deleteAllFromTable() 41 | 42 | fun deleteAllFromUsers() = sqlClient.deleteAllFromTable() 43 | 44 | fun selectAllUsers() = sqlClient.selectAll() 45 | 46 | fun selectFirstByFirstame(firstname: String) = sqlClient.select() 47 | .where { it[H2User::firstname] eq firstname } 48 | .fetchFirst() 49 | } 50 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.3.72' 3 | id 'io.spring.dependency-management' version '1.0.9.RELEASE' 4 | id 'org.springframework.boot' version '2.3.0.RC1' 5 | } 6 | 7 | repositories { 8 | jcenter() 9 | maven { url 'https://repo.spring.io/milestone' } 10 | } 11 | 12 | dependencies { 13 | implementation 'org.springframework.boot:spring-boot-starter-webflux' 14 | implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' 15 | implementation 'org.springframework.data:spring-data-r2dbc' 16 | implementation 'io.r2dbc:r2dbc-postgresql' 17 | implementation 'io.projectreactor.kotlin:reactor-kotlin-extensions' 18 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor' 19 | 20 | implementation 'com.pullvert:kotysa-spring-data-r2dbc:0.1.1' 21 | implementation 'org.springframework.fu:spring-fu-kofu:0.3.0.M3' 22 | implementation 'org.testcontainers:postgresql:1.14.1' 23 | 24 | testImplementation 'io.r2dbc:r2dbc-h2' 25 | testImplementation 'org.springframework.boot:spring-boot-starter-test' 26 | } 27 | 28 | compileKotlin { 29 | kotlinOptions { 30 | freeCompilerArgs = ['-Xjsr305=strict', '-Xjvm-default=enable'] 31 | jvmTarget = '1.8' 32 | } 33 | } 34 | 35 | compileTestKotlin { 36 | kotlinOptions { 37 | freeCompilerArgs = ['-Xjsr305=strict', '-Xjvm-default=enable'] 38 | jvmTarget = '1.8' 39 | } 40 | } 41 | 42 | test { 43 | useJUnitPlatform() 44 | testLogging { 45 | events 'passed', 'failed', 'skipped' 46 | showStandardStreams = true 47 | } 48 | } 49 | 50 | wrapper { 51 | gradleVersion = '6.3' 52 | distributionType = Wrapper.DistributionType.ALL 53 | } 54 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/DbTypeChoice.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import com.pullvert.kotysa.h2.H2TablesDsl 8 | import com.pullvert.kotysa.postgresql.PostgresqlTablesDsl 9 | import com.pullvert.kotysa.sqlite.SqLiteTablesDsl 10 | 11 | /** 12 | * Supported Database Choice 13 | */ 14 | public object DbTypeChoice { 15 | 16 | /** 17 | * Configure Functional Table Mapping support for H2 18 | * @sample com.pullvert.kotysa.sample.h2Tables 19 | * @see H2TablesDsl 20 | */ 21 | public fun h2(dsl: H2TablesDsl.() -> Unit): Tables { 22 | val tablesDsl = H2TablesDsl(dsl) 23 | return tablesDsl.initialize(tablesDsl, DbType.H2) 24 | } 25 | 26 | /** 27 | * Configure Functional Table Mapping support for SqLite 28 | * @sample com.pullvert.kotysa.sample.sqLiteTables 29 | * @see H2TablesDsl 30 | */ 31 | public fun sqlite(dsl: SqLiteTablesDsl.() -> Unit): Tables { 32 | val tablesDsl = SqLiteTablesDsl(dsl) 33 | return tablesDsl.initialize(tablesDsl, DbType.SQLITE) 34 | } 35 | 36 | /** 37 | * Configure Functional Table Mapping support for PostgreSQL 38 | * @sample com.pullvert.kotysa.sample.postgresqlTables 39 | * @see H2TablesDsl 40 | */ 41 | public fun postgresql(dsl: PostgresqlTablesDsl.() -> Unit): Tables { 42 | val tablesDsl = PostgresqlTablesDsl(dsl) 43 | return tablesDsl.initialize(tablesDsl, DbType.POSTGRESQL) 44 | } 45 | } 46 | 47 | /** 48 | * Choose the database's Type 49 | * 50 | * @see TablesDsl 51 | * @see DbTypeChoice 52 | */ 53 | public fun tables(): DbTypeChoice = DbTypeChoice 54 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/AbstractSqlClientSelectR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import com.pullvert.kotysa.AbstractRow 8 | import com.pullvert.kotysa.DefaultSqlClientSelect 9 | import com.pullvert.kotysa.Field 10 | import io.r2dbc.spi.Row 11 | import org.springframework.data.r2dbc.core.DatabaseClient 12 | import org.springframework.data.r2dbc.core.RowsFetchSpec 13 | 14 | 15 | internal abstract class AbstractSqlClientSelectR2dbc protected constructor() : DefaultSqlClientSelect() { 16 | 17 | protected interface Return : DefaultSqlClientSelect.Return { 18 | 19 | val client: DatabaseClient 20 | 21 | fun fetch(): RowsFetchSpec = with(properties) { 22 | var executeSpec = client.execute(selectSql()) 23 | 24 | whereClauses 25 | .mapNotNull { typedWhereClause -> typedWhereClause.whereClause.value } 26 | .forEachIndexed { index, value -> 27 | executeSpec = executeSpec.bind(index, value) 28 | } 29 | 30 | executeSpec.map { r, _ -> 31 | val row = R2dbcRow(r, selectInformation.fieldIndexMap) 32 | selectInformation.select(row, row) 33 | } 34 | } 35 | 36 | @Suppress("UNCHECKED_CAST") 37 | private class R2dbcRow( 38 | private val r2bcRow: Row, 39 | fieldIndexMap: Map 40 | ) : AbstractRow(fieldIndexMap) { 41 | override fun get(index: Int, type: Class) = r2bcRow.get(index, type) as T 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/AbstractSqlClientR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import com.pullvert.kotysa.DefaultSqlClient 8 | import com.pullvert.kotysa.SqlType 9 | import com.pullvert.kotysa.getTable 10 | import com.pullvert.kotysa.toCallable 11 | import org.springframework.data.r2dbc.core.DatabaseClient 12 | import kotlin.reflect.KClass 13 | 14 | /** 15 | * see [spring-data-r2dbc doc](https://docs.spring.io/spring-data/r2dbc/docs/1.0.x/reference/html/#reference) 16 | */ 17 | internal interface AbstractSqlClientR2dbc : DefaultSqlClient { 18 | 19 | val client: DatabaseClient 20 | 21 | fun executeCreateTable(tableClass: KClass) = 22 | client.execute(createTableSql(tableClass)) 23 | 24 | fun executeInsert(row: T): DatabaseClient.GenericExecuteSpec { 25 | var executeSpec = client.execute(insertSql(row)) 26 | val table = tables.getTable(row::class) 27 | var index = 0 28 | table.columns.values.forEach { column -> 29 | val value = column.entityGetter(row) 30 | executeSpec = if (value == null) { 31 | // do nothing for null values with default or Serial type 32 | if (column.defaultValue != null || SqlType.SERIAL == column.sqlType) { 33 | executeSpec 34 | } else { 35 | executeSpec.bindNull(index++, (column.entityGetter.toCallable().returnType.classifier as KClass<*>).java) 36 | } 37 | } else { 38 | executeSpec.bind(index++, value) 39 | } 40 | } 41 | return executeSpec 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/TableDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import kotlin.reflect.KClass 8 | 9 | 10 | @KotysaMarker 11 | public abstract class TableDsl>( 12 | private val init: U.() -> Unit, 13 | private val tableClass: KClass 14 | ) { 15 | 16 | public lateinit var name: String 17 | private val columns = mutableMapOf<(T) -> Any?, Column>() 18 | public lateinit var primaryKey: PrimaryKey 19 | 20 | protected fun addColumn(column: Column) { 21 | require(!columns.containsKey(column.entityGetter)) { 22 | "Trying to map property \"${column.entityGetter}\" to multiple columns" 23 | } 24 | require(tableClass.members.contains(column.entityGetter.toCallable())) { 25 | "Trying to map property \"${column.entityGetter}\", which is not a property of entity class \"${tableClass.qualifiedName}\"" 26 | } 27 | if (column.isPrimaryKey) { 28 | check(!::primaryKey.isInitialized) { 29 | "Table must not declare more than one Primary Key" 30 | } 31 | primaryKey = SinglePrimaryKey(column.pkName, column) 32 | } 33 | columns[column.entityGetter] = column 34 | } 35 | 36 | @PublishedApi 37 | internal fun initialize(initialize: U): Table<*> { 38 | init(initialize) 39 | if (!::name.isInitialized) { 40 | name = tableClass.simpleName!! 41 | } 42 | require(::primaryKey.isInitialized) { "Table primary key is mandatory" } 43 | require(columns.isNotEmpty()) { "Table must declare at least one column" } 44 | val table = TableImpl(tableClass, name, columns, primaryKey) 45 | // associate table to all its columns 46 | columns.forEach { (_, c) -> c.table = table } 47 | return table 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/AbstractSqlClientUpdateR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import com.pullvert.kotysa.DefaultSqlClientDeleteOrUpdate 8 | import com.pullvert.kotysa.toCallable 9 | import org.springframework.data.r2dbc.core.DatabaseClient 10 | import org.springframework.data.r2dbc.core.FetchSpec 11 | import kotlin.reflect.KClass 12 | 13 | 14 | internal abstract class AbstractSqlClientUpdateR2dbc protected constructor() : DefaultSqlClientDeleteOrUpdate() { 15 | 16 | protected interface Return : DefaultSqlClientDeleteOrUpdate.Return { 17 | val client: DatabaseClient 18 | 19 | fun fetch(): FetchSpec> = with(properties) { 20 | require(setValues.isNotEmpty()) { "At least one value must be set in Update" } 21 | 22 | var executeSpec = client.execute(updateTableSql()) 23 | 24 | var index = 0 25 | setValues.forEach { (column, value) -> 26 | executeSpec = if (value == null) { 27 | executeSpec.bindNull(index, (column.entityGetter.toCallable().returnType.classifier as KClass<*>).java) 28 | } else { 29 | executeSpec.bind(index, value) 30 | } 31 | index++ 32 | } 33 | 34 | whereClauses 35 | .mapNotNull { typedWhereClause -> typedWhereClause.whereClause.value } 36 | .forEach { value -> 37 | executeSpec = executeSpec.bind(index, value) 38 | index++ 39 | } 40 | 41 | executeSpec.fetch() 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/main/kotlin/com/sample/Repositories.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import com.pullvert.kotysa.CoroutinesSqlClient 4 | import java.util.* 5 | 6 | private val role_user_uuid = UUID.fromString("79e9eb45-2835-49c8-ad3b-c951b591bc7f") 7 | private val role_admin_uuid = UUID.fromString("67d4306e-d99d-4e54-8b1d-5b1e92691a4e") 8 | 9 | class UserRepository(private val client: CoroutinesSqlClient) { 10 | 11 | suspend fun count() = client.countAll() 12 | 13 | fun findAll() = client.selectAll() 14 | 15 | suspend fun findOne(id: Int) = 16 | client.select() 17 | .where { it[User::id] eq id } 18 | .fetchOne() 19 | 20 | fun selectWithJoin() = 21 | client.select { 22 | UserDto("${it[User::firstname]} ${it[User::lastname]}", it[User::alias], it[Role::label]) 23 | } 24 | .innerJoin().on { it[User::roleId] } 25 | .fetchAll() 26 | 27 | suspend fun deleteAll() = client.deleteAllFromTable() 28 | 29 | suspend fun save(user: User) = client.insert(user) 30 | 31 | suspend fun init() { 32 | client.createTable() 33 | deleteAll() 34 | save(User("John", "Doe", false, role_user_uuid, id = 123)) 35 | save(User("Big", "Boss", true, role_admin_uuid, "TheBoss")) 36 | } 37 | } 38 | 39 | class RoleRepository(private val client: CoroutinesSqlClient) { 40 | suspend fun deleteAll() = client.deleteAllFromTable() 41 | 42 | suspend fun save(role: Role) = client.insert(role) 43 | 44 | suspend fun init() { 45 | client.createTable() 46 | deleteAll() 47 | save(Role("user", role_user_uuid)) 48 | save(Role("admin", role_admin_uuid)) 49 | save(Role("god")) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/postgresql/AbstractUserRepositoryPostgresql.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.postgresql 6 | 7 | import com.pullvert.kotysa.r2dbc.Repository 8 | import com.pullvert.kotysa.r2dbc.sqlClient 9 | import com.pullvert.kotysa.test.* 10 | import org.springframework.data.r2dbc.core.DatabaseClient 11 | 12 | 13 | abstract class AbstractUserRepositoryPostgresql(dbClient: DatabaseClient) : Repository { 14 | 15 | protected val sqlClient = dbClient.sqlClient(postgresqlTables) 16 | 17 | override fun init() { 18 | createTables() 19 | .then(insertRoles()) 20 | .then(insertUsers()) 21 | .block() 22 | } 23 | 24 | override fun delete() { 25 | deleteAllFromUsers() 26 | .then(deleteAllFromRole()) 27 | .block() 28 | } 29 | 30 | private fun createTables() = 31 | sqlClient.createTable() 32 | .then(sqlClient.createTable()) 33 | 34 | private fun insertRoles() = sqlClient.insert(postgresqlUser, postgresqlAdmin, postgresqlGod) 35 | 36 | fun insertUsers() = sqlClient.insert(postgresqlJdoe, postgresqlBboss) 37 | 38 | fun insertJDoe() = sqlClient.insert(postgresqlJdoe) 39 | 40 | private fun deleteAllFromRole() = sqlClient.deleteAllFromTable() 41 | 42 | fun deleteAllFromUsers() = sqlClient.deleteAllFromTable() 43 | 44 | fun selectAllUsers() = sqlClient.selectAll() 45 | 46 | fun selectFirstByFirstame(firstname: String) = sqlClient.select() 47 | .where { it[PostgresqlUser::firstname] eq firstname } 48 | .fetchFirst() 49 | } 50 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/TableColumnPropertyProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.LocalTime 10 | import java.time.OffsetDateTime 11 | import java.util.* 12 | 13 | 14 | public interface TableColumnPropertyProvider { 15 | 16 | public operator fun get(getter: (T) -> String): NotNullStringColumnProperty 17 | 18 | public operator fun get(getter: (T) -> String?): NullableStringColumnProperty 19 | 20 | public operator fun get(getter: (T) -> LocalDateTime): NotNullLocalDateTimeColumnProperty 21 | 22 | public operator fun get(getter: (T) -> LocalDateTime?): NullableLocalDateTimeColumnProperty 23 | 24 | public operator fun get(getter: (T) -> LocalDate): NotNullLocalDateColumnProperty 25 | 26 | public operator fun get(getter: (T) -> LocalDate?): NullableLocalDateColumnProperty 27 | 28 | public operator fun get(getter: (T) -> OffsetDateTime): NotNullOffsetDateTimeColumnProperty 29 | 30 | public operator fun get(getter: (T) -> OffsetDateTime?): NullableOffsetDateTimeColumnProperty 31 | 32 | public operator fun get(getter: (T) -> LocalTime): NotNullLocalTimeColumnProperty 33 | 34 | public operator fun get(getter: (T) -> LocalTime?): NullableLocalTimeColumnProperty 35 | 36 | public operator fun get(getter: (T) -> Boolean): NotNullBooleanColumnProperty 37 | 38 | public operator fun get(getter: (T) -> UUID): NotNullUuidColumnProperty 39 | 40 | public operator fun get(getter: (T) -> UUID?): NullableUuidColumnProperty 41 | 42 | public operator fun get(getter: (T) -> Int): NotNullIntColumnProperty 43 | 44 | public operator fun get(getter: (T) -> Int?): NullableIntColumnProperty 45 | } 46 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/main/kotlin/com/sample/Repositories.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import com.pullvert.kotysa.r2dbc.ReactorSqlClient 4 | import java.util.* 5 | 6 | private val role_user_uuid = UUID.fromString("79e9eb45-2835-49c8-ad3b-c951b591bc7f") 7 | private val role_admin_uuid = UUID.fromString("67d4306e-d99d-4e54-8b1d-5b1e92691a4e") 8 | 9 | class UserRepository(private val client: ReactorSqlClient) { 10 | 11 | fun count() = client.countAll() 12 | 13 | fun findAll() = client.selectAll() 14 | 15 | fun findOne(id: Int) = 16 | client.select() 17 | .where { it[User::id] eq id } 18 | .fetchOne() 19 | 20 | fun selectWithJoin() = 21 | client.select { 22 | UserDto("${it[User::firstname]} ${it[User::lastname]}", it[User::alias], it[Role::label]) } 23 | .innerJoin().on { it[User::roleId] } 24 | .fetchAll() 25 | 26 | fun deleteAll() = client.deleteAllFromTable() 27 | 28 | fun save(user: User) = client.insert(user) 29 | 30 | fun init() { 31 | client.createTable() 32 | .then(deleteAll()) 33 | .then(save(User("John", "Doe", false, role_user_uuid, id = 123))) 34 | .then(save(User("Big", "Boss", true, role_admin_uuid, "TheBoss"))) 35 | .block() 36 | } 37 | } 38 | 39 | class RoleRepository(private val client: ReactorSqlClient) { 40 | fun deleteAll() = client.deleteAllFromTable() 41 | 42 | fun save(role: Role) = client.insert(role) 43 | 44 | fun init() { 45 | client.createTable() 46 | .then(deleteAll()) 47 | .then(save(Role("user", role_user_uuid))) 48 | .then(save(Role("admin", role_admin_uuid))) 49 | .then(save(Role("god"))) 50 | .block() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docs/sql-queries.md: -------------------------------------------------------------------------------- 1 | ### Write Type-Safe Queries with SqlClient 2 | 3 | **SqlClient** supports : 4 | * ```select``` that returns one (```fetchOne()``` and ```fetchFirst()```) or several (```fetchAll()```) results 5 | * ```createTable``` for table creation 6 | * ```insert``` for single or multiple rows insertion 7 | * ```deleteFromTable``` that returns number of deleted rows 8 | * ```updateTable``` to update fields, returns number of updated rows 9 | 10 | ```kotlin 11 | fun createTable() = sqlClient.createTable() 12 | 13 | fun insert() = sqlClient.insert(jdoe, bboss) 14 | 15 | fun deleteAll() = sqlClient.deleteAllFromTable() 16 | 17 | fun deleteById(id: UUID) = sqlClient.deleteFromTable() 18 | .where { it[User::id] eq id } 19 | .execute() 20 | 21 | fun selectAll() = sqlClient.selectAll() 22 | 23 | fun countAll() = sqlClient.countAll() 24 | 25 | fun countWithAlias() = sqlClient.select { count { it[User::alias] } }.fetchOne() 26 | 27 | fun selectAllMappedToDto() = 28 | sqlClient.select { 29 | UserDto("${it[User::firstname]} ${it[User::lastname]}", 30 | it[User::alias]) 31 | }.fetchAll() 32 | 33 | fun selectFirstByFirstname(firstname: String) = sqlClient.select() 34 | .where { it[User::firstname] eq firstname } 35 | // null String forbidden ^^^^^^^^^ 36 | .fetchFirst() 37 | 38 | fun selectAllByAlias(alias1: String?, alias2: String?) = sqlClient.select() 39 | .where { it[User::alias] eq alias1 } 40 | // null String accepted ^^^^^ , if alias1==null, gives "WHERE user.alias IS NULL" 41 | .or { it[User::alias] eq alias2 } 42 | .fetchAll() 43 | 44 | fun updateFirstname(newFirstname: String) = sqlClient.updateTable() 45 | .set { it[User::firstname] = newFirstname } 46 | .execute() 47 | 48 | val jdoe = User("John", "Doe", false) 49 | val bboss = User("Big", "Boss", true, "TheBoss") 50 | 51 | data class UserDto( 52 | val name: String, 53 | val alias: String? 54 | ) 55 | ``` 56 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/CoroutinesSqlClientR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import com.pullvert.kotysa.* 8 | import org.springframework.data.r2dbc.core.DatabaseClient 9 | import org.springframework.data.r2dbc.core.await 10 | import kotlin.reflect.KClass 11 | 12 | /** 13 | * @sample com.pullvert.kotysa.r2dbc.sample.UserRepositoryR2dbcCoroutines 14 | */ 15 | private class CoroutinesSqlClientR2Dbc internal constructor( 16 | override val client: DatabaseClient, 17 | override val tables: Tables 18 | ) : CoroutinesSqlClient(), AbstractSqlClientR2dbc { 19 | 20 | override fun select(resultClass: KClass, dsl: (SelectDslApi.(ValueProvider) -> T)?): CoroutinesSqlClientSelect.Select = 21 | CoroutinesSqlClientSelectR2dbc.Select(client, tables, resultClass, dsl) 22 | 23 | override suspend fun createTable(tableClass: KClass) = 24 | executeCreateTable(tableClass).await() 25 | 26 | override suspend fun insert(row: T) = 27 | executeInsert(row).await() 28 | 29 | override suspend fun insert(vararg rows: Any) { 30 | checkRowsAreMapped(*rows) 31 | 32 | rows.forEach { row -> insert(row) } 33 | } 34 | 35 | override fun deleteFromTable(tableClass: KClass): CoroutinesSqlClientDeleteOrUpdate.DeleteOrUpdate = 36 | CoroutinesSqlClientDeleteR2dbc.Delete(client, tables, tableClass) 37 | 38 | override fun updateTable(tableClass: KClass): CoroutinesSqlClientDeleteOrUpdate.Update = 39 | CoroutinesSqlClientUpdateR2dbc.Update(client, tables, tableClass) 40 | } 41 | 42 | /** 43 | * Create a [CoroutinesSqlClient] from a R2DBC [DatabaseClient] with [Tables] mapping 44 | * 45 | * @sample com.pullvert.kotysa.r2dbc.sample.UserRepositoryR2dbcCoroutines 46 | */ 47 | public fun DatabaseClient.coSqlClient(tables: Tables): CoroutinesSqlClient = CoroutinesSqlClientR2Dbc(this, tables) 48 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/build.gradle: -------------------------------------------------------------------------------- 1 | repositories { 2 | maven { url 'https://repo.spring.io/milestone' } 3 | // maven { url 'https://repo.spring.io/snapshot' } 4 | } 5 | 6 | dependencies { 7 | api platform(project(':kotysa-platform')) 8 | api project(':kotysa-core') 9 | 10 | // import BOM 11 | implementation platform("io.r2dbc:r2dbc-bom:$r2dbc_bom_version") 12 | implementation platform("org.testcontainers:testcontainers-bom:$testcontainers_bom_version") 13 | 14 | api "io.projectreactor.kotlin:reactor-kotlin-extensions:$reactor_kotlin_extension_version" 15 | 16 | implementation "org.springframework.data:spring-data-r2dbc:$spring_data_r2dbc_version" 17 | implementation 'org.jetbrains.kotlin:kotlin-reflect' 18 | 19 | compileOnly 'org.jetbrains.kotlinx:kotlinx-coroutines-reactive' 20 | 21 | testImplementation testFixtures(project(':kotysa-core')) 22 | 23 | testImplementation "org.springframework.fu:spring-fu-kofu:$spring_fu_version" 24 | testImplementation 'io.r2dbc:r2dbc-h2' 25 | testImplementation 'io.r2dbc:r2dbc-postgresql' 26 | testImplementation 'org.testcontainers:postgresql' 27 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactive' 28 | testImplementation "ch.qos.logback:logback-classic:$logback_version" 29 | testImplementation 'org.junit.jupiter:junit-jupiter-api' 30 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test' 31 | testImplementation "org.assertj:assertj-core:$assertj_version" 32 | 33 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' 34 | } 35 | 36 | compileKotlin { 37 | kotlinOptions { 38 | freeCompilerArgs = ['-Xjsr305=strict', '-Xjvm-default=enable', '-Xexplicit-api=strict'] 39 | jvmTarget = '1.8' 40 | } 41 | } 42 | 43 | compileTestKotlin { 44 | kotlinOptions { 45 | freeCompilerArgs = ['-Xjsr305=strict', '-Xjvm-default=enable'] 46 | jvmTarget = '1.8' 47 | } 48 | } 49 | 50 | //dokka { 51 | // configuration { 52 | // externalDocumentationLink { 53 | // url = new URL("https://docs.spring.io/spring-data/r2dbc/docs/1.0.x/api/") 54 | // } 55 | // } 56 | //} 57 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/SqlClientR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import com.pullvert.kotysa.SelectDslApi 8 | import com.pullvert.kotysa.Tables 9 | import com.pullvert.kotysa.ValueProvider 10 | import org.springframework.data.r2dbc.core.DatabaseClient 11 | import reactor.core.publisher.Mono 12 | import kotlin.reflect.KClass 13 | 14 | /** 15 | * see [spring-data-r2dbc doc](https://docs.spring.io/spring-data/r2dbc/docs/1.0.x/reference/html/#reference) 16 | * @sample com.pullvert.kotysa.r2dbc.sample.UserRepositoryR2dbc 17 | */ 18 | private class SqlClientR2dbc( 19 | override val client: DatabaseClient, 20 | override val tables: Tables 21 | ) : ReactorSqlClient(), AbstractSqlClientR2dbc { 22 | 23 | override fun select(resultClass: KClass, dsl: (SelectDslApi.(ValueProvider) -> T)?): ReactorSqlClientSelect.Select = 24 | SqlClientSelectR2dbc.Select(client, tables, resultClass, dsl) 25 | 26 | override fun createTable(tableClass: KClass) = 27 | executeCreateTable(tableClass).then() 28 | 29 | override fun insert(row: T) = 30 | executeInsert(row).then() 31 | 32 | override fun insert(vararg rows: Any): Mono { 33 | checkRowsAreMapped(*rows) 34 | 35 | return rows.fold(Mono.empty(), { mono, row -> mono.then(insert(row)) }) 36 | } 37 | 38 | override fun deleteFromTable(tableClass: KClass): ReactorSqlClientDeleteOrUpdate.DeleteOrUpdate = 39 | SqlClientDeleteR2dbc.Delete(client, tables, tableClass) 40 | 41 | override fun updateTable(tableClass: KClass): ReactorSqlClientDeleteOrUpdate.Update = 42 | SqlClientUpdateR2dbc.Update(client, tables, tableClass) 43 | } 44 | 45 | /** 46 | * Create a [ReactorSqlClient] from a R2DBC [DatabaseClient] with [Tables] mapping 47 | * 48 | * @sample com.pullvert.kotysa.r2dbc.sample.UserRepositoryR2dbc 49 | */ 50 | public fun DatabaseClient.sqlClient(tables: Tables): ReactorSqlClient = SqlClientR2dbc(this, tables) 51 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/ValueProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.LocalTime 10 | import java.time.OffsetDateTime 11 | import java.util.* 12 | 13 | 14 | public interface ValueProvider { 15 | 16 | public operator fun get(getter: (T) -> String, alias: String? = null): String 17 | 18 | public operator fun get(getter: (T) -> String?, alias: String? = null, `_`: Nullable = Nullable.TRUE): String? 19 | 20 | public operator fun get(getter: (T) -> LocalDateTime, alias: String? = null): LocalDateTime 21 | 22 | public operator fun get(getter: (T) -> LocalDateTime?, alias: String? = null, `_`: Nullable = Nullable.TRUE): LocalDateTime? 23 | 24 | public operator fun get(getter: (T) -> LocalDate, alias: String? = null): LocalDate 25 | 26 | public operator fun get(getter: (T) -> LocalDate?, alias: String? = null, `_`: Nullable = Nullable.TRUE): LocalDate? 27 | 28 | public operator fun get(getter: (T) -> OffsetDateTime, alias: String? = null): OffsetDateTime 29 | 30 | public operator fun get(getter: (T) -> OffsetDateTime?, alias: String? = null, `_`: Nullable = Nullable.TRUE): OffsetDateTime? 31 | 32 | public operator fun get(getter: (T) -> LocalTime, alias: String? = null): LocalTime 33 | 34 | public operator fun get(getter: (T) -> LocalTime?, alias: String? = null, `_`: Nullable = Nullable.TRUE): LocalTime? 35 | 36 | public operator fun get(getter: (T) -> Boolean, alias: String? = null): Boolean 37 | 38 | public operator fun get(getter: (T) -> UUID, alias: String? = null): UUID 39 | 40 | public operator fun get(getter: (T) -> UUID?, alias: String? = null, `_`: Nullable = Nullable.TRUE): UUID? 41 | 42 | public operator fun get(getter: (T) -> Int, alias: String? = null): Int 43 | 44 | public operator fun get(getter: (T) -> Int?, alias: String? = null, `_`: Nullable = Nullable.TRUE): Int? 45 | } 46 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/postgresql/AbstractR2dbcPostgresqlTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.postgresql 6 | 7 | import com.pullvert.kotysa.r2dbc.Repository 8 | import org.junit.jupiter.api.AfterAll 9 | import org.springframework.beans.factory.getBean 10 | import org.springframework.boot.WebApplicationType 11 | import org.springframework.boot.context.event.ApplicationReadyEvent 12 | import org.springframework.context.ConfigurableApplicationContext 13 | import org.springframework.fu.kofu.application 14 | import org.springframework.fu.kofu.r2dbc.r2dbcPostgresql 15 | import org.testcontainers.containers.PostgreSQLContainer 16 | 17 | class KPostgreSQLContainer : PostgreSQLContainer() 18 | 19 | 20 | abstract class AbstractR2dbcPostgresqlTest { 21 | 22 | protected abstract val repository: T 23 | 24 | protected inline fun startContext(): ConfigurableApplicationContext { 25 | // PostgreSQL testcontainers must be started first to get random Docker mapped port 26 | val postgresqlContainer = KPostgreSQLContainer() 27 | .withDatabaseName("postgres") 28 | .withUsername("postgres") 29 | .withPassword("") 30 | postgresqlContainer.start() 31 | 32 | return application(WebApplicationType.NONE) { 33 | beans { 34 | bean { postgresqlContainer } 35 | bean() 36 | } 37 | listener { 38 | ref().init() 39 | } 40 | r2dbcPostgresql { 41 | port = postgresqlContainer.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT) 42 | } 43 | }.run() 44 | } 45 | 46 | protected abstract val context: ConfigurableApplicationContext 47 | 48 | protected inline fun getContextRepository() = context.getBean() 49 | 50 | @AfterAll 51 | fun afterAll() = context.run { 52 | repository.delete() 53 | getBean>().stop() 54 | close() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/h2/H2Column.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.h2 6 | 7 | import com.pullvert.kotysa.* 8 | import kotlin.reflect.KClass 9 | 10 | internal interface TimestampWithTimeZoneColumn : Column 11 | 12 | internal class TimestampWithTimeZoneColumnNotNull internal constructor( 13 | override val entityGetter: (T) -> U, 14 | override val name: String, 15 | override val sqlType: SqlType, 16 | override val isPrimaryKey: Boolean, 17 | override val pkName: String?, 18 | override val defaultValue: U?, 19 | override val fkClass: KClass<*>?, 20 | override val fkName: String? 21 | ) : AbstractColumn(), TimestampWithTimeZoneColumn, ColumnNotNull, NoAutoIncrement 22 | 23 | internal class TimestampWithTimeZoneColumnNullable internal constructor( 24 | override val entityGetter: (T) -> U, 25 | override val name: String, 26 | override val sqlType: SqlType, 27 | override val fkClass: KClass<*>?, 28 | override val fkName: String? 29 | ) : AbstractColumn(), TimestampWithTimeZoneColumn, ColumnNullable, NoAutoIncrement 30 | 31 | 32 | internal interface Time9Column : Column 33 | 34 | 35 | internal class Time9ColumnNotNull internal constructor( 36 | override val entityGetter: (T) -> U, 37 | override val name: String, 38 | override val sqlType: SqlType, 39 | override val isPrimaryKey: Boolean, 40 | override val pkName: String?, 41 | override val defaultValue: U?, 42 | override val fkClass: KClass<*>?, 43 | override val fkName: String? 44 | ) : AbstractColumn(), Time9Column, ColumnNotNull, NoAutoIncrement 45 | 46 | 47 | internal class Time9ColumnNullable internal constructor( 48 | override val entityGetter: (T) -> U, 49 | override val name: String, 50 | override val sqlType: SqlType, 51 | override val fkClass: KClass<*>?, 52 | override val fkName: String? 53 | ) : AbstractColumn(), Time9Column, ColumnNullable, NoAutoIncrement 54 | -------------------------------------------------------------------------------- /kotysa-core/src/testFixtures/kotlin/com/pullvert/kotysa/test/CommonTestEntities.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.test 6 | 7 | 8 | data class UserDto( 9 | val name: String, 10 | val alias: String? 11 | ) 12 | 13 | 14 | data class UserWithRoleDto( 15 | val lastname: String, 16 | val role: String 17 | ) 18 | 19 | // test inheritance 20 | val inherited = Inherited("id", "name", "firstname") 21 | 22 | 23 | interface Nameable { 24 | val name: String 25 | } 26 | 27 | 28 | interface DummyIntermediary : Nameable 29 | 30 | 31 | open class Inherited( 32 | private val id: String, 33 | override val name: String, 34 | val firstname: String? 35 | ) : DummyIntermediary, Entity { 36 | 37 | override fun getId() = id 38 | 39 | // try to bring ambiguity for reflection on name val 40 | protected fun name() = "" 41 | 42 | internal fun getName() = "" 43 | @Suppress("UNUSED_PARAMETER") 44 | fun getName(dummyParam: Boolean) = "" 45 | 46 | // not a data class so needs hashCode & equals functions 47 | 48 | override fun equals(other: Any?): Boolean { 49 | if (this === other) return true 50 | if (javaClass != other?.javaClass) return false 51 | 52 | other as Inherited 53 | 54 | if (name != other.name) return false 55 | if (firstname != other.firstname) return false 56 | 57 | return true 58 | } 59 | 60 | override fun hashCode(): Int { 61 | var result = name.hashCode() 62 | result = 31 * result + (firstname?.hashCode() ?: 0) 63 | return result 64 | } 65 | } 66 | 67 | val javaJdoe: JavaUser 68 | get() { 69 | val javaUser = JavaUser() 70 | javaUser.login = "jdoe" 71 | javaUser.firstname = "John" 72 | javaUser.lastname = "Doe" 73 | javaUser.isAdmin = false 74 | return javaUser 75 | } 76 | 77 | val javaBboss: JavaUser 78 | get() { 79 | val javaUser = JavaUser() 80 | javaUser.login = "bboss" 81 | javaUser.firstname = "Big" 82 | javaUser.lastname = "Boss" 83 | javaUser.isAdmin = true 84 | javaUser.alias1 = "TheBoss" 85 | javaUser.alias2 = "TheBoss" 86 | javaUser.alias3 = "TheBoss" 87 | return javaUser 88 | } 89 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/h2/H2ColumnBuilder.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.h2 6 | 7 | import com.pullvert.kotysa.* 8 | 9 | public class TimestampWithTimeZoneColumnBuilderNotNull internal constructor( 10 | entityGetter: (T) -> U? 11 | ) : ColumnNotNullBuilder, T, U>(SqlType.TIMESTAMP_WITH_TIME_ZONE, entityGetter) { 12 | 13 | internal constructor(entityGetter: (T) -> U?, props: ColumnBuilderProps) : this(entityGetter) { 14 | this.props = props 15 | } 16 | 17 | override fun build() = with(props) { 18 | TimestampWithTimeZoneColumnNotNull(entityGetter, columnName, sqlType, isPK, pkName, defaultValue, fkClass, fkName) 19 | } 20 | } 21 | 22 | public class TimestampWithTimeZoneColumnBuilderNullable internal constructor( 23 | entityGetter: (T) -> U? 24 | ) : ColumnNullableBuilder, T, U>(SqlType.TIMESTAMP_WITH_TIME_ZONE, entityGetter) { 25 | override fun build() = with(props) { 26 | TimestampWithTimeZoneColumnNullable(entityGetter, columnName, sqlType, fkClass, fkName) 27 | } 28 | 29 | override fun defaultValue(defaultValue: U): TimestampWithTimeZoneColumnBuilderNotNull { 30 | props.defaultValue = defaultValue 31 | return TimestampWithTimeZoneColumnBuilderNotNull(entityGetter, props) 32 | } 33 | } 34 | 35 | public class Time9ColumnBuilderNotNull internal constructor( 36 | entityGetter: (T) -> U? 37 | ) : ColumnNotNullBuilder, T, U>(SqlType.TIME9, entityGetter) { 38 | 39 | internal constructor(entityGetter: (T) -> U?, props: ColumnBuilderProps) : this(entityGetter) { 40 | this.props = props 41 | } 42 | 43 | override fun build() = with(props) { 44 | Time9ColumnNotNull(entityGetter, columnName, sqlType, isPK, pkName, defaultValue, fkClass, fkName) 45 | } 46 | } 47 | 48 | public class Time9ColumnBuilderNullable internal constructor( 49 | entityGetter: (T) -> U? 50 | ) : ColumnNullableBuilder, T, U>(SqlType.TIME9, entityGetter) { 51 | override fun build() = with(props) { 52 | Time9ColumnNullable(entityGetter, columnName, sqlType, fkClass, fkName) 53 | } 54 | 55 | override fun defaultValue(defaultValue: U): Time9ColumnBuilderNotNull { 56 | props.defaultValue = defaultValue 57 | return Time9ColumnBuilderNotNull(entityGetter, props) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/sqlite/SqLiteColumnDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.sqlite 6 | 7 | import com.pullvert.kotysa.* 8 | import java.time.LocalDate 9 | import java.time.LocalDateTime 10 | import java.time.LocalTime 11 | import java.time.OffsetDateTime 12 | 13 | /** 14 | * see [SqLite Data types](https://www.sqlite.org/datatype3.html) 15 | */ 16 | public class SqLiteColumnDsl internal constructor( 17 | init: SqLiteColumnDsl.(TableColumnPropertyProvider) -> ColumnBuilder<*, T, *> 18 | ) : ColumnDsl>(init) { 19 | 20 | public fun NotNullStringColumnProperty.text(): TextColumnBuilderNotNull = 21 | TextColumnBuilderNotNull(getter) 22 | 23 | public fun NullableStringColumnProperty.text(): TextColumnBuilderNullable = 24 | TextColumnBuilderNullable(getter) 25 | 26 | public fun NotNullLocalDateTimeColumnProperty.text(): TextColumnBuilderNotNull = 27 | TextColumnBuilderNotNull(getter) 28 | 29 | public fun NullableLocalDateTimeColumnProperty.text(): TextColumnBuilderNullable = 30 | TextColumnBuilderNullable(getter) 31 | 32 | public fun NotNullLocalDateColumnProperty.text(): TextColumnBuilderNotNull = 33 | TextColumnBuilderNotNull(getter) 34 | 35 | public fun NullableLocalDateColumnProperty.text(): TextColumnBuilderNullable = 36 | TextColumnBuilderNullable(getter) 37 | 38 | public fun NotNullOffsetDateTimeColumnProperty.text(): TextColumnBuilderNotNull = 39 | TextColumnBuilderNotNull(getter) 40 | 41 | public fun NullableOffsetDateTimeColumnProperty.text(): TextColumnBuilderNullable = 42 | TextColumnBuilderNullable(getter) 43 | 44 | public fun NotNullLocalTimeColumnProperty.text(): TextColumnBuilderNotNull = 45 | TextColumnBuilderNotNull(getter) 46 | 47 | public fun NullableLocalTimeColumnProperty.text(): TextColumnBuilderNullable = 48 | TextColumnBuilderNullable(getter) 49 | 50 | public fun NotNullBooleanColumnProperty.integer(): IntegerColumnBuilderNotNull = 51 | IntegerColumnBuilderNotNull(getter) 52 | 53 | public fun NotNullIntColumnProperty.integer(): IntegerColumnBuilderNotNull = 54 | IntegerColumnBuilderNotNull(getter) 55 | 56 | public fun NullableIntColumnProperty.integer(): IntegerColumnBuilderNullable = 57 | IntegerColumnBuilderNullable(getter) 58 | } 59 | -------------------------------------------------------------------------------- /kotysa-android/src/test/kotlin/com/pullvert/kotysa/android/SqLiteSelectTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android 6 | 7 | import android.database.sqlite.SQLiteOpenHelper 8 | import com.pullvert.kotysa.NonUniqueResultException 9 | import com.pullvert.kotysa.Tables 10 | import com.pullvert.kotysa.test.* 11 | import org.assertj.core.api.Assertions.assertThat 12 | import org.assertj.core.api.Assertions.assertThatThrownBy 13 | import org.junit.Test 14 | 15 | class SqLiteSelectTest : AbstractSqLiteTest() { 16 | 17 | override fun getRepository(dbHelper: DbHelper, sqLiteTables: Tables) = 18 | UserRepositorySelect(dbHelper, sqLiteTables) 19 | 20 | @Test 21 | fun `Verify selectAll returns all users`() { 22 | assertThat(repository.selectAll()) 23 | .hasSize(2) 24 | .containsExactlyInAnyOrder(sqLiteJdoe, sqLiteBboss) 25 | } 26 | 27 | @Test 28 | fun `Verify selectOneNonUnique throws NonUniqueResultException`() { 29 | assertThatThrownBy { repository.selectOneNonUnique() } 30 | .isInstanceOf(NonUniqueResultException::class.java) 31 | } 32 | 33 | @Test 34 | fun `Verify selectAllMappedToDto does the mapping`() { 35 | assertThat(repository.selectAllMappedToDto().toList()) 36 | .hasSize(2) 37 | .containsExactlyInAnyOrder( 38 | UserDto("John Doe", null), 39 | UserDto("Big Boss", "TheBoss")) 40 | } 41 | 42 | @Test 43 | fun `Verify selectWithJoin works correctly`() { 44 | assertThat(repository.selectWithJoin()) 45 | .hasSize(2) 46 | .containsExactlyInAnyOrder( 47 | UserWithRoleDto(sqLiteJdoe.lastname, sqLiteUser.label), 48 | UserWithRoleDto(sqLiteBboss.lastname, sqLiteAdmin.label) 49 | ) 50 | } 51 | } 52 | 53 | class UserRepositorySelect(sqLiteOpenHelper: SQLiteOpenHelper, tables: Tables) : AbstractUserRepository(sqLiteOpenHelper, tables) { 54 | 55 | fun selectOneNonUnique() = sqlClient.select() 56 | .fetchOne() 57 | 58 | fun selectAllMappedToDto() = 59 | sqlClient.select { 60 | UserDto("${it[SqLiteUser::firstname]} ${it[SqLiteUser::lastname]}", 61 | it[SqLiteUser::alias]) 62 | }.fetchAll() 63 | 64 | fun selectWithJoin() = 65 | sqlClient.select { UserWithRoleDto(it[SqLiteUser::lastname], it[SqLiteRole::label]) } 66 | .innerJoin().on { it[SqLiteUser::roleId] } 67 | .fetchAll() 68 | } 69 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/TablesDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import kotlin.reflect.KClass 8 | 9 | 10 | @KotysaMarker 11 | public abstract class TablesDsl, U : TableDsl<*, *>> protected constructor(private val init: T.() -> Unit) { 12 | 13 | private val allTables = mutableMapOf, Table<*>>() 14 | private val allColumns = mutableMapOf<(Any) -> Any?, Column<*, *>>() 15 | 16 | @PublishedApi 17 | internal fun table(tableClass: KClass, dsl: U.() -> Unit) { 18 | check(!allTables.containsKey(tableClass)) { 19 | "Trying to map entity class \"${tableClass.qualifiedName}\" to multiple tables" 20 | } 21 | val table = initializeTable(tableClass, dsl) 22 | allTables[tableClass] = table 23 | @Suppress("UNCHECKED_CAST") 24 | allColumns.putAll(table.columns as Map Any?, Column<*, *>>) 25 | } 26 | 27 | protected abstract fun initializeTable(tableClass: KClass, dsl: U.() -> Unit): Table<*> 28 | 29 | internal fun initialize(initialize: T, dbType: DbType): Tables { 30 | init(initialize) 31 | require(allTables.isNotEmpty()) { "Tables must declare at least one table" } 32 | val tables = Tables(allTables, allColumns, dbType) 33 | 34 | // resolve foreign keys to referenced primary key column 35 | resolveFkReferencedColumn(allColumns, tables) 36 | 37 | // build foreign keys 38 | buildForeignKeys(allTables) 39 | 40 | return tables 41 | } 42 | 43 | private fun resolveFkReferencedColumn(allColumns: MutableMap<(Any) -> Any?, Column<*, *>>, tables: Tables) { 44 | allColumns.filterValues { column -> column.fkClass != null } 45 | .forEach { (_, column) -> 46 | val referencedTable = tables.getTable(column.fkClass!!) 47 | val referencedTablePK = referencedTable.primaryKey 48 | require(referencedTablePK is SinglePrimaryKey<*, *>) { 49 | "Only table with single column primary key is currently supported, ${referencedTable.name} is not" 50 | } 51 | column.fkColumn = referencedTablePK.column 52 | } 53 | } 54 | 55 | private fun buildForeignKeys(allTables: MutableMap, Table<*>>) { 56 | val foreignKeyNames = mutableSetOf() 57 | allTables.values.forEach { table -> 58 | val foreignKeys = mutableSetOf() 59 | // first loop with user provided FK names 60 | table.columns.values.mapNotNull { column -> column.fkName } 61 | .forEach { fkName -> 62 | require(!foreignKeyNames.contains(fkName)) { 63 | "Foreign key names must be unique, $fkName is duplicated" 64 | } 65 | foreignKeyNames.add(fkName) 66 | } 67 | 68 | // then complete loop on all foreign keys 69 | table.columns.filterValues { column -> column.fkColumn != null } 70 | .forEach { (_, column) -> 71 | foreignKeys.add(SingleForeignKey(column.fkName, column, column.fkColumn!!)) 72 | } 73 | table.foreignKeys = foreignKeys 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/FieldAccess.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.LocalTime 10 | import java.time.OffsetDateTime 11 | import java.util.* 12 | 13 | 14 | @Suppress("UNCHECKED_CAST") 15 | internal class FieldAccess internal constructor( 16 | private val availableColumns: Map Any?, Column<*, *>>, 17 | private val dbType: DbType 18 | ) { 19 | 20 | internal fun getField(getter: (T) -> String, alias: String?) = 21 | NotNullStringColumnField(availableColumns, getter, dbType, alias) 22 | 23 | internal fun getField(getter: (T) -> String?, alias: String?) = 24 | NullableStringColumnField(availableColumns, getter, dbType, alias) 25 | 26 | internal fun getField(getter: (T) -> LocalDateTime, alias: String?) = 27 | NotNullLocalDateTimeColumnField(availableColumns, getter, dbType, alias) 28 | 29 | internal fun getField(getter: (T) -> LocalDateTime?, alias: String?) = 30 | NullableLocalDateTimeColumnField(availableColumns, getter, dbType, alias) 31 | 32 | internal fun getField(getter: (T) -> LocalDate, alias: String?) = 33 | NotNullLocalDateColumnField(availableColumns, getter, dbType, alias) 34 | 35 | internal fun getField(getter: (T) -> LocalDate?, alias: String?) = 36 | NullableLocalDateColumnField(availableColumns, getter, dbType, alias) 37 | 38 | internal fun getField(getter: (T) -> OffsetDateTime, alias: String?) = 39 | NotNullOffsetDateTimeColumnField(availableColumns, getter, dbType, alias) 40 | 41 | internal fun getField(getter: (T) -> OffsetDateTime?, alias: String?) = 42 | NullableOffsetDateTimeColumnField(availableColumns, getter, dbType, alias) 43 | 44 | internal fun getField(getter: (T) -> LocalTime, alias: String?) = 45 | NotNullLocalTimeColumnField(availableColumns, getter, dbType, alias) 46 | 47 | internal fun getField(getter: (T) -> LocalTime?, alias: String?) = 48 | NullableLocalTimeColumnField(availableColumns, getter, dbType, alias) 49 | 50 | internal fun getField(getter: (T) -> Boolean, alias: String?) = 51 | NotNullBooleanColumnField(availableColumns, getter, dbType, alias) 52 | 53 | internal fun getField(getter: (T) -> UUID, alias: String?) = 54 | NotNullUuidColumnField(availableColumns, getter, dbType, alias) 55 | 56 | internal fun getField(getter: (T) -> UUID?, alias: String?) = 57 | NullableUuidColumnField(availableColumns, getter, dbType, alias) 58 | 59 | internal fun getField(getter: (T) -> Int, alias: String?) = 60 | NotNullIntColumnField(availableColumns, getter, dbType, alias) 61 | 62 | internal fun getField(getter: (T) -> Int?, alias: String?) = 63 | NullableIntColumnField(availableColumns, getter, dbType, alias) 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) 2 | [![Download](https://api.bintray.com/packages/pull-vert/kotysa/kotysa/images/download.svg) ](https://bintray.com/pull-vert/kotysa/kotysa/_latestVersion) 3 | 4 | # Kotysa 5 | 6 | The idiomatic way to write **Ko**tlin **ty**pe-**sa**fe SQL. 7 | 8 | :exclamation: **This project has migrated to [new location](https://github.com/ufoss-org/kotysa) :exclamation: 9 | 10 | ## Easy to use : 3 steps only 11 | ### step 1 -> Create Kotlin entities 12 | data classes are great for that ! 13 | ```kotlin 14 | data class Role( 15 | val label: String, 16 | val id: UUID = UUID.randomUUID() 17 | ) 18 | 19 | data class User( 20 | val firstname: String, 21 | val roleId: UUID, 22 | val alias: String? = null, 23 | val id: UUID = UUID.randomUUID() 24 | ) 25 | ``` 26 | 27 | ### step 2 -> Describe database model with [type-safe DSL](docs/table-modelling.md), based on these entities 28 | ```kotlin 29 | val tables = 30 | tables().postgresql { // choose database type 31 | table { 32 | name = "roles" 33 | column { it[Role::id].uuid().primaryKey } 34 | column { it[Role::label].varchar() } 35 | } 36 | table { 37 | name = "users" 38 | column { it[User::id].uuid().primaryKey } 39 | column { it[User::firstname].varchar().name("first-name") } 40 | column { it[User::roleId].uuid().foreignKey() } 41 | column { it[User::alias].varchar() } 42 | } 43 | } 44 | ``` 45 | 46 | ### step 3 -> Write SQL queries with [type-safe SqlClient DSL](docs/sql-queries.md) 47 | Kotysa will generate SQL for you ! 48 | ```kotlin 49 | // return all admin users 50 | val admins = sqlClient.select() 51 | .innerJoin().on { it[User::roleId] } 52 | .where { it[Role::label] eq "admin" } 53 | .fetchAll() 54 | ``` 55 | 56 | **No annotations, no code generation, just regular Kotlin code ! No JPA, just pure SQL !** 57 | 58 | ## Getting started 59 | Kotysa is agnostic from Sql Engine (SqLite on Android, R2DBC, JDBC in the future) : 60 | * use Kotysa with [Spring data R2DBC](kotysa-spring-data-r2dbc/README.md) 61 | * use Kotysa with [SqLite on Android](kotysa-android/README.md) 62 | 63 | See sample projects [here](samples). 64 | 65 | Kotysa provides [Kotlin Coroutines first class support with R2DBC](kotysa-spring-data-r2dbc/README.md#coroutines-first-class-support) 66 | 67 | Kotysa is **not production ready yet**, some key features are still missing. Regular early releases will provide new features (see [next milestones](https://github.com/pull-vert/kotysa/milestones)). 68 | 69 | Type safety relies on type and nullability of the Entity property (or getter). 70 | 71 | ## Build from sources 72 | 73 | * Clone Kotysa repository 74 | * Use a JDK 1.8 75 | * You need a local docker, like docker-ce. Some integration tests use testcontainers to start real databases like PostgreSQL 76 | * Kotysa can be easily built with the gradle wrapper 77 | 78 | ```bash 79 | $ ./gradlew clean buildNeeded 80 | ``` 81 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/h2/R2DbcSelectH2Test.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.h2 6 | 7 | import com.pullvert.kotysa.NonUniqueResultException 8 | import com.pullvert.kotysa.count 9 | import com.pullvert.kotysa.test.* 10 | import org.assertj.core.api.Assertions.assertThat 11 | import org.assertj.core.api.Assertions.assertThatThrownBy 12 | import org.junit.jupiter.api.Test 13 | import org.springframework.data.r2dbc.core.DatabaseClient 14 | 15 | 16 | class R2DbcSelectH2Test : AbstractR2dbcH2Test() { 17 | override val context = startContext() 18 | 19 | override val repository = getContextRepository() 20 | 21 | @Test 22 | fun `Verify selectAll returns all users`() { 23 | assertThat(repository.selectAllUsers().toIterable()) 24 | .hasSize(2) 25 | .containsExactlyInAnyOrder(h2Jdoe, h2Bboss) 26 | } 27 | 28 | @Test 29 | fun `Verify countUsers returns 2`() { 30 | assertThat(repository.countAllUsers().block()!!) 31 | .isEqualTo(2L) 32 | } 33 | 34 | @Test 35 | fun `Verify countUsers with alias returns 1`() { 36 | assertThat(repository.countUsersWithAlias().block()!!) 37 | .isEqualTo(1L) 38 | } 39 | 40 | @Test 41 | fun `Verify selectOneNonUnique throws NonUniqueResultException`() { 42 | assertThatThrownBy { repository.selectOneNonUnique().block() } 43 | .isInstanceOf(NonUniqueResultException::class.java) 44 | } 45 | 46 | @Test 47 | fun `Verify selectAllMappedToDto does the mapping`() { 48 | assertThat(repository.selectAllMappedToDto().toIterable()) 49 | .hasSize(2) 50 | .containsExactlyInAnyOrder( 51 | UserDto("${h2Jdoe.firstname} ${h2Jdoe.lastname}", h2Jdoe.alias), 52 | UserDto("${h2Bboss.firstname} ${h2Bboss.lastname}", h2Bboss.alias)) 53 | } 54 | 55 | @Test 56 | fun `Verify selectWithJoin works correctly`() { 57 | assertThat(repository.selectWithJoin().toIterable()) 58 | .hasSize(2) 59 | .containsExactlyInAnyOrder( 60 | UserWithRoleDto(h2Jdoe.lastname, h2User.label), 61 | UserWithRoleDto(h2Bboss.lastname, h2Admin.label) 62 | ) 63 | } 64 | } 65 | 66 | 67 | class UserRepositoryH2Select(dbClient: DatabaseClient) : AbstractUserRepositoryH2(dbClient) { 68 | 69 | fun countAllUsers() = sqlClient.countAll() 70 | 71 | fun countUsersWithAlias() = sqlClient.select { count { it[H2User::alias] } }.fetchOne() 72 | 73 | fun selectOneNonUnique() = sqlClient.select() 74 | .fetchOne() 75 | 76 | fun selectAllMappedToDto() = 77 | sqlClient.select { 78 | UserDto("${it[H2User::firstname]} ${it[H2User::lastname]}", 79 | it[H2User::alias]) 80 | }.fetchAll() 81 | 82 | fun selectWithJoin() = 83 | sqlClient.select { UserWithRoleDto(it[H2User::lastname], it[H2Role::label]) } 84 | .innerJoin().on { it[H2User::roleId] } 85 | .fetchAll() 86 | } 87 | -------------------------------------------------------------------------------- /kotysa-android/src/test/kotlin/com/pullvert/kotysa/android/sample/sqlClientSqLite.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android.sample 6 | 7 | import android.database.sqlite.SQLiteOpenHelper 8 | import com.pullvert.kotysa.android.sqlClient 9 | 10 | @Suppress("UNUSED_VARIABLE") 11 | class UserRepositorySqLite(sqLiteOpenHelper: SQLiteOpenHelper) { 12 | 13 | data class Role( 14 | val label: String, 15 | val id: String 16 | ) 17 | 18 | data class User( 19 | val firstname: String, 20 | val lastname: String, 21 | val isAdmin: Boolean, 22 | val roleId: String, 23 | val alias: String? = null, 24 | val id: String 25 | ) 26 | 27 | val tables = 28 | com.pullvert.kotysa.tables().sqlite { 29 | // choose database type 30 | table { 31 | name = "roles" 32 | column { it[Role::id].text().primaryKey() } 33 | column { it[Role::label].text() } 34 | } 35 | table { 36 | name = "users" 37 | column { it[User::id].text().primaryKey() } 38 | column { it[User::firstname].text().name("fname") } 39 | column { it[User::lastname].text().name("lname") } 40 | column { it[User::isAdmin].integer() } 41 | column { it[User::roleId].text().foreignKey() } 42 | column { it[User::alias].text() } 43 | } 44 | } 45 | 46 | val roleUser = Role("user", "ghi") 47 | val roleAdmin = Role("admin", "jkl") 48 | 49 | val userJdoe = User("John", "Doe", false, roleUser.id, id = "abc") 50 | val userBboss = User("Big", "Boss", true, roleAdmin.id, "TheBoss", "def") 51 | 52 | private class UserWithRoleDto( 53 | val lastname: String, 54 | val role: String 55 | ) 56 | 57 | private val sqlClient = sqLiteOpenHelper.sqlClient(tables) 58 | 59 | fun simplifiedExample() = sqlClient.run { 60 | createTable() 61 | deleteAllFromTable() 62 | insert(userJdoe, userBboss) 63 | 64 | val count = countAll() 65 | 66 | val all = selectAll() 67 | 68 | val johny = select { UserWithRoleDto(it[User::lastname], it[Role::label]) } 69 | .innerJoin().on { it[User::roleId] } 70 | .where { it[User::alias] eq "Johny" } 71 | // null String accepted ^^^^^ , if alias=null, gives "WHERE user.alias IS NULL" 72 | .or { it[User::alias] eq "Johnny" } 73 | .fetchFirst() 74 | 75 | val nbUpdated = updateTable() 76 | .set { it[User::lastname] = "NewLastName" } 77 | .innerJoin().on { it[User::roleId] } 78 | .where { it[Role::label] eq roleUser.label } 79 | // null String forbidden ^^^^^^^^^^^^ 80 | .execute() 81 | 82 | val nbDeleted = deleteFromTable() 83 | .innerJoin().on { it[User::roleId] } 84 | .where { it[Role::label] eq roleUser.label } 85 | .execute() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/sample/coroutinesSqlClientR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.sample 6 | 7 | import com.pullvert.kotysa.r2dbc.coSqlClient 8 | import com.pullvert.kotysa.tables 9 | import org.springframework.data.r2dbc.core.DatabaseClient 10 | import java.util.* 11 | 12 | 13 | @Suppress("UNUSED_VARIABLE") 14 | class UserRepositoryR2dbcCoroutines(dbClient: DatabaseClient) { 15 | 16 | private data class Role( 17 | val label: String, 18 | val id: UUID = UUID.randomUUID() 19 | ) 20 | 21 | private data class User( 22 | val firstname: String, 23 | val lastname: String, 24 | val isAdmin: Boolean, 25 | val roleId: UUID, 26 | val alias: String? = null, 27 | val id: UUID = UUID.randomUUID() 28 | ) 29 | 30 | private val tables = 31 | tables().h2 { 32 | table { 33 | name = "roles" 34 | column { it[Role::id].uuid().primaryKey() } 35 | column { it[Role::label].varchar() } 36 | } 37 | table { 38 | name = "users" 39 | column { it[User::id].uuid().primaryKey() } 40 | column { it[User::firstname].varchar().name("fname") } 41 | column { it[User::lastname].varchar().name("lname") } 42 | column { it[User::isAdmin].boolean() } 43 | column { it[User::roleId].uuid().foreignKey() } 44 | column { it[User::alias].varchar() } 45 | } 46 | } 47 | 48 | private val roleUser = Role("user") 49 | private val roleAdmin = Role("admin") 50 | 51 | private val userJdoe = User("John", "Doe", false, roleUser.id) 52 | private val userBboss = User("Big", "Boss", true, roleAdmin.id, "TheBoss") 53 | 54 | private class UserWithRoleDto( 55 | val lastname: String, 56 | val role: String 57 | ) 58 | 59 | private val sqlClient = dbClient.coSqlClient(tables) 60 | 61 | suspend fun simplifiedExample() = sqlClient.apply { 62 | createTable() 63 | deleteAllFromTable() 64 | insert(userJdoe, userBboss) 65 | 66 | val count = countAll() 67 | 68 | val all = selectAll() 69 | 70 | val johny = select { UserWithRoleDto(it[User::lastname], it[Role::label]) } 71 | .innerJoin().on { it[User::roleId] } 72 | .where { it[User::alias] eq "Johny" } 73 | // null String accepted ^^^^^ , if alias=null, gives "WHERE user.alias IS NULL" 74 | .or { it[User::alias] eq "Johnny" } 75 | .fetchFirst() 76 | 77 | val nbUpdated = updateTable() 78 | .set { it[User::lastname] = "NewLastName" } 79 | .innerJoin().on { it[User::roleId] } 80 | .where { it[Role::label] eq roleUser.label } 81 | // null String forbidden ^^^^^^^^^^^^ 82 | .execute() 83 | 84 | val nbDeleted = deleteFromTable() 85 | .innerJoin().on { it[User::roleId] } 86 | .where { it[Role::label] eq roleUser.label } 87 | .execute() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | @rem Execute Gradle 88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 89 | 90 | :end 91 | @rem End local scope for the variables with windows NT shell 92 | if "%ERRORLEVEL%"=="0" goto mainEnd 93 | 94 | :fail 95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 96 | rem the _cmd.exe /c_ return code! 97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 98 | exit /b 1 99 | 100 | :mainEnd 101 | if "%OS%"=="Windows_NT" endlocal 102 | 103 | :omega 104 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/sample/sqlClientR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.sample 6 | 7 | import com.pullvert.kotysa.r2dbc.sqlClient 8 | import com.pullvert.kotysa.tables 9 | import org.springframework.data.r2dbc.core.DatabaseClient 10 | import java.util.* 11 | 12 | 13 | 14 | @Suppress("UNUSED_VARIABLE") 15 | class UserRepositoryR2dbc(dbClient: DatabaseClient) { 16 | 17 | private data class Role( 18 | val label: String, 19 | val id: UUID = UUID.randomUUID() 20 | ) 21 | 22 | private data class User( 23 | val firstname: String, 24 | val lastname: String, 25 | val isAdmin: Boolean, 26 | val roleId: UUID, 27 | val alias: String? = null, 28 | val id: UUID = UUID.randomUUID() 29 | ) 30 | 31 | private val tables = 32 | tables().h2 { 33 | table { 34 | name = "roles" 35 | column { it[Role::id].uuid().primaryKey() } 36 | column { it[Role::label].varchar() } 37 | } 38 | table { 39 | name = "users" 40 | column { it[User::id].uuid().primaryKey() } 41 | column { it[User::firstname].varchar().name("fname") } 42 | column { it[User::lastname].varchar().name("lname") } 43 | column { it[User::isAdmin].boolean() } 44 | column { it[User::roleId].uuid().foreignKey() } 45 | column { it[User::alias].varchar() } 46 | } 47 | } 48 | 49 | private val roleUser = Role("user") 50 | private val roleAdmin = Role("admin") 51 | 52 | private val userJdoe = User("John", "Doe", false, roleUser.id) 53 | private val userBboss = User("Big", "Boss", true, roleAdmin.id, "TheBoss") 54 | 55 | private class UserWithRoleDto( 56 | val lastname: String, 57 | val role: String 58 | ) 59 | 60 | private val sqlClient = dbClient.sqlClient(tables) 61 | 62 | fun simplifiedExample() = sqlClient.apply { 63 | createTable() // CREATE TABLE IF NOT EXISTS 64 | .then(deleteAllFromTable()) 65 | .then(insert(userJdoe, userBboss)) 66 | 67 | val count = countAll() 68 | 69 | val all = selectAll() 70 | 71 | val johny = select { UserWithRoleDto(it[User::lastname], it[Role::label]) } 72 | .innerJoin().on { it[User::roleId] } 73 | .where { it[User::alias] eq "Johny" } 74 | // null String accepted ^^^^^ , if alias=null, gives "WHERE user.alias IS NULL" 75 | .or { it[User::alias] eq "Johnny" } 76 | .fetchFirst() 77 | 78 | val nbUpdated = updateTable() 79 | .set { it[User::lastname] = "NewLastName" } 80 | .innerJoin().on { it[User::roleId] } 81 | .where { it[Role::label] eq roleUser.label } 82 | // null String forbidden ^^^^^^^^^^^^ 83 | .execute() 84 | 85 | val nbDeleted = deleteFromTable() 86 | .innerJoin().on { it[User::roleId] } 87 | .where { it[Role::label] eq roleUser.label } 88 | .execute() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/h2/H2ColumnDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.h2 6 | 7 | import com.pullvert.kotysa.* 8 | import java.time.LocalDate 9 | import java.time.LocalDateTime 10 | import java.time.LocalTime 11 | import java.time.OffsetDateTime 12 | import java.util.* 13 | 14 | /** 15 | * see [H2 Data types](http://h2database.com/html/datatypes.html) 16 | */ 17 | public class H2ColumnDsl internal constructor( 18 | init: H2ColumnDsl.(TableColumnPropertyProvider) -> ColumnBuilder<*, T, *> 19 | ) : ColumnDsl>(init) { 20 | 21 | public fun NotNullStringColumnProperty.varchar(): VarcharColumnBuilderNotNull = 22 | VarcharColumnBuilderNotNull(getter) 23 | 24 | public fun NullableStringColumnProperty.varchar(): VarcharColumnBuilderNullable = 25 | VarcharColumnBuilderNullable(getter) 26 | 27 | public fun NotNullLocalDateTimeColumnProperty.timestamp(): TimestampColumnBuilderNotNull = 28 | TimestampColumnBuilderNotNull(getter) 29 | 30 | public fun NullableLocalDateTimeColumnProperty.timestamp(): TimestampColumnBuilderNullable = 31 | TimestampColumnBuilderNullable(getter) 32 | 33 | public fun NotNullLocalDateTimeColumnProperty.dateTime(): DateTimeColumnBuilderNotNull = 34 | DateTimeColumnBuilderNotNull(getter) 35 | 36 | public fun NullableLocalDateTimeColumnProperty.dateTime(): DateTimeColumnBuilderNullable = 37 | DateTimeColumnBuilderNullable(getter) 38 | 39 | public fun NotNullLocalDateColumnProperty.date(): DateColumnBuilderNotNull = 40 | DateColumnBuilderNotNull(getter) 41 | 42 | public fun NullableLocalDateColumnProperty.date(): DateColumnBuilderNullable = 43 | DateColumnBuilderNullable(getter) 44 | 45 | public fun NotNullOffsetDateTimeColumnProperty.timestampWithTimeZone() 46 | : TimestampWithTimeZoneColumnBuilderNotNull =TimestampWithTimeZoneColumnBuilderNotNull(getter) 47 | 48 | public fun NullableOffsetDateTimeColumnProperty.timestampWithTimeZone() 49 | : TimestampWithTimeZoneColumnBuilderNullable = TimestampWithTimeZoneColumnBuilderNullable(getter) 50 | 51 | public fun NotNullLocalTimeColumnProperty.time9(): Time9ColumnBuilderNotNull = 52 | Time9ColumnBuilderNotNull(getter) 53 | 54 | public fun NullableLocalTimeColumnProperty.time9(): Time9ColumnBuilderNullable = 55 | Time9ColumnBuilderNullable(getter) 56 | 57 | public fun NotNullBooleanColumnProperty.boolean(): BooleanColumnBuilderNotNull = 58 | BooleanColumnBuilderNotNull(getter) 59 | 60 | public fun NotNullUuidColumnProperty.uuid(): UuidColumnBuilderNotNull = 61 | UuidColumnBuilderNotNull(getter) 62 | 63 | public fun NullableUuidColumnProperty.uuid(): UuidColumnBuilderNullable = 64 | UuidColumnBuilderNullable(getter) 65 | 66 | public fun NotNullIntColumnProperty.integer(): IntegerColumnBuilderNotNull = 67 | IntegerColumnBuilderNotNull(getter) 68 | 69 | public fun NullableIntColumnProperty.integer(): IntegerColumnBuilderNullable = 70 | IntegerColumnBuilderNullable(getter) 71 | } 72 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/postgresql/R2DbcSelectPostgresqlTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.postgresql 6 | 7 | import com.pullvert.kotysa.NonUniqueResultException 8 | import com.pullvert.kotysa.count 9 | import com.pullvert.kotysa.test.* 10 | import org.assertj.core.api.Assertions.assertThat 11 | import org.assertj.core.api.Assertions.assertThatThrownBy 12 | import org.junit.jupiter.api.Test 13 | import org.springframework.data.r2dbc.core.DatabaseClient 14 | 15 | 16 | class R2DbcSelectPostgresqlTest : AbstractR2dbcPostgresqlTest() { 17 | override val context = startContext() 18 | 19 | override val repository = getContextRepository() 20 | 21 | @Test 22 | fun `Verify selectAll returns all users`() { 23 | assertThat(repository.selectAllUsers().toIterable()) 24 | .hasSize(2) 25 | .containsExactlyInAnyOrder(postgresqlJdoe, postgresqlBboss) 26 | } 27 | 28 | @Test 29 | fun `Verify countUsers returns 2`() { 30 | assertThat(repository.countAllUsers().block()!!) 31 | .isEqualTo(2L) 32 | } 33 | 34 | @Test 35 | fun `Verify countUsers with alias returns 1`() { 36 | assertThat(repository.countUsersWithAlias().block()!!) 37 | .isEqualTo(1L) 38 | } 39 | 40 | @Test 41 | fun `Verify selectOneNonUnique throws NonUniqueResultException`() { 42 | assertThatThrownBy { repository.selectOneNonUnique().block() } 43 | .isInstanceOf(NonUniqueResultException::class.java) 44 | } 45 | 46 | @Test 47 | fun `Verify selectAllMappedToDto does the mapping`() { 48 | assertThat(repository.selectAllMappedToDto().toIterable()) 49 | .hasSize(2) 50 | .containsExactlyInAnyOrder( 51 | UserDto("${postgresqlJdoe.firstname} ${postgresqlJdoe.lastname}", postgresqlJdoe.alias), 52 | UserDto("${postgresqlBboss.firstname} ${postgresqlBboss.lastname}", postgresqlBboss.alias)) 53 | } 54 | 55 | @Test 56 | fun `Verify selectWithJoin works correctly`() { 57 | assertThat(repository.selectWithJoin().toIterable()) 58 | .hasSize(2) 59 | .containsExactlyInAnyOrder( 60 | UserWithRoleDto(postgresqlJdoe.lastname, postgresqlUser.label), 61 | UserWithRoleDto(postgresqlBboss.lastname, postgresqlAdmin.label) 62 | ) 63 | } 64 | } 65 | 66 | 67 | class UserRepositoryPostgresqlSelect(dbClient: DatabaseClient) : AbstractUserRepositoryPostgresql(dbClient) { 68 | 69 | fun countAllUsers() = sqlClient.countAll() 70 | 71 | fun countUsersWithAlias() = sqlClient.select { count { it[PostgresqlUser::alias] } }.fetchOne() 72 | 73 | fun selectOneNonUnique() = sqlClient.select() 74 | .fetchOne() 75 | 76 | fun selectAllMappedToDto() = 77 | sqlClient.select { 78 | UserDto("${it[PostgresqlUser::firstname]} ${it[PostgresqlUser::lastname]}", 79 | it[PostgresqlUser::alias]) 80 | }.fetchAll() 81 | 82 | fun selectWithJoin() = 83 | sqlClient.select { UserWithRoleDto(it[PostgresqlUser::lastname], it[PostgresqlRole::label]) } 84 | .innerJoin().on { it[PostgresqlUser::roleId] } 85 | .fetchAll() 86 | } 87 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/h2/R2DbcInheritanceH2Test.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.h2 6 | 7 | import com.pullvert.kotysa.r2dbc.Repository 8 | import com.pullvert.kotysa.r2dbc.sqlClient 9 | import com.pullvert.kotysa.tables 10 | import com.pullvert.kotysa.test.Entity 11 | import com.pullvert.kotysa.test.Inherited 12 | import com.pullvert.kotysa.test.Nameable 13 | import com.pullvert.kotysa.test.inherited 14 | import org.assertj.core.api.Assertions.assertThat 15 | import org.junit.jupiter.api.Test 16 | import org.springframework.data.r2dbc.core.DatabaseClient 17 | 18 | 19 | class R2DbcInheritanceH2Test : AbstractR2dbcH2Test() { 20 | override val context = startContext() 21 | 22 | override val repository = getContextRepository() 23 | 24 | @Test 25 | fun `Verify extension function selectById finds inherited`() { 26 | assertThat(repository.selectById("id").block()) 27 | .isEqualTo(inherited) 28 | } 29 | 30 | @Test 31 | fun `Verify selectInheritedById finds inherited`() { 32 | assertThat(repository.selectInheritedById("id").block()) 33 | .isEqualTo(inherited) 34 | } 35 | 36 | @Test 37 | fun `Verify selectFirstByName finds inherited`() { 38 | assertThat(repository.selectFirstByName("name").block()) 39 | .isEqualTo(inherited) 40 | } 41 | 42 | @Test 43 | fun `Verify deleteById deletes inherited`() { 44 | assertThat(repository.deleteById("id").block()!!) 45 | .isEqualTo(1) 46 | assertThat(repository.selectAll().toIterable()) 47 | .isEmpty() 48 | // re-insert 49 | repository.insert().block() 50 | } 51 | } 52 | 53 | private val tables = 54 | tables().h2 { 55 | table { 56 | name = "inherited" 57 | column { it[Inherited::getId].varchar().primaryKey() } 58 | column { it[Inherited::name].varchar() } 59 | column { it[Inherited::firstname].varchar() } 60 | } 61 | } 62 | 63 | 64 | class InheritanceH2Repository(dbClient: DatabaseClient) : Repository { 65 | 66 | val sqlClient = dbClient.sqlClient(tables) 67 | 68 | override fun init() { 69 | createTable() 70 | .then(insert()) 71 | .block() 72 | } 73 | 74 | override fun delete() { 75 | deleteAll() 76 | .block() 77 | } 78 | 79 | private fun createTable() = sqlClient.createTable() 80 | 81 | fun insert() = sqlClient.insert(inherited) 82 | 83 | private fun deleteAll() = sqlClient.deleteAllFromTable() 84 | 85 | fun selectAll() = sqlClient.selectAll() 86 | 87 | fun selectInheritedById(id: String) = 88 | sqlClient.select().where { it[Inherited::getId] eq id }.fetchOne() 89 | } 90 | 91 | inline fun > InheritanceH2Repository.selectById(id: String) = 92 | sqlClient.select().where { it[Entity::getId] eq id }.fetchOne() 93 | 94 | inline fun InheritanceH2Repository.selectFirstByName(name: String) = 95 | sqlClient.select().where { it[Nameable::name] eq name }.fetchFirst() 96 | 97 | inline fun > InheritanceH2Repository.deleteById(id: String) = 98 | sqlClient.deleteFromTable().where { it[Entity::getId] eq id }.execute() 99 | -------------------------------------------------------------------------------- /kotysa-android/src/test/kotlin/com/pullvert/kotysa/android/SqLiteUpdateDeleteTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android 6 | 7 | import android.database.sqlite.SQLiteOpenHelper 8 | import com.pullvert.kotysa.Tables 9 | import com.pullvert.kotysa.test.* 10 | import org.assertj.core.api.Assertions.assertThat 11 | import org.junit.Test 12 | 13 | class SqLiteUpdateDeleteTest : AbstractSqLiteTest() { 14 | 15 | override fun getRepository(dbHelper: DbHelper, sqLiteTables: Tables) = 16 | UserRepositoryUpdateDelete(dbHelper, sqLiteTables) 17 | 18 | @Test 19 | fun `Verify deleteAllFromUser works correctly`() { 20 | assertThat(repository.deleteAllFromUsers()) 21 | .isEqualTo(2) 22 | assertThat(repository.selectAll().toList()) 23 | .isEmpty() 24 | // re-insert users 25 | repository.insertUsers() 26 | } 27 | 28 | @Test 29 | fun `Verify deleteUserById works`() { 30 | assertThat(repository.deleteUserById(sqLiteJdoe.id)) 31 | .isEqualTo(1) 32 | assertThat(repository.selectAll()) 33 | .hasSize(1) 34 | // re-insertUsers jdoe 35 | repository.insertJDoe() 36 | } 37 | 38 | @Test 39 | fun `Verify deleteUserWithJoin works`() { 40 | assertThat(repository.deleteUserWithJoin(sqLiteUser.label)) 41 | .isEqualTo(1) 42 | assertThat(repository.selectAll()) 43 | .hasSize(1) 44 | .containsOnly(sqLiteBboss) 45 | // re-insertUsers jdoe 46 | repository.insertJDoe() 47 | } 48 | 49 | @Test 50 | fun `Verify updateLastname works`() { 51 | assertThat(repository.updateLastname("Do")) 52 | .isEqualTo(1) 53 | assertThat(repository.selectFirstByFirstame(sqLiteJdoe.firstname)) 54 | .extracting { user -> user?.lastname } 55 | .isEqualTo("Do") 56 | repository.updateLastname(sqLiteJdoe.lastname) 57 | } 58 | 59 | @Test 60 | fun `Verify updateWithJoin works`() { 61 | assertThat(repository.updateWithJoin("Do", sqLiteUser.label)) 62 | .isEqualTo(1) 63 | assertThat(repository.selectFirstByFirstame(sqLiteJdoe.firstname)) 64 | .extracting { user -> user?.lastname } 65 | .isEqualTo("Do") 66 | repository.updateLastname(sqLiteJdoe.lastname) 67 | } 68 | } 69 | 70 | class UserRepositoryUpdateDelete(sqLiteOpenHelper: SQLiteOpenHelper, tables: Tables) : AbstractUserRepository(sqLiteOpenHelper, tables) { 71 | 72 | fun deleteUserById(id: String) = sqlClient.deleteFromTable() 73 | .where { it[SqLiteUser::id] eq id } 74 | .execute() 75 | 76 | fun deleteUserWithJoin(roleLabel: String) = sqlClient.deleteFromTable() 77 | .innerJoin().on { it[SqLiteUser::roleId] } 78 | .where { it[SqLiteRole::label] eq roleLabel } 79 | .execute() 80 | 81 | fun updateLastname(newLastname: String) = sqlClient.updateTable() 82 | .set { it[SqLiteUser::lastname] = newLastname } 83 | .where { it[SqLiteUser::id] eq sqLiteJdoe.id } 84 | .execute() 85 | 86 | fun updateWithJoin(newLastname: String, roleLabel: String) = sqlClient.updateTable() 87 | .set { it[SqLiteUser::lastname] = newLastname } 88 | .innerJoin().on { it[SqLiteUser::roleId] } 89 | .where { it[SqLiteRole::label] eq roleLabel } 90 | .execute() 91 | } 92 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/postgresql/PostgresqlColumnDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.postgresql 6 | 7 | import com.pullvert.kotysa.* 8 | import com.pullvert.kotysa.h2.TimestampWithTimeZoneColumnBuilderNotNull 9 | import com.pullvert.kotysa.h2.TimestampWithTimeZoneColumnBuilderNullable 10 | import java.time.LocalDate 11 | import java.time.LocalDateTime 12 | import java.time.LocalTime 13 | import java.time.OffsetDateTime 14 | import java.util.* 15 | 16 | /** 17 | * see [Postgres Data types](https://www.postgresql.org/docs/11/datatype.html) 18 | */ 19 | public class PostgresqlColumnDsl internal constructor( 20 | init: PostgresqlColumnDsl.(TableColumnPropertyProvider) -> ColumnBuilder<*, T, *> 21 | ) : ColumnDsl>(init) { 22 | 23 | public fun NotNullStringColumnProperty.varchar(): VarcharColumnBuilderNotNull = 24 | VarcharColumnBuilderNotNull(getter) 25 | 26 | public fun NullableStringColumnProperty.varchar(): VarcharColumnBuilderNullable = 27 | VarcharColumnBuilderNullable(getter) 28 | 29 | public fun NotNullLocalDateTimeColumnProperty.timestamp(): TimestampColumnBuilderNotNull = 30 | TimestampColumnBuilderNotNull(getter) 31 | 32 | public fun NullableLocalDateTimeColumnProperty.timestamp(): TimestampColumnBuilderNullable = 33 | TimestampColumnBuilderNullable(getter) 34 | 35 | public fun NotNullLocalDateColumnProperty.date(): DateColumnBuilderNotNull = 36 | DateColumnBuilderNotNull(getter) 37 | 38 | public fun NullableLocalDateColumnProperty.date(): DateColumnBuilderNullable = 39 | DateColumnBuilderNullable(getter) 40 | 41 | public fun NotNullOffsetDateTimeColumnProperty.timestampWithTimeZone() 42 | : TimestampWithTimeZoneColumnBuilderNotNull = TimestampWithTimeZoneColumnBuilderNotNull(getter) 43 | 44 | public fun NullableOffsetDateTimeColumnProperty.timestampWithTimeZone() 45 | : TimestampWithTimeZoneColumnBuilderNullable = TimestampWithTimeZoneColumnBuilderNullable(getter) 46 | 47 | public fun NotNullLocalTimeColumnProperty.time(): TimeColumnBuilderNotNull = 48 | TimeColumnBuilderNotNull(getter) 49 | 50 | public fun NullableLocalTimeColumnProperty.time(): TimeColumnBuilderNullable = 51 | TimeColumnBuilderNullable(getter) 52 | 53 | public fun NotNullBooleanColumnProperty.boolean(): BooleanColumnBuilderNotNull = 54 | BooleanColumnBuilderNotNull(getter) 55 | 56 | public fun NotNullUuidColumnProperty.uuid(): UuidColumnBuilderNotNull = 57 | UuidColumnBuilderNotNull(getter) 58 | 59 | public fun NullableUuidColumnProperty.uuid(): UuidColumnBuilderNullable = 60 | UuidColumnBuilderNullable(getter) 61 | 62 | public fun NotNullIntColumnProperty.integer(): IntegerNoAutoIncrementColumnBuilderNotNull = 63 | IntegerNoAutoIncrementColumnBuilderNotNull(getter) 64 | 65 | public fun NullableIntColumnProperty.integer(): IntegerNoAutoIncrementColumnBuilderNullable = 66 | IntegerNoAutoIncrementColumnBuilderNullable(getter) 67 | 68 | public fun NotNullIntColumnProperty.serial(): SerialColumnBuilder = SerialColumnBuilder(getter) 69 | 70 | public fun NullableIntColumnProperty.serial(): SerialColumnBuilder = SerialColumnBuilder(getter) 71 | } 72 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/ColumnDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.LocalTime 10 | import java.time.OffsetDateTime 11 | import java.util.* 12 | import kotlin.reflect.KFunction 13 | 14 | 15 | @KotysaMarker 16 | public abstract class ColumnDsl> internal constructor( 17 | private val init: U.(TableColumnPropertyProvider) -> ColumnBuilder<*, T, *> 18 | ) : TableColumnPropertyProvider { 19 | 20 | override fun get(getter: (T) -> String): NotNullStringColumnProperty = NotNullStringColumnProperty(getter) 21 | 22 | override fun get(getter: (T) -> String?): NullableStringColumnProperty { 23 | checkNullableGetter(getter) 24 | return NullableStringColumnProperty(getter) 25 | } 26 | 27 | override fun get(getter: (T) -> LocalDateTime): NotNullLocalDateTimeColumnProperty = NotNullLocalDateTimeColumnProperty(getter) 28 | 29 | override fun get(getter: (T) -> LocalDateTime?): NullableLocalDateTimeColumnProperty { 30 | checkNullableGetter(getter) 31 | return NullableLocalDateTimeColumnProperty(getter) 32 | } 33 | 34 | override fun get(getter: (T) -> LocalDate): NotNullLocalDateColumnProperty = NotNullLocalDateColumnProperty(getter) 35 | 36 | override fun get(getter: (T) -> LocalDate?): NullableLocalDateColumnProperty { 37 | checkNullableGetter(getter) 38 | return NullableLocalDateColumnProperty(getter) 39 | } 40 | 41 | override fun get(getter: (T) -> OffsetDateTime): NotNullOffsetDateTimeColumnProperty = NotNullOffsetDateTimeColumnProperty(getter) 42 | 43 | override fun get(getter: (T) -> OffsetDateTime?): NullableOffsetDateTimeColumnProperty { 44 | checkNullableGetter(getter) 45 | return NullableOffsetDateTimeColumnProperty(getter) 46 | } 47 | 48 | override fun get(getter: (T) -> LocalTime): NotNullLocalTimeColumnProperty = NotNullLocalTimeColumnProperty(getter) 49 | 50 | override fun get(getter: (T) -> LocalTime?): NullableLocalTimeColumnProperty { 51 | checkNullableGetter(getter) 52 | return NullableLocalTimeColumnProperty(getter) 53 | } 54 | 55 | override fun get(getter: (T) -> Boolean): NotNullBooleanColumnProperty = NotNullBooleanColumnProperty(getter) 56 | 57 | override fun get(getter: (T) -> UUID): NotNullUuidColumnProperty = NotNullUuidColumnProperty(getter) 58 | 59 | override fun get(getter: (T) -> UUID?): NullableUuidColumnProperty { 60 | checkNullableGetter(getter) 61 | return NullableUuidColumnProperty(getter) 62 | } 63 | 64 | override fun get(getter: (T) -> Int): NotNullIntColumnProperty = NotNullIntColumnProperty(getter) 65 | 66 | override fun get(getter: (T) -> Int?): NullableIntColumnProperty { 67 | checkNullableGetter(getter) 68 | return NullableIntColumnProperty(getter) 69 | } 70 | 71 | private fun checkNullableGetter(getter: (T) -> Any?) { 72 | if (getter !is KFunction<*>) { 73 | require(getter.toCallable().returnType.isMarkedNullable) { "\"$getter\" doesn't have a nullable return type" } 74 | } 75 | } 76 | 77 | @Suppress("UNCHECKED_CAST") 78 | internal fun initialize(initialize: U): Column { 79 | val columnBuilder = init(initialize, initialize) 80 | if (!columnBuilder.isColumnNameInitialized) { 81 | columnBuilder.props.columnName = columnBuilder.props.entityGetter.toCallable().name 82 | } 83 | return columnBuilder.build() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/SqlClientSelectR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import com.pullvert.kotysa.* 8 | import org.springframework.dao.IncorrectResultSizeDataAccessException 9 | import org.springframework.data.r2dbc.core.DatabaseClient 10 | import reactor.core.publisher.Flux 11 | import reactor.core.publisher.Mono 12 | import kotlin.reflect.KClass 13 | 14 | 15 | internal class SqlClientSelectR2dbc private constructor() : AbstractSqlClientSelectR2dbc() { 16 | 17 | internal class Select internal constructor( 18 | override val client: DatabaseClient, 19 | override val tables: Tables, 20 | override val resultClass: KClass, 21 | override val dsl: (SelectDslApi.(ValueProvider) -> T)? 22 | ) : ReactorSqlClientSelect.Select(), DefaultSqlClientSelect.Select, Whereable, Return { 23 | override val properties: Properties = initProperties() 24 | 25 | override fun join(joinClass: KClass, alias: String?, type: JoinType): ReactorSqlClientSelect.Joinable = 26 | Joinable(client, properties, joinClass, alias, type) 27 | } 28 | 29 | private class Joinable internal constructor( 30 | private val client: DatabaseClient, 31 | private val properties: Properties, 32 | private val joinClass: KClass, 33 | private val alias: String?, 34 | private val type: JoinType 35 | ) : ReactorSqlClientSelect.Joinable { 36 | 37 | override fun on(dsl: (FieldProvider) -> ColumnField<*, *>): ReactorSqlClientSelect.Join { 38 | val join = Join(client, properties) 39 | join.addJoinClause(dsl, joinClass, alias, type) 40 | return join 41 | } 42 | } 43 | 44 | private class Join internal constructor( 45 | override val client: DatabaseClient, 46 | override val properties: Properties 47 | ) : DefaultSqlClientSelect.Join, ReactorSqlClientSelect.Join, Whereable, Return 48 | 49 | private interface Whereable : DefaultSqlClientSelect.Whereable, ReactorSqlClientSelect.Whereable { 50 | val client: DatabaseClient 51 | 52 | override fun where(dsl: WhereDsl.(FieldProvider) -> WhereClause): ReactorSqlClientSelect.Where { 53 | val where = Where(client, properties) 54 | where.addWhereClause(dsl) 55 | return where 56 | } 57 | } 58 | 59 | private class Where internal constructor( 60 | override val client: DatabaseClient, 61 | override val properties: Properties 62 | ) : DefaultSqlClientSelect.Where, ReactorSqlClientSelect.Where, Return { 63 | 64 | override fun and(dsl: WhereDsl.(FieldProvider) -> WhereClause): ReactorSqlClientSelect.Where { 65 | addAndClause(dsl) 66 | return this 67 | } 68 | 69 | override fun or(dsl: WhereDsl.(FieldProvider) -> WhereClause): ReactorSqlClientSelect.Where { 70 | addOrClause(dsl) 71 | return this 72 | } 73 | } 74 | 75 | private interface Return : AbstractSqlClientSelectR2dbc.Return, ReactorSqlClientSelect.Return { 76 | 77 | override fun fetchOne(): Mono = fetch().one() 78 | .onErrorMap(IncorrectResultSizeDataAccessException::class.java) { NonUniqueResultException() } 79 | 80 | override fun fetchFirst(): Mono = fetch().first() 81 | override fun fetchAll(): Flux = fetch().all() 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/postgresql/R2DbcInheritancePostgresqlTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.postgresql 6 | 7 | import com.pullvert.kotysa.r2dbc.Repository 8 | import com.pullvert.kotysa.r2dbc.sqlClient 9 | import com.pullvert.kotysa.tables 10 | import com.pullvert.kotysa.test.Entity 11 | import com.pullvert.kotysa.test.Inherited 12 | import com.pullvert.kotysa.test.Nameable 13 | import com.pullvert.kotysa.test.inherited 14 | import org.assertj.core.api.Assertions.assertThat 15 | import org.junit.jupiter.api.Test 16 | import org.springframework.data.r2dbc.core.DatabaseClient 17 | 18 | 19 | class R2DbcInheritancePostgresqlTest : AbstractR2dbcPostgresqlTest() { 20 | override val context = startContext() 21 | 22 | override val repository = getContextRepository() 23 | 24 | @Test 25 | fun `Verify extension function selectById finds inherited`() { 26 | assertThat(repository.selectById("id").block()) 27 | .isEqualTo(inherited) 28 | } 29 | 30 | @Test 31 | fun `Verify selectInheritedById finds inherited`() { 32 | assertThat(repository.selectInheritedById("id").block()) 33 | .isEqualTo(inherited) 34 | } 35 | 36 | @Test 37 | fun `Verify selectFirstByName finds inherited`() { 38 | assertThat(repository.selectFirstByName("name").block()) 39 | .isEqualTo(inherited) 40 | } 41 | 42 | @Test 43 | fun `Verify deleteById deletes inherited`() { 44 | assertThat(repository.deleteById("id").block()!!) 45 | .isEqualTo(1) 46 | assertThat(repository.selectAll().toIterable()) 47 | .isEmpty() 48 | // re-insert 49 | repository.insert().block() 50 | } 51 | } 52 | 53 | private val tables = 54 | tables().postgresql { 55 | table { 56 | name = "inherited" 57 | column { it[Inherited::getId].varchar().primaryKey() } 58 | column { it[Inherited::name].varchar() } 59 | column { it[Inherited::firstname].varchar() } 60 | } 61 | } 62 | 63 | 64 | class InheritancePostgresqlRepository(dbClient: DatabaseClient) : Repository { 65 | 66 | val sqlClient = dbClient.sqlClient(tables) 67 | 68 | override fun init() { 69 | createTable() 70 | .then(insert()) 71 | .block() 72 | } 73 | 74 | override fun delete() { 75 | deleteAll() 76 | .block() 77 | } 78 | 79 | private fun createTable() = sqlClient.createTable() 80 | 81 | fun insert() = sqlClient.insert(inherited) 82 | 83 | private fun deleteAll() = sqlClient.deleteAllFromTable() 84 | 85 | fun selectAll() = sqlClient.selectAll() 86 | 87 | fun selectInheritedById(id: String) = 88 | sqlClient.select().where { it[Inherited::getId] eq id }.fetchOne() 89 | } 90 | 91 | inline fun > InheritancePostgresqlRepository.selectById(id: String) = 92 | sqlClient.select().where { it[Entity::getId] eq id }.fetchOne() 93 | 94 | inline fun InheritancePostgresqlRepository.selectFirstByName(name: String) = 95 | sqlClient.select().where { it[Nameable::name] eq name }.fetchFirst() 96 | 97 | inline fun > InheritancePostgresqlRepository.deleteById(id: String) = 98 | sqlClient.deleteFromTable().where { it[Entity::getId] eq id }.execute() 99 | -------------------------------------------------------------------------------- /samples/kotysa-reactive-r2dbc/src/main/kotlin/com/sample/Configurations.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import com.pullvert.kotysa.r2dbc.sqlClient 4 | import com.pullvert.kotysa.tables 5 | import org.springframework.boot.context.event.ApplicationReadyEvent 6 | import org.springframework.data.r2dbc.core.DatabaseClient 7 | import org.springframework.fu.kofu.configuration 8 | import org.springframework.fu.kofu.r2dbc.r2dbcH2 9 | import org.springframework.fu.kofu.r2dbc.r2dbcPostgresql 10 | import org.springframework.fu.kofu.webflux.webFlux 11 | import org.testcontainers.containers.PostgreSQLContainer 12 | 13 | private class KPostgreSQLContainer : PostgreSQLContainer() 14 | 15 | val dataConfig = configuration { 16 | beans { 17 | val tables = if (profiles.contains("test")) h2Tables else postgresqlTables 18 | bean { ref().sqlClient(tables) } 19 | bean() 20 | bean() 21 | } 22 | if (profiles.any { profile -> "dev" == profile || "test" == profile }) { 23 | listener { 24 | ref().init() 25 | ref().init() 26 | } 27 | } 28 | if (profiles.contains("test")) { 29 | r2dbcH2() 30 | } else { 31 | // PostgreSQL testcontainers must be started first to get random Docker mapped port 32 | val postgresqlContainer = KPostgreSQLContainer() 33 | .withDatabaseName("postgres") 34 | .withUsername("postgres") 35 | .withPassword("") 36 | postgresqlContainer.start() 37 | 38 | r2dbcPostgresql { 39 | port = postgresqlContainer.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT) 40 | } 41 | } 42 | } 43 | 44 | internal val h2Tables = 45 | tables().h2 { 46 | table { 47 | name = "roles" 48 | column { it[Role::id].uuid().primaryKey() } 49 | column { it[Role::label].varchar() } 50 | } 51 | table { 52 | name = "users" 53 | column { it[User::id].integer().autoIncrement().primaryKey("PK_users") } 54 | column { it[User::firstname].varchar().name("fname") } 55 | column { it[User::lastname].varchar().name("lname") } 56 | column { it[User::isAdmin].boolean().name("is_admin") } 57 | column { it[User::roleId].uuid().foreignKey("FK_users_roles").name("role_id") } 58 | column { it[User::creationTime].dateTime().name("creation_time") } 59 | column { it[User::alias].varchar() } 60 | } 61 | } 62 | 63 | internal val postgresqlTables = 64 | tables().postgresql { 65 | table { 66 | name = "roles" 67 | column { it[Role::id].uuid().primaryKey() } 68 | column { it[Role::label].varchar() } 69 | } 70 | table { 71 | name = "users" 72 | column { it[User::id].serial().primaryKey("PK_users") } 73 | column { it[User::firstname].varchar().name("fname") } 74 | column { it[User::lastname].varchar().name("lname") } 75 | column { it[User::isAdmin].boolean().name("is_admin") } 76 | column { it[User::roleId].uuid().foreignKey("FK_users_roles").name("role_id") } 77 | column { it[User::creationTime].timestamp().name("creation_time") } 78 | column { it[User::alias].varchar() } 79 | } 80 | } 81 | 82 | val webConfig = configuration { 83 | beans { 84 | bean() 85 | bean(::routes) 86 | } 87 | webFlux { 88 | port = if (profiles.any { profile -> "dev" == profile || "test" == profile }) 8181 else 8080 89 | codecs { 90 | jackson() 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/UpdateSetDsl.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.LocalTime 10 | import java.time.OffsetDateTime 11 | import java.util.* 12 | 13 | 14 | @KotysaMarker 15 | public class UpdateSetDsl internal constructor( 16 | private val init: (FieldSetter) -> Unit, 17 | availableColumns: Map Any?, Column<*, *>>, 18 | dbType: DbType 19 | ) : FieldSetter { 20 | 21 | private val fieldAccess = FieldAccess(availableColumns, dbType) 22 | private lateinit var columnField: ColumnField 23 | private var value: Any? = null 24 | 25 | override fun set(getter: (T) -> String, value: String) { 26 | addValue(fieldAccess.getField(getter, null), value) 27 | } 28 | 29 | override fun set(getter: (T) -> String?, value: String?): Nullable { 30 | addValue(fieldAccess.getField(getter, null), value) 31 | return Nullable.TRUE 32 | } 33 | 34 | override fun set(getter: (T) -> LocalDateTime, value: LocalDateTime) { 35 | addValue(fieldAccess.getField(getter, null), value) 36 | } 37 | 38 | override fun set(getter: (T) -> LocalDateTime?, value: LocalDateTime?): Nullable { 39 | addValue(fieldAccess.getField(getter, null), value) 40 | return Nullable.TRUE 41 | } 42 | 43 | override fun set(getter: (T) -> LocalDate, value: LocalDate) { 44 | addValue(fieldAccess.getField(getter, null), value) 45 | } 46 | 47 | override fun set(getter: (T) -> LocalDate?, value: LocalDate?): Nullable { 48 | addValue(fieldAccess.getField(getter, null), value) 49 | return Nullable.TRUE 50 | } 51 | 52 | override fun set(getter: (T) -> OffsetDateTime, value: OffsetDateTime) { 53 | addValue(fieldAccess.getField(getter, null), value) 54 | } 55 | 56 | override fun set(getter: (T) -> OffsetDateTime?, value: OffsetDateTime?): Nullable { 57 | addValue(fieldAccess.getField(getter, null), value) 58 | return Nullable.TRUE 59 | } 60 | 61 | override fun set(getter: (T) -> LocalTime, value: LocalTime) { 62 | addValue(fieldAccess.getField(getter, null), value) 63 | } 64 | 65 | override fun set(getter: (T) -> LocalTime?, value: LocalTime?): Nullable { 66 | addValue(fieldAccess.getField(getter, null), value) 67 | return Nullable.TRUE 68 | } 69 | 70 | override fun set(getter: (T) -> Boolean, value: Boolean) { 71 | addValue(fieldAccess.getField(getter, null), value) 72 | } 73 | 74 | override fun set(getter: (T) -> UUID, value: UUID) { 75 | addValue(fieldAccess.getField(getter, null), value) 76 | } 77 | 78 | override fun set(getter: (T) -> UUID?, value: UUID?): Nullable { 79 | addValue(fieldAccess.getField(getter, null), value) 80 | return Nullable.TRUE 81 | } 82 | 83 | override fun set(getter: (T) -> Int, value: Int) { 84 | addValue(fieldAccess.getField(getter, null), value) 85 | } 86 | 87 | override fun set(getter: (T) -> Int?, value: Int?): Nullable { 88 | addValue(fieldAccess.getField(getter, null), value) 89 | return Nullable.TRUE 90 | } 91 | 92 | private fun addValue(columnField: ColumnField, value: Any?) { 93 | require(!this::columnField.isInitialized) { "Only one value assignment is required" } 94 | this.columnField = columnField 95 | this.value = value 96 | } 97 | 98 | internal fun initialize(): Pair, Any?> { 99 | init(this) 100 | require(::columnField.isInitialized) { "One value assignment is required" } 101 | return Pair(columnField, value) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /kotysa-android/src/main/kotlin/com/pullvert/kotysa/android/SqlClientSqLite.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.android 6 | 7 | import android.content.ContentValues 8 | import android.database.sqlite.SQLiteDatabase 9 | import android.database.sqlite.SQLiteOpenHelper 10 | import com.pullvert.kotysa.* 11 | import java.time.LocalDate 12 | import java.time.LocalDateTime 13 | import java.time.LocalTime 14 | import java.time.OffsetDateTime 15 | import java.time.format.DateTimeFormatter 16 | import kotlin.reflect.KClass 17 | 18 | /** 19 | * @sample com.pullvert.kotysa.android.sample.UserRepositorySqLite 20 | */ 21 | internal class SqlClientSqLite( 22 | private val client: SQLiteOpenHelper, 23 | override val tables: Tables 24 | ) : BlockingSqlClient(), DefaultSqlClient { 25 | 26 | override fun select(resultClass: KClass, 27 | dsl: (SelectDslApi.(ValueProvider) -> T)?): BlockingSqlClientSelect.Select = 28 | SqlClientSelectSqLite.Select(client.readableDatabase, tables, resultClass, dsl) 29 | 30 | override fun createTable(tableClass: KClass) { 31 | val createTableSql = createTableSql(tableClass) 32 | return client.writableDatabase.execSQL(createTableSql) 33 | } 34 | 35 | override fun insert(row: T) { 36 | val table = tables.getTable(row::class) 37 | val contentValues = ContentValues(table.columns.size) 38 | table.columns.values 39 | .filterNot { column -> column.entityGetter(row) == null && column.defaultValue != null } 40 | .forEach { column -> contentValues.put(column.name, column.entityGetter(row)) } 41 | 42 | // debug query 43 | insertSqlDebug(row) 44 | 45 | client.writableDatabase.insert(table.name, null, contentValues) 46 | } 47 | 48 | override fun insert(vararg rows: Any) { 49 | checkRowsAreMapped(*rows) 50 | 51 | rows.forEach { row -> insert(row) } 52 | } 53 | 54 | override fun deleteFromTable(tableClass: KClass): BlockingSqlClientDeleteOrUpdate.DeleteOrUpdate = 55 | SqlClientDeleteSqLite.Delete(client.writableDatabase, tables, tableClass) 56 | 57 | override fun updateTable(tableClass: KClass): BlockingSqlClientDeleteOrUpdate.Update = 58 | SqlClientUpdateSqLite.Update(client.writableDatabase, tables, tableClass) 59 | } 60 | 61 | internal fun ContentValues.put(name: String, value: Any?) { 62 | if (value != null) { 63 | when (value) { 64 | is Int -> put(name, value) 65 | is Byte -> put(name, value) 66 | is Long -> put(name, value) 67 | is Float -> put(name, value) 68 | is Short -> put(name, value) 69 | is Double -> put(name, value) 70 | is String -> put(name, value) 71 | is Boolean -> put(name, value) 72 | is ByteArray -> put(name, value) 73 | // Date are stored as String 74 | is LocalDate -> put(name, value.format(DateTimeFormatter.ISO_LOCAL_DATE)) 75 | is LocalDateTime -> put(name, value.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) 76 | is OffsetDateTime -> put(name, value.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) 77 | is LocalTime -> put(name, value.format(DateTimeFormatter.ISO_LOCAL_TIME)) 78 | else -> throw UnsupportedOperationException( 79 | "${value.javaClass.canonicalName} is not supported by Android SqLite") 80 | } 81 | } else { 82 | putNull(name) 83 | } 84 | } 85 | 86 | /** 87 | * Create a [BlockingSqlClient] from a Android SqLite [SQLiteDatabase] with [Tables] mapping 88 | * 89 | * @sample com.pullvert.kotysa.android.sample.UserRepositorySqLite 90 | */ 91 | public fun SQLiteOpenHelper.sqlClient(tables: Tables): BlockingSqlClient = SqlClientSqLite(this, tables) 92 | -------------------------------------------------------------------------------- /samples/kotysa-coroutines-r2dbc/src/main/kotlin/com/sample/Configurations.kt: -------------------------------------------------------------------------------- 1 | package com.sample 2 | 3 | import com.pullvert.kotysa.r2dbc.coSqlClient 4 | import com.pullvert.kotysa.tables 5 | import kotlinx.coroutines.runBlocking 6 | import org.springframework.boot.context.event.ApplicationReadyEvent 7 | import org.springframework.data.r2dbc.core.DatabaseClient 8 | import org.springframework.fu.kofu.configuration 9 | import org.springframework.fu.kofu.r2dbc.r2dbcH2 10 | import org.springframework.fu.kofu.r2dbc.r2dbcPostgresql 11 | import org.springframework.fu.kofu.webflux.webFlux 12 | import org.testcontainers.containers.PostgreSQLContainer 13 | 14 | private class KPostgreSQLContainer : PostgreSQLContainer() 15 | 16 | val dataConfig = configuration { 17 | beans { 18 | val tables = if (profiles.contains("test")) h2Tables else postgresqlTables 19 | bean { ref().coSqlClient(tables) } 20 | bean() 21 | bean() 22 | } 23 | if (profiles.any { profile -> "dev" == profile || "test" == profile }) { 24 | listener { 25 | runBlocking { 26 | ref().init() 27 | ref().init() 28 | } 29 | } 30 | } 31 | if (profiles.contains("test")) { 32 | r2dbcH2() 33 | } else { 34 | // PostgreSQL testcontainers must be started first to get random Docker mapped port 35 | val postgresqlContainer = KPostgreSQLContainer() 36 | .withDatabaseName("postgres") 37 | .withUsername("postgres") 38 | .withPassword("") 39 | postgresqlContainer.start() 40 | 41 | r2dbcPostgresql { 42 | port = postgresqlContainer.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT) 43 | } 44 | } 45 | } 46 | 47 | internal val h2Tables = 48 | tables().h2 { 49 | table { 50 | name = "roles" 51 | column { it[Role::id].uuid().primaryKey() } 52 | column { it[Role::label].varchar() } 53 | } 54 | table { 55 | name = "users" 56 | column { it[User::id].integer().autoIncrement().primaryKey("PK_users") } 57 | column { it[User::firstname].varchar().name("fname") } 58 | column { it[User::lastname].varchar().name("lname") } 59 | column { it[User::isAdmin].boolean().name("is_admin") } 60 | column { it[User::roleId].uuid().foreignKey("FK_users_roles").name("role_id") } 61 | column { it[User::creationTime].dateTime().name("creation_time") } 62 | column { it[User::alias].varchar() } 63 | } 64 | } 65 | 66 | internal val postgresqlTables = 67 | tables().postgresql { 68 | table { 69 | name = "roles" 70 | column { it[Role::id].uuid().primaryKey() } 71 | column { it[Role::label].varchar() } 72 | } 73 | table { 74 | name = "users" 75 | column { it[User::id].serial().primaryKey("PK_users") } 76 | column { it[User::firstname].varchar().name("fname") } 77 | column { it[User::lastname].varchar().name("lname") } 78 | column { it[User::isAdmin].boolean().name("is_admin") } 79 | column { it[User::roleId].uuid().foreignKey("FK_users_roles").name("role_id") } 80 | column { it[User::creationTime].timestamp().name("creation_time") } 81 | column { it[User::alias].varchar() } 82 | } 83 | } 84 | 85 | val webConfig = configuration { 86 | beans { 87 | bean() 88 | bean(::routes) 89 | } 90 | webFlux { 91 | port = if (profiles.any { profile -> "dev" == profile || "test" == profile }) 8181 else 8080 92 | codecs { 93 | jackson() 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /kotysa-core/src/main/kotlin/com/pullvert/kotysa/AbstractRow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa 6 | 7 | import java.time.LocalDate 8 | import java.time.LocalDateTime 9 | import java.time.LocalTime 10 | import java.time.OffsetDateTime 11 | import java.util.* 12 | import kotlin.reflect.KClass 13 | 14 | /** 15 | * Represents a row returned from a query. 16 | */ 17 | @Suppress("UNCHECKED_CAST") 18 | public abstract class AbstractRow(private val fieldIndexMap: Map) : SelectDslApi(), ValueProvider { 19 | 20 | override fun count(resultClass: KClass, dsl: ((FieldProvider) -> ColumnField)?, alias: String?): Long = 21 | this[fieldIndexMap.filterKeys { field -> 22 | field is CountField<*, *> && field.dsl == dsl && field.alias == alias 23 | }.values.first()]!! 24 | 25 | override operator fun get(getter: (T) -> String, alias: String?): String = 26 | this[getIndexdByGetterAndAlias(getter, alias)]!! 27 | 28 | override operator fun get(getter: (T) -> String?, alias: String?, `_`: Nullable): String? = 29 | this[getIndexdByGetterAndAlias(getter, alias)] 30 | 31 | override operator fun get(getter: (T) -> LocalDateTime, alias: String?): LocalDateTime = 32 | this[getIndexdByGetterAndAlias(getter, alias)]!! 33 | 34 | override operator fun get(getter: (T) -> LocalDateTime?, alias: String?, `_`: Nullable): LocalDateTime? = 35 | this[getIndexdByGetterAndAlias(getter, alias)] 36 | 37 | override operator fun get(getter: (T) -> LocalDate, alias: String?): LocalDate = 38 | this[getIndexdByGetterAndAlias(getter, alias)]!! 39 | 40 | override operator fun get(getter: (T) -> LocalDate?, alias: String?, `_`: Nullable): LocalDate? = 41 | this[getIndexdByGetterAndAlias(getter, alias)] 42 | 43 | override operator fun get(getter: (T) -> OffsetDateTime, alias: String?): OffsetDateTime = 44 | this[getIndexdByGetterAndAlias(getter, alias)]!! 45 | 46 | override operator fun get(getter: (T) -> OffsetDateTime?, alias: String?, `_`: Nullable): OffsetDateTime? = 47 | this[getIndexdByGetterAndAlias(getter, alias)] 48 | 49 | override operator fun get(getter: (T) -> LocalTime, alias: String?): LocalTime = 50 | this[getIndexdByGetterAndAlias(getter, alias)]!! 51 | 52 | override operator fun get(getter: (T) -> LocalTime?, alias: String?, `_`: Nullable): LocalTime? = 53 | this[getIndexdByGetterAndAlias(getter, alias)] 54 | 55 | override operator fun get(getter: (T) -> Boolean, alias: String?): Boolean = 56 | this[getIndexdByGetterAndAlias(getter, alias)]!! 57 | 58 | override operator fun get(getter: (T) -> UUID, alias: String?): UUID = 59 | this[getIndexdByGetterAndAlias(getter, alias)]!! 60 | 61 | override operator fun get(getter: (T) -> UUID?, alias: String?, `_`: Nullable): UUID? = 62 | this[getIndexdByGetterAndAlias(getter, alias)] 63 | 64 | override operator fun get(getter: (T) -> Int, alias: String?): Int = 65 | this[getIndexdByGetterAndAlias(getter, alias)]!! 66 | 67 | override operator fun get(getter: (T) -> Int?, alias: String?, `_`: Nullable): Int? = 68 | this[getIndexdByGetterAndAlias(getter, alias)] 69 | 70 | /** 71 | * Returns the element at the specified index in the list of returned fields of row 72 | */ 73 | protected abstract fun get(index: Int, type: Class): T? 74 | 75 | private inline operator fun get(index: Int) = get(index, T::class.java) 76 | 77 | private fun getIndexdByGetterAndAlias(getter: (T) -> U, alias: String?) = 78 | fieldIndexMap.filterKeys { field -> 79 | field is ColumnField<*, *> && field.column.entityGetter == getter && field.alias == alias 80 | }.values.first() 81 | } 82 | -------------------------------------------------------------------------------- /kotysa-core/src/testFixtures/java/com/pullvert/kotysa/test/JavaUser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.test; 6 | 7 | import java.util.Objects; 8 | 9 | /** 10 | * Basic Entity 11 | */ 12 | public class JavaUser { 13 | 14 | private String login; 15 | 16 | private String firstname; 17 | 18 | private String lastname; 19 | 20 | private boolean isAdmin; 21 | 22 | private String alias1; 23 | 24 | private String alias2; 25 | 26 | private String alias3; 27 | 28 | public String getLogin() { 29 | return login; 30 | } 31 | 32 | public void setLogin(String login) { 33 | this.login = login; 34 | } 35 | 36 | public String getFirstname() { 37 | return firstname; 38 | } 39 | 40 | public void setFirstname(String firstname) { 41 | this.firstname = firstname; 42 | } 43 | 44 | public String getLastname() { 45 | return lastname; 46 | } 47 | 48 | public void setLastname(String lastname) { 49 | this.lastname = lastname; 50 | } 51 | 52 | public boolean isAdmin() { 53 | return isAdmin; 54 | } 55 | 56 | public void setAdmin(boolean admin) { 57 | isAdmin = admin; 58 | } 59 | 60 | @javax.annotation.Nullable 61 | public String getAlias1() { 62 | return alias1; 63 | } 64 | 65 | public void setAlias1(String alias1) { 66 | this.alias1 = alias1; 67 | } 68 | 69 | @org.jetbrains.annotations.Nullable 70 | public String getAlias2() { 71 | return alias2; 72 | } 73 | 74 | public void setAlias2(String alias2) { 75 | this.alias2 = alias2; 76 | } 77 | 78 | @org.springframework.lang.Nullable 79 | public String getAlias3() { 80 | return alias3; 81 | } 82 | 83 | public void setAlias3(String alias3) { 84 | this.alias3 = alias3; 85 | } 86 | 87 | @Override 88 | public boolean equals(Object o) { 89 | if (this == o) { 90 | return true; 91 | } 92 | if (o == null || getClass() != o.getClass()) { 93 | return false; 94 | } 95 | 96 | JavaUser javaUser = (JavaUser) o; 97 | 98 | if (isAdmin != javaUser.isAdmin) { 99 | return false; 100 | } 101 | if (!Objects.equals(login, javaUser.login)) { 102 | return false; 103 | } 104 | if (!Objects.equals(firstname, javaUser.firstname)) { 105 | return false; 106 | } 107 | if (!Objects.equals(lastname, javaUser.lastname)) { 108 | return false; 109 | } 110 | if (!Objects.equals(alias1, javaUser.alias1)) { 111 | return false; 112 | } 113 | if (!Objects.equals(alias2, javaUser.alias2)) { 114 | return false; 115 | } 116 | return Objects.equals(alias3, javaUser.alias3); 117 | 118 | } 119 | 120 | @Override 121 | public int hashCode() { 122 | int result = login != null ? login.hashCode() : 0; 123 | result = 31 * result + (firstname != null ? firstname.hashCode() : 0); 124 | result = 31 * result + (lastname != null ? lastname.hashCode() : 0); 125 | result = 31 * result + (isAdmin ? 1 : 0); 126 | result = 31 * result + (alias1 != null ? alias1.hashCode() : 0); 127 | result = 31 * result + (alias2 != null ? alias2.hashCode() : 0); 128 | result = 31 * result + (alias3 != null ? alias3.hashCode() : 0); 129 | return result; 130 | } 131 | 132 | @Override 133 | public String toString() { 134 | return "JavaUser{" + 135 | "id='" + login + '\'' + 136 | ", firstname='" + firstname + '\'' + 137 | ", lastname='" + lastname + '\'' + 138 | ", isAdmin=" + isAdmin + 139 | ", alias1='" + alias1 + '\'' + 140 | ", alias2='" + alias2 + '\'' + 141 | ", alias3='" + alias3 + '\'' + 142 | '}'; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/main/kotlin/com/pullvert/kotysa/r2dbc/SqlClientDeleteR2dbc.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc 6 | 7 | import com.pullvert.kotysa.* 8 | import org.springframework.data.r2dbc.core.DatabaseClient 9 | import reactor.core.publisher.Mono 10 | import kotlin.reflect.KClass 11 | 12 | 13 | internal class SqlClientDeleteR2dbc private constructor() : AbstractSqlClientDeleteR2dbc() { 14 | 15 | internal class Delete internal constructor( 16 | override val client: DatabaseClient, 17 | override val tables: Tables, 18 | override val tableClass: KClass 19 | ) : ReactorSqlClientDeleteOrUpdate.DeleteOrUpdate(), DeleteOrUpdate, Return { 20 | override val properties: Properties = initProperties() 21 | 22 | override fun join(joinClass: KClass, alias: String?, type: JoinType): ReactorSqlClientDeleteOrUpdate.Joinable = 23 | Joinable(client, properties, joinClass, alias, type) 24 | 25 | override fun where(dsl: TypedWhereDsl.(TypedFieldProvider) -> WhereClause): ReactorSqlClientDeleteOrUpdate.TypedWhere { 26 | val where = TypedWhere(client, properties) 27 | where.addWhereClause(dsl) 28 | return where 29 | } 30 | } 31 | 32 | private class Joinable internal constructor( 33 | private val client: DatabaseClient, 34 | private val properties: Properties, 35 | private val joinClass: KClass, 36 | private val alias: String?, 37 | private val type: JoinType 38 | ) : ReactorSqlClientDeleteOrUpdate.Joinable { 39 | 40 | override fun on(dsl: (FieldProvider) -> ColumnField<*, *>): ReactorSqlClientDeleteOrUpdate.Join { 41 | val join = Join(client, properties) 42 | join.addJoinClause(dsl, joinClass, alias, type) 43 | return join 44 | } 45 | } 46 | 47 | private class Join internal constructor( 48 | override val client: DatabaseClient, 49 | override val properties: Properties 50 | ) : DefaultSqlClientDeleteOrUpdate.Join, ReactorSqlClientDeleteOrUpdate.Join, Return { 51 | override fun where(dsl: WhereDsl.(FieldProvider) -> WhereClause): ReactorSqlClientDeleteOrUpdate.Where { 52 | val where = Where(client, properties) 53 | where.addWhereClause(dsl) 54 | return where 55 | } 56 | } 57 | 58 | private class Where internal constructor( 59 | override val client: DatabaseClient, 60 | override val properties: Properties 61 | ) : DefaultSqlClientDeleteOrUpdate.Where, ReactorSqlClientDeleteOrUpdate.Where, Return { 62 | 63 | override fun and(dsl: WhereDsl.(FieldProvider) -> WhereClause): ReactorSqlClientDeleteOrUpdate.Where { 64 | addAndClause(dsl) 65 | return this 66 | } 67 | 68 | override fun or(dsl: WhereDsl.(FieldProvider) -> WhereClause): ReactorSqlClientDeleteOrUpdate.Where { 69 | addOrClause(dsl) 70 | return this 71 | } 72 | } 73 | 74 | private class TypedWhere internal constructor( 75 | override val client: DatabaseClient, 76 | override val properties: Properties 77 | ) : DefaultSqlClientDeleteOrUpdate.TypedWhere, ReactorSqlClientDeleteOrUpdate.TypedWhere, Return { 78 | 79 | override fun and( 80 | dsl: TypedWhereDsl.(TypedFieldProvider) -> WhereClause 81 | ): ReactorSqlClientDeleteOrUpdate.TypedWhere { 82 | addAndClause(dsl) 83 | return this 84 | } 85 | 86 | override fun or(dsl: TypedWhereDsl.(TypedFieldProvider) -> WhereClause): ReactorSqlClientDeleteOrUpdate.TypedWhere { 87 | addOrClause(dsl) 88 | return this 89 | } 90 | } 91 | 92 | private interface Return : AbstractSqlClientDeleteR2dbc.Return, ReactorSqlClientDeleteOrUpdate.Return { 93 | 94 | override fun execute(): Mono = fetch().rowsUpdated() 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /kotysa-spring-data-r2dbc/src/test/kotlin/com/pullvert/kotysa/r2dbc/h2/R2DbcSelectUuidH2Test.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * This is free and unencumbered software released into the public domain, following 3 | */ 4 | 5 | package com.pullvert.kotysa.r2dbc.h2 6 | 7 | import com.pullvert.kotysa.r2dbc.Repository 8 | import com.pullvert.kotysa.r2dbc.sqlClient 9 | import com.pullvert.kotysa.test.* 10 | import org.assertj.core.api.Assertions.assertThat 11 | import org.junit.jupiter.api.Test 12 | import org.springframework.data.r2dbc.core.DatabaseClient 13 | import java.util.* 14 | 15 | 16 | class R2DbcSelectUuidH2Test : AbstractR2dbcH2Test() { 17 | override val context = startContext() 18 | 19 | override val repository = getContextRepository() 20 | 21 | @Test 22 | fun `Verify selectAllByRoleIdNotNull finds both results`() { 23 | assertThat(repository.selectAllByRoleIdNotNull(h2User.id).toIterable()) 24 | .hasSize(2) 25 | .containsExactlyInAnyOrder(h2UuidWithNullable, h2UuidWithoutNullable) 26 | } 27 | 28 | @Test 29 | fun `Verify selectAllByRoleIdNotNullNotEq finds no results`() { 30 | assertThat(repository.selectAllByRoleIdNotNullNotEq(h2User.id).toIterable()) 31 | .isEmpty() 32 | } 33 | 34 | @Test 35 | fun `Verify selectAllByRoleIdNullable finds h2UuidWithNullable`() { 36 | assertThat(repository.selectAllByRoleIdNullable(h2Admin.id).toIterable()) 37 | .hasSize(1) 38 | .containsExactlyInAnyOrder(h2UuidWithNullable) 39 | } 40 | 41 | @Test 42 | fun `Verify selectAllByRoleIdNullable finds h2UuidWithoutNullable`() { 43 | assertThat(repository.selectAllByRoleIdNullable(null).toIterable()) 44 | .hasSize(1) 45 | .containsExactlyInAnyOrder(h2UuidWithoutNullable) 46 | } 47 | 48 | @Test 49 | fun `Verify selectAllByRoleIdNullableNotEq finds h2UuidWithoutNullable`() { 50 | assertThat(repository.selectAllByRoleIdNullableNotEq(h2Admin.id).toIterable()) 51 | .isEmpty() 52 | } 53 | 54 | @Test 55 | fun `Verify selectAllByRoleIdNullableNotEq finds no results`() { 56 | assertThat(repository.selectAllByRoleIdNullableNotEq(null).toIterable()) 57 | .hasSize(1) 58 | .containsExactlyInAnyOrder(h2UuidWithNullable) 59 | } 60 | } 61 | 62 | 63 | class UuidRepositoryH2Select(dbClient: DatabaseClient) : Repository { 64 | 65 | private val sqlClient = dbClient.sqlClient(h2Tables) 66 | 67 | override fun init() { 68 | createTables() 69 | .then(insertRoles()) 70 | .then(insertUuids()) 71 | .block() 72 | } 73 | 74 | override fun delete() { 75 | deleteAllFromUuid() 76 | .then(deleteAllFromRole()) 77 | .block() 78 | } 79 | 80 | private fun createTables() = 81 | sqlClient.createTable() 82 | .then(sqlClient.createTable()) 83 | 84 | private fun insertRoles() = sqlClient.insert(h2User, h2Admin) 85 | 86 | private fun insertUuids() = sqlClient.insert(h2UuidWithNullable, h2UuidWithoutNullable) 87 | 88 | private fun deleteAllFromRole() = sqlClient.deleteAllFromTable() 89 | 90 | private fun deleteAllFromUuid() = sqlClient.deleteAllFromTable() 91 | 92 | fun selectAllByRoleIdNotNull(roleId: UUID) = sqlClient.select() 93 | .where { it[H2Uuid::roleIdNotNull] eq roleId } 94 | .fetchAll() 95 | 96 | fun selectAllByRoleIdNotNullNotEq(roleId: UUID) = sqlClient.select() 97 | .where { it[H2Uuid::roleIdNotNull] notEq roleId } 98 | .fetchAll() 99 | 100 | fun selectAllByRoleIdNullable(roleId: UUID?) = sqlClient.select() 101 | .where { it[H2Uuid::roleIdNullable] eq roleId } 102 | .fetchAll() 103 | 104 | fun selectAllByRoleIdNullableNotEq(roleId: UUID?) = sqlClient.select() 105 | .where { it[H2Uuid::roleIdNullable] notEq roleId } 106 | .fetchAll() 107 | } 108 | --------------------------------------------------------------------------------