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..b52f754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,12 @@ - Date format: YYYY-MM-dd -## 2.3.0 / 2026-05-16 +## 2.3.0 / 2026-05-xx ### 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 `AGP`'s version to `9.2.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+` diff --git a/ROADMAP.md b/ROADMAP.md index 9f81acb..60759e9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,8 +2,6 @@ ## High Priority -* Support FOREIGN KEY DSL (2.2.0 ✅) -* Support CREATE INDEX DSL (2.2.0 ✅) * Support INSERT OR REPLACE ## Medium Priority @@ -18,4 +16,9 @@ ## Low Priority * Support store instances of kotlinx.datetime -* Support CHECK keyword \ No newline at end of file +* Support CHECK keyword + +## Supported + +* Support FOREIGN KEY DSL (2.2.0 ✅) +* Support CREATE INDEX DSL (2.2.0 ✅) \ 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/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/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