allow apps to specify actions to take on database corruption error
let the user specify an interface impl class to specify the actions
to take when db corruption is detected.
this class is specified when the database is opened/created.
Change-Id: I84eb57208c8fedfa7235805b0ec58165efdc1560
diff --git a/core/java/android/database/DatabaseErrorHandler.java b/core/java/android/database/DatabaseErrorHandler.java
new file mode 100644
index 0000000..f0c5452
--- /dev/null
+++ b/core/java/android/database/DatabaseErrorHandler.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.database;
+
+import android.database.sqlite.SQLiteDatabase;
+
+/**
+ * An interface to let the apps define the actions to take when the following errors are detected
+ * database corruption
+ */
+public interface DatabaseErrorHandler {
+
+ /**
+ * defines the method to be invoked when database corruption is detected.
+ * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption
+ * is detected.
+ */
+ void onCorruption(SQLiteDatabase dbObj);
+}
diff --git a/core/java/android/database/DefaultDatabaseErrorHandler.java b/core/java/android/database/DefaultDatabaseErrorHandler.java
new file mode 100644
index 0000000..98aa54a
--- /dev/null
+++ b/core/java/android/database/DefaultDatabaseErrorHandler.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.database;
+
+import java.io.File;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.util.Log;
+import android.util.Pair;
+
+/**
+ * Default class used defining the actions to take when the following errors are detected
+ * database corruption
+ */
+public final class DefaultDatabaseErrorHandler implements DatabaseErrorHandler {
+
+ private static final String TAG = "DefaultDatabaseErrorHandler";
+
+ /**
+ * defines the default method to be invoked when database corruption is detected.
+ * @param dbObj the {@link SQLiteDatabase} object representing the database on which corruption
+ * is detected.
+ */
+ public void onCorruption(SQLiteDatabase dbObj) {
+ Log.e(TAG, "Corruption reported by sqlite on database: " + dbObj.getPath());
+
+ // is the corruption detected even before database could be 'opened'?
+ if (!dbObj.isOpen()) {
+ // database files are not even openable. delete this database file.
+ // NOTE if the database has attached databases, then any of them could be corrupt.
+ // and not deleting all of them could cause corrupted database file to remain and
+ // make the application crash on database open operation. To avoid this problem,
+ // the application should provide its own {@link DatabaseErrorHandler} impl class
+ // to delete ALL files of the database (including the attached databases).
+ if (!dbObj.getPath().equalsIgnoreCase(":memory")) {
+ // not memory database.
+ try {
+ new File(dbObj.getPath()).delete();
+ } catch (Exception e) {
+ /* ignore */
+ }
+ }
+ return;
+ }
+
+ try {
+ // Close the database, which will cause subsequent operations to fail.
+ try {
+ dbObj.close();
+ } catch (SQLiteException e) {
+ /* ignore */
+ }
+ } finally {
+ // Delete all files of this corrupt database and/or attached databases
+ for (Pair<String, String> p : dbObj.getAttachedDbs()) {
+ Log.e(TAG, "deleting the database file: " + p.second);
+ if (!p.second.equalsIgnoreCase(":memory:")) {
+ // delete file if it is a non-memory database file
+ try {
+ new File(p.second).delete();
+ } catch (Exception e) {
+ /* ignore */
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/core/java/android/database/sqlite/SQLiteDatabase.java b/core/java/android/database/sqlite/SQLiteDatabase.java
index 01c8a38..93f13af 100644
--- a/core/java/android/database/sqlite/SQLiteDatabase.java
+++ b/core/java/android/database/sqlite/SQLiteDatabase.java
@@ -21,7 +21,9 @@
import android.app.ActivityThread;
import android.content.ContentValues;
import android.database.Cursor;
+import android.database.DatabaseErrorHandler;
import android.database.DatabaseUtils;
+import android.database.DefaultDatabaseErrorHandler;
import android.database.SQLException;
import android.database.sqlite.SQLiteDebug.DbStats;
import android.os.Debug;
@@ -295,6 +297,11 @@
private static final String LOG_SLOW_QUERIES_PROPERTY = "db.log.slow_query_threshold";
private final int mSlowQueryThreshold;
+ /** {@link DatabaseErrorHandler} to be used when SQLite returns any of the following errors
+ * Corruption
+ * */
+ private DatabaseErrorHandler errorHandler;
+
/**
* @param closable
*/
@@ -352,19 +359,8 @@
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 {
- // 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.
- if (!mPath.equalsIgnoreCase(":memory")) {
- // delete is only for non-memory database files
- new File(mPath).delete();
- }
- }
+ errorHandler.onCorruption(this);
}
/**
@@ -816,10 +812,25 @@
* @throws SQLiteException if the database cannot be opened
*/
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) {
- SQLiteDatabase sqliteDatabase = null;
+ return openDatabase(path, factory, flags, new DefaultDatabaseErrorHandler());
+ }
+
+ /**
+ * same as {@link #openDatabase(String, CursorFactory, int)} except for an additional param
+ * errorHandler.
+ * @param errorHandler the {@link DatabaseErrorHandler} obj to be used when database
+ * corruption is detected on the database.
+ */
+ public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
+ DatabaseErrorHandler errorHandler) {
+ SQLiteDatabase sqliteDatabase = new SQLiteDatabase(path, factory, flags);
+
+ // set the ErrorHandler to be used when SQLite reports exceptions
+ sqliteDatabase.errorHandler = errorHandler;
+
try {
// Open the database.
- sqliteDatabase = new SQLiteDatabase(path, factory, flags);
+ sqliteDatabase.openDatabase(path, flags);
if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
sqliteDatabase.enableSqlTracing(path);
}
@@ -827,14 +838,8 @@
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);
- if (!path.equalsIgnoreCase(":memory")) {
- // delete is only for non-memory database files
- new File(path).delete();
- }
+ // Database is not even openable.
+ errorHandler.onCorruption(sqliteDatabase);
sqliteDatabase = new SQLiteDatabase(path, factory, flags);
}
ActiveDatabases.getInstance().mActiveDatabases.add(
@@ -842,6 +847,18 @@
return sqliteDatabase;
}
+ private void openDatabase(String path, int flags) {
+ // Open the database.
+ dbopen(path, flags);
+ try {
+ setLocale(Locale.getDefault());
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to setLocale(). closing the database", e);
+ dbclose();
+ throw e;
+ }
+ }
+
/**
* Equivalent to openDatabase(file.getPath(), factory, CREATE_IF_NECESSARY).
*/
@@ -857,6 +874,17 @@
}
/**
+ * same as {@link #openOrCreateDatabase(String, CursorFactory)} except for an additional param
+ * errorHandler.
+ * @param errorHandler the {@link DatabaseErrorHandler} obj to be used when database
+ * corruption is detected on the database.
+ */
+ public static SQLiteDatabase openOrCreateDatabase(String path, CursorFactory factory,
+ DatabaseErrorHandler errorHandler) {
+ return openDatabase(path, factory, CREATE_IF_NECESSARY, errorHandler);
+ }
+
+ /**
* Create a memory backed SQLite database. Its contents will be destroyed
* when the database is closed.
*
@@ -1821,21 +1849,10 @@
mSlowQueryThreshold = SystemProperties.getInt(LOG_SLOW_QUERIES_PROPERTY, -1);
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() {
@@ -2139,7 +2156,7 @@
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);
+ ArrayList<Pair<String, String>> attachedDbs = db.getAttachedDbs();
if (attachedDbs == null) {
continue;
}
@@ -2193,24 +2210,74 @@
}
/**
- * returns list of full pathnames of all attached databases
- * including the main database
- * TODO: move this to {@link DatabaseUtils}
+ * returns list of full pathnames of all attached databases including the main database
+ * @return ArrayList of pairs of (database name, database file path) or null if the database
+ * is not open.
*/
- private static ArrayList<Pair<String, String>> getAttachedDbs(SQLiteDatabase dbObj) {
- if (!dbObj.isOpen()) {
+ public ArrayList<Pair<String, String>> getAttachedDbs() {
+ if (!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)));
+ Cursor c = null;
+ try {
+ c = rawQuery("pragma database_list;", null);
+ while (c.moveToNext()) {
+ // sqlite returns a row for each database in the returned list of databases.
+ // in each row,
+ // 1st column is the database name such as main, or the database
+ // name specified on the "ATTACH" command
+ // 2nd column is the database file path.
+ attachedDbs.add(new Pair<String, String>(c.getString(1), c.getString(2)));
+ }
+ } finally {
+ if (c != null) {
+ c.close();
+ }
}
- c.close();
return attachedDbs;
}
/**
+ * run pragma integrity_check on the given database (and all the attached databases)
+ * and return true if the given database (and all its attached databases) pass integrity_check,
+ * false otherwise.
+ *
+ * if the result is false, then this method logs the errors reported by the integrity_check
+ * command execution.
+ *
+ * @return true if the given database (and all its attached databases) pass integrity_check,
+ * false otherwise
+ */
+ public boolean isDatabaseIntegrityOk() {
+ if (!isOpen()) {
+ throw new IllegalStateException("database: " + getPath() + " is NOT open");
+ }
+ ArrayList<Pair<String, String>> attachedDbs = getAttachedDbs();
+ if (attachedDbs == null) {
+ throw new IllegalStateException("databaselist for: " + getPath() + " couldn't " +
+ "be retrieved. probably because the database is closed");
+ }
+ boolean isDatabaseCorrupt = false;
+ for (int i = 0; i < attachedDbs.size(); i++) {
+ Pair<String, String> p = attachedDbs.get(i);
+ SQLiteStatement prog = null;
+ try {
+ prog = compileStatement("PRAGMA " + p.first + ".integrity_check(1);");
+ String rslt = prog.simpleQueryForString();
+ if (!rslt.equalsIgnoreCase("ok")) {
+ // integrity_checker failed on main or attached databases
+ isDatabaseCorrupt = true;
+ Log.e(TAG, "PRAGMA integrity_check on " + p.second + " returned: " + rslt);
+ }
+ } finally {
+ if (prog != null) prog.close();
+ }
+ }
+ return isDatabaseCorrupt;
+ }
+
+ /**
* Native call to open the database.
*
* @param path The full path to the database
diff --git a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java
index 656029d..49bd4e8 100644
--- a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java
+++ b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java
@@ -519,13 +519,13 @@
assertEquals(1, c.getCount());
assertTrue(c.moveToFirst());
assertEquals("don't forget to handled 's", c.getString(1));
- c.deactivate();
+ c.close();
// make sure code should checking null string properly so that
// it won't crash
try {
mDatabase.query("test", new String[]{"_id"},
- "_id=?", new String[]{null}, null, null, null);
+ "_id=?", null, null, null, null);
fail("expected exception not thrown");
} catch (IllegalArgumentException e) {
// expected
@@ -1023,6 +1023,34 @@
}
}
+ @MediumTest
+ public void testUnionsWithBindArgs() {
+ /* make sure unions with bindargs work http://b/issue?id=1061291 */
+ mDatabase.execSQL("CREATE TABLE A (i int);");
+ mDatabase.execSQL("create table B (k int);");
+ mDatabase.execSQL("create table C (n int);");
+ mDatabase.execSQL("insert into A values(1);");
+ mDatabase.execSQL("insert into A values(2);");
+ mDatabase.execSQL("insert into A values(3);");
+ mDatabase.execSQL("insert into B values(201);");
+ mDatabase.execSQL("insert into B values(202);");
+ mDatabase.execSQL("insert into B values(203);");
+ mDatabase.execSQL("insert into C values(901);");
+ mDatabase.execSQL("insert into C values(902);");
+ String s = "select i from A where i > 2 " +
+ "UNION select k from B where k > 201 " +
+ "UNION select n from C where n !=900;";
+ Cursor c = mDatabase.rawQuery(s, null);
+ int n = c.getCount();
+ c.close();
+ String s1 = "select i from A where i > ? " +
+ "UNION select k from B where k > ? " +
+ "UNION select n from C where n != ?;";
+ Cursor c1 = mDatabase.rawQuery(s1, new String[]{"2", "201", "900"});
+ assertEquals(n, c1.getCount());
+ c1.close();
+ }
+
/**
* This test is available only when the platform has a locale with the language "ja".
* It finishes without failure when it is not available.