From 3d293ba10bfb9cf6232e543c2b3533ba4c7c1b8b Mon Sep 17 00:00:00 2001 From: Oleg Babichev Date: Wed, 16 Jul 2025 14:29:56 +0200 Subject: [PATCH 1/2] fix: Fix the duplicated segments on SQLServer dialect --- .../statements/InsertSuspendExecutable.kt | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/statements/InsertSuspendExecutable.kt b/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/statements/InsertSuspendExecutable.kt index 77a004ca09..fad5fb16e6 100644 --- a/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/statements/InsertSuspendExecutable.kt +++ b/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/statements/InsertSuspendExecutable.kt @@ -144,6 +144,8 @@ open class InsertSuspendExecutable>( val dialect = currentDialect val sendsResultsOnFailure = dialect is MysqlDialect && dialect !is MariaDBDialect + var isSqlServerBatchInsert = false + mapSegments { segment -> var values: MutableMap, Any?>? = null var count: Int? = null @@ -154,7 +156,9 @@ open class InsertSuspendExecutable>( count = segment.value().toInt() } - if (segment is Result.RowSegment) { + if (segment is Result.RowSegment && !isSQLServerLastRowId(segment, isSqlServerBatchInsert)) { + isSqlServerBatchInsert = isSqlServerBatchInsert || isSQLServerBatchSegment(segment) + val row = R2DBCRow(segment.row(), typeMapping) if (columnIndexesInResultSet == null) { @@ -218,17 +222,6 @@ open class InsertSuspendExecutable>( values.takeIf { !sendsResultsOnFailure }?.let { resultSetsValues.add(it) } } - // SQL Server returns a duplicate terminal row result from batch, even though it only attempts the correct executions; - // This happens with mapSegments(), mapRows(), and exec(), but not with plain SQL execution; - // So there must be some reason on our end that such a result is included - // Todo investigate why SQL Server delivers a duplicate final record - if (currentDialect is SQLServerDialect && resultSetsValues.size > 1 && resultSetsCounts.sum() >= 1) { - // equality check is tricky because of potential type mismatch - // e.g. An Int id returns a duplicated final row of type BigDecimal - is this maybe the mssql last_inserted_id? - val lastIndex = resultSetsValues.lastIndex - resultSetsValues.removeAt(lastIndex) - } - // Some databases, like H2 and MariaDB, aren't returning UpdateCount segments; // The workaround below therefore fails for upsert operations // Todo review alternatives for these dialects @@ -275,6 +268,28 @@ open class InsertSuspendExecutable>( } } + /* This check is needed for SQLServer batch insert. The problem is that R2DBC driver for SQLServer database + returns extra `Result.RowSegment` with the id of the last row. Every insert of batch is returned as + a segment with `GENERATED_KEYS` column name in metadata, but after them the one extra segment with `id` name + is also returned. + + We can't just filter segments with name `id`, because that name is also returned in general insert for column + with name `id`. + + This check is quite optimistic, and we recognize the whole insert as batch insert if there is at least one + `GENERATED_KEYS` segment in the whole sequence. */ + private fun isSQLServerLastRowId(segment: Result.RowSegment, isSqlServerBatchInsert: Boolean): Boolean { + return currentDialect is SQLServerDialect && isSqlServerBatchInsert && segment.row().metadata.columnMetadatas.let { + it.size == 1 && it[0].name != "GENERATED_KEYS" + } + } + + private fun isSQLServerBatchSegment(segment: Result.RowSegment): Boolean { + return currentDialect is SQLServerDialect && segment.row().metadata.columnMetadatas.let { + it.size == 1 && it[0].name == "GENERATED_KEYS" + } + } + /** * Returns all the columns for which value can not be derived without actual request. * From ea3b55a539dacff6053ac091c7fab2d675c55afc Mon Sep 17 00:00:00 2001 From: Oleg Babichev Date: Fri, 18 Jul 2025 09:10:39 +0200 Subject: [PATCH 2/2] fix: Review issues --- .../statements/InsertSuspendExecutable.kt | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/statements/InsertSuspendExecutable.kt b/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/statements/InsertSuspendExecutable.kt index fad5fb16e6..b5ec6ed46a 100644 --- a/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/statements/InsertSuspendExecutable.kt +++ b/exposed-r2dbc/src/main/kotlin/org/jetbrains/exposed/v1/r2dbc/statements/InsertSuspendExecutable.kt @@ -8,12 +8,7 @@ import org.jetbrains.exposed.v1.core.* import org.jetbrains.exposed.v1.core.statements.BatchReplaceStatement import org.jetbrains.exposed.v1.core.statements.InsertStatement import org.jetbrains.exposed.v1.core.statements.ReplaceStatement -import org.jetbrains.exposed.v1.core.vendors.MariaDBDialect -import org.jetbrains.exposed.v1.core.vendors.MysqlDialect -import org.jetbrains.exposed.v1.core.vendors.PostgreSQLDialect -import org.jetbrains.exposed.v1.core.vendors.SQLServerDialect -import org.jetbrains.exposed.v1.core.vendors.currentDialect -import org.jetbrains.exposed.v1.core.vendors.inProperCase +import org.jetbrains.exposed.v1.core.vendors.* import org.jetbrains.exposed.v1.r2dbc.R2dbcTransaction import org.jetbrains.exposed.v1.r2dbc.statements.api.R2DBCRow import org.jetbrains.exposed.v1.r2dbc.statements.api.R2dbcPreparedStatementApi @@ -156,8 +151,8 @@ open class InsertSuspendExecutable>( count = segment.value().toInt() } - if (segment is Result.RowSegment && !isSQLServerLastRowId(segment, isSqlServerBatchInsert)) { - isSqlServerBatchInsert = isSqlServerBatchInsert || isSQLServerBatchSegment(segment) + if (segment is Result.RowSegment && !isSQLServerLastRowId(segment, isSqlServerBatchInsert, dialect)) { + isSqlServerBatchInsert = isSqlServerBatchInsert || isSQLServerBatchSegment(segment, dialect) val row = R2DBCRow(segment.row(), typeMapping) @@ -278,14 +273,14 @@ open class InsertSuspendExecutable>( This check is quite optimistic, and we recognize the whole insert as batch insert if there is at least one `GENERATED_KEYS` segment in the whole sequence. */ - private fun isSQLServerLastRowId(segment: Result.RowSegment, isSqlServerBatchInsert: Boolean): Boolean { - return currentDialect is SQLServerDialect && isSqlServerBatchInsert && segment.row().metadata.columnMetadatas.let { + private fun isSQLServerLastRowId(segment: Result.RowSegment, isSqlServerBatchInsert: Boolean, dialect: DatabaseDialect): Boolean { + return dialect is SQLServerDialect && isSqlServerBatchInsert && segment.row().metadata.columnMetadatas.let { it.size == 1 && it[0].name != "GENERATED_KEYS" } } - private fun isSQLServerBatchSegment(segment: Result.RowSegment): Boolean { - return currentDialect is SQLServerDialect && segment.row().metadata.columnMetadatas.let { + private fun isSQLServerBatchSegment(segment: Result.RowSegment, dialect: DatabaseDialect): Boolean { + return dialect is SQLServerDialect && segment.row().metadata.columnMetadatas.let { it.size == 1 && it[0].name == "GENERATED_KEYS" } }