From 27fdd02d42707d991abf89cdc24b3b07a49da0fe Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Thu, 15 Oct 2026 16:15:18 +0800 Subject: [PATCH 1/5] 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 b9bc48d825af254533f598288cf3a152f7ec65fa Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Thu, 15 Oct 2026 16:15:45 +0800 Subject: [PATCH 2/5] Correct API names and generated-key types in guide examples --- README.md | 2 +- README_CN.md | 2 +- sqllin-dsl/doc/getting-start-cn.md | 2 +- sqllin-dsl/doc/getting-start.md | 4 ++-- sqllin-dsl/doc/modify-database-and-transaction-cn.md | 2 +- sqllin-dsl/doc/modify-database-and-transaction.md | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 781532fb..090a21f4 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ fun sample() { table SELECT WHERE (age LTE 5) GROUP_BY age HAVING (upper(name) EQ "TOM") ORDER_BY (age to DESC) LIMIT 2 OFFSET 1 } } - selectStatement.getResult().forEach { person -> + selectStatement.getResults().forEach { person -> println(person.name) } } diff --git a/README_CN.md b/README_CN.md index f2adc20e..6612439c 100644 --- a/README_CN.md +++ b/README_CN.md @@ -18,7 +18,7 @@ fun sample() { table SELECT WHERE (age LTE 5) GROUP_BY age HAVING (upper(name) EQ "TOM") ORDER_BY (age to DESC) LIMIT 2 OFFSET 1 } } - selectStatement.getResult().forEach { person -> + selectStatement.getResults().forEach { person -> println(person.name) } } diff --git a/sqllin-dsl/doc/getting-start-cn.md b/sqllin-dsl/doc/getting-start-cn.md index 22c3b643..97079415 100644 --- a/sqllin-dsl/doc/getting-start-cn.md +++ b/sqllin-dsl/doc/getting-start-cn.md @@ -141,7 +141,7 @@ val database = Database( 除此之外,你还可以使用 `DSLDBConfiguration`,它允许你在 `create` 和 `upgrade` 回调中使用类型安全的 SQL DSL,而不是原始的 SQL 字符串: ```kotlin -import com.ctrip.sqllin.driver.DSLDBConfiguration +import com.ctrip.sqllin.dsl.DSLDBConfiguration import com.ctrip.sqllin.dsl.Database val database = Database( diff --git a/sqllin-dsl/doc/getting-start.md b/sqllin-dsl/doc/getting-start.md index a81105bf..84e38965 100644 --- a/sqllin-dsl/doc/getting-start.md +++ b/sqllin-dsl/doc/getting-start.md @@ -150,7 +150,7 @@ only work on Android 9 and higher. And, because [sqlite-jdbc](https://github.com Alternatively, you can use `DSLDBConfiguration` which allows you to use the type-safe SQL DSL in the `create` and `upgrade` callbacks instead of raw SQL strings: ```kotlin -import com.ctrip.sqllin.driver.DSLDBConfiguration +import com.ctrip.sqllin.dsl.DSLDBConfiguration import com.ctrip.sqllin.dsl.Database val database = Database( @@ -396,7 +396,7 @@ Use `@CollateNoCase` to make string comparisons case-insensitive: ```kotlin import com.ctrip.sqllin.dsl.annotation.DBRow import com.ctrip.sqllin.dsl.annotation.PrimaryKey -import com.ctrip.sqllin.dsl.annotation.NoCase +import com.ctrip.sqllin.dsl.annotation.CollateNoCase import com.ctrip.sqllin.dsl.annotation.Unique import kotlinx.serialization.Serializable diff --git a/sqllin-dsl/doc/modify-database-and-transaction-cn.md b/sqllin-dsl/doc/modify-database-and-transaction-cn.md index c94807cd..f017d336 100644 --- a/sqllin-dsl/doc/modify-database-and-transaction-cn.md +++ b/sqllin-dsl/doc/modify-database-and-transaction-cn.md @@ -19,7 +19,7 @@ import kotlinx.serialization.Serializable @Serializable data class Person( @PrimaryKey(autoIncrement = true) - val id: Long = 0, + val id: Long? = null, val name: String, val age: Int, ) diff --git a/sqllin-dsl/doc/modify-database-and-transaction.md b/sqllin-dsl/doc/modify-database-and-transaction.md index 318ec34b..7277772d 100644 --- a/sqllin-dsl/doc/modify-database-and-transaction.md +++ b/sqllin-dsl/doc/modify-database-and-transaction.md @@ -22,7 +22,7 @@ import kotlinx.serialization.Serializable @Serializable data class Person( @PrimaryKey(autoIncrement = true) - val id: Long = 0, + val id: Long? = null, val name: String, val age: Int, ) From 161b49a3ff06ab8f4e1c2a6297d90fdbde8c1bb1 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 16 Oct 2026 03:10:12 +0800 Subject: [PATCH 3/5] 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(",")) From 41979f3b8cfb499a06fbba8ee84a46194db5f544 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Fri, 16 Oct 2026 03:10:49 +0800 Subject: [PATCH 4/5] Correct query guide result method examples --- CHANGELOG.md | 1 + sqllin-dsl/doc/query-cn.md | 4 ++-- sqllin-dsl/doc/query.md | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 325669b3..65792a53 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 documentation: the query guide called `SelectStatement.getResult()`, which doesn't exist; the method is `getResults()` ### sqllin-driver diff --git a/sqllin-dsl/doc/query-cn.md b/sqllin-dsl/doc/query-cn.md index 2eec4a11..ab890ef5 100644 --- a/sqllin-dsl/doc/query-cn.md +++ b/sqllin-dsl/doc/query-cn.md @@ -14,7 +14,7 @@ fun sample() { selectStatement = table SELECT X } } - selectStatement.getResult().forEach { person -> + selectStatement.getResults().forEach { person -> println(person) } } @@ -76,7 +76,7 @@ fun sample() { selectStatement = table SELECT WHERE (age LTE 5) GROUP_BY age HAVING (upper(name) EQ "TOM") ORDER_BY (age to DESC) LIMIT 2 OFFSET 1 } } - selectStatement.getResult().forEach { person -> + selectStatement.getResults().forEach { person -> println(person) } } diff --git a/sqllin-dsl/doc/query.md b/sqllin-dsl/doc/query.md index 1c71bb44..edd50372 100644 --- a/sqllin-dsl/doc/query.md +++ b/sqllin-dsl/doc/query.md @@ -16,7 +16,7 @@ fun sample() { selectStatement = table SELECT X } } - selectStatement.getResult().forEach { person -> + selectStatement.getResults().forEach { person -> println(person) } } @@ -78,7 +78,7 @@ fun sample() { selectStatement = table SELECT WHERE (age LTE 5) GROUP_BY age HAVING (upper(name) EQ "TOM") ORDER_BY (age to DESC) LIMIT 2 OFFSET 1 } } - selectStatement.getResult().forEach { person -> + selectStatement.getResults().forEach { person -> println(person) } } From 99295200d89ab8a3a926cd993bb637aa6c07e108 Mon Sep 17 00:00:00 2001 From: qiaoyuang Date: Sat, 17 Oct 2026 12:04:13 +0100 Subject: [PATCH 5/5] Expand the changelog entries of B7 and B8 The entries that came with the two pull requests were short. B7's now says why INSERT failed, a trailing comma in the column list and a VALUES tuple never closed, as the key that the database assigns is left out, and which forms weren't affected: a key declared before another property, and INSERT_WITH_ID, INSERT_OR_IGNORE and INSERT_OR_REPLACE, which write the key. B8's only named the query guide's getResult(); it now lists every correction: getResult() in the READMEs too, the import of DSLDBConfiguration, the CollateNoCase annotation, and the type of an autoIncrement key. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f767a980..c340c7e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,8 @@ * 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 -* Fix documentation: the query guide called `SelectStatement.getResult()`, which doesn't exist; the method is `getResults()` +* Fix: `INSERT` failed with `near ")": syntax error` when the primary key that the database assigns, a `Long?` property annotated with `@PrimaryKey`, was the last property of the class: as `INSERT` leaves such a key out, the column list kept a trailing comma, and the `VALUES` tuple was never closed. A key declared before another property wasn't affected, and neither were `INSERT_WITH_ID`, `INSERT_OR_IGNORE` and `INSERT_OR_REPLACE`, which write the key +* Fix documentation: examples that didn't compile. The READMEs and the query guide called `SelectStatement.getResult()`, which doesn't exist, rather than `getResults()`; the guide to getting started imported `DSLDBConfiguration` from `com.ctrip.sqllin.driver` rather than `com.ctrip.sqllin.dsl`, and, in English, the annotation `NoCase`, which doesn't exist, rather than `CollateNoCase`; and the guide to modifying the database declared an `autoIncrement` primary key as `Long = 0`, which the processor rejects, rather than `Long? = null` ### sqllin-driver