Loading core/java/android/database/DatabaseUtils.java +37 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,8 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Static utility methods for dealing with databases and {@link Cursor}s. Loading Loading @@ -1584,6 +1586,38 @@ public class DatabaseUtils { return sql.substring(0, 3).toUpperCase(Locale.ROOT); } /** * A regular expression that matches the first three characters in a SQL statement, after * skipping past comments and whitespace. PREFIX_GROUP_NUM is the regex group that contains * the matching prefix string. If PREFIX_REGEX is changed, PREFIX_GROUP_NUM may require an * update too. */ private static final String PREFIX_REGEX = "(" // Zero-or more... + "\\s+" // Leading space + "|" + "--.*?\n" // Line comment + "|" + "/\\*[\\w\\W]*?\\*/" // Block comment + ")*" + "(\\w\\w\\w)"; // Three word-characters private static final int PREFIX_GROUP_NUM = 2; private static final Pattern sPrefixPattern = Pattern.compile(PREFIX_REGEX); /** * Return the three-letter prefix of a SQL statement, skipping past whitespace and comments. * Comments either start with "--" and run to the end of the line or are C-style block * comments. The function returns null if a prefix could not be found. */ private static String getSqlStatementPrefixExtended(String sql) { Matcher m = sPrefixPattern.matcher(sql); if (m.lookingAt()) { return m.group(PREFIX_GROUP_NUM).toUpperCase(Locale.ROOT); } else { return null; } } /** * Return the extended statement type for the SQL statement. This is not a public API and it * can return values that are not publicly visible. Loading Loading @@ -1630,6 +1664,9 @@ public class DatabaseUtils { */ public static int getSqlStatementTypeExtended(@NonNull String sql) { int type = categorizeStatement(getSqlStatementPrefixSimple(sql), sql); if (type == STATEMENT_COMMENT) { type = categorizeStatement(getSqlStatementPrefixExtended(sql), sql); } return type; } Loading core/tests/coretests/src/android/database/DatabaseUtilsTest.java +22 −7 Original line number Diff line number Diff line Loading @@ -96,13 +96,28 @@ public class DatabaseUtilsTest { assertEquals(othr, getSqlStatementType("-- cmt\n SE")); assertEquals(othr, getSqlStatementType("WITH")); // Test the extended statement types. final int wit = STATEMENT_WITH; assertEquals(wit, getSqlStatementTypeExtended("WITH")); final int cmt = STATEMENT_COMMENT; assertEquals(cmt, getSqlStatementTypeExtended("-- cmt\n SELECT")); // Verify that leading line-comments are skipped. assertEquals(sel, getSqlStatementType("-- cmt\n SELECT")); assertEquals(sel, getSqlStatementType("-- line 1\n-- line 2\n SELECT")); assertEquals(sel, getSqlStatementType("-- line 1\nSELECT")); // Verify that embedded comments do not confuse the scanner. assertEquals(sel, getSqlStatementType("-- line 1\nSELECT\n-- line 3\n")); // Verify that leading block-comments are skipped. assertEquals(sel, getSqlStatementType("/* foo */SELECT")); assertEquals(sel, getSqlStatementType("/* line 1\n line 2\n*/\nSELECT")); assertEquals(sel, getSqlStatementType("/* UPDATE\nline 2*/\nSELECT")); // Verify that embedded comment characters do not confuse the scanner. assertEquals(sel, getSqlStatementType("/* Foo /* /* // ** */SELECT")); // Mix it up with comment types assertEquals(sel, getSqlStatementType("/* foo */ -- bar\n SELECT")); // Test the extended statement types. Note that the STATEMENT_COMMENT type is not possible, // since leading comments are skipped. final int with = STATEMENT_WITH; assertEquals(with, getSqlStatementTypeExtended("WITH")); final int cre = STATEMENT_CREATE; assertEquals(cre, getSqlStatementTypeExtended("CREATE TABLE t1 (i int)")); Loading core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java +116 −0 Original line number Diff line number Diff line Loading @@ -39,9 +39,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Phaser; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; Loading Loading @@ -231,4 +235,116 @@ public class SQLiteDatabaseTest { // This exception is expected. } } /** * Count the number of rows in the database <count> times. The answer must match <expected> * every time. Any errors are reported back to the main thread through the <errors> * array. The ticker forces the database reads to be interleaved with database operations from * the sibling threads. */ private void concurrentReadOnlyReader(SQLiteDatabase database, int count, long expected, List<Throwable> errors, Phaser ticker) { final String query = "--comment\nSELECT count(*) from t1"; try { for (int i = count; i > 0; i--) { ticker.arriveAndAwaitAdvance(); long r = DatabaseUtils.longForQuery(database, query, null); if (r != expected) { // The type of the exception is not important. Only the message matters. throw new RuntimeException( String.format("concurrentRead expected %d, got %d", expected, r)); } } } catch (Throwable t) { errors.add(t); } finally { ticker.arriveAndDeregister(); } } /** * Insert a new row <count> times. Any errors are reported back to the main thread through * the <errors> array. The ticker forces the database reads to be interleaved with database * operations from the sibling threads. */ private void concurrentImmediateWriter(SQLiteDatabase database, int count, List<Throwable> errors, Phaser ticker) { database.beginTransaction(); try { int n = 100; for (int i = count; i > 0; i--) { ticker.arriveAndAwaitAdvance(); database.execSQL(String.format("INSERT INTO t1 (i) VALUES (%d)", n++)); } database.setTransactionSuccessful(); } catch (Throwable t) { errors.add(t); } finally { database.endTransaction(); ticker.arriveAndDeregister(); } } /** * This test verifies that a read-only transaction can be started, and it is deferred. A * deferred transaction does not take a database locks until the database is accessed. This * test verifies that the implicit connection selection process correctly identifies * read-only transactions even when they are preceded by a comment. */ @Test public void testReadOnlyTransaction() throws Exception { // Enable WAL for concurrent read and write transactions. mDatabase.enableWriteAheadLogging(); // Create the t1 table and put some data in it. mDatabase.beginTransaction(); try { mDatabase.execSQL("CREATE TABLE t1 (i int);"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (2)"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (3)"); mDatabase.setTransactionSuccessful(); } finally { mDatabase.endTransaction(); } // Threads install errors in this array. final List<Throwable> errors = Collections.synchronizedList(new ArrayList<Throwable>()); // This forces the read and write threads to execute in a lock-step, round-robin fashion. Phaser ticker = new Phaser(3); // Create three threads that will perform transactions. One thread is a writer and two // are readers. The intent is that the readers begin before the writer commits, so the // readers always see a database with two rows. Thread readerA = new Thread(() -> { concurrentReadOnlyReader(mDatabase, 4, 2, errors, ticker); }); Thread readerB = new Thread(() -> { concurrentReadOnlyReader(mDatabase, 4, 2, errors, ticker); }); Thread writerC = new Thread(() -> { concurrentImmediateWriter(mDatabase, 4, errors, ticker); }); readerA.start(); readerB.start(); writerC.start(); // All three threads should have completed. Give the total set 1s. The 10ms delay for // the second and third threads is just a small, positive number. readerA.join(1000); assertFalse(readerA.isAlive()); readerB.join(10); assertFalse(readerB.isAlive()); writerC.join(10); assertFalse(writerC.isAlive()); // The writer added 4 rows to the database. long r = DatabaseUtils.longForQuery(mDatabase, "SELECT count(*) from t1", null); assertEquals(6, r); assertTrue("ReadThread failed with errors: " + errors, errors.isEmpty()); } } Loading
core/java/android/database/DatabaseUtils.java +37 −0 Original line number Diff line number Diff line Loading @@ -47,6 +47,8 @@ import java.util.Arrays; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Static utility methods for dealing with databases and {@link Cursor}s. Loading Loading @@ -1584,6 +1586,38 @@ public class DatabaseUtils { return sql.substring(0, 3).toUpperCase(Locale.ROOT); } /** * A regular expression that matches the first three characters in a SQL statement, after * skipping past comments and whitespace. PREFIX_GROUP_NUM is the regex group that contains * the matching prefix string. If PREFIX_REGEX is changed, PREFIX_GROUP_NUM may require an * update too. */ private static final String PREFIX_REGEX = "(" // Zero-or more... + "\\s+" // Leading space + "|" + "--.*?\n" // Line comment + "|" + "/\\*[\\w\\W]*?\\*/" // Block comment + ")*" + "(\\w\\w\\w)"; // Three word-characters private static final int PREFIX_GROUP_NUM = 2; private static final Pattern sPrefixPattern = Pattern.compile(PREFIX_REGEX); /** * Return the three-letter prefix of a SQL statement, skipping past whitespace and comments. * Comments either start with "--" and run to the end of the line or are C-style block * comments. The function returns null if a prefix could not be found. */ private static String getSqlStatementPrefixExtended(String sql) { Matcher m = sPrefixPattern.matcher(sql); if (m.lookingAt()) { return m.group(PREFIX_GROUP_NUM).toUpperCase(Locale.ROOT); } else { return null; } } /** * Return the extended statement type for the SQL statement. This is not a public API and it * can return values that are not publicly visible. Loading Loading @@ -1630,6 +1664,9 @@ public class DatabaseUtils { */ public static int getSqlStatementTypeExtended(@NonNull String sql) { int type = categorizeStatement(getSqlStatementPrefixSimple(sql), sql); if (type == STATEMENT_COMMENT) { type = categorizeStatement(getSqlStatementPrefixExtended(sql), sql); } return type; } Loading
core/tests/coretests/src/android/database/DatabaseUtilsTest.java +22 −7 Original line number Diff line number Diff line Loading @@ -96,13 +96,28 @@ public class DatabaseUtilsTest { assertEquals(othr, getSqlStatementType("-- cmt\n SE")); assertEquals(othr, getSqlStatementType("WITH")); // Test the extended statement types. final int wit = STATEMENT_WITH; assertEquals(wit, getSqlStatementTypeExtended("WITH")); final int cmt = STATEMENT_COMMENT; assertEquals(cmt, getSqlStatementTypeExtended("-- cmt\n SELECT")); // Verify that leading line-comments are skipped. assertEquals(sel, getSqlStatementType("-- cmt\n SELECT")); assertEquals(sel, getSqlStatementType("-- line 1\n-- line 2\n SELECT")); assertEquals(sel, getSqlStatementType("-- line 1\nSELECT")); // Verify that embedded comments do not confuse the scanner. assertEquals(sel, getSqlStatementType("-- line 1\nSELECT\n-- line 3\n")); // Verify that leading block-comments are skipped. assertEquals(sel, getSqlStatementType("/* foo */SELECT")); assertEquals(sel, getSqlStatementType("/* line 1\n line 2\n*/\nSELECT")); assertEquals(sel, getSqlStatementType("/* UPDATE\nline 2*/\nSELECT")); // Verify that embedded comment characters do not confuse the scanner. assertEquals(sel, getSqlStatementType("/* Foo /* /* // ** */SELECT")); // Mix it up with comment types assertEquals(sel, getSqlStatementType("/* foo */ -- bar\n SELECT")); // Test the extended statement types. Note that the STATEMENT_COMMENT type is not possible, // since leading comments are skipped. final int with = STATEMENT_WITH; assertEquals(with, getSqlStatementTypeExtended("WITH")); final int cre = STATEMENT_CREATE; assertEquals(cre, getSqlStatementTypeExtended("CREATE TABLE t1 (i int)")); Loading
core/tests/coretests/src/android/database/sqlite/SQLiteDatabaseTest.java +116 −0 Original line number Diff line number Diff line Loading @@ -39,9 +39,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Phaser; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; Loading Loading @@ -231,4 +235,116 @@ public class SQLiteDatabaseTest { // This exception is expected. } } /** * Count the number of rows in the database <count> times. The answer must match <expected> * every time. Any errors are reported back to the main thread through the <errors> * array. The ticker forces the database reads to be interleaved with database operations from * the sibling threads. */ private void concurrentReadOnlyReader(SQLiteDatabase database, int count, long expected, List<Throwable> errors, Phaser ticker) { final String query = "--comment\nSELECT count(*) from t1"; try { for (int i = count; i > 0; i--) { ticker.arriveAndAwaitAdvance(); long r = DatabaseUtils.longForQuery(database, query, null); if (r != expected) { // The type of the exception is not important. Only the message matters. throw new RuntimeException( String.format("concurrentRead expected %d, got %d", expected, r)); } } } catch (Throwable t) { errors.add(t); } finally { ticker.arriveAndDeregister(); } } /** * Insert a new row <count> times. Any errors are reported back to the main thread through * the <errors> array. The ticker forces the database reads to be interleaved with database * operations from the sibling threads. */ private void concurrentImmediateWriter(SQLiteDatabase database, int count, List<Throwable> errors, Phaser ticker) { database.beginTransaction(); try { int n = 100; for (int i = count; i > 0; i--) { ticker.arriveAndAwaitAdvance(); database.execSQL(String.format("INSERT INTO t1 (i) VALUES (%d)", n++)); } database.setTransactionSuccessful(); } catch (Throwable t) { errors.add(t); } finally { database.endTransaction(); ticker.arriveAndDeregister(); } } /** * This test verifies that a read-only transaction can be started, and it is deferred. A * deferred transaction does not take a database locks until the database is accessed. This * test verifies that the implicit connection selection process correctly identifies * read-only transactions even when they are preceded by a comment. */ @Test public void testReadOnlyTransaction() throws Exception { // Enable WAL for concurrent read and write transactions. mDatabase.enableWriteAheadLogging(); // Create the t1 table and put some data in it. mDatabase.beginTransaction(); try { mDatabase.execSQL("CREATE TABLE t1 (i int);"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (2)"); mDatabase.execSQL("INSERT INTO t1 (i) VALUES (3)"); mDatabase.setTransactionSuccessful(); } finally { mDatabase.endTransaction(); } // Threads install errors in this array. final List<Throwable> errors = Collections.synchronizedList(new ArrayList<Throwable>()); // This forces the read and write threads to execute in a lock-step, round-robin fashion. Phaser ticker = new Phaser(3); // Create three threads that will perform transactions. One thread is a writer and two // are readers. The intent is that the readers begin before the writer commits, so the // readers always see a database with two rows. Thread readerA = new Thread(() -> { concurrentReadOnlyReader(mDatabase, 4, 2, errors, ticker); }); Thread readerB = new Thread(() -> { concurrentReadOnlyReader(mDatabase, 4, 2, errors, ticker); }); Thread writerC = new Thread(() -> { concurrentImmediateWriter(mDatabase, 4, errors, ticker); }); readerA.start(); readerB.start(); writerC.start(); // All three threads should have completed. Give the total set 1s. The 10ms delay for // the second and third threads is just a small, positive number. readerA.join(1000); assertFalse(readerA.isAlive()); readerB.join(10); assertFalse(readerB.isAlive()); writerC.join(10); assertFalse(writerC.isAlive()); // The writer added 4 rows to the database. long r = DatabaseUtils.longForQuery(mDatabase, "SELECT count(*) from t1", null); assertEquals(6, r); assertTrue("ReadThread failed with errors: " + errors, errors.isEmpty()); } }