-
Notifications
You must be signed in to change notification settings - Fork 677
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: EXPOSED-355 Support INSERT...RETURNING statement (#2060)
* feat: EXPOSED-355 Support INSERT...RETURNING statement - Add support for RETURNING clause with insert and upsert statements. - Implement a ReturningStatement as the basis for update and deletes too. - Implementation covers PostgreSQL and SQLite only currently. * feat: EXPOSED-355 Support INSERT...RETURNING statement Switch ReturningStatement to be iterable instead of auto-executing and returning a List<ResultRow>. Add samples to KDocs.
- Loading branch information
Showing
7 changed files
with
272 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/ReturningStatement.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Expression<*>>, | ||
val mainStatement: Statement<*> | ||
) : Iterable<ResultRow>, Statement<ResultSet>(mainStatement.type, listOf(table)) { | ||
protected val transaction | ||
get() = TransactionManager.current() | ||
|
||
override fun PreparedStatementApi.executeInternal(transaction: Transaction): ResultSet = executeQuery() | ||
|
||
override fun arguments(): Iterable<Iterable<Pair<IColumnType<*>, 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<ResultRow> { | ||
val resultIterator = ResultIterator(transaction.exec(this)!!) | ||
return Iterable { resultIterator }.iterator() | ||
} | ||
|
||
private inner class ResultIterator(val rs: ResultSet) : Iterator<ResultRow> { | ||
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
99 changes: 99 additions & 0 deletions
99
exposed-tests/src/test/kotlin/org/jetbrains/exposed/sql/tests/shared/dml/ReturningTests.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<IllegalStateException> { // 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<ReturningStatement>(stmt) | ||
assertEquals(0, Items.selectAll().count()) | ||
} | ||
} | ||
} |