summaryrefslogtreecommitdiff
path: root/platform/default/mbgl/storage/offline_database.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'platform/default/mbgl/storage/offline_database.cpp')
-rw-r--r--platform/default/mbgl/storage/offline_database.cpp1044
1 files changed, 612 insertions, 432 deletions
diff --git a/platform/default/mbgl/storage/offline_database.cpp b/platform/default/mbgl/storage/offline_database.cpp
index 6edd051c43..69768b1b0a 100644
--- a/platform/default/mbgl/storage/offline_database.cpp
+++ b/platform/default/mbgl/storage/offline_database.cpp
@@ -13,76 +13,72 @@ namespace mbgl {
OfflineDatabase::OfflineDatabase(std::string path_, uint64_t maximumCacheSize_)
: path(std::move(path_)),
maximumCacheSize(maximumCacheSize_) {
- ensureSchema();
+ initialize();
}
OfflineDatabase::~OfflineDatabase() {
- // Deleting these SQLite objects may result in exceptions, but we're in a destructor, so we
- // can't throw anything.
- try {
- statements.clear();
- db.reset();
- } catch (mapbox::sqlite::Exception& ex) {
- Log::Error(Event::Database, (int)ex.code, ex.what());
- }
+ closeDatabase();
}
-void OfflineDatabase::connect(int flags) {
- db = std::make_unique<mapbox::sqlite::Database>(path.c_str(), flags);
- db->setBusyTimeout(Milliseconds::max());
- db->exec("PRAGMA foreign_keys = ON");
+void OfflineDatabase::handleError(const mapbox::sqlite::Exception& ex) {
+ Log::Error(Event::Database, (int)ex.code, ex.what());
+
+ if (ex.code == mapbox::sqlite::ResultCode::Corrupt ||
+ ex.code == mapbox::sqlite::ResultCode::NotADB) {
+ removeExisting();
+ }
}
-void OfflineDatabase::ensureSchema() {
- if (path != ":memory:") {
+bool OfflineDatabase::initialize() {
+ for (uint32_t tries = 0; tries < 2; tries++) {
try {
- connect(mapbox::sqlite::ReadWrite);
-
- switch (userVersion()) {
- case 0: break; // cache-only database; ok to delete
- case 1: break; // cache-only database; ok to delete
- case 2: migrateToVersion3(); // fall through
- case 3: // no-op and fall through
- case 4: migrateToVersion5(); // fall through
- case 5: migrateToVersion6(); // fall through
- case 6: return;
- default: break; // downgrade, delete the database
- }
-
- removeExisting();
- connect(mapbox::sqlite::ReadWrite | mapbox::sqlite::Create);
+ openDatabase();
+ return true;
} catch (mapbox::sqlite::Exception& ex) {
- if (ex.code != mapbox::sqlite::ResultCode::CantOpen && ex.code != mapbox::sqlite::ResultCode::NotADB) {
- Log::Error(Event::Database, "Unexpected error connecting to database: %s", ex.what());
- throw;
- }
-
- try {
- if (ex.code == mapbox::sqlite::ResultCode::NotADB) {
- removeExisting();
- }
- connect(mapbox::sqlite::ReadWrite | mapbox::sqlite::Create);
- } catch (...) {
- Log::Error(Event::Database, "Unexpected error creating database: %s", util::toString(std::current_exception()).c_str());
- throw;
- }
+ handleError(ex);
+ closeDatabase();
}
}
+ return false;
+}
- try {
- #include "offline_schema.cpp.include"
+void OfflineDatabase::openDatabase() {
+ assert(!db);
+ db = std::make_unique<mapbox::sqlite::Database>(path.c_str(), mapbox::sqlite::ReadWriteCreate);
+ db->setBusyTimeout(Milliseconds::max());
+ db->exec("PRAGMA foreign_keys = ON");
- connect(mapbox::sqlite::ReadWrite | mapbox::sqlite::Create);
+ const auto version = userVersion();
+ switch (version) {
+ case 0: // New database, or a very old database that uses different tables.
+ createSchema();
+ break;
+
+ // Migrate existing database. We're using fall throughs.
+ case 2:
+ migrateToVersion3();
+ case 3:
+ case 4:
+ migrateToVersion5();
+ case 5:
+ migrateToVersion6();
+ case 6:
+ // Current database version
+ break;
+
+ // Delete unknown database version by treating it as a corrupt database.
+ default:
+ throw mapbox::sqlite::Exception{ mapbox::sqlite::ResultCode::Corrupt,
+ "Unknown database version " + util::toString(version) };
+ }
+}
- // If you change the schema you must write a migration from the previous version.
- db->exec("PRAGMA auto_vacuum = INCREMENTAL");
- db->exec("PRAGMA journal_mode = DELETE");
- db->exec("PRAGMA synchronous = FULL");
- db->exec(schema);
- db->exec("PRAGMA user_version = 6");
- } catch (...) {
- Log::Error(Event::Database, "Unexpected error creating database schema: %s", util::toString(std::current_exception()).c_str());
- throw;
+void OfflineDatabase::closeDatabase() {
+ if (db) {
+ statements.clear();
+ db.reset();
+ } else {
+ assert(statements.empty());
}
}
@@ -93,8 +89,7 @@ int OfflineDatabase::userVersion() {
void OfflineDatabase::removeExisting() {
Log::Warning(Event::Database, "Removing existing incompatible offline database");
- statements.clear();
- db.reset();
+ closeDatabase();
try {
util::deleteFile(path);
@@ -103,6 +98,19 @@ void OfflineDatabase::removeExisting() {
}
}
+void OfflineDatabase::createSchema() {
+ db->exec("PRAGMA auto_vacuum = INCREMENTAL");
+ db->exec("PRAGMA journal_mode = DELETE");
+ db->exec("PRAGMA synchronous = FULL");
+
+ #include "offline_schema.cpp.include"
+ mapbox::sqlite::Transaction transaction(*db);
+ db->exec(schema);
+ transaction.commit();
+
+ db->exec("PRAGMA user_version = 6");
+}
+
void OfflineDatabase::migrateToVersion3() {
db->exec("PRAGMA auto_vacuum = INCREMENTAL");
db->exec("VACUUM");
@@ -130,6 +138,7 @@ void OfflineDatabase::migrateToVersion6() {
}
mapbox::sqlite::Statement& OfflineDatabase::getStatement(const char* sql) {
+ assert(db);
auto it = statements.find(sql);
if (it == statements.end()) {
it = statements.emplace(sql, std::make_unique<mapbox::sqlite::Statement>(*db, sql)).first;
@@ -169,14 +178,14 @@ std::pair<bool, uint64_t> OfflineDatabase::putInternal(const Resource& resource,
return { false, 0 };
}
- std::string compressedData;
+ std::shared_ptr<const std::string> data;
bool compressed = false;
- uint64_t size = 0;
-
+ size_t size = 0;
if (response.data) {
- compressedData = util::compress(*response.data);
+ auto compressedData = util::compress(*response.data);
compressed = compressedData.size() < response.data->size();
- size = compressed ? compressedData.size() : response.data->size();
+ data = compressed ? std::make_shared<std::string>(std::move(compressedData)) : response.data;
+ size = data->size();
}
if (evict_ && !evict(size)) {
@@ -184,171 +193,209 @@ std::pair<bool, uint64_t> OfflineDatabase::putInternal(const Resource& resource,
return { false, 0 };
}
- bool inserted;
-
if (resource.kind == Resource::Kind::Tile) {
assert(resource.tileData);
- inserted = putTile(*resource.tileData, response,
- compressed ? compressedData : response.data ? *response.data : "",
- compressed);
+ return putTile(*resource.tileData, response, data, compressed);
} else {
- inserted = putResource(resource, response,
- compressed ? compressedData : response.data ? *response.data : "",
- compressed);
+ return putResource(resource, response, data, compressed);
}
-
- return { inserted, size };
}
optional<std::pair<Response, uint64_t>> OfflineDatabase::getResource(const Resource& resource) {
+ if (!db && !initialize()) {
+ return {};
+ }
+
// Update accessed timestamp used for LRU eviction.
- {
+ try {
mapbox::sqlite::Query accessedQuery{ getStatement("UPDATE resources SET accessed = ?1 WHERE url = ?2") };
accessedQuery.bind(1, util::now());
accessedQuery.bind(2, resource.url);
accessedQuery.run();
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ // Ignore access update failures so that we can still read from a database even if the disk
+ // is full (and we can't create a journal file), or when the file system is readonly.
+
+ // handleError() may close the database, so ensure that we try to reopen/recreate it before
+ // proceeding.
+ if (!db && !initialize()) {
+ return {};
+ }
}
- // clang-format off
- mapbox::sqlite::Query query{ getStatement(
- // 0 1 2 3 4 5
- "SELECT etag, expires, must_revalidate, modified, data, compressed "
- "FROM resources "
- "WHERE url = ?") };
- // clang-format on
+ try {
+ // clang-format off
+ mapbox::sqlite::Query query{ getStatement(
+ // 0 1 2 3 4 5
+ "SELECT etag, expires, must_revalidate, modified, data, compressed "
+ "FROM resources "
+ "WHERE url = ?") };
+ // clang-format on
- query.bind(1, resource.url);
+ query.bind(1, resource.url);
- if (!query.run()) {
- return {};
- }
-
- Response response;
- uint64_t size = 0;
+ if (!query.run()) {
+ return {};
+ }
- response.etag = query.get<optional<std::string>>(0);
- response.expires = query.get<optional<Timestamp>>(1);
- response.mustRevalidate = query.get<bool>(2);
- response.modified = query.get<optional<Timestamp>>(3);
+ Response response;
+ uint64_t size = 0;
+
+ response.etag = query.get<optional<std::string>>(0);
+ response.expires = query.get<optional<Timestamp>>(1);
+ response.mustRevalidate = query.get<bool>(2);
+ response.modified = query.get<optional<Timestamp>>(3);
+
+ auto data = query.get<optional<std::string>>(4);
+ if (!data) {
+ response.noContent = true;
+ } else if (query.get<bool>(5)) {
+ response.data = std::make_shared<std::string>(util::decompress(*data));
+ size = data->length();
+ } else {
+ response.data = std::make_shared<std::string>(*data);
+ size = data->length();
+ }
- auto data = query.get<optional<std::string>>(4);
- if (!data) {
- response.noContent = true;
- } else if (query.get<bool>(5)) {
- response.data = std::make_shared<std::string>(util::decompress(*data));
- size = data->length();
- } else {
- response.data = std::make_shared<std::string>(*data);
- size = data->length();
+ return std::make_pair(response, size);
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ return {};
}
-
- return std::make_pair(response, size);
}
optional<int64_t> OfflineDatabase::hasResource(const Resource& resource) {
- mapbox::sqlite::Query query{ getStatement("SELECT length(data) FROM resources WHERE url = ?") };
- query.bind(1, resource.url);
- if (!query.run()) {
+ if (!db && !initialize()) {
return {};
}
- return query.get<optional<int64_t>>(0);
+ try {
+ mapbox::sqlite::Query query{ getStatement("SELECT length(data) FROM resources WHERE url = ?") };
+ query.bind(1, resource.url);
+ if (query.run()) {
+ return query.get<optional<int64_t>>(0);
+ }
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ }
+
+ return {};
}
-bool OfflineDatabase::putResource(const Resource& resource,
- const Response& response,
- const std::string& data,
- bool compressed) {
+std::pair<bool, uint64_t> OfflineDatabase::putResource(const Resource& resource,
+ const Response& response,
+ const std::shared_ptr<const std::string>& data,
+ bool compressed) {
+ if (!db && !initialize()) {
+ return { false, 0 };
+ }
+
if (response.notModified) {
+ try {
+ // clang-format off
+ mapbox::sqlite::Query notModifiedQuery{ getStatement(
+ "UPDATE resources "
+ "SET accessed = ?1, "
+ " expires = ?2, "
+ " must_revalidate = ?3 "
+ "WHERE url = ?4 ") };
+ // clang-format on
+
+ notModifiedQuery.bind(1, util::now());
+ notModifiedQuery.bind(2, response.expires);
+ notModifiedQuery.bind(3, response.mustRevalidate);
+ notModifiedQuery.bind(4, resource.url);
+ notModifiedQuery.run();
+ return { false, data ? data->size() : 0 };
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ return { false, 0 };
+ }
+ }
+
+ try {
+ // We can't use REPLACE because it would change the id value.
+
+ // Begin an immediate-mode transaction to ensure that two writers do not attempt
+ // to INSERT a resource at the same moment.
+ assert(db);
+ mapbox::sqlite::Transaction transaction(*db, mapbox::sqlite::Transaction::Immediate);
+
// clang-format off
- mapbox::sqlite::Query notModifiedQuery{ getStatement(
+ mapbox::sqlite::Query updateQuery{ getStatement(
"UPDATE resources "
- "SET accessed = ?1, "
- " expires = ?2, "
- " must_revalidate = ?3 "
- "WHERE url = ?4 ") };
+ "SET kind = ?1, "
+ " etag = ?2, "
+ " expires = ?3, "
+ " must_revalidate = ?4, "
+ " modified = ?5, "
+ " accessed = ?6, "
+ " data = ?7, "
+ " compressed = ?8 "
+ "WHERE url = ?9 ") };
// clang-format on
- notModifiedQuery.bind(1, util::now());
- notModifiedQuery.bind(2, response.expires);
- notModifiedQuery.bind(3, response.mustRevalidate);
- notModifiedQuery.bind(4, resource.url);
- notModifiedQuery.run();
- return false;
- }
-
- // We can't use REPLACE because it would change the id value.
+ updateQuery.bind(1, int(resource.kind));
+ updateQuery.bind(2, response.etag);
+ updateQuery.bind(3, response.expires);
+ updateQuery.bind(4, response.mustRevalidate);
+ updateQuery.bind(5, response.modified);
+ updateQuery.bind(6, util::now());
+ updateQuery.bind(9, resource.url);
+
+ if (response.noContent || !data) {
+ updateQuery.bind(7, nullptr);
+ updateQuery.bind(8, false);
+ } else {
+ updateQuery.bindBlob(7, data->data(), data->size(), false);
+ updateQuery.bind(8, compressed);
+ }
- // Begin an immediate-mode transaction to ensure that two writers do not attempt
- // to INSERT a resource at the same moment.
- mapbox::sqlite::Transaction transaction(*db, mapbox::sqlite::Transaction::Immediate);
+ updateQuery.run();
+ if (updateQuery.changes() != 0) {
+ transaction.commit();
+ return { false, data ? data->size() : 0 };
+ }
- // clang-format off
- mapbox::sqlite::Query updateQuery{ getStatement(
- "UPDATE resources "
- "SET kind = ?1, "
- " etag = ?2, "
- " expires = ?3, "
- " must_revalidate = ?4, "
- " modified = ?5, "
- " accessed = ?6, "
- " data = ?7, "
- " compressed = ?8 "
- "WHERE url = ?9 ") };
- // clang-format on
+ // clang-format off
+ mapbox::sqlite::Query insertQuery{ getStatement(
+ "INSERT INTO resources (url, kind, etag, expires, must_revalidate, modified, accessed, data, compressed) "
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) ") };
+ // clang-format on
- updateQuery.bind(1, int(resource.kind));
- updateQuery.bind(2, response.etag);
- updateQuery.bind(3, response.expires);
- updateQuery.bind(4, response.mustRevalidate);
- updateQuery.bind(5, response.modified);
- updateQuery.bind(6, util::now());
- updateQuery.bind(9, resource.url);
-
- if (response.noContent) {
- updateQuery.bind(7, nullptr);
- updateQuery.bind(8, false);
- } else {
- updateQuery.bindBlob(7, data.data(), data.size(), false);
- updateQuery.bind(8, compressed);
- }
+ insertQuery.bind(1, resource.url);
+ insertQuery.bind(2, int(resource.kind));
+ insertQuery.bind(3, response.etag);
+ insertQuery.bind(4, response.expires);
+ insertQuery.bind(5, response.mustRevalidate);
+ insertQuery.bind(6, response.modified);
+ insertQuery.bind(7, util::now());
+
+ if (response.noContent || !data) {
+ insertQuery.bind(8, nullptr);
+ insertQuery.bind(9, false);
+ } else {
+ insertQuery.bindBlob(8, data->data(), data->size(), false);
+ insertQuery.bind(9, compressed);
+ }
- updateQuery.run();
- if (updateQuery.changes() != 0) {
+ insertQuery.run();
transaction.commit();
- return false;
- }
- // clang-format off
- mapbox::sqlite::Query insertQuery{ getStatement(
- "INSERT INTO resources (url, kind, etag, expires, must_revalidate, modified, accessed, data, compressed) "
- "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) ") };
- // clang-format on
-
- insertQuery.bind(1, resource.url);
- insertQuery.bind(2, int(resource.kind));
- insertQuery.bind(3, response.etag);
- insertQuery.bind(4, response.expires);
- insertQuery.bind(5, response.mustRevalidate);
- insertQuery.bind(6, response.modified);
- insertQuery.bind(7, util::now());
-
- if (response.noContent) {
- insertQuery.bind(8, nullptr);
- insertQuery.bind(9, false);
- } else {
- insertQuery.bindBlob(8, data.data(), data.size(), false);
- insertQuery.bind(9, compressed);
+ return { true, data ? data->size() : 0 };
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ return { false, 0 };
}
-
- insertQuery.run();
- transaction.commit();
-
- return true;
}
optional<std::pair<Response, uint64_t>> OfflineDatabase::getTile(const Resource::TileData& tile) {
- {
+ if (!db && !initialize()) {
+ return {};
+ }
+
+ try {
// clang-format off
mapbox::sqlite::Query accessedQuery{ getStatement(
"UPDATE tiles "
@@ -367,184 +414,222 @@ optional<std::pair<Response, uint64_t>> OfflineDatabase::getTile(const Resource:
accessedQuery.bind(5, tile.y);
accessedQuery.bind(6, tile.z);
accessedQuery.run();
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ // Ignore access update failures so that we can still read from a database even if the disk
+ // is full (and we can't create a journal file), or when the file system is readonly.
+
+ // handleError() may close the database, so ensure that we try to reopen/recreate it before
+ // proceeding.
+ if (!db && !initialize()) {
+ return {};
+ }
}
- // clang-format off
- mapbox::sqlite::Query query{ getStatement(
- // 0 1 2, 3, 4, 5
- "SELECT etag, expires, must_revalidate, modified, data, compressed "
- "FROM tiles "
- "WHERE url_template = ?1 "
- " AND pixel_ratio = ?2 "
- " AND x = ?3 "
- " AND y = ?4 "
- " AND z = ?5 ") };
- // clang-format on
-
- query.bind(1, tile.urlTemplate);
- query.bind(2, tile.pixelRatio);
- query.bind(3, tile.x);
- query.bind(4, tile.y);
- query.bind(5, tile.z);
+ try {
+ // clang-format off
+ mapbox::sqlite::Query query{ getStatement(
+ // 0 1 2, 3, 4, 5
+ "SELECT etag, expires, must_revalidate, modified, data, compressed "
+ "FROM tiles "
+ "WHERE url_template = ?1 "
+ " AND pixel_ratio = ?2 "
+ " AND x = ?3 "
+ " AND y = ?4 "
+ " AND z = ?5 ") };
+ // clang-format on
- if (!query.run()) {
- return {};
- }
+ query.bind(1, tile.urlTemplate);
+ query.bind(2, tile.pixelRatio);
+ query.bind(3, tile.x);
+ query.bind(4, tile.y);
+ query.bind(5, tile.z);
- Response response;
- uint64_t size = 0;
+ if (!query.run()) {
+ return {};
+ }
- response.etag = query.get<optional<std::string>>(0);
- response.expires = query.get<optional<Timestamp>>(1);
- response.mustRevalidate = query.get<bool>(2);
- response.modified = query.get<optional<Timestamp>>(3);
+ Response response;
+ uint64_t size = 0;
+
+ response.etag = query.get<optional<std::string>>(0);
+ response.expires = query.get<optional<Timestamp>>(1);
+ response.mustRevalidate = query.get<bool>(2);
+ response.modified = query.get<optional<Timestamp>>(3);
+
+ optional<std::string> data = query.get<optional<std::string>>(4);
+ if (!data) {
+ response.noContent = true;
+ } else if (query.get<bool>(5)) {
+ response.data = std::make_shared<std::string>(util::decompress(*data));
+ size = data->length();
+ } else {
+ response.data = std::make_shared<std::string>(*data);
+ size = data->length();
+ }
- optional<std::string> data = query.get<optional<std::string>>(4);
- if (!data) {
- response.noContent = true;
- } else if (query.get<bool>(5)) {
- response.data = std::make_shared<std::string>(util::decompress(*data));
- size = data->length();
- } else {
- response.data = std::make_shared<std::string>(*data);
- size = data->length();
+ return std::make_pair(response, size);
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ return {};
}
-
- return std::make_pair(response, size);
}
optional<int64_t> OfflineDatabase::hasTile(const Resource::TileData& tile) {
- // clang-format off
- mapbox::sqlite::Query size{ getStatement(
- "SELECT length(data) "
- "FROM tiles "
- "WHERE url_template = ?1 "
- " AND pixel_ratio = ?2 "
- " AND x = ?3 "
- " AND y = ?4 "
- " AND z = ?5 ") };
- // clang-format on
+ if (!db && !initialize()) {
+ return {};
+ }
+
+ try {
+ // clang-format off
+ mapbox::sqlite::Query size{ getStatement(
+ "SELECT length(data) "
+ "FROM tiles "
+ "WHERE url_template = ?1 "
+ " AND pixel_ratio = ?2 "
+ " AND x = ?3 "
+ " AND y = ?4 "
+ " AND z = ?5 ") };
+ // clang-format on
- size.bind(1, tile.urlTemplate);
- size.bind(2, tile.pixelRatio);
- size.bind(3, tile.x);
- size.bind(4, tile.y);
- size.bind(5, tile.z);
+ size.bind(1, tile.urlTemplate);
+ size.bind(2, tile.pixelRatio);
+ size.bind(3, tile.x);
+ size.bind(4, tile.y);
+ size.bind(5, tile.z);
- if (!size.run()) {
- return {};
+ if (size.run()) {
+ return size.get<optional<int64_t>>(0);
+ }
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
}
- return size.get<optional<int64_t>>(0);
+ return {};
}
-bool OfflineDatabase::putTile(const Resource::TileData& tile,
- const Response& response,
- const std::string& data,
- bool compressed) {
+std::pair<bool, uint64_t> OfflineDatabase::putTile(const Resource::TileData& tile,
+ const Response& response,
+ const std::shared_ptr<const std::string>& data,
+ bool compressed) {
+ if (!db && !initialize()) {
+ return { false, 0 };
+ }
+
if (response.notModified) {
+ try {
+ // clang-format off
+ mapbox::sqlite::Query notModifiedQuery{ getStatement(
+ "UPDATE tiles "
+ "SET accessed = ?1, "
+ " expires = ?2, "
+ " must_revalidate = ?3 "
+ "WHERE url_template = ?4 "
+ " AND pixel_ratio = ?5 "
+ " AND x = ?6 "
+ " AND y = ?7 "
+ " AND z = ?8 ") };
+ // clang-format on
+
+ notModifiedQuery.bind(1, util::now());
+ notModifiedQuery.bind(2, response.expires);
+ notModifiedQuery.bind(3, response.mustRevalidate);
+ notModifiedQuery.bind(4, tile.urlTemplate);
+ notModifiedQuery.bind(5, tile.pixelRatio);
+ notModifiedQuery.bind(6, tile.x);
+ notModifiedQuery.bind(7, tile.y);
+ notModifiedQuery.bind(8, tile.z);
+ notModifiedQuery.run();
+ return { false, data ? data->size() : 0 };
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ return { false, 0 };
+ }
+ }
+
+ try {
+ // We can't use REPLACE because it would change the id value.
+
+ // Begin an immediate-mode transaction to ensure that two writers do not attempt
+ // to INSERT a resource at the same moment.
+ assert(db);
+ mapbox::sqlite::Transaction transaction(*db, mapbox::sqlite::Transaction::Immediate);
+
// clang-format off
- mapbox::sqlite::Query notModifiedQuery{ getStatement(
+ mapbox::sqlite::Query updateQuery{ getStatement(
"UPDATE tiles "
- "SET accessed = ?1, "
- " expires = ?2, "
- " must_revalidate = ?3 "
- "WHERE url_template = ?4 "
- " AND pixel_ratio = ?5 "
- " AND x = ?6 "
- " AND y = ?7 "
- " AND z = ?8 ") };
+ "SET modified = ?1, "
+ " etag = ?2, "
+ " expires = ?3, "
+ " must_revalidate = ?4, "
+ " accessed = ?5, "
+ " data = ?6, "
+ " compressed = ?7 "
+ "WHERE url_template = ?8 "
+ " AND pixel_ratio = ?9 "
+ " AND x = ?10 "
+ " AND y = ?11 "
+ " AND z = ?12 ") };
// clang-format on
- notModifiedQuery.bind(1, util::now());
- notModifiedQuery.bind(2, response.expires);
- notModifiedQuery.bind(3, response.mustRevalidate);
- notModifiedQuery.bind(4, tile.urlTemplate);
- notModifiedQuery.bind(5, tile.pixelRatio);
- notModifiedQuery.bind(6, tile.x);
- notModifiedQuery.bind(7, tile.y);
- notModifiedQuery.bind(8, tile.z);
- notModifiedQuery.run();
- return false;
- }
-
- // We can't use REPLACE because it would change the id value.
+ updateQuery.bind(1, response.modified);
+ updateQuery.bind(2, response.etag);
+ updateQuery.bind(3, response.expires);
+ updateQuery.bind(4, response.mustRevalidate);
+ updateQuery.bind(5, util::now());
+ updateQuery.bind(8, tile.urlTemplate);
+ updateQuery.bind(9, tile.pixelRatio);
+ updateQuery.bind(10, tile.x);
+ updateQuery.bind(11, tile.y);
+ updateQuery.bind(12, tile.z);
+
+ if (response.noContent || !data) {
+ updateQuery.bind(6, nullptr);
+ updateQuery.bind(7, false);
+ } else {
+ updateQuery.bindBlob(6, data->data(), data->size(), false);
+ updateQuery.bind(7, compressed);
+ }
- // Begin an immediate-mode transaction to ensure that two writers do not attempt
- // to INSERT a resource at the same moment.
- mapbox::sqlite::Transaction transaction(*db, mapbox::sqlite::Transaction::Immediate);
+ updateQuery.run();
+ if (updateQuery.changes() != 0) {
+ transaction.commit();
+ return { false, data ? data->size() : 0 };
+ }
- // clang-format off
- mapbox::sqlite::Query updateQuery{ getStatement(
- "UPDATE tiles "
- "SET modified = ?1, "
- " etag = ?2, "
- " expires = ?3, "
- " must_revalidate = ?4, "
- " accessed = ?5, "
- " data = ?6, "
- " compressed = ?7 "
- "WHERE url_template = ?8 "
- " AND pixel_ratio = ?9 "
- " AND x = ?10 "
- " AND y = ?11 "
- " AND z = ?12 ") };
- // clang-format on
+ // clang-format off
+ mapbox::sqlite::Query insertQuery{ getStatement(
+ "INSERT INTO tiles (url_template, pixel_ratio, x, y, z, modified, must_revalidate, etag, expires, accessed, data, compressed) "
+ "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)") };
+ // clang-format on
- updateQuery.bind(1, response.modified);
- updateQuery.bind(2, response.etag);
- updateQuery.bind(3, response.expires);
- updateQuery.bind(4, response.mustRevalidate);
- updateQuery.bind(5, util::now());
- updateQuery.bind(8, tile.urlTemplate);
- updateQuery.bind(9, tile.pixelRatio);
- updateQuery.bind(10, tile.x);
- updateQuery.bind(11, tile.y);
- updateQuery.bind(12, tile.z);
-
- if (response.noContent) {
- updateQuery.bind(6, nullptr);
- updateQuery.bind(7, false);
- } else {
- updateQuery.bindBlob(6, data.data(), data.size(), false);
- updateQuery.bind(7, compressed);
- }
+ insertQuery.bind(1, tile.urlTemplate);
+ insertQuery.bind(2, tile.pixelRatio);
+ insertQuery.bind(3, tile.x);
+ insertQuery.bind(4, tile.y);
+ insertQuery.bind(5, tile.z);
+ insertQuery.bind(6, response.modified);
+ insertQuery.bind(7, response.mustRevalidate);
+ insertQuery.bind(8, response.etag);
+ insertQuery.bind(9, response.expires);
+ insertQuery.bind(10, util::now());
+
+ if (response.noContent || !data) {
+ insertQuery.bind(11, nullptr);
+ insertQuery.bind(12, false);
+ } else {
+ insertQuery.bindBlob(11, data->data(), data->size(), false);
+ insertQuery.bind(12, compressed);
+ }
- updateQuery.run();
- if (updateQuery.changes() != 0) {
+ insertQuery.run();
transaction.commit();
- return false;
- }
-
- // clang-format off
- mapbox::sqlite::Query insertQuery{ getStatement(
- "INSERT INTO tiles (url_template, pixel_ratio, x, y, z, modified, must_revalidate, etag, expires, accessed, data, compressed) "
- "VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)") };
- // clang-format on
- insertQuery.bind(1, tile.urlTemplate);
- insertQuery.bind(2, tile.pixelRatio);
- insertQuery.bind(3, tile.x);
- insertQuery.bind(4, tile.y);
- insertQuery.bind(5, tile.z);
- insertQuery.bind(6, response.modified);
- insertQuery.bind(7, response.mustRevalidate);
- insertQuery.bind(8, response.etag);
- insertQuery.bind(9, response.expires);
- insertQuery.bind(10, util::now());
-
- if (response.noContent) {
- insertQuery.bind(11, nullptr);
- insertQuery.bind(12, false);
- } else {
- insertQuery.bindBlob(11, data.data(), data.size(), false);
- insertQuery.bind(12, compressed);
+ return { true, data ? data->size() : 0 };
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ return { false, 0 };
}
-
- insertQuery.run();
- transaction.commit();
-
- return true;
}
std::vector<OfflineRegion> OfflineDatabase::listRegions() {
@@ -605,6 +690,10 @@ void OfflineDatabase::deleteRegion(OfflineRegion&& region) {
}
optional<std::pair<Response, uint64_t>> OfflineDatabase::getRegionResource(int64_t regionID, const Resource& resource) {
+ if (!db && !initialize()) {
+ return {};
+ }
+
auto response = getInternal(resource);
if (response) {
@@ -638,8 +727,22 @@ uint64_t OfflineDatabase::putRegionResource(int64_t regionID, const Resource& re
return size;
}
-bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
+bool OfflineDatabase::markUsed(int64_t regionID, const mbgl::Resource& resource) {
if (resource.kind == Resource::Kind::Tile) {
+ return markTileUsed(regionID, resource);
+ } else {
+ return markResourceUsed(regionID, resource);
+ }
+}
+
+bool OfflineDatabase::markTileUsed(int64_t regionID, const Resource& resource) {
+ assert(resource.kind == Resource::Kind::Tile);
+
+ if (!db && !initialize()) {
+ return false;
+ }
+
+ try {
// clang-format off
mapbox::sqlite::Query insertQuery{ getStatement(
"INSERT OR IGNORE INTO region_tiles (region_id, tile_id) "
@@ -664,7 +767,17 @@ bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
if (insertQuery.changes() == 0) {
return false;
}
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ // handleError() may close the database, so ensure that we try to reopen/recreate it
+ // before proceeding.
+ if (!db && !initialize()) {
+ return false;
+ }
+ }
+
+ try {
// clang-format off
mapbox::sqlite::Query selectQuery{ getStatement(
"SELECT region_id "
@@ -678,6 +791,7 @@ bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
"LIMIT 1 ") };
// clang-format on
+ const Resource::TileData& tile = *resource.tileData;
selectQuery.bind(1, regionID);
selectQuery.bind(2, tile.urlTemplate);
selectQuery.bind(3, tile.pixelRatio);
@@ -685,7 +799,20 @@ bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
selectQuery.bind(5, tile.y);
selectQuery.bind(6, tile.z);
return !selectQuery.run();
- } else {
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ return false;
+ }
+}
+
+bool OfflineDatabase::markResourceUsed(int64_t regionID, const Resource& resource) {
+ assert(resource.kind != Resource::Kind::Tile);
+
+ if (!db && !initialize()) {
+ return false;
+ }
+
+ try {
// clang-format off
mapbox::sqlite::Query insertQuery{ getStatement(
"INSERT OR IGNORE INTO region_resources (region_id, resource_id) "
@@ -701,7 +828,17 @@ bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
if (insertQuery.changes() == 0) {
return false;
}
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+
+ // handleError() may close the database, so ensure that we try to reopen/recreate it
+ // before proceeding.
+ if (!db && !initialize()) {
+ return false;
+ }
+ }
+ try {
// clang-format off
mapbox::sqlite::Query selectQuery{ getStatement(
"SELECT region_id "
@@ -714,15 +851,28 @@ bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
selectQuery.bind(1, regionID);
selectQuery.bind(2, resource.url);
return !selectQuery.run();
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ return false;
}
}
OfflineRegionDefinition OfflineDatabase::getRegionDefinition(int64_t regionID) {
- mapbox::sqlite::Query query{ getStatement("SELECT definition FROM regions WHERE id = ?1") };
- query.bind(1, regionID);
- query.run();
+ if (!db && !initialize()) {
+ throw std::runtime_error("Could not find offline region");
+ }
+
+ try {
+ mapbox::sqlite::Query query{ getStatement("SELECT definition FROM regions WHERE id = ?1") };
+ query.bind(1, regionID);
+ if (query.run()) {
+ return decodeOfflineRegionDefinition(query.get<std::string>(0));
+ }
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ }
- return decodeOfflineRegionDefinition(query.get<std::string>(0));
+ throw std::runtime_error("Could not find offline region");
}
OfflineRegionStatus OfflineDatabase::getRegionCompletedStatus(int64_t regionID) {
@@ -740,29 +890,50 @@ OfflineRegionStatus OfflineDatabase::getRegionCompletedStatus(int64_t regionID)
}
std::pair<int64_t, int64_t> OfflineDatabase::getCompletedResourceCountAndSize(int64_t regionID) {
- // clang-format off
- mapbox::sqlite::Query query{ getStatement(
- "SELECT COUNT(*), SUM(LENGTH(data)) "
- "FROM region_resources, resources "
- "WHERE region_id = ?1 "
- "AND resource_id = resources.id ") };
- // clang-format on
- query.bind(1, regionID);
- query.run();
- return { query.get<int64_t>(0), query.get<int64_t>(1) };
+ if (!db && !initialize()) {
+ return { 0, 0 };
+ }
+
+ try {
+ // clang-format off
+ mapbox::sqlite::Query query{ getStatement(
+ "SELECT COUNT(*), SUM(LENGTH(data)) "
+ "FROM region_resources, resources "
+ "WHERE region_id = ?1 "
+ "AND resource_id = resources.id ") };
+ // clang-format on
+ query.bind(1, regionID);
+ if (query.run()) {
+ return { query.get<int64_t>(0), query.get<int64_t>(1) };
+ }
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ }
+
+ return { 0, 0 };
}
std::pair<int64_t, int64_t> OfflineDatabase::getCompletedTileCountAndSize(int64_t regionID) {
- // clang-format off
- mapbox::sqlite::Query query{ getStatement(
- "SELECT COUNT(*), SUM(LENGTH(data)) "
- "FROM region_tiles, tiles "
- "WHERE region_id = ?1 "
- "AND tile_id = tiles.id ") };
- // clang-format on
- query.bind(1, regionID);
- query.run();
- return { query.get<int64_t>(0), query.get<int64_t>(1) };
+ if (!db && !initialize()) {
+ return { 0, 0 };
+ }
+
+ try {
+ // clang-format off
+ mapbox::sqlite::Query query{ getStatement(
+ "SELECT COUNT(*), SUM(LENGTH(data)) "
+ "FROM region_tiles, tiles "
+ "WHERE region_id = ?1 "
+ "AND tile_id = tiles.id ") };
+ // clang-format on
+ query.bind(1, regionID);
+ query.run();
+ return { query.get<int64_t>(0), query.get<int64_t>(1) };
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ }
+
+ return { 0, 0 };
}
template <class T>
@@ -783,80 +954,89 @@ T OfflineDatabase::getPragma(const char* sql) {
// delete an arbitrary number of old cache entries. The free pages approach saves
// us from calling VACCUM or keeping a running total, which can be costly.
bool OfflineDatabase::evict(uint64_t neededFreeSize) {
- uint64_t pageSize = getPragma<int64_t>("PRAGMA page_size");
- uint64_t pageCount = getPragma<int64_t>("PRAGMA page_count");
-
- auto usedSize = [&] {
- return pageSize * (pageCount - getPragma<int64_t>("PRAGMA freelist_count"));
- };
+ if (!db && !initialize()) {
+ return false;
+ }
- // The addition of pageSize is a fudge factor to account for non `data` column
- // size, and because pages can get fragmented on the database.
- while (usedSize() + neededFreeSize + pageSize > maximumCacheSize) {
- // clang-format off
- mapbox::sqlite::Query accessedQuery{ getStatement(
- "SELECT max(accessed) "
- "FROM ( "
- " SELECT accessed "
- " FROM resources "
- " LEFT JOIN region_resources "
- " ON resource_id = resources.id "
- " WHERE resource_id IS NULL "
- " UNION ALL "
- " SELECT accessed "
- " FROM tiles "
- " LEFT JOIN region_tiles "
- " ON tile_id = tiles.id "
- " WHERE tile_id IS NULL "
- " ORDER BY accessed ASC LIMIT ?1 "
- ") "
- ) };
- accessedQuery.bind(1, 50);
- // clang-format on
- if (!accessedQuery.run()) {
- return false;
+ try {
+ uint64_t pageSize = getPragma<int64_t>("PRAGMA page_size");
+ uint64_t pageCount = getPragma<int64_t>("PRAGMA page_count");
+
+ auto usedSize = [&] {
+ return pageSize * (pageCount - getPragma<int64_t>("PRAGMA freelist_count"));
+ };
+
+ // The addition of pageSize is a fudge factor to account for non `data` column
+ // size, and because pages can get fragmented on the database.
+ while (usedSize() + neededFreeSize + pageSize > maximumCacheSize) {
+ // clang-format off
+ mapbox::sqlite::Query accessedQuery{ getStatement(
+ "SELECT max(accessed) "
+ "FROM ( "
+ " SELECT accessed "
+ " FROM resources "
+ " LEFT JOIN region_resources "
+ " ON resource_id = resources.id "
+ " WHERE resource_id IS NULL "
+ " UNION ALL "
+ " SELECT accessed "
+ " FROM tiles "
+ " LEFT JOIN region_tiles "
+ " ON tile_id = tiles.id "
+ " WHERE tile_id IS NULL "
+ " ORDER BY accessed ASC LIMIT ?1 "
+ ") "
+ ) };
+ accessedQuery.bind(1, 50);
+ // clang-format on
+ if (!accessedQuery.run()) {
+ return false;
+ }
+ Timestamp accessed = accessedQuery.get<Timestamp>(0);
+
+ // clang-format off
+ mapbox::sqlite::Query resourceQuery{ getStatement(
+ "DELETE FROM resources "
+ "WHERE id IN ( "
+ " SELECT id FROM resources "
+ " LEFT JOIN region_resources "
+ " ON resource_id = resources.id "
+ " WHERE resource_id IS NULL "
+ " AND accessed <= ?1 "
+ ") ") };
+ // clang-format on
+ resourceQuery.bind(1, accessed);
+ resourceQuery.run();
+ const uint64_t resourceChanges = resourceQuery.changes();
+
+ // clang-format off
+ mapbox::sqlite::Query tileQuery{ getStatement(
+ "DELETE FROM tiles "
+ "WHERE id IN ( "
+ " SELECT id FROM tiles "
+ " LEFT JOIN region_tiles "
+ " ON tile_id = tiles.id "
+ " WHERE tile_id IS NULL "
+ " AND accessed <= ?1 "
+ ") ") };
+ // clang-format on
+ tileQuery.bind(1, accessed);
+ tileQuery.run();
+ const uint64_t tileChanges = tileQuery.changes();
+
+ // The cached value of offlineTileCount does not need to be updated
+ // here because only non-offline tiles can be removed by eviction.
+
+ if (resourceChanges == 0 && tileChanges == 0) {
+ return false;
+ }
}
- Timestamp accessed = accessedQuery.get<Timestamp>(0);
-
- // clang-format off
- mapbox::sqlite::Query resourceQuery{ getStatement(
- "DELETE FROM resources "
- "WHERE id IN ( "
- " SELECT id FROM resources "
- " LEFT JOIN region_resources "
- " ON resource_id = resources.id "
- " WHERE resource_id IS NULL "
- " AND accessed <= ?1 "
- ") ") };
- // clang-format on
- resourceQuery.bind(1, accessed);
- resourceQuery.run();
- const uint64_t resourceChanges = resourceQuery.changes();
-
- // clang-format off
- mapbox::sqlite::Query tileQuery{ getStatement(
- "DELETE FROM tiles "
- "WHERE id IN ( "
- " SELECT id FROM tiles "
- " LEFT JOIN region_tiles "
- " ON tile_id = tiles.id "
- " WHERE tile_id IS NULL "
- " AND accessed <= ?1 "
- ") ") };
- // clang-format on
- tileQuery.bind(1, accessed);
- tileQuery.run();
- const uint64_t tileChanges = tileQuery.changes();
- // The cached value of offlineTileCount does not need to be updated
- // here because only non-offline tiles can be removed by eviction.
-
- if (resourceChanges == 0 && tileChanges == 0) {
- return false;
- }
+ return true;
+ } catch (mapbox::sqlite::Exception& ex) {
+ handleError(ex);
+ return false;
}
-
- return true;
}
void OfflineDatabase::setOfflineMapboxTileCountLimit(uint64_t limit) {