summaryrefslogtreecommitdiff
path: root/chromium/sql
diff options
context:
space:
mode:
authorAndras Becsi <andras.becsi@digia.com>2014-03-18 13:16:26 +0100
committerFrederik Gladhorn <frederik.gladhorn@digia.com>2014-03-20 15:55:39 +0100
commit3f0f86b0caed75241fa71c95a5d73bc0164348c5 (patch)
tree92b9fb00f2e9e90b0be2262093876d4f43b6cd13 /chromium/sql
parente90d7c4b152c56919d963987e2503f9909a666d2 (diff)
downloadqtwebengine-chromium-3f0f86b0caed75241fa71c95a5d73bc0164348c5.tar.gz
Update to new stable branch 1750
This also includes an updated ninja and chromium dependencies needed on Windows. Change-Id: Icd597d80ed3fa4425933c9f1334c3c2e31291c42 Reviewed-by: Zoltan Arvai <zarvai@inf.u-szeged.hu> Reviewed-by: Zeno Albisser <zeno.albisser@digia.com>
Diffstat (limited to 'chromium/sql')
-rw-r--r--chromium/sql/connection.cc52
-rw-r--r--chromium/sql/connection.h40
-rw-r--r--chromium/sql/connection_unittest.cc111
-rw-r--r--chromium/sql/meta_table.cc90
-rw-r--r--chromium/sql/meta_table.h14
-rw-r--r--chromium/sql/meta_table_unittest.cc47
-rw-r--r--chromium/sql/recovery.cc337
-rw-r--r--chromium/sql/recovery.h83
-rw-r--r--chromium/sql/recovery_unittest.cc421
-rw-r--r--chromium/sql/statement.cc29
-rw-r--r--chromium/sql/statement.h11
11 files changed, 1065 insertions, 170 deletions
diff --git a/chromium/sql/connection.cc b/chromium/sql/connection.cc
index 097edd7a1ea..0caa9edc5b5 100644
--- a/chromium/sql/connection.cc
+++ b/chromium/sql/connection.cc
@@ -133,7 +133,7 @@ namespace sql {
Connection::ErrorIgnorerCallback* Connection::current_ignorer_cb_ = NULL;
// static
-bool Connection::ShouldIgnore(int error) {
+bool Connection::ShouldIgnoreSqliteError(int error) {
if (!current_ignorer_cb_)
return false;
return current_ignorer_cb_->Run(error);
@@ -213,7 +213,7 @@ Connection::~Connection() {
bool Connection::Open(const base::FilePath& path) {
if (!histogram_tag_.empty()) {
int64 size_64 = 0;
- if (file_util::GetFileSize(path, &size_64)) {
+ if (base::GetFileSize(path, &size_64)) {
size_t sample = static_cast<size_t>(size_64 / 1024);
std::string full_histogram_name = "Sqlite.SizeKB." + histogram_tag_;
base::HistogramBase* histogram =
@@ -641,7 +641,7 @@ bool Connection::Execute(const char* sql) {
int error = ExecuteAndReturnErrorCode(sql);
if (error != SQLITE_OK)
- error = OnSqliteError(error, NULL);
+ error = OnSqliteError(error, NULL, sql);
// This needs to be a FATAL log because the error case of arriving here is
// that there's a malformed SQL statement. This can arise in development if
@@ -702,7 +702,7 @@ scoped_refptr<Connection::StatementRef> Connection::GetUniqueStatement(
DLOG(FATAL) << "SQL compile error " << GetErrorMessage();
// It could also be database corruption.
- OnSqliteError(rc, NULL);
+ OnSqliteError(rc, NULL, sql);
return new StatementRef(NULL, NULL, false);
}
return new StatementRef(this, stmt, true);
@@ -864,7 +864,7 @@ bool Connection::OpenInternal(const std::string& file_name,
// purposes.
UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.OpenFailure", err);
- OnSqliteError(err, NULL);
+ OnSqliteError(err, NULL, "-- sqlite3_open()");
bool was_poisoned = poisoned_;
Close();
@@ -881,9 +881,9 @@ bool Connection::OpenInternal(const std::string& file_name,
int mode = 0;
// TODO(shess): Arguably, failure to retrieve and change
// permissions should be fatal if the file exists.
- if (file_util::GetPosixFilePermissions(file_path, &mode)) {
- mode &= file_util::FILE_PERMISSION_USER_MASK;
- file_util::SetPosixFilePermissions(file_path, mode);
+ if (base::GetPosixFilePermissions(file_path, &mode)) {
+ mode &= base::FILE_PERMISSION_USER_MASK;
+ base::SetPosixFilePermissions(file_path, mode);
// SQLite sets the permissions on these files from the main
// database on create. Set them here in case they already exist
@@ -891,8 +891,8 @@ bool Connection::OpenInternal(const std::string& file_name,
// be fatal unless the file doesn't exist.
base::FilePath journal_path(file_name + FILE_PATH_LITERAL("-journal"));
base::FilePath wal_path(file_name + FILE_PATH_LITERAL("-wal"));
- file_util::SetPosixFilePermissions(journal_path, mode);
- file_util::SetPosixFilePermissions(wal_path, mode);
+ base::SetPosixFilePermissions(journal_path, mode);
+ base::SetPosixFilePermissions(wal_path, mode);
}
}
#endif // defined(OS_POSIX)
@@ -1022,14 +1022,19 @@ void Connection::AddTaggedHistogram(const std::string& name,
histogram->Add(sample);
}
-int Connection::OnSqliteError(int err, sql::Statement *stmt) {
+int Connection::OnSqliteError(int err, sql::Statement *stmt, const char* sql) {
UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.Error", err);
AddTaggedHistogram("Sqlite.Error", err);
// Always log the error.
- LOG(ERROR) << "sqlite error " << err
+ if (!sql && stmt)
+ sql = stmt->GetSQLStatement();
+ if (!sql)
+ sql = "-- unknown";
+ LOG(ERROR) << histogram_tag_ << " sqlite error " << err
<< ", errno " << GetLastErrno()
- << ": " << GetErrorMessage();
+ << ": " << GetErrorMessage()
+ << ", sql: " << sql;
if (!error_callback_.is_null()) {
// Fire from a copy of the callback in case of reentry into
@@ -1040,14 +1045,26 @@ int Connection::OnSqliteError(int err, sql::Statement *stmt) {
}
// The default handling is to assert on debug and to ignore on release.
- if (!ShouldIgnore(err))
+ if (!ShouldIgnoreSqliteError(err))
DLOG(FATAL) << GetErrorMessage();
return err;
}
-// TODO(shess): Allow specifying integrity_check versus quick_check.
+bool Connection::FullIntegrityCheck(std::vector<std::string>* messages) {
+ return IntegrityCheckHelper("PRAGMA integrity_check", messages);
+}
+
+bool Connection::QuickIntegrityCheck() {
+ std::vector<std::string> messages;
+ if (!IntegrityCheckHelper("PRAGMA quick_check", &messages))
+ return false;
+ return messages.size() == 1 && messages[0] == "ok";
+}
+
// TODO(shess): Allow specifying maximum results (default 100 lines).
-bool Connection::IntegrityCheck(std::vector<std::string>* messages) {
+bool Connection::IntegrityCheckHelper(
+ const char* pragma_sql,
+ std::vector<std::string>* messages) {
messages->clear();
// This has the side effect of setting SQLITE_RecoveryMode, which
@@ -1060,8 +1077,7 @@ bool Connection::IntegrityCheck(std::vector<std::string>* messages) {
bool ret = false;
{
- const char kSql[] = "PRAGMA integrity_check";
- sql::Statement stmt(GetUniqueStatement(kSql));
+ sql::Statement stmt(GetUniqueStatement(pragma_sql));
// The pragma appears to return all results (up to 100 by default)
// as a single string. This doesn't appear to be an API contract,
diff --git a/chromium/sql/connection.h b/chromium/sql/connection.h
index 79386064228..54c3f8b7b8e 100644
--- a/chromium/sql/connection.h
+++ b/chromium/sql/connection.h
@@ -150,11 +150,16 @@ class SQL_EXPORT Connection {
// histogram is recorded.
void AddTaggedHistogram(const std::string& name, size_t sample) const;
- // Run "PRAGMA integrity_check" and post each line of results into
- // |messages|. Returns the success of running the statement - per
- // the SQLite documentation, if no errors are found the call should
- // succeed, and a single value "ok" should be in messages.
- bool IntegrityCheck(std::vector<std::string>* messages);
+ // Run "PRAGMA integrity_check" and post each line of
+ // results into |messages|. Returns the success of running the
+ // statement - per the SQLite documentation, if no errors are found the
+ // call should succeed, and a single value "ok" should be in messages.
+ bool FullIntegrityCheck(std::vector<std::string>* messages);
+
+ // Runs "PRAGMA quick_check" and, unlike the FullIntegrityCheck method,
+ // interprets the results returning true if the the statement executes
+ // without error and results in a single "ok" value.
+ bool QuickIntegrityCheck() WARN_UNUSED_RESULT;
// Initialization ------------------------------------------------------------
@@ -394,6 +399,12 @@ class SQL_EXPORT Connection {
// SELECT type, name, tbl_name, sql FROM sqlite_master ORDER BY 1, 2, 3, 4;
std::string GetSchema() const;
+ // Clients which provide an error_callback don't see the
+ // error-handling at the end of OnSqliteError(). Expose to allow
+ // those clients to work appropriately with ScopedErrorIgnorer in
+ // tests.
+ static bool ShouldIgnoreSqliteError(int error);
+
private:
// For recovery module.
friend class Recovery;
@@ -436,7 +447,6 @@ class SQL_EXPORT Connection {
// See test/scoped_error_ignorer.h.
typedef base::Callback<bool(int)> ErrorIgnorerCallback;
static ErrorIgnorerCallback* current_ignorer_cb_;
- static bool ShouldIgnore(int error);
static void SetErrorIgnorer(ErrorIgnorerCallback* ignorer);
static void ResetErrorIgnorer();
@@ -511,9 +521,17 @@ class SQL_EXPORT Connection {
void StatementRefCreated(StatementRef* ref);
void StatementRefDeleted(StatementRef* ref);
- // Called by Statement objects when an sqlite function returns an error.
- // The return value is the error code reflected back to client code.
- int OnSqliteError(int err, Statement* stmt);
+ // Called when a sqlite function returns an error, which is passed
+ // as |err|. The return value is the error code to be reflected
+ // back to client code. |stmt| is non-NULL if the error relates to
+ // an sql::Statement instance. |sql| is non-NULL if the error
+ // relates to non-statement sql code (Execute, for instance). Both
+ // can be NULL, but both should never be set.
+ // NOTE(shess): Originally, the return value was intended to allow
+ // error handlers to transparently convert errors into success.
+ // Unfortunately, transactions are not generally restartable, so
+ // this did not work out.
+ int OnSqliteError(int err, Statement* stmt, const char* sql);
// Like |Execute()|, but retries if the database is locked.
bool ExecuteWithTimeout(const char* sql, base::TimeDelta ms_timeout)
@@ -527,6 +545,10 @@ class SQL_EXPORT Connection {
// case for const functions).
scoped_refptr<StatementRef> GetUntrackedStatement(const char* sql) const;
+ bool IntegrityCheckHelper(
+ const char* pragma_sql,
+ std::vector<std::string>* messages) WARN_UNUSED_RESULT;
+
// The actual sqlite database. Will be NULL before Init has been called or if
// Init resulted in an error.
sqlite3* db_;
diff --git a/chromium/sql/connection_unittest.cc b/chromium/sql/connection_unittest.cc
index 445db3459bd..304ddcddbcc 100644
--- a/chromium/sql/connection_unittest.cc
+++ b/chromium/sql/connection_unittest.cc
@@ -11,6 +11,7 @@
#include "sql/statement.h"
#include "sql/test/error_callback_support.h"
#include "sql/test/scoped_error_ignorer.h"
+#include "sql/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/sqlite/sqlite3.h"
@@ -422,10 +423,10 @@ TEST_F(SQLConnectionTest, RazeEmptyDB) {
db().Close();
{
- file_util::ScopedFILE file(file_util::OpenFile(db_path(), "rb+"));
+ file_util::ScopedFILE file(base::OpenFile(db_path(), "rb+"));
ASSERT_TRUE(file.get() != NULL);
ASSERT_EQ(0, fseek(file.get(), 0, SEEK_SET));
- ASSERT_TRUE(file_util::TruncateFile(file.get()));
+ ASSERT_TRUE(base::TruncateFile(file.get()));
}
ASSERT_TRUE(db().Open(db_path()));
@@ -440,7 +441,7 @@ TEST_F(SQLConnectionTest, RazeNOTADB) {
ASSERT_FALSE(base::PathExists(db_path()));
{
- file_util::ScopedFILE file(file_util::OpenFile(db_path(), "wb"));
+ file_util::ScopedFILE file(base::OpenFile(db_path(), "wb"));
ASSERT_TRUE(file.get() != NULL);
const char* kJunk = "This is the hour of our discontent.";
@@ -473,7 +474,7 @@ TEST_F(SQLConnectionTest, RazeNOTADB2) {
db().Close();
{
- file_util::ScopedFILE file(file_util::OpenFile(db_path(), "rb+"));
+ file_util::ScopedFILE file(base::OpenFile(db_path(), "rb+"));
ASSERT_TRUE(file.get() != NULL);
ASSERT_EQ(0, fseek(file.get(), 0, SEEK_SET));
@@ -502,32 +503,14 @@ TEST_F(SQLConnectionTest, RazeNOTADB2) {
// essential for cases where the Open() can fail entirely, so the
// Raze() cannot happen later. Additionally test that when the
// callback does this during Open(), the open is retried and succeeds.
-//
-// Most corruptions seen in the wild seem to happen when two pages in
-// the database were not written transactionally (the transaction
-// changed both, but one wasn't successfully written for some reason).
-// A special case of that is when the header indicates that the
-// database contains more pages than are in the file. This breaks
-// things at a very basic level, verify that Raze() can handle it.
TEST_F(SQLConnectionTest, RazeCallbackReopen) {
const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
ASSERT_TRUE(db().Execute(kCreateSql));
ASSERT_EQ(1, SqliteMasterCount(&db()));
- int page_size = 0;
- {
- sql::Statement s(db().GetUniqueStatement("PRAGMA page_size"));
- ASSERT_TRUE(s.Step());
- page_size = s.ColumnInt(0);
- }
db().Close();
- // Trim a single page from the end of the file.
- {
- file_util::ScopedFILE file(file_util::OpenFile(db_path(), "rb+"));
- ASSERT_TRUE(file.get() != NULL);
- ASSERT_EQ(0, fseek(file.get(), -page_size, SEEK_END));
- ASSERT_TRUE(file_util::TruncateFile(file.get()));
- }
+ // Corrupt the database so that nothing works, including PRAGMAs.
+ ASSERT_TRUE(sql::test::CorruptSizeInHeader(db_path()));
// Open() will succeed, even though the PRAGMA calls within will
// fail with SQLITE_CORRUPT, as will this PRAGMA.
@@ -706,12 +689,12 @@ TEST_F(SQLConnectionTest, UserPermission) {
// read access for the database and journal.
ASSERT_TRUE(base::PathExists(db_path()));
ASSERT_TRUE(base::PathExists(journal));
- mode = file_util::FILE_PERMISSION_MASK;
- EXPECT_TRUE(file_util::GetPosixFilePermissions(db_path(), &mode));
- ASSERT_NE((mode & file_util::FILE_PERMISSION_USER_MASK), mode);
- mode = file_util::FILE_PERMISSION_MASK;
- EXPECT_TRUE(file_util::GetPosixFilePermissions(journal, &mode));
- ASSERT_NE((mode & file_util::FILE_PERMISSION_USER_MASK), mode);
+ mode = base::FILE_PERMISSION_MASK;
+ EXPECT_TRUE(base::GetPosixFilePermissions(db_path(), &mode));
+ ASSERT_NE((mode & base::FILE_PERMISSION_USER_MASK), mode);
+ mode = base::FILE_PERMISSION_MASK;
+ EXPECT_TRUE(base::GetPosixFilePermissions(journal, &mode));
+ ASSERT_NE((mode & base::FILE_PERMISSION_USER_MASK), mode);
// Re-open with restricted permissions and verify that the modes
// changed for both the main database and the journal.
@@ -720,12 +703,12 @@ TEST_F(SQLConnectionTest, UserPermission) {
ASSERT_TRUE(db().Open(db_path()));
ASSERT_TRUE(base::PathExists(db_path()));
ASSERT_TRUE(base::PathExists(journal));
- mode = file_util::FILE_PERMISSION_MASK;
- EXPECT_TRUE(file_util::GetPosixFilePermissions(db_path(), &mode));
- ASSERT_EQ((mode & file_util::FILE_PERMISSION_USER_MASK), mode);
- mode = file_util::FILE_PERMISSION_MASK;
- EXPECT_TRUE(file_util::GetPosixFilePermissions(journal, &mode));
- ASSERT_EQ((mode & file_util::FILE_PERMISSION_USER_MASK), mode);
+ mode = base::FILE_PERMISSION_MASK;
+ EXPECT_TRUE(base::GetPosixFilePermissions(db_path(), &mode));
+ ASSERT_EQ((mode & base::FILE_PERMISSION_USER_MASK), mode);
+ mode = base::FILE_PERMISSION_MASK;
+ EXPECT_TRUE(base::GetPosixFilePermissions(journal, &mode));
+ ASSERT_EQ((mode & base::FILE_PERMISSION_USER_MASK), mode);
// Delete and re-create the database, the restriction should still apply.
db().Close();
@@ -733,16 +716,16 @@ TEST_F(SQLConnectionTest, UserPermission) {
ASSERT_TRUE(db().Open(db_path()));
ASSERT_TRUE(base::PathExists(db_path()));
ASSERT_FALSE(base::PathExists(journal));
- mode = file_util::FILE_PERMISSION_MASK;
- EXPECT_TRUE(file_util::GetPosixFilePermissions(db_path(), &mode));
- ASSERT_EQ((mode & file_util::FILE_PERMISSION_USER_MASK), mode);
+ mode = base::FILE_PERMISSION_MASK;
+ EXPECT_TRUE(base::GetPosixFilePermissions(db_path(), &mode));
+ ASSERT_EQ((mode & base::FILE_PERMISSION_USER_MASK), mode);
// Verify that journal creation inherits the restriction.
EXPECT_TRUE(db().Execute("CREATE TABLE x (x)"));
ASSERT_TRUE(base::PathExists(journal));
- mode = file_util::FILE_PERMISSION_MASK;
- EXPECT_TRUE(file_util::GetPosixFilePermissions(journal, &mode));
- ASSERT_EQ((mode & file_util::FILE_PERMISSION_USER_MASK), mode);
+ mode = base::FILE_PERMISSION_MASK;
+ EXPECT_TRUE(base::GetPosixFilePermissions(journal, &mode));
+ ASSERT_EQ((mode & base::FILE_PERMISSION_USER_MASK), mode);
}
#endif // defined(OS_POSIX)
@@ -839,4 +822,48 @@ TEST_F(SQLConnectionTest, Attach) {
EXPECT_FALSE(db().IsSQLValid("SELECT count(*) from other.bar"));
}
+TEST_F(SQLConnectionTest, Basic_QuickIntegrityCheck) {
+ const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
+ ASSERT_TRUE(db().Execute(kCreateSql));
+ EXPECT_TRUE(db().QuickIntegrityCheck());
+ db().Close();
+
+ ASSERT_TRUE(sql::test::CorruptSizeInHeader(db_path()));
+
+ {
+ sql::ScopedErrorIgnorer ignore_errors;
+ ignore_errors.IgnoreError(SQLITE_CORRUPT);
+ ASSERT_TRUE(db().Open(db_path()));
+ EXPECT_FALSE(db().QuickIntegrityCheck());
+ ASSERT_TRUE(ignore_errors.CheckIgnoredErrors());
+ }
+}
+
+TEST_F(SQLConnectionTest, Basic_FullIntegrityCheck) {
+ const std::string kOk("ok");
+ std::vector<std::string> messages;
+
+ const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)";
+ ASSERT_TRUE(db().Execute(kCreateSql));
+ EXPECT_TRUE(db().FullIntegrityCheck(&messages));
+ EXPECT_EQ(1u, messages.size());
+ EXPECT_EQ(kOk, messages[0]);
+ db().Close();
+
+ ASSERT_TRUE(sql::test::CorruptSizeInHeader(db_path()));
+
+ {
+ sql::ScopedErrorIgnorer ignore_errors;
+ ignore_errors.IgnoreError(SQLITE_CORRUPT);
+ ASSERT_TRUE(db().Open(db_path()));
+ EXPECT_TRUE(db().FullIntegrityCheck(&messages));
+ EXPECT_LT(1u, messages.size());
+ EXPECT_NE(kOk, messages[0]);
+ ASSERT_TRUE(ignore_errors.CheckIgnoredErrors());
+ }
+
+ // TODO(shess): CorruptTableOrIndex could be used to produce a
+ // file that would pass the quick check and fail the full check.
+}
+
} // namespace
diff --git a/chromium/sql/meta_table.cc b/chromium/sql/meta_table.cc
index c7e803c1cc1..b1b95626f8a 100644
--- a/chromium/sql/meta_table.cc
+++ b/chromium/sql/meta_table.cc
@@ -5,16 +5,52 @@
#include "sql/meta_table.h"
#include "base/logging.h"
+#include "base/metrics/histogram.h"
#include "base/strings/string_util.h"
#include "sql/connection.h"
#include "sql/statement.h"
#include "sql/transaction.h"
-namespace sql {
+namespace {
// Key used in our meta table for version numbers.
-static const char kVersionKey[] = "version";
-static const char kCompatibleVersionKey[] = "last_compatible_version";
+const char kVersionKey[] = "version";
+const char kCompatibleVersionKey[] = "last_compatible_version";
+
+// Used to track success/failure of deprecation checks.
+enum DeprecationEventType {
+ // Database has info, but no meta table. This is probably bad.
+ DEPRECATION_DATABASE_NOT_EMPTY = 0,
+
+ // No meta, unable to query sqlite_master. This is probably bad.
+ DEPRECATION_DATABASE_UNKNOWN,
+
+ // Failure querying meta table, corruption or similar problem likely.
+ DEPRECATION_FAILED_VERSION,
+
+ // Version key not found in meta table. Some sort of update error likely.
+ DEPRECATION_NO_VERSION,
+
+ // Version was out-dated, database successfully razed. Should only
+ // happen once per long-idle user, low volume expected.
+ DEPRECATION_RAZED,
+
+ // Version was out-dated, database raze failed. This user's
+ // database will be stuck.
+ DEPRECATION_RAZE_FAILED,
+
+ // Always keep this at the end.
+ DEPRECATION_EVENT_MAX,
+};
+
+void RecordDeprecationEvent(DeprecationEventType deprecation_event) {
+ UMA_HISTOGRAM_ENUMERATION("Sqlite.DeprecationVersionResult",
+ deprecation_event, DEPRECATION_EVENT_MAX);
+}
+
+} // namespace
+
+namespace sql {
MetaTable::MetaTable() : db_(NULL) {
}
@@ -28,6 +64,54 @@ bool MetaTable::DoesTableExist(sql::Connection* db) {
return db->DoesTableExist("meta");
}
+// static
+void MetaTable::RazeIfDeprecated(Connection* db, int deprecated_version) {
+ DCHECK_GT(deprecated_version, 0);
+ DCHECK_EQ(0, db->transaction_nesting());
+
+ if (!DoesTableExist(db)) {
+ sql::Statement s(db->GetUniqueStatement(
+ "SELECT COUNT(*) FROM sqlite_master"));
+ if (s.Step()) {
+ if (s.ColumnInt(0) != 0) {
+ RecordDeprecationEvent(DEPRECATION_DATABASE_NOT_EMPTY);
+ }
+ // NOTE(shess): Empty database at first run is expected, so
+ // don't histogram that case.
+ } else {
+ RecordDeprecationEvent(DEPRECATION_DATABASE_UNKNOWN);
+ }
+ return;
+ }
+
+ // TODO(shess): Share sql with PrepareGetStatement().
+ sql::Statement s(db->GetUniqueStatement(
+ "SELECT value FROM meta WHERE key=?"));
+ s.BindCString(0, kVersionKey);
+ if (!s.Step()) {
+ if (!s.Succeeded()) {
+ RecordDeprecationEvent(DEPRECATION_FAILED_VERSION);
+ } else {
+ RecordDeprecationEvent(DEPRECATION_NO_VERSION);
+ }
+ return;
+ }
+
+ int version = s.ColumnInt(0);
+ s.Clear(); // Clear potential automatic transaction for Raze().
+ if (version <= deprecated_version) {
+ if (db->Raze()) {
+ RecordDeprecationEvent(DEPRECATION_RAZED);
+ } else {
+ RecordDeprecationEvent(DEPRECATION_RAZE_FAILED);
+ }
+ return;
+ }
+
+ // NOTE(shess): Successfully getting a version which is not
+ // deprecated is expected, so don't histogram that case.
+}
+
bool MetaTable::Init(Connection* db, int version, int compatible_version) {
DCHECK(!db_ && db);
db_ = db;
diff --git a/chromium/sql/meta_table.h b/chromium/sql/meta_table.h
index 0f4ee72c3f9..918a2614ac8 100644
--- a/chromium/sql/meta_table.h
+++ b/chromium/sql/meta_table.h
@@ -23,6 +23,20 @@ class SQL_EXPORT MetaTable {
// Returns true if the 'meta' table exists.
static bool DoesTableExist(Connection* db);
+ // If the current version of the database is less than or equal to
+ // |deprecated_version|, raze the database. Must be called outside
+ // of a transaction.
+ // TODO(shess): At this time the database is razed IFF meta exists
+ // and contains a version row with value <= deprecated_version. It
+ // may make sense to also raze if meta exists but has no version
+ // row, or if meta doesn't exist. In those cases if the database is
+ // not already empty, it probably resulted from a broken
+ // initialization.
+ // TODO(shess): Folding this into Init() would allow enforcing
+ // |deprecated_version|<|version|. But Init() is often called in a
+ // transaction.
+ static void RazeIfDeprecated(Connection* db, int deprecated_version);
+
// Initializes the MetaTableHelper, creating the meta table if necessary. For
// new tables, it will initialize the version number to |version| and the
// compatible version number to |compatible_version|. Versions must be
diff --git a/chromium/sql/meta_table_unittest.cc b/chromium/sql/meta_table_unittest.cc
index 3fbc499f417..2ffb4bdb568 100644
--- a/chromium/sql/meta_table_unittest.cc
+++ b/chromium/sql/meta_table_unittest.cc
@@ -41,6 +41,53 @@ TEST_F(SQLMetaTableTest, DoesTableExist) {
EXPECT_TRUE(sql::MetaTable::DoesTableExist(&db()));
}
+TEST_F(SQLMetaTableTest, RazeIfDeprecated) {
+ const int kDeprecatedVersion = 1;
+ const int kVersion = 2;
+
+ // Setup a current database.
+ {
+ sql::MetaTable meta_table;
+ EXPECT_TRUE(meta_table.Init(&db(), kVersion, kVersion));
+ EXPECT_TRUE(db().Execute("CREATE TABLE t(c)"));
+ EXPECT_TRUE(db().DoesTableExist("t"));
+ }
+
+ // Table should should still exist if the database version is new enough.
+ sql::MetaTable::RazeIfDeprecated(&db(), kDeprecatedVersion);
+ EXPECT_TRUE(db().DoesTableExist("t"));
+
+ // TODO(shess): It may make sense to Raze() if meta isn't present or
+ // version isn't present. See meta_table.h TODO on RazeIfDeprecated().
+
+ // Table should still exist if the version is not available.
+ EXPECT_TRUE(db().Execute("DELETE FROM meta WHERE key = 'version'"));
+ {
+ sql::MetaTable meta_table;
+ EXPECT_TRUE(meta_table.Init(&db(), kVersion, kVersion));
+ EXPECT_EQ(0, meta_table.GetVersionNumber());
+ }
+ sql::MetaTable::RazeIfDeprecated(&db(), kDeprecatedVersion);
+ EXPECT_TRUE(db().DoesTableExist("t"));
+
+ // Table should still exist if meta table is missing.
+ EXPECT_TRUE(db().Execute("DROP TABLE meta"));
+ sql::MetaTable::RazeIfDeprecated(&db(), kDeprecatedVersion);
+ EXPECT_TRUE(db().DoesTableExist("t"));
+
+ // Setup meta with deprecated version.
+ {
+ sql::MetaTable meta_table;
+ EXPECT_TRUE(meta_table.Init(&db(), kDeprecatedVersion, kDeprecatedVersion));
+ }
+
+ // Deprecation check should remove the table.
+ EXPECT_TRUE(db().DoesTableExist("t"));
+ sql::MetaTable::RazeIfDeprecated(&db(), kDeprecatedVersion);
+ EXPECT_FALSE(sql::MetaTable::DoesTableExist(&db()));
+ EXPECT_FALSE(db().DoesTableExist("t"));
+}
+
TEST_F(SQLMetaTableTest, VersionNumber) {
// Compatibility versions one less than the main versions to make
// sure the values aren't being crossed with each other.
diff --git a/chromium/sql/recovery.cc b/chromium/sql/recovery.cc
index 9016f8a7eff..42f1a9af6e9 100644
--- a/chromium/sql/recovery.cc
+++ b/chromium/sql/recovery.cc
@@ -5,13 +5,103 @@
#include "sql/recovery.h"
#include "base/files/file_path.h"
+#include "base/format_macros.h"
#include "base/logging.h"
+#include "base/metrics/histogram.h"
#include "base/metrics/sparse_histogram.h"
+#include "base/strings/string_util.h"
+#include "base/strings/stringprintf.h"
#include "sql/connection.h"
+#include "sql/statement.h"
#include "third_party/sqlite/sqlite3.h"
namespace sql {
+namespace {
+
+enum RecoveryEventType {
+ // Init() completed successfully.
+ RECOVERY_SUCCESS_INIT = 0,
+
+ // Failed to open temporary database to recover into.
+ RECOVERY_FAILED_OPEN_TEMPORARY,
+
+ // Failed to initialize recover vtable system.
+ RECOVERY_FAILED_VIRTUAL_TABLE_INIT,
+
+ // System SQLite doesn't support vtable.
+ RECOVERY_FAILED_VIRTUAL_TABLE_SYSTEM_SQLITE,
+
+ // Failed attempting to enable writable_schema.
+ RECOVERY_FAILED_WRITABLE_SCHEMA,
+
+ // Failed to attach the corrupt database to the temporary database.
+ RECOVERY_FAILED_ATTACH,
+
+ // Backup() successfully completed.
+ RECOVERY_SUCCESS_BACKUP,
+
+ // Failed sqlite3_backup_init(). Error code in Sqlite.RecoveryHandle.
+ RECOVERY_FAILED_BACKUP_INIT,
+
+ // Failed sqlite3_backup_step(). Error code in Sqlite.RecoveryStep.
+ RECOVERY_FAILED_BACKUP_STEP,
+
+ // AutoRecoverTable() successfully completed.
+ RECOVERY_SUCCESS_AUTORECOVER,
+
+ // The target table contained a type which the code is not equipped
+ // to handle. This should only happen if things are fubar.
+ RECOVERY_FAILED_AUTORECOVER_UNRECOGNIZED_TYPE,
+
+ // The target table does not exist.
+ RECOVERY_FAILED_AUTORECOVER_MISSING_TABLE,
+
+ // The recovery virtual table creation failed.
+ RECOVERY_FAILED_AUTORECOVER_CREATE,
+
+ // Copying data from the recovery table to the target table failed.
+ RECOVERY_FAILED_AUTORECOVER_INSERT,
+
+ // Dropping the recovery virtual table failed.
+ RECOVERY_FAILED_AUTORECOVER_DROP,
+
+ // SetupMeta() successfully completed.
+ RECOVERY_SUCCESS_SETUP_META,
+
+ // Failure creating recovery meta table.
+ RECOVERY_FAILED_META_CREATE,
+
+ // GetMetaVersionNumber() successfully completed.
+ RECOVERY_SUCCESS_META_VERSION,
+
+ // Failed in querying recovery meta table.
+ RECOVERY_FAILED_META_QUERY,
+
+ // No version key in recovery meta table.
+ RECOVERY_FAILED_META_NO_VERSION,
+
+ // Always keep this at the end.
+ RECOVERY_EVENT_MAX,
+};
+
+void RecordRecoveryEvent(RecoveryEventType recovery_event) {
+ UMA_HISTOGRAM_ENUMERATION("Sqlite.RecoveryEvents",
+ recovery_event, RECOVERY_EVENT_MAX);
+}
+
+} // namespace
+
+// static
+bool Recovery::FullRecoverySupported() {
+ // TODO(shess): See comment in Init().
+#if defined(USE_SYSTEM_SQLITE)
+ return false;
+#else
+ return true;
+#endif
+}
+
// static
scoped_ptr<Recovery> Recovery::Begin(
Connection* connection,
@@ -37,6 +127,13 @@ void Recovery::Unrecoverable(scoped_ptr<Recovery> r) {
// ~Recovery() will RAZE_AND_POISON.
}
+// static
+void Recovery::Rollback(scoped_ptr<Recovery> r) {
+ // TODO(shess): HISTOGRAM to track? Or just have people crash out?
+ // Crash and dump?
+ r->Shutdown(POISON);
+}
+
Recovery::Recovery(Connection* connection)
: db_(connection),
recover_db_() {
@@ -69,8 +166,26 @@ bool Recovery::Init(const base::FilePath& db_path) {
// more complicated.
db_->RollbackAllTransactions();
- if (!recover_db_.OpenTemporary())
+ // Disable exclusive locking mode so that the attached database can
+ // access things. The locking_mode change is not active until the
+ // next database access, so immediately force an access. Enabling
+ // writable_schema allows processing through certain kinds of
+ // corruption.
+ // TODO(shess): It would be better to just close the handle, but it
+ // is necessary for the final backup which rewrites things. It
+ // might be reasonable to close then re-open the handle.
+ ignore_result(db_->Execute("PRAGMA writable_schema=1"));
+ ignore_result(db_->Execute("PRAGMA locking_mode=NORMAL"));
+ ignore_result(db_->Execute("SELECT COUNT(*) FROM sqlite_master"));
+
+ // TODO(shess): If this is a common failure case, it might be
+ // possible to fall back to a memory database. But it probably
+ // implies that the SQLite tmpdir logic is busted, which could cause
+ // a variety of other random issues in our code.
+ if (!recover_db_.OpenTemporary()) {
+ RecordRecoveryEvent(RECOVERY_FAILED_OPEN_TEMPORARY);
return false;
+ }
// TODO(shess): Figure out a story for USE_SYSTEM_SQLITE. The
// virtual table implementation relies on SQLite internals for some
@@ -84,20 +199,29 @@ bool Recovery::Init(const base::FilePath& db_path) {
#if !defined(USE_SYSTEM_SQLITE)
int rc = recoverVtableInit(recover_db_.db_);
if (rc != SQLITE_OK) {
+ RecordRecoveryEvent(RECOVERY_FAILED_VIRTUAL_TABLE_INIT);
LOG(ERROR) << "Failed to initialize recover module: "
<< recover_db_.GetErrorMessage();
return false;
}
+#else
+ // If this is infrequent enough, just wire it to Raze().
+ RecordRecoveryEvent(RECOVERY_FAILED_VIRTUAL_TABLE_SYSTEM_SQLITE);
#endif
// Turn on |SQLITE_RecoveryMode| for the handle, which allows
// reading certain broken databases.
- if (!recover_db_.Execute("PRAGMA writable_schema=1"))
+ if (!recover_db_.Execute("PRAGMA writable_schema=1")) {
+ RecordRecoveryEvent(RECOVERY_FAILED_WRITABLE_SCHEMA);
return false;
+ }
- if (!recover_db_.AttachDatabase(db_path, "corrupt"))
+ if (!recover_db_.AttachDatabase(db_path, "corrupt")) {
+ RecordRecoveryEvent(RECOVERY_FAILED_ATTACH);
return false;
+ }
+ RecordRecoveryEvent(RECOVERY_SUCCESS_INIT);
return true;
}
@@ -140,11 +264,14 @@ bool Recovery::Backup() {
sqlite3_backup* backup = sqlite3_backup_init(db_->db_, kMain,
recover_db_.db_, kMain);
if (!backup) {
+ RecordRecoveryEvent(RECOVERY_FAILED_BACKUP_INIT);
+
// Error code is in the destination database handle.
- int err = sqlite3_errcode(db_->db_);
+ int err = sqlite3_extended_errcode(db_->db_);
UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryHandle", err);
LOG(ERROR) << "sqlite3_backup_init() failed: "
<< sqlite3_errmsg(db_->db_);
+
return false;
}
@@ -159,6 +286,7 @@ bool Recovery::Backup() {
DCHECK_GT(pages, 0);
if (rc != SQLITE_DONE) {
+ RecordRecoveryEvent(RECOVERY_FAILED_BACKUP_STEP);
UMA_HISTOGRAM_SPARSE_SLOWLY("Sqlite.RecoveryStep", rc);
LOG(ERROR) << "sqlite3_backup_step() failed: "
<< sqlite3_errmsg(db_->db_);
@@ -187,6 +315,7 @@ bool Recovery::Backup() {
// Clean up the recovery db, and terminate the main database
// connection.
+ RecordRecoveryEvent(RECOVERY_SUCCESS_BACKUP);
Shutdown(POISON);
return true;
}
@@ -204,4 +333,204 @@ void Recovery::Shutdown(Recovery::Disposition raze) {
db_ = NULL;
}
+bool Recovery::AutoRecoverTable(const char* table_name,
+ size_t extend_columns,
+ size_t* rows_recovered) {
+ // Query the info for the recovered table in database [main].
+ std::string query(
+ base::StringPrintf("PRAGMA main.table_info(%s)", table_name));
+ Statement s(db()->GetUniqueStatement(query.c_str()));
+
+ // The columns of the recover virtual table.
+ std::vector<std::string> create_column_decls;
+
+ // The columns to select from the recover virtual table when copying
+ // to the recovered table.
+ std::vector<std::string> insert_columns;
+
+ // If PRIMARY KEY is a single INTEGER column, then it is an alias
+ // for ROWID. The primary key can be compound, so this can only be
+ // determined after processing all column data and tracking what is
+ // seen. |pk_column_count| counts the columns in the primary key.
+ // |rowid_decl| stores the ROWID version of the last INTEGER column
+ // seen, which is at |rowid_ofs| in |create_column_decls|.
+ size_t pk_column_count = 0;
+ size_t rowid_ofs; // Only valid if rowid_decl is set.
+ std::string rowid_decl; // ROWID version of column |rowid_ofs|.
+
+ while (s.Step()) {
+ const std::string column_name(s.ColumnString(1));
+ const std::string column_type(s.ColumnString(2));
+ const bool not_null = s.ColumnBool(3);
+ const int default_type = s.ColumnType(4);
+ const bool default_is_null = (default_type == COLUMN_TYPE_NULL);
+ const int pk_column = s.ColumnInt(5);
+
+ if (pk_column > 0) {
+ // TODO(shess): http://www.sqlite.org/pragma.html#pragma_table_info
+ // documents column 5 as the index of the column in the primary key
+ // (zero for not in primary key). I find that it is always 1 for
+ // columns in the primary key. Since this code is very dependent on
+ // that pragma, review if the implementation changes.
+ DCHECK_EQ(pk_column, 1);
+ ++pk_column_count;
+ }
+
+ // Construct column declaration as "name type [optional constraint]".
+ std::string column_decl = column_name;
+
+ // SQLite's affinity detection is documented at:
+ // http://www.sqlite.org/datatype3.html#affname
+ // The gist of it is that CHAR, TEXT, and INT use substring matches.
+ // TODO(shess): It would be nice to unit test the type handling,
+ // but it is not obvious to me how to write a test which would
+ // fail appropriately when something was broken. It would have to
+ // somehow use data which would allow detecting the various type
+ // coercions which happen. If STRICT could be enabled, type
+ // mismatches could be detected by which rows are filtered.
+ if (column_type.find("INT") != std::string::npos) {
+ if (pk_column == 1) {
+ rowid_ofs = create_column_decls.size();
+ rowid_decl = column_name + " ROWID";
+ }
+ column_decl += " INTEGER";
+ } else if (column_type.find("CHAR") != std::string::npos ||
+ column_type.find("TEXT") != std::string::npos) {
+ column_decl += " TEXT";
+ } else if (column_type == "BLOB") {
+ column_decl += " BLOB";
+ } else if (column_type.find("DOUB") != std::string::npos) {
+ column_decl += " FLOAT";
+ } else {
+ // TODO(shess): AFAICT, there remain:
+ // - contains("CLOB") -> TEXT
+ // - contains("REAL") -> FLOAT
+ // - contains("FLOA") -> FLOAT
+ // - other -> "NUMERIC"
+ // Just code those in as they come up.
+ NOTREACHED() << " Unsupported type " << column_type;
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_UNRECOGNIZED_TYPE);
+ return false;
+ }
+
+ // If column has constraint "NOT NULL", then inserting NULL into
+ // that column will fail. If the column has a non-NULL DEFAULT
+ // specified, the INSERT will handle it (see below). If the
+ // DEFAULT is also NULL, the row must be filtered out.
+ // TODO(shess): The above scenario applies to INSERT OR REPLACE,
+ // whereas INSERT OR IGNORE drops such rows.
+ // http://www.sqlite.org/lang_conflict.html
+ if (not_null && default_is_null)
+ column_decl += " NOT NULL";
+
+ create_column_decls.push_back(column_decl);
+
+ // Per the NOTE in the header file, convert NULL values to the
+ // DEFAULT. All columns could be IFNULL(column_name,default), but
+ // the NULL case would require special handling either way.
+ if (default_is_null) {
+ insert_columns.push_back(column_name);
+ } else {
+ // The default value appears to be pre-quoted, as if it is
+ // literally from the sqlite_master CREATE statement.
+ std::string default_value = s.ColumnString(4);
+ insert_columns.push_back(base::StringPrintf(
+ "IFNULL(%s,%s)", column_name.c_str(), default_value.c_str()));
+ }
+ }
+
+ // Receiving no column information implies that the table doesn't exist.
+ if (create_column_decls.empty()) {
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_MISSING_TABLE);
+ return false;
+ }
+
+ // If the PRIMARY KEY was a single INTEGER column, convert it to ROWID.
+ if (pk_column_count == 1 && !rowid_decl.empty())
+ create_column_decls[rowid_ofs] = rowid_decl;
+
+ // Additional columns accept anything.
+ // TODO(shess): ignoreN isn't well namespaced. But it will fail to
+ // execute in case of conflicts.
+ for (size_t i = 0; i < extend_columns; ++i) {
+ create_column_decls.push_back(
+ base::StringPrintf("ignore%" PRIuS " ANY", i));
+ }
+
+ std::string recover_create(base::StringPrintf(
+ "CREATE VIRTUAL TABLE temp.recover_%s USING recover(corrupt.%s, %s)",
+ table_name,
+ table_name,
+ JoinString(create_column_decls, ',').c_str()));
+
+ std::string recover_insert(base::StringPrintf(
+ "INSERT OR REPLACE INTO main.%s SELECT %s FROM temp.recover_%s",
+ table_name,
+ JoinString(insert_columns, ',').c_str(),
+ table_name));
+
+ std::string recover_drop(base::StringPrintf(
+ "DROP TABLE temp.recover_%s", table_name));
+
+ if (!db()->Execute(recover_create.c_str())) {
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_CREATE);
+ return false;
+ }
+
+ if (!db()->Execute(recover_insert.c_str())) {
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_INSERT);
+ ignore_result(db()->Execute(recover_drop.c_str()));
+ return false;
+ }
+
+ *rows_recovered = db()->GetLastChangeCount();
+
+ // TODO(shess): Is leaving the recover table around a breaker?
+ if (!db()->Execute(recover_drop.c_str())) {
+ RecordRecoveryEvent(RECOVERY_FAILED_AUTORECOVER_DROP);
+ return false;
+ }
+ RecordRecoveryEvent(RECOVERY_SUCCESS_AUTORECOVER);
+ return true;
+}
+
+bool Recovery::SetupMeta() {
+ const char kCreateSql[] =
+ "CREATE VIRTUAL TABLE temp.recover_meta USING recover"
+ "("
+ "corrupt.meta,"
+ "key TEXT NOT NULL,"
+ "value ANY" // Whatever is stored.
+ ")";
+ if (!db()->Execute(kCreateSql)) {
+ RecordRecoveryEvent(RECOVERY_FAILED_META_CREATE);
+ return false;
+ }
+ RecordRecoveryEvent(RECOVERY_SUCCESS_SETUP_META);
+ return true;
+}
+
+bool Recovery::GetMetaVersionNumber(int* version) {
+ DCHECK(version);
+ // TODO(shess): DCHECK(db()->DoesTableExist("temp.recover_meta"));
+ // Unfortunately, DoesTableExist() queries sqlite_master, not
+ // sqlite_temp_master.
+
+ const char kVersionSql[] =
+ "SELECT value FROM temp.recover_meta WHERE key = 'version'";
+ sql::Statement recovery_version(db()->GetUniqueStatement(kVersionSql));
+ if (!recovery_version.Step()) {
+ if (!recovery_version.Succeeded()) {
+ RecordRecoveryEvent(RECOVERY_FAILED_META_QUERY);
+ } else {
+ RecordRecoveryEvent(RECOVERY_FAILED_META_NO_VERSION);
+ }
+ return false;
+ }
+
+ RecordRecoveryEvent(RECOVERY_SUCCESS_META_VERSION);
+ *version = recovery_version.ColumnInt(0);
+ return true;
+}
+
} // namespace sql
diff --git a/chromium/sql/recovery.h b/chromium/sql/recovery.h
index c0bb6da236d..60fb6747b83 100644
--- a/chromium/sql/recovery.h
+++ b/chromium/sql/recovery.h
@@ -27,10 +27,28 @@ namespace sql {
// scoped_ptr<sql::Recovery> r =
// sql::Recovery::Begin(orig_db, orig_db_path);
// if (r) {
-// if (r.db()->Execute(kCreateSchemaSql) &&
-// r.db()->Execute(kCopyDataFromOrigSql)) {
-// sql::Recovery::Recovered(r.Pass());
+// // Create the schema to recover to. On failure, clear the
+// // database.
+// if (!r.db()->Execute(kCreateSchemaSql)) {
+// sql::Recovery::Unrecoverable(r.Pass());
+// return;
// }
+//
+// // Recover data in "mytable".
+// size_t rows_recovered = 0;
+// if (!r.AutoRecoverTable("mytable", 0, &rows_recovered)) {
+// sql::Recovery::Unrecoverable(r.Pass());
+// return;
+// }
+//
+// // Manually cleanup additional constraints.
+// if (!r.db()->Execute(kCleanupSql)) {
+// sql::Recovery::Unrecoverable(r.Pass());
+// return;
+// }
+//
+// // Commit the recovered data to the original database file.
+// sql::Recovery::Recovered(r.Pass());
// }
// }
//
@@ -41,6 +59,15 @@ class SQL_EXPORT Recovery {
public:
~Recovery();
+ // This module is intended to be used in concert with a virtual
+ // table module (see third_party/sqlite/src/src/recover.c). If the
+ // build defines USE_SYSTEM_SQLITE, this module will not be present.
+ // TODO(shess): I am still debating how to handle this - perhaps it
+ // will just imply Unrecoverable(). This is exposed to allow tests
+ // to adapt to the cases, please do not rely on it in production
+ // code.
+ static bool FullRecoverySupported();
+
// Begin the recovery process by opening a temporary database handle
// and attach the existing database to it at "corrupt". To prevent
// deadlock, all transactions on |connection| are rolled back.
@@ -64,7 +91,7 @@ class SQL_EXPORT Recovery {
// If Recovered() is not called, the destructor will call
// Unrecoverable().
//
- // TODO(shess): At this time, this function an fail while leaving
+ // TODO(shess): At this time, this function can fail while leaving
// the original database intact. Figure out which failure cases
// should go to RazeAndClose() instead.
static bool Recovered(scoped_ptr<Recovery> r) WARN_UNUSED_RESULT;
@@ -73,9 +100,57 @@ class SQL_EXPORT Recovery {
// database is razed, and the handle poisoned.
static void Unrecoverable(scoped_ptr<Recovery> r);
+ // When initially developing recovery code, sometimes the possible
+ // database states are not well-understood without further
+ // diagnostics. Abandon recovery but do not raze the original
+ // database.
+ // NOTE(shess): Only call this when adding recovery support. In the
+ // steady state, all databases should progress to recovered or razed.
+ static void Rollback(scoped_ptr<Recovery> r);
+
// Handle to the temporary recovery database.
sql::Connection* db() { return &recover_db_; }
+ // Attempt to recover the named table from the corrupt database into
+ // the recovery database using a temporary recover virtual table.
+ // The virtual table schema is derived from the named table's schema
+ // in database [main]. Data is copied using INSERT OR REPLACE, so
+ // duplicates overwrite each other.
+ //
+ // |extend_columns| allows recovering tables which have excess
+ // columns relative to the target schema. The recover virtual table
+ // treats more data than specified as a sign of corruption.
+ //
+ // Returns true if all operations succeeded, with the number of rows
+ // recovered in |*rows_recovered|.
+ //
+ // NOTE(shess): Due to a flaw in the recovery virtual table, at this
+ // time this code injects the DEFAULT value of the target table in
+ // locations where the recovery table returns NULL. This is not
+ // entirely correct, because it happens both when there is a short
+ // row (correct) but also where there is an actual NULL value
+ // (incorrect).
+ //
+ // TODO(shess): Flag for INSERT OR REPLACE vs IGNORE.
+ // TODO(shess): Handle extended table names.
+ bool AutoRecoverTable(const char* table_name,
+ size_t extend_columns,
+ size_t* rows_recovered);
+
+ // Setup a recover virtual table at temp.recover_meta, reading from
+ // corrupt.meta. Returns true if created.
+ // TODO(shess): Perhaps integrate into Begin().
+ // TODO(shess): Add helpers to fetch additional items from the meta
+ // table as needed.
+ bool SetupMeta();
+
+ // Fetch the version number from temp.recover_meta. Returns false
+ // if the query fails, or if there is no version row. Otherwise
+ // returns true, with the version in |*version_number|.
+ //
+ // Only valid to call after successful SetupMeta().
+ bool GetMetaVersionNumber(int* version_number);
+
private:
explicit Recovery(Connection* connection);
diff --git a/chromium/sql/recovery_unittest.cc b/chromium/sql/recovery_unittest.cc
index fc7c2f2dc5b..ce3884bbc08 100644
--- a/chromium/sql/recovery_unittest.cc
+++ b/chromium/sql/recovery_unittest.cc
@@ -2,16 +2,18 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <string>
+
#include "base/bind.h"
-#include "base/file_util.h"
+#include "base/files/file_path.h"
#include "base/files/scoped_temp_dir.h"
-#include "base/logging.h"
-#include "base/strings/stringprintf.h"
+#include "base/strings/string_number_conversions.h"
#include "sql/connection.h"
#include "sql/meta_table.h"
#include "sql/recovery.h"
#include "sql/statement.h"
#include "sql/test/scoped_error_ignorer.h"
+#include "sql/test/test_helpers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/sqlite/sqlite3.h"
@@ -32,7 +34,15 @@ std::string ExecuteWithResults(sql::Connection* db,
for (int i = 0; i < s.ColumnCount(); ++i) {
if (i > 0)
ret += column_sep;
- ret += s.ColumnString(i);
+ if (s.ColumnType(i) == sql::COLUMN_TYPE_NULL) {
+ ret += "<null>";
+ } else if (s.ColumnType(i) == sql::COLUMN_TYPE_BLOB) {
+ ret += "<x'";
+ ret += base::HexEncode(s.ColumnBlob(i), s.ColumnByteLength(i));
+ ret += "'>";
+ } else {
+ ret += s.ColumnString(i);
+ }
}
}
return ret;
@@ -48,49 +58,6 @@ std::string GetSchema(sql::Connection* db) {
return ExecuteWithResults(db, kSql, "|", "\n");
}
-int GetPageSize(sql::Connection* db) {
- sql::Statement s(db->GetUniqueStatement("PRAGMA page_size"));
- EXPECT_TRUE(s.Step());
- return s.ColumnInt(0);
-}
-
-// Get |name|'s root page number in the database.
-int GetRootPage(sql::Connection* db, const char* name) {
- const char kPageSql[] = "SELECT rootpage FROM sqlite_master WHERE name = ?";
- sql::Statement s(db->GetUniqueStatement(kPageSql));
- s.BindString(0, name);
- EXPECT_TRUE(s.Step());
- return s.ColumnInt(0);
-}
-
-// Helper to read a SQLite page into a buffer. |page_no| is 1-based
-// per SQLite usage.
-bool ReadPage(const base::FilePath& path, size_t page_no,
- char* buf, size_t page_size) {
- file_util::ScopedFILE file(file_util::OpenFile(path, "rb"));
- if (!file.get())
- return false;
- if (0 != fseek(file.get(), (page_no - 1) * page_size, SEEK_SET))
- return false;
- if (1u != fread(buf, page_size, 1, file.get()))
- return false;
- return true;
-}
-
-// Helper to write a SQLite page into a buffer. |page_no| is 1-based
-// per SQLite usage.
-bool WritePage(const base::FilePath& path, size_t page_no,
- const char* buf, size_t page_size) {
- file_util::ScopedFILE file(file_util::OpenFile(path, "rb+"));
- if (!file.get())
- return false;
- if (0 != fseek(file.get(), (page_no - 1) * page_size, SEEK_SET))
- return false;
- if (1u != fwrite(buf, page_size, 1, file.get()))
- return false;
- return true;
-}
-
class SQLRecoveryTest : public testing::Test {
public:
SQLRecoveryTest() {}
@@ -285,24 +252,12 @@ TEST_F(SQLRecoveryTest, RecoverCorruptIndex) {
ASSERT_TRUE(db().CommitTransaction());
}
-
-
- // Capture the index's root page into |buf|.
- int index_page = GetRootPage(&db(), "x_id");
- int page_size = GetPageSize(&db());
- scoped_ptr<char[]> buf(new char[page_size]);
- ASSERT_TRUE(ReadPage(db_path(), index_page, buf.get(), page_size));
-
- // Delete the row from the table and index.
- ASSERT_TRUE(db().Execute("DELETE FROM x WHERE id = 0"));
-
- // Close to clear any cached data.
db().Close();
- // Put the stale index page back.
- ASSERT_TRUE(WritePage(db_path(), index_page, buf.get(), page_size));
-
- // At this point, the index references a value not in the table.
+ // Delete a row from the table, while leaving the index entry which
+ // references it.
+ const char kDeleteSql[] = "DELETE FROM x WHERE id = 0";
+ ASSERT_TRUE(sql::test::CorruptTableOrIndex(db_path(), "x_id", kDeleteSql));
ASSERT_TRUE(Reopen());
@@ -357,25 +312,12 @@ TEST_F(SQLRecoveryTest, RecoverCorruptTable) {
ASSERT_TRUE(db().CommitTransaction());
}
-
- // Capture the table's root page into |buf|.
- // Find the page the table is stored on.
- const int table_page = GetRootPage(&db(), "x");
- const int page_size = GetPageSize(&db());
- scoped_ptr<char[]> buf(new char[page_size]);
- ASSERT_TRUE(ReadPage(db_path(), table_page, buf.get(), page_size));
-
- // Delete the row from the table and index.
- ASSERT_TRUE(db().Execute("DELETE FROM x WHERE id = 0"));
-
- // Close to clear any cached data.
db().Close();
- // Put the stale table page back.
- ASSERT_TRUE(WritePage(db_path(), table_page, buf.get(), page_size));
+ // Delete a row from the index while leaving a table entry.
+ const char kDeleteSql[] = "DELETE FROM x WHERE id = 0";
+ ASSERT_TRUE(sql::test::CorruptTableOrIndex(db_path(), "x", kDeleteSql));
- // At this point, the table contains a value not referenced by the
- // index.
// TODO(shess): Figure out a query which causes SQLite to notice
// this organically. Meanwhile, just handle it manually.
@@ -420,6 +362,327 @@ TEST_F(SQLRecoveryTest, RecoverCorruptTable) {
const char kSelectSql[] = "SELECT v FROM x WHERE id = 0";
EXPECT_EQ("100", ExecuteWithResults(&db(), kSelectSql, "|", ","));
}
+
+TEST_F(SQLRecoveryTest, Meta) {
+ const int kVersion = 3;
+ const int kCompatibleVersion = 2;
+
+ {
+ sql::MetaTable meta;
+ EXPECT_TRUE(meta.Init(&db(), kVersion, kCompatibleVersion));
+ EXPECT_EQ(kVersion, meta.GetVersionNumber());
+ }
+
+ // Test expected case where everything works.
+ {
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ EXPECT_TRUE(recovery->SetupMeta());
+ int version = 0;
+ EXPECT_TRUE(recovery->GetMetaVersionNumber(&version));
+ EXPECT_EQ(kVersion, version);
+
+ sql::Recovery::Rollback(recovery.Pass());
+ }
+ ASSERT_TRUE(Reopen()); // Handle was poisoned.
+
+ // Test version row missing.
+ EXPECT_TRUE(db().Execute("DELETE FROM meta WHERE key = 'version'"));
+ {
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ EXPECT_TRUE(recovery->SetupMeta());
+ int version = 0;
+ EXPECT_FALSE(recovery->GetMetaVersionNumber(&version));
+ EXPECT_EQ(0, version);
+
+ sql::Recovery::Rollback(recovery.Pass());
+ }
+ ASSERT_TRUE(Reopen()); // Handle was poisoned.
+
+ // Test meta table missing.
+ EXPECT_TRUE(db().Execute("DROP TABLE meta"));
+ {
+ sql::ScopedErrorIgnorer ignore_errors;
+ ignore_errors.IgnoreError(SQLITE_CORRUPT); // From virtual table.
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ EXPECT_FALSE(recovery->SetupMeta());
+ ASSERT_TRUE(ignore_errors.CheckIgnoredErrors());
+ }
+}
+
+// Baseline AutoRecoverTable() test.
+TEST_F(SQLRecoveryTest, AutoRecoverTable) {
+ // BIGINT and VARCHAR to test type affinity.
+ const char kCreateSql[] = "CREATE TABLE x (id BIGINT, t TEXT, v VARCHAR)";
+ ASSERT_TRUE(db().Execute(kCreateSql));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (11, 'This is', 'a test')"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (5, 'That was', 'a test')"));
+
+ // Save aside a copy of the original schema and data.
+ const std::string orig_schema(GetSchema(&db()));
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1";
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n"));
+
+ // Create a lame-duck table which will not be propagated by recovery to
+ // detect that the recovery code actually ran.
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)"));
+ ASSERT_NE(orig_schema, GetSchema(&db()));
+
+ {
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql));
+
+ // Save a copy of the temp db's schema before recovering the table.
+ const char kTempSchemaSql[] = "SELECT name, sql FROM sqlite_temp_master";
+ const std::string temp_schema(
+ ExecuteWithResults(recovery->db(), kTempSchemaSql, "|", "\n"));
+
+ size_t rows = 0;
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows));
+ EXPECT_EQ(2u, rows);
+
+ // Test that any additional temp tables were cleaned up.
+ EXPECT_EQ(temp_schema,
+ ExecuteWithResults(recovery->db(), kTempSchemaSql, "|", "\n"));
+
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass()));
+ }
+
+ // Since the database was not corrupt, the entire schema and all
+ // data should be recovered.
+ ASSERT_TRUE(Reopen());
+ ASSERT_EQ(orig_schema, GetSchema(&db()));
+ ASSERT_EQ(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n"));
+
+ // Recovery fails if the target table doesn't exist.
+ {
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql));
+
+ // TODO(shess): Should this failure implicitly lead to Raze()?
+ size_t rows = 0;
+ EXPECT_FALSE(recovery->AutoRecoverTable("y", 0, &rows));
+
+ sql::Recovery::Unrecoverable(recovery.Pass());
+ }
+}
+
+// Test that default values correctly replace nulls. The recovery
+// virtual table reads directly from the database, so DEFAULT is not
+// interpretted at that level.
+TEST_F(SQLRecoveryTest, AutoRecoverTableWithDefault) {
+ ASSERT_TRUE(db().Execute("CREATE TABLE x (id INTEGER)"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (5)"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (15)"));
+
+ // ALTER effectively leaves the new columns NULL in the first two
+ // rows. The row with 17 will get the default injected at insert
+ // time, while the row with 42 will get the actual value provided.
+ // Embedded "'" to make sure default-handling continues to be quoted
+ // correctly.
+ ASSERT_TRUE(db().Execute("ALTER TABLE x ADD COLUMN t TEXT DEFAULT 'a''a'"));
+ ASSERT_TRUE(db().Execute("ALTER TABLE x ADD COLUMN b BLOB DEFAULT x'AA55'"));
+ ASSERT_TRUE(db().Execute("ALTER TABLE x ADD COLUMN i INT DEFAULT 93"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x (id) VALUES (17)"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (42, 'b', x'1234', 12)"));
+
+ // Save aside a copy of the original schema and data.
+ const std::string orig_schema(GetSchema(&db()));
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1";
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n"));
+
+ // Create a lame-duck table which will not be propagated by recovery to
+ // detect that the recovery code actually ran.
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)"));
+ ASSERT_NE(orig_schema, GetSchema(&db()));
+
+ // Mechanically adjust the stored schema and data to allow detecting
+ // where the default value is coming from. The target table is just
+ // like the original with the default for [t] changed, to signal
+ // defaults coming from the recovery system. The two %5 rows should
+ // get the target-table default for [t], while the others should get
+ // the source-table default.
+ std::string final_schema(orig_schema);
+ std::string final_data(orig_data);
+ size_t pos;
+ while ((pos = final_schema.find("'a''a'")) != std::string::npos) {
+ final_schema.replace(pos, 6, "'c''c'");
+ }
+ while ((pos = final_data.find("5|a'a")) != std::string::npos) {
+ final_data.replace(pos, 5, "5|c'c");
+ }
+
+ {
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ // Different default to detect which table provides the default.
+ ASSERT_TRUE(recovery->db()->Execute(final_schema.c_str()));
+
+ size_t rows = 0;
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows));
+ EXPECT_EQ(4u, rows);
+
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass()));
+ }
+
+ // Since the database was not corrupt, the entire schema and all
+ // data should be recovered.
+ ASSERT_TRUE(Reopen());
+ ASSERT_EQ(final_schema, GetSchema(&db()));
+ ASSERT_EQ(final_data, ExecuteWithResults(&db(), kXSql, "|", "\n"));
+}
+
+// Test that rows with NULL in a NOT NULL column are filtered
+// correctly. In the wild, this would probably happen due to
+// corruption, but here it is simulated by recovering a table which
+// allowed nulls into a table which does not.
+TEST_F(SQLRecoveryTest, AutoRecoverTableNullFilter) {
+ const char kOrigSchema[] = "CREATE TABLE x (id INTEGER, t TEXT)";
+ const char kFinalSchema[] = "CREATE TABLE x (id INTEGER, t TEXT NOT NULL)";
+
+ ASSERT_TRUE(db().Execute(kOrigSchema));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (5, null)"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (15, 'this is a test')"));
+
+ // Create a lame-duck table which will not be propagated by recovery to
+ // detect that the recovery code actually ran.
+ ASSERT_EQ(kOrigSchema, GetSchema(&db()));
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)"));
+ ASSERT_NE(kOrigSchema, GetSchema(&db()));
+
+ {
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ ASSERT_TRUE(recovery->db()->Execute(kFinalSchema));
+
+ size_t rows = 0;
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows));
+ EXPECT_EQ(1u, rows);
+
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass()));
+ }
+
+ // The schema should be the same, but only one row of data should
+ // have been recovered.
+ ASSERT_TRUE(Reopen());
+ ASSERT_EQ(kFinalSchema, GetSchema(&db()));
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1";
+ ASSERT_EQ("15|this is a test", ExecuteWithResults(&db(), kXSql, "|", "\n"));
+}
+
+// Test AutoRecoverTable with a ROWID alias.
+TEST_F(SQLRecoveryTest, AutoRecoverTableWithRowid) {
+ // The rowid alias is almost always the first column, intentionally
+ // put it later.
+ const char kCreateSql[] =
+ "CREATE TABLE x (t TEXT, id INTEGER PRIMARY KEY NOT NULL)";
+ ASSERT_TRUE(db().Execute(kCreateSql));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES ('This is a test', null)"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES ('That was a test', null)"));
+
+ // Save aside a copy of the original schema and data.
+ const std::string orig_schema(GetSchema(&db()));
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1";
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n"));
+
+ // Create a lame-duck table which will not be propagated by recovery to
+ // detect that the recovery code actually ran.
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)"));
+ ASSERT_NE(orig_schema, GetSchema(&db()));
+
+ {
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql));
+
+ size_t rows = 0;
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows));
+ EXPECT_EQ(2u, rows);
+
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass()));
+ }
+
+ // Since the database was not corrupt, the entire schema and all
+ // data should be recovered.
+ ASSERT_TRUE(Reopen());
+ ASSERT_EQ(orig_schema, GetSchema(&db()));
+ ASSERT_EQ(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n"));
+}
+
+// Test that a compound primary key doesn't fire the ROWID code.
+TEST_F(SQLRecoveryTest, AutoRecoverTableWithCompoundKey) {
+ const char kCreateSql[] =
+ "CREATE TABLE x ("
+ "id INTEGER NOT NULL,"
+ "id2 TEXT NOT NULL,"
+ "t TEXT,"
+ "PRIMARY KEY (id, id2)"
+ ")";
+ ASSERT_TRUE(db().Execute(kCreateSql));
+
+ // NOTE(shess): Do not accidentally use [id] 1, 2, 3, as those will
+ // be the ROWID values.
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (1, 'a', 'This is a test')"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (1, 'b', 'That was a test')"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (2, 'a', 'Another test')"));
+
+ // Save aside a copy of the original schema and data.
+ const std::string orig_schema(GetSchema(&db()));
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1";
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n"));
+
+ // Create a lame-duck table which will not be propagated by recovery to
+ // detect that the recovery code actually ran.
+ ASSERT_TRUE(db().Execute("CREATE TABLE y (c TEXT)"));
+ ASSERT_NE(orig_schema, GetSchema(&db()));
+
+ {
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql));
+
+ size_t rows = 0;
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 0, &rows));
+ EXPECT_EQ(3u, rows);
+
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass()));
+ }
+
+ // Since the database was not corrupt, the entire schema and all
+ // data should be recovered.
+ ASSERT_TRUE(Reopen());
+ ASSERT_EQ(orig_schema, GetSchema(&db()));
+ ASSERT_EQ(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n"));
+}
+
+// Test |extend_columns| support.
+TEST_F(SQLRecoveryTest, AutoRecoverTableExtendColumns) {
+ const char kCreateSql[] = "CREATE TABLE x (id INTEGER PRIMARY KEY, t0 TEXT)";
+ ASSERT_TRUE(db().Execute(kCreateSql));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (1, 'This is')"));
+ ASSERT_TRUE(db().Execute("INSERT INTO x VALUES (2, 'That was')"));
+
+ // Save aside a copy of the original schema and data.
+ const std::string orig_schema(GetSchema(&db()));
+ const char kXSql[] = "SELECT * FROM x ORDER BY 1";
+ const std::string orig_data(ExecuteWithResults(&db(), kXSql, "|", "\n"));
+
+ // Modify the table to add a column, and add data to that column.
+ ASSERT_TRUE(db().Execute("ALTER TABLE x ADD COLUMN t1 TEXT"));
+ ASSERT_TRUE(db().Execute("UPDATE x SET t1 = 'a test'"));
+ ASSERT_NE(orig_schema, GetSchema(&db()));
+ ASSERT_NE(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n"));
+
+ {
+ scoped_ptr<sql::Recovery> recovery = sql::Recovery::Begin(&db(), db_path());
+ ASSERT_TRUE(recovery->db()->Execute(kCreateSql));
+ size_t rows = 0;
+ EXPECT_TRUE(recovery->AutoRecoverTable("x", 1, &rows));
+ EXPECT_EQ(2u, rows);
+ ASSERT_TRUE(sql::Recovery::Recovered(recovery.Pass()));
+ }
+
+ // Since the database was not corrupt, the entire schema and all
+ // data should be recovered.
+ ASSERT_TRUE(Reopen());
+ ASSERT_EQ(orig_schema, GetSchema(&db()));
+ ASSERT_EQ(orig_data, ExecuteWithResults(&db(), kXSql, "|", "\n"));
+}
#endif // !defined(USE_SYSTEM_SQLITE)
} // namespace
diff --git a/chromium/sql/statement.cc b/chromium/sql/statement.cc
index da2c58fd69d..985497ea276 100644
--- a/chromium/sql/statement.cc
+++ b/chromium/sql/statement.cc
@@ -16,11 +16,13 @@ namespace sql {
// only have to check the ref's validity bit.
Statement::Statement()
: ref_(new Connection::StatementRef(NULL, NULL, false)),
+ stepped_(false),
succeeded_(false) {
}
Statement::Statement(scoped_refptr<Connection::StatementRef> ref)
: ref_(ref),
+ stepped_(false),
succeeded_(false) {
}
@@ -50,10 +52,12 @@ bool Statement::CheckValid() const {
}
bool Statement::Run() {
+ DCHECK(!stepped_);
ref_->AssertIOAllowed();
if (!CheckValid())
return false;
+ stepped_ = true;
return CheckError(sqlite3_step(ref_->stmt())) == SQLITE_DONE;
}
@@ -62,6 +66,7 @@ bool Statement::Step() {
if (!CheckValid())
return false;
+ stepped_ = true;
return CheckError(sqlite3_step(ref_->stmt())) == SQLITE_ROW;
}
@@ -77,6 +82,7 @@ void Statement::Reset(bool clear_bound_vars) {
}
succeeded_ = false;
+ stepped_ = false;
}
bool Statement::Succeeded() const {
@@ -87,6 +93,7 @@ bool Statement::Succeeded() const {
}
bool Statement::BindNull(int col) {
+ DCHECK(!stepped_);
if (!is_valid())
return false;
@@ -98,6 +105,7 @@ bool Statement::BindBool(int col, bool val) {
}
bool Statement::BindInt(int col, int val) {
+ DCHECK(!stepped_);
if (!is_valid())
return false;
@@ -105,6 +113,7 @@ bool Statement::BindInt(int col, int val) {
}
bool Statement::BindInt64(int col, int64 val) {
+ DCHECK(!stepped_);
if (!is_valid())
return false;
@@ -112,6 +121,7 @@ bool Statement::BindInt64(int col, int64 val) {
}
bool Statement::BindDouble(int col, double val) {
+ DCHECK(!stepped_);
if (!is_valid())
return false;
@@ -119,6 +129,7 @@ bool Statement::BindDouble(int col, double val) {
}
bool Statement::BindCString(int col, const char* val) {
+ DCHECK(!stepped_);
if (!is_valid())
return false;
@@ -127,6 +138,7 @@ bool Statement::BindCString(int col, const char* val) {
}
bool Statement::BindString(int col, const std::string& val) {
+ DCHECK(!stepped_);
if (!is_valid())
return false;
@@ -137,11 +149,12 @@ bool Statement::BindString(int col, const std::string& val) {
SQLITE_TRANSIENT));
}
-bool Statement::BindString16(int col, const string16& value) {
+bool Statement::BindString16(int col, const base::string16& value) {
return BindString(col, UTF16ToUTF8(value));
}
bool Statement::BindBlob(int col, const void* val, int val_len) {
+ DCHECK(!stepped_);
if (!is_valid())
return false;
@@ -222,12 +235,12 @@ std::string Statement::ColumnString(int col) const {
return result;
}
-string16 Statement::ColumnString16(int col) const {
+base::string16 Statement::ColumnString16(int col) const {
if (!CheckValid())
- return string16();
+ return base::string16();
std::string s = ColumnString(col);
- return !s.empty() ? UTF8ToUTF16(s) : string16();
+ return !s.empty() ? UTF8ToUTF16(s) : base::string16();
}
int Statement::ColumnByteLength(int col) const {
@@ -258,16 +271,16 @@ bool Statement::ColumnBlobAsString(int col, std::string* blob) {
return true;
}
-bool Statement::ColumnBlobAsString16(int col, string16* val) const {
+bool Statement::ColumnBlobAsString16(int col, base::string16* val) const {
if (!CheckValid())
return false;
const void* data = ColumnBlob(col);
- size_t len = ColumnByteLength(col) / sizeof(char16);
+ size_t len = ColumnByteLength(col) / sizeof(base::char16);
val->resize(len);
if (val->size() != len)
return false;
- val->assign(reinterpret_cast<const char16*>(data), len);
+ val->assign(reinterpret_cast<const base::char16*>(data), len);
return true;
}
@@ -309,7 +322,7 @@ int Statement::CheckError(int err) {
// Please don't add DCHECKs here, OnSqliteError() already has them.
succeeded_ = (err == SQLITE_OK || err == SQLITE_ROW || err == SQLITE_DONE);
if (!succeeded_ && ref_.get() && ref_->connection())
- return ref_->connection()->OnSqliteError(err, this);
+ return ref_->connection()->OnSqliteError(err, this, NULL);
return err;
}
diff --git a/chromium/sql/statement.h b/chromium/sql/statement.h
index 5fedc53a09b..e192e607251 100644
--- a/chromium/sql/statement.h
+++ b/chromium/sql/statement.h
@@ -108,7 +108,7 @@ class SQL_EXPORT Statement {
bool BindDouble(int col, double val);
bool BindCString(int col, const char* val);
bool BindString(int col, const std::string& val);
- bool BindString16(int col, const string16& value);
+ bool BindString16(int col, const base::string16& value);
bool BindBlob(int col, const void* value, int value_len);
// Retrieving ----------------------------------------------------------------
@@ -131,7 +131,7 @@ class SQL_EXPORT Statement {
int64 ColumnInt64(int col) const;
double ColumnDouble(int col) const;
std::string ColumnString(int col) const;
- string16 ColumnString16(int col) const;
+ base::string16 ColumnString16(int col) const;
// When reading a blob, you can get a raw pointer to the underlying data,
// along with the length, or you can just ask us to copy the blob into a
@@ -139,7 +139,7 @@ class SQL_EXPORT Statement {
int ColumnByteLength(int col) const;
const void* ColumnBlob(int col) const;
bool ColumnBlobAsString(int col, std::string* blob);
- bool ColumnBlobAsString16(int col, string16* val) const;
+ bool ColumnBlobAsString16(int col, base::string16* val) const;
bool ColumnBlobAsVector(int col, std::vector<char>* val) const;
bool ColumnBlobAsVector(int col, std::vector<unsigned char>* val) const;
@@ -178,6 +178,11 @@ class SQL_EXPORT Statement {
// guaranteed non-NULL.
scoped_refptr<Connection::StatementRef> ref_;
+ // Set after Step() or Run() are called, reset by Reset(). Used to
+ // prevent accidental calls to API functions which would not work
+ // correctly after stepping has started.
+ bool stepped_;
+
// See Succeeded() for what this holds.
bool succeeded_;