• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2009 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.providers.media.util;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertNotNull;
21 import static org.junit.Assert.assertTrue;
22 import static org.junit.Assert.fail;
23 
24 import android.content.ContentValues;
25 import android.content.Context;
26 import android.database.Cursor;
27 import android.database.sqlite.SQLiteCursor;
28 import android.database.sqlite.SQLiteCursorDriver;
29 import android.database.sqlite.SQLiteDatabase;
30 import android.database.sqlite.SQLiteQuery;
31 import android.os.Build;
32 import android.os.CancellationSignal;
33 import android.os.OperationCanceledException;
34 
35 import androidx.test.InstrumentationRegistry;
36 import androidx.test.runner.AndroidJUnit4;
37 
38 import com.google.common.util.concurrent.Uninterruptibles;
39 
40 import org.junit.After;
41 import org.junit.Before;
42 import org.junit.Test;
43 import org.junit.runner.RunWith;
44 
45 import java.util.Arrays;
46 import java.util.HashMap;
47 import java.util.Map;
48 import java.util.Objects;
49 import java.util.concurrent.Semaphore;
50 
51 @RunWith(AndroidJUnit4.class)
52 public class SQLiteQueryBuilderTest {
53     private SQLiteDatabase mDatabase;
54     private SQLiteQueryBuilder mStrictBuilder;
55 
56     private final String TEST_TABLE_NAME = "test";
57     private final String EMPLOYEE_TABLE_NAME = "employee";
58     private static final String DATABASE_FILE = "database_test.db";
59 
60     @Before
setUp()61     public void setUp() throws Exception {
62         final Context context = InstrumentationRegistry.getTargetContext();
63 
64         context.deleteDatabase(DATABASE_FILE);
65         mDatabase = Objects.requireNonNull(
66                 context.openOrCreateDatabase(DATABASE_FILE, Context.MODE_PRIVATE, null));
67 
68         createEmployeeTable();
69         createStrictQueryBuilder();
70     }
71 
72     @After
tearDown()73     public void tearDown() throws Exception {
74         final Context context = InstrumentationRegistry.getTargetContext();
75 
76         mDatabase.close();
77         context.deleteDatabase(DATABASE_FILE);
78     }
79 
80     @Test
testConstructor()81     public void testConstructor() {
82         new SQLiteQueryBuilder();
83     }
84 
85     @Test
testSetDistinct()86     public void testSetDistinct() {
87         String expected;
88         SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder();
89         sqliteQueryBuilder.setTables(TEST_TABLE_NAME);
90         sqliteQueryBuilder.setDistinct(false);
91         sqliteQueryBuilder.appendWhere("age=20");
92         String sql = sqliteQueryBuilder.buildQuery(new String[] { "age", "address" },
93                 null, null, null, null, null);
94         assertEquals(TEST_TABLE_NAME, sqliteQueryBuilder.getTables());
95         expected = "SELECT age, address FROM " + TEST_TABLE_NAME + " WHERE (age=20)";
96         assertEquals(expected, sql);
97 
98         sqliteQueryBuilder = new SQLiteQueryBuilder();
99         sqliteQueryBuilder.setTables(EMPLOYEE_TABLE_NAME);
100         sqliteQueryBuilder.setDistinct(true);
101         sqliteQueryBuilder.appendWhere("age>32");
102         sql = sqliteQueryBuilder.buildQuery(new String[] { "age", "address" },
103                 null, null, null, null, null);
104         assertEquals(EMPLOYEE_TABLE_NAME, sqliteQueryBuilder.getTables());
105         expected = "SELECT DISTINCT age, address FROM " + EMPLOYEE_TABLE_NAME + " WHERE (age>32)";
106         assertEquals(expected, sql);
107 
108         sqliteQueryBuilder = new SQLiteQueryBuilder();
109         sqliteQueryBuilder.setTables(EMPLOYEE_TABLE_NAME);
110         sqliteQueryBuilder.setDistinct(true);
111         sqliteQueryBuilder.appendWhereEscapeString("age>32");
112         sql = sqliteQueryBuilder.buildQuery(new String[] { "age", "address" },
113                 null, null, null, null, null);
114         assertEquals(EMPLOYEE_TABLE_NAME, sqliteQueryBuilder.getTables());
115         expected = "SELECT DISTINCT age, address FROM " + EMPLOYEE_TABLE_NAME
116                 + " WHERE ('age>32')";
117         assertEquals(expected, sql);
118     }
119 
120     @Test
testSetProjectionMap()121     public void testSetProjectionMap() {
122         String expected;
123         Map<String, String> projectMap = new HashMap<String, String>();
124         projectMap.put("EmployeeName", "name");
125         projectMap.put("EmployeeAge", "age");
126         projectMap.put("EmployeeAddress", "address");
127         SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder();
128         sqliteQueryBuilder.setTables(TEST_TABLE_NAME);
129         sqliteQueryBuilder.setDistinct(false);
130         sqliteQueryBuilder.setProjectionMap(projectMap);
131         String sql = sqliteQueryBuilder.buildQuery(new String[] { "EmployeeName", "EmployeeAge" },
132                 null, null, null, null, null);
133         expected = "SELECT name, age FROM " + TEST_TABLE_NAME;
134         assertEquals(expected, sql);
135 
136         sql = sqliteQueryBuilder.buildQuery(null, // projectionIn is null
137                 null, null, null, null, null);
138         assertTrue(sql.matches("SELECT (age|name|address), (age|name|address), (age|name|address) "
139                 + "FROM " + TEST_TABLE_NAME));
140         assertTrue(sql.contains("age"));
141         assertTrue(sql.contains("name"));
142         assertTrue(sql.contains("address"));
143 
144         sqliteQueryBuilder.setProjectionMap(null);
145         sql = sqliteQueryBuilder.buildQuery(new String[] { "name", "address" },
146                 null, null, null, null, null);
147         assertTrue(sql.matches("SELECT (name|address), (name|address) "
148                 + "FROM " + TEST_TABLE_NAME));
149         assertTrue(sql.contains("name"));
150         assertTrue(sql.contains("address"));
151     }
152 
153     private static class MockCursor extends SQLiteCursor {
MockCursor(SQLiteCursorDriver driver, String editTable, SQLiteQuery query)154         public MockCursor(SQLiteCursorDriver driver,
155                 String editTable, SQLiteQuery query) {
156             super(driver, editTable, query);
157         }
158     }
159 
160     @Test
testBuildQueryString()161     public void testBuildQueryString() {
162         String expected;
163         final String[] DEFAULT_TEST_PROJECTION = new String [] { "name", "age", "sum(salary)" };
164         final String DEFAULT_TEST_WHERE = "age > 25";
165         final String DEFAULT_HAVING = "sum(salary) > 3000";
166 
167         String sql = SQLiteQueryBuilder.buildQueryString(false, "Employee",
168                 DEFAULT_TEST_PROJECTION,
169                 DEFAULT_TEST_WHERE, "name", DEFAULT_HAVING, "name", "100");
170 
171         expected = "SELECT name, age, sum(salary) FROM Employee WHERE " + DEFAULT_TEST_WHERE +
172                 " GROUP BY name " +
173                 "HAVING " + DEFAULT_HAVING + " " +
174                 "ORDER BY name " +
175                 "LIMIT 100";
176         assertEquals(expected, sql);
177     }
178 
179     @Test
testBuildQuery()180     public void testBuildQuery() {
181         final String[] DEFAULT_TEST_PROJECTION = new String[] { "name", "sum(salary)" };
182         final String DEFAULT_TEST_WHERE = "age > 25";
183         final String DEFAULT_HAVING = "sum(salary) > 2000";
184 
185         SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder();
186         sqliteQueryBuilder.setTables(TEST_TABLE_NAME);
187         sqliteQueryBuilder.setDistinct(false);
188         String sql = sqliteQueryBuilder.buildQuery(DEFAULT_TEST_PROJECTION,
189                 DEFAULT_TEST_WHERE, "name", DEFAULT_HAVING, "name", "2");
190         String expected = "SELECT name, sum(salary) FROM " + TEST_TABLE_NAME
191                 + " WHERE (" + DEFAULT_TEST_WHERE + ") " +
192                 "GROUP BY name HAVING " + DEFAULT_HAVING + " ORDER BY name LIMIT 2";
193         assertEquals(expected, sql);
194     }
195 
196     @Test
testAppendColumns()197     public void testAppendColumns() {
198         StringBuilder sb = new StringBuilder();
199         String[] columns = new String[] { "name", "age" };
200 
201         assertEquals("", sb.toString());
202         SQLiteQueryBuilder.appendColumns(sb, columns);
203         assertEquals("name, age ", sb.toString());
204     }
205 
206     @Test
testAppendWhereStandalone()207     public void testAppendWhereStandalone() {
208         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
209         qb.setTables("Employee");
210         qb.appendWhereStandalone("A");
211         qb.appendWhereStandalone("B");
212         qb.appendWhereStandalone("C");
213 
214         final String query = qb.buildQuery(null, null, null, null, null, null);
215         assertTrue(query.contains("(A) AND (B) AND (C)"));
216     }
217 
218     @Test
testQuery()219     public void testQuery() {
220         SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder();
221         sqliteQueryBuilder.setTables("Employee");
222         Cursor cursor = sqliteQueryBuilder.query(mDatabase,
223                 new String[] { "name", "sum(salary)" }, null, null,
224                 "name", "sum(salary)>1000", "name", null, null);
225         assertNotNull(cursor);
226         assertEquals(3, cursor.getCount());
227 
228         final int COLUMN_NAME_INDEX = 0;
229         final int COLUMN_SALARY_INDEX = 1;
230         cursor.moveToFirst();
231         assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX));
232         assertEquals(4500, cursor.getInt(COLUMN_SALARY_INDEX));
233         cursor.moveToNext();
234         assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX));
235         assertEquals(4000, cursor.getInt(COLUMN_SALARY_INDEX));
236         cursor.moveToNext();
237         assertEquals("jack", cursor.getString(COLUMN_NAME_INDEX));
238         assertEquals(3500, cursor.getInt(COLUMN_SALARY_INDEX));
239 
240         sqliteQueryBuilder = new SQLiteQueryBuilder();
241         sqliteQueryBuilder.setTables(EMPLOYEE_TABLE_NAME);
242         cursor = sqliteQueryBuilder.query(mDatabase,
243                 new String[] { "name", "sum(salary)" }, null, null,
244                 "name", "sum(salary)>1000", "name", "2" // limit is 2
245                 , null);
246         assertNotNull(cursor);
247         assertEquals(2, cursor.getCount());
248         cursor.moveToFirst();
249         assertEquals("Jim", cursor.getString(COLUMN_NAME_INDEX));
250         assertEquals(4500, cursor.getInt(COLUMN_SALARY_INDEX));
251         cursor.moveToNext();
252         assertEquals("Mike", cursor.getString(COLUMN_NAME_INDEX));
253         assertEquals(4000, cursor.getInt(COLUMN_SALARY_INDEX));
254     }
255 
256     @Test
testCancelableQuery_WhenNotCanceled_ReturnsResultSet()257     public void testCancelableQuery_WhenNotCanceled_ReturnsResultSet() {
258         CancellationSignal cancellationSignal = new CancellationSignal();
259         SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder();
260         sqliteQueryBuilder.setTables("Employee");
261         Cursor cursor = sqliteQueryBuilder.query(mDatabase,
262                 new String[] { "name", "sum(salary)" }, null, null,
263                 "name", "sum(salary)>1000", "name", null, cancellationSignal);
264 
265         assertEquals(3, cursor.getCount());
266     }
267 
268     @Test
testCancelableQuery_WhenCanceledBeforeQuery_ThrowsImmediately()269     public void testCancelableQuery_WhenCanceledBeforeQuery_ThrowsImmediately() {
270         CancellationSignal cancellationSignal = new CancellationSignal();
271         SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder();
272         sqliteQueryBuilder.setTables("Employee");
273 
274         cancellationSignal.cancel();
275         try {
276             sqliteQueryBuilder.query(mDatabase,
277                     new String[] { "name", "sum(salary)" }, null, null,
278                     "name", "sum(salary)>1000", "name", null, cancellationSignal);
279             fail("Expected OperationCanceledException");
280         } catch (OperationCanceledException ex) {
281             // expected
282         }
283     }
284 
285     @Test
testCancelableQuery_WhenCanceledAfterQuery_ThrowsWhenExecuted()286     public void testCancelableQuery_WhenCanceledAfterQuery_ThrowsWhenExecuted() {
287         CancellationSignal cancellationSignal = new CancellationSignal();
288         SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder();
289         sqliteQueryBuilder.setTables("Employee");
290 
291         Cursor cursor = sqliteQueryBuilder.query(mDatabase,
292                 new String[] { "name", "sum(salary)" }, null, null,
293                 "name", "sum(salary)>1000", "name", null, cancellationSignal);
294 
295         cancellationSignal.cancel();
296         try {
297             cursor.getCount(); // force execution
298             fail("Expected OperationCanceledException");
299         } catch (OperationCanceledException ex) {
300             // expected
301         }
302     }
303 
304     @Test
testCancelableQuery_WhenCanceledDueToContention_StopsWaitingAndThrows()305     public void testCancelableQuery_WhenCanceledDueToContention_StopsWaitingAndThrows() {
306         for (int i = 0; i < 5; i++) {
307             final CancellationSignal cancellationSignal = new CancellationSignal();
308             final Semaphore barrier1 = new Semaphore(0);
309             final Semaphore barrier2 = new Semaphore(0);
310             Thread contentionThread = new Thread() {
311                 @Override
312                 public void run() {
313                     mDatabase.beginTransaction(); // acquire the only available connection
314                     barrier1.release(); // release query to start running
315                     try {
316                         barrier2.acquire(); // wait for test to end
317                     } catch (InterruptedException e) {
318                     }
319                     mDatabase.endTransaction(); // release the connection
320                 }
321             };
322             Thread cancellationThread = new Thread() {
323                 @Override
324                 public void run() {
325                     try {
326                         Thread.sleep(300);
327                     } catch (InterruptedException ex) {
328                     }
329                     cancellationSignal.cancel();
330                 }
331             };
332             try {
333                 SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder();
334                 sqliteQueryBuilder.setTables("Employee");
335 
336                 contentionThread.start();
337                 cancellationThread.start();
338 
339                 try {
340                     barrier1.acquire(); // wait for contention thread to start transaction
341                 } catch (InterruptedException e) {
342                 }
343 
344                 final long startTime = System.nanoTime();
345                 try {
346                     Cursor cursor = sqliteQueryBuilder.query(mDatabase,
347                             new String[] { "name", "sum(salary)" }, null, null,
348                             "name", "sum(salary)>1000", "name", null, cancellationSignal);
349                     cursor.getCount(); // force execution
350                     fail("Expected OperationCanceledException");
351                 } catch (OperationCanceledException ex) {
352                     // expected
353                 }
354 
355                 // We want to confirm that the query really was blocked trying to acquire a
356                 // connection for a certain amount of time before it was freed by cancel.
357                 final long waitTime = System.nanoTime() - startTime;
358                 if (waitTime > 150 * 1000000L) {
359                     return; // success!
360                 }
361             } finally {
362                 barrier1.release();
363                 barrier2.release();
364                 Uninterruptibles.joinUninterruptibly(contentionThread);
365                 Uninterruptibles.joinUninterruptibly(cancellationThread);
366             }
367         }
368 
369         // Occasionally we might miss the timing deadline due to factors in the
370         // environment, but if after several trials we still couldn't demonstrate
371         // that the query was blocked, then the test must be broken.
372         fail("Could not prove that the query actually blocked before cancel() was called.");
373     }
374 
375     @Test
testCancelableQuery_WhenCanceledDuringLongRunningQuery_CancelsQueryAndThrows()376     public void testCancelableQuery_WhenCanceledDuringLongRunningQuery_CancelsQueryAndThrows() {
377         // Populate a table with a bunch of integers.
378         mDatabase.execSQL("CREATE TABLE x (v INTEGER);");
379         for (int i = 0; i < 100; i++) {
380             mDatabase.execSQL("INSERT INTO x VALUES (?)", new Object[] { i });
381         }
382 
383         for (int i = 0; i < 5; i++) {
384             final CancellationSignal cancellationSignal = new CancellationSignal();
385             Thread cancellationThread = new Thread() {
386                 @Override
387                 public void run() {
388                     try {
389                         Thread.sleep(300);
390                     } catch (InterruptedException ex) {
391                     }
392                     cancellationSignal.cancel();
393                 }
394             };
395             try {
396                 // Build an unsatisfiable 5-way cross-product query over 100 values but
397                 // produces no output.  This should force SQLite to loop for a long time
398                 // as it tests 10^10 combinations.
399                 SQLiteQueryBuilder sqliteQueryBuilder = new SQLiteQueryBuilder();
400                 sqliteQueryBuilder.setTables("x AS a, x AS b, x AS c, x AS d, x AS e");
401 
402                 cancellationThread.start();
403 
404                 final long startTime = System.nanoTime();
405                 try {
406                     Cursor cursor = sqliteQueryBuilder.query(mDatabase, null,
407                             "a.v + b.v + c.v + d.v + e.v > 1000000",
408                             null, null, null, null, null, cancellationSignal);
409                     cursor.getCount(); // force execution
410                     fail("Expected OperationCanceledException");
411                 } catch (OperationCanceledException ex) {
412                     // expected
413                 }
414 
415                 // We want to confirm that the query really was running and then got
416                 // canceled midway.
417                 final long waitTime = System.nanoTime() - startTime;
418                 if (waitTime > 150 * 1000000L && waitTime < 600 * 1000000L) {
419                     return; // success!
420                 }
421             } finally {
422                 Uninterruptibles.joinUninterruptibly(cancellationThread);
423             }
424         }
425 
426         // Occasionally we might miss the timing deadline due to factors in the
427         // environment, but if after several trials we still couldn't demonstrate
428         // that the query was canceled, then the test must be broken.
429         fail("Could not prove that the query actually canceled midway during execution.");
430     }
431 
432     @Test
testUpdate()433     public void testUpdate() throws Exception {
434         final ContentValues values = new ContentValues();
435         values.put("name", "Anonymous");
436         values.put("salary", 0);
437 
438         {
439             final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
440             qb.setTables("employee");
441             qb.appendWhere("month=3");
442             assertEquals(2, qb.update(mDatabase, values, null, null));
443         }
444         {
445             final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
446             qb.setTables("employee");
447             assertEquals(1, qb.update(mDatabase, values, "month=?", new String[] { "2" }));
448         }
449     }
450 
451     @Test
testDelete()452     public void testDelete() throws Exception {
453         {
454             final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
455             qb.setTables("employee");
456             qb.appendWhere("month=3");
457             assertEquals(2, qb.delete(mDatabase, null, null));
458             assertEquals(0, qb.delete(mDatabase, null, null));
459         }
460         {
461             final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
462             qb.setTables("employee");
463             assertEquals(1, qb.delete(mDatabase, "month=?", new String[] { "2" }));
464             assertEquals(0, qb.delete(mDatabase, "month=?", new String[] { "2" }));
465         }
466     }
467 
468     @Test
testStrictQuery()469     public void testStrictQuery() throws Exception {
470         final SQLiteQueryBuilder qb = mStrictBuilder;
471 
472         // Should normally only be able to see one row
473         try (Cursor c = qb.query(mDatabase, null, null, null, null, null, null, null, null)) {
474             assertEquals(1, c.getCount());
475         }
476 
477         // Trying sneaky queries should fail; even if they somehow succeed, we
478         // shouldn't get to see any other data.
479         try (Cursor c = qb.query(mDatabase, null, "1=1", null, null, null, null, null, null)) {
480             assertEquals(1, c.getCount());
481         } catch (Exception tolerated) {
482         }
483         try (Cursor c = qb.query(mDatabase, null, "1=1 --", null, null, null, null, null, null)) {
484             assertEquals(1, c.getCount());
485         } catch (Exception tolerated) {
486         }
487         try (Cursor c = qb.query(mDatabase, null, "1=1) OR (1=1", null, null, null, null, null, null)) {
488             assertEquals(1, c.getCount());
489         } catch (Exception tolerated) {
490         }
491         try (Cursor c = qb.query(mDatabase, null, "1=1)) OR ((1=1", null, null, null, null, null, null)) {
492             assertEquals(1, c.getCount());
493         } catch (Exception tolerated) {
494         }
495     }
496 
497     @Test
testStrictUpdate()498     public void testStrictUpdate() throws Exception {
499         final SQLiteQueryBuilder qb = mStrictBuilder;
500 
501         final ContentValues values = new ContentValues();
502         values.put("name", "Anonymous");
503 
504         // Should normally only be able to update one row
505         assertEquals(1, qb.update(mDatabase, values, null, null));
506 
507         // Trying sneaky queries should fail; even if they somehow succeed, we
508         // shouldn't get to see any other data.
509         try {
510             assertEquals(1, qb.update(mDatabase, values, "1=1", null));
511         } catch (Exception tolerated) {
512         }
513         try {
514             assertEquals(1, qb.update(mDatabase, values, "1=1 --", null));
515         } catch (Exception tolerated) {
516         }
517         try {
518             assertEquals(1, qb.update(mDatabase, values, "1=1) OR (1=1", null));
519         } catch (Exception tolerated) {
520         }
521         try {
522             assertEquals(1, qb.update(mDatabase, values, "1=1)) OR ((1=1", null));
523         } catch (Exception tolerated) {
524         }
525     }
526 
527     @Test
testStrictDelete()528     public void testStrictDelete() throws Exception {
529         final SQLiteQueryBuilder qb = mStrictBuilder;
530 
531         // Should normally only be able to update one row
532         createEmployeeTable();
533         assertEquals(1, qb.delete(mDatabase, null, null));
534 
535         // Trying sneaky queries should fail; even if they somehow succeed, we
536         // shouldn't get to see any other data.
537         try {
538             createEmployeeTable();
539             assertEquals(1, qb.delete(mDatabase, "1=1", null));
540         } catch (Exception tolerated) {
541         }
542         try {
543             createEmployeeTable();
544             assertEquals(1, qb.delete(mDatabase, "1=1 --", null));
545         } catch (Exception tolerated) {
546         }
547         try {
548             createEmployeeTable();
549             assertEquals(1, qb.delete(mDatabase, "1=1) OR (1=1", null));
550         } catch (Exception tolerated) {
551         }
552         try {
553             createEmployeeTable();
554             assertEquals(1, qb.delete(mDatabase, "1=1)) OR ((1=1", null));
555         } catch (Exception tolerated) {
556         }
557     }
558 
559     private static final String[] COLUMNS_VALID = new String[] {
560             "_id",
561     };
562 
563     private static final String[] COLUMNS_INVALID = new String[] {
564             "salary",
565             "MAX(salary)",
566             "undefined",
567             "(secret_column IN secret_table)",
568             "(SELECT secret_column FROM secret_table)",
569     };
570 
571     @Test
testStrictQueryProjection()572     public void testStrictQueryProjection() throws Exception {
573         for (String column : COLUMNS_VALID) {
574             assertStrictQueryValid(
575                     new String[] { column }, null, null, null, null, null, null);
576         }
577         for (String column : COLUMNS_INVALID) {
578             assertStrictQueryInvalid(
579                     new String[] { column }, null, null, null, null, null, null);
580         }
581     }
582 
583     @Test
testStrictQueryWhere()584     public void testStrictQueryWhere() throws Exception {
585         for (String column : COLUMNS_VALID) {
586             assertStrictQueryValid(
587                     null, column + ">0", null, null, null, null, null);
588             assertStrictQueryValid(
589                     null, "_id>" + column, null, null, null, null, null);
590         }
591         for (String column : COLUMNS_INVALID) {
592             assertStrictQueryInvalid(
593                     null, column + ">0", null, null, null, null, null);
594             assertStrictQueryInvalid(
595                     null, "_id>" + column, null, null, null, null, null);
596         }
597     }
598 
599     @Test
testStrictQueryGroupBy()600     public void testStrictQueryGroupBy() {
601         for (String column : COLUMNS_VALID) {
602             assertStrictQueryValid(
603                     null, null, null, column, null, null, null);
604             assertStrictQueryValid(
605                     null, null, null, "_id," + column, null, null, null);
606         }
607         for (String column : COLUMNS_INVALID) {
608             assertStrictQueryInvalid(
609                     null, null, null, column, null, null, null);
610             assertStrictQueryInvalid(
611                     null, null, null, "_id," + column, null, null, null);
612         }
613     }
614 
615     @Test
testStrictQueryHaving()616     public void testStrictQueryHaving() {
617         for (String column : COLUMNS_VALID) {
618             assertStrictQueryValid(
619                     null, null, null, "_id", column, null, null);
620         }
621         for (String column : COLUMNS_INVALID) {
622             assertStrictQueryInvalid(
623                     null, null, null, "_id", column, null, null);
624         }
625     }
626 
627     @Test
testStrictQueryOrderBy()628     public void testStrictQueryOrderBy() {
629         for (String column : COLUMNS_VALID) {
630             assertStrictQueryValid(
631                     null, null, null, null, null, column, null);
632             assertStrictQueryValid(
633                     null, null, null, null, null, column + " ASC", null);
634             assertStrictQueryValid(
635                     null, null, null, null, null, "_id COLLATE NOCASE ASC," + column, null);
636         }
637         for (String column : COLUMNS_INVALID) {
638             assertStrictQueryInvalid(
639                     null, null, null, null, null, column, null);
640             assertStrictQueryInvalid(
641                     null, null, null, null, null, column + " ASC", null);
642             assertStrictQueryInvalid(
643                     null, null, null, null, null, "_id COLLATE NOCASE ASC," + column, null);
644         }
645     }
646 
647     @Test
testStrictQueryLimit()648     public void testStrictQueryLimit() {
649         assertStrictQueryValid(
650                 null, null, null, null, null, null, "32");
651         assertStrictQueryValid(
652                 null, null, null, null, null, null, "0,32");
653         assertStrictQueryValid(
654                 null, null, null, null, null, null, "32 OFFSET 0");
655 
656         for (String column : COLUMNS_VALID) {
657             assertStrictQueryInvalid(
658                     null, null, null, null, null, null, column);
659         }
660         for (String column : COLUMNS_INVALID) {
661             assertStrictQueryInvalid(
662                     null, null, null, null, null, null, column);
663         }
664     }
665 
666     @Test
testStrictInsertValues()667     public void testStrictInsertValues() throws Exception {
668         final ContentValues values = new ContentValues();
669         for (String column : COLUMNS_VALID) {
670             values.clear();
671             values.put(column, 42);
672             assertStrictInsertValid(values);
673         }
674         for (String column : COLUMNS_INVALID) {
675             values.clear();
676             values.put(column, 42);
677             assertStrictInsertInvalid(values);
678         }
679     }
680 
681     @Test
testStrictUpdateValues()682     public void testStrictUpdateValues() throws Exception {
683         final ContentValues values = new ContentValues();
684         for (String column : COLUMNS_VALID) {
685             values.clear();
686             values.put(column, 42);
687             assertStrictUpdateValid(values, null, null);
688         }
689         for (String column : COLUMNS_INVALID) {
690             values.clear();
691             values.put(column, 42);
692             assertStrictUpdateInvalid(values, null, null);
693         }
694     }
695 
assertEnforceStrictGrammarRelaxedByTargetSdk(String selection)696     private static void assertEnforceStrictGrammarRelaxedByTargetSdk(String selection) {
697         final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
698         final HashMap<String, String> map = new HashMap<>();
699         map.put("_data", "_data");
700         map.put("date_added", "date_added");
701         map.put("date_modified", "date_modified");
702         map.put("media_type", "media_type");
703         builder.setProjectionMap(map);
704         {
705             builder.setTargetSdkVersion(Build.VERSION_CODES.Q);
706             builder.enforceStrictGrammar(selection, null, null, null, null);
707         }
708         try {
709             builder.setTargetSdkVersion(Build.VERSION_CODES.R);
710             builder.enforceStrictGrammar(selection, null, null, null, null);
711             fail("Expected to throw");
712         } catch (IllegalArgumentException expected) {
713         }
714     }
715 
716     @Test
testStrict_154193772()717     public void testStrict_154193772() {
718         final String selection = "(LOWER(_data) LIKE \"%.wmv\" OR LOWER(_data) LIKE \"%.wm\" OR LOWER(_data) LIKE \"%.wtv\" OR LOWER(_data) LIKE \"%.asf\" OR LOWER(_data) LIKE \"%.hls\" OR LOWER(_data) LIKE \"%.mp4\" OR LOWER(_data) LIKE \"%.m4v\" OR LOWER(_data) LIKE \"%.mov\" OR LOWER(_data) LIKE \"%.mp4v\" OR LOWER(_data) LIKE \"%.3g2\" OR LOWER(_data) LIKE \"%.3gp\" OR LOWER(_data) LIKE \"%.3gp2\" OR LOWER(_data) LIKE \"%.3gpp\" OR LOWER(_data) LIKE \"%.mj2\" OR LOWER(_data) LIKE \"%.qt\" OR LOWER(_data) LIKE \"%.external\" OR LOWER(_data) LIKE \"%.mov\" OR LOWER(_data) LIKE \"%.asf\" OR LOWER(_data) LIKE \"%.avi\" OR LOWER(_data) LIKE \"%.divx\" OR LOWER(_data) LIKE \"%.mpg\" OR LOWER(_data) LIKE \"%.mpeg\" OR LOWER(_data) LIKE \"%.mkv\" OR LOWER(_data) LIKE \"%.webm\" OR LOWER(_data) LIKE \"%.mk3d\" OR LOWER(_data) LIKE \"%.mks\" OR LOWER(_data) LIKE \"%.3gp\" OR LOWER(_data) LIKE \"%.mpegts\" OR LOWER(_data) LIKE \"%.ts\" OR LOWER(_data) LIKE \"%.m2ts\" OR LOWER(_data) LIKE \"%.m2t\") AND (date_added >= ? OR date_modified >=?)";
719         assertEnforceStrictGrammarRelaxedByTargetSdk(selection);
720     }
721 
722     @Test
testStrict_156554363()723     public void testStrict_156554363() {
724         final String selection = "date_added>? AND media_type=0 AND (_data LIKE \"%.mov\" OR _data LIKE \"%.MOV\"";
725         assertEnforceStrictGrammarRelaxedByTargetSdk(selection);
726     }
727 
728     @Test
testStrict_156832140()729     public void testStrict_156832140() {
730         final String selection = "_data LIKE \"%com.gopro.smarty%\"";
731         assertEnforceStrictGrammarRelaxedByTargetSdk(selection);
732     }
733 
734     @Test
testStrict_156136746()735     public void testStrict_156136746() {
736         // Verify that both keywords column names are allowed to be
737         // case-insensitive, per the SQLite specification
738         assertStrictQueryValid(new String[] { "Name", "Max(Month)" },
739                 "IfNull(Month,-1) Between 1100 And 1900", null,
740                 "Month", "Month In (1,2)", null, null);
741     }
742 
743     @Test
testStrict_158537159()744     public void testStrict_158537159() {
745         final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
746         final HashMap<String, String> map = new HashMap<>();
747         map.put("bucket_id", "bucket_id");
748         builder.setProjectionMap(map);
749         final String sortOrder = "bucket_id COLLATE LOCALIZED ASC";
750         {
751             builder.setTargetSdkVersion(Build.VERSION_CODES.Q);
752             builder.enforceStrictGrammar(null, null, null, sortOrder, null);
753         }
754         try {
755             builder.setTargetSdkVersion(Build.VERSION_CODES.R);
756             builder.enforceStrictGrammar(null, null, null, sortOrder, null);
757             fail("Expected to throw");
758         } catch (IllegalArgumentException expected) {
759         }
760     }
761 
762     @Test
testStrictCustomCollator()763     public void testStrictCustomCollator() {
764         final SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
765         final HashMap<String, String> map = new HashMap<>();
766         map.put("bucket_id", "bucket_id");
767         builder.setProjectionMap(map);
768 
769         final String sortOrder = "bucket_id COLLATE custom_zh ASC";
770         builder.enforceStrictGrammar(null, null, null, sortOrder, null);
771     }
772 
assertStrictInsertValid(ContentValues values)773     private void assertStrictInsertValid(ContentValues values) {
774         mStrictBuilder.insert(mDatabase, values);
775     }
776 
assertStrictInsertInvalid(ContentValues values)777     private void assertStrictInsertInvalid(ContentValues values) {
778         try {
779             mStrictBuilder.insert(mDatabase, values);
780             fail(Arrays.asList(values).toString());
781         } catch (Exception expected) {
782         }
783     }
784 
assertStrictUpdateValid(ContentValues values, String selection, String[] selectionArgs)785     private void assertStrictUpdateValid(ContentValues values, String selection,
786             String[] selectionArgs) {
787         mStrictBuilder.update(mDatabase, values, selection, selectionArgs);
788     }
789 
assertStrictUpdateInvalid(ContentValues values, String selection, String[] selectionArgs)790     private void assertStrictUpdateInvalid(ContentValues values, String selection,
791             String[] selectionArgs) {
792         try {
793             mStrictBuilder.update(mDatabase, values, selection, selectionArgs);
794             fail(Arrays.asList(values, selection, selectionArgs).toString());
795         } catch (Exception expected) {
796         }
797     }
798 
assertStrictQueryValid(String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit)799     private void assertStrictQueryValid(String[] projectionIn, String selection,
800             String[] selectionArgs, String groupBy, String having, String sortOrder, String limit) {
801         try (Cursor c = mStrictBuilder.query(mDatabase, projectionIn, selection, selectionArgs,
802                 groupBy, having, sortOrder, limit, null)) {
803         }
804     }
805 
assertStrictQueryInvalid(String[] projectionIn, String selection, String[] selectionArgs, String groupBy, String having, String sortOrder, String limit)806     private void assertStrictQueryInvalid(String[] projectionIn, String selection,
807             String[] selectionArgs, String groupBy, String having, String sortOrder, String limit) {
808         try (Cursor c = mStrictBuilder.query(mDatabase, projectionIn, selection, selectionArgs,
809                 groupBy, having, sortOrder, limit, null)) {
810             fail(Arrays.asList(projectionIn, selection, selectionArgs,
811                     groupBy, having, sortOrder, limit).toString());
812         } catch (Exception expected) {
813         }
814     }
815 
createEmployeeTable()816     private void createEmployeeTable() {
817         mDatabase.execSQL("DROP TABLE IF EXISTS employee;");
818         mDatabase.execSQL("CREATE TABLE employee (_id INTEGER PRIMARY KEY, " +
819                 "name TEXT, month INTEGER, salary INTEGER);");
820         mDatabase.execSQL("INSERT INTO employee (name, month, salary) " +
821                 "VALUES ('Mike', '1', '1000');");
822         mDatabase.execSQL("INSERT INTO employee (name, month, salary) " +
823                 "VALUES ('Mike', '2', '3000');");
824         mDatabase.execSQL("INSERT INTO employee (name, month, salary) " +
825                 "VALUES ('jack', '1', '2000');");
826         mDatabase.execSQL("INSERT INTO employee (name, month, salary) " +
827                 "VALUES ('jack', '3', '1500');");
828         mDatabase.execSQL("INSERT INTO employee (name, month, salary) " +
829                 "VALUES ('Jim', '1', '1000');");
830         mDatabase.execSQL("INSERT INTO employee (name, month, salary) " +
831                 "VALUES ('Jim', '3', '3500');");
832     }
833 
createStrictQueryBuilder()834     private void createStrictQueryBuilder() {
835         mStrictBuilder = new SQLiteQueryBuilder();
836         mStrictBuilder.setTables("employee");
837         mStrictBuilder.setStrict(true);
838         mStrictBuilder.setStrictColumns(true);
839         mStrictBuilder.setStrictGrammar(true);
840         mStrictBuilder.appendWhere("month=2");
841 
842         final Map<String, String> projectionMap = new HashMap<>();
843         projectionMap.put("_id", "_id");
844         projectionMap.put("name", "name");
845         projectionMap.put("month", "month");
846         mStrictBuilder.setProjectionMap(projectionMap);
847     }
848 }
849