summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruno de Oliveira Abinader <bruno@mapbox.com>2017-01-05 12:09:30 -0400
committerBruno de Oliveira Abinader <bruno@mapbox.com>2017-01-23 19:06:53 +0200
commit88b7fc3929acbe119b3dbfa2a9f6dbd04b338ebe (patch)
treea2e8c9e41630346f8030810a2e34ee602bd9cc9e
parenteb772258cfa1af3a530ce9ff750c86aff20f96cf (diff)
downloadqtlocation-mapboxgl-88b7fc3929acbe119b3dbfa2a9f6dbd04b338ebe.tar.gz
[Qt] Implement mapbox::sqlite::{Database,Statement} using QtSql
-rw-r--r--cmake/test-files.cmake1
-rw-r--r--platform/qt/config.cmake2
-rw-r--r--platform/qt/qt.cmake2
-rw-r--r--platform/qt/qt4.cmake1
-rw-r--r--platform/qt/qt5.cmake3
-rw-r--r--platform/qt/src/sqlite3.cpp411
-rw-r--r--test/storage/offline_database.test.cpp19
-rw-r--r--test/storage/sqlite.test.cpp27
8 files changed, 444 insertions, 22 deletions
diff --git a/cmake/test-files.cmake b/cmake/test-files.cmake
index 5d2b63d13f..76e0857142 100644
--- a/cmake/test-files.cmake
+++ b/cmake/test-files.cmake
@@ -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
diff --git a/platform/qt/config.cmake b/platform/qt/config.cmake
index e1c454c757..50e374174b 100644
--- a/platform/qt/config.cmake
+++ b/platform/qt/config.cmake
@@ -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}
)
diff --git a/platform/qt/qt.cmake b/platform/qt/qt.cmake
index bd7c863f98..32ffe89bfe 100644
--- a/platform/qt/qt.cmake
+++ b/platform/qt/qt.cmake
@@ -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
diff --git a/platform/qt/qt4.cmake b/platform/qt/qt4.cmake
index 45c299c8a8..d6d7d89417 100644
--- a/platform/qt/qt4.cmake
+++ b/platform/qt/qt4.cmake
@@ -5,6 +5,7 @@ set(MBGL_QT_LIBRARIES
PRIVATE Qt4::QtGui
PRIVATE Qt4::QtNetwork
PRIVATE Qt4::QtOpenGL
+ PRIVATE Qt4::QtSql
)
target_link_libraries(qmapboxgl
diff --git a/platform/qt/qt5.cmake b/platform/qt/qt5.cmake
index 47e178d132..9c3dde60cd 100644
--- a/platform/qt/qt5.cmake
+++ b/platform/qt/qt5.cmake
@@ -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
diff --git a/platform/qt/src/sqlite3.cpp b/platform/qt/src/sqlite3.cpp
new file mode 100644
index 0000000000..57725b8233
--- /dev/null
+++ b/platform/qt/src/sqlite3.cpp
@@ -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
diff --git a/test/storage/offline_database.test.cpp b/test/storage/offline_database.test.cpp
index 5e8da106da..2e25835d80 100644
--- a/test/storage/offline_database.test.cpp
+++ b/test/storage/offline_database.test.cpp
@@ -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;
diff --git a/test/storage/sqlite.test.cpp b/test/storage/sqlite.test.cpp
new file mode 100644
index 0000000000..dbd7a09868
--- /dev/null
+++ b/test/storage/sqlite.test.cpp
@@ -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);
+}