wallet/ISqlit3Impl.cpp

239 lines
9.3 KiB
C++
Raw Normal View History

// this is implementation class of pure virtual interface base class between sqlite3,
// which speaks only C and utf8 char[]
// and wxWidgets which speaks only C++ and unicode strings.
//
// In this code I continually declare stuff public that should be private,
// but that is OK, because declared in a cpp file, not a header file,
// and thus they remain private to any code outside this particular cpp file.
// When the compiler complains that something is inaccessible, I don't muck
// around with friend functions and suchlike, which rapidly gets surprisingly
// complicated, I just make it public, but only public to this one file.
#include <assert.h>
#include <string> // for basic_string, allocator, char_traits
#include <initializer_list> // for initializer_list
#include <memory> // for shared_ptr, unique_ptr
#include <span>
#include "ISqlite3.h"
#include "sqlite3/sqlite3.h"
static auto error_message(int rc, sqlite3* pdb) {
return std::string("Sqlite3 Error: ") + sqlite3_errmsg(pdb) + ". Sqlite3 error number=" + std::to_string(rc);
}
void sqlite3_init() {
if (sqlite3_initialize() != SQLITE_OK) {
errorCode = 7;
szError = "Fatal Error: Sqlite library did not init.";
// Cannot log the error, because logging not set up yet, so logging itself causes an exception
throw FatalException(szError.c_str());
}
}
class IcompiledImpl_sql;
static int callback(void* NotUsed, int argc, char** argv, char** azColName) {
std::string str;
str.reserve(256);
for (int i = 0; i < argc; i++) {
str =str + "\t\"" + azColName[i]+ R"|("=)|" + (argv[i]!=nullptr ? argv[i] : "NULL");
}
ILogMessage(str.c_str());
return 0;
}
class ISqlite3Impl :
public ISqlite3
{
public:
sqlite3* pdb;
ISqlite3Impl() = delete;
ISqlite3Impl(const char* dbName, int flags) {
#ifndef NDEBUG
pdb = nullptr;
#endif
int rc =sqlite3_open_v2(dbName, &pdb, flags, nullptr);
if (rc != SQLITE_OK) throw SQLexception(error_message(rc, pdb));
assert(pdb != nullptr);
// pdb can never be nullptr, since the sqlite3_open_v2 command always initializes
// it even if open fails
}
void exec(const char* szsql) override {
char* zErrMsg = nullptr;
int rc = sqlite3_exec(pdb, szsql, callback, nullptr, &zErrMsg);
if (rc != SQLITE_OK) {
SQLexception e(std::string("SQL Exec Error: ") + zErrMsg);
sqlite3_free(zErrMsg);
throw e;
}
}
~ISqlite3Impl() override {
exec("PRAGMA optimize;"); //If we have multiple threads, will want to call this only once, and we will also want to call the check pragma in a separate thread with a separate connection.
int rc{ sqlite3_close(pdb) };
if (rc == SQLITE_OK) {}
else {
std::string err(error_message(rc, pdb) + ". Bad destruction of ISqlite3Impl");
ILogError(err.c_str());
queue_error_message(err.c_str()); //Does not actually pop up a message, which would be extremely bad in a destructor, instead queues an event which causes the message to pop up.
}
// If called before everything is finalized, will return SQL_BUSY
// but that is a coding error.
// sqlite3_close_v2 sets everything to shutdown when everything is finalized,
// but this is C++. We do our own memory management, and if we need
// sqlite3_close_v2 we are doing it wrong.
}
};
// Factory method to open database.
ISqlite3* Sqlite3_open(const char * db_name) {
return new ISqlite3Impl(db_name, SQLITE_OPEN_READWRITE);
}
// Factory method to create database.
ISqlite3 * Sqlite3_create(const char* db_name) {
return new ISqlite3Impl(db_name, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
}
class IcompiledImpl_sql :
public Icompiled_sql
{
friend class ISqlite3Impl;
private:
sqlite3_stmt *pStmt;
ISqlite3Impl *pdbImplOwn;
auto e(int rc) {
assert(rc != SQLITE_OK);
return SQLexception(error_message(rc, pdbImplOwn->pdb));
}
public:
IcompiledImpl_sql() = delete;
IcompiledImpl_sql(
ISqlite3 * pdbImpl, /* wrapped database handle */
const char *zSql /* SQL statement, UTF-8 encoded */
) {
assert(pdbImpl!=nullptr);
assert(zSql!=nullptr);
pdbImplOwn = static_cast<ISqlite3Impl*> (pdbImpl); //static downcast
// Static downcast is safe because the base is pure virtual,
// and we have only one derived type,
// If we ever have multiple derived types, I will use an enum
// in the pure virtual base class and continue to use static downcasts.
// Or, better, derive additional implementation classes from ISqlite3Impl so that
// downcast to ISqlite3Impl must always work.
assert(pdbImplOwn);
// If an instance of the derived class exists, has to a valid upcast.
const char *pzTail;
int rc = sqlite3_prepare_v3(
pdbImplOwn->pdb, /* Database handle */
zSql, /* SQL statement, UTF-8 encoded */
-1, /* Maximum length of zSql in bytes. */
SQLITE_PREPARE_PERSISTENT,
&pStmt, /* OUT: Statement handle */
&pzTail /* OUT: Pointer to unused portion of zSql */
);
if (rc != SQLITE_OK) throw e(rc);
}
~IcompiledImpl_sql() override{
int rc=sqlite3_finalize(pStmt);
if (rc == SQLITE_OK) {}
else {
std::string err(error_message(rc, pdbImplOwn->pdb) + ". Bad destruction of Icompiled_sql");
ILogError(err.c_str());
// This error should only ever happen if object failed to compile, in which case we have already handled the error
// Hence we do not queue an event to pop up a message, only log the error. (Unless ILogError pops up a message, which it might, but normally does not)
}
}
virtual void Isqlite3_bind(int param, std::span<const uint8_t> blob) override {
int rc = sqlite3_bind_blob(pStmt, param, &blob[0], static_cast<int>(blob.size_bytes()), SQLITE_STATIC);
if (rc != SQLITE_OK) throw e(rc);
}
virtual void Isqlite3_bind(int param, int i) override {
int rc = sqlite3_bind_int(pStmt, param, i);
if (rc != SQLITE_OK) throw e(rc);
}
virtual void Isqlite3_bind(int param, int64_t i) override {
int rc = sqlite3_bind_int64(pStmt, param, i);
if (rc != SQLITE_OK) throw e(rc);
}
virtual void Isqlite3_bind(int param) override {
int rc = sqlite3_bind_null(pStmt, param);
if (rc != SQLITE_OK) throw e(rc);
}
virtual void Isqlite3_bind(int param, const char* str) override {
int rc = sqlite3_bind_text(pStmt, param, str, -1, SQLITE_STATIC);
if (rc != SQLITE_OK) throw e(rc);
}
virtual sql_result Isqlite3_step() override {
int rc = sqlite3_step(pStmt);
sql_result ret;
switch (rc & 0xFF) {
case SQLITE_DONE:
ret = DONE;
break;
case SQLITE_ROW:
ret = ROW;
break;
case SQLITE_BUSY:
//ret = BUSY;
// As handling busy is hard, we will always use WAL mode and only allow one thread in one process write to the database.
// If we need many threads, perhaps in many processes, to write, they will all channel through a single thread
// in a single process whose transactions are never substantially larger than thirty two kilobytes.
// As a result, we should never get SQL_BUSY codes except in pathological cases that are OK to handle by
// terminating or reporting to the user that his operation has failed because database abnormally busy.
// When we are building the blockchain, every process but one will see the blockchain as instantaneously changing
// from n blocks to n+1 blocks when a single transaction updates the root and anciliary data.
// We will build the blockchain hash table in postfix format, with patricia tree nodes that
// have skiplink format stored only in memory and rebuilt each startup so that it grows append only,
throw SQLexception("Abnormal busy database");
break;
case SQLITE_MISUSE:
//ret = MISUSE;
throw e(rc);
break;
default:
//ret = SQL_ERROR;
throw e(rc);
}
return ret;
}
virtual std::span<const uint8_t> Isqlite3_column_blob(int iCol) const override {
return std::span<const uint8_t>((const uint8_t*)sqlite3_column_blob(pStmt, iCol), sqlite3_column_bytes(pStmt, iCol));
// returns the null pointer if null
}
virtual int Isqlite3_column_int(int iCol) const override {
return sqlite3_column_int(pStmt, iCol);
}
virtual int64_t Isqlite3_column_int64(int iCol) const override {
return sqlite3_column_int64(pStmt, iCol);
}
virtual char *Isqlite3_column_text(int iCol) const override {
return static_cast<std::nullptr_t>(sqlite3_column_text(pStmt, iCol));
/* returns pointer to zero length string if null. If we need to distinguish between zero length strings and nulls, need the type function.
We can store any type in any column, and read any type from any column, but if something unexpected is in a column, it gets converted to the expected type on being read back. For example an integer gets converted a decimal string if read as a blob or as text.
It is very rarely valid to store different types in the same column, except that null is permissible. The difference between null and zero matters, but the case of null is usually dealt with by sql code, not C code. */
}
virtual void Isqlite3_reset()override {
int rc = sqlite3_reset(pStmt);
if (rc != SQLITE_OK) throw e(rc);
// sqlite3_reset returns extended error codes
// https://sqlite.org/c3ref/reset.html
}
};
/* Factory method to prepare a compiled sql statement
Uses automatic upcast. You always want to start with the most derived smart pointer if you can, and let the compiler take care of default upcasting.*/
Icompiled_sql* sqlite3_prepare(ISqlite3 *pdbImpl, const char * zSql) {
return new IcompiledImpl_sql(
pdbImpl, /* Database handle */
zSql /* SQL statement, UTF-8 encoded */
);
}