I’ve recently been polishing my pet-project (dupa – duplicate analyzer). It uses SQLite3 under the hood, so I figured I’d come up with a really simple C++ wrapper around the official SQLite3 C library. Certainly, it is not a full-fledged product, but I think this is how a modern C++ interface to a small database should look like, because you can unleash the expressiveness of C++ structured bindings.
This is an example snippet from dupa showing what I mean:
for (const auto &[path, cksum, size, mtime] :
db.Query(
"SELECT path, cksum, size, mtime FROM FileList")) {
// do stuff with path, cksum, size, mtime
}
The most important thing to me was to make it possible to specify the types of the columns next to the query string and not repeat those type specifications further. This is still not LINQ-smart, but I think it’s as good as it gets in C++.
Here are examples from different approaches (from SQLiteCpp and sqlite3cc):
SQLite::Statement query(db,
"SELECT path, cksum, size, mtime FROM FileList");
while (query.executeStep())
{
const std::string &path = query.getColumn(0);
const Cksum &cksum = query.getColumn(1);
const off_t &size = query.getColumn(2);
const time_t &mtime = query.getColumn(3);
// do stuff with path, cksum, size, mtime
}
for(const auto &i : sqlite::query(conn,
"SELECT path, cksum, size, mtime FROM FileList")) {
const std::string &path;
const Cksum &cksum;
const off_t &size;
const time_t &mtime;
i >> path >> cksum >> size >> mtime;
// do stuff with path, cksum, size, mtime
}
They respectively overload the cast operators or stream operators. While this allows you to achieve what I wanted to, it doesn’t force you to – you can still accidentally access the same column of a query result as 2 different types or at the very least, makes the code more verbose.
Writing to a database with my library is also C++ish and type-safe (simplified snippet from dupa):
auto out = db.Prepare(
"INSERT INTO EqClass(id, nodes, weight, interesting) "
"VALUES(?, ?, ?, 0)");
for (const auto &eq_class : classes) {
out->Write(reinterpret_cast(eq_class.get()),
eq_class->GetNumNodes(), eq_class->GetWeight());
}
Alternatively, if you’re a fan of functional programming, you can write it that way too:
auto out = db.Prepare(
"INSERT INTO EqClass(id, nodes, weight, interesting) "
"VALUES(?, ?, ?, 0)");
std::transform(classes.begin(), classes.end(), out->begin(),
[](const std::unique_ptr &eq_class) {
return std::make_tuple(
reinterpret_cast(eq_class.get()),
eq_class->GetNumNodes(), eq_class->GetWeight());
});
How it’s achieved? The cornerstone of this SQLite wrapper is the decision that we’ll be binding the types of columns with input or output streams to the database. It is achieved by DBInStream
and DBOutStream
being variadically templated by the column types. These streams are created on Query
and Prepare
invocations. An straight-forward implication of this decision is that an iterator over DBInStream
has to return a tuple typed the same way as DBInStream
. The rest is just C++17 awesomeness. There are some gory details obviously. Take a look at src/db_lib* in dupa and if you have questions, I’ll happily reply.
If you’re interested in moving this database library forward, please let me know and I’ll rip it out and start a new project with it.