diff --git a/CHANGELOG.md b/CHANGELOG.md index 41888bc5..71b6e19b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,33 +11,10 @@ * Migrate the Android instrumented tests to `Robolectric`, they now run on the JVM as host unit tests against `API 26` and `API 37`, and no longer need an emulator * Move the `sqllin-driver` tests back into the `sqllin-driver` module's `commonTest`, and remove the `sqllin-driver-test` module -### sqllin-dsl - -* **Breaking change**: The parameter of annotation `@PrimaryKey` renamed from `isAutoincrement` to `autoIncrement`, aligning it with the name already used in the documentation and with the naming of the other annotations. Call sites using the named argument `@PrimaryKey(isAutoincrement = true)` must be updated to `@PrimaryKey(autoIncrement = true)`; positional usage such as `@PrimaryKey(true)` is unaffected -* **Breaking change**: The nullability of a `@PrimaryKey` property now decides who supplies its value. A `Long?` key is assigned by the database, as before. A non-null `Long` key is now allowed: it remains an `INTEGER PRIMARY KEY`, a rowid alias, but is supplied by the caller and written by every `INSERT`. A key of any other type must be non-null and is declared `NOT NULL`. Previously every `@PrimaryKey` was forced to be nullable, against the annotation's own documentation, and because SQLite does not let `PRIMARY KEY` imply `NOT NULL` on such a column, a `String` key could hold `NULL` in any number of rows. `autoIncrement = true` now requires a `Long?` key, and a `ULong?` key, which used to be stored as `NULL` because it was left out of `INSERT` without being a rowid alias, is now rejected. To migrate, drop the `?` from any non-`Long` `@PrimaryKey`; a single-column `@CompositePrimaryKey` that only existed to hold a caller-supplied `Long` key can become `@PrimaryKey val id: Long` -* **Breaking change**: `@CompositePrimaryKey` now requires at least two properties. A single-column primary key is declared with `@PrimaryKey`, which for a `Long` key maps to `INTEGER`, a rowid alias, where a single-column `@CompositePrimaryKey` mapped it to `BIGINT`. To migrate, replace a lone `@CompositePrimaryKey` with `@PrimaryKey` -* **Breaking change**: The DSL APIs `ALERT_ADD_COLUMN` and `ALERT_RENAME_TABLE_TO` renamed to `ALTER_ADD_COLUMN` and `ALTER_RENAME_TABLE_TO`, correcting a misspelling of the SQL keyword `ALTER`. The internal `Alert` operation object is renamed to `Alter` accordingly -* Fix: the ALTER operations emitted the invalid keyword `ALERT TABLE` instead of `ALTER TABLE`, so `ALTER_ADD_COLUMN`, `ALTER_RENAME_TABLE_TO`, `RENAME_COLUMN` and `DROP_COLUMN` all failed at runtime and had never worked. The tests that covered them swallowed the failure, which is why it went unnoticed -* Fix documentation: the KDoc of `DatabaseScope` now states that statement execution is deferred until the scope exits, and its example no longer reads a `SelectStatement`'s results while the scope is still open, which throws -* Fix documentation: the `CREATE_INDEX` and `CREATE_UNIQUE_INDEX` examples referenced a `KClass.table` extension that does not exist in the library, and referred to columns by property reference instead of through the generated table object -* Fix: the SQL string functions added in 2.2.0, `substr`, `trim`, `ltrim`, `rtrim`, `replace`, `instr` and `printf`, now carry the same DSL marker as the other SQL functions, so IntelliJ IDEA highlights their calls the same way -* Fix documentation: the installation guide now declares the task dependencies the generated code needs, as SQLlin's own builds always did. Without them Gradle fails the build, in particular when another KSP processor runs in the same module. It also states that each generated object is named after its class with a `Table` suffix, not after the table - ### sqllin-driver * Update `sqlite-jdbc`'s version to `3.53.4.0` -### sqllin-processor - -* Fix: the visibility of the class annotated with `@DBRow` is now propagated to the generated table object. An `internal` `@DBRow` class used to produce a `public` object, which failed to compile with `EXPOSED_SUPER_CLASS`, `EXPOSED_FUNCTION_RETURN_TYPE` and `EXPOSED_RECEIVER_TYPE`. A `@DBRow` class that is neither `public` nor `internal` is now reported as an error -* Fix: every `SetClause` property generated for a column declared after a nullable `Long` `@PrimaryKey` was typed nullable regardless of the column's own declaration, so `UPDATE ... SET { column = null }` compiled against `NOT NULL` columns and failed only at runtime. Each property now takes the nullability its own column declares -* Fix: the columns of a `@CompositePrimaryKey` are now declared `NOT NULL`. SQLite, unlike standard SQL, does not let a table-level `PRIMARY KEY` imply it on a rowid table, so such a key used to accept `NULL`, and any number of rows sharing the same key once a `NULL` was part of it. SQLlin itself could not write those `NULL`s, but anything else writing to the database could, and SQLlin then read them back as `0` or an empty string. This only changes the schema of tables created from now on; an existing table keeps its schema, as SQLite cannot add `NOT NULL` to an existing column -* **Breaking change**: an `ON_DELETE_SET_DEFAULT` or `ON_UPDATE_SET_DEFAULT` foreign key now requires the column to declare `@Default`, as the documentation always said. On `@References` the check was inverted: it accepted a non-null column without a default, whose parent row then could not be deleted (`NOT NULL constraint failed`), and rejected a nullable one. On `@ForeignKey` groups there was no check at all, so a nullable column without a default compiled and was set to `NULL`; that is now rejected too. To migrate, add `@Default` to the column, or use `ON_DELETE_SET_NULL` / `ON_UPDATE_SET_NULL` where setting it to `NULL` is the intent. Both checks hold whichever order `@Default` and the foreign key annotation are written in -* Fix: a computed property of a `@DBRow` class, one without a backing field such as `val title: String get() = ...`, no longer becomes a column. kotlinx.serialization doesn't serialize such a property, but it was given a `NOT NULL` column that `INSERT` never wrote, so every insert failed with `NOT NULL constraint failed`, and an accessor that looked its column up past the end of the serializer's descriptor -* Fix: a `@DBRow` property of a type no column can hold, such as a `List`, is now a compile-time error naming the property. It used to be skipped silently: left out of `CREATE TABLE` while its serializer still wrote and read it, so `INSERT` and `SELECT` failed at runtime with "no column named", and as the last property it left a trailing comma that made `CREATE TABLE` itself fail. Annotate such a property with `kotlinx.serialization.Transient` to keep it out of the table -* Fix: the generated table objects no longer produce a `DSL_MARKER_APPLIED_TO_WRONG_TARGET` warning for every column, which Kotlin 2.3.20 and later report in the module that compiles them. Their `@ColumnNameDslMaker` is there for IntelliJ IDEA's DSL highlighting rather than for the compiler's DSL scope control, so the warning is now suppressed on each generated object -* Fix: the generated `SetClause` setter of a non-null enum column no longer uses a safe call, `value?.ordinal`, which the module compiling the generated code reported as unnecessary. Only a nullable enum column keeps it - ## 2.3.0 / 2026-08-20 ### All 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 08252927..f25d179c 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 @@ -87,9 +87,6 @@ class AndroidTest { @Test fun testSchemaModification() = commonTest.testSchemaModification() - @Test - fun testPrimaryKeyNullability() = commonTest.testPrimaryKeyNullability() - @Test fun testStringOperators() = commonTest.testStringOperators() 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 fe35213f..d4743474 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 @@ -20,7 +20,6 @@ import com.ctrip.sqllin.driver.DatabaseConfiguration import com.ctrip.sqllin.driver.DatabasePath import com.ctrip.sqllin.dsl.DSLDBConfiguration import com.ctrip.sqllin.dsl.Database -import com.ctrip.sqllin.dsl.DatabaseScope import com.ctrip.sqllin.dsl.annotation.AdvancedInsertAPI import com.ctrip.sqllin.dsl.annotation.ExperimentalDSLDatabaseAPI import com.ctrip.sqllin.dsl.sql.X @@ -551,8 +550,8 @@ class CommonBasicTest(private val path: DatabasePath) { assertEquals(30, personResults[1].age) // Test 2: String primary key - val product1 = Product(sku = "SKU-WIDGET", name = "Widget", price = 19.99) - val product2 = Product(sku = "SKU-GADGET", name = "Gadget", price = 29.99) + val product1 = Product(sku = null, name = "Widget", price = 19.99) + val product2 = Product(sku = null, name = "Gadget", price = 29.99) lateinit var productStatement: SelectStatement database { @@ -564,10 +563,8 @@ class CommonBasicTest(private val path: DatabasePath) { val productResults = productStatement.getResults() assertEquals(2, productResults.size) - assertEquals("SKU-WIDGET", productResults[0].sku) assertEquals("Widget", productResults[0].name) assertEquals(19.99, productResults[0].price) - assertEquals("SKU-GADGET", productResults[1].sku) assertEquals("Gadget", productResults[1].name) assertEquals(29.99, productResults[1].price) @@ -691,7 +688,7 @@ class CommonBasicTest(private val path: DatabasePath) { fun testCreateInDatabaseScope() { Database(getNewAPIDBConfig()).databaseAutoClose { database -> val person = PersonWithId(id = null, name = "Grace", age = 40) - val product = Product(sku = "SKU-THING", name = "Thingamajig", price = 49.99) + val product = Product(sku = null, name = "Thingamajig", price = 49.99) lateinit var personStatement: SelectStatement lateinit var productStatement: SelectStatement @@ -1098,193 +1095,194 @@ class CommonBasicTest(private val path: DatabasePath) { } } - /** - * Exercises every ALTER operation as one realistic schema migration. - * - * 'alter_target' is created in the shape of [AlterBefore] (`id`, `name`, `legacy`) and migrated - * to the shape of [AlterAfter] (`id`, `fullName`, `nickname`) by ADD COLUMN, RENAME COLUMN and - * DROP COLUMN, then renamed to 'alter_renamed' and back. Every step is verified by reading the - * table through the entity matching the shape it should have at that point, so a step that does - * not actually run makes the test fail instead of passing quietly. - */ @OptIn(ExperimentalDSLDatabaseAPI::class) fun testSchemaModification() { Database(getNewAPIDBConfig()).databaseAutoClose { database -> + // Test 1: ALERT_ADD_COLUMN + // Note: ALERT operations have a typo in the DSL - should be "ALTER TABLE" not "ALERT TABLE" + // This test verifies the DSL compiles and the statement can be created + val person = PersonWithId(id = null, name = "Charlie", age = 35) + database { - CREATE(AlterBeforeTable) - AlterBeforeTable { table -> - table INSERT AlterBefore(id = null, name = "Charlie", legacy = 7) + PersonWithIdTable { table -> + table INSERT person } } - // Reading the migrated shape must fail first: 'nickname' does not exist yet. - assertEquals( - true, - database.selectFails { AlterAfterTable SELECT X }, - "'nickname' should not exist before ADD COLUMN", - ) + try { + database { + PersonWithIdTable ALERT_ADD_COLUMN PersonWithIdTable.name + } + } catch (e: Exception) { + // Expected to fail with current implementation due to "ALERT TABLE" typo + e.printStackTrace() + } - // ADD COLUMN. The receiver must be the table whose serializer declares the new column, - // because the column's SQL type is resolved from that descriptor. + lateinit var personStatement: SelectStatement database { - AlterAfterTable ALTER_ADD_COLUMN AlterAfterTable.nickname + personStatement = PersonWithIdTable SELECT X } + assertEquals(1, personStatement.getResults().size) + assertEquals("Charlie", personStatement.getResults().first().name) + + // Test 2: ALERT_RENAME_TABLE_TO with TableObject + val student1 = StudentWithAutoincrement(id = null, studentName = "Diana", grade = 90) + val student2 = StudentWithAutoincrement(id = null, studentName = "Ethan", grade = 85) - // RENAME COLUMN, naming the old column by string. Both entities map to 'alter_target'. database { - AlterAfterTable.RENAME_COLUMN("name", AlterAfterTable.fullName) + StudentWithAutoincrementTable { table -> + table INSERT listOf(student1, student2) + } } - // Both steps landed: the table now has 'fullName' and 'nickname', and still 'legacy'. - lateinit var withLegacy: SelectStatement + lateinit var studentStatement1: SelectStatement database { - withLegacy = AlterWithLegacyTable SELECT X + studentStatement1 = StudentWithAutoincrementTable SELECT X } - assertEquals(1, withLegacy.getResults().size) - assertEquals("Charlie", withLegacy.getResults().first().fullName) - assertEquals(null, withLegacy.getResults().first().nickname) - assertEquals(7, withLegacy.getResults().first().legacy) + assertEquals(2, studentStatement1.getResults().size) - lateinit var migrated: SelectStatement - database { - migrated = AlterAfterTable SELECT X + try { + database { + StudentWithAutoincrementTable ALERT_RENAME_TABLE_TO StudentWithAutoincrementTable + } + } catch (e: Exception) { + // Expected to fail with current implementation + e.printStackTrace() } - assertEquals(1, migrated.getResults().size) - assertEquals("Charlie", migrated.getResults().first().fullName) - assertEquals(null, migrated.getResults().first().nickname) - // RENAME TO, with a Table receiver, inside a transaction. + lateinit var studentStatement2: SelectStatement database { - transaction { - AlterAfterTable ALTER_RENAME_TABLE_TO AlterRenamedTable - } + studentStatement2 = StudentWithAutoincrementTable SELECT X } + assertEquals(2, studentStatement2.getResults().size) + + // Test 3: ALERT_RENAME_TABLE_TO with String + val enrollment = Enrollment(studentId = 1, courseId = 101, semester = "Spring 2025") - lateinit var renamed: SelectStatement database { - renamed = AlterRenamedTable SELECT X + EnrollmentTable { table -> + table INSERT enrollment + } } - assertEquals(1, renamed.getResults().size) - assertEquals("Charlie", renamed.getResults().first().fullName) - assertEquals( - true, - database.selectFails { AlterAfterTable SELECT X }, - "'alter_target' should not exist after RENAME TO", - ) + try { + database { + "enrollment" ALERT_RENAME_TABLE_TO EnrollmentTable + } + } catch (e: Exception) { + // Expected to fail with current implementation + e.printStackTrace() + } - // RENAME TO again, this time through the String receiver overload, renaming it back. + lateinit var enrollmentStatement: SelectStatement database { - "alter_renamed" ALTER_RENAME_TABLE_TO AlterAfterTable + enrollmentStatement = EnrollmentTable SELECT X } + assertEquals(1, enrollmentStatement.getResults().size) + assertEquals("Spring 2025", enrollmentStatement.getResults().first().semester) + + // Test 4: RENAME_COLUMN with ClauseElement + val book = Book(name = "Test Book", author = "Test Author", pages = 200, price = 15.99) - lateinit var renamedBack: SelectStatement database { - renamedBack = AlterAfterTable SELECT X + BookTable { table -> + table INSERT book + } } - assertEquals(1, renamedBack.getResults().size) - assertEquals("Charlie", renamedBack.getResults().first().fullName) - // DROP COLUMN last, because it needs SQLite 3.35+ (2021) and the Android framework only - // bundles that from API 34 on. Keeping it last means its failure on older SQLite cannot - // disturb the steps above. Where it does run, its effect is asserted. - var legacyDropped = true try { database { - AlterBeforeTable DROP_COLUMN AlterBeforeTable.legacy + BookTable.RENAME_COLUMN(BookTable.name, BookTable.author) } } catch (e: Exception) { - legacyDropped = false + // Expected to fail with current implementation + e.printStackTrace() } - if (legacyDropped) { - assertEquals( - true, - database.selectFails { AlterWithLegacyTable SELECT X }, - "'legacy' should be gone after DROP COLUMN", - ) + + lateinit var bookStatement: SelectStatement + database { + bookStatement = BookTable SELECT X } - } - } + assertEquals(1, bookStatement.getResults().size) - /** - * Runs [block] in its own database scope and reports whether the query failed. The results are - * read as well as executed, because the Android driver's `rawQuery` is lazy: a missing table or - * column surfaces only once the cursor is actually read, not when the statement runs. - */ - private fun Database.selectFails(block: DatabaseScope.() -> SelectStatement<*>): Boolean = - try { - var statement: SelectStatement<*>? = null - this.invoke { statement = block() } - statement!!.getResults() - false - } catch (e: Exception) { - true - } + // Test 5: RENAME_COLUMN with String + val category = Category(name = "Fiction", code = 100) - /** - * Compile-time check, never called: the generated `SetClause` properties must carry the - * nullability the entity declares. [PersonWithId] declares a `Long?` primary key *followed by* - * non-null columns, which is the order that used to leak the key's nullability into every later - * column. These assignments only compile while `name` and `age` are generated as non-null. - */ - @Suppress("unused", "UNUSED_VARIABLE") - private fun checkSetClauseNullability(clause: SetClause): Unit = with(PersonWithIdTable) { - val id: Long? = clause.id - val name: String = clause.name - val age: Age = clause.age - } + database { + CategoryTable { table -> + table INSERT category + } + } - /** - * Covers how a single `@PrimaryKey`'s nullability decides who supplies its value: a `Long?` key is - * assigned by the database; a non-null `Long` key is supplied by the caller yet stays a rowid alias; - * and a key of any other type is supplied by the caller and declared `NOT NULL`, which SQLite would - * otherwise not imply for it. - */ - @OptIn(ExperimentalDSLDatabaseAPI::class) - fun testPrimaryKeyNullability() { - // Both Long keys are rowid aliases; only the non-Long key needs NOT NULL spelled out. - assertEquals(true, PersonWithIdTable.createSQL.contains("id INTEGER PRIMARY KEY,")) - assertEquals(true, RemoteMovieTable.createSQL.contains("id INTEGER PRIMARY KEY,")) - assertEquals(true, ProductTable.createSQL.contains("sku TEXT PRIMARY KEY NOT NULL,")) + try { + database { + CategoryTable.RENAME_COLUMN("name", CategoryTable.code) + } + } catch (e: Exception) { + // Expected to fail with current implementation + e.printStackTrace() + } - Database(getNewAPIDBConfig()).databaseAutoClose { database -> + lateinit var categoryStatement: SelectStatement database { - CREATE(RemoteMovieTable) + categoryStatement = CategoryTable SELECT X } + assertEquals(1, categoryStatement.getResults().size) + assertEquals(100, categoryStatement.getResults().first().code) + + // Test 6: DROP_COLUMN + val dropPerson = PersonWithId(id = null, name = "Frank", age = 40) - // A caller-supplied Long key is written by a plain INSERT rather than left for the database. - lateinit var movies: SelectStatement database { - RemoteMovieTable { table -> - table INSERT listOf( - RemoteMovie(id = 603, title = "The Matrix"), - RemoteMovie(id = 27205, title = "Inception"), - ) - movies = table SELECT ORDER_BY(id to ASC) + PersonWithIdTable { table -> + table INSERT dropPerson } } - assertEquals(listOf(603L, 27205L), movies.getResults().map { it.id }) - // ...and it is a real primary key: inserting the same ID again is rejected. - var duplicateFailed = false try { database { - RemoteMovieTable INSERT RemoteMovie(id = 603, title = "The Matrix Reloaded") + PersonWithIdTable DROP_COLUMN PersonWithIdTable.age } } catch (e: Exception) { - duplicateFailed = true + // Expected to fail with current implementation or SQLite version + e.printStackTrace() } - assertEquals(true, duplicateFailed, "A duplicate caller-supplied key should be rejected") - // A Long? key is still assigned by the database. - lateinit var people: SelectStatement + lateinit var dropStatement: SelectStatement + database { + dropStatement = PersonWithIdTable SELECT WHERE (PersonWithIdTable.name EQ "Frank") + } + assertEquals(1, dropStatement.getResults().size) + + // Test 7: ALERT operations within a transaction + val txPerson1 = PersonWithId(id = null, name = "Grace", age = 28) + val txPerson2 = PersonWithId(id = null, name = "Henry", age = 32) + database { PersonWithIdTable { table -> - table INSERT PersonWithId(id = null, name = "Ivy", age = 21) - people = table SELECT X + table INSERT listOf(txPerson1, txPerson2) + } + } + + try { + database { + transaction { + PersonWithIdTable ALERT_ADD_COLUMN PersonWithIdTable.age + PersonWithIdTable.RENAME_COLUMN("name", PersonWithIdTable.name) + } } + } catch (e: Exception) { + // Expected to fail with current implementation + e.printStackTrace() } - assertNotEquals(null, people.getResults().first().id) + + lateinit var txStatement: SelectStatement + database { + txStatement = PersonWithIdTable SELECT WHERE (PersonWithIdTable.name EQ "Grace" OR (PersonWithIdTable.name EQ "Henry")) + } + assertEquals(2, txStatement.getResults().size) + assertEquals(true, txStatement.getResults().any { it.name == "Grace" }) + assertEquals(true, txStatement.getResults().any { it.name == "Henry" }) } } @@ -1689,16 +1687,15 @@ class CommonBasicTest(private val path: DatabasePath) { ProductTable.CREATE_UNIQUE_INDEX("idx_unique_product_name", ProductTable.name) } - val product1 = Product(sku = "SKU-WIDGET-1", name = "Widget", price = 19.99) + val product1 = Product(sku = null, name = "Widget", price = 19.99) database { ProductTable { table -> table INSERT product1 } } - // Try to insert duplicate - should fail. The SKU differs on purpose, so the only constraint the - // second product can violate is the unique index on 'name'. - val product2 = Product(sku = "SKU-WIDGET-2", name = "Widget", price = 29.99) + // Try to insert duplicate - should fail + val product2 = Product(sku = null, name = "Widget", price = 29.99) var duplicateFailed = false try { database { @@ -1787,18 +1784,10 @@ class CommonBasicTest(private val path: DatabasePath) { assertEquals(true, studentSQL.contains("CREATE TABLE student_with_autoincrement")) assertEquals(true, studentSQL.contains("id INTEGER PRIMARY KEY AUTOINCREMENT")) - // A computed property isn't serialized, so it must not become a column: Book declares `title` that way - assertEquals(false, BookTable.createSQL.contains("title")) - // Test 3: Table with composite primary key val enrollmentSQL = EnrollmentTable.createSQL assertEquals(true, enrollmentSQL.contains("CREATE TABLE enrollment")) assertEquals(true, enrollmentSQL.contains("PRIMARY KEY(studentId,courseId)")) - // SQLite doesn't let a table-level PRIMARY KEY imply NOT NULL, so each key column must declare it - assertEquals(true, enrollmentSQL.contains("studentId BIGINT NOT NULL,")) - assertEquals(true, enrollmentSQL.contains("courseId BIGINT NOT NULL,")) - assertEquals(true, FKProductTable.createSQL.contains("categoryId INT NOT NULL,")) - assertEquals(true, FKProductTable.createSQL.contains("productCode TEXT NOT NULL,")) // Test 4: Table with enum fields (stored as INT) val userSQL = UserAccountTable.createSQL 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 de7610c3..da6cd9a6 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 @@ -71,11 +71,7 @@ data class Book( val author: String, val price: Price, val pages: PageCount, -) { - // Computed, so kotlinx.serialization doesn't serialize it: it must not become a column, or every INSERT, - // which writes only the serialized properties, would leave that column empty - val title: String get() = "$name by $author" -} +) @DBRow("category") @Serializable @@ -120,7 +116,7 @@ data class PersonWithId( @DBRow("product") @Serializable data class Product( - @PrimaryKey val sku: String, + @PrimaryKey val sku: String?, val name: String, val price: Price, ) @@ -128,7 +124,7 @@ data class Product( @DBRow("student_with_autoincrement") @Serializable data class StudentWithAutoincrement( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val studentName: String, val grade: Grade, ) @@ -144,7 +140,7 @@ data class Enrollment( @DBRow("file_data") @Serializable data class FileData( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val fileName: String, val content: ByteArray, val metadata: String, @@ -179,7 +175,7 @@ data class FileData( @DBRow("user_account") @Serializable data class UserAccount( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val username: String, val email: String, val status: UserStatus, @@ -193,7 +189,7 @@ data class UserAccount( @DBRow("task") @Serializable data class Task( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val title: String, val priority: Priority?, val description: String, @@ -206,7 +202,7 @@ data class Task( @DBRow("unique_email_test") @Serializable data class UniqueEmailTest( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @Unique val email: String, val name: String, ) @@ -218,7 +214,7 @@ data class UniqueEmailTest( @DBRow("collate_nocase_test") @Serializable data class CollateNoCaseTest( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @CollateNoCase val username: String, @CollateNoCase @Unique val email: String, val description: String, @@ -231,7 +227,7 @@ data class CollateNoCaseTest( @DBRow("composite_unique_test") @Serializable data class CompositeUniqueTest( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @CompositeUnique(0) val groupA: String, @CompositeUnique(0) val groupB: Int, @CompositeUnique(1) val groupC: String, @@ -246,7 +242,7 @@ data class CompositeUniqueTest( @DBRow("multi_group_unique_test") @Serializable data class MultiGroupUniqueTest( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @CompositeUnique(0, 1) val userId: Int, @CompositeUnique(0) val eventType: String, @CompositeUnique(1) val timestamp: Long, @@ -260,7 +256,7 @@ data class MultiGroupUniqueTest( @DBRow("combined_constraints_test") @Serializable data class CombinedConstraintsTest( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @Unique @CollateNoCase val code: String, @Unique val serial: String, val value: Int, @@ -276,7 +272,7 @@ data class CombinedConstraintsTest( @DBRow("fk_user") @Serializable data class FKUser( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @Unique val email: String, val name: String, ) @@ -287,7 +283,7 @@ data class FKUser( @DBRow("fk_order") @Serializable data class FKOrder( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @com.ctrip.sqllin.dsl.annotation.References( tableName = "fk_user", foreignKeys = ["id"], @@ -304,7 +300,7 @@ data class FKOrder( @DBRow("fk_post") @Serializable data class FKPost( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @com.ctrip.sqllin.dsl.annotation.References( tableName = "fk_user", foreignKeys = ["id"], @@ -321,7 +317,7 @@ data class FKPost( @DBRow("fk_profile") @Serializable data class FKProfile( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @com.ctrip.sqllin.dsl.annotation.References( tableName = "fk_user", foreignKeys = ["id"], @@ -355,7 +351,7 @@ data class FKProduct( trigger = com.ctrip.sqllin.dsl.annotation.Trigger.ON_DELETE_CASCADE ) data class FKOrderItem( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @com.ctrip.sqllin.dsl.annotation.ForeignKey(group = 0, reference = "categoryId") val productCategory: Int, @com.ctrip.sqllin.dsl.annotation.ForeignKey(group = 0, reference = "productCode") @@ -370,7 +366,7 @@ data class FKOrderItem( @DBRow("fk_comment") @Serializable data class FKComment( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @com.ctrip.sqllin.dsl.annotation.References( tableName = "fk_user", foreignKeys = ["id"], @@ -398,7 +394,7 @@ data class FKComment( @DBRow("default_values_test") @Serializable data class DefaultValuesTest( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val name: String, @com.ctrip.sqllin.dsl.annotation.Default("'active'") val status: String, @com.ctrip.sqllin.dsl.annotation.Default("0") val loginCount: Int, @@ -413,7 +409,7 @@ data class DefaultValuesTest( @DBRow("default_nullable_test") @Serializable data class DefaultNullableTest( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val name: String, @com.ctrip.sqllin.dsl.annotation.Default("'In Stock'") val availability: String?, @com.ctrip.sqllin.dsl.annotation.Default("100") val quantity: Int?, @@ -426,7 +422,7 @@ data class DefaultNullableTest( @DBRow("default_fk_parent") @Serializable data class DefaultFKParent( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val name: String, ) @@ -442,83 +438,9 @@ data class DefaultFKParent( trigger = com.ctrip.sqllin.dsl.annotation.Trigger.ON_DELETE_SET_DEFAULT ) data class DefaultFKChild( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @com.ctrip.sqllin.dsl.annotation.ForeignKey(group = 0, reference = "id") @com.ctrip.sqllin.dsl.annotation.Default("0") val parentId: Long, val description: String, -) -/** - * An `internal` entity, used to verify that the processor propagates the entity's visibility - * to the generated table object. If it doesn't, the generated `public` object triggers - * EXPOSED_SUPER_CLASS, EXPOSED_FUNCTION_RETURN_TYPE and EXPOSED_RECEIVER_TYPE errors, and - * this module fails to compile. - */ -@DBRow("internal_visibility") -@Serializable -internal data class InternalVisibility( - @PrimaryKey(autoIncrement = true) val id: Long?, - val name: String, -) - -/** - * The 'alter_target' table in its shape *before* the migration exercised by - * `testSchemaModification`: it has `name` and `legacy`, and no `nickname`. - */ -@DBRow("alter_target") -@Serializable -data class AlterBefore( - @PrimaryKey(autoIncrement = true) val id: Long?, - val name: String, - val legacy: Int, -) - -/** - * The same 'alter_target' table in its shape *after* the migration: `nickname` has been added, - * `name` has been renamed to `fullName`, and `legacy` has been dropped. Mapping two entities onto - * one table name is what lets the test read the table back through whichever shape it should - * currently have, so a migration step that silently does nothing fails the test. - */ -@DBRow("alter_target") -@Serializable -data class AlterAfter( - @PrimaryKey(autoIncrement = true) val id: Long?, - val fullName: String, - val nickname: String?, -) - -/** - * The migrated shape plus the `legacy` column, used purely as a probe: selecting it succeeds while - * `legacy` is still present and fails once DROP COLUMN has removed it. - */ -@DBRow("alter_target") -@Serializable -data class AlterWithLegacy( - @PrimaryKey(autoIncrement = true) val id: Long?, - val fullName: String, - val nickname: String?, - val legacy: Int, -) - -/** - * Supplies the destination table name for the `ALTER_RENAME_TABLE_TO` step; same shape as - * [AlterAfter]. - */ -@DBRow("alter_renamed") -@Serializable -data class AlterRenamed( - @PrimaryKey(autoIncrement = true) val id: Long?, - val fullName: String, - val nickname: String?, -) - -/** - * A non-null `Long` primary key: supplied by the caller, as an ID assigned by a remote service would - * be, yet still an `INTEGER PRIMARY KEY` and so still an alias for SQLite's rowid. - */ -@DBRow("remote_movie") -@Serializable -data class RemoteMovie( - @PrimaryKey val id: Long, - val title: String, -) +) \ No newline at end of file diff --git a/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/TestPrimitiveTypeForKSP.kt b/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/TestPrimitiveTypeForKSP.kt index 9f0a29cf..9b62e20f 100644 --- a/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/TestPrimitiveTypeForKSP.kt +++ b/sqllin-dsl-test/src/commonMain/kotlin/com/ctrip/sqllin/dsl/test/TestPrimitiveTypeForKSP.kt @@ -45,9 +45,4 @@ class TestPrimitiveTypeForKSP( val testEnum: Priority, val testTypeAlias: Code, @Transient val testTransient: Int = 0, - // No column can hold a List, so this only compiles while @Transient keeps it out of the table - @Transient val testTransientUnsupported: List = emptyList(), -) { - // Nor this, which compiles because a computed property isn't serialized and so isn't a column at all - val testComputedUnsupported: List get() = emptyList() -} \ No newline at end of file +) \ No newline at end of file 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 08a65ba2..e44a5bf5 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 @@ -79,9 +79,6 @@ class JvmTest { @Test fun testSchemaModification() = commonTest.testSchemaModification() - @Test - fun testPrimaryKeyNullability() = commonTest.testPrimaryKeyNullability() - @Test fun testStringOperators() = commonTest.testStringOperators() 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 12695beb..ef1f1367 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 @@ -95,9 +95,6 @@ class NativeTest { @Test fun testSchemaModification() = commonTest.testSchemaModification() - @Test - fun testPrimaryKeyNullability() = commonTest.testPrimaryKeyNullability() - @Test fun testStringOperators() = commonTest.testStringOperators() diff --git a/sqllin-dsl/doc/getting-start-cn.md b/sqllin-dsl/doc/getting-start-cn.md index f08a727e..8b70fd08 100644 --- a/sqllin-dsl/doc/getting-start-cn.md +++ b/sqllin-dsl/doc/getting-start-cn.md @@ -7,8 +7,6 @@ 将 _sqllin-dsl_、_sqllin-driver_ 以及 _sqllin-processor_ 依赖添加到你的 `build.gradle.kts`: ```kotlin -import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask - plugins { kotlin("multiplatform") kotlin("plugin.serialization") @@ -45,19 +43,7 @@ dependencies { // sqllin-processor add("kspCommonMainMetadata", "com.ctrip.kotlin:sqllin-processor:$sqllinVersion") } - -// The generated code is a source directory of commonMain, so every task that reads it has to run after KSP -afterEvaluate { - tasks { - matching { (it is KotlinCompilationTask<*> || it.name.startsWith("ksp")) && it.name != "kspCommonMainKotlinMetadata" } - .configureEach { dependsOn("kspCommonMainKotlinMetadata") } - } -} ``` - -最后一段是必需的。生成的表对象作为 `commonMain` 的源码目录加入工程,因此每个 Kotlin 编译任务都会读取 -`kspCommonMainKotlinMetadata` 的输出;如果你的工程还运行着其他 KSP 处理器(比如 Room 或 Koin Annotations),它们的 KSP 任务也会读取。 -一个任务读取另一个任务的输出却没有声明对它的依赖时,Gradle 会让构建失败。KSP 自己的任务按名字匹配,因为不同 KSP 版本的任务类型不同。 > 注意:如果你想将 SQLlin 的依赖添加到你的 Kotlin/Native 可执行程序工程,有时你需要正确添加对 SQLite 的 `linkerOpts` 到你的 > `build.gradle.kts`。你可以参考 [issue #48](https://github.com/ctripcorp/SQLlin/issues/48) 来获取更多信息。 @@ -164,7 +150,7 @@ val database = Database( when (oldVersion) { 1 -> { // Example: Add a new column in version 2 - PersonTable ALTER_ADD_COLUMN PersonTable.email + PersonTable ALERT_ADD_COLUMN PersonTable.email } } } @@ -205,9 +191,6 @@ data class Person( `@DBRow` 的参数 `tableName` 表示数据库中的表名,请确保传入正确的值。如果不手动传入,_sqllin-processor_ 将会使用类名作为表名,比如 `Person` 类的默认表名是"Person"。 -对于每个 `@DBRow` 类,_sqllin-processor_ 都会生成一个以类名加 `Table` 后缀命名的对象,比如 `Person` 对应 `PersonTable`, -与 `tableName` 的取值无关。使用 DSL 编写 SQL 时用的就是这个对象。 - 在 _sqllin-dsl_ 中,对象序列化为 SQL 语句,或者从游标中反序列化依赖 _kotlinx.serialization_,所以你需要在你的 data class 上添加 `@Serializable` 注解。因此,如果你想在序列化或反序列化以及 `Table` 类生成的时候忽略某些属性,你可以给你的属性添加 `kotlinx.serialization.Transient` 注解。 @@ -234,23 +217,11 @@ data class Person( ) ``` -**重要的类型和可空性规则:** 属性的可空性决定了主键的值由谁提供。 - -- **`Long?`,由数据库分配**:映射到 SQLite 的 `INTEGER PRIMARY KEY`,它作为内部 `rowid` 的别名。当插入 `id = null` 的新记录时,SQLite 会自动生成 ID。 - -- **`Long`,由你提供**:同样映射到 `INTEGER PRIMARY KEY`,因此仍然是 `rowid` 的别名,但每次插入都会写入你提供的值。适用于来自外部的数字主键,例如远端服务分配的 ID: +**重要的类型和可空性规则:** -```kotlin -@DBRow -@Serializable -data class Movie( - @PrimaryKey - val id: Long, // Non-nullable, user-provided, still a rowid alias - val title: String, -) -``` +- **对于自增的 `Long` 主键**:属性**必须**声明为可空类型(`Long?`)。这会映射到 SQLite 的 `INTEGER PRIMARY KEY`,它作为内部 `rowid` 的别名。当插入 `id = null` 的新记录时,SQLite 会自动生成 ID。 -- **其他类型(String、Int 等),由你提供**:属性**必须**是非空的,映射为 `TEXT PRIMARY KEY NOT NULL` 这样的列。除 `Long` 以外任何类型的可空主键都会导致编译错误。插入时必须提供唯一值: +- **对于其他类型(String、Int 等)**:属性**必须**是非空的。插入时必须提供唯一值: ```kotlin @DBRow @@ -262,7 +233,7 @@ data class User( ) ``` -`autoIncrement` 参数启用更严格的自增行为(使用 `AUTOINCREMENT` 关键字),确保行 ID 永远不会被重用。它要求属性为 `Long?`,这是唯一一种由数据库分配值的主键。 +`autoIncrement` 参数启用更严格的自增行为(使用 `AUTOINCREMENT` 关键字),确保行 ID 永远不会被重用。这仅对 `Long?` 属性有意义。 #### 使用 @CompositePrimaryKey 定义组合主键 @@ -286,8 +257,8 @@ data class Enrollment( **重要规则:** -- 必须在同一个类中对**至少两个属性**应用 `@CompositePrimaryKey`;只标注一个会导致编译错误,单列主键应使用 `@PrimaryKey` -- 所有带有 `@CompositePrimaryKey` 的属性**必须是非空的**,并在生成的表中声明为 `NOT NULL` +- 你可以在同一个类中对**多个属性**应用 `@CompositePrimaryKey` +- 所有带有 `@CompositePrimaryKey` 的属性**必须是非空的** - 你**不能**在同一个类中混合使用 `@PrimaryKey` 和 `@CompositePrimaryKey` - 只能使用其中一个 - 所有 `@CompositePrimaryKey` 属性的组合形成表的组合主键 @@ -308,7 +279,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @Unique val email: String, // Each email must be unique @Unique val username: String, // Each username must be unique val displayName: String, @@ -338,7 +309,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class Enrollment( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @CompositeUnique(0) val studentId: Int, @CompositeUnique(0) val courseId: Int, val enrollmentDate: String, @@ -359,7 +330,7 @@ data class Enrollment( @DBRow @Serializable data class Event( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @CompositeUnique(0, 1) val userId: Int, // Part of groups 0 and 1 @CompositeUnique(0) val eventType: String, // Part of group 0 @CompositeUnique(1) val timestamp: Long, // Part of group 1 @@ -392,7 +363,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @CollateNoCase @Unique val email: String, // Case-insensitive unique email @CollateNoCase val username: String, // Case-insensitive username val bio: String, @@ -422,7 +393,7 @@ data class User( @DBRow @Serializable data class Product( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @Unique @CollateNoCase val code: String, // Unique and case-insensitive val name: String, val price: Double, @@ -442,7 +413,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val name: String, @Default("'active'") val status: String, // String default @Default("0") val loginCount: Int, // Numeric default @@ -474,7 +445,7 @@ data class User( @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References( tableName = "User", foreignKeys = ["id"], @@ -507,7 +478,7 @@ val status: String ### 支持的类型 -SQLlin 支持以下 Kotlin 类型用于 `@DBRow` 数据类的属性。其他任何类型的属性都会导致编译错误;如果想让这样的属性不进入表中,请为它加上 `kotlinx.serialization.Transient` 注解: +SQLlin 支持以下 Kotlin 类型用于 `@DBRow` 数据类的属性: #### 数值类型 - **整数类型:** `Byte`、`Short`、`Int`、`Long` @@ -563,7 +534,7 @@ enum class UserStatus { @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val username: String, val status: UserStatus, // Stored as 0, 1, 2, or 3 val priority: Priority?, // Nullable enum is also supported @@ -634,7 +605,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val name: String, val email: String, ) @@ -642,7 +613,7 @@ data class User( @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References( tableName = "User", foreignKeys = ["id"], @@ -691,7 +662,7 @@ data class Product( constraintName = "fk_product" ) data class OrderItem( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @ForeignKey(group = 0, reference = "categoryId") val productCategory: Int, @ForeignKey(group = 0, reference = "productCode") @@ -719,7 +690,7 @@ data class OrderItem( @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_CASCADE) val userId: Long, val amount: Double, @@ -732,7 +703,7 @@ data class Order( @DBRow @Serializable data class Post( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_SET_NULL) val authorId: Long?, // Must be nullable! val content: String, @@ -745,7 +716,7 @@ data class Post( @DBRow @Serializable data class OrderItem( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "Order", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_RESTRICT) val orderId: Long, val productId: Long, @@ -758,7 +729,7 @@ data class OrderItem( @DBRow @Serializable data class Comment( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_SET_DEFAULT) val userId: Long = 0L, // Default to 0 (anonymous user) val content: String, @@ -793,7 +764,7 @@ UPDATE 操作也有相同的操作: @ForeignKeyGroup(group = 0, tableName = "User", trigger = Trigger.ON_DELETE_CASCADE) @ForeignKeyGroup(group = 1, tableName = "Product", trigger = Trigger.ON_DELETE_RESTRICT) data class OrderItem( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @ForeignKey(group = 0, reference = "id") val userId: Long, @ForeignKey(group = 1, reference = "id") val productId: Long, val quantity: Int, @@ -813,7 +784,7 @@ data class OrderItem( @DBRow @Serializable data class OrderItem( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_CASCADE) val userId: Long, @References(tableName = "Product", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_RESTRICT) @@ -830,7 +801,7 @@ data class OrderItem( @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References( tableName = "User", foreignKeys = ["id"], @@ -865,7 +836,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @Unique val email: String, val name: String, ) @@ -874,7 +845,7 @@ data class User( @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_CASCADE) val userId: Long, val amount: Double, @@ -885,7 +856,7 @@ data class Order( @DBRow @Serializable data class Post( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_SET_NULL) val authorId: Long?, // Nullable - posts can exist without author val title: String, diff --git a/sqllin-dsl/doc/getting-start.md b/sqllin-dsl/doc/getting-start.md index 5687b3fa..8b721c8d 100644 --- a/sqllin-dsl/doc/getting-start.md +++ b/sqllin-dsl/doc/getting-start.md @@ -9,8 +9,6 @@ Add the dependencies of _sqllin-dsl_, _sqllin-driver_ and _sqllin-processor_ into your `build.gradle.kts`: ```kotlin -import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask - plugins { kotlin("multiplatform") kotlin("plugin.serialization") @@ -47,21 +45,8 @@ dependencies { // sqllin-processor add("kspCommonMainMetadata", "com.ctrip.kotlin:sqllin-processor:$sqllinVersion") } - -// The generated code is a source directory of commonMain, so every task that reads it has to run after KSP -afterEvaluate { - tasks { - matching { (it is KotlinCompilationTask<*> || it.name.startsWith("ksp")) && it.name != "kspCommonMainKotlinMetadata" } - .configureEach { dependsOn("kspCommonMainKotlinMetadata") } - } -} ``` -The last block is required. The generated table objects are added to `commonMain` as a source directory, so every Kotlin -compilation reads the output of `kspCommonMainKotlinMetadata`, and so does every other KSP task when your project also runs -another KSP processor, such as Room or Koin Annotations. Gradle fails the build when a task reads the output of another task -without depending on it. KSP's own tasks are matched by name, because their types differ between KSP versions. - > Note: If you want to add dependencies of SQLlin into your Kotlin/Native executable program projects, sometimes you need to add the `linkerOpts` > of SQLite into your `build.gradle.kts` correctly. You can refer to [issue #48](https://github.com/ctripcorp/SQLlin/issues/48) to get more information. @@ -173,7 +158,7 @@ val database = Database( when (oldVersion) { 1 -> { // Example: Add a new column in version 2 - PersonTable ALTER_ADD_COLUMN PersonTable.email + PersonTable ALERT_ADD_COLUMN PersonTable.email } } } @@ -216,9 +201,6 @@ The `@DBRow`'s param `tableName` represents the table name in Database, please e the correct value. If you don't pass the parameter manually, _sqllin-processor_ will use the class name as table name, for example, `Person`'s default table name is "Person". -For each `@DBRow` class, _sqllin-processor_ generates an object named after the class with a `Table` suffix, such as -`PersonTable` for `Person`, whatever its `tableName` is. That object is what you write SQL against with the DSL. - In _sqllin-dsl_, objects are serialized to SQL and deserialized from cursor depend on _kotlinx.serialization_. So, you also need to add the `@Serializable` onto your data classes. Therefore, if you want to ignore some properties when serialization or deserialization and `Table` classes generation, you can annotate your properties with `kotlinx.serialization.Transient`. @@ -245,23 +227,11 @@ data class Person( ) ``` -**Important type and nullability rules:** the nullability of the property decides who supplies the key's value. - -- **`Long?`, assigned by the database**: This maps to SQLite's `INTEGER PRIMARY KEY`, which acts as an alias for the internal `rowid`. When inserting a new record with `id = null`, SQLite automatically generates the ID. - -- **`Long`, supplied by you**: This also maps to `INTEGER PRIMARY KEY`, so it is still a `rowid` alias, but every insert writes the value you provide. Use it for numeric keys that come from elsewhere, such as IDs assigned by a remote service: +**Important type and nullability rules:** -```kotlin -@DBRow -@Serializable -data class Movie( - @PrimaryKey - val id: Long, // Non-nullable, user-provided, still a rowid alias - val title: String, -) -``` +- **For `Long` primary keys with auto-increment**: The property **must** be declared as nullable (`Long?`). This maps to SQLite's `INTEGER PRIMARY KEY` which acts as an alias for the internal `rowid`. When inserting a new record with `id = null`, SQLite automatically generates the ID. -- **Other types (String, Int, etc.), supplied by you**: The property **must** be non-nullable, and maps to a column such as `TEXT PRIMARY KEY NOT NULL`. A nullable primary key of any type other than `Long` is a compile-time error. You must provide a unique value when inserting: +- **For other types (String, Int, etc.)**: The property **must** be non-nullable. You must provide a unique value when inserting: ```kotlin @DBRow @@ -273,7 +243,7 @@ data class User( ) ``` -The `autoIncrement` parameter enables stricter auto-incrementing behavior (using `AUTOINCREMENT` keyword), ensuring row IDs are never reused. It requires a `Long?` property, the only kind of key the database assigns. +The `autoIncrement` parameter enables stricter auto-incrementing behavior (using `AUTOINCREMENT` keyword), ensuring row IDs are never reused. This is only meaningful for `Long?` properties. #### Composite Primary Key with @CompositePrimaryKey @@ -297,8 +267,8 @@ data class Enrollment( **Important rules:** -- Apply `@CompositePrimaryKey` to **at least two properties** in the same class; annotating only one is a compile-time error, since a single-column primary key is declared with `@PrimaryKey` -- All properties with `@CompositePrimaryKey` **must be non-nullable**, and are declared `NOT NULL` in the generated table +- You can apply `@CompositePrimaryKey` to **multiple properties** in the same class +- All properties with `@CompositePrimaryKey` **must be non-nullable** - You **cannot** mix `@PrimaryKey` and `@CompositePrimaryKey` in the same class - use one or the other - The combination of all `@CompositePrimaryKey` properties forms the table's composite primary key @@ -319,7 +289,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @Unique val email: String, // Each email must be unique @Unique val username: String, // Each username must be unique val displayName: String, @@ -349,7 +319,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class Enrollment( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @CompositeUnique(0) val studentId: Int, @CompositeUnique(0) val courseId: Int, val enrollmentDate: String, @@ -370,7 +340,7 @@ data class Enrollment( @DBRow @Serializable data class Event( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @CompositeUnique(0, 1) val userId: Int, // Part of groups 0 and 1 @CompositeUnique(0) val eventType: String, // Part of group 0 @CompositeUnique(1) val timestamp: Long, // Part of group 1 @@ -403,7 +373,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @CollateNoCase @Unique val email: String, // Case-insensitive unique email @CollateNoCase val username: String, // Case-insensitive username val bio: String, @@ -433,7 +403,7 @@ You can combine multiple constraint annotations on the same property: @DBRow @Serializable data class Product( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @Unique @CollateNoCase val code: String, // Unique and case-insensitive val name: String, val price: Double, @@ -453,7 +423,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val name: String, @Default("'active'") val status: String, // String default @Default("0") val loginCount: Int, // Numeric default @@ -485,7 +455,7 @@ Default values are **required** when using `ON_DELETE_SET_DEFAULT` or `ON_UPDATE @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References( tableName = "User", foreignKeys = ["id"], @@ -518,7 +488,7 @@ val status: String ### Supported Types -SQLlin supports the following Kotlin types for properties in `@DBRow` data classes. A property of any other type is a compile-time error; to keep such a property out of the table, annotate it with `kotlinx.serialization.Transient`: +SQLlin supports the following Kotlin types for properties in `@DBRow` data classes: #### Numeric Types - **Integer types:** `Byte`, `Short`, `Int`, `Long` @@ -574,7 +544,7 @@ enum class UserStatus { @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val username: String, val status: UserStatus, // Stored as 0, 1, 2, or 3 val priority: Priority?, // Nullable enum is also supported @@ -645,7 +615,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, val name: String, val email: String, ) @@ -653,7 +623,7 @@ data class User( @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References( tableName = "User", foreignKeys = ["id"], @@ -702,7 +672,7 @@ data class Product( constraintName = "fk_product" ) data class OrderItem( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @ForeignKey(group = 0, reference = "categoryId") val productCategory: Int, @ForeignKey(group = 0, reference = "productCode") @@ -730,7 +700,7 @@ Triggers define what happens when a referenced row is deleted or updated. SQLlin @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_CASCADE) val userId: Long, val amount: Double, @@ -743,7 +713,7 @@ data class Order( @DBRow @Serializable data class Post( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_SET_NULL) val authorId: Long?, // Must be nullable! val content: String, @@ -756,7 +726,7 @@ data class Post( @DBRow @Serializable data class OrderItem( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "Order", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_RESTRICT) val orderId: Long, val productId: Long, @@ -769,7 +739,7 @@ data class OrderItem( @DBRow @Serializable data class Comment( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_SET_DEFAULT) val userId: Long = 0L, // Default to 0 (anonymous user) val content: String, @@ -804,7 +774,7 @@ A table can have multiple foreign key constraints to different parent tables: @ForeignKeyGroup(group = 0, tableName = "User", trigger = Trigger.ON_DELETE_CASCADE) @ForeignKeyGroup(group = 1, tableName = "Product", trigger = Trigger.ON_DELETE_RESTRICT) data class OrderItem( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @ForeignKey(group = 0, reference = "id") val userId: Long, @ForeignKey(group = 1, reference = "id") val productId: Long, val quantity: Int, @@ -824,7 +794,7 @@ Or using `@References`: @DBRow @Serializable data class OrderItem( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_CASCADE) val userId: Long, @References(tableName = "Product", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_RESTRICT) @@ -841,7 +811,7 @@ You can optionally name your foreign key constraints for better error messages a @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References( tableName = "User", foreignKeys = ["id"], @@ -876,7 +846,7 @@ import kotlinx.serialization.Serializable @DBRow @Serializable data class User( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @Unique val email: String, val name: String, ) @@ -885,7 +855,7 @@ data class User( @DBRow @Serializable data class Order( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_CASCADE) val userId: Long, val amount: Double, @@ -896,7 +866,7 @@ data class Order( @DBRow @Serializable data class Post( - @PrimaryKey(autoIncrement = true) val id: Long?, + @PrimaryKey(isAutoincrement = true) val id: Long?, @References(tableName = "User", foreignKeys = ["id"], trigger = Trigger.ON_DELETE_SET_NULL) val authorId: Long?, // Nullable - posts can exist without author val title: String, diff --git a/sqllin-dsl/doc/modify-database-and-transaction-cn.md b/sqllin-dsl/doc/modify-database-and-transaction-cn.md index e2962a58..805da063 100644 --- a/sqllin-dsl/doc/modify-database-and-transaction-cn.md +++ b/sqllin-dsl/doc/modify-database-and-transaction-cn.md @@ -4,7 +4,7 @@ ## 表结构操作 -SQLlin 提供了用于管理表结构的类型安全 DSL 操作:CREATE、DROP 和 ALTER。 +SQLlin 提供了用于管理表结构的类型安全 DSL 操作:CREATE、DROP 和 ALTER(在 API 中称为 ALERT)。 ### CREATE - 创建表 @@ -61,7 +61,7 @@ fun sample() { ### ALTER - 修改表结构 -SQLlin 提供了多种 ALTER 操作来修改现有的表结构: +SQLlin 提供了多种 ALTER(ALERT)操作来修改现有的表结构: #### 添加列 @@ -78,7 +78,7 @@ data class Person( fun sample() { database { - PersonTable ALTER_ADD_COLUMN PersonTable.email + PersonTable ALERT_ADD_COLUMN PersonTable.email } } ``` @@ -91,10 +91,10 @@ fun sample() { fun sample() { database { // Rename using Table object - PersonTable ALTER_RENAME_TABLE_TO NewPersonTable + PersonTable ALERT_RENAME_TABLE_TO NewPersonTable // Or rename using old table name as String - "old_person" ALTER_RENAME_TABLE_TO NewPersonTable + "old_person" ALERT_RENAME_TABLE_TO NewPersonTable } } ``` @@ -149,7 +149,7 @@ val database = Database( when (oldVersion) { 1 -> { // Upgrade from version 1 to 2 - PersonTable ALTER_ADD_COLUMN PersonTable.email + PersonTable ALERT_ADD_COLUMN PersonTable.email CREATE(AddressTable) } } diff --git a/sqllin-dsl/doc/modify-database-and-transaction.md b/sqllin-dsl/doc/modify-database-and-transaction.md index fde5524c..2f480b94 100644 --- a/sqllin-dsl/doc/modify-database-and-transaction.md +++ b/sqllin-dsl/doc/modify-database-and-transaction.md @@ -7,7 +7,7 @@ we start to learn how to write SQL statements with SQLlin. ## Table Structure Operations -SQLlin provides type-safe DSL operations for managing table structures: CREATE, DROP, and ALTER. +SQLlin provides type-safe DSL operations for managing table structures: CREATE, DROP, and ALTER (referred to as ALERT in the API). ### CREATE - Creating Tables @@ -64,7 +64,7 @@ fun sample() { ### ALTER - Modifying Table Structure -SQLlin provides several ALTER operations for modifying existing table structures: +SQLlin provides several ALTER (ALERT) operations for modifying existing table structures: #### Add Column @@ -81,7 +81,7 @@ data class Person( fun sample() { database { - PersonTable ALTER_ADD_COLUMN PersonTable.email + PersonTable ALERT_ADD_COLUMN PersonTable.email } } ``` @@ -94,10 +94,10 @@ Rename an existing table to a new name: fun sample() { database { // Rename using Table object - PersonTable ALTER_RENAME_TABLE_TO NewPersonTable + PersonTable ALERT_RENAME_TABLE_TO NewPersonTable // Or rename using old table name as String - "old_person" ALTER_RENAME_TABLE_TO NewPersonTable + "old_person" ALERT_RENAME_TABLE_TO NewPersonTable } } ``` @@ -152,7 +152,7 @@ val database = Database( when (oldVersion) { 1 -> { // Upgrade from version 1 to 2 - PersonTable ALTER_ADD_COLUMN PersonTable.email + PersonTable ALERT_ADD_COLUMN PersonTable.email CREATE(AddressTable) } } diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DatabaseScope.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DatabaseScope.kt index 7b995311..07f58cd9 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DatabaseScope.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DatabaseScope.kt @@ -23,7 +23,7 @@ import com.ctrip.sqllin.dsl.annotation.StatementDslMaker import com.ctrip.sqllin.dsl.sql.Table import com.ctrip.sqllin.dsl.sql.X import com.ctrip.sqllin.dsl.sql.clause.* -import com.ctrip.sqllin.dsl.sql.operation.Alter +import com.ctrip.sqllin.dsl.sql.operation.Alert import com.ctrip.sqllin.dsl.sql.operation.Create import com.ctrip.sqllin.dsl.sql.operation.Delete import com.ctrip.sqllin.dsl.sql.operation.Drop @@ -53,50 +53,34 @@ import kotlin.jvm.JvmName * - **SELECT**: Query records with WHERE, ORDER BY, LIMIT, GROUP BY, JOIN, and UNION * - **CREATE**: Create tables from data class definitions * - **DROP**: Remove tables from the database - * - **ALTER**: Modify table structures (add columns, rename tables/columns, drop columns) + * - **ALERT (ALTER)**: Modify table structures (add columns, rename tables/columns, drop columns) * * Transaction support: * - Use [transaction] to execute multiple statements atomically * - Transactions can be nested and are automatically committed or rolled back * - * **Execution is deferred**: no statement runs until the scope exits. A [SelectStatement] built - * inside the scope therefore holds no results while the scope is still open, and calling - * `getResults()` on it there throws [IllegalStateException]. Hold the statement in a variable - * declared outside the scope and read its results after the scope has exited, as shown below. - * For the same reason a query's result cannot inform a write in the same scope: a read-modify-write - * has to be split into two scopes. - * * Example: * ```kotlin - * // Create and modify table structure * database { + * // Create and modify table structure * CREATE(PersonTable) - * PersonTable ALTER_ADD_COLUMN PersonTable.email - * } + * PersonTable ALERT_ADD_COLUMN email * - * // Modify data, and build a query whose results are read once the scope has exited - * lateinit var adults: SelectStatement - * database { - * PersonTable { table -> - * transaction { - * table INSERT person - * table UPDATE SET { name = "Alice" } WHERE (age GTE 18) - * } - * adults = table SELECT WHERE(age GTE 18) LIMIT 10 + * // Data manipulation + * transaction { + * PersonTable INSERT person + * PersonTable UPDATE SET { name = "Alice" } WHERE (age GTE 18) * } - * } - * // Every statement above ran when the scope exited, so the results are available only here - * val results = adults.getResults() + * val adults = PersonTable SELECT WHERE(age GTE 18) LIMIT 10 * - * // Cleanup - * database { + * // Cleanup * PersonTable.DROP() * } * ``` * * @author Yuang Qiao */ -@Suppress("UNCHECKED_CAST", "DSL_MARKER_APPLIED_TO_WRONG_TARGET") +@Suppress("UNCHECKED_CAST") public class DatabaseScope internal constructor( private val databaseConnection: DatabaseConnection, private val enableSimpleSQLLog: Boolean, @@ -220,9 +204,6 @@ public class DatabaseScope internal constructor( * the database auto-generate it. For normal inserts where the database should generate IDs * automatically, use [INSERT] instead. * - * This only matters for a `Long?` primary key. If the key is always supplied by the caller, - * declare it as a non-null `Long` instead, and a plain [INSERT] writes it. - * * This function is particularly useful for: * - Data migration from another database where you need to preserve existing IDs * - Testing scenarios where you need predictable, specific ID values @@ -646,8 +627,8 @@ public class DatabaseScope internal constructor( * Example: * ```kotlin * database { - * UserTable.CREATE_INDEX("idx_user_email", UserTable.email) - * UserTable.CREATE_INDEX("idx_user_name_age", UserTable.name, UserTable.age) + * User::class.table.CREATE_INDEX("idx_user_email", User::email) + * User::class.table.CREATE_INDEX("idx_user_name_age", User::name, User::age) * } * ``` * @@ -671,8 +652,8 @@ public class DatabaseScope internal constructor( * Example: * ```kotlin * database { - * UserTable.CREATE_UNIQUE_INDEX("idx_unique_email", UserTable.email) - * ProductTable.CREATE_UNIQUE_INDEX("idx_unique_sku", ProductTable.sku) + * User::class.table.CREATE_UNIQUE_INDEX("idx_unique_email", User::email) + * Product::class.table.CREATE_UNIQUE_INDEX("idx_unique_sku", Product::sku) * } * ``` * @@ -731,7 +712,7 @@ public class DatabaseScope internal constructor( @JvmName("drop") public fun Table.DROP(): Unit = DROP(this) - // ========== ALTER Operations ========== + // ========== ALERT (ALTER) Operations ========== /** * Adds a new column to an existing table. @@ -743,7 +724,7 @@ public class DatabaseScope internal constructor( * Example: * ```kotlin * database { - * PersonTable ALTER_ADD_COLUMN email + * PersonTable ALERT_ADD_COLUMN email * } * ``` * @@ -751,8 +732,8 @@ public class DatabaseScope internal constructor( */ @ExperimentalDSLDatabaseAPI @StatementDslMaker - public infix fun Table.ALTER_ADD_COLUMN(column: ClauseElement) { - val statement = Alter.addColumn(this, column, databaseConnection) + public infix fun Table.ALERT_ADD_COLUMN(column: ClauseElement) { + val statement = Alert.addColumn(this, column, databaseConnection) addStatement(statement) } @@ -762,7 +743,7 @@ public class DatabaseScope internal constructor( * Example: * ```kotlin * database { - * PersonTable ALTER_RENAME_TABLE_TO NewPersonTable + * PersonTable ALERT_RENAME_TABLE_TO NewPersonTable * } * ``` * @@ -770,8 +751,8 @@ public class DatabaseScope internal constructor( */ @ExperimentalDSLDatabaseAPI @StatementDslMaker - public infix fun Table.ALTER_RENAME_TABLE_TO(newTable: Table<*>) { - val statement = Alter.renameTable(tableName, newTable, databaseConnection) + public infix fun Table.ALERT_RENAME_TABLE_TO(newTable: Table<*>) { + val statement = Alert.renameTable(tableName, newTable, databaseConnection) addStatement(statement) } @@ -783,7 +764,7 @@ public class DatabaseScope internal constructor( * Example: * ```kotlin * database { - * "old_person" ALTER_RENAME_TABLE_TO NewPersonTable + * "old_person" ALERT_RENAME_TABLE_TO NewPersonTable * } * ``` * @@ -792,8 +773,8 @@ public class DatabaseScope internal constructor( */ @ExperimentalDSLDatabaseAPI @StatementDslMaker - public infix fun String.ALTER_RENAME_TABLE_TO(newTable: Table<*>) { - val statement = Alter.renameTable(this, newTable, databaseConnection) + public infix fun String.ALERT_RENAME_TABLE_TO(newTable: Table<*>) { + val statement = Alert.renameTable(this, newTable, databaseConnection) addStatement(statement) } @@ -816,7 +797,7 @@ public class DatabaseScope internal constructor( @ExperimentalDSLDatabaseAPI @StatementDslMaker public fun Table.RENAME_COLUMN(oldColumn: R, newColumn: R) { - val statement = Alter.renameColumn(this, oldColumn.valueName, newColumn, databaseConnection) + val statement = Alert.renameColumn(this, oldColumn.valueName, newColumn, databaseConnection) addStatement(statement) } @@ -839,7 +820,7 @@ public class DatabaseScope internal constructor( @ExperimentalDSLDatabaseAPI @StatementDslMaker public fun Table.RENAME_COLUMN(oldColumnName: String, newColumn: ClauseElement) { - val statement = Alter.renameColumn(this, oldColumnName, newColumn, databaseConnection) + val statement = Alert.renameColumn(this, oldColumnName, newColumn, databaseConnection) addStatement(statement) } @@ -862,7 +843,7 @@ public class DatabaseScope internal constructor( @ExperimentalDSLDatabaseAPI @StatementDslMaker public infix fun Table.DROP_COLUMN(column: ClauseElement) { - val statement = Alter.dropColumn(this, column, databaseConnection) + val statement = Alert.dropColumn(this, column, databaseConnection) addStatement(statement) } diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/CreateStatementModifiers.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/CreateStatementModifiers.kt index f53966f2..6f7b6555 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/CreateStatementModifiers.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/CreateStatementModifiers.kt @@ -30,36 +30,32 @@ package com.ctrip.sqllin.dsl.annotation * Additionally, if a property in the class is marked with [PrimaryKey], the class cannot also use the [CompositePrimaryKey] annotation. * * ### Type and Nullability Rules - * The nullability of the property decides who supplies the key's value: + * The behavior of this annotation differs based on the type of property it annotates. + * The following rules must be followed: * - * - **`Long?`: assigned by the database.** - * The property maps to an `INTEGER PRIMARY KEY` column, an alias for SQLite's internal `rowid`. - * Insert an object whose key is `null` and the database assigns the next ID; a plain `INSERT` - * leaves the column out for that reason. + * - **When annotating a `Long` property**: + * The property **must** be declared as a nullable type (`Long?`). This triggers a special + * SQLite mechanism, mapping the property to an `INTEGER PRIMARY KEY` column, which acts as + * an alias for the database's internal `rowid`. This is typically used for auto-incrementing + * keys, where the database assigns an ID upon insertion of a new object (when its ID is `null`). * - * - **`Long`: supplied by the caller.** - * The property still maps to an `INTEGER PRIMARY KEY` column, so it is still an alias for `rowid`, - * but every `INSERT` writes the value you provide. Use this for a numeric key that comes from - * elsewhere, such as an ID assigned by a remote service. + * - **When annotating all other types (e.g., `String`, `Int`)**: + * The property **must** be declared as a non-nullable type (e.g., `String`). + * This creates a standard, user-provided primary key (such as `TEXT PRIMARY KEY`). + * You must provide a unique, non-null value for this property upon insertion. * - * - **Any other type (e.g. `String`, `Int`): supplied by the caller, and must be non-null.** - * The property maps to a column such as `TEXT PRIMARY KEY NOT NULL`. A nullable key of any type - * other than `Long` is a compile-time error, since nothing would ever assign its value. The - * `NOT NULL` is spelled out because SQLite, unlike standard SQL, does not let `PRIMARY KEY` imply it - * on such a column. - * - * @property autoIncrement Indicates whether to append the `AUTOINCREMENT` keyword to the + * @property isAutoincrement Indicates whether to append the `AUTOINCREMENT` keyword to the * `INTEGER PRIMARY KEY` column in the `CREATE TABLE` statement. This enables a stricter * auto-incrementing strategy that ensures row IDs are never reused. - * **Important Note**: This parameter requires a property of type `Long?`, the only kind of key the - * database assigns. Setting it to `true` on any other property is a compile-time error. + * **Important Note**: This parameter is only meaningful when annotating a property of type `Long?`. + * Setting this to `true` on non-Long properties will result in a compile-time error. * * @see DBRow * @see CompositePrimaryKey */ @Target(AnnotationTarget.PROPERTY) @Retention(AnnotationRetention.BINARY) -public annotation class PrimaryKey(val autoIncrement: Boolean = false) +public annotation class PrimaryKey(val isAutoincrement: Boolean = false) /** * Marks a property as a part of a composite primary key for the table. @@ -70,15 +66,12 @@ public annotation class PrimaryKey(val autoIncrement: Boolean = false) * will form the table's composite primary key. * * ### Important Rules - * - At least two properties must be annotated with [CompositePrimaryKey]; annotating only one is a - * compile-time error. A single-column primary key is declared with [PrimaryKey]. + * - A class can have multiple properties annotated with [CompositePrimaryKey]. * - If a class uses [CompositePrimaryKey] on any of its properties, it **cannot** also use * the [PrimaryKey] annotation on any other property. A table can only have one primary key, * which is either a single column or a composite of multiple columns. * - All properties annotated with [CompositePrimaryKey] must be of a **non-nullable** type - * (e.g., `String`, `Int`, `Long`), as primary key columns cannot contain `NULL` values. They are - * declared `NOT NULL` in the generated table, because SQLite, unlike standard SQL, does not let - * `PRIMARY KEY` imply it. + * (e.g., `String`, `Int`, `Long`), as primary key columns cannot contain `NULL` values. * * @see DBRow * @see PrimaryKey diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DslMakers.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DslMakers.kt index d2377b9d..faea3ce2 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DslMakers.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DslMakers.kt @@ -16,17 +16,10 @@ package com.ctrip.sqllin.dsl.annotation -/* - * These DSL markers exist for IntelliJ IDEA, which gives a call to any function or property annotated with a - * @DslMarker annotation one of its four DSL highlighting styles. They are applied to functions and properties - * for that reason, and so don't provide the compiler's DSL scope control, which @DslMarker only gives when - * applied to types. The compiler reports DSL_MARKER_APPLIED_TO_WRONG_TARGET for that use, so it's suppressed - * wherever these annotations are applied. - */ - /** - * DSL marker that highlights calls to SQL statement functions (SELECT, INSERT, UPDATE, DELETE, WHERE, ...) in - * IntelliJ IDEA. + * DSL marker for SQL statement functions to prevent implicit receiver nesting. + * + * Applied to top-level SQL statement functions (SELECT, INSERT, UPDATE, DELETE). * * @author Yuang Qiao */ @@ -36,7 +29,9 @@ package com.ctrip.sqllin.dsl.annotation internal annotation class StatementDslMaker /** - * DSL marker that highlights SQL keywords, such as `X` and the `ASC` and `DESC` ordering, in IntelliJ IDEA. + * DSL marker for SQL keyword classes and properties to prevent implicit receiver nesting. + * + * Applied to SQL keyword constructs (WHERE, ORDER BY, etc.) and their properties. * * @author Yuang Qiao */ @@ -46,7 +41,9 @@ internal annotation class StatementDslMaker internal annotation class KeyWordDslMaker /** - * DSL marker that highlights calls to SQL functions (aggregate, numeric and string functions) in IntelliJ IDEA. + * DSL marker for SQL function builders to prevent implicit receiver nesting. + * + * Applied to SQL function builder functions (aggregate functions, etc.). * * @author Yuang Qiao */ @@ -56,7 +53,7 @@ internal annotation class KeyWordDslMaker internal annotation class FunctionDslMaker /** - * DSL marker that highlights the generated column properties in IntelliJ IDEA. + * DSL marker for generated column name properties. * * This annotation is applied by sqllin-processor to generated table column properties. * **Do not use this annotation manually** - it is intended for code generation only. diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/PrimaryKeyInfo.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/PrimaryKeyInfo.kt index 8841e5ff..07be95d1 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/PrimaryKeyInfo.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/PrimaryKeyInfo.kt @@ -28,16 +28,14 @@ package com.ctrip.sqllin.dsl.sql * When a table has a single primary key column (marked with `@PrimaryKey`): * - [primaryKeyName] contains the column name * - [compositePrimaryKeys] is `null` - * - [isGeneratedByDatabase] is `true` if the key is a `Long?`, whose value the database assigns. A - * non-null `Long` key is also an `INTEGER PRIMARY KEY` (a rowid alias) but is supplied by the caller, - * so it is `false` there, as it is for a key of any other type - * - [isAutomaticIncrement] is `true` if `@PrimaryKey(autoIncrement = true)` was specified + * - [isRowId] is `true` if the key is a `Long?` type (maps to SQLite's INTEGER PRIMARY KEY/rowid) + * - [isAutomaticIncrement] is `true` if `@PrimaryKey(isAutoincrement = true)` was specified * * **Composite Primary Key:** * When a table has multiple primary key columns (marked with `@CompositePrimaryKey`): * - [primaryKeyName] is `null` * - [compositePrimaryKeys] contains the list of column names forming the composite key - * - [isGeneratedByDatabase] is `false` (the caller supplies every column of a composite key) + * - [isRowId] is `false` (composite keys cannot use rowid alias) * - [isAutomaticIncrement] is `false` (composite keys cannot auto-increment) * * **No Primary Key:** @@ -45,8 +43,7 @@ package com.ctrip.sqllin.dsl.sql * * @property primaryKeyName The name of the single primary key column, or `null` for composite keys * @property isAutomaticIncrement Whether the primary key uses SQLite's AUTOINCREMENT keyword - * @property isGeneratedByDatabase Whether the database assigns the primary key's value, in which case - * a plain INSERT leaves the column out + * @property isRowId Whether the primary key is a `Long?` type that maps to SQLite's rowid * @property compositePrimaryKeys List of column names forming a composite primary key, or `null` for single keys * * @author Yuang Qiao @@ -54,6 +51,6 @@ package com.ctrip.sqllin.dsl.sql public class PrimaryKeyInfo( internal val primaryKeyName: String?, internal val isAutomaticIncrement: Boolean, - internal val isGeneratedByDatabase: Boolean, + internal val isRowId: Boolean, internal val compositePrimaryKeys: List?, ) \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/Table.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/Table.kt index fa7e8b0d..710491cb 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/Table.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/Table.kt @@ -96,7 +96,7 @@ public abstract class Table( * @Serializable * @DBRow * data class User( - * @PrimaryKey(autoIncrement = true) val id: Long?, + * @PrimaryKey(isAutoincrement = true) val id: Long?, * @Unique @CollateNoCase val email: String, * val name: String, * val age: Int diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/BaseJoinClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/BaseJoinClause.kt index f852622c..6cb79ad0 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/BaseJoinClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/BaseJoinClause.kt @@ -65,17 +65,14 @@ public sealed class NaturalJoinClause(vararg tables: Table<*>) : BaseJoinClau */ public sealed class JoinClause(vararg tables: Table<*>) : BaseJoinClause(*tables) -@Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") @StatementDslMaker public infix fun JoinStatementWithoutCondition.ON(condition: SelectCondition): JoinSelectStatement = convertToJoinSelectStatement(condition) -@Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") @StatementDslMaker public inline infix fun JoinStatementWithoutCondition.USING(clauseElement: ClauseElement): JoinSelectStatement = USING(listOf(clauseElement)) -@Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") @StatementDslMaker public infix fun JoinStatementWithoutCondition.USING(clauseElements: Iterable): JoinSelectStatement = convertToJoinSelectStatement(clauseElements) \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/ConditionClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/ConditionClause.kt index dee44476..9d895476 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/ConditionClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/ConditionClause.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") - package com.ctrip.sqllin.dsl.sql.clause import com.ctrip.sqllin.dsl.annotation.StatementDslMaker diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/CrossJoinClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/CrossJoinClause.kt index d1401deb..36687ceb 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/CrossJoinClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/CrossJoinClause.kt @@ -47,6 +47,5 @@ internal class CrossJoinClause(vararg tables: Table<*>) : NaturalJoinClause CROSS_JOIN(vararg tables: Table<*>): NaturalJoinClause = CrossJoinClause(*tables) \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/Function.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/Function.kt index ddd579dc..d028707b 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/Function.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/Function.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") - package com.ctrip.sqllin.dsl.sql.clause import com.ctrip.sqllin.dsl.annotation.FunctionDslMaker @@ -223,7 +221,6 @@ public fun Table.length(element: ClauseBlob): ClauseNumber = * @param len The length of the substring to extract * @return ClauseString representing the extracted substring */ -@FunctionDslMaker public fun Table.substr(element: ClauseString, start: Int, len: Int): ClauseString = ClauseString("substr(${element.valueName},$start,$len)", this, true) @@ -241,7 +238,6 @@ public fun Table.substr(element: ClauseString, start: Int, len: Int): Cla * @param element The string to trim * @return ClauseString with whitespace removed from both ends */ -@FunctionDslMaker public fun Table.trim(element: ClauseString): ClauseString = ClauseString("trim(${element.valueName})", this, true) @@ -259,7 +255,6 @@ public fun Table.trim(element: ClauseString): ClauseString = * @param element The string to trim * @return ClauseString with leading whitespace removed */ -@FunctionDslMaker public fun Table.ltrim(element: ClauseString): ClauseString = ClauseString("ltrim(${element.valueName})", this, true) @@ -277,7 +272,6 @@ public fun Table.ltrim(element: ClauseString): ClauseString = * @param element The string to trim * @return ClauseString with trailing whitespace removed */ -@FunctionDslMaker public fun Table.rtrim(element: ClauseString): ClauseString = ClauseString("rtrim(${element.valueName})", this, true) @@ -297,7 +291,6 @@ public fun Table.rtrim(element: ClauseString): ClauseString = * @param new The replacement string * @return ClauseString with replacements applied */ -@FunctionDslMaker public fun Table.replace(element: ClauseString, old: String, new: String): ClauseString = ClauseString("replace(${element.valueName},'$old','$new')", this, true) @@ -317,7 +310,6 @@ public fun Table.replace(element: ClauseString, old: String, new: String) * @param sub The substring to find * @return ClauseNumber representing the position (1-indexed) or 0 if not found */ -@FunctionDslMaker public fun Table.instr(element: ClauseString, sub: String): ClauseNumber = ClauseNumber("instr(${element.valueName},'$sub')", this, true) @@ -337,6 +329,5 @@ public fun Table.instr(element: ClauseString, sub: String): ClauseNumber * @param element The value to format * @return ClauseString with the formatted result */ -@FunctionDslMaker public fun Table.printf(format: String, element: ClauseString): ClauseString = ClauseString("printf('$format',${element.valueName})", this, true) \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/GroupByClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/GroupByClause.kt index c1824149..cf4cb1e8 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/GroupByClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/GroupByClause.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") - package com.ctrip.sqllin.dsl.sql.clause import com.ctrip.sqllin.dsl.annotation.StatementDslMaker diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/HavingClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/HavingClause.kt index 6309dbbc..2cd0c316 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/HavingClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/HavingClause.kt @@ -41,7 +41,6 @@ internal class HavingClause(val selectCondition: SelectCondition) : Condition override val clauseName: String = "HAVING" } -@Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") @StatementDslMaker public infix fun GroupBySelectStatement.HAVING(condition: SelectCondition): HavingSelectStatement = appendToHaving(HavingClause(condition)).also { diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/InnerJoinClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/InnerJoinClause.kt index fb7728a5..78bc8ebe 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/InnerJoinClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/InnerJoinClause.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") - package com.ctrip.sqllin.dsl.sql.clause import com.ctrip.sqllin.dsl.annotation.StatementDslMaker diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/LeftOuterJoinClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/LeftOuterJoinClause.kt index 951e3041..9ba0235e 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/LeftOuterJoinClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/LeftOuterJoinClause.kt @@ -46,7 +46,6 @@ internal class LeftOuterJoinClause( * // Returns all users, including those without orders * ``` */ -@Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") @StatementDslMaker public fun LEFT_OUTER_JOIN(vararg tables: Table<*>): JoinClause = LeftOuterJoinClause(*tables) @@ -76,6 +75,5 @@ internal class NaturalLeftOuterJoinClause( * SELECT(user) NATURAL_LEFT_OUTER_JOIN (profile) * ``` */ -@Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") @StatementDslMaker public fun NATURAL_LEFT_OUTER_JOIN(vararg tables: Table<*>): NaturalJoinClause = NaturalLeftOuterJoinClause(*tables) \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/LimitClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/LimitClause.kt index f74dee3e..6ab9df05 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/LimitClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/LimitClause.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") - package com.ctrip.sqllin.dsl.sql.clause import com.ctrip.sqllin.dsl.annotation.StatementDslMaker diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/OrderByClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/OrderByClause.kt index 8e332c19..08b63330 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/OrderByClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/OrderByClause.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") - package com.ctrip.sqllin.dsl.sql.clause import com.ctrip.sqllin.dsl.annotation.KeyWordDslMaker diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/SetClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/SetClause.kt index e34fc9b0..ca846a5e 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/SetClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/SetClause.kt @@ -81,6 +81,5 @@ public class SetClause : Clause { }.toString() } -@Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") @StatementDslMaker public inline fun SET(block: SetClause.() -> Unit): SetClause = SetClause().apply(block) \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/WhereClause.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/WhereClause.kt index 24d0c207..c4e5426d 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/WhereClause.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/clause/WhereClause.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:Suppress("DSL_MARKER_APPLIED_TO_WRONG_TARGET") - package com.ctrip.sqllin.dsl.sql.clause import com.ctrip.sqllin.dsl.annotation.StatementDslMaker 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..229d5b50 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 @@ -34,16 +34,14 @@ import kotlinx.serialization.descriptors.SerialDescriptor * ``` * * Handles primary key logic: - * - For a `Long?` primary key, whose value the database assigns, omits the ID column unless - * [isInsertWithId] is true - * - For a primary key the caller supplies (a non-null `Long`, any other type, or a composite key), - * includes all columns + * - For auto-increment `Long?` primary keys, omits the ID column unless [isInsertWithId] is true + * - For user-provided primary keys or composite keys, includes all columns * * @param table The table definition containing serialization and primary key metadata * @param builder StringBuilder to append the SQL to * @param values The entities to insert * @param parameters Mutable list to collect parameterized query values - * @param isInsertWithId Whether to include the primary key column even when the database would assign it + * @param isInsertWithId Whether to include the primary key column for rowid-backed keys */ internal fun encodeEntities2InsertValues( table: Table, @@ -53,7 +51,7 @@ internal fun encodeEntities2InsertValues( isInsertWithId: Boolean, ) = with(builder) { val isInsertId = table.primaryKeyInfo?.run { - !isGeneratedByDatabase || isInsertWithId + !isRowId || isInsertWithId } ?: true val serializer = table.kSerializer() append('(') 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 1774afd8..5dd86bde 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 @@ -30,14 +30,14 @@ import kotlinx.serialization.modules.SerializersModule * parameterized VALUES clauses. All values (including null, numbers, strings, ByteArray, etc.) * are converted to `?` placeholders and collected in [parameters] for safe execution. * - * Leaves out the primary key field named [primaryKeyName] when [isInsertId] is `false`, so that the - * database assigns its value. + * Automatically skips the primary key field if [primaryKeyName] is provided, allowing + * database auto-increment to generate the value. * * Example output: `(?, ?, ?)` with parameters: ["John", 30, byteArray] * * @param parameters Mutable list to accumulate parameter values * @param primaryKeyName Name of primary key field to skip, or null to include all fields - * @param isInsertId Whether to encode the primary key field; `false` leaves it for the database to assign + * @param isInsertId whether ignore encoding the special primary key that represents rowid in SQLite * * @author Yuang Qiao */ diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Alter.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Alert.kt similarity index 90% rename from sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Alter.kt rename to sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Alert.kt index b3dd96cf..a897c29c 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Alter.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Alert.kt @@ -23,7 +23,10 @@ import com.ctrip.sqllin.dsl.sql.statement.SingleStatement import com.ctrip.sqllin.dsl.sql.statement.TableStructureStatement /** - * ALTER operation for modifying database table structures. + * ALERT (ALTER) operation for modifying database table structures. + * + * Note: This is named "Alert" but generates SQL ALTER TABLE statements. The naming follows + * the existing codebase convention. * * Supports common table modification operations: * - **ADD COLUMN**: Add a new column to an existing table @@ -35,12 +38,12 @@ import com.ctrip.sqllin.dsl.sql.statement.TableStructureStatement * ```kotlin * database { * // Add a new column - * PersonTable ALTER_ADD_COLUMN email + * PersonTable ALERT_ADD_COLUMN email * * // Rename table - * PersonTable ALTER_RENAME_TABLE_TO NewPersonTable + * PersonTable ALERT_RENAME_TABLE_TO NewPersonTable * // or from old name - * "old_person" ALTER_RENAME_TABLE_TO NewPersonTable + * "old_person" ALERT_RENAME_TABLE_TO NewPersonTable * * // Rename column * PersonTable.RENAME_COLUMN(oldName, newName) @@ -52,16 +55,16 @@ import com.ctrip.sqllin.dsl.sql.statement.TableStructureStatement * } * ``` * - * @see com.ctrip.sqllin.dsl.DatabaseScope.ALTER_ADD_COLUMN - * @see com.ctrip.sqllin.dsl.DatabaseScope.ALTER_RENAME_TABLE_TO + * @see com.ctrip.sqllin.dsl.DatabaseScope.ALERT_ADD_COLUMN + * @see com.ctrip.sqllin.dsl.DatabaseScope.ALERT_RENAME_TABLE_TO * @see com.ctrip.sqllin.dsl.DatabaseScope.RENAME_COLUMN * @see com.ctrip.sqllin.dsl.DatabaseScope.DROP_COLUMN * @author Yuang Qiao */ -internal object Alter : Operation { +internal object Alert : Operation { override val sqlStr: String - get() = "ALTER TABLE " + get() = "ALERT TABLE " private const val ADD_COLUMN = " ADD COLUMN " private const val RENAME_TABLE = " RENAME TO " diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/statement/OtherStatement.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/statement/OtherStatement.kt index 4822c357..3c2bdc99 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/statement/OtherStatement.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/statement/OtherStatement.kt @@ -77,7 +77,7 @@ public class InsertStatement internal constructor( } /** - * CREATE, DROP, ALTER statement (final form). + * CREATE, DROP, ALERT statement (final form). * * Represents a complete CREATE TABLE operation. Does not support parameterized queries * since DDL statements use direct SQL execution. diff --git a/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ClauseProcessor.kt b/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ClauseProcessor.kt index 73d62224..d2906bac 100644 --- a/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ClauseProcessor.kt +++ b/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ClauseProcessor.kt @@ -17,7 +17,6 @@ package com.ctrip.sqllin.processor import com.google.devtools.ksp.getClassDeclarationByName -import com.google.devtools.ksp.getVisibility import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor @@ -94,19 +93,6 @@ class ClauseProcessor( if (classDeclaration.annotations.all { !it.annotationType.resolve().isAssignableFrom(serializableType) }) continue // Don't handle the classes that didn't be annotated 'Serializable' - // The generated table object must not be more visible than the entity it is built for, - // otherwise an 'internal' @DBRow class produces a 'public' object that exposes it. - val visibility = classDeclaration.getVisibility() - if (visibility != Visibility.PUBLIC && visibility != Visibility.INTERNAL) { - environment.logger.error( - "The class annotated with '@DBRow' must be 'public' or 'internal', but " + - "'${classDeclaration.simpleName.asString()}' is '${visibility.name.lowercase()}'.", - classDeclaration, - ) - continue - } - val visibilityModifier = if (visibility == Visibility.INTERNAL) "internal " else "" - val foreignKeyParser = ForeignKeyParser() foreignKeyParser.parseGroups(classDeclaration.annotations) @@ -117,31 +103,6 @@ class ClauseProcessor( it.annotationType.resolve().declaration.qualifiedName?.asString() == ANNOTATION_DATABASE_ROW_NAME }?.arguments?.firstOrNull()?.value?.takeIf { (it as? String)?.isNotBlank() == true } ?: className - // Keep exactly the properties the serializer writes, in its order, as the generated accessors look a column - // up by its index in the serializer's descriptor. That leaves out @Transient properties and, because - // kotlinx.serialization only serializes properties backed by a field, computed ones like `val x get() = ...` - val transientName = resolver.getClassDeclarationByName(ANNOTATION_TRANSIENT)!!.asStarProjectedType() - val propertyList = classDeclaration.getAllProperties().filter { property -> - property.hasBackingField && - !property.annotations.any { ksAnnotation -> ksAnnotation.annotationType.resolve().isAssignableFrom(transientName) } - }.toList() - - // Every stored property needs a column. A property of a type no column can hold used to be skipped silently: - // left out of CREATE TABLE while its serializer still wrote and read it, so it only failed at runtime, and as - // the last property it left a trailing comma that made CREATE TABLE itself invalid. Report all of them here. - val unsupportedProperties = propertyList.filter { getClauseElementTypeStr(it) == null } - unsupportedProperties.forEach { property -> - environment.logger.error( - "The property '${property.simpleName.asString()}' of '@DBRow' class '$className' has the type " + - "'${property.type.resolve()}', which no column can hold. Supported types are Byte, Short, Int, Long, " + - "Float, Double and their unsigned variants, Boolean, Char, String, ByteArray, enum classes, and type " + - "aliases of these. To keep the property out of the table, annotate it with @kotlinx.serialization.Transient.", - property, - ) - } - if (unsupportedProperties.isNotEmpty()) - continue - val outputStream = environment.codeGenerator.createNewFile( dependencies = classDeclaration.containingFile?.let { Dependencies(true, it) } ?: Dependencies(true), packageName = packageName, @@ -161,14 +122,12 @@ class ClauseProcessor( writer.write("import com.ctrip.sqllin.dsl.sql.PrimaryKeyInfo\n") writer.write("import com.ctrip.sqllin.dsl.sql.Table\n\n") - // The column properties carry @ColumnNameDslMaker for IntelliJ IDEA's DSL highlighting, a target the compiler - // flags as having no effect on scope control. This code is compiled in the user's module, so keep it quiet. - writer.write("@Suppress(\"DSL_MARKER_APPLIED_TO_WRONG_TARGET\")\n") - writer.write("${visibilityModifier}object $objectName : Table<$className>(\"$tableName\") {\n\n") + writer.write("object $objectName : Table<$className>(\"$tableName\") {\n\n") writer.write(" override fun kSerializer() = $className.serializer()\n\n") writer.write(" inline operator fun invoke(block: $objectName.(table: $objectName) -> R): R = this.block(this)\n\n") + val transientName = resolver.getClassDeclarationByName(ANNOTATION_TRANSIENT)!!.asStarProjectedType() val columnConstraintParser = ColumnConstraintParser(resolver) @@ -178,9 +137,14 @@ class ClauseProcessor( append('(') } + // Filter out @Transient properties and convert to list for indexed iteration + val propertyList = classDeclaration.getAllProperties().filter { classDeclaration -> + !classDeclaration.annotations.any { ksAnnotation -> ksAnnotation.annotationType.resolve().isAssignableFrom(transientName) } + }.toList() + // Process each property to generate column definitions propertyList.forEachIndexed { index, property -> - val clauseElementTypeName = checkNotNull(getClauseElementTypeStr(property)) // Rejected above + val clauseElementTypeName = getClauseElementTypeStr(property) ?: return@forEachIndexed val propertyName = property.simpleName.asString() val elementName = "$className.serializer().descriptor.getElementName($index)" val isNotNull = property.type.resolve().nullability == Nullability.NOT_NULL @@ -204,9 +168,14 @@ class ClauseProcessor( writer.write(" get() = $clauseElementTypeName($elementName, this)\n\n") writer.write(" @ColumnNameDslMaker\n") writer.write(" var SetClause<$className>.$propertyName: ${property.typeName}") - writer.write(if (isNotNull) "\n" else "?\n") + val nullableSymbol = when { + columnConstraintParser.isRowId -> "?\n" + isNotNull -> "\n" + else -> "?\n" + } + writer.write(nullableSymbol) writer.write(" get() = ${getSetClauseGetterValue(property)}\n") - writer.write(" set(value) = ${appendFunction(elementName, property, isNotNull)}\n\n") + writer.write(" set(value) = ${appendFunction(elementName, property)}\n\n") } columnConstraintParser.generateCodeForPrimaryKey(writer, createSQLBuilder) @@ -341,30 +310,27 @@ class ClauseProcessor( * Generates the appropriate append function call for SetClause setters. * Supports typealiases by resolving them to their underlying types. * - * For enum types, converts the enum value to its ordinal before appending, with a safe call only - * when the enum is nullable. + * For enum types, converts the enum value to its ordinal before appending. + * Handles nullable enums with safe-call operator. * * @param elementName The serialized element name * @param property The property declaration - * @param isNotNull Whether the setter's `value` is non-null, as for the SetClause property it belongs to * @return The append function call string, or null if unsupported type */ - private fun appendFunction(elementName: String, property: KSPropertyDeclaration, isNotNull: Boolean): String? { - // A safe call on a non-null value is reported as unnecessary, in the module compiling the generated code - val appendEnum = "appendAny($elementName, value${if (isNotNull) "" else "?"}.ordinal)" - return when (val declaration = property.type.resolve().declaration) { - is KSTypeAlias -> { - val realDeclaration = declaration.type.resolve().declaration - appendFunctionByTypeName(elementName, realDeclaration.typeName) ?: kotlin.run { - if (realDeclaration is KSClassDeclaration && realDeclaration.classKind == ClassKind.ENUM_CLASS) - appendEnum - else - null - } + private fun appendFunction(elementName: String, property: KSPropertyDeclaration): String? = when ( + val declaration = property.type.resolve().declaration + ) { + is KSTypeAlias -> { + val realDeclaration = declaration.type.resolve().declaration + appendFunctionByTypeName(elementName, realDeclaration.typeName) ?: kotlin.run { + if (realDeclaration is KSClassDeclaration && realDeclaration.classKind == ClassKind.ENUM_CLASS) + "appendAny($elementName, value?.ordinal)" + else + null } - is KSClassDeclaration if declaration.classKind == ClassKind.ENUM_CLASS -> appendEnum - else -> appendFunctionByTypeName(elementName, declaration.typeName) } + is KSClassDeclaration if declaration.classKind == ClassKind.ENUM_CLASS -> "appendAny($elementName, value?.ordinal)" + else -> appendFunctionByTypeName(elementName, declaration.typeName) } /** diff --git a/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ColumnConstraintParser.kt b/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ColumnConstraintParser.kt index db37295c..854afadc 100644 --- a/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ColumnConstraintParser.kt +++ b/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ColumnConstraintParser.kt @@ -65,13 +65,11 @@ import java.io.Writer * * ### Validation Rules * - Cannot use both [@PrimaryKey] and [@CompositePrimaryKey] on the same property - * - A [@PrimaryKey] may be nullable only when it is a `Long`: a `Long?` key is left for the database - * to assign, while a non-null `Long` or a key of any other type is supplied by the caller + * - Primary key properties must be nullable (SQLite rowid aliasing requirement) * - Only one [@PrimaryKey] annotation allowed per table - * - AUTOINCREMENT requires a `Long?` key, the only kind of key the database assigns + * - AUTOINCREMENT requires Long type (mapped to INTEGER in SQLite) * - [@CollateNoCase] can only be applied to String or Char properties * - [@CompositePrimaryKey] properties must be non-nullable - * - [@CompositePrimaryKey] needs at least two properties; a single-column key uses [@PrimaryKey] * * @param resolver KSP resolver for looking up annotation types * @@ -94,9 +92,7 @@ class ColumnConstraintParser(resolver: Resolver) { const val PROMPT_CANT_ADD_BOTH_ANNOTATION = "You can't add both @PrimaryKey and @CompositePrimaryKey to the same property." const val PROMPT_PRIMARY_KEY_MUST_NOT_NULL = "The primary key must be not-null." - const val PROMPT_NULLABLE_PRIMARY_KEY_MUST_BE_LONG = "Only a primary key of type Long can be nullable, which leaves its value for the database to assign. A primary key of any other type is supplied by the caller and must be not-null." - const val PROMPT_AUTO_INCREMENT_REQUIRES_NULLABLE_LONG = """The parameter "autoIncrement = true" in annotation PrimaryKey requires the primary key to be a nullable Long (Long?), the only kind of key whose value the database assigns.""" - const val PROMPT_COMPOSITE_PRIMARY_KEY_SINGLE_COLUMN = "A composite primary key needs at least two columns. Use @PrimaryKey for a single-column primary key, such as `@PrimaryKey val id: Long` for a numeric key you supply yourself." + const val PROMPT_PRIMARY_KEY_TYPE = """The primary key's type must be Long when you set the the parameter "isAutoincrement = true" in annotation PrimaryKey.""" const val PROMPT_PRIMARY_KEY_USE_COUNT = "You only could use PrimaryKey to annotate one property in a class." const val PROMPT_NO_CASE_MUST_FOR_TEXT = "You only could add annotation @CollateNoCase for a String or Char typed property." } @@ -109,7 +105,8 @@ class ColumnConstraintParser(resolver: Resolver) { // Primary key tracking for metadata generation private var primaryKeyName: String? = null private var isAutomaticIncrement = false - private var isGeneratedByDatabase = false + var isRowId = false + private set private val compositePrimaryKeys = ArrayList() private var isContainsPrimaryKey = false @@ -133,24 +130,16 @@ class ColumnConstraintParser(resolver: Resolver) { * * #### Primary Key * ```kotlin - * @PrimaryKey(autoIncrement = true) - * val id: Long? // assigned by the database + * @PrimaryKey(isAutoincrement = true) + * val id: Long? * // Generated: id INTEGER PRIMARY KEY AUTOINCREMENT - * - * @PrimaryKey - * val id: Long // supplied by the caller, still a rowid alias - * // Generated: id INTEGER PRIMARY KEY - * - * @PrimaryKey - * val sku: String // supplied by the caller - * // Generated: sku TEXT PRIMARY KEY NOT NULL * ``` * * #### Composite Primary Key * ```kotlin * @CompositePrimaryKey * val userId: Long - * // Column: userId BIGINT NOT NULL + * // Column: userId BIGINT * // Later appended: ,PRIMARY KEY(userId,productId) * ``` * @@ -174,7 +163,7 @@ class ColumnConstraintParser(resolver: Resolver) { * 1. Determine SQLite type via [getSQLiteType] * 2. Apply PRIMARY KEY constraint if [@PrimaryKey] present * 3. Collect [@CompositePrimaryKey] columns for table-level constraint - * 4. Apply NOT NULL to every non-nullable column except a rowid alias, primary key columns included + * 4. Apply NOT NULL for non-nullable, non-PK columns * 5. Apply COLLATE NOCASE if [@CollateNoCase] present * 6. Apply UNIQUE if [@Unique] present * 7. Collect [@CompositeUnique] groups for table-level constraints @@ -184,7 +173,7 @@ class ColumnConstraintParser(resolver: Resolver) { * - Sets [primaryKeyName] for single-column primary keys * - Adds to [compositePrimaryKeys] for composite primary keys * - Populates [compositeUniqueColumns] for composite unique constraints - * - Updates [isAutomaticIncrement] and [isGeneratedByDatabase] flags + * - Updates [isAutomaticIncrement] and [isRowId] flags * * @param createSQLBuilder StringBuilder to append column definition and constraints to * @param property The property declaration to process @@ -211,41 +200,33 @@ class ColumnConstraintParser(resolver: Resolver) { val type = getSQLiteType(property, isPrimaryKey) append(type) - // Only a Long @PrimaryKey becomes `INTEGER PRIMARY KEY`, an alias of SQLite's rowid - val isRowIdAlias = isPrimaryKey && type == " INTEGER" - // Handle @PrimaryKey annotation if (isPrimaryKey) { check(!annotationKSType.any { it.isAssignableFrom(compositePrimaryKeyName) }) { PROMPT_CANT_ADD_BOTH_ANNOTATION } + check(!isNotNull) { PROMPT_PRIMARY_KEY_MUST_NOT_NULL } check(!isContainsPrimaryKey) { PROMPT_PRIMARY_KEY_USE_COUNT } isContainsPrimaryKey = true primaryKeyName = propertyName - // Only a rowid alias gets its value assigned by the database. Declaring that key nullable is - // what asks the database to assign it; any other key is supplied by the caller, so it can't be nullable. - check(isNotNull || isRowIdAlias) { PROMPT_NULLABLE_PRIMARY_KEY_MUST_BE_LONG } - isGeneratedByDatabase = isRowIdAlias && !isNotNull - append(" PRIMARY KEY") isAutomaticIncrement = property.annotations.find { it.annotationType.resolve().declaration.qualifiedName?.asString() == ANNOTATION_PRIMARY_KEY }?.arguments?.firstOrNull()?.value as? Boolean ?: false + val isLong = type == " INTEGER" || type == " BIGINT" if (isAutomaticIncrement) { - check(isGeneratedByDatabase) { PROMPT_AUTO_INCREMENT_REQUIRES_NULLABLE_LONG } + check(isLong) { PROMPT_PRIMARY_KEY_TYPE } append(" AUTOINCREMENT") } + isRowId = isLong } else if (annotationKSType.any { it.isAssignableFrom(compositePrimaryKeyName) }) { // Handle @CompositePrimaryKey - collect for table-level constraint check(isNotNull) { PROMPT_PRIMARY_KEY_MUST_NOT_NULL } compositePrimaryKeys.add(propertyName) - } - - // A rowid alias is the only column SQLite itself keeps from being NULL. On a rowid table, PRIMARY KEY - // doesn't imply NOT NULL for any other column, a single key or a part of a composite one alike, so every - // other non-null column spells it out. - if (isNotNull && !isRowIdAlias) + } else if (isNotNull) { + // Add NOT NULL constraint for non-nullable, non-PK columns append(" NOT NULL") + } // Handle @CollateNoCase annotation - must be on text columns if (annotationKSType.any { it.isAssignableFrom(noCaseAnnotationName) }) { @@ -305,7 +286,7 @@ class ColumnConstraintParser(resolver: Resolver) { * override val primaryKeyInfo = PrimaryKeyInfo( * primaryKeyName = "id", * isAutomaticIncrement = true, - * isGeneratedByDatabase = true, + * isRowId = true, * compositePrimaryKeys = null, * ) * ``` @@ -315,7 +296,7 @@ class ColumnConstraintParser(resolver: Resolver) { * override val primaryKeyInfo = PrimaryKeyInfo( * primaryKeyName = null, * isAutomaticIncrement = false, - * isGeneratedByDatabase = false, + * isRowId = false, * compositePrimaryKeys = listOf( * "userId", * "productId", @@ -340,7 +321,7 @@ class ColumnConstraintParser(resolver: Resolver) { * This method reads state accumulated by [parseProperty]: * - [primaryKeyName]: Name of single-column primary key (if any) * - [isAutomaticIncrement]: Whether AUTOINCREMENT is enabled - * - [isGeneratedByDatabase]: Whether the database assigns the primary key's value + * - [isRowId]: Whether the primary key can serve as SQLite rowid alias * - [compositePrimaryKeys]: List of columns in composite primary key * - [compositeUniqueColumns]: Map of group number to columns for UNIQUE constraints * @@ -351,11 +332,6 @@ class ColumnConstraintParser(resolver: Resolver) { * @see com.ctrip.sqllin.dsl.sql.PrimaryKeyInfo */ fun generateCodeForPrimaryKey(writer: Writer, createSQLBuilder: StringBuilder) { - // Standard SQL accepts a one-column `PRIMARY KEY(col)`, but @PrimaryKey already declares that key, and does it - // better for a Long: it maps to INTEGER, a rowid alias, where this path maps a Long to BIGINT. Only known here, - // once every property has been parsed. - check(compositePrimaryKeys.size != 1) { PROMPT_COMPOSITE_PRIMARY_KEY_SINGLE_COLUMN } - // Write the override instance for property `primaryKeyInfo`. with(writer) { if (primaryKeyName == null && compositePrimaryKeys.isEmpty()) { @@ -368,7 +344,7 @@ class ColumnConstraintParser(resolver: Resolver) { write(" primaryKeyName = \"$primaryKeyName\",\n") } write(" isAutomaticIncrement = $isAutomaticIncrement,\n") - write(" isGeneratedByDatabase = $isGeneratedByDatabase,\n") + write(" isRowId = $isRowId,\n") if (compositePrimaryKeys.isEmpty()) { write(" compositePrimaryKeys = null,\n") } else { diff --git a/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ForeignKeyParser.kt b/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ForeignKeyParser.kt index abf8614f..586a1977 100644 --- a/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ForeignKeyParser.kt +++ b/sqllin-processor/src/main/kotlin/com/ctrip/sqllin/processor/ForeignKeyParser.kt @@ -64,7 +64,6 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration * - [@ForeignKeyGroup] groups must have unique group numbers * - [@ForeignKey] annotations must reference a declared [@ForeignKeyGroup] * - Properties with `ON_DELETE_SET_NULL` or `ON_UPDATE_SET_NULL` must be nullable - * - Properties with `ON_DELETE_SET_DEFAULT` or `ON_UPDATE_SET_DEFAULT` must declare [@Default] * - [@References] foreignKeys array cannot be empty * - Foreign key groups must have at least one [@ForeignKey] property * @@ -187,8 +186,6 @@ class ForeignKeyParser { * - Ensures `tableName` is not blank * - Validates that `foreignKeys` array is not empty * - Checks that properties with SET_NULL triggers are nullable - * - Checks that properties with SET_DEFAULT triggers declare a default value, wherever the - * [@Default] annotation appears relative to the foreign key one * - Verifies that referenced [@ForeignKeyGroup] exists * * @param createSQLBuilder StringBuilder to append SQL fragments to (for @References only) @@ -205,7 +202,6 @@ class ForeignKeyParser { isNotNull: Boolean, ) { val columnReferenceEntities = ArrayList() - val setDefaultGroups = ArrayList() var defaultValue = "" annotations.forEach { annotation -> when (annotation.annotationType.resolve().declaration.qualifiedName?.asString()) { @@ -253,9 +249,6 @@ class ForeignKeyParser { if ((triggerEnumName == "ON_DELETE_SET_NULL" || triggerEnumName == "ON_UPDATE_SET_NULL") && isNotNull) { throw IllegalArgumentException("Can't use trigger `ON_DELETE_SET_NULL` or `ON_UPDATE_SET_NULL` on a non-null property in foreign key group `$group`.") } - // Checked once all annotations are read: @Default may come after @ForeignKey - if (triggerEnumName == "ON_DELETE_SET_DEFAULT" || triggerEnumName == "ON_UPDATE_SET_DEFAULT") - setDefaultGroups.add(group) columns.add(propertyName) references.add(reference) } @@ -269,15 +262,8 @@ class ForeignKeyParser { } } - // ON ... SET DEFAULT writes the column's default, which is NULL without @Default. That fails on a non-null - // column, and on a nullable one it is only ON ... SET NULL spelled differently, so a default is required. - val hasDefaultValue = defaultValue.isNotEmpty() - setDefaultGroups.forEach { group -> - if (!hasDefaultValue) - throw IllegalArgumentException("Can't use trigger `ON_DELETE_SET_DEFAULT` or `ON_UPDATE_SET_DEFAULT` on a property without @Default in foreign key group `$group`. Without one the column's default is NULL, which fails on a non-null property and is only `ON_DELETE_SET_NULL` or `ON_UPDATE_SET_NULL` on a nullable one.") - } - with(createSQLBuilder) { + val hasDefaultValue = defaultValue.isNotEmpty() if (hasDefaultValue) { append(" DEFAULT ") append(defaultValue) @@ -301,7 +287,7 @@ class ForeignKeyParser { "ON DELETE SET NULL", "ON UPDATE SET NULL" -> check(!isNotNull) { "Can't use trigger `ON_DELETE_SET_NULL` or `ON_UPDATE_SET_NULL` on a non-null property." } "ON DELETE SET DEFAULT", "ON UPDATE SET DEFAULT" -> - check(hasDefaultValue) { "Can't use trigger `ON_DELETE_SET_DEFAULT` or `ON_UPDATE_SET_DEFAULT` on a property without @Default. Without one the column's default is NULL, which fails on a non-null property and is only `ON_DELETE_SET_NULL` or `ON_UPDATE_SET_NULL` on a nullable one." } + check(isNotNull || hasDefaultValue) { "The column must be nullable or have a default value when using trigger 'ON DELETE SET DEFAULT' or 'ON UPDATE SET DEFAULT'" } } append(' ') append(it.triggerSQL)