diff --git a/CHANGELOG.md b/CHANGELOG.md index 52a3265..037a5f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ * Update `Kotlin`'s version to `2.2.20` * Remove the Desuger configuration +### sqllin-dsl + +* Optimized performance for SQL assembly +* New experimental API: `DatabaseScope#CREATE` +* New experimental API: `DatabaseScope#DROP` +* New experimental API: `DatabaseSceop#ALERT` + ### sqllin-driver * Update the `sqlite-jdbc`'s version to `3.50.3.0` diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..be8ed76 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,15 @@ +# SQLlin Roadmap + +## High Priority + +* Support the key word REFERENCE +* Support JOIN sub-query +* Fix the bug of storing ByteArray in DSL + +## Medium Priority + +* Support WASM platform + +## Low Priority + +* Support store instances of kotlinx.datetime \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 788c736..690ba06 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -VERSION=1.4.4 +VERSION=2.0.0 GROUP_ID=com.ctrip.kotlin #Maven Publishing Information diff --git a/sample/src/commonMain/kotlin/com/ctrip/sqllin/sample/Sample.kt b/sample/src/commonMain/kotlin/com/ctrip/sqllin/sample/Sample.kt index 5371001..bbf9be8 100644 --- a/sample/src/commonMain/kotlin/com/ctrip/sqllin/sample/Sample.kt +++ b/sample/src/commonMain/kotlin/com/ctrip/sqllin/sample/Sample.kt @@ -16,9 +16,10 @@ package com.ctrip.sqllin.sample -import com.ctrip.sqllin.driver.DatabaseConfiguration +import com.ctrip.sqllin.dsl.DSLDBConfiguration import com.ctrip.sqllin.dsl.Database import com.ctrip.sqllin.dsl.annotation.DBRow +import com.ctrip.sqllin.dsl.annotation.PrimaryKey import com.ctrip.sqllin.dsl.sql.clause.* import com.ctrip.sqllin.dsl.sql.clause.OrderByWay.DESC import com.ctrip.sqllin.dsl.sql.statement.SelectStatement @@ -30,34 +31,33 @@ import kotlinx.serialization.Serializable /** * Sample - * @author yaqiao + * @author Yuang Qiao */ object Sample { private val db by lazy { Database( - DatabaseConfiguration( + DSLDBConfiguration( name = "Person.db", path = databasePath, version = 1, create = { - // You must write SQL to String when the database is created or upgraded - it.execSQL("CREATE TABLE person (id INTEGER PRIMARY KEY AUTOINCREMENT, age INTEGER, name TEXT);") - it.execSQL("CREATE TABLE transcript (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, math INTEGER NOT NULL, english INTEGER NOT NULL);") + PersonTable { + CREATE() + } + this CREATE TranscriptTable }, - upgrade = { _, _, _ -> - // You must write SQL to String when the database is created or upgraded - } + upgrade = { _, _ -> } ), enableSimpleSQLLog = true, ) } fun sample() { - val tom = Person(age = 4, name = "Tom") - val jerry = Person(age = 3, name = "Jerry") - val jack = Person(age = 8, name = "Jack") + val tom = Person(id = 0, age = 4, name = "Tom") + val jerry = Person(id = 1, age = 3, name = "Jerry") + val jack = Person(id = 2, age = 8, name = "Jack") lateinit var selectStatement: SelectStatement db { @@ -113,6 +113,7 @@ object Sample { @DBRow("person") @Serializable data class Person( + @PrimaryKey val id: Long?, val age: Int?, val name: String?, ) @@ -120,6 +121,7 @@ data class Person( @DBRow("transcript") @Serializable data class Transcript( + @PrimaryKey val id: Long?, val name: String?, val math: Int, val english: Int, diff --git a/sqllin-driver/src/commonMain/kotlin/com/ctrip/sqllin/driver/CommonCursor.kt b/sqllin-driver/src/commonMain/kotlin/com/ctrip/sqllin/driver/CommonCursor.kt index 7e01a65..4c743a4 100644 --- a/sqllin-driver/src/commonMain/kotlin/com/ctrip/sqllin/driver/CommonCursor.kt +++ b/sqllin-driver/src/commonMain/kotlin/com/ctrip/sqllin/driver/CommonCursor.kt @@ -18,10 +18,9 @@ package com.ctrip.sqllin.driver /** * SQLite Cursor common abstract - * @author yaqiao + * @author Yuang Qiao */ -@OptIn(ExperimentalStdlibApi::class) public interface CommonCursor : AutoCloseable { public fun getInt(columnIndex: Int): Int diff --git a/sqllin-dsl-test/src/commonTest/kotlin/com/ctrip/sqllin/dsl/test/CommonBasicTest.kt b/sqllin-dsl-test/src/commonTest/kotlin/com/ctrip/sqllin/dsl/test/CommonBasicTest.kt index 76f42b0..94e0d5a 100644 --- a/sqllin-dsl-test/src/commonTest/kotlin/com/ctrip/sqllin/dsl/test/CommonBasicTest.kt +++ b/sqllin-dsl-test/src/commonTest/kotlin/com/ctrip/sqllin/dsl/test/CommonBasicTest.kt @@ -16,8 +16,8 @@ package com.ctrip.sqllin.dsl.test -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.sql.X import com.ctrip.sqllin.dsl.sql.clause.* @@ -41,9 +41,6 @@ class CommonBasicTest(private val path: DatabasePath) { companion object { const val DATABASE_NAME = "BookStore.db" - const val SQL_CREATE_BOOK = "create table book (id integer primary key autoincrement, name text, author text, pages integer, price real)" - const val SQL_CREATE_CATEGORY = "create table category (id integer primary key autoincrement, name text, code integer)" - const val SQL_CREATE_NULL_TESTER = "create table NullTester (id integer primary key autoincrement, paramInt integer, paramString text, paramDouble real)" } private inline fun Database.databaseAutoClose(block: (Database) -> Unit) = try { @@ -429,12 +426,12 @@ class CommonBasicTest(private val path: DatabasePath) { } fun testNullValue() { - val config = DatabaseConfiguration( + val config = DSLDBConfiguration( name = DATABASE_NAME, path = path, version = 1, create = { - it.execSQL(SQL_CREATE_NULL_TESTER) + CREATE(NullTesterTable) } ) Database(config, true).databaseAutoClose { database -> @@ -493,14 +490,14 @@ class CommonBasicTest(private val path: DatabasePath) { } } - private fun getDefaultDBConfig(): DatabaseConfiguration = - DatabaseConfiguration( + private fun getDefaultDBConfig(): DSLDBConfiguration = + DSLDBConfiguration ( name = DATABASE_NAME, path = path, version = 1, create = { - it.execSQL(SQL_CREATE_BOOK) - it.execSQL(SQL_CREATE_CATEGORY) + CREATE(BookTable) + CREATE(CategoryTable) } ) } \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DSLDBConfiguration.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DSLDBConfiguration.kt new file mode 100644 index 0000000..d588df7 --- /dev/null +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DSLDBConfiguration.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2025 Ctrip.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ctrip.sqllin.dsl + +import com.ctrip.sqllin.driver.DatabaseConfiguration +import com.ctrip.sqllin.driver.DatabasePath +import com.ctrip.sqllin.driver.JournalMode +import com.ctrip.sqllin.driver.SynchronousMode + +/** + * DSL database configuration + * @author Yuang Qiao + */ + +public data class DSLDBConfiguration( + val name: String, + val path: DatabasePath, + val version: Int, + val isReadOnly: Boolean = false, + val inMemory: Boolean = false, + val journalMode: JournalMode = JournalMode.WAL, + val synchronousMode: SynchronousMode = SynchronousMode.NORMAL, + val busyTimeout: Int = 5000, + val lookasideSlotSize: Int = 0, + val lookasideSlotCount: Int = 0, + val create: DatabaseScope.() -> Unit = {}, + val upgrade: DatabaseScope.(oldVersion: Int, newVersion: Int) -> Unit = { _, _ -> }, +) { + internal infix fun convertToDatabaseConfiguration(enableSimpleSQLLog: Boolean): DatabaseConfiguration = DatabaseConfiguration( + name, + path, + version, + isReadOnly, + inMemory, + journalMode, + synchronousMode, + busyTimeout, + lookasideSlotSize, + lookasideSlotCount, + create = { + val database = Database(it, enableSimpleSQLLog) + database { + create() + } + }, + upgrade = { databaseConnection, oldVersion, newVersion -> + val database = Database(databaseConnection, enableSimpleSQLLog) + database { + upgrade(oldVersion, newVersion) + } + } + ) +} diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/Database.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/Database.kt index 2a1c4bc..97f2dde 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/Database.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/Database.kt @@ -16,38 +16,20 @@ package com.ctrip.sqllin.dsl -import com.ctrip.sqllin.driver.DatabaseConfiguration -import com.ctrip.sqllin.driver.DatabasePath -import com.ctrip.sqllin.driver.openDatabase +import com.ctrip.sqllin.driver.DatabaseConnection import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock /** * Database object - * @author yaqiao + * @author Yuang Qiao */ -public class Database( - configuration: DatabaseConfiguration, +public class Database internal constructor( + private val databaseConnection: DatabaseConnection, private val enableSimpleSQLLog: Boolean = false, ) { - public constructor( - name: String, - path: DatabasePath, - version: Int, - enableSimpleSQLLog: Boolean = false, - ) : this( - DatabaseConfiguration( - name = name, - path = path, - version = version, - ), - enableSimpleSQLLog, - ) - - private val databaseConnection = openDatabase(configuration) - /** * Close the database connection. */ diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DatabaseCreators.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DatabaseCreators.kt new file mode 100644 index 0000000..19e71d8 --- /dev/null +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/DatabaseCreators.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2025 Ctrip.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ctrip.sqllin.dsl + +import com.ctrip.sqllin.driver.DatabaseConfiguration +import com.ctrip.sqllin.driver.DatabasePath +import com.ctrip.sqllin.driver.openDatabase + +/** + * Factory functions for Database + * @author Yuang Qiao + */ + +public fun Database( + configuration: DatabaseConfiguration, + enableSimpleSQLLog: Boolean = false, +): Database = Database(openDatabase(configuration), enableSimpleSQLLog) + +public fun Database( + name: String, + path: DatabasePath, + version: Int, + enableSimpleSQLLog: Boolean = false, +): Database = Database( + DatabaseConfiguration( + name = name, + path = path, + version = version, + ), + enableSimpleSQLLog +) + +public fun Database( + dsldbConfiguration: DSLDBConfiguration, + enableSimpleSQLLog: Boolean = false, +): Database = Database( + configuration = dsldbConfiguration convertToDatabaseConfiguration enableSimpleSQLLog, + enableSimpleSQLLog = enableSimpleSQLLog, +) 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 78f6691..d112108 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 @@ -17,10 +17,12 @@ package com.ctrip.sqllin.dsl import com.ctrip.sqllin.driver.DatabaseConnection +import com.ctrip.sqllin.dsl.annotation.AdvancedInsertAPI 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.Create import com.ctrip.sqllin.dsl.sql.operation.Delete import com.ctrip.sqllin.dsl.sql.operation.Insert import com.ctrip.sqllin.dsl.sql.operation.Select @@ -30,10 +32,11 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.modules.EmptySerializersModule import kotlinx.serialization.serializer import kotlin.concurrent.Volatile +import kotlin.jvm.JvmName /** * The database scope, it's used to restrict the scope that write DSL SQL statements - * @author yaqiao + * @author Yuang Qiao */ @Suppress("UNCHECKED_CAST") @@ -109,6 +112,19 @@ public class DatabaseScope internal constructor( public infix fun Table.INSERT(entity: T): Unit = INSERT(listOf(entity)) + + @AdvancedInsertAPI + @StatementDslMaker + public infix fun Table.INSERT_WITH_ID(entities: Iterable) { + val statement = Insert.insert(this, databaseConnection, entities, true) + addStatement(statement) + } + + @AdvancedInsertAPI + @StatementDslMaker + public infix fun Table.INSERT_WITH_ID(entity: T): Unit = + INSERT_WITH_ID(listOf(entity)) + /** * Update. */ @@ -155,7 +171,6 @@ public class DatabaseScope internal constructor( public inline infix fun Table.SELECT_DISTINCT(x: X): FinalSelectStatement = select(kSerializer(), true) - @StatementDslMaker public fun Table.select(serializer: KSerializer, isDistinct: Boolean): FinalSelectStatement { val container = getSelectStatementGroup() val statement = Select.select(this, isDistinct, serializer, databaseConnection, container) @@ -323,4 +338,17 @@ public class DatabaseScope internal constructor( addSelectStatement(statement) return statement } + + /** + * CREATE + */ + @StatementDslMaker + public infix fun CREATE(table: Table) { + val statement = Create.create(table, databaseConnection) + addStatement(statement) + } + + @StatementDslMaker + @JvmName("create") + public fun Table.CREATE(): Unit = CREATE(this) } \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DBRow.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DBRow.kt index e18f5d2..5e0c631 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DBRow.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DBRow.kt @@ -18,8 +18,9 @@ package com.ctrip.sqllin.dsl.annotation /** * Annotation for where property - * @author yaqiao + * @author Yuang Qiao */ @Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) public annotation class DBRow(val tableName: String = "") \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DslMaker.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DslMaker.kt index 900fe6c..dee2613 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DslMaker.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/DslMaker.kt @@ -18,17 +18,25 @@ package com.ctrip.sqllin.dsl.annotation /** * Dsl maker annotations - * @author yaqiao + * @author Yuang Qiao */ @DslMarker +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.BINARY) public annotation class StatementDslMaker @DslMarker +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) public annotation class KeyWordDslMaker @DslMarker +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.BINARY) public annotation class FunctionDslMaker @DslMarker +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) public annotation class ColumnNameDslMaker \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/PrimaryKey.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/PrimaryKey.kt new file mode 100644 index 0000000..43db6b6 --- /dev/null +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/annotation/PrimaryKey.kt @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2025 Ctrip.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ctrip.sqllin.dsl.annotation + +/** + * Mark the primary key(s) for a table + * @author Yuang Qiao + */ + +/** + * Marks a property as the primary key for a table within a class annotated with [DBRow]. + * + * This annotation defines how a data model maps to the primary key of a database table. + * Within a given `@DBRow` class, **only one** property can be marked with this annotation. + * To define a primary key that consists of multiple columns, use the [CompositePrimaryKey] annotation instead. + * Additionally, if a property in the class is marked with [PrimaryKey], the class cannot also use the [CompositePrimaryKey] annotation. + * + * ### Type and Nullability Rules + * The behavior of this annotation differs based on the type of property it annotates. + * The following rules must be followed: + * + * - **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`). + * + * - **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. + * + * @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 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 isAutoincrement: Boolean = false) + +/** + * Marks a property as a part of a composite primary key for the table. + * + * This annotation is used to define a primary key that consists of multiple columns. + * Unlike [PrimaryKey], you can apply this annotation to **multiple properties** within the + * same [DBRow] class. The combination of all properties marked with [CompositePrimaryKey] + * will form the table's composite primary key. + * + * ### Important Rules + * - 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. + * + * @see DBRow + * @see PrimaryKey + * + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +public annotation class CompositePrimaryKey + +/** + * A marker annotation for DSL functions that are considered advanced and require explicit opt-in. + * + * This library contains certain powerful APIs that are intended for special use cases and can + * lead to unexpected behavior or data integrity issues if used improperly. This annotation + * is used to protect such APIs and ensure they are used intentionally. + * + * Any function marked with [AdvancedInsertAPI] is part of this advanced feature set. To call + * such a function, you must explicitly acknowledge its use by annotating your own calling + * function or class with `@OptIn(AdvancedInsertAPI::class)`. This acts as a contract, + * confirming that you understand the implications of the API. + * + * A primary example is an API that allows for the manual insertion of a record with a + * specific primary key ID (e.g., `INSERT_WITH_ID`), which bypasses the database's automatic + * ID generation. This is useful for data migration but is unsafe for regular inserts. + * + * @see OptIn + * @see RequiresOptIn + */ +@RequiresOptIn( + message = "This is a special-purpose API for inserting a record with a predefined value for its `INTEGER PRIMARY KEY` (the rowid-backed key). " + + "It is intended for use cases like data migration or testing. " + + "For all standard operations where the database should generate the ID, you must use the `INSERT` API instead.", +) +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.BINARY) +public annotation class AdvancedInsertAPI \ No newline at end of file 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 new file mode 100644 index 0000000..ac44557 --- /dev/null +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/PrimaryKeyInfo.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 Ctrip.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ctrip.sqllin.dsl.sql + +/** + * Describe the information of primary key(s) + * @author Yuang Qiao + */ + +public class PrimaryKeyInfo( + internal val primaryKeyName: String?, + internal val isAutomaticIncrement: 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 4519ece..6b7b38a 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 @@ -20,11 +20,13 @@ import kotlinx.serialization.KSerializer /** * SQL table - * @author yaqiao + * @author Yuang Qiao */ public abstract class Table( internal val tableName: String, ) { public abstract fun kSerializer(): KSerializer + + public abstract val primaryKeyInfo: PrimaryKeyInfo? } \ No newline at end of file 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 aa3a7ac..4fd7475 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 @@ -14,40 +14,83 @@ * limitations under the License. */ -@file:OptIn(ExperimentalSerializationApi::class) - package com.ctrip.sqllin.dsl.sql.compiler -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.SerializationStrategy +import com.ctrip.sqllin.dsl.sql.Table import kotlinx.serialization.descriptors.SerialDescriptor /** * Some function that used for encode entities to SQL - * @author yaqiao + * @author Yuang Qiao */ -internal fun encodeEntities2InsertValues(serializer: SerializationStrategy, values: Iterable, parameters: MutableList): String = buildString { +internal fun encodeEntities2InsertValues( + table: Table, + builder: StringBuilder, + values: Iterable, + parameters: MutableList, + isInsertWithId: Boolean, +) = with(builder) { + val isInsertId = table.primaryKeyInfo?.run { + !isRowId || isInsertWithId + } ?: true + val serializer = table.kSerializer() append('(') - appendDBColumnName(serializer.descriptor) + val primaryKeyIndex = appendDBColumnName(serializer.descriptor, table.primaryKeyInfo?.primaryKeyName, isInsertId) + if (primaryKeyIndex >= 0) + parameters.removeAt(primaryKeyIndex) append(')') append(" values ") val iterator = values.iterator() - do { + fun appendNext() { val value = iterator.next() val encoder = InsertValuesEncoder(parameters) encoder.encodeSerializableValue(serializer, value) append(encoder.valuesSQL) - val hasNext = iterator.hasNext() - if (hasNext) append(',') - } while (hasNext) + } + if (iterator.hasNext()) { + appendNext() + } else { + return@with + } + while (iterator.hasNext()) { + append(',') + appendNext() + } +} + +internal fun StringBuilder.appendDBColumnName( + descriptor: SerialDescriptor, + primaryKeyName: String?, + isInsertId: Boolean, +): Int = if (isInsertId) { + appendDBColumnName(descriptor) + -1 +} else { + var index = -1 + if (descriptor.elementsCount > 0) { + val elementName = descriptor.getElementName(0) + if (elementName != primaryKeyName) + append(elementName) + else + index = 0 + } + for (i in 1 ..< descriptor.elementsCount) { + append(',') + val elementName = descriptor.getElementName(i) + if (elementName != primaryKeyName) + append(elementName) + else + index = i + } + index } -@OptIn(ExperimentalSerializationApi::class) internal infix fun StringBuilder.appendDBColumnName(descriptor: SerialDescriptor) { - for (i in 0 ..< descriptor.elementsCount) { - if (i != 0) - append(',') + if (descriptor.elementsCount > 0) + append(descriptor.getElementName(0)) + for (i in 1 ..< descriptor.elementsCount) { + append(',') append(descriptor.getElementName(i)) } } \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Create.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Create.kt new file mode 100644 index 0000000..12a6920 --- /dev/null +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Create.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2025 Ctrip.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ctrip.sqllin.dsl.sql.operation + +import com.ctrip.sqllin.driver.DatabaseConnection +import com.ctrip.sqllin.dsl.sql.Table +import com.ctrip.sqllin.dsl.sql.statement.CreateStatement + +/** + * SQL create + * @author Yuang Qiao + */ + +internal object Create : Operation { + + override val sqlStr: String + get() = "CREATE TABLE " + + fun create(table: Table, connection: DatabaseConnection): CreateStatement = + CreateStatement(buildSQL(table), connection) + + private fun buildSQL(table: Table): String = buildString { + append(sqlStr) + append(table.tableName) + append(" (") + val tableDescriptor = table.kSerializer().descriptor + val lastIndex = tableDescriptor.elementsCount - 1 + for (elementIndex in 0 .. lastIndex) { + val elementName = tableDescriptor.getElementName(elementIndex) + val descriptor = tableDescriptor.getElementDescriptor(elementIndex) + val type = with(descriptor.serialName) { + when { + startsWith(FullNameCache.BYTE) || startsWith(FullNameCache.UBYTE) -> " TINYINT" + startsWith(FullNameCache.SHORT) || startsWith(FullNameCache.USHORT) -> " SMALLINT" + startsWith(FullNameCache.INT) || startsWith(FullNameCache.UINT) -> " INT" + startsWith(FullNameCache.LONG) -> if (elementName == table.primaryKeyInfo?.primaryKeyName) " INTEGER" else " BIGINT" + startsWith(FullNameCache.ULONG) -> " BIGINT" + startsWith(FullNameCache.FLOAT) -> " FLOAT" + startsWith(FullNameCache.DOUBLE) -> " DOUBLE" + startsWith(FullNameCache.BOOLEAN) -> " BOOLEAN" + startsWith(FullNameCache.CHAR) -> " CHAR(1)" + startsWith(FullNameCache.STRING) -> " TEXT" + startsWith(FullNameCache.BYTE_ARRAY) -> " BLOB" + else -> throw IllegalStateException("Hasn't support the type '$this' yet") + } + } + val isNullable = descriptor.isNullable + append(elementName) + append(type) + if (elementName == table.primaryKeyInfo?.primaryKeyName) { + if (table.primaryKeyInfo?.isAutomaticIncrement == true && type == FullNameCache.LONG) + append(" PRIMARY KEY AUTOINCREMENT") + else + append(" PRIMARY KEY") + } else if (isNullable) { + if (elementIndex < lastIndex) + append(',') + + } else { + if (elementIndex < lastIndex) + append(" NOT NULL,") + else + append(" NOT NULL") + } + } + table.primaryKeyInfo?.compositePrimaryKeys?.joinTo( + buffer = this, + separator = ",", + prefix = ", PRIMARY KEY ", + postfix = ")" + ) + append(')') + } +} \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/FullNameCache.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/FullNameCache.kt new file mode 100644 index 0000000..7a65088 --- /dev/null +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/FullNameCache.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2025 Ctrip.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.ctrip.sqllin.dsl.sql.operation + +/** + * Cache for primitive types' qualified name + * @author Yuang Qiao + */ + +internal object FullNameCache { + + val BYTE = Byte::class.qualifiedName!! + val SHORT = Short::class.qualifiedName!! + val INT = Int::class.qualifiedName!! + val LONG = Long::class.qualifiedName!! + + val UBYTE = UByte::class.qualifiedName!! + val USHORT = UShort::class.qualifiedName!! + val UINT = UInt::class.qualifiedName!! + val ULONG = ULong::class.qualifiedName!! + + val FLOAT = Float::class.qualifiedName!! + val DOUBLE = Double::class.qualifiedName!! + + val BOOLEAN = Boolean::class.qualifiedName!! + + val CHAR = Char::class.qualifiedName!! + val STRING = String::class.qualifiedName!! + + val BYTE_ARRAY = ByteArray::class.qualifiedName!! +} \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Insert.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Insert.kt index e2bc531..f2377a4 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Insert.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/operation/Insert.kt @@ -24,7 +24,7 @@ import com.ctrip.sqllin.dsl.sql.compiler.encodeEntities2InsertValues /** * SQL insert - * @author yaqiao + * @author Yuang Qiao */ internal object Insert : Operation { @@ -32,13 +32,13 @@ internal object Insert : Operation { override val sqlStr: String get() = "INSERT INTO " - fun insert(table: Table, connection: DatabaseConnection, entities: Iterable): SingleStatement { + fun insert(table: Table, connection: DatabaseConnection, entities: Iterable, isInsertWithId: Boolean = false): SingleStatement { val parameters = ArrayList() val sql = buildString { append(sqlStr) append(table.tableName) append(' ') - append(encodeEntities2InsertValues(table.kSerializer(), entities, parameters)) + encodeEntities2InsertValues(table, this,entities, parameters, isInsertWithId) } return InsertStatement(sql, connection, parameters.takeIf { it.isNotEmpty() }) } 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 6e94365..abdc811 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 @@ -20,7 +20,7 @@ import com.ctrip.sqllin.driver.DatabaseConnection /** * Update statement without 'WHERE' clause, that could execute or link 'WHERE' clause - * @author yaqiao + * @author Yuang Qiao */ public class UpdateStatementWithoutWhereClause internal constructor( @@ -47,3 +47,11 @@ public class InsertStatement internal constructor( ) : SingleStatement(sqlStr) { public override fun execute(): Unit = connection.executeInsert(sqlStr, params) } + +public class CreateStatement internal constructor( + sqlStr: String, + private val connection: DatabaseConnection, +) : SingleStatement(sqlStr) { + override fun execute(): Unit = connection.execSQL(sqlStr, params) + override val parameters: MutableList? = null +} \ No newline at end of file diff --git a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/statement/SelectStatement.kt b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/statement/SelectStatement.kt index 431182a..6d4a133 100644 --- a/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/statement/SelectStatement.kt +++ b/sqllin-dsl/src/commonMain/kotlin/com/ctrip/sqllin/dsl/sql/statement/SelectStatement.kt @@ -26,7 +26,7 @@ import kotlin.concurrent.Volatile /** * Select statement - * @author yaqiao + * @author Yuang Qiao */ public sealed class SelectStatement( @@ -47,7 +47,6 @@ public sealed class SelectStatement( cursor = connection.query(sqlStr, params) } - @OptIn(ExperimentalStdlibApi::class) public fun getResults(): List = result ?: cursor?.use { val decoder = QueryDecoder(it) result = buildList { 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 e2811d1..a9ffc0a 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 @@ -27,7 +27,7 @@ import java.io.OutputStreamWriter /** * Generate the clause properties for data classes that present the database entity - * @author yaqiao + * @author Yuang Qiao */ class ClauseProcessor( @@ -36,8 +36,15 @@ class ClauseProcessor( private companion object { const val ANNOTATION_DATABASE_ROW_NAME = "com.ctrip.sqllin.dsl.annotation.DBRow" + const val ANNOTATION_PRIMARY_KEY = "com.ctrip.sqllin.dsl.annotation.PrimaryKey" + const val ANNOTATION_COMPOSITE_PRIMARY_KEY = "com.ctrip.sqllin.dsl.annotation.CompositePrimaryKey" const val ANNOTATION_SERIALIZABLE = "kotlinx.serialization.Serializable" const val ANNOTATION_TRANSIENT = "kotlinx.serialization.Transient" + + 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_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." } @Suppress("UNCHECKED_CAST") @@ -74,6 +81,7 @@ class ClauseProcessor( writer.write("import com.ctrip.sqllin.dsl.sql.clause.ClauseNumber\n") writer.write("import com.ctrip.sqllin.dsl.sql.clause.ClauseString\n") writer.write("import com.ctrip.sqllin.dsl.sql.clause.SetClause\n") + writer.write("import com.ctrip.sqllin.dsl.sql.PrimaryKeyInfo\n") writer.write("import com.ctrip.sqllin.dsl.sql.Table\n\n") writer.write("object $objectName : Table<$className>(\"$tableName\") {\n\n") @@ -82,12 +90,43 @@ class ClauseProcessor( 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 primaryKeyAnnotationName = resolver.getClassDeclarationByName(ANNOTATION_PRIMARY_KEY)!!.asStarProjectedType() + val compositePrimaryKeyName = resolver.getClassDeclarationByName(ANNOTATION_COMPOSITE_PRIMARY_KEY)!!.asStarProjectedType() + + var primaryKeyName: String? = null + var isAutomaticIncrement = false + var isRowId = false + val compositePrimaryKeys = ArrayList() + var isContainsPrimaryKey = false + classDeclaration.getAllProperties().filter { classDeclaration -> !classDeclaration.annotations.any { ksAnnotation -> ksAnnotation.annotationType.resolve().isAssignableFrom(transientName) } }.forEachIndexed { index, property -> 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 + + // Collect the information of the primary key(s). + val annotations = property.annotations.map { it.annotationType.resolve() } + val isPrimaryKey = annotations.any { it.isAssignableFrom(primaryKeyAnnotationName) } + val isLong = property.typeName == Long::class.qualifiedName + if (isPrimaryKey) { + check(!annotations.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 + isAutomaticIncrement = property.annotations.find { + it.annotationType.resolve().declaration.qualifiedName?.asString() == ANNOTATION_PRIMARY_KEY + }?.arguments?.firstOrNull()?.value as? Boolean ?: false + if (isAutomaticIncrement) + check(isLong) { PROMPT_PRIMARY_KEY_TYPE } + isRowId = isLong + } else if (annotations.any { it.isAssignableFrom(compositePrimaryKeyName) }) { + check(isNotNull) { PROMPT_PRIMARY_KEY_MUST_NOT_NULL } + compositePrimaryKeys.add(propertyName) + } // Write 'SelectClause' code. writer.write(" @ColumnNameDslMaker\n") @@ -96,14 +135,42 @@ class ClauseProcessor( // Write 'SetClause' code. writer.write(" @ColumnNameDslMaker\n") - val isNotNull = property.type.resolve().nullability == Nullability.NOT_NULL writer.write(" var SetClause<$className>.$propertyName: ${property.typeName}") - val nullableSymbol = if (isNotNull) "\n" else "?\n" + val nullableSymbol = when { + 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("}") + + // Write the override instance for property `primaryKeyInfo`. + if (primaryKeyName == null && compositePrimaryKeys.isEmpty()) { + writer.write(" override val primaryKeyInfo = null\n\n") + writer.write("}\n") + return@use + } + writer.write(" override val primaryKeyInfo = PrimaryKeyInfo(\n") + if (primaryKeyName == null) { + writer.write(" primaryKeyName = null,\n") + } else { + writer.write(" primaryKeyName = \"$primaryKeyName\",\n") + } + writer.write(" isAutomaticIncrement = $isAutomaticIncrement,\n") + writer.write(" isRowId = $isRowId,\n") + if (compositePrimaryKeys.isEmpty()) { + writer.write(" compositePrimaryKeys = null,\n") + } else { + writer.write(" compositePrimaryKeys = listOf(\n") + compositePrimaryKeys.forEach { + writer.write(" $it,\n") + } + writer.write(" )\n") + } + writer.write(" )\n\n") + writer.write("}\n") } } return invalidateDBRowClasses