diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34d1cfb6..263ec503 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,6 @@ Pull requests are always welcome and can be a quick way to get your fix or impro - By contributing to Ktorm, you agree to uphold our [Code of Conduct](CODE_OF_CONDUCT.md). - By contributing to Ktorm, you agree that your contributions will be licensed under [Apache License 2.0](LICENSE). -- Coding Conventions are very import. Refer to the [Kotlin Style Guide](https://kotlinlang.org/docs/reference/coding-conventions.html) for the recommended coding standards of Ktorm. +- Coding Conventions are very important. Refer to the [Kotlin Style Guide](https://kotlinlang.org/docs/reference/coding-conventions.html) for the recommended coding standards of Ktorm. - If you've added code that should be tested, add tests and ensure they all pass. If you've changed APIs, update the documentation. - If it's your first time contributing to Ktorm, please also update the `build.gradle` file, add your GitHub ID to the developers info, which will let more people know your contributions. diff --git a/build.gradle b/build.gradle index dff43a29..55c9ce83 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { kotlinVersion = "1.4.21" - detektVersion = "1.12.0-RC1" + detektVersion = "1.15.0" } repositories { jcenter() diff --git a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt index 584d83f2..2c619283 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/database/SqlDialect.kt @@ -17,6 +17,7 @@ package org.ktorm.database import org.ktorm.expression.ArgumentExpression +import org.ktorm.expression.ForUpdateOption import org.ktorm.expression.QueryExpression import org.ktorm.expression.SqlFormatter import java.sql.Statement @@ -50,6 +51,10 @@ public interface SqlDialect { */ public fun createSqlFormatter(database: Database, beautifySql: Boolean, indentSize: Int): SqlFormatter { return object : SqlFormatter(database, beautifySql, indentSize) { + override fun writeForUpdate(forUpdate: ForUpdateOption) { + throw DialectFeatureNotSupportedException("ForUpdate is not supported in Standard SQL.") + } + override fun writePagination(expr: QueryExpression) { throw DialectFeatureNotSupportedException("Pagination is not supported in Standard SQL.") } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt index 71dbc793..71d5f359 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/dsl/Query.kt @@ -763,13 +763,13 @@ public fun Query.joinToString( } /** - * Indicate that this query should aquire the record-lock, the generated SQL would be `select ... for update`. + * Indicate that this query should acquire the record-lock, the generated SQL will depend on the SqlDialect. * * @since 3.1.0 */ -public fun Query.forUpdate(): Query { +public fun Query.forUpdate(forUpdate: ForUpdateOption): Query { val expr = when (expression) { - is SelectExpression -> expression.copy(forUpdate = true) + is SelectExpression -> expression.copy(forUpdate = forUpdate) is UnionExpression -> throw IllegalStateException("SELECT FOR UPDATE is not supported in a union expression.") } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt index 2cc56c55..1f926889 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/entity/EntitySequence.kt @@ -19,6 +19,7 @@ package org.ktorm.entity import org.ktorm.database.Database import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.dsl.* +import org.ktorm.expression.ForUpdateOption import org.ktorm.expression.OrderByExpression import org.ktorm.expression.SelectExpression import org.ktorm.schema.BaseTable @@ -1505,10 +1506,11 @@ public fun EntitySequence.joinToString( } /** - * Indicate that this query should aquire the record-lock, the generated SQL would be `select ... for update`. + * Indicate that this query should acquire the record-lock, the generated SQL will depend on the SqlDialect. * * @since 3.1.0 */ -public fun > EntitySequence.forUpdate(): EntitySequence { - return this.withExpression(expression.copy(forUpdate = true)) +public fun > EntitySequence.forUpdate( + forUpdate: ForUpdateOption): EntitySequence { + return this.withExpression(expression.copy(forUpdate = forUpdate)) } diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt index e15fe706..096b17c1 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlExpressions.kt @@ -115,7 +115,8 @@ public sealed class QueryExpression : QuerySourceExpression() { * @property groupBy the grouping conditions, represents the `group by` clause of SQL. * @property having the having condition, represents the `having` clause of SQL. * @property isDistinct mark if this query is distinct, true means the SQL is `select distinct ...`. - * @property forUpdate mark if this query should aquire the record-lock, true means the SQL is `select ... for update`. + * @property forUpdate mark if this query should acquire the record-lock, non-null will generate a dialect-specific + * `for update` clause. */ public data class SelectExpression( val columns: List> = emptyList(), @@ -124,7 +125,7 @@ public data class SelectExpression( val groupBy: List> = emptyList(), val having: ScalarExpression? = null, val isDistinct: Boolean = false, - val forUpdate: Boolean = false, + val forUpdate: ForUpdateOption = ForUpdateOption.None, override val orderBy: List = emptyList(), override val offset: Int? = null, override val limit: Int? = null, @@ -132,6 +133,15 @@ public data class SelectExpression( override val extraProperties: Map = emptyMap() ) : QueryExpression() +/** + * ForUpdateOption, database-specific implementations are in support module for each database dialect. + */ +public interface ForUpdateOption { + public companion object { + public val None: ForUpdateOption = object : ForUpdateOption {} + } +} + /** * Union expression, represents a `union` statement of SQL. * diff --git a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt index 854b81f0..199e45ce 100644 --- a/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt +++ b/ktorm-core/src/main/kotlin/org/ktorm/expression/SqlFormatter.kt @@ -388,12 +388,14 @@ public abstract class SqlFormatter( if (expr.offset != null || expr.limit != null) { writePagination(expr) } - if (expr.forUpdate) { - writeKeyword("for update ") + if (expr.forUpdate != ForUpdateOption.None) { + writeForUpdate(expr.forUpdate) } return expr } + protected abstract fun writeForUpdate(forUpdate: ForUpdateOption) + override fun visitQuerySource(expr: QuerySourceExpression): QuerySourceExpression { when (expr) { is TableExpression -> { diff --git a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt index c58e0bc1..85c5086a 100644 --- a/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt +++ b/ktorm-core/src/test/kotlin/org/ktorm/dsl/QueryTest.kt @@ -259,33 +259,6 @@ class QueryTest : BaseTest() { println(query.sql) } - @Test - fun testSelctForUpdate() { - database.useTransaction { - val employee = database - .sequenceOf(Employees, withReferences = false) - .filter { it.id eq 1 } - .forUpdate() - .first() - - val future = Executors.newSingleThreadExecutor().submit { - employee.name = "vince" - employee.flushChanges() - } - - try { - future.get(5, TimeUnit.SECONDS) - throw AssertionError() - } catch (e: ExecutionException) { - // Expected, the record is locked. - e.printStackTrace() - } catch (e: TimeoutException) { - // Expected, the record is locked. - e.printStackTrace() - } - } - } - @Test fun testFlatMap() { val names = database diff --git a/ktorm-support-mysql/ktorm-support-mysql.gradle b/ktorm-support-mysql/ktorm-support-mysql.gradle index 3e2f1f00..6b401a15 100644 --- a/ktorm-support-mysql/ktorm-support-mysql.gradle +++ b/ktorm-support-mysql/ktorm-support-mysql.gradle @@ -5,5 +5,5 @@ dependencies { testImplementation project(path: ":ktorm-core", configuration: "testOutput") testImplementation project(":ktorm-jackson") testImplementation "mysql:mysql-connector-java:8.0.13" - testImplementation "org.testcontainers:mysql:1.11.3" + testImplementation "org.testcontainers:mysql:1.15.1" } \ No newline at end of file diff --git a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt index 3a6c8aa2..79113620 100644 --- a/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt +++ b/ktorm-support-mysql/src/main/kotlin/org/ktorm/support/mysql/MySqlDialect.kt @@ -17,10 +17,15 @@ package org.ktorm.support.mysql import org.ktorm.database.Database +import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType import org.ktorm.schema.VarcharSqlType +import org.ktorm.support.mysql.MySqlForUpdateOption.ForShare +import org.ktorm.support.mysql.MySqlForUpdateOption.ForUpdate +import org.ktorm.support.mysql.Version.MySql5 +import org.ktorm.support.mysql.Version.MySql8 /** * [SqlDialect] implementation for MySQL database. @@ -32,12 +37,33 @@ public open class MySqlDialect : SqlDialect { } } +private enum class Version { + MySql5, MySql8 +} + +/** + * Thrown to indicate that the MySql version is not supported by the dialect. + * + * @param databaseMetaData used to format the exception's message. + */ +public class UnsupportedMySqlVersionException(productVersion: String) : + UnsupportedOperationException("Unsupported MySql version $productVersion.") { + private companion object { + private const val serialVersionUID = 1L + } +} + /** * [SqlFormatter] implementation for MySQL, formatting SQL expressions as strings with their execution arguments. */ public open class MySqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { + private val version: Version = when { + database.productVersion.startsWith("5") -> MySql5 + database.productVersion.startsWith("8") -> MySql8 + else -> throw UnsupportedMySqlVersionException(database.productVersion) + } override fun visit(expr: SqlExpression): SqlExpression { val result = when (expr) { @@ -129,6 +155,30 @@ public open class MySqlFormatter( write(") ") return expr } + + override fun writeForUpdate(forUpdate: ForUpdateOption) { + when { + forUpdate == ForUpdate -> writeKeyword("for update ") + forUpdate == ForShare && version == MySql5 -> writeKeyword("lock in share mode ") + forUpdate == ForShare && version == MySql8 -> writeKeyword("for share ") + else -> throw DialectFeatureNotSupportedException( + "Unsupported ForUpdateOption ${forUpdate::class.java.name}." + ) + } + } +} + +/** + * MySql Specific ForUpdateOptions. + */ +public sealed class MySqlForUpdateOption : ForUpdateOption { + /** + * The generated SQL would be `select ... lock in share mode` for MySql 5 and `select ... for share` for MySql 8. + **/ + public object ForShare : MySqlForUpdateOption() + + /** The generated SQL would be `select ... for update`. */ + public object ForUpdate : MySqlForUpdateOption() } /** diff --git a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt index 4ca68ead..125a240f 100644 --- a/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt +++ b/ktorm-support-mysql/src/test/kotlin/org/ktorm/support/mysql/MySqlTest.kt @@ -36,7 +36,7 @@ class MySqlTest : BaseTest() { const val ID_3 = 3 const val ID_4 = 4 - class KMySqlContainer : MySQLContainer() + class KMySqlContainer : MySQLContainer("mysql:8") @ClassRule @JvmField @@ -417,12 +417,12 @@ class MySqlTest : BaseTest() { } @Test - fun testSelctForUpdate() { + fun testSelectForUpdate() { database.useTransaction { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate() + .forUpdate(MySqlForUpdateOption.ForUpdate) .first() val future = Executors.newSingleThreadExecutor().submit { diff --git a/ktorm-support-oracle/ktorm-support-oracle.gradle b/ktorm-support-oracle/ktorm-support-oracle.gradle index 6231376e..0c4bdc8c 100644 --- a/ktorm-support-oracle/ktorm-support-oracle.gradle +++ b/ktorm-support-oracle/ktorm-support-oracle.gradle @@ -4,5 +4,5 @@ dependencies { testImplementation project(path: ":ktorm-core", configuration: "testOutput") testImplementation fileTree(dir: "lib", includes: ["*.jar"]) - testImplementation "org.testcontainers:oracle-xe:1.11.3" + testImplementation "org.testcontainers:oracle-xe:1.15.1" } \ No newline at end of file diff --git a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt index 834530d1..8cc7e843 100644 --- a/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt +++ b/ktorm-support-oracle/src/main/kotlin/org/ktorm/support/oracle/OracleDialect.kt @@ -21,6 +21,7 @@ import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType +import org.ktorm.support.oracle.OracleForUpdateOption.ForUpdate /** * [SqlDialect] implementation for Oracle database. @@ -32,6 +33,14 @@ public open class OracleDialect : SqlDialect { } } +/** + * Oracle Specific ForUpdateOptions. + */ +public sealed class OracleForUpdateOption : ForUpdateOption { + /** The generated SQL would be `select ... for update`. */ + public object ForUpdate : OracleForUpdateOption() +} + /** * [SqlFormatter] implementation for Oracle, formatting SQL expressions as strings with their execution arguments. */ @@ -51,11 +60,20 @@ public open class OracleFormatter( return identifier.startsWith('_') || super.shouldQuote(identifier) } + override fun writeForUpdate(forUpdate: ForUpdateOption) { + when (forUpdate) { + ForUpdate -> writeKeyword("for update ") + else -> throw DialectFeatureNotSupportedException( + "Unsupported ForUpdateOption ${forUpdate::class.java.name}." + ) + } + } + override fun visitQuery(expr: QueryExpression): QueryExpression { if (expr.offset == null && expr.limit == null) { return super.visitQuery(expr) } - if (expr is SelectExpression && expr.forUpdate) { + if (expr is SelectExpression && expr.forUpdate != ForUpdateOption.None) { throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") } diff --git a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt index beb868f1..6a465f2c 100644 --- a/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt +++ b/ktorm-support-oracle/src/test/kotlin/org/ktorm/support/oracle/OracleTest.kt @@ -155,7 +155,7 @@ class OracleTest : BaseTest() { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate() + .forUpdate(OracleForUpdateOption.ForUpdate) .single() val future = Executors.newSingleThreadExecutor().submit { diff --git a/ktorm-support-postgresql/ktorm-support-postgresql.gradle b/ktorm-support-postgresql/ktorm-support-postgresql.gradle index c2917f96..a7c8e612 100644 --- a/ktorm-support-postgresql/ktorm-support-postgresql.gradle +++ b/ktorm-support-postgresql/ktorm-support-postgresql.gradle @@ -4,5 +4,5 @@ dependencies { testImplementation project(path: ":ktorm-core", configuration: "testOutput") testImplementation "org.postgresql:postgresql:42.2.5" - testImplementation "org.testcontainers:postgresql:1.11.3" + testImplementation "org.testcontainers:postgresql:1.15.1" } \ No newline at end of file diff --git a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt index 7aae5851..829c88da 100644 --- a/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt +++ b/ktorm-support-postgresql/src/main/kotlin/org/ktorm/support/postgresql/PostgreSqlDialect.kt @@ -17,9 +17,11 @@ package org.ktorm.support.postgresql import org.ktorm.database.Database +import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* import org.ktorm.schema.IntSqlType +import org.ktorm.schema.Table /** * [SqlDialect] implementation for PostgreSQL database. @@ -31,12 +33,61 @@ public open class PostgreSqlDialect : SqlDialect { } } +/** + * Postgres Specific ForUpdateOption. See docs: https://www.postgresql.org/docs/13/sql-select.html#SQL-FOR-UPDATE-SHARE + */ +public class PostgresForUpdateOption( + private val lockStrength: LockStrength, + private val onLock: OnLock, + private vararg val tables: Table<*> +) : ForUpdateOption { + + /** + * Generates SQL locking clause. + */ + public fun toLockingClause(): String { + val lockingClause = StringBuilder(lockStrength.keywords) + if (tables.isNotEmpty()) { + tables.joinTo(lockingClause, prefix = "of ", postfix = " ") { it.tableName } + } + onLock.keywords?.let { lockingClause.append(it) } + return lockingClause.toString() + } + + /** + * Lock strength. + */ + public enum class LockStrength(public val keywords: String) { + Update("for update "), + NoKeyUpdate("for no key update "), + Share("for share "), + KeyShare("for key share ") + } + + /** + * Behavior when a lock is detected. + */ + public enum class OnLock(public val keywords: String?) { + Wait(null), + NoWait("no wait "), + SkipLocked("skip locked ") + } +} + /** * [SqlFormatter] implementation for PostgreSQL, formatting SQL expressions as strings with their execution arguments. */ public open class PostgreSqlFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { + override fun writeForUpdate(forUpdate: ForUpdateOption) { + when (forUpdate) { + is PostgresForUpdateOption -> writeKeyword(forUpdate.toLockingClause()) + else -> throw DialectFeatureNotSupportedException( + "Unsupported ForUpdateOption ${forUpdate::class.java.name}." + ) + } + } override fun checkColumnName(name: String) { val maxLength = database.maxColumnNameLength diff --git a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt index 2a026e7d..b3eed969 100644 --- a/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt +++ b/ktorm-support-postgresql/src/test/kotlin/org/ktorm/support/postgresql/PostgreSqlTest.kt @@ -16,6 +16,8 @@ import org.ktorm.schema.ColumnDeclaring import org.ktorm.schema.Table import org.ktorm.schema.int import org.ktorm.schema.varchar +import org.ktorm.support.postgresql.PostgresForUpdateOption.LockStrength.Update +import org.ktorm.support.postgresql.PostgresForUpdateOption.OnLock.Wait import org.testcontainers.containers.PostgreSQLContainer import java.time.LocalDate import java.util.concurrent.ExecutionException @@ -29,7 +31,7 @@ import java.util.concurrent.TimeoutException class PostgreSqlTest : BaseTest() { companion object { - class KPostgreSqlContainer : PostgreSQLContainer() + class KPostgreSqlContainer : PostgreSQLContainer("postgres:13-alpine") @ClassRule @JvmField @@ -393,12 +395,12 @@ class PostgreSqlTest : BaseTest() { } @Test - fun testSelctForUpdate() { + fun testSelectForUpdate() { database.useTransaction { val employee = database .sequenceOf(Employees, withReferences = false) .filter { it.id eq 1 } - .forUpdate() + .forUpdate(PostgresForUpdateOption(lockStrength = Update, onLock = Wait)) .first() val future = Executors.newSingleThreadExecutor().submit { diff --git a/ktorm-support-sqlite/ktorm-support-sqlite.gradle b/ktorm-support-sqlite/ktorm-support-sqlite.gradle index 03952d87..9c274ae2 100644 --- a/ktorm-support-sqlite/ktorm-support-sqlite.gradle +++ b/ktorm-support-sqlite/ktorm-support-sqlite.gradle @@ -3,5 +3,5 @@ dependencies { api project(":ktorm-core") testImplementation project(path: ":ktorm-core", configuration: "testOutput") - testImplementation "org.xerial:sqlite-jdbc:3.28.0" + testImplementation "org.xerial:sqlite-jdbc:3.34.0" } diff --git a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt index d6d90cd9..8b3a51cb 100644 --- a/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt +++ b/ktorm-support-sqlite/src/main/kotlin/org/ktorm/support/sqlite/SQLiteDialect.kt @@ -18,6 +18,7 @@ package org.ktorm.support.sqlite import org.ktorm.database.* import org.ktorm.expression.ArgumentExpression +import org.ktorm.expression.ForUpdateOption import org.ktorm.expression.QueryExpression import org.ktorm.expression.SqlFormatter import org.ktorm.schema.IntSqlType @@ -62,6 +63,9 @@ public open class SQLiteDialect : SqlDialect { public open class SQLiteFormatter( database: Database, beautifySql: Boolean, indentSize: Int ) : SqlFormatter(database, beautifySql, indentSize) { + override fun writeForUpdate(forUpdate: ForUpdateOption) { + throw DialectFeatureNotSupportedException("SQLite does not support SELECT ... FOR UPDATE.") + } override fun writePagination(expr: QueryExpression) { newLine(Indentation.SAME) diff --git a/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle b/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle index 24c74768..bcdb531e 100644 --- a/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle +++ b/ktorm-support-sqlserver/ktorm-support-sqlserver.gradle @@ -4,5 +4,5 @@ dependencies { api "com.microsoft.sqlserver:mssql-jdbc:7.2.2.jre8" testImplementation project(path: ":ktorm-core", configuration: "testOutput") - testImplementation "org.testcontainers:mssqlserver:1.11.3" + testImplementation "org.testcontainers:mssqlserver:1.15.1" } diff --git a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt index 062cae3a..e7cd298e 100644 --- a/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt +++ b/ktorm-support-sqlserver/src/main/kotlin/org/ktorm/support/sqlserver/SqlServerDialect.kt @@ -20,6 +20,7 @@ import org.ktorm.database.Database import org.ktorm.database.DialectFeatureNotSupportedException import org.ktorm.database.SqlDialect import org.ktorm.expression.* +import org.ktorm.support.sqlserver.SqlServerForUpdateOption.ForUpdate /** * [SqlDialect] implementation for Microsoft SqlServer database. @@ -31,6 +32,14 @@ public open class SqlServerDialect : SqlDialect { } } +/** + * SqlServer Specific ForUpdateOptions. + */ +public sealed class SqlServerForUpdateOption : ForUpdateOption { + /** The generated SQL would be `select ... for update`. */ + public object ForUpdate : SqlServerForUpdateOption() +} + /** * [SqlFormatter] implementation for SqlServer, formatting SQL expressions as strings with their execution arguments. */ @@ -45,11 +54,20 @@ public open class SqlServerFormatter( } } + override fun writeForUpdate(forUpdate: ForUpdateOption) { + when (forUpdate) { + ForUpdate -> writeKeyword("for update ") + else -> throw DialectFeatureNotSupportedException( + "Unsupported ForUpdateOption ${forUpdate::class.java.name}." + ) + } + } + override fun visitQuery(expr: QueryExpression): QueryExpression { if (expr.offset == null && expr.limit == null) { return super.visitQuery(expr) } - if (expr is SelectExpression && expr.forUpdate) { + if (expr is SelectExpression && expr.forUpdate != ForUpdateOption.None) { throw DialectFeatureNotSupportedException("SELECT FOR UPDATE not supported when using offset/limit params.") } diff --git a/ktorm-support-sqlserver/src/test/kotlin/org/ktorm/support/sqlserver/SqlServerTest.kt b/ktorm-support-sqlserver/src/test/kotlin/org/ktorm/support/sqlserver/SqlServerTest.kt index 93a5cb0d..b4205390 100644 --- a/ktorm-support-sqlserver/src/test/kotlin/org/ktorm/support/sqlserver/SqlServerTest.kt +++ b/ktorm-support-sqlserver/src/test/kotlin/org/ktorm/support/sqlserver/SqlServerTest.kt @@ -37,7 +37,7 @@ class SqlServerTest : BaseTest() { const val ID_3 = 3 const val ID_4 = 4 - class KSqlServerContainer : MSSQLServerContainer() + class KSqlServerContainer : MSSQLServerContainer("mcr.microsoft.com/mssql/server:2017-CU12") @ClassRule @JvmField