Skip to content

Commit

Permalink
Tag all sqlite errors with the db name
Browse files Browse the repository at this point in the history
sqlite errors are quite ambiguous on their own (e.g. "I/O error") so it
is helpful to always relate their instances to the database that triggered
them.
  • Loading branch information
jmmv committed Jul 7, 2015
1 parent 8003c86 commit 9df5d27
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 34 deletions.
66 changes: 59 additions & 7 deletions utils/sqlite/exceptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,51 @@ extern "C" {
#include <sqlite3.h>
}

#include <string>

#include "utils/format/macros.hpp"
#include "utils/fs/path.hpp"
#include "utils/optional.ipp"
#include "utils/sqlite/c_gate.hpp"
#include "utils/sqlite/database.hpp"

namespace fs = utils::fs;
namespace sqlite = utils::sqlite;

using utils::optional;


namespace {


/// Formats the database filename returned by sqlite for user consumption.
///
/// \param db_filename An optional database filename.
///
/// \return A string describing the filename.
static std::string
format_db_filename(const optional< fs::path >& db_filename)
{
if (db_filename)
return db_filename.get().str();
else
return "in-memory";
}


} // anonymous namespace


/// Constructs a new error with a plain-text message.
///
/// \param db_filename_ Database filename as returned by database::db_filename()
/// for error reporting purposes.
/// \param message The plain-text error message.
sqlite::error::error(const std::string& message) :
std::runtime_error(message)
sqlite::error::error(const optional< fs::path >& db_filename_,
const std::string& message) :
std::runtime_error(F("%s (sqlite db: %s)") % message %
format_db_filename(db_filename_)),
_db_filename(db_filename_)
{
}

Expand All @@ -53,13 +87,26 @@ sqlite::error::~error(void) throw()
}


/// Returns the path to the database that raised this error.
///
/// \return A database filename as returned by database::db_filename().
const optional< fs::path >&
sqlite::error::db_filename(void) const
{
return _db_filename;
}


