diff --git a/CHANGELOG.md b/CHANGELOG.md index 325669b..c340c7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +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 `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 diff --git a/README.md b/README.md index 781532f..090a21f 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 f2adc20..6612439 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-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 e4586cb..4b08ce5 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 f47dcf5..2b10277 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 2fe9073..97564b3 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 50053aa..aca4fac 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 92e1121..978caf6 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/doc/getting-start-cn.md b/sqllin-dsl/doc/getting-start-cn.md index 0cbc7d6..1e59d27 100644 --- a/sqllin-dsl/doc/getting-start-cn.md +++ b/sqllin-dsl/doc/getting-start-cn.md @@ -143,7 +143,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 bc561d4..c847544 100644 --- a/sqllin-dsl/doc/getting-start.md +++ b/sqllin-dsl/doc/getting-start.md @@ -153,7 +153,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( @@ -399,7 +399,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 0aa20db..cfb9c90 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 90e6c36..f99982e 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, ) diff --git a/sqllin-dsl/doc/query-cn.md b/sqllin-dsl/doc/query-cn.md index 2eec4a1..ab890ef 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 1c71bb4..edd5037 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) } } 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 227f6e8..ffb0df9 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 ..< 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 1a7eb2b..2ce9912 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.