summaryrefslogtreecommitdiffstats
path: root/core/java/android/database/sqlite/SQLiteDatabase.java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java/android/database/sqlite/SQLiteDatabase.java')
-rw-r--r--core/java/android/database/sqlite/SQLiteDatabase.java726
1 files changed, 605 insertions, 121 deletions
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 9ebf5d9..fb5507d 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -16,10 +16,14 @@
package android.database.sqlite;
+import com.google.android.collect.Maps;
+
+import android.app.ActivityThread;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.SQLException;
+import android.database.sqlite.SQLiteDebug.DbStats;
import android.os.Debug;
import android.os.SystemClock;
import android.os.SystemProperties;
@@ -27,15 +31,22 @@ import android.text.TextUtils;
import android.util.Config;
import android.util.EventLog;
import android.util.Log;
+import android.util.Pair;
import java.io.File;
+import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
+import java.util.Random;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Pattern;
/**
* Exposes methods to manage a SQLite database.
@@ -60,65 +71,61 @@ public class SQLiteDatabase extends SQLiteClosable {
/**
* Algorithms used in ON CONFLICT clause
* http://www.sqlite.org/lang_conflict.html
- * @hide
*/
- public enum ConflictAlgorithm {
- /**
- * When a constraint violation occurs, an immediate ROLLBACK occurs,
- * thus ending the current transaction, and the command aborts with a
- * return code of SQLITE_CONSTRAINT. If no transaction is active
- * (other than the implied transaction that is created on every command)
- * then this algorithm works the same as ABORT.
- */
- ROLLBACK("ROLLBACK"),
+ /**
+ * When a constraint violation occurs, an immediate ROLLBACK occurs,
+ * thus ending the current transaction, and the command aborts with a
+ * return code of SQLITE_CONSTRAINT. If no transaction is active
+ * (other than the implied transaction that is created on every command)
+ * then this algorithm works the same as ABORT.
+ */
+ public static final int CONFLICT_ROLLBACK = 1;
- /**
- * When a constraint violation occurs,no ROLLBACK is executed
- * so changes from prior commands within the same transaction
- * are preserved. This is the default behavior.
- */
- ABORT("ABORT"),
+ /**
+ * When a constraint violation occurs,no ROLLBACK is executed
+ * so changes from prior commands within the same transaction
+ * are preserved. This is the default behavior.
+ */
+ public static final int CONFLICT_ABORT = 2;
- /**
- * When a constraint violation occurs, the command aborts with a return
- * code SQLITE_CONSTRAINT. But any changes to the database that
- * the command made prior to encountering the constraint violation
- * are preserved and are not backed out.
- */
- FAIL("FAIL"),
+ /**
+ * When a constraint violation occurs, the command aborts with a return
+ * code SQLITE_CONSTRAINT. But any changes to the database that
+ * the command made prior to encountering the constraint violation
+ * are preserved and are not backed out.
+ */
+ public static final int CONFLICT_FAIL = 3;
- /**
- * When a constraint violation occurs, the one row that contains
- * the constraint violation is not inserted or changed.
- * But the command continues executing normally. Other rows before and
- * after the row that contained the constraint violation continue to be
- * inserted or updated normally. No error is returned.
- */
- IGNORE("IGNORE"),
+ /**
+ * When a constraint violation occurs, the one row that contains
+ * the constraint violation is not inserted or changed.
+ * But the command continues executing normally. Other rows before and
+ * after the row that contained the constraint violation continue to be
+ * inserted or updated normally. No error is returned.
+ */
+ public static final int CONFLICT_IGNORE = 4;
- /**
- * When a UNIQUE constraint violation occurs, the pre-existing rows that
- * are causing the constraint violation are removed prior to inserting
- * or updating the current row. Thus the insert or update always occurs.
- * The command continues executing normally. No error is returned.
- * If a NOT NULL constraint violation occurs, the NULL value is replaced
- * by the default value for that column. If the column has no default
- * value, then the ABORT algorithm is used. If a CHECK constraint
- * violation occurs then the IGNORE algorithm is used. When this conflict
- * resolution strategy deletes rows in order to satisfy a constraint,
- * it does not invoke delete triggers on those rows.
- * This behavior might change in a future release.
- */
- REPLACE("REPLACE");
+ /**
+ * When a UNIQUE constraint violation occurs, the pre-existing rows that
+ * are causing the constraint violation are removed prior to inserting
+ * or updating the current row. Thus the insert or update always occurs.
+ * The command continues executing normally. No error is returned.
+ * If a NOT NULL constraint violation occurs, the NULL value is replaced
+ * by the default value for that column. If the column has no default
+ * value, then the ABORT algorithm is used. If a CHECK constraint
+ * violation occurs then the IGNORE algorithm is used. When this conflict
+ * resolution strategy deletes rows in order to satisfy a constraint,
+ * it does not invoke delete triggers on those rows.
+ * This behavior might change in a future release.
+ */
+ public static final int CONFLICT_REPLACE = 5;
- private final String mValue;
- ConflictAlgorithm(String value) {
- mValue = value;
- }
- public String value() {
- return mValue;
- }
- }
+ /**
+ * use the following when no conflict action is specified.
+ */
+ public static final int CONFLICT_NONE = 0;
+ private static final String[] CONFLICT_VALUES = new String[]
+ {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "};
/**
* Maximum Length Of A LIKE Or GLOB Pattern
@@ -198,8 +205,31 @@ public class SQLiteDatabase extends SQLiteClosable {
private static final int SLEEP_AFTER_YIELD_QUANTUM = 1000;
+ // The pattern we remove from database filenames before
+ // potentially logging them.
+ private static final Pattern EMAIL_IN_DB_PATTERN = Pattern.compile("[\\w\\.\\-]+@[\\w\\.\\-]+");
+
private long mLastLockMessageTime = 0L;
+ // Things related to query logging/sampling for debugging
+ // slow/frequent queries during development. Always log queries
+ // which take (by default) 500ms+; shorter queries are sampled
+ // accordingly. Commit statements, which are typically slow, are
+ // logged together with the most recently executed SQL statement,
+ // for disambiguation. The 500ms value is configurable via a
+ // SystemProperty, but developers actively debugging database I/O
+ // should probably use the regular log tunable,
+ // LOG_SLOW_QUERIES_PROPERTY, defined below.
+ private static int sQueryLogTimeInMillis = 0; // lazily initialized
+ private static final int QUERY_LOG_SQL_LENGTH = 64;
+ private static final String COMMIT_SQL = "COMMIT;";
+ private final Random mRandom = new Random();
+ private String mLastSqlStatement = null;
+
+ // String prefix for slow database query EventLog records that show
+ // lock acquistions of the database.
+ /* package */ static final String GET_LOCK_LOG_PREFIX = "GETLOCK:";
+
/** Used by native code, do not rename */
/* package */ int mNativeHandle = 0;
@@ -209,6 +239,9 @@ public class SQLiteDatabase extends SQLiteClosable {
/** The path for the database file */
private String mPath;
+ /** The anonymized path for the database file for logging purposes */
+ private String mPathForLogs = null; // lazily populated
+
/** The flags passed to open/create */
private int mFlags;
@@ -217,11 +250,41 @@ public class SQLiteDatabase extends SQLiteClosable {
private WeakHashMap<SQLiteClosable, Object> mPrograms;
- private final RuntimeException mLeakedException;
+ /**
+ * for each instance of this class, a cache is maintained to store
+ * the compiled query statement ids returned by sqlite database.
+ * key = sql statement with "?" for bind args
+ * value = {@link SQLiteCompiledSql}
+ * If an application opens the database and keeps it open during its entire life, then
+ * there will not be an overhead of compilation of sql statements by sqlite.
+ *
+ * why is this cache NOT static? because sqlite attaches compiledsql statements to the
+ * struct created when {@link SQLiteDatabase#openDatabase(String, CursorFactory, int)} is
+ * invoked.
+ *
+ * this cache has an upper limit of mMaxSqlCacheSize (settable by calling the method
+ * (@link setMaxCacheSize(int)}). its default is 0 - i.e., no caching by default because
+ * most of the apps don't use "?" syntax in their sql, caching is not useful for them.
+ */
+ /* package */ Map<String, SQLiteCompiledSql> mCompiledQueries = Maps.newHashMap();
+ /**
+ * @hide
+ */
+ public static final int MAX_SQL_CACHE_SIZE = 250;
+ private int mMaxSqlCacheSize = MAX_SQL_CACHE_SIZE; // max cache size per Database instance
+ private int mCacheFullWarnings;
+ private static final int MAX_WARNINGS_ON_CACHESIZE_CONDITION = 1;
+
+ /** maintain stats about number of cache hits and misses */
+ private int mNumCacheHits;
+ private int mNumCacheMisses;
- // package visible, since callers will access directly to minimize overhead in the case
- // that logging is not enabled.
- /* package */ final boolean mLogStats;
+ /** the following 2 members maintain the time when a database is opened and closed */
+ private String mTimeOpened = null;
+ private String mTimeClosed = null;
+
+ /** Used to find out where this object was created in case it never got closed. */
+ private Throwable mStackTrace = null;
// System property that enables logging of slow queries. Specify the threshold in ms.
private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold";
@@ -251,6 +314,9 @@ public class SQLiteDatabase extends SQLiteClosable {
@Override
protected void onAllReferencesReleased() {
if (isOpen()) {
+ if (SQLiteDebug.DEBUG_SQL_CACHE) {
+ mTimeClosed = getTime();
+ }
dbclose();
}
}
@@ -281,15 +347,18 @@ public class SQLiteDatabase extends SQLiteClosable {
private boolean mLockingEnabled = true;
/* package */ void onCorruption() {
+ Log.e(TAG, "Removing corrupt database: " + mPath);
+ EventLog.writeEvent(EVENT_DB_CORRUPT, mPath);
try {
// Close the database (if we can), which will cause subsequent operations to fail.
close();
} finally {
- Log.e(TAG, "Removing corrupt database: " + mPath);
- EventLog.writeEvent(EVENT_DB_CORRUPT, mPath);
// Delete the corrupt file. Don't re-create it now -- that would just confuse people
// -- but the next time someone tries to open it, they can set it up from scratch.
- new File(mPath).delete();
+ if (!mPath.equalsIgnoreCase(":memory")) {
+ // delete is only for non-memory database files
+ new File(mPath).delete();
+ }
}
}
@@ -432,6 +501,9 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) {
lockForced();
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
boolean ok = false;
try {
// If this thread already had the lock then get out
@@ -476,6 +548,9 @@ public class SQLiteDatabase extends SQLiteClosable {
* are committed and rolled back.
*/
public void endTransaction() {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
if (!mLock.isHeldByCurrentThread()) {
throw new IllegalStateException("no transaction pending");
}
@@ -502,7 +577,7 @@ public class SQLiteDatabase extends SQLiteClosable {
}
}
if (mTransactionIsSuccessful) {
- execSQL("COMMIT;");
+ execSQL(COMMIT_SQL);
} else {
try {
execSQL("ROLLBACK;");
@@ -536,6 +611,9 @@ public class SQLiteDatabase extends SQLiteClosable {
* transaction is already marked as successful.
*/
public void setTransactionSuccessful() {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
if (!mLock.isHeldByCurrentThread()) {
throw new IllegalStateException("no transaction pending");
}
@@ -733,18 +811,30 @@ public class SQLiteDatabase extends SQLiteClosable {
* @throws SQLiteException if the database cannot be opened
*/
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) {
- SQLiteDatabase db = null;
+ SQLiteDatabase sqliteDatabase = null;
try {
// Open the database.
- return new SQLiteDatabase(path, factory, flags);
+ sqliteDatabase = new SQLiteDatabase(path, factory, flags);
+ if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
+ sqliteDatabase.enableSqlTracing(path);
+ }
+ if (SQLiteDebug.DEBUG_SQL_TIME) {
+ sqliteDatabase.enableSqlProfiling(path);
+ }
} catch (SQLiteDatabaseCorruptException e) {
// Try to recover from this, if we can.
// TODO: should we do this for other open failures?
Log.e(TAG, "Deleting and re-creating corrupt database " + path, e);
EventLog.writeEvent(EVENT_DB_CORRUPT, path);
- new File(path).delete();
- return new SQLiteDatabase(path, factory, flags);
+ if (!path.equalsIgnoreCase(":memory")) {
+ // delete is only for non-memory database files
+ new File(path).delete();
+ }
+ sqliteDatabase = new SQLiteDatabase(path, factory, flags);
}
+ ActiveDatabases.getInstance().mActiveDatabases.add(
+ new WeakReference<SQLiteDatabase>(sqliteDatabase));
+ return sqliteDatabase;
}
/**
@@ -781,16 +871,29 @@ public class SQLiteDatabase extends SQLiteClosable {
* Close the database.
*/
public void close() {
+ if (!isOpen()) {
+ return; // already closed
+ }
lock();
try {
closeClosable();
- releaseReference();
+ // close this database instance - regardless of its reference count value
+ onAllReferencesReleased();
} finally {
unlock();
}
}
private void closeClosable() {
+ /* deallocate all compiled sql statement objects from mCompiledQueries cache.
+ * this should be done before de-referencing all {@link SQLiteClosable} objects
+ * from this database object because calling
+ * {@link SQLiteClosable#onAllReferencesReleasedFromContainer()} could cause the database
+ * to be closed. sqlite doesn't let a database close if there are
+ * any unfinalized statements - such as the compiled-sql objects in mCompiledQueries.
+ */
+ deallocCachedSqlStatements();
+
Iterator<Map.Entry<SQLiteClosable, Object>> iter = mPrograms.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry<SQLiteClosable, Object> entry = iter.next();
@@ -814,6 +917,9 @@ public class SQLiteDatabase extends SQLiteClosable {
public int getVersion() {
SQLiteStatement prog = null;
lock();
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
try {
prog = new SQLiteStatement(this, "PRAGMA user_version;");
long version = prog.simpleQueryForLong();
@@ -841,6 +947,9 @@ public class SQLiteDatabase extends SQLiteClosable {
public long getMaximumSize() {
SQLiteStatement prog = null;
lock();
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
try {
prog = new SQLiteStatement(this,
"PRAGMA max_page_count;");
@@ -862,6 +971,9 @@ public class SQLiteDatabase extends SQLiteClosable {
public long setMaximumSize(long numBytes) {
SQLiteStatement prog = null;
lock();
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
try {
long pageSize = getPageSize();
long numPages = numBytes / pageSize;
@@ -887,6 +999,9 @@ public class SQLiteDatabase extends SQLiteClosable {
public long getPageSize() {
SQLiteStatement prog = null;
lock();
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
try {
prog = new SQLiteStatement(this,
"PRAGMA page_size;");
@@ -1023,6 +1138,9 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
public SQLiteStatement compileStatement(String sql) throws SQLException {
lock();
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
try {
return new SQLiteStatement(this, sql);
} finally {
@@ -1102,6 +1220,9 @@ public class SQLiteDatabase extends SQLiteClosable {
boolean distinct, String table, String[] columns,
String selection, String[] selectionArgs, String groupBy,
String having, String orderBy, String limit) {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
String sql = SQLiteQueryBuilder.buildQueryString(
distinct, table, columns, selection, groupBy, having, orderBy, limit);
@@ -1208,6 +1329,9 @@ public class SQLiteDatabase extends SQLiteClosable {
public Cursor rawQueryWithFactory(
CursorFactory cursorFactory, String sql, String[] selectionArgs,
String editTable) {
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
long timeStart = 0;
if (Config.LOGV || mSlowQueryThreshold != -1) {
@@ -1225,9 +1349,9 @@ public class SQLiteDatabase extends SQLiteClosable {
if (Config.LOGV || mSlowQueryThreshold != -1) {
// Force query execution
+ int count = -1;
if (cursor != null) {
- cursor.moveToFirst();
- cursor.moveToPosition(-1);
+ count = cursor.getCount();
}
long duration = System.currentTimeMillis() - timeStart;
@@ -1237,7 +1361,7 @@ public class SQLiteDatabase extends SQLiteClosable {
"query (" + duration + " ms): " + driver.toString() + ", args are "
+ (selectionArgs != null
? TextUtils.join(",", selectionArgs)
- : "<null>"));
+ : "<null>") + ", count is " + count);
}
}
}
@@ -1283,7 +1407,7 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
public long insert(String table, String nullColumnHack, ContentValues values) {
try {
- return insertWithOnConflict(table, nullColumnHack, values, null);
+ return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
} catch (SQLException e) {
Log.e(TAG, "Error inserting " + values, e);
return -1;
@@ -1305,7 +1429,7 @@ public class SQLiteDatabase extends SQLiteClosable {
*/
public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
throws SQLException {
- return insertWithOnConflict(table, nullColumnHack, values, null);
+ return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
}
/**
@@ -1322,7 +1446,7 @@ public class SQLiteDatabase extends SQLiteClosable {
public long replace(String table, String nullColumnHack, ContentValues initialValues) {
try {
return insertWithOnConflict(table, nullColumnHack, initialValues,
- ConflictAlgorithm.REPLACE);
+ CONFLICT_REPLACE);
} catch (SQLException e) {
Log.e(TAG, "Error inserting " + initialValues, e);
return -1;
@@ -1344,7 +1468,7 @@ public class SQLiteDatabase extends SQLiteClosable {
public long replaceOrThrow(String table, String nullColumnHack,
ContentValues initialValues) throws SQLException {
return insertWithOnConflict(table, nullColumnHack, initialValues,
- ConflictAlgorithm.REPLACE);
+ CONFLICT_REPLACE);
}
/**
@@ -1357,12 +1481,14 @@ public class SQLiteDatabase extends SQLiteClosable {
* @param initialValues this map contains the initial column values for the
* row. The keys should be the column names and the values the
* column values
- * @param algorithm {@link ConflictAlgorithm} for insert conflict resolver
- * @return the row ID of the newly inserted row, or -1 if an error occurred
- * @hide
+ * @param conflictAlgorithm for insert conflict resolver
+ * @return the row ID of the newly inserted row
+ * OR the primary key of the existing row if the input param 'conflictAlgorithm' =
+ * {@link #CONFLICT_IGNORE}
+ * OR -1 if any error
*/
public long insertWithOnConflict(String table, String nullColumnHack,
- ContentValues initialValues, ConflictAlgorithm algorithm) {
+ ContentValues initialValues, int conflictAlgorithm) {
if (!isOpen()) {
throw new IllegalStateException("database not open");
}
@@ -1370,10 +1496,7 @@ public class SQLiteDatabase extends SQLiteClosable {
// Measurements show most sql lengths <= 152
StringBuilder sql = new StringBuilder(152);
sql.append("INSERT");
- if (algorithm != null) {
- sql.append(" OR ");
- sql.append(algorithm.value());
- }
+ sql.append(CONFLICT_VALUES[conflictAlgorithm]);
sql.append(" INTO ");
sql.append(table);
// Measurements show most values lengths < 40
@@ -1457,10 +1580,10 @@ public class SQLiteDatabase extends SQLiteClosable {
* whereClause.
*/
public int delete(String table, String whereClause, String[] whereArgs) {
+ lock();
if (!isOpen()) {
throw new IllegalStateException("database not open");
}
- lock();
SQLiteStatement statement = null;
try {
statement = compileStatement("DELETE FROM " + table
@@ -1473,7 +1596,6 @@ public class SQLiteDatabase extends SQLiteClosable {
}
}
statement.execute();
- statement.close();
return lastChangeCount();
} catch (SQLiteDatabaseCorruptException e) {
onCorruption();
@@ -1497,7 +1619,7 @@ public class SQLiteDatabase extends SQLiteClosable {
* @return the number of rows affected
*/
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
- return updateWithOnConflict(table, values, whereClause, whereArgs, null);
+ return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE);
}
/**
@@ -1508,28 +1630,18 @@ public class SQLiteDatabase extends SQLiteClosable {
* valid value that will be translated to NULL.
* @param whereClause the optional WHERE clause to apply when updating.
* Passing null will update all rows.
- * @param algorithm {@link ConflictAlgorithm} for update conflict resolver
+ * @param conflictAlgorithm for update conflict resolver
* @return the number of rows affected
- * @hide
*/
public int updateWithOnConflict(String table, ContentValues values,
- String whereClause, String[] whereArgs, ConflictAlgorithm algorithm) {
- if (!isOpen()) {
- throw new IllegalStateException("database not open");
- }
-
+ String whereClause, String[] whereArgs, int conflictAlgorithm) {
if (values == null || values.size() == 0) {
throw new IllegalArgumentException("Empty values");
}
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
- if (algorithm != null) {
- sql.append("OR ");
- sql.append(algorithm.value());
- sql.append(" ");
- }
-
+ sql.append(CONFLICT_VALUES[conflictAlgorithm]);
sql.append(table);
sql.append(" SET ");
@@ -1551,6 +1663,9 @@ public class SQLiteDatabase extends SQLiteClosable {
}
lock();
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
SQLiteStatement statement = null;
try {
statement = compileStatement(sql.toString());
@@ -1575,7 +1690,6 @@ public class SQLiteDatabase extends SQLiteClosable {
// Run the program and then cleanup
statement.execute();
- statement.close();
int numChangedRows = lastChangeCount();
if (Config.LOGD && Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Updated " + numChangedRows + " using " + values + " and " + sql);
@@ -1603,9 +1717,12 @@ public class SQLiteDatabase extends SQLiteClosable {
* @throws SQLException If the SQL string is invalid for some reason
*/
public void execSQL(String sql) throws SQLException {
- boolean logStats = mLogStats;
- long timeStart = logStats ? SystemClock.elapsedRealtime() : 0;
+ long timeStart = SystemClock.uptimeMillis();
lock();
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
+ logTimeStat(mLastSqlStatement, timeStart, GET_LOCK_LOG_PREFIX);
try {
native_execSQL(sql);
} catch (SQLiteDatabaseCorruptException e) {
@@ -1614,8 +1731,14 @@ public class SQLiteDatabase extends SQLiteClosable {
} finally {
unlock();
}
- if (logStats) {
- logTimeStat(false /* not a read */, timeStart, SystemClock.elapsedRealtime());
+
+ // Log commit statements along with the most recently executed
+ // SQL statement for disambiguation. Note that instance
+ // equality to COMMIT_SQL is safe here.
+ if (sql == COMMIT_SQL) {
+ logTimeStat(mLastSqlStatement, timeStart, COMMIT_SQL);
+ } else {
+ logTimeStat(sql, timeStart, null);
}
}
@@ -1632,10 +1755,11 @@ public class SQLiteDatabase extends SQLiteClosable {
if (bindArgs == null) {
throw new IllegalArgumentException("Empty bindArgs");
}
-
- boolean logStats = mLogStats;
- long timeStart = logStats ? SystemClock.elapsedRealtime() : 0;
+ long timeStart = SystemClock.uptimeMillis();
lock();
+ if (!isOpen()) {
+ throw new IllegalStateException("database not open");
+ }
SQLiteStatement statement = null;
try {
statement = compileStatement(sql);
@@ -1655,21 +1779,14 @@ public class SQLiteDatabase extends SQLiteClosable {
}
unlock();
}
- if (logStats) {
- logTimeStat(false /* not a read */, timeStart, SystemClock.elapsedRealtime());
- }
+ logTimeStat(sql, timeStart);
}
@Override
protected void finalize() {
if (isOpen()) {
- if (mPrograms.isEmpty()) {
- Log.e(TAG, "Leak found", mLeakedException);
- } else {
- IllegalStateException leakProgram = new IllegalStateException(
- "mPrograms size " + mPrograms.size(), mLeakedException);
- Log.e(TAG, "Leak found", leakProgram);
- }
+ Log.e(TAG, "close() was never explicitly called on database '" +
+ mPath + "' ", mStackTrace);
closeClosable();
onAllReferencesReleased();
}
@@ -1689,23 +1806,30 @@ public class SQLiteDatabase extends SQLiteClosable {
}
mFlags = flags;
mPath = path;
- mLogStats = "1".equals(android.os.SystemProperties.get("db.logstats"));
mSlowQueryThreshold = SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1);
-
- mLeakedException = new IllegalStateException(path +
- " SQLiteDatabase created and never closed");
+ mStackTrace = new DatabaseObjectNotClosedException().fillInStackTrace();
mFactory = factory;
dbopen(mPath, mFlags);
+ if (SQLiteDebug.DEBUG_SQL_CACHE) {
+ mTimeOpened = getTime();
+ }
mPrograms = new WeakHashMap<SQLiteClosable,Object>();
try {
setLocale(Locale.getDefault());
} catch (RuntimeException e) {
Log.e(TAG, "Failed to setLocale() when constructing, closing the database", e);
dbclose();
+ if (SQLiteDebug.DEBUG_SQL_CACHE) {
+ mTimeClosed = getTime();
+ }
throw e;
}
}
+ private String getTime() {
+ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS ").format(System.currentTimeMillis());
+ }
+
/**
* return whether the DB is opened as read only.
* @return true if DB is opened as read only
@@ -1734,8 +1858,83 @@ public class SQLiteDatabase extends SQLiteClosable {
return mPath;
}
- /* package */ void logTimeStat(boolean read, long begin, long end) {
- EventLog.writeEvent(EVENT_DB_OPERATION, mPath, read ? 0 : 1, end - begin);
+ /* package */ void logTimeStat(String sql, long beginMillis) {
+ logTimeStat(sql, beginMillis, null);
+ }
+
+ /* package */ void logTimeStat(String sql, long beginMillis, String prefix) {
+ // Keep track of the last statement executed here, as this is
+ // the common funnel through which all methods of hitting
+ // libsqlite eventually flow.
+ mLastSqlStatement = sql;
+
+ // Sample fast queries in proportion to the time taken.
+ // Quantize the % first, so the logged sampling probability
+ // exactly equals the actual sampling rate for this query.
+
+ int samplePercent;
+ long durationMillis = SystemClock.uptimeMillis() - beginMillis;
+ if (durationMillis == 0 && prefix == GET_LOCK_LOG_PREFIX) {
+ // The common case is locks being uncontended. Don't log those,
+ // even at 1%, which is our default below.
+ return;
+ }
+ if (sQueryLogTimeInMillis == 0) {
+ sQueryLogTimeInMillis = SystemProperties.getInt("db.db_operation.threshold_ms", 500);
+ }
+ if (durationMillis >= sQueryLogTimeInMillis) {
+ samplePercent = 100;
+ } else {;
+ samplePercent = (int) (100 * durationMillis / sQueryLogTimeInMillis) + 1;
+ if (mRandom.nextInt(100) >= samplePercent) return;
+ }
+
+ // Note: the prefix will be "COMMIT;" or "GETLOCK:" when non-null. We wait to do
+ // it here so we avoid allocating in the common case.
+ if (prefix != null) {
+ sql = prefix + sql;
+ }
+
+ if (sql.length() > QUERY_LOG_SQL_LENGTH) sql = sql.substring(0, QUERY_LOG_SQL_LENGTH);
+
+ // ActivityThread.currentPackageName() only returns non-null if the
+ // current thread is an application main thread. This parameter tells
+ // us whether an event loop is blocked, and if so, which app it is.
+ //
+ // Sadly, there's no fast way to determine app name if this is *not* a
+ // main thread, or when we are invoked via Binder (e.g. ContentProvider).
+ // Hopefully the full path to the database will be informative enough.
+
+ String blockingPackage = ActivityThread.currentPackageName();
+ if (blockingPackage == null) blockingPackage = "";
+
+ EventLog.writeEvent(
+ EVENT_DB_OPERATION,
+ getPathForLogs(),
+ sql,
+ durationMillis,
+ blockingPackage,
+ samplePercent);
+ }
+
+ /**
+ * Removes email addresses from database filenames before they're
+ * logged to the EventLog where otherwise apps could potentially
+ * read them.
+ */
+ private String getPathForLogs() {
+ if (mPathForLogs != null) {
+ return mPathForLogs;
+ }
+ if (mPath == null) {
+ return null;
+ }
+ if (mPath.indexOf('@') == -1) {
+ mPathForLogs = mPath;
+ } else {
+ mPathForLogs = EMAIL_IN_DB_PATTERN.matcher(mPath).replaceAll("XX@YY");
+ }
+ return mPathForLogs;
}
/**
@@ -1754,6 +1953,267 @@ public class SQLiteDatabase extends SQLiteClosable {
}
}
+ /*
+ * ============================================================================
+ *
+ * The following methods deal with compiled-sql cache
+ * ============================================================================
+ */
+ /**
+ * adds the given sql and its compiled-statement-id-returned-by-sqlite to the
+ * cache of compiledQueries attached to 'this'.
+ *
+ * if there is already a {@link SQLiteCompiledSql} in compiledQueries for the given sql,
+ * the new {@link SQLiteCompiledSql} object is NOT inserted into the cache (i.e.,the current
+ * mapping is NOT replaced with the new mapping).
+ */
+ /* package */ void addToCompiledQueries(String sql, SQLiteCompiledSql compiledStatement) {
+ if (mMaxSqlCacheSize == 0) {
+ // for this database, there is no cache of compiled sql.
+ if (SQLiteDebug.DEBUG_SQL_CACHE) {
+ Log.v(TAG, "|NOT adding_sql_to_cache|" + getPath() + "|" + sql);
+ }
+ return;
+ }
+
+ SQLiteCompiledSql compiledSql = null;
+ synchronized(mCompiledQueries) {
+ // don't insert the new mapping if a mapping already exists
+ compiledSql = mCompiledQueries.get(sql);
+ if (compiledSql != null) {
+ return;
+ }
+ // add this <sql, compiledStatement> to the cache
+ if (mCompiledQueries.size() == mMaxSqlCacheSize) {
+ /*
+ * cache size of {@link #mMaxSqlCacheSize} is not enough for this app.
+ * log a warning MAX_WARNINGS_ON_CACHESIZE_CONDITION times
+ * chances are it is NOT using ? for bindargs - so caching is useless.
+ * TODO: either let the callers set max cchesize for their app, or intelligently
+ * figure out what should be cached for a given app.
+ */
+ if (++mCacheFullWarnings == MAX_WARNINGS_ON_CACHESIZE_CONDITION) {
+ Log.w(TAG, "Reached MAX size for compiled-sql statement cache for database " +
+ getPath() + "; i.e., NO space for this sql statement in cache: " +
+ sql + ". Please change your sql statements to use '?' for " +
+ "bindargs, instead of using actual values");
+ }
+ // don't add this entry to cache
+ } else {
+ // cache is NOT full. add this to cache.
+ mCompiledQueries.put(sql, compiledStatement);
+ if (SQLiteDebug.DEBUG_SQL_CACHE) {
+ Log.v(TAG, "|adding_sql_to_cache|" + getPath() + "|" +
+ mCompiledQueries.size() + "|" + sql);
+ }
+ }
+ }
+ return;
+ }
+
+
+ private void deallocCachedSqlStatements() {
+ synchronized (mCompiledQueries) {
+ for (SQLiteCompiledSql compiledSql : mCompiledQueries.values()) {
+ compiledSql.releaseSqlStatement();
+ }
+ mCompiledQueries.clear();
+ }
+ }
+
+ /**
+ * from the compiledQueries cache, returns the compiled-statement-id for the given sql.
+ * returns null, if not found in the cache.
+ */
+ /* package */ SQLiteCompiledSql getCompiledStatementForSql(String sql) {
+ SQLiteCompiledSql compiledStatement = null;
+ boolean cacheHit;
+ synchronized(mCompiledQueries) {
+ if (mMaxSqlCacheSize == 0) {
+ // for this database, there is no cache of compiled sql.
+ if (SQLiteDebug.DEBUG_SQL_CACHE) {
+ Log.v(TAG, "|cache NOT found|" + getPath());
+ }
+ return null;
+ }
+ cacheHit = (compiledStatement = mCompiledQueries.get(sql)) != null;
+ }
+ if (cacheHit) {
+ mNumCacheHits++;
+ } else {
+ mNumCacheMisses++;
+ }
+
+ if (SQLiteDebug.DEBUG_SQL_CACHE) {
+ Log.v(TAG, "|cache_stats|" +
+ getPath() + "|" + mCompiledQueries.size() +
+ "|" + mNumCacheHits + "|" + mNumCacheMisses +
+ "|" + cacheHit + "|" + mTimeOpened + "|" + mTimeClosed + "|" + sql);
+ }
+ return compiledStatement;
+ }
+
+ /**
+ * returns true if the given sql is cached in compiled-sql cache.
+ * @hide
+ */
+ public boolean isInCompiledSqlCache(String sql) {
+ synchronized(mCompiledQueries) {
+ return mCompiledQueries.containsKey(sql);
+ }
+ }
+
+ /**
+ * purges the given sql from the compiled-sql cache.
+ * @hide
+ */
+ public void purgeFromCompiledSqlCache(String sql) {
+ synchronized(mCompiledQueries) {
+ mCompiledQueries.remove(sql);
+ }
+ }
+
+ /**
+ * remove everything from the compiled sql cache
+ * @hide
+ */
+ public void resetCompiledSqlCache() {
+ synchronized(mCompiledQueries) {
+ mCompiledQueries.clear();
+ }
+ }
+
+ /**
+ * return the current maxCacheSqlCacheSize
+ * @hide
+ */
+ public synchronized int getMaxSqlCacheSize() {
+ return mMaxSqlCacheSize;
+ }
+
+ /**
+ * set the max size of the compiled sql cache for this database after purging the cache.
+ * (size of the cache = number of compiled-sql-statements stored in the cache).
+ *
+ * max cache size can ONLY be increased from its current size (default = 0).
+ * if this method is called with smaller size than the current value of mMaxSqlCacheSize,
+ * then IllegalStateException is thrown
+ *
+ * synchronized because we don't want t threads to change cache size at the same time.
+ * @param cacheSize the size of the cache. can be (0 to MAX_SQL_CACHE_SIZE)
+ * @throws IllegalStateException if input cacheSize > MAX_SQL_CACHE_SIZE or < 0 or
+ * < the value set with previous setMaxSqlCacheSize() call.
+ *
+ * @hide
+ */
+ public synchronized void setMaxSqlCacheSize(int cacheSize) {
+ if (cacheSize > MAX_SQL_CACHE_SIZE || cacheSize < 0) {
+ throw new IllegalStateException("expected value between 0 and " + MAX_SQL_CACHE_SIZE);
+ } else if (cacheSize < mMaxSqlCacheSize) {
+ throw new IllegalStateException("cannot set cacheSize to a value less than the value " +
+ "set with previous setMaxSqlCacheSize() call.");
+ }
+ mMaxSqlCacheSize = cacheSize;
+ }
+
+ static class ActiveDatabases {
+ private static final ActiveDatabases activeDatabases = new ActiveDatabases();
+ private HashSet<WeakReference<SQLiteDatabase>> mActiveDatabases =
+ new HashSet<WeakReference<SQLiteDatabase>>();
+ private ActiveDatabases() {} // disable instantiation of this class
+ static ActiveDatabases getInstance() {return activeDatabases;}
+ }
+
+ /**
+ * this method is used to collect data about ALL open databases in the current process.
+ * bugreport is a user of this data.
+ */
+ /* package */ static ArrayList<DbStats> getDbStats() {
+ ArrayList<DbStats> dbStatsList = new ArrayList<DbStats>();
+ for (WeakReference<SQLiteDatabase> w : ActiveDatabases.getInstance().mActiveDatabases) {
+ SQLiteDatabase db = w.get();
+ if (db == null || !db.isOpen()) {
+ continue;
+ }
+ // get SQLITE_DBSTATUS_LOOKASIDE_USED for the db
+ int lookasideUsed = db.native_getDbLookaside();
+
+ // get the lastnode of the dbname
+ String path = db.getPath();
+ int indx = path.lastIndexOf("/");
+ String lastnode = path.substring((indx != -1) ? ++indx : 0);
+
+ // get list of attached dbs and for each db, get its size and pagesize
+ ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs(db);
+ if (attachedDbs == null) {
+ continue;
+ }
+ for (int i = 0; i < attachedDbs.size(); i++) {
+ Pair<String, String> p = attachedDbs.get(i);
+ long pageCount = getPragmaVal(db, p.first + ".page_count;");
+
+ // first entry in the attached db list is always the main database
+ // don't worry about prefixing the dbname with "main"
+ String dbName;
+ if (i == 0) {
+ dbName = lastnode;
+ } else {
+ // lookaside is only relevant for the main db
+ lookasideUsed = 0;
+ dbName = " (attached) " + p.first;
+ // if the attached db has a path, attach the lastnode from the path to above
+ if (p.second.trim().length() > 0) {
+ int idx = p.second.lastIndexOf("/");
+ dbName += " : " + p.second.substring((idx != -1) ? ++idx : 0);
+ }
+ }
+ if (pageCount > 0) {
+ dbStatsList.add(new DbStats(dbName, pageCount, db.getPageSize(),
+ lookasideUsed));
+ }
+ }
+ }
+ return dbStatsList;
+ }
+
+ /**
+ * get the specified pragma value from sqlite for the specified database.
+ * only handles pragma's that return int/long.
+ * NO JAVA locks are held in this method.
+ * TODO: use this to do all pragma's in this class
+ */
+ private static long getPragmaVal(SQLiteDatabase db, String pragma) {
+ if (!db.isOpen()) {
+ return 0;
+ }
+ SQLiteStatement prog = null;
+ try {
+ prog = new SQLiteStatement(db, "PRAGMA " + pragma);
+ long val = prog.simpleQueryForLong();
+ return val;
+ } finally {
+ if (prog != null) prog.close();
+ }
+ }
+
+ /**
+ * returns list of full pathnames of all attached databases
+ * including the main database
+ * TODO: move this to {@link DatabaseUtils}
+ */
+ private static ArrayList<Pair<String, String>> getAttachedDbs(SQLiteDatabase dbObj) {
+ if (!dbObj.isOpen()) {
+ return null;
+ }
+ ArrayList<Pair<String, String>> attachedDbs = new ArrayList<Pair<String, String>>();
+ Cursor c = dbObj.rawQuery("pragma database_list;", null);
+ while (c.moveToNext()) {
+ attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2)));
+ }
+ c.close();
+ return attachedDbs;
+ }
+
/**
* Native call to open the database.
*
@@ -1762,6 +2222,23 @@ public class SQLiteDatabase extends SQLiteClosable {
private native void dbopen(String path, int flags);
/**
+ * Native call to setup tracing of all sql statements
+ *
+ * @param path the full path to the database
+ */
+ private native void enableSqlTracing(String path);
+
+ /**
+ * Native call to setup profiling of all sql statements.
+ * currently, sqlite's profiling = printing of execution-time
+ * (wall-clock time) of each of the sql statements, as they
+ * are executed.
+ *
+ * @param path the full path to the database
+ */
+ private native void enableSqlProfiling(String path);
+
+ /**
* Native call to execute a raw SQL statement. {@link #lock} must be held
* when calling this method.
*
@@ -1790,4 +2267,11 @@ public class SQLiteDatabase extends SQLiteClosable {
* @return the number of changes made in the last statement executed.
*/
/* package */ native int lastChangeCount();
+
+ /**
+ * return the SQLITE_DBSTATUS_LOOKASIDE_USED documented here
+ * http://www.sqlite.org/c3ref/c_dbstatus_lookaside_used.html
+ * @return int value of SQLITE_DBSTATUS_LOOKASIDE_USED
+ */
+ private native int native_getDbLookaside();
}