├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | GETTERS_AND_SETTERS
108 | KEEP
109 |
110 |
111 | DEPENDENT_METHODS
112 | BREADTH_FIRST
113 |
114 |
115 | OVERRIDDEN_METHODS
116 | KEEP
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | xmlns:android
134 | .*
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | xmlns:.*
144 | .*
145 |
146 |
147 | BY_NAME
148 |
149 |
150 |
151 |
152 |
153 |
154 | .*:id
155 | http://schemas.android.com/apk/res/android
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | style
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 | .*:theme
174 | http://schemas.android.com/apk/res/android
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 | .*:layout_width
184 | http://schemas.android.com/apk/res/android
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 | .*:layout_height
194 | http://schemas.android.com/apk/res/android
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | .*:layout_gravity
204 | http://schemas.android.com/apk/res/android
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 | .*:layout_weight
214 | http://schemas.android.com/apk/res/android
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | .*:layout_alignParentTop
224 | http://schemas.android.com/apk/res/android
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 | .*:layout_alignTop
234 | http://schemas.android.com/apk/res/android
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 | .*:layout_alignParentStart
244 | http://schemas.android.com/apk/res/android
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 | .*:layout_alignStart
254 | http://schemas.android.com/apk/res/android
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 | .*:layout_alignParentEnd
264 | http://schemas.android.com/apk/res/android
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 | .*:layout_alignEnd
274 | http://schemas.android.com/apk/res/android
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 | .*:layout_alignParentBottom
284 | http://schemas.android.com/apk/res/android
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 | .*:layout_alignBottom
294 | http://schemas.android.com/apk/res/android
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 | .*:layout_alignBaseline
304 | http://schemas.android.com/apk/res/android
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 | .*:layout_alignWithParentIfMissing
314 | http://schemas.android.com/apk/res/android
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 | .*:layout_below
324 | http://schemas.android.com/apk/res/android
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 | .*:layout_toStartOf
334 | http://schemas.android.com/apk/res/android
335 |
336 |
337 |
338 |
339 |
340 |
341 |
342 |
343 | .*:layout_toEndOf
344 | http://schemas.android.com/apk/res/android
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 | .*:layout_above
354 | http://schemas.android.com/apk/res/android
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 | .*:layout_centerInParent
364 | http://schemas.android.com/apk/res/android
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 | .*:layout_centerHorizontal
374 | http://schemas.android.com/apk/res/android
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 | .*:layout_centerVertical
384 | http://schemas.android.com/apk/res/android
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 | .*:layout_constraintTop_to.*Of
394 | http://schemas.android.com/apk/res-auto
395 |
396 |
397 |
398 |
399 |
400 |
401 |
402 |
403 | .*:layout_constraintStart_to.*Of
404 | http://schemas.android.com/apk/res-auto
405 |
406 |
407 |
408 |
409 |
410 |
411 |
412 |
413 | .*:layout_constraintEnd_to.*Of
414 | http://schemas.android.com/apk/res-auto
415 |
416 |
417 |
418 |
419 |
420 |
421 |
422 |
423 | .*:layout_constraintBottom_to.*Of
424 | http://schemas.android.com/apk/res-auto
425 |
426 |
427 |
428 |
429 |
430 |
431 |
432 |
433 | .*:layout_constraint.*
434 | http://schemas.android.com/apk/res-auto
435 |
436 |
437 |
438 |
439 |
440 |
441 |
442 |
443 | .*:layout_margin
444 | http://schemas.android.com/apk/res/android
445 |
446 |
447 |
448 |
449 |
450 |
451 |
452 |
453 | .*:layout_marginTop
454 | http://schemas.android.com/apk/res/android
455 |
456 |
457 |
458 |
459 |
460 |
461 |
462 |
463 | .*:layout_marginStart
464 | http://schemas.android.com/apk/res/android
465 |
466 |
467 |
468 |
469 |
470 |
471 |
472 |
473 | .*:layout_marginEnd
474 | http://schemas.android.com/apk/res/android
475 |
476 |
477 |
478 |
479 |
480 |
481 |
482 |
483 | .*:layout_marginBottom
484 | http://schemas.android.com/apk/res/android
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 | .*:layout_.*
494 | http://schemas.android.com/apk/res/android
495 |
496 |
497 |
498 |
499 |
500 |
501 |
502 |
503 | .*:layout_.*
504 | http://schemas.android.com/apk/res-auto
505 |
506 |
507 |
508 |
509 |
510 |
511 |
512 |
513 | .*:minWidth
514 | http://schemas.android.com/apk/res/android
515 |
516 |
517 |
518 |
519 |
520 |
521 |
522 |
523 | .*:minHeight
524 | http://schemas.android.com/apk/res/android
525 |
526 |
527 |
528 |
529 |
530 |
531 |
532 |
533 | .*:padding
534 | http://schemas.android.com/apk/res/android
535 |
536 |
537 |
538 |
539 |
540 |
541 |
542 |
543 | .*:paddingTop
544 | http://schemas.android.com/apk/res/android
545 |
546 |
547 |
548 |
549 |
550 |
551 |
552 |
553 | .*:paddingStart
554 | http://schemas.android.com/apk/res/android
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 |
563 | .*:paddingEnd
564 | http://schemas.android.com/apk/res/android
565 |
566 |
567 |
568 |
569 |
570 |
571 |
572 |
573 | .*:paddingBottom
574 | http://schemas.android.com/apk/res/android
575 |
576 |
577 |
578 |
579 |
580 |
581 |
582 |
583 | .*:gravity
584 | http://schemas.android.com/apk/res/android
585 |
586 |
587 |
588 |
589 |
590 |
591 |
592 |
593 | .*:navigationIcon
594 | http://schemas.android.com/apk/res/android
595 |
596 |
597 |
598 |
599 |
600 |
601 |
602 |
603 | .*:navigationContentDescription
604 | http://schemas.android.com/apk/res/android
605 |
606 |
607 |
608 |
609 |
610 |
611 |
612 |
613 | .*:navigationIcon
614 | http://schemas.android.com/apk/res-auto
615 |
616 |
617 |
618 |
619 |
620 |
621 |
622 |
623 | .*:navigationContentDescription
624 | http://schemas.android.com/apk/res-auto
625 |
626 |
627 |
628 |
629 |
630 |
631 |
632 |
633 | .*:titleTextAppearance
634 | http://schemas.android.com/apk/res/android
635 |
636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 | .*:titleTextAppearance
644 | http://schemas.android.com/apk/res-auto
645 |
646 |
647 |
648 |
649 |
650 |
651 |
652 |
653 | .*:titleTextSize
654 | http://schemas.android.com/apk/res/android
655 |
656 |
657 |
658 |
659 |
660 |
661 |
662 |
663 | .*:titleTextSize
664 | http://schemas.android.com/apk/res-auto
665 |
666 |
667 |
668 |
669 |
670 |
671 |
672 |
673 | .*:titleTextColor
674 | http://schemas.android.com/apk/res/android
675 |
676 |
677 |
678 |
679 |
680 |
681 |
682 |
683 | .*:titleTextColor
684 | http://schemas.android.com/apk/res-auto
685 |
686 |
687 |
688 |
689 |
690 |
691 |
692 |
693 | .*:title
694 | http://schemas.android.com/apk/res/android
695 |
696 |
697 |
698 |
699 |
700 |
701 |
702 |
703 | .*:title
704 | http://schemas.android.com/apk/res-auto
705 |
706 |
707 |
708 |
709 |
710 |
711 |
712 |
713 | .*:inputType
714 | http://schemas.android.com/apk/res/android
715 |
716 |
717 |
718 |
719 |
720 |
721 |
722 |
723 | .*:imeOptions
724 | http://schemas.android.com/apk/res/android
725 |
726 |
727 |
728 |
729 |
730 |
731 |
732 |
733 | .*:maxLines
734 | http://schemas.android.com/apk/res/android
735 |
736 |
737 |
738 |
739 |
740 |
741 |
742 |
743 | .*:ellipsize
744 | http://schemas.android.com/apk/res/android
745 |
746 |
747 |
748 |
749 |
750 |
751 |
752 |
753 | .*scrollHorizontally
754 | http://schemas.android.com/apk/res/android
755 |
756 |
757 |
758 |
759 |
760 |
761 |
762 |
763 | .*:lineSpacingExtra
764 | http://schemas.android.com/apk/res/android
765 |
766 |
767 |
768 |
769 |
770 |
771 |
772 |
773 | .*:lineSpacingMultiplier
774 | http://schemas.android.com/apk/res/android
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 | .*:textIsSelectable
784 | http://schemas.android.com/apk/res/android
785 |
786 |
787 |
788 |
789 |
790 |
791 |
792 |
793 | .*:drawablePadding
794 | http://schemas.android.com/apk/res/android
795 |
796 |
797 |
798 |
799 |
800 |
801 |
802 |
803 | .*:drawableTop
804 | http://schemas.android.com/apk/res/android
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 | .*:drawableStart
814 | http://schemas.android.com/apk/res/android
815 |
816 |
817 |
818 |
819 |
820 |
821 |
822 |
823 | .*:drawableEnd
824 | http://schemas.android.com/apk/res/android
825 |
826 |
827 |
828 |
829 |
830 |
831 |
832 |
833 | .*:drawableBottom
834 | http://schemas.android.com/apk/res/android
835 |
836 |
837 |
838 |
839 |
840 |
841 |
842 |
843 | .*:textAppearance
844 | http://schemas.android.com/apk/res/android
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 |
853 | .*:textSize
854 | http://schemas.android.com/apk/res/android
855 |
856 |
857 |
858 |
859 |
860 |
861 |
862 |
863 | .*:textColor
864 | http://schemas.android.com/apk/res/android
865 |
866 |
867 |
868 |
869 |
870 |
871 |
872 |
873 | .*:textColorHint
874 | http://schemas.android.com/apk/res/android
875 |
876 |
877 |
878 |
879 |
880 |
881 |
882 |
883 | .*:text
884 | http://schemas.android.com/apk/res/android
885 |
886 |
887 |
888 |
889 |
890 |
891 |
892 |
893 | .*:hint
894 | http://schemas.android.com/apk/res/android
895 |
896 |
897 |
898 |
899 |
900 |
901 |
902 |
903 | .*:contentDescription
904 | http://schemas.android.com/apk/res/android
905 |
906 |
907 |
908 |
909 |
910 |
911 |
912 |
913 | .*:scaleType
914 | http://schemas.android.com/apk/res/android
915 |
916 |
917 |
918 |
919 |
920 |
921 |
922 |
923 | .*:src
924 | http://schemas.android.com/apk/res/android
925 |
926 |
927 |
928 |
929 |
930 |
931 |
932 |
933 | .*srcCompat
934 | http://schemas.android.com/apk/res-auto
935 |
936 |
937 |
938 |
939 |
940 |
941 |
942 |
943 | .*:tint
944 | http://schemas.android.com/apk/res/android
945 |
946 |
947 |
948 |
949 |
950 |
951 |
952 |
953 | .*:enabled
954 | http://schemas.android.com/apk/res/android
955 |
956 |
957 |
958 |
959 |
960 |
961 |
962 |
963 | .*:visibility
964 | http://schemas.android.com/apk/res/android
965 |
966 |
967 |
968 |
969 |
970 |
971 |
972 |
973 | .*:background
974 | http://schemas.android.com/apk/res/android
975 |
976 |
977 |
978 |
979 |
980 |
981 |
982 |
983 | .*:backgroundTint
984 | http://schemas.android.com/apk/res/android
985 |
986 |
987 |
988 |
989 |
990 |
991 |
992 |
993 | .*:orientation
994 | http://schemas.android.com/apk/res/android
995 |
996 |
997 |
998 |
999 |
1000 |
1001 |
1002 |
1003 | .*:elevation
1004 | http://schemas.android.com/apk/res/android
1005 |
1006 |
1007 |
1008 |
1009 |
1010 |
1011 |
1012 |
1013 | .*:scrollbars
1014 | http://schemas.android.com/apk/res/android
1015 |
1016 |
1017 |
1018 |
1019 |
1020 |
1021 |
1022 |
1023 | .*:popupTheme
1024 | http://schemas.android.com/apk/res/android
1025 |
1026 |
1027 |
1028 |
1029 |
1030 |
1031 |
1032 |
1033 | .*:popupTheme
1034 | http://schemas.android.com/apk/res-auto
1035 |
1036 |
1037 |
1038 |
1039 |
1040 |
1041 |
1042 |
1043 | .*:gravity
1044 | http://schemas.android.com/tools
1045 |
1046 |
1047 |
1048 |
1049 |
1050 |
1051 |
1052 |
1053 | .*:titleTextAppearance
1054 | http://schemas.android.com/tools
1055 |
1056 |
1057 |
1058 |
1059 |
1060 |
1061 |
1062 |
1063 | .*:titleTextSize
1064 | http://schemas.android.com/tools
1065 |
1066 |
1067 |
1068 |
1069 |
1070 |
1071 |
1072 |
1073 | .*:titleTextColor
1074 | http://schemas.android.com/tools
1075 |
1076 |
1077 |
1078 |
1079 |
1080 |
1081 |
1082 |
1083 | .*:title
1084 | http://schemas.android.com/tools
1085 |
1086 |
1087 |
1088 |
1089 |
1090 |
1091 |
1092 |
1093 | .*:ellipsize
1094 | http://schemas.android.com/tools
1095 |
1096 |
1097 |
1098 |
1099 |
1100 |
1101 |
1102 |
1103 | .*:lineSpacingExtra
1104 | http://schemas.android.com/tools
1105 |
1106 |
1107 |
1108 |
1109 |
1110 |
1111 |
1112 |
1113 | .*:lineSpacingMultiplier
1114 | http://schemas.android.com/tools
1115 |
1116 |
1117 |
1118 |
1119 |
1120 |
1121 |
1122 |
1123 | .*:drawablePadding
1124 | http://schemas.android.com/tools
1125 |
1126 |
1127 |
1128 |
1129 |
1130 |
1131 |
1132 |
1133 | .*drawableTop
1134 | http://schemas.android.com/tools
1135 |
1136 |
1137 |
1138 |
1139 |
1140 |
1141 |
1142 |
1143 | .*:drawableStart
1144 | http://schemas.android.com/tools
1145 |
1146 |
1147 |
1148 |
1149 |
1150 |
1151 |
1152 |
1153 | .*:drawableEnd
1154 | http://schemas.android.com/tools
1155 |
1156 |
1157 |
1158 |
1159 |
1160 |
1161 |
1162 |
1163 | .*:drawableBottom
1164 | http://schemas.android.com/tools
1165 |
1166 |
1167 |
1168 |
1169 |
1170 |
1171 |
1172 |
1173 | .*:textAppearance
1174 | http://schemas.android.com/tools
1175 |
1176 |
1177 |
1178 |
1179 |
1180 |
1181 |
1182 |
1183 | .*:textSize
1184 | http://schemas.android.com/tools
1185 |
1186 |
1187 |
1188 |
1189 |
1190 |
1191 |
1192 |
1193 | .*:textColor
1194 | http://schemas.android.com/tools
1195 |
1196 |
1197 |
1198 |
1199 |
1200 |
1201 |
1202 |
1203 | .*:textColorHint
1204 | http://schemas.android.com/tools
1205 |
1206 |
1207 |
1208 |
1209 |
1210 |
1211 |
1212 |
1213 | .*:text
1214 | http://schemas.android.com/tools
1215 |
1216 |
1217 |
1218 |
1219 |
1220 |
1221 |
1222 |
1223 | .*:hint
1224 | http://schemas.android.com/tools
1225 |
1226 |
1227 |
1228 |
1229 |
1230 |
1231 |
1232 |
1233 | .*:scaleType
1234 | http://schemas.android.com/tools
1235 |
1236 |
1237 |
1238 |
1239 |
1240 |
1241 |
1242 |
1243 | .*:src
1244 | http://schemas.android.com/tools
1245 |
1246 |
1247 |
1248 |
1249 |
1250 |
1251 |
1252 |
1253 | .*srcCompat
1254 | http://schemas.android.com/tools
1255 |
1256 |
1257 |
1258 |
1259 |
1260 |
1261 |
1262 |
1263 | .*:tint
1264 | http://schemas.android.com/tools
1265 |
1266 |
1267 |
1268 |
1269 |
1270 |
1271 |
1272 |
1273 | .*:enabled
1274 | http://schemas.android.com/tools
1275 |
1276 |
1277 |
1278 |
1279 |
1280 |
1281 |
1282 |
1283 | .*:visibility
1284 | http://schemas.android.com/tools
1285 |
1286 |
1287 |
1288 |
1289 |
1290 |
1291 |
1292 |
1293 | .*:background
1294 | http://schemas.android.com/tools
1295 |
1296 |
1297 |
1298 |
1299 |
1300 |
1301 |
1302 |
1303 | .*:backgroundTint
1304 | http://schemas.android.com/tools
1305 |
1306 |
1307 |
1308 |
1309 |
1310 |
1311 |
1312 |
1313 | .*:elevation
1314 | http://schemas.android.com/tools
1315 |
1316 |
1317 |
1318 |
1319 |
1320 |
1321 |
1322 |
1323 | .*:theme
1324 | http://schemas.android.com/tools
1325 |
1326 |
1327 |
1328 |
1329 |
1330 |
1331 |
1332 |
1333 | .:*
1334 | http://schemas.android.com/tools
1335 |
1336 |
1337 |
1338 |
1339 |
1340 |
1341 |
1342 |
1343 |
1344 |
1345 |
1346 |
1347 |
1348 |
1349 |
1350 |
1351 |
1352 |
1353 |
1354 |
1355 |
1356 |
1357 |
1358 |
1359 |
1360 |
1361 |
1362 |
1363 |
1364 |
1365 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------