• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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