diff options
author | Bruno de Oliveira Abinader <bruno@mapbox.com> | 2017-01-05 12:09:30 -0400 |
---|---|---|
committer | Bruno de Oliveira Abinader <bruno@mapbox.com> | 2017-01-23 19:06:53 +0200 |
commit | 88b7fc3929acbe119b3dbfa2a9f6dbd04b338ebe (patch) | |
tree | a2e8c9e41630346f8030810a2e34ee602bd9cc9e | |
parent | eb772258cfa1af3a530ce9ff750c86aff20f96cf (diff) | |
download | qtlocation-mapboxgl-88b7fc3929acbe119b3dbfa2a9f6dbd04b338ebe.tar.gz |
[Qt] Implement mapbox::sqlite::{Database,Statement} using QtSql
-rw-r--r-- | cmake/test-files.cmake | 1 | ||||
-rw-r--r-- | platform/qt/config.cmake | 2 | ||||
-rw-r--r-- | platform/qt/qt.cmake | 2 | ||||
-rw-r--r-- | platform/qt/qt4.cmake | 1 | ||||
-rw-r--r-- | platform/qt/qt5.cmake | 3 | ||||
-rw-r--r-- | platform/qt/src/sqlite3.cpp | 411 | ||||
-rw-r--r-- | test/storage/offline_database.test.cpp | 19 | ||||
-rw-r--r-- | test/storage/sqlite.test.cpp | 27 |
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); +} |