Redo the SQLite wrapper.

I wrote the old interface before playing much with SQLite. Now that I've
played around with it a bit, I found many ways to make the interface more
pleasant and fool-proof:

* it opens the database in a mode that honors foreign keys and
  returns extended result codes.
* it forces locking to avoid SQLITE_BUSY and
  sqlite3_{changes,last_insert_rowid} race conditions.
* it supports named bind parameters.
* it defers some errors until Step() to reduce caller verbosity.
* it automatically handles calling reset, which was quite easy to forget.
* it remembers the Step() return value, which makes the row loop every so
  slightly more pleasant.
* it tracks transaction status.
This commit is contained in:
Scott Lamb 2016-01-15 23:48:30 -08:00
parent 4c7eed293f
commit 442b953f28
4 changed files with 513 additions and 114 deletions

View File

@ -31,6 +31,8 @@
-- schema.sql: SQLite3 database schema for Moonfire NVR. -- schema.sql: SQLite3 database schema for Moonfire NVR.
-- See also design/schema.md. -- See also design/schema.md.
pragma journal_mode = wal;
create table camera ( create table camera (
id integer primary key, id integer primary key,
uuid blob unique not null, uuid blob unique not null,

View File

@ -49,15 +49,19 @@ namespace {
class SqliteTest : public testing::Test { class SqliteTest : public testing::Test {
protected: protected:
SqliteTest() { SqliteTest() {
tmpdir_ = PrepareTempDirOrDie("sqlite-test");
std::string error_message; std::string error_message;
CHECK(db_.Open(":memory:", SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, CHECK(db_.Open(StrCat(tmpdir_, "/db").c_str(),
&error_message)) SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, &error_message))
<< error_message; << error_message;
std::string create_sql = ReadFileOrDie("../src/schema.sql"); 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_; Database db_;
}; };
@ -66,31 +70,33 @@ TEST_F(SqliteTest, JustCreate) {}
TEST_F(SqliteTest, BindAndColumn) { TEST_F(SqliteTest, BindAndColumn) {
std::string error_message; std::string error_message;
auto insert_stmt = db_.Prepare( auto insert_stmt = db_.Prepare(
"insert into camera (uuid, short_name, retain_bytes) " "insert into camera (uuid, short_name, retain_bytes) "
"values (?, ?, ?)", " values (:uuid, :short_name, :retain_bytes)",
nullptr, &error_message); 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, const char kBlob[] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f}; 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f};
re2::StringPiece blob_piece = re2::StringPiece(kBlob, sizeof(kBlob)); re2::StringPiece blob_piece = re2::StringPiece(kBlob, sizeof(kBlob));
const char kText[] = "foo"; const char kText[] = "foo";
const int64_t kInt64 = INT64_C(0xdeadbeeffeedface); 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 = DatabaseContext ctx(&db_);
db_.Prepare("select uuid, short_name, retain_bytes from camera", nullptr, {
&error_message); auto run = ctx.Borrow(&insert_stmt);
ASSERT_TRUE(select_stmt != nullptr) << error_message; run.BindBlob(1, blob_piece);
ASSERT_EQ(SQLITE_ROW, select_stmt->Step()); run.BindText(2, kText);
EXPECT_EQ(ToHex(blob_piece, true), ToHex(select_stmt->ColumnBlob(0), true)); run.BindInt64(3, kInt64);
EXPECT_EQ(kText, select_stmt->ColumnText(1).as_string()); ASSERT_EQ(SQLITE_DONE, run.Step()) << run.error_message();
EXPECT_EQ(kInt64, select_stmt->ColumnInt64(2)); }
ASSERT_EQ(SQLITE_DONE, select_stmt->Step());
{
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 } // namespace

View File

@ -32,118 +32,361 @@
#include "sqlite.h" #include "sqlite.h"
#include <mutex>
#include <glog/logging.h> #include <glog/logging.h>
#include "string.h" #include "string.h"
namespace moonfire_nvr { namespace moonfire_nvr {
bool Statement::BindBlob(int param, re2::StringPiece value, namespace {
std::string *error_message) {
int err = sqlite3_bind_blob64(me_, param, value.data(), value.size(), void LogCallback(void *, int err_code, const char *msg) {
SQLITE_TRANSIENT); LOG(ERROR) << "(" << err_code << ") " << msg;
if (err != SQLITE_OK) { }
*error_message = sqlite3_errstr(err);
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; 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; return true;
} }
bool Statement::BindInt64(int param, int64_t value, bool DatabaseContext::CommitTransaction(std::string *error_message) {
std::string *error_message) { if (!transaction_open_) {
int err = sqlite3_bind_int64(me_, param, value); *error_message = "transaction not open!";
if (err != SQLITE_OK) {
*error_message = sqlite3_errstr(err);
return false; 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; return true;
} }
bool Statement::BindText(int param, re2::StringPiece value, void DatabaseContext::RollbackTransaction() {
std::string *error_message) { if (!transaction_open_) {
int err = sqlite3_bind_text64(me_, param, value.data(), value.size(), LOG(WARNING) << this << ": rollback failed: transaction not open!";
SQLITE_TRANSIENT, SQLITE_UTF8); return;
if (err != SQLITE_OK) {
*error_message = sqlite3_errstr(err);
return false;
} }
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. // Order matters: call _blob first, then _bytes.
const void *data = sqlite3_column_blob(me_, col); const void *data = sqlite3_column_blob(statement_->me_, col);
size_t len = sqlite3_column_bytes(me_, col); size_t len = sqlite3_column_bytes(statement_->me_, col);
return re2::StringPiece(reinterpret_cast<const char *>(data), len); return re2::StringPiece(reinterpret_cast<const char *>(data), len);
} }
int64_t Statement::ColumnInt64(int col) { int64_t RunningStatement::ColumnInt64(int col) {
return sqlite3_column_int64(me_, 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. // Order matters: call _text first, then _bytes.
const unsigned char *data = sqlite3_column_text(me_, col); const unsigned char *data = sqlite3_column_text(statement_->me_, col);
size_t len = sqlite3_column_bytes(me_, col); size_t len = sqlite3_column_bytes(statement_->me_, col);
return re2::StringPiece(reinterpret_cast<const char *>(data), len); return re2::StringPiece(reinterpret_cast<const char *>(data), len);
} }
int Statement::Step() { return sqlite3_step(me_); }
Database::~Database() { Database::~Database() {
begin_transaction_ = Statement();
commit_transaction_ = Statement();
rollback_transaction_ = Statement();
int err = sqlite3_close(me_); int err = sqlite3_close(me_);
CHECK_EQ(SQLITE_OK, err) << "sqlite3_close: " << sqlite3_errstr(err); CHECK_EQ(SQLITE_OK, err) << "sqlite3_close: " << sqlite3_errstr(err);
} }
bool Database::Open(const char *filename, int flags, bool Database::Open(const char *filename, int flags,
std::string *error_message) { std::string *error_message) {
int err = sqlite3_open_v2(filename, &me_, flags, nullptr); std::call_once(global_setup, &GlobalSetup);
if (err != SQLITE_OK) { int ret = sqlite3_open_v2(filename, &me_, flags, nullptr);
*error_message = sqlite3_errstr(err); if (ret != SQLITE_OK) {
*error_message = sqlite3_errstr(ret);
return false; 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; return true;
} }
std::unique_ptr<Statement> Database::Prepare(re2::StringPiece sql, size_t *used, Statement Database::Prepare(re2::StringPiece sql, size_t *used,
std::string *error_message) { std::string *error_message) {
std::unique_ptr<Statement> statement(new Statement); Statement statement;
const char *tail; const char *tail;
int err = 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) { if (err != SQLITE_OK) {
*error_message = sqlite3_errstr(err); *error_message = sqlite3_errstr(err);
statement.release(); return statement;
} }
if (used != nullptr) { if (used != nullptr) {
*used = tail - sql.data(); *used = tail - sql.data();
} }
if (statement->me_ == nullptr) { if (statement.me_ == nullptr) {
error_message->clear(); error_message->clear();
statement.release();
} }
return statement; return statement;
} }
bool RunStatements(Database *db, re2::StringPiece stmts, bool RunStatements(DatabaseContext *ctx, re2::StringPiece stmts,
std::string *error_message) { std::string *error_message) {
while (true) { while (true) {
size_t used; size_t used;
auto stmt = db->Prepare(stmts, &used, error_message); auto stmt = ctx->db()->Prepare(stmts, &used, error_message);
if (stmt == nullptr) { if (!stmt.valid()) {
// Statement didn't parse. If |error_message| is empty, there are just no // 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. // more statements. Otherwise this is due to an error. Either way, return.
return error_message->empty(); return error_message->empty();
} }
VLOG(1) << "Running statement:\n" << stmts.substr(0, used).as_string(); VLOG(1) << "Running statement:\n" << stmts.substr(0, used).as_string();
stmts.remove_prefix(used); int64_t rows = 0;
int ret = stmt->Step(); auto run = ctx->Borrow(&stmt);
if (ret != SQLITE_DONE) { 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 = *error_message =
StrCat("Unexpected status \"", sqlite3_errstr(ret), StrCat("Unexpected error ", run.error_message(),
"\" from statement: \"", stmts.substr(0, used), "\""); " from statement: \"", stmts.substr(0, used), "\"");
return false; return false;
} }
VLOG(1) << "Statement succeeded.";
stmts.remove_prefix(used);
} }
} }

View File

@ -36,91 +36,239 @@
#ifndef MOONFIRE_NVR_SQLITE_H #ifndef MOONFIRE_NVR_SQLITE_H
#define MOONFIRE_NVR_SQLITE_H #define MOONFIRE_NVR_SQLITE_H
#include <memory> #include <functional>
#include <mutex>
#include <string>
#include <glog/logging.h>
#include <re2/stringpiece.h> #include <re2/stringpiece.h>
#include <sqlite3.h> #include <sqlite3.h>
#include "common.h"
namespace moonfire_nvr { 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 { class Statement {
public: public:
~Statement() { sqlite3_finalize(me_); } Statement() {}
Statement(Statement &&);
void operator=(Statement &&);
~Statement();
// Bind a value (of various types). bool valid() const { return me_ != nullptr; }
// |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);
private: private:
friend class Database; friend class Database;
Statement() {} friend class DatabaseContext;
Statement(const Statement &) = delete; friend class RunningStatement;
Statement &operator=(const Statement &) = delete;
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 { class Database {
public: public:
Database() {} Database() {}
Database(const Database &) = delete; Database(const Database &) = delete;
Database &operator=(const Database &) = delete; Database &operator=(const Database &) = delete;
// PRE: there are no unfinalized prepared statements or unfinished backup // PRE: all DatabaseContext and Statement objects have been deleted.
// objects.
~Database(); ~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); 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 // |used|, if non-null, will be updated with the number of bytes used from
// |sql| on success. (Only the first statement is parsed.) // |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. // |error_message| will be empty if there is simply no statement to parse.
std::unique_ptr<Statement> Prepare(re2::StringPiece sql, size_t *used, Statement Prepare(re2::StringPiece sql, size_t *used,
std::string *error_message); std::string *error_message);
// Return the number of rows modified/inserted/deleted by the last DML
// statement executed.
int Changes() { return sqlite3_changes(me_); }
private: private:
friend class DatabaseContext;
friend class RunningStatement;
sqlite3 *me_ = nullptr; 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<std::mutex> lock_;
bool transaction_open_ = false;
}; };
// Convenience routines below. // Convenience routines below.
// Run through all the statements in |stmts|. // Run through all the statements in |stmts|.
// Return error if any do not parse or return something other than SQLITE_DONE // Return error if any do not parse or return something other than SQLITE_DONE
// when stepped. // when stepped. (SQLITE_ROW returns are skipped over, though. This is useful
bool RunStatements(Database *db, re2::StringPiece stmts, // for "pragma journal_mode = wal;" which returns a row.)
bool RunStatements(DatabaseContext *ctx, re2::StringPiece stmts,
std::string *error_message); std::string *error_message);
} // namespace moonfire_nvr } // namespace moonfire_nvr