package android.database; import static android.os.Build.VERSION_CODES.LOLLIPOP; import static com.google.common.truth.Truth.assertThat; import static java.util.concurrent.TimeUnit.SECONDS; import static org.junit.Assert.assertThrows; import android.content.ContentValues; import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteException; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; import androidx.test.filters.Suppress; import com.google.common.base.Ascii; import com.google.common.base.Throwables; import com.google.common.io.ByteStreams; import java.io.File; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.annotation.Config; import org.robolectric.annotation.internal.DoNotInstrument; /** Compatibility test for {@link android.database.sqlite.SQLiteDatabase} */ @DoNotInstrument @RunWith(AndroidJUnit4.class) public class SQLiteDatabaseTest { private SQLiteDatabase database; private File databasePath; @Before public void setUp() { databasePath = ApplicationProvider.getApplicationContext().getDatabasePath("database.db"); databasePath.getParentFile().mkdirs(); database = SQLiteDatabase.openOrCreateDatabase(databasePath, null); database.execSQL( "CREATE TABLE table_name (\n" + " id INTEGER PRIMARY KEY AUTOINCREMENT,\n" + " first_column VARCHAR(255),\n" + " second_column BINARY,\n" + " name VARCHAR(255),\n" + " big_int INTEGER\n" + ");"); } @After public void tearDown() { database.close(); assertThat(databasePath.delete()).isTrue(); } @Test public void shouldGetBlobFromString() { String s = "this is a string"; ContentValues values = new ContentValues(); values.put("first_column", s); database.insert("table_name", null, values); try (Cursor data = database.query("table_name", new String[] {"first_column"}, null, null, null, null, null)) { assertThat(data.getCount()).isEqualTo(1); data.moveToFirst(); byte[] columnBytes = data.getBlob(0); byte[] expected = Arrays.copyOf(s.getBytes(), s.length() + 1); // include zero-terminal assertThat(columnBytes).isEqualTo(expected); } } @Test public void shouldGetBlobFromNullString() { ContentValues values = new ContentValues(); values.put("first_column", (String) null); database.insert("table_name", null, values); try (Cursor data = database.query("table_name", new String[] {"first_column"}, null, null, null, null, null)) { assertThat(data.getCount()).isEqualTo(1); data.moveToFirst(); assertThat(data.getBlob(0)).isEqualTo(null); } } @Test public void shouldGetBlobFromEmptyString() { ContentValues values = new ContentValues(); values.put("first_column", ""); database.insert("table_name", null, values); try (Cursor data = database.query("table_name", new String[] {"first_column"}, null, null, null, null, null)) { assertThat(data.getCount()).isEqualTo(1); data.moveToFirst(); assertThat(data.getBlob(0)).isEqualTo(new byte[] {0}); } } @Test public void shouldThrowWhenForeignKeysConstraintIsViolated() { database.execSQL( "CREATE TABLE artist(\n" + " artistid INTEGER PRIMARY KEY, \n" + " artistname TEXT\n" + ");\n"); database.execSQL( "CREATE TABLE track(\n" + " trackid INTEGER, \n" + " trackname TEXT, \n" + " trackartist INTEGER,\n" + " FOREIGN KEY(trackartist) REFERENCES artist(artistid)\n" + ");"); database.execSQL("PRAGMA foreign_keys=ON"); database.execSQL("INSERT into artist (artistid, artistname) VALUES (1, 'Kanye')"); database.execSQL( "INSERT into track (trackid, trackname, trackartist) VALUES (1, 'Good Life', 1)"); SQLiteConstraintException ex = assertThrows(SQLiteConstraintException.class, () -> database.execSQL("delete from artist")); assertThat(Ascii.toLowerCase(Throwables.getStackTraceAsString(ex))).contains("foreign key"); } @Test public void shouldDeleteWithLikeEscape() { ContentValues values = new ContentValues(); values.put("first_column", "test"); database.insert("table_name", null, values); String select = "first_column LIKE ? ESCAPE ?"; String[] selectArgs = { "test", Character.toString('\\'), }; assertThat(database.delete("table_name", select, selectArgs)).isEqualTo(1); } @Test public void shouldThrowsExceptionWhenQueryingUsingExecSQL() { SQLiteException e = assertThrows(SQLiteException.class, () -> database.execSQL("select 1")); assertThat(e) .hasMessageThat() .contains("Queries can be performed using SQLiteDatabase query or rawQuery methods only."); } @Test public void close_withExclusiveLockingMode() { database.rawQuery("PRAGMA locking_mode = EXCLUSIVE", new String[0]).close(); ContentValues values = new ContentValues(); values.put("first_column", ""); database.insert("table_name", null, values); database.close(); database = SQLiteDatabase.openOrCreateDatabase(databasePath, null); database.insert("table_name", null, values); } static class MyCursorWindow extends CursorWindow { public MyCursorWindow(String name) { super(name); } /** Make the finalize method public */ @Override public void finalize() throws Throwable { super.finalize(); } } // TODO(hoisie): This test crashes in emulators, enable when it is fixed in Android. // Use Suppress here to stop it from running on emulators, but not on Robolectric @Suppress @Test public void cursorWindow_finalize_concurrentStressTest() throws Throwable { final PrintStream originalErr = System.err; // discard stderr output for this test to prevent CloseGuard logspam. System.setErr(new PrintStream(ByteStreams.nullOutputStream())); try { ExecutorService executor = Executors.newFixedThreadPool(4); for (int i = 0; i < 1000; i++) { final MyCursorWindow cursorWindow = new MyCursorWindow(String.valueOf(i)); for (int j = 0; j < 4; j++) { executor.execute( () -> { try { cursorWindow.finalize(); } catch (Throwable e) { throw new AssertionError(e); } }); } } executor.shutdown(); executor.awaitTermination(100, SECONDS); } finally { System.setErr(originalErr); } } @Test @Config(minSdk = LOLLIPOP) @SdkSuppress(minSdkVersion = LOLLIPOP) public void collate_unicode() { String[] names = new String[] {"aaa", "abc", "ABC", "bbb"}; for (String name : names) { ContentValues values = new ContentValues(); values.put("name", name); database.insert("table_name", null, values); } Cursor c = database.rawQuery("SELECT name from table_name ORDER BY name COLLATE UNICODE ASC", null); c.moveToFirst(); ArrayList sorted = new ArrayList<>(); while (!c.isAfterLast()) { sorted.add(c.getString(0)); c.moveToNext(); } c.close(); assertThat(sorted).containsExactly("aaa", "abc", "ABC", "bbb").inOrder(); } @Test @Config(minSdk = LOLLIPOP) @SdkSuppress(minSdkVersion = LOLLIPOP) public void regex_selection() { ContentValues values = new ContentValues(); values.put("first_column", "test"); database.insert("table_name", null, values); String select = "first_column regexp ?"; String[] selectArgs = { "test", }; assertThat(database.delete("table_name", select, selectArgs)).isEqualTo(1); } }