Repository: square/sqlbrite Branch: trunk Commit: d4cf6ef96745 Files: 70 Total size: 245.7 KB Directory structure: gitextract_8j63fi6p/ ├── .buildscript/ │ └── deploy_snapshot.sh ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── RELEASING.md ├── build.gradle ├── gradle/ │ ├── gradle-mvn-push.gradle │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── sample/ │ ├── build.gradle │ ├── debug.keystore │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── example/ │ │ └── sqlbrite/ │ │ └── todo/ │ │ ├── TodoApp.java │ │ ├── TodoComponent.java │ │ ├── TodoModule.java │ │ ├── db/ │ │ │ ├── Db.java │ │ │ ├── DbCallback.java │ │ │ ├── DbModule.java │ │ │ ├── TodoItem.java │ │ │ └── TodoList.java │ │ └── ui/ │ │ ├── ItemsAdapter.java │ │ ├── ItemsFragment.java │ │ ├── ListsAdapter.java │ │ ├── ListsFragment.java │ │ ├── ListsItem.java │ │ ├── MainActivity.java │ │ ├── NewItemFragment.java │ │ └── NewListFragment.java │ └── res/ │ ├── anim/ │ │ ├── slide_in_left.xml │ │ ├── slide_in_right.xml │ │ ├── slide_out_left.xml │ │ └── slide_out_right.xml │ ├── layout/ │ │ ├── items.xml │ │ ├── lists.xml │ │ ├── new_item.xml │ │ └── new_list.xml │ └── values/ │ └── strings.xml ├── settings.gradle ├── sqlbrite/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── sqlbrite3/ │ │ ├── BlockingRecordingObserver.java │ │ ├── BriteContentResolverTest.java │ │ ├── BriteDatabaseTest.java │ │ ├── QueryObservableTest.java │ │ ├── QueryTest.java │ │ ├── RecordingObserver.java │ │ ├── SqlBriteTest.java │ │ ├── TestDb.java │ │ └── TestScheduler.java │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── squareup/ │ └── sqlbrite3/ │ ├── BriteContentResolver.java │ ├── BriteDatabase.java │ ├── QueryObservable.java │ ├── QueryToListOperator.java │ ├── QueryToOneOperator.java │ ├── QueryToOptionalOperator.java │ └── SqlBrite.java ├── sqlbrite-kotlin/ │ ├── build.gradle │ ├── gradle.properties │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ └── java/ │ └── com/ │ └── squareup/ │ └── sqlbrite3/ │ └── extensions.kt └── sqlbrite-lint/ ├── build.gradle └── src/ ├── main/ │ └── java/ │ └── com/ │ └── squareup/ │ └── sqlbrite3/ │ ├── BriteIssueRegistry.kt │ └── SqlBriteArgCountDetector.kt └── test/ └── java/ └── com/ └── squareup/ └── sqlbrite3/ └── SqlBriteArgCountDetectorTest.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .buildscript/deploy_snapshot.sh ================================================ #!/bin/bash # # Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo. # # Adapted from https://coderwall.com/p/9b_lfq and # http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/ SLUG="square/sqlbrite" JDK="oraclejdk8" BRANCH="master" set -e if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'." elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'." elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then echo "Skipping snapshot deployment: was pull request." elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'." else echo "Deploying snapshot..." ./gradlew clean uploadArchives echo "Snapshot deployed!" fi ================================================ FILE: .gitignore ================================================ # IntelliJ IDEA .idea *.iml # Gradle .gradle gradlew.bat build local.properties reports # Apple .DS_Store ================================================ FILE: .travis.yml ================================================ language: android android: components: - tools - platform-tools jdk: - oraclejdk8 before_install: # Install SDK license so Android Gradle plugin can install deps. - mkdir "$ANDROID_HOME/licenses" || true - echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license" # Install the rest of tools (e.g., avdmanager) - sdkmanager tools # Install the system image - sdkmanager "system-images;android-18;default;armeabi-v7a" # Create and start emulator for the script. Meant to race the install task. - echo no | avdmanager create avd --force -n test -k "system-images;android-18;default;armeabi-v7a" - $ANDROID_HOME/emulator/emulator -avd test -no-audio -no-window & install: ./gradlew clean assemble assembleAndroidTest --stacktrace before_script: - android-wait-for-emulator - adb shell input keyevent 82 script: ./gradlew check connectedCheck --stacktrace after_success: - .buildscript/deploy_snapshot.sh env: global: - secure: "NIWC0zkThskXn7uduTJ1yT78voqEgzEfw8tOImGNBjZ/NDU6yxM4bh+tq+fnkn5ENjELV6fgcYd2DUJSWmkFD2k9ZMRNLm//AqlQihl8aT+DpWhDdCkQjnolHnjm1O7+ys7Q/vswBZEzkBxzIgivajZEzvjarQItJjbpBftQ0Cs=" - secure: "ahPT9EzJVpkM4q2HA/VBxUzgicvfdOOZaEvOiQKJofy1FrLjrBS2LFxqCbyffg0sjGUyvBMLg767CSt/0xRRFWIpsjxCfmvEmAURi89zdZ8MUNXIwe7x/0lXCdQIt8eueq3Qh5qFwJUy4aFbzVvcmMXKswWzw1O0+IcvYX00/xc=" branches: except: - gh-pages notifications: email: false sudo: false cache: directories: - $HOME/.gradle ================================================ FILE: CHANGELOG.md ================================================ Change Log ========== Version 3.2.0 *(2018-03-05)* ---------------------------- * New: Add `query(SupportSQLiteQuery)` method for one-off queries. Version 3.1.1 *(2018-02-12)* ---------------------------- * Fix: Useless `BuildConfig` classes are no longer included. * Fix: Eliminate Java interop checks for Kotlin extensions as they're only for Kotlin consumers and the checks exist in the Java code they delegate to anyway. Version 3.1.0 *(2017-12-18)* ---------------------------- * New: `inTransaction` Kotlin extension function which handles starting, marking successful, and ending a transaction. * New: Embedded lint check which validates the number of arguments passed to `query` and `createQuery` match the number of expected arguments of the SQL statement. * Fix: Properly indent multi-line SQL statements in the logs for `query`. Version 3.0.0 *(2017-11-28)* ---------------------------- Group ID has changed to `com.squareup.sqlbrite3`. * New: Build on top of the Android architecture components Sqlite support library. This allows swapping out the underlying Sqlite implementation to that of your choosing. Because of the way the Sqlite support library works, there is no interop bridge between 1.x or 2.x to this new version. If you haven't fully migrated to 2.x, complete that migration first and then upgrade to 3.x all at once. Version 2.0.0 *(2017-07-07)* ---------------------------- Group ID has changed to `com.squareup.sqlbrite2`. * New: RxJava 2.x support. Backpressure is no longer supported as evidenced by the use of `Observable`. If you want to slow down query notifications based on backpressure or another metric like time then you should apply those operators yourself. * New: `mapToOptional` for queries that return 0 or 1 rows. * New: `sqlbrite-kotlin` module provides `mapTo*` extension functions for `Observable`. * New: `sqlbrite-interop` module allows bridging 1.x and 2.x libraries together so that notifications from each trigger queries from the other. Note: This version only supports RxJava 2. Version 1.1.2 *(2017-06-30)* ---------------------------- * Internal architecture changes to support the upcoming 2.0 release and a bridge allowing both 1.x and 2.x to be used at the same time. Version 1.1.1 *(2016-12-20)* ---------------------------- * Fix: Correct spelling of `getWritableDatabase()` to match `SQLiteOpenHelper`. Version 1.1.0 *(2016-12-16)* ---------------------------- * New: Expose `getReadableDatabase()` and `getWriteableDatabase()` convenience methods. * Fix: Do not cache instances of the readable and writable database internally as the framework does this by default. Version 1.0.0 *(2016-12-02)* ---------------------------- * RxJava dependency updated to 1.2.3. * Restore `@WorkerThread` annotations to methods which do I/O. If you're using Java 8 with Retrolambda or Jack you need to use version 2.3 or newer of the Android Gradle plugin to have these annotations correctly handled by lint. Version 0.8.0 *(2016-10-21)* ---------------------------- * New: A `Transformer` can be supplied which is applied to each returned observable. * New: `newNonExclusiveTransaction()` starts transactions in `IMMEDIATE` mode. See the platform or SQLite documentation for more information. * New: APIs for insert/update/delete which allow providing a compiled `SQLiteStatement`. Version 0.7.0 *(2016-07-06)* ---------------------------- * New: Allow `mapTo*` mappers to return `null` values. This is useful when querying on a single, nullable column for which `null` is a valid value. * Fix: When `mapToOne` does not emit a value downstream, request another value from upstream to ensure fixed-item requests (such as `take(1)`) as properly honored. * Fix: Add logging to synchronous `execute` methods. Version 0.6.3 *(2016-04-13)* ---------------------------- * `QueryObservable` constructor is now public allow instances to be created for tests. Version 0.6.2 *(2016-03-01)* ---------------------------- * Fix: Document explicitly and correctly handle the fact that `Query.run()` can return `null` in some situations. The `mapToOne`, `mapToOneOrDefault`, `mapToList`, and `asRows` helpers have all been updated to handle this case and each is documented with their respective behavior. Version 0.6.1 *(2016-02-29)* ---------------------------- * Fix: Apply backpressure strategy between database/content provider and the supplied `Scheduler`. This guards against backpressure exceptions when the scheduler is unable to keep up with the rate at which queries are being triggered. * Fix: Indent the subsequent lines of a multi-line queries when logging. Version 0.6.0 *(2016-02-17)* ---------------------------- * New: Require a `Scheduler` when wrapping a database or content provider which will be used when sending query triggers. This allows the query to be run in subsequent operators without needing an additional `observeOn`. It also eliminates the need to use `subscribeOn` since the supplied `Scheduler` will be used for all emissions (similar to RxJava's `timer`, `interval`, etc.). This also corrects a potential violation of the RxJava contract and potential source of bugs in that all triggers will occur on the supplied `Scheduler`. Previously the initial value would trigger synchronously (on the subscribing thread) while subsequent ones trigger on the thread which performed the transaction. The new behavior puts the initial trigger on the same thread as all subsequent triggers and also does not force transactions to block while sending triggers. Version 0.5.1 *(2016-02-03)* ---------------------------- * New: Query logs now contain timing information on how long they took to execute. This only covers the time until a `Cursor` was made available, not object mapping or delivering to subscribers. * Fix: Switch query logging to happen when `Query.run` is called, not when a query is triggered. * Fix: Check for subscribing inside a transaction using a more accurate primitive. Version 0.5.0 *(2015-12-09)* ---------------------------- * New: Expose `mapToOne`, `mapToOneOrDefault`, and `mapToList` as static methods on `Query`. These mirror the behavior of the methods of the same name on `QueryObservable` but can be used later in a stream by passing the returned `Operator` instances to `lift()` (e.g., `take(1).lift(Query.mapToOne(..))`). * Requires RxJava 1.1.0 or newer. Version 0.4.1 *(2015-10-19)* ---------------------------- * New: `execute` method provides the ability to execute arbitrary SQL statements. * New: `executeAndTrigger` method provides the ability to execute arbitrary SQL statements and notifying any queries to update on the specified table. * Fix: `Query.asRows` no longer calls `onCompleted` when the downstream subscriber has unsubscribed. Version 0.4.0 *(2015-09-22)* ---------------------------- * New: `mapToOneOrDefault` replaces `mapToOneOrNull` for more flexibility. * Fix: Notifications of table updates as the result of a transaction now occur after the transaction has been applied. Previous the notification would happen during the commit at which time it was invalid to create a new transaction in a subscriber. Version 0.3.1 *(2015-09-02)* ---------------------------- * New: `mapToOne` and `mapToOneOrNull` operators on `QueryObservable`. These work on queries which return 0 or 1 rows and are a convenience for turning them into a type `T` given a mapper of type `Func1` (the same which can be used for `mapToList`). * Fix: Remove `@WorkerThread` annotations for now. Various combinations of lint, RxJava, and retrolambda can cause false-positives. Version 0.3.0 *(2015-08-31)* ---------------------------- * Transactions are now exposed as objects instead of methods. Call `newTransaction()` to start a transaction. On the `Transaction` instance, call `markSuccessful()` to indicate success and `end()` to commit or rollback the transaction. The `Transaction` instance implements `Closeable` to allow its use in a try-with-resources construct. See the `newTransaction()` Javadoc for more information. * `Query` instances can now be turned directly into an `Observable` by calling `asRows` with a `Func1` that maps rows to a type `T`. This allows easy filtering and limiting in memory rather than in the query. See the `asRows` Javadoc for more information. * `createQuery` now returns a `QueryObservable` which offers a `mapToList` operator. This operator also takes a `Func1` for mapping rows to a type `T`, but instead of individual rows it collects all the rows into a list. For large query results or frequently updated tables this can create a lot of objects. See the `mapToList` Javadoc for more information. * New: Nullability, `@CheckResult`, and `@WorkerThread` annotations on all APIs allow a more useful interaction with lint in consuming projects. Version 0.2.1 *(2015-07-14)* ---------------------------- * Fix: Add support for backpressure. Version 0.2.0 *(2015-06-30)* ---------------------------- * An `Observable` can now be created from wrapping a `ContentResolver` in order to observe queries from another app's content provider. * `SqlBrite` class is now a factory for both a `BriteDatabase` (the `SQLiteOpenHelper` wrapper) and `BriteContentResolver` (the `ContentResolver` wrapper). Version 0.1.0 *(2015-02-21)* ---------------------------- Initial release. ================================================ FILE: CONTRIBUTING.md ================================================ Contributing ============ If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Please also make sure your code compiles by running `./gradlew clean build`. Before your code can be accepted into the project you must also sign the [Individual Contributor License Agreement (CLA)][1]. [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 ================================================ FILE: LICENSE.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ SQL Brite ========= A lightweight wrapper around `SupportSQLiteOpenHelper` and `ContentResolver` which introduces reactive stream semantics to queries. # Deprecated This library is no longer actively developed and is considered complete. Its database features (and far, far more) are now offered by [SQLDelight](https://github.com/cashapp/sqldelight/) and its [upgrading guide](https://github.com/cashapp/sqldelight/blob/1.0.0/UPGRADING.md) offers some migration help. For content provider monitoring please use [Copper](https://github.com/cashapp/copper) instead. Usage ----- Create a `SqlBrite` instance which is an adapter for the library functionality. ```java SqlBrite sqlBrite = new SqlBrite.Builder().build(); ``` Pass a `SupportSQLiteOpenHelper` instance and a `Scheduler` to create a `BriteDatabase`. ```java BriteDatabase db = sqlBrite.wrapDatabaseHelper(openHelper, Schedulers.io()); ``` A `Scheduler` is required for a few reasons, but the most important is that query notifications can trigger on the thread of your choice. The query can then be run without blocking the main thread or the thread which caused the trigger. The `BriteDatabase.createQuery` method is similar to `SupportSQLiteDatabase.query` except it takes an additional parameter of table(s) on which to listen for changes. Subscribe to the returned `Observable` which will immediately notify with a `Query` to run. ```java Observable users = db.createQuery("users", "SELECT * FROM users"); users.subscribe(new Consumer() { @Override public void accept(Query query) { Cursor cursor = query.run(); // TODO parse data... } }); ``` Unlike a traditional `query`, updates to the specified table(s) will trigger additional notifications for as long as you remain subscribed to the observable. This means that when you insert, update, or delete data, any subscribed queries will update with the new data instantly. ```java final AtomicInteger queries = new AtomicInteger(); users.subscribe(new Consumer() { @Override public void accept(Query query) { queries.getAndIncrement(); } }); System.out.println("Queries: " + queries.get()); // Prints 1 db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("jw", "Jake Wharton")); db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("mattp", "Matt Precious")); db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("strong", "Alec Strong")); System.out.println("Queries: " + queries.get()); // Prints 4 ``` In the previous example we re-used the `BriteDatabase` object "db" for inserts. All insert, update, or delete operations must go through this object in order to correctly notify subscribers. Unsubscribe from the returned `Subscription` to stop getting updates. ```java final AtomicInteger queries = new AtomicInteger(); Subscription s = users.subscribe(new Consumer() { @Override public void accept(Query query) { queries.getAndIncrement(); } }); System.out.println("Queries: " + queries.get()); // Prints 1 db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("jw", "Jake Wharton")); db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("mattp", "Matt Precious")); s.unsubscribe(); db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("strong", "Alec Strong")); System.out.println("Queries: " + queries.get()); // Prints 3 ``` Use transactions to prevent large changes to the data from spamming your subscribers. ```java final AtomicInteger queries = new AtomicInteger(); users.subscribe(new Consumer() { @Override public void accept(Query query) { queries.getAndIncrement(); } }); System.out.println("Queries: " + queries.get()); // Prints 1 Transaction transaction = db.newTransaction(); try { db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("jw", "Jake Wharton")); db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("mattp", "Matt Precious")); db.insert("users", SQLiteDatabase.CONFLICT_ABORT, createUser("strong", "Alec Strong")); transaction.markSuccessful(); } finally { transaction.end(); } System.out.println("Queries: " + queries.get()); // Prints 2 ``` *Note: You can also use try-with-resources with a `Transaction` instance.* Since queries are just regular RxJava `Observable` objects, operators can also be used to control the frequency of notifications to subscribers. ```java users.debounce(500, MILLISECONDS).subscribe(new Consumer() { @Override public void accept(Query query) { // TODO... } }); ``` The `SqlBrite` object can also wrap a `ContentResolver` for observing a query on another app's content provider. ```java BriteContentResolver resolver = sqlBrite.wrapContentProvider(contentResolver, Schedulers.io()); Observable query = resolver.createQuery(/*...*/); ``` The full power of RxJava's operators are available for combining, filtering, and triggering any number of queries and data changes. Philosophy ---------- SQL Brite's only responsibility is to be a mechanism for coordinating and composing the notification of updates to tables such that you can update queries as soon as data changes. This library is not an ORM. It is not a type-safe query mechanism. It won't serialize the same POJOs you use for Gson. It's not going to perform database migrations for you. Some of these features are offered by [SQL Delight][sqldelight] which can be used with SQL Brite. Download -------- ```groovy implementation 'com.squareup.sqlbrite3:sqlbrite:3.2.0' ``` For the 'kotlin' module that adds extension functions to `Observable`: ```groovy implementation 'com.squareup.sqlbrite3:sqlbrite-kotlin:3.2.0' ``` Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. License ------- Copyright 2015 Square, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. [snap]: https://oss.sonatype.org/content/repositories/snapshots/ [sqldelight]: https://github.com/square/sqldelight/ ================================================ FILE: RELEASING.md ================================================ Releasing ======== 1. Change the version in `gradle.properties` to a non-SNAPSHOT version. 2. Update the `CHANGELOG.md` for the impending release. 3. Update the `README.md` with the new version. 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version) 5. `./gradlew clean uploadArchives`. 6. Visit [Sonatype Nexus](https://oss.sonatype.org/) and promote the artifact. 7. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version) 8. Update the `gradle.properties` to the next SNAPSHOT version. 9. `git commit -am "Prepare next development version."` 10. `git push && git push --tags` If step 5 or 6 fails, drop the Sonatype repo, fix the problem, commit, and start again at step 5. ================================================ FILE: build.gradle ================================================ buildscript { ext.versions = [ 'minSdk': 14, 'compileSdk': 27, 'kotlin': '1.1.60', 'lint': '26.0.1' ] repositories { mavenCentral() google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}" } } allprojects { repositories { mavenCentral() google() jcenter() } group = GROUP version = VERSION_NAME } ext { // Android dependencies. supportV4 = 'com.android.support:support-v4:27.0.0' supportAnnotations = 'com.android.support:support-annotations:27.0.0' supportTestRunner = 'com.android.support.test:runner:0.5' supportSqlite = 'android.arch.persistence:db:1.0.0' supportSqliteFramework = 'android.arch.persistence:db-framework:1.0.0' // Third-party dependencies. kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib:${versions.kotlin}" dagger = 'com.google.dagger:dagger:2.13' daggerCompiler = 'com.google.dagger:dagger-compiler:2.13' butterKnifeRuntime = 'com.jakewharton:butterknife:8.8.1' butterKnifeCompiler = 'com.jakewharton:butterknife-compiler:8.8.1' timber = 'com.jakewharton.timber:timber:4.6.0' autoValue = 'com.google.auto.value:auto-value:1.5' autoValueParcel = 'com.ryanharter.auto.value:auto-value-parcel:0.2.5' rxJava = 'io.reactivex.rxjava2:rxjava:2.1.3' rxAndroid = 'io.reactivex.rxjava2:rxandroid:2.0.1' rxBinding = 'com.jakewharton.rxbinding2:rxbinding:2.0.0' junit = 'junit:junit:4.12' truth = 'com.google.truth:truth:0.36' // Lint dependencies. lintApi = "com.android.tools.lint:lint-api:${versions.lint}" lint = "com.android.tools.lint:lint:${versions.lint}" lintTests = "com.android.tools.lint:lint-tests:${versions.lint}" } configurations { osstrich } dependencies { osstrich 'com.squareup.osstrich:osstrich:1.2.0' } task publishV1Javadoc(type: JavaExec) { classpath = configurations.osstrich main = 'com.squareup.osstrich.JavadocPublisher' args = [ 'build/javadoc', 'https://github.com/square/sqlbrite', 'com.squareup.sqlbrite' ] } task publishV2Javadoc(type: JavaExec) { classpath = configurations.osstrich main = 'com.squareup.osstrich.JavadocPublisher' args = [ 'build/javadoc', 'https://github.com/square/sqlbrite', 'com.squareup.sqlbrite2' ] } task publishV3Javadoc(type: JavaExec) { classpath = configurations.osstrich main = 'com.squareup.osstrich.JavadocPublisher' args = [ 'build/javadoc', 'https://github.com/square/sqlbrite', 'com.squareup.sqlbrite3' ] } task publishJavadoc(dependsOn: [publishV1Javadoc, publishV2Javadoc, publishV3Javadoc]) ================================================ FILE: gradle/gradle-mvn-push.gradle ================================================ /* * Copyright 2013 Chris Banes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ apply plugin: 'maven' apply plugin: 'signing' def isReleaseBuild() { return VERSION_NAME.contains("SNAPSHOT") == false } def getRepositoryUsername() { return hasProperty('SONATYPE_NEXUS_USERNAME') ? SONATYPE_NEXUS_USERNAME : "" } def getRepositoryPassword() { return hasProperty('SONATYPE_NEXUS_PASSWORD') ? SONATYPE_NEXUS_PASSWORD : "" } afterEvaluate { project -> uploadArchives { repositories { mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } pom.groupId = GROUP pom.artifactId = POM_ARTIFACT_ID pom.version = VERSION_NAME repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) } snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { authentication(userName: getRepositoryUsername(), password: getRepositoryPassword()) } pom.project { name POM_NAME packaging POM_PACKAGING description POM_DESCRIPTION url POM_URL scm { url POM_SCM_URL connection POM_SCM_CONNECTION developerConnection POM_SCM_DEV_CONNECTION } licenses { license { name POM_LICENCE_NAME url POM_LICENCE_URL distribution POM_LICENCE_DIST } } developers { developer { id POM_DEVELOPER_ID name POM_DEVELOPER_NAME } } } } } } signing { required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") } sign configurations.archives } task androidJavadocs(type: Javadoc) { if (!project.plugins.hasPlugin('kotlin-android')) { source = android.sourceSets.main.java.srcDirs } classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) if (JavaVersion.current().isJava8Compatible()) { options.addStringOption('Xdoclint:none', '-quiet') } } task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { classifier = 'javadoc' from androidJavadocs.destinationDir } task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.sourceFiles } artifacts { archives androidSourcesJar archives androidJavadocsJar } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip ================================================ FILE: gradle.properties ================================================ GROUP=com.squareup.sqlbrite3 VERSION_NAME=3.2.1-SNAPSHOT POM_DESCRIPTION=A lightweight wrapper around SQLiteOpenHelper which introduces reactive stream semantics to SQL operations. POM_URL=http://github.com/square/sqlbrite/ POM_SCM_URL=http://github.com/square/sqlbrite/ POM_SCM_CONNECTION=scm:git:git://github.com/square/sqlbrite.git POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/sqlbrite.git POM_LICENCE_NAME=The Apache Software License, Version 2.0 POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt POM_LICENCE_DIST=repo POM_DEVELOPER_ID=square POM_DEVELOPER_NAME=Square, Inc. ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn () { echo "$*" } die () { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: sample/build.gradle ================================================ apply plugin: 'com.android.application' dependencies { implementation rootProject.ext.supportV4 implementation rootProject.ext.supportAnnotations implementation rootProject.ext.dagger annotationProcessor rootProject.ext.daggerCompiler implementation rootProject.ext.butterKnifeRuntime annotationProcessor rootProject.ext.butterKnifeCompiler implementation rootProject.ext.timber implementation rootProject.ext.rxJava implementation rootProject.ext.rxAndroid implementation rootProject.ext.rxBinding compileOnly rootProject.ext.autoValue annotationProcessor rootProject.ext.autoValue annotationProcessor rootProject.ext.autoValueParcel implementation project(':sqlbrite') implementation rootProject.ext.supportSqliteFramework } android { compileSdkVersion versions.compileSdk compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } lintOptions { textOutput 'stdout' textReport true ignore 'InvalidPackage' // Provided AutoValue pulls in Guava and friends. Doesn't end up in APK. } defaultConfig { minSdkVersion versions.minSdk targetSdkVersion versions.compileSdk applicationId 'com.example.sqlbrite.todo' versionCode 1 versionName '1.0' } signingConfigs { debug { storeFile file('debug.keystore') storePassword 'android' keyAlias 'android' keyPassword 'android' } } buildTypes { debug { applicationIdSuffix '.development' signingConfig signingConfigs.debug } } } ================================================ FILE: sample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/TodoApp.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo; import android.app.Application; import android.content.Context; import timber.log.Timber; public final class TodoApp extends Application { private TodoComponent mainComponent; @Override public void onCreate() { super.onCreate(); if (BuildConfig.DEBUG) { Timber.plant(new Timber.DebugTree()); } mainComponent = DaggerTodoComponent.builder().todoModule(new TodoModule(this)).build(); } public static TodoComponent getComponent(Context context) { return ((TodoApp) context.getApplicationContext()).mainComponent; } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/TodoComponent.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo; import com.example.sqlbrite.todo.ui.ItemsFragment; import com.example.sqlbrite.todo.ui.ListsFragment; import com.example.sqlbrite.todo.ui.NewItemFragment; import com.example.sqlbrite.todo.ui.NewListFragment; import dagger.Component; import javax.inject.Singleton; @Singleton @Component(modules = TodoModule.class) public interface TodoComponent { void inject(ListsFragment fragment); void inject(ItemsFragment fragment); void inject(NewItemFragment fragment); void inject(NewListFragment fragment); } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/TodoModule.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo; import android.app.Application; import com.example.sqlbrite.todo.db.DbModule; import dagger.Module; import dagger.Provides; import javax.inject.Singleton; @Module( includes = { DbModule.class, } ) public final class TodoModule { private final Application application; TodoModule(Application application) { this.application = application; } @Provides @Singleton Application provideApplication() { return application; } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/db/Db.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.db; import android.database.Cursor; public final class Db { public static final int BOOLEAN_FALSE = 0; public static final int BOOLEAN_TRUE = 1; public static String getString(Cursor cursor, String columnName) { return cursor.getString(cursor.getColumnIndexOrThrow(columnName)); } public static boolean getBoolean(Cursor cursor, String columnName) { return getInt(cursor, columnName) == BOOLEAN_TRUE; } public static long getLong(Cursor cursor, String columnName) { return cursor.getLong(cursor.getColumnIndexOrThrow(columnName)); } public static int getInt(Cursor cursor, String columnName) { return cursor.getInt(cursor.getColumnIndexOrThrow(columnName)); } private Db() { throw new AssertionError("No instances."); } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/db/DbCallback.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.db; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.db.SupportSQLiteOpenHelper; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL; final class DbCallback extends SupportSQLiteOpenHelper.Callback { private static final int VERSION = 1; private static final String CREATE_LIST = "" + "CREATE TABLE " + TodoList.TABLE + "(" + TodoList.ID + " INTEGER NOT NULL PRIMARY KEY," + TodoList.NAME + " TEXT NOT NULL," + TodoList.ARCHIVED + " INTEGER NOT NULL DEFAULT 0" + ")"; private static final String CREATE_ITEM = "" + "CREATE TABLE " + TodoItem.TABLE + "(" + TodoItem.ID + " INTEGER NOT NULL PRIMARY KEY," + TodoItem.LIST_ID + " INTEGER NOT NULL REFERENCES " + TodoList.TABLE + "(" + TodoList.ID + ")," + TodoItem.DESCRIPTION + " TEXT NOT NULL," + TodoItem.COMPLETE + " INTEGER NOT NULL DEFAULT 0" + ")"; private static final String CREATE_ITEM_LIST_ID_INDEX = "CREATE INDEX item_list_id ON " + TodoItem.TABLE + " (" + TodoItem.LIST_ID + ")"; DbCallback() { super(VERSION); } @Override public void onCreate(SupportSQLiteDatabase db) { db.execSQL(CREATE_LIST); db.execSQL(CREATE_ITEM); db.execSQL(CREATE_ITEM_LIST_ID_INDEX); long groceryListId = db.insert(TodoList.TABLE, CONFLICT_FAIL, new TodoList.Builder() .name("Grocery List") .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(groceryListId) .description("Beer") .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(groceryListId) .description("Point Break on DVD") .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(groceryListId) .description("Bad Boys 2 on DVD") .build()); long holidayPresentsListId = db.insert(TodoList.TABLE, CONFLICT_FAIL, new TodoList.Builder() .name("Holiday Presents") .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(holidayPresentsListId) .description("Pogo Stick for Jake W.") .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(holidayPresentsListId) .description("Jack-in-the-box for Alec S.") .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(holidayPresentsListId) .description("Pogs for Matt P.") .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(holidayPresentsListId) .description("Cola for Jesse W.") .build()); long workListId = db.insert(TodoList.TABLE, CONFLICT_FAIL, new TodoList.Builder() .name("Work Items") .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(workListId) .description("Finish SqlBrite library") .complete(true) .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(workListId) .description("Finish SqlBrite sample app") .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder() .listId(workListId) .description("Publish SqlBrite to GitHub") .build()); long birthdayPresentsListId = db.insert(TodoList.TABLE, CONFLICT_FAIL, new TodoList.Builder() .name("Birthday Presents") .archived(true) .build()); db.insert(TodoItem.TABLE, CONFLICT_FAIL, new TodoItem.Builder().listId(birthdayPresentsListId) .description("New car") .complete(true) .build()); } @Override public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) { } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/db/DbModule.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.db; import android.app.Application; import android.arch.persistence.db.SupportSQLiteOpenHelper; import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration; import android.arch.persistence.db.SupportSQLiteOpenHelper.Factory; import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory; import com.squareup.sqlbrite3.BriteDatabase; import com.squareup.sqlbrite3.SqlBrite; import dagger.Module; import dagger.Provides; import io.reactivex.schedulers.Schedulers; import javax.inject.Singleton; import timber.log.Timber; @Module public final class DbModule { @Provides @Singleton SqlBrite provideSqlBrite() { return new SqlBrite.Builder() .logger(new SqlBrite.Logger() { @Override public void log(String message) { Timber.tag("Database").v(message); } }) .build(); } @Provides @Singleton BriteDatabase provideDatabase(SqlBrite sqlBrite, Application application) { Configuration configuration = Configuration.builder(application) .name("todo.db") .callback(new DbCallback()) .build(); Factory factory = new FrameworkSQLiteOpenHelperFactory(); SupportSQLiteOpenHelper helper = factory.create(configuration); BriteDatabase db = sqlBrite.wrapDatabaseHelper(helper, Schedulers.io()); db.setLoggingEnabled(true); return db; } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/db/TodoItem.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.db; import android.content.ContentValues; import android.database.Cursor; import android.os.Parcelable; import com.google.auto.value.AutoValue; import io.reactivex.functions.Function; @AutoValue public abstract class TodoItem implements Parcelable { public static final String TABLE = "todo_item"; public static final String ID = "_id"; public static final String LIST_ID = "todo_list_id"; public static final String DESCRIPTION = "description"; public static final String COMPLETE = "complete"; public abstract long id(); public abstract long listId(); public abstract String description(); public abstract boolean complete(); public static final Function MAPPER = new Function() { @Override public TodoItem apply(Cursor cursor) { long id = Db.getLong(cursor, ID); long listId = Db.getLong(cursor, LIST_ID); String description = Db.getString(cursor, DESCRIPTION); boolean complete = Db.getBoolean(cursor, COMPLETE); return new AutoValue_TodoItem(id, listId, description, complete); } }; public static final class Builder { private final ContentValues values = new ContentValues(); public Builder id(long id) { values.put(ID, id); return this; } public Builder listId(long listId) { values.put(LIST_ID, listId); return this; } public Builder description(String description) { values.put(DESCRIPTION, description); return this; } public Builder complete(boolean complete) { values.put(COMPLETE, complete ? Db.BOOLEAN_TRUE : Db.BOOLEAN_FALSE); return this; } public ContentValues build() { return values; // TODO defensive copy? } } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/db/TodoList.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.db; import android.content.ContentValues; import android.os.Parcelable; import com.google.auto.value.AutoValue; // Note: normally I wouldn't prefix table classes but I didn't want 'List' to be overloaded. @AutoValue public abstract class TodoList implements Parcelable { public static final String TABLE = "todo_list"; public static final String ID = "_id"; public static final String NAME = "name"; public static final String ARCHIVED = "archived"; public abstract long id(); public abstract String name(); public abstract boolean archived(); public static final class Builder { private final ContentValues values = new ContentValues(); public Builder id(long id) { values.put(ID, id); return this; } public Builder name(String name) { values.put(NAME, name); return this; } public Builder archived(boolean archived) { values.put(ARCHIVED, archived); return this; } public ContentValues build() { return values; // TODO defensive copy? } } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/ui/ItemsAdapter.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.ui; import android.content.Context; import android.text.SpannableString; import android.text.style.StrikethroughSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.CheckedTextView; import com.example.sqlbrite.todo.db.TodoItem; import io.reactivex.functions.Consumer; import java.util.Collections; import java.util.List; final class ItemsAdapter extends BaseAdapter implements Consumer> { private final LayoutInflater inflater; private List items = Collections.emptyList(); public ItemsAdapter(Context context) { inflater = LayoutInflater.from(context); } @Override public void accept(List items) { this.items = items; notifyDataSetChanged(); } @Override public int getCount() { return items.size(); } @Override public TodoItem getItem(int position) { return items.get(position); } @Override public long getItemId(int position) { return getItem(position).id(); } @Override public boolean hasStableIds() { return true; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(android.R.layout.simple_list_item_multiple_choice, parent, false); } TodoItem item = getItem(position); CheckedTextView textView = (CheckedTextView) convertView; textView.setChecked(item.complete()); CharSequence description = item.description(); if (item.complete()) { SpannableString spannable = new SpannableString(description); spannable.setSpan(new StrikethroughSpan(), 0, description.length(), 0); description = spannable; } textView.setText(description); return convertView; } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/ui/ItemsFragment.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.ui; import android.app.Activity; import android.database.Cursor; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import butterknife.BindView; import butterknife.ButterKnife; import com.example.sqlbrite.todo.R; import com.example.sqlbrite.todo.TodoApp; import com.example.sqlbrite.todo.db.Db; import com.example.sqlbrite.todo.db.TodoItem; import com.example.sqlbrite.todo.db.TodoList; import com.jakewharton.rxbinding2.widget.AdapterViewItemClickEvent; import com.jakewharton.rxbinding2.widget.RxAdapterView; import com.squareup.sqlbrite3.BriteDatabase; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import io.reactivex.schedulers.Schedulers; import javax.inject.Inject; import static android.database.sqlite.SQLiteDatabase.CONFLICT_NONE; import static android.support.v4.view.MenuItemCompat.SHOW_AS_ACTION_IF_ROOM; import static android.support.v4.view.MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT; import static com.squareup.sqlbrite3.SqlBrite.Query; public final class ItemsFragment extends Fragment { private static final String KEY_LIST_ID = "list_id"; private static final String LIST_QUERY = "SELECT * FROM " + TodoItem.TABLE + " WHERE " + TodoItem.LIST_ID + " = ? ORDER BY " + TodoItem.COMPLETE + " ASC"; private static final String COUNT_QUERY = "SELECT COUNT(*) FROM " + TodoItem.TABLE + " WHERE " + TodoItem.COMPLETE + " = " + Db.BOOLEAN_FALSE + " AND " + TodoItem.LIST_ID + " = ?"; private static final String TITLE_QUERY = "SELECT " + TodoList.NAME + " FROM " + TodoList.TABLE + " WHERE " + TodoList.ID + " = ?"; public interface Listener { void onNewItemClicked(long listId); } public static ItemsFragment newInstance(long listId) { Bundle arguments = new Bundle(); arguments.putLong(KEY_LIST_ID, listId); ItemsFragment fragment = new ItemsFragment(); fragment.setArguments(arguments); return fragment; } @Inject BriteDatabase db; @BindView(android.R.id.list) ListView listView; @BindView(android.R.id.empty) View emptyView; private Listener listener; private ItemsAdapter adapter; private CompositeDisposable disposables; private long getListId() { return getArguments().getLong(KEY_LIST_ID); } @Override public void onAttach(Activity activity) { if (!(activity instanceof Listener)) { throw new IllegalStateException("Activity must implement fragment Listener."); } super.onAttach(activity); TodoApp.getComponent(activity).inject(this); setHasOptionsMenu(true); listener = (Listener) activity; adapter = new ItemsAdapter(activity); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); MenuItem item = menu.add(R.string.new_item) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { listener.onNewItemClicked(getListId()); return true; } }); MenuItemCompat.setShowAsAction(item, SHOW_AS_ACTION_IF_ROOM | SHOW_AS_ACTION_WITH_TEXT); } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.items, container, false); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ButterKnife.bind(this, view); listView.setEmptyView(emptyView); listView.setAdapter(adapter); RxAdapterView.itemClickEvents(listView) // .observeOn(Schedulers.io()) .subscribe(new Consumer() { @Override public void accept(AdapterViewItemClickEvent event) { boolean newValue = !adapter.getItem(event.position()).complete(); db.update(TodoItem.TABLE, CONFLICT_NONE, new TodoItem.Builder().complete(newValue).build(), TodoItem.ID + " = ?", String.valueOf(event.id())); } }); } @Override public void onResume() { super.onResume(); String listId = String.valueOf(getListId()); disposables = new CompositeDisposable(); Observable itemCount = db.createQuery(TodoItem.TABLE, COUNT_QUERY, listId) // .map(new Function() { @Override public Integer apply(Query query) { Cursor cursor = query.run(); try { if (!cursor.moveToNext()) { throw new AssertionError("No rows"); } return cursor.getInt(0); } finally { cursor.close(); } } }); Observable listName = db.createQuery(TodoList.TABLE, TITLE_QUERY, listId).map(new Function() { @Override public String apply(Query query) { Cursor cursor = query.run(); try { if (!cursor.moveToNext()) { throw new AssertionError("No rows"); } return cursor.getString(0); } finally { cursor.close(); } } }); disposables.add( Observable.combineLatest(listName, itemCount, new BiFunction() { @Override public String apply(String listName, Integer itemCount) { return listName + " (" + itemCount + ")"; } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer() { @Override public void accept(String title) throws Exception { getActivity().setTitle(title); } })); disposables.add(db.createQuery(TodoItem.TABLE, LIST_QUERY, listId) .mapToList(TodoItem.MAPPER) .observeOn(AndroidSchedulers.mainThread()) .subscribe(adapter)); } @Override public void onPause() { super.onPause(); disposables.dispose(); } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/ui/ListsAdapter.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.ui; import android.content.Context; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import io.reactivex.functions.Consumer; import java.util.Collections; import java.util.List; final class ListsAdapter extends BaseAdapter implements Consumer> { private final LayoutInflater inflater; private List items = Collections.emptyList(); public ListsAdapter(Context context) { this.inflater = LayoutInflater.from(context); } @Override public void accept(List items) { this.items = items; notifyDataSetChanged(); } @Override public int getCount() { return items.size(); } @Override public ListsItem getItem(int position) { return items.get(position); } @Override public long getItemId(int position) { return getItem(position).id(); } @Override public boolean hasStableIds() { return true; } @Override public View getView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = inflater.inflate(android.R.layout.simple_list_item_1, parent, false); } ListsItem item = getItem(position); ((TextView) convertView).setText(item.name() + " (" + item.itemCount() + ")"); return convertView; } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/ui/ListsFragment.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.ui; import android.app.Activity; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.view.MenuItemCompat; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.ListView; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnItemClick; import com.example.sqlbrite.todo.R; import com.example.sqlbrite.todo.TodoApp; import com.squareup.sqlbrite3.BriteDatabase; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import javax.inject.Inject; import static android.support.v4.view.MenuItemCompat.SHOW_AS_ACTION_IF_ROOM; import static android.support.v4.view.MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT; public final class ListsFragment extends Fragment { interface Listener { void onListClicked(long id); void onNewListClicked(); } static ListsFragment newInstance() { return new ListsFragment(); } @Inject BriteDatabase db; @BindView(android.R.id.list) ListView listView; @BindView(android.R.id.empty) View emptyView; private Listener listener; private ListsAdapter adapter; private Disposable disposable; @Override public void onAttach(Activity activity) { if (!(activity instanceof Listener)) { throw new IllegalStateException("Activity must implement fragment Listener."); } super.onAttach(activity); TodoApp.getComponent(activity).inject(this); setHasOptionsMenu(true); listener = (Listener) activity; adapter = new ListsAdapter(activity); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); MenuItem item = menu.add(R.string.new_list) .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { listener.onNewListClicked(); return true; } }); MenuItemCompat.setShowAsAction(item, SHOW_AS_ACTION_IF_ROOM | SHOW_AS_ACTION_WITH_TEXT); } @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { return inflater.inflate(R.layout.lists, container, false); } @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); ButterKnife.bind(this, view); listView.setEmptyView(emptyView); listView.setAdapter(adapter); } @OnItemClick(android.R.id.list) void listClicked(long listId) { listener.onListClicked(listId); } @Override public void onResume() { super.onResume(); getActivity().setTitle("To-Do"); disposable = db.createQuery(ListsItem.TABLES, ListsItem.QUERY) .mapToList(ListsItem.MAPPER) .observeOn(AndroidSchedulers.mainThread()) .subscribe(adapter); } @Override public void onPause() { super.onPause(); disposable.dispose(); } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/ui/ListsItem.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.ui; import android.database.Cursor; import android.os.Parcelable; import com.example.sqlbrite.todo.db.Db; import com.example.sqlbrite.todo.db.TodoItem; import com.example.sqlbrite.todo.db.TodoList; import com.google.auto.value.AutoValue; import io.reactivex.functions.Function; import java.util.Arrays; import java.util.Collection; @AutoValue abstract class ListsItem implements Parcelable { private static String ALIAS_LIST = "list"; private static String ALIAS_ITEM = "item"; private static String LIST_ID = ALIAS_LIST + "." + TodoList.ID; private static String LIST_NAME = ALIAS_LIST + "." + TodoList.NAME; private static String ITEM_COUNT = "item_count"; private static String ITEM_ID = ALIAS_ITEM + "." + TodoItem.ID; private static String ITEM_LIST_ID = ALIAS_ITEM + "." + TodoItem.LIST_ID; public static Collection TABLES = Arrays.asList(TodoList.TABLE, TodoItem.TABLE); public static String QUERY = "" + "SELECT " + LIST_ID + ", " + LIST_NAME + ", COUNT(" + ITEM_ID + ") as " + ITEM_COUNT + " FROM " + TodoList.TABLE + " AS " + ALIAS_LIST + " LEFT OUTER JOIN " + TodoItem.TABLE + " AS " + ALIAS_ITEM + " ON " + LIST_ID + " = " + ITEM_LIST_ID + " GROUP BY " + LIST_ID; abstract long id(); abstract String name(); abstract int itemCount(); static Function MAPPER = new Function() { @Override public ListsItem apply(Cursor cursor) { long id = Db.getLong(cursor, TodoList.ID); String name = Db.getString(cursor, TodoList.NAME); int itemCount = Db.getInt(cursor, ITEM_COUNT); return new AutoValue_ListsItem(id, name, itemCount); } }; } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/ui/MainActivity.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.ui; import android.os.Bundle; import android.support.v4.app.FragmentActivity; import com.example.sqlbrite.todo.R; public final class MainActivity extends FragmentActivity implements ListsFragment.Listener, ItemsFragment.Listener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { getSupportFragmentManager().beginTransaction() .add(android.R.id.content, ListsFragment.newInstance()) .commit(); } } @Override public void onListClicked(long id) { getSupportFragmentManager().beginTransaction() .setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right) .replace(android.R.id.content, ItemsFragment.newInstance(id)) .addToBackStack(null) .commit(); } @Override public void onNewListClicked() { NewListFragment.newInstance().show(getSupportFragmentManager(), "new-list"); } @Override public void onNewItemClicked(long listId) { NewItemFragment.newInstance(listId).show(getSupportFragmentManager(), "new-item"); } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/ui/NewItemFragment.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.ui; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; import com.example.sqlbrite.todo.R; import com.example.sqlbrite.todo.TodoApp; import com.example.sqlbrite.todo.db.TodoItem; import com.jakewharton.rxbinding2.widget.RxTextView; import com.squareup.sqlbrite3.BriteDatabase; import io.reactivex.Observable; import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; import javax.inject.Inject; import static android.database.sqlite.SQLiteDatabase.CONFLICT_NONE; import static butterknife.ButterKnife.findById; public final class NewItemFragment extends DialogFragment { private static final String KEY_LIST_ID = "list_id"; public static NewItemFragment newInstance(long listId) { Bundle arguments = new Bundle(); arguments.putLong(KEY_LIST_ID, listId); NewItemFragment fragment = new NewItemFragment(); fragment.setArguments(arguments); return fragment; } private final PublishSubject createClicked = PublishSubject.create(); @Inject BriteDatabase db; private long getListId() { return getArguments().getLong(KEY_LIST_ID); } @Override public void onAttach(Activity activity) { super.onAttach(activity); TodoApp.getComponent(activity).inject(this); } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Context context = getActivity(); View view = LayoutInflater.from(context).inflate(R.layout.new_item, null); EditText name = findById(view, android.R.id.input); Observable.combineLatest(createClicked, RxTextView.textChanges(name), new BiFunction() { @Override public String apply(String ignored, CharSequence text) { return text.toString(); } }) // .observeOn(Schedulers.io()) .subscribe(new Consumer() { @Override public void accept(String description) { db.insert(TodoItem.TABLE, CONFLICT_NONE, new TodoItem.Builder().listId(getListId()).description(description).build()); } }); return new AlertDialog.Builder(context) // .setTitle(R.string.new_item) .setView(view) .setPositiveButton(R.string.create, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { createClicked.onNext("clicked"); } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(@NonNull DialogInterface dialog, int which) { } }) .create(); } } ================================================ FILE: sample/src/main/java/com/example/sqlbrite/todo/ui/NewListFragment.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.example.sqlbrite.todo.ui; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.DialogFragment; import android.view.LayoutInflater; import android.view.View; import android.widget.EditText; import com.example.sqlbrite.todo.R; import com.example.sqlbrite.todo.TodoApp; import com.example.sqlbrite.todo.db.TodoList; import com.jakewharton.rxbinding2.widget.RxTextView; import com.squareup.sqlbrite3.BriteDatabase; import io.reactivex.Observable; import io.reactivex.functions.BiFunction; import io.reactivex.functions.Consumer; import io.reactivex.schedulers.Schedulers; import io.reactivex.subjects.PublishSubject; import javax.inject.Inject; import static android.database.sqlite.SQLiteDatabase.CONFLICT_NONE; import static butterknife.ButterKnife.findById; public final class NewListFragment extends DialogFragment { public static NewListFragment newInstance() { return new NewListFragment(); } private final PublishSubject createClicked = PublishSubject.create(); @Inject BriteDatabase db; @Override public void onAttach(Activity activity) { super.onAttach(activity); TodoApp.getComponent(activity).inject(this); } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Context context = getActivity(); View view = LayoutInflater.from(context).inflate(R.layout.new_list, null); EditText name = findById(view, android.R.id.input); Observable.combineLatest(createClicked, RxTextView.textChanges(name), new BiFunction() { @Override public String apply(String ignored, CharSequence text) { return text.toString(); } }) // .observeOn(Schedulers.io()) .subscribe(new Consumer() { @Override public void accept(String name) { db.insert(TodoList.TABLE, CONFLICT_NONE, new TodoList.Builder().name(name).build()); } }); return new AlertDialog.Builder(context) // .setTitle(R.string.new_list) .setView(view) .setPositiveButton(R.string.create, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { createClicked.onNext("clicked"); } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(@NonNull DialogInterface dialog, int which) { } }) .create(); } } ================================================ FILE: sample/src/main/res/anim/slide_in_left.xml ================================================ ================================================ FILE: sample/src/main/res/anim/slide_in_right.xml ================================================ ================================================ FILE: sample/src/main/res/anim/slide_out_left.xml ================================================ ================================================ FILE: sample/src/main/res/anim/slide_out_right.xml ================================================ ================================================ FILE: sample/src/main/res/layout/items.xml ================================================ ================================================ FILE: sample/src/main/res/layout/lists.xml ================================================ ================================================ FILE: sample/src/main/res/layout/new_item.xml ================================================ ================================================ FILE: sample/src/main/res/layout/new_list.xml ================================================ ================================================ FILE: sample/src/main/res/values/strings.xml ================================================ SqlBrite To-Do Create Cancel New List Name New Item Description ================================================ FILE: settings.gradle ================================================ include ':sqlbrite' include ':sqlbrite-kotlin' include ':sqlbrite-lint' include ':sample' rootProject.name = 'sqlbrite-root' ================================================ FILE: sqlbrite/build.gradle ================================================ apply plugin: 'com.android.library' dependencies { api rootProject.ext.rxJava api rootProject.ext.supportSqlite implementation rootProject.ext.supportAnnotations androidTestImplementation rootProject.ext.supportTestRunner androidTestImplementation rootProject.ext.truth androidTestImplementation rootProject.ext.supportSqliteFramework lintChecks project(':sqlbrite-lint') } android { compileSdkVersion versions.compileSdk defaultConfig { minSdkVersion versions.minSdk testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } lintOptions { textOutput 'stdout' textReport true } // TODO replace with https://issuetracker.google.com/issues/72050365 once released. libraryVariants.all { it.generateBuildConfig.enabled = false } } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') ================================================ FILE: sqlbrite/gradle.properties ================================================ POM_ARTIFACT_ID=sqlbrite POM_NAME=SqlBrite POM_PACKAGING=aar ================================================ FILE: sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/BlockingRecordingObserver.java ================================================ /* * Copyright (C) 2016 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; final class BlockingRecordingObserver extends RecordingObserver { protected Object takeEvent() { try { Object item = events.pollFirst(1, SECONDS); if (item == null) { throw new AssertionError("No items."); } return item; } catch (InterruptedException e) { throw new RuntimeException(e); } } @Override public void assertNoMoreEvents() { try { assertThat(events.pollFirst(1, SECONDS)).isNull(); } catch (InterruptedException e) { throw new RuntimeException(e); } } } ================================================ FILE: sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/BriteContentResolverTest.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.content.ContentResolver; import android.content.ContentValues; import android.database.Cursor; import android.database.MatrixCursor; import android.net.Uri; import android.test.ProviderTestCase2; import android.test.mock.MockContentProvider; import com.squareup.sqlbrite3.SqlBrite.Query; import io.reactivex.Observable; import io.reactivex.ObservableSource; import io.reactivex.ObservableTransformer; import io.reactivex.subjects.PublishSubject; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import static com.google.common.truth.Truth.assertThat; public final class BriteContentResolverTest extends ProviderTestCase2 { private static final Uri AUTHORITY = Uri.parse("content://test_authority"); private static final Uri TABLE = AUTHORITY.buildUpon().appendPath("test_table").build(); private static final String KEY = "test_key"; private static final String VALUE = "test_value"; private final List logs = new ArrayList<>(); private final RecordingObserver o = new BlockingRecordingObserver(); private final TestScheduler scheduler = new TestScheduler(); private final PublishSubject killSwitch = PublishSubject.create(); private ContentResolver contentResolver; private BriteContentResolver db; public BriteContentResolverTest() { super(TestContentProvider.class, AUTHORITY.getAuthority()); } @Override protected void setUp() throws Exception { super.setUp(); contentResolver = getMockContentResolver(); SqlBrite.Logger logger = new SqlBrite.Logger() { @Override public void log(String message) { logs.add(message); } }; ObservableTransformer queryTransformer = new ObservableTransformer() { @Override public ObservableSource apply(Observable upstream) { return upstream.takeUntil(killSwitch); } }; db = new BriteContentResolver(contentResolver, logger, scheduler, queryTransformer); getProvider().init(getContext().getContentResolver()); } @Override public void tearDown() { o.assertNoMoreEvents(); o.dispose(); } public void testLoggerEnabled() { db.setLoggingEnabled(true); db.createQuery(TABLE, null, null, null, null, false).subscribe(o); o.assertCursor().isExhausted(); contentResolver.insert(TABLE, values("key1", "value1")); o.assertCursor().hasRow("key1", "value1").isExhausted(); assertThat(logs).isNotEmpty(); } public void testLoggerDisabled() { db.setLoggingEnabled(false); contentResolver.insert(TABLE, values("key1", "value1")); assertThat(logs).isEmpty(); } public void testCreateQueryObservesInsert() { db.createQuery(TABLE, null, null, null, null, false).subscribe(o); o.assertCursor().isExhausted(); contentResolver.insert(TABLE, values("key1", "val1")); o.assertCursor().hasRow("key1", "val1").isExhausted(); } public void testCreateQueryObservesUpdate() { contentResolver.insert(TABLE, values("key1", "val1")); db.createQuery(TABLE, null, null, null, null, false).subscribe(o); o.assertCursor().hasRow("key1", "val1").isExhausted(); contentResolver.update(TABLE, values("key1", "val2"), null, null); o.assertCursor().hasRow("key1", "val2").isExhausted(); } public void testCreateQueryObservesDelete() { contentResolver.insert(TABLE, values("key1", "val1")); db.createQuery(TABLE, null, null, null, null, false).subscribe(o); o.assertCursor().hasRow("key1", "val1").isExhausted(); contentResolver.delete(TABLE, null, null); o.assertCursor().isExhausted(); } public void testUnsubscribeDoesNotTrigger() { db.createQuery(TABLE, null, null, null, null, false).subscribe(o); o.assertCursor().isExhausted(); o.dispose(); contentResolver.insert(TABLE, values("key1", "val1")); o.assertNoMoreEvents(); assertThat(logs).isEmpty(); } public void testQueryNotNotifiedWhenQueryTransformerDisposed() { db.createQuery(TABLE, null, null, null, null, false).subscribe(o); o.assertCursor().isExhausted(); killSwitch.onNext("kill"); o.assertIsCompleted(); contentResolver.insert(TABLE, values("key1", "val1")); o.assertNoMoreEvents(); } public void testInitialValueAndTriggerUsesScheduler() { scheduler.runTasksImmediately(false); db.createQuery(TABLE, null, null, null, null, false).subscribe(o); o.assertNoMoreEvents(); scheduler.triggerActions(); o.assertCursor().isExhausted(); contentResolver.insert(TABLE, values("key1", "val1")); o.assertNoMoreEvents(); scheduler.triggerActions(); o.assertCursor().hasRow("key1", "val1").isExhausted(); } private ContentValues values(String key, String value) { ContentValues result = new ContentValues(); result.put(KEY, key); result.put(VALUE, value); return result; } public static final class TestContentProvider extends MockContentProvider { private final Map storage = new LinkedHashMap<>(); private ContentResolver contentResolver; void init(ContentResolver contentResolver) { this.contentResolver = contentResolver; } @Override public Uri insert(Uri uri, ContentValues values) { storage.put(values.getAsString(KEY), values.getAsString(VALUE)); contentResolver.notifyChange(uri, null); return Uri.parse(AUTHORITY + "/" + values.getAsString(KEY)); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { for (String key : storage.keySet()) { storage.put(key, values.getAsString(VALUE)); } contentResolver.notifyChange(uri, null); return storage.size(); } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int result = storage.size(); storage.clear(); contentResolver.notifyChange(uri, null); return result; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { MatrixCursor result = new MatrixCursor(new String[] { KEY, VALUE }); for (Map.Entry entry : storage.entrySet()) { result.addRow(new Object[] { entry.getKey(), entry.getValue() }); } return result; } } } ================================================ FILE: sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/BriteDatabaseTest.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.annotation.TargetApi; import android.arch.persistence.db.SimpleSQLiteQuery; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.db.SupportSQLiteOpenHelper; import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration; import android.arch.persistence.db.SupportSQLiteOpenHelper.Factory; import android.arch.persistence.db.SupportSQLiteStatement; import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory; import android.content.ContentValues; import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteException; import android.os.Build; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SdkSuppress; import android.support.test.runner.AndroidJUnit4; import com.squareup.sqlbrite3.BriteDatabase.Transaction; import com.squareup.sqlbrite3.RecordingObserver.CursorAssert; import com.squareup.sqlbrite3.TestDb.Employee; import io.reactivex.Observable; import io.reactivex.ObservableSource; import io.reactivex.ObservableTransformer; import io.reactivex.functions.Consumer; import io.reactivex.subjects.PublishSubject; import java.io.Closeable; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CountDownLatch; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import static android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE; import static android.database.sqlite.SQLiteDatabase.CONFLICT_NONE; import static com.google.common.truth.Truth.assertThat; import static com.squareup.sqlbrite3.SqlBrite.Query; import static com.squareup.sqlbrite3.TestDb.BOTH_TABLES; import static com.squareup.sqlbrite3.TestDb.EmployeeTable.NAME; import static com.squareup.sqlbrite3.TestDb.EmployeeTable.USERNAME; import static com.squareup.sqlbrite3.TestDb.SELECT_EMPLOYEES; import static com.squareup.sqlbrite3.TestDb.SELECT_MANAGER_LIST; import static com.squareup.sqlbrite3.TestDb.TABLE_EMPLOYEE; import static com.squareup.sqlbrite3.TestDb.TABLE_MANAGER; import static com.squareup.sqlbrite3.TestDb.employee; import static com.squareup.sqlbrite3.TestDb.manager; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.fail; @RunWith(AndroidJUnit4.class) // public final class BriteDatabaseTest { private final TestDb testDb = new TestDb(); private final List logs = new ArrayList<>(); private final RecordingObserver o = new RecordingObserver(); private final TestScheduler scheduler = new TestScheduler(); private final PublishSubject killSwitch = PublishSubject.create(); @Rule public final TemporaryFolder dbFolder = new TemporaryFolder(); private SupportSQLiteDatabase real; private BriteDatabase db; @Before public void setUp() throws IOException { Configuration configuration = Configuration.builder(InstrumentationRegistry.getContext()) .callback(testDb) .name(dbFolder.newFile().getPath()) .build(); Factory factory = new FrameworkSQLiteOpenHelperFactory(); SupportSQLiteOpenHelper helper = factory.create(configuration); real = helper.getWritableDatabase(); SqlBrite.Logger logger = new SqlBrite.Logger() { @Override public void log(String message) { logs.add(message); } }; ObservableTransformer queryTransformer = new ObservableTransformer() { @Override public ObservableSource apply(Observable upstream) { return upstream.takeUntil(killSwitch); } }; db = new BriteDatabase(helper, logger, scheduler, queryTransformer); } @After public void tearDown() { o.assertNoMoreEvents(); } @Test public void loggerEnabled() { db.setLoggingEnabled(true); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); assertThat(logs).isNotEmpty(); } @Test public void loggerDisabled() { db.setLoggingEnabled(false); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); assertThat(logs).isEmpty(); } @Test public void loggerIndentsSqlForCreateQuery() { db.setLoggingEnabled(true); QueryObservable query = db.createQuery(TABLE_EMPLOYEE, "SELECT\n1"); query.subscribe(new Consumer() { @Override public void accept(Query query) throws Exception { query.run().close(); } }); assertThat(logs).containsExactly("" + "QUERY\n" + " tables: [employee]\n" + " sql: SELECT\n" + " 1"); } @Test public void loggerIndentsSqlForQuery() { db.setLoggingEnabled(true); db.query("SELECT\n1").close(); assertThat(logs).containsExactly("" + "QUERY\n" + " sql: SELECT\n" + " 1\n" + " args: []"); } @Test public void loggerIndentsSqlForExecute() { db.setLoggingEnabled(true); db.execute("PRAGMA\ncompile_options"); assertThat(logs).containsExactly("" + "EXECUTE\n" + " sql: PRAGMA\n" + " compile_options"); } @Test public void loggerIndentsSqlForExecuteWithArgs() { db.setLoggingEnabled(true); db.execute("PRAGMA\ncompile_options", new Object[0]); assertThat(logs).containsExactly("" + "EXECUTE\n" + " sql: PRAGMA\n" + " compile_options\n" + " args: []"); } @Test public void closePropagates() { db.close(); assertThat(real.isOpen()).isFalse(); } @Test public void query() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); } @Test public void queryWithQueryObject() { db.createQuery(TABLE_EMPLOYEE, new SimpleSQLiteQuery(SELECT_EMPLOYEES)).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); } @Test public void queryMapToList() { List employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .mapToList(Employee.MAPPER) .blockingFirst(); assertThat(employees).containsExactly( // new Employee("alice", "Alice Allison"), // new Employee("bob", "Bob Bobberson"), // new Employee("eve", "Eve Evenson")); } @Test public void queryMapToOne() { Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") .mapToOne(Employee.MAPPER) .blockingFirst(); assertThat(employees).isEqualTo(new Employee("alice", "Alice Allison")); } @Test public void queryMapToOneOrDefault() { Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") .mapToOneOrDefault(Employee.MAPPER, new Employee("wrong", "Wrong Person")) .blockingFirst(); assertThat(employees).isEqualTo(new Employee("alice", "Alice Allison")); } @Test public void badQueryCallsError() { // safeSubscribe is needed because the error occurs in onNext and will otherwise bubble up // to the thread exception handler. db.createQuery(TABLE_EMPLOYEE, "SELECT * FROM missing").safeSubscribe(o); o.assertErrorContains("no such table: missing"); } @Test public void queryWithArgs() { db.createQuery( TABLE_EMPLOYEE, SELECT_EMPLOYEES + " WHERE " + USERNAME + " = ?", "bob") .subscribe(o); o.assertCursor() .hasRow("bob", "Bob Bobberson") .isExhausted(); } @Test public void queryObservesInsert() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .isExhausted(); } @Test public void queryInitialValueAndTriggerUsesScheduler() { scheduler.runTasksImmediately(false); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertNoMoreEvents(); scheduler.triggerActions(); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); o.assertNoMoreEvents(); scheduler.triggerActions(); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .isExhausted(); } @Test public void queryNotNotifiedWhenInsertFails() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.insert(TABLE_EMPLOYEE, CONFLICT_IGNORE, employee("bob", "Bob Bobberson")); o.assertNoMoreEvents(); } @Test public void queryNotNotifiedWhenQueryTransformerUnsubscribes() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); killSwitch.onNext("kill"); o.assertIsCompleted(); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); o.assertNoMoreEvents(); } @Test public void queryObservesUpdate() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); ContentValues values = new ContentValues(); values.put(NAME, "Robert Bobberson"); db.update(TABLE_EMPLOYEE, CONFLICT_NONE, values, USERNAME + " = 'bob'"); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Robert Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); } @Test public void queryNotNotifiedWhenUpdateAffectsZeroRows() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); ContentValues values = new ContentValues(); values.put(NAME, "John Johnson"); db.update(TABLE_EMPLOYEE, CONFLICT_NONE, values, USERNAME + " = 'john'"); o.assertNoMoreEvents(); } @Test public void queryObservesDelete() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.delete(TABLE_EMPLOYEE, USERNAME + " = 'bob'"); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("eve", "Eve Evenson") .isExhausted(); } @Test public void queryNotNotifiedWhenDeleteAffectsZeroRows() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.delete(TABLE_EMPLOYEE, USERNAME + " = 'john'"); o.assertNoMoreEvents(); } @Test public void queryMultipleTables() { db.createQuery(BOTH_TABLES, SELECT_MANAGER_LIST).subscribe(o); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); } @Test public void queryMultipleTablesWithQueryObject() { db.createQuery(BOTH_TABLES, new SimpleSQLiteQuery(SELECT_MANAGER_LIST)).subscribe(o); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); } @Test public void queryMultipleTablesObservesChanges() { db.createQuery(BOTH_TABLES, SELECT_MANAGER_LIST).subscribe(o); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); // A new employee triggers, despite the fact that it's not in our result set. db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); // A new manager also triggers and it is in our result set. db.insert(TABLE_MANAGER, CONFLICT_NONE, manager(testDb.bobId, testDb.eveId)); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .hasRow("Bob Bobberson", "Eve Evenson") .isExhausted(); } @Test public void queryMultipleTablesObservesChangesOnlyOnce() { // Employee table is in this list twice. We should still only be notified once for a change. List tables = Arrays.asList(TABLE_EMPLOYEE, TABLE_MANAGER, TABLE_EMPLOYEE); db.createQuery(tables, SELECT_MANAGER_LIST).subscribe(o); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); ContentValues values = new ContentValues(); values.put(NAME, "Even Evenson"); db.update(TABLE_EMPLOYEE, CONFLICT_NONE, values, USERNAME + " = 'eve'"); o.assertCursor() .hasRow("Even Evenson", "Alice Allison") .isExhausted(); } @Test public void queryNotNotifiedAfterDispose() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); o.dispose(); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); o.assertNoMoreEvents(); } @Test public void queryOnlyNotifiedAfterSubscribe() { Observable query = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES); o.assertNoMoreEvents(); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); o.assertNoMoreEvents(); query.subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .isExhausted(); } @Test public void executeSqlNoTrigger() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .skip(1) // Skip initial .subscribe(o); db.execute("UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = 'Zach'"); o.assertNoMoreEvents(); } @Test public void executeSqlWithArgsNoTrigger() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .skip(1) // Skip initial .subscribe(o); db.execute("UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = ?", "Zach"); o.assertNoMoreEvents(); } @Test public void executeSqlAndTrigger() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeAndTrigger(TABLE_EMPLOYEE, "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = 'Zach'"); o.assertCursor() .hasRow("alice", "Zach") .hasRow("bob", "Zach") .hasRow("eve", "Zach") .isExhausted(); } @Test public void executeSqlAndTriggerMultipleTables() { db.createQuery(TABLE_MANAGER, SELECT_MANAGER_LIST).subscribe(o); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); final RecordingObserver employeeObserver = new RecordingObserver(); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(employeeObserver); employeeObserver.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); final Set tablesToTrigger = Collections.unmodifiableSet(new HashSet<>(BOTH_TABLES)); db.executeAndTrigger(tablesToTrigger, "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = 'Zach'"); o.assertCursor() .hasRow("Zach", "Zach") .isExhausted(); employeeObserver.assertCursor() .hasRow("alice", "Zach") .hasRow("bob", "Zach") .hasRow("eve", "Zach") .isExhausted(); } @Test public void executeSqlAndTriggerWithNoTables() { db.createQuery(TABLE_MANAGER, SELECT_MANAGER_LIST).subscribe(o); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); db.executeAndTrigger(Collections.emptySet(), "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = 'Zach'"); o.assertNoMoreEvents(); } @Test public void executeSqlThrowsAndDoesNotTrigger() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .skip(1) // Skip initial .subscribe(o); try { db.executeAndTrigger(TABLE_EMPLOYEE, "UPDATE not_a_table SET " + NAME + " = 'Zach'"); fail(); } catch (SQLException ignored) { } o.assertNoMoreEvents(); } @Test public void executeSqlWithArgsAndTrigger() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeAndTrigger(TABLE_EMPLOYEE, "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = ?", "Zach"); o.assertCursor() .hasRow("alice", "Zach") .hasRow("bob", "Zach") .hasRow("eve", "Zach") .isExhausted(); } @Test public void executeSqlWithArgsThrowsAndDoesNotTrigger() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .skip(1) // Skip initial .subscribe(o); try { db.executeAndTrigger(TABLE_EMPLOYEE, "UPDATE not_a_table SET " + NAME + " = ?", "Zach"); fail(); } catch (SQLException ignored) { } o.assertNoMoreEvents(); } @Test public void executeSqlWithArgsAndTriggerWithMultipleTables() { db.createQuery(TABLE_MANAGER, SELECT_MANAGER_LIST).subscribe(o); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); final RecordingObserver employeeObserver = new RecordingObserver(); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(employeeObserver); employeeObserver.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); final Set tablesToTrigger = Collections.unmodifiableSet(new HashSet<>(BOTH_TABLES)); db.executeAndTrigger(tablesToTrigger, "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = ?", "Zach"); o.assertCursor() .hasRow("Zach", "Zach") .isExhausted(); employeeObserver.assertCursor() .hasRow("alice", "Zach") .hasRow("bob", "Zach") .hasRow("eve", "Zach") .isExhausted(); } @Test public void executeSqlWithArgsAndTriggerWithNoTables() { db.createQuery(BOTH_TABLES, SELECT_MANAGER_LIST).subscribe(o); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); db.executeAndTrigger(Collections.emptySet(), "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = ?", "Zach"); o.assertNoMoreEvents(); } @Test public void executeInsertAndTrigger() { SupportSQLiteStatement statement = real.compileStatement("INSERT INTO " + TABLE_EMPLOYEE + " (" + NAME + ", " + USERNAME + ") " + "VALUES ('Chad Chadson', 'chad')"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeInsert(TABLE_EMPLOYEE, statement); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("chad", "Chad Chadson") .isExhausted(); } @Test public void executeInsertAndDontTrigger() { SupportSQLiteStatement statement = real.compileStatement("INSERT OR IGNORE INTO " + TABLE_EMPLOYEE + " (" + NAME + ", " + USERNAME + ") " + "VALUES ('Alice Allison', 'alice')"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeInsert(TABLE_EMPLOYEE, statement); o.assertNoMoreEvents(); } @Test public void executeInsertAndTriggerMultipleTables() { SupportSQLiteStatement statement = real.compileStatement("INSERT INTO " + TABLE_EMPLOYEE + " (" + NAME + ", " + USERNAME + ") " + "VALUES ('Chad Chadson', 'chad')"); final RecordingObserver managerObserver = new RecordingObserver(); db.createQuery(TABLE_MANAGER, SELECT_MANAGER_LIST).subscribe(managerObserver); managerObserver.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); final Set employeeAndManagerTables = Collections.unmodifiableSet(new HashSet<>( BOTH_TABLES)); db.executeInsert(employeeAndManagerTables, statement); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("chad", "Chad Chadson") .isExhausted(); managerObserver.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); } @Test public void executeInsertAndTriggerNoTables() { SupportSQLiteStatement statement = real.compileStatement("INSERT INTO " + TABLE_EMPLOYEE + " (" + NAME + ", " + USERNAME + ") " + "VALUES ('Chad Chadson', 'chad')"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeInsert(Collections.emptySet(), statement); o.assertNoMoreEvents(); } @Test public void executeInsertThrowsAndDoesNotTrigger() { SupportSQLiteStatement statement = real.compileStatement("INSERT INTO " + TABLE_EMPLOYEE + " (" + NAME + ", " + USERNAME + ") " + "VALUES ('Alice Allison', 'alice')"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .skip(1) // Skip initial .subscribe(o); try { db.executeInsert(TABLE_EMPLOYEE, statement); fail(); } catch (SQLException ignored) { } o.assertNoMoreEvents(); } @Test public void executeInsertWithArgsAndTrigger() { SupportSQLiteStatement statement = real.compileStatement("INSERT INTO " + TABLE_EMPLOYEE + " (" + NAME + ", " + USERNAME + ") VALUES (?, ?)"); statement.bindString(1, "Chad Chadson"); statement.bindString(2, "chad"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeInsert(TABLE_EMPLOYEE, statement); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("chad", "Chad Chadson") .isExhausted(); } @Test public void executeInsertWithArgsThrowsAndDoesNotTrigger() { SupportSQLiteStatement statement = real.compileStatement("INSERT INTO " + TABLE_EMPLOYEE + " (" + NAME + ", " + USERNAME + ") VALUES (?, ?)"); statement.bindString(1, "Alice Aliison"); statement.bindString(2, "alice"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .skip(1) // Skip initial .subscribe(o); try { db.executeInsert(TABLE_EMPLOYEE, statement); fail(); } catch (SQLException ignored) { } o.assertNoMoreEvents(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.HONEYCOMB) @Test public void executeUpdateDeleteAndTrigger() { SupportSQLiteStatement statement = real.compileStatement( "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = 'Zach'"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeUpdateDelete(TABLE_EMPLOYEE, statement); o.assertCursor() .hasRow("alice", "Zach") .hasRow("bob", "Zach") .hasRow("eve", "Zach") .isExhausted(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.HONEYCOMB) @Test public void executeUpdateDeleteAndDontTrigger() { SupportSQLiteStatement statement = real.compileStatement("" + "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = 'Zach'" + " WHERE " + NAME + " = 'Rob'"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeUpdateDelete(TABLE_EMPLOYEE, statement); o.assertNoMoreEvents(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.HONEYCOMB) @Test public void executeUpdateDeleteAndTriggerWithMultipleTables() { SupportSQLiteStatement statement = real.compileStatement( "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = 'Zach'"); final RecordingObserver managerObserver = new RecordingObserver(); db.createQuery(TABLE_MANAGER, SELECT_MANAGER_LIST).subscribe(managerObserver); managerObserver.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); final Set employeeAndManagerTables = Collections.unmodifiableSet(new HashSet<>(BOTH_TABLES)); db.executeUpdateDelete(employeeAndManagerTables, statement); o.assertCursor() .hasRow("alice", "Zach") .hasRow("bob", "Zach") .hasRow("eve", "Zach") .isExhausted(); managerObserver.assertCursor() .hasRow("Zach", "Zach") .isExhausted(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.HONEYCOMB) @Test public void executeUpdateDeleteAndTriggerWithNoTables() { SupportSQLiteStatement statement = real.compileStatement( "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = 'Zach'"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeUpdateDelete(Collections.emptySet(), statement); o.assertNoMoreEvents(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.HONEYCOMB) @Test public void executeUpdateDeleteThrowsAndDoesNotTrigger() { SupportSQLiteStatement statement = real.compileStatement( "UPDATE " + TABLE_EMPLOYEE + " SET " + USERNAME + " = 'alice'"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .skip(1) // Skip initial .subscribe(o); try { db.executeUpdateDelete(TABLE_EMPLOYEE, statement); fail(); } catch (SQLException ignored) { } o.assertNoMoreEvents(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.HONEYCOMB) @Test public void executeUpdateDeleteWithArgsAndTrigger() { SupportSQLiteStatement statement = real.compileStatement( "UPDATE " + TABLE_EMPLOYEE + " SET " + NAME + " = ?"); statement.bindString(1, "Zach"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.executeUpdateDelete(TABLE_EMPLOYEE, statement); o.assertCursor() .hasRow("alice", "Zach") .hasRow("bob", "Zach") .hasRow("eve", "Zach") .isExhausted(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.HONEYCOMB) @Test public void executeUpdateDeleteWithArgsThrowsAndDoesNotTrigger() { SupportSQLiteStatement statement = real.compileStatement( "UPDATE " + TABLE_EMPLOYEE + " SET " + USERNAME + " = ?"); statement.bindString(1, "alice"); db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .skip(1) // Skip initial .subscribe(o); try { db.executeUpdateDelete(TABLE_EMPLOYEE, statement); fail(); } catch (SQLException ignored) { } o.assertNoMoreEvents(); } @Test public void transactionOnlyNotifiesOnce() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); Transaction transaction = db.newTransaction(); try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("nick", "Nick Nickers")); o.assertNoMoreEvents(); transaction.markSuccessful(); } finally { transaction.end(); } o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .hasRow("nick", "Nick Nickers") .isExhausted(); } @Test public void transactionCreatedFromTransactionNotificationWorks() { // Tests the case where a transaction is created in the subscriber to a query which gets // notified as the result of another transaction being committed. With improper ordering, this // can result in creating a new transaction before the old is committed on the underlying DB. db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .subscribe(new Consumer() { @Override public void accept(Query query) { db.newTransaction().end(); } }); Transaction transaction = db.newTransaction(); try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); transaction.markSuccessful(); } finally { transaction.end(); } } @Test public void transactionIsCloseable() throws IOException { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); Transaction transaction = db.newTransaction(); //noinspection UnnecessaryLocalVariable Closeable closeableTransaction = transaction; // Verify type is implemented. try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("nick", "Nick Nickers")); transaction.markSuccessful(); } finally { closeableTransaction.close(); } o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .hasRow("nick", "Nick Nickers") .isExhausted(); } @Test public void transactionDoesNotThrow() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); Transaction transaction = db.newTransaction(); try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("nick", "Nick Nickers")); transaction.markSuccessful(); } finally { transaction.close(); // Transactions should not throw on close(). } o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .hasRow("nick", "Nick Nickers") .isExhausted(); } @Test public void queryCreatedDuringTransactionThrows() { //noinspection CheckResult db.newTransaction(); try { //noinspection CheckResult db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES); fail(); } catch (IllegalStateException e) { assertThat(e.getMessage()).startsWith("Cannot create observable query in transaction."); } } @Test public void querySubscribedToDuringTransactionThrows() { Observable query = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES); db.newTransaction(); query.subscribe(o); o.assertErrorContains("Cannot subscribe to observable query in a transaction."); } @Test public void callingEndMultipleTimesThrows() { Transaction transaction = db.newTransaction(); transaction.end(); try { transaction.end(); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessage("Not in transaction."); } } @Test public void querySubscribedToDuringTransactionOnDifferentThread() throws InterruptedException { Transaction transaction = db.newTransaction(); final CountDownLatch latch = new CountDownLatch(1); new Thread() { @Override public void run() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); latch.countDown(); } }.start(); Thread.sleep(500); // Wait for the thread to block on initial query. o.assertNoMoreEvents(); transaction.end(); // Allow other queries to continue. latch.await(500, MILLISECONDS); // Wait for thread to observe initial query. o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); } @Test public void queryCreatedBeforeTransactionButSubscribedAfter() { Observable query = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES); Transaction transaction = db.newTransaction(); try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("nick", "Nick Nickers")); transaction.markSuccessful(); } finally { transaction.end(); } query.subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .hasRow("nick", "Nick Nickers") .isExhausted(); } @Test public void synchronousQueryDuringTransaction() { Transaction transaction = db.newTransaction(); try { transaction.markSuccessful(); assertCursor(db.query(SELECT_EMPLOYEES)) .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); } finally { transaction.end(); } } @Test public void synchronousQueryDuringTransactionSeesChanges() { Transaction transaction = db.newTransaction(); try { assertCursor(db.query(SELECT_EMPLOYEES)) .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("nick", "Nick Nickers")); assertCursor(db.query(SELECT_EMPLOYEES)) .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .hasRow("nick", "Nick Nickers") .isExhausted(); transaction.markSuccessful(); } finally { transaction.end(); } } @Test public void synchronousQueryWithSupportSQLiteQueryDuringTransaction() { Transaction transaction = db.newTransaction(); try { transaction.markSuccessful(); assertCursor(db.query(new SimpleSQLiteQuery(SELECT_EMPLOYEES))) .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); } finally { transaction.end(); } } @Test public void synchronousQueryWithSupportSQLiteQueryDuringTransactionSeesChanges() { Transaction transaction = db.newTransaction(); try { assertCursor(db.query(new SimpleSQLiteQuery(SELECT_EMPLOYEES))) .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("nick", "Nick Nickers")); assertCursor(db.query(new SimpleSQLiteQuery(SELECT_EMPLOYEES))) .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .hasRow("nick", "Nick Nickers") .isExhausted(); transaction.markSuccessful(); } finally { transaction.end(); } } @Test public void nestedTransactionsOnlyNotifyOnce() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); Transaction transactionOuter = db.newTransaction(); try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); Transaction transactionInner = db.newTransaction(); try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("nick", "Nick Nickers")); transactionInner.markSuccessful(); } finally { transactionInner.end(); } transactionOuter.markSuccessful(); } finally { transactionOuter.end(); } o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .hasRow("john", "John Johnson") .hasRow("nick", "Nick Nickers") .isExhausted(); } @Test public void nestedTransactionsOnMultipleTables() { db.createQuery(BOTH_TABLES, SELECT_MANAGER_LIST).subscribe(o); o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .isExhausted(); Transaction transactionOuter = db.newTransaction(); try { Transaction transactionInner = db.newTransaction(); try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); transactionInner.markSuccessful(); } finally { transactionInner.end(); } transactionInner = db.newTransaction(); try { db.insert(TABLE_MANAGER, CONFLICT_NONE, manager(testDb.aliceId, testDb.bobId)); transactionInner.markSuccessful(); } finally { transactionInner.end(); } transactionOuter.markSuccessful(); } finally { transactionOuter.end(); } o.assertCursor() .hasRow("Eve Evenson", "Alice Allison") .hasRow("Alice Allison", "Bob Bobberson") .isExhausted(); } @Test public void emptyTransactionDoesNotNotify() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); Transaction transaction = db.newTransaction(); try { transaction.markSuccessful(); } finally { transaction.end(); } o.assertNoMoreEvents(); } @Test public void transactionRollbackDoesNotNotify() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES).subscribe(o); o.assertCursor() .hasRow("alice", "Alice Allison") .hasRow("bob", "Bob Bobberson") .hasRow("eve", "Eve Evenson") .isExhausted(); Transaction transaction = db.newTransaction(); try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("john", "John Johnson")); db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("nick", "Nick Nickers")); // No call to set successful. } finally { transaction.end(); } o.assertNoMoreEvents(); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) @SdkSuppress(minSdkVersion = Build.VERSION_CODES.HONEYCOMB) @Test public void nonExclusiveTransactionWorks() throws InterruptedException { final CountDownLatch transactionStarted = new CountDownLatch(1); final CountDownLatch transactionProceed = new CountDownLatch(1); final CountDownLatch transactionCompleted = new CountDownLatch(1); new Thread() { @Override public void run() { Transaction transaction = db.newNonExclusiveTransaction(); transactionStarted.countDown(); try { db.insert(TABLE_EMPLOYEE, CONFLICT_NONE, employee("hans", "Hans Hanson")); transactionProceed.await(10, SECONDS); } catch (InterruptedException e) { throw new RuntimeException("Exception in transaction thread", e); } transaction.markSuccessful(); transaction.close(); transactionCompleted.countDown(); } }.start(); assertThat(transactionStarted.await(10, SECONDS)).isTrue(); //Simple query Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") .lift(Query.mapToOne(Employee.MAPPER)) .blockingFirst(); assertThat(employees).isEqualTo(new Employee("alice", "Alice Allison")); transactionProceed.countDown(); assertThat(transactionCompleted.await(10, SECONDS)).isTrue(); } @Test public void badQueryThrows() { try { //noinspection CheckResult db.query("SELECT * FROM missing"); fail(); } catch (SQLiteException e) { assertThat(e.getMessage()).contains("no such table: missing"); } } @Test public void badInsertThrows() { try { db.insert("missing", CONFLICT_NONE, employee("john", "John Johnson")); fail(); } catch (SQLiteException e) { assertThat(e.getMessage()).contains("no such table: missing"); } } @Test public void badUpdateThrows() { try { db.update("missing", CONFLICT_NONE, employee("john", "John Johnson"), "1"); fail(); } catch (SQLiteException e) { assertThat(e.getMessage()).contains("no such table: missing"); } } @Test public void badDeleteThrows() { try { db.delete("missing", "1"); fail(); } catch (SQLiteException e) { assertThat(e.getMessage()).contains("no such table: missing"); } } private static CursorAssert assertCursor(Cursor cursor) { return new CursorAssert(cursor); } } ================================================ FILE: sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/QueryObservableTest.java ================================================ /* * Copyright (C) 2017 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.database.Cursor; import android.database.MatrixCursor; import com.squareup.sqlbrite3.QueryObservable; import com.squareup.sqlbrite3.SqlBrite.Query; import io.reactivex.Observable; import io.reactivex.functions.Function; import org.junit.Test; public final class QueryObservableTest { @Test public void mapToListThrowsFromQueryRun() { final IllegalStateException error = new IllegalStateException("test exception"); Query query = new Query() { @Override public Cursor run() { throw error; } }; new QueryObservable(Observable.just(query)) // .mapToList(new Function() { @Override public Object apply(Cursor cursor) { throw new AssertionError("Must not be called"); } }) // .test() // .assertNoValues() // .assertError(error); } @Test public void mapToListThrowsFromMapFunction() { Query query = new Query() { @Override public Cursor run() { MatrixCursor cursor = new MatrixCursor(new String[] { "col1" }); cursor.addRow(new Object[] { "value1" }); return cursor; } }; final IllegalStateException error = new IllegalStateException("test exception"); new QueryObservable(Observable.just(query)) // .mapToList(new Function() { @Override public Object apply(Cursor cursor) { throw error; } }) // .test() // .assertNoValues() // .assertError(error); } } ================================================ FILE: sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/QueryTest.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.arch.persistence.db.SupportSQLiteOpenHelper; import android.arch.persistence.db.SupportSQLiteOpenHelper.Configuration; import android.arch.persistence.db.SupportSQLiteOpenHelper.Factory; import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory; import android.database.Cursor; import android.os.Build; import android.support.annotation.Nullable; import android.support.test.InstrumentationRegistry; import android.support.test.filters.SdkSuppress; import com.squareup.sqlbrite3.SqlBrite.Query; import com.squareup.sqlbrite3.TestDb.Employee; import io.reactivex.Observable; import io.reactivex.functions.Function; import io.reactivex.observers.TestObserver; import io.reactivex.schedulers.Schedulers; import java.util.List; import java.util.Optional; import org.junit.Before; import org.junit.Test; import static com.google.common.truth.Truth.assertThat; import static com.squareup.sqlbrite3.TestDb.Employee.MAPPER; import static com.squareup.sqlbrite3.TestDb.SELECT_EMPLOYEES; import static com.squareup.sqlbrite3.TestDb.TABLE_EMPLOYEE; import static org.junit.Assert.fail; public final class QueryTest { private BriteDatabase db; @Before public void setUp() { Configuration configuration = Configuration.builder(InstrumentationRegistry.getContext()) .callback(new TestDb()) .build(); Factory factory = new FrameworkSQLiteOpenHelperFactory(); SupportSQLiteOpenHelper helper = factory.create(configuration); SqlBrite sqlBrite = new SqlBrite.Builder().build(); db = sqlBrite.wrapDatabaseHelper(helper, Schedulers.trampoline()); } @Test public void mapToOne() { Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") .lift(Query.mapToOne(MAPPER)) .blockingFirst(); assertThat(employees).isEqualTo(new Employee("alice", "Alice Allison")); } @Test public void mapToOneThrowsWhenMapperReturnsNull() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") .lift(Query.mapToOne(new Function() { @Override public Employee apply(Cursor cursor) throws Exception { return null; } })) .test() .assertError(NullPointerException.class) .assertErrorMessage("QueryToOne mapper returned null"); } @Test public void mapToOneThrowsOnMultipleRows() { Observable employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 2") // .lift(Query.mapToOne(MAPPER)); try { employees.blockingFirst(); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessage("Cursor returned more than 1 row"); } } @Test public void mapToOneIgnoresNullCursor() { Query nully = new Query() { @Nullable @Override public Cursor run() { return null; } }; TestObserver observer = new TestObserver<>(); Observable.just(nully) .lift(Query.mapToOne(MAPPER)) .subscribe(observer); observer.assertNoValues(); observer.assertComplete(); } @Test public void mapToOneOrDefault() { Employee employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") .lift(Query.mapToOneOrDefault( MAPPER, new Employee("fred", "Fred Frederson"))) .blockingFirst(); assertThat(employees).isEqualTo(new Employee("alice", "Alice Allison")); } @Test public void mapToOneOrDefaultDisallowsNullDefault() { try { Query.mapToOneOrDefault(MAPPER, null); fail(); } catch (NullPointerException e) { assertThat(e).hasMessage("defaultValue == null"); } } @Test public void mapToOneOrDefaultThrowsWhenMapperReturnsNull() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") .lift(Query.mapToOneOrDefault(new Function() { @Override public Employee apply(Cursor cursor) throws Exception { return null; } }, new Employee("fred", "Fred Frederson"))) .test() .assertError(NullPointerException.class) .assertErrorMessage("QueryToOne mapper returned null"); } @Test public void mapToOneOrDefaultThrowsOnMultipleRows() { Observable employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 2") // .lift(Query.mapToOneOrDefault( MAPPER, new Employee("fred", "Fred Frederson"))); try { employees.blockingFirst(); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessage("Cursor returned more than 1 row"); } } @Test public void mapToOneOrDefaultReturnsDefaultWhenNullCursor() { Employee defaultEmployee = new Employee("bob", "Bob Bobberson"); Query nully = new Query() { @Nullable @Override public Cursor run() { return null; } }; TestObserver observer = new TestObserver<>(); Observable.just(nully) .lift(Query.mapToOneOrDefault(MAPPER, defaultEmployee)) .subscribe(observer); observer.assertValues(defaultEmployee); observer.assertComplete(); } @Test public void mapToList() { List employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) .lift(Query.mapToList(MAPPER)) .blockingFirst(); assertThat(employees).containsExactly( // new Employee("alice", "Alice Allison"), // new Employee("bob", "Bob Bobberson"), // new Employee("eve", "Eve Evenson")); } @Test public void mapToListEmptyWhenNoRows() { List employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " WHERE 1=2") .lift(Query.mapToList(MAPPER)) .blockingFirst(); assertThat(employees).isEmpty(); } @Test public void mapToListReturnsNullOnMapperNull() { Function mapToNull = new Function() { private int count; @Override public Employee apply(Cursor cursor) throws Exception { return count++ == 2 ? null : MAPPER.apply(cursor); } }; List employees = db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES) // .lift(Query.mapToList(mapToNull)) // .blockingFirst(); assertThat(employees).containsExactly( new Employee("alice", "Alice Allison"), new Employee("bob", "Bob Bobberson"), null); } @Test public void mapToListIgnoresNullCursor() { Query nully = new Query() { @Nullable @Override public Cursor run() { return null; } }; TestObserver> subscriber = new TestObserver<>(); Observable.just(nully) .lift(Query.mapToList(MAPPER)) .subscribe(subscriber); subscriber.assertNoValues(); subscriber.assertComplete(); } @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) @Test public void mapToOptional() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") .lift(Query.mapToOptional(MAPPER)) .test() .assertValue(Optional.of(new Employee("alice", "Alice Allison"))); } @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) @Test public void mapToOptionalThrowsWhenMapperReturnsNull() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 1") .lift(Query.mapToOptional(new Function() { @Override public Employee apply(Cursor cursor) throws Exception { return null; } })) .test() .assertError(NullPointerException.class) .assertErrorMessage("QueryToOne mapper returned null"); } @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) @Test public void mapToOptionalThrowsOnMultipleRows() { db.createQuery(TABLE_EMPLOYEE, SELECT_EMPLOYEES + " LIMIT 2") // .lift(Query.mapToOptional(MAPPER)) .test() .assertError(IllegalStateException.class) .assertErrorMessage("Cursor returned more than 1 row"); } @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) @Test public void mapToOptionalIgnoresNullCursor() { Query nully = new Query() { @Nullable @Override public Cursor run() { return null; } }; Observable.just(nully) .lift(Query.mapToOptional(MAPPER)) .test() .assertValue(Optional.empty()); } } ================================================ FILE: sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/RecordingObserver.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.database.Cursor; import android.util.Log; import io.reactivex.observers.DisposableObserver; import java.util.concurrent.BlockingDeque; import java.util.concurrent.LinkedBlockingDeque; import static com.google.common.truth.Truth.assertThat; import static com.squareup.sqlbrite3.SqlBrite.Query; class RecordingObserver extends DisposableObserver { private static final Object COMPLETED = ""; private static final String TAG = RecordingObserver.class.getSimpleName(); final BlockingDeque events = new LinkedBlockingDeque<>(); @Override public final void onComplete() { Log.d(TAG, "onCompleted"); events.add(COMPLETED); } @Override public final void onError(Throwable e) { Log.d(TAG, "onError " + e.getClass().getSimpleName() + " " + e.getMessage()); events.add(e); } @Override public final void onNext(Query value) { Log.d(TAG, "onNext " + value); events.add(value.run()); } protected Object takeEvent() { Object item = events.removeFirst(); if (item == null) { throw new AssertionError("No items."); } return item; } public final CursorAssert assertCursor() { Object event = takeEvent(); assertThat(event).isInstanceOf(Cursor.class); return new CursorAssert((Cursor) event); } public final void assertErrorContains(String expected) { Object event = takeEvent(); assertThat(event).isInstanceOf(Throwable.class); assertThat(((Throwable) event).getMessage()).contains(expected); } public final void assertIsCompleted() { Object event = takeEvent(); assertThat(event).isEqualTo(COMPLETED); } public void assertNoMoreEvents() { assertThat(events).isEmpty(); } static final class CursorAssert { private final Cursor cursor; private int row = 0; CursorAssert(Cursor cursor) { this.cursor = cursor; } public CursorAssert hasRow(Object... values) { assertThat(cursor.moveToNext()).named("row " + (row + 1) + " exists").isTrue(); row += 1; assertThat(cursor.getColumnCount()).named("column count").isEqualTo(values.length); for (int i = 0; i < values.length; i++) { assertThat(cursor.getString(i)) .named("row " + row + " column '" + cursor.getColumnName(i) + "'") .isEqualTo(values[i]); } return this; } public void isExhausted() { if (cursor.moveToNext()) { StringBuilder data = new StringBuilder(); for (int i = 0; i < cursor.getColumnCount(); i++) { if (i > 0) data.append(", "); data.append(cursor.getString(i)); } throw new AssertionError("Expected no more rows but was: " + data); } cursor.close(); } } } ================================================ FILE: sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/SqlBriteTest.java ================================================ package com.squareup.sqlbrite3; import android.database.Cursor; import android.database.MatrixCursor; import android.support.annotation.Nullable; import android.support.test.runner.AndroidJUnit4; import com.squareup.sqlbrite3.SqlBrite.Query; import io.reactivex.functions.Function; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import org.junit.runner.RunWith; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; @RunWith(AndroidJUnit4.class) @SuppressWarnings("CheckResult") public final class SqlBriteTest { private static final String FIRST_NAME = "first_name"; private static final String LAST_NAME = "last_name"; private static final String[] COLUMN_NAMES = { FIRST_NAME, LAST_NAME }; @Test public void builderDisallowsNull() { SqlBrite.Builder builder = new SqlBrite.Builder(); try { builder.logger(null); fail(); } catch (NullPointerException e) { assertThat(e).hasMessage("logger == null"); } try { builder.queryTransformer(null); fail(); } catch (NullPointerException e) { assertThat(e).hasMessage("queryTransformer == null"); } } @Test public void asRowsEmpty() { MatrixCursor cursor = new MatrixCursor(COLUMN_NAMES); Query query = new CursorQuery(cursor); List names = query.asRows(Name.MAP).toList().blockingGet(); assertThat(names).isEmpty(); } @Test public void asRows() { MatrixCursor cursor = new MatrixCursor(COLUMN_NAMES); cursor.addRow(new Object[] { "Alice", "Allison" }); cursor.addRow(new Object[] { "Bob", "Bobberson" }); Query query = new CursorQuery(cursor); List names = query.asRows(Name.MAP).toList().blockingGet(); assertThat(names).containsExactly(new Name("Alice", "Allison"), new Name("Bob", "Bobberson")); } @Test public void asRowsStopsWhenUnsubscribed() { MatrixCursor cursor = new MatrixCursor(COLUMN_NAMES); cursor.addRow(new Object[] { "Alice", "Allison" }); cursor.addRow(new Object[] { "Bob", "Bobberson" }); Query query = new CursorQuery(cursor); final AtomicInteger count = new AtomicInteger(); query.asRows(new Function() { @Override public Name apply(Cursor cursor) throws Exception { count.incrementAndGet(); return Name.MAP.apply(cursor); } }).take(1).blockingFirst(); assertThat(count.get()).isEqualTo(1); } @Test public void asRowsEmptyWhenNullCursor() { Query nully = new Query() { @Nullable @Override public Cursor run() { return null; } }; final AtomicInteger count = new AtomicInteger(); nully.asRows(new Function() { @Override public Name apply(Cursor cursor) throws Exception { count.incrementAndGet(); return Name.MAP.apply(cursor); } }).test().assertNoValues().assertComplete(); assertThat(count.get()).isEqualTo(0); } static final class Name { static final Function MAP = new Function() { @Override public Name apply(Cursor cursor) { return new Name( // cursor.getString(cursor.getColumnIndexOrThrow(FIRST_NAME)), cursor.getString(cursor.getColumnIndexOrThrow(LAST_NAME))); } }; final String first; final String last; Name(String first, String last) { this.first = first; this.last = last; } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Name)) return false; Name other = (Name) o; return first.equals(other.first) && last.equals(other.last); } @Override public int hashCode() { return first.hashCode() * 17 + last.hashCode(); } @Override public String toString() { return "Name[" + first + ' ' + last + ']'; } } static final class CursorQuery extends Query { private final Cursor cursor; CursorQuery(Cursor cursor) { this.cursor = cursor; } @Override public Cursor run() { return cursor; } } } ================================================ FILE: sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/TestDb.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.db.SupportSQLiteOpenHelper; import android.content.ContentValues; import android.database.Cursor; import android.support.annotation.NonNull; import io.reactivex.functions.Function; import java.util.Arrays; import java.util.Collection; import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL; import static com.squareup.sqlbrite3.TestDb.EmployeeTable.ID; import static com.squareup.sqlbrite3.TestDb.EmployeeTable.NAME; import static com.squareup.sqlbrite3.TestDb.EmployeeTable.USERNAME; import static com.squareup.sqlbrite3.TestDb.ManagerTable.EMPLOYEE_ID; import static com.squareup.sqlbrite3.TestDb.ManagerTable.MANAGER_ID; final class TestDb extends SupportSQLiteOpenHelper.Callback { static final String TABLE_EMPLOYEE = "employee"; static final String TABLE_MANAGER = "manager"; static final String SELECT_EMPLOYEES = "SELECT " + USERNAME + ", " + NAME + " FROM " + TABLE_EMPLOYEE; static final String SELECT_MANAGER_LIST = "" + "SELECT e." + NAME + ", m." + NAME + " " + "FROM " + TABLE_MANAGER + " AS manager " + "JOIN " + TABLE_EMPLOYEE + " AS e " + "ON manager." + EMPLOYEE_ID + " = e." + ID + " " + "JOIN " + TABLE_EMPLOYEE + " as m " + "ON manager." + MANAGER_ID + " = m." + ID; static final Collection BOTH_TABLES = Arrays.asList(TABLE_EMPLOYEE, TABLE_MANAGER); interface EmployeeTable { String ID = "_id"; String USERNAME = "username"; String NAME = "name"; } static final class Employee { static final Function MAPPER = new Function() { @Override public Employee apply(Cursor cursor) { return new Employee( // cursor.getString(cursor.getColumnIndexOrThrow(EmployeeTable.USERNAME)), cursor.getString(cursor.getColumnIndexOrThrow(EmployeeTable.NAME))); } }; final String username; final String name; Employee(String username, String name) { this.username = username; this.name = name; } @Override public boolean equals(Object o) { if (o == this) return true; if (!(o instanceof Employee)) return false; Employee other = (Employee) o; return username.equals(other.username) && name.equals(other.name); } @Override public int hashCode() { return username.hashCode() * 17 + name.hashCode(); } @Override public String toString() { return "Employee[" + username + ' ' + name + ']'; } } interface ManagerTable { String ID = "_id"; String EMPLOYEE_ID = "employee_id"; String MANAGER_ID = "manager_id"; } private static final String CREATE_EMPLOYEE = "CREATE TABLE " + TABLE_EMPLOYEE + " (" + EmployeeTable.ID + " INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + EmployeeTable.USERNAME + " TEXT NOT NULL UNIQUE, " + EmployeeTable.NAME + " TEXT NOT NULL)"; private static final String CREATE_MANAGER = "CREATE TABLE " + TABLE_MANAGER + " (" + ManagerTable.ID + " INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " + ManagerTable.EMPLOYEE_ID + " INTEGER NOT NULL UNIQUE REFERENCES " + TABLE_EMPLOYEE + "(" + EmployeeTable.ID + "), " + ManagerTable.MANAGER_ID + " INTEGER NOT NULL REFERENCES " + TABLE_EMPLOYEE + "(" + EmployeeTable.ID + "))"; long aliceId; long bobId; long eveId; TestDb() { super(1); } @Override public void onCreate(@NonNull SupportSQLiteDatabase db) { db.execSQL("PRAGMA foreign_keys=ON"); db.execSQL(CREATE_EMPLOYEE); aliceId = db.insert(TABLE_EMPLOYEE, CONFLICT_FAIL, employee("alice", "Alice Allison")); bobId = db.insert(TABLE_EMPLOYEE, CONFLICT_FAIL, employee("bob", "Bob Bobberson")); eveId = db.insert(TABLE_EMPLOYEE, CONFLICT_FAIL, employee("eve", "Eve Evenson")); db.execSQL(CREATE_MANAGER); db.insert(TABLE_MANAGER, CONFLICT_FAIL, manager(eveId, aliceId)); } static ContentValues employee(String username, String name) { ContentValues values = new ContentValues(); values.put(EmployeeTable.USERNAME, username); values.put(EmployeeTable.NAME, name); return values; } static ContentValues manager(long employeeId, long managerId) { ContentValues values = new ContentValues(); values.put(ManagerTable.EMPLOYEE_ID, employeeId); values.put(ManagerTable.MANAGER_ID, managerId); return values; } @Override public void onUpgrade(@NonNull SupportSQLiteDatabase db, int oldVersion, int newVersion) { throw new AssertionError(); } } ================================================ FILE: sqlbrite/src/androidTest/java/com/squareup/sqlbrite3/TestScheduler.java ================================================ /* * Copyright (C) 2016 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import io.reactivex.Scheduler; import io.reactivex.annotations.NonNull; import io.reactivex.disposables.Disposable; import java.util.concurrent.TimeUnit; final class TestScheduler extends Scheduler { private final io.reactivex.schedulers.TestScheduler delegate = new io.reactivex.schedulers.TestScheduler(); private boolean runTasksImmediately = true; public void runTasksImmediately(boolean runTasksImmediately) { this.runTasksImmediately = runTasksImmediately; } public void triggerActions() { delegate.triggerActions(); } @Override public Worker createWorker() { return new TestWorker(); } class TestWorker extends Worker { private final Worker delegateWorker = delegate.createWorker(); @Override public Disposable schedule(@NonNull Runnable run, long delay, @NonNull TimeUnit unit) { Disposable disposable = delegateWorker.schedule(run, delay, unit); if (runTasksImmediately) { triggerActions(); } return disposable; } @Override public void dispose() { delegateWorker.dispose(); } @Override public boolean isDisposed() { return delegateWorker.isDisposed(); } } } ================================================ FILE: sqlbrite/src/main/AndroidManifest.xml ================================================ ================================================ FILE: sqlbrite/src/main/java/com/squareup/sqlbrite3/BriteContentResolver.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.content.ContentResolver; import android.database.ContentObserver; import android.database.Cursor; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.support.annotation.CheckResult; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.squareup.sqlbrite3.SqlBrite.Logger; import com.squareup.sqlbrite3.SqlBrite.Query; import io.reactivex.Observable; import io.reactivex.ObservableEmitter; import io.reactivex.ObservableOnSubscribe; import io.reactivex.ObservableTransformer; import io.reactivex.Scheduler; import io.reactivex.functions.Cancellable; import java.util.Arrays; import static com.squareup.sqlbrite3.QueryObservable.QUERY_OBSERVABLE; import static java.lang.System.nanoTime; import static java.util.concurrent.TimeUnit.NANOSECONDS; /** * A lightweight wrapper around {@link ContentResolver} which allows for continuously observing * the result of a query. Create using a {@link SqlBrite} instance. */ public final class BriteContentResolver { final Handler contentObserverHandler = new Handler(Looper.getMainLooper()); final ContentResolver contentResolver; private final Logger logger; private final Scheduler scheduler; private final ObservableTransformer queryTransformer; volatile boolean logging; BriteContentResolver(ContentResolver contentResolver, Logger logger, Scheduler scheduler, ObservableTransformer queryTransformer) { this.contentResolver = contentResolver; this.logger = logger; this.scheduler = scheduler; this.queryTransformer = queryTransformer; } /** Control whether debug logging is enabled. */ public void setLoggingEnabled(boolean enabled) { logging = enabled; } /** * Create an observable which will notify subscribers with a {@linkplain Query query} for * execution. Subscribers are responsible for always closing {@link Cursor} instance * returned from the {@link Query}. *

