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

Commit d27f3177 authored by Lee Shombert's avatar Lee Shombert
Browse files

Improve SQL statement type guesses

Bug: 277114870

This improves the function that guesses at the SQL statement type.  An
internal function gives finer grained guesses.  These guesses are not
part of the public API.

The new guesses:
 1. CREATE - a table create statement (maps to public DDL)
 2. COMMENT - a leading comment (maps to public OTHER)
 3. WITH - a leading with statement (e.g., CTE) (maps to public OTHER)

The finer grained guess is used in one place.  'WITH' statements are
cached in the prepared statement cache.  This is correct behavior
because a 'WITH' statement is always either a STATEMENT_SELECT or
STATEMENT_UPDATED as well, and these are both cached.

Unit tests for the new statement type guesser have been added.  There
will not be any CTS tests because the new function is internal.  Also,
a concurrency test has been added to verify that read-only statements
with comments are correctly assigned to non-primary database
connections and therefore can run in parallel with read/write
statements.

Test: atest
 * FrameworksCoreTests:android.database
 * CtsDatabaseTestCases

Change-Id: Icfd16a60c9ebf2291bd6a4a76d03495eb01e34e9
parent 4c0797b1
Loading
Loading
Loading
Loading
+87 −37
Original line number Diff line number Diff line
@@ -77,6 +77,16 @@ public class DatabaseUtils {
    /** One of the values returned by {@link #getSqlStatementType(String)}. */
    public static final int STATEMENT_OTHER = 99;

    // The following statement types are "extended" and are for internal use only.  These types
    // are not public and are never returned by {@link #getSqlStatementType(String)}.

    /** An internal statement type @hide **/
    public static final int STATEMENT_WITH = 100;
    /** An internal statement type @hide **/
    public static final int STATEMENT_CREATE = 101;
    /** An internal statement type denoting a comment. @hide **/
    public static final int STATEMENT_COMMENT = 102;

    /**
     * Special function for writing an exception result at the header of
     * a parcel, to be used when returning an exception from a transaction.
@@ -1563,6 +1573,79 @@ public class DatabaseUtils {
        db.close();
    }

    /**
     * The legacy prefix matcher.
     */
    private static String getSqlStatementPrefixSimple(@NonNull String sql) {
        sql = sql.trim();
        if (sql.length() < 3) {
            return null;
        }
        return sql.substring(0, 3).toUpperCase(Locale.ROOT);
    }

    /**
     * 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.
     * @hide
     */
    private static int categorizeStatement(@NonNull String prefix, @NonNull String sql) {
        if (prefix == null) return STATEMENT_OTHER;

        switch (prefix) {
            case "SEL": return STATEMENT_SELECT;
            case "INS":
            case "UPD":
            case "REP":
            case "DEL": return STATEMENT_UPDATE;
            case "ATT": return STATEMENT_ATTACH;
            case "COM":
            case "END": return STATEMENT_COMMIT;
            case "ROL":
                if (sql.toUpperCase(Locale.ROOT).contains(" TO ")) {
                    // Rollback to savepoint.
                    return STATEMENT_OTHER;
                }
                return STATEMENT_ABORT;
            case "BEG": return STATEMENT_BEGIN;
            case "PRA": return STATEMENT_PRAGMA;
            case "CRE": return STATEMENT_CREATE;
            case "DRO":
            case "ALT": return STATEMENT_DDL;
            case "ANA":
            case "DET": return STATEMENT_UNPREPARED;
            case "WIT": return STATEMENT_WITH;
            default:
                if (prefix.startsWith("--") || prefix.startsWith("/*")) {
                    return STATEMENT_COMMENT;
                }
                return STATEMENT_OTHER;
        }
    }

    /**
     * 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.
     * @hide
     */
    public static int getSqlStatementTypeExtended(@NonNull String sql) {
        int type = categorizeStatement(getSqlStatementPrefixSimple(sql), sql);
        return type;
    }

    /**
     * Convert an extended statement type to a public SQL statement type value.
     * @hide
     */
    public static int getSqlStatementType(int extended) {
        switch (extended) {
            case STATEMENT_CREATE: return STATEMENT_DDL;
            case STATEMENT_WITH: return STATEMENT_OTHER;
            case STATEMENT_COMMENT: return STATEMENT_OTHER;
        }
        return extended;
    }

    /**
     * Returns one of the following which represent the type of the given SQL statement.
     * <ol>
@@ -1572,49 +1655,16 @@ public class DatabaseUtils {
     *   <li>{@link #STATEMENT_BEGIN}</li>
     *   <li>{@link #STATEMENT_COMMIT}</li>
     *   <li>{@link #STATEMENT_ABORT}</li>
     *   <li>{@link #STATEMENT_PRAGMA}</li>
     *   <li>{@link #STATEMENT_DDL}</li>
     *   <li>{@link #STATEMENT_UNPREPARED}</li>
     *   <li>{@link #STATEMENT_OTHER}</li>
     * </ol>
     * @param sql the SQL statement whose type is returned by this method
     * @return one of the values listed above
     */
    public static int getSqlStatementType(String sql) {
        sql = sql.trim();
        if (sql.length() < 3) {
            return STATEMENT_OTHER;
        }
        String prefixSql = sql.substring(0, 3).toUpperCase(Locale.ROOT);
        if (prefixSql.equals("SEL")) {
            return STATEMENT_SELECT;
        } else if (prefixSql.equals("INS") ||
                prefixSql.equals("UPD") ||
                prefixSql.equals("REP") ||
                prefixSql.equals("DEL")) {
            return STATEMENT_UPDATE;
        } else if (prefixSql.equals("ATT")) {
            return STATEMENT_ATTACH;
        } else if (prefixSql.equals("COM")) {
            return STATEMENT_COMMIT;
        } else if (prefixSql.equals("END")) {
            return STATEMENT_COMMIT;
        } else if (prefixSql.equals("ROL")) {
            boolean isRollbackToSavepoint = sql.toUpperCase(Locale.ROOT).contains(" TO ");
            if (isRollbackToSavepoint) {
                Log.w(TAG, "Statement '" + sql
                        + "' may not work on API levels 16-27, use ';" + sql + "' instead");
                return STATEMENT_OTHER;
            }
            return STATEMENT_ABORT;
        } else if (prefixSql.equals("BEG")) {
            return STATEMENT_BEGIN;
        } else if (prefixSql.equals("PRA")) {
            return STATEMENT_PRAGMA;
        } else if (prefixSql.equals("CRE") || prefixSql.equals("DRO") ||
                prefixSql.equals("ALT")) {
            return STATEMENT_DDL;
        } else if (prefixSql.equals("ANA") || prefixSql.equals("DET")) {
            return STATEMENT_UNPREPARED;
        }
        return STATEMENT_OTHER;
        return getSqlStatementType(getSqlStatementTypeExtended(sql));
    }

    /**
+3 −2
Original line number Diff line number Diff line
@@ -1096,7 +1096,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen
        seqNum = mPreparedStatementCache.getLastSeqNum();
        try {
            final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr);
            final int type = DatabaseUtils.getSqlStatementType(sql);
            final int type = DatabaseUtils.getSqlStatementTypeExtended(sql);
            final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr);
            statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly,
                    seqNum);
@@ -1279,7 +1279,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen

    private static boolean isCacheable(int statementType) {
        if (statementType == DatabaseUtils.STATEMENT_UPDATE
                || statementType == DatabaseUtils.STATEMENT_SELECT) {
            || statementType == DatabaseUtils.STATEMENT_SELECT
            || statementType == DatabaseUtils.STATEMENT_WITH) {
            return true;
        }
        return false;
+44 −0
Original line number Diff line number Diff line
@@ -17,6 +17,15 @@
package android.database;

import static android.database.DatabaseUtils.bindSelection;
import static android.database.DatabaseUtils.getSqlStatementType;
import static android.database.DatabaseUtils.getSqlStatementTypeExtended;
import static android.database.DatabaseUtils.STATEMENT_COMMENT;
import static android.database.DatabaseUtils.STATEMENT_CREATE;
import static android.database.DatabaseUtils.STATEMENT_DDL;
import static android.database.DatabaseUtils.STATEMENT_OTHER;
import static android.database.DatabaseUtils.STATEMENT_SELECT;
import static android.database.DatabaseUtils.STATEMENT_UPDATE;
import static android.database.DatabaseUtils.STATEMENT_WITH;

import static org.junit.Assert.assertEquals;

@@ -63,4 +72,39 @@ public class DatabaseUtilsTest {
                bindSelection("foo=?10 AND bar=? AND meow=?1",
                        1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
    }

    @Test
    public void testStatementType() {
        final int sel = STATEMENT_SELECT;
        assertEquals(sel, getSqlStatementType("SELECT"));
        assertEquals(sel, getSqlStatementType("  SELECT"));
        assertEquals(sel, getSqlStatementType(" \n SELECT"));

        final int upd = STATEMENT_UPDATE;
        assertEquals(upd, getSqlStatementType("UPDATE"));
        assertEquals(upd, getSqlStatementType("  UPDATE"));
        assertEquals(upd, getSqlStatementType(" \n UPDATE"));

        final int ddl = STATEMENT_DDL;
        assertEquals(ddl, getSqlStatementType("ALTER TABLE t1 ADD COLUMN j int"));
        assertEquals(ddl, getSqlStatementType("CREATE TABLE t1 (i int)"));

        // Short statements, leading comments, and WITH are decoded to "other" in the public API.
        final int othr = STATEMENT_OTHER;
        assertEquals(othr, getSqlStatementType("SE"));
        assertEquals(othr, getSqlStatementType("SE LECT"));
        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"));

        final int cre = STATEMENT_CREATE;
        assertEquals(cre, getSqlStatementTypeExtended("CREATE TABLE t1 (i int)"));
    }
}