[Qt] Implement mapbox::sqlite::{Database,Statement} using QtSql

This commit is contained in:
Bruno de Oliveira Abinader
2017-01-05 12:09:30 -04:00
parent eb772258cf
commit 88b7fc3929
8 changed files with 444 additions and 22 deletions

View File

@@ -68,6 +68,7 @@ set(MBGL_TEST_FILES
test/storage/offline_download.test.cpp
test/storage/online_file_source.test.cpp
test/storage/resource.test.cpp
test/storage/sqlite.test.cpp
# style/conversion
test/style/conversion/function.test.cpp

View File

@@ -23,8 +23,6 @@ macro(mbgl_platform_core)
PRIVATE platform/qt/include
)
target_add_mason_package(mbgl-core PRIVATE sqlite)
target_link_libraries(mbgl-core
${MBGL_QT_LIBRARIES}
)

View File

@@ -25,7 +25,6 @@ set(MBGL_QT_FILES
PRIVATE platform/default/mbgl/storage/offline_database.hpp
PRIVATE platform/default/mbgl/storage/offline_download.cpp
PRIVATE platform/default/mbgl/storage/offline_download.hpp
PRIVATE platform/default/sqlite3.cpp
PRIVATE platform/default/sqlite3.hpp
# Misc
@@ -45,6 +44,7 @@ set(MBGL_QT_FILES
PRIVATE platform/qt/src/image.cpp
PRIVATE platform/qt/src/run_loop.cpp
PRIVATE platform/qt/src/run_loop_impl.hpp
PRIVATE platform/qt/src/sqlite3.cpp
PRIVATE platform/qt/src/string_stdlib.cpp
PRIVATE platform/qt/src/timer.cpp
PRIVATE platform/qt/src/timer_impl.hpp

View File

@@ -5,6 +5,7 @@ set(MBGL_QT_LIBRARIES
PRIVATE Qt4::QtGui
PRIVATE Qt4::QtNetwork
PRIVATE Qt4::QtOpenGL
PRIVATE Qt4::QtSql
)
target_link_libraries(qmapboxgl

View File

@@ -5,6 +5,7 @@ find_package(Qt5Network REQUIRED)
find_package(Qt5OpenGL REQUIRED)
find_package(Qt5Quick REQUIRED)
find_package(Qt5Widgets REQUIRED)
find_package(Qt5Sql REQUIRED)
set(MBGL_QT_LIBRARIES
PRIVATE Qt5::Core
@@ -13,6 +14,7 @@ set(MBGL_QT_LIBRARIES
PRIVATE Qt5::Network
PRIVATE Qt5::OpenGL
PRIVATE Qt5::Quick
PRIVATE Qt5::Sql
)
target_sources(qmapboxgl
@@ -31,6 +33,7 @@ target_link_libraries(qmapboxgl
PRIVATE Qt5::Location
PRIVATE Qt5::OpenGL
PRIVATE Qt5::Quick
PRIVATE Qt5::Sql
)
target_link_libraries(mbgl-qt

411
platform/qt/src/sqlite3.cpp Normal file
View File

@@ -0,0 +1,411 @@
#include "sqlite3.hpp"
#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QStringList>
#include <QVariant>
#include <cassert>
#include <cstring>
#include <cstdio>
#include <chrono>
#include <mbgl/util/chrono.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/optional.hpp>
#include <mbgl/util/string.hpp>
#include <mbgl/util/traits.hpp>
static uint32_t count = 0;
namespace mapbox {
namespace sqlite {
// https://www.sqlite.org/rescode.html#ok
static_assert(mbgl::underlying_type(Exception::OK) == 0, "error");
// https://www.sqlite.org/rescode.html#cantopen
static_assert(mbgl::underlying_type(Exception::CANTOPEN) == 14, "error");
// https://www.sqlite.org/rescode.html#notadb
static_assert(mbgl::underlying_type(Exception::NOTADB) == 26, "error");
void checkQueryError(const QSqlQuery& query) {
QSqlError lastError = query.lastError();
if (lastError.type() != QSqlError::NoError) {
#if QT_VERSION >= 0x050300
throw Exception { lastError.nativeErrorCode().toInt(), lastError.text().toStdString() };
#else
throw Exception { lastError.number(), lastError.text().toStdString() };
#endif
}
}
void checkDatabaseError(const QSqlDatabase &db) {
QSqlError lastError = db.lastError();
if (lastError.type() != QSqlError::NoError) {
#if QT_VERSION >= 0x050300
throw Exception { lastError.nativeErrorCode().toInt(), lastError.text().toStdString() };
#else
throw Exception { lastError.number(), lastError.text().toStdString() };
#endif
}
}
class DatabaseImpl {
public:
DatabaseImpl(const char* filename, int flags)
: db(QSqlDatabase::addDatabase("QSQLITE", QString::fromStdString(mbgl::util::toString(count++)))) {
QString connectOptions = db.connectOptions();
if (flags & OpenFlag::ReadOnly) {
if (!connectOptions.isEmpty()) connectOptions.append(';');
connectOptions.append("QSQLITE_OPEN_READONLY");
}
if (flags & OpenFlag::SharedCache) {
if (!connectOptions.isEmpty()) connectOptions.append(';');
connectOptions.append("QSQLITE_ENABLE_SHARED_CACHE");
}
db.setConnectOptions(connectOptions);
db.setDatabaseName(QString(filename));
if (!db.open()) {
checkDatabaseError(db);
}
}
~DatabaseImpl() {
db.close();
checkDatabaseError(db);
}
QSqlDatabase db;
};
class StatementImpl {
public:
StatementImpl(const QString& sql, const QSqlDatabase& db) : query(db) {
query.setForwardOnly(true);
if (!query.prepare(sql)) {
checkQueryError(query);
}
}
~StatementImpl() {
query.clear();
}
QSqlQuery query;
int64_t lastInsertRowId = 0;
int64_t changes = 0;
};
template <typename T>
using optional = std::experimental::optional<T>;
Database::Database(const std::string& file, int flags)
: impl(std::make_unique<DatabaseImpl>(file.c_str(), flags)) {
assert(impl);
}
Database::Database(Database &&other)
: impl(std::move(other.impl)) {
assert(impl);
}
Database &Database::operator=(Database &&other) {
std::swap(impl, other.impl);
assert(impl);
return *this;
}
Database::~Database() {
}
Database::operator bool() const {
return impl.operator bool();
}
void Database::setBusyTimeout(std::chrono::milliseconds timeout) {
assert(impl);
std::string timeoutStr = mbgl::util::toString(timeout.count());
QString connectOptions = impl->db.connectOptions();
if (connectOptions.isEmpty()) {
if (!connectOptions.isEmpty()) connectOptions.append(';');
connectOptions.append("QSQLITE_BUSY_TIMEOUT=").append(QString::fromStdString(timeoutStr));
}
if (impl->db.isOpen()) {
impl->db.close();
}
impl->db.setConnectOptions(connectOptions);
if (!impl->db.open()) {
checkDatabaseError(impl->db);
}
}
void Database::exec(const std::string &sql) {
assert(impl);
QStringList statements = QString::fromStdString(sql).split(';', QString::SkipEmptyParts);
statements.removeAll("\n");
for (QString statement : statements) {
if (!statement.endsWith(';')) {
statement.append(';');
}
QSqlQuery query(impl->db);
query.setForwardOnly(true);
query.prepare(statement);
if (!query.exec()) {
checkQueryError(query);
}
}
}
Statement Database::prepare(const char *query) {
return Statement(this, query);
}
Statement::Statement(Database *db, const char *sql)
: impl(std::make_unique<StatementImpl>(QString(sql), db->impl->db)) {
assert(impl);
}
Statement::Statement(Statement &&other)
: impl(std::move(other.impl)) {
assert(impl);
}
Statement &Statement::operator=(Statement &&other) {
assert(impl);
std::swap(impl, other.impl);
return *this;
}
Statement::~Statement() {
}
Statement::operator bool() const {
assert(impl);
return impl.operator bool();
}
template void Statement::bind(int, int8_t);
template void Statement::bind(int, int32_t);
template void Statement::bind(int, int64_t);
template void Statement::bind(int, uint8_t);
template void Statement::bind(int, bool);
template <typename T>
void Statement::bind(int offset, T value) {
assert(impl);
// Field numbering starts at 0.
impl->query.bindValue(offset - 1, QVariant::fromValue<T>(value), QSql::In);
checkQueryError(impl->query);
}
template <>
void Statement::bind(int offset, std::nullptr_t) {
assert(impl);
// Field numbering starts at 0.
impl->query.bindValue(offset - 1, QVariant(QVariant::Invalid), QSql::In);
checkQueryError(impl->query);
}
template <>
void Statement::bind(int offset, mbgl::Timestamp value) {
bind(offset, std::chrono::system_clock::to_time_t(value));
}
template <>
void Statement::bind(int offset, optional<std::string> value) {
if (value) {
bind(offset, *value);
} else {
bind(offset, nullptr);
}
}
template <>
void Statement::bind(int offset, optional<mbgl::Timestamp> value) {
if (value) {
bind(offset, *value);
} else {
bind(offset, nullptr);
}
}
void Statement::bind(int offset, const char* value, std::size_t length, bool /* retain */) {
assert(impl);
if (length > std::numeric_limits<int>::max()) {
// Kept for consistence with the default implementation.
throw std::range_error("value too long");
}
// Field numbering starts at 0.
impl->query.bindValue(offset - 1, QString::fromLatin1(value, length), QSql::In);
checkQueryError(impl->query);
}
void Statement::bind(int offset, const std::string& value, bool /* retain */) {
// Field numbering starts at 0.
impl->query.bindValue(offset - 1, QString::fromStdString(value), QSql::In);
checkQueryError(impl->query);
}
void Statement::bindBlob(int offset, const void* value, std::size_t length, bool /* retain */) {
assert(impl);
const char* value = reinterpret_cast<const char*>(value_);
// Field numbering starts at 0.
impl->query.bindValue(offset - 1, QByteArray(reinterpret_cast<const char*>(value), length), QSql::In | QSql::Binary);
checkQueryError(impl->query);
}
void Statement::bindBlob(int offset, const std::vector<uint8_t>& value, bool retain) {
bindBlob(offset, value.data(), value.size(), retain);
}
bool Statement::run() {
assert(impl);
if (impl->query.isValid()) {
return impl->query.next();
}
assert(!impl->query.isActive());
impl->query.setForwardOnly(true);
if (!impl->query.exec()) {
checkQueryError(impl->query);
}
impl->lastInsertRowId = impl->query.lastInsertId().value<int64_t>();
impl->changes = impl->query.numRowsAffected();
return impl->query.next();
}
template int Statement::get(int);
template int64_t Statement::get(int);
template double Statement::get(int);
template <typename T> T Statement::get(int offset) {
assert(impl && impl->query.isValid());
QVariant value = impl->query.value(offset);
checkQueryError(impl->query);
return value.value<T>();
}
template <> std::vector<uint8_t> Statement::get(int offset) {
assert(impl && impl->query.isValid());
QByteArray byteArray = impl->query.value(offset).toByteArray();
checkQueryError(impl->query);
std::vector<uint8_t> blob(byteArray.begin(), byteArray.end());
return blob;
}
template <> mbgl::Timestamp Statement::get(int offset) {
assert(impl && impl->query.isValid());
QVariant value = impl->query.value(offset);
checkQueryError(impl->query);
return std::chrono::time_point_cast<std::chrono::seconds>(
std::chrono::system_clock::from_time_t(value.value<std::time_t>()));
}
template <> optional<int64_t> Statement::get(int offset) {
assert(impl && impl->query.isValid());
QVariant value = impl->query.value(offset);
checkQueryError(impl->query);
if (value.isNull())
return {};
return { value.value<int64_t>() };
}
template <> optional<double> Statement::get(int offset) {
assert(impl && impl->query.isValid());
QVariant value = impl->query.value(offset);
checkQueryError(impl->query);
if (value.isNull())
return {};
return { value.value<double>() };
}
template <> std::string Statement::get(int offset) {
assert(impl && impl->query.isValid());
QByteArray value = impl->query.value(offset).toByteArray();
checkQueryError(impl->query);
return std::string(value.constData(), value.size());
}
template <> optional<std::string> Statement::get(int offset) {
assert(impl && impl->query.isValid());
QByteArray value = impl->query.value(offset).toByteArray();
checkQueryError(impl->query);
if (value.isNull())
return {};
return { std::string(value.constData(), value.size()) };
}
template <> optional<mbgl::Timestamp> Statement::get(int offset) {
assert(impl && impl->query.isValid());
QVariant value = impl->query.value(offset);
checkQueryError(impl->query);
if (value.isNull())
return {};
return { std::chrono::time_point_cast<mbgl::Seconds>(
std::chrono::system_clock::from_time_t(value.value<std::time_t>())) };
}
void Statement::reset() {
assert(impl);
impl->query.finish();
}
void Statement::clearBindings() {
// no-op
}
int64_t Statement::lastInsertRowId() const {
assert(impl);
return impl->lastInsertRowId;
}
uint64_t Statement::changes() const {
assert(impl);
return (impl->changes < 0 ? 0 : impl->changes);
}
Transaction::Transaction(Database& db_, Mode mode)
: db(db_) {
switch (mode) {
case Deferred:
db.exec("BEGIN DEFERRED TRANSACTION");
break;
case Immediate:
db.exec("BEGIN IMMEDIATE TRANSACTION");
break;
case Exclusive:
db.exec("BEGIN EXCLUSIVE TRANSACTION");
break;
}
}
Transaction::~Transaction() {
if (needRollback) {
try {
rollback();
} catch (...) {
// Ignore failed rollbacks in destructor.
}
}
}
void Transaction::commit() {
needRollback = false;
db.exec("COMMIT TRANSACTION");
}
void Transaction::rollback() {
needRollback = false;
db.exec("ROLLBACK TRANSACTION");
}
} // namespace sqlite
} // namespace mapbox

View File

@@ -80,25 +80,6 @@ private:
} // namespace
TEST(OfflineDatabase, Statement) {
using namespace mbgl;
mapbox::sqlite::Database db(":memory:", mapbox::sqlite::ReadWrite | mapbox::sqlite::Create);
db.exec("CREATE TABLE test (id INTEGER)");
mapbox::sqlite::Statement stmt1 = db.prepare("INSERT INTO test (id) VALUES (?1)");
stmt1.bind(1, 0);
stmt1.run();
ASSERT_EQ(stmt1.lastInsertRowId(), 1);
ASSERT_EQ(stmt1.changes(), 1);
mapbox::sqlite::Statement stmt2 = db.prepare("INSERT INTO test (id) VALUES (?1)");
stmt2.bind(1, 0);
stmt2.run();
ASSERT_EQ(stmt1.lastInsertRowId(), 1);
ASSERT_EQ(stmt2.lastInsertRowId(), 2);
ASSERT_EQ(stmt2.changes(), 1);
}
TEST(OfflineDatabase, TEST_REQUIRES_WRITE(Create)) {
using namespace mbgl;

View File

@@ -0,0 +1,27 @@
#include <mbgl/test/util.hpp>
#include <gtest/gtest.h>
#include <sqlite3.hpp>
TEST(SQLite, Statement) {
using namespace mbgl;
mapbox::sqlite::Database db(":memory:", mapbox::sqlite::Create | mapbox::sqlite::ReadWrite);
db.exec("CREATE TABLE test (id INTEGER);");
mapbox::sqlite::Statement stmt1 = db.prepare("INSERT INTO test (id) VALUES (?1);");
ASSERT_EQ(stmt1.lastInsertRowId(), 0);
ASSERT_EQ(stmt1.changes(), 0u);
stmt1.bind(1, 10);
stmt1.run();
ASSERT_EQ(stmt1.lastInsertRowId(), 1);
ASSERT_EQ(stmt1.changes(), 1u);
mapbox::sqlite::Statement stmt2 = db.prepare("INSERT INTO test (id) VALUES (?1);");
ASSERT_EQ(stmt2.lastInsertRowId(), 0);
ASSERT_EQ(stmt2.changes(), 0u);
stmt2.bind(1, 20);
stmt2.run();
ASSERT_EQ(stmt2.lastInsertRowId(), 2);
ASSERT_EQ(stmt2.changes(), 1u);
}