SQLlin Kotlin Multiplatform ORM

repository·main·Indexed 42 days ago

https://github.com/ctripcorp/sqllin

A Kotlin Multiplatform ORM providing a type-safe DSL for SQLite operations. It leverages KSP for code generation and kotlinx.serialization for object mapping. SQLlin supports Common, Android (7.0+), JVM (Java 11+), iOS, macOS, watchOS, tvOS, Linux, and Windows. It consists of three modules: sqllin-dsl for high-level operations, sqllin-processor for annotation processing, and sqllin-driver for low-level multiplatform SQLite APIs.

Tokens
16.3K
Snippets
49
Records
61
Agent score
50%

What's inside SQLlin

  1. Overview of SQLlin

    main

    SQLlin is a Kotlin Multiplatform ORM library that uses a DSL (Domain Specific Language) and KSP (Kotlin Symbol Processing) to manage SQLite databases. It allows you to write SQL statements directly in Kotlin code, which are verified by the Kotlin compiler.

    Key features:

    • Type-safe SQL: Write queries using Kotlin DSL that are checked at compile time.
    • Direct Object Mapping: Insert and query Kotlin objects directly using kotlinx.serialization for serialization and deserialization.
    • Multiplatform Support: Works on Common, Android (7.0+), JVM (Java 11+), iOS, macOS, watchOS, tvOS, Linux, and Windows.
  2. Connect multiple clauses in a SELECT statement

    main

    SQLlin enforces correct SQL clause ordering. When chaining multiple clauses, you must follow the allowed connection sequence.

    Clause Connection Rules:

    Clause/StatementConnectable
    SELECTWHERE, ORDER BY, LIMIT, GROUP BY
    WHERELIMIT, ORDER BY, GROUP BY
    GROUP BYHAVING, ORDER BY
    HAVINGORDER BY, LIMIT
    ORDER BYLIMIT
    LIMITOFFSET
    OFFSET/

    Example of a complex chained query:

    fun sample() {
        lateinit var selectStatement: SelectStatement<Person>
        database {
            PersonTable { table ->
                 selectStatement = table SELECT WHERE (age LTE 5) GROUP_BY age HAVING (upper(name) EQ "TOM") ORDER_BY (age to DESC) LIMIT 2 OFFSET 1
            }
        }
        selectStatement.getResult().forEach { person ->
            println(person)
        }
    }
  3. Define primary keys using @PrimaryKey and @CompositePrimaryKey

    main

    SQLlin provides annotations to define table primary keys. You cannot mix @PrimaryKey and @CompositePrimaryKey in the same class.

    Single Primary Key (@PrimaryKey)

    • Auto-incrementing Long: The property must be nullable (Long?). This maps to SQLite's INTEGER PRIMARY KEY (alias for rowid). When inserting, passing null allows SQLite to auto-generate the ID.
    • Other types (String, Int, etc.): The property must be non-nullable. You must provide a unique value upon insertion.
    • autoIncrement parameter: Enables stricter behavior using the AUTOINCREMENT keyword, ensuring IDs are never reused. This only applies to Long? types.

    Composite Primary Key (@CompositePrimaryKey)

    • Use this when a key consists of multiple columns.
    • Apply @CompositePrimaryKey to multiple properties within the class.
    • All properties marked with @CompositePrimaryKey must be non-nullable.
    // Single Primary Key (Auto-increment)
    @DBRow
    @Serializable
    data class Person(
        @PrimaryKey(autoIncrement = true)
        val id: Long? = null,
        val name: String,
        val age: Int,
    )
    
    // Composite Primary Key
    @DBRow
    @Serializable
    data class Enrollment(
        @CompositePrimaryKey
        val studentId: Long,
        @CompositePrimaryKey
        val courseId: Long,
        val enrollmentDate: String,
    )
  4. Use DSLDBConfiguration for type-safe schema management

    main

    The DSLDBConfiguration (currently experimental) allows you to manage your database schema using a type-safe SQL DSL instead of raw SQL strings within the create and upgrade callbacks.

    Note: DSLDBConfiguration is intended to eventually replace DatabaseConfiguration. It is recommended to use it for new projects to ensure future compatibility.

    Available DSL operations in these callbacks include CREATE, DROP, and ALTER (e.g., ALERT_ADD_COLUMN), which mirror the operations available in standard database { } blocks.

    import com.ctrip.sqllin.driver.DSLDBConfiguration
    import com.ctrip.sqllin.dsl.Database
    
    val database = Database(
        DSLDBConfiguration(
            name = "Person.db",
            path = getGlobalDatabasePath(),
            version = 1,
            create = {
                // Use type-safe DSL instead of raw SQL
                CREATE(PersonTable)
            },
            upgrade = { oldVersion, newVersion ->
                when (oldVersion) {
                    1 -> {
                        // Example: Add a new column in version 2
                        PersonTable ALERT_ADD_COLUMN PersonTable.email
                    }
                }
            }
        )
    )
  5. Define Primary Keys with @PrimaryKey and @CompositePrimaryKey

    main

    SQLlin provides two ways to define primary keys depending on whether you have a single column or multiple columns as the key.

    Single Primary Key (@PrimaryKey)

    • Auto-incrementing Long: The property must be nullable (Long?). This maps to SQLite's INTEGER PRIMARY KEY (rowid alias). When inserting, passing null allows SQLite to generate the ID.
    • Other types (String, Int, etc.): The property must be non-nullable. You must provide a unique value during insertion.
    • autoIncrement parameter: Enables stricter behavior using the AUTOINCREMENT keyword to ensure row IDs are never reused. This only applies to Long? properties.

    Composite Primary Key (@CompositePrimaryKey)

    Use this when the primary key consists of multiple columns.

    • Apply @CompositePrimaryKey to multiple properties in the same class.
    • All properties marked with @CompositePrimaryKey must be non-nullable.
    • Constraint: You cannot mix @PrimaryKey and @CompositePrimaryKey in the same class.

    Note: For Long? auto-incrementing keys, use @PrimaryKey(autoIncrement = true).

    // Single Primary Key
    @DBRow
    @Serializable
    data class Person(
        @PrimaryKey(autoIncrement = true)
        val id: Long? = null,
        val name: String,
        val age: Int,
    )
    
    // Composite Primary Key
    @DBRow
    @Serializable
    data class Enrollment(
        @CompositePrimaryKey
        val studentId: Long,
        @CompositePrimaryKey
        val courseId: Long,
        val enrollmentDate: String,
    )
  6. Chain multiple SELECT clauses

    main

    SQLlin enforces correct SQL clause ordering through its DSL. When combining multiple clauses, you must follow the allowed connection rules.

    Clause Connection Rules:

    Clause/StatementConnectable
    SELECTWHERE, ORDER BY, LIMIT, GROUP BY
    WHERELIMIT, ORDER BY, GROUP BY
    GROUP BYHAVING, ORDER BY
    HAVINGORDER BY, LIMIT
    ORDER BYLIMIT
    LIMITOFFSET
    OFFSET/
  7. Understand the purpose of the sqllin-dsl-test module

    main

    The sqllin-dsl-test module is not a standalone unit testing tool. Instead, it serves as the primary implementation of the unit tests for the sqllin-dsl module.

    This architectural decision was made because KSP (Kotlin Symbol Processing) currently does not support code generation for commonTest. To ensure that generated code is available for testing, all unit test code for sqllin-dsl is consolidated within this specific module.

  8. Configure Foreign Key constraints

    main

    SQLlin supports foreign keys to maintain referential integrity.

    1. Enable Foreign Key Enforcement (CRITICAL)

    By default, SQLite does not enforce foreign keys. You must explicitly enable it for every connection before creating tables:

    database {
        PRAGMA_FOREIGN_KEYS(true)
        CREATE(UserTable)
        CREATE(OrderTable)
    }

    2. Defining Foreign Keys

    Method 1: Column-level (@References)

    Recommended for simple, single-column foreign keys.

    @References(
        tableName = "User",
        foreignKeys = ["id"],
        trigger = Trigger.ON_DELETE_CASCADE
    )
    val userId: Long

    Method 2: Table-level (@ForeignKeyGroup + @ForeignKey)

    Required for composite foreign keys (referencing multiple columns).

    @ForeignKeyGroup(
        group = 0,
        tableName = "Product",
        trigger = Trigger.ON_DELETE_CASCADE,
        constraintName = "fk_product"
    )
    data class OrderItem(
        @ForeignKey(group = 0, reference = "categoryId") val productCategory: Int,
        @ForeignKey(group = 0, reference = "productCode") val productCode: String,
    )

    3. Supported Triggers

    TriggerParent ActionChild BehaviorRequires Nullable?
    (Default)AllowNo changeNo
    CASCADEAllowChild is deleted/updatedNo
    SET_NULLAllowForeign key set to NULLYes
    SET_DEFAULTAllowForeign key set to @DefaultNo
    RESTRICTBlockOperation failsNo
    // Example of @References
    @DBRow
    @Serializable
    data class Order(
        @PrimaryKey(isAutoincrement = true) val id: Long?,
        @References(
            tableName = "User",
            foreignKeys = ["id"],
            trigger = Trigger.ON_DELETE_CASCADE
        )
        val userId: Long,
        val amount: Double
    )
  9. Understand the SQLlin architecture

    main

    SQLlin is composed of three main modules:

    1. sqllin-dsl: Provides the DSL implementations for writing SQL statements. This is the primary module for most users.
    2. sqllin-processor: A KSP-based module that processes annotations and generates the necessary code to work with sqllin-dsl.
    3. sqllin-driver: A set of low-level, common multiplatform SQLite APIs. Note: It is generally not recommended to use sqllin-driver directly; instead, use sqllin-dsl for high-level database operations.
  10. Add column constraints with @Unique and @CompositeUnique

    main

    Use these annotations to enforce uniqueness constraints on your columns.

    Single Column Uniqueness (@Unique)

    • Enforces that no two rows have the same value in a specific column.
    • Multiple NULL values are allowed (as NULL != NULL in SQL).
    • To prevent NULL values, use a non-nullable Kotlin type (e.g., String).

    Multi-Column Uniqueness (@CompositeUnique)

    • Ensures the combination of multiple columns is unique.
    • Grouping: Use the group parameter to define which properties belong to which constraint. Properties with the same group number are combined into a single constraint. If no group is specified, it defaults to 0.
    • A single property can belong to multiple groups by providing multiple indices (e.g., @CompositeUnique(0, 1)).
    // Single Unique
    @Unique val email: String
    
    // Composite Unique (Group 0)
    @CompositeUnique(0) val studentId: Int,
    @CompositeUnique(0) val courseId: Int
    
    // Multiple Groups
    @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