mirror of
https://github.com/scottlamb/moonfire-nvr.git
synced 2024-12-27 15:45:55 -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.
|
-- 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,
|
||||||
|
@ -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_;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,30 +71,32 @@ 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
|
||||||
|
335
src/sqlite.cc
335
src/sqlite.cc
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
238
src/sqlite.h
238
src/sqlite.h
@ -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
|
private:
|
||||||
// statement executed.
|
friend class DatabaseContext;
|
||||||
int Changes() { return sqlite3_changes(me_); }
|
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:
|
private:
|
||||||
sqlite3 *me_ = nullptr;
|
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
|
||||||
|
Loading…
Reference in New Issue
Block a user