mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2024-12-26 07:05:56 -05:00
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:
parent
4c7eed293f
commit
442b953f28
@ -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,
|
||||
|
@ -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
|
||||
|
337
src/sqlite.cc
337
src/sqlite.cc
@ -32,118 +32,361 @@
|
||||
|
||||
#include "sqlite.h"
|
||||
|
||||
#include <mutex>
|
||||
|
||||
#include <glog/logging.h>
|
||||
|
||||
#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<const char *>(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<const char *>(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<Statement> Database::Prepare(re2::StringPiece sql, size_t *used,
|
||||
std::string *error_message) {
|
||||
std::unique_ptr<Statement> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
240
src/sqlite.h
240
src/sqlite.h
@ -36,91 +36,239 @@
|
||||
#ifndef 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 <sqlite3.h>
|
||||
|
||||
#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<Statement> 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<std::mutex> 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
|
||||
|
Loading…
Reference in New Issue
Block a user