diff options
-rw-r--r-- | app/app.gyp | 14 | ||||
-rw-r--r-- | app/sql/connection.cc | 304 | ||||
-rw-r--r-- | app/sql/connection.h | 310 | ||||
-rw-r--r-- | app/sql/connection_unittest.cc | 113 | ||||
-rw-r--r-- | app/sql/meta_table.cc | 143 | ||||
-rw-r--r-- | app/sql/meta_table.h | 87 | ||||
-rw-r--r-- | app/sql/statement.cc | 212 | ||||
-rw-r--r-- | app/sql/statement.h | 132 | ||||
-rw-r--r-- | app/sql/statement_unittest.cc | 80 | ||||
-rw-r--r-- | app/sql/transaction.cc | 51 | ||||
-rw-r--r-- | app/sql/transaction.h | 56 | ||||
-rw-r--r-- | app/sql/transaction_unittest.cc | 139 | ||||
-rw-r--r-- | chrome/browser/history/text_database.cc | 205 | ||||
-rw-r--r-- | chrome/browser/history/text_database.h | 29 |
14 files changed, 1740 insertions, 135 deletions
diff --git a/app/app.gyp b/app/app.gyp index 41b7ee6..b610b02 100644 --- a/app/app.gyp +++ b/app/app.gyp @@ -50,6 +50,7 @@ '../skia/skia.gyp:skia', '../third_party/icu/icu.gyp:icui18n', '../third_party/icu/icu.gyp:icuuc', + '../third_party/sqlite/sqlite.gyp:sqlite', ], 'include_dirs': [ '..', @@ -110,6 +111,14 @@ 'resource_bundle_mac.mm', 'slide_animation.cc', 'slide_animation.h', + 'sql/connection.cc', + 'sql/connection.h', + 'sql/meta_table.cc', + 'sql/meta_table.h', + 'sql/statement.cc', + 'sql/statement.h', + 'sql/transaction.cc', + 'sql/transaction.h', 'theme_provider.cc', 'theme_provider.h', 'throb_animation.cc', @@ -198,8 +207,11 @@ 'os_exchange_data_win_unittest.cc', 'run_all_unittests.cc', 'test_suite.h', + 'sql/connection_unittest.cc', + 'sql/statement_unittest.cc', + 'sql/transaction_unittest.cc', 'tree_node_iterator_unittest.cc', - 'win_util_unittest.cc', + 'win_util_unittest.cc', ], 'include_dirs': [ '..', diff --git a/app/sql/connection.cc b/app/sql/connection.cc new file mode 100644 index 0000000..4878e1b --- /dev/null +++ b/app/sql/connection.cc @@ -0,0 +1,304 @@ +// Copyright (c) 2009 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 "app/sql/connection.h" + +#include <string.h> + +#include "app/sql/statement.h" +#include "base/file_path.h" +#include "base/logging.h" +#include "base/string_util.h" +#include "third_party/sqlite/preprocessed/sqlite3.h" + +namespace sql { + +bool StatementID::operator<(const StatementID& other) const { + if (number_ != other.number_) + return number_ < other.number_; + return strcmp(str_, other.str_) < 0; +} + +Connection::StatementRef::StatementRef() + : connection_(NULL), + stmt_(NULL) { +} + +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_) { + 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) { +} + +Connection::~Connection() { + Close(); +} + +bool Connection::Init(const FilePath& path) { +#if defined(OS_WIN) + // We want the default encoding to always be UTF-8, so we use the + // 8-bit version of open(). + int err = sqlite3_open(WideToUTF8(path.value()).c_str(), &db_); +#elif defined(OS_POSIX) + int err = sqlite3_open(path.value().c_str(), &db_); +#endif + + if (err != SQLITE_OK) { + db_ = NULL; + return false; + } + + if (page_size_ != 0) { + if (!Execute(StringPrintf("PRAGMA page_size=%d", page_size_).c_str())) + NOTREACHED() << "Could not set page size"; + } + + if (cache_size_ != 0) { + if (!Execute(StringPrintf("PRAGMA cache_size=%d", cache_size_).c_str())) + NOTREACHED() << "Could not set page size"; + } + + if (exclusive_locking_) { + if (!Execute("PRAGMA locking_mode=EXCLUSIVE")) + NOTREACHED() << "Could not set locking mode."; + } + + return true; +} + +void Connection::Close() { + statement_cache_.clear(); + DCHECK(open_statements_.empty()); + if (db_) { + sqlite3_close(db_); + db_ = NULL; + } +} + +void Connection::Preload() { + if (!db_) { + NOTREACHED(); + 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 || !dummy.Run()) + return; + + sqlite3Preload(db_); +} + +bool Connection::BeginTransaction() { + if (needs_rollback_) { + DCHECK(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 || !begin.Run()) + return false; + } + transaction_nesting_++; + return success; +} + +void Connection::RollbackTransaction() { + if (!transaction_nesting_) { + NOTREACHED() << "Rolling back a nonexistant 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_) { + NOTREACHED() << "Rolling back a nonexistant 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")); + if (!commit) + return false; + return commit.Run(); +} + +bool Connection::Execute(const char* sql) { + if (!db_) + return false; + return sqlite3_exec(db_, sql, NULL, NULL, NULL) == SQLITE_OK; +} + +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) { + if (!db_) + return new StatementRef(this, NULL); // Return inactive statement. + + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(db_, sql, -1, &stmt, NULL) != SQLITE_OK) { + // Treat this as non-fatal, it can occur in a number of valid cases, and + // callers should be doing their own error handling. + DLOG(WARNING) << "SQL compile error " << GetErrorMessage(); + return new StatementRef(this, NULL); + } + return new StatementRef(this, stmt); +} + +bool Connection::DoesTableExist(const char* table_name) { + Statement statement(GetUniqueStatement( + "SELECT name FROM sqlite_master " + "WHERE type='table' AND name=?")); + if (!statement) + return false; + statement.BindString(0, table_name); + return statement.Step(); // Table exists if any row was returned. +} + +bool Connection::DoesColumnExist(const char* table_name, + const char* column_name) { + std::string sql("PRAGMA TABLE_INFO("); + sql.append(table_name); + sql.append(")"); + + Statement statement(GetUniqueStatement(sql.c_str())); + if (!statement) + return false; + + while (statement.Step()) { + if (!statement.ColumnString(1).compare(column_name)) + return true; + } + return false; +} + +int64 Connection::GetLastInsertRowId() const { + if (!db_) { + NOTREACHED(); + return 0; + } + return sqlite3_last_insert_rowid(db_); +} + +int Connection::GetErrorCode() const { + if (!db_) + return SQLITE_ERROR; + return sqlite3_errcode(db_); +} + +const char* Connection::GetErrorMessage() const { + if (!db_) + return "sql::Connection has no connection."; + return sqlite3_errmsg(db_); +} + +void Connection::DoRollback() { + Statement rollback(GetCachedStatement(SQL_FROM_HERE, "ROLLBACK")); + if (rollback) + rollback.Run(); +} + +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()) + NOTREACHED(); + 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(); +} + +} // namespace sql diff --git a/app/sql/connection.h b/app/sql/connection.h new file mode 100644 index 0000000..2948980 --- /dev/null +++ b/app/sql/connection.h @@ -0,0 +1,310 @@ +// Copyright (c) 2009 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_CONNECTION_H_ +#define APP_SQL_CONNECTION_H_ + +#include <map> +#include <set> + +#include "base/basictypes.h" +#include "base/ref_counted.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 { + private: + class StatementRef; // Forward declaration, see real one below. + + public: + // The database is opened by calling Init(). 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 adtabase. 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 Init() 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 Init() to have an effect. + void set_exclusive_locking() { exclusive_locking_ = true; } + + // Initialization ------------------------------------------------------------ + + // Initializes the SQL connection for the given file, returning true if the + // file could be opened. + bool Init(const FilePath& path); + + // 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(); + + // 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). + bool Execute(const char* sql); + + // 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. + // + // The SQL may have an error, so the caller must check validity of the + // statement before using it. + // + // 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); + + // 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); + + // Returns true if a column with the given name exists in the given table. + bool DoesColumnExist(const char* table_name, const char* column_name); + + // Returns sqlite's internal ID for the last inserted row. Valid only + // immediately after an insert. + int64 GetLastInsertRowId() const; + + // Errors -------------------------------------------------------------------- + + // Returns the error code associated with the last sqlite operation. + int GetErrorCode() const; + + // Returns a pointer to a statically allocated string associated with the + // last sqlite operation. + const char* GetErrorMessage() const; + + private: + // Statement access StatementRef which we don't want to expose to erverybody + // (they should go through Statement). + friend class Statement; + + // 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 StatementRef : public base::RefCounted<StatementRef> { + public: + // Default constructor initializes to an invalid statement. + StatementRef(); + StatementRef(Connection* connection, sqlite3_stmt* stmt); + ~StatementRef(); + + // 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(); + + private: + 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(); + + // 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_; + + DISALLOW_COPY_AND_ASSIGN(Connection); +}; + +} // namespace sql + +#endif // APP_SQL_CONNECTION_H_ diff --git a/app/sql/connection_unittest.cc b/app/sql/connection_unittest.cc new file mode 100644 index 0000000..70c9ffc --- /dev/null +++ b/app/sql/connection_unittest.cc @@ -0,0 +1,113 @@ +// Copyright (c) 2009 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 "app/sql/connection.h" +#include "app/sql/statement.h" +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/path_service.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/preprocessed/sqlite3.h" + +class SQLConnectionTest : public testing::Test { + public: + SQLConnectionTest() {} + + void SetUp() { + ASSERT_TRUE(PathService::Get(base::DIR_TEMP, &path_)); + path_ = path_.AppendASCII("SQLConnectionTest.db"); + file_util::Delete(path_, false); + ASSERT_TRUE(db_.Init(path_)); + } + + void TearDown() { + db_.Close(); + + // If this fails something is going on with cleanup and later tests may + // fail, so we want to identify problems right away. + ASSERT_TRUE(file_util::Delete(path_, false)); + } + + sql::Connection& db() { return db_; } + + private: + FilePath path_; + 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_FALSE(db().Execute("CREATE TAB foo (a, b")); + EXPECT_EQ(SQLITE_ERROR, db().GetErrorCode()); +} + +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_FALSE(!s); // Test ! operator for validity. + + 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_FALSE(!s); // Test ! operator for validity. + + 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, 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 nonexistant 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)); +} + diff --git a/app/sql/meta_table.cc b/app/sql/meta_table.cc new file mode 100644 index 0000000..03a1245 --- /dev/null +++ b/app/sql/meta_table.cc @@ -0,0 +1,143 @@ +// Copyright (c) 2009 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 "app/sql/meta_table.h" + +#include "app/sql/connection.h" +#include "app/sql/statement.h" +#include "base/logging.h" +#include "base/string_util.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() { +} + +bool MetaTable::Init(Connection* db, int version, int compatible_version) { + DCHECK(!db_ && db); + db_ = db; + if (!db_->DoesTableExist("meta")) { + 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 true; +} + +bool MetaTable::SetValue(const char* key, const std::string& value) { + Statement s; + if (!PrepareSetStatement(&s, key)) + return false; + s.BindString(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::SetValue(const char* key, int value) { + Statement s; + if (!PrepareSetStatement(&s, key)) + return false; + + s.BindInt(1, value); + return s.Run(); +} + +bool MetaTable::GetValue(const char* key, int* value) { + Statement s; + if (!PrepareGetStatement(&s, key)) + return false; + + *value = s.ColumnInt(0); + return true; +} + +bool MetaTable::SetValue(const char* key, int64 value) { + Statement s; + if (!PrepareSetStatement(&s, key)) + return false; + s.BindInt64(1, value); + return s.Run(); +} + +bool MetaTable::GetValue(const char* key, int64* value) { + Statement s; + if (!PrepareGetStatement(&s, key)) + return false; + + *value = s.ColumnInt64(0); + return true; +} + +void MetaTable::SetVersionNumber(int version) { + SetValue(kVersionKey, version); +} + +int MetaTable::GetVersionNumber() { + int version = 0; + if (!GetValue(kVersionKey, &version)) + return 0; + return version; +} + +void MetaTable::SetCompatibleVersionNumber(int version) { + SetValue(kCompatibleVersionKey, version); +} + +int MetaTable::GetCompatibleVersionNumber() { + int version = 0; + if (!GetValue(kCompatibleVersionKey, &version)) + return 0; + return version; +} + +bool 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 (?,?)")); + if (!*statement) { + NOTREACHED() << db_->GetErrorMessage(); + return false; + } + statement->BindCString(0, key); + return true; +} + +bool MetaTable::PrepareGetStatement(Statement* statement, const char* key) { + DCHECK(db_ && statement); + statement->Assign(db_->GetCachedStatement(SQL_FROM_HERE, + "SELECT value FROM meta WHERE key=?")); + if (!*statement) { + NOTREACHED() << db_->GetErrorMessage(); + return false; + } + statement->BindCString(0, key); + if (!statement->Step()) + return false; + return true; +} + +} // namespace sql + diff --git a/app/sql/meta_table.h b/app/sql/meta_table.h new file mode 100644 index 0000000..6ccee17 --- /dev/null +++ b/app/sql/meta_table.h @@ -0,0 +1,87 @@ +// Copyright (c) 2009 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_META_TABLE_H_ +#define APP_SQL_META_TABLE_H_ + +#include <string> + +#include "base/basictypes.h" + +namespace sql { + +class Connection; +class Statement; + +class MetaTable { + public: + MetaTable(); + ~MetaTable(); + + // 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|. + // + // The name of the database in the sqlite connection (for tables named with + // the "db_name.table_name" scheme is given in |db_name|. If empty, it is + // assumed there is no database name. + bool Init(Connection* db, + int version, + int compatible_version); + + // 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); + + private: + // Conveniences to prepare the two types of statements used by + // MetaTableHelper. + bool PrepareSetStatement(Statement* statement, const char* key); + bool PrepareGetStatement(Statement* statement, const char* key); + + Connection* db_; + + // Name of the database within the connection, if there is one. When empty, + // there is no special database name and the table name can be used + // unqualified. + std::string db_name_; + + DISALLOW_COPY_AND_ASSIGN(MetaTable); +}; + +} // namespace sql + +#endif // APP_SQL_META_TABLE_H_ diff --git a/app/sql/statement.cc b/app/sql/statement.cc new file mode 100644 index 0000000..0b419ba --- /dev/null +++ b/app/sql/statement.cc @@ -0,0 +1,212 @@ +// Copyright (c) 2009 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 "app/sql/statement.h" + +#include "base/logging.h" +#include "third_party/sqlite/preprocessed/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(); +} + +void Statement::Assign(scoped_refptr<Connection::StatementRef> ref) { + ref_ = ref; +} + +bool Statement::Run() { + if (!is_valid()) + return false; + return CheckError(sqlite3_step(ref_->stmt())) == SQLITE_DONE; +} + +bool Statement::Step() { + if (!is_valid()) + return false; + return CheckError(sqlite3_step(ref_->stmt())) == SQLITE_ROW; +} + +void Statement::Reset() { + if (is_valid()) + CheckError(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()) { + int err = CheckError(sqlite3_bind_null(ref_->stmt(), col + 1)); + DCHECK(err == SQLITE_OK) << ref_->connection()->GetErrorMessage(); + return err == SQLITE_OK; + } + return false; +} + +bool Statement::BindInt(int col, int val) { + if (is_valid()) { + int err = CheckError(sqlite3_bind_int(ref_->stmt(), col + 1, val)); + DCHECK(err == SQLITE_OK) << ref_->connection()->GetErrorMessage(); + return err == SQLITE_OK; + } + return false; +} + +bool Statement::BindInt64(int col, int64 val) { + if (is_valid()) { + int err = CheckError(sqlite3_bind_int64(ref_->stmt(), col + 1, val)); + DCHECK(err == SQLITE_OK) << ref_->connection()->GetErrorMessage(); + return err == SQLITE_OK; + } + return false; +} + +bool Statement::BindDouble(int col, double val) { + if (is_valid()) { + int err = CheckError(sqlite3_bind_double(ref_->stmt(), col + 1, val)); + DCHECK(err == SQLITE_OK) << ref_->connection()->GetErrorMessage(); + return err == SQLITE_OK; + } + return false; +} + +bool Statement::BindCString(int col, const char* val) { + if (is_valid()) { + int err = CheckError(sqlite3_bind_text(ref_->stmt(), col + 1, val, -1, + SQLITE_TRANSIENT)); + DCHECK(err == SQLITE_OK) << ref_->connection()->GetErrorMessage(); + return err == SQLITE_OK; + } + return false; +} + +bool Statement::BindString(int col, const std::string& val) { + if (is_valid()) { + int err = CheckError(sqlite3_bind_text(ref_->stmt(), col + 1, val.data(), + val.size(), SQLITE_TRANSIENT)); + DCHECK(err == SQLITE_OK) << ref_->connection()->GetErrorMessage(); + return err == SQLITE_OK; + } + return false; +} + +bool Statement::BindBlob(int col, const void* val, int val_len) { + if (is_valid()) { + int err = CheckError(sqlite3_bind_blob(ref_->stmt(), col + 1, + val, val_len, SQLITE_TRANSIENT)); + DCHECK(err == SQLITE_OK) << ref_->connection()->GetErrorMessage(); + return err == SQLITE_OK; + } + return false; +} + +int Statement::ColumnCount() const { + if (!is_valid()) { + NOTREACHED(); + return 0; + } + return sqlite3_column_count(ref_->stmt()); +} + +int Statement::ColumnInt(int col) const { + if (!is_valid()) { + NOTREACHED(); + return 0; + } + return sqlite3_column_int(ref_->stmt(), col); +} + +int64 Statement::ColumnInt64(int col) const { + if (!is_valid()) { + NOTREACHED(); + return 0; + } + return sqlite3_column_int64(ref_->stmt(), col); +} + +double Statement::ColumnDouble(int col) const { + if (!is_valid()) { + NOTREACHED(); + return 0; + } + return sqlite3_column_double(ref_->stmt(), col); +} + +std::string Statement::ColumnString(int col) const { + if (!is_valid()) { + NOTREACHED(); + return 0; + } + 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; +} + +int Statement::ColumnByteLength(int col) { + if (!is_valid()) { + NOTREACHED(); + return 0; + } + return sqlite3_column_bytes(ref_->stmt(), col); +} + +const void* Statement::ColumnBlob(int col) { + if (!is_valid()) { + NOTREACHED(); + return NULL; + } + + return sqlite3_column_blob(ref_->stmt(), col); +} + +void Statement::ColumnBlobAsVector(int col, std::vector<char>* val) { + val->clear(); + if (!is_valid()) { + NOTREACHED(); + return; + } + + 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); + } +} + +int Statement::CheckError(int err) { + succeeded_ = (err == SQLITE_OK || err == SQLITE_ROW || err == SQLITE_DONE); + + // TODO(brettw) enhance this to process the error. + return err; +} + +} // namespace sql diff --git a/app/sql/statement.h b/app/sql/statement.h new file mode 100644 index 0000000..371bc4e --- /dev/null +++ b/app/sql/statement.h @@ -0,0 +1,132 @@ +// Copyright (c) 2009 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_STATEMENT_H_ +#define APP_SQL_STATEMENT_H_ + +#include <string> +#include <vector> + +#include "app/sql/connection.h" +#include "base/basictypes.h" +#include "base/ref_counted.h" + +namespace sql { + +// Normal usage: +// sql::Statement s = connection_.GetUniqueStatement(...); +// if (!s) // You should check for errors before using the statement. +// return false; +// +// s.BindInt(0, a); +// if (s.Step()) +// return s.ColumnString(0); +class Statement { + public: + // Creates an uninitialized statement. The statement will be invalid until + // you initialize it via Assign. + Statement(); + + 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); + + // 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 serioud error and the database + // has to be reset. + bool is_valid() const { return ref_->is_valid(); } + + // These operators allow conveniently checking if the statement is valid + // or not. See the pattern above for an example. + operator bool() const { return is_valid(); } + bool operator!() const { return !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 clearing all + // the bound variables and any current result row. + void Reset(); + + // 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 failure. 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 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 BindBlob(int col, const void* value, int value_len); + + // Retrieving ---------------------------------------------------------------- + + // Returns the number of output columns in the result. + int ColumnCount() const; + + // These all take a 0-based argument index. + int ColumnInt(int col) const; + int64 ColumnInt64(int col) const; + double ColumnDouble(int col) const; + std::string ColumnString(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 void* ColumnBlob(int col); + void ColumnBlobAsVector(int col, std::vector<char>* val); + + 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); + + // 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 // APP_SQL_STATEMENT_H_ diff --git a/app/sql/statement_unittest.cc b/app/sql/statement_unittest.cc new file mode 100644 index 0000000..30a869c --- /dev/null +++ b/app/sql/statement_unittest.cc @@ -0,0 +1,80 @@ +// Copyright (c) 2009 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 "app/sql/connection.h" +#include "app/sql/statement.h" +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/path_service.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/preprocessed/sqlite3.h" + +class SQLStatementTest : public testing::Test { + public: + SQLStatementTest() {} + + void SetUp() { + ASSERT_TRUE(PathService::Get(base::DIR_TEMP, &path_)); + path_ = path_.AppendASCII("SQLStatementTest.db"); + file_util::Delete(path_, false); + ASSERT_TRUE(db_.Init(path_)); + } + + void TearDown() { + db_.Close(); + + // If this fails something is going on with cleanup and later tests may + // fail, so we want to identify problems right away. + ASSERT_TRUE(file_util::Delete(path_, false)); + } + + sql::Connection& db() { return db_; } + + private: + FilePath path_; + sql::Connection db_; +}; + +TEST_F(SQLStatementTest, Assign) { + sql::Statement s; + EXPECT_FALSE(s); // bool conversion operator. + EXPECT_TRUE(!s); // ! operator. + EXPECT_FALSE(s.is_valid()); + + s.Assign(db().GetUniqueStatement("CREATE TABLE foo (a, b)")); + EXPECT_TRUE(s); + EXPECT_FALSE(!s); + 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(); + 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(); + 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()); +} diff --git a/app/sql/transaction.cc b/app/sql/transaction.cc new file mode 100644 index 0000000..79a198b --- /dev/null +++ b/app/sql/transaction.cc @@ -0,0 +1,51 @@ +// Copyright (c) 2009 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 "app/sql/transaction.h" + +#include "app/sql/connection.h" +#include "base/logging.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 nonexistant 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 nonexistant transaction. " + << "Did you remember to call Begin() and check its return?"; + return false; + } + is_open_ = false; + return connection_->CommitTransaction(); +} + +} // namespace sql diff --git a/app/sql/transaction.h b/app/sql/transaction.h new file mode 100644 index 0000000..1e00c6f --- /dev/null +++ b/app/sql/transaction.h @@ -0,0 +1,56 @@ +// Copyright (c) 2009 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_TRANSACTION_H_ +#define APP_SQL_TRANSACTION_H_ + +#include "base/basictypes.h" + +namespace sql { + +class Connection; + +class 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. + 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 // APP_SQL_TRANSACTION_H_ diff --git a/app/sql/transaction_unittest.cc b/app/sql/transaction_unittest.cc new file mode 100644 index 0000000..0da79e3 --- /dev/null +++ b/app/sql/transaction_unittest.cc @@ -0,0 +1,139 @@ +// Copyright (c) 2009 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 "app/sql/connection.h" +#include "app/sql/statement.h" +#include "app/sql/transaction.h" +#include "base/file_path.h" +#include "base/file_util.h" +#include "base/path_service.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/sqlite/preprocessed/sqlite3.h" + +class SQLTransactionTest : public testing::Test { + public: + SQLTransactionTest() {} + + void SetUp() { + ASSERT_TRUE(PathService::Get(base::DIR_TEMP, &path_)); + path_ = path_.AppendASCII("SQLStatementTest.db"); + file_util::Delete(path_, false); + ASSERT_TRUE(db_.Init(path_)); + + ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)")); + } + + void TearDown() { + db_.Close(); + + // If this fails something is going on with cleanup and later tests may + // fail, so we want to identify problems right away. + ASSERT_TRUE(file_util::Delete(path_, false)); + } + + 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: + FilePath path_; + 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()); +} diff --git a/chrome/browser/history/text_database.cc b/chrome/browser/history/text_database.cc index 5ffdc95..d56788ca 100644 --- a/chrome/browser/history/text_database.cc +++ b/chrome/browser/history/text_database.cc @@ -1,4 +1,4 @@ -// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Copyright (c) 2009 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. @@ -7,11 +7,12 @@ #include "chrome/browser/history/text_database.h" +#include "app/sql/connection.h" +#include "app/sql/statement.h" +#include "app/sql/transaction.h" #include "base/file_util.h" #include "base/logging.h" #include "base/string_util.h" -#include "chrome/common/sqlite_compiled_statement.h" -#include "chrome/common/sqlite_utils.h" // There are two tables in each database, one full-text search (FTS) table which // indexes the contents and title of the pages. The other is a regular SQLITE @@ -51,48 +52,19 @@ const char kBodyColumnIndex[] = "2"; // The string prepended to the database identifier to generate the filename. const FilePath::CharType kFilePrefix[] = FILE_PATH_LITERAL("History Index "); -// We do not allow rollback, but this simple scoper makes it easy to always -// remember to commit a begun transaction. This protects against some errors -// caused by a crash in the middle of a transaction, although doesn't give us -// the full protection of a transaction's rollback abilities. -class ScopedTransactionCommitter { - public: - ScopedTransactionCommitter(TextDatabase* db) : db_(db) { - db_->BeginTransaction(); - } - ~ScopedTransactionCommitter() { - db_->CommitTransaction(); - } - private: - TextDatabase* db_; -}; - - } // namespace TextDatabase::TextDatabase(const FilePath& path, DBIdent id, bool allow_create) - : db_(NULL), - statement_cache_(NULL), - path_(path), + : path_(path), ident_(id), - allow_create_(allow_create), - transaction_nesting_(0) { + allow_create_(allow_create) { // Compute the file name. file_name_ = path_.Append(IDToFileName(ident_)); } TextDatabase::~TextDatabase() { - if (statement_cache_) { - // Must release these statements before closing the DB. - delete statement_cache_; - statement_cache_ = NULL; - } - if (db_) { - sqlite3_close(db_); - db_ = NULL; - } } // static @@ -144,30 +116,28 @@ bool TextDatabase::Init() { return false; } - // Attach the database to our index file. - if (OpenSqliteDb(file_name_, &db_) != SQLITE_OK) - return false; - statement_cache_ = new SqliteStatementCache(db_); - // Set the database page size to something a little larger to give us // better performance (we're typically seek rather than bandwidth limited). // This only has an effect before any tables have been created, otherwise // this is a NOP. Must be a power of 2 and a max of 8192. - sqlite3_exec(db_, "PRAGMA page_size=4096", NULL, NULL, NULL); + db_.set_page_size(2096); // The default cache size is 2000 which give >8MB of data. Since we will often // have 2-3 of these objects, each with their own 8MB, this adds up very fast. // We therefore reduce the size so when there are multiple objects, we're not // too big. - sqlite3_exec(db_, "PRAGMA cache_size=512", NULL, NULL, NULL); + db_.set_cache_size(512); // Run the database in exclusive mode. Nobody else should be accessing the // database while we're running, and this will give somewhat improved perf. - sqlite3_exec(db_, "PRAGMA locking_mode=EXCLUSIVE", NULL, NULL, NULL); + db_.set_exclusive_locking(); + + // Attach the database to our index file. + if (!db_.Init(file_name_)) + return false; // Meta table tracking version information. - if (!meta_table_.Init(std::string(), kCurrentVersionNumber, - kCompatibleVersionNumber, db_)) + if (!meta_table_.Init(&db_, kCurrentVersionNumber, kCompatibleVersionNumber)) return false; if (meta_table_.GetCompatibleVersionNumber() > kCurrentVersionNumber) { // This version is too new. We don't bother notifying the user on this @@ -184,46 +154,39 @@ bool TextDatabase::Init() { } void TextDatabase::BeginTransaction() { - if (!transaction_nesting_) - sqlite3_exec(db_, "BEGIN TRANSACTION", NULL, NULL, NULL); - transaction_nesting_++; + db_.BeginTransaction(); } void TextDatabase::CommitTransaction() { - DCHECK(transaction_nesting_); - transaction_nesting_--; - if (!transaction_nesting_) - sqlite3_exec(db_, "COMMIT", NULL, NULL, NULL); + db_.CommitTransaction(); } bool TextDatabase::CreateTables() { // FTS table of page contents. - if (!DoesSqliteTableExist(db_, "pages")) { - if (sqlite3_exec(db_, - "CREATE VIRTUAL TABLE pages USING fts2(" + if (!db_.DoesTableExist("pages")) { + if (!db_.Execute("CREATE VIRTUAL TABLE pages USING fts2(" "TOKENIZE icu," "url LONGVARCHAR," "title LONGVARCHAR," - "body LONGVARCHAR)", NULL, NULL, NULL) != SQLITE_OK) + "body LONGVARCHAR)")) return false; } // Non-FTS table containing URLs and times so we can efficiently find them // using a regular index (all FTS columns are special and are treated as // full-text-search, which is not what we want when retrieving this data). - if (!DoesSqliteTableExist(db_, "info")) { + if (!db_.DoesTableExist("info")) { // Note that there is no point in creating an index over time. Since // we must always query the entire FTS table (it can not efficiently do // subsets), we will always end up doing that first, and joining the info // table off of that. - if (sqlite3_exec(db_, "CREATE TABLE info(time INTEGER NOT NULL)", - NULL, NULL, NULL) != SQLITE_OK) + if (!db_.Execute("CREATE TABLE info(time INTEGER NOT NULL)")) return false; } // Create the index. This will fail when the index already exists, so we just // ignore the error. - sqlite3_exec(db_, "CREATE INDEX info_time ON info(time)", NULL, NULL, NULL); + db_.Execute("CREATE INDEX info_time ON info(time)"); return true; } @@ -231,85 +194,97 @@ bool TextDatabase::AddPageData(Time time, const std::string& url, const std::string& title, const std::string& contents) { - ScopedTransactionCommitter committer(this); + sql::Transaction committer(&db_); + if (!committer.Begin()) + return false; // Add to the pages table. - SQLITE_UNIQUE_STATEMENT(add_to_pages, *statement_cache_, - "INSERT INTO pages (url, title, body) VALUES (?,?,?)"); - if (!add_to_pages.is_valid()) + sql::Statement add_to_pages(db_.GetCachedStatement(SQL_FROM_HERE, + "INSERT INTO pages (url, title, body) VALUES (?,?,?)")); + if (!add_to_pages) { + NOTREACHED() << db_.GetErrorMessage(); return false; - add_to_pages->bind_string(0, url); - add_to_pages->bind_string(1, title); - add_to_pages->bind_string(2, contents); - if (add_to_pages->step() != SQLITE_DONE) { - NOTREACHED() << sqlite3_errmsg(db_); + } + add_to_pages.BindString(0, url); + add_to_pages.BindString(1, title); + add_to_pages.BindString(2, contents); + if (!add_to_pages.Run()) { + NOTREACHED() << db_.GetErrorMessage(); return false; } - int64 rowid = sqlite3_last_insert_rowid(db_); + int64 rowid = db_.GetLastInsertRowId(); // Add to the info table with the same rowid. - SQLITE_UNIQUE_STATEMENT(add_to_info, *statement_cache_, - "INSERT INTO info (rowid, time) VALUES (?,?)"); - if (!add_to_info.is_valid()) + sql::Statement add_to_info(db_.GetCachedStatement(SQL_FROM_HERE, + "INSERT INTO info (rowid, time) VALUES (?,?)")); + if (!add_to_info) { + NOTREACHED() << db_.GetErrorMessage(); return false; - add_to_info->bind_int64(0, rowid); - add_to_info->bind_int64(1, time.ToInternalValue()); - if (add_to_info->step() != SQLITE_DONE) { - NOTREACHED() << sqlite3_errmsg(db_); + } + add_to_info.BindInt64(0, rowid); + add_to_info.BindInt64(1, time.ToInternalValue()); + if (!add_to_info.Run()) { + NOTREACHED() << db_.GetErrorMessage(); return false; } - return true; + return committer.Commit(); } void TextDatabase::DeletePageData(Time time, const std::string& url) { // First get all rows that match. Selecing on time (which has an index) allows // us to avoid brute-force searches on the full-text-index table (there will // generally be only one match per time). - SQLITE_UNIQUE_STATEMENT(select_ids, *statement_cache_, + sql::Statement select_ids(db_.GetCachedStatement(SQL_FROM_HERE, "SELECT info.rowid " "FROM info JOIN pages ON info.rowid = pages.rowid " - "WHERE info.time=? AND pages.url=?"); - if (!select_ids.is_valid()) + "WHERE info.time=? AND pages.url=?")); + if (!select_ids) return; - select_ids->bind_int64(0, time.ToInternalValue()); - select_ids->bind_string(1, url); + select_ids.BindInt64(0, time.ToInternalValue()); + select_ids.BindString(1, url); std::set<int64> rows_to_delete; - while (select_ids->step() == SQLITE_ROW) - rows_to_delete.insert(select_ids->column_int64(0)); + while (select_ids.Step()) + rows_to_delete.insert(select_ids.ColumnInt64(0)); // Delete from the pages table. - SQLITE_UNIQUE_STATEMENT(delete_page, *statement_cache_, - "DELETE FROM pages WHERE rowid=?"); - if (!delete_page.is_valid()) + sql::Statement delete_page(db_.GetCachedStatement(SQL_FROM_HERE, + "DELETE FROM pages WHERE rowid=?")); + if (!delete_page) return; for (std::set<int64>::const_iterator i = rows_to_delete.begin(); i != rows_to_delete.end(); ++i) { - delete_page->bind_int64(0, *i); - delete_page->step(); - delete_page->reset(); + delete_page.BindInt64(0, *i); + if (!delete_page.Run()) { + NOTREACHED(); + return; + } + delete_page.Reset(); } // Delete from the info table. - SQLITE_UNIQUE_STATEMENT(delete_info, *statement_cache_, - "DELETE FROM info WHERE rowid=?"); - if (!delete_info.is_valid()) + sql::Statement delete_info(db_.GetCachedStatement(SQL_FROM_HERE, + "DELETE FROM info WHERE rowid=?")); + if (!delete_info) return; for (std::set<int64>::const_iterator i = rows_to_delete.begin(); i != rows_to_delete.end(); ++i) { - delete_info->bind_int64(0, *i); - delete_info->step(); - delete_info->reset(); + delete_info.BindInt64(0, *i); + if (!delete_info.Run()) { + NOTREACHED(); + return; + } + delete_info.Reset(); } } void TextDatabase::Optimize() { - SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_, - "SELECT OPTIMIZE(pages) FROM pages LIMIT 1"); - if (!statement.is_valid()) + sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, + "SELECT OPTIMIZE(pages) FROM pages LIMIT 1")); + if (!statement) return; - statement->step(); + statement.Run(); } void TextDatabase::GetTextMatches(const std::string& query, @@ -319,13 +294,13 @@ void TextDatabase::GetTextMatches(const std::string& query, Time* first_time_searched) { *first_time_searched = options.begin_time; - SQLITE_UNIQUE_STATEMENT(statement, *statement_cache_, + sql::Statement statement(db_.GetCachedStatement(SQL_FROM_HERE, "SELECT url, title, time, offsets(pages), body " "FROM pages LEFT OUTER JOIN info ON pages.rowid = info.rowid " "WHERE pages MATCH ? AND time >= ? AND time < ? " "ORDER BY time DESC " - "LIMIT ?"); - if (!statement.is_valid()) + "LIMIT ?")); + if (!statement) return; // When their values indicate "unspecified", saturate the numbers to the max @@ -337,17 +312,17 @@ void TextDatabase::GetTextMatches(const std::string& query, int effective_max_count = options.max_count ? options.max_count : std::numeric_limits<int>::max(); - statement->bind_string(0, query); - statement->bind_int64(1, effective_begin_time); - statement->bind_int64(2, effective_end_time); - statement->bind_int(3, effective_max_count); + statement.BindString(0, query); + statement.BindInt64(1, effective_begin_time); + statement.BindInt64(2, effective_end_time); + statement.BindInt(3, effective_max_count); - while (statement->step() == SQLITE_ROW) { + while (statement.Step()) { // TODO(brettw) allow canceling the query in the middle. // if (canceled_or_something) // break; - GURL url(statement->column_string(0)); + GURL url(statement.ColumnString(0)); if (options.most_recent_visit_only) { URLSet::const_iterator found_url = found_urls->find(url); if (found_url != found_urls->end()) @@ -359,14 +334,14 @@ void TextDatabase::GetTextMatches(const std::string& query, Match& match = results->at(results->size() - 1); match.url.Swap(&url); - match.title = UTF8ToWide(statement->column_string(1)); - match.time = Time::FromInternalValue(statement->column_int64(2)); + match.title = UTF8ToWide(statement.ColumnString(1)); + match.time = Time::FromInternalValue(statement.ColumnInt64(2)); // Extract any matches in the title. - std::string offsets_str = statement->column_string(3); + std::string offsets_str = statement.ColumnString(3); Snippet::ExtractMatchPositions(offsets_str, kTitleColumnIndex, &match.title_match_positions); - Snippet::ConvertMatchPositionsToWide(statement->column_string(1), + Snippet::ConvertMatchPositionsToWide(statement.ColumnString(1), &match.title_match_positions); // Extract the matches in the body. @@ -375,7 +350,7 @@ void TextDatabase::GetTextMatches(const std::string& query, &match_positions); // Compute the snippet based on those matches. - std::string body = statement->column_string(4); + std::string body = statement.ColumnString(4); match.snippet.ComputeSnippet(match_positions, body); } @@ -392,7 +367,7 @@ void TextDatabase::GetTextMatches(const std::string& query, *first_time_searched = results->back().time; } - statement->reset(); + statement.Reset(); } } // namespace history diff --git a/chrome/browser/history/text_database.h b/chrome/browser/history/text_database.h index 797e3e9..e15ac52 100644 --- a/chrome/browser/history/text_database.h +++ b/chrome/browser/history/text_database.h @@ -1,22 +1,20 @@ -// Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. +// Copyright (c) 2009 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 CHROME_BROWSER_HISTORY_TEXT_DATABASE_H__ -#define CHROME_BROWSER_HISTORY_TEXT_DATABASE_H__ +#ifndef CHROME_BROWSER_HISTORY_TEXT_DATABASE_H_ +#define CHROME_BROWSER_HISTORY_TEXT_DATABASE_H_ #include <set> #include <vector> +#include "app/sql/connection.h" +#include "app/sql/meta_table.h" #include "base/basictypes.h" #include "base/file_path.h" #include "chrome/browser/history/history_types.h" -#include "chrome/browser/meta_table_helper.h" #include "googleurl/src/gurl.h" -struct sqlite3; -class SqliteStatementCache; - namespace history { // Encapsulation of a full-text indexed database file. @@ -144,9 +142,8 @@ class TextDatabase { // Ensures that the tables and indices are created. Returns true on success. bool CreateTables(); - // See the constructor. - sqlite3* db_; - SqliteStatementCache* statement_cache_; + // The sql database. Not valid until Init is called. + sql::Connection db_; const FilePath path_; const DBIdent ident_; @@ -155,17 +152,11 @@ class TextDatabase { // Full file name of the file on disk, computed in Init(). FilePath file_name_; - // Nesting levels of transactions. Since sqlite only allows one open - // transaction, we simulate nested transactions by mapping the outermost one - // to a real transaction. Since this object never needs to do ROLLBACK, losing - // the ability for all transactions to rollback is inconsequential. - int transaction_nesting_; - - MetaTableHelper meta_table_; + sql::MetaTable meta_table_; - DISALLOW_EVIL_CONSTRUCTORS(TextDatabase); + DISALLOW_COPY_AND_ASSIGN(TextDatabase); }; } // namespace history -#endif // CHROME_BROWSER_HISTORY_TEXT_DATABASE_H__ +#endif // CHROME_BROWSER_HISTORY_TEXT_DATABASE_H_ |