diff --git a/exposed-core/api/exposed-core.api b/exposed-core/api/exposed-core.api index 213dd6da2a..b663c8e38b 100644 --- a/exposed-core/api/exposed-core.api +++ b/exposed-core/api/exposed-core.api @@ -1718,6 +1718,8 @@ public final class org/jetbrains/exposed/sql/QueriesKt { public static final fun insertIgnore (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/AbstractQuery;Ljava/util/List;)Ljava/lang/Integer; public static synthetic fun insertIgnore$default (Lorg/jetbrains/exposed/sql/Table;Lorg/jetbrains/exposed/sql/AbstractQuery;Ljava/util/List;ILjava/lang/Object;)Ljava/lang/Integer; public static final fun insertIgnoreAndGetId (Lorg/jetbrains/exposed/dao/id/IdTable;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/dao/id/EntityID; + public static final fun insertReturning (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/ReturningStatement; + public static synthetic fun insertReturning$default (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/statements/ReturningStatement; public static final fun replace (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/ReplaceStatement; public static final fun select (Lorg/jetbrains/exposed/sql/FieldSet;Lkotlin/jvm/functions/Function1;)Lorg/jetbrains/exposed/sql/Query; public static final fun select (Lorg/jetbrains/exposed/sql/FieldSet;Lorg/jetbrains/exposed/sql/Op;)Lorg/jetbrains/exposed/sql/Query; @@ -1738,6 +1740,8 @@ public final class org/jetbrains/exposed/sql/QueriesKt { public static synthetic fun update$default (Lorg/jetbrains/exposed/sql/Table;Lkotlin/jvm/functions/Function1;Ljava/lang/Integer;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)I public static final fun upsert (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/UpsertStatement; public static synthetic fun upsert$default (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/statements/UpsertStatement; + public static final fun upsertReturning (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lorg/jetbrains/exposed/sql/statements/ReturningStatement; + public static synthetic fun upsertReturning$default (Lorg/jetbrains/exposed/sql/Table;[Lorg/jetbrains/exposed/sql/Column;Ljava/util/List;Ljava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lorg/jetbrains/exposed/sql/statements/ReturningStatement; } public class org/jetbrains/exposed/sql/Query : org/jetbrains/exposed/sql/AbstractQuery { @@ -3000,6 +3004,19 @@ public class org/jetbrains/exposed/sql/statements/ReplaceStatement : org/jetbrai public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; } +public class org/jetbrains/exposed/sql/statements/ReturningStatement : org/jetbrains/exposed/sql/statements/Statement, java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker { + public fun (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Lorg/jetbrains/exposed/sql/statements/Statement;)V + public fun arguments ()Ljava/lang/Iterable; + public synthetic fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/Object; + public fun executeInternal (Lorg/jetbrains/exposed/sql/statements/api/PreparedStatementApi;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/sql/ResultSet; + public final fun getMainStatement ()Lorg/jetbrains/exposed/sql/statements/Statement; + public final fun getReturningExpressions ()Ljava/util/List; + public final fun getTable ()Lorg/jetbrains/exposed/sql/Table; + protected final fun getTransaction ()Lorg/jetbrains/exposed/sql/Transaction; + public fun iterator ()Ljava/util/Iterator; + public fun prepareSQL (Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; +} + public class org/jetbrains/exposed/sql/statements/SQLServerBatchInsertStatement : org/jetbrains/exposed/sql/statements/BatchInsertStatement { public fun (Lorg/jetbrains/exposed/sql/Table;ZZ)V public synthetic fun (Lorg/jetbrains/exposed/sql/Table;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V @@ -3667,6 +3684,7 @@ public abstract class org/jetbrains/exposed/sql/vendors/FunctionProvider { public fun regexp (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/Expression;ZLorg/jetbrains/exposed/sql/QueryBuilder;)V public fun replace (Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Transaction;Z)Ljava/lang/String; public static synthetic fun replace$default (Lorg/jetbrains/exposed/sql/vendors/FunctionProvider;Lorg/jetbrains/exposed/sql/Table;Ljava/util/List;Ljava/lang/String;Lorg/jetbrains/exposed/sql/Transaction;ZILjava/lang/Object;)Ljava/lang/String; + public fun returning (Ljava/lang/String;Ljava/util/List;Lorg/jetbrains/exposed/sql/Transaction;)Ljava/lang/String; public fun second (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun stdDevPop (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V public fun stdDevSamp (Lorg/jetbrains/exposed/sql/Expression;Lorg/jetbrains/exposed/sql/QueryBuilder;)V diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt index e752cf661d..a8a38b0bde 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt @@ -381,6 +381,23 @@ fun T.insertIgnore( columns: List> = this.columns.filter { !it.columnType.isAutoInc || it.autoIncColumnType?.nextValExpression != null } ): Int? = InsertSelectStatement(columns, selectQuery, true).execute(TransactionManager.current()) +/** + * Represents the SQL statement that inserts new rows into a table and returns specified data from the inserted rows. + * + * @param returning Columns and expressions to include in the returned data. This defaults to all columns in the table. + * @return A [ReturningStatement] that will be executed once iterated over, providing [ResultRow]s containing the specified + * expressions mapped to their resulting data. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.ReturningTests.testInsertReturning + */ +fun T.insertReturning( + returning: List> = columns, + body: T.(InsertStatement) -> Unit +): ReturningStatement { + val insert = InsertStatement(this) + body(insert) + return ReturningStatement(this, returning, insert) +} + /** * Represents the SQL statement that updates rows of a table. * @@ -434,6 +451,35 @@ fun T.upsert( execute(TransactionManager.current()) } +/** + * Represents the SQL statement that either inserts a new row into a table, or updates the existing row if insertion would + * violate a unique constraint, and also returns specified data from the modified rows. + * + * @param keys (optional) Columns to include in the condition that determines a unique constraint match. If no columns are + * provided, primary keys will be used. If the table does not have any primary keys, the first unique index will be attempted. + * @param returning Columns and expressions to include in the returned data. This defaults to all columns in the table. + * @param onUpdate List of pairs of specific columns to update and the expressions to update them with. + * If left null, all columns will be updated with the values provided for the insert. + * @param onUpdateExclude List of specific columns to exclude from updating. + * If left null, all columns will be updated with the values provided for the insert. + * @param where Condition that determines which rows to update, if a unique violation is found. + * @return A [ReturningStatement] that will be executed once iterated over, providing [ResultRow]s containing the specified + * expressions mapped to their resulting data. + * @sample org.jetbrains.exposed.sql.tests.shared.dml.ReturningTests.testUpsertReturning + */ +fun T.upsertReturning( + vararg keys: Column<*>, + returning: List> = columns, + onUpdate: List, Expression<*>>>? = null, + onUpdateExclude: List>? = null, + where: (SqlExpressionBuilder.() -> Op)? = null, + body: T.(UpsertStatement) -> Unit +): ReturningStatement { + val update = UpsertStatement(this, *keys, onUpdate = onUpdate, onUpdateExclude = onUpdateExclude, where = where?.let { SqlExpressionBuilder.it() }) + body(update) + return ReturningStatement(this, returning, update) +} + /** * Represents the SQL statement that either batch inserts new rows into a table, or updates the existing rows if insertions violate unique constraints. * diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/ReturningStatement.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/ReturningStatement.kt new file mode 100644 index 0000000000..0a76fa25a4 --- /dev/null +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/ReturningStatement.kt @@ -0,0 +1,66 @@ +package org.jetbrains.exposed.sql.statements + +import org.jetbrains.exposed.sql.Expression +import org.jetbrains.exposed.sql.IColumnType +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.Transaction +import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi +import org.jetbrains.exposed.sql.transactions.TransactionManager +import java.sql.ResultSet + +/** + * Represents the underlying SQL [mainStatement] that also returns a result set with data from any modified rows. + * + * @param table Table to perform the main statement on and return results from. + * @param returningExpressions Columns or expressions to include in the returned result set. + * @param mainStatement The statement to append the RETURNING clause to. This may be an insert, update, or delete statement. + */ +open class ReturningStatement( + val table: Table, + val returningExpressions: List>, + val mainStatement: Statement<*> +) : Iterable, Statement(mainStatement.type, listOf(table)) { + protected val transaction + get() = TransactionManager.current() + + override fun PreparedStatementApi.executeInternal(transaction: Transaction): ResultSet = executeQuery() + + override fun arguments(): Iterable, Any?>>> = mainStatement.arguments() + + override fun prepareSQL(transaction: Transaction, prepared: Boolean): String { + val mainSql = mainStatement.prepareSQL(transaction, prepared) + return transaction.db.dialect.functionProvider.returning(mainSql, returningExpressions, transaction) + } + + override fun iterator(): Iterator { + val resultIterator = ResultIterator(transaction.exec(this)!!) + return Iterable { resultIterator }.iterator() + } + + private inner class ResultIterator(val rs: ResultSet) : Iterator { + val fieldIndex = returningExpressions.withIndex().associateBy({ it.value }, { it.index }) + + private var hasNext = false + set(value) { + field = value + if (!field) { + rs.statement?.close() + transaction.openResultSetsCount-- + } + } + + init { + hasNext = rs.next() + } + + override fun hasNext(): Boolean = hasNext + + override operator fun next(): ResultRow { + if (!hasNext) throw NoSuchElementException() + val result = ResultRow.create(rs, fieldIndex) + hasNext = rs.next() + return result + } + } +} diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt index e8536c5fdd..42a06f5200 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/FunctionProvider.kt @@ -743,4 +743,23 @@ abstract class FunctionProvider { /** Appends optional parameters to an EXPLAIN query. */ protected open fun StringBuilder.appendOptionsToExplain(options: String) { append("$options ") } + + /** + * Returns the SQL command that performs an insert, update, or delete, and also returns data from any modified rows. + * + * **Note:** This operation is not supported by all vendors, please check the documentation. + * + * @param mainSql SQL string representing the underlying statement before appending a RETURNING clause. + * @param returning Columns and expressions to include in the returned result set. + * @param transaction Transaction where the operation is executed. + */ + open fun returning( + mainSql: String, + returning: List>, + transaction: Transaction + ): String { + transaction.throwUnsupportedException( + "There's no generic SQL for a command with a RETURNING clause. There must be a vendor specific implementation." + ) + } } diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt index 89ae7eab99..c21edbefb7 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/PostgreSQL.kt @@ -320,6 +320,18 @@ internal object PostgreSQLFunctionProvider : FunctionProvider() { } override fun StringBuilder.appendOptionsToExplain(options: String) { append("($options) ") } + + override fun returning( + mainSql: String, + returning: List>, + transaction: Transaction + ): String { + return with(QueryBuilder(true)) { + +"$mainSql RETURNING " + returning.appendTo { +it } + toString() + } + } } /** diff --git a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt index a2cc410a2e..1466e94be3 100644 --- a/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt +++ b/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/vendors/SQLiteDialect.kt @@ -252,6 +252,18 @@ internal object SQLiteFunctionProvider : FunctionProvider() { val sql = super.explain(false, null, internalStatement, transaction) return sql.replaceFirst("EXPLAIN ", "EXPLAIN QUERY PLAN ") } + + override fun returning( + mainSql: String, + returning: List>, + transaction: Transaction + ): String { + return with(QueryBuilder(true)) { + +"$mainSql RETURNING " + returning.appendTo { +it } + toString() + } + } } /** diff --git a/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ReturningTests.kt b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ReturningTests.kt new file mode 100644 index 0000000000..b72e7b5c86 --- /dev/null +++ b/exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ReturningTests.kt @@ -0,0 +1,99 @@ +package org.jetbrains.exposed.sql.tests.shared.dml + +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.* +import org.jetbrains.exposed.sql.SqlExpressionBuilder.times +import org.jetbrains.exposed.sql.statements.ReturningStatement +import org.jetbrains.exposed.sql.tests.DatabaseTestsBase +import org.jetbrains.exposed.sql.tests.TestDB +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs + +class ReturningTests : DatabaseTestsBase() { + private val returningSupportedDb = TestDB.postgreSQLRelatedDB.toSet() + TestDB.SQLITE + + object Items : IntIdTable("items") { + val name = varchar("name", 32) + val price = double("price") + } + + @Test + fun testInsertReturning() { + withTables(TestDB.enabledDialects() - returningSupportedDb, Items) { + // return all columns by default + val result1 = Items.insertReturning { + it[name] = "A" + it[price] = 99.0 + }.single() + assertEquals(1, result1[Items.id].value) + assertEquals("A", result1[Items.name]) + assertEquals(99.0, result1[Items.price]) + + val result2 = Items.insertReturning(listOf(Items.id, Items.name)) { + it[name] = "B" + it[price] = 200.0 + }.single() + assertEquals(2, result2[Items.id].value) + assertEquals("B", result2[Items.name]) + + assertFailsWith { // Items.price not in record set + result2[Items.price] + } + + assertEquals(2, Items.selectAll().count()) + } + } + + @Test + fun testUpsertReturning() { + withTables(TestDB.enabledDialects() - returningSupportedDb, Items) { + // return all columns by default + val result1 = Items.upsertReturning { + it[name] = "A" + it[price] = 99.0 + }.single() + assertEquals(1, result1[Items.id].value) + assertEquals("A", result1[Items.name]) + assertEquals(99.0, result1[Items.price]) + + val result2 = Items.upsertReturning( + returning = listOf(Items.name, Items.price), + onUpdate = listOf(Items.price to Items.price.times(10.0)) + ) { + it[id] = 1 + it[name] = "B" + it[price] = 200.0 + }.single() + assertEquals("A", result2[Items.name]) + assertEquals(990.0, result2[Items.price]) + + val result3 = Items.upsertReturning( + returning = listOf(Items.name), + onUpdateExclude = listOf(Items.price), + where = { Items.price greater 500.0 } + ) { + it[id] = 1 + it[name] = "B" + it[price] = 200.0 + }.single() + assertEquals("B", result3[Items.name]) + + assertEquals(1, Items.selectAll().count()) + } + } + + @Test + fun testReturningWithNoResults() { + withTables(TestDB.enabledDialects() - returningSupportedDb, Items) { + // statement not executed if not iterated over + val stmt = Items.insertReturning { + it[name] = "A" + it[price] = 99.0 + } + assertIs(stmt) + assertEquals(0, Items.selectAll().count()) + } + } +}