/* * 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<String> logs = new ArrayList<>(); private final RecordingObserver o = new RecordingObserver(); private final TestScheduler scheduler = new TestScheduler(); private final PublishSubject<Object> 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<Query, Query> queryTransformer = new ObservableTransformer<Query, Query>() { @Override public ObservableSource<Query> apply(Observable<Query> 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<Query>() { @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<Employee> 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<String> 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> 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<String> 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.<String>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<String> 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.<String>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<String> 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.<String>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<String> 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.<String>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<Query>() { @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> 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> 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); } }