├── .ci-java-version ├── .editorconfig ├── .github └── workflows │ ├── pr.yml │ ├── publish_release.yml │ └── publish_snapshot_release.yml ├── .gitignore ├── .idea └── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── Dangerfile.df.kts ├── LICENSE ├── README.md ├── build.gradle.kts ├── changelog_config.json ├── detekt.yml ├── format ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── integration ├── build.gradle.kts └── src │ ├── androidUnitTest │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ └── integration │ │ └── AndroidxSqliteCommonIntegrationTests.android.kt │ ├── commonMain │ └── sqldelight │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ └── integration │ │ └── Record.sq │ ├── commonTest │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ └── integration │ │ ├── AndroidxSqliteCommonIntegrationTests.kt │ │ ├── AndroidxSqliteConcurrencyIntegrationTest.kt │ │ └── AndroidxSqliteIntegrationTest.kt │ ├── jvmTest │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ └── integration │ │ └── AndroidxSqliteCommonIntegrationTests.jvm.kt │ └── nativeTest │ └── kotlin │ └── com │ └── eygraber │ └── sqldelight │ └── androidx │ └── driver │ └── integration │ └── AndroidxSqliteCommonIntegrationTests.native.kt ├── library ├── build.gradle.kts ├── gradle.properties └── src │ ├── androidMain │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ ├── AndroidxSqliteDatabaseType.android.kt │ │ └── TransactionsThreadLocal.kt │ ├── androidUnitTest │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ └── AndroidxSqliteCommonTests.android.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ ├── AndroidxSqliteConfiguration.kt │ │ ├── AndroidxSqliteDatabaseType.kt │ │ ├── AndroidxSqliteDriver.kt │ │ ├── AndroidxSqliteHelpers.kt │ │ └── ConnectionPool.kt │ ├── commonTest │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ ├── AndroidxSqliteCallbackTest.kt │ │ ├── AndroidxSqliteCommonTests.kt │ │ ├── AndroidxSqliteConcurrencyTest.kt │ │ ├── AndroidxSqliteDriverOpenFlagsTest.kt │ │ ├── AndroidxSqliteDriverTest.kt │ │ ├── AndroidxSqliteEphemeralTest.kt │ │ ├── AndroidxSqliteQueryTest.kt │ │ └── AndroidxSqliteTransacterTest.kt │ ├── jvmMain │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ ├── AndroidxSqliteDatabaseType.jvm.kt │ │ └── TransactionsThreadLocal.kt │ ├── jvmTest │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ └── AndroidxSqliteCommonTests.jvm.kt │ ├── nativeMain │ └── kotlin │ │ └── com │ │ └── eygraber │ │ └── sqldelight │ │ └── androidx │ │ └── driver │ │ ├── ThreadLocalId.kt │ │ └── TransactionsThreadLocal.kt │ └── nativeTest │ └── kotlin │ └── com │ └── eygraber │ └── sqldelight │ └── androidx │ └── driver │ └── AndroidxSqliteCommonTests.native.kt ├── renovate.json └── settings.gradle.kts /.ci-java-version: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # noinspection EditorConfigKeyCorrectness 2 | [*.{kt,kts}] 3 | ij_kotlin_allow_trailing_comma = true 4 | ij_kotlin_allow_trailing_comma_on_call_site = true 5 | ij_kotlin_imports_layout=*,java.**,javax.**,kotlin.**,^ 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | ktlint_code_style = android_studio 10 | ktlint_experimental = enabled 11 | ktlint_ignore_back_ticked_identifier = true 12 | ktlint_standard_annotation = disabled 13 | ktlint_standard_blank-line-between-when-conditions = disabled 14 | ktlint_standard_class-signature = disabled 15 | ktlint_standard_comment-wrapping = disabled 16 | # disabled because of https://github.com/pinterest/ktlint/issues/2182#issuecomment-1863408507 17 | ktlint_standard_condition-wrapping = disabled 18 | ktlint_standard_filename = disabled 19 | ktlint_standard_function-naming = disabled 20 | ktlint_standard_function-signature = disabled 21 | ktlint_standard_keyword-spacing = disabled 22 | ktlint_standard_package-name = disabled 23 | ktlint_standard_property-naming = disabled 24 | ktlint_standard_spacing-between-declarations-with-annotations = disabled 25 | max_line_length = 120 26 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | pull_request 5 | 6 | jobs: 7 | danger: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - name: Danger 13 | uses: danger/kotlin@1.3.3 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | assemble: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: actions/setup-java@v4 23 | with: 24 | distribution: 'zulu' 25 | java-version-file: .ci-java-version 26 | 27 | - name: Setup Gradle 28 | uses: gradle/actions/setup-gradle@v4 29 | with: 30 | gradle-version: wrapper 31 | 32 | - name: Run assemble task 33 | run: ./gradlew assemble 34 | 35 | detekt: 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - uses: actions/setup-java@v4 41 | with: 42 | distribution: 'zulu' 43 | java-version-file: .ci-java-version 44 | 45 | - name: Setup Gradle 46 | uses: gradle/actions/setup-gradle@v4 47 | with: 48 | gradle-version: wrapper 49 | 50 | - name: Run detekt 51 | run: ./gradlew detektAll 52 | 53 | ktlint: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | 58 | - name: Run ktlint 59 | run: ./format --no-format 60 | 61 | lint: 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v4 65 | 66 | - uses: actions/setup-java@v4 67 | with: 68 | distribution: 'zulu' 69 | java-version-file: .ci-java-version 70 | 71 | - name: Setup Gradle 72 | uses: gradle/actions/setup-gradle@v4 73 | with: 74 | gradle-version: wrapper 75 | 76 | - name: Run Android lint 77 | run: ./gradlew lintRelease 78 | 79 | test: 80 | strategy: 81 | matrix: 82 | os: [ macos-latest, ubuntu-latest ] 83 | runs-on: ${{matrix.os}} 84 | steps: 85 | - uses: actions/checkout@v4 86 | 87 | - uses: actions/setup-java@v4 88 | with: 89 | distribution: 'zulu' 90 | java-version-file: .ci-java-version 91 | 92 | - name: Setup Gradle 93 | uses: gradle/actions/setup-gradle@v4 94 | with: 95 | gradle-version: wrapper 96 | 97 | - name: Run tests 98 | run: ./gradlew allTests 99 | if: matrix.os == 'ubuntu-latest' 100 | 101 | - name: Run Apple tests 102 | run: ./gradlew iosX64Test macosX64Test 103 | if: matrix.os == 'macos-latest' 104 | 105 | env: 106 | GRADLE_OPTS: -Dorg.gradle.daemon=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx16g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1024m" 107 | -------------------------------------------------------------------------------- /.github/workflows/publish_release.yml: -------------------------------------------------------------------------------- 1 | name: Publish a release 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | VERSION_FILE: gradle.properties 8 | VERSION_EXTRACT_PATTERN: '(?<=version=).+' 9 | GH_USER_NAME: github.actor 10 | GRADLE_OPTS: -Dorg.gradle.daemon=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx16g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1024m" 11 | 12 | jobs: 13 | publish_artifacts: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | token: ${{ secrets.PUSH_PAT }} 20 | 21 | - name: Generate versions 22 | uses: HardNorth/github-version-generate@v1 23 | with: 24 | version-source: file 25 | version-file: ${{ env.VERSION_FILE }} 26 | version-file-extraction-pattern: ${{ env.VERSION_EXTRACT_PATTERN }} 27 | 28 | - uses: actions/setup-java@v4 29 | with: 30 | distribution: 'zulu' 31 | java-version-file: .ci-java-version 32 | 33 | - name: Setup Gradle 34 | uses: gradle/actions/setup-gradle@v4 35 | with: 36 | gradle-version: wrapper 37 | 38 | - name: Publish the artifacts 39 | env: 40 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 41 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 42 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} 43 | run: ./gradlew publishAllPublicationsToMavenCentralRepository -Pversion=${{ env.RELEASE_VERSION }} 44 | 45 | - name: Create, checkout, and push release branch 46 | run: | 47 | git config user.name eygraber 48 | git config user.email 1100381+eygraber@users.noreply.github.com 49 | git checkout -b releases/${{ env.RELEASE_VERSION }} 50 | git push origin releases/${{ env.RELEASE_VERSION }} 51 | 52 | - name: Import GPG Key 53 | uses: crazy-max/ghaction-import-gpg@v6 54 | with: 55 | gpg_private_key: ${{ secrets.GIT_SIGNING_PRIVATE_KEY }} 56 | passphrase: ${{ secrets.GIT_SIGNING_PRIVATE_KEY_PASSWORD }} 57 | git_user_signingkey: true 58 | git_commit_gpgsign: true 59 | git_tag_gpgsign: true 60 | 61 | - name: Store SHA of HEAD commit on ENV 62 | run: echo "GIT_HEAD=$(git rev-parse HEAD)" >> $GITHUB_ENV 63 | 64 | - name: Create tag 65 | id: create_tag 66 | uses: actions/github-script@v7 67 | with: 68 | github-token: ${{ secrets.PUSH_PAT }} 69 | script: | 70 | const {GIT_HEAD} = process.env 71 | github.rest.git.createRef({ 72 | owner: context.repo.owner, 73 | repo: context.repo.repo, 74 | ref: "refs/tags/${{ env.RELEASE_VERSION }}", 75 | sha: `${GIT_HEAD}` 76 | }) 77 | 78 | - name: Build changelog 79 | id: build_changelog 80 | uses: mikepenz/release-changelog-builder-action@v5 81 | with: 82 | configuration: "changelog_config.json" 83 | toTag: ${{ env.RELEASE_VERSION }} 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | 87 | - name: Create release 88 | id: create_release 89 | uses: ncipollo/release-action@v1 90 | with: 91 | body: ${{ steps.build_changelog.outputs.changelog }} 92 | name: Release ${{ env.RELEASE_VERSION }} 93 | tag: ${{ env.RELEASE_VERSION }} 94 | token: ${{ secrets.PUSH_PAT }} 95 | 96 | - uses: actions/checkout@v4 97 | with: 98 | ref: ${{ github.event.head_ref }} 99 | token: ${{ secrets.PUSH_PAT }} 100 | 101 | - uses: actions/setup-java@v4 102 | with: 103 | distribution: 'zulu' 104 | java-version-file: .ci-java-version 105 | 106 | - name: Setup Gradle 107 | uses: gradle/actions/setup-gradle@v4 108 | with: 109 | gradle-version: wrapper 110 | 111 | - name: Prepare next dev version 112 | id: prepare_next_dev 113 | run: | 114 | sed -i -e 's/${{ env.CURRENT_VERSION }}/${{ env.NEXT_VERSION }}/g' gradle.properties && \ 115 | sed -i -E -e 's/sqldelight-androidx-driver(:|\/)[0-9]+\.[0-9]+\.[0-9]+/sqldelight-androidx-driver\1${{ env.RELEASE_VERSION }}/g' README.md 116 | 117 | - name: Commit next dev version 118 | id: commit_next_dev 119 | uses: EndBug/add-and-commit@v9 120 | with: 121 | add: "['gradle.properties', 'README.md']" 122 | default_author: github_actions 123 | message: "Prepare next dev version" 124 | -------------------------------------------------------------------------------- /.github/workflows/publish_snapshot_release.yml: -------------------------------------------------------------------------------- 1 | name: Publish a snapshot release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | publish_snapshot: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'zulu' 17 | java-version-file: .ci-java-version 18 | 19 | - name: Setup Gradle 20 | uses: gradle/actions/setup-gradle@v4 21 | with: 22 | gradle-version: wrapper 23 | dependency-graph: generate-and-submit 24 | 25 | - name: Publish the artifacts 26 | env: 27 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} 28 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} 29 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ARTIFACT_SIGNING_PRIVATE_KEY }} 30 | run: ./gradlew publish 31 | 32 | env: 33 | GRADLE_OPTS: -Dorg.gradle.daemon=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs="-Xmx16g -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1024m" 34 | DEPENDENCY_GRAPH_INCLUDE_CONFIGURATIONS: runtimeClasspath|releaseRuntimeClasspath 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .kotlin 4 | /local.properties 5 | /.idea 6 | # Make an exception for the code style 7 | !.idea/codeStyles/ 8 | .DS_Store 9 | build/ 10 | /captures 11 | .externalNativeBuild 12 | .cxx 13 | danger_out.json 14 | tmp 15 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1365 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /Dangerfile.df.kts: -------------------------------------------------------------------------------- 1 | import systems.danger.kotlin.* 2 | import java.util.Locale 3 | 4 | danger(args) { 5 | with(github) { 6 | val labelNames = issue.labels.map { it.name }.toSet() 7 | 8 | /* 9 | # -------------------------------------------------------------------------------------------------------------------- 10 | # Check if labels were added to the pull request 11 | #-------------------------------------------------------------------------------------------------------------------- 12 | */ 13 | val labelsToFilter = setOf("hold", "skip-release-notes") 14 | val acceptableLabels = labelNames.filter { it !in labelsToFilter } 15 | 16 | if(acceptableLabels.isEmpty()) { 17 | fail("PR needs labels (hold and skip release notes don't count)") 18 | } 19 | 20 | /* 21 | # -------------------------------------------------------------------------------------------------------------------- 22 | # Don't merge if there is a hold label applied 23 | # -------------------------------------------------------------------------------------------------------------------- 24 | */ 25 | if("hold" in labelNames) fail("This PR cannot be merged with a hold label applied") 26 | 27 | /* 28 | # -------------------------------------------------------------------------------------------------------------------- 29 | # Check if merge commits were added to the pull request 30 | # -------------------------------------------------------------------------------------------------------------------- 31 | */ 32 | val mergeCommitRegex = Regex("^Merge branch '${pullRequest.base.ref}'.*") 33 | if(git.commits.any { it.message.matches(mergeCommitRegex) }) { 34 | fail("Please rebase to get rid of the merge commits in this PR") 35 | } 36 | } 37 | 38 | /* 39 | # -------------------------------------------------------------------------------------------------------------------- 40 | # Make sure that no crash files or dumps are in the commit 41 | # -------------------------------------------------------------------------------------------------------------------- 42 | */ 43 | val touchedFiles = git.createdFiles + git.modifiedFiles 44 | if(touchedFiles.any { it.startsWith("hs_err_pid") || it.startsWith("java_pid") }) { 45 | fail("Please remove any error logs (hs_err_pid*.log) or heap dumps (java_pid*.hprof)") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Eliezer Graber 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SqlDelight AndroidX Driver 2 | 3 | `sqldelight-androidx-driver` provides a [SQLDelight] `SqlDriver` that wraps the [AndroidX Kotlin Multiplatform SQLite] 4 | libraries. 5 | 6 | It works with any of the available implementations of AndroidX SQLite; see their documentation for more information. 7 | 8 | ## Gradle 9 | 10 | ```kotlin 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | dependencies { 16 | implementation("com.eygraber:sqldelight-androidx-driver:0.0.13") 17 | } 18 | ``` 19 | 20 | ## Usage 21 | Assuming the following configuration: 22 | 23 | ```kotlin 24 | sqldelight { 25 | databases { 26 | create("Database") 27 | } 28 | } 29 | ``` 30 | 31 | you get started by creating a `AndroidxSqliteDriver`: 32 | 33 | ```kotlin 34 | Database( 35 | AndroidxSqliteDriver( 36 | driver = BundledSQLiteDriver(), 37 | type = AndroidxSqliteDatabaseType.File(""), 38 | schema = Database.Schema, 39 | ) 40 | ) 41 | ``` 42 | 43 | on Android and JVM you can pass a `File`: 44 | 45 | ```kotlin 46 | Database( 47 | AndroidxSqliteDriver( 48 | driver = BundledSQLiteDriver(), 49 | type = AndroidxSqliteDatabaseType.File(File("my.db")), 50 | schema = Database.Schema, 51 | ) 52 | ) 53 | ``` 54 | 55 | and on Android you can pass a `Context` to create the file in the app's database directory: 56 | 57 | ```kotlin 58 | Database( 59 | AndroidxSqliteDriver( 60 | driver = BundledSQLiteDriver(), 61 | type = AndroidxSqliteDatabaseType.File(context, "my.db"), 62 | schema = Database.Schema, 63 | ) 64 | ) 65 | ``` 66 | 67 | If you want to provide `OpenFlags` to the bundled or native driver, you can use: 68 | 69 | ```kotlin 70 | Database( 71 | AndroidxSqliteDriver( 72 | createConnection = { name -> 73 | BundledSQLiteDriver().open(name, SQLITE_OPEN_READWRITE or SQLITE_OPEN_CREATE) 74 | }, 75 | type = AndroidxSqliteDatabaseType.File(""), 76 | schema = Database.Schema, 77 | ) 78 | ) 79 | ``` 80 | 81 | It will handle calling the `create` and `migrate` functions on your schema for you, and keep track of the database's version. 82 | 83 | ## Connection Pooling 84 | 85 | By default, one connection will be used for both reading and writing, and only one thread can acquire that connection at a time. 86 | If you have WAL enabled, you could (and should) set the amount of pooled reader connections that will be used: 87 | 88 | ```kotlin 89 | AndroidxSqliteDriver( 90 | ..., 91 | readerConnections = 4, 92 | ..., 93 | ) 94 | ``` 95 | 96 | On Android you can defer to the system to determine how many reader connections there should be[1]: 97 | 98 | ```kotlin 99 | // Based on SQLiteGlobal.getWALConnectionPoolSize() 100 | fun getWALConnectionPoolSize() { 101 | val resources = Resources.getSystem() 102 | val resId = 103 | resources.getIdentifier("db_connection_pool_size", "integer", "android") 104 | return if (resId != 0) { 105 | resources.getInteger(resId) 106 | } else { 107 | 2 108 | } 109 | } 110 | ``` 111 | 112 | See [WAL & Dispatchers] for more information about how to configure dispatchers to use for reads and writes. 113 | 114 | > [!NOTE] 115 | > In-Memory and temporary databases will always use 0 reader connections i.e. there will be a single connection 116 | 117 | [1]: https://blog.p-y.wtf/parallelism-with-android-sqlite#heading-secondary-connections 118 | [AndroidX Kotlin Multiplatform SQLite]: https://developer.android.com/kotlin/multiplatform/sqlite 119 | [SQLDelight]: https://github.com/sqldelight/sqldelight 120 | [WAL & Dispatchers]: https://blog.p-y.wtf/parallelism-with-android-sqlite#heading-wal-amp-dispatchers 121 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.eygraber.conventions.kotlin.KotlinFreeCompilerArg 2 | import com.eygraber.conventions.tasks.deleteRootBuildDirWhenCleaning 3 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 5 | 6 | buildscript { 7 | dependencies { 8 | classpath(libs.buildscript.android) 9 | classpath(libs.buildscript.androidCacheFix) 10 | classpath(libs.buildscript.detekt) 11 | classpath(libs.buildscript.dokka) 12 | classpath(libs.buildscript.kotlin) 13 | classpath(libs.buildscript.publish) 14 | } 15 | } 16 | 17 | plugins { 18 | base 19 | alias(libs.plugins.conventions) 20 | } 21 | 22 | deleteRootBuildDirWhenCleaning() 23 | 24 | gradleConventionsDefaults { 25 | android { 26 | sdkVersions( 27 | compileSdk = libs.versions.android.sdk.compile, 28 | targetSdk = libs.versions.android.sdk.target, 29 | minSdk = libs.versions.android.sdk.min, 30 | ) 31 | } 32 | 33 | kotlin { 34 | jvmTargetVersion = JvmTarget.JVM_11 35 | explicitApiMode = ExplicitApiMode.Strict 36 | freeCompilerArgs += KotlinFreeCompilerArg.AllowExpectActualClasses 37 | } 38 | } 39 | 40 | gradleConventionsKmpDefaults { 41 | targets( 42 | KmpTarget.Android, 43 | KmpTarget.Ios, 44 | KmpTarget.Jvm, 45 | KmpTarget.Linux, 46 | KmpTarget.Macos, 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /changelog_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "categories": [ 3 | { 4 | "title": "## ✨ Enhancements", 5 | "labels": ["enhancement"] 6 | }, 7 | { 8 | "title": "## ⚙️ Chores", 9 | "labels": ["chore"] 10 | }, 11 | { 12 | "title": "## 🐛 Bugs", 13 | "labels": ["bug"] 14 | }, 15 | { 16 | "title": "## 🧪 Tests", 17 | "labels": ["test"] 18 | }, 19 | { 20 | "title": "## \uD83D\uDCD6 Documentation", 21 | "labels": ["documentation"] 22 | }, 23 | { 24 | "title": "\uD83D\uDCE6 Dependencies", 25 | "labels": ["dependencies"] 26 | }, 27 | { 28 | "title": "## \uD83D\uDCA5 GitHub Actions", 29 | "labels": ["gh-actions"] 30 | }, 31 | { 32 | "title": "## \uD83D\uDC18 Gradle Improvements", 33 | "labels": ["gradle"] 34 | } 35 | ], 36 | "ignore_labels": [ 37 | "duplicate", "good first issue", "help wanted", "invalid", "question", "wontfix", "hold", "skip release notes" 38 | ], 39 | "sort": "ASC", 40 | "template": "${{CHANGELOG}}", 41 | "pr_template": "- ${{TITLE}} (#${{NUMBER}})", 42 | "empty_template": "- no changes", 43 | "label_extractor": [ 44 | { 45 | "pattern": "(.) (.+)", 46 | "target": "$1" 47 | }, 48 | { 49 | "pattern": "(.) (.+)", 50 | "target": "$1", 51 | "on_property": "title" 52 | } 53 | ], 54 | "transformers": [ 55 | { 56 | "pattern": "[\\-\\*] (\\[(...|TEST|CI|SKIP)\\])( )?(.+?)\n(.+?[\\-\\*] )(.+)", 57 | "target": "- $4\n - $6" 58 | } 59 | ], 60 | "max_tags_to_fetch": 200, 61 | "max_pull_requests": 200, 62 | "max_back_track_time_days": 365, 63 | "tag_resolver": { 64 | "method": "semver" 65 | }, 66 | "base_branches": [ 67 | "master" 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | build: 2 | maxIssues: 0 3 | excludeCorrectable: false 4 | weights: 5 | # complexity: 2 6 | # LongParameterList: 1 7 | # style: 1 8 | # comments: 1 9 | 10 | config: 11 | validation: true 12 | warningsAsErrors: true 13 | 14 | processors: 15 | active: true 16 | exclude: 17 | # - 'FunctionCountProcessor' 18 | # - 'PropertyCountProcessor' 19 | # - 'ClassCountProcessor' 20 | # - 'PackageCountProcessor' 21 | # - 'KtFileCountProcessor' 22 | 23 | console-reports: 24 | active: true 25 | exclude: 26 | - 'ProjectStatisticsReport' 27 | - 'ComplexityReport' 28 | - 'NotificationReport' 29 | # - 'FindingsReport' 30 | - 'FileBasedFindingsReport' 31 | 32 | comments: 33 | active: false 34 | AbsentOrWrongFileLicense: 35 | active: false 36 | licenseTemplateFile: 'license.template' 37 | licenseTemplateIsRegex: false 38 | CommentOverPrivateFunction: 39 | active: false 40 | CommentOverPrivateProperty: 41 | active: false 42 | DeprecatedBlockTag: 43 | active: false 44 | EndOfSentenceFormat: 45 | active: false 46 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)' 47 | UndocumentedPublicClass: 48 | active: false 49 | searchInNestedClass: true 50 | searchInInnerClass: true 51 | searchInInnerObject: true 52 | searchInInnerInterface: true 53 | UndocumentedPublicFunction: 54 | active: false 55 | UndocumentedPublicProperty: 56 | active: false 57 | 58 | complexity: 59 | active: true 60 | ComplexCondition: 61 | active: true 62 | threshold: 4 63 | ComplexInterface: 64 | active: false 65 | threshold: 10 66 | includeStaticDeclarations: false 67 | includePrivateDeclarations: false 68 | CyclomaticComplexMethod: 69 | active: false 70 | threshold: 15 71 | ignoreSingleWhenExpression: false 72 | ignoreSimpleWhenEntries: false 73 | ignoreNestingFunctions: false 74 | nestingFunctions: ['run', 'let', 'apply', 'with', 'also', 'use', 'forEach', 'isNotNull', 'ifNull'] 75 | LabeledExpression: 76 | active: true 77 | ignoredLabels: [] 78 | LargeClass: 79 | active: false 80 | threshold: 600 81 | LongMethod: 82 | active: false 83 | threshold: 60 84 | LongParameterList: 85 | active: false 86 | functionThreshold: 6 87 | constructorThreshold: 7 88 | ignoreDefaultParameters: false 89 | ignoreDataClasses: true 90 | ignoreAnnotated: [] 91 | MethodOverloading: 92 | active: true 93 | threshold: 6 94 | NamedArguments: 95 | active: false 96 | threshold: 3 97 | NestedBlockDepth: 98 | active: true 99 | threshold: 6 100 | ReplaceSafeCallChainWithRun: 101 | active: false 102 | StringLiteralDuplication: 103 | active: false 104 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 105 | threshold: 3 106 | ignoreAnnotation: true 107 | excludeStringsWithLessThan5Characters: true 108 | ignoreStringsRegex: '$^' 109 | TooManyFunctions: 110 | active: false 111 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 112 | thresholdInFiles: 11 113 | thresholdInClasses: 11 114 | thresholdInInterfaces: 11 115 | thresholdInObjects: 11 116 | thresholdInEnums: 11 117 | ignoreDeprecated: false 118 | ignorePrivate: false 119 | ignoreOverridden: false 120 | 121 | coroutines: 122 | active: true 123 | GlobalCoroutineUsage: 124 | active: false 125 | RedundantSuspendModifier: 126 | active: true 127 | SleepInsteadOfDelay: 128 | active: true 129 | SuspendFunWithFlowReturnType: 130 | active: true 131 | 132 | empty-blocks: 133 | active: true 134 | EmptyCatchBlock: 135 | active: true 136 | allowedExceptionNameRegex: '_|(ignore|expected).*' 137 | EmptyClassBlock: 138 | active: true 139 | EmptyDefaultConstructor: 140 | active: true 141 | EmptyDoWhileBlock: 142 | active: true 143 | EmptyElseBlock: 144 | active: true 145 | EmptyFinallyBlock: 146 | active: true 147 | EmptyForBlock: 148 | active: true 149 | EmptyFunctionBlock: 150 | active: false 151 | ignoreOverridden: false 152 | EmptyIfBlock: 153 | active: true 154 | EmptyInitBlock: 155 | active: true 156 | EmptyKtFile: 157 | active: true 158 | EmptySecondaryConstructor: 159 | active: true 160 | EmptyTryBlock: 161 | active: true 162 | EmptyWhenBlock: 163 | active: true 164 | EmptyWhileBlock: 165 | active: true 166 | 167 | exceptions: 168 | active: true 169 | ExceptionRaisedInUnexpectedLocation: 170 | active: true 171 | methodNames: ['toString', 'hashCode', 'equals', 'finalize'] 172 | InstanceOfCheckForException: 173 | active: false 174 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 175 | NotImplementedDeclaration: 176 | active: true 177 | ObjectExtendsThrowable: 178 | active: false 179 | PrintStackTrace: 180 | active: false 181 | RethrowCaughtException: 182 | active: true 183 | ReturnFromFinally: 184 | active: true 185 | ignoreLabeled: false 186 | SwallowedException: 187 | active: false 188 | ignoredExceptionTypes: 189 | - InterruptedException 190 | - NumberFormatException 191 | - ParseException 192 | - MalformedURLException 193 | allowedExceptionNameRegex: '_|(ignore|expected).*' 194 | ThrowingExceptionFromFinally: 195 | active: false 196 | ThrowingExceptionInMain: 197 | active: false 198 | ThrowingExceptionsWithoutMessageOrCause: 199 | active: false 200 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 201 | exceptions: 202 | - IllegalArgumentException 203 | - IllegalStateException 204 | - IOException 205 | ThrowingNewInstanceOfSameException: 206 | active: true 207 | TooGenericExceptionCaught: 208 | active: false 209 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 210 | exceptionNames: 211 | - ArrayIndexOutOfBoundsException 212 | - Error 213 | - Exception 214 | - IllegalMonitorStateException 215 | - NullPointerException 216 | - IndexOutOfBoundsException 217 | - RuntimeException 218 | - Throwable 219 | allowedExceptionNameRegex: '_|(ignore|expected).*' 220 | TooGenericExceptionThrown: 221 | active: false 222 | exceptionNames: 223 | - Error 224 | - Exception 225 | - Throwable 226 | - RuntimeException 227 | 228 | naming: 229 | active: true 230 | ClassNaming: 231 | active: true 232 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 233 | classPattern: '[A-Z][a-zA-Z0-9]*' 234 | ConstructorParameterNaming: 235 | active: true 236 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 237 | parameterPattern: '[a-z][A-Za-z0-9]*' 238 | privateParameterPattern: '[a-z][A-Za-z0-9]*' 239 | excludeClassPattern: '$^' 240 | EnumNaming: 241 | active: true 242 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 243 | enumEntryPattern: '([A-Z][a-z0-9]+)((\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?' 244 | ForbiddenClassName: 245 | active: false 246 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 247 | forbiddenName: [] 248 | FunctionMaxLength: 249 | active: false 250 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 251 | maximumFunctionNameLength: 30 252 | FunctionMinLength: 253 | active: false 254 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 255 | minimumFunctionNameLength: 3 256 | FunctionNaming: 257 | active: true 258 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 259 | functionPattern: '^([[a-z][A-Z]$][a-zA-Z$0-9]*)|(`.*`)$' 260 | excludeClassPattern: '$^' 261 | ignoreAnnotated: ['Composable'] 262 | FunctionParameterNaming: 263 | active: true 264 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 265 | parameterPattern: '[a-z][A-Za-z0-9]*' 266 | excludeClassPattern: '$^' 267 | InvalidPackageDeclaration: 268 | active: false 269 | excludes: ['*.kts'] 270 | rootPackage: '' 271 | MatchingDeclarationName: 272 | active: false 273 | mustBeFirst: true 274 | MemberNameEqualsClassName: 275 | active: true 276 | ignoreOverridden: true 277 | NoNameShadowing: 278 | active: true 279 | NonBooleanPropertyPrefixedWithIs: 280 | active: true 281 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 282 | ObjectPropertyNaming: 283 | active: true 284 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 285 | constantPattern: '[A-Z][_A-Z0-9]*' 286 | propertyPattern: '[A-Za-z][A-Za-z0-9]*' 287 | privatePropertyPattern: '(_)?[A-Za-z][A-Za-z0-9]*' 288 | PackageNaming: 289 | active: false 290 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 291 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*' 292 | TopLevelPropertyNaming: 293 | active: true 294 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 295 | constantPattern: '[A-Z][_A-Z0-9]*' 296 | propertyPattern: '[A-Za-z][A-Za-z0-9]*' 297 | privatePropertyPattern: '_?[A-Za-z][A-Za-z0-9]*' 298 | VariableMaxLength: 299 | active: false 300 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 301 | maximumVariableNameLength: 64 302 | VariableMinLength: 303 | active: false 304 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 305 | minimumVariableNameLength: 1 306 | VariableNaming: 307 | active: true 308 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 309 | variablePattern: '[a-z][A-Za-z0-9]*' 310 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' 311 | excludeClassPattern: '$^' 312 | 313 | performance: 314 | active: true 315 | ArrayPrimitive: 316 | active: true 317 | CouldBeSequence: 318 | active: true 319 | ForEachOnRange: 320 | active: true 321 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 322 | SpreadOperator: 323 | active: false 324 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 325 | UnnecessaryTemporaryInstantiation: 326 | active: true 327 | 328 | potential-bugs: 329 | active: true 330 | CastToNullableType: 331 | active: false 332 | Deprecation: 333 | active: true 334 | DontDowncastCollectionTypes: 335 | active: false 336 | DoubleMutabilityForCollection: 337 | active: false 338 | EqualsAlwaysReturnsTrueOrFalse: 339 | active: false 340 | EqualsWithHashCodeExist: 341 | active: true 342 | ExitOutsideMain: 343 | active: false 344 | ExplicitGarbageCollectionCall: 345 | active: true 346 | HasPlatformType: 347 | active: false 348 | IgnoredReturnValue: 349 | active: false 350 | restrictToConfig: true 351 | returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult'] 352 | ImplicitDefaultLocale: 353 | active: true 354 | ImplicitUnitReturnType: 355 | active: false 356 | allowExplicitReturnType: true 357 | InvalidRange: 358 | active: true 359 | IteratorHasNextCallsNextMethod: 360 | active: true 361 | IteratorNotThrowingNoSuchElementException: 362 | active: true 363 | LateinitUsage: 364 | active: false 365 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 366 | ignoreAnnotated: [] 367 | ignoreOnClassesPattern: '' 368 | MapGetWithNotNullAssertionOperator: 369 | active: false 370 | NullableToStringCall: 371 | active: false 372 | UnconditionalJumpStatementInLoop: 373 | active: true 374 | UnnecessaryNotNullOperator: 375 | active: true 376 | UnnecessarySafeCall: 377 | active: true 378 | UnreachableCatchBlock: 379 | active: false 380 | UnreachableCode: 381 | active: true 382 | UnsafeCallOnNullableType: 383 | active: true 384 | UnsafeCast: 385 | active: false 386 | UnusedUnaryOperator: 387 | active: false 388 | UselessPostfixExpression: 389 | active: true 390 | WrongEqualsTypeParameter: 391 | active: true 392 | 393 | style: 394 | active: true 395 | BracesOnIfStatements: 396 | active: true 397 | ClassOrdering: 398 | active: false 399 | CollapsibleIfStatements: 400 | active: false 401 | DataClassContainsFunctions: 402 | active: false 403 | conversionFunctionPrefix: ['to'] 404 | DataClassShouldBeImmutable: 405 | active: true 406 | DestructuringDeclarationWithTooManyEntries: 407 | active: false 408 | maxDestructuringEntries: 3 409 | EqualsNullCall: 410 | active: true 411 | EqualsOnSignatureLine: 412 | active: false 413 | ExplicitCollectionElementAccessMethod: 414 | active: true 415 | ExplicitItLambdaParameter: 416 | active: true 417 | ExpressionBodySyntax: 418 | active: true 419 | includeLineWrapping: true 420 | ForbiddenComment: 421 | active: true 422 | comments: ['FIXME:', 'STOPSHIP:'] 423 | ForbiddenImport: 424 | active: false 425 | imports: [] 426 | forbiddenPatterns: '' 427 | ForbiddenMethodCall: 428 | active: false 429 | methods: ['kotlin.io.println', 'kotlin.io.print'] 430 | ForbiddenVoid: 431 | active: true 432 | ignoreOverridden: true 433 | ignoreUsageInGenerics: true 434 | FunctionOnlyReturningConstant: 435 | active: false 436 | ignoreOverridableFunction: true 437 | ignoreActualFunction: true 438 | excludedFunctions: ['describeContents'] 439 | ignoreAnnotated: ['dagger.Provides'] 440 | LoopWithTooManyJumpStatements: 441 | active: false 442 | maxJumpCount: 1 443 | MagicNumber: 444 | active: false 445 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 446 | ignoreNumbers: ['-1', '0', '1', '2'] 447 | ignoreHashCodeFunction: true 448 | ignorePropertyDeclaration: false 449 | ignoreLocalVariableDeclaration: false 450 | ignoreConstantDeclaration: true 451 | ignoreCompanionObjectPropertyDeclaration: true 452 | ignoreAnnotation: false 453 | ignoreNamedArgument: true 454 | ignoreEnums: false 455 | ignoreRanges: false 456 | ignoreExtensionFunctions: true 457 | MandatoryBracesLoops: 458 | active: true 459 | MaxLineLength: 460 | active: false # handled by ktlint 461 | maxLineLength: 120 462 | MayBeConst: 463 | active: true 464 | ModifierOrder: 465 | active: false 466 | MultilineLambdaItParameter: 467 | active: true 468 | NestedClassesVisibility: 469 | active: true 470 | NewLineAtEndOfFile: 471 | active: true 472 | NoTabs: 473 | active: true 474 | ObjectLiteralToLambda: 475 | active: true 476 | OptionalAbstractKeyword: 477 | active: true 478 | OptionalUnit: 479 | active: true 480 | PreferToOverPairSyntax: 481 | active: true 482 | ProtectedMemberInFinalClass: 483 | active: true 484 | RedundantExplicitType: 485 | active: true 486 | RedundantHigherOrderMapUsage: 487 | active: false 488 | RedundantVisibilityModifierRule: 489 | active: false 490 | ReturnCount: 491 | active: true 492 | max: 2 493 | excludedFunctions: ['equals'] 494 | excludeLabeled: false 495 | excludeReturnFromLambda: true 496 | excludeGuardClauses: true 497 | SafeCast: 498 | active: true 499 | SerialVersionUIDInSerializableClass: 500 | active: true 501 | SpacingBetweenPackageAndImports: 502 | active: true 503 | ThrowsCount: 504 | active: true 505 | max: 3 506 | # excludeGuardClauses: true 507 | TrailingWhitespace: 508 | active: true 509 | UnderscoresInNumericLiterals: 510 | active: true 511 | acceptableLength: 4 512 | UnnecessaryAbstractClass: 513 | active: false 514 | ignoreAnnotated: ['dagger.Module'] 515 | UnnecessaryAnnotationUseSiteTarget: 516 | active: true 517 | UnnecessaryApply: 518 | active: true 519 | UnnecessaryFilter: 520 | active: false 521 | UnnecessaryInheritance: 522 | active: true 523 | UnnecessaryLet: 524 | active: true 525 | UnnecessaryParentheses: 526 | active: true 527 | UntilInsteadOfRangeTo: 528 | active: true 529 | UnusedImports: 530 | active: true 531 | UnusedPrivateClass: 532 | active: true 533 | UnusedPrivateMember: 534 | active: false 535 | allowedNames: '(_|ignored|expected|serialVersionUID)' 536 | UseArrayLiteralsInAnnotations: 537 | active: true 538 | UseCheckOrError: 539 | active: true 540 | UseDataClass: 541 | active: false 542 | ignoreAnnotated: [] 543 | allowVars: false 544 | UseEmptyCounterpart: 545 | active: true 546 | UseIfEmptyOrIfBlank: 547 | active: true 548 | UseIfInsteadOfWhen: 549 | active: false 550 | UseIsNullOrEmpty: 551 | active: true 552 | UseOrEmpty: 553 | active: true 554 | UseRequire: 555 | active: true 556 | UseRequireNotNull: 557 | active: true 558 | UselessCallOnNotNull: 559 | active: true 560 | UtilityClassWithPublicConstructor: 561 | active: true 562 | VarCouldBeVal: 563 | active: true 564 | WildcardImport: 565 | active: true 566 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**'] 567 | excludeImports: [] 568 | -------------------------------------------------------------------------------- /format: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | version=$(sed -n 's/ktlint = "\(.*\)"/\1/p' gradle/libs.versions.toml) 4 | url="https://github.com/pinterest/ktlint/releases/download/$version/ktlint" 5 | 6 | # Set the destination directory and file name 7 | destination_dir="tmp" 8 | file_name="ktlint-$version" 9 | 10 | mkdir -p $destination_dir 11 | 12 | # setting nullglob ensures proper behavior if nothing matches the glob 13 | shopt -s nullglob 14 | for file in $destination_dir/ktlint-*; do 15 | if [[ "$file" != "$destination_dir/$file_name" ]]; then 16 | rm "$file" 17 | fi 18 | done 19 | shopt -u nullglob 20 | 21 | # Check if the file already exists in the destination directory 22 | if [ ! -e "$destination_dir/$file_name" ]; then 23 | if command -v curl >/dev/null 2>&1; then 24 | curl -LJO "$url" 25 | mv "ktlint" "$destination_dir/$file_name" 26 | elif command -v wget >/dev/null 2>&1; then 27 | wget -O "$destination_dir/$file_name" "$url" 28 | else 29 | echo "Error: curl or wget not found. Please install either curl or wget." 30 | exit 1 31 | fi 32 | 33 | chmod +x "$destination_dir/$file_name" 34 | fi 35 | 36 | should_format=true 37 | for arg in "$@"; do 38 | if [ "$arg" == "--no-format" ]; then 39 | should_format=false 40 | set -- "${@//--no-format/}" 41 | break 42 | fi 43 | done 44 | 45 | args=() 46 | 47 | if [ "$should_format" = true ]; then 48 | args+=("--format") 49 | fi 50 | 51 | args+=("$@") 52 | 53 | "$destination_dir/$file_name" **/*.kt **/*.kts \!**/build/** \!Dangerfile.df.kts --color --color-name=YELLOW "${args[@]}" 54 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4g -XX:ReservedCodeCacheSize=240m -XX:+UseCompressedOops -XX:+UseParallelGC -XX:MetaspaceSize=256m -Dfile.encoding=UTF-8 2 | kotlin.daemon.jvm.options=-Xmx4g -XX:ReservedCodeCacheSize=240m -XX:+UseCompressedOops -XX:+UseParallelGC -XX:MetaspaceSize=256m -Dfile.encoding=UTF-8 3 | 4 | group=com.eygraber 5 | version=0.0.14-SNAPSHOT 6 | 7 | POM_URL=https://github.com/eygraber/sqldelight-androidx-driver/ 8 | POM_SCM_URL=https://github.com/eygraber/sqldelight-androidx-driver/ 9 | POM_SCM_CONNECTION=scm:git:git://github.com/eygraber/sqldelight-androidx-driver.git 10 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/eygraber/sqldelight-androidx-driver.git 11 | 12 | POM_LICENCE_NAME=MIT 13 | POM_LICENCE_DIST=repo 14 | 15 | POM_DEVELOPER_ID=eygraber 16 | POM_DEVELOPER_NAME=Eliezer Graber 17 | POM_DEVELOPER_URL=https://github.com/eygraber 18 | 19 | # Android 20 | android.useAndroidX=true 21 | android.enableJetifier=false 22 | android.enableR8.fullMode=true 23 | android.nonFinalResIds=false 24 | android.nonTransitiveRClass=true 25 | 26 | android.defaults.buildfeatures.aidl=false 27 | android.defaults.buildfeatures.buildconfig=false 28 | android.defaults.buildfeatures.renderscript=false 29 | android.defaults.buildfeatures.resvalues=false 30 | android.defaults.buildfeatures.shaders=false 31 | 32 | android.experimental.cacheCompileLibResources=true 33 | android.experimental.enableSourceSetPathsMap=true 34 | 35 | systemProp.org.gradle.android.cache-fix.ignoreVersionCheck=true 36 | 37 | # Gradle 38 | org.gradle.caching=true 39 | org.gradle.parallel=true 40 | org.gradle.configuration-cache=false 41 | # https://youtrack.jetbrains.com/issue/KT-55701 42 | org.gradle.configureondemand=false 43 | 44 | # Kotlin 45 | kotlin.native.enableKlibsCrossCompilation=true 46 | kotlin.native.ignoreDisabledTargets=true 47 | 48 | #Misc 49 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 50 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 51 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.10.1" 3 | 4 | android-sdk-compile = "34" 5 | android-sdk-target = "34" 6 | android-sdk-min = "21" 7 | 8 | androidxSqlite = "2.5.1" 9 | 10 | atomicfu = "0.27.0" 11 | 12 | cashapp-sqldelight = "2.1.0" 13 | 14 | conventions = "0.0.83" 15 | 16 | detekt = "1.23.8" 17 | 18 | dokka = "2.0.0" 19 | 20 | kotlin = "2.1.21" 21 | kotlinx-coroutines = "1.10.2" 22 | 23 | ktlint = "1.6.0" 24 | 25 | okio = "3.12.0" 26 | 27 | publish = "0.32.0" 28 | 29 | [plugins] 30 | atomicfu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "atomicfu" } 31 | conventions = { id = "com.eygraber.conventions", version.ref = "conventions" } 32 | sqldelight = { id = "app.cash.sqldelight", version.ref = "cashapp-sqldelight" } 33 | 34 | [libraries] 35 | androidx-collections = "androidx.collection:collection:1.5.0" 36 | androidx-sqlite = { module = "androidx.sqlite:sqlite", version.ref = "androidxSqlite" } 37 | androidx-sqliteBundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "androidxSqlite" } 38 | androidx-sqliteFramework = { module = "androidx.sqlite:sqlite-framework", version.ref = "androidxSqlite" } 39 | 40 | atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "atomicfu" } 41 | 42 | buildscript-android = { module = "com.android.tools.build:gradle", version.ref = "agp" } 43 | buildscript-androidCacheFix = { module = "gradle.plugin.org.gradle.android:android-cache-fix-gradle-plugin", version = "3.0.1" } 44 | buildscript-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } 45 | buildscript-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } 46 | buildscript-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } 47 | buildscript-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publish" } 48 | 49 | cashapp-sqldelight-coroutines = { module = "app.cash.sqldelight:coroutines-extensions", version.ref = "cashapp-sqldelight" } 50 | cashapp-sqldelight-dialect = { module = "app.cash.sqldelight:sqlite-3-38-dialect", version.ref = "cashapp-sqldelight" } 51 | cashapp-sqldelight-runtime = { module = "app.cash.sqldelight:runtime", version.ref = "cashapp-sqldelight" } 52 | 53 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 54 | 55 | # not actually used; just here so renovate picks it up 56 | ktlint = { module = "com.pinterest.ktlint:ktlint-bom", version.ref = "ktlint" } 57 | 58 | okio = { module = "com.squareup.okio:okio", version.ref = "okio" } 59 | 60 | test-androidx-core = "androidx.test:core:1.6.1" 61 | test-junit = { module = "junit:junit", version = "4.13.2" } 62 | test-kotlin = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 63 | test-kotlin-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 64 | test-kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } 65 | test-robolectric = { module = "org.robolectric:robolectric", version = "4.14.1" } 66 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eygraber/sqldelight-androidx-driver/c689d8d2a98856af42d00a82bae1cba8f18a58ec/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /integration/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.api.variant.HasUnitTest 2 | import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode 3 | 4 | plugins { 5 | id("com.eygraber.conventions-kotlin-multiplatform") 6 | id("com.eygraber.conventions-android-library") 7 | id("com.eygraber.conventions-detekt") 8 | alias(libs.plugins.sqldelight) 9 | } 10 | 11 | android { 12 | namespace = "com.eygraber.sqldelight.androidx.driver.integration" 13 | } 14 | 15 | kotlin { 16 | defaultKmpTargets( 17 | project = project, 18 | ) 19 | 20 | sourceSets { 21 | androidUnitTest.dependencies { 22 | implementation(libs.test.junit) 23 | implementation(libs.test.androidx.core) 24 | implementation(libs.test.robolectric) 25 | } 26 | 27 | commonTest.dependencies { 28 | implementation(projects.library) 29 | 30 | implementation(libs.androidx.sqliteBundled) 31 | implementation(libs.cashapp.sqldelight.coroutines) 32 | implementation(libs.cashapp.sqldelight.runtime) 33 | 34 | implementation(libs.kotlinx.coroutines.core) 35 | 36 | implementation(libs.test.kotlin) 37 | implementation(libs.test.kotlinx.coroutines) 38 | } 39 | 40 | jvmTest.dependencies { 41 | implementation(libs.test.kotlin.junit) 42 | } 43 | 44 | nativeTest.dependencies { 45 | implementation(libs.okio) 46 | } 47 | } 48 | } 49 | 50 | sqldelight { 51 | linkSqlite = false 52 | 53 | databases { 54 | create("AndroidXDb") { 55 | dialect(libs.cashapp.sqldelight.dialect) 56 | 57 | packageName = "com.eygraber.sqldelight.androidx.driver.integration" 58 | 59 | schemaOutputDirectory = file("src/main/sqldelight/migrations") 60 | 61 | deriveSchemaFromMigrations = false 62 | treatNullAsUnknownForEquality = true 63 | } 64 | } 65 | } 66 | 67 | gradleConventions { 68 | kotlin { 69 | explicitApiMode = ExplicitApiMode.Disabled 70 | } 71 | } 72 | 73 | androidComponents { 74 | onVariants { variant -> 75 | (variant as HasUnitTest).unitTest?.let { unitTest -> 76 | with(unitTest.runtimeConfiguration.resolutionStrategy.dependencySubstitution) { 77 | val bundledArtifact = libs.androidx.sqliteBundled.get().toString() 78 | val bundledJvmArtifact = bundledArtifact.replace("sqlite-bundled", "sqlite-bundled-jvm") 79 | substitute(module(bundledArtifact)) 80 | .using(module(bundledJvmArtifact)) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /integration/src/androidUnitTest/kotlin/com/eygraber/sqldelight/androidx/driver/integration/AndroidxSqliteCommonIntegrationTests.android.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver.integration 2 | 3 | import java.io.File 4 | 5 | actual fun deleteFile(name: String) { 6 | File(name).delete() 7 | } 8 | -------------------------------------------------------------------------------- /integration/src/commonMain/sqldelight/com/eygraber/sqldelight/androidx/driver/integration/Record.sq: -------------------------------------------------------------------------------- 1 | CREATE TABLE Record( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | userId TEXT NOT NULL, 4 | record BLOB NOT NULL 5 | ); 6 | 7 | CREATE INDEX IndexRecordUserId ON Record(userId); 8 | 9 | insert: 10 | INSERT INTO Record(userId, record) VALUES (:userId, :withRecord); 11 | 12 | top: 13 | SELECT * FROM Record ORDER BY id ASC LIMIT 1; 14 | 15 | delete: 16 | DELETE FROM Record WHERE id = :whereId; 17 | 18 | countForUser: 19 | SELECT COUNT(*) FROM Record WHERE userId = :whereUserId; 20 | -------------------------------------------------------------------------------- /integration/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/integration/AndroidxSqliteCommonIntegrationTests.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver.integration 2 | 3 | expect fun deleteFile(name: String) 4 | -------------------------------------------------------------------------------- /integration/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/integration/AndroidxSqliteConcurrencyIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver.integration 2 | 3 | import app.cash.sqldelight.coroutines.asFlow 4 | import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConfiguration 5 | import kotlinx.coroutines.delay 6 | import kotlinx.coroutines.flow.collect 7 | import kotlinx.coroutines.flow.collectLatest 8 | import kotlinx.coroutines.flow.distinctUntilChangedBy 9 | import kotlinx.coroutines.flow.firstOrNull 10 | import kotlinx.coroutines.launch 11 | import kotlinx.coroutines.test.runTest 12 | import kotlin.random.Random 13 | import kotlin.test.Test 14 | 15 | class AndroidxSqliteConcurrencyIntegrationTest : AndroidxSqliteIntegrationTest() { 16 | @Test 17 | fun concurrentQueriesWithMultipleReadersDoNotShareCachedStatementsAcrossConnections() = runTest { 18 | // having 2 readers instead of the default 4 makes it more 19 | // likely to have concurrent readers using the same cached statement 20 | configuration = AndroidxSqliteConfiguration( 21 | readerConnectionsCount = 2, 22 | ) 23 | 24 | launch { 25 | val deleteJob = launch { 26 | database 27 | .recordQueries 28 | .top() 29 | .asFlow() 30 | .mapToOneNotNull() 31 | .distinctUntilChangedBy { it.id } 32 | .collectLatest { top -> 33 | database.withTransaction { 34 | database 35 | .recordQueries 36 | .delete(whereId = top.id) 37 | } 38 | } 39 | } 40 | 41 | val concurrentTopObserverJob = launch { 42 | database 43 | .recordQueries 44 | .top() 45 | .asFlow() 46 | .mapToOneNotNull() 47 | .distinctUntilChangedBy { it.id } 48 | .collect() 49 | } 50 | 51 | launch { 52 | delay(1000) 53 | database 54 | .recordQueries 55 | .countForUser( 56 | whereUserId = "1", 57 | ) 58 | .asFlow() 59 | .mapToOne() 60 | .firstOrNull { 61 | it == 0L 62 | } 63 | 64 | deleteJob.cancel() 65 | concurrentTopObserverJob.cancel() 66 | } 67 | 68 | launch { 69 | repeat(50) { 70 | database.withTransaction { 71 | database 72 | .recordQueries 73 | .insert( 74 | userId = "1", 75 | withRecord = Random.nextBytes(1_024), 76 | ) 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /integration/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/integration/AndroidxSqliteIntegrationTest.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver.integration 2 | 3 | import androidx.sqlite.driver.bundled.BundledSQLiteDriver 4 | import app.cash.sqldelight.Query 5 | import app.cash.sqldelight.TransactionWithoutReturn 6 | import app.cash.sqldelight.coroutines.mapToOne 7 | import app.cash.sqldelight.coroutines.mapToOneNotNull 8 | import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteConfiguration 9 | import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteDatabaseType 10 | import com.eygraber.sqldelight.androidx.driver.AndroidxSqliteDriver 11 | import kotlinx.coroutines.CoroutineDispatcher 12 | import kotlinx.coroutines.DelicateCoroutinesApi 13 | import kotlinx.coroutines.ExperimentalCoroutinesApi 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.newFixedThreadPoolContext 16 | import kotlinx.coroutines.newSingleThreadContext 17 | import kotlinx.coroutines.withContext 18 | import kotlin.test.AfterTest 19 | 20 | abstract class AndroidxSqliteIntegrationTest { 21 | open var type: AndroidxSqliteDatabaseType = AndroidxSqliteDatabaseType.File("test.db") 22 | 23 | @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) 24 | private fun readDispatcher(): CoroutineDispatcher? = when { 25 | configuration.readerConnectionsCount >= 1 -> newFixedThreadPoolContext( 26 | nThreads = configuration.readerConnectionsCount, 27 | name = "db-reads", 28 | ) 29 | else -> null 30 | } 31 | 32 | open var configuration = AndroidxSqliteConfiguration() 33 | set(value) { 34 | field = value 35 | readDispatcher = readDispatcher() 36 | } 37 | 38 | private var readDispatcher: CoroutineDispatcher? = readDispatcher() 39 | 40 | @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) 41 | val writeDispatcher: CoroutineDispatcher = newSingleThreadContext("db-writes") 42 | 43 | suspend inline fun AndroidXDb.withTransaction( 44 | crossinline transactionBlock: TransactionWithoutReturn.() -> Unit, 45 | ) { 46 | withContext(writeDispatcher) { 47 | transaction { 48 | transactionBlock() 49 | } 50 | } 51 | } 52 | 53 | fun Flow>.mapToOne(): Flow = mapToOne(readDispatcher ?: writeDispatcher) 54 | fun Flow>.mapToOneNotNull(): Flow = mapToOneNotNull(readDispatcher ?: writeDispatcher) 55 | 56 | val driver by lazy { 57 | AndroidxSqliteDriver( 58 | createConnection = { name -> 59 | BundledSQLiteDriver().open(name) 60 | }, 61 | databaseType = type, 62 | schema = AndroidXDb.Schema, 63 | configuration = configuration, 64 | ) 65 | } 66 | 67 | val database by lazy { 68 | AndroidXDb(driver = driver) 69 | } 70 | 71 | @AfterTest 72 | fun cleanup() { 73 | driver.close() 74 | 75 | (type as? AndroidxSqliteDatabaseType.File)?.let { type -> 76 | val dbName = type.databaseFilePath 77 | deleteFile(dbName) 78 | deleteFile("$dbName-shm") 79 | deleteFile("$dbName-wal") 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /integration/src/jvmTest/kotlin/com/eygraber/sqldelight/androidx/driver/integration/AndroidxSqliteCommonIntegrationTests.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver.integration 2 | 3 | import java.io.File 4 | 5 | actual fun deleteFile(name: String) { 6 | File(name).delete() 7 | } 8 | -------------------------------------------------------------------------------- /integration/src/nativeTest/kotlin/com/eygraber/sqldelight/androidx/driver/integration/AndroidxSqliteCommonIntegrationTests.native.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver.integration 2 | 3 | import okio.FileSystem 4 | import okio.Path.Companion.toPath 5 | 6 | actual fun deleteFile(name: String) { 7 | FileSystem.SYSTEM.delete(name.toPath()) 8 | } 9 | -------------------------------------------------------------------------------- /library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.eygraber.conventions-kotlin-multiplatform") 3 | id("com.eygraber.conventions-android-library") 4 | id("com.eygraber.conventions-detekt") 5 | id("com.eygraber.conventions-publish-maven-central") 6 | alias(libs.plugins.atomicfu) 7 | } 8 | 9 | android { 10 | namespace = "com.eygraber.sqldelight.androidx.driver" 11 | } 12 | 13 | kotlin { 14 | defaultKmpTargets( 15 | project = project, 16 | ) 17 | 18 | sourceSets { 19 | androidMain.dependencies { 20 | implementation(libs.atomicfu) 21 | } 22 | 23 | androidUnitTest.dependencies { 24 | implementation(libs.androidx.sqliteFramework) 25 | 26 | implementation(libs.test.junit) 27 | implementation(libs.test.androidx.core) 28 | implementation(libs.test.robolectric) 29 | } 30 | 31 | commonMain.dependencies { 32 | implementation(libs.androidx.collections) 33 | 34 | api(libs.androidx.sqlite) 35 | api(libs.cashapp.sqldelight.runtime) 36 | 37 | implementation(libs.kotlinx.coroutines.core) 38 | } 39 | 40 | commonTest.dependencies { 41 | implementation(libs.kotlinx.coroutines.core) 42 | 43 | implementation(libs.test.kotlin) 44 | implementation(libs.test.kotlinx.coroutines) 45 | } 46 | 47 | jvmTest.dependencies { 48 | implementation(libs.androidx.sqliteBundled) 49 | implementation(libs.test.kotlin.junit) 50 | } 51 | 52 | nativeTest.dependencies { 53 | implementation(libs.androidx.sqliteBundled) 54 | implementation(libs.okio) 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /library/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=sqldelight-androidx-driver 2 | POM_NAME=SqlDelight AndroidX Driver 3 | POM_DESCRIPTION=A SQLDelight Driver that wraps AndroidX Kotlin Multiplatform SQLite 4 | -------------------------------------------------------------------------------- /library/src/androidMain/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteDatabaseType.android.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import android.content.Context 4 | import java.io.File as JavaFile 5 | 6 | public fun AndroidxSqliteDatabaseType.Companion.File( 7 | context: Context, 8 | name: String, 9 | ): AndroidxSqliteDatabaseType.File = AndroidxSqliteDatabaseType.File(context.getDatabasePath(name).absolutePath) 10 | 11 | public fun AndroidxSqliteDatabaseType.Companion.File( 12 | file: JavaFile, 13 | ): AndroidxSqliteDatabaseType.File = AndroidxSqliteDatabaseType.File(file.absolutePath) 14 | -------------------------------------------------------------------------------- /library/src/androidMain/kotlin/com/eygraber/sqldelight/androidx/driver/TransactionsThreadLocal.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import app.cash.sqldelight.Transacter 4 | 5 | internal actual class TransactionsThreadLocal actual constructor() { 6 | private val transactions = ThreadLocal() 7 | 8 | internal actual fun get(): Transacter.Transaction? = transactions.get() 9 | 10 | internal actual fun set(transaction: Transacter.Transaction?) { 11 | transactions.set(transaction) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /library/src/androidUnitTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteCommonTests.android.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import androidx.sqlite.SQLiteConnection 4 | import androidx.sqlite.SQLiteDriver 5 | import androidx.sqlite.driver.AndroidSQLiteDriver 6 | import app.cash.sqldelight.Transacter 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | import org.junit.Assert 10 | import org.junit.runner.RunWith 11 | import org.robolectric.RobolectricTestRunner 12 | import java.io.File 13 | import java.util.concurrent.Semaphore 14 | 15 | @RunWith(RobolectricTestRunner::class) 16 | actual class CommonCallbackTest : AndroidxSqliteCallbackTest() 17 | 18 | @RunWith(RobolectricTestRunner::class) 19 | actual class CommonConcurrencyTest : AndroidxSqliteConcurrencyTest() 20 | 21 | @RunWith(RobolectricTestRunner::class) 22 | actual class CommonDriverTest : AndroidxSqliteDriverTest() 23 | 24 | @RunWith(RobolectricTestRunner::class) 25 | actual class CommonDriverOpenFlagsTest : AndroidxSqliteDriverOpenFlagsTest() 26 | 27 | @RunWith(RobolectricTestRunner::class) 28 | actual class CommonQueryTest : AndroidxSqliteQueryTest() 29 | 30 | @RunWith(RobolectricTestRunner::class) 31 | actual class CommonTransacterTest : AndroidxSqliteTransacterTest() 32 | 33 | @RunWith(RobolectricTestRunner::class) 34 | actual class CommonEphemeralTest : AndroidxSqliteEphemeralTest() 35 | 36 | actual fun androidxSqliteTestDriver(): SQLiteDriver = AndroidSQLiteDriver() 37 | 38 | actual fun androidxSqliteTestCreateConnection(): (String) -> SQLiteConnection = { name -> 39 | AndroidSQLiteDriver().open(name) 40 | } 41 | 42 | @Suppress("InjectDispatcher") 43 | actual val IoDispatcher: CoroutineDispatcher get() = Dispatchers.IO 44 | 45 | actual fun deleteFile(name: String) { 46 | File(name).delete() 47 | } 48 | 49 | actual inline fun assertChecksThreadConfinement( 50 | transacter: Transacter, 51 | crossinline scope: Transacter.(T.() -> Unit) -> Unit, 52 | crossinline block: T.() -> Unit, 53 | ) { 54 | lateinit var thread: Thread 55 | var result: Result? = null 56 | val semaphore = Semaphore(0) 57 | 58 | transacter.scope { 59 | thread = kotlin.concurrent.thread { 60 | result = runCatching { 61 | this@scope.block() 62 | } 63 | 64 | semaphore.release() 65 | } 66 | } 67 | 68 | semaphore.acquire() 69 | thread.interrupt() 70 | Assert.assertThrows(IllegalStateException::class.java) { 71 | result!!.getOrThrow() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | /** 4 | * [sqlite.org journal_mode](https://www.sqlite.org/pragma.html#pragma_journal_mode) 5 | */ 6 | public enum class SqliteJournalMode(internal val value: String) { 7 | Delete("DELETE"), 8 | Truncate("TRUNCATE"), 9 | Persist("PERSIST"), 10 | Memory("MEMORY"), 11 | @Suppress("EnumNaming") 12 | WAL("WAL"), 13 | Off("OFF"), 14 | } 15 | 16 | /** 17 | * [sqlite.org synchronous](https://www.sqlite.org/pragma.html#pragma_synchronous) 18 | */ 19 | public enum class SqliteSync(internal val value: String) { 20 | Off("OFF"), 21 | Normal("NORMAL"), 22 | Full("FULL"), 23 | Extra("EXTRA"), 24 | } 25 | 26 | public class AndroidxSqliteConfiguration( 27 | /** 28 | * The maximum size of the prepared statement cache for each database connection. 29 | * 30 | * Default is 25. 31 | */ 32 | public val cacheSize: Int = 25, 33 | /** 34 | * True if foreign key constraints are enabled. 35 | * 36 | * Default is false. 37 | */ 38 | public var isForeignKeyConstraintsEnabled: Boolean = false, 39 | /** 40 | * Journal mode to use. 41 | * 42 | * Default is [SqliteJournalMode.WAL]. 43 | */ 44 | public var journalMode: SqliteJournalMode = SqliteJournalMode.WAL, 45 | /** 46 | * Synchronous mode to use. 47 | * 48 | * Default is [SqliteSync.Full] unless [journalMode] is set to [SqliteJournalMode.WAL] in which case it is [SqliteSync.Normal]. 49 | */ 50 | public var sync: SqliteSync = when(journalMode) { 51 | SqliteJournalMode.WAL -> SqliteSync.Normal 52 | SqliteJournalMode.Delete, 53 | SqliteJournalMode.Truncate, 54 | SqliteJournalMode.Persist, 55 | SqliteJournalMode.Memory, 56 | SqliteJournalMode.Off, 57 | -> SqliteSync.Full 58 | }, 59 | /** 60 | * The max amount of read connections that will be kept in the [ConnectionPool]. 61 | * 62 | * Defaults to 4 when [journalMode] is [SqliteJournalMode.WAL], otherwise 0 (since reads are blocked by writes). 63 | * 64 | * The default for [SqliteJournalMode.WAL] may be changed in the future to be based on how many CPUs are available. 65 | */ 66 | public val readerConnectionsCount: Int = when(journalMode) { 67 | SqliteJournalMode.WAL -> 4 68 | else -> 0 69 | }, 70 | ) 71 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteDatabaseType.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | public sealed interface AndroidxSqliteDatabaseType { 4 | public data class File(val databaseFilePath: String) : AndroidxSqliteDatabaseType 5 | public data object Memory : AndroidxSqliteDatabaseType 6 | public data object Temporary : AndroidxSqliteDatabaseType 7 | 8 | public companion object 9 | } 10 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteDriver.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import androidx.collection.LruCache 4 | import androidx.sqlite.SQLiteConnection 5 | import androidx.sqlite.SQLiteDriver 6 | import androidx.sqlite.SQLiteStatement 7 | import androidx.sqlite.execSQL 8 | import app.cash.sqldelight.Query 9 | import app.cash.sqldelight.Transacter 10 | import app.cash.sqldelight.TransacterImpl 11 | import app.cash.sqldelight.db.AfterVersion 12 | import app.cash.sqldelight.db.QueryResult 13 | import app.cash.sqldelight.db.SqlCursor 14 | import app.cash.sqldelight.db.SqlDriver 15 | import app.cash.sqldelight.db.SqlPreparedStatement 16 | import app.cash.sqldelight.db.SqlSchema 17 | import kotlinx.atomicfu.atomic 18 | import kotlinx.atomicfu.locks.SynchronizedObject 19 | import kotlinx.atomicfu.locks.synchronized 20 | 21 | internal expect class TransactionsThreadLocal() { 22 | internal fun get(): Transacter.Transaction? 23 | internal fun set(transaction: Transacter.Transaction?) 24 | } 25 | 26 | /** 27 | * @param databaseType Specifies the type of the database file 28 | * (see [Sqlite open documentation](https://www.sqlite.org/c3ref/open.html)). 29 | * 30 | * @see AndroidxSqliteDriver 31 | * @see SqlSchema.create 32 | * @see SqlSchema.migrate 33 | */ 34 | public class AndroidxSqliteDriver( 35 | createConnection: (String) -> SQLiteConnection, 36 | databaseType: AndroidxSqliteDatabaseType, 37 | private val schema: SqlSchema>, 38 | private val configuration: AndroidxSqliteConfiguration = AndroidxSqliteConfiguration(), 39 | private val migrateEmptySchema: Boolean = false, 40 | private val onConfigure: ConfigurableDatabase.() -> Unit = {}, 41 | private val onCreate: AndroidxSqliteDriver.() -> Unit = {}, 42 | private val onUpdate: AndroidxSqliteDriver.(Long, Long) -> Unit = { _, _ -> }, 43 | private val onOpen: AndroidxSqliteDriver.() -> Unit = {}, 44 | connectionPool: ConnectionPool? = null, 45 | vararg migrationCallbacks: AfterVersion, 46 | ) : SqlDriver { 47 | public constructor( 48 | driver: SQLiteDriver, 49 | databaseType: AndroidxSqliteDatabaseType, 50 | schema: SqlSchema>, 51 | configuration: AndroidxSqliteConfiguration = AndroidxSqliteConfiguration(), 52 | migrateEmptySchema: Boolean = false, 53 | onConfigure: ConfigurableDatabase.() -> Unit = {}, 54 | onCreate: SqlDriver.() -> Unit = {}, 55 | onUpdate: SqlDriver.(Long, Long) -> Unit = { _, _ -> }, 56 | onOpen: SqlDriver.() -> Unit = {}, 57 | connectionPool: ConnectionPool? = null, 58 | vararg migrationCallbacks: AfterVersion, 59 | ) : this( 60 | createConnection = driver::open, 61 | databaseType = databaseType, 62 | schema = schema, 63 | configuration = configuration, 64 | migrateEmptySchema = migrateEmptySchema, 65 | onConfigure = onConfigure, 66 | onCreate = onCreate, 67 | onUpdate = onUpdate, 68 | onOpen = onOpen, 69 | connectionPool = connectionPool, 70 | migrationCallbacks = migrationCallbacks, 71 | ) 72 | 73 | @Suppress("NonBooleanPropertyPrefixedWithIs") 74 | private val isFirstInteraction = atomic(true) 75 | 76 | private val connectionPool by lazy { 77 | connectionPool ?: AndroidxDriverConnectionPool( 78 | createConnection = createConnection, 79 | name = when(databaseType) { 80 | is AndroidxSqliteDatabaseType.File -> databaseType.databaseFilePath 81 | AndroidxSqliteDatabaseType.Memory -> ":memory:" 82 | AndroidxSqliteDatabaseType.Temporary -> "" 83 | }, 84 | isFileBased = when(databaseType) { 85 | is AndroidxSqliteDatabaseType.File -> true 86 | AndroidxSqliteDatabaseType.Memory -> false 87 | AndroidxSqliteDatabaseType.Temporary -> false 88 | }, 89 | configuration = configuration, 90 | ) 91 | } 92 | 93 | private val transactions = TransactionsThreadLocal() 94 | 95 | private val statementsCache = HashMap>() 96 | 97 | private fun getStatementCache(connection: SQLiteConnection) = 98 | when { 99 | configuration.cacheSize > 0 -> 100 | statementsCache.getOrPut(connection) { 101 | object : LruCache(configuration.cacheSize) { 102 | override fun entryRemoved( 103 | evicted: Boolean, 104 | key: Int, 105 | oldValue: AndroidxStatement, 106 | newValue: AndroidxStatement?, 107 | ) { 108 | if(evicted) oldValue.close() 109 | } 110 | } 111 | } 112 | 113 | else -> null 114 | } 115 | 116 | private var skipStatementsCache = true 117 | 118 | private val listenersLock = SynchronizedObject() 119 | private val listeners = linkedMapOf>() 120 | 121 | private val migrationCallbacks = migrationCallbacks 122 | 123 | /** 124 | * True if foreign key constraints are enabled. 125 | * 126 | * This function will block until all connections have been updated. 127 | * 128 | * An exception will be thrown if this is called from within a transaction. 129 | */ 130 | public fun setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled: Boolean) { 131 | check(currentTransaction() == null) { 132 | "setForeignKeyConstraintsEnabled cannot be called from within a transaction" 133 | } 134 | 135 | connectionPool.setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled) 136 | } 137 | 138 | /** 139 | * Journal mode to use. 140 | * 141 | * This function will block until all connections have been updated. 142 | * 143 | * An exception will be thrown if this is called from within a transaction. 144 | */ 145 | public fun setJournalMode(journalMode: SqliteJournalMode) { 146 | check(currentTransaction() == null) { 147 | "setJournalMode cannot be called from within a transaction" 148 | } 149 | 150 | connectionPool.setJournalMode(journalMode) 151 | } 152 | 153 | /** 154 | * Synchronous mode to use. 155 | * 156 | * This function will block until all connections have been updated. 157 | * 158 | * An exception will be thrown if this is called from within a transaction. 159 | */ 160 | public fun setSync(sync: SqliteSync) { 161 | check(currentTransaction() == null) { 162 | "setSync cannot be called from within a transaction" 163 | } 164 | 165 | connectionPool.setSync(sync) 166 | } 167 | 168 | override fun addListener(vararg queryKeys: String, listener: Query.Listener) { 169 | synchronized(listenersLock) { 170 | queryKeys.forEach { 171 | listeners.getOrPut(it) { linkedSetOf() }.add(listener) 172 | } 173 | } 174 | } 175 | 176 | override fun removeListener(vararg queryKeys: String, listener: Query.Listener) { 177 | synchronized(listenersLock) { 178 | queryKeys.forEach { 179 | listeners[it]?.remove(listener) 180 | } 181 | } 182 | } 183 | 184 | override fun notifyListeners(vararg queryKeys: String) { 185 | val listenersToNotify = linkedSetOf() 186 | synchronized(listenersLock) { 187 | queryKeys.forEach { listeners[it]?.let(listenersToNotify::addAll) } 188 | } 189 | listenersToNotify.forEach(Query.Listener::queryResultsChanged) 190 | } 191 | 192 | override fun newTransaction(): QueryResult { 193 | createOrMigrateIfNeeded() 194 | 195 | val enclosing = transactions.get() 196 | val transactionConnection = when(enclosing) { 197 | null -> connectionPool.acquireWriterConnection() 198 | else -> (enclosing as Transaction).connection 199 | } 200 | val transaction = Transaction(enclosing, transactionConnection) 201 | if(enclosing == null) { 202 | transactionConnection.execSQL("BEGIN IMMEDIATE") 203 | } 204 | 205 | transactions.set(transaction) 206 | 207 | return QueryResult.Value(transaction) 208 | } 209 | 210 | override fun currentTransaction(): Transacter.Transaction? = transactions.get() 211 | 212 | private inner class Transaction( 213 | override val enclosingTransaction: Transacter.Transaction?, 214 | val connection: SQLiteConnection, 215 | ) : Transacter.Transaction() { 216 | override fun endTransaction(successful: Boolean): QueryResult { 217 | if(enclosingTransaction == null) { 218 | try { 219 | if(successful) { 220 | connection.execSQL("COMMIT") 221 | } else { 222 | connection.execSQL("ROLLBACK") 223 | } 224 | } finally { 225 | connectionPool.releaseWriterConnection() 226 | } 227 | } 228 | transactions.set(enclosingTransaction) 229 | return QueryResult.Unit 230 | } 231 | } 232 | 233 | private fun execute( 234 | identifier: Int?, 235 | connection: SQLiteConnection, 236 | createStatement: (SQLiteConnection) -> AndroidxStatement, 237 | binders: (SqlPreparedStatement.() -> Unit)?, 238 | result: AndroidxStatement.() -> T, 239 | ): QueryResult.Value { 240 | val statementsCache = if(!skipStatementsCache) getStatementCache(connection) else null 241 | var statement: AndroidxStatement? = null 242 | if(identifier != null && statementsCache != null) { 243 | // remove temporarily from the cache if present 244 | statement = statementsCache.remove(identifier) 245 | } 246 | if(statement == null) { 247 | statement = createStatement(connection) 248 | } 249 | try { 250 | if(binders != null) { 251 | statement.binders() 252 | } 253 | return QueryResult.Value(statement.result()) 254 | } finally { 255 | if(identifier != null && !skipStatementsCache) { 256 | statement.reset() 257 | 258 | // put the statement back in the cache 259 | // closing any statement with this identifier 260 | // that was put into the cache while we used this one 261 | statementsCache?.put(identifier, statement)?.close() 262 | } else { 263 | statement.close() 264 | } 265 | } 266 | } 267 | 268 | override fun execute( 269 | identifier: Int?, 270 | sql: String, 271 | parameters: Int, 272 | binders: (SqlPreparedStatement.() -> Unit)?, 273 | ): QueryResult { 274 | createOrMigrateIfNeeded() 275 | 276 | val transaction = currentTransaction() 277 | if(transaction == null) { 278 | val writerConnection = connectionPool.acquireWriterConnection() 279 | try { 280 | return execute( 281 | identifier = identifier, 282 | connection = writerConnection, 283 | createStatement = { c -> 284 | AndroidxPreparedStatement( 285 | sql = sql, 286 | statement = c.prepare(sql), 287 | ) 288 | }, 289 | binders = binders, 290 | result = { execute() }, 291 | ) 292 | } finally { 293 | connectionPool.releaseWriterConnection() 294 | } 295 | } else { 296 | val connection = (transaction as Transaction).connection 297 | return execute( 298 | identifier = identifier, 299 | connection = connection, 300 | createStatement = { c -> 301 | AndroidxPreparedStatement( 302 | sql = sql, 303 | statement = c.prepare(sql), 304 | ) 305 | }, 306 | binders = binders, 307 | result = { execute() }, 308 | ) 309 | } 310 | } 311 | 312 | override fun executeQuery( 313 | identifier: Int?, 314 | sql: String, 315 | mapper: (SqlCursor) -> QueryResult, 316 | parameters: Int, 317 | binders: (SqlPreparedStatement.() -> Unit)?, 318 | ): QueryResult.Value { 319 | createOrMigrateIfNeeded() 320 | 321 | val transaction = currentTransaction() 322 | if(transaction == null) { 323 | val connection = connectionPool.acquireReaderConnection() 324 | try { 325 | return execute( 326 | identifier = identifier, 327 | connection = connection, 328 | createStatement = { c -> 329 | AndroidxQuery( 330 | sql = sql, 331 | statement = c.prepare(sql), 332 | argCount = parameters, 333 | ) 334 | }, 335 | binders = binders, 336 | result = { executeQuery(mapper) }, 337 | ) 338 | } finally { 339 | connectionPool.releaseReaderConnection(connection) 340 | } 341 | } else { 342 | val connection = (transaction as Transaction).connection 343 | return execute( 344 | identifier = identifier, 345 | connection = connection, 346 | createStatement = { c -> 347 | AndroidxQuery( 348 | sql = sql, 349 | statement = c.prepare(sql), 350 | argCount = parameters, 351 | ) 352 | }, 353 | binders = binders, 354 | result = { executeQuery(mapper) }, 355 | ) 356 | } 357 | } 358 | 359 | /** 360 | * It is the caller's responsibility to ensure that no threads 361 | * are using any of the connections starting from when close is invoked 362 | */ 363 | override fun close() { 364 | statementsCache.values.forEach { it.evictAll() } 365 | statementsCache.clear() 366 | connectionPool.close() 367 | } 368 | 369 | private val createOrMigrateLock = SynchronizedObject() 370 | private var isNestedUnderCreateOrMigrate = false 371 | private fun createOrMigrateIfNeeded() { 372 | if(isFirstInteraction.value) { 373 | synchronized(createOrMigrateLock) { 374 | if(isFirstInteraction.value && !isNestedUnderCreateOrMigrate) { 375 | isNestedUnderCreateOrMigrate = true 376 | 377 | ConfigurableDatabase(this).onConfigure() 378 | 379 | val writerConnection = connectionPool.acquireWriterConnection() 380 | val currentVersion = try { 381 | writerConnection.prepare("PRAGMA user_version").use { getVersion -> 382 | when { 383 | getVersion.step() -> getVersion.getLong(0) 384 | else -> 0 385 | } 386 | } 387 | } finally { 388 | connectionPool.releaseWriterConnection() 389 | } 390 | 391 | if(currentVersion == 0L && !migrateEmptySchema || currentVersion < schema.version) { 392 | val driver = this 393 | val transacter = object : TransacterImpl(driver) {} 394 | 395 | transacter.transaction { 396 | when(currentVersion) { 397 | 0L -> schema.create(driver).value 398 | else -> schema.migrate(driver, currentVersion, schema.version, *migrationCallbacks).value 399 | } 400 | skipStatementsCache = configuration.cacheSize == 0 401 | when(currentVersion) { 402 | 0L -> onCreate() 403 | else -> onUpdate(currentVersion, schema.version) 404 | } 405 | writerConnection.prepare("PRAGMA user_version = ${schema.version}").use { it.step() } 406 | } 407 | } else { 408 | skipStatementsCache = configuration.cacheSize == 0 409 | } 410 | 411 | onOpen() 412 | 413 | isFirstInteraction.value = false 414 | } 415 | } 416 | } 417 | } 418 | } 419 | 420 | internal interface AndroidxStatement : SqlPreparedStatement { 421 | fun execute(): Long 422 | fun executeQuery(mapper: (SqlCursor) -> QueryResult): R 423 | fun reset() 424 | fun close() 425 | } 426 | 427 | private class AndroidxPreparedStatement( 428 | private val sql: String, 429 | private val statement: SQLiteStatement, 430 | ) : AndroidxStatement { 431 | override fun bindBytes(index: Int, bytes: ByteArray?) { 432 | if(bytes == null) statement.bindNull(index + 1) else statement.bindBlob(index + 1, bytes) 433 | } 434 | 435 | override fun bindLong(index: Int, long: Long?) { 436 | if(long == null) statement.bindNull(index + 1) else statement.bindLong(index + 1, long) 437 | } 438 | 439 | override fun bindDouble(index: Int, double: Double?) { 440 | if(double == null) statement.bindNull(index + 1) else statement.bindDouble(index + 1, double) 441 | } 442 | 443 | override fun bindString(index: Int, string: String?) { 444 | if(string == null) statement.bindNull(index + 1) else statement.bindText(index + 1, string) 445 | } 446 | 447 | override fun bindBoolean(index: Int, boolean: Boolean?) { 448 | if(boolean == null) { 449 | statement.bindNull(index + 1) 450 | } else { 451 | statement.bindLong(index + 1, if(boolean) 1L else 0L) 452 | } 453 | } 454 | 455 | override fun executeQuery(mapper: (SqlCursor) -> QueryResult): R = 456 | throw UnsupportedOperationException() 457 | 458 | override fun execute(): Long { 459 | var cont = true 460 | while(cont) { 461 | cont = statement.step() 462 | } 463 | return statement.getColumnCount().toLong() 464 | } 465 | 466 | override fun toString() = sql 467 | 468 | override fun reset() { 469 | statement.reset() 470 | } 471 | 472 | override fun close() { 473 | statement.close() 474 | } 475 | } 476 | 477 | private class AndroidxQuery( 478 | private val sql: String, 479 | private val statement: SQLiteStatement, 480 | argCount: Int, 481 | ) : AndroidxStatement { 482 | private val binds = MutableList<((SQLiteStatement) -> Unit)?>(argCount) { null } 483 | 484 | override fun bindBytes(index: Int, bytes: ByteArray?) { 485 | binds[index] = { if(bytes == null) it.bindNull(index + 1) else it.bindBlob(index + 1, bytes) } 486 | } 487 | 488 | override fun bindLong(index: Int, long: Long?) { 489 | binds[index] = { if(long == null) it.bindNull(index + 1) else it.bindLong(index + 1, long) } 490 | } 491 | 492 | override fun bindDouble(index: Int, double: Double?) { 493 | binds[index] = 494 | { if(double == null) it.bindNull(index + 1) else it.bindDouble(index + 1, double) } 495 | } 496 | 497 | override fun bindString(index: Int, string: String?) { 498 | binds[index] = 499 | { if(string == null) it.bindNull(index + 1) else it.bindText(index + 1, string) } 500 | } 501 | 502 | override fun bindBoolean(index: Int, boolean: Boolean?) { 503 | binds[index] = { statement -> 504 | if(boolean == null) { 505 | statement.bindNull(index + 1) 506 | } else { 507 | statement.bindLong(index + 1, if(boolean) 1L else 0L) 508 | } 509 | } 510 | } 511 | 512 | override fun execute() = throw UnsupportedOperationException() 513 | 514 | override fun executeQuery(mapper: (SqlCursor) -> QueryResult): R { 515 | for(action in binds) { 516 | requireNotNull(action).invoke(statement) 517 | } 518 | 519 | return mapper(AndroidxCursor(statement)).value 520 | } 521 | 522 | override fun toString() = sql 523 | 524 | override fun reset() { 525 | statement.reset() 526 | } 527 | 528 | override fun close() { 529 | statement.close() 530 | } 531 | } 532 | 533 | private class AndroidxCursor( 534 | private val statement: SQLiteStatement, 535 | ) : SqlCursor { 536 | 537 | override fun next(): QueryResult.Value = QueryResult.Value(statement.step()) 538 | override fun getString(index: Int) = 539 | if(statement.isNull(index)) null else statement.getText(index) 540 | 541 | override fun getLong(index: Int) = if(statement.isNull(index)) null else statement.getLong(index) 542 | override fun getBytes(index: Int) = 543 | if(statement.isNull(index)) null else statement.getBlob(index) 544 | 545 | override fun getDouble(index: Int) = 546 | if(statement.isNull(index)) null else statement.getDouble(index) 547 | 548 | override fun getBoolean(index: Int) = 549 | if(statement.isNull(index)) null else statement.getLong(index) == 1L 550 | } 551 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteHelpers.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import app.cash.sqldelight.db.QueryResult 4 | import app.cash.sqldelight.db.SqlCursor 5 | import app.cash.sqldelight.db.SqlPreparedStatement 6 | 7 | public class ConfigurableDatabase( 8 | private val driver: AndroidxSqliteDriver, 9 | ) { 10 | public fun setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled: Boolean) { 11 | driver.setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled) 12 | } 13 | 14 | public fun setJournalMode(journalMode: SqliteJournalMode) { 15 | driver.setJournalMode(journalMode) 16 | } 17 | 18 | public fun setSync(sync: SqliteSync) { 19 | driver.setSync(sync) 20 | } 21 | 22 | public fun executePragma( 23 | pragma: String, 24 | parameters: Int = 0, 25 | binders: (SqlPreparedStatement.() -> Unit)? = null, 26 | ) { 27 | driver.execute(null, "PRAGMA $pragma;", parameters, binders) 28 | } 29 | 30 | public fun executePragmaQuery( 31 | pragma: String, 32 | mapper: (SqlCursor) -> QueryResult, 33 | parameters: Int = 0, 34 | binders: (SqlPreparedStatement.() -> Unit)? = null, 35 | ): QueryResult.Value = driver.executeQuery(null, "PRAGMA $pragma;", mapper, parameters, binders) 36 | } 37 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/com/eygraber/sqldelight/androidx/driver/ConnectionPool.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import androidx.sqlite.SQLiteConnection 4 | import androidx.sqlite.SQLiteStatement 5 | import kotlinx.coroutines.channels.Channel 6 | import kotlinx.coroutines.runBlocking 7 | import kotlinx.coroutines.sync.Mutex 8 | import kotlinx.coroutines.sync.withLock 9 | 10 | public interface ConnectionPool : AutoCloseable { 11 | public fun acquireWriterConnection(): SQLiteConnection 12 | public fun releaseWriterConnection() 13 | public fun acquireReaderConnection(): SQLiteConnection 14 | public fun releaseReaderConnection(connection: SQLiteConnection) 15 | public fun setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled: Boolean) 16 | public fun setJournalMode(journalMode: SqliteJournalMode) 17 | public fun setSync(sync: SqliteSync) 18 | } 19 | 20 | internal class AndroidxDriverConnectionPool( 21 | private val createConnection: (String) -> SQLiteConnection, 22 | private val name: String, 23 | private val isFileBased: Boolean, 24 | private val configuration: AndroidxSqliteConfiguration, 25 | ) : ConnectionPool { 26 | private val writerConnection: SQLiteConnection by lazy { 27 | createConnection(name).withConfiguration() 28 | } 29 | private val writerMutex = Mutex() 30 | 31 | private val maxReaderConnectionsCount = when { 32 | isFileBased -> configuration.readerConnectionsCount 33 | else -> 0 34 | } 35 | 36 | private val readerChannel = Channel>(capacity = maxReaderConnectionsCount) 37 | 38 | init { 39 | repeat(maxReaderConnectionsCount) { 40 | readerChannel.trySend( 41 | lazy { 42 | createConnection(name).withConfiguration() 43 | }, 44 | ) 45 | } 46 | } 47 | 48 | /** 49 | * Acquires the writer connection, blocking if it's currently in use. 50 | * @return The writer SQLiteConnection 51 | */ 52 | override fun acquireWriterConnection() = runBlocking { 53 | writerMutex.lock() 54 | writerConnection 55 | } 56 | 57 | /** 58 | * Releases the writer connection (mutex unlocks automatically). 59 | */ 60 | override fun releaseWriterConnection() { 61 | writerMutex.unlock() 62 | } 63 | 64 | /** 65 | * Acquires a reader connection, blocking if none are available. 66 | * @return A reader SQLiteConnection 67 | */ 68 | override fun acquireReaderConnection() = when(maxReaderConnectionsCount) { 69 | 0 -> acquireWriterConnection() 70 | else -> runBlocking { 71 | readerChannel.receive().value 72 | } 73 | } 74 | 75 | /** 76 | * Releases a reader connection back to the pool. 77 | * @param connection The SQLiteConnection to release 78 | */ 79 | override fun releaseReaderConnection(connection: SQLiteConnection) { 80 | when(maxReaderConnectionsCount) { 81 | 0 -> releaseWriterConnection() 82 | else -> runBlocking { 83 | readerChannel.send(lazy { connection }) 84 | } 85 | } 86 | } 87 | 88 | override fun setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled: Boolean) { 89 | configuration.isForeignKeyConstraintsEnabled = isForeignKeyConstraintsEnabled 90 | val foreignKeys = if(isForeignKeyConstraintsEnabled) "ON" else "OFF" 91 | runPragmaOnAllConnections("PRAGMA foreign_keys = $foreignKeys;") 92 | } 93 | 94 | override fun setJournalMode(journalMode: SqliteJournalMode) { 95 | configuration.journalMode = journalMode 96 | runPragmaOnAllConnections("PRAGMA journal_mode = ${configuration.journalMode.value};") 97 | } 98 | 99 | override fun setSync(sync: SqliteSync) { 100 | configuration.sync = sync 101 | runPragmaOnAllConnections("PRAGMA synchronous = ${configuration.sync.value};") 102 | } 103 | 104 | private fun runPragmaOnAllConnections(sql: String) { 105 | val writer = acquireWriterConnection() 106 | try { 107 | writer.writePragma(sql) 108 | } finally { 109 | releaseWriterConnection() 110 | } 111 | 112 | if(maxReaderConnectionsCount > 0) { 113 | runBlocking { 114 | repeat(maxReaderConnectionsCount) { 115 | val reader = readerChannel.receive() 116 | try { 117 | reader.value.writePragma(sql) 118 | } finally { 119 | releaseReaderConnection(reader.value) 120 | } 121 | } 122 | } 123 | } 124 | } 125 | 126 | private fun SQLiteConnection.withConfiguration(): SQLiteConnection = this.apply { 127 | val foreignKeys = if(configuration.isForeignKeyConstraintsEnabled) "ON" else "OFF" 128 | writePragma("PRAGMA foreign_keys = $foreignKeys;") 129 | writePragma("PRAGMA journal_mode = ${configuration.journalMode.value};") 130 | writePragma("PRAGMA synchronous = ${configuration.sync.value};") 131 | } 132 | 133 | /** 134 | * Closes all connections in the pool. 135 | */ 136 | override fun close() { 137 | runBlocking { 138 | writerMutex.withLock { 139 | writerConnection.close() 140 | } 141 | repeat(maxReaderConnectionsCount) { 142 | val reader = readerChannel.receive() 143 | if(reader.isInitialized()) reader.value.close() 144 | } 145 | readerChannel.close() 146 | } 147 | } 148 | } 149 | 150 | private fun SQLiteConnection.writePragma(sql: String) { 151 | prepare(sql).use(SQLiteStatement::step) 152 | } 153 | -------------------------------------------------------------------------------- /library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteCallbackTest.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import app.cash.sqldelight.db.AfterVersion 4 | import app.cash.sqldelight.db.QueryResult 5 | import app.cash.sqldelight.db.SqlDriver 6 | import app.cash.sqldelight.db.SqlSchema 7 | import kotlin.test.AfterTest 8 | import kotlin.test.BeforeTest 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | 12 | abstract class AndroidxSqliteCallbackTest { 13 | private val schema = object : SqlSchema> { 14 | override val version: Long = 1 15 | 16 | override fun create(driver: SqlDriver): QueryResult.Value { 17 | driver.execute( 18 | 0, 19 | """ 20 | |CREATE TABLE test ( 21 | | id INTEGER PRIMARY KEY, 22 | | value TEXT 23 | |); 24 | """.trimMargin(), 25 | 0, 26 | ) 27 | driver.execute( 28 | 1, 29 | """ 30 | |CREATE TABLE nullability_test ( 31 | | id INTEGER PRIMARY KEY, 32 | | integer_value INTEGER, 33 | | text_value TEXT, 34 | | blob_value BLOB, 35 | | real_value REAL 36 | |); 37 | """.trimMargin(), 38 | 0, 39 | ) 40 | return QueryResult.Unit 41 | } 42 | 43 | override fun migrate( 44 | driver: SqlDriver, 45 | oldVersion: Long, 46 | newVersion: Long, 47 | vararg callbacks: AfterVersion, 48 | ) = QueryResult.Unit 49 | } 50 | 51 | private val schemaWithUpdate = object : SqlSchema> { 52 | override val version: Long = 2 53 | 54 | override fun create(driver: SqlDriver): QueryResult.Value { 55 | driver.execute( 56 | 0, 57 | """ 58 | |CREATE TABLE test ( 59 | | id INTEGER PRIMARY KEY, 60 | | value TEXT 61 | |); 62 | """.trimMargin(), 63 | 0, 64 | ) 65 | driver.execute( 66 | 1, 67 | """ 68 | |CREATE TABLE nullability_test ( 69 | | id INTEGER PRIMARY KEY, 70 | | integer_value INTEGER, 71 | | text_value TEXT, 72 | | blob_value BLOB, 73 | | real_value REAL 74 | |); 75 | """.trimMargin(), 76 | 0, 77 | ) 78 | return QueryResult.Unit 79 | } 80 | 81 | override fun migrate( 82 | driver: SqlDriver, 83 | oldVersion: Long, 84 | newVersion: Long, 85 | vararg callbacks: AfterVersion, 86 | ): QueryResult.Value { 87 | if(newVersion == 2L) { 88 | driver.execute( 89 | 0, 90 | """ 91 | |CREATE TABLE test2 ( 92 | | id INTEGER PRIMARY KEY, 93 | | value TEXT 94 | |); 95 | """.trimMargin(), 96 | 0, 97 | ) 98 | } 99 | return QueryResult.Unit 100 | } 101 | } 102 | 103 | private val dbName = "com.eygraber.sqldelight.androidx.driver.test.db" 104 | 105 | private fun setupDatabase( 106 | schema: SqlSchema>, 107 | onConfigure: ConfigurableDatabase.() -> Unit, 108 | onCreate: SqlDriver.() -> Unit, 109 | onUpdate: SqlDriver.(Long, Long) -> Unit, 110 | onOpen: SqlDriver.() -> Unit, 111 | ): SqlDriver = AndroidxSqliteDriver( 112 | driver = androidxSqliteTestDriver(), 113 | databaseType = AndroidxSqliteDatabaseType.File(dbName), 114 | schema = schema, 115 | onConfigure = onConfigure, 116 | onCreate = onCreate, 117 | onUpdate = onUpdate, 118 | onOpen = onOpen, 119 | ) 120 | 121 | @BeforeTest 122 | fun clearNamedDb() { 123 | deleteFile(dbName) 124 | deleteFile("$dbName-shm") 125 | deleteFile("$dbName-wal") 126 | } 127 | 128 | @AfterTest 129 | fun clearNamedDbPostTests() { 130 | deleteFile(dbName) 131 | deleteFile("$dbName-shm") 132 | deleteFile("$dbName-wal") 133 | } 134 | 135 | @Test 136 | fun `create and open callbacks are invoked once when opening a new database`() { 137 | var configure = 0 138 | var create = 0 139 | var update = 0 140 | var open = 0 141 | 142 | val driver = setupDatabase( 143 | schema = schema, 144 | onConfigure = { configure++ }, 145 | onCreate = { create++ }, 146 | onUpdate = { _, _ -> update++ }, 147 | onOpen = { open++ }, 148 | ) 149 | 150 | assertEquals(0, configure) 151 | assertEquals(0, create) 152 | assertEquals(0, update) 153 | assertEquals(0, open) 154 | 155 | driver.execute(null, "PRAGMA user_version", 0) 156 | 157 | assertEquals(1, configure) 158 | assertEquals(1, create) 159 | assertEquals(0, update) 160 | assertEquals(1, open) 161 | } 162 | 163 | @Test 164 | fun `create is invoked once and open is invoked twice when opening a new database closing it and then opening it again`() { 165 | var configure = 0 166 | var create = 0 167 | var update = 0 168 | var open = 0 169 | 170 | var driver = setupDatabase( 171 | schema = schema, 172 | onConfigure = { configure++ }, 173 | onCreate = { create++ }, 174 | onUpdate = { _, _ -> update++ }, 175 | onOpen = { open++ }, 176 | ) 177 | 178 | assertEquals(0, configure) 179 | assertEquals(0, create) 180 | assertEquals(0, update) 181 | assertEquals(0, open) 182 | 183 | driver.execute(null, "PRAGMA user_version", 0) 184 | 185 | assertEquals(1, configure) 186 | assertEquals(1, create) 187 | assertEquals(0, update) 188 | assertEquals(1, open) 189 | 190 | driver.close() 191 | 192 | driver = setupDatabase( 193 | schema = schema, 194 | onConfigure = { configure++ }, 195 | onCreate = { create++ }, 196 | onUpdate = { _, _ -> update++ }, 197 | onOpen = { open++ }, 198 | ) 199 | 200 | assertEquals(1, configure) 201 | assertEquals(1, create) 202 | assertEquals(0, update) 203 | assertEquals(1, open) 204 | 205 | driver.execute(null, "PRAGMA user_version", 0) 206 | 207 | assertEquals(2, configure) 208 | assertEquals(1, create) 209 | assertEquals(0, update) 210 | assertEquals(2, open) 211 | } 212 | 213 | @Test 214 | fun `create is invoked once and open is invoked twice and update is invoked once when opening a new database closing it and then opening it again with a new version`() { 215 | var configure = 0 216 | var create = 0 217 | var update = 0 218 | var open = 0 219 | 220 | var driver = setupDatabase( 221 | schema = schema, 222 | onConfigure = { configure++ }, 223 | onCreate = { create++ }, 224 | onUpdate = { _, _ -> update++ }, 225 | onOpen = { open++ }, 226 | ) 227 | 228 | assertEquals(0, configure) 229 | assertEquals(0, create) 230 | assertEquals(0, update) 231 | assertEquals(0, open) 232 | 233 | driver.execute(null, "PRAGMA user_version", 0) 234 | 235 | assertEquals(1, configure) 236 | assertEquals(1, create) 237 | assertEquals(0, update) 238 | assertEquals(1, open) 239 | 240 | driver.close() 241 | 242 | var fromVersion = -1L 243 | var toVersion = -1L 244 | driver = setupDatabase( 245 | schema = schemaWithUpdate, 246 | onConfigure = { configure++ }, 247 | onCreate = { create++ }, 248 | onUpdate = { from, to -> 249 | fromVersion = from 250 | toVersion = to 251 | update++ 252 | }, 253 | onOpen = { open++ }, 254 | ) 255 | 256 | assertEquals(1, configure) 257 | assertEquals(1, create) 258 | assertEquals(0, update) 259 | assertEquals(-1, fromVersion) 260 | assertEquals(-1, toVersion) 261 | assertEquals(1, open) 262 | 263 | driver.execute(null, "PRAGMA user_version", 0) 264 | 265 | assertEquals(2, configure) 266 | assertEquals(1, create) 267 | assertEquals(1, update) 268 | assertEquals(1, fromVersion) 269 | assertEquals(2, toVersion) 270 | assertEquals(2, open) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteCommonTests.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import androidx.sqlite.SQLiteConnection 4 | import androidx.sqlite.SQLiteDriver 5 | import app.cash.sqldelight.Transacter 6 | import kotlinx.coroutines.CoroutineDispatcher 7 | 8 | expect class CommonCallbackTest() : AndroidxSqliteCallbackTest 9 | expect class CommonConcurrencyTest() : AndroidxSqliteConcurrencyTest 10 | expect class CommonDriverTest() : AndroidxSqliteDriverTest 11 | expect class CommonDriverOpenFlagsTest() : AndroidxSqliteDriverOpenFlagsTest 12 | expect class CommonQueryTest() : AndroidxSqliteQueryTest 13 | expect class CommonTransacterTest() : AndroidxSqliteTransacterTest 14 | 15 | expect class CommonEphemeralTest() : AndroidxSqliteEphemeralTest 16 | 17 | expect fun androidxSqliteTestDriver(): SQLiteDriver 18 | expect fun androidxSqliteTestCreateConnection(): (String) -> SQLiteConnection 19 | 20 | expect val IoDispatcher: CoroutineDispatcher 21 | 22 | expect fun deleteFile(name: String) 23 | 24 | expect inline fun assertChecksThreadConfinement( 25 | transacter: Transacter, 26 | crossinline scope: Transacter.(T.() -> Unit) -> Unit, 27 | crossinline block: T.() -> Unit, 28 | ) 29 | -------------------------------------------------------------------------------- /library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteConcurrencyTest.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import app.cash.sqldelight.TransacterImpl 4 | import app.cash.sqldelight.db.AfterVersion 5 | import app.cash.sqldelight.db.QueryResult 6 | import app.cash.sqldelight.db.SqlDriver 7 | import app.cash.sqldelight.db.SqlSchema 8 | import kotlinx.coroutines.Job 9 | import kotlinx.coroutines.joinAll 10 | import kotlinx.coroutines.launch 11 | import kotlinx.coroutines.test.runTest 12 | import kotlin.test.AfterTest 13 | import kotlin.test.BeforeTest 14 | import kotlin.test.Test 15 | import kotlin.test.assertEquals 16 | 17 | abstract class AndroidxSqliteConcurrencyTest { 18 | private val schema = object : SqlSchema> { 19 | override val version: Long = 1 20 | 21 | override fun create(driver: SqlDriver): QueryResult.Value { 22 | driver.execute( 23 | 0, 24 | """ 25 | |CREATE TABLE test ( 26 | | id INTEGER PRIMARY KEY NOT NULL, 27 | | value TEXT DEFAULT NULL 28 | |); 29 | """.trimMargin(), 30 | 0, 31 | ) 32 | return QueryResult.Unit 33 | } 34 | 35 | override fun migrate( 36 | driver: SqlDriver, 37 | oldVersion: Long, 38 | newVersion: Long, 39 | vararg callbacks: AfterVersion, 40 | ) = QueryResult.Unit 41 | } 42 | 43 | private val dbName = "com.eygraber.sqldelight.androidx.driver.test.db" 44 | 45 | private fun setupDatabase( 46 | schema: SqlSchema>, 47 | onCreate: SqlDriver.() -> Unit, 48 | onUpdate: SqlDriver.(Long, Long) -> Unit, 49 | onOpen: SqlDriver.() -> Unit, 50 | onConfigure: ConfigurableDatabase.() -> Unit = { setJournalMode(SqliteJournalMode.WAL) }, 51 | ): SqlDriver = AndroidxSqliteDriver( 52 | createConnection = androidxSqliteTestCreateConnection(), 53 | databaseType = AndroidxSqliteDatabaseType.File(dbName), 54 | schema = schema, 55 | onConfigure = onConfigure, 56 | onCreate = onCreate, 57 | onUpdate = onUpdate, 58 | onOpen = onOpen, 59 | ) 60 | 61 | @BeforeTest 62 | fun clearNamedDb() { 63 | deleteFile(dbName) 64 | deleteFile("$dbName-shm") 65 | deleteFile("$dbName-wal") 66 | } 67 | 68 | @AfterTest 69 | fun clearNamedDbPostTests() { 70 | clearNamedDb() 71 | } 72 | 73 | @Test 74 | fun `many concurrent transactions are handled in order`() = runTest { 75 | val driver = setupDatabase( 76 | schema = schema, 77 | onCreate = {}, 78 | onUpdate = { _, _ -> }, 79 | onOpen = {}, 80 | ) 81 | val transacter = object : TransacterImpl(driver) {} 82 | 83 | val jobs = mutableListOf() 84 | repeat(200) { a -> 85 | jobs += launch(IoDispatcher) { 86 | if(a.mod(2) == 0) { 87 | transacter.transaction { 88 | val lastId = driver.executeQuery( 89 | identifier = null, 90 | sql = "SELECT id FROM test ORDER BY id DESC LIMIT 1;", 91 | mapper = { cursor -> 92 | if(cursor.next().value) { 93 | QueryResult.Value(cursor.getLong(0) ?: -1L) 94 | } else { 95 | QueryResult.Value(-1L) 96 | } 97 | }, 98 | parameters = 0, 99 | binders = null, 100 | ).value 101 | driver.execute(null, "INSERT INTO test(id) VALUES (${lastId + 1});", 0, null) 102 | } 103 | } 104 | else { 105 | driver.execute(null, "UPDATE test SET value = 'test' WHERE id = 0;", 0, null) 106 | } 107 | } 108 | } 109 | 110 | jobs.joinAll() 111 | 112 | val lastId = driver.executeQuery( 113 | identifier = null, 114 | sql = "SELECT id FROM test ORDER BY id DESC LIMIT 1;", 115 | mapper = { cursor -> 116 | if(cursor.next().value) { 117 | QueryResult.Value(cursor.getLong(0) ?: -1L) 118 | } else { 119 | QueryResult.Value(-1L) 120 | } 121 | }, 122 | parameters = 0, 123 | binders = null, 124 | ).value 125 | 126 | assertEquals(99, lastId) 127 | } 128 | 129 | @Test 130 | fun `callbacks are only invoked once despite many concurrent transactions`() = runTest { 131 | var create = 0 132 | var update = 0 133 | var open = 0 134 | var configure = 0 135 | 136 | val driver = setupDatabase( 137 | schema = schema, 138 | onCreate = { create++ }, 139 | onUpdate = { _, _ -> update++ }, 140 | onOpen = { open++ }, 141 | onConfigure = { configure++ }, 142 | ) 143 | val jobs = mutableListOf() 144 | repeat(100) { 145 | jobs += launch(IoDispatcher) { 146 | driver.execute(null, "PRAGMA journal_mode = WAL;", 0, null) 147 | } 148 | } 149 | 150 | jobs.joinAll() 151 | 152 | assertEquals(1, create) 153 | assertEquals(0, update) 154 | assertEquals(1, open) 155 | assertEquals(1, configure) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteDriverOpenFlagsTest.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import app.cash.sqldelight.Transacter 4 | import app.cash.sqldelight.TransacterImpl 5 | import app.cash.sqldelight.db.AfterVersion 6 | import app.cash.sqldelight.db.QueryResult 7 | import app.cash.sqldelight.db.SqlCursor 8 | import app.cash.sqldelight.db.SqlDriver 9 | import app.cash.sqldelight.db.SqlPreparedStatement 10 | import app.cash.sqldelight.db.SqlSchema 11 | import kotlin.test.AfterTest 12 | import kotlin.test.BeforeTest 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | import kotlin.test.assertFalse 16 | import kotlin.test.assertNull 17 | import kotlin.test.assertTrue 18 | 19 | abstract class AndroidxSqliteDriverOpenFlagsTest { 20 | private lateinit var driver: SqlDriver 21 | private val schema = object : SqlSchema> { 22 | override val version: Long = 1 23 | 24 | override fun create(driver: SqlDriver): QueryResult.Value { 25 | driver.execute( 26 | 0, 27 | """ 28 | |CREATE TABLE test ( 29 | | id INTEGER PRIMARY KEY, 30 | | value TEXT 31 | |); 32 | """.trimMargin(), 33 | 0, 34 | ) 35 | driver.execute( 36 | 1, 37 | """ 38 | |CREATE TABLE nullability_test ( 39 | | id INTEGER PRIMARY KEY, 40 | | integer_value INTEGER, 41 | | text_value TEXT, 42 | | blob_value BLOB, 43 | | real_value REAL 44 | |); 45 | """.trimMargin(), 46 | 0, 47 | ) 48 | return QueryResult.Unit 49 | } 50 | 51 | override fun migrate( 52 | driver: SqlDriver, 53 | oldVersion: Long, 54 | newVersion: Long, 55 | vararg callbacks: AfterVersion, 56 | ) = QueryResult.Unit 57 | } 58 | private var transacter: Transacter? = null 59 | 60 | private fun setupDatabase( 61 | schema: SqlSchema>, 62 | ): SqlDriver = AndroidxSqliteDriver(androidxSqliteTestCreateConnection(), AndroidxSqliteDatabaseType.Memory, schema) 63 | 64 | private fun changes(): Long? = 65 | // wrap in a transaction to ensure read happens on transaction thread/connection 66 | transacter?.transactionWithResult { 67 | val mapper: (SqlCursor) -> QueryResult = { cursor -> 68 | cursor.next() 69 | QueryResult.Value(cursor.getLong(0)) 70 | } 71 | driver.executeQuery(null, "SELECT changes()", mapper, 0).value 72 | } 73 | 74 | @BeforeTest 75 | fun setup() { 76 | driver = setupDatabase(schema = schema) 77 | transacter = object : TransacterImpl(driver) {} 78 | } 79 | 80 | @AfterTest 81 | fun tearDown() { 82 | transacter = null 83 | driver.close() 84 | } 85 | 86 | @Test 87 | fun insertCanRunMultipleTimes() { 88 | val insert = { binders: SqlPreparedStatement.() -> Unit -> 89 | driver.execute(2, "INSERT INTO test VALUES (?, ?);", 2, binders) 90 | } 91 | 92 | fun query(mapper: (SqlCursor) -> QueryResult) { 93 | driver.executeQuery(3, "SELECT * FROM test", mapper, 0) 94 | } 95 | 96 | query { cursor -> 97 | assertFalse(cursor.next().value) 98 | QueryResult.Unit 99 | } 100 | 101 | insert { 102 | bindLong(0, 1) 103 | bindString(1, "Alec") 104 | } 105 | 106 | query { cursor -> 107 | assertTrue(cursor.next().value) 108 | assertFalse(cursor.next().value) 109 | QueryResult.Unit 110 | } 111 | 112 | assertEquals(1, changes()) 113 | 114 | query { cursor -> 115 | assertTrue(cursor.next().value) 116 | assertEquals(1, cursor.getLong(0)) 117 | assertEquals("Alec", cursor.getString(1)) 118 | QueryResult.Unit 119 | } 120 | 121 | insert { 122 | bindLong(0, 2) 123 | bindString(1, "Jake") 124 | } 125 | assertEquals(1, changes()) 126 | 127 | query { cursor -> 128 | assertTrue(cursor.next().value) 129 | assertEquals(1, cursor.getLong(0)) 130 | assertEquals("Alec", cursor.getString(1)) 131 | assertTrue(cursor.next().value) 132 | assertEquals(2, cursor.getLong(0)) 133 | assertEquals("Jake", cursor.getString(1)) 134 | QueryResult.Unit 135 | } 136 | 137 | driver.execute(5, "DELETE FROM test", 0) 138 | assertEquals(2, changes()) 139 | 140 | query { cursor -> 141 | assertFalse(cursor.next().value) 142 | QueryResult.Unit 143 | } 144 | } 145 | 146 | @Test 147 | fun queryCanRunMultipleTimes() { 148 | val insert = { binders: SqlPreparedStatement.() -> Unit -> 149 | driver.execute(2, "INSERT INTO test VALUES (?, ?);", 2, binders) 150 | } 151 | 152 | insert { 153 | bindLong(0, 1) 154 | bindString(1, "Alec") 155 | } 156 | assertEquals(1, changes()) 157 | insert { 158 | bindLong(0, 2) 159 | bindString(1, "Jake") 160 | } 161 | assertEquals(1, changes()) 162 | 163 | fun query(binders: SqlPreparedStatement.() -> Unit, mapper: (SqlCursor) -> QueryResult) { 164 | driver.executeQuery(6, "SELECT * FROM test WHERE value = ?", mapper, 1, binders) 165 | } 166 | 167 | query( 168 | binders = { 169 | bindString(0, "Jake") 170 | }, 171 | mapper = { cursor -> 172 | assertTrue(cursor.next().value) 173 | assertEquals(2, cursor.getLong(0)) 174 | assertEquals("Jake", cursor.getString(1)) 175 | QueryResult.Unit 176 | }, 177 | ) 178 | 179 | // Second time running the query is fine 180 | query( 181 | binders = { 182 | bindString(0, "Jake") 183 | }, 184 | mapper = { cursor -> 185 | assertTrue(cursor.next().value) 186 | assertEquals(2, cursor.getLong(0)) 187 | assertEquals("Jake", cursor.getString(1)) 188 | QueryResult.Unit 189 | }, 190 | ) 191 | } 192 | 193 | @Test 194 | fun sqlResultSetGettersReturnNullIfTheColumnValuesAreNULL() { 195 | val insert = { binders: SqlPreparedStatement.() -> Unit -> 196 | driver.execute(7, "INSERT INTO nullability_test VALUES (?, ?, ?, ?, ?);", 5, binders) 197 | } 198 | insert { 199 | bindLong(0, 1) 200 | bindLong(1, null) 201 | bindString(2, null) 202 | bindBytes(3, null) 203 | bindDouble(4, null) 204 | } 205 | assertEquals(1, changes()) 206 | 207 | val mapper: (SqlCursor) -> QueryResult = { cursor -> 208 | assertTrue(cursor.next().value) 209 | assertEquals(1, cursor.getLong(0)) 210 | assertNull(cursor.getLong(1)) 211 | assertNull(cursor.getString(2)) 212 | assertNull(cursor.getBytes(3)) 213 | assertNull(cursor.getDouble(4)) 214 | QueryResult.Unit 215 | } 216 | driver.executeQuery(8, "SELECT * FROM nullability_test", mapper, 0) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteDriverTest.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import androidx.sqlite.SQLiteException 4 | import app.cash.sqldelight.Transacter 5 | import app.cash.sqldelight.TransacterImpl 6 | import app.cash.sqldelight.db.AfterVersion 7 | import app.cash.sqldelight.db.QueryResult 8 | import app.cash.sqldelight.db.SqlCursor 9 | import app.cash.sqldelight.db.SqlDriver 10 | import app.cash.sqldelight.db.SqlPreparedStatement 11 | import app.cash.sqldelight.db.SqlSchema 12 | import app.cash.sqldelight.db.use 13 | import kotlin.test.AfterTest 14 | import kotlin.test.BeforeTest 15 | import kotlin.test.Test 16 | import kotlin.test.assertEquals 17 | import kotlin.test.assertFalse 18 | import kotlin.test.assertNotSame 19 | import kotlin.test.assertNull 20 | import kotlin.test.assertSame 21 | import kotlin.test.assertTrue 22 | 23 | abstract class AndroidxSqliteDriverTest { 24 | private lateinit var driver: SqlDriver 25 | private val schema = object : SqlSchema> { 26 | override val version: Long = 1 27 | 28 | override fun create(driver: SqlDriver): QueryResult.Value { 29 | driver.execute( 30 | 0, 31 | """ 32 | |CREATE TABLE test ( 33 | | id INTEGER PRIMARY KEY, 34 | | value TEXT 35 | |); 36 | """.trimMargin(), 37 | 0, 38 | ) 39 | driver.execute( 40 | 1, 41 | """ 42 | |CREATE TABLE nullability_test ( 43 | | id INTEGER PRIMARY KEY, 44 | | integer_value INTEGER, 45 | | text_value TEXT, 46 | | blob_value BLOB, 47 | | real_value REAL 48 | |); 49 | """.trimMargin(), 50 | 0, 51 | ) 52 | return QueryResult.Unit 53 | } 54 | 55 | override fun migrate( 56 | driver: SqlDriver, 57 | oldVersion: Long, 58 | newVersion: Long, 59 | vararg callbacks: AfterVersion, 60 | ) = QueryResult.Unit 61 | } 62 | private var transacter: Transacter? = null 63 | 64 | private fun setupDatabase( 65 | schema: SqlSchema>, 66 | ): SqlDriver = AndroidxSqliteDriver(androidxSqliteTestDriver(), AndroidxSqliteDatabaseType.Memory, schema) 67 | 68 | private fun useSingleItemCacheDriver(block: (AndroidxSqliteDriver) -> Unit) { 69 | AndroidxSqliteDriver( 70 | androidxSqliteTestDriver(), 71 | AndroidxSqliteDatabaseType.Memory, 72 | schema, 73 | AndroidxSqliteConfiguration(cacheSize = 1), 74 | ).use(block) 75 | } 76 | 77 | private fun changes(): Long? = 78 | // wrap in a transaction to ensure read happens on transaction thread/connection 79 | transacter?.transactionWithResult { 80 | val mapper: (SqlCursor) -> QueryResult = { cursor -> 81 | cursor.next() 82 | QueryResult.Value(cursor.getLong(0)) 83 | } 84 | driver.executeQuery(null, "SELECT changes()", mapper, 0).value 85 | } 86 | 87 | @BeforeTest 88 | fun setup() { 89 | driver = setupDatabase(schema = schema) 90 | transacter = object : TransacterImpl(driver) {} 91 | } 92 | 93 | @AfterTest 94 | fun tearDown() { 95 | transacter = null 96 | driver.close() 97 | } 98 | 99 | @Test 100 | fun insertCanRunMultipleTimes() { 101 | val insert = { binders: SqlPreparedStatement.() -> Unit -> 102 | driver.execute(2, "INSERT INTO test VALUES (?, ?);", 2, binders) 103 | } 104 | 105 | fun query(mapper: (SqlCursor) -> QueryResult) { 106 | driver.executeQuery(3, "SELECT * FROM test", mapper, 0) 107 | } 108 | 109 | query { cursor -> 110 | assertFalse(cursor.next().value) 111 | QueryResult.Unit 112 | } 113 | 114 | insert { 115 | bindLong(0, 1) 116 | bindString(1, "Alec") 117 | } 118 | 119 | query { cursor -> 120 | assertTrue(cursor.next().value) 121 | assertFalse(cursor.next().value) 122 | QueryResult.Unit 123 | } 124 | 125 | assertEquals(1, changes()) 126 | 127 | query { cursor -> 128 | assertTrue(cursor.next().value) 129 | assertEquals(1, cursor.getLong(0)) 130 | assertEquals("Alec", cursor.getString(1)) 131 | QueryResult.Unit 132 | } 133 | 134 | insert { 135 | bindLong(0, 2) 136 | bindString(1, "Jake") 137 | } 138 | assertEquals(1, changes()) 139 | 140 | query { cursor -> 141 | assertTrue(cursor.next().value) 142 | assertEquals(1, cursor.getLong(0)) 143 | assertEquals("Alec", cursor.getString(1)) 144 | assertTrue(cursor.next().value) 145 | assertEquals(2, cursor.getLong(0)) 146 | assertEquals("Jake", cursor.getString(1)) 147 | QueryResult.Unit 148 | } 149 | 150 | driver.execute(5, "DELETE FROM test", 0) 151 | assertEquals(2, changes()) 152 | 153 | query { cursor -> 154 | assertFalse(cursor.next().value) 155 | QueryResult.Unit 156 | } 157 | } 158 | 159 | @Test 160 | fun queryCanRunMultipleTimes() { 161 | val insert = { binders: SqlPreparedStatement.() -> Unit -> 162 | driver.execute(2, "INSERT INTO test VALUES (?, ?);", 2, binders) 163 | } 164 | 165 | insert { 166 | bindLong(0, 1) 167 | bindString(1, "Alec") 168 | } 169 | assertEquals(1, changes()) 170 | insert { 171 | bindLong(0, 2) 172 | bindString(1, "Jake") 173 | } 174 | assertEquals(1, changes()) 175 | 176 | fun query(binders: SqlPreparedStatement.() -> Unit, mapper: (SqlCursor) -> QueryResult) { 177 | driver.executeQuery(6, "SELECT * FROM test WHERE value = ?", mapper, 1, binders) 178 | } 179 | 180 | query( 181 | binders = { 182 | bindString(0, "Jake") 183 | }, 184 | mapper = { cursor -> 185 | assertTrue(cursor.next().value) 186 | assertEquals(2, cursor.getLong(0)) 187 | assertEquals("Jake", cursor.getString(1)) 188 | QueryResult.Unit 189 | }, 190 | ) 191 | 192 | // Second time running the query is fine 193 | query( 194 | binders = { 195 | bindString(0, "Jake") 196 | }, 197 | mapper = { cursor -> 198 | assertTrue(cursor.next().value) 199 | assertEquals(2, cursor.getLong(0)) 200 | assertEquals("Jake", cursor.getString(1)) 201 | QueryResult.Unit 202 | }, 203 | ) 204 | } 205 | 206 | @Test 207 | fun sqlResultSetGettersReturnNullIfTheColumnValuesAreNULL() { 208 | val insert = { binders: SqlPreparedStatement.() -> Unit -> 209 | driver.execute(7, "INSERT INTO nullability_test VALUES (?, ?, ?, ?, ?);", 5, binders) 210 | } 211 | insert { 212 | bindLong(0, 1) 213 | bindLong(1, null) 214 | bindString(2, null) 215 | bindBytes(3, null) 216 | bindDouble(4, null) 217 | } 218 | assertEquals(1, changes()) 219 | 220 | val mapper: (SqlCursor) -> QueryResult = { cursor -> 221 | assertTrue(cursor.next().value) 222 | assertEquals(1, cursor.getLong(0)) 223 | assertNull(cursor.getLong(1)) 224 | assertNull(cursor.getString(2)) 225 | assertNull(cursor.getBytes(3)) 226 | assertNull(cursor.getDouble(4)) 227 | QueryResult.Unit 228 | } 229 | driver.executeQuery(8, "SELECT * FROM nullability_test", mapper, 0) 230 | } 231 | 232 | @Test 233 | fun `cached statement can be reused`() { 234 | useSingleItemCacheDriver { driver -> 235 | lateinit var bindable: SqlPreparedStatement 236 | driver.executeQuery(2, "SELECT * FROM test", { QueryResult.Unit }, 0, { bindable = this }) 237 | 238 | driver.executeQuery( 239 | 2, 240 | "SELECT * FROM test", 241 | { QueryResult.Unit }, 242 | 0, 243 | { 244 | assertSame(bindable, this) 245 | }, 246 | ) 247 | } 248 | } 249 | 250 | @Test 251 | fun `cached statement is evicted and closed`() { 252 | useSingleItemCacheDriver { driver -> 253 | lateinit var bindable: SqlPreparedStatement 254 | driver.executeQuery(2, "SELECT * FROM test", { QueryResult.Unit }, 0, { bindable = this }) 255 | 256 | driver.executeQuery(3, "SELECT * FROM test", { QueryResult.Unit }, 0) 257 | 258 | driver.executeQuery( 259 | 2, 260 | "SELECT * FROM test", 261 | { QueryResult.Unit }, 262 | 0, 263 | { 264 | assertNotSame(bindable, this) 265 | }, 266 | ) 267 | } 268 | } 269 | 270 | @Test 271 | fun `uncached statement is closed`() { 272 | useSingleItemCacheDriver { driver -> 273 | lateinit var bindable: AndroidxStatement 274 | driver.execute(null, "SELECT * FROM test", 0) { 275 | bindable = this as AndroidxStatement 276 | } 277 | 278 | try { 279 | bindable.execute() 280 | throw AssertionError("Expected an IllegalStateException (attempt to re-open an already-closed object)") 281 | } catch(ignored: SQLiteException) { 282 | } 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteEphemeralTest.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import app.cash.sqldelight.Query 4 | import app.cash.sqldelight.db.AfterVersion 5 | import app.cash.sqldelight.db.QueryResult 6 | import app.cash.sqldelight.db.SqlCursor 7 | import app.cash.sqldelight.db.SqlDriver 8 | import app.cash.sqldelight.db.SqlSchema 9 | import kotlin.test.AfterTest 10 | import kotlin.test.BeforeTest 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertNull 14 | 15 | /** 16 | * Test for SQLite ephemeral database configurations 17 | * */ 18 | abstract class AndroidxSqliteEphemeralTest { 19 | private enum class Type { 20 | IN_MEMORY, 21 | NAMED, 22 | TEMPORARY, 23 | } 24 | 25 | private val schema = object : SqlSchema> { 26 | override val version: Long = 1 27 | 28 | override fun create(driver: SqlDriver): QueryResult.Value { 29 | driver.execute( 30 | null, 31 | """ 32 | CREATE TABLE test ( 33 | id INTEGER NOT NULL PRIMARY KEY, 34 | value TEXT NOT NULL 35 | ); 36 | """.trimIndent(), 37 | 0, 38 | ) 39 | return QueryResult.Unit 40 | } 41 | 42 | override fun migrate( 43 | driver: SqlDriver, 44 | oldVersion: Long, 45 | newVersion: Long, 46 | vararg callbacks: AfterVersion, 47 | ) = QueryResult.Unit // No-op. 48 | } 49 | 50 | private val mapper = { cursor: SqlCursor -> 51 | TestData( 52 | cursor.getLong(0)!!, 53 | cursor.getString(1)!!, 54 | ) 55 | } 56 | 57 | private val dbName = "com.eygraber.sqldelight.androidx.driver.test.db" 58 | 59 | private fun setupDatabase( 60 | type: Type, 61 | ): SqlDriver = when(type) { 62 | Type.IN_MEMORY -> AndroidxSqliteDriver(androidxSqliteTestDriver(), AndroidxSqliteDatabaseType.Memory, schema) 63 | Type.NAMED -> AndroidxSqliteDriver(androidxSqliteTestDriver(), AndroidxSqliteDatabaseType.File(dbName), schema) 64 | Type.TEMPORARY -> AndroidxSqliteDriver(androidxSqliteTestDriver(), AndroidxSqliteDatabaseType.Temporary, schema) 65 | } 66 | 67 | @BeforeTest 68 | fun clearNamedDb() { 69 | deleteFile(dbName) 70 | deleteFile("$dbName-shm") 71 | deleteFile("$dbName-wal") 72 | } 73 | 74 | @AfterTest 75 | fun clearNamedDbPostTests() { 76 | deleteFile(dbName) 77 | deleteFile("$dbName-shm") 78 | deleteFile("$dbName-wal") 79 | } 80 | 81 | @Test 82 | fun inMemoryCreatesIndependentDatabase() { 83 | val data1 = TestData(1, "val1") 84 | val driver1 = setupDatabase(Type.IN_MEMORY) 85 | driver1.insertTestData(data1) 86 | assertEquals(data1, driver1.testDataQuery().executeAsOne()) 87 | 88 | val driver2 = setupDatabase(Type.IN_MEMORY) 89 | assertNull(driver2.testDataQuery().executeAsOneOrNull()) 90 | driver1.close() 91 | driver2.close() 92 | } 93 | 94 | @Test 95 | fun temporaryCreatesIndependentDatabase() { 96 | val data1 = TestData(1, "val1") 97 | val driver1 = setupDatabase(Type.TEMPORARY) 98 | driver1.insertTestData(data1) 99 | assertEquals(data1, driver1.testDataQuery().executeAsOne()) 100 | 101 | val driver2 = setupDatabase(Type.TEMPORARY) 102 | assertNull(driver2.testDataQuery().executeAsOneOrNull()) 103 | driver1.close() 104 | driver2.close() 105 | } 106 | 107 | @Test 108 | fun namedCreatesSharedDatabase() { 109 | val data1 = TestData(1, "val1") 110 | val driver1 = setupDatabase(Type.NAMED) 111 | driver1.insertTestData(data1) 112 | assertEquals(data1, driver1.testDataQuery().executeAsOne()) 113 | 114 | val driver2 = setupDatabase(Type.NAMED) 115 | assertEquals(data1, driver2.testDataQuery().executeAsOne()) 116 | driver1.close() 117 | assertEquals(data1, driver2.testDataQuery().executeAsOne()) 118 | driver2.close() 119 | 120 | val driver3 = setupDatabase(Type.NAMED) 121 | assertEquals(data1, driver3.testDataQuery().executeAsOne()) 122 | driver3.close() 123 | } 124 | 125 | private fun SqlDriver.insertTestData(testData: TestData) { 126 | execute(1, "INSERT INTO test VALUES (?, ?)", 2) { 127 | bindLong(0, testData.id) 128 | bindString(1, testData.value) 129 | } 130 | } 131 | 132 | private fun SqlDriver.testDataQuery(): Query = object : Query(mapper) { 133 | override fun execute( 134 | mapper: (SqlCursor) -> QueryResult, 135 | ): QueryResult = executeQuery(0, "SELECT * FROM test", mapper, 0, null) 136 | 137 | override fun addListener(listener: Listener) { 138 | addListener("test", listener = listener) 139 | } 140 | 141 | override fun removeListener(listener: Listener) { 142 | removeListener("test", listener = listener) 143 | } 144 | } 145 | 146 | private data class TestData(val id: Long, val value: String) 147 | } 148 | -------------------------------------------------------------------------------- /library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteQueryTest.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import app.cash.sqldelight.Query 4 | import app.cash.sqldelight.db.AfterVersion 5 | import app.cash.sqldelight.db.QueryResult 6 | import app.cash.sqldelight.db.SqlCursor 7 | import app.cash.sqldelight.db.SqlDriver 8 | import app.cash.sqldelight.db.SqlSchema 9 | import kotlin.test.AfterTest 10 | import kotlin.test.BeforeTest 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertNull 14 | import kotlin.test.assertTrue 15 | 16 | abstract class AndroidxSqliteQueryTest { 17 | private val mapper = { cursor: SqlCursor -> 18 | TestData( 19 | cursor.getLong(0)!!, 20 | cursor.getString(1)!!, 21 | ) 22 | } 23 | 24 | private lateinit var driver: SqlDriver 25 | 26 | private fun setupDatabase( 27 | schema: SqlSchema>, 28 | ): SqlDriver = AndroidxSqliteDriver(androidxSqliteTestDriver(), AndroidxSqliteDatabaseType.Memory, schema) 29 | 30 | @BeforeTest 31 | fun setup() { 32 | driver = setupDatabase( 33 | schema = object : SqlSchema> { 34 | override val version: Long = 1 35 | 36 | override fun create(driver: SqlDriver): QueryResult.Value { 37 | driver.execute( 38 | null, 39 | """ 40 | CREATE TABLE test ( 41 | id INTEGER NOT NULL PRIMARY KEY, 42 | value TEXT NOT NULL 43 | ); 44 | """.trimIndent(), 45 | 0, 46 | ) 47 | return QueryResult.Unit 48 | } 49 | 50 | override fun migrate( 51 | driver: SqlDriver, 52 | oldVersion: Long, 53 | newVersion: Long, 54 | vararg callbacks: AfterVersion, 55 | ) = QueryResult.Unit // No-op. 56 | }, 57 | ) 58 | } 59 | 60 | @AfterTest 61 | fun tearDown() { 62 | driver.close() 63 | } 64 | 65 | @Test 66 | fun executeAsOne() { 67 | val data1 = TestData(1, "val1") 68 | insertTestData(data1) 69 | 70 | assertEquals(data1, testDataQuery().executeAsOne()) 71 | } 72 | 73 | @Test 74 | fun executeAsOneTwoTimes() { 75 | val data1 = TestData(1, "val1") 76 | insertTestData(data1) 77 | 78 | val query = testDataQuery() 79 | 80 | assertEquals(query.executeAsOne(), query.executeAsOne()) 81 | } 82 | 83 | @Test 84 | fun executeAsOneThrowsNpeForNoRows() { 85 | try { 86 | testDataQuery().executeAsOne() 87 | throw AssertionError("Expected an IllegalStateException") 88 | } catch(ignored: NullPointerException) { 89 | } 90 | } 91 | 92 | @Test 93 | fun executeAsOneThrowsIllegalStateExceptionForManyRows() { 94 | try { 95 | insertTestData(TestData(1, "val1")) 96 | insertTestData(TestData(2, "val2")) 97 | 98 | testDataQuery().executeAsOne() 99 | throw AssertionError("Expected an IllegalStateException") 100 | } catch(ignored: IllegalStateException) { 101 | } 102 | } 103 | 104 | @Test 105 | fun executeAsOneOrNull() { 106 | val data1 = TestData(1, "val1") 107 | insertTestData(data1) 108 | 109 | val query = testDataQuery() 110 | assertEquals(data1, query.executeAsOneOrNull()) 111 | } 112 | 113 | @Test 114 | fun executeAsOneOrNullReturnsNullForNoRows() { 115 | assertNull(testDataQuery().executeAsOneOrNull()) 116 | } 117 | 118 | @Test 119 | fun executeAsOneOrNullThrowsIllegalStateExceptionForManyRows() { 120 | try { 121 | insertTestData(TestData(1, "val1")) 122 | insertTestData(TestData(2, "val2")) 123 | 124 | testDataQuery().executeAsOneOrNull() 125 | throw AssertionError("Expected an IllegalStateException") 126 | } catch(ignored: IllegalStateException) { 127 | } 128 | } 129 | 130 | @Test 131 | fun executeAsList() { 132 | val data1 = TestData(1, "val1") 133 | val data2 = TestData(2, "val2") 134 | 135 | insertTestData(data1) 136 | insertTestData(data2) 137 | 138 | assertEquals(listOf(data1, data2), testDataQuery().executeAsList()) 139 | } 140 | 141 | @Test 142 | fun executeAsListForNoRows() { 143 | assertTrue(testDataQuery().executeAsList().isEmpty()) 144 | } 145 | 146 | @Test 147 | fun notifyDataChangedNotifiesListeners() { 148 | var notifies = 0 149 | val query = testDataQuery() 150 | val listener = Query.Listener { notifies++ } 151 | 152 | query.addListener(listener) 153 | assertEquals(0, notifies) 154 | 155 | driver.notifyListeners("test") 156 | assertEquals(1, notifies) 157 | } 158 | 159 | @Test 160 | fun removeListenerActuallyRemovesListener() { 161 | var notifies = 0 162 | val query = testDataQuery() 163 | val listener = Query.Listener { notifies++ } 164 | 165 | query.addListener(listener) 166 | query.removeListener(listener) 167 | driver.notifyListeners("test") 168 | assertEquals(0, notifies) 169 | } 170 | 171 | private fun insertTestData(testData: TestData) { 172 | driver.execute(1, "INSERT INTO test VALUES (?, ?)", 2) { 173 | bindLong(0, testData.id) 174 | bindString(1, testData.value) 175 | } 176 | } 177 | 178 | private fun testDataQuery(): Query = object : Query(mapper) { 179 | override fun execute( 180 | mapper: (SqlCursor) -> QueryResult, 181 | ): QueryResult = driver.executeQuery(0, "SELECT * FROM test", mapper, 0, null) 182 | 183 | override fun addListener(listener: Listener) { 184 | driver.addListener("test", listener = listener) 185 | } 186 | 187 | override fun removeListener(listener: Listener) { 188 | driver.removeListener("test", listener = listener) 189 | } 190 | } 191 | 192 | private data class TestData(val id: Long, val value: String) 193 | } 194 | -------------------------------------------------------------------------------- /library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteTransacterTest.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import androidx.sqlite.SQLiteConnection 4 | import app.cash.sqldelight.TransacterImpl 5 | import app.cash.sqldelight.db.AfterVersion 6 | import app.cash.sqldelight.db.QueryResult 7 | import app.cash.sqldelight.db.SqlDriver 8 | import app.cash.sqldelight.db.SqlSchema 9 | import kotlin.test.AfterTest 10 | import kotlin.test.BeforeTest 11 | import kotlin.test.Test 12 | import kotlin.test.assertEquals 13 | import kotlin.test.assertFails 14 | import kotlin.test.assertFailsWith 15 | import kotlin.test.assertNull 16 | import kotlin.test.assertTrue 17 | 18 | abstract class AndroidxSqliteTransacterTest { 19 | private lateinit var transacter: TransacterImpl 20 | private lateinit var driver: SqlDriver 21 | 22 | private fun setupDatabase( 23 | schema: SqlSchema>, 24 | connectionPool: ConnectionPool? = null, 25 | ): SqlDriver = AndroidxSqliteDriver( 26 | driver = androidxSqliteTestDriver(), 27 | databaseType = AndroidxSqliteDatabaseType.Memory, 28 | schema = schema, 29 | connectionPool = connectionPool, 30 | ) 31 | 32 | @BeforeTest 33 | fun setup() { 34 | val driver = setupDatabase( 35 | object : SqlSchema> { 36 | override val version = 1L 37 | override fun create(driver: SqlDriver): QueryResult.Value = QueryResult.Unit 38 | override fun migrate( 39 | driver: SqlDriver, 40 | oldVersion: Long, 41 | newVersion: Long, 42 | vararg callbacks: AfterVersion, 43 | ): QueryResult.Value = QueryResult.Unit 44 | }, 45 | ) 46 | transacter = object : TransacterImpl(driver) {} 47 | this.driver = driver 48 | } 49 | 50 | @AfterTest 51 | fun teardown() { 52 | driver.close() 53 | } 54 | 55 | @Test 56 | fun ifBeginningANonEnclosedTransactionFails_furtherTransactionsAreNotBlockedFromBeginning() { 57 | this.driver.close() 58 | 59 | val driver = setupDatabase( 60 | object : SqlSchema> { 61 | override val version = 1L 62 | override fun create(driver: SqlDriver): QueryResult.Value = QueryResult.Unit 63 | override fun migrate( 64 | driver: SqlDriver, 65 | oldVersion: Long, 66 | newVersion: Long, 67 | vararg callbacks: AfterVersion, 68 | ): QueryResult.Value = QueryResult.Unit 69 | }, 70 | connectionPool = FirstTransactionsFailConnectionPool(), 71 | ) 72 | val transacter = object : TransacterImpl(driver) {} 73 | this.driver = driver 74 | assertFails { 75 | transacter.transaction(noEnclosing = true) {} 76 | } 77 | assertNull(driver.currentTransaction()) 78 | transacter.transaction(noEnclosing = true) {} 79 | } 80 | 81 | @Test 82 | fun afterCommitRunsAfterTransactionCommits() { 83 | var counter = 0 84 | transacter.transaction { 85 | afterCommit { counter++ } 86 | assertEquals(0, counter) 87 | } 88 | 89 | assertEquals(1, counter) 90 | } 91 | 92 | @Test 93 | fun afterCommitDoesNotRunAfterTransactionRollbacks() { 94 | var counter = 0 95 | transacter.transaction { 96 | afterCommit { counter++ } 97 | assertEquals(0, counter) 98 | rollback() 99 | } 100 | 101 | assertEquals(0, counter) 102 | } 103 | 104 | @Test 105 | fun afterCommitRunsAfterEnclosingTransactionCommits() { 106 | var counter = 0 107 | transacter.transaction { 108 | afterCommit { counter++ } 109 | assertEquals(0, counter) 110 | 111 | transaction { 112 | afterCommit { counter++ } 113 | assertEquals(0, counter) 114 | } 115 | 116 | assertEquals(0, counter) 117 | } 118 | 119 | assertEquals(2, counter) 120 | } 121 | 122 | @Test 123 | fun afterCommitDoesNotRunInNestedTransactionWhenEnclosingRollsBack() { 124 | var counter = 0 125 | transacter.transaction { 126 | afterCommit { counter++ } 127 | assertEquals(0, counter) 128 | 129 | transaction { 130 | afterCommit { counter++ } 131 | } 132 | 133 | rollback() 134 | } 135 | 136 | assertEquals(0, counter) 137 | } 138 | 139 | @Test 140 | fun afterCommitDoesNotRunInNestedTransactionWhenNestedRollsBack() { 141 | var counter = 0 142 | transacter.transaction { 143 | afterCommit { counter++ } 144 | assertEquals(0, counter) 145 | 146 | transaction { 147 | afterCommit { counter++ } 148 | rollback() 149 | } 150 | 151 | throw AssertionError() 152 | } 153 | 154 | assertEquals(0, counter) 155 | } 156 | 157 | @Test 158 | fun afterRollbackNoOpsIfTheTransactionNeverRollsBack() { 159 | var counter = 0 160 | transacter.transaction { 161 | afterRollback { counter++ } 162 | } 163 | 164 | assertEquals(0, counter) 165 | } 166 | 167 | @Test 168 | fun afterRollbackRunsAfterARollbackOccurs() { 169 | var counter = 0 170 | transacter.transaction { 171 | afterRollback { counter++ } 172 | rollback() 173 | } 174 | 175 | assertEquals(1, counter) 176 | } 177 | 178 | @Test 179 | fun afterRollbackRunsAfterAnInnerTransactionRollsBack() { 180 | var counter = 0 181 | transacter.transaction { 182 | afterRollback { counter++ } 183 | transaction { 184 | rollback() 185 | } 186 | throw AssertionError() 187 | } 188 | 189 | assertEquals(1, counter) 190 | } 191 | 192 | @Test 193 | fun afterRollbackRunsInAnInnerTransactionWhenTheOuterTransactionRollsBack() { 194 | var counter = 0 195 | transacter.transaction { 196 | transaction { 197 | afterRollback { counter++ } 198 | } 199 | rollback() 200 | } 201 | 202 | assertEquals(1, counter) 203 | } 204 | 205 | @Test 206 | fun transactionsCloseThemselvesOutProperly() { 207 | var counter = 0 208 | transacter.transaction { 209 | afterCommit { counter++ } 210 | } 211 | 212 | transacter.transaction { 213 | afterCommit { counter++ } 214 | } 215 | 216 | assertEquals(2, counter) 217 | } 218 | 219 | @Test 220 | fun settingNoEnclosingFailsIfThereIsACurrentlyRunningTransaction() { 221 | transacter.transaction(noEnclosing = true) { 222 | assertFailsWith { 223 | transacter.transaction(noEnclosing = true) { 224 | throw AssertionError() 225 | } 226 | } 227 | } 228 | } 229 | 230 | @Test 231 | fun anExceptionThrownInPostRollbackFunctionIsCombinedWithTheExceptionInTheMainBody() { 232 | class ExceptionA : RuntimeException() 233 | class ExceptionB : RuntimeException() 234 | 235 | val t = assertFailsWith { 236 | transacter.transaction { 237 | afterRollback { 238 | throw ExceptionA() 239 | } 240 | throw ExceptionB() 241 | } 242 | } 243 | assertTrue("Exception thrown in body not in message($t)") { t.toString().contains("ExceptionA") } 244 | assertTrue("Exception thrown in rollback not in message($t)") { t.toString().contains("ExceptionB") } 245 | } 246 | 247 | @Test 248 | fun weCanReturnAValueFromATransaction() { 249 | val result: String = transacter.transactionWithResult { "sup" } 250 | 251 | assertEquals(result, "sup") 252 | } 253 | 254 | @Test 255 | fun weCanRollbackWithValueFromATransaction() { 256 | val result: String = transacter.transactionWithResult { 257 | rollback("rollback") 258 | 259 | @Suppress("UNREACHABLE_CODE") 260 | "sup" 261 | } 262 | 263 | assertEquals(result, "rollback") 264 | } 265 | 266 | @Test 267 | fun `detect the afterRollback call has escaped the original transaction thread in transaction`() { 268 | assertChecksThreadConfinement( 269 | transacter = transacter, 270 | scope = { transaction(false, it) }, 271 | block = { afterRollback {} }, 272 | ) 273 | } 274 | 275 | @Test 276 | fun `detect the afterCommit call has escaped the original transaction thread in transaction`() { 277 | assertChecksThreadConfinement( 278 | transacter = transacter, 279 | scope = { transaction(false, it) }, 280 | block = { afterCommit {} }, 281 | ) 282 | } 283 | 284 | @Test 285 | fun `detect the rollback call has escaped the original transaction thread in transaction`() { 286 | assertChecksThreadConfinement( 287 | transacter = transacter, 288 | scope = { transaction(false, it) }, 289 | block = { rollback() }, 290 | ) 291 | } 292 | 293 | @Test 294 | fun `detect the afterRollback call has escaped the original transaction thread in transactionWithReturn`() { 295 | assertChecksThreadConfinement( 296 | transacter = transacter, 297 | scope = { transactionWithResult(false, it) }, 298 | block = { afterRollback {} }, 299 | ) 300 | } 301 | 302 | @Test 303 | fun `detect the afterCommit call has escaped the original transaction thread in transactionWithReturn`() { 304 | assertChecksThreadConfinement( 305 | transacter = transacter, 306 | scope = { transactionWithResult(false, it) }, 307 | block = { afterCommit {} }, 308 | ) 309 | } 310 | 311 | @Test 312 | fun `detect the rollback call has escaped the original transaction thread in transactionWithReturn`() { 313 | assertChecksThreadConfinement( 314 | transacter = transacter, 315 | scope = { transactionWithResult(false, it) }, 316 | block = { rollback(Unit) }, 317 | ) 318 | } 319 | } 320 | 321 | private class FirstTransactionsFailConnectionPool : ConnectionPool { 322 | private val firstTransactionFailConnection = object : SQLiteConnection { 323 | private var isFirstBeginTransaction = true 324 | 325 | private val connection = androidxSqliteTestDriver().open(":memory:") 326 | 327 | override fun close() { 328 | connection.close() 329 | } 330 | 331 | override fun prepare(sql: String) = 332 | if(sql == "BEGIN IMMEDIATE" && isFirstBeginTransaction) { 333 | isFirstBeginTransaction = false 334 | error("Throwing an error") 335 | } 336 | else { 337 | connection.prepare(sql) 338 | } 339 | } 340 | 341 | override fun close() { 342 | firstTransactionFailConnection.close() 343 | } 344 | override fun acquireWriterConnection() = firstTransactionFailConnection 345 | override fun releaseWriterConnection() {} 346 | override fun acquireReaderConnection() = firstTransactionFailConnection 347 | override fun releaseReaderConnection(connection: SQLiteConnection) {} 348 | override fun setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled: Boolean) {} 349 | override fun setJournalMode(journalMode: SqliteJournalMode) {} 350 | override fun setSync(sync: SqliteSync) {} 351 | } 352 | -------------------------------------------------------------------------------- /library/src/jvmMain/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteDatabaseType.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import java.io.File as JavaFile 4 | 5 | public fun AndroidxSqliteDatabaseType.Companion.File( 6 | file: JavaFile, 7 | ): AndroidxSqliteDatabaseType.File = AndroidxSqliteDatabaseType.File(file.absolutePath) 8 | -------------------------------------------------------------------------------- /library/src/jvmMain/kotlin/com/eygraber/sqldelight/androidx/driver/TransactionsThreadLocal.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import app.cash.sqldelight.Transacter 4 | 5 | internal actual class TransactionsThreadLocal actual constructor() { 6 | private val transactions = ThreadLocal() 7 | 8 | internal actual fun get(): Transacter.Transaction? = transactions.get() 9 | 10 | internal actual fun set(transaction: Transacter.Transaction?) { 11 | transactions.set(transaction) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /library/src/jvmTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteCommonTests.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import androidx.sqlite.SQLiteConnection 4 | import androidx.sqlite.SQLiteDriver 5 | import androidx.sqlite.driver.bundled.BundledSQLiteDriver 6 | import app.cash.sqldelight.Transacter 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | import org.junit.Assert 10 | import java.io.File 11 | import java.util.concurrent.Semaphore 12 | 13 | actual class CommonCallbackTest : AndroidxSqliteCallbackTest() 14 | actual class CommonConcurrencyTest : AndroidxSqliteConcurrencyTest() 15 | actual class CommonDriverTest : AndroidxSqliteDriverTest() 16 | actual class CommonDriverOpenFlagsTest : AndroidxSqliteDriverOpenFlagsTest() 17 | actual class CommonQueryTest : AndroidxSqliteQueryTest() 18 | actual class CommonTransacterTest : AndroidxSqliteTransacterTest() 19 | actual class CommonEphemeralTest : AndroidxSqliteEphemeralTest() 20 | 21 | actual fun androidxSqliteTestDriver(): SQLiteDriver = BundledSQLiteDriver() 22 | 23 | actual fun androidxSqliteTestCreateConnection(): (String) -> SQLiteConnection = { name -> 24 | BundledSQLiteDriver().open(name) 25 | } 26 | 27 | @Suppress("InjectDispatcher") 28 | actual val IoDispatcher: CoroutineDispatcher get() = Dispatchers.IO 29 | 30 | actual fun deleteFile(name: String) { 31 | File(name).delete() 32 | } 33 | 34 | actual inline fun assertChecksThreadConfinement( 35 | transacter: Transacter, 36 | crossinline scope: Transacter.(T.() -> Unit) -> Unit, 37 | crossinline block: T.() -> Unit, 38 | ) { 39 | lateinit var thread: Thread 40 | var result: Result? = null 41 | val semaphore = Semaphore(0) 42 | 43 | transacter.scope { 44 | thread = kotlin.concurrent.thread { 45 | result = runCatching { 46 | this@scope.block() 47 | } 48 | 49 | semaphore.release() 50 | } 51 | } 52 | 53 | semaphore.acquire() 54 | thread.interrupt() 55 | Assert.assertThrows(IllegalStateException::class.java) { 56 | result!!.getOrThrow() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /library/src/nativeMain/kotlin/com/eygraber/sqldelight/androidx/driver/ThreadLocalId.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import kotlin.concurrent.AtomicInt 4 | 5 | internal object ThreadLocalId { 6 | private val id = AtomicInt(0) 7 | 8 | fun next(): Int = id.incrementAndGet() 9 | } 10 | -------------------------------------------------------------------------------- /library/src/nativeMain/kotlin/com/eygraber/sqldelight/androidx/driver/TransactionsThreadLocal.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import androidx.collection.mutableIntObjectMapOf 4 | import app.cash.sqldelight.Transacter 5 | import kotlin.native.concurrent.ThreadLocal 6 | 7 | @ThreadLocal 8 | private object ThreadLocalTransactions { 9 | val threadLocalMap = mutableIntObjectMapOf() 10 | } 11 | 12 | internal actual class TransactionsThreadLocal actual constructor() { 13 | private val threadLocalId = ThreadLocalId.next() 14 | 15 | actual fun get() = ThreadLocalTransactions.threadLocalMap[threadLocalId] 16 | 17 | actual fun set(transaction: Transacter.Transaction?) { 18 | when(transaction) { 19 | null -> ThreadLocalTransactions.threadLocalMap.remove(threadLocalId) 20 | else -> ThreadLocalTransactions.threadLocalMap[threadLocalId] = transaction 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /library/src/nativeTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteCommonTests.native.kt: -------------------------------------------------------------------------------- 1 | package com.eygraber.sqldelight.androidx.driver 2 | 3 | import androidx.sqlite.SQLiteConnection 4 | import androidx.sqlite.SQLiteDriver 5 | import androidx.sqlite.driver.bundled.BundledSQLiteDriver 6 | import app.cash.sqldelight.Transacter 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.IO 10 | import okio.FileSystem 11 | import okio.Path.Companion.toPath 12 | import kotlin.concurrent.AtomicInt 13 | import kotlin.concurrent.AtomicReference 14 | import kotlin.native.concurrent.ObsoleteWorkersApi 15 | import kotlin.native.concurrent.Worker 16 | import kotlin.test.assertFailsWith 17 | 18 | actual class CommonCallbackTest : AndroidxSqliteCallbackTest() 19 | actual class CommonConcurrencyTest : AndroidxSqliteConcurrencyTest() 20 | actual class CommonDriverTest : AndroidxSqliteDriverTest() 21 | actual class CommonDriverOpenFlagsTest : AndroidxSqliteDriverOpenFlagsTest() 22 | actual class CommonQueryTest : AndroidxSqliteQueryTest() 23 | actual class CommonTransacterTest : AndroidxSqliteTransacterTest() 24 | actual class CommonEphemeralTest : AndroidxSqliteEphemeralTest() 25 | 26 | actual fun androidxSqliteTestDriver(): SQLiteDriver = BundledSQLiteDriver() 27 | 28 | actual fun androidxSqliteTestCreateConnection(): (String) -> SQLiteConnection = { name -> 29 | BundledSQLiteDriver().open(name) 30 | } 31 | 32 | @Suppress("InjectDispatcher") 33 | actual val IoDispatcher: CoroutineDispatcher get() = Dispatchers.IO 34 | 35 | actual fun deleteFile(name: String) { 36 | FileSystem.SYSTEM.delete(name.toPath()) 37 | } 38 | 39 | @OptIn(ObsoleteWorkersApi::class) 40 | actual inline fun assertChecksThreadConfinement( 41 | transacter: Transacter, 42 | crossinline scope: Transacter.(T.() -> Unit) -> Unit, 43 | crossinline block: T.() -> Unit, 44 | ) { 45 | val resultRef = AtomicReference?>(null) 46 | val semaphore = AtomicInt(0) 47 | 48 | transacter.scope { 49 | val worker = Worker.start() 50 | worker.executeAfter(0L) { 51 | resultRef.value = runCatching { 52 | this@scope.block() 53 | } 54 | semaphore.value = 1 55 | } 56 | worker.requestTermination() 57 | } 58 | 59 | while(semaphore.value == 0) { 60 | Worker.current.processQueue() 61 | } 62 | 63 | assertFailsWith { 64 | resultRef.value!!.getOrThrow() 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "enabledManagers": ["gradle", "gradle-wrapper", "github-actions"], 6 | "labels": ["dependencies"], 7 | "prHourlyLimit": 3, 8 | "packageRules": [ 9 | { 10 | "groupName": "gradle-conventions", 11 | "matchPackagePrefixes": ["com.eygraber.conventions"], 12 | "automerge": true, 13 | "registryUrls": [ 14 | "https://repo.maven.apache.org/maven2/" 15 | ] 16 | }, 17 | { 18 | "groupName": "gradle-develocity-plugin", 19 | "matchPackagePrefixes": ["com.gradle.develocity"], 20 | "automerge": true, 21 | "registryUrls": [ 22 | "https://plugins.gradle.org/m2" 23 | ] 24 | }, 25 | { 26 | "matchDatasources": ["maven"], 27 | "depType": "dependencies", 28 | "registryUrls": [ 29 | "https://repo.maven.apache.org/maven2/", 30 | "https://dl.google.com/dl/android/maven2/", 31 | "https://plugins.gradle.org/m2" 32 | ] 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.eygraber.conventions.Env 2 | import com.eygraber.conventions.repositories.addCommonRepositories 3 | 4 | pluginManagement { 5 | repositories { 6 | google { 7 | content { 8 | includeGroupByRegex("com\\.google.*") 9 | includeGroupByRegex("com\\.android.*") 10 | includeGroupByRegex("androidx.*") 11 | } 12 | } 13 | 14 | mavenCentral() 15 | 16 | maven("https://oss.sonatype.org/content/repositories/snapshots") { 17 | mavenContent { 18 | snapshotsOnly() 19 | } 20 | } 21 | 22 | maven("https://s01.oss.sonatype.org/content/repositories/snapshots") { 23 | mavenContent { 24 | snapshotsOnly() 25 | } 26 | } 27 | 28 | gradlePluginPortal() 29 | } 30 | } 31 | 32 | @Suppress("UnstableApiUsage") 33 | dependencyResolutionManagement { 34 | // comment this out for now because it doesn't work with KMP js 35 | // https://youtrack.jetbrains.com/issue/KT-55620/KJS-Gradle-plugin-doesnt-support-repositoriesMode 36 | // repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 37 | 38 | repositories { 39 | addCommonRepositories( 40 | includeMavenCentral = true, 41 | includeMavenCentralSnapshots = true, 42 | includeGoogle = true, 43 | ) 44 | } 45 | } 46 | 47 | plugins { 48 | id("com.eygraber.conventions.settings") version "0.0.83" 49 | id("com.gradle.develocity") version "4.0.2" 50 | } 51 | 52 | rootProject.name = "sqldelight-androidx-driver" 53 | 54 | include(":integration") 55 | include(":library") 56 | 57 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 58 | 59 | develocity { 60 | buildScan { 61 | termsOfUseUrl = "https://gradle.com/terms-of-service" 62 | publishing.onlyIf { Env.isCI } 63 | if (Env.isCI) { 64 | termsOfUseAgree = "yes" 65 | } 66 | } 67 | } 68 | --------------------------------------------------------------------------------