1 package android.database; 2 3 import static android.os.Build.VERSION_CODES.M; 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 import static org.junit.Assume.assumeTrue; 8 9 import android.content.ContentValues; 10 import android.database.sqlite.SQLiteConstraintException; 11 import android.database.sqlite.SQLiteDatabase; 12 import android.database.sqlite.SQLiteException; 13 import android.os.Build; 14 import androidx.test.core.app.ApplicationProvider; 15 import androidx.test.ext.junit.runners.AndroidJUnit4; 16 import androidx.test.filters.SdkSuppress; 17 import androidx.test.filters.Suppress; 18 import com.google.common.base.Ascii; 19 import com.google.common.base.Throwables; 20 import com.google.common.io.ByteStreams; 21 import java.io.File; 22 import java.io.PrintStream; 23 import java.lang.reflect.Field; 24 import java.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.concurrent.ExecutorService; 27 import java.util.concurrent.Executors; 28 import org.junit.After; 29 import org.junit.Before; 30 import org.junit.Test; 31 import org.junit.runner.RunWith; 32 33 /** Compatibility test for {@link android.database.sqlite.SQLiteDatabase} */ 34 @RunWith(AndroidJUnit4.class) 35 public class SQLiteDatabaseTest { 36 37 private SQLiteDatabase database; 38 private File databasePath; 39 40 @Before setUp()41 public void setUp() throws Exception { 42 assumeTrue(isNativeSqliteMode()); 43 databasePath = ApplicationProvider.getApplicationContext().getDatabasePath("database.db"); 44 databasePath.getParentFile().mkdirs(); 45 46 database = SQLiteDatabase.openOrCreateDatabase(databasePath, null); 47 database.execSQL( 48 "CREATE TABLE table_name (\n" 49 + " id INTEGER PRIMARY KEY AUTOINCREMENT,\n" 50 + " first_column VARCHAR(255),\n" 51 + " second_column BINARY,\n" 52 + " name VARCHAR(255),\n" 53 + " big_int INTEGER\n" 54 + ");"); 55 } 56 57 /** 58 * If running on Robolectric, this test only works in RobolectriSQLite native mode. Attempt to 59 * figure this out 60 */ isNativeSqliteMode()61 private boolean isNativeSqliteMode() throws IllegalAccessException, NoSuchFieldException { 62 if (!Build.FINGERPRINT.contains("robolectric")) { 63 // not robolectric, using native sql of course 64 return true; 65 } 66 Field internalRoboField = CursorWindow.class.getField("__robo_data__"); 67 return internalRoboField 68 .get(new CursorWindow("cw")) 69 .getClass() 70 .getName() 71 .contains("ShadowNativeCursorWindow"); 72 } 73 74 @After tearDown()75 public void tearDown() { 76 if (database != null) { 77 database.close(); 78 } 79 if (databasePath != null) { 80 assertThat(databasePath.delete()).isTrue(); 81 } 82 } 83 84 @Test shouldGetBlobFromString()85 public void shouldGetBlobFromString() { 86 String s = "this is a string"; 87 ContentValues values = new ContentValues(); 88 values.put("first_column", s); 89 90 database.insert("table_name", null, values); 91 92 try (Cursor data = 93 database.query("table_name", new String[] {"first_column"}, null, null, null, null, null)) { 94 assertThat(data.getCount()).isEqualTo(1); 95 data.moveToFirst(); 96 byte[] columnBytes = data.getBlob(0); 97 byte[] expected = Arrays.copyOf(s.getBytes(), s.length() + 1); // include zero-terminal 98 assertThat(columnBytes).isEqualTo(expected); 99 } 100 } 101 102 @Test shouldGetBlobFromNullString()103 public void shouldGetBlobFromNullString() { 104 ContentValues values = new ContentValues(); 105 values.put("first_column", (String) null); 106 database.insert("table_name", null, values); 107 108 try (Cursor data = 109 database.query("table_name", new String[] {"first_column"}, null, null, null, null, null)) { 110 assertThat(data.getCount()).isEqualTo(1); 111 data.moveToFirst(); 112 assertThat(data.getBlob(0)).isEqualTo(null); 113 } 114 } 115 116 @Test shouldGetBlobFromEmptyString()117 public void shouldGetBlobFromEmptyString() { 118 ContentValues values = new ContentValues(); 119 values.put("first_column", ""); 120 database.insert("table_name", null, values); 121 122 try (Cursor data = 123 database.query("table_name", new String[] {"first_column"}, null, null, null, null, null)) { 124 assertThat(data.getCount()).isEqualTo(1); 125 data.moveToFirst(); 126 assertThat(data.getBlob(0)).isEqualTo(new byte[] {0}); 127 } 128 } 129 130 @Test shouldThrowWhenForeignKeysConstraintIsViolated()131 public void shouldThrowWhenForeignKeysConstraintIsViolated() { 132 database.execSQL( 133 "CREATE TABLE artist(\n" 134 + " artistid INTEGER PRIMARY KEY, \n" 135 + " artistname TEXT\n" 136 + ");\n"); 137 138 database.execSQL( 139 "CREATE TABLE track(\n" 140 + " trackid INTEGER, \n" 141 + " trackname TEXT, \n" 142 + " trackartist INTEGER,\n" 143 + " FOREIGN KEY(trackartist) REFERENCES artist(artistid)\n" 144 + ");"); 145 146 database.execSQL("PRAGMA foreign_keys=ON"); 147 database.execSQL("INSERT into artist (artistid, artistname) VALUES (1, 'Kanye')"); 148 database.execSQL( 149 "INSERT into track (trackid, trackname, trackartist) VALUES (1, 'Good Life', 1)"); 150 SQLiteConstraintException ex = 151 assertThrows(SQLiteConstraintException.class, () -> database.execSQL("delete from artist")); 152 assertThat(Ascii.toLowerCase(Throwables.getStackTraceAsString(ex))).contains("foreign key"); 153 } 154 155 @Test shouldDeleteWithLikeEscape()156 public void shouldDeleteWithLikeEscape() { 157 ContentValues values = new ContentValues(); 158 values.put("first_column", "test"); 159 database.insert("table_name", null, values); 160 String select = "first_column LIKE ? ESCAPE ?"; 161 String[] selectArgs = { 162 "test", Character.toString('\\'), 163 }; 164 assertThat(database.delete("table_name", select, selectArgs)).isEqualTo(1); 165 } 166 167 @Test shouldThrowsExceptionWhenQueryingUsingExecSQL()168 public void shouldThrowsExceptionWhenQueryingUsingExecSQL() { 169 SQLiteException e = assertThrows(SQLiteException.class, () -> database.execSQL("select 1")); 170 assertThat(e) 171 .hasMessageThat() 172 .contains("Queries can be performed using SQLiteDatabase query or rawQuery methods only."); 173 } 174 175 @Test close_withExclusiveLockingMode()176 public void close_withExclusiveLockingMode() { 177 database.rawQuery("PRAGMA locking_mode = EXCLUSIVE", new String[0]).close(); 178 ContentValues values = new ContentValues(); 179 values.put("first_column", ""); 180 database.insert("table_name", null, values); 181 database.close(); 182 183 database = SQLiteDatabase.openOrCreateDatabase(databasePath, null); 184 database.insert("table_name", null, values); 185 } 186 187 static class MyCursorWindow extends CursorWindow { MyCursorWindow(String name)188 public MyCursorWindow(String name) { 189 super(name); 190 } 191 192 /** Make the finalize method public for testing purposes. */ 193 @SuppressWarnings("Finalize") 194 @Override finalize()195 public void finalize() throws Throwable { 196 super.finalize(); 197 } 198 } 199 200 // TODO(hoisie): This test crashes in emulators, enable when it is fixed in Android. 201 // Use Suppress here to stop it from running on emulators, but not on Robolectric 202 @Suppress 203 @Test cursorWindow_finalize_concurrentStressTest()204 public void cursorWindow_finalize_concurrentStressTest() throws Throwable { 205 final PrintStream originalErr = System.err; 206 // discard stderr output for this test to prevent CloseGuard logspam. 207 System.setErr(new PrintStream(ByteStreams.nullOutputStream())); 208 try { 209 ExecutorService executor = Executors.newFixedThreadPool(4); 210 for (int i = 0; i < 1000; i++) { 211 final MyCursorWindow cursorWindow = new MyCursorWindow(String.valueOf(i)); 212 for (int j = 0; j < 4; j++) { 213 executor.execute( 214 () -> { 215 try { 216 cursorWindow.finalize(); 217 } catch (Throwable e) { 218 throw new AssertionError(e); 219 } 220 }); 221 } 222 } 223 executor.shutdown(); 224 executor.awaitTermination(100, SECONDS); 225 } finally { 226 System.setErr(originalErr); 227 } 228 } 229 230 @Test collate_unicode()231 public void collate_unicode() { 232 String[] names = new String[] {"aaa", "abc", "ABC", "bbb"}; 233 for (String name : names) { 234 ContentValues values = new ContentValues(); 235 values.put("name", name); 236 database.insert("table_name", null, values); 237 } 238 Cursor c = 239 database.rawQuery("SELECT name from table_name ORDER BY name COLLATE UNICODE ASC", null); 240 c.moveToFirst(); 241 ArrayList<String> sorted = new ArrayList<>(); 242 while (!c.isAfterLast()) { 243 sorted.add(c.getString(0)); 244 c.moveToNext(); 245 } 246 c.close(); 247 assertThat(sorted).containsExactly("aaa", "abc", "ABC", "bbb").inOrder(); 248 } 249 250 @Test regex_selection()251 public void regex_selection() { 252 ContentValues values = new ContentValues(); 253 values.put("first_column", "test"); 254 database.insert("table_name", null, values); 255 String select = "first_column regexp ?"; 256 String[] selectArgs = { 257 "test", 258 }; 259 assertThat(database.delete("table_name", select, selectArgs)).isEqualTo(1); 260 } 261 262 @Test 263 @SdkSuppress(minSdkVersion = M) // This test fails on emulators for SDKs 21 and 22 fts4()264 public void fts4() { 265 database.execSQL( 266 "CREATE VIRTUAL TABLE documents USING fts4 (" 267 + "id INTEGER PRIMARY KEY, " 268 + "title TEXT, " 269 + "content TEXT" 270 + ")"); 271 ContentValues values = new ContentValues(); 272 values.put("title", "Title1"); 273 values.put("content", "Hello World"); 274 database.insert("documents", null, values); 275 276 String[] columns = {"id"}; 277 Cursor results = 278 database.query( 279 "documents", 280 columns, 281 "documents MATCH 'content:*Wor* OR title:*Wor*'", 282 null, 283 null, 284 null, 285 null); 286 assertThat(results.getCount()).isEqualTo(1); 287 results.close(); 288 289 // Android does not support parenthesis in MATCH clauses 290 // See https://github.com/robolectric/robolectric/issues/8495 291 results = 292 database.query( 293 "documents", 294 columns, 295 "documents MATCH '(content:*Wor* OR title:*Wor*)'", 296 null, 297 null, 298 null, 299 null); 300 assertThat(results.getCount()).isEqualTo(0); 301 results.close(); 302 } 303 304 @Test uniqueConstraintViolation_errorMessage()305 public void uniqueConstraintViolation_errorMessage() { 306 database.execSQL( 307 "CREATE TABLE my_table(\n" 308 + " _id INTEGER PRIMARY KEY AUTOINCREMENT, \n" 309 + " unique_column TEXT UNIQUE\n" 310 + ");\n"); 311 ContentValues values = new ContentValues(); 312 values.put("unique_column", "test"); 313 database.insertOrThrow("my_table", null, values); 314 SQLiteConstraintException exception = 315 assertThrows( 316 SQLiteConstraintException.class, 317 () -> database.insertOrThrow("my_table", null, values)); 318 assertThat(exception) 319 .hasMessageThat() 320 .startsWith("UNIQUE constraint failed: my_table.unique_column"); 321 assertThat(exception).hasMessageThat().contains("code 2067"); 322 } 323 } 324