diff --git a/src/schema.sql b/src/schema.sql index 09a1744..77d9372 100644 --- a/src/schema.sql +++ b/src/schema.sql @@ -31,6 +31,8 @@ -- schema.sql: SQLite3 database schema for Moonfire NVR. -- See also design/schema.md. +pragma journal_mode = wal; + create table camera ( id integer primary key, uuid blob unique not null, diff --git a/src/sqlite-test.cc b/src/sqlite-test.cc index 4dad0e5..d727b55 100644 --- a/src/sqlite-test.cc +++ b/src/sqlite-test.cc @@ -49,15 +49,19 @@ namespace { class SqliteTest : public testing::Test { protected: SqliteTest() { + tmpdir_ = PrepareTempDirOrDie("sqlite-test"); + std::string error_message; - CHECK(db_.Open(":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, - &error_message)) + CHECK(db_.Open(StrCat(tmpdir_, "/db").c_str(), + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, &error_message)) << error_message; std::string create_sql = ReadFileOrDie("../src/schema.sql"); - CHECK(RunStatements(&db_, create_sql, &error_message)) << error_message; + DatabaseContext ctx(&db_); + CHECK(RunStatements(&ctx, create_sql, &error_message)) << error_message; } + std::string tmpdir_; Database db_; }; @@ -66,31 +70,33 @@ TEST_F(SqliteTest, JustCreate) {} TEST_F(SqliteTest, BindAndColumn) { std::string error_message; auto insert_stmt = db_.Prepare( - "insert into camera (uuid, short_name, retain_bytes) " - "values (?, ?, ?)", + "insert into camera (uuid, short_name, retain_bytes) " + " values (:uuid, :short_name, :retain_bytes)", nullptr, &error_message); - ASSERT_TRUE(insert_stmt != nullptr) << error_message; + ASSERT_TRUE(insert_stmt.valid()) << error_message; const char kBlob[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; re2::StringPiece blob_piece = re2::StringPiece(kBlob, sizeof(kBlob)); const char kText[] = "foo"; const int64_t kInt64 = INT64_C(0xdeadbeeffeedface); - ASSERT_TRUE(insert_stmt->BindBlob(1, blob_piece, &error_message)) - << error_message; - ASSERT_TRUE(insert_stmt->BindText(2, kText, &error_message)) << error_message; - ASSERT_TRUE(insert_stmt->BindInt64(3, kInt64, &error_message)) - << error_message; - ASSERT_EQ(SQLITE_DONE, insert_stmt->Step()); - auto select_stmt = - db_.Prepare("select uuid, short_name, retain_bytes from camera", nullptr, - &error_message); - ASSERT_TRUE(select_stmt != nullptr) << error_message; - ASSERT_EQ(SQLITE_ROW, select_stmt->Step()); - EXPECT_EQ(ToHex(blob_piece, true), ToHex(select_stmt->ColumnBlob(0), true)); - EXPECT_EQ(kText, select_stmt->ColumnText(1).as_string()); - EXPECT_EQ(kInt64, select_stmt->ColumnInt64(2)); - ASSERT_EQ(SQLITE_DONE, select_stmt->Step()); + DatabaseContext ctx(&db_); + { + auto run = ctx.Borrow(&insert_stmt); + run.BindBlob(1, blob_piece); + run.BindText(2, kText); + run.BindInt64(3, kInt64); + ASSERT_EQ(SQLITE_DONE, run.Step()) << run.error_message(); + } + + { + auto run = ctx.UseOnce("select uuid, short_name, retain_bytes from camera"); + ASSERT_EQ(SQLITE_ROW, run.Step()) << run.error_message(); + EXPECT_EQ(ToHex(blob_piece, true), ToHex(run.ColumnBlob(0), true)); + EXPECT_EQ(kText, run.ColumnText(1).as_string()); + EXPECT_EQ(kInt64, run.ColumnInt64(2)); + ASSERT_EQ(SQLITE_DONE, run.Step()) << run.error_message(); + } } } // namespace diff --git a/src/sqlite.cc b/src/sqlite.cc index 66c2886..beca66c 100644 --- a/src/sqlite.cc +++ b/src/sqlite.cc @@ -32,118 +32,361 @@ #include "sqlite.h" +#include + #include #include "string.h" namespace moonfire_nvr { -bool Statement::BindBlob(int param, re2::StringPiece value, - std::string *error_message) { - int err = sqlite3_bind_blob64(me_, param, value.data(), value.size(), - SQLITE_TRANSIENT); - if (err != SQLITE_OK) { - *error_message = sqlite3_errstr(err); +namespace { + +void LogCallback(void *, int err_code, const char *msg) { + LOG(ERROR) << "(" << err_code << ") " << msg; +} + +void GlobalSetup() { + VLOG(1) << "Installing sqlite3 log callback"; + sqlite3_config(SQLITE_CONFIG_LOG, &LogCallback, nullptr); +} + +std::once_flag global_setup; + +} // namespace + +Statement::Statement(Statement &&other) { *this = std::move(other); } + +void Statement::operator=(Statement &&other) { + Clear(); + memcpy(this, &other, sizeof(Statement)); + other.me_ = nullptr; + other.borrowed_ = false; +} + +Statement::~Statement() { Clear(); } + +void Statement::Clear() { + CHECK(!borrowed_) << "can't delete statement while still borrowed!"; + sqlite3_finalize(me_); +} + +DatabaseContext::DatabaseContext(Database *db) : db_(db), lock_(db->ctx_mu_) {} + +DatabaseContext::~DatabaseContext() { + if (transaction_open_) { + LOG(WARNING) << this << ": transaction left open! closing in destructor."; + RollbackTransaction(); + } +} + +bool DatabaseContext::BeginTransaction(std::string *error_message) { + if (transaction_open_) { + *error_message = "transaction already open!"; return false; } + sqlite3_step(db_->begin_transaction_.me_); + int ret = sqlite3_reset(db_->begin_transaction_.me_); + if (ret != SQLITE_OK) { + *error_message = sqlite3_errstr(ret); + return false; + } + transaction_open_ = true; return true; } -bool Statement::BindInt64(int param, int64_t value, - std::string *error_message) { - int err = sqlite3_bind_int64(me_, param, value); - if (err != SQLITE_OK) { - *error_message = sqlite3_errstr(err); +bool DatabaseContext::CommitTransaction(std::string *error_message) { + if (!transaction_open_) { + *error_message = "transaction not open!"; return false; } + sqlite3_step(db_->commit_transaction_.me_); + int ret = sqlite3_reset(db_->commit_transaction_.me_); + if (ret != SQLITE_OK) { + *error_message = sqlite3_errstr(ret); + return false; + } + transaction_open_ = false; return true; } -bool Statement::BindText(int param, re2::StringPiece value, - std::string *error_message) { - int err = sqlite3_bind_text64(me_, param, value.data(), value.size(), - SQLITE_TRANSIENT, SQLITE_UTF8); - if (err != SQLITE_OK) { - *error_message = sqlite3_errstr(err); - return false; +void DatabaseContext::RollbackTransaction() { + if (!transaction_open_) { + LOG(WARNING) << this << ": rollback failed: transaction not open!"; + return; } - return true; + sqlite3_step(db_->rollback_transaction_.me_); + int ret = sqlite3_reset(db_->rollback_transaction_.me_); + if (ret != SQLITE_OK) { + LOG(WARNING) << this << ": rollback failed: " << sqlite3_errstr(ret); + return; + } + transaction_open_ = false; } -re2::StringPiece Statement::ColumnBlob(int col) { +RunningStatement DatabaseContext::Borrow(Statement *statement) { + return RunningStatement(statement, std::string(), false); +} + +RunningStatement DatabaseContext::UseOnce(re2::StringPiece sql) { + std::string error_message; + auto *statement = new Statement(db_->Prepare(sql, nullptr, &error_message)); + return RunningStatement(statement, error_message, true); +} + +RunningStatement::RunningStatement(Statement *statement, + const std::string &deferred_error, + bool owns_statement) + : statement_(statement), + error_message_(deferred_error), + owns_statement_(owns_statement) { + CHECK(!statement->borrowed_) << "Statement already borrowed!"; + statement->borrowed_ = true; + if (!error_message_.empty()) { + status_ = SQLITE_MISUSE; + } else if (statement == nullptr) { + status_ = SQLITE_MISUSE; + error_message_ = "invalid statement"; + } +} + +RunningStatement::~RunningStatement() { + CHECK(statement_->borrowed_) << "Statement no longer borrowed!"; + sqlite3_reset(statement_->me_); + sqlite3_clear_bindings(statement_->me_); + statement_->borrowed_ = false; + if (owns_statement_) { + delete statement_; + } +} + +void RunningStatement::BindBlob(int param, re2::StringPiece value) { + if (status_ != SQLITE_OK) { + return; + } + status_ = sqlite3_bind_blob64(statement_->me_, param, value.data(), + value.size(), SQLITE_TRANSIENT); + if (status_ != SQLITE_OK) { + error_message_ = StrCat("Unable to bind parameter ", param, ": ", + sqlite3_errstr(status_)); + } +} + +void RunningStatement::BindBlob(const char *name, re2::StringPiece value) { + if (status_ != SQLITE_OK) { + return; + } + int param = sqlite3_bind_parameter_index(statement_->me_, name); + if (param == 0) { + status_ = SQLITE_MISUSE; + error_message_ = StrCat("Unable to bind parameter ", name, ": not found."); + return; + } + status_ = sqlite3_bind_blob64(statement_->me_, param, value.data(), + value.size(), SQLITE_TRANSIENT); + if (status_ != SQLITE_OK) { + error_message_ = StrCat("Unable to bind parameter ", name, ": ", + sqlite3_errstr(status_)); + } +} + +void RunningStatement::BindInt64(int param, int64_t value) { + if (status_ != SQLITE_OK) { + return; + } + status_ = sqlite3_bind_int64(statement_->me_, param, value); + if (status_ != SQLITE_OK) { + error_message_ = StrCat("Unable to bind parameter ", param, ": ", + sqlite3_errstr(status_)); + } +} + +void RunningStatement::BindInt64(const char *name, int64_t value) { + if (status_ != SQLITE_OK) { + return; + } + int param = sqlite3_bind_parameter_index(statement_->me_, name); + if (param == 0) { + status_ = SQLITE_MISUSE; + error_message_ = StrCat("Unable to bind parameter ", name, ": not found."); + return; + } + status_ = sqlite3_bind_int64(statement_->me_, param, value); + if (status_ != SQLITE_OK) { + error_message_ = StrCat("Unable to bind parameter ", name, ": ", + sqlite3_errstr(status_)); + } +} + +void RunningStatement::BindText(int param, re2::StringPiece value) { + if (status_ != SQLITE_OK) { + return; + } + status_ = sqlite3_bind_text64(statement_->me_, param, value.data(), + value.size(), SQLITE_TRANSIENT, SQLITE_UTF8); + if (status_ != SQLITE_OK) { + error_message_ = StrCat("Unable to bind parameter ", param, ": ", + sqlite3_errstr(status_)); + } +} + +void RunningStatement::BindText(const char *name, re2::StringPiece value) { + if (status_ != SQLITE_OK) { + return; + } + int param = sqlite3_bind_parameter_index(statement_->me_, name); + if (param == 0) { + error_message_ = StrCat("Unable to bind parameter ", name, ": not found."); + return; + } + status_ = sqlite3_bind_text64(statement_->me_, param, value.data(), + value.size(), SQLITE_TRANSIENT, SQLITE_UTF8); + if (status_ != SQLITE_OK) { + error_message_ = StrCat("Unable to bind parameter ", name, ": ", + sqlite3_errstr(status_)); + } +} + +int RunningStatement::Step() { + if (status_ != SQLITE_OK && status_ != SQLITE_ROW) { + return status_; + } + status_ = sqlite3_step(statement_->me_); + error_message_ = sqlite3_errstr(status_); + return status_; +} + +int RunningStatement::ColumnType(int col) { + return sqlite3_column_type(statement_->me_, col); +} + +re2::StringPiece RunningStatement::ColumnBlob(int col) { // Order matters: call _blob first, then _bytes. - const void *data = sqlite3_column_blob(me_, col); - size_t len = sqlite3_column_bytes(me_, col); + const void *data = sqlite3_column_blob(statement_->me_, col); + size_t len = sqlite3_column_bytes(statement_->me_, col); return re2::StringPiece(reinterpret_cast(data), len); } -int64_t Statement::ColumnInt64(int col) { - return sqlite3_column_int64(me_, col); +int64_t RunningStatement::ColumnInt64(int col) { + return sqlite3_column_int64(statement_->me_, col); } -re2::StringPiece Statement::ColumnText(int col) { +re2::StringPiece RunningStatement::ColumnText(int col) { // Order matters: call _text first, then _bytes. - const unsigned char *data = sqlite3_column_text(me_, col); - size_t len = sqlite3_column_bytes(me_, col); + const unsigned char *data = sqlite3_column_text(statement_->me_, col); + size_t len = sqlite3_column_bytes(statement_->me_, col); return re2::StringPiece(reinterpret_cast(data), len); } -int Statement::Step() { return sqlite3_step(me_); } - Database::~Database() { + begin_transaction_ = Statement(); + commit_transaction_ = Statement(); + rollback_transaction_ = Statement(); int err = sqlite3_close(me_); CHECK_EQ(SQLITE_OK, err) << "sqlite3_close: " << sqlite3_errstr(err); } bool Database::Open(const char *filename, int flags, std::string *error_message) { - int err = sqlite3_open_v2(filename, &me_, flags, nullptr); - if (err != SQLITE_OK) { - *error_message = sqlite3_errstr(err); + std::call_once(global_setup, &GlobalSetup); + int ret = sqlite3_open_v2(filename, &me_, flags, nullptr); + if (ret != SQLITE_OK) { + *error_message = sqlite3_errstr(ret); return false; } + + ret = sqlite3_extended_result_codes(me_, 1); + if (ret != SQLITE_OK) { + sqlite3_close(me_); + me_ = nullptr; + *error_message = + StrCat("while enabling extended result codes: ", sqlite3_errstr(ret)); + return false; + } + + Statement pragma_foreignkeys; + struct StatementToInitialize { + Statement *p; + re2::StringPiece sql; + }; + StatementToInitialize stmts[] = { + {&begin_transaction_, "begin transaction;"}, + {&commit_transaction_, "commit transaction;"}, + {&rollback_transaction_, "rollback transaction;"}, + {&pragma_foreignkeys, "pragma foreign_keys = true;"}}; + + for (const auto &stmt : stmts) { + *stmt.p = Prepare(stmt.sql, nullptr, error_message); + if (!stmt.p->valid()) { + sqlite3_close(me_); + me_ = nullptr; + *error_message = StrCat("while preparing SQL for \"", stmt.sql, "\": ", + *error_message); + return false; + } + } + + ret = sqlite3_step(pragma_foreignkeys.me_); + sqlite3_reset(pragma_foreignkeys.me_); + if (ret != SQLITE_DONE) { + sqlite3_close(me_); + me_ = nullptr; + *error_message = + StrCat("while enabling foreign keys: ", sqlite3_errstr(ret)); + return false; + } + return true; } -std::unique_ptr Database::Prepare(re2::StringPiece sql, size_t *used, - std::string *error_message) { - std::unique_ptr statement(new Statement); +Statement Database::Prepare(re2::StringPiece sql, size_t *used, + std::string *error_message) { + Statement statement; const char *tail; int err = - sqlite3_prepare_v2(me_, sql.data(), sql.size(), &statement->me_, &tail); + sqlite3_prepare_v2(me_, sql.data(), sql.size(), &statement.me_, &tail); if (err != SQLITE_OK) { *error_message = sqlite3_errstr(err); - statement.release(); + return statement; } if (used != nullptr) { *used = tail - sql.data(); } - if (statement->me_ == nullptr) { + if (statement.me_ == nullptr) { error_message->clear(); - statement.release(); } return statement; } -bool RunStatements(Database *db, re2::StringPiece stmts, +bool RunStatements(DatabaseContext *ctx, re2::StringPiece stmts, std::string *error_message) { while (true) { size_t used; - auto stmt = db->Prepare(stmts, &used, error_message); - if (stmt == nullptr) { + auto stmt = ctx->db()->Prepare(stmts, &used, error_message); + if (!stmt.valid()) { // Statement didn't parse. If |error_message| is empty, there are just no // more statements. Otherwise this is due to an error. Either way, return. return error_message->empty(); } VLOG(1) << "Running statement:\n" << stmts.substr(0, used).as_string(); - stmts.remove_prefix(used); - int ret = stmt->Step(); - if (ret != SQLITE_DONE) { + int64_t rows = 0; + auto run = ctx->Borrow(&stmt); + while (run.Step() == SQLITE_ROW) { + ++rows; + } + if (rows > 0) { + VLOG(1) << "Statement returned " << rows << " row(s)."; + } + if (run.status() != SQLITE_DONE) { + VLOG(1) << "Statement failed with " << run.status() << ": " + << run.error_message(); *error_message = - StrCat("Unexpected status \"", sqlite3_errstr(ret), - "\" from statement: \"", stmts.substr(0, used), "\""); + StrCat("Unexpected error ", run.error_message(), + " from statement: \"", stmts.substr(0, used), "\""); return false; } + VLOG(1) << "Statement succeeded."; + stmts.remove_prefix(used); } } diff --git a/src/sqlite.h b/src/sqlite.h index e5f85fc..3759dee 100644 --- a/src/sqlite.h +++ b/src/sqlite.h @@ -36,91 +36,239 @@ #ifndef MOONFIRE_NVR_SQLITE_H #define MOONFIRE_NVR_SQLITE_H -#include +#include +#include +#include +#include #include #include +#include "common.h" + namespace moonfire_nvr { -// Thread-compatible (not thread-safe). +// Prepared statement. Movable, not copyable. +// The caller can obtain a Statement via Database::Prepare +// and use one via DatabaseContext::Borrow. class Statement { public: - ~Statement() { sqlite3_finalize(me_); } + Statement() {} + Statement(Statement &&); + void operator=(Statement &&); + ~Statement(); - // Bind a value (of various types). - // |param| is 1-indexed. - // In the case of BindBlob, |value| will be copied immediately using - // SQLITE_TRANSIENT. - // In the case of BindText, UTF-8 is assumed. - bool BindBlob(int param, re2::StringPiece value, std::string *error_message); - bool BindInt64(int param, int64_t value, std::string *error_message); - bool BindText(int param, re2::StringPiece value, std::string *error_message); - - bool Reset(std::string *error_message); - - // Evaluate the statement. - // Returns a SQLite3 result code or extended result code. - // (Notably including SQLITE_ROW and SQLITE_DONE.) - int Step(); - - // Retrieve a value of various types. - // StringPiece values are only valid until a type conversion or a following - // call to Step(), Reset(), or Statement's destructor. - // Note that these don't have any real way to report error. - // In particular, on error a default value is returned, but that's a valid - // value. The error code is set on the database, but it's not guaranteed to - // not be set otherwise. - re2::StringPiece ColumnBlob(int col); - int64_t ColumnInt64(int col); - re2::StringPiece ColumnText(int col); + bool valid() const { return me_ != nullptr; } private: friend class Database; - Statement() {} - Statement(const Statement &) = delete; - Statement &operator=(const Statement &) = delete; + friend class DatabaseContext; + friend class RunningStatement; - sqlite3_stmt *me_ = nullptr; + Statement(const Statement &) = delete; + void operator=(const Statement &) = delete; + + void Clear(); + + sqlite3_stmt *me_ = nullptr; // owned. + bool borrowed_ = false; }; -// Thread-safe; the database is used in the default SQLITE_CONFIG_SERIALIZED. class Database { public: Database() {} Database(const Database &) = delete; Database &operator=(const Database &) = delete; - // PRE: there are no unfinalized prepared statements or unfinished backup - // objects. + // PRE: all DatabaseContext and Statement objects have been deleted. ~Database(); + // Open the database and do initial setup. + // + // Foreign keys will always be enabled via "pragma foreign_keys = true;". + // + // extended result codes will always be enabled via + // sqlite3_extended_result_codes. bool Open(const char *filename, int flags, std::string *error_message); - // Prepare a statement. + // Prepare a statement. Thread-safe. // // |used|, if non-null, will be updated with the number of bytes used from // |sql| on success. (Only the first statement is parsed.) // - // Returns the statement, or nullptr if there is no valid statement. + // Returns a statement, which may or may not be valid(). // |error_message| will be empty if there is simply no statement to parse. - std::unique_ptr Prepare(re2::StringPiece sql, size_t *used, - std::string *error_message); - - // Return the number of rows modified/inserted/deleted by the last DML - // statement executed. - int Changes() { return sqlite3_changes(me_); } + Statement Prepare(re2::StringPiece sql, size_t *used, + std::string *error_message); private: + friend class DatabaseContext; + friend class RunningStatement; sqlite3 *me_ = nullptr; + Statement begin_transaction_; + Statement commit_transaction_; + Statement rollback_transaction_; + + std::mutex ctx_mu_; // used by DatabaseContext. +}; + +// A running statement; get via DatabaseContext::Borrow or +// DatabaseContext::UseOnce. Example uses: +// +// { +// DatabaseContext ctx(&db); +// auto run = ctx.UseOnce("insert into table (column) values (:column)"); +// run.BindText(":column", "value"); +// if (run.Step() != SQLITE_DONE) { +// LOG(ERROR) << "Operation failed: " << run.error_message(); +// return; +// } +// int64_t rowid = ctx.last_insert_rowid(); +// } +// +// Statement select_stmt = db.Prepare( +// "select rowid from table;", nullptr, &error_message); +// ... +// { +// auto run = ctx.Borrow(&select_stmt); +// while (run.Step() == SQLITE_ROW) { +// int64_t rowid = op.ColumnInt64(0); +// ... +// } +// if (run.status() != SQLITE_DONE) { +// LOG(ERROR) << "Operation failed: " << run.error_message(); +// } +// } +class RunningStatement { + public: + RunningStatement(RunningStatement &&) = default; + + // Reset/unbind/return the statement for the next use (in the case of + // Borrow) or delete it (in the case of UseOnce). + ~RunningStatement(); + + // Bind a value to a parameter. Call before the first Step. + // |param| is indexed from 1 (unlike columns!). + // + // StringPiece |value|s will be copied; they do not need to outlive the + // Bind{Blob,Text} call. Text values are assumed to be UTF-8. + // + // Errors are deferred until Step() for simplicity of caller code. + void BindBlob(int param, re2::StringPiece value); + void BindBlob(const char *param, re2::StringPiece value); + void BindText(int param, re2::StringPiece value); + void BindText(const char *param, re2::StringPiece value); + void BindInt64(int param, int64_t value); + void BindInt64(const char *param, int64_t value); + + // Advance the statement, returning SQLITE_ROW, SQLITE_DONE, or an error. + // Note that this may return a "deferred error" if UseOnce failed to parse + // the SQL or if a bind failed. + int Step(); + + // Convenience function; re-return the last status from Step(). + int status() { return status_; } + + // Return a stringified version of the last status. + // This may have more information than sqlite3_errstr(status()), + // in the case of "deferred errors". + std::string error_message() { return error_message_; } + + // Column accessors, to be called after Step() returns SQLITE_ROW. + // Columns are indexed from 0 (unlike bind parameters!). + // StringPiece values are valid only until a type conversion, the following + // NextRow() call, or destruction of the RunningStatement, whichever + // happens first. + // + // Note there is no useful way to report error here. In particular, the + // underlying SQLite functions return a default value on error, which can't + // be distinguished from a legitimate value. The error code is set on the + // database, but it's not guaranteed to *not* be set if there's no error. + + // Return the type of a given column; if SQLITE_NULL, the value is null. + // As noted in sqlite3_column_type() documentation, this is only meaningful + // if other Column* calls have not forced a type conversion. + int ColumnType(int col); + + re2::StringPiece ColumnBlob(int col); + int64_t ColumnInt64(int col); + re2::StringPiece ColumnText(int col); + + private: + friend class DatabaseContext; + RunningStatement(Statement *stmt, const std::string &deferred_error, + bool own_statement); + RunningStatement(const RunningStatement &) = delete; + void operator=(const RunningStatement &) = delete; + + Statement *statement_ = nullptr; // maybe owned; see owns_statement_. + std::string error_message_; + int status_ = SQLITE_OK; + bool owns_statement_ = false; +}; + +// A scoped database lock and transaction manager. +// +// Moonfire NVR does all SQLite operations under a lock, to avoid SQLITE_BUSY +// and so that calls such as sqlite3_last_insert_rowid return useful values. +// This class implicitly acquires the lock on entry / releases it on exit. +// In the future, it may have instrumentation to track slow operations. +class DatabaseContext { + public: + // Acquire a lock on |db|, which must already be opened. + explicit DatabaseContext(Database *db); + DatabaseContext(const DatabaseContext &) = delete; + void operator=(const DatabaseContext &) = delete; + + // Release the lock and, if an explicit transaction is active, roll it + // back with a logged warning. + ~DatabaseContext(); + + // Begin a transaction, or return false and fill |error_message|. + // If successful, the caller should explicitly call CommitTransaction or + // RollbackTransaction before the DatabaseContext goes out of scope. + bool BeginTransaction(std::string *error_message); + + // Commit the transaction, or return false and fill |error_message|. + bool CommitTransaction(std::string *error_message); + + // Roll back the transaction, logging error on failure. + // The error code is not returned; there's nothing useful the caller can do. + void RollbackTransaction(); + + // Borrow a prepared statement to run. + // |statement| should outlive the RunningStatement. It can't be borrowed + // twice simultaneously, but two similar statements can be run side-by-side + // (in the same context). + RunningStatement Borrow(Statement *statement); + + // Use the given |sql| once. + // Note that parse errors are "deferred" until RunningStatement::Step(). + RunningStatement UseOnce(re2::StringPiece sql); + + // Return the number of changes for the last DML statement (insert, update, or + // delete), as with sqlite3_changes. + int64_t changes() { return sqlite3_changes(db_->me_); } + + // Return the last rowid inserted into a table that does not specify "WITHOUT + // ROWID", as with sqlite3_last_insert_rowid. + int64_t last_insert_rowid() { return sqlite3_last_insert_rowid(db_->me_); } + + Database *db() { return db_; } + + private: + Database *db_; + std::lock_guard lock_; + bool transaction_open_ = false; }; // Convenience routines below. // Run through all the statements in |stmts|. // Return error if any do not parse or return something other than SQLITE_DONE -// when stepped. -bool RunStatements(Database *db, re2::StringPiece stmts, +// when stepped. (SQLITE_ROW returns are skipped over, though. This is useful +// for "pragma journal_mode = wal;" which returns a row.) +bool RunStatements(DatabaseContext *ctx, re2::StringPiece stmts, std::string *error_message); } // namespace moonfire_nvr