* Subscribers will receive an immediate notification for initial data as well as subsequent * notifications for when the supplied {@code uri}'s data changes. Unsubscribe when you no longer * want updates to a query. *

* Since content resolver triggers are inherently asynchronous, items emitted from the returned * observable use the {@link Scheduler} supplied to {@link SqlBrite#wrapContentProvider}. For * consistency, the immediate notification sent on subscribe also uses this scheduler. As such, * calling {@link Observable#subscribeOn subscribeOn} on the returned observable has no effect. *

* Note: To skip the immediate notification and only receive subsequent notifications when data * has changed call {@code skip(1)} on the returned observable. *

* Warning: this method does not perform the query! Only by subscribing to the returned * {@link Observable} will the operation occur. * * @see ContentResolver#query(Uri, String[], String, String[], String) * @see ContentResolver#registerContentObserver(Uri, boolean, ContentObserver) */ @CheckResult @NonNull public QueryObservable createQuery(@NonNull final Uri uri, @Nullable final String[] projection, @Nullable final String selection, @Nullable final String[] selectionArgs, @Nullable final String sortOrder, final boolean notifyForDescendents) { final Query query = new Query() { @Override public Cursor run() { long startNanos = nanoTime(); Cursor cursor = contentResolver.query(uri, projection, selection, selectionArgs, sortOrder); if (logging) { long tookMillis = NANOSECONDS.toMillis(nanoTime() - startNanos); log("QUERY (%sms)\n uri: %s\n projection: %s\n selection: %s\n selectionArgs: %s\n " + "sortOrder: %s\n notifyForDescendents: %s", tookMillis, uri, Arrays.toString(projection), selection, Arrays.toString(selectionArgs), sortOrder, notifyForDescendents); } return cursor; } }; Observable queries = Observable.create(new ObservableOnSubscribe() { @Override public void subscribe(final ObservableEmitter e) throws Exception { final ContentObserver observer = new ContentObserver(contentObserverHandler) { @Override public void onChange(boolean selfChange) { if (!e.isDisposed()) { e.onNext(query); } } }; contentResolver.registerContentObserver(uri, notifyForDescendents, observer); e.setCancellable(new Cancellable() { @Override public void cancel() throws Exception { contentResolver.unregisterContentObserver(observer); } }); if (!e.isDisposed()) { e.onNext(query); // Trigger initial query. } } }); return queries // .observeOn(scheduler) // .compose(queryTransformer) // Apply the user's query transformer. .to(QUERY_OBSERVABLE); } void log(String message, Object... args) { if (args.length > 0) message = String.format(message, args); logger.log(message); } } ================================================ FILE: sqlbrite/src/main/java/com/squareup/sqlbrite3/BriteDatabase.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.arch.persistence.db.SimpleSQLiteQuery; import android.arch.persistence.db.SupportSQLiteDatabase; import android.arch.persistence.db.SupportSQLiteOpenHelper; import android.arch.persistence.db.SupportSQLiteOpenHelper.Callback; import android.arch.persistence.db.SupportSQLiteQuery; import android.arch.persistence.db.SupportSQLiteStatement; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteTransactionListener; import android.support.annotation.CheckResult; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.WorkerThread; import com.squareup.sqlbrite3.SqlBrite.Logger; import com.squareup.sqlbrite3.SqlBrite.Query; import io.reactivex.Observable; import io.reactivex.ObservableTransformer; import io.reactivex.Scheduler; import io.reactivex.functions.Consumer; import io.reactivex.functions.Function; import io.reactivex.functions.Predicate; import io.reactivex.subjects.PublishSubject; import io.reactivex.subjects.Subject; import java.io.Closeable; import java.lang.annotation.Retention; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import static android.database.sqlite.SQLiteDatabase.CONFLICT_ABORT; import static android.database.sqlite.SQLiteDatabase.CONFLICT_FAIL; import static android.database.sqlite.SQLiteDatabase.CONFLICT_IGNORE; import static android.database.sqlite.SQLiteDatabase.CONFLICT_NONE; import static android.database.sqlite.SQLiteDatabase.CONFLICT_REPLACE; import static android.database.sqlite.SQLiteDatabase.CONFLICT_ROLLBACK; import static com.squareup.sqlbrite3.QueryObservable.QUERY_OBSERVABLE; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.util.Collections.singletonList; /** * A lightweight wrapper around {@link SupportSQLiteOpenHelper} which allows for continuously * observing the result of a query. Create using a {@link SqlBrite} instance. */ public final class BriteDatabase implements Closeable { private final SupportSQLiteOpenHelper helper; private final Logger logger; private final ObservableTransformer queryTransformer; // Package-private to avoid synthetic accessor method for 'transaction' instance. final ThreadLocal transactions = new ThreadLocal<>(); private final Subject> triggers = PublishSubject.create(); private final Transaction transaction = new Transaction() { @Override public void markSuccessful() { if (logging) log("TXN SUCCESS %s", transactions.get()); getWritableDatabase().setTransactionSuccessful(); } @Override public boolean yieldIfContendedSafely() { return getWritableDatabase().yieldIfContendedSafely(); } @Override public boolean yieldIfContendedSafely(long sleepAmount, TimeUnit sleepUnit) { return getWritableDatabase().yieldIfContendedSafely(sleepUnit.toMillis(sleepAmount)); } @Override public void end() { SqliteTransaction transaction = transactions.get(); if (transaction == null) { throw new IllegalStateException("Not in transaction."); } SqliteTransaction newTransaction = transaction.parent; transactions.set(newTransaction); if (logging) log("TXN END %s", transaction); getWritableDatabase().endTransaction(); // Send the triggers after ending the transaction in the DB. if (transaction.commit) { sendTableTrigger(transaction); } } @Override public void close() { end(); } }; private final Consumer ensureNotInTransaction = new Consumer() { @Override public void accept(Object ignored) throws Exception { if (transactions.get() != null) { throw new IllegalStateException("Cannot subscribe to observable query in a transaction."); } } }; private final Scheduler scheduler; // Package-private to avoid synthetic accessor method for 'transaction' instance. volatile boolean logging; BriteDatabase(SupportSQLiteOpenHelper helper, Logger logger, Scheduler scheduler, ObservableTransformer queryTransformer) { this.helper = helper; this.logger = logger; this.scheduler = scheduler; this.queryTransformer = queryTransformer; } /** * Control whether debug logging is enabled. */ public void setLoggingEnabled(boolean enabled) { logging = enabled; } /** * Create and/or open a database. This will be the same object returned by * {@link SupportSQLiteOpenHelper#getWritableDatabase} unless some problem, such as a full disk, * requires the database to be opened read-only. In that case, a read-only * database object will be returned. If the problem is fixed, a future call * to {@link SupportSQLiteOpenHelper#getWritableDatabase} may succeed, in which case the read-only * database object will be closed and the read/write object will be returned * in the future. * *

Like {@link SupportSQLiteOpenHelper#getWritableDatabase}, this method may * take a long time to return, so you should not call it from the * application main thread, including from * {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}. * * @throws android.database.sqlite.SQLiteException if the database cannot be opened * @return a database object valid until {@link SupportSQLiteOpenHelper#getWritableDatabase} * or {@link #close} is called. */ @NonNull @CheckResult @WorkerThread public SupportSQLiteDatabase getReadableDatabase() { return helper.getReadableDatabase(); } /** * Create and/or open a database that will be used for reading and writing. * The first time this is called, the database will be opened and * {@link Callback#onCreate}, {@link Callback#onUpgrade} and/or {@link Callback#onOpen} will be * called. * *

Once opened successfully, the database is cached, so you can * call this method every time you need to write to the database. * (Make sure to call {@link #close} when you no longer need the database.) * Errors such as bad permissions or a full disk may cause this method * to fail, but future attempts may succeed if the problem is fixed.

* *

Database upgrade may take a long time, you * should not call this method from the application main thread, including * from {@link android.content.ContentProvider#onCreate ContentProvider.onCreate()}. * * @throws android.database.sqlite.SQLiteException if the database cannot be opened for writing * @return a read/write database object valid until {@link #close} is called */ @NonNull @CheckResult @WorkerThread public SupportSQLiteDatabase getWritableDatabase() { return helper.getWritableDatabase(); } void sendTableTrigger(Set tables) { SqliteTransaction transaction = transactions.get(); if (transaction != null) { transaction.addAll(tables); } else { if (logging) log("TRIGGER %s", tables); triggers.onNext(tables); } } /** * Begin a transaction for this thread. *

* Transactions may nest. If the transaction is not in progress, then a database connection is * obtained and a new transaction is started. Otherwise, a nested transaction is started. *

* Each call to {@code newTransaction} must be matched exactly by a call to * {@link Transaction#end()}. To mark a transaction as successful, call * {@link Transaction#markSuccessful()} before calling {@link Transaction#end()}. If the * transaction is not successful, or if any of its nested transactions were not successful, then * the entire transaction will be rolled back when the outermost transaction is ended. *

* Transactions queue up all query notifications until they have been applied. *

* Here is the standard idiom for transactions: * *

{@code
   * try (Transaction transaction = db.newTransaction()) {
   *   ...
   *   transaction.markSuccessful();
   * }
   * }
* * Manually call {@link Transaction#end()} when try-with-resources is not available: *
{@code
   * Transaction transaction = db.newTransaction();
   * try {
   *   ...
   *   transaction.markSuccessful();
   * } finally {
   *   transaction.end();
   * }
   * }
* * * @see SupportSQLiteDatabase#beginTransaction() */ @CheckResult @NonNull public Transaction newTransaction() { SqliteTransaction transaction = new SqliteTransaction(transactions.get()); transactions.set(transaction); if (logging) log("TXN BEGIN %s", transaction); getWritableDatabase().beginTransactionWithListener(transaction); return this.transaction; } /** * Begins a transaction in IMMEDIATE mode for this thread. *

* Transactions may nest. If the transaction is not in progress, then a database connection is * obtained and a new transaction is started. Otherwise, a nested transaction is started. *

* Each call to {@code newNonExclusiveTransaction} must be matched exactly by a call to * {@link Transaction#end()}. To mark a transaction as successful, call * {@link Transaction#markSuccessful()} before calling {@link Transaction#end()}. If the * transaction is not successful, or if any of its nested transactions were not successful, then * the entire transaction will be rolled back when the outermost transaction is ended. *

* Transactions queue up all query notifications until they have been applied. *

* Here is the standard idiom for transactions: * *

{@code
   * try (Transaction transaction = db.newNonExclusiveTransaction()) {
   *   ...
   *   transaction.markSuccessful();
   * }
   * }
* * Manually call {@link Transaction#end()} when try-with-resources is not available: *
{@code
   * Transaction transaction = db.newNonExclusiveTransaction();
   * try {
   *   ...
   *   transaction.markSuccessful();
   * } finally {
   *   transaction.end();
   * }
   * }
* * * @see SupportSQLiteDatabase#beginTransactionNonExclusive() */ @CheckResult @NonNull public Transaction newNonExclusiveTransaction() { SqliteTransaction transaction = new SqliteTransaction(transactions.get()); transactions.set(transaction); if (logging) log("TXN BEGIN %s", transaction); getWritableDatabase().beginTransactionWithListenerNonExclusive(transaction); return this.transaction; } /** * Close the underlying {@link SupportSQLiteOpenHelper} and remove cached readable and writeable * databases. This does not prevent existing observables from retaining existing references as * well as attempting to create new ones for new subscriptions. */ @Override public void close() { helper.close(); } /** * Create an observable which will notify subscribers with a {@linkplain Query query} for * execution. Subscribers are responsible for always closing {@link Cursor} instance * returned from the {@link Query}. *

* Subscribers will receive an immediate notification for initial data as well as subsequent * notifications for when the supplied {@code table}'s data changes through the {@code insert}, * {@code update}, and {@code delete} methods of this class. Unsubscribe when you no longer want * updates to a query. *

* Since database triggers are inherently asynchronous, items emitted from the returned * observable use the {@link Scheduler} supplied to {@link SqlBrite#wrapDatabaseHelper}. For * consistency, the immediate notification sent on subscribe also uses this scheduler. As such, * calling {@link Observable#subscribeOn subscribeOn} on the returned observable has no effect. *

* Note: To skip the immediate notification and only receive subsequent notifications when data * has changed call {@code skip(1)} on the returned observable. *

* Warning: this method does not perform the query! Only by subscribing to the returned * {@link Observable} will the operation occur. * * @see SupportSQLiteDatabase#query(String, Object[]) */ @CheckResult @NonNull public QueryObservable createQuery(@NonNull final String table, @NonNull String sql, @NonNull Object... args) { return createQuery(new DatabaseQuery(singletonList(table), new SimpleSQLiteQuery(sql, args))); } /** * See {@link #createQuery(String, String, Object...)} for usage. This overload allows for * monitoring multiple tables for changes. * * @see SupportSQLiteDatabase#query(String, Object[]) */ @CheckResult @NonNull public QueryObservable createQuery(@NonNull final Iterable tables, @NonNull String sql, @NonNull Object... args) { return createQuery(new DatabaseQuery(tables, new SimpleSQLiteQuery(sql, args))); } /** * Create an observable which will notify subscribers with a {@linkplain Query query} for * execution. Subscribers are responsible for always closing {@link Cursor} instance * returned from the {@link Query}. *

* Subscribers will receive an immediate notification for initial data as well as subsequent * notifications for when the supplied {@code table}'s data changes through the {@code insert}, * {@code update}, and {@code delete} methods of this class. Unsubscribe when you no longer want * updates to a query. *

* Since database triggers are inherently asynchronous, items emitted from the returned * observable use the {@link Scheduler} supplied to {@link SqlBrite#wrapDatabaseHelper}. For * consistency, the immediate notification sent on subscribe also uses this scheduler. As such, * calling {@link Observable#subscribeOn subscribeOn} on the returned observable has no effect. *

* Note: To skip the immediate notification and only receive subsequent notifications when data * has changed call {@code skip(1)} on the returned observable. *

* Warning: this method does not perform the query! Only by subscribing to the returned * {@link Observable} will the operation occur. * * @see SupportSQLiteDatabase#query(SupportSQLiteQuery) */ @CheckResult @NonNull public QueryObservable createQuery(@NonNull final String table, @NonNull SupportSQLiteQuery query) { return createQuery(new DatabaseQuery(singletonList(table), query)); } /** * See {@link #createQuery(String, SupportSQLiteQuery)} for usage. This overload allows for * monitoring multiple tables for changes. * * @see SupportSQLiteDatabase#query(SupportSQLiteQuery) */ @CheckResult @NonNull public QueryObservable createQuery(@NonNull final Iterable tables, @NonNull SupportSQLiteQuery query) { return createQuery(new DatabaseQuery(tables, query)); } @CheckResult @NonNull private QueryObservable createQuery(DatabaseQuery query) { if (transactions.get() != null) { throw new IllegalStateException("Cannot create observable query in transaction. " + "Use query() for a query inside a transaction."); } return triggers // .filter(query) // DatabaseQuery filters triggers to on tables we care about. .map(query) // DatabaseQuery maps to itself to save an allocation. .startWith(query) // .observeOn(scheduler) // .compose(queryTransformer) // Apply the user's query transformer. .doOnSubscribe(ensureNotInTransaction) .to(QUERY_OBSERVABLE); } /** * Runs the provided SQL and returns a {@link Cursor} over the result set. * * @see SupportSQLiteDatabase#query(String, Object[]) */ @CheckResult @WorkerThread public Cursor query(@NonNull String sql, @NonNull Object... args) { Cursor cursor = getReadableDatabase().query(sql, args); if (logging) { log("QUERY\n sql: %s\n args: %s", indentSql(sql), Arrays.toString(args)); } return cursor; } /** * Runs the provided {@link SupportSQLiteQuery} and returns a {@link Cursor} over the result set. * * @see SupportSQLiteDatabase#query(SupportSQLiteQuery) */ @CheckResult @WorkerThread public Cursor query(@NonNull SupportSQLiteQuery query) { Cursor cursor = getReadableDatabase().query(query); if (logging) { log("QUERY\n sql: %s", indentSql(query.getSql())); } return cursor; } /** * Insert a row into the specified {@code table} and notify any subscribed queries. * * @see SupportSQLiteDatabase#insert(String, int, ContentValues) */ @WorkerThread public long insert(@NonNull String table, @ConflictAlgorithm int conflictAlgorithm, @NonNull ContentValues values) { SupportSQLiteDatabase db = getWritableDatabase(); if (logging) { log("INSERT\n table: %s\n values: %s\n conflictAlgorithm: %s", table, values, conflictString(conflictAlgorithm)); } long rowId = db.insert(table, conflictAlgorithm, values); if (logging) log("INSERT id: %s", rowId); if (rowId != -1) { // Only send a table trigger if the insert was successful. sendTableTrigger(Collections.singleton(table)); } return rowId; } /** * Delete rows from the specified {@code table} and notify any subscribed queries. This method * will not trigger a notification if no rows were deleted. * * @see SupportSQLiteDatabase#delete(String, String, Object[]) */ @WorkerThread public int delete(@NonNull String table, @Nullable String whereClause, @Nullable String... whereArgs) { SupportSQLiteDatabase db = getWritableDatabase(); if (logging) { log("DELETE\n table: %s\n whereClause: %s\n whereArgs: %s", table, whereClause, Arrays.toString(whereArgs)); } int rows = db.delete(table, whereClause, whereArgs); if (logging) log("DELETE affected %s %s", rows, rows != 1 ? "rows" : "row"); if (rows > 0) { // Only send a table trigger if rows were affected. sendTableTrigger(Collections.singleton(table)); } return rows; } /** * Update rows in the specified {@code table} and notify any subscribed queries. This method * will not trigger a notification if no rows were updated. * * @see SupportSQLiteDatabase#update(String, int, ContentValues, String, Object[]) */ @WorkerThread public int update(@NonNull String table, @ConflictAlgorithm int conflictAlgorithm, @NonNull ContentValues values, @Nullable String whereClause, @Nullable String... whereArgs) { SupportSQLiteDatabase db = getWritableDatabase(); if (logging) { log("UPDATE\n table: %s\n values: %s\n whereClause: %s\n whereArgs: %s\n conflictAlgorithm: %s", table, values, whereClause, Arrays.toString(whereArgs), conflictString(conflictAlgorithm)); } int rows = db.update(table, conflictAlgorithm, values, whereClause, whereArgs); if (logging) log("UPDATE affected %s %s", rows, rows != 1 ? "rows" : "row"); if (rows > 0) { // Only send a table trigger if rows were affected. sendTableTrigger(Collections.singleton(table)); } return rows; } /** * Execute {@code sql} provided it is NOT a {@code SELECT} or any other SQL statement that * returns data. No data can be returned (such as the number of affected rows). Instead, use * {@link #insert}, {@link #update}, et al, when possible. *

* No notifications will be sent to queries if {@code sql} affects the data of a table. * * @see SupportSQLiteDatabase#execSQL(String) */ @WorkerThread public void execute(String sql) { if (logging) log("EXECUTE\n sql: %s", indentSql(sql)); getWritableDatabase().execSQL(sql); } /** * Execute {@code sql} provided it is NOT a {@code SELECT} or any other SQL statement that * returns data. No data can be returned (such as the number of affected rows). Instead, use * {@link #insert}, {@link #update}, et al, when possible. *

* No notifications will be sent to queries if {@code sql} affects the data of a table. * * @see SupportSQLiteDatabase#execSQL(String, Object[]) */ @WorkerThread public void execute(String sql, Object... args) { if (logging) log("EXECUTE\n sql: %s\n args: %s", indentSql(sql), Arrays.toString(args)); getWritableDatabase().execSQL(sql, args); } /** * Execute {@code sql} provided it is NOT a {@code SELECT} or any other SQL statement that * returns data. No data can be returned (such as the number of affected rows). Instead, use * {@link #insert}, {@link #update}, et al, when possible. *

* A notification to queries for {@code table} will be sent after the statement is executed. * * @see SupportSQLiteDatabase#execSQL(String) */ @WorkerThread public void executeAndTrigger(String table, String sql) { executeAndTrigger(Collections.singleton(table), sql); } /** * See {@link #executeAndTrigger(String, String)} for usage. This overload allows for triggering multiple tables. * * @see BriteDatabase#executeAndTrigger(String, String) */ @WorkerThread public void executeAndTrigger(Set tables, String sql) { execute(sql); sendTableTrigger(tables); } /** * Execute {@code sql} provided it is NOT a {@code SELECT} or any other SQL statement that * returns data. No data can be returned (such as the number of affected rows). Instead, use * {@link #insert}, {@link #update}, et al, when possible. *

* A notification to queries for {@code table} will be sent after the statement is executed. * * @see SupportSQLiteDatabase#execSQL(String, Object[]) */ @WorkerThread public void executeAndTrigger(String table, String sql, Object... args) { executeAndTrigger(Collections.singleton(table), sql, args); } /** * See {@link #executeAndTrigger(String, String, Object...)} for usage. This overload allows for triggering multiple tables. * * @see BriteDatabase#executeAndTrigger(String, String, Object...) */ @WorkerThread public void executeAndTrigger(Set tables, String sql, Object... args) { execute(sql, args); sendTableTrigger(tables); } /** * Execute {@code statement}, if the the number of rows affected by execution of this SQL * statement is of any importance to the caller - for example, UPDATE / DELETE SQL statements. * * @return the number of rows affected by this SQL statement execution. * @throws android.database.SQLException If the SQL string is invalid * * @see SupportSQLiteStatement#executeUpdateDelete() */ @WorkerThread public int executeUpdateDelete(String table, SupportSQLiteStatement statement) { return executeUpdateDelete(Collections.singleton(table), statement); } /** * See {@link #executeUpdateDelete(String, SupportSQLiteStatement)} for usage. This overload * allows for triggering multiple tables. * * @see BriteDatabase#executeUpdateDelete(String, SupportSQLiteStatement) */ @WorkerThread public int executeUpdateDelete(Set tables, SupportSQLiteStatement statement) { if (logging) log("EXECUTE\n %s", statement); int rows = statement.executeUpdateDelete(); if (rows > 0) { // Only send a table trigger if rows were affected. sendTableTrigger(tables); } return rows; } /** * Execute {@code statement} and return the ID of the row inserted due to this call. * The SQL statement should be an INSERT for this to be a useful call. * * @return the row ID of the last row inserted, if this insert is successful. -1 otherwise. * * @throws android.database.SQLException If the SQL string is invalid * * @see SupportSQLiteStatement#executeInsert() */ @WorkerThread public long executeInsert(String table, SupportSQLiteStatement statement) { return executeInsert(Collections.singleton(table), statement); } /** * See {@link #executeInsert(String, SupportSQLiteStatement)} for usage. This overload allows for * triggering multiple tables. * * @see BriteDatabase#executeInsert(String, SupportSQLiteStatement) */ @WorkerThread public long executeInsert(Set tables, SupportSQLiteStatement statement) { if (logging) log("EXECUTE\n %s", statement); long rowId = statement.executeInsert(); if (rowId != -1) { // Only send a table trigger if the insert was successful. sendTableTrigger(tables); } return rowId; } /** An in-progress database transaction. */ public interface Transaction extends Closeable { /** * End a transaction. See {@link #newTransaction()} for notes about how to use this and when * transactions are committed and rolled back. * * @see SupportSQLiteDatabase#endTransaction() */ @WorkerThread void end(); /** * Marks the current transaction as successful. Do not do any more database work between * calling this and calling {@link #end()}. Do as little non-database work as possible in that * situation too. If any errors are encountered between this and {@link #end()} the transaction * will still be committed. * * @see SupportSQLiteDatabase#setTransactionSuccessful() */ @WorkerThread void markSuccessful(); /** * Temporarily end the transaction to let other threads run. The transaction is assumed to be * successful so far. Do not call {@link #markSuccessful()} before calling this. When this * returns a new transaction will have been created but not marked as successful. This assumes * that there are no nested transactions (newTransaction has only been called once) and will * throw an exception if that is not the case. * * @return true if the transaction was yielded * * @see SupportSQLiteDatabase#yieldIfContendedSafely() */ @WorkerThread boolean yieldIfContendedSafely(); /** * Temporarily end the transaction to let other threads run. The transaction is assumed to be * successful so far. Do not call {@link #markSuccessful()} before calling this. When this * returns a new transaction will have been created but not marked as successful. This assumes * that there are no nested transactions (newTransaction has only been called once) and will * throw an exception if that is not the case. * * @param sleepAmount if > 0, sleep this long before starting a new transaction if * the lock was actually yielded. This will allow other background threads to make some * more progress than they would if we started the transaction immediately. * @return true if the transaction was yielded * * @see SupportSQLiteDatabase#yieldIfContendedSafely(long) */ @WorkerThread boolean yieldIfContendedSafely(long sleepAmount, TimeUnit sleepUnit); /** * Equivalent to calling {@link #end()} */ @WorkerThread @Override void close(); } @IntDef({ CONFLICT_ABORT, CONFLICT_FAIL, CONFLICT_IGNORE, CONFLICT_NONE, CONFLICT_REPLACE, CONFLICT_ROLLBACK }) @Retention(SOURCE) private @interface ConflictAlgorithm { } static String indentSql(String sql) { return sql.replace("\n", "\n "); } void log(String message, Object... args) { if (args.length > 0) message = String.format(message, args); logger.log(message); } private static String conflictString(@ConflictAlgorithm int conflictAlgorithm) { switch (conflictAlgorithm) { case CONFLICT_ABORT: return "abort"; case CONFLICT_FAIL: return "fail"; case CONFLICT_IGNORE: return "ignore"; case CONFLICT_NONE: return "none"; case CONFLICT_REPLACE: return "replace"; case CONFLICT_ROLLBACK: return "rollback"; default: return "unknown (" + conflictAlgorithm + ')'; } } static final class SqliteTransaction extends LinkedHashSet implements SQLiteTransactionListener { final SqliteTransaction parent; boolean commit; SqliteTransaction(SqliteTransaction parent) { this.parent = parent; } @Override public void onBegin() { } @Override public void onCommit() { commit = true; } @Override public void onRollback() { } @Override public String toString() { String name = String.format("%08x", System.identityHashCode(this)); return parent == null ? name : name + " [" + parent.toString() + ']'; } } final class DatabaseQuery extends Query implements Function, Query>, Predicate> { private final Iterable tables; private final SupportSQLiteQuery query; DatabaseQuery(Iterable tables, SupportSQLiteQuery query) { this.tables = tables; this.query = query; } @Override public Cursor run() { if (transactions.get() != null) { throw new IllegalStateException("Cannot execute observable query in a transaction."); } Cursor cursor = getReadableDatabase().query(query); if (logging) { log("QUERY\n tables: %s\n sql: %s", tables, indentSql(query.getSql())); } return cursor; } @Override public String toString() { return query.getSql(); } @Override public Query apply(Set ignored) { return this; } @Override public boolean test(Set strings) { for (String table : tables) { if (strings.contains(table)) { return true; } } return false; } } } ================================================ FILE: sqlbrite/src/main/java/com/squareup/sqlbrite3/QueryObservable.java ================================================ package com.squareup.sqlbrite3; import android.database.Cursor; import android.os.Build; import android.support.annotation.CheckResult; import android.support.annotation.NonNull; import android.support.annotation.RequiresApi; import com.squareup.sqlbrite3.SqlBrite.Query; import io.reactivex.Observable; import io.reactivex.Observer; import io.reactivex.functions.Function; import java.util.List; import java.util.Optional; /** An {@link Observable} of {@link Query} which offers query-specific convenience operators. */ public final class QueryObservable extends Observable { static final Function, QueryObservable> QUERY_OBSERVABLE = new Function, QueryObservable>() { @Override public QueryObservable apply(Observable queryObservable) { return new QueryObservable(queryObservable); } }; private final Observable upstream; public QueryObservable(Observable upstream) { this.upstream = upstream; } @Override protected void subscribeActual(Observer observer) { upstream.subscribe(observer); } /** * Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each * emitted {@link Query} which returns a single row to {@code T}. *

* It is an error for a query to pass through this operator with more than 1 row in its result * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows * do not emit an item. *

* This method is equivalent to: *

{@code
   * flatMap(q -> q.asRows(mapper).take(1))
   * }
* and a convenience operator for: *
{@code
   * lift(Query.mapToOne(mapper))
   * }
* * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. */ @CheckResult @NonNull public final Observable mapToOne(@NonNull Function mapper) { return lift(Query.mapToOne(mapper)); } /** * Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each * emitted {@link Query} which returns a single row to {@code T}. *

* It is an error for a query to pass through this operator with more than 1 row in its result * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows * emit {@code defaultValue}. *

* This method is equivalent to: *

{@code
   * flatMap(q -> q.asRows(mapper).take(1).defaultIfEmpty(defaultValue))
   * }
* and a convenience operator for: *
{@code
   * lift(Query.mapToOneOrDefault(mapper, defaultValue))
   * }
* * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. * @param defaultValue Value returned if result set is empty */ @CheckResult @NonNull public final Observable mapToOneOrDefault(@NonNull Function mapper, @NonNull T defaultValue) { return lift(Query.mapToOneOrDefault(mapper, defaultValue)); } /** * Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each * emitted {@link Query} which returns a single row to {@code Optional}. *

* It is an error for a query to pass through this operator with more than 1 row in its result * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows * emit {@link Optional#empty() Optional.empty()} *

* This method is equivalent to: *

{@code
   * flatMap(q -> q.asRows(mapper).take(1).map(Optional::of).defaultIfEmpty(Optional.empty())
   * }
* and a convenience operator for: *
{@code
   * lift(Query.mapToOptional(mapper))
   * }
* * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. */ @RequiresApi(Build.VERSION_CODES.N) @CheckResult @NonNull public final Observable> mapToOptional(@NonNull Function mapper) { return lift(Query.mapToOptional(mapper)); } /** * Given a function mapping the current row of a {@link Cursor} to {@code T}, transform each * emitted {@link Query} to a {@code List}. *

* Be careful using this operator as it will always consume the entire cursor and create objects * for each row, every time this observable emits a new query. On tables whose queries update * frequently or very large result sets this can result in the creation of many objects. *

* This method is equivalent to: *

{@code
   * flatMap(q -> q.asRows(mapper).toList())
   * }
* and a convenience operator for: *
{@code
   * lift(Query.mapToList(mapper))
   * }
*

* Consider using {@link Query#asRows} if you need to limit or filter in memory. * * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. */ @CheckResult @NonNull public final Observable> mapToList(@NonNull Function mapper) { return lift(Query.mapToList(mapper)); } } ================================================ FILE: sqlbrite/src/main/java/com/squareup/sqlbrite3/QueryToListOperator.java ================================================ /* * Copyright (C) 2017 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.database.Cursor; import io.reactivex.ObservableOperator; import io.reactivex.Observer; import io.reactivex.exceptions.Exceptions; import io.reactivex.functions.Function; import io.reactivex.observers.DisposableObserver; import io.reactivex.plugins.RxJavaPlugins; import java.util.ArrayList; import java.util.List; final class QueryToListOperator implements ObservableOperator, SqlBrite.Query> { private final Function mapper; QueryToListOperator(Function mapper) { this.mapper = mapper; } @Override public Observer apply(Observer> observer) { return new MappingObserver<>(observer, mapper); } static final class MappingObserver extends DisposableObserver { private final Observer> downstream; private final Function mapper; MappingObserver(Observer> downstream, Function mapper) { this.downstream = downstream; this.mapper = mapper; } @Override protected void onStart() { downstream.onSubscribe(this); } @Override public void onNext(SqlBrite.Query query) { try { Cursor cursor = query.run(); if (cursor == null || isDisposed()) { return; } List items = new ArrayList<>(cursor.getCount()); try { while (cursor.moveToNext()) { items.add(mapper.apply(cursor)); } } finally { cursor.close(); } if (!isDisposed()) { downstream.onNext(items); } } catch (Throwable e) { Exceptions.throwIfFatal(e); onError(e); } } @Override public void onComplete() { if (!isDisposed()) { downstream.onComplete(); } } @Override public void onError(Throwable e) { if (isDisposed()) { RxJavaPlugins.onError(e); } else { downstream.onError(e); } } } } ================================================ FILE: sqlbrite/src/main/java/com/squareup/sqlbrite3/QueryToOneOperator.java ================================================ /* * Copyright (C) 2017 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.database.Cursor; import android.support.annotation.Nullable; import io.reactivex.ObservableOperator; import io.reactivex.Observer; import io.reactivex.exceptions.Exceptions; import io.reactivex.functions.Function; import io.reactivex.observers.DisposableObserver; import io.reactivex.plugins.RxJavaPlugins; final class QueryToOneOperator implements ObservableOperator { private final Function mapper; private final T defaultValue; /** A null {@code defaultValue} means nothing will be emitted when empty. */ QueryToOneOperator(Function mapper, @Nullable T defaultValue) { this.mapper = mapper; this.defaultValue = defaultValue; } @Override public Observer apply(Observer observer) { return new MappingObserver<>(observer, mapper, defaultValue); } static final class MappingObserver extends DisposableObserver { private final Observer downstream; private final Function mapper; private final T defaultValue; MappingObserver(Observer downstream, Function mapper, T defaultValue) { this.downstream = downstream; this.mapper = mapper; this.defaultValue = defaultValue; } @Override protected void onStart() { downstream.onSubscribe(this); } @Override public void onNext(SqlBrite.Query query) { try { T item = null; Cursor cursor = query.run(); if (cursor != null) { try { if (cursor.moveToNext()) { item = mapper.apply(cursor); if (item == null) { downstream.onError(new NullPointerException("QueryToOne mapper returned null")); return; } if (cursor.moveToNext()) { throw new IllegalStateException("Cursor returned more than 1 row"); } } } finally { cursor.close(); } } if (!isDisposed()) { if (item != null) { downstream.onNext(item); } else if (defaultValue != null) { downstream.onNext(defaultValue); } } } catch (Throwable e) { Exceptions.throwIfFatal(e); onError(e); } } @Override public void onComplete() { if (!isDisposed()) { downstream.onComplete(); } } @Override public void onError(Throwable e) { if (isDisposed()) { RxJavaPlugins.onError(e); } else { downstream.onError(e); } } } } ================================================ FILE: sqlbrite/src/main/java/com/squareup/sqlbrite3/QueryToOptionalOperator.java ================================================ /* * Copyright (C) 2017 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.database.Cursor; import android.os.Build; import android.support.annotation.RequiresApi; import io.reactivex.ObservableOperator; import io.reactivex.Observer; import io.reactivex.exceptions.Exceptions; import io.reactivex.functions.Function; import io.reactivex.observers.DisposableObserver; import io.reactivex.plugins.RxJavaPlugins; import java.util.Optional; @RequiresApi(Build.VERSION_CODES.N) final class QueryToOptionalOperator implements ObservableOperator, SqlBrite.Query> { private final Function mapper; QueryToOptionalOperator(Function mapper) { this.mapper = mapper; } @Override public Observer apply(Observer> observer) { return new MappingObserver<>(observer, mapper); } static final class MappingObserver extends DisposableObserver { private final Observer> downstream; private final Function mapper; MappingObserver(Observer> downstream, Function mapper) { this.downstream = downstream; this.mapper = mapper; } @Override protected void onStart() { downstream.onSubscribe(this); } @Override public void onNext(SqlBrite.Query query) { try { T item = null; Cursor cursor = query.run(); if (cursor != null) { try { if (cursor.moveToNext()) { item = mapper.apply(cursor); if (item == null) { downstream.onError(new NullPointerException("QueryToOne mapper returned null")); return; } if (cursor.moveToNext()) { throw new IllegalStateException("Cursor returned more than 1 row"); } } } finally { cursor.close(); } } if (!isDisposed()) { downstream.onNext(Optional.ofNullable(item)); } } catch (Throwable e) { Exceptions.throwIfFatal(e); onError(e); } } @Override public void onComplete() { if (!isDisposed()) { downstream.onComplete(); } } @Override public void onError(Throwable e) { if (isDisposed()) { RxJavaPlugins.onError(e); } else { downstream.onError(e); } } } } ================================================ FILE: sqlbrite/src/main/java/com/squareup/sqlbrite3/SqlBrite.java ================================================ /* * Copyright (C) 2015 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3; import android.arch.persistence.db.SupportSQLiteOpenHelper; import android.content.ContentResolver; import android.database.Cursor; import android.os.Build; import android.support.annotation.CheckResult; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.annotation.WorkerThread; import android.util.Log; import io.reactivex.Observable; import io.reactivex.ObservableEmitter; import io.reactivex.ObservableOnSubscribe; import io.reactivex.ObservableOperator; import io.reactivex.ObservableTransformer; import io.reactivex.Scheduler; import io.reactivex.functions.Function; import java.util.List; import java.util.Optional; /** * A lightweight wrapper around {@link SupportSQLiteOpenHelper} which allows for continuously * observing the result of a query. */ public final class SqlBrite { static final Logger DEFAULT_LOGGER = new Logger() { @Override public void log(String message) { Log.d("SqlBrite", message); } }; static final ObservableTransformer DEFAULT_TRANSFORMER = new ObservableTransformer() { @Override public Observable apply(Observable queryObservable) { return queryObservable; } }; public static final class Builder { private Logger logger = DEFAULT_LOGGER; private ObservableTransformer queryTransformer = DEFAULT_TRANSFORMER; @CheckResult public Builder logger(@NonNull Logger logger) { if (logger == null) throw new NullPointerException("logger == null"); this.logger = logger; return this; } @CheckResult public Builder queryTransformer(@NonNull ObservableTransformer queryTransformer) { if (queryTransformer == null) throw new NullPointerException("queryTransformer == null"); this.queryTransformer = queryTransformer; return this; } @CheckResult public SqlBrite build() { return new SqlBrite(logger, queryTransformer); } } final Logger logger; final ObservableTransformer queryTransformer; SqlBrite(@NonNull Logger logger, @NonNull ObservableTransformer queryTransformer) { this.logger = logger; this.queryTransformer = queryTransformer; } /** * Wrap a {@link SupportSQLiteOpenHelper} for observable queries. *

* While not strictly required, instances of this class assume that they will be the only ones * interacting with the underlying {@link SupportSQLiteOpenHelper} and it is required for * automatic notifications of table changes to work. See {@linkplain BriteDatabase#createQuery the * query method} for more information on that behavior. * * @param scheduler The {@link Scheduler} on which items from {@link BriteDatabase#createQuery} * will be emitted. */ @CheckResult @NonNull public BriteDatabase wrapDatabaseHelper( @NonNull SupportSQLiteOpenHelper helper, @NonNull Scheduler scheduler) { return new BriteDatabase(helper, logger, scheduler, queryTransformer); } /** * Wrap a {@link ContentResolver} for observable queries. * * @param scheduler The {@link Scheduler} on which items from * {@link BriteContentResolver#createQuery} will be emitted. */ @CheckResult @NonNull public BriteContentResolver wrapContentProvider( @NonNull ContentResolver contentResolver, @NonNull Scheduler scheduler) { return new BriteContentResolver(contentResolver, logger, scheduler, queryTransformer); } /** An executable query. */ public static abstract class Query { /** * Creates an {@linkplain ObservableOperator operator} which transforms a query returning a * single row to a {@code T} using {@code mapper}. Use with {@link Observable#lift}. *

* It is an error for a query to pass through this operator with more than 1 row in its result * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows * do not emit an item. *

* This operator ignores {@code null} cursors returned from {@link #run()}. * * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. */ @CheckResult @NonNull // public static ObservableOperator mapToOne(@NonNull Function mapper) { return new QueryToOneOperator<>(mapper, null); } /** * Creates an {@linkplain ObservableOperator operator} which transforms a query returning a * single row to a {@code T} using {@code mapper}. Use with {@link Observable#lift}. *

* It is an error for a query to pass through this operator with more than 1 row in its result * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows * emit {@code defaultValue}. *

* This operator emits {@code defaultValue} if {@code null} is returned from {@link #run()}. * * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. * @param defaultValue Value returned if result set is empty */ @SuppressWarnings("ConstantConditions") // Public API contract. @CheckResult @NonNull public static ObservableOperator mapToOneOrDefault( @NonNull Function mapper, @NonNull T defaultValue) { if (defaultValue == null) throw new NullPointerException("defaultValue == null"); return new QueryToOneOperator<>(mapper, defaultValue); } /** * Creates an {@linkplain ObservableOperator operator} which transforms a query returning a * single row to a {@code Optional} using {@code mapper}. Use with {@link Observable#lift}. *

* It is an error for a query to pass through this operator with more than 1 row in its result * set. Use {@code LIMIT 1} on the underlying SQL query to prevent this. Result sets with 0 rows * emit {@link Optional#empty() Optional.empty()}. *

* This operator ignores {@code null} cursors returned from {@link #run()}. * * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. */ @RequiresApi(Build.VERSION_CODES.N) // @CheckResult @NonNull // public static ObservableOperator, Query> mapToOptional( @NonNull Function mapper) { return new QueryToOptionalOperator<>(mapper); } /** * Creates an {@linkplain ObservableOperator operator} which transforms a query to a * {@code List} using {@code mapper}. Use with {@link Observable#lift}. *

* Be careful using this operator as it will always consume the entire cursor and create objects * for each row, every time this observable emits a new query. On tables whose queries update * frequently or very large result sets this can result in the creation of many objects. *

* This operator ignores {@code null} cursors returned from {@link #run()}. * * @param mapper Maps the current {@link Cursor} row to {@code T}. May not return null. */ @CheckResult @NonNull public static ObservableOperator, Query> mapToList( @NonNull Function mapper) { return new QueryToListOperator<>(mapper); } /** * Execute the query on the underlying database and return the resulting cursor. * * @return A {@link Cursor} with query results, or {@code null} when the query could not be * executed due to a problem with the underlying store. Unfortunately it is not well documented * when {@code null} is returned. It usually involves a problem in communicating with the * underlying store and should either be treated as failure or ignored for retry at a later * time. */ @CheckResult @WorkerThread @Nullable public abstract Cursor run(); /** * Execute the query on the underlying database and return an Observable of each row mapped to * {@code T} by {@code mapper}. *

* Standard usage of this operation is in {@code flatMap}: *

{@code
     * flatMap(q -> q.asRows(Item.MAPPER).toList())
     * }
* However, the above is a more-verbose but identical operation as * {@link QueryObservable#mapToList}. This {@code asRows} method should be used when you need * to limit or filter the items separate from the actual query. *
{@code
     * flatMap(q -> q.asRows(Item.MAPPER).take(5).toList())
     * // or...
     * flatMap(q -> q.asRows(Item.MAPPER).filter(i -> i.isActive).toList())
     * }
*

* Note: Limiting results or filtering will almost always be faster in the database as part of * a query and should be preferred, where possible. *

* The resulting observable will be empty if {@code null} is returned from {@link #run()}. */ @CheckResult @NonNull public final Observable asRows(final Function mapper) { return Observable.create(new ObservableOnSubscribe() { @Override public void subscribe(ObservableEmitter e) throws Exception { Cursor cursor = run(); if (cursor != null) { try { while (cursor.moveToNext() && !e.isDisposed()) { e.onNext(mapper.apply(cursor)); } } finally { cursor.close(); } } if (!e.isDisposed()) { e.onComplete(); } } }); } } /** A simple indirection for logging debug messages. */ public interface Logger { void log(String message); } } ================================================ FILE: sqlbrite-kotlin/build.gradle ================================================ apply plugin: 'com.android.library' apply plugin: 'org.jetbrains.kotlin.android' dependencies { api project(':sqlbrite') api rootProject.ext.kotlinStdLib } android { compileSdkVersion versions.compileSdk defaultConfig { minSdkVersion versions.minSdk } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } lintOptions { textOutput 'stdout' textReport true } // TODO replace with https://issuetracker.google.com/issues/72050365 once released. libraryVariants.all { it.generateBuildConfig.enabled = false } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { freeCompilerArgs = ['-Xno-param-assertions'] } } apply from: rootProject.file('gradle/gradle-mvn-push.gradle') ================================================ FILE: sqlbrite-kotlin/gradle.properties ================================================ POM_ARTIFACT_ID=sqlbrite-kotlin POM_NAME=SqlBrite (Kotlin Extensions) POM_PACKAGING=aar ================================================ FILE: sqlbrite-kotlin/src/main/AndroidManifest.xml ================================================ ================================================ FILE: sqlbrite-kotlin/src/main/java/com/squareup/sqlbrite3/extensions.kt ================================================ /* * Copyright (C) 2017 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ @file:Suppress("NOTHING_TO_INLINE") // Extensions provided for intentional convenience. package com.squareup.sqlbrite3 import android.database.Cursor import android.support.annotation.RequiresApi import com.squareup.sqlbrite3.BriteDatabase.Transaction import com.squareup.sqlbrite3.SqlBrite.Query import io.reactivex.Observable import java.util.Optional typealias Mapper = (Cursor) -> T /** * Transforms an observable of single-row [Query] to an observable of `T` using `mapper`. * * It is an error for a query to pass through this operator with more than 1 row in its result set. * Use `LIMIT 1` on the underlying SQL query to prevent this. Result sets with 0 rows do not emit * an item. * * This operator ignores null cursors returned from [Query.run]. * * @param mapper Maps the current [Cursor] row to `T`. May not return null. */ inline fun Observable.mapToOne(noinline mapper: Mapper): Observable = lift(Query.mapToOne(mapper)) /** * Transforms an observable of single-row [Query] to an observable of `T` using `mapper` * * It is an error for a query to pass through this operator with more than 1 row in its result set. * Use `LIMIT 1` on the underlying SQL query to prevent this. Result sets with 0 rows emit * `default`. * * This operator emits `defaultValue` if null is returned from [Query.run]. * * @param mapper Maps the current [Cursor] row to `T`. May not return null. * @param default Value returned if result set is empty */ inline fun Observable.mapToOneOrDefault(default: T, noinline mapper: Mapper): Observable = lift(Query.mapToOneOrDefault(mapper, default)) /** * Transforms an observable of single-row [Query] to an observable of `T` using `mapper. * * It is an error for a query to pass through this operator with more than 1 row in its result set. * Use `LIMIT 1` on the underlying SQL query to prevent this. Result sets with 0 rows emit * `default`. * * This operator ignores null cursors returned from [Query.run]. * * @param mapper Maps the current [Cursor] row to `T`. May not return null. */ @RequiresApi(24) inline fun Observable.mapToOptional(noinline mapper: Mapper): Observable> = lift(Query.mapToOptional(mapper)) /** * Transforms an observable of [Query] to `List` using `mapper` for each row. * * Be careful using this operator as it will always consume the entire cursor and create objects * for each row, every time this observable emits a new query. On tables whose queries update * frequently or very large result sets this can result in the creation of many objects. * * This operator ignores null cursors returned from [Query.run]. * * @param mapper Maps the current [Cursor] row to `T`. May not return null. */ inline fun Observable.mapToList(noinline mapper: Mapper): Observable> = lift(Query.mapToList(mapper)) /** * Run the database interactions in `body` inside of a transaction. * * @param exclusive Uses [BriteDatabase.newTransaction] if true, otherwise * [BriteDatabase.newNonExclusiveTransaction]. */ inline fun BriteDatabase.inTransaction( exclusive: Boolean = true, body: BriteDatabase.(Transaction) -> T ): T { val transaction = if (exclusive) newTransaction() else newNonExclusiveTransaction() try { val result = body(transaction) transaction.markSuccessful() return result } finally { transaction.end() } } ================================================ FILE: sqlbrite-lint/build.gradle ================================================ apply plugin: 'kotlin' dependencies { compileOnly rootProject.ext.kotlinStdLib compileOnly rootProject.ext.lintApi testImplementation rootProject.ext.junit testImplementation rootProject.ext.lint testImplementation rootProject.ext.lintTests } jar { manifest { attributes("Lint-Registry-v2": "com.squareup.sqlbrite3.BriteIssueRegistry") } } ================================================ FILE: sqlbrite-lint/src/main/java/com/squareup/sqlbrite3/BriteIssueRegistry.kt ================================================ /* * Copyright (C) 2017 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3 import com.android.tools.lint.client.api.IssueRegistry class BriteIssueRegistry : IssueRegistry() { override fun getIssues() = listOf(SqlBriteArgCountDetector.ISSUE) } ================================================ FILE: sqlbrite-lint/src/main/java/com/squareup/sqlbrite3/SqlBriteArgCountDetector.kt ================================================ /* * Copyright (C) 2017 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3 import com.android.tools.lint.detector.api.Category import com.android.tools.lint.detector.api.ConstantEvaluator.evaluateString import com.android.tools.lint.detector.api.Detector import com.android.tools.lint.detector.api.Implementation import com.android.tools.lint.detector.api.Issue import com.android.tools.lint.detector.api.JavaContext import com.android.tools.lint.detector.api.Scope.JAVA_FILE import com.android.tools.lint.detector.api.Scope.TEST_SOURCES import com.android.tools.lint.detector.api.Severity import com.intellij.psi.PsiMethod import org.jetbrains.uast.UCallExpression import java.util.EnumSet private const val BRITE_DATABASE = "com.squareup.sqlbrite3.BriteDatabase" private const val QUERY_METHOD_NAME = "query" private const val CREATE_QUERY_METHOD_NAME = "createQuery" class SqlBriteArgCountDetector : Detector(), Detector.UastScanner { companion object { val ISSUE: Issue = Issue.create( "SqlBriteArgCount", "Number of provided arguments doesn't match number " + "of arguments specified in query", "When providing arguments to query you need to provide the same amount of " + "arguments that is specified in query.", Category.MESSAGES, 9, Severity.ERROR, Implementation(SqlBriteArgCountDetector::class.java, EnumSet.of(JAVA_FILE, TEST_SOURCES))) } override fun getApplicableMethodNames() = listOf(CREATE_QUERY_METHOD_NAME, QUERY_METHOD_NAME) override fun visitMethod(context: JavaContext, call: UCallExpression, method: PsiMethod) { val evaluator = context.evaluator if (evaluator.isMemberInClass(method, BRITE_DATABASE)) { // Skip non varargs overloads. if (!method.isVarArgs) return // Position of sql parameter depends on method. val sql = evaluateString(context, call.valueArguments[if (call.isQueryMethod()) 0 else 1], true) ?: return // Count only vararg arguments. val argumentsCount = call.valueArgumentCount - if (call.isQueryMethod()) 1 else 2 val questionMarksCount = sql.count { it == '?' } if (argumentsCount != questionMarksCount) { val requiredArguments = "$questionMarksCount ${"argument".pluralize(questionMarksCount)}" val actualArguments = "$argumentsCount ${"argument".pluralize(argumentsCount)}" context.report(ISSUE, call, context.getLocation(call), "Wrong argument count, " + "query $sql requires $requiredArguments, but was provided $actualArguments") } } } private fun UCallExpression.isQueryMethod() = methodName == QUERY_METHOD_NAME private fun String.pluralize(count: Int) = if (count == 1) this else this + "s" } ================================================ FILE: sqlbrite-lint/src/test/java/com/squareup/sqlbrite3/SqlBriteArgCountDetectorTest.kt ================================================ /* * Copyright (C) 2017 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.squareup.sqlbrite3 import com.android.tools.lint.checks.infrastructure.TestFiles.java import com.android.tools.lint.checks.infrastructure.TestLintTask.lint import org.junit.Test class SqlBriteArgCountDetectorTest { companion object { private val BRITE_DATABASE_STUB = java( """ package com.squareup.sqlbrite3; public final class BriteDatabase { public void query(String sql, Object... args) { } public void createQuery(String table, String sql, Object... args) { } // simulate createQuery with SupportSQLiteQuery query parameter public void createQuery(String table, int something) { } } """.trimIndent() ) } @Test fun cleanCaseWithWithQueryAsLiteral() { lint().files( BRITE_DATABASE_STUB, java( """ package test.pkg; import com.squareup.sqlbrite3.BriteDatabase; public class Test { private static final String QUERY = "SELECT name FROM table WHERE id = ?"; public void test() { BriteDatabase db = new BriteDatabase(); db.query(QUERY, "id"); } } """.trimIndent())) .issues(SqlBriteArgCountDetector.ISSUE) .run() .expectClean() } @Test fun cleanCaseWithQueryAsBinaryExpression() { lint().files( BRITE_DATABASE_STUB, java( """ package test.pkg; import com.squareup.sqlbrite3.BriteDatabase; public class Test { private static final String QUERY = "SELECT name FROM table WHERE "; public void test() { BriteDatabase db = new BriteDatabase(); db.query(QUERY + "id = ?", "id"); } } """.trimIndent())) .issues(SqlBriteArgCountDetector.ISSUE) .run() .expectClean() } @Test fun cleanCaseWithQueryThatCantBeEvaluated() { lint().files( BRITE_DATABASE_STUB, java( """ package test.pkg; import com.squareup.sqlbrite3.BriteDatabase; public class Test { private static final String QUERY = "SELECT name FROM table WHERE id = ?"; public void test() { BriteDatabase db = new BriteDatabase(); db.query(query(), "id"); } private String query() { return QUERY + " age = ?"; } } """.trimIndent())) .issues(SqlBriteArgCountDetector.ISSUE) .run() .expectClean() } @Test fun cleanCaseWithNonVarargMethodCall() { lint().files( BRITE_DATABASE_STUB, java( """ package test.pkg; import com.squareup.sqlbrite3.BriteDatabase; public class Test { public void test() { BriteDatabase db = new BriteDatabase(); db.createQuery("table", 42); } } """.trimIndent())) .issues(SqlBriteArgCountDetector.ISSUE) .run() .expectClean() } @Test fun queryMethodWithWrongNumberOfArguments() { lint().files( BRITE_DATABASE_STUB, java( """ package test.pkg; import com.squareup.sqlbrite3.BriteDatabase; public class Test { private static final String QUERY = "SELECT name FROM table WHERE id = ?"; public void test() { BriteDatabase db = new BriteDatabase(); db.query(QUERY); } } """.trimIndent())) .issues(SqlBriteArgCountDetector.ISSUE) .run() .expect("src/test/pkg/Test.java:10: " + "Error: Wrong argument count, query SELECT name FROM table WHERE id = ?" + " requires 1 argument, but was provided 0 arguments [SqlBriteArgCount]\n" + " db.query(QUERY);\n" + " ~~~~~~~~~~~~~~~\n" + "1 errors, 0 warnings") } @Test fun createQueryMethodWithWrongNumberOfArguments() { lint().files( BRITE_DATABASE_STUB, java( """ package test.pkg; import com.squareup.sqlbrite3.BriteDatabase; public class Test { private static final String QUERY = "SELECT name FROM table WHERE id = ?"; public void test() { BriteDatabase db = new BriteDatabase(); db.createQuery("table", QUERY); } } """.trimIndent())) .issues(SqlBriteArgCountDetector.ISSUE) .run() .expect("src/test/pkg/Test.java:10: " + "Error: Wrong argument count, query SELECT name FROM table WHERE id = ?" + " requires 1 argument, but was provided 0 arguments [SqlBriteArgCount]\n" + " db.createQuery(\"table\", QUERY);\n" + " ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n" + "1 errors, 0 warnings") } }