diff --git a/dialects/hsql/src/main/kotlin/app/cash/sqldelight/dialects/hsql/grammar/hsql.bnf b/dialects/hsql/src/main/kotlin/app/cash/sqldelight/dialects/hsql/grammar/hsql.bnf index 6cce18d0de3..65befeda298 100644 --- a/dialects/hsql/src/main/kotlin/app/cash/sqldelight/dialects/hsql/grammar/hsql.bnf +++ b/dialects/hsql/src/main/kotlin/app/cash/sqldelight/dialects/hsql/grammar/hsql.bnf @@ -79,7 +79,8 @@ column_constraint ::= [ CONSTRAINT {identifier} ] ( implements = "com.alecstrong.sql.psi.core.psi.SqlColumnConstraint" override = true } -bind_parameter ::= ( DEFAULT | '?' | ':' {identifier} ) { +bind_parameter ::= DEFAULT | ( '?' | ':' {identifier} ) { + mixin = "app.cash.sqldelight.dialect.grammar.mixins.BindParameterMixin" extends = "com.alecstrong.sql.psi.core.psi.impl.SqlBindParameterImpl" implements = "com.alecstrong.sql.psi.core.psi.SqlBindParameter" override = true diff --git a/dialects/mysql/src/main/kotlin/app/cash/sqldelight/dialects/mysql/grammar/MySql.bnf b/dialects/mysql/src/main/kotlin/app/cash/sqldelight/dialects/mysql/grammar/MySql.bnf index 9230ea0ec3d..26505f8f889 100644 --- a/dialects/mysql/src/main/kotlin/app/cash/sqldelight/dialects/mysql/grammar/MySql.bnf +++ b/dialects/mysql/src/main/kotlin/app/cash/sqldelight/dialects/mysql/grammar/MySql.bnf @@ -111,7 +111,8 @@ column_constraint ::= [ CONSTRAINT {identifier} ] ( implements = "com.alecstrong.sql.psi.core.psi.SqlColumnConstraint" override = true } -bind_parameter ::= ( '?' | ':' {identifier} ) { +bind_parameter ::= DEFAULT | ( '?' | ':' {identifier} ) { + mixin = "app.cash.sqldelight.dialect.grammar.mixins.BindParameterMixin" extends = "com.alecstrong.sql.psi.core.psi.impl.SqlBindParameterImpl" implements = "com.alecstrong.sql.psi.core.psi.SqlBindParameter" override = true diff --git a/dialects/postgresql/src/main/kotlin/app/cash/sqldelight/dialects/postgresql/grammar/PostgreSql.bnf b/dialects/postgresql/src/main/kotlin/app/cash/sqldelight/dialects/postgresql/grammar/PostgreSql.bnf index 50f85881755..9d6e5f64c5c 100644 --- a/dialects/postgresql/src/main/kotlin/app/cash/sqldelight/dialects/postgresql/grammar/PostgreSql.bnf +++ b/dialects/postgresql/src/main/kotlin/app/cash/sqldelight/dialects/postgresql/grammar/PostgreSql.bnf @@ -110,7 +110,8 @@ type_name ::= ( implements = "com.alecstrong.sql.psi.core.psi.SqlTypeName" override = true } -bind_parameter ::= ( DEFAULT | '?' | ':' {identifier} ) { +bind_parameter ::= DEFAULT | ( '?' | ':' {identifier} ) { + mixin = "app.cash.sqldelight.dialects.postgresql.grammar.mixins.BindParameterMixin" extends = "com.alecstrong.sql.psi.core.psi.impl.SqlBindParameterImpl" implements = "com.alecstrong.sql.psi.core.psi.SqlBindParameter" override = true @@ -118,7 +119,7 @@ bind_parameter ::= ( DEFAULT | '?' | ':' {identifier} ) { identity_clause ::= 'IDENTITY' -generated_clause ::= GENERATED ( (ALWAYS AS <> 'STORED') | ( (ALWAYS | BY DEFAULT) AS identity_clause ) ) { +generated_clause ::= GENERATED ( (ALWAYS AS LP <> RP 'STORED') | ( (ALWAYS | BY DEFAULT) AS identity_clause ) ) { extends = "com.alecstrong.sql.psi.core.psi.impl.SqlGeneratedClauseImpl" implements = "com.alecstrong.sql.psi.core.psi.SqlGeneratedClause" override = true diff --git a/dialects/postgresql/src/main/kotlin/app/cash/sqldelight/dialects/postgresql/grammar/mixins/BindParameterMixin.kt b/dialects/postgresql/src/main/kotlin/app/cash/sqldelight/dialects/postgresql/grammar/mixins/BindParameterMixin.kt new file mode 100644 index 00000000000..05cd7cb9f77 --- /dev/null +++ b/dialects/postgresql/src/main/kotlin/app/cash/sqldelight/dialects/postgresql/grammar/mixins/BindParameterMixin.kt @@ -0,0 +1,11 @@ +package app.cash.sqldelight.dialects.postgresql.grammar.mixins + +import app.cash.sqldelight.dialect.grammar.mixins.BindParameterMixin +import com.intellij.lang.ASTNode + +abstract class BindParameterMixin(node: ASTNode) : BindParameterMixin(node) { + override fun replaceWith(isAsync: Boolean, index: Int): String = when { + isAsync -> "$$index" + else -> "?" + } +} diff --git a/dialects/postgresql/src/test/kotlin/app/cash/sqldelight/dialects/postgres/PostgreSqlFixturesTest.kt b/dialects/postgresql/src/test/kotlin/app/cash/sqldelight/dialects/postgres/PostgreSqlFixturesTest.kt index 3443e8b9e90..1a332d2dd64 100644 --- a/dialects/postgresql/src/test/kotlin/app/cash/sqldelight/dialects/postgres/PostgreSqlFixturesTest.kt +++ b/dialects/postgresql/src/test/kotlin/app/cash/sqldelight/dialects/postgres/PostgreSqlFixturesTest.kt @@ -15,6 +15,7 @@ class PostgreSqlFixturesTest(name: String, fixtureRoot: File) : FixturesTest(nam "?1" to "?", "?2" to "?", "BLOB" to "TEXT", + "id TEXT GENERATED ALWAYS AS (2) UNIQUE NOT NULL" to "id TEXT GENERATED ALWAYS AS (2) STORED UNIQUE NOT NULL", ) override fun setupDialect() { diff --git a/dialects/sqlite-3-18/src/main/kotlin/app/cash/sqldelight/dialects/sqlite_3_18/grammar/sqlite.bnf b/dialects/sqlite-3-18/src/main/kotlin/app/cash/sqldelight/dialects/sqlite_3_18/grammar/sqlite.bnf index 2f0c664fd28..4297b957696 100644 --- a/dialects/sqlite-3-18/src/main/kotlin/app/cash/sqldelight/dialects/sqlite_3_18/grammar/sqlite.bnf +++ b/dialects/sqlite-3-18/src/main/kotlin/app/cash/sqldelight/dialects/sqlite_3_18/grammar/sqlite.bnf @@ -27,6 +27,7 @@ int_data_type ::= 'INTEGER' real_data_type ::= 'REAL' bind_parameter ::= ( '?' [digit] | ':' {identifier} ) { + mixin = "app.cash.sqldelight.dialect.grammar.mixins.BindParameterMixin" extends = "com.alecstrong.sql.psi.core.psi.impl.SqlBindParameterImpl" implements = "com.alecstrong.sql.psi.core.psi.SqlBindParameter" override = true diff --git a/drivers/r2dbc-driver/src/main/kotlin/app/cash/sqldelight/driver/r2dbc/R2dbcDriver.kt b/drivers/r2dbc-driver/src/main/kotlin/app/cash/sqldelight/driver/r2dbc/R2dbcDriver.kt index 346cac32c0c..2e8013f9561 100644 --- a/drivers/r2dbc-driver/src/main/kotlin/app/cash/sqldelight/driver/r2dbc/R2dbcDriver.kt +++ b/drivers/r2dbc-driver/src/main/kotlin/app/cash/sqldelight/driver/r2dbc/R2dbcDriver.kt @@ -48,7 +48,7 @@ class R2dbcDriver(private val connection: Connection) : SqlDriver { return QueryResult.AsyncValue { val result = prepared.execute().awaitSingle() - return@AsyncValue result.rowsUpdated.awaitFirstOrNull() ?: 0 + return@AsyncValue result.rowsUpdated.awaitFirstOrNull()?.toLong() ?: 0 } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e3bda41ff40..da7af9832c3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,7 +76,7 @@ testhelp = { module = "co.touchlab:testhelp", version.ref = "testhelp" } burst = { module = "com.squareup.burst:burst-junit4", version = "1.2.0" } testParameterInjector = { module = "com.google.testparameterinjector:test-parameter-injector", version = "1.8" } -r2dbc = { module = "io.r2dbc:r2dbc-spi", version = "1.0.0.RELEASE" } +r2dbc = { module = "io.r2dbc:r2dbc-spi", version = "0.9.1.RELEASE" } [plugins] android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/sqldelight-compiler/dialect/src/main/kotlin/app/cash/sqldelight/dialect/grammar/mixins/BindParameterMixin.kt b/sqldelight-compiler/dialect/src/main/kotlin/app/cash/sqldelight/dialect/grammar/mixins/BindParameterMixin.kt new file mode 100644 index 00000000000..ec7aa13969d --- /dev/null +++ b/sqldelight-compiler/dialect/src/main/kotlin/app/cash/sqldelight/dialect/grammar/mixins/BindParameterMixin.kt @@ -0,0 +1,15 @@ +package app.cash.sqldelight.dialect.grammar.mixins + +import com.alecstrong.sql.psi.core.psi.SqlBindParameter +import com.alecstrong.sql.psi.core.psi.SqlCompositeElementImpl +import com.intellij.lang.ASTNode + +abstract class BindParameterMixin(node: ASTNode) : SqlCompositeElementImpl(node), SqlBindParameter { + /** + * Overwrite, if the user provided sql parameter should be overwritten by sqldelight with [replaceWith]. + * + * Some sql dialects support other bind parameter besides `?`, but sqldelight should still replace the + * user provided parameter with [replaceWith] for a homogen generated code. + */ + open fun replaceWith(isAsync: Boolean, index: Int): String = "?" +} diff --git a/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/compiler/QueryGenerator.kt b/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/compiler/QueryGenerator.kt index 4ca108d707c..8c399b6a35b 100644 --- a/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/compiler/QueryGenerator.kt +++ b/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/compiler/QueryGenerator.kt @@ -19,6 +19,7 @@ import app.cash.sqldelight.core.lang.util.rawSqlText import app.cash.sqldelight.core.lang.util.sqFile import app.cash.sqldelight.core.psi.SqlDelightStmtClojureStmtList import app.cash.sqldelight.dialect.api.IntermediateType +import app.cash.sqldelight.dialect.grammar.mixins.BindParameterMixin import com.alecstrong.sql.psi.core.psi.SqlBinaryEqualityExpr import com.alecstrong.sql.psi.core.psi.SqlBindExpr import com.alecstrong.sql.psi.core.psi.SqlStmt @@ -174,36 +175,39 @@ abstract class QueryGenerator( precedingArrays.add(type.name) argumentCounts.add("${type.name}.size") } else { - nonArrayBindArgsCount += 1 - - if (!treatNullAsUnknownForEquality && type.javaType.isNullable) { - val parent = bindArg?.parent - if (parent is SqlBinaryEqualityExpr) { - needsFreshStatement = true - - var symbol = parent.childOfType(SqlTypes.EQ) ?: parent.childOfType(SqlTypes.EQ2) - val nullableEquality: String - if (symbol != null) { - nullableEquality = "${symbol.leftWhitspace()}IS${symbol.rightWhitespace()}" - } else { - symbol = parent.childOfType(SqlTypes.NEQ) ?: parent.childOfType(SqlTypes.NEQ2)!! - nullableEquality = "${symbol.leftWhitspace()}IS NOT${symbol.rightWhitespace()}" + val bindParameter = bindArg?.bindParameter as? BindParameterMixin + if (bindParameter == null || bindParameter.text != "DEFAULT") { + nonArrayBindArgsCount += 1 + + if (!treatNullAsUnknownForEquality && type.javaType.isNullable) { + val parent = bindArg?.parent + if (parent is SqlBinaryEqualityExpr) { + needsFreshStatement = true + + var symbol = parent.childOfType(SqlTypes.EQ) ?: parent.childOfType(SqlTypes.EQ2) + val nullableEquality: String + if (symbol != null) { + nullableEquality = "${symbol.leftWhitspace()}IS${symbol.rightWhitespace()}" + } else { + symbol = parent.childOfType(SqlTypes.NEQ) ?: parent.childOfType(SqlTypes.NEQ2)!! + nullableEquality = "${symbol.leftWhitspace()}IS NOT${symbol.rightWhitespace()}" + } + + val block = CodeBlock.of("if (${type.name} == null) \"$nullableEquality\" else \"${symbol.text}\"") + replacements.add(symbol.range to "\${ $block }") } - - val block = CodeBlock.of("if (${type.name} == null) \"$nullableEquality\" else \"${symbol.text}\"") - replacements.add(symbol.range to "\${ $block }") } - } - // Binds each parameter to the statement: - // statement.bindLong(0, id) - bindStatements.add(type.preparedStatementBinder(offset, extractedVariables[type])) + // Binds each parameter to the statement: + // statement.bindLong(0, id) + bindStatements.add(type.preparedStatementBinder(offset, extractedVariables[type])) - // Replace the named argument with a non named/indexed argument. - // This allows us to use the same algorithm for non Sqlite dialects - // :name becomes ? - if (bindArg != null) { - replacements.add(bindArg.range to "?") + // Replace the named argument with a non named/indexed argument. + // This allows us to use the same algorithm for non Sqlite dialects + // :name becomes ? + if (bindParameter != null) { + replacements.add(bindArg.range to bindParameter.replaceWith(generateAsync, index = nonArrayBindArgsCount)) + } } } } @@ -293,7 +297,7 @@ abstract class QueryGenerator( """ if (result%L == 0L) throw %T(%S) """.trimIndent(), - if (generateAsync) ".await()" else ".value", + if (generateAsync) "" else ".value", ClassName("app.cash.sqldelight.db", "OptimisticLockException"), "UPDATE on ${query.tablesAffected.single().name} failed because optimistic lock ${optimisticLock.name} did not match", ) diff --git a/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/compiler/model/BindableQuery.kt b/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/compiler/model/BindableQuery.kt index cf079f086c4..8d1dd5b895d 100644 --- a/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/compiler/model/BindableQuery.kt +++ b/sqldelight-compiler/src/main/kotlin/app/cash/sqldelight/core/compiler/model/BindableQuery.kt @@ -32,6 +32,7 @@ import app.cash.sqldelight.dialect.api.PrimitiveType.ARGUMENT import app.cash.sqldelight.dialect.api.PrimitiveType.BOOLEAN import app.cash.sqldelight.dialect.api.PrimitiveType.INTEGER import app.cash.sqldelight.dialect.api.PrimitiveType.NULL +import app.cash.sqldelight.dialect.grammar.mixins.BindParameterMixin import com.alecstrong.sql.psi.core.psi.SqlAnnotatedElement import com.alecstrong.sql.psi.core.psi.SqlBindExpr import com.alecstrong.sql.psi.core.psi.SqlBindParameter @@ -92,29 +93,32 @@ abstract class BindableQuery( val namesSeen = mutableSetOf() var maxIndexSeen = 0 statement.findChildrenOfType().forEach { bindArg -> - bindArg.bindParameter.node.findChildByType(SqlTypes.DIGIT)?.text?.toInt()?.let { index -> - if (!indexesSeen.add(index)) { - result.findAndReplace(bindArg, index) { it.index == index } + val bindParameter = bindArg.bindParameter + if (bindParameter is BindParameterMixin && bindParameter.text != "DEFAULT") { + bindParameter.node.findChildByType(SqlTypes.DIGIT)?.text?.toInt()?.let { index -> + if (!indexesSeen.add(index)) { + result.findAndReplace(bindArg, index) { it.index == index } + return@forEach + } + maxIndexSeen = maxOf(maxIndexSeen, index) + result.add(Argument(index, typeResolver.argumentType(bindArg), mutableListOf(bindArg))) return@forEach } - maxIndexSeen = maxOf(maxIndexSeen, index) - result.add(Argument(index, typeResolver.argumentType(bindArg), mutableListOf(bindArg))) - return@forEach - } - bindArg.bindParameter.identifier?.let { - if (!namesSeen.add(it.text)) { - result.findAndReplace(bindArg) { (_, type, _) -> type.name == it.text } + bindParameter.identifier?.let { + if (!namesSeen.add(it.text)) { + result.findAndReplace(bindArg) { (_, type, _) -> type.name == it.text } + return@forEach + } + val index = ++maxIndexSeen + indexesSeen.add(index) + manuallyNamedIndexes.add(index) + result.add(Argument(index, typeResolver.argumentType(bindArg).copy(name = it.text), mutableListOf(bindArg))) return@forEach } val index = ++maxIndexSeen indexesSeen.add(index) - manuallyNamedIndexes.add(index) - result.add(Argument(index, typeResolver.argumentType(bindArg).copy(name = it.text), mutableListOf(bindArg))) - return@forEach + result.add(Argument(index, typeResolver.argumentType(bindArg), mutableListOf(bindArg))) } - val index = ++maxIndexSeen - indexesSeen.add(index) - result.add(Argument(index, typeResolver.argumentType(bindArg), mutableListOf(bindArg))) } // If there are still naming conflicts (edge case where the name we generate is the same as diff --git a/sqldelight-gradle-plugin/build.gradle b/sqldelight-gradle-plugin/build.gradle index 406fa0f96eb..5948c4787c9 100644 --- a/sqldelight-gradle-plugin/build.gradle +++ b/sqldelight-gradle-plugin/build.gradle @@ -124,6 +124,8 @@ tasks.named('dockerTest') { ":sqlite-migrations:publishAllPublicationsToInstallLocallyRepository", ":sqldelight-compiler:publishAllPublicationsToInstallLocallyRepository", ":sqldelight-gradle-plugin:publishAllPublicationsToInstallLocallyRepository", + ":drivers:r2dbc-driver:publishAllPublicationsToInstallLocallyRepository", + ":extensions:async-extensions:publishAllPublicationsToInstallLocallyRepository", ) } diff --git a/sqldelight-gradle-plugin/src/dockerTest/kotlin/app/cash/sqldelight/dialect/DialectIntegrationTests.kt b/sqldelight-gradle-plugin/src/dockerTest/kotlin/app/cash/sqldelight/dialect/DialectIntegrationTests.kt index 0ea5af6bffa..256921c818d 100644 --- a/sqldelight-gradle-plugin/src/dockerTest/kotlin/app/cash/sqldelight/dialect/DialectIntegrationTests.kt +++ b/sqldelight-gradle-plugin/src/dockerTest/kotlin/app/cash/sqldelight/dialect/DialectIntegrationTests.kt @@ -16,6 +16,15 @@ class DialectIntegrationTests { Truth.assertThat(result.output).contains("BUILD SUCCESSFUL") } + @Test fun integrationTestsMySqlAsync() { + val runner = GradleRunner.create() + .withCommonConfiguration(File("src/test/integration-mysql-async")) + .withArguments("clean", "check", "--stacktrace") + + val result = runner.build() + Truth.assertThat(result.output).contains("BUILD SUCCESSFUL") + } + @Test fun integrationTestsMySqlSchemaDefinitions() { val runner = GradleRunner.create() .withCommonConfiguration(File("src/test/integration-mysql-schema")) @@ -34,6 +43,15 @@ class DialectIntegrationTests { Truth.assertThat(result.output).contains("BUILD SUCCESSFUL") } + @Test fun integrationTestsPostgreSqlAsync() { + val runner = GradleRunner.create() + .withCommonConfiguration(File("src/test/integration-postgresql-async")) + .withArguments("clean", "check", "--stacktrace") + + val result = runner.build() + Truth.assertThat(result.output).contains("BUILD SUCCESSFUL") + } + @Test fun `dialect accepts version catalog dependency`() { val runner = GradleRunner.create() .withCommonConfiguration(File("src/test/integration-catalog")) diff --git a/sqldelight-gradle-plugin/src/test/integration-hsql/src/main/sqldelight/app/cash/sqldelight/hsql/integration/Dog.sq b/sqldelight-gradle-plugin/src/test/integration-hsql/src/main/sqldelight/app/cash/sqldelight/hsql/integration/Dog.sq index f1d41860df4..e8a76594385 100644 --- a/sqldelight-gradle-plugin/src/test/integration-hsql/src/main/sqldelight/app/cash/sqldelight/hsql/integration/Dog.sq +++ b/sqldelight-gradle-plugin/src/test/integration-hsql/src/main/sqldelight/app/cash/sqldelight/hsql/integration/Dog.sq @@ -8,7 +8,7 @@ CREATE TABLE dog ( insertDog: INSERT INTO dog (name, breed, is_good, id) -VALUES (?, ?, ?, ?); +VALUES (?, ?, DEFAULT, ?); selectDogs: SELECT * diff --git a/sqldelight-gradle-plugin/src/test/integration-hsql/src/test/kotlin/app/cash/sqldelight/hsql/integration/HsqlTest.kt b/sqldelight-gradle-plugin/src/test/integration-hsql/src/test/kotlin/app/cash/sqldelight/hsql/integration/HsqlTest.kt index 94cd092ee67..3068d964ad3 100644 --- a/sqldelight-gradle-plugin/src/test/integration-hsql/src/test/kotlin/app/cash/sqldelight/hsql/integration/HsqlTest.kt +++ b/sqldelight-gradle-plugin/src/test/integration-hsql/src/test/kotlin/app/cash/sqldelight/hsql/integration/HsqlTest.kt @@ -29,7 +29,7 @@ class HsqlTest { } @Test fun simpleSelect() { - database.dogQueries.insertDog("Tilda", "Pomeranian", true, 1) + database.dogQueries.insertDog("Tilda", "Pomeranian", 1) assertThat(database.dogQueries.selectDogs().executeAsOne()) .isEqualTo( Dog( diff --git a/sqldelight-gradle-plugin/src/test/integration-mysql-async/build.gradle b/sqldelight-gradle-plugin/src/test/integration-mysql-async/build.gradle index f50a9ed2829..8b1f9614cb1 100644 --- a/sqldelight-gradle-plugin/src/test/integration-mysql-async/build.gradle +++ b/sqldelight-gradle-plugin/src/test/integration-mysql-async/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'app.cash.sqldelight' sqldelight { MyDatabase { - packageName = "app.cash.sqldelight.mysql.integration" + packageName = "app.cash.sqldelight.mysql.integration.async" dialect("app.cash.sqldelight:mysql-dialect:${app.cash.sqldelight.VersionKt.VERSION}") generateAsync = true } @@ -26,7 +26,7 @@ dependencies { implementation "org.testcontainers:r2dbc:1.16.2" implementation "dev.miku:r2dbc-mysql:0.8.2.RELEASE" implementation "app.cash.sqldelight:r2dbc-driver:${app.cash.sqldelight.VersionKt.VERSION}" - implementation "app.cash.sqldelight:coroutines-extensions:${app.cash.sqldelight.VersionKt.VERSION}" + implementation "app.cash.sqldelight:async-extensions:${app.cash.sqldelight.VersionKt.VERSION}" implementation libs.truth implementation libs.kotlin.coroutines.core implementation libs.kotlin.coroutines.test diff --git a/sqldelight-gradle-plugin/src/test/integration-mysql-async/settings.gradle b/sqldelight-gradle-plugin/src/test/integration-mysql-async/settings.gradle index 5b846acd802..4995799c151 100644 --- a/sqldelight-gradle-plugin/src/test/integration-mysql-async/settings.gradle +++ b/sqldelight-gradle-plugin/src/test/integration-mysql-async/settings.gradle @@ -1,3 +1,3 @@ apply from: "../settings.gradle" -rootProject.name = 'sqldelight-mysql-integration' +rootProject.name = 'sqldelight-mysql-integration-async' diff --git a/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/Dates.sq b/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/async/Dates.sq similarity index 100% rename from sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/Dates.sq rename to sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/async/Dates.sq diff --git a/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/Dog.sq b/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/async/Dog.sq similarity index 91% rename from sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/Dog.sq rename to sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/async/Dog.sq index 977e509000b..5ffd3d40620 100644 --- a/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/Dog.sq +++ b/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/main/sqldelight/app/cash/sqldelight/mysql/integration/async/Dog.sq @@ -6,7 +6,7 @@ CREATE TABLE dog ( insertDog: INSERT INTO dog -VALUES (?, ?, ?); +VALUES (?, ?, DEFAULT); selectDogs: SELECT * diff --git a/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/test/kotlin/app/cash/sqldelight/mysql/integration/MySqlTest.kt b/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/test/kotlin/app/cash/sqldelight/mysql/integration/async/MySqlTest.kt similarity index 65% rename from sqldelight-gradle-plugin/src/test/integration-mysql-async/src/test/kotlin/app/cash/sqldelight/mysql/integration/MySqlTest.kt rename to sqldelight-gradle-plugin/src/test/integration-mysql-async/src/test/kotlin/app/cash/sqldelight/mysql/integration/async/MySqlTest.kt index 259142b9261..ec657a6af6b 100644 --- a/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/test/kotlin/app/cash/sqldelight/mysql/integration/MySqlTest.kt +++ b/sqldelight-gradle-plugin/src/test/integration-mysql-async/src/test/kotlin/app/cash/sqldelight/mysql/integration/async/MySqlTest.kt @@ -1,5 +1,8 @@ -package app.cash.sqldelight.mysql.integration +package app.cash.sqldelight.mysql.integration.async +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitCreate import app.cash.sqldelight.driver.r2dbc.R2dbcDriver import com.google.common.truth.Truth.assertThat import io.r2dbc.spi.ConnectionFactories @@ -13,13 +16,13 @@ class MySqlTest { val connection = factory.create().awaitSingle() val driver = R2dbcDriver(connection) - val db = MyDatabase(driver).also { MyDatabase.Schema.create(driver) } + val db = MyDatabase(driver).also { MyDatabase.Schema.awaitCreate(driver) } block(db) } @Test fun simpleSelect() = runTest { database -> - database.dogQueries.insertDog("Tilda", "Pomeranian", true) - assertThat(database.dogQueries.selectDogs().executeAsOne()) + database.dogQueries.insertDog("Tilda", "Pomeranian") + assertThat(database.dogQueries.selectDogs().awaitAsOne()) .isEqualTo( Dog( name = "Tilda", @@ -32,15 +35,15 @@ class MySqlTest { @Test fun simpleSelectWithIn() = runTest { database -> with(database) { - dogQueries.insertDog("Tilda", "Pomeranian", true) - dogQueries.insertDog("Tucker", "Portuguese Water Dog", true) - dogQueries.insertDog("Cujo", "Pomeranian", false) - dogQueries.insertDog("Buddy", "Pomeranian", true) + dogQueries.insertDog("Tilda", "Pomeranian") + dogQueries.insertDog("Tucker", "Portuguese Water Dog") + dogQueries.insertDog("Cujo", "Pomeranian") + dogQueries.insertDog("Buddy", "Pomeranian") assertThat( dogQueries.selectDogsByBreedAndNames( breed = "Pomeranian", name = listOf("Tilda", "Buddy"), - ).executeAsList(), + ).awaitAsList(), ) .containsExactly( Dog( diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql-async/build.gradle b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/build.gradle new file mode 100644 index 00000000000..601620d8c43 --- /dev/null +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/build.gradle @@ -0,0 +1,33 @@ +buildscript { + apply from: "${projectDir.absolutePath}/../buildscript.gradle" +} + +apply plugin: 'org.jetbrains.kotlin.jvm' +apply plugin: 'app.cash.sqldelight' + +sqldelight { + MyDatabase { + packageName = "app.cash.sqldelight.postgresql.integration.async" + dialect("app.cash.sqldelight:postgresql-dialect:${app.cash.sqldelight.VersionKt.VERSION}") + generateAsync = true + } +} + +repositories { + maven { + url "file://${projectDir.absolutePath}/../../../../build/localMaven" + } + mavenCentral() +} + +dependencies { + implementation libs.postgresJdbc + implementation "org.testcontainers:postgresql:1.16.2" + implementation "org.testcontainers:r2dbc:1.16.2" + implementation "org.postgresql:r2dbc-postgresql:0.9.2.RELEASE" + implementation "app.cash.sqldelight:r2dbc-driver:${app.cash.sqldelight.VersionKt.VERSION}" + implementation "app.cash.sqldelight:async-extensions:${app.cash.sqldelight.VersionKt.VERSION}" + implementation libs.truth + implementation libs.kotlin.coroutines.test + implementation libs.kotlin.coroutines.reactive +} diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql-async/settings.gradle b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/settings.gradle new file mode 100644 index 00000000000..2af80808ac6 --- /dev/null +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/settings.gradle @@ -0,0 +1,3 @@ +apply from: "../settings.gradle" + +rootProject.name = 'sqldelight-postgresql-integration-async' diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/Arrays.sq b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/Arrays.sq new file mode 100644 index 00000000000..d4a440d0ab0 --- /dev/null +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/Arrays.sq @@ -0,0 +1,9 @@ +CREATE TABLE arrays( + intArray INTEGER[], + textArray TEXT[] +); + +insertAndReturn: +INSERT INTO arrays +VALUES (?, ?) +RETURNING *; \ No newline at end of file diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/Dates.sq b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/Dates.sq new file mode 100644 index 00000000000..115883395cf --- /dev/null +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/Dates.sq @@ -0,0 +1,17 @@ +CREATE TABLE dates( + date DATE NOT NULL, + time TIME NOT NULL, + timestamp TIMESTAMP NOT NULL, + timestamp_with_timezone TIMESTAMP WITH TIME ZONE NOT NULL +); + +insertDate: +INSERT INTO dates +VALUES (?, ?, ?, ?) +RETURNING *; + +selectDateTrunc: +SELECT date_trunc('hour', timestamp), date_trunc('hour', timestamp_with_timezone) FROM dates; + +selectNow: +SELECT NOW(); diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/Dog.sq b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/Dog.sq new file mode 100644 index 00000000000..138341ababe --- /dev/null +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/Dog.sq @@ -0,0 +1,23 @@ +CREATE TABLE dog ( + name VARCHAR(8) NOT NULL, + breed TEXT NOT NULL, + is_good INTEGER NOT NULL DEFAULT 1 +); + +insertDog: +INSERT INTO dog +VALUES (?, ?, DEFAULT); + +selectDogs: +SELECT * +FROM dog; + +selectGoodDogs: +SELECT * +FROM dog +WHERE :someBoolean AND 1 = 1; + +insertAndReturn: +INSERT INTO dog +VALUES (?, ?, DEFAULT) +RETURNING *; diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/OneEntity.sq b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/OneEntity.sq new file mode 100644 index 00000000000..8bad31ffcad --- /dev/null +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/OneEntity.sq @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS oneEntity ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); + +insert: +INSERT INTO oneEntity(name) +VALUES (?); + +selectAll: +SELECT * FROM oneEntity; + +insertUpdate: +INSERT INTO oneEntity AS one VALUES ? ON CONFLICT(id) DO UPDATE SET id = one.id + EXCLUDED.id, name = EXCLUDED.name; diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/WithLock.sq b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/WithLock.sq new file mode 100644 index 00000000000..bc1648ec9a8 --- /dev/null +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/async/WithLock.sq @@ -0,0 +1,25 @@ +CREATE TABLE withLock( + id SERIAL AS VALUE NOT NULL, + version INTEGER AS LOCK NOT NULL DEFAULT 0, + text TEXT NOT NULL +); + +insertText: +INSERT INTO withLock (text) +VALUES (?) +RETURNING *; + +updateText: +UPDATE withLock +SET + text = :text, + version = :version + 1 +WHERE + id = :id AND + version = :version +; + +selectForId: +SELECT * +FROM withLock +WHERE id = :id; \ No newline at end of file diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/test/kotlin/app/cash/sqldelight/postgresql/integration/async/PostgreSqlTest.kt b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/test/kotlin/app/cash/sqldelight/postgresql/integration/async/PostgreSqlTest.kt new file mode 100644 index 00000000000..2d90cebfd70 --- /dev/null +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql-async/src/test/kotlin/app/cash/sqldelight/postgresql/integration/async/PostgreSqlTest.kt @@ -0,0 +1,162 @@ +package app.cash.sqldelight.postgresql.integration.async + +import app.cash.sqldelight.async.coroutines.awaitAsList +import app.cash.sqldelight.async.coroutines.awaitAsOne +import app.cash.sqldelight.async.coroutines.awaitCreate +import app.cash.sqldelight.db.OptimisticLockException +import app.cash.sqldelight.driver.r2dbc.R2dbcDriver +import com.google.common.truth.Truth.assertThat +import io.r2dbc.spi.ConnectionFactories +import kotlinx.coroutines.reactive.awaitSingle +import org.junit.Assert +import org.junit.Test +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.OffsetDateTime +import java.time.ZoneOffset + +class PostgreSqlTest { + private val factory = ConnectionFactories.get("r2dbc:tc:postgresql:///myDb?TC_IMAGE_TAG=9.6.8") + + private fun runTest(block: suspend (MyDatabase) -> Unit) = kotlinx.coroutines.test.runTest { + val connection = factory.create().awaitSingle() + val driver = R2dbcDriver(connection) + val db = MyDatabase(driver) + MyDatabase.Schema.awaitCreate(driver) + block(db) + } + + @Test fun simpleSelect() = runTest { database -> + Assert.assertEquals(0, database.dogQueries.selectDogs().awaitAsList().size) + database.dogQueries.insertDog("Tilda", "Pomeranian") + assertThat(database.dogQueries.selectDogs().awaitAsOne()) + .isEqualTo( + Dog( + name = "Tilda", + breed = "Pomeranian", + is_good = 1, + ), + ) + } + + @Test fun booleanSelect() = runTest { database -> + database.dogQueries.insertDog("Tilda", "Pomeranian") + assertThat(database.dogQueries.selectGoodDogs(true).awaitAsOne()) + .isEqualTo( + Dog( + name = "Tilda", + breed = "Pomeranian", + is_good = 1, + ), + ) + } + + @Test fun returningInsert() = runTest { database -> + assertThat(database.dogQueries.insertAndReturn("Tilda", "Pomeranian").awaitAsOne()) + .isEqualTo( + Dog( + name = "Tilda", + breed = "Pomeranian", + is_good = 1, + ), + ) + } + + @Test fun testDates() = runTest { database -> + assertThat( + database.datesQueries.insertDate( + date = LocalDate.of(2020, 1, 1), + time = LocalTime.of(21, 30, 59, 10000), + timestamp = LocalDateTime.of(2020, 1, 1, 21, 30, 59, 10000), + timestamp_with_timezone = OffsetDateTime.of(1980, 4, 9, 20, 15, 45, 0, ZoneOffset.ofHours(0)), + ).awaitAsOne(), + ) + .isEqualTo( + Dates( + date = LocalDate.of(2020, 1, 1), + time = LocalTime.of(21, 30, 59, 10000), + timestamp = LocalDateTime.of(2020, 1, 1, 21, 30, 59, 10000), + timestamp_with_timezone = OffsetDateTime.of(1980, 4, 9, 20, 15, 45, 0, ZoneOffset.ofHours(0)), + ), + ) + } + + @Test fun testDateTrunc() = runTest { database -> + database.datesQueries.insertDate( + date = LocalDate.of(2020, 1, 1), + time = LocalTime.of(21, 30, 59, 10000), + timestamp = LocalDateTime.of(2020, 1, 1, 21, 30, 59, 10000), + timestamp_with_timezone = OffsetDateTime.of(1980, 4, 9, 20, 15, 45, 0, ZoneOffset.ofHours(0)), + ).awaitAsOne() + + assertThat( + database.datesQueries.selectDateTrunc().awaitAsOne(), + ) + .isEqualTo( + SelectDateTrunc( + date_trunc = LocalDateTime.of(2020, 1, 1, 21, 0, 0, 0), + date_trunc_ = OffsetDateTime.of(1980, 4, 9, 20, 0, 0, 0, ZoneOffset.ofHours(0)), + ), + ) + } + + @Test fun testSerial() = runTest { database -> + database.run { + oneEntityQueries.transaction { + oneEntityQueries.insert("name1") + oneEntityQueries.insert("name2") + oneEntityQueries.insert("name3") + } + assertThat(oneEntityQueries.selectAll().awaitAsList().map { it.id }).containsExactly(1, 2, 3) + } + } + + @Test fun testArrays() = runTest { database -> + with(database.arraysQueries.insertAndReturn(arrayOf(1, 2), arrayOf("one", "two")).awaitAsOne()) { + assertThat(intArray!!.asList()).containsExactly(1, 2).inOrder() + assertThat(textArray!!.asList()).containsExactly("one", "two").inOrder() + } + } + + @Test fun now() = runTest { database -> + val now = database.datesQueries.selectNow().awaitAsOne() + assertThat(now).isNotNull() + assertThat(now).isGreaterThan(OffsetDateTime.MIN) + } + + @Test fun successfulOptimisticLock() = runTest { database -> + with(database.withLockQueries) { + val row = insertText("sup").awaitAsOne() + + updateText( + id = row.id, + version = row.version, + text = "sup2", + ) + + assertThat(selectForId(row.id).awaitAsOne().text).isEqualTo("sup2") + } + } + + @Test fun unsuccessfulOptimisticLock() = runTest { database -> + with(database.withLockQueries) { + val row = insertText("sup").awaitAsOne() + + updateText( + id = row.id, + version = row.version, + text = "sup2", + ) + + try { + updateText( + id = row.id, + version = row.version, + text = "sup3", + ) + Assert.fail() + } catch (e: OptimisticLockException) { } + } + } +} diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/Dog.sq b/sqldelight-gradle-plugin/src/test/integration-postgresql/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/Dog.sq index 77c09eda934..138341ababe 100644 --- a/sqldelight-gradle-plugin/src/test/integration-postgresql/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/Dog.sq +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql/src/main/sqldelight/app/cash/sqldelight/postgresql/integration/Dog.sq @@ -6,7 +6,7 @@ CREATE TABLE dog ( insertDog: INSERT INTO dog -VALUES (?, ?, ?); +VALUES (?, ?, DEFAULT); selectDogs: SELECT * @@ -19,5 +19,5 @@ WHERE :someBoolean AND 1 = 1; insertAndReturn: INSERT INTO dog -VALUES (?, ?, ?) -RETURNING *; \ No newline at end of file +VALUES (?, ?, DEFAULT) +RETURNING *; diff --git a/sqldelight-gradle-plugin/src/test/integration-postgresql/src/test/kotlin/app/cash/sqldelight/postgresql/integration/PostgreSqlTest.kt b/sqldelight-gradle-plugin/src/test/integration-postgresql/src/test/kotlin/app/cash/sqldelight/postgresql/integration/PostgreSqlTest.kt index 28ebb8b8957..7c21fac9725 100644 --- a/sqldelight-gradle-plugin/src/test/integration-postgresql/src/test/kotlin/app/cash/sqldelight/postgresql/integration/PostgreSqlTest.kt +++ b/sqldelight-gradle-plugin/src/test/integration-postgresql/src/test/kotlin/app/cash/sqldelight/postgresql/integration/PostgreSqlTest.kt @@ -36,7 +36,7 @@ class PostgreSqlTest { } @Test fun simpleSelect() { - database.dogQueries.insertDog("Tilda", "Pomeranian", 1) + database.dogQueries.insertDog("Tilda", "Pomeranian") assertThat(database.dogQueries.selectDogs().executeAsOne()) .isEqualTo( Dog( @@ -48,7 +48,7 @@ class PostgreSqlTest { } @Test fun booleanSelect() { - database.dogQueries.insertDog("Tilda", "Pomeranian", 1) + database.dogQueries.insertDog("Tilda", "Pomeranian") assertThat(database.dogQueries.selectGoodDogs(true).executeAsOne()) .isEqualTo( Dog( @@ -60,7 +60,7 @@ class PostgreSqlTest { } @Test fun returningInsert() { - assertThat(database.dogQueries.insertAndReturn("Tilda", "Pomeranian", 1).executeAsOne()) + assertThat(database.dogQueries.insertAndReturn("Tilda", "Pomeranian").executeAsOne()) .isEqualTo( Dog( name = "Tilda",