diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e751bf1..b1e5ece 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,10 +3,12 @@ name: Build & Test on: push: branches: - - '*' + - main + - 'release/**' pull_request: - branches: - - '*' + branches: + - main + - 'release/**' jobs: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fa84e76..6427609 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,87 +41,13 @@ jobs: ${{ runner.os }}-konan- - name: Build sqllin-driver - run: ./gradlew :sqllin-driver:assemble -PonCICD - - - name: Build sqllin-dsl - run: ./gradlew :sqllin-dsl:assemble -PonCICD - - - name: Publish to MavenCentral - run: ./publish_apple_android_jvm.sh - - build-on-windows: - runs-on: windows-latest - timeout-minutes: 60 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@v3 - - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: 21 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Cache Kotlin/Native - uses: actions/cache@v4 - with: - path: ~/.konan - key: ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle.kts') }} - restore-keys: | - ${{ runner.os }}-konan- - - - name: Build sqllin-driver - run: ./gradlew :sqllin-driver:mingwX64MainKlibrary - - - name: Build sqllin-dsl - run: ./gradlew :sqllin-dsl:mingwX64MainKlibrary - - - name: Publish to MavenCentral - run: ./gradlew :sqllin-driver:publishMingwX64PublicationToMavenCentralRepository && ./gradlew :sqllin-dsl:publishMingwX64PublicationToMavenCentralRepository - - build-on-linux: - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Validate Gradle Wrapper - uses: gradle/actions/wrapper-validation@v3 - - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - distribution: 'zulu' - java-version: 21 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Cache Kotlin/Native - uses: actions/cache@v4 - with: - path: ~/.konan - key: ${{ runner.os }}-konan-${{ hashFiles('**/*.gradle.kts') }} - restore-keys: | - ${{ runner.os }}-konan- - - - name: Build sqllin-driver - run: ./gradlew :sqllin-driver:assemble -PonCICD + run: ./gradlew :sqllin-driver:assemble - name: Build sqllin-processor run: ./gradlew :sqllin-processor:assemble - name: Build sqllin-dsl - run: ./gradlew :sqllin-dsl:assemble -PonCICD + run: ./gradlew :sqllin-dsl:assemble - name: Publish to MavenCentral - run: ./publish_linux_processor.sh + run: ./publish_all.sh \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 470ac33..bdba5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,24 +2,28 @@ - Date format: YYYY-MM-dd -## 2.3.0 / 2026-05-16 +## 2.3.0 / 2026-08-20 ### All -* Update `Kotlin`'s version to `2.3.21` -* Update `AGP`'s version to `9.0.0`, migrated from `com.android.library` plugin to `com.android.kotlin.multiplatform.library` +* Update `Kotlin`'s version to `2.4.10` +* Update `AGP`'s version to `9.3.1`, migrated from `com.android.library` plugin to `com.android.kotlin.multiplatform.library` * Update `kotlinx.serialization`'s version to `1.11.0` * Update `kotlinx.coroutines`'s version to `1.11.0` * Fix documentation: Android minimum supported version has been `7.0+` (API 24) since `2.0.0`, the README incorrectly stated `6.0+` * **Breaking change**: Drop `iosX64`, `macosX64`, `watchosX64`, and `tvosX64` target support +### sqllin-dsl + +* New DSL API: `DatabaseScope#INSERT_OR_REPLACE` for SQL syntax `INSERT OR REPLACE` + ### sqllin-driver -* Update `sqlite-jdbc`'s version to `3.53.1.0` +* Update `sqlite-jdbc`'s version to `3.53.2.1` ### sqllin-processor -* Update `KSP`'s version to `2.3.7` +* Update `KSP`'s version to `2.3.11` ## 2.2.0 / 2025-12-15 diff --git a/ROADMAP.md b/ROADMAP.md index 9f81acb..7feef9f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,11 +1,5 @@ # SQLlin Roadmap -## High Priority - -* Support FOREIGN KEY DSL (2.2.0 ✅) -* Support CREATE INDEX DSL (2.2.0 ✅) -* Support INSERT OR REPLACE - ## Medium Priority * Support WASM platform DSL @@ -18,4 +12,10 @@ ## Low Priority * Support store instances of kotlinx.datetime -* Support CHECK keyword \ No newline at end of file +* Support CHECK keyword + +## Supported + +* Support INSERT OR REPLACE (2.3.0 ✅) +* Support FOREIGN KEY DSL (2.2.0 ✅) +* Support CREATE INDEX DSL (2.2.0 ✅) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 2eafc6c..b206ad9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,6 +17,8 @@ org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=16g -XX:+HeapDumpOnOutOfMemoryErr org.gradle.workers.max=16 org.gradle.parallel=true org.gradle.caching=true +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true #Android android.useAndroidX=true @@ -31,4 +33,4 @@ kotlin.native.ignoreDisabledTargets=true kotlin.jvm.target.validation.mode=warning kotlin.native.binary.pagedAllocator=false kotlin.native.binary.latin1Strings=true -#kotlin.compiler.execution.strategy=out-of-process \ No newline at end of file +#kotlin.compiler.execution.strategy=out-of-process diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 99ca92a..0886eca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,18 @@ [versions] -kotlin = "2.3.21" -agp = "9.2.1" -ksp = "2.3.7" +kotlin = "2.4.10" +agp = "9.3.1" +ksp = "2.3.11" serialization = "1.11.0" coroutines = "1.11.0" androidx-annotation = "1.10.0" androidx-test = "1.7.0" androidx-test-runner = "1.7.0" -sqlite-jdbc = "3.53.1.0" +sqlite-jdbc = "3.53.2.1" jvm-toolchain = "21" android-sdk-compile = "37" android-sdk-min = "24" -vanniktech-maven-publish = "0.36.0" +vanniktech-maven-publish = "0.37.0" [libraries] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c1627d7..96052ec 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Mar 08 15:11:46 CST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME \ No newline at end of file diff --git a/publish_apple_android_jvm.sh b/publish_all.sh similarity index 64% rename from publish_apple_android_jvm.sh rename to publish_all.sh index d8c076a..463ec18 100755 --- a/publish_apple_android_jvm.sh +++ b/publish_all.sh @@ -1,3 +1,4 @@ # Publish artifacts on macOS env -./gradlew :sqllin-driver:publishAllPublicationsToMavenCentralRepository -PonCICD -./gradlew :sqllin-dsl:publishAllPublicationsToMavenCentralRepository -PonCICD \ No newline at end of file +./gradlew :sqllin-driver:publishAllPublicationsToMavenCentralRepository +./gradlew :sqllin-dsl:publishAllPublicationsToMavenCentralRepository +./gradlew :sqllin-processor:publishMavenPublicationToMavenCentralRepository \ No newline at end of file diff --git a/publish_linux_processor.sh b/publish_linux_processor.sh deleted file mode 100755 index fef590e..0000000 --- a/publish_linux_processor.sh +++ /dev/null @@ -1,6 +0,0 @@ -# Publish artifacts on Linux env -./gradlew :sqllin-driver:publishLinuxX64PublicationToMavenCentralRepository -./gradlew :sqllin-driver:publishLinuxArm64PublicationToMavenCentralRepository -./gradlew :sqllin-processor:publishMavenPublicationToMavenCentralRepository -./gradlew :sqllin-dsl:publishLinuxX64PublicationToMavenCentralRepository -./gradlew :sqllin-dsl:publishLinuxArm64PublicationToMavenCentralRepository \ No newline at end of file diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 3275348..d43f0f6 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -24,7 +24,7 @@ kotlin { iosSimulatorArm64() compilerOptions { - freeCompilerArgs.addAll("-Xexpect-actual-classes", "-Xcontext-parameters") + freeCompilerArgs.addAll("-Xexpect-actual-classes") } sourceSets { @@ -48,9 +48,7 @@ dependencies { afterEvaluate { // WORKAROUND: both register() and named() fail – https://github.com/gradle/gradle/issues/9331 tasks { - withType> { - if (name != "kspCommonMainKotlinMetadata") - dependsOn("kspCommonMainKotlinMetadata") - } + matching { (it is KotlinCompilationTask<*> || it.name.startsWith("ksp")) && it.name != "kspCommonMainKotlinMetadata" } + .configureEach { dependsOn("kspCommonMainKotlinMetadata") } } -} \ No newline at end of file +} diff --git a/sqllin-driver-test/build.gradle.kts b/sqllin-driver-test/build.gradle.kts index ec99121..e71a424 100644 --- a/sqllin-driver-test/build.gradle.kts +++ b/sqllin-driver-test/build.gradle.kts @@ -47,7 +47,7 @@ kotlin { } compilerOptions { - freeCompilerArgs.addAll("-Xexpect-actual-classes", "-Xcontext-parameters") + freeCompilerArgs.addAll("-Xexpect-actual-classes") } sourceSets { diff --git a/sqllin-driver-test/build/konan.txt b/sqllin-driver-test/build/konan.txt deleted file mode 100644 index aaf65fa..0000000 --- a/sqllin-driver-test/build/konan.txt +++ /dev/null @@ -1 +0,0 @@ -/Users/qiaoyuang/.konan/kotlin-native-prebuilt-macos-x86_64-2.3.21 \ No newline at end of file diff --git a/sqllin-driver-test/build/kotlin/commonizedNativeDistributionLocation.txt b/sqllin-driver-test/build/kotlin/commonizedNativeDistributionLocation.txt deleted file mode 100644 index 731dc41..0000000 --- a/sqllin-driver-test/build/kotlin/commonizedNativeDistributionLocation.txt +++ /dev/null @@ -1 +0,0 @@ -/Users/qiaoyuang/.konan/kotlin-native-prebuilt-macos-x86_64-2.3.21/klib/commonized/2.3.21 \ No newline at end of file diff --git a/sqllin-driver/build.gradle.kts b/sqllin-driver/build.gradle.kts index a38cbf1..c410c16 100644 --- a/sqllin-driver/build.gradle.kts +++ b/sqllin-driver/build.gradle.kts @@ -50,7 +50,7 @@ kotlin { } compilerOptions { - freeCompilerArgs.addAll("-Xexpect-actual-classes", "-Xcontext-parameters") + freeCompilerArgs.addAll("-Xexpect-actual-classes") } sourceSets { diff --git a/sqllin-dsl-test/build.gradle.kts b/sqllin-dsl-test/build.gradle.kts index af2c09d..287bdd4 100644 --- a/sqllin-dsl-test/build.gradle.kts +++ b/sqllin-dsl-test/build.gradle.kts @@ -50,7 +50,7 @@ kotlin { } compilerOptions { - freeCompilerArgs.addAll("-Xexpect-actual-classes", "-Xcontext-parameters") + freeCompilerArgs.addAll("-Xexpect-actual-classes") } sourceSets { @@ -109,9 +109,7 @@ dependencies { afterEvaluate { // WORKAROUND: both register() and named() fail – https://github.com/gradle/gradle/issues/9331 tasks { - withType> { - if (name != "kspCommonMainKotlinMetadata") - dependsOn("kspCommonMainKotlinMetadata") - } + matching { (it is KotlinCompilationTask<*> || it.name.startsWith("ksp")) && it.name != "kspCommonMainKotlinMetadata" } + .configureEach { dependsOn("kspCommonMainKotlinMetadata") } } -} \ No newline at end of file +} diff --git a/sqllin-dsl-test/src/androidDeviceTest/kotlin/com/ctrip/sqllin/dsl/test/AndroidTest.kt b/sqllin-dsl-test/src/androidDeviceTest/kotlin/com/ctrip/sqllin/dsl/test/AndroidTest.kt index edf9ab0..2dc174b 100644 --- a/sqllin-dsl-test/src/androidDeviceTest/kotlin/com/ctrip/sqllin/dsl/test/AndroidTest.kt +++ b/sqllin-dsl-test/src/androidDeviceTest/kotlin/com/ctrip/sqllin/dsl/test/AndroidTest.kt @@ -67,6 +67,9 @@ class AndroidTest { @Test fun testInsertWithId() = commonTest.testInsertWithId() + @Test + fun testInsertOrReplace() = commonTest.testInsertOrReplace() + @Test fun testCreateInDatabaseScope() = commonTest.testCreateInDatabaseScope() 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 56b8a41..d474347 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 @@ -633,6 +633,58 @@ class CommonBasicTest(private val path: DatabasePath) { } } + @OptIn(AdvancedInsertAPI::class) + fun testInsertOrReplace() { + Database(getNewAPIDBConfig()).databaseAutoClose { database -> + // Insert an initial entity with a known ID + val original = PersonWithId(id = 100L, name = "Eve", age = 28) + database { + PersonWithIdTable { table -> + table INSERT_WITH_ID original + } + } + + lateinit var selectStatement: SelectStatement + database { + selectStatement = PersonWithIdTable SELECT X + } + assertEquals(1, selectStatement.getResults().size) + assertEquals(100L, selectStatement.getResults().first().id) + assertEquals("Eve", selectStatement.getResults().first().name) + + // INSERT_OR_REPLACE with the same PK — should replace the existing row + val replacement = PersonWithId(id = 100L, name = "Eve Updated", age = 29) + database { + PersonWithIdTable { table -> + table INSERT_OR_REPLACE replacement + } + } + + database { + selectStatement = PersonWithIdTable SELECT X + } + val resultsAfterReplace = selectStatement.getResults() + assertEquals(1, resultsAfterReplace.size) + assertEquals(100L, resultsAfterReplace.first().id) + assertEquals("Eve Updated", resultsAfterReplace.first().name) + assertEquals(29, resultsAfterReplace.first().age) + + // INSERT_OR_REPLACE with a new entity (null ID) — should insert without conflict + val newEntity = PersonWithId(id = null, name = "Frank", age = 35) + database { + PersonWithIdTable { table -> + table INSERT_OR_REPLACE newEntity + } + } + + database { + selectStatement = PersonWithIdTable SELECT X + } + assertEquals(2, selectStatement.getResults().size) + assertEquals(true, selectStatement.getResults().any { it.name == "Frank" }) + } + } + fun testCreateInDatabaseScope() { Database(getNewAPIDBConfig()).databaseAutoClose { database -> val person = PersonWithId(id = null, name = "Grace", age = 40) 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 9c5edd7..e44a5bf 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 @@ -61,6 +61,9 @@ class JvmTest { @Test fun testInsertWithId() = commonTest.testInsertWithId() + @Test + fun testInsertOrReplace() = commonTest.testInsertOrReplace() + @Test fun testCreateInDatabaseScope() = commonTest.testCreateInDatabaseScope() 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 f521246..ef1f136 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 @@ -77,6 +77,9 @@ class NativeTest { @Test fun testInsertWithId() = commonTest.testInsertWithId() + @Test + fun testInsertOrReplace() = commonTest.testInsertOrReplace() + @Test fun testCreateInDatabaseScope() = commonTest.testCreateInDatabaseScope() diff --git a/sqllin-dsl/build.gradle.kts b/sqllin-dsl/build.gradle.kts index 404e88f..a795658 100644 --- a/sqllin-dsl/build.gradle.kts +++ b/sqllin-dsl/build.gradle.kts @@ -47,7 +47,7 @@ kotlin { mingwX64() compilerOptions { - freeCompilerArgs.addAll("-Xexpect-actual-classes", "-Xcontext-parameters") + freeCompilerArgs.addAll("-Xexpect-actual-classes") } sourceSets { 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 1d294e7..07f58cd 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 @@ -47,6 +47,7 @@ import kotlin.jvm.JvmName * * Supported operations: * - **INSERT**: Add entities to tables + * - **INSERT OR REPLACE**: Insert or replace entities on PRIMARY KEY / UNIQUE conflict * - **UPDATE**: Modify existing records with SET and WHERE clauses * - **DELETE**: Remove records with WHERE clauses * - **SELECT**: Query records with WHERE, ORDER BY, LIMIT, GROUP BY, JOIN, and UNION @@ -253,6 +254,49 @@ public class DatabaseScope internal constructor( public infix fun Table.INSERT_WITH_ID(entity: T): Unit = INSERT_WITH_ID(listOf(entity)) + /** + * Inserts multiple entities into the table, replacing any existing rows that conflict on + * PRIMARY KEY or UNIQUE constraints. + * + * When a conflict is detected, the existing row is deleted and the new row is inserted in its + * place (`INSERT OR REPLACE INTO ...`). When there is no conflict, the behaviour is identical + * to a plain [INSERT]. + * + * The primary key column is always included in the VALUES clause so that SQLite can detect + * conflicts. If the primary key field is `null` for a rowid-backed key, SQLite auto-generates + * the ID and no conflict can occur by primary key. + * + * Example: + * ```kotlin + * val person = PersonWithId(id = 42L, name = "Alice Updated", age = 26) + * PersonWithIdTable INSERT_OR_REPLACE person + * ``` + * + * @see INSERT for standard inserts with auto-generated IDs + */ + @StatementDslMaker + public infix fun Table.INSERT_OR_REPLACE(entities: Iterable) { + val statement = Insert.insertOrReplace(this, databaseConnection, entities) + addStatement(statement) + } + + /** + * Inserts a single entity into the table, replacing any existing row that conflicts on + * PRIMARY KEY or UNIQUE constraints. + * + * Example: + * ```kotlin + * val person = PersonWithId(id = 42L, name = "Alice Updated", age = 26) + * PersonWithIdTable INSERT_OR_REPLACE person + * ``` + * + * @see INSERT_OR_REPLACE for batch inserts with conflict replacement + * @see INSERT for standard inserts with auto-generated IDs + */ + @StatementDslMaker + public infix fun Table.INSERT_OR_REPLACE(entity: T): Unit = + INSERT_OR_REPLACE(listOf(entity)) + // ========== UPDATE Operations ========== /** 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 26852d3..dec5668 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 @@ -59,4 +59,15 @@ internal object Insert : Operation { } return InsertStatement(sql, connection, parameters) } + + fun insertOrReplace(table: Table, connection: DatabaseConnection, entities: Iterable): SingleStatement { + val parameters = ArrayList() + val sql = buildString { + append("INSERT OR REPLACE INTO ") + append(table.tableName) + append(' ') + encodeEntities2InsertValues(table, this, entities, parameters, isInsertWithId = true) + } + return InsertStatement(sql, connection, parameters) + } } \ No newline at end of file