// 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 #include // for basic_string, allocator, char_traits #include // for initializer_list #include // for shared_ptr, unique_ptr #include #include "ISqlite3.h" #include "sqlite3.h" /*Because sqlite3.h is frigging enormous and does no end of strange, clever things that I do not understand, I do not include it my standard header files, only in this file, which does not include my standard header files. The ISqlit3Impl.cpp provides a low level C++ interface to the even lower level C interface of sqlite3.c, and ISqlite3.h provides an interface between high level c++ish code, and the low level C++ish code of ISqlit3Impl, and sqlite3.h, included only in ISqlit3Imp.cpp, provides an even lower level interface, which the rest of my code does not use, only ISqlite3Impl.cpp, between C++ and C The rest of my code cannot see the interface class, only is purely virtual base class. Objects of the implementation class generated by the factories in Sqlit3Impl are naked old fashioned pointers to the heap, which are presented by db_accessors.cpp wrapped in std:unique.ptrs. The rest of my code cannot contain variables of the implementation class on the stack. The interface classes in db_accessors cannot be default constructed. which is to say, cannot contain a nullptr. The default constructor is explicitly deleted, because it is a bug waiting to happen which has tripped me up too many times. I avoid having any file that includes both sqlite3.h and the headers for my standard high level environment. It is just too big and too complicated. Note that sqlite3.cpp does *not* include sqlite3.h either. Any changes to the sqlite3.h headers have to be duplicated in both files. Also note that the copy of sqlite.h that lives in sqlite3.c does not work in the main program environment, and vice versa. They are not the same even though the documentation says they are the same and will work. Like I said, too clever by half. sqlite3.cpp is very low level C code, assembly written in C. only includes stdarg.h, does not link to most standard libraries. In lieu of linking to standard libraries, tries to adapt to its environment independently. */ 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); } } 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 MyException(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) { auto e = MyException(zErrMsg, rc); 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); } MyException::MyException(int i, sqlite3* pdb) noexcept: err_number(i), err(error_message(i, pdb)) {} // 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 MyException(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 (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()); // Finalize must eventually be called on all compiled sql statements, or else we get // a memory leak. // This error should only ever happen if something went wrong with this statement earlier, // 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 blob) override { int rc = sqlite3_bind_blob(pStmt, param, &blob[0], static_cast(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 MyException("Abnormal busy database", 2^14+2); break; case SQLITE_MISUSE: //ret = MISUSE; throw e(rc); break; default: //ret = SQL_ERROR; throw e(rc); } return ret; } virtual std::span Isqlite3_column_blob(int iCol) const override { return std::span((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(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 */ ); }