1 package android.database; 2 3 import static android.os.Build.VERSION_CODES.LOLLIPOP; 4 import static com.google.common.truth.Truth.assertThat; 5 import static java.util.concurrent.TimeUnit.SECONDS; 6 import static org.junit.Assert.assertThrows; 7 8 import android.content.ContentValues; 9 import android.database.sqlite.SQLiteConstraintException; 10 import android.database.sqlite.SQLiteDatabase; 11 import android.database.sqlite.SQLiteException; 12 import androidx.test.core.app.ApplicationProvider; 13 import androidx.test.ext.junit.runners.AndroidJUnit4; 14 import androidx.test.filters.SdkSuppress; 15 import com.google.common.base.Ascii; 16 import com.google.common.base.Throwables; 17 import com.google.common.io.ByteStreams; 18 import java.io.File; 19 import java.io.PrintStream; 20 import java.util.ArrayList; 21 import java.util.Arrays; 22 import java.util.concurrent.ExecutorService; 23 import java.util.concurrent.Executors; 24 import org.junit.After; 25 import org.junit.Before; 26 import org.junit.Test; 27 import org.junit.runner.RunWith; 28 import org.robolectric.annotation.Config; 29 import org.robolectric.annotation.internal.DoNotInstrument; 30 31 /** Compatibility test for {@link android.database.sqlite.SQLiteDatabase} */ 32 @DoNotInstrument 33 @RunWith(AndroidJUnit4.class) 34 public class SQLiteDatabaseTest { 35 36 private SQLiteDatabase database; 37 private File databasePath; 38 39 @Before setUp()40 public void setUp() { 41 databasePath = ApplicationProvider.getApplicationContext().getDatabasePath("database.db"); 42 databasePath.getParentFile().mkdirs(); 43 44 database = SQLiteDatabase.openOrCreateDatabase(databasePath, null); 45 database.execSQL( 46 "CREATE TABLE table_name (\n" 47 + " id INTEGER PRIMARY KEY AUTOINCREMENT,\n" 48 + " first_column VARCHAR(255),\n" 49 + " second_column BINARY,\n" 50 + " name VARCHAR(255),\n" 51 + " big_int INTEGER\n" 52 + ");"); 53 } 54 55 @After tearDown()56 public void tearDown() { 57 database.close(); 58 assertThat(databasePath.delete()).isTrue(); 59 } 60 61 @Test shouldGetBlobFromString()62 public void shouldGetBlobFromString() { 63 String s = "this is a string"; 64 ContentValues values = new ContentValues(); 65 values.put("first_column", s); 66 67 database.insert("table_name", null, values); 68 69 try (Cursor data = 70 database.query("table_name", new String[] {"first_column"}, null, null, null, null, null)) { 71 assertThat(data.getCount()).isEqualTo(1); 72 data.moveToFirst(); 73 byte[] columnBytes = data.getBlob(0); 74 byte[] expected = Arrays.copyOf(s.getBytes(), s.length() + 1); // include zero-terminal 75 assertThat(columnBytes).isEqualTo(expected); 76 } 77 } 78 79 @Test shouldGetBlobFromNullString()80 public void shouldGetBlobFromNullString() { 81 ContentValues values = new ContentValues(); 82 values.put("first_column", (String) null); 83 database.insert("table_name", null, values); 84 85 try (Cursor data = 86 database.query("table_name", new String[] {"first_column"}, null, null, null, null, null)) { 87 assertThat(data.getCount()).isEqualTo(1); 88 data.moveToFirst(); 89 assertThat(data.getBlob(0)).isEqualTo(null); 90 } 91 } 92 93 @Test shouldGetBlobFromEmptyString()94 public void shouldGetBlobFromEmptyString() { 95 ContentValues values = new ContentValues(); 96 values.put("first_column", ""); 97 database.insert("table_name", null, values); 98 99 try (Cursor data = 100 database.query("table_name", new String[] {"first_column"}, null, null, null, null, null)) { 101 assertThat(data.getCount()).isEqualTo(1); 102 data.moveToFirst(); 103 assertThat(data.getBlob(0)).isEqualTo(new byte[] {0}); 104 } 105 } 106 107 @Test shouldThrowWhenForeignKeysConstraintIsViolated()108 public void shouldThrowWhenForeignKeysConstraintIsViolated() { 109 database.execSQL( 110 "CREATE TABLE artist(\n" 111 + " artistid INTEGER PRIMARY KEY, \n" 112 + " artistname TEXT\n" 113 + ");\n"); 114 115 database.execSQL( 116 "CREATE TABLE track(\n" 117 + " trackid INTEGER, \n" 118 + " trackname TEXT, \n" 119 + " trackartist INTEGER,\n" 120 + " FOREIGN KEY(trackartist) REFERENCES artist(artistid)\n" 121 + ");"); 122 123 database.execSQL("PRAGMA foreign_keys=ON"); 124 database.execSQL("INSERT into artist (artistid, artistname) VALUES (1, 'Kanye')"); 125 database.execSQL( 126 "INSERT into track (trackid, trackname, trackartist) VALUES (1, 'Good Life', 1)"); 127 SQLiteConstraintException ex = 128 assertThrows(SQLiteConstraintException.class, () -> database.execSQL("delete from artist")); 129 assertThat(Ascii.toLowerCase(Throwables.getStackTraceAsString(ex))).contains("foreign key"); 130 } 131 132 @Test shouldDeleteWithLikeEscape()133 public void shouldDeleteWithLikeEscape() { 134 ContentValues values = new ContentValues(); 135 values.put("first_column", "test"); 136 database.insert("table_name", null, values); 137 String select = "first_column LIKE ? ESCAPE ?"; 138 String[] selectArgs = { 139 "test", Character.toString('\\'), 140 }; 141 assertThat(database.delete("table_name", select, selectArgs)).isEqualTo(1); 142 } 143 144 @Test shouldThrowsExceptionWhenQueryingUsingExecSQL()145 public void shouldThrowsExceptionWhenQueryingUsingExecSQL() { 146 SQLiteException e = assertThrows(SQLiteException.class, () -> database.execSQL("select 1")); 147 assertThat(e) 148 .hasMessageThat() 149 .contains("Queries can be performed using SQLiteDatabase query or rawQuery methods only."); 150 } 151 152 @Test close_withExclusiveLockingMode()153 public void close_withExclusiveLockingMode() { 154 database.rawQuery("PRAGMA locking_mode = EXCLUSIVE", new String[0]).close(); 155 ContentValues values = new ContentValues(); 156 values.put("first_column", ""); 157 database.insert("table_name", null, values); 158 database.close(); 159 160 database = SQLiteDatabase.openOrCreateDatabase(databasePath, null); 161 database.insert("table_name", null, values); 162 } 163 164 static class MyCursorWindow extends CursorWindow { MyCursorWindow(String name)165 public MyCursorWindow(String name) { 166 super(name); 167 } 168 169 /** Make the finalize method public */ 170 @Override finalize()171 public void finalize() throws Throwable { 172 super.finalize(); 173 } 174 } 175 176 // TODO(hoisie): This test crashes in emulators, enable when it is fixed in Android. 177 @SdkSuppress(minSdkVersion = 34) 178 @Test cursorWindow_finalize_concurrentStressTest()179 public void cursorWindow_finalize_concurrentStressTest() throws Throwable { 180 final PrintStream originalErr = System.err; 181 // discard stderr output for this test to prevent CloseGuard logspam. 182 System.setErr(new PrintStream(ByteStreams.nullOutputStream())); 183 try { 184 ExecutorService executor = Executors.newFixedThreadPool(4); 185 for (int i = 0; i < 1000; i++) { 186 final MyCursorWindow cursorWindow = new MyCursorWindow(String.valueOf(i)); 187 for (int j = 0; j < 4; j++) { 188 executor.execute( 189 () -> { 190 try { 191 cursorWindow.finalize(); 192 } catch (Throwable e) { 193 throw new AssertionError(e); 194 } 195 }); 196 } 197 } 198 executor.shutdown(); 199 executor.awaitTermination(100, SECONDS); 200 } finally { 201 System.setErr(originalErr); 202 } 203 } 204 205 @Test 206 @Config(minSdk = LOLLIPOP) 207 @SdkSuppress(minSdkVersion = LOLLIPOP) collate_unicode()208 public void collate_unicode() { 209 String[] names = new String[] {"aaa", "abc", "ABC", "bbb"}; 210 for (String name : names) { 211 ContentValues values = new ContentValues(); 212 values.put("name", name); 213 database.insert("table_name", null, values); 214 } 215 Cursor c = 216 database.rawQuery("SELECT name from table_name ORDER BY name COLLATE UNICODE ASC", null); 217 c.moveToFirst(); 218 ArrayList<String> sorted = new ArrayList<>(); 219 while (!c.isAfterLast()) { 220 sorted.add(c.getString(0)); 221 c.moveToNext(); 222 } 223 c.close(); 224 assertThat(sorted).containsExactly("aaa", "abc", "ABC", "bbb").inOrder(); 225 } 226 } 227