Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ccc2dd02 authored by Lee Shombert's avatar Lee Shombert Committed by Android (Google) Code Review
Browse files

Merge "Scan SQL comments when categorizing statements" into main

parents de9da704 bc44c6b0
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
@@ -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.
@@ -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.
@@ -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;
    }

+22 −7
Original line number Diff line number Diff line
@@ -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)"));
+116 −0
Original line number Diff line number Diff line
@@ -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;

@@ -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());
    }
}