diff options
author | Torne (Richard Coles) <torne@google.com> | 2012-11-14 11:43:16 +0000 |
---|---|---|
committer | Torne (Richard Coles) <torne@google.com> | 2012-11-14 11:43:16 +0000 |
commit | 5821806d5e7f356e8fa4b058a389a808ea183019 (patch) | |
tree | e19f4793aac92e2c0d9a01087019a60d6657d838 /sql | |
parent | 8e79a8efe247f109aafd917a69e8a392961b3687 (diff) | |
download | chromium_org-5821806d5e7f356e8fa4b058a389a808ea183019.tar.gz |
Merge from Chromium at DEPS revision r167172
This commit was generated by merge_to_master.py.
Change-Id: Ib8d56fd5ae39a2d7e8c91dcd76cc6d13f25f2aab
Diffstat (limited to 'sql')
-rw-r--r-- | sql/DEPS | 3 | ||||
-rw-r--r-- | sql/OWNERS | 3 | ||||
-rw-r--r-- | sql/connection.cc | 648 | ||||
-rw-r--r-- | sql/connection.h | 465 | ||||
-rw-r--r-- | sql/connection_unittest.cc | 297 | ||||
-rw-r--r-- | sql/diagnostic_error_delegate.h | 38 | ||||
-rw-r--r-- | sql/error_delegate_util.cc | 80 | ||||
-rw-r--r-- | sql/error_delegate_util.h | 41 | ||||
-rw-r--r-- | sql/init_status.h | 23 | ||||
-rw-r--r-- | sql/meta_table.cc | 153 | ||||
-rw-r--r-- | sql/meta_table.h | 87 | ||||
-rw-r--r-- | sql/run_all_unittests.cc | 11 | ||||
-rw-r--r-- | sql/sql.gyp | 89 | ||||
-rw-r--r-- | sql/sql.target.mk | 153 | ||||
-rw-r--r-- | sql/sql_export.h | 29 | ||||
-rw-r--r-- | sql/sqlite_features_unittest.cc | 100 | ||||
-rw-r--r-- | sql/statement.cc | 314 | ||||
-rw-r--r-- | sql/statement.h | 189 | ||||
-rw-r--r-- | sql/statement_unittest.cc | 152 | ||||
-rw-r--r-- | sql/transaction.cc | 51 | ||||
-rw-r--r-- | sql/transaction.h | 60 | ||||
-rw-r--r-- | sql/transaction_unittest.cc | 133 |
22 files changed, 3119 insertions, 0 deletions
diff --git a/sql/DEPS b/sql/DEPS new file mode 100644 index 0000000000..0f76c3a31e --- /dev/null +++ b/sql/DEPS @@ -0,0 +1,3 @@ +include_rules = [ + "+third_party/sqlite", +] diff --git a/sql/OWNERS b/sql/OWNERS new file mode 100644 index 0000000000..1ea5750da9 --- /dev/null +++ b/sql/OWNERS @@ -0,0 +1,3 @@ +erikwright@chromium.org +shess@chromium.org +gbillock@chromium.org diff --git a/sql/connection.cc b/sql/connection.cc new file mode 100644 index 0000000000..1de9fc5cfa --- /dev/null +++ b/sql/connection.cc @@ -0,0 +1,648 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "sql/connection.h" + +#include <string.h> + +#include "base/file_path.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "base/stringprintf.h" +#include "base/utf_string_conversions.h" +#include "sql/statement.h" +#include "third_party/sqlite/sqlite3.h" + +namespace { + +// Spin for up to a second waiting for the lock to clear when setting +// up the database. +// TODO(shess): Better story on this. http://crbug.com/56559 +const int kBusyTimeoutSeconds = 1; + +class ScopedBusyTimeout { + public: + explicit ScopedBusyTimeout(sqlite3* db) + : db_(db) { + } + ~ScopedBusyTimeout() { + sqlite3_busy_timeout(db_, 0); + } + + int SetTimeout(base::TimeDelta timeout) { + DCHECK_LT(timeout.InMilliseconds(), INT_MAX); + return sqlite3_busy_timeout(db_, + static_cast<int>(timeout.InMilliseconds())); + } + + private: + sqlite3* db_; +}; + +// Helper to "safely" enable writable_schema. No error checking +// because it is reasonable to just forge ahead in case of an error. +// If turning it on fails, then most likely nothing will work, whereas +// if turning it off fails, it only matters if some code attempts to +// continue working with the database and tries to modify the +// sqlite_master table (none of our code does this). +class ScopedWritableSchema { + public: + explicit ScopedWritableSchema(sqlite3* db) + : db_(db) { + sqlite3_exec(db_, "PRAGMA writable_schema=1", NULL, NULL, NULL); + } + ~ScopedWritableSchema() { + sqlite3_exec(db_, "PRAGMA writable_schema=0", NULL, NULL, NULL); + } + + private: + sqlite3* db_; +}; + +} // namespace + +namespace sql { + +bool StatementID::operator<(const StatementID& other) const { + if (number_ != other.number_) + return number_ < other.number_; + return strcmp(str_, other.str_) < 0; +} + +ErrorDelegate::~ErrorDelegate() { +} + +Connection::StatementRef::StatementRef() + : connection_(NULL), + stmt_(NULL) { +} + +Connection::StatementRef::StatementRef(sqlite3_stmt* stmt) + : connection_(NULL), + stmt_(stmt) { +} + +Connection::StatementRef::StatementRef(Connection* connection, + sqlite3_stmt* stmt) + : connection_(connection), + stmt_(stmt) { + connection_->StatementRefCreated(this); +} + +Connection::StatementRef::~StatementRef() { + if (connection_) + connection_->StatementRefDeleted(this); + Close(); +} + +void Connection::StatementRef::Close() { + if (stmt_) { + // Call to AssertIOAllowed() cannot go at the beginning of the function + // because Close() is called unconditionally from destructor to clean + // connection_. And if this is inactive statement this won't cause any + // disk access and destructor most probably will be called on thread + // not allowing disk access. + // TODO(paivanof@gmail.com): This should move to the beginning + // of the function. http://crbug.com/136655. + AssertIOAllowed(); + sqlite3_finalize(stmt_); + stmt_ = NULL; + } + connection_ = NULL; // The connection may be getting deleted. +} + +Connection::Connection() + : db_(NULL), + page_size_(0), + cache_size_(0), + exclusive_locking_(false), + transaction_nesting_(0), + needs_rollback_(false), + in_memory_(false), + error_delegate_(NULL) { +} + +Connection::~Connection() { + Close(); +} + +bool Connection::Open(const FilePath& path) { +#if defined(OS_WIN) + return OpenInternal(WideToUTF8(path.value())); +#elif defined(OS_POSIX) + return OpenInternal(path.value()); +#endif +} + +bool Connection::OpenInMemory() { + in_memory_ = true; + return OpenInternal(":memory:"); +} + +void Connection::Close() { + // TODO(shess): Calling "PRAGMA journal_mode = DELETE" at this point + // will delete the -journal file. For ChromiumOS or other more + // embedded systems, this is probably not appropriate, whereas on + // desktop it might make some sense. + + // sqlite3_close() needs all prepared statements to be finalized. + // Release all cached statements, then assert that the client has + // released all statements. + statement_cache_.clear(); + DCHECK(open_statements_.empty()); + + // Additionally clear the prepared statements, because they contain + // weak references to this connection. This case has come up when + // error-handling code is hit in production. + ClearCache(); + + if (db_) { + // Call to AssertIOAllowed() cannot go at the beginning of the function + // because Close() must be called from destructor to clean + // statement_cache_, it won't cause any disk access and it most probably + // will happen on thread not allowing disk access. + // TODO(paivanof@gmail.com): This should move to the beginning + // of the function. http://crbug.com/136655. + AssertIOAllowed(); + // TODO(shess): Histogram for failure. + sqlite3_close(db_); + db_ = NULL; + } +} + +void Connection::Preload() { + AssertIOAllowed(); + + if (!db_) { + DLOG(FATAL) << "Cannot preload null db"; + return; + } + + // A statement must be open for the preload command to work. If the meta + // table doesn't exist, it probably means this is a new database and there + // is nothing to preload (so it's OK we do nothing). + if (!DoesTableExist("meta")) + return; + Statement dummy(GetUniqueStatement("SELECT * FROM meta")); + if (!dummy.Step()) + return; + +#if !defined(USE_SYSTEM_SQLITE) + // This function is only defined in Chromium's version of sqlite. + // Do not call it when using system sqlite. + sqlite3_preload(db_); +#endif +} + +// Create an in-memory database with the existing database's page +// size, then backup that database over the existing database. +bool Connection::Raze() { + AssertIOAllowed(); + + if (!db_) { + DLOG(FATAL) << "Cannot raze null db"; + return false; + } + + if (transaction_nesting_ > 0) { + DLOG(FATAL) << "Cannot raze within a transaction"; + return false; + } + + sql::Connection null_db; + if (!null_db.OpenInMemory()) { + DLOG(FATAL) << "Unable to open in-memory database."; + return false; + } + + if (page_size_) { + // Enforce SQLite restrictions on |page_size_|. + DCHECK(!(page_size_ & (page_size_ - 1))) + << " page_size_ " << page_size_ << " is not a power of two."; + const int kSqliteMaxPageSize = 32768; // from sqliteLimit.h + DCHECK_LE(page_size_, kSqliteMaxPageSize); + const std::string sql = StringPrintf("PRAGMA page_size=%d", page_size_); + if (!null_db.Execute(sql.c_str())) + return false; + } + +#if defined(OS_ANDROID) + // Android compiles with SQLITE_DEFAULT_AUTOVACUUM. Unfortunately, + // in-memory databases do not respect this define. + // TODO(shess): Figure out a way to set this without using platform + // specific code. AFAICT from sqlite3.c, the only way to do it + // would be to create an actual filesystem database, which is + // unfortunate. + if (!null_db.Execute("PRAGMA auto_vacuum = 1")) + return false; +#endif + + // The page size doesn't take effect until a database has pages, and + // at this point the null database has none. Changing the schema + // version will create the first page. This will not affect the + // schema version in the resulting database, as SQLite's backup + // implementation propagates the schema version from the original + // connection to the new version of the database, incremented by one + // so that other readers see the schema change and act accordingly. + if (!null_db.Execute("PRAGMA schema_version = 1")) + return false; + + // SQLite tracks the expected number of database pages in the first + // page, and if it does not match the total retrieved from a + // filesystem call, treats the database as corrupt. This situation + // breaks almost all SQLite calls. "PRAGMA writable_schema" can be + // used to hint to SQLite to soldier on in that case, specifically + // for purposes of recovery. [See SQLITE_CORRUPT_BKPT case in + // sqlite3.c lockBtree().] + // TODO(shess): With this, "PRAGMA auto_vacuum" and "PRAGMA + // page_size" can be used to query such a database. + ScopedWritableSchema writable_schema(db_); + + sqlite3_backup* backup = sqlite3_backup_init(db_, "main", + null_db.db_, "main"); + if (!backup) { + DLOG(FATAL) << "Unable to start sqlite3_backup()."; + return false; + } + + // -1 backs up the entire database. + int rc = sqlite3_backup_step(backup, -1); + int pages = sqlite3_backup_pagecount(backup); + sqlite3_backup_finish(backup); + + // The destination database was locked. + if (rc == SQLITE_BUSY) { + return false; + } + + // The entire database should have been backed up. + if (rc != SQLITE_DONE) { + DLOG(FATAL) << "Unable to copy entire null database."; + return false; + } + + // Exactly one page should have been backed up. If this breaks, + // check this function to make sure assumptions aren't being broken. + DCHECK_EQ(pages, 1); + + return true; +} + +bool Connection::RazeWithTimout(base::TimeDelta timeout) { + if (!db_) { + DLOG(FATAL) << "Cannot raze null db"; + return false; + } + + ScopedBusyTimeout busy_timeout(db_); + busy_timeout.SetTimeout(timeout); + return Raze(); +} + +bool Connection::BeginTransaction() { + if (needs_rollback_) { + DCHECK_GT(transaction_nesting_, 0); + + // When we're going to rollback, fail on this begin and don't actually + // mark us as entering the nested transaction. + return false; + } + + bool success = true; + if (!transaction_nesting_) { + needs_rollback_ = false; + + Statement begin(GetCachedStatement(SQL_FROM_HERE, "BEGIN TRANSACTION")); + if (!begin.Run()) + return false; + } + transaction_nesting_++; + return success; +} + +void Connection::RollbackTransaction() { + if (!transaction_nesting_) { + DLOG(FATAL) << "Rolling back a nonexistent transaction"; + return; + } + + transaction_nesting_--; + + if (transaction_nesting_ > 0) { + // Mark the outermost transaction as needing rollback. + needs_rollback_ = true; + return; + } + + DoRollback(); +} + +bool Connection::CommitTransaction() { + if (!transaction_nesting_) { + DLOG(FATAL) << "Rolling back a nonexistent transaction"; + return false; + } + transaction_nesting_--; + + if (transaction_nesting_ > 0) { + // Mark any nested transactions as failing after we've already got one. + return !needs_rollback_; + } + + if (needs_rollback_) { + DoRollback(); + return false; + } + + Statement commit(GetCachedStatement(SQL_FROM_HERE, "COMMIT")); + return commit.Run(); +} + +int Connection::ExecuteAndReturnErrorCode(const char* sql) { + AssertIOAllowed(); + if (!db_) + return false; + return sqlite3_exec(db_, sql, NULL, NULL, NULL); +} + +bool Connection::Execute(const char* sql) { + int error = ExecuteAndReturnErrorCode(sql); + if (error != SQLITE_OK) + error = OnSqliteError(error, NULL); + + // 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 + // a change alters the schema but not all queries adjust. + if (error == SQLITE_ERROR) + DLOG(FATAL) << "SQL Error in " << sql << ", " << GetErrorMessage(); + return error == SQLITE_OK; +} + +bool Connection::ExecuteWithTimeout(const char* sql, base::TimeDelta timeout) { + if (!db_) + return false; + + ScopedBusyTimeout busy_timeout(db_); + busy_timeout.SetTimeout(timeout); + return Execute(sql); +} + +bool Connection::HasCachedStatement(const StatementID& id) const { + return statement_cache_.find(id) != statement_cache_.end(); +} + +scoped_refptr<Connection::StatementRef> Connection::GetCachedStatement( + const StatementID& id, + const char* sql) { + CachedStatementMap::iterator i = statement_cache_.find(id); + if (i != statement_cache_.end()) { + // Statement is in the cache. It should still be active (we're the only + // one invalidating cached statements, and we'll remove it from the cache + // if we do that. Make sure we reset it before giving out the cached one in + // case it still has some stuff bound. + DCHECK(i->second->is_valid()); + sqlite3_reset(i->second->stmt()); + return i->second; + } + + scoped_refptr<StatementRef> statement = GetUniqueStatement(sql); + if (statement->is_valid()) + statement_cache_[id] = statement; // Only cache valid statements. + return statement; +} + +scoped_refptr<Connection::StatementRef> Connection::GetUniqueStatement( + const char* sql) { + AssertIOAllowed(); + + if (!db_) + return new StatementRef(); // Return inactive statement. + + sqlite3_stmt* stmt = NULL; + int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + // This is evidence of a syntax error in the incoming SQL. + DLOG(FATAL) << "SQL compile error " << GetErrorMessage(); + + // It could also be database corruption. + OnSqliteError(rc, NULL); + return new StatementRef(); + } + return new StatementRef(this, stmt); +} + +scoped_refptr<Connection::StatementRef> Connection::GetUntrackedStatement( + const char* sql) const { + if (!db_) + return new StatementRef(); // Return inactive statement. + + sqlite3_stmt* stmt = NULL; + int rc = sqlite3_prepare_v2(db_, sql, -1, &stmt, NULL); + if (rc != SQLITE_OK) { + // This is evidence of a syntax error in the incoming SQL. + DLOG(FATAL) << "SQL compile error " << GetErrorMessage(); + return new StatementRef(); + } + return new StatementRef(stmt); +} + +bool Connection::IsSQLValid(const char* sql) { + AssertIOAllowed(); + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(db_, sql, -1, &stmt, NULL) != SQLITE_OK) + return false; + + sqlite3_finalize(stmt); + return true; +} + +bool Connection::DoesTableExist(const char* table_name) const { + return DoesTableOrIndexExist(table_name, "table"); +} + +bool Connection::DoesIndexExist(const char* index_name) const { + return DoesTableOrIndexExist(index_name, "index"); +} + +bool Connection::DoesTableOrIndexExist( + const char* name, const char* type) const { + const char* kSql = "SELECT name FROM sqlite_master WHERE type=? AND name=?"; + Statement statement(GetUntrackedStatement(kSql)); + statement.BindString(0, type); + statement.BindString(1, name); + + return statement.Step(); // Table exists if any row was returned. +} + +bool Connection::DoesColumnExist(const char* table_name, + const char* column_name) const { + std::string sql("PRAGMA TABLE_INFO("); + sql.append(table_name); + sql.append(")"); + + Statement statement(GetUntrackedStatement(sql.c_str())); + while (statement.Step()) { + if (!statement.ColumnString(1).compare(column_name)) + return true; + } + return false; +} + +int64 Connection::GetLastInsertRowId() const { + if (!db_) { + DLOG(FATAL) << "Illegal use of connection without a db"; + return 0; + } + return sqlite3_last_insert_rowid(db_); +} + +int Connection::GetLastChangeCount() const { + if (!db_) { + DLOG(FATAL) << "Illegal use of connection without a db"; + return 0; + } + return sqlite3_changes(db_); +} + +int Connection::GetErrorCode() const { + if (!db_) + return SQLITE_ERROR; + return sqlite3_errcode(db_); +} + +int Connection::GetLastErrno() const { + if (!db_) + return -1; + + int err = 0; + if (SQLITE_OK != sqlite3_file_control(db_, NULL, SQLITE_LAST_ERRNO, &err)) + return -2; + + return err; +} + +const char* Connection::GetErrorMessage() const { + if (!db_) + return "sql::Connection has no connection."; + return sqlite3_errmsg(db_); +} + +bool Connection::OpenInternal(const std::string& file_name) { + AssertIOAllowed(); + + if (db_) { + DLOG(FATAL) << "sql::Connection is already open."; + return false; + } + + int err = sqlite3_open(file_name.c_str(), &db_); + if (err != SQLITE_OK) { + OnSqliteError(err, NULL); + Close(); + db_ = NULL; + return false; + } + + // Enable extended result codes to provide more color on I/O errors. + // Not having extended result codes is not a fatal problem, as + // Chromium code does not attempt to handle I/O errors anyhow. The + // current implementation always returns SQLITE_OK, the DCHECK is to + // quickly notify someone if SQLite changes. + err = sqlite3_extended_result_codes(db_, 1); + DCHECK_EQ(err, SQLITE_OK) << "Could not enable extended result codes"; + + // If indicated, lock up the database before doing anything else, so + // that the following code doesn't have to deal with locking. + // TODO(shess): This code is brittle. Find the cases where code + // doesn't request |exclusive_locking_| and audit that it does the + // right thing with SQLITE_BUSY, and that it doesn't make + // assumptions about who might change things in the database. + // http://crbug.com/56559 + if (exclusive_locking_) { + // TODO(shess): This should probably be a full CHECK(). Code + // which requests exclusive locking but doesn't get it is almost + // certain to be ill-tested. + if (!Execute("PRAGMA locking_mode=EXCLUSIVE")) + DLOG(FATAL) << "Could not set locking mode: " << GetErrorMessage(); + } + + // http://www.sqlite.org/pragma.html#pragma_journal_mode + // DELETE (default) - delete -journal file to commit. + // TRUNCATE - truncate -journal file to commit. + // PERSIST - zero out header of -journal file to commit. + // journal_size_limit provides size to trim to in PERSIST. + // TODO(shess): Figure out if PERSIST and journal_size_limit really + // matter. In theory, it keeps pages pre-allocated, so if + // transactions usually fit, it should be faster. + ignore_result(Execute("PRAGMA journal_mode = PERSIST")); + ignore_result(Execute("PRAGMA journal_size_limit = 16384")); + + const base::TimeDelta kBusyTimeout = + base::TimeDelta::FromSeconds(kBusyTimeoutSeconds); + + if (page_size_ != 0) { + // Enforce SQLite restrictions on |page_size_|. + DCHECK(!(page_size_ & (page_size_ - 1))) + << " page_size_ " << page_size_ << " is not a power of two."; + const int kSqliteMaxPageSize = 32768; // from sqliteLimit.h + DCHECK_LE(page_size_, kSqliteMaxPageSize); + const std::string sql = StringPrintf("PRAGMA page_size=%d", page_size_); + if (!ExecuteWithTimeout(sql.c_str(), kBusyTimeout)) + DLOG(FATAL) << "Could not set page size: " << GetErrorMessage(); + } + + if (cache_size_ != 0) { + const std::string sql = StringPrintf("PRAGMA cache_size=%d", cache_size_); + if (!ExecuteWithTimeout(sql.c_str(), kBusyTimeout)) + DLOG(FATAL) << "Could not set cache size: " << GetErrorMessage(); + } + + if (!ExecuteWithTimeout("PRAGMA secure_delete=ON", kBusyTimeout)) { + DLOG(FATAL) << "Could not enable secure_delete: " << GetErrorMessage(); + Close(); + return false; + } + + return true; +} + +void Connection::DoRollback() { + Statement rollback(GetCachedStatement(SQL_FROM_HERE, "ROLLBACK")); + rollback.Run(); + needs_rollback_ = false; +} + +void Connection::StatementRefCreated(StatementRef* ref) { + DCHECK(open_statements_.find(ref) == open_statements_.end()); + open_statements_.insert(ref); +} + +void Connection::StatementRefDeleted(StatementRef* ref) { + StatementRefSet::iterator i = open_statements_.find(ref); + if (i == open_statements_.end()) + DLOG(FATAL) << "Could not find statement"; + else + open_statements_.erase(i); +} + +void Connection::ClearCache() { + statement_cache_.clear(); + + // The cache clear will get most statements. There may be still be references + // to some statements that are held by others (including one-shot statements). + // This will deactivate them so they can't be used again. + for (StatementRefSet::iterator i = open_statements_.begin(); + i != open_statements_.end(); ++i) + (*i)->Close(); +} + +int Connection::OnSqliteError(int err, sql::Statement *stmt) { + if (error_delegate_.get()) + return error_delegate_->OnError(err, this, stmt); + // The default handling is to assert on debug and to ignore on release. + DLOG(FATAL) << GetErrorMessage(); + return err; +} + +} // namespace sql diff --git a/sql/connection.h b/sql/connection.h new file mode 100644 index 0000000000..8cf4d715f5 --- /dev/null +++ b/sql/connection.h @@ -0,0 +1,465 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SQL_CONNECTION_H_ +#define SQL_CONNECTION_H_ + +#include <map> +#include <set> +#include <string> + +#include "base/basictypes.h" +#include "base/compiler_specific.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/threading/thread_restrictions.h" +#include "base/time.h" +#include "sql/sql_export.h" + +class FilePath; +struct sqlite3; +struct sqlite3_stmt; + +namespace sql { + +class Statement; + +// Uniquely identifies a statement. There are two modes of operation: +// +// - In the most common mode, you will use the source file and line number to +// identify your statement. This is a convienient way to get uniqueness for +// a statement that is only used in one place. Use the SQL_FROM_HERE macro +// to generate a StatementID. +// +// - In the "custom" mode you may use the statement from different places or +// need to manage it yourself for whatever reason. In this case, you should +// make up your own unique name and pass it to the StatementID. This name +// must be a static string, since this object only deals with pointers and +// assumes the underlying string doesn't change or get deleted. +// +// This object is copyable and assignable using the compiler-generated +// operator= and copy constructor. +class StatementID { + public: + // Creates a uniquely named statement with the given file ane line number. + // Normally you will use SQL_FROM_HERE instead of calling yourself. + StatementID(const char* file, int line) + : number_(line), + str_(file) { + } + + // Creates a uniquely named statement with the given user-defined name. + explicit StatementID(const char* unique_name) + : number_(-1), + str_(unique_name) { + } + + // This constructor is unimplemented and will generate a linker error if + // called. It is intended to try to catch people dynamically generating + // a statement name that will be deallocated and will cause a crash later. + // All strings must be static and unchanging! + explicit StatementID(const std::string& dont_ever_do_this); + + // We need this to insert into our map. + bool operator<(const StatementID& other) const; + + private: + int number_; + const char* str_; +}; + +#define SQL_FROM_HERE sql::StatementID(__FILE__, __LINE__) + +class Connection; + +// ErrorDelegate defines the interface to implement error handling and recovery +// for sqlite operations. This allows the rest of the classes to return true or +// false while the actual error code and causing statement are delivered using +// the OnError() callback. +// The tipical usage is to centralize the code designed to handle database +// corruption, low-level IO errors or locking violations. +class SQL_EXPORT ErrorDelegate { + public: + virtual ~ErrorDelegate(); + + // |error| is an sqlite result code as seen in sqlite3.h. |connection| is the + // db connection where the error happened and |stmt| is our best guess at the + // statement that triggered the error. Do not store these pointers. + // + // |stmt| MAY BE NULL if there is no statement causing the problem (i.e. on + // initialization). + // + // If the error condition has been fixed and the original statement succesfuly + // re-tried then returning SQLITE_OK is appropriate; otherwise it is + // recommended that you return the original |error| or the appropriate error + // code. + virtual int OnError(int error, Connection* connection, Statement* stmt) = 0; +}; + +class SQL_EXPORT Connection { + private: + class StatementRef; // Forward declaration, see real one below. + + public: + // The database is opened by calling Open[InMemory](). Any uncommitted + // transactions will be rolled back when this object is deleted. + Connection(); + ~Connection(); + + // Pre-init configuration ---------------------------------------------------- + + // Sets the page size that will be used when creating a new database. This + // must be called before Init(), and will only have an effect on new + // databases. + // + // From sqlite.org: "The page size must be a power of two greater than or + // equal to 512 and less than or equal to SQLITE_MAX_PAGE_SIZE. The maximum + // value for SQLITE_MAX_PAGE_SIZE is 32768." + void set_page_size(int page_size) { page_size_ = page_size; } + + // Sets the number of pages that will be cached in memory by sqlite. The + // total cache size in bytes will be page_size * cache_size. This must be + // called before Open() to have an effect. + void set_cache_size(int cache_size) { cache_size_ = cache_size; } + + // Call to put the database in exclusive locking mode. There is no "back to + // normal" flag because of some additional requirements sqlite puts on this + // transaition (requires another access to the DB) and because we don't + // actually need it. + // + // Exclusive mode means that the database is not unlocked at the end of each + // transaction, which means there may be less time spent initializing the + // next transaction because it doesn't have to re-aquire locks. + // + // This must be called before Open() to have an effect. + void set_exclusive_locking() { exclusive_locking_ = true; } + + // Sets the object that will handle errors. Recomended that it should be set + // before calling Open(). If not set, the default is to ignore errors on + // release and assert on debug builds. + // Takes ownership of |delegate|. + void set_error_delegate(ErrorDelegate* delegate) { + error_delegate_.reset(delegate); + } + + // Initialization ------------------------------------------------------------ + + // Initializes the SQL connection for the given file, returning true if the + // file could be opened. You can call this or OpenInMemory. + bool Open(const FilePath& path) WARN_UNUSED_RESULT; + + // Initializes the SQL connection for a temporary in-memory database. There + // will be no associated file on disk, and the initial database will be + // empty. You can call this or Open. + bool OpenInMemory() WARN_UNUSED_RESULT; + + // Returns trie if the database has been successfully opened. + bool is_open() const { return !!db_; } + + // Closes the database. This is automatically performed on destruction for + // you, but this allows you to close the database early. You must not call + // any other functions after closing it. It is permissable to call Close on + // an uninitialized or already-closed database. + void Close(); + + // Pre-loads the first <cache-size> pages into the cache from the file. + // If you expect to soon use a substantial portion of the database, this + // is much more efficient than allowing the pages to be populated organically + // since there is no per-page hard drive seeking. If the file is larger than + // the cache, the last part that doesn't fit in the cache will be brought in + // organically. + // + // This function assumes your class is using a meta table on the current + // database, as it openes a transaction on the meta table to force the + // database to be initialized. You should feel free to initialize the meta + // table after calling preload since the meta table will already be in the + // database if it exists, and if it doesn't exist, the database won't + // generally exist either. + void Preload(); + + // Raze the database to the ground. This approximates creating a + // fresh database from scratch, within the constraints of SQLite's + // locking protocol (locks and open handles can make doing this with + // filesystem operations problematic). Returns true if the database + // was razed. + // + // false is returned if the database is locked by some other + // process. RazeWithTimeout() may be used if appropriate. + // + // NOTE(shess): Raze() will DCHECK in the following situations: + // - database is not open. + // - the connection has a transaction open. + // - a SQLite issue occurs which is structural in nature (like the + // statements used are broken). + // Since Raze() is expected to be called in unexpected situations, + // these all return false, since it is unlikely that the caller + // could fix them. + // + // The database's page size is taken from |page_size_|. The + // existing database's |auto_vacuum| setting is lost (the + // possibility of corruption makes it unreliable to pull it from the + // existing database). To re-enable on the empty database requires + // running "PRAGMA auto_vacuum = 1;" then "VACUUM". + // + // NOTE(shess): For Android, SQLITE_DEFAULT_AUTOVACUUM is set to 1, + // so Raze() sets auto_vacuum to 1. + // + // TODO(shess): Raze() needs a connection so cannot clear SQLITE_NOTADB. + // TODO(shess): Bake auto_vacuum into Connection's API so it can + // just pick up the default. + bool Raze(); + bool RazeWithTimout(base::TimeDelta timeout); + + // Transactions -------------------------------------------------------------- + + // Transaction management. We maintain a virtual transaction stack to emulate + // nested transactions since sqlite can't do nested transactions. The + // limitation is you can't roll back a sub transaction: if any transaction + // fails, all transactions open will also be rolled back. Any nested + // transactions after one has rolled back will return fail for Begin(). If + // Begin() fails, you must not call Commit or Rollback(). + // + // Normally you should use sql::Transaction to manage a transaction, which + // will scope it to a C++ context. + bool BeginTransaction(); + void RollbackTransaction(); + bool CommitTransaction(); + + // Returns the current transaction nesting, which will be 0 if there are + // no open transactions. + int transaction_nesting() const { return transaction_nesting_; } + + // Statements ---------------------------------------------------------------- + + // Executes the given SQL string, returning true on success. This is + // normally used for simple, 1-off statements that don't take any bound + // parameters and don't return any data (e.g. CREATE TABLE). + // + // This will DCHECK if the |sql| contains errors. + // + // Do not use ignore_result() to ignore all errors. Use + // ExecuteAndReturnErrorCode() and ignore only specific errors. + bool Execute(const char* sql) WARN_UNUSED_RESULT; + + // Like Execute(), but returns the error code given by SQLite. + int ExecuteAndReturnErrorCode(const char* sql) WARN_UNUSED_RESULT; + + // Returns true if we have a statement with the given identifier already + // cached. This is normally not necessary to call, but can be useful if the + // caller has to dynamically build up SQL to avoid doing so if it's already + // cached. + bool HasCachedStatement(const StatementID& id) const; + + // Returns a statement for the given SQL using the statement cache. It can + // take a nontrivial amount of work to parse and compile a statement, so + // keeping commonly-used ones around for future use is important for + // performance. + // + // If the |sql| has an error, an invalid, inert StatementRef is returned (and + // the code will crash in debug). The caller must deal with this eventuality, + // either by checking validity of the |sql| before calling, by correctly + // handling the return of an inert statement, or both. + // + // The StatementID and the SQL must always correspond to one-another. The + // ID is the lookup into the cache, so crazy things will happen if you use + // different SQL with the same ID. + // + // You will normally use the SQL_FROM_HERE macro to generate a statement + // ID associated with the current line of code. This gives uniqueness without + // you having to manage unique names. See StatementID above for more. + // + // Example: + // sql::Statement stmt(connection_.GetCachedStatement( + // SQL_FROM_HERE, "SELECT * FROM foo")); + // if (!stmt) + // return false; // Error creating statement. + scoped_refptr<StatementRef> GetCachedStatement(const StatementID& id, + const char* sql); + + // Used to check a |sql| statement for syntactic validity. If the statement is + // valid SQL, returns true. + bool IsSQLValid(const char* sql); + + // Returns a non-cached statement for the given SQL. Use this for SQL that + // is only executed once or only rarely (there is overhead associated with + // keeping a statement cached). + // + // See GetCachedStatement above for examples and error information. + scoped_refptr<StatementRef> GetUniqueStatement(const char* sql); + + // Info querying ------------------------------------------------------------- + + // Returns true if the given table exists. + bool DoesTableExist(const char* table_name) const; + + // Returns true if the given index exists. + bool DoesIndexExist(const char* index_name) const; + + // Returns true if a column with the given name exists in the given table. + bool DoesColumnExist(const char* table_name, const char* column_name) const; + + // Returns sqlite's internal ID for the last inserted row. Valid only + // immediately after an insert. + int64 GetLastInsertRowId() const; + + // Returns sqlite's count of the number of rows modified by the last + // statement executed. Will be 0 if no statement has executed or the database + // is closed. + int GetLastChangeCount() const; + + // Errors -------------------------------------------------------------------- + + // Returns the error code associated with the last sqlite operation. + int GetErrorCode() const; + + // Returns the errno associated with GetErrorCode(). See + // SQLITE_LAST_ERRNO in SQLite documentation. + int GetLastErrno() const; + + // Returns a pointer to a statically allocated string associated with the + // last sqlite operation. + const char* GetErrorMessage() const; + + private: + // Statement accesses StatementRef which we don't want to expose to everybody + // (they should go through Statement). + friend class Statement; + + // Internal initialize function used by both Init and InitInMemory. The file + // name is always 8 bits since we want to use the 8-bit version of + // sqlite3_open. The string can also be sqlite's special ":memory:" string. + bool OpenInternal(const std::string& file_name); + + // Check whether the current thread is allowed to make IO calls, but only + // if database wasn't open in memory. Function is inlined to be a no-op in + // official build. + void AssertIOAllowed() { + if (!in_memory_) + base::ThreadRestrictions::AssertIOAllowed(); + } + + // Internal helper for DoesTableExist and DoesIndexExist. + bool DoesTableOrIndexExist(const char* name, const char* type) const; + + // A StatementRef is a refcounted wrapper around a sqlite statement pointer. + // Refcounting allows us to give these statements out to sql::Statement + // objects while also optionally maintaining a cache of compiled statements + // by just keeping a refptr to these objects. + // + // A statement ref can be valid, in which case it can be used, or invalid to + // indicate that the statement hasn't been created yet, has an error, or has + // been destroyed. + // + // The Connection may revoke a StatementRef in some error cases, so callers + // should always check validity before using. + class SQL_EXPORT StatementRef : public base::RefCounted<StatementRef> { + public: + // Default constructor initializes to an invalid statement. + StatementRef(); + explicit StatementRef(sqlite3_stmt* stmt); + StatementRef(Connection* connection, sqlite3_stmt* stmt); + + // When true, the statement can be used. + bool is_valid() const { return !!stmt_; } + + // If we've not been linked to a connection, this will be NULL. Guaranteed + // non-NULL when is_valid(). + Connection* connection() const { return connection_; } + + // Returns the sqlite statement if any. If the statement is not active, + // this will return NULL. + sqlite3_stmt* stmt() const { return stmt_; } + + // Destroys the compiled statement and marks it NULL. The statement will + // no longer be active. + void Close(); + + // Check whether the current thread is allowed to make IO calls, but only + // if database wasn't open in memory. + void AssertIOAllowed() { if (connection_) connection_->AssertIOAllowed(); } + + private: + friend class base::RefCounted<StatementRef>; + + ~StatementRef(); + + Connection* connection_; + sqlite3_stmt* stmt_; + + DISALLOW_COPY_AND_ASSIGN(StatementRef); + }; + friend class StatementRef; + + // Executes a rollback statement, ignoring all transaction state. Used + // internally in the transaction management code. + void DoRollback(); + + // Called by a StatementRef when it's being created or destroyed. See + // open_statements_ below. + void StatementRefCreated(StatementRef* ref); + void StatementRefDeleted(StatementRef* ref); + + // Frees all cached statements from statement_cache_. + void ClearCache(); + + // 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); + + // Like |Execute()|, but retries if the database is locked. + bool ExecuteWithTimeout(const char* sql, base::TimeDelta ms_timeout) + WARN_UNUSED_RESULT; + + // Internal helper for const functions. Like GetUniqueStatement(), + // except the statement is not entered into open_statements_, + // allowing this function to be const. Open statements can block + // closing the database, so only use in cases where the last ref is + // released before close could be called (which should always be the + // case for const functions). + scoped_refptr<StatementRef> GetUntrackedStatement(const char* sql) const; + + // The actual sqlite database. Will be NULL before Init has been called or if + // Init resulted in an error. + sqlite3* db_; + + // Parameters we'll configure in sqlite before doing anything else. Zero means + // use the default value. + int page_size_; + int cache_size_; + bool exclusive_locking_; + + // All cached statements. Keeping a reference to these statements means that + // they'll remain active. + typedef std::map<StatementID, scoped_refptr<StatementRef> > + CachedStatementMap; + CachedStatementMap statement_cache_; + + // A list of all StatementRefs we've given out. Each ref must register with + // us when it's created or destroyed. This allows us to potentially close + // any open statements when we encounter an error. + typedef std::set<StatementRef*> StatementRefSet; + StatementRefSet open_statements_; + + // Number of currently-nested transactions. + int transaction_nesting_; + + // True if any of the currently nested transactions have been rolled back. + // When we get to the outermost transaction, this will determine if we do + // a rollback instead of a commit. + bool needs_rollback_; + + // True if database is open with OpenInMemory(), False if database is open + // with Open(). + bool in_memory_; + + // This object handles errors resulting from all forms of executing sqlite + // commands or statements. It can be null which means default handling. + scoped_ptr<ErrorDelegate> error_delegate_; + + DISALLOW_COPY_AND_ASSIGN(Connection); +}; + +} // namespace sql + +#endif // SQL_CONNECTION_H_ diff --git a/sql/connection_unittest.cc b/sql/connection_unittest.cc new file mode 100644 index 0000000000..8036409613 --- /dev/null +++ b/sql/connection_unittest.cc @@ -0,0 +1,297 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/file_util.h" +#include "base/scoped_temp_dir.h" +#include "sql/connection.h" +#include "sql/statement.h" +#include "sql/meta_table.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/sqlite3.h" + +class SQLConnectionTest : public testing::Test { + public: + SQLConnectionTest() {} + + void SetUp() { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + ASSERT_TRUE(db_.Open(db_path())); + } + + void TearDown() { + db_.Close(); + } + + sql::Connection& db() { return db_; } + + FilePath db_path() { + return temp_dir_.path().AppendASCII("SQLConnectionTest.db"); + } + + private: + ScopedTempDir temp_dir_; + sql::Connection db_; +}; + +TEST_F(SQLConnectionTest, Execute) { + // Valid statement should return true. + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + EXPECT_EQ(SQLITE_OK, db().GetErrorCode()); + + // Invalid statement should fail. + ASSERT_EQ(SQLITE_ERROR, + db().ExecuteAndReturnErrorCode("CREATE TAB foo (a, b")); + EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode()); +} + +TEST_F(SQLConnectionTest, ExecuteWithErrorCode) { + ASSERT_EQ(SQLITE_OK, + db().ExecuteAndReturnErrorCode("CREATE TABLE foo (a, b)")); + ASSERT_EQ(SQLITE_ERROR, + db().ExecuteAndReturnErrorCode("CREATE TABLE TABLE")); + ASSERT_EQ(SQLITE_ERROR, + db().ExecuteAndReturnErrorCode( + "INSERT INTO foo(a, b) VALUES (1, 2, 3, 4)")); +} + +TEST_F(SQLConnectionTest, CachedStatement) { + sql::StatementID id1("foo", 12); + + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo(a, b) VALUES (12, 13)")); + + // Create a new cached statement. + { + sql::Statement s(db().GetCachedStatement(id1, "SELECT a FROM foo")); + ASSERT_TRUE(s.is_valid()); + + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + } + + // The statement should be cached still. + EXPECT_TRUE(db().HasCachedStatement(id1)); + + { + // Get the same statement using different SQL. This should ignore our + // SQL and use the cached one (so it will be valid). + sql::Statement s(db().GetCachedStatement(id1, "something invalid(")); + ASSERT_TRUE(s.is_valid()); + + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + } + + // Make sure other statements aren't marked as cached. + EXPECT_FALSE(db().HasCachedStatement(SQL_FROM_HERE)); +} + +TEST_F(SQLConnectionTest, IsSQLValidTest) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().IsSQLValid("SELECT a FROM foo")); + ASSERT_FALSE(db().IsSQLValid("SELECT no_exist FROM foo")); +} + +TEST_F(SQLConnectionTest, DoesStuffExist) { + // Test DoesTableExist. + EXPECT_FALSE(db().DoesTableExist("foo")); + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + EXPECT_TRUE(db().DoesTableExist("foo")); + + // Should be case sensitive. + EXPECT_FALSE(db().DoesTableExist("FOO")); + + // Test DoesColumnExist. + EXPECT_FALSE(db().DoesColumnExist("foo", "bar")); + EXPECT_TRUE(db().DoesColumnExist("foo", "a")); + + // Testing for a column on a nonexistent table. + EXPECT_FALSE(db().DoesColumnExist("bar", "b")); +} + +TEST_F(SQLConnectionTest, GetLastInsertRowId) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (id INTEGER PRIMARY KEY, value)")); + + ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)")); + + // Last insert row ID should be valid. + int64 row = db().GetLastInsertRowId(); + EXPECT_LT(0, row); + + // It should be the primary key of the row we just inserted. + sql::Statement s(db().GetUniqueStatement("SELECT value FROM foo WHERE id=?")); + s.BindInt64(0, row); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); +} + +TEST_F(SQLConnectionTest, Rollback) { + ASSERT_TRUE(db().BeginTransaction()); + ASSERT_TRUE(db().BeginTransaction()); + EXPECT_EQ(2, db().transaction_nesting()); + db().RollbackTransaction(); + EXPECT_FALSE(db().CommitTransaction()); + EXPECT_TRUE(db().BeginTransaction()); +} + +// Test that sql::Connection::Raze() results in a database without the +// tables from the original database. +TEST_F(SQLConnectionTest, Raze) { + const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)"; + ASSERT_TRUE(db().Execute(kCreateSql)); + ASSERT_TRUE(db().Execute("INSERT INTO foo (value) VALUES (12)")); + + int pragma_auto_vacuum = 0; + { + sql::Statement s(db().GetUniqueStatement("PRAGMA auto_vacuum")); + ASSERT_TRUE(s.Step()); + pragma_auto_vacuum = s.ColumnInt(0); + ASSERT_TRUE(pragma_auto_vacuum == 0 || pragma_auto_vacuum == 1); + } + + // If auto_vacuum is set, there's an extra page to maintain a freelist. + const int kExpectedPageCount = 2 + pragma_auto_vacuum; + + { + sql::Statement s(db().GetUniqueStatement("PRAGMA page_count")); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(kExpectedPageCount, s.ColumnInt(0)); + } + + { + sql::Statement s(db().GetUniqueStatement("SELECT * FROM sqlite_master")); + ASSERT_TRUE(s.Step()); + EXPECT_EQ("table", s.ColumnString(0)); + EXPECT_EQ("foo", s.ColumnString(1)); + EXPECT_EQ("foo", s.ColumnString(2)); + // Table "foo" is stored in the last page of the file. + EXPECT_EQ(kExpectedPageCount, s.ColumnInt(3)); + EXPECT_EQ(kCreateSql, s.ColumnString(4)); + } + + ASSERT_TRUE(db().Raze()); + + { + sql::Statement s(db().GetUniqueStatement("PRAGMA page_count")); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(1, s.ColumnInt(0)); + } + + { + sql::Statement s(db().GetUniqueStatement("SELECT * FROM sqlite_master")); + ASSERT_FALSE(s.Step()); + } + + { + sql::Statement s(db().GetUniqueStatement("PRAGMA auto_vacuum")); + ASSERT_TRUE(s.Step()); + // The new database has the same auto_vacuum as a fresh database. + EXPECT_EQ(pragma_auto_vacuum, s.ColumnInt(0)); + } +} + +// Test that Raze() maintains page_size. +TEST_F(SQLConnectionTest, RazePageSize) { + // Fetch the default page size and double it for use in this test. + // Scoped to release statement before Close(). + int default_page_size = 0; + { + sql::Statement s(db().GetUniqueStatement("PRAGMA page_size")); + ASSERT_TRUE(s.Step()); + default_page_size = s.ColumnInt(0); + } + ASSERT_GT(default_page_size, 0); + const int kPageSize = 2 * default_page_size; + + // Re-open the database to allow setting the page size. + db().Close(); + db().set_page_size(kPageSize); + ASSERT_TRUE(db().Open(db_path())); + + // page_size should match the indicated value. + sql::Statement s(db().GetUniqueStatement("PRAGMA page_size")); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(kPageSize, s.ColumnInt(0)); + + // After raze, page_size should still match the indicated value. + ASSERT_TRUE(db().Raze()); + s.Reset(true); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(kPageSize, s.ColumnInt(0)); +} + +// Test that Raze() results are seen in other connections. +TEST_F(SQLConnectionTest, RazeMultiple) { + const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)"; + ASSERT_TRUE(db().Execute(kCreateSql)); + + sql::Connection other_db; + ASSERT_TRUE(other_db.Open(db_path())); + + // Check that the second connection sees the table. + const char *kTablesQuery = "SELECT COUNT(*) FROM sqlite_master"; + sql::Statement s(other_db.GetUniqueStatement(kTablesQuery)); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(1, s.ColumnInt(0)); + ASSERT_FALSE(s.Step()); // Releases the shared lock. + + ASSERT_TRUE(db().Raze()); + + // The second connection sees the updated database. + s.Reset(true); + ASSERT_TRUE(s.Step()); + ASSERT_EQ(0, s.ColumnInt(0)); +} + +TEST_F(SQLConnectionTest, RazeLocked) { + const char* kCreateSql = "CREATE TABLE foo (id INTEGER PRIMARY KEY, value)"; + ASSERT_TRUE(db().Execute(kCreateSql)); + + // Open a transaction and write some data in a second connection. + // This will acquire a PENDING or EXCLUSIVE transaction, which will + // cause the raze to fail. + sql::Connection other_db; + ASSERT_TRUE(other_db.Open(db_path())); + ASSERT_TRUE(other_db.BeginTransaction()); + const char* kInsertSql = "INSERT INTO foo VALUES (1, 'data')"; + ASSERT_TRUE(other_db.Execute(kInsertSql)); + + ASSERT_FALSE(db().Raze()); + + // Works after COMMIT. + ASSERT_TRUE(other_db.CommitTransaction()); + ASSERT_TRUE(db().Raze()); + + // Re-create the database. + ASSERT_TRUE(db().Execute(kCreateSql)); + ASSERT_TRUE(db().Execute(kInsertSql)); + + // An unfinished read transaction in the other connection also + // blocks raze. + const char *kQuery = "SELECT COUNT(*) FROM foo"; + sql::Statement s(other_db.GetUniqueStatement(kQuery)); + ASSERT_TRUE(s.Step()); + ASSERT_FALSE(db().Raze()); + + // Complete the statement unlocks the database. + ASSERT_FALSE(s.Step()); + ASSERT_TRUE(db().Raze()); +} + +#if defined(OS_ANDROID) +TEST_F(SQLConnectionTest, SetTempDirForSQL) { + + sql::MetaTable meta_table; + // Below call needs a temporary directory in sqlite3 + // On Android, it can pass only when the temporary directory is set. + // Otherwise, sqlite3 doesn't find the correct directory to store + // temporary files and will report the error 'unable to open + // database file'. + ASSERT_TRUE(meta_table.Init(&db(), 4, 4)); +} +#endif + +// TODO(shess): Spin up a background thread to hold other_db, to more +// closely match real life. That would also allow testing +// RazeWithTimeout(). diff --git a/sql/diagnostic_error_delegate.h b/sql/diagnostic_error_delegate.h new file mode 100644 index 0000000000..78b3d9d815 --- /dev/null +++ b/sql/diagnostic_error_delegate.h @@ -0,0 +1,38 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SQL_DIAGNOSTIC_ERROR_DELEGATE_H_ +#define SQL_DIAGNOSTIC_ERROR_DELEGATE_H_ + +#include "base/logging.h" +#include "sql/connection.h" +#include "sql/error_delegate_util.h" +#include "sql/sql_export.h" + +namespace sql { + +// This class handles the exceptional sqlite errors that we might encounter +// if for example the db is corrupted. Right now we just generate a UMA +// histogram for release and an assert for debug builds. +// See error_delegate_util.h for an explanation as to why this class is a +// template. +template <class UniqueT> +class DiagnosticErrorDelegate : public ErrorDelegate { + public: + DiagnosticErrorDelegate() {} + virtual ~DiagnosticErrorDelegate() {} + + virtual int OnError(int error, Connection* connection, + Statement* stmt) { + LogAndRecordErrorInHistogram<UniqueT>(error, connection); + return error; + } + + private: + DISALLOW_COPY_AND_ASSIGN(DiagnosticErrorDelegate); +}; + +} // namespace sql + +#endif // SQL_DIAGNOSTIC_ERROR_DELEGATE_H_ diff --git a/sql/error_delegate_util.cc b/sql/error_delegate_util.cc new file mode 100644 index 0000000000..37fe006947 --- /dev/null +++ b/sql/error_delegate_util.cc @@ -0,0 +1,80 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "sql/error_delegate_util.h" + +#include "third_party/sqlite/sqlite3.h" + +namespace sql { + +bool IsErrorCatastrophic(int error) { + switch (error) { + case SQLITE_DONE: + case SQLITE_OK: + // Theoretically, the wrapped delegate might have resolved the error, and + // we would end up here. + return false; + + case SQLITE_CORRUPT: + case SQLITE_NOTADB: + // Highly unlikely we would ever recover from these. + return true; + + case SQLITE_CANTOPEN: + // TODO(erikwright): Figure out what this means. + return false; + + case SQLITE_IOERR: + // This could be broken blocks, in which case deleting the DB would be a + // good idea. But it might also be transient. + // TODO(erikwright): Figure out if we can distinguish between the two, + // or determine through metrics analysis to what extent these failures are + // transient. + return false; + + case SQLITE_BUSY: + // Presumably transient. + return false; + + case SQLITE_TOOBIG: + case SQLITE_FULL: + case SQLITE_NOMEM: + // Not a problem with the database. + return false; + + case SQLITE_READONLY: + // Presumably either transient or we don't have the privileges to + // move/delete the file anyway. + return false; + + case SQLITE_CONSTRAINT: + case SQLITE_ERROR: + // These probgably indicate a programming error or a migration failure + // that we prefer not to mask. + return false; + + case SQLITE_LOCKED: + case SQLITE_INTERNAL: + case SQLITE_PERM: + case SQLITE_ABORT: + case SQLITE_INTERRUPT: + case SQLITE_NOTFOUND: + case SQLITE_PROTOCOL: + case SQLITE_EMPTY: + case SQLITE_SCHEMA: + case SQLITE_MISMATCH: + case SQLITE_MISUSE: + case SQLITE_NOLFS: + case SQLITE_AUTH: + case SQLITE_FORMAT: + case SQLITE_RANGE: + case SQLITE_ROW: + // None of these appear in error reports, so for now let's not try to + // guess at how to handle them. + return false; + } + return false; +} + +} // namespace sql diff --git a/sql/error_delegate_util.h b/sql/error_delegate_util.h new file mode 100644 index 0000000000..6b90ccf8b4 --- /dev/null +++ b/sql/error_delegate_util.h @@ -0,0 +1,41 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SQL_ERROR_DELEGATE_UTIL_H_ +#define SQL_ERROR_DELEGATE_UTIL_H_ + +#include "base/metrics/histogram.h" +#include "sql/connection.h" +#include "sql/sql_export.h" + +namespace sql { + +// Returns true if it is highly unlikely that the database can recover from +// |error|. +SQL_EXPORT bool IsErrorCatastrophic(int error); + +// Log error in console in debug mode and generate a UMA histogram in release +// mode for |error| for |UniqueT::name()|. +// This function is templated because histograms need to be singletons. That is +// why they are always static at the function scope. The template parameter +// makes the compiler create unique functions that don't share the same static +// variable. +template <class UniqueT> +void LogAndRecordErrorInHistogram(int error, + sql::Connection* connection) { + LOG(ERROR) << "sqlite error " << error + << ", errno " << connection->GetLastErrno() + << ": " << connection->GetErrorMessage(); + + // Trim off the extended error codes. + error &= 0xff; + + // The histogram values from sqlite result codes currently go from 1 to 26 + // but 50 gives them room to grow. + UMA_HISTOGRAM_ENUMERATION(UniqueT::name(), error, 50); +} + +} // namespace sql + +#endif // SQL_ERROR_DELEGATE_UTIL_H_ diff --git a/sql/init_status.h b/sql/init_status.h new file mode 100644 index 0000000000..8002b4353b --- /dev/null +++ b/sql/init_status.h @@ -0,0 +1,23 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef APP_SQL_INIT_STATUS_H_ +#define APP_SQL_INIT_STATUS_H_ + +namespace sql { + +// Used as the return value for some databases' init functions. +enum InitStatus { + INIT_OK, + + // Some error, usually I/O related opening the file. + INIT_FAILURE, + + // The database is from a future version of the app and cannot be read. + INIT_TOO_NEW, +}; + +} // namespace sql + +#endif // APP_SQL_INIT_STATUS_H_ diff --git a/sql/meta_table.cc b/sql/meta_table.cc new file mode 100644 index 0000000000..45f4ee09db --- /dev/null +++ b/sql/meta_table.cc @@ -0,0 +1,153 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "sql/meta_table.h" + +#include "base/logging.h" +#include "base/string_util.h" +#include "sql/connection.h" +#include "sql/statement.h" +#include "sql/transaction.h" + +namespace sql { + +// Key used in our meta table for version numbers. +static const char kVersionKey[] = "version"; +static const char kCompatibleVersionKey[] = "last_compatible_version"; + +MetaTable::MetaTable() : db_(NULL) { +} + +MetaTable::~MetaTable() { +} + +// static +bool MetaTable::DoesTableExist(sql::Connection* db) { + DCHECK(db); + return db->DoesTableExist("meta"); +} + +bool MetaTable::Init(Connection* db, int version, int compatible_version) { + DCHECK(!db_ && db); + db_ = db; + + // If values stored are null or missing entirely, 0 will be reported. + // Require new clients to start with a greater initial version. + DCHECK_GT(version, 0); + DCHECK_GT(compatible_version, 0); + + // Make sure the table is created an populated atomically. + sql::Transaction transaction(db_); + if (!transaction.Begin()) + return false; + + if (!DoesTableExist(db)) { + if (!db_->Execute("CREATE TABLE meta" + "(key LONGVARCHAR NOT NULL UNIQUE PRIMARY KEY, value LONGVARCHAR)")) + return false; + + // Note: there is no index over the meta table. We currently only have a + // couple of keys, so it doesn't matter. If we start storing more stuff in + // there, we should create an index. + SetVersionNumber(version); + SetCompatibleVersionNumber(compatible_version); + } + return transaction.Commit(); +} + +void MetaTable::Reset() { + db_ = NULL; +} + +void MetaTable::SetVersionNumber(int version) { + DCHECK_GT(version, 0); + SetValue(kVersionKey, version); +} + +int MetaTable::GetVersionNumber() { + int version = 0; + return GetValue(kVersionKey, &version) ? version : 0; +} + +void MetaTable::SetCompatibleVersionNumber(int version) { + DCHECK_GT(version, 0); + SetValue(kCompatibleVersionKey, version); +} + +int MetaTable::GetCompatibleVersionNumber() { + int version = 0; + return GetValue(kCompatibleVersionKey, &version) ? version : 0; +} + +bool MetaTable::SetValue(const char* key, const std::string& value) { + Statement s; + PrepareSetStatement(&s, key); + s.BindString(1, value); + return s.Run(); +} + +bool MetaTable::SetValue(const char* key, int value) { + Statement s; + PrepareSetStatement(&s, key); + s.BindInt(1, value); + return s.Run(); +} + +bool MetaTable::SetValue(const char* key, int64 value) { + Statement s; + PrepareSetStatement(&s, key); + s.BindInt64(1, value); + return s.Run(); +} + +bool MetaTable::GetValue(const char* key, std::string* value) { + Statement s; + if (!PrepareGetStatement(&s, key)) + return false; + + *value = s.ColumnString(0); + return true; +} + +bool MetaTable::GetValue(const char* key, int* value) { + Statement s; + if (!PrepareGetStatement(&s, key)) + return false; + + *value = s.ColumnInt(0); + return true; +} + +bool MetaTable::GetValue(const char* key, int64* value) { + Statement s; + if (!PrepareGetStatement(&s, key)) + return false; + + *value = s.ColumnInt64(0); + return true; +} + +bool MetaTable::DeleteKey(const char* key) { + DCHECK(db_); + Statement s(db_->GetUniqueStatement("DELETE FROM meta WHERE key=?")); + s.BindCString(0, key); + return s.Run(); +} + +void MetaTable::PrepareSetStatement(Statement* statement, const char* key) { + DCHECK(db_ && statement); + statement->Assign(db_->GetCachedStatement(SQL_FROM_HERE, + "INSERT OR REPLACE INTO meta (key,value) VALUES (?,?)")); + statement->BindCString(0, key); +} + +bool MetaTable::PrepareGetStatement(Statement* statement, const char* key) { + DCHECK(db_ && statement); + statement->Assign(db_->GetCachedStatement(SQL_FROM_HERE, + "SELECT value FROM meta WHERE key=?")); + statement->BindCString(0, key); + return statement->Step(); +} + +} // namespace sql diff --git a/sql/meta_table.h b/sql/meta_table.h new file mode 100644 index 0000000000..0f4ee72c3f --- /dev/null +++ b/sql/meta_table.h @@ -0,0 +1,87 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SQL_META_TABLE_H_ +#define SQL_META_TABLE_H_ + +#include <string> + +#include "base/basictypes.h" +#include "sql/sql_export.h" + +namespace sql { + +class Connection; +class Statement; + +class SQL_EXPORT MetaTable { + public: + MetaTable(); + ~MetaTable(); + + // Returns true if the 'meta' table exists. + static bool DoesTableExist(Connection* db); + + // 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 + // greater than 0 to distinguish missing versions (see GetVersionNumber()). + bool Init(Connection* db, int version, int compatible_version); + + // Resets this MetaTable object, making another call to Init() possible. + void Reset(); + + // The version number of the database. This should be the version number of + // the creator of the file. The version number will be 0 if there is no + // previously set version number. + // + // See also Get/SetCompatibleVersionNumber(). + void SetVersionNumber(int version); + int GetVersionNumber(); + + // The compatible version number is the lowest version of the code that this + // database can be read by. If there are minor changes or additions, old + // versions of the code can still work with the database without failing. + // + // For example, if an optional column is added to a table in version 3, the + // new code will set the version to 3, and the compatible version to 2, since + // the code expecting version 2 databases can still read and write the table. + // + // Rule of thumb: check the version number when you're upgrading, but check + // the compatible version number to see if you can read the file at all. If + // it's larger than you code is expecting, fail. + // + // The compatible version number will be 0 if there is no previously set + // compatible version number. + void SetCompatibleVersionNumber(int version); + int GetCompatibleVersionNumber(); + + // Set the given arbitrary key with the given data. Returns true on success. + bool SetValue(const char* key, const std::string& value); + bool SetValue(const char* key, int value); + bool SetValue(const char* key, int64 value); + + // Retrieves the value associated with the given key. This will use sqlite's + // type conversion rules. It will return true on success. + bool GetValue(const char* key, std::string* value); + bool GetValue(const char* key, int* value); + bool GetValue(const char* key, int64* value); + + // Deletes the key from the table. + bool DeleteKey(const char* key); + + private: + // Conveniences to prepare the two types of statements used by + // MetaTableHelper. + void PrepareSetStatement(Statement* statement, const char* key); + bool PrepareGetStatement(Statement* statement, const char* key); + + Connection* db_; + + DISALLOW_COPY_AND_ASSIGN(MetaTable); +}; + +} // namespace sql + +#endif // SQL_META_TABLE_H_ diff --git a/sql/run_all_unittests.cc b/sql/run_all_unittests.cc new file mode 100644 index 0000000000..969b09164a --- /dev/null +++ b/sql/run_all_unittests.cc @@ -0,0 +1,11 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/test/main_hook.h" +#include "base/test/test_suite.h" + +int main(int argc, char** argv) { + MainHook hook(main, argc, argv); + return base::TestSuite(argc, argv).Run(); +} diff --git a/sql/sql.gyp b/sql/sql.gyp new file mode 100644 index 0000000000..86962b3d02 --- /dev/null +++ b/sql/sql.gyp @@ -0,0 +1,89 @@ +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +{ + 'variables': { + 'chromium_code': 1, + }, + 'targets': [ + { + 'target_name': 'sql', + 'type': '<(component)', + 'dependencies': [ + '../base/base.gyp:base', + '../third_party/sqlite/sqlite.gyp:sqlite', + ], + 'defines': [ 'SQL_IMPLEMENTATION' ], + 'sources': [ + 'connection.cc', + 'connection.h', + 'diagnostic_error_delegate.h', + 'error_delegate_util.cc', + 'error_delegate_util.h', + 'init_status.h', + 'meta_table.cc', + 'meta_table.h', + 'statement.cc', + 'statement.h', + 'transaction.cc', + 'transaction.h', + ], + }, + { + 'target_name': 'sql_unittests', + 'type': '<(gtest_target_type)', + 'dependencies': [ + 'sql', + '../base/base.gyp:test_support_base', + '../testing/gtest.gyp:gtest', + ], + 'sources': [ + 'run_all_unittests.cc', + 'connection_unittest.cc', + 'sqlite_features_unittest.cc', + 'statement_unittest.cc', + 'transaction_unittest.cc', + ], + 'include_dirs': [ + '..', + ], + 'conditions': [ + ['os_posix==1 and OS!="mac" and OS!="ios"', { + 'conditions': [ + ['linux_use_tcmalloc==1', { + 'dependencies': [ + '../base/allocator/allocator.gyp:allocator', + ], + }], + ], + }], + ['OS == "android" and gtest_target_type == "shared_library"', { + 'dependencies': [ + '../testing/android/native_test.gyp:native_test_native_code', + ], + }], + ], + }, + ], + 'conditions': [ + # Special target to wrap a gtest_target_type==shared_library + # sql_unittests into an android apk for execution. + ['OS == "android" and gtest_target_type == "shared_library"', { + 'targets': [ + { + 'target_name': 'sql_unittests_apk', + 'type': 'none', + 'dependencies': [ + 'sql_unittests', + ], + 'variables': { + 'test_suite_name': 'sql_unittests', + 'input_shlib_path': '<(SHARED_LIB_DIR)/<(SHARED_LIB_PREFIX)sql_unittests<(SHARED_LIB_SUFFIX)', + }, + 'includes': [ '../build/apk_test.gypi' ], + }, + ], + }], + ], +} diff --git a/sql/sql.target.mk b/sql/sql.target.mk new file mode 100644 index 0000000000..029beef85b --- /dev/null +++ b/sql/sql.target.mk @@ -0,0 +1,153 @@ +# This file is generated by gyp; do not edit. + +include $(CLEAR_VARS) + +LOCAL_MODULE_CLASS := STATIC_LIBRARIES +LOCAL_MODULE := sql_sql_gyp +LOCAL_MODULE_SUFFIX := .a +LOCAL_MODULE_TAGS := optional +gyp_intermediate_dir := $(call local-intermediates-dir) +gyp_shared_intermediate_dir := $(call intermediates-dir-for,GYP,shared) + +# Make sure our deps are built first. +GYP_TARGET_DEPENDENCIES := + +GYP_GENERATED_OUTPUTS := + +# Make sure our deps and generated files are built first. +LOCAL_ADDITIONAL_DEPENDENCIES := $(GYP_TARGET_DEPENDENCIES) $(GYP_GENERATED_OUTPUTS) + +LOCAL_CPP_EXTENSION := .cc +LOCAL_GENERATED_SOURCES := + +GYP_COPIED_SOURCE_ORIGIN_DIRS := + +LOCAL_SRC_FILES := \ + sql/connection.cc \ + sql/error_delegate_util.cc \ + sql/meta_table.cc \ + sql/statement.cc \ + sql/transaction.cc + + +# Flags passed to both C and C++ files. +MY_CFLAGS := \ + -Werror \ + -fno-exceptions \ + -fno-strict-aliasing \ + -Wall \ + -Wno-unused-parameter \ + -Wno-missing-field-initializers \ + -fvisibility=hidden \ + -pipe \ + -fPIC \ + -mthumb \ + -march=armv7-a \ + -mtune=cortex-a8 \ + -mfloat-abi=softfp \ + -mfpu=vfpv3-d16 \ + -fno-tree-sra \ + -fuse-ld=gold \ + -Wno-psabi \ + -mthumb-interwork \ + -ffunction-sections \ + -funwind-tables \ + -g \ + -fstack-protector \ + -fno-short-enums \ + -finline-limit=64 \ + -Wa,--noexecstack \ + -Wno-error=extra \ + -Wno-error=ignored-qualifiers \ + -Wno-error=type-limits \ + -Wno-error=non-virtual-dtor \ + -Wno-error=sign-promo \ + -Os \ + -g \ + -fomit-frame-pointer \ + -fdata-sections \ + -ffunction-sections + +MY_CFLAGS_C := + +MY_DEFS := \ + '-D_FILE_OFFSET_BITS=64' \ + '-DNO_TCMALLOC' \ + '-DDISABLE_NACL' \ + '-DCHROMIUM_BUILD' \ + '-DUSE_LIBJPEG_TURBO=1' \ + '-DUSE_PROPRIETARY_CODECS' \ + '-DENABLE_PEPPER_THREADING' \ + '-DENABLE_GPU=1' \ + '-DUSE_OPENSSL=1' \ + '-DENABLE_EGLIMAGE=1' \ + '-DUSE_SKIA=1' \ + '-DSQL_IMPLEMENTATION' \ + '-D__STDC_CONSTANT_MACROS' \ + '-D__STDC_FORMAT_MACROS' \ + '-DANDROID' \ + '-D__GNU_SOURCE=1' \ + '-DUSE_STLPORT=1' \ + '-D_STLP_USE_PTR_SPECIALIZATIONS=1' \ + '-DCHROME_SYMBOLS_ID=""' \ + '-DANDROID_UPSTREAM_BRINGUP=1' \ + '-DDYNAMIC_ANNOTATIONS_ENABLED=1' \ + '-DWTF_USE_DYNAMIC_ANNOTATIONS=1' \ + '-D_DEBUG' + +LOCAL_CFLAGS := $(MY_CFLAGS_C) $(MY_CFLAGS) $(MY_DEFS) + +# Include paths placed before CFLAGS/CPPFLAGS +LOCAL_C_INCLUDES := \ + $(LOCAL_PATH) \ + $(LOCAL_PATH)/third_party/sqlite \ + $(GYP_ABS_ANDROID_TOP_DIR)/frameworks/wilhelm/include \ + $(GYP_ABS_ANDROID_TOP_DIR)/bionic \ + $(GYP_ABS_ANDROID_TOP_DIR)/external/stlport/stlport + +LOCAL_C_INCLUDES := $(GYP_COPIED_SOURCE_ORIGIN_DIRS) $(LOCAL_C_INCLUDES) + +# Flags passed to only C++ (and not C) files. +LOCAL_CPPFLAGS := \ + -fno-rtti \ + -fno-threadsafe-statics \ + -fvisibility-inlines-hidden \ + -Wsign-compare \ + -Wno-abi \ + -Wno-error=c++0x-compat + +### Rules for final target. + +LOCAL_LDFLAGS := \ + -Wl,-z,noexecstack \ + -fPIC \ + -Wl,-z,relro \ + -Wl,-z,now \ + -fuse-ld=gold \ + -nostdlib \ + -Wl,--no-undefined \ + -Wl,--exclude-libs=ALL \ + -Wl,--icf=safe \ + -Wl,-O1 \ + -Wl,--as-needed \ + -Wl,--gc-sections + + +LOCAL_STATIC_LIBRARIES := + +# Enable grouping to fix circular references +LOCAL_GROUP_STATIC_LIBRARIES := true + +LOCAL_SHARED_LIBRARIES := \ + libstlport \ + libdl + +# Add target alias to "gyp_all_modules" target. +.PHONY: gyp_all_modules +gyp_all_modules: sql_sql_gyp + +# Alias gyp target name. +.PHONY: sql +sql: sql_sql_gyp + +include $(BUILD_STATIC_LIBRARY) diff --git a/sql/sql_export.h b/sql/sql_export.h new file mode 100644 index 0000000000..7ab3355849 --- /dev/null +++ b/sql/sql_export.h @@ -0,0 +1,29 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SQL_EXPORT_H_ +#define SQL_EXPORT_H_ + +#if defined(COMPONENT_BUILD) +#if defined(WIN32) + +#if defined(SQL_IMPLEMENTATION) +#define SQL_EXPORT __declspec(dllexport) +#else +#define SQL_EXPORT __declspec(dllimport) +#endif // defined(SQL_IMPLEMENTATION) + +#else // defined(WIN32) +#if defined(SQL_IMPLEMENTATION) +#define SQL_EXPORT __attribute__((visibility("default"))) +#else +#define SQL_EXPORT +#endif +#endif + +#else // defined(COMPONENT_BUILD) +#define SQL_EXPORT +#endif + +#endif // SQL_EXPORT_H_ diff --git a/sql/sqlite_features_unittest.cc b/sql/sqlite_features_unittest.cc new file mode 100644 index 0000000000..7e750e97c6 --- /dev/null +++ b/sql/sqlite_features_unittest.cc @@ -0,0 +1,100 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <string> + +#include "base/file_util.h" +#include "base/scoped_temp_dir.h" +#include "sql/connection.h" +#include "sql/statement.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/sqlite3.h" + +// Test that certain features are/are-not enabled in our SQLite. + +namespace { + +class StatementErrorHandler : public sql::ErrorDelegate { + public: + StatementErrorHandler(int* error, std::string* sql_text) + : error_(error), + sql_text_(sql_text) {} + + virtual ~StatementErrorHandler() {} + + virtual int OnError(int error, sql::Connection* connection, + sql::Statement* stmt) OVERRIDE { + *error_ = error; + const char* sql_txt = stmt ? stmt->GetSQLStatement() : NULL; + *sql_text_ = sql_txt ? sql_txt : "no statement available"; + return error; + } + + private: + int* error_; + std::string* sql_text_; + + DISALLOW_COPY_AND_ASSIGN(StatementErrorHandler); +}; + +class SQLiteFeaturesTest : public testing::Test { + public: + SQLiteFeaturesTest() : error_(SQLITE_OK) {} + + void SetUp() { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + ASSERT_TRUE(db_.Open(temp_dir_.path().AppendASCII("SQLStatementTest.db"))); + + // The error delegate will set |error_| and |sql_text_| when any sqlite + // statement operation returns an error code. + db_.set_error_delegate(new StatementErrorHandler(&error_, &sql_text_)); + } + + void TearDown() { + // If any error happened the original sql statement can be found in + // |sql_text_|. + EXPECT_EQ(SQLITE_OK, error_); + db_.Close(); + } + + sql::Connection& db() { return db_; } + + int sqlite_error() const { + return error_; + } + + private: + ScopedTempDir temp_dir_; + sql::Connection db_; + + // The error code of the most recent error. + int error_; + // Original statement which has caused the error. + std::string sql_text_; +}; + +// Do not include fts1 support, it is not useful, and nobody is +// looking at it. +TEST_F(SQLiteFeaturesTest, NoFTS1) { + ASSERT_EQ(SQLITE_ERROR, db().ExecuteAndReturnErrorCode( + "CREATE VIRTUAL TABLE foo USING fts1(x)")); +} + +#if !defined(OS_IOS) +// fts2 is used for older history files, so we're signed on for keeping our +// version up-to-date. iOS does not include fts2, so this test does not run on +// iOS. +// TODO(shess): Think up a crazy way to get out from having to support +// this forever. +TEST_F(SQLiteFeaturesTest, FTS2) { + ASSERT_TRUE(db().Execute("CREATE VIRTUAL TABLE foo USING fts2(x)")); +} +#endif + +// fts3 is used for current history files, and also for WebDatabase. +TEST_F(SQLiteFeaturesTest, FTS3) { + ASSERT_TRUE(db().Execute("CREATE VIRTUAL TABLE foo USING fts3(x)")); +} + +} // namespace diff --git a/sql/statement.cc b/sql/statement.cc new file mode 100644 index 0000000000..84dfd2eb9a --- /dev/null +++ b/sql/statement.cc @@ -0,0 +1,314 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "sql/statement.h" + +#include "base/logging.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "third_party/sqlite/sqlite3.h" + +namespace sql { + +// This empty constructor initializes our reference with an empty one so that +// we don't have to NULL-check the ref_ to see if the statement is valid: we +// only have to check the ref's validity bit. +Statement::Statement() + : ref_(new Connection::StatementRef), + succeeded_(false) { +} + +Statement::Statement(scoped_refptr<Connection::StatementRef> ref) + : ref_(ref), + succeeded_(false) { +} + +Statement::~Statement() { + // Free the resources associated with this statement. We assume there's only + // one statement active for a given sqlite3_stmt at any time, so this won't + // mess with anything. + Reset(true); +} + +void Statement::Assign(scoped_refptr<Connection::StatementRef> ref) { + Reset(true); + ref_ = ref; +} + +void Statement::Clear() { + Assign(new Connection::StatementRef); + succeeded_ = false; +} + +bool Statement::CheckValid() const { + if (!is_valid()) + DLOG(FATAL) << "Cannot call mutating statements on an invalid statement."; + return is_valid(); +} + +bool Statement::Run() { + ref_->AssertIOAllowed(); + if (!CheckValid()) + return false; + + return CheckError(sqlite3_step(ref_->stmt())) == SQLITE_DONE; +} + +bool Statement::Step() { + ref_->AssertIOAllowed(); + if (!CheckValid()) + return false; + + return CheckError(sqlite3_step(ref_->stmt())) == SQLITE_ROW; +} + +void Statement::Reset(bool clear_bound_vars) { + ref_->AssertIOAllowed(); + if (is_valid()) { + // We don't call CheckError() here because sqlite3_reset() returns + // the last error that Step() caused thereby generating a second + // spurious error callback. + if (clear_bound_vars) + sqlite3_clear_bindings(ref_->stmt()); + sqlite3_reset(ref_->stmt()); + } + + succeeded_ = false; +} + +bool Statement::Succeeded() const { + if (!is_valid()) + return false; + + return succeeded_; +} + +bool Statement::BindNull(int col) { + if (!is_valid()) + return false; + + return CheckOk(sqlite3_bind_null(ref_->stmt(), col + 1)); +} + +bool Statement::BindBool(int col, bool val) { + return BindInt(col, val ? 1 : 0); +} + +bool Statement::BindInt(int col, int val) { + if (!is_valid()) + return false; + + return CheckOk(sqlite3_bind_int(ref_->stmt(), col + 1, val)); +} + +bool Statement::BindInt64(int col, int64 val) { + if (!is_valid()) + return false; + + return CheckOk(sqlite3_bind_int64(ref_->stmt(), col + 1, val)); +} + +bool Statement::BindDouble(int col, double val) { + if (!is_valid()) + return false; + + return CheckOk(sqlite3_bind_double(ref_->stmt(), col + 1, val)); +} + +bool Statement::BindCString(int col, const char* val) { + if (!is_valid()) + return false; + + return CheckOk( + sqlite3_bind_text(ref_->stmt(), col + 1, val, -1, SQLITE_TRANSIENT)); +} + +bool Statement::BindString(int col, const std::string& val) { + if (!is_valid()) + return false; + + return CheckOk(sqlite3_bind_text(ref_->stmt(), + col + 1, + val.data(), + val.size(), + SQLITE_TRANSIENT)); +} + +bool Statement::BindString16(int col, const string16& value) { + return BindString(col, UTF16ToUTF8(value)); +} + +bool Statement::BindBlob(int col, const void* val, int val_len) { + if (!is_valid()) + return false; + + return CheckOk( + sqlite3_bind_blob(ref_->stmt(), col + 1, val, val_len, SQLITE_TRANSIENT)); +} + +int Statement::ColumnCount() const { + if (!is_valid()) + return 0; + + return sqlite3_column_count(ref_->stmt()); +} + +ColType Statement::ColumnType(int col) const { + // Verify that our enum matches sqlite's values. + COMPILE_ASSERT(COLUMN_TYPE_INTEGER == SQLITE_INTEGER, integer_no_match); + COMPILE_ASSERT(COLUMN_TYPE_FLOAT == SQLITE_FLOAT, float_no_match); + COMPILE_ASSERT(COLUMN_TYPE_TEXT == SQLITE_TEXT, integer_no_match); + COMPILE_ASSERT(COLUMN_TYPE_BLOB == SQLITE_BLOB, blob_no_match); + COMPILE_ASSERT(COLUMN_TYPE_NULL == SQLITE_NULL, null_no_match); + + return static_cast<ColType>(sqlite3_column_type(ref_->stmt(), col)); +} + +ColType Statement::DeclaredColumnType(int col) const { + std::string column_type(sqlite3_column_decltype(ref_->stmt(), col)); + StringToLowerASCII(&column_type); + + if (column_type == "integer") + return COLUMN_TYPE_INTEGER; + else if (column_type == "float") + return COLUMN_TYPE_FLOAT; + else if (column_type == "text") + return COLUMN_TYPE_TEXT; + else if (column_type == "blob") + return COLUMN_TYPE_BLOB; + + return COLUMN_TYPE_NULL; +} + +bool Statement::ColumnBool(int col) const { + return !!ColumnInt(col); +} + +int Statement::ColumnInt(int col) const { + if (!CheckValid()) + return 0; + + return sqlite3_column_int(ref_->stmt(), col); +} + +int64 Statement::ColumnInt64(int col) const { + if (!CheckValid()) + return 0; + + return sqlite3_column_int64(ref_->stmt(), col); +} + +double Statement::ColumnDouble(int col) const { + if (!CheckValid()) + return 0; + + return sqlite3_column_double(ref_->stmt(), col); +} + +std::string Statement::ColumnString(int col) const { + if (!CheckValid()) + return ""; + + const char* str = reinterpret_cast<const char*>( + sqlite3_column_text(ref_->stmt(), col)); + int len = sqlite3_column_bytes(ref_->stmt(), col); + + std::string result; + if (str && len > 0) + result.assign(str, len); + return result; +} + +string16 Statement::ColumnString16(int col) const { + if (!CheckValid()) + return string16(); + + std::string s = ColumnString(col); + return !s.empty() ? UTF8ToUTF16(s) : string16(); +} + +int Statement::ColumnByteLength(int col) const { + if (!CheckValid()) + return 0; + + return sqlite3_column_bytes(ref_->stmt(), col); +} + +const void* Statement::ColumnBlob(int col) const { + if (!CheckValid()) + return NULL; + + return sqlite3_column_blob(ref_->stmt(), col); +} + +bool Statement::ColumnBlobAsString(int col, std::string* blob) { + if (!CheckValid()) + return false; + + const void* p = ColumnBlob(col); + size_t len = ColumnByteLength(col); + blob->resize(len); + if (blob->size() != len) { + return false; + } + blob->assign(reinterpret_cast<const char*>(p), len); + return true; +} + +bool Statement::ColumnBlobAsString16(int col, string16* val) const { + if (!CheckValid()) + return false; + + const void* data = ColumnBlob(col); + size_t len = ColumnByteLength(col) / sizeof(char16); + val->resize(len); + if (val->size() != len) + return false; + val->assign(reinterpret_cast<const char16*>(data), len); + return true; +} + +bool Statement::ColumnBlobAsVector(int col, std::vector<char>* val) const { + val->clear(); + + if (!CheckValid()) + return false; + + const void* data = sqlite3_column_blob(ref_->stmt(), col); + int len = sqlite3_column_bytes(ref_->stmt(), col); + if (data && len > 0) { + val->resize(len); + memcpy(&(*val)[0], data, len); + } + return true; +} + +bool Statement::ColumnBlobAsVector( + int col, + std::vector<unsigned char>* val) const { + return ColumnBlobAsVector(col, reinterpret_cast< std::vector<char>* >(val)); +} + +const char* Statement::GetSQLStatement() { + return sqlite3_sql(ref_->stmt()); +} + +bool Statement::CheckOk(int err) const { + // Binding to a non-existent variable is evidence of a serious error. + // TODO(gbillock,shess): make this invalidate the statement so it + // can't wreak havoc. + if (err == SQLITE_RANGE) + DLOG(FATAL) << "Bind value out of range"; + return err == SQLITE_OK; +} + +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_ && is_valid()) + return ref_->connection()->OnSqliteError(err, this); + return err; +} + +} // namespace sql diff --git a/sql/statement.h b/sql/statement.h new file mode 100644 index 0000000000..bd00b0d68d --- /dev/null +++ b/sql/statement.h @@ -0,0 +1,189 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SQL_STATEMENT_H_ +#define SQL_STATEMENT_H_ + +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/memory/ref_counted.h" +#include "base/string16.h" +#include "sql/connection.h" +#include "sql/sql_export.h" + +namespace sql { + +// Possible return values from ColumnType in a statement. These should match +// the values in sqlite3.h. +enum ColType { + COLUMN_TYPE_INTEGER = 1, + COLUMN_TYPE_FLOAT = 2, + COLUMN_TYPE_TEXT = 3, + COLUMN_TYPE_BLOB = 4, + COLUMN_TYPE_NULL = 5, +}; + +// Normal usage: +// sql::Statement s(connection_.GetUniqueStatement(...)); +// s.BindInt(0, a); +// if (s.Step()) +// return s.ColumnString(0); +// +// If there are errors getting the statement, the statement will be inert; no +// mutating or database-access methods will work. If you need to check for +// validity, use: +// if (!s.is_valid()) +// return false; +// +// Step() and Run() just return true to signal success. If you want to handle +// specific errors such as database corruption, install an error handler in +// in the connection object using set_error_delegate(). +class SQL_EXPORT Statement { + public: + // Creates an uninitialized statement. The statement will be invalid until + // you initialize it via Assign. + Statement(); + + explicit Statement(scoped_refptr<Connection::StatementRef> ref); + ~Statement(); + + // Initializes this object with the given statement, which may or may not + // be valid. Use is_valid() to check if it's OK. + void Assign(scoped_refptr<Connection::StatementRef> ref); + + // Resets the statement to an uninitialized state corrosponding to + // the default constructor, releasing the StatementRef. + void Clear(); + + // Returns true if the statement can be executed. All functions can still + // be used if the statement is invalid, but they will return failure or some + // default value. This is because the statement can become invalid in the + // middle of executing a command if there is a serious error and the database + // has to be reset. + bool is_valid() const { return ref_->is_valid(); } + + // Running ------------------------------------------------------------------- + + // Executes the statement, returning true on success. This is like Step but + // for when there is no output, like an INSERT statement. + bool Run(); + + // Executes the statement, returning true if there is a row of data returned. + // You can keep calling Step() until it returns false to iterate through all + // the rows in your result set. + // + // When Step returns false, the result is either that there is no more data + // or there is an error. This makes it most convenient for loop usage. If you + // need to disambiguate these cases, use Succeeded(). + // + // Typical example: + // while (s.Step()) { + // ... + // } + // return s.Succeeded(); + bool Step(); + + // Resets the statement to its initial condition. This includes any current + // result row, and also the bound variables if the |clear_bound_vars| is true. + void Reset(bool clear_bound_vars); + + // Returns true if the last executed thing in this statement succeeded. If + // there was no last executed thing or the statement is invalid, this will + // return false. + bool Succeeded() const; + + // Binding ------------------------------------------------------------------- + + // These all take a 0-based argument index and return true on success. You + // may not always care about the return value (they'll DCHECK if they fail). + // The main thing you may want to check is when binding large blobs or + // strings there may be out of memory. + bool BindNull(int col); + bool BindBool(int col, bool val); + bool BindInt(int col, int val); + bool BindInt64(int col, int64 val); + 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 BindBlob(int col, const void* value, int value_len); + + // Retrieving ---------------------------------------------------------------- + + // Returns the number of output columns in the result. + int ColumnCount() const; + + // Returns the type associated with the given column. + // + // Watch out: the type may be undefined if you've done something to cause a + // "type conversion." This means requesting the value of a column of a type + // where that type is not the native type. For safety, call ColumnType only + // on a column before getting the value out in any way. + ColType ColumnType(int col) const; + ColType DeclaredColumnType(int col) const; + + // These all take a 0-based argument index. + bool ColumnBool(int col) const; + int ColumnInt(int col) const; + int64 ColumnInt64(int col) const; + double ColumnDouble(int col) const; + std::string ColumnString(int col) const; + 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 + // vector. Danger! ColumnBlob may return NULL if there is no data! + 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 ColumnBlobAsVector(int col, std::vector<char>* val) const; + bool ColumnBlobAsVector(int col, std::vector<unsigned char>* val) const; + + // Diagnostics -------------------------------------------------------------- + + // Returns the original text of sql statement. Do not keep a pointer to it. + const char* GetSQLStatement(); + + private: + // This is intended to check for serious errors and report them to the + // connection object. It takes a sqlite error code, and returns the same + // code. Currently this function just updates the succeeded flag, but will be + // enhanced in the future to do the notification. + int CheckError(int err); + + // Contraction for checking an error code against SQLITE_OK. Does not set the + // succeeded flag. + bool CheckOk(int err) const; + + // Should be called by all mutating methods to check that the statement is + // valid. Returns true if the statement is valid. DCHECKS and returns false + // if it is not. + // The reason for this is to handle two specific cases in which a Statement + // may be invalid. The first case is that the programmer made an SQL error. + // Those cases need to be DCHECKed so that we are guaranteed to find them + // before release. The second case is that the computer has an error (probably + // out of disk space) which is prohibiting the correct operation of the + // database. Our testing apparatus should not exhibit this defect, but release + // situations may. Therefore, the code is handling disjoint situations in + // release and test. In test, we're ensuring correct SQL. In release, we're + // ensuring that contracts are honored in error edge cases. + bool CheckValid() const; + + // The actual sqlite statement. This may be unique to us, or it may be cached + // by the connection, which is why it's refcounted. This pointer is + // guaranteed non-NULL. + scoped_refptr<Connection::StatementRef> ref_; + + // See Succeeded() for what this holds. + bool succeeded_; + + DISALLOW_COPY_AND_ASSIGN(Statement); +}; + +} // namespace sql + +#endif // SQL_STATEMENT_H_ diff --git a/sql/statement_unittest.cc b/sql/statement_unittest.cc new file mode 100644 index 0000000000..a7a23d8294 --- /dev/null +++ b/sql/statement_unittest.cc @@ -0,0 +1,152 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <string> + +#include "base/file_util.h" +#include "base/scoped_temp_dir.h" +#include "sql/connection.h" +#include "sql/statement.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/sqlite3.h" + +namespace { + +class StatementErrorHandler : public sql::ErrorDelegate { + public: + StatementErrorHandler(int* error, std::string* sql_text) + : error_(error), + sql_text_(sql_text) {} + + virtual ~StatementErrorHandler() {} + + virtual int OnError(int error, sql::Connection* connection, + sql::Statement* stmt) OVERRIDE { + *error_ = error; + const char* sql_txt = stmt ? stmt->GetSQLStatement() : NULL; + *sql_text_ = sql_txt ? sql_txt : "no statement available"; + return error; + } + + private: + int* error_; + std::string* sql_text_; + + DISALLOW_COPY_AND_ASSIGN(StatementErrorHandler); +}; + +class SQLStatementTest : public testing::Test { + public: + SQLStatementTest() : error_(SQLITE_OK) {} + + void SetUp() { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + ASSERT_TRUE(db_.Open(temp_dir_.path().AppendASCII("SQLStatementTest.db"))); + // The error delegate will set |error_| and |sql_text_| when any sqlite + // statement operation returns an error code. + db_.set_error_delegate(new StatementErrorHandler(&error_, &sql_text_)); + } + + void TearDown() { + // If any error happened the original sql statement can be found in + // |sql_text_|. + EXPECT_EQ(SQLITE_OK, error_); + db_.Close(); + } + + sql::Connection& db() { return db_; } + + int sqlite_error() const { return error_; } + + void ResetError() { + error_ = SQLITE_OK; + sql_text_.clear(); + } + + private: + ScopedTempDir temp_dir_; + sql::Connection db_; + + // The error code of the most recent error. + int error_; + // Original statement which caused the error. + std::string sql_text_; +}; + +} // namespace + +TEST_F(SQLStatementTest, Assign) { + sql::Statement s; + EXPECT_FALSE(s.is_valid()); + + s.Assign(db().GetUniqueStatement("CREATE TABLE foo (a, b)")); + EXPECT_TRUE(s.is_valid()); +} + +TEST_F(SQLStatementTest, Run) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)")); + + sql::Statement s(db().GetUniqueStatement("SELECT b FROM foo WHERE a=?")); + EXPECT_FALSE(s.Succeeded()); + + // Stepping it won't work since we haven't bound the value. + EXPECT_FALSE(s.Step()); + + // Run should fail since this produces output, and we should use Step(). This + // gets a bit wonky since sqlite says this is OK so succeeded is set. + s.Reset(true); + s.BindInt(0, 3); + EXPECT_FALSE(s.Run()); + EXPECT_EQ(SQLITE_ROW, db().GetErrorCode()); + EXPECT_TRUE(s.Succeeded()); + + // Resetting it should put it back to the previous state (not runnable). + s.Reset(true); + EXPECT_FALSE(s.Succeeded()); + + // Binding and stepping should produce one row. + s.BindInt(0, 3); + EXPECT_TRUE(s.Step()); + EXPECT_TRUE(s.Succeeded()); + EXPECT_EQ(12, s.ColumnInt(0)); + EXPECT_FALSE(s.Step()); + EXPECT_TRUE(s.Succeeded()); +} + +TEST_F(SQLStatementTest, BasicErrorCallback) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a INTEGER PRIMARY KEY, b)")); + EXPECT_EQ(SQLITE_OK, sqlite_error()); + // Insert in the foo table the primary key. It is an error to insert + // something other than an number. This error causes the error callback + // handler to be called with SQLITE_MISMATCH as error code. + sql::Statement s(db().GetUniqueStatement("INSERT INTO foo (a) VALUES (?)")); + EXPECT_TRUE(s.is_valid()); + s.BindCString(0, "bad bad"); + EXPECT_FALSE(s.Run()); + EXPECT_EQ(SQLITE_MISMATCH, sqlite_error()); + ResetError(); +} + +TEST_F(SQLStatementTest, Reset) { + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (3, 12)")); + ASSERT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (4, 13)")); + + sql::Statement s(db().GetUniqueStatement( + "SELECT b FROM foo WHERE a = ? ")); + s.BindInt(0, 3); + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + ASSERT_FALSE(s.Step()); + + s.Reset(false); + // Verify that we can get all rows again. + ASSERT_TRUE(s.Step()); + EXPECT_EQ(12, s.ColumnInt(0)); + EXPECT_FALSE(s.Step()); + + s.Reset(true); + ASSERT_FALSE(s.Step()); +} diff --git a/sql/transaction.cc b/sql/transaction.cc new file mode 100644 index 0000000000..06bcbebebd --- /dev/null +++ b/sql/transaction.cc @@ -0,0 +1,51 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "sql/transaction.h" + +#include "base/logging.h" +#include "sql/connection.h" + +namespace sql { + +Transaction::Transaction(Connection* connection) + : connection_(connection), + is_open_(false) { +} + +Transaction::~Transaction() { + if (is_open_) + connection_->RollbackTransaction(); +} + +bool Transaction::Begin() { + if (is_open_) { + NOTREACHED() << "Beginning a transaction twice!"; + return false; + } + is_open_ = connection_->BeginTransaction(); + return is_open_; +} + +void Transaction::Rollback() { + if (!is_open_) { + NOTREACHED() << "Attempting to roll back a nonexistent transaction. " + << "Did you remember to call Begin() and check its return?"; + return; + } + is_open_ = false; + connection_->RollbackTransaction(); +} + +bool Transaction::Commit() { + if (!is_open_) { + NOTREACHED() << "Attempting to commit a nonexistent transaction. " + << "Did you remember to call Begin() and check its return?"; + return false; + } + is_open_ = false; + return connection_->CommitTransaction(); +} + +} // namespace sql diff --git a/sql/transaction.h b/sql/transaction.h new file mode 100644 index 0000000000..788a002f83 --- /dev/null +++ b/sql/transaction.h @@ -0,0 +1,60 @@ +// Copyright (c) 2012 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SQL_TRANSACTION_H_ +#define SQL_TRANSACTION_H_ + +#include "base/basictypes.h" +#include "sql/sql_export.h" + +namespace sql { + +class Connection; + +class SQL_EXPORT Transaction { + public: + // Creates the scoped transaction object. You MUST call Begin() to begin the + // transaction. If you have begun a transaction and not committed it, the + // constructor will roll back the transaction. If you want to commit, you + // need to manually call Commit before this goes out of scope. + // + // Nested transactions are supported. See sql::Connection::BeginTransaction + // for details. + explicit Transaction(Connection* connection); + ~Transaction(); + + // Returns true when there is a transaction that has been successfully begun. + bool is_open() const { return is_open_; } + + // Begins the transaction. This uses the default sqlite "deferred" transaction + // type, which means that the DB lock is lazily acquired the next time the + // database is accessed, not in the begin transaction command. + // + // Returns false on failure. Note that if this fails, you shouldn't do + // anything you expect to be actually transactional, because it won't be! + bool Begin(); + + // Rolls back the transaction. This will happen automatically if you do + // nothing when the transaction goes out of scope. + void Rollback(); + + // Commits the transaction, returning true on success. This will return + // false if sqlite could not commit it, or if another transaction in the + // same outermost transaction has been rolled back (which necessitates a + // rollback of all transactions in that outermost one). + bool Commit(); + + private: + Connection* connection_; + + // True when the transaction is open, false when it's already been committed + // or rolled back. + bool is_open_; + + DISALLOW_COPY_AND_ASSIGN(Transaction); +}; + +} // namespace sql + +#endif // SQL_TRANSACTION_H_ diff --git a/sql/transaction_unittest.cc b/sql/transaction_unittest.cc new file mode 100644 index 0000000000..f306a5cb6e --- /dev/null +++ b/sql/transaction_unittest.cc @@ -0,0 +1,133 @@ +// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "base/file_util.h" +#include "base/scoped_temp_dir.h" +#include "sql/connection.h" +#include "sql/statement.h" +#include "sql/transaction.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/sqlite3.h" + +class SQLTransactionTest : public testing::Test { + public: + SQLTransactionTest() {} + + void SetUp() { + ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); + ASSERT_TRUE(db_.Open( + temp_dir_.path().AppendASCII("SQLTransactionTest.db"))); + + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + } + + void TearDown() { + db_.Close(); + } + + sql::Connection& db() { return db_; } + + // Returns the number of rows in table "foo". + int CountFoo() { + sql::Statement count(db().GetUniqueStatement("SELECT count(*) FROM foo")); + count.Step(); + return count.ColumnInt(0); + } + + private: + ScopedTempDir temp_dir_; + sql::Connection db_; +}; + +TEST_F(SQLTransactionTest, Commit) { + { + sql::Transaction t(&db()); + EXPECT_FALSE(t.is_open()); + EXPECT_TRUE(t.Begin()); + EXPECT_TRUE(t.is_open()); + + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + + t.Commit(); + EXPECT_FALSE(t.is_open()); + } + + EXPECT_EQ(1, CountFoo()); +} + +TEST_F(SQLTransactionTest, Rollback) { + // Test some basic initialization, and that rollback runs when you exit the + // scope. + { + sql::Transaction t(&db()); + EXPECT_FALSE(t.is_open()); + EXPECT_TRUE(t.Begin()); + EXPECT_TRUE(t.is_open()); + + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + } + + // Nothing should have been committed since it was implicitly rolled back. + EXPECT_EQ(0, CountFoo()); + + // Test explicit rollback. + sql::Transaction t2(&db()); + EXPECT_FALSE(t2.is_open()); + EXPECT_TRUE(t2.Begin()); + + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + t2.Rollback(); + EXPECT_FALSE(t2.is_open()); + + // Nothing should have been committed since it was explicitly rolled back. + EXPECT_EQ(0, CountFoo()); +} + +// Rolling back any part of a transaction should roll back all of them. +TEST_F(SQLTransactionTest, NestedRollback) { + EXPECT_EQ(0, db().transaction_nesting()); + + // Outermost transaction. + { + sql::Transaction outer(&db()); + EXPECT_TRUE(outer.Begin()); + EXPECT_EQ(1, db().transaction_nesting()); + + // The first inner one gets committed. + { + sql::Transaction inner1(&db()); + EXPECT_TRUE(inner1.Begin()); + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + EXPECT_EQ(2, db().transaction_nesting()); + + inner1.Commit(); + EXPECT_EQ(1, db().transaction_nesting()); + } + + // One row should have gotten inserted. + EXPECT_EQ(1, CountFoo()); + + // The second inner one gets rolled back. + { + sql::Transaction inner2(&db()); + EXPECT_TRUE(inner2.Begin()); + EXPECT_TRUE(db().Execute("INSERT INTO foo (a, b) VALUES (1, 2)")); + EXPECT_EQ(2, db().transaction_nesting()); + + inner2.Rollback(); + EXPECT_EQ(1, db().transaction_nesting()); + } + + // A third inner one will fail in Begin since one has already been rolled + // back. + EXPECT_EQ(1, db().transaction_nesting()); + { + sql::Transaction inner3(&db()); + EXPECT_FALSE(inner3.Begin()); + EXPECT_EQ(1, db().transaction_nesting()); + } + } + EXPECT_EQ(0, db().transaction_nesting()); + EXPECT_EQ(0, CountFoo()); +} |