Contents
Introduction
SQlite3 is a super handy database format for prototyping and certainly for deployment on embedded device and smartphones/tablets. There a no quirks with respect to automatic setup and long dependencies as it would be the case when trying to run PostgreSQL or MariaDB on e.g an Android device. Furthermore, SQLite3 even provides some very simple simple concurrency features (see SQLite3 Concurrency example and benchmark). However, using the SQLite C API in a C++ project requires programming a significant amount of boilerplate and requires a lot of “C thinking” which can cause some slow-downs, especially in the field of data science and machine learning as most people working in this field might be familiar with easy SQLite usage as with Python.
C++ Wrappers for SQLite3
There are a couple of modern C++ wrappers for SQLite3 available. The most prominent are:
- Qt SQL
- general purpose SQL database connection class/layer
- QTL
- general purpose SQL database connection class/layer
- SQLiteC++ (SQliteCpp)
- SQLite3++
- sqlite3cc
- sqlite_modern_cpp
- VSQlite++
- it depends on boost which might be considered a disadvantage
- wxSQLite3
I made a Debian/Ubuntu build script for SQLiteCpp available on github. If ArchLinux or Manjaro is used, there exists a build script in the AUR.
Examples
Let’s have a look at QtSql.
QtSql
NB!: According to Qt’s documentation a QCoreApplication object needs to be initialized before using QtSql as it requires Qt’s event loop to be available. However, it seems to work without initializing a QCoreApplication as well (not tested properbly but in the example blow it works).
In general, there are two ways to use the QtSql wrapper:
- write a class and use class functions to trigger sql actions
- use it within any Qt loop/object or main function
It is important to point out that there exists something that is called qt_sql_default_connection
. Let’s assume we have two databases we want to use. The correct way to use them is specify each database with a different connection name. Furthermore, we have to specifcy the connection type - in our case "QSQLITE"
:
QSqlDatabase db_0 = QSqlDatabase::addDatabase("QSQLITE", "DB_0");
QSqlDatabase db_1 = QSqlDatabase::addDatabase("QSQLITE", "DB_1");
if we don’t specify a connection name, then Qt would set a default connection name which results in the following error(s):
QSqlDatabase db_0 = QSqlDatabase::addDatabase("QSQLITE");
db_0.setDatabaseName(db_0_filepath);
if (!db_0.open()) {
qDebug() << "Opening DB 0 failed - terminating";
} else {
qDebug() << "Opening DB 0 opened";
}
QSqlDatabase db_1 = QSqlDatabase::addDatabase("QSQLITE");
db_1.setDatabaseName(db_1_filepath);
if (!db_1.open()) {
qDebug() << "Opening DB 1 failed - terminating";
} else {
qDebug() << "Opening DB 1 opened";
}
Opening DB 0 opened
QSqlDatabasePrivate::removeDatabase: connection 'qt_sql_default_connection' is still in use, all queries will cease to work.
QSqlDatabasePrivate::addDatabase: duplicate connection name 'qt_sql_default_connection', old connection removed.
Opening DB 1 opened
Therefore, do not use any default connection. It makes debugging more challenging - especially if someone reading the source is not really familiar with all of Qt’s internals.
If we would perform any query, even if the query is initialized as QSqlQuery query_0(db_0);
, it will execute the command in db_1
and not db_0
because there seems to be some fallback mechanism to the “default connection name”.
Qt provides a class to handle queries and more importantly to bind values to queries. It is called QSqlQuery
. SQlQuery
can be used to execute any SQL command. Calling .exec()
returns whether or not command was executed successful. If we run a SELECT
command, then we’ll receive the the data and have to iterate over the query using next()
. Then the values with known positions are converted to the correct data format/type.
QString select_data(
"SELECT ID,NAME,SOME_BIN,SOME_REAL "
"FROM test_table");
query_0.clear();
query_0.prepare(select_data);
if (!query_0.exec()) {
qDebug() << query_0.lastError();
db_0.rollback();
} else {
while (query_0.next()) {
qDebug() << "ID: " << query_0.value(0).toInt();
qDebug() << "NAME: " << query_0.value(1).toString();
qDebug() << "SOME_BIN: " << query_0.value(2).toByteArray();
qDebug() << "SOME_REAL: " << query_0.value(3).toFloat();
}
}
Here is the full source code of the Qt example:
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <string>
#include <QtGlobal>
#include <QDebug>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlRecord>
#include <QSql>
#include <QString>
int main()
{
const char* home = getenv("HOME");
if (home == nullptr) {
qDebug() << "home direction not found - terminating";
return 1;
}
QString home_dir(home);
QString db_0_filepath = home_dir + "/db_0.sqlite3";
QString db_1_filepath = home_dir + "/db_1.sqlite3";
if (std::filesystem::exists(db_0_filepath.toStdString())) {
std::filesystem::remove(db_0_filepath.toStdString());
}
if (std::filesystem::exists(db_1_filepath.toStdString())) {
std::filesystem::remove(db_1_filepath.toStdString());
}
QSqlDatabase db_0 = QSqlDatabase::addDatabase("QSQLITE", "DB_0");
db_0.setDatabaseName(db_0_filepath);
if (!db_0.open()) {
qDebug() << "Opening DB 0 failed - terminating";
} else {
qDebug() << "Opening DB 0 opened";
}
QSqlDatabase db_1 = QSqlDatabase::addDatabase("QSQLITE", "DB_1");
db_1.setDatabaseName(db_1_filepath);
if (!db_1.open()) {
qDebug() << "Opening DB 1 failed - terminating";
} else {
qDebug() << "Opening DB 1 opened";
}
// create table in databases
QString table_gen(
"CREATE TABLE test_table("
"ID INTEGER PRIMARY KEY AUTOINCREMENT,"
"NAME TEXT NOT NULL,"
"SOME_BIN BINARY NOT NULL,"
"SOME_REAL REAL NOT NULL,"
"TS TIMESTAMP DEFAULT CURRENT_TIMESTAMP);");
QSqlQuery query_0(db_0);
query_0.prepare(table_gen);
if (!query_0.exec()) {
qDebug() << query_0.lastError();
db_0.rollback();
} else {
qDebug() << "Query exec successful on DB 0";
db_0.commit();
}
QSqlQuery query_1(db_1);
query_1.prepare(table_gen);
if (!query_1.exec()) {
qDebug() << query_1.lastError();
db_1.rollback();
} else {
qDebug() << "Query exec successful on DB 1";
db_1.commit();
}
const QByteArray binary_array_0 = QByteArray::fromHex("FEFF00FFA8");
const double some_number_0 = 3.14;
const QString some_name_0("Some name");
const QByteArray binary_array_1 = QByteArray::fromHex("AE0000FFA8");
const double some_number_1 = 42.01;
const QString some_name_1("Some other name");
QString insert_data(
"INSERT INTO test_table (NAME, SOME_BIN, SOME_REAL) "
"VALUES (:name, :somebin, :somereal);");
query_0.clear();
query_0.prepare(insert_data);
query_0.bindValue(":name", some_name_0);
query_0.bindValue(":somebin", binary_array_0);
query_0.bindValue(":somereal", some_number_0);
if (!query_0.exec()) {
qDebug() << query_0.lastError();
db_0.rollback();
} else {
qDebug() << "Query exec successful on DB 0";
db_0.commit();
}
query_1.clear();
query_1.prepare(insert_data);
query_1.bindValue(":name", some_name_1);
query_1.bindValue(":somebin", binary_array_1);
query_1.bindValue(":somereal", some_number_1);
if (!query_1.exec()) {
qDebug() << query_1.lastError();
db_1.rollback();
} else {
qDebug() << "Query exec successful on DB 1";
db_1.commit();
}
QString select_data(
"SELECT ID,NAME,SOME_BIN,SOME_REAL "
"FROM test_table");
query_0.clear();
query_0.prepare(select_data);
if (!query_0.exec()) {
qDebug() << query_0.lastError();
db_0.rollback();
} else {
while (query_0.next()) {
qDebug() << "ID: " << query_0.value(0).toInt();
qDebug() << "NAME: " << query_0.value(1).toString();
qDebug() << "SOME_BIN: " << query_0.value(2).toByteArray();
qDebug() << "SOME_REAL: " << query_0.value(3).toFloat();
}
}
db_0.close();
db_1.close();
return 0;
}
NB!: If float
is used instead of double
, then there are some rounding errors which change the number inserted into the SQLite3 database.