diff --git a/.gitignore b/.gitignore index d49a8b5..28ec81e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .gradle +.kotlin build .idea diff --git a/README.md b/README.md index f605d49..3282176 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ ## Note -This project currently serves our own use and there are temporarily no guides or docs. See [DatabaseClient.kt](lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/DatabaseClient.kt) and [DatabaseClientSql.kt](lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/DatabaseClientSql.kt) for the major APIs. - Only PostgreSQL with [Reactive PostgreSQL Client](https://vertx.io/docs/vertx-pg-client/java/) is currently supported. ## Maven coordinate @@ -16,7 +14,117 @@ Only PostgreSQL with [Reactive PostgreSQL Client](https://vertx.io/docs/vertx-pg "com.huanshankeji:exposed-vertx-sql-client-postgresql:$version" ``` -## About the code +## Basic usage guide + +Here is a basic usage guide. This project currently serves our own use, therefore, there are temporarily no detailed docs, APIs are experimental, tests are incomplete, and please expect bugs. To learn more in addition to the guide below, see [DatabaseClient.kt](lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/DatabaseClient.kt) and [DatabaseClientSql.kt](lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/DatabaseClientSql.kt) for the major APIs. + +### Create a `DatabaseClient` + +```kotlin +val socketConnectionConfig = + ConnectionConfig.Socket("localhost", user = "user", password = "password", database = "database") +val exposedDatabase = exposedDatabaseConnectPostgreSql(socketConnectionConfig) +val databaseClient = createPgPoolDatabaseClient( + vertx, socketConnectionConfig, exposedDatabase = exposedDatabase +) +``` + +### Example table definitions + +```kotlin +object Examples : IntIdTable("examples") { + val name = varchar("name", 64) +} + +val tables = arrayOf(Examples) +``` + +### Use `exposedTransaction` to execute original blocking Exposed code + +For example, to create tables: + +```kotlin +withContext(Dispatchers.IO) { + databaseClient.exposedTransaction { + SchemaUtils.create(*tables) + } +} +``` + +### CRUD (DML and DQL) operations with `DatabaseClient` + +#### Core APIs + +With these core APIs, you create and execute Exposed `Statement`s. You don't need to learn many new APIs, and the `Statement`s are more composable and easily editable. + +```kotlin +// The Exposed `Table` extension functions `insert`, `update`, and `delete` execute eagerly so `insertStatement`, `updateStatement`, `deleteStatement` have to be used. + +val insertRowCount = databaseClient.executeUpdate(Examples.insertStatement { it[name] = "A" }) +assert(insertRowCount == 1) +// `executeSingleUpdate` function requires that there is only 1 row updated and returns `Unit`. +databaseClient.executeSingleUpdate(Examples.insertStatement { it[name] = "B" }) +// `executeSingleOrNoUpdate` requires that there is 0 or 1 row updated and returns `Boolean`. +val isInserted = databaseClient.executeSingleOrNoUpdate(Examples.insertIgnoreStatement { it[name] = "B" }) +assert(isInserted) + +val updateRowCount = + databaseClient.executeUpdate(Examples.updateStatement({ Examples.id eq 1 }) { it[name] = "AA" }) +assert(updateRowCount == 1) + +// The Exposed `Table` extension function `select` doesn't execute eagerly so it can be used directly. +val exampleName = databaseClient.executeQuery(Examples.select(Examples.name).where(Examples.id eq 1)) + .single()[Examples.name] + +databaseClient.executeSingleUpdate(Examples.deleteWhereStatement { Examples.id eq 1 }) // The function `deleteWhereStatement` still depends on the old DSL and will be updated. +databaseClient.executeSingleUpdate(Examples.deleteIgnoreWhereStatement { id eq 2 }) +``` + +#### Extension SQL DSL APIs + +With these extension APIs, your code becomes more concise, but it might be more difficult when you need to compose statements or edit the code: + +```kotlin +databaseClient.insert(Examples) { it[name] = "A" } +val isInserted = databaseClient.insertIgnore(Examples) { it[name] = "B" } + +val updateRowCount = databaseClient.update(Examples, { Examples.id eq 1 }) { it[name] = "AA" } + +val exampleName1 = + databaseClient.select(Examples) { select(Examples.name).where(Examples.id eq 1) }.single()[Examples.name] +// This function still depends on the old SELECT DSL and will be updated. +val exampleName2 = + databaseClient.selectSingleColumn(Examples, Examples.name) { selectAll().where(Examples.id eq 2) }.single() + +val deleteRowCount1 = databaseClient.deleteWhere(Examples) { id eq 1 } +assert(deleteRowCount1 == 1) +val deleteRowCount2 = databaseClient.deleteIgnoreWhere(Examples) { id eq 2 } +assert(deleteRowCount2 == 1) +``` + +#### APIs using [Exposed GADT mapping](https://github.com/huanshankeji/exposed-adt-mapping) + +Please read [that library's basic usage guide](https://github.com/huanshankeji/exposed-adt-mapping?tab=readme-ov-file#basic-usage-guide) first. Here are examples of this library that correspond to [that library's CRUD operations](https://github.com/huanshankeji/exposed-adt-mapping?tab=readme-ov-file#crud-operations). + +```kotlin +val directorId = 1 +val director = Director(directorId, "George Lucas") +databaseClient.insert(Directors, director, Mappers.director) + +val episodeIFilmDetails = FilmDetails(1, "Star Wars: Episode I – The Phantom Menace", directorId) +// insert without the ID since it's `AUTO_INCREMENT` +databaseClient.insert(Films, episodeIFilmDetails, Mappers.filmDetailsWithDirectorId) + +val filmId = 2 +val episodeIIFilmDetails = FilmDetails(2, "Star Wars: Episode II – Attack of the Clones", directorId) +val filmWithDirectorId = FilmWithDirectorId(filmId, episodeIIFilmDetails) +databaseClient.insert(Films, filmWithDirectorId, Mappers.filmWithDirectorId) // insert with the ID + +val fullFilms = databaseClient.select(filmsLeftJoinDirectors, Mappers.fullFilm) { + select(Films.filmId inList listOf(1, 2)) // This API still depends on the old SELECT DSL and will be refactored. +} +``` + +### About the code -Also see https://github.com/huanshankeji/kotlin-common/tree/main/exposed for some dependency code which serves this -library. +Also see for some dependency code which serves this library. diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index c7076f6..bf8774c 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,7 +15,8 @@ repositories { } dependencies { - implementation(kotlin("gradle-plugin", "1.9.23")) - implementation("com.huanshankeji:common-gradle-dependencies:0.7.1-20240314") - implementation("com.huanshankeji.team:gradle-plugins:0.5.1") + // With Kotlin 2.0.20, a "Could not parse POM" build error occurs in the JVM projects of some dependent projects. + implementation(kotlin("gradle-plugin", "2.0.10")) + implementation("com.huanshankeji:common-gradle-dependencies:0.8.0-20241016") // don't use a snapshot version in a main branch + implementation("com.huanshankeji.team:gradle-plugins:0.6.0") // don't use a snapshot version in a main branch } diff --git a/buildSrc/src/main/kotlin/VersionsAndDependencies.kt b/buildSrc/src/main/kotlin/VersionsAndDependencies.kt index e958818..4453aa3 100644 --- a/buildSrc/src/main/kotlin/VersionsAndDependencies.kt +++ b/buildSrc/src/main/kotlin/VersionsAndDependencies.kt @@ -2,12 +2,13 @@ import com.huanshankeji.CommonDependencies import com.huanshankeji.CommonGradleClasspathDependencies import com.huanshankeji.CommonVersions -val projectVersion = "0.3.0" +val projectVersion = "0.4.0" -val commonVersions = CommonVersions() +// don't use a snapshot version in a main branch +val commonVersions = CommonVersions(kotlinCommon = "0.5.1") val commonDependencies = CommonDependencies(commonVersions) val commonGradleClasspathDependencies = CommonGradleClasspathDependencies(commonVersions) object DependencyVersions { - val exposedAdtMapping = "0.1.0" + val exposedAdtMapping = "0.2.0" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd49..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ea3535..79eb9d0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index 1aa94a4..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 25da30d..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/ConnectionConfig.kt b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/ConnectionConfig.kt new file mode 100644 index 0000000..09c7365 --- /dev/null +++ b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/ConnectionConfig.kt @@ -0,0 +1,24 @@ +package com.huanshankeji.exposedvertxsqlclient + +sealed interface ConnectionConfig { + val userAndRole: String + val database: String + + class Socket( + val host: String, + val port: Int? = null, // `null` for the default port + val user: String, + val password: String, + override val database: String + ) : ConnectionConfig { + override val userAndRole: String get() = user + } + + class UnixDomainSocketWithPeerAuthentication( + val path: String, + val role: String, + override val database: String + ) : ConnectionConfig { + override val userAndRole: String get() = role + } +} \ No newline at end of file diff --git a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/ConnectionType.kt b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/ConnectionType.kt new file mode 100644 index 0000000..54d1b56 --- /dev/null +++ b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/ConnectionType.kt @@ -0,0 +1,5 @@ +package com.huanshankeji.exposedvertxsqlclient + +enum class ConnectionType { + Socket, UnixDomainSocketWithPeerAuthentication +} \ No newline at end of file diff --git a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/DatabaseClient.kt b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/DatabaseClient.kt index 67936fd..f66ac8a 100644 --- a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/DatabaseClient.kt +++ b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/DatabaseClient.kt @@ -1,6 +1,7 @@ package com.huanshankeji.exposedvertxsqlclient import arrow.core.* +import com.huanshankeji.collections.singleOrNullIfEmpty import com.huanshankeji.exposedvertxsqlclient.ConnectionConfig.Socket import com.huanshankeji.exposedvertxsqlclient.ConnectionConfig.UnixDomainSocketWithPeerAuthentication import com.huanshankeji.exposedvertxsqlclient.sql.selectExpression @@ -9,7 +10,7 @@ import com.huanshankeji.vertx.kotlin.coroutines.coroutineToFuture import com.huanshankeji.vertx.kotlin.sqlclient.executeBatchAwaitForSqlResultSequence import io.vertx.core.Vertx import io.vertx.core.buffer.Buffer -import io.vertx.kotlin.coroutines.await +import io.vertx.kotlin.coroutines.coAwait import io.vertx.kotlin.sqlclient.poolOptionsOf import io.vertx.pgclient.PgConnectOptions import io.vertx.pgclient.PgConnection @@ -31,7 +32,7 @@ import kotlin.sequences.Sequence import org.jetbrains.exposed.sql.Transaction as ExposedTransaction @ExperimentalEvscApi -typealias ExposedArguments = Iterable> +typealias ExposedArguments = Iterable, Any?>> @ExperimentalEvscApi fun Statement<*>.singleStatementArguments() = @@ -94,7 +95,7 @@ class DatabaseClient( val logSql: Boolean = false ) { suspend fun close() { - vertxSqlClient.close().await() + vertxSqlClient.close().coAwait() // How to close The Exposed `Database`? } @@ -108,7 +109,7 @@ class DatabaseClient( suspend fun executePlainSql(sql: String): RowSet = /** Use [SqlClient.preparedQuery] here because of [PgConnectOptions.setCachePreparedStatements]. */ - vertxSqlClient.preparedQuery(sql).execute().await() + vertxSqlClient.preparedQuery(sql).execute().coAwait() suspend fun executePlainSqlUpdate(sql: String): Int = executePlainSql(sql).rowCount() @@ -161,7 +162,7 @@ class DatabaseClient( return vertxSqlClient.preparedQuery(sql) .transformQuery() .run { if (argTuple === null) execute() else execute(argTuple) } - .await() + .coAwait() } suspend fun executeForVertxSqlClientRowSet(statement: Statement<*>): RowSet = @@ -171,10 +172,24 @@ class DatabaseClient( suspend fun executeWithMapping(statement: Statement<*>, RowMapper: Function): RowSet = execute(statement) { mapping(RowMapper) } + // TODO call `getFieldExpressionSet` inside existing transactions (the ones used to prepare the query) to further optimize the performance + @ExperimentalEvscApi + fun FieldSet.getFieldExpressionSetWithTransaction() = + exposedTransaction { getFieldExpressionSet() } + + @Deprecated("This function is called nowhere except `Row.toExposedResultRowWithTransaction`. Consider inlining and removing it.") + @ExperimentalEvscApi + fun Query.getFieldExpressionSetWithTransaction() = + set.getFieldExpressionSetWithTransaction() + + @ExperimentalEvscApi + fun Row.toExposedResultRowWithTransaction(query: Query) = + toExposedResultRow(query.getFieldExpressionSetWithTransaction()) + suspend inline fun executeQuery( query: Query, crossinline resultRowMapper: ResultRow.() -> Data ): RowSet = - executeWithMapping(query) { row -> row.toExposedResultRow(query).resultRowMapper() } + executeWithMapping(query) { row -> row.toExposedResultRowWithTransaction(query).resultRowMapper() } suspend fun executeQuery(query: Query): RowSet = executeQuery(query) { this } @@ -279,7 +294,7 @@ class DatabaseClient( suspend inline fun executeBatchQuery( fieldSet: FieldSet, queries: Iterable, crossinline resultRowMapper: ResultRow.() -> Data ): Sequence> { - val fieldExpressionSet = fieldSet.getFieldExpressionSet() + val fieldExpressionSet = fieldSet.getFieldExpressionSetWithTransaction() return executeBatch(queries) { mapping { row -> row.toExposedResultRow(fieldExpressionSet).resultRowMapper() } } @@ -288,6 +303,11 @@ class DatabaseClient( suspend fun executeBatchQuery(fieldSet: FieldSet, queries: Iterable): Sequence> = executeBatchQuery(fieldSet, queries) { this } + /* + TODO Consider basing it on `Sequence` instead of `Iterable` so there is less wrapping and conversion + when mapping as sequences, such as `asSequence` and `toIterable`. + Also consider adding both versions. + */ /** * Executes a batch of update statements, including [InsertStatement] and [UpdateStatement]. * @see org.jetbrains.exposed.sql.batchInsert @@ -303,10 +323,13 @@ class DatabaseClient( fun RowSet.singleResult(): R = single() -// TODO consider moving into "kotlin-common" and renaming to "singleOrZero" /** "single or no" means differently here from [Iterable.singleOrNull]. */ +@Deprecated( + "Just use `singleOrNullIfEmpty` from \"kotlin-common\".", + ReplaceWith("this.singleOrNullIfEmpty()", "com.huanshankeji.collections.singleOrNullIfEmpty") +) fun RowSet.singleOrNoResult(): R? = - if (none()) null else single() + singleOrNullIfEmpty() fun Row.toExposedResultRow(fieldExpressionSet: Set>) = ResultRow.createAndFillValues( @@ -320,13 +343,29 @@ fun Row.toExposedResultRow(fieldExpressionSet: Set>) = }.toMap() ) +private const val USE_THE_ONE_IN_DATABASE_CLIENT_BECAUSE_TRANSACTION_REQUIRED_MESSAGE = + "Use the one in `DatabaseClient` because a transaction may be required." + +/** + * An Exposed transaction is required if the [FieldSet] contains custom functions that depend on dialects. + */ +//@Deprecated(USE_THE_ONE_IN_DATABASE_CLIENT_BECAUSE_TRANSACTION_REQUIRED_MESSAGE) fun FieldSet.getFieldExpressionSet() = /** [org.jetbrains.exposed.sql.AbstractQuery.ResultIterator.fieldsIndex] */ realFields.toSet() +/** + * @see FieldSet.getFieldExpressionSet + */ +@Deprecated("This function is called nowhere except `Row.toExposedResultRow`. Consider inlining and removing it.") +//@Deprecated(USE_THE_ONE_IN_DATABASE_CLIENT_BECAUSE_TRANSACTION_REQUIRED_MESSAGE) fun Query.getFieldExpressionSet() = set.getFieldExpressionSet() +/** + * @see FieldSet.getFieldExpressionSet + */ +//@Deprecated(USE_THE_ONE_IN_DATABASE_CLIENT_BECAUSE_TRANSACTION_REQUIRED_MESSAGE) fun Row.toExposedResultRow(query: Query) = toExposedResultRow(query.getFieldExpressionSet()) @@ -349,7 +388,7 @@ suspend fun DatabaseClient.withTransaction(function: suspend (Databa coroutineScope { vertxSqlClient.withTransaction { coroutineToFuture { function(DatabaseClient(it, exposedDatabase)) } - }.await() + }.coAwait() } suspend fun DatabaseClient.withPgTransaction(function: suspend (DatabaseClient) -> T): T = @@ -359,7 +398,7 @@ suspend fun DatabaseClient.withPgTransaction(function: suspend (Data } suspend fun DatabaseClient.withTransactionCommitOrRollback(function: suspend (DatabaseClient) -> Option): Option { - val transaction = vertxSqlClient.begin().await() + val transaction = vertxSqlClient.begin().coAwait() return try { val result = function(this) when (result) { @@ -424,33 +463,6 @@ suspend fun DatabaseClient.withSavepointAndRollbackIfThrowsOrFalse ): Boolean = withSavepointAndRollbackIfThrowsOrLeft(savepointName) { if (function(it)) Unit.right() else Unit.left() }.isRight() -enum class ConnectionType { - Socket, UnixDomainSocketWithPeerAuthentication -} - -sealed interface ConnectionConfig { - val userAndRole: String - val database: String - - class Socket( - val host: String, - val port: Int? = null, // `null` for the default port - val user: String, - val password: String, - override val database: String - ) : ConnectionConfig { - override val userAndRole: String get() = user - } - - class UnixDomainSocketWithPeerAuthentication( - val path: String, - val role: String, - override val database: String - ) : ConnectionConfig { - override val userAndRole: String get() = role - } -} - // TODO: use `ConnectionConfig` as the argument directly // can be used for a shared Exposed `Database` among `DatabaseClient`s diff --git a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/EvscConfig.kt b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/EvscConfig.kt new file mode 100644 index 0000000..fae1efb --- /dev/null +++ b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/EvscConfig.kt @@ -0,0 +1,16 @@ +package com.huanshankeji.exposedvertxsqlclient + +@ExperimentalEvscApi +interface IEvscConfig { + val exposedConnectionConfig: ConnectionConfig.Socket + val vertxSqlClientConnectionConfig: ConnectionConfig +} + +/** + * This API is not used in the factory function parameter types yet. TODO + */ +@ExperimentalEvscApi +class EvscConfig( + override val exposedConnectionConfig: ConnectionConfig.Socket, + override val vertxSqlClientConnectionConfig: ConnectionConfig +) : IEvscConfig diff --git a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/LocalConnectionConfig.kt b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/LocalConnectionConfig.kt index c56498d..dbfd61c 100644 --- a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/LocalConnectionConfig.kt +++ b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/LocalConnectionConfig.kt @@ -3,6 +3,10 @@ package com.huanshankeji.exposedvertxsqlclient import com.huanshankeji.net.LOCALHOST // TODO: move to a separate package and consider adding a prefix word such as "default" or "conventional" as this class is not general enough +/** + * A kind of connection config that can produce both a [ConnectionConfig.Socket] and a [ConnectionConfig.UnixDomainSocketWithPeerAuthentication] + * to connect to a local database server. + */ class LocalConnectionConfig(val database: String, val user: String, val socketConnectionPassword: String) { companion object { const val UNIX_DOMAIN_SOCKET_PATH = "/var/run/postgresql" @@ -15,3 +19,11 @@ class LocalConnectionConfig(val database: String, val user: String, val socketCo val unixDomainSocketWithPeerAuthenticationConnectionConfig = ConnectionConfig.UnixDomainSocketWithPeerAuthentication(UNIX_DOMAIN_SOCKET_PATH, user, database) } + +@ExperimentalEvscApi +fun LocalConnectionConfig.toPerformantUnixEvscConfig() = + EvscConfig(socketConnectionConfig, unixDomainSocketWithPeerAuthenticationConnectionConfig) + +@ExperimentalEvscApi +fun LocalConnectionConfig.toUniversalEvscConfig() = + EvscConfig(socketConnectionConfig, socketConnectionConfig) diff --git a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/classpropertymapping/ClassPropertyMapping.kt b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/classpropertymapping/ClassPropertyMapping.kt index 078c4db..784ede4 100644 --- a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/classpropertymapping/ClassPropertyMapping.kt +++ b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/classpropertymapping/ClassPropertyMapping.kt @@ -9,7 +9,8 @@ import kotlin.reflect.KClass /** * @see ClassPropertyColumnMappings */ -typealias ClassPropertyColumnIndexMappings = Nothing // TODO +// since Kotlin 2.0.0: "'Nothing' property type can't be specified with type alias." +typealias ClassPropertyColumnIndexMappings = Unit // TODO typealias VertxSqlClientRowDataQueryMapper = RowDataQueryMapper diff --git a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/DatabaseClientSql.kt b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/DatabaseClientSql.kt index 454f62d..4d0741a 100644 --- a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/DatabaseClientSql.kt +++ b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/DatabaseClientSql.kt @@ -25,15 +25,18 @@ suspend inline fun DatabaseClient<*>.select( ): RowSet = executeQuery(columnSet.buildQuery(), resultRowMapper) +// TODO adapt to the new SELECT DSL or deprecate suspend inline fun DatabaseClient<*>.select( columnSet: ColumnSet, buildQuery: ColumnSet.() -> Query ): RowSet = @Suppress("MoveLambdaOutsideParentheses") select(columnSet, buildQuery, { this }) + +// TODO adapt to the new SELECT DSL or deprecate /** * SQL: `SELECT FROM ;`. - * Examples: `SELECT COUNT(*) FROM
;`, `SELECT COUNT(*) FROM
;`. + * Examples: `SELECT COUNT(*) FROM
;`, `SELECT SUM() FROM
;`. */ @ExperimentalEvscApi suspend fun DatabaseClient<*>.selectTableExpression( @@ -59,6 +62,7 @@ suspend inline fun DatabaseClient<*>.executeSingleColumnSelectQuery( ): RowSet = selectSingleColumn(columnSet, column, buildQuery, mapper) +// TODO adapt to the new SELECT DSL or deprecate suspend fun DatabaseClient<*>.selectSingleColumn( columnSet: ColumnSet, column: Column, buildQuery: FieldSet.() -> Query ): RowSet = @@ -70,6 +74,7 @@ suspend fun DatabaseClient<*>.executeSingleColumnSelectQuery( ): RowSet = selectSingleColumn(columnSet, column, buildQuery) +// TODO adapt to the new SELECT DSL or deprecate suspend fun > DatabaseClient<*>.selectSingleEntityIdColumn( columnSet: ColumnSet, column: Column>, buildQuery: FieldSet.() -> Query ): RowSet = diff --git a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/mapping/DatabaseClientSqlWithMapper.kt b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/mapping/DatabaseClientSqlWithMapper.kt index ea125a8..c6319e5 100644 --- a/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/mapping/DatabaseClientSqlWithMapper.kt +++ b/lib/src/main/kotlin/com/huanshankeji/exposedvertxsqlclient/sql/mapping/DatabaseClientSqlWithMapper.kt @@ -9,7 +9,6 @@ import com.huanshankeji.exposed.deleteWhereStatement import com.huanshankeji.exposedvertxsqlclient.DatabaseClient import com.huanshankeji.exposedvertxsqlclient.ExperimentalEvscApi import com.huanshankeji.exposedvertxsqlclient.sql.* -import com.huanshankeji.exposedvertxsqlclient.toExposedResultRow import com.huanshankeji.vertx.sqlclient.datamapping.RowDataQueryMapper import io.vertx.sqlclient.RowSet import org.jetbrains.exposed.sql.* @@ -23,7 +22,7 @@ suspend fun DatabaseClient<*>.executeQuery( query: Query, dataQueryMapper: DataQueryMapper ): RowSet = - executeWithMapping(query) { row -> dataQueryMapper.resultRowToData(row.toExposedResultRow(query)) } + executeWithMapping(query) { row -> dataQueryMapper.resultRowToData(row.toExposedResultRowWithTransaction(query)) } @ExperimentalEvscApi suspend fun DatabaseClient<*>.executeVertxSqlClientRowQuery( diff --git a/lib/src/test/kotlin/com/huanshankeji/exposedvertxsqlclient/Examples.kt b/lib/src/test/kotlin/com/huanshankeji/exposedvertxsqlclient/Examples.kt new file mode 100644 index 0000000..d87ca02 --- /dev/null +++ b/lib/src/test/kotlin/com/huanshankeji/exposedvertxsqlclient/Examples.kt @@ -0,0 +1,76 @@ +package com.huanshankeji.exposedvertxsqlclient + +import com.huanshankeji.exposed.* +import com.huanshankeji.exposedvertxsqlclient.sql.* +import com.huanshankeji.exposedvertxsqlclient.sql.mapping.deleteIgnoreWhere +import com.huanshankeji.exposedvertxsqlclient.sql.mapping.deleteWhere +import io.vertx.core.Vertx +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.SchemaUtils +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.selectAll + +object Examples : IntIdTable("examples") { + val name = varchar("name", 64) +} + +val tables = arrayOf(Examples) + +@OptIn(ExperimentalEvscApi::class) +suspend fun examples(vertx: Vertx) { + val socketConnectionConfig = + ConnectionConfig.Socket("localhost", user = "user", password = "password", database = "database") + val exposedDatabase = exposedDatabaseConnectPostgreSql(socketConnectionConfig) + val databaseClient = createPgPoolDatabaseClient( + vertx, socketConnectionConfig, exposedDatabase = exposedDatabase + ) + + withContext(Dispatchers.IO) { + databaseClient.exposedTransaction { + SchemaUtils.create(*tables) + } + } + + run { + // The Exposed `Table` extension functions `insert`, `update`, and `delete` execute eagerly so `insertStatement`, `updateStatement`, `deleteStatement` have to be used. + + val insertRowCount = databaseClient.executeUpdate(Examples.insertStatement { it[name] = "A" }) + assert(insertRowCount == 1) + // `executeSingleUpdate` function requires that there is only 1 row updated and returns `Unit`. + databaseClient.executeSingleUpdate(Examples.insertStatement { it[name] = "B" }) + // `executeSingleOrNoUpdate` requires that there is 0 or 1 row updated and returns `Boolean`. + val isInserted = databaseClient.executeSingleOrNoUpdate(Examples.insertIgnoreStatement { it[name] = "B" }) + assert(isInserted) + + val updateRowCount = + databaseClient.executeUpdate(Examples.updateStatement({ Examples.id eq 1 }) { it[name] = "AA" }) + assert(updateRowCount == 1) + + // The Exposed `Table` extension function `select` doesn't execute eagerly so it can be used directly. + val exampleName = databaseClient.executeQuery(Examples.select(Examples.name).where(Examples.id eq 1)) + .single()[Examples.name] + + databaseClient.executeSingleUpdate(Examples.deleteWhereStatement { Examples.id eq 1 }) // The function `deleteWhereStatement` still depends on the old DSL and will be updated. + databaseClient.executeSingleUpdate(Examples.deleteIgnoreWhereStatement { id eq 2 }) + } + + run { + databaseClient.insert(Examples) { it[name] = "A" } + val isInserted = databaseClient.insertIgnore(Examples) { it[name] = "B" } + + val updateRowCount = databaseClient.update(Examples, { Examples.id eq 1 }) { it[name] = "AA" } + + val exampleName1 = + databaseClient.select(Examples) { select(Examples.name).where(Examples.id eq 1) }.single()[Examples.name] + // This function still depends on the old SELECT DSL and will be updated. + val exampleName2 = + databaseClient.selectSingleColumn(Examples, Examples.name) { selectAll().where(Examples.id eq 2) }.single() + + val deleteRowCount1 = databaseClient.deleteWhere(Examples) { id eq 1 } + assert(deleteRowCount1 == 1) + val deleteRowCount2 = databaseClient.deleteIgnoreWhere(Examples) { id eq 2 } + assert(deleteRowCount2 == 1) + } +} \ No newline at end of file diff --git a/lib/src/test/kotlin/com/huanshankeji/exposedvertxsqlclient/MappingExamples.kt b/lib/src/test/kotlin/com/huanshankeji/exposedvertxsqlclient/MappingExamples.kt new file mode 100644 index 0000000..4daca30 --- /dev/null +++ b/lib/src/test/kotlin/com/huanshankeji/exposedvertxsqlclient/MappingExamples.kt @@ -0,0 +1,101 @@ +package com.huanshankeji.exposedvertxsqlclient + +import com.huanshankeji.exposed.datamapping.classproperty.PropertyColumnMappingConfig +import com.huanshankeji.exposed.datamapping.classproperty.reflectionBasedClassPropertyDataMapper +import com.huanshankeji.exposedvertxsqlclient.sql.mapping.insert +import com.huanshankeji.exposedvertxsqlclient.sql.mapping.select +import io.vertx.sqlclient.Pool +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList +import org.jetbrains.exposed.sql.select + +// copied and adapted from https://github.com/huanshankeji/exposed-adt-mapping/blob/main/lib/src/test/kotlin/com/huanshankeji/exposed/datamapping/classproperty/Examples.kt +// Update accordingly to keep the code consistent. +// TODO also consider publishing the "exposed-adt-mapping" example code as a library and depend on it + +object Directors : IntIdTable("directors") { + val directorId = id + val name = varchar("name", 50) +} + +object Films : IntIdTable() { + val filmId = id + val sequelId = integer("sequel_id").uniqueIndex() + val name = varchar("name", 50) + val directorId = integer("director_id").references(Directors.directorId) +} + +val filmsLeftJoinDirectors = Films leftJoin Directors + + +typealias DirectorId = Int + +class Director(val directorId: DirectorId, val name: String) + +class FilmDetails( + val sequelId: Int, + val name: String, + val director: DirectorT +) +typealias FilmDetailsWithDirectorId = FilmDetails + +typealias FilmId = Int + +class Film(val filmId: FilmId, val filmDetails: FilmDetails) +typealias FilmWithDirectorId = Film +typealias FullFilm = Film + + +object Mappers { + val director = reflectionBasedClassPropertyDataMapper(Directors) + val filmDetailsWithDirectorId = reflectionBasedClassPropertyDataMapper( + Films, + propertyColumnMappingConfigMapOverride = mapOf( + // The default name is the property name "director", but there is no column property with such a name, therefore we need to pass a custom name. + FilmDetailsWithDirectorId::director to PropertyColumnMappingConfig.create(columnPropertyName = Films::directorId.name) + ) + ) + val filmWithDirectorId = reflectionBasedClassPropertyDataMapper( + Films, + propertyColumnMappingConfigMapOverride = mapOf( + FilmWithDirectorId::filmDetails to PropertyColumnMappingConfig.create( + // You can pass a nested custom mapper. + customMapper = filmDetailsWithDirectorId + ) + ) + ) + val fullFilm = reflectionBasedClassPropertyDataMapper( + filmsLeftJoinDirectors, + propertyColumnMappingConfigMapOverride = mapOf( + FullFilm::filmDetails to PropertyColumnMappingConfig.create( + adt = PropertyColumnMappingConfig.Adt.Product( + mapOf( + // Because `name` is a duplicate name column so a custom mapper has to be passed here, otherwise the `CHOOSE_FIRST` option maps the data property `Director::name` to the wrong column `Films::name`. + FilmDetails::director to PropertyColumnMappingConfig.create(customMapper = director) + ) + ) + ) + ) + ) +} + + +@OptIn(ExperimentalEvscApi::class) +suspend fun mappingExamples(databaseClient: DatabaseClient) { + val directorId = 1 + val director = Director(directorId, "George Lucas") + databaseClient.insert(Directors, director, Mappers.director) + + val episodeIFilmDetails = FilmDetails(1, "Star Wars: Episode I – The Phantom Menace", directorId) + // insert without the ID since it's `AUTO_INCREMENT` + databaseClient.insert(Films, episodeIFilmDetails, Mappers.filmDetailsWithDirectorId) + + val filmId = 2 + val episodeIIFilmDetails = FilmDetails(2, "Star Wars: Episode II – Attack of the Clones", directorId) + val filmWithDirectorId = FilmWithDirectorId(filmId, episodeIIFilmDetails) + databaseClient.insert(Films, filmWithDirectorId, Mappers.filmWithDirectorId) // insert with the ID + + val fullFilms = databaseClient.select(filmsLeftJoinDirectors, Mappers.fullFilm) { + select(Films.filmId inList listOf(1, 2)) // This API still depends on the old SELECT DSL and will be refactored. + } +}