From 27fdd02d42707d991abf89cdc24b3b07a49da0fe Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Thu, 15 Oct 2026 16:15:18 +0800 Subject: [PATCH 1/2] Fix INSERT when the generated primary key is the last property --- .../com/ctrip/sqllin/dsl/test/AndroidTest.kt | 3 ++ .../ctrip/sqllin/dsl/test/CommonBasicTest.kt | 9 ++++++ .../com/ctrip/sqllin/dsl/test/Entities.kt | 7 +++++ .../com/ctrip/sqllin/dsl/test/JvmTest.kt | 3 ++ .../com/ctrip/sqllin/dsl/test/NativeTest.kt | 3 ++ .../dsl/sql/compiler/EncodeEntities2SQL.kt | 15 +++------- .../dsl/sql/compiler/InsertValuesEncoder.kt | 28 ++++--------------- 7 files changed, 35 insertions(+), 33 deletions(-) diff --git a/sqllin-dsl-test/src/androidHostTest/kotlin/com/ctrip/sqllin/dsl/test/AndroidTest.kt b/sqllin-dsl-test/src/androidHostTest/kotlin/com/ctrip/sqllin/dsl/test/AndroidTest.kt index d5e14c47..64326e1b 100644 --- a/sqllin-dsl-test/src/androidHostTest/kotlin/com/ctrip/sqllin/dsl/test/AndroidTest.kt +++ b/sqllin-dsl-test/src/androidHostTest/kotlin/com/ctrip/sqllin/dsl/test/AndroidTest.kt @@ -27,6 +27,9 @@ class AndroidTest { @Test fun testInsert() = commonTest.testInsert() + @Test + fun testInsertGeneratedKeyLast() = commonTest.testInsertGeneratedKeyLast() + @Test fun testDelete() = commonTest.testDelete() diff --git a/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/CommonBasicTest.kt b/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/CommonBasicTest.kt index 8483ed38..618a0284 100644 --- a/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/CommonBasicTest.kt +++ b/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/CommonBasicTest.kt @@ -83,6 +83,15 @@ class CommonBasicTest(private val path: DatabasePath) { assertEquals(book, statement?.getResults()?.firstOrNull()) } + fun testInsertGeneratedKeyLast() = Database(getDefaultDBConfig()).databaseAutoClose { database -> + val result = database { + CREATE(GeneratedKeyLastTable) + GeneratedKeyLastTable INSERT listOf(GeneratedKeyLast("first"), GeneratedKeyLast("second")) + GeneratedKeyLastTable SELECT ORDER_BY(GeneratedKeyLastTable.id) + } + assertEquals(listOf(GeneratedKeyLast("first", 1L), GeneratedKeyLast("second", 2L)), result.getResults()) + } + fun testDelete() = Database(getDefaultDBConfig(), true).databaseAutoClose { database -> val book1 = Book(name = "The Da Vinci Code", author = "Dan Brown", pages = 454, price = 16.96) val book2 = Book(name = "The Lost Symbol", author = "Dan Brown", pages = 510, price = 19.95) diff --git a/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/Entities.kt b/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/Entities.kt index 2996f930..f8f276cd 100644 --- a/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/Entities.kt +++ b/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/Entities.kt @@ -125,6 +125,13 @@ data class PersonWithId( val age: Age, ) +@DBRow("generated_key_last") +@Serializable +data class GeneratedKeyLast( + val name: String, + @PrimaryKey val id: Long? = null, +) + @DBRow("product") @Serializable data class Product( diff --git a/sqllin-dsl-test/src/jvmTest/kotlin/com/ctrip/sqllin/dsl/test/JvmTest.kt b/sqllin-dsl-test/src/jvmTest/kotlin/com/ctrip/sqllin/dsl/test/JvmTest.kt index 3d277003..ed31bae1 100644 --- a/sqllin-dsl-test/src/jvmTest/kotlin/com/ctrip/sqllin/dsl/test/JvmTest.kt +++ b/sqllin-dsl-test/src/jvmTest/kotlin/com/ctrip/sqllin/dsl/test/JvmTest.kt @@ -19,6 +19,9 @@ class JvmTest { @Test fun testInsert() = commonTest.testInsert() + @Test + fun testInsertGeneratedKeyLast() = commonTest.testInsertGeneratedKeyLast() + @Test fun testDelete() = commonTest.testDelete() diff --git a/sqllin-dsl-test/src/nativeTest/kotlin/com/ctrip/sqllin/dsl/test/NativeTest.kt b/sqllin-dsl-test/src/nativeTest/kotlin/com/ctrip/sqllin/dsl/test/NativeTest.kt index ef0ee1d6..2b87a90c 100644 --- a/sqllin-dsl-test/src/nativeTest/kotlin/com/ctrip/sqllin/dsl/test/NativeTest.kt +++ b/sqllin-dsl-test/src/nativeTest/kotlin/com/ctrip/sqllin/dsl/test/NativeTest.kt @@ -35,6 +35,9 @@ class NativeTest { @Test fun testInsert() = commonTest.testInsert() + @Test + fun testInsertGeneratedKeyLast() = commonTest.testInsertGeneratedKeyLast() + @Test fun testDelete() = commonTest.testDelete() diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/EncodeEntities2SQL.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/EncodeEntities2SQL.kt index 227f6e88..003849a5 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/EncodeEntities2SQL.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/EncodeEntities2SQL.kt @@ -95,17 +95,10 @@ internal fun StringBuilder.appendDBColumnName( if (isInsertId) { appendDBColumnName(descriptor) } else { - val lastIndex = descriptor.elementsCount - 1 - for (i in 0 ..< lastIndex) { - val elementName = descriptor.getElementName(i) - if (elementName != primaryKeyName) { - append(elementName) - append(',') - } - } - val elementName = descriptor.getElementName(lastIndex) - if (elementName != primaryKeyName) - append(elementName) + val columnNames = (0 until descriptor.elementsCount) + .map(descriptor::getElementName) + .filter { it != primaryKeyName } + append(columnNames.joinToString(",")) } } diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/InsertValuesEncoder.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/InsertValuesEncoder.kt index 1a7eb2b6..2ce9912e 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/InsertValuesEncoder.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/InsertValuesEncoder.kt @@ -50,49 +50,33 @@ internal class InsertValuesEncoder( override val serializersModule: SerializersModule = EmptySerializersModule() - private var elementsIndex = 0 - private var elementsCount = 0 + private var encodedElements = 0 /** * StringBuilder accumulating the SQL VALUES clause. */ private val sqlStrBuilder = StringBuilder("(") - /** - * Appends comma between values or closing parenthesis after the last value. - * - * Format: `?, ?, ?)` - */ - private fun appendTail() { - val symbol = if (elementsIndex < elementsCount - 1) - ',' - else - ')' - sqlStrBuilder.append(symbol) - } - /** * Appends a parameter placeholder and records the value. * * @param value The parameter value (any type including null) */ private fun appendAny(value: Any?) { + if (encodedElements++ > 0) + sqlStrBuilder.append(',') sqlStrBuilder.append('?') parameters.add(value) - appendTail() } /** * The complete SQL VALUES clause generated by this encoder. */ val valuesSQL - get() = sqlStrBuilder.toString() + get() = "$sqlStrBuilder)" - override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean { - elementsCount = descriptor.elementsCount - elementsIndex = index - return isInsertId || descriptor.getElementName(index) != primaryKeyName - } + override fun encodeElement(descriptor: SerialDescriptor, index: Int): Boolean = + isInsertId || descriptor.getElementName(index) != primaryKeyName /** * Encodes any non-null value as a parameter placeholder. From 161b49a3ff06ab8f4e1c2a6297d90fdbde8c1bb1 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 16 Oct 2026 03:10:12 +0800 Subject: [PATCH 2/2] Address review feedback for generated-key INSERT --- CHANGELOG.md | 1 + .../com/ctrip/sqllin/dsl/sql/compiler/EncodeEntities2SQL.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 325669b3..a866c8e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * Fix: the default `lookasideSlotSize` and `lookasideSlotCount` of `DSLDBConfiguration` disabled SQLite's lookaside memory allocator on Kotlin/Native and on Android 9 and higher, as those of `DatabaseConfiguration` did; they are now -1, see sqllin-driver * Fix: on the JVM and Kotlin/Native, a blocking `database { }` scope on another thread could take the TEMP table and triggers that count the changes of observed queries with it, as its transaction can be open while they are created, the connection being locked for each statement rather than for a transaction: its rollback removed them, so the next write failed with "no such table", and once another table was observed, the changes of the table they belonged to were no longer counted. The tracker now creates them again where they are gone, and counts a change of their tables. A failure to tell the observed queries of a change no longer takes the place of the failure of the statement that made it. The guide to observed queries and the documentation of `Database#invoke` and `Database#observe` now say to write through `suspendedScope` while queries are observed, as they run on another thread, and that a blocking scope isn't serialized with them * Fix documentation: the example of the guide to concurrency safety called `database suspendScope { }`, which doesn't exist, in a function that wasn't a `suspend` one; it is `database suspendedScope { }`, in a `suspend` function +* Fix: INSERT failed with a syntax error when the database-assigned nullable Long primary key was the last property of the class ### sqllin-driver diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/EncodeEntities2SQL.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/EncodeEntities2SQL.kt index 003849a5..ffb0df9b 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/EncodeEntities2SQL.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/compiler/EncodeEntities2SQL.kt @@ -95,7 +95,7 @@ internal fun StringBuilder.appendDBColumnName( if (isInsertId) { appendDBColumnName(descriptor) } else { - val columnNames = (0 until descriptor.elementsCount) + val columnNames = (0 ..< descriptor.elementsCount) .map(descriptor::getElementName) .filter { it != primaryKeyName } append(columnNames.joinToString(","))