/// Constructs a new error.
///
/// \param db_filename_ Database filename as returned by database::db_filename()
/// for error reporting purposes.
/// \param api_function_ The name of the API function that caused the error.
/// \param message_ The plain-text error message provided by SQLite.
sqlite::api_error::api_error(const std::string& api_function_,
sqlite::api_error::api_error(const optional< fs::path >& db_filename_,
const std::string& api_function_,
const std::string& message_) :
error(message_),
error(db_filename_, message_),
_api_function(api_function_)
{
}
Expand All @@ -83,7 +130,8 @@ sqlite::api_error::from_database(database& database_,
const std::string& api_function_)
{
::sqlite3* c_db = database_c_gate(database_).c_database();
return api_error(api_function_, ::sqlite3_errmsg(c_db));
return api_error(database_.db_filename(), api_function_,
::sqlite3_errmsg(c_db));
}


Expand All @@ -99,9 +147,13 @@ sqlite::api_error::api_function(void) const

/// Constructs a new error.
///
/// \param db_filename_ Database filename as returned by database::db_filename()
/// for error reporting purposes.
/// \param name_ The name of the unknown column.
sqlite::invalid_column_error::invalid_column_error(const std::string& name_) :
error(F("Unknown column '%s'") % name_),
sqlite::invalid_column_error::invalid_column_error(
const optional< fs::path >& db_filename_,
const std::string& name_) :
error(db_filename_, F("Unknown column '%s'") % name_),
_column_name(name_)
{
}
Expand Down
16 changes: 13 additions & 3 deletions utils/sqlite/exceptions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
#include <stdexcept>
#include <string>

#include "utils/fs/path_fwd.hpp"
#include "utils/optional.hpp"
#include "utils/sqlite/database_fwd.hpp"

namespace utils {
Expand All @@ -43,9 +45,15 @@ namespace sqlite {

/// Base exception for sqlite errors.
class error : public std::runtime_error {
/// Path to the database that raised this error.
utils::optional< utils::fs::path > _db_filename;

public:
explicit error(const std::string&);
explicit error(const utils::optional< utils::fs::path >&,
const std::string&);
virtual ~error(void) throw();

const utils::optional< utils::fs::path >& db_filename(void) const;
};


Expand All @@ -55,7 +63,8 @@ class api_error : public error {
std::string _api_function;

public:
explicit api_error(const std::string&, const std::string&);
explicit api_error(const utils::optional< utils::fs::path >&,
const std::string&, const std::string&);
virtual ~api_error(void) throw();

static api_error from_database(database&, const std::string&);
Expand All @@ -70,7 +79,8 @@ class invalid_column_error : public error {
std::string _column_name;

public:
explicit invalid_column_error(const std::string&);
explicit invalid_column_error(const utils::optional< utils::fs::path >&,
const std::string&);
virtual ~invalid_column_error(void) throw();

const std::string& column_name(void) const;
Expand Down
66 changes: 49 additions & 17 deletions utils/sqlite/exceptions_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,62 +28,94 @@

#include "utils/sqlite/exceptions.hpp"

extern "C" {
#include <sqlite3.h>
}

#include <cstring>
#include <string>

#include <atf-c++.hpp>

#include "utils/fs/path.hpp"
#include "utils/optional.ipp"
#include "utils/sqlite/c_gate.hpp"
#include "utils/sqlite/database.hpp"

namespace fs = utils::fs;
namespace sqlite = utils::sqlite;

using utils::none;


ATF_TEST_CASE_WITHOUT_HEAD(error__no_filename);
ATF_TEST_CASE_BODY(error__no_filename)
{
const sqlite::database db = sqlite::database::in_memory();
const sqlite::error e(db.db_filename(), "Some text");
ATF_REQUIRE_EQ("Some text (sqlite db: in-memory)", std::string(e.what()));
ATF_REQUIRE_EQ(db.db_filename(), e.db_filename());
}

ATF_TEST_CASE_WITHOUT_HEAD(error);
ATF_TEST_CASE_BODY(error)

ATF_TEST_CASE_WITHOUT_HEAD(error__with_filename);
ATF_TEST_CASE_BODY(error__with_filename)
{
const sqlite::error e("Some text");
ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0);
const sqlite::database db = sqlite::database::open(
fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create);
const sqlite::error e(db.db_filename(), "Some text");
ATF_REQUIRE_MATCH("Some text \\(sqlite db: .*/test.db\\)",
std::string(e.what()));
ATF_REQUIRE_EQ(db.db_filename(), e.db_filename());
}


ATF_TEST_CASE_WITHOUT_HEAD(api_error__explicit);
ATF_TEST_CASE_BODY(api_error__explicit)
{
const sqlite::api_error e("some_function", "Some text");
ATF_REQUIRE(std::strcmp("Some text", e.what()) == 0);
const sqlite::api_error e(none, "some_function", "Some text");
ATF_REQUIRE_EQ("Some text (sqlite db: in-memory)", std::string(e.what()));
ATF_REQUIRE_EQ("some_function", e.api_function());
}


ATF_TEST_CASE_WITHOUT_HEAD(api_error__from_database);
ATF_TEST_CASE_BODY(api_error__from_database)
{
::sqlite3* raw_db;
ATF_REQUIRE_EQ(SQLITE_CANTOPEN, ::sqlite3_open_v2("missing.db", &raw_db,
SQLITE_OPEN_READONLY, NULL));
sqlite::database db = sqlite::database::open(
fs::path("test.db"), sqlite::open_readwrite | sqlite::open_create);

// Use the raw sqlite3 API to cause an error. Our C++ wrappers catch all
// errors and reraise them as exceptions, but here we want to handle the raw
// error directly for testing purposes.
sqlite::database_c_gate gate(db);
::sqlite3_stmt* dummy_stmt;
const char* query = "ABCDE INVALID QUERY";
(void)::sqlite3_prepare_v2(gate.c_database(), query, std::strlen(query),
&dummy_stmt, NULL);

sqlite::database gate = sqlite::database_c_gate::connect(raw_db);
const sqlite::api_error e = sqlite::api_error::from_database(
gate, "real_function");
ATF_REQUIRE(std::strcmp("unable to open database file", e.what()) == 0);
db, "real_function");
ATF_REQUIRE_MATCH(".*ABCDE.*\\(sqlite db: .*/test.db\\)",
std::string(e.what()));
ATF_REQUIRE_EQ("real_function", e.api_function());

::sqlite3_close(raw_db);
}


ATF_TEST_CASE_WITHOUT_HEAD(invalid_column_error);
ATF_TEST_CASE_BODY(invalid_column_error)
{
const sqlite::invalid_column_error e("some_name");
ATF_REQUIRE(std::strcmp("Unknown column 'some_name'", e.what()) == 0);
const sqlite::invalid_column_error e(none, "some_name");
ATF_REQUIRE_EQ("Unknown column 'some_name' (sqlite db: in-memory)",
std::string(e.what()));
ATF_REQUIRE_EQ("some_name", e.column_name());
}


ATF_INIT_TEST_CASES(tcs)
{
ATF_ADD_TEST_CASE(tcs, error);
ATF_ADD_TEST_CASE(tcs, error__no_filename);
ATF_ADD_TEST_CASE(tcs, error__with_filename);

ATF_ADD_TEST_CASE(tcs, api_error__explicit);
ATF_ADD_TEST_CASE(tcs, api_error__from_database);
Expand Down
21 changes: 14 additions & 7 deletions utils/sqlite/statement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ extern "C" {
#include "utils/noncopyable.hpp"
#include "utils/sanity.hpp"
#include "utils/sqlite/c_gate.hpp"
#include "utils/sqlite/database.hpp"
#include "utils/sqlite/exceptions.hpp"

namespace sqlite = utils::sqlite;
Expand Down Expand Up @@ -261,7 +262,7 @@ sqlite::statement::column_id(const char* name)

const std::map< std::string, int >::const_iterator iter = cache.find(name);
if (iter == cache.end())
throw invalid_column_error(name);
throw invalid_column_error(_pimpl->db.db_filename(), name);
else
return (*iter).second;
}
Expand Down Expand Up @@ -371,7 +372,8 @@ sqlite::statement::safe_column_blob(const char* name)
{
const int column = column_id(name);
if (column_type(column) != sqlite::type_blob)
throw sqlite::error(F("Column '%s' is not a blob") % name);
throw sqlite::error(_pimpl->db.db_filename(),
F("Column '%s' is not a blob") % name);
return column_blob(column);
}

Expand All @@ -389,7 +391,8 @@ sqlite::statement::safe_column_double(const char* name)
{
const int column = column_id(name);
if (column_type(column) != sqlite::type_float)
throw sqlite::error(F("Column '%s' is not a float") % name);
throw sqlite::error(_pimpl->db.db_filename(),
F("Column '%s' is not a float") % name);
return column_double(column);
}

Expand All @@ -407,7 +410,8 @@ sqlite::statement::safe_column_int(const char* name)
{
const int column = column_id(name);
if (column_type(column) != sqlite::type_integer)
throw sqlite::error(F("Column '%s' is not an integer") % name);
throw sqlite::error(_pimpl->db.db_filename(),
F("Column '%s' is not an integer") % name);
return column_int(column);
}

Expand All @@ -425,7 +429,8 @@ sqlite::statement::safe_column_int64(const char* name)
{
const int column = column_id(name);
if (column_type(column) != sqlite::type_integer)
throw sqlite::error(F("Column '%s' is not an integer") % name);
throw sqlite::error(_pimpl->db.db_filename(),
F("Column '%s' is not an integer") % name);
return column_int64(column);
}

Expand All @@ -443,7 +448,8 @@ sqlite::statement::safe_column_text(const char* name)
{
const int column = column_id(name);
if (column_type(column) != sqlite::type_text)
throw sqlite::error(F("Column '%s' is not a string") % name);
throw sqlite::error(_pimpl->db.db_filename(),
F("Column '%s' is not a string") % name);
return column_text(column);
}

Expand All @@ -462,7 +468,8 @@ sqlite::statement::safe_column_bytes(const char* name)
const int column = column_id(name);
if (column_type(column) != sqlite::type_blob &&
column_type(column) != sqlite::type_text)
throw sqlite::error(F("Column '%s' is not a blob or a string") % name);
throw sqlite::error(_pimpl->db.db_filename(),
F("Column '%s' is not a blob or a string") % name);
return column_bytes(column);
}

Expand Down

0 comments on commit 9df5d27

Please sign in to comment.