A thin, safe and convenient modern C++ wrapper for SQLite API.
Using SQLite C API from C++ can be quite tedious and error prone. While the API is generally clean and object-oriented, various aspects of it, notably resource management and error handling can be very tricky to get right. There is also plenty of void *
and ...
that make it easy to make a mistake as well as other annoyances. The purpose of this library is to provide a C++ wrapper that alleviates all these problems without introducing any overhead.
- C++17 or greater
- SQLite 3.7.15 or greater
- 0 overhead. Using the library should introduce no overhead compared to an equivalent and correct C API use when compiled with optimizations turned on. In particular it should not introduce any memory allocations where an equivalent correct C code wouldn't.
- No new concepts Using the library should not require from the developer to learn new high level concepts compared to plain SQLite.
- Depend only on public API The library shouldn't depend on any implementation details of SQLite beyond what is documented in the public API or forms necessary logical consequence of it.
- RAII for resource management Using the library should free the developer from manually managing various
close
,finalize
etc. methods - Error safety Correct error handling with SQLite C API is notoriously hard. While there is an overall strategy it follows there are many exceptions (pun not intended) and special cases. Getting additional information about errors correctly is also quite tricky. The library should wrap all of this in one simple and coherent approach that does the right thing and frees the developer from dealing with it.
- Mix and match It should be possible to mix usage of C++ and plain C API in any combination.
- Wrapper transparency You should be able to convert freely from C API pointers to C++ wrappers and back. The translation should ideally be identity preserving: if you create a C++ wrapper of a C pointer and later observer the C pointer in the code, you should be able to get the original wrapper back from it with 0 overhead.
- Type safety No
void *
or...
if possible. - Const and noexcept safety Logically non mutating methods should be
const
. Functions that do not throw -noexcept
- Simplify overloaded APIs Some SQLite APIs are badly overloaded. They have a large number of parameters and do different things depending on which ones are specified and which ones are left out. When possible these should be separated into distinct calls.
- Support each and every every SQLite interface. There is no reason to do waste time supporting deprecated interfaces, for example. If these are needed by client code it can always access them via C API
- Provide higher level abstractions not directly exposed by SQLite, e.g. cursors or type safe construction of SQL statements.
- Extend Serialized threading mode (see https://sqlite.org/threadsafe.html) to the wrapper. Serialized mode is a design mistake (equivalent to Java's original synchronized collections) and trying to extend it to the wrapper library without violating 0 overhead principle is impossible. The library should work with a database in this mode but not provide Serialized guarantees to its own calls.
- Support C++ older than C++17
- Support UTF16 SQLite interfaces.
Here is a small example of opening a database that demonstrates many of the features of the library.
std::filesystem::path dbFolder = ...;
auto db = database::open((dbFolder / "database.db").string(),
SQLITE_OPEN_READWRITE |
SQLITE_OPEN_NOMUTEX |
SQLITE_OPEN_PRIVATECACHE);
db->config<SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION>(1, nullptr);
db->config<SQLITE_DBCONFIG_ENABLE_FKEY>(1, nullptr);
db->exec("PRAGMA journal_mode=WAL");
auto st = statement::create(*db, "PRAGMA user_version");
if (!st->step())
throw std::runtime_error("No user_version in database");
auto version = st->column_value<int64_t>(0);
st = statement::create(*db, "SELECT value FROM metadata WHERE key = 'schemaHash'");
if (!st->step())
throw std::runtime_error("No schema hash in database");
std::string schemaHash(st->column_value<std::string_view>(0));
if (schemaHash != g_dbSchemaHash)
throw std::runtime_error("Database schema mismatch");
With modern CMake you can easily integrate ThinSQLite++ as follows:
include(FetchContent)
FetchContent_Declare(thinsqlitepp
GIT_REPOSITORY git@github.com:gershnik/thinsqlitepp.git
GIT_TAG <desired tag like v1.0>
GIT_SHALLOW TRUE
)
...
FetchContent_MakeAvailable(thinsqlitepp)
...
target_link_libraries(mytarget
PRIVATE
thinsqlitepp::thinsqlitepp
)
Alternatively you can download the library from Releases page, unpack it somewhere and do this
add_subdirectory(PATH_WHERE_YOU_UNPACKED_IT_TO, thinsqlitepp)
...
target_link_libraries(mytarget
PRIVATE
thinsqlitepp::thinsqlitepp
)
You can also build and install this library on your system using CMake.
- download the library from Releases page, unpack it into SOME_PATH
- On command line:
cd SOME_PATH
cmake -S . -B build
cmake --build build
#install to /usr/local
sudo cmake --install build
#or for a different prefix
#cmake --install build --prefix /usr
Once the library has been installed it can be used int the following ways:
Set the include directory to <prefix>/include
where <prefix>
is the install prefix from above.
find_package(thinsqlitepp)
target_link_libraries(mytarget
PRIVATE
thinsqlitepp::thinsqlitepp
)
Add the output of pkg-config --cflags thinsqlitepp
to your compiler flags.
Note that the default installation prefix /usr/local
might not be in the list of places your
pkg-config
looks into. If so you might need to do:
export PKG_CONFIG_PATH=/usr/local/share/pkgconfig
before running pkg-config
You can also simply download the headers of this repository from Releases page
(named thinsqlitepp-X.Y.tar.gz
), unpack it somewhere and add its inc
to your include path.
Whichever method you use in order to use ThinSQLite++ your compiler needs to be set to C++17 mode or higher. ThinSQLite++ should compile cleanly even on a highest warnings level.
This library relies on some of the same configuration macros as SQLite itself to enable some optional functionality. Currently the following macros are used:
SQLITE_OMIT_LOAD_EXTENSION
SQLITE_OMIT_PROGRESS_CALLBACK
SQLITE_OMIT_DECLTYPE
If your SQLite is built using any of these you should define them for any code that uses this library too.
Note that macOS built-in SQLite is built with SQLITE_OMIT_LOAD_EXTENSION
and the relevant functions aren't even present in its sqlite3.h
header file. Therefore, you must define this macro in order to use this library with it.
All errors are reported via C++ exceptions. There is no attempt to produce a parallel error code based interface (like std::filesystem
does) or expected/outcome based one. This is a deliberate choice. Duplicating the entire library and maintaining it in such condition is a huge drain on developer resources. Exceptions, with all their issues, are by far simpler and easier way to write code and are perfectly fine for vast majority of users. If you are in one of the environments which cannot use exceptions for some reason - you can use the C API or wait until deterministic exceptions make their way into C++ standard.
C++ "wrapper" classes aren't real. Instead each sqlite3_foo
C object pointer is treated as a pointer to a "fake" C++ class sqlitepp::foo
. Member functions cast the this
pointer back and invoke the corresponding C API. Why do that instead of the usual "class containing a pointer" approach? Because it produces less overhead and allows identity preserving conversions. A traditional approach can be done it 2 ways. In the first, wrapper class is a non-copyable, move-only entity (like unique_ptr
). This requires passing pointers to it around - and adds double indirection. It is true that inlining can often eliminate overhead here but not always. Most problematically, now you cannot go back from C pointer to the C++ object that owns it. This gets ugly in a C callback. In the second approach you end up with 2 classes: non owning wrapper that is passed around by value and an "owner" (like unique_ptr
) for it. This can be made to work as well as the fake classes approach but at the price of having way more code and introducing two new non standard concepts that library user needs to learn.
Is fake classes approach legal from C++ standard point of view? I don't know but I wouldn't be surprised if not. Probably casting from pointer to Foo to pointer to an unrelated Bar and back is undefined behavior even if both are simple aggregates. Having said that, there isn't a conceivable platform or compiler where this would fail to work. In fact, many C libraries rely on just that for their working. So at the end simple and portable is better than complex and standard conforming (at least from my point of view).
Currently the library is supplied as header-only. The purpose of this library is to be a thin
wrapper so vast majority of it is by design inline. There are only a couple of places where using non-inline implementation would be even mildly beneficial (an exception::what
virtual function and one or two longer functions) to library user and dealing with all the issues surrounding providing a library seems to be not worth it. Having said that, the library headers are structured in such a way as to allow making a separate compilation possible. If there is a need in the future it should be relatively simple to add this.
As mentioned in Non Goals there is no attempt to extend Serialized SQLite mode to this library. The problem is that even if each SQLite call itself is protected by a mutex internally calling them one after another might require additional locking, if you need their results to be consistent. This is most notable when you want to extract Database error code or message after a failed call.
Adding such locking is a pure overhead for sane users who don't use Serialized mode (even checking for nullptr
mutex has a cost) and brings no benefit to unfortunate people who do use it. Correct C code that uses such mode still needs to lock externally in all the places where this library would have done it.