Files
MapLibreParticleLab/platform/default/mbgl/storage/offline_database.cpp
Konstantin Käfer 0ecdbebb09 [core] consolidate database opening workflow
Before this change, we've tried to open the database in read/write, but not create mode. In situations where the database didn't exist yet, this logged an error to the console, and we proceeded to opening it again in read/write/create mode, which actually created the file. The reason we did this is so that we could detect really old caching databases from January 2016 in case a developer upgraded from an older SDK (iOS v3.1 and earlier, Android v3.2 and earlier) that didn't have https://github.com/mapbox/mapbox-gl-native/pull/3715 yet.

However, these error messages, while innocent, look scary in the console and some users suspect that it's a bug. This change opens the file directly in read/write/create mode, omitting the first failed attempt. To handle old cache databases, we're now deleting the `http_cache` table, which was the only table in those old databases, and create the new schema, rather than deleting the entire file and recreating the Database object. In most scenarios, this will lead to one fewer opening attempt, while the database migration will continue to work for the few users who upgrade all the way from a January 2016 SDK.

Additionally, this fixes a mismatch between the Qt and non-Qt implementation: Qt doesn't support opening a file in read/write mode without the create flag. This means that we've seen a different control flow on Qt compared to the non-Qt implementation when opening a database.
2018-06-12 17:41:16 +02:00

954 lines
31 KiB
C++

#include <mbgl/storage/offline_database.hpp>
#include <mbgl/storage/response.hpp>
#include <mbgl/util/compression.hpp>
#include <mbgl/util/io.hpp>
#include <mbgl/util/string.hpp>
#include <mbgl/util/chrono.hpp>
#include <mbgl/util/logging.hpp>
#include "sqlite3.hpp"
namespace mbgl {
OfflineDatabase::OfflineDatabase(std::string path_, uint64_t maximumCacheSize_)
: path(std::move(path_)),
maximumCacheSize(maximumCacheSize_) {
ensureSchema();
}
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());
}
}
void OfflineDatabase::ensureSchema() {
auto result = mapbox::sqlite::Database::tryOpen(path, mapbox::sqlite::ReadWriteCreate);
if (result.is<mapbox::sqlite::Exception>()) {
const auto& ex = result.get<mapbox::sqlite::Exception>();
if (ex.code == mapbox::sqlite::ResultCode::NotADB) {
// Corrupted; blow it away.
removeExisting();
result = mapbox::sqlite::Database::open(path, mapbox::sqlite::ReadWriteCreate);
} else {
Log::Error(Event::Database, "Unexpected error connecting to database: %s", ex.what());
throw ex;
}
}
try {
assert(result.is<mapbox::sqlite::Database>());
db = std::make_unique<mapbox::sqlite::Database>(std::move(result.get<mapbox::sqlite::Database>()));
db->setBusyTimeout(Milliseconds::max());
db->exec("PRAGMA foreign_keys = ON");
switch (userVersion()) {
case 0:
case 1:
// Newly created database, or old cache-only database; remove old table if it exists.
removeOldCacheTable();
break;
case 2:
migrateToVersion3();
// fall through
case 3:
case 4:
migrateToVersion5();
// fall through
case 5:
migrateToVersion6();
// fall through
case 6:
// happy path; we're done
return;
default:
// downgrade, delete the database
removeExisting();
break;
}
} catch (const mapbox::sqlite::Exception& ex) {
// Unfortunately, SQLITE_NOTADB is not always reported upon opening the database.
// Apparently sometimes it is delayed until the first read operation.
if (ex.code == mapbox::sqlite::ResultCode::NotADB) {
removeExisting();
} else {
throw;
}
}
try {
#include "offline_schema.cpp.include"
// When downgrading the database, or when the database is corrupt, we've deleted the old database handle,
// so we need to reopen it.
if (!db) {
db = std::make_unique<mapbox::sqlite::Database>(mapbox::sqlite::Database::open(path, mapbox::sqlite::ReadWriteCreate));
db->setBusyTimeout(Milliseconds::max());
db->exec("PRAGMA foreign_keys = ON");
}
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;
}
}
int OfflineDatabase::userVersion() {
return static_cast<int>(getPragma<int64_t>("PRAGMA user_version"));
}
void OfflineDatabase::removeExisting() {
Log::Warning(Event::Database, "Removing existing incompatible offline database");
statements.clear();
db.reset();
try {
util::deleteFile(path);
} catch (util::IOException& ex) {
Log::Error(Event::Database, ex.code, ex.what());
}
}
void OfflineDatabase::removeOldCacheTable() {
db->exec("DROP TABLE IF EXISTS http_cache");
db->exec("VACUUM");
}
void OfflineDatabase::migrateToVersion3() {
db->exec("PRAGMA auto_vacuum = INCREMENTAL");
db->exec("VACUUM");
db->exec("PRAGMA user_version = 3");
}
// Schema version 4 was WAL journal + NORMAL sync. It was reverted during pre-
// release development and the migration was removed entirely to avoid potential
// conflicts from quickly (and needlessly) switching journal and sync modes.
//
// See: https://github.com/mapbox/mapbox-gl-native/pull/6320
void OfflineDatabase::migrateToVersion5() {
db->exec("PRAGMA journal_mode = DELETE");
db->exec("PRAGMA synchronous = FULL");
db->exec("PRAGMA user_version = 5");
}
void OfflineDatabase::migrateToVersion6() {
mapbox::sqlite::Transaction transaction(*db);
db->exec("ALTER TABLE resources ADD COLUMN must_revalidate INTEGER NOT NULL DEFAULT 0");
db->exec("ALTER TABLE tiles ADD COLUMN must_revalidate INTEGER NOT NULL DEFAULT 0");
db->exec("PRAGMA user_version = 6");
transaction.commit();
}
mapbox::sqlite::Statement& OfflineDatabase::getStatement(const char* sql) {
auto it = statements.find(sql);
if (it == statements.end()) {
it = statements.emplace(sql, std::make_unique<mapbox::sqlite::Statement>(*db, sql)).first;
}
return *it->second;
}
optional<Response> OfflineDatabase::get(const Resource& resource) {
auto result = getInternal(resource);
return result ? result->first : optional<Response>();
}
optional<std::pair<Response, uint64_t>> OfflineDatabase::getInternal(const Resource& resource) {
if (resource.kind == Resource::Kind::Tile) {
assert(resource.tileData);
return getTile(*resource.tileData);
} else {
return getResource(resource);
}
}
optional<int64_t> OfflineDatabase::hasInternal(const Resource& resource) {
if (resource.kind == Resource::Kind::Tile) {
assert(resource.tileData);
return hasTile(*resource.tileData);
} else {
return hasResource(resource);
}
}
std::pair<bool, uint64_t> OfflineDatabase::put(const Resource& resource, const Response& response) {
mapbox::sqlite::Transaction transaction(*db, mapbox::sqlite::Transaction::Immediate);
auto result = putInternal(resource, response, true);
transaction.commit();
return result;
}
std::pair<bool, uint64_t> OfflineDatabase::putInternal(const Resource& resource, const Response& response, bool evict_) {
if (response.error) {
return { false, 0 };
}
std::string compressedData;
bool compressed = false;
uint64_t size = 0;
if (response.data) {
compressedData = util::compress(*response.data);
compressed = compressedData.size() < response.data->size();
size = compressed ? compressedData.size() : response.data->size();
}
if (evict_ && !evict(size)) {
Log::Info(Event::Database, "Unable to make space for entry");
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);
} else {
inserted = putResource(resource, response,
compressed ? compressedData : response.data ? *response.data : "",
compressed);
}
return { inserted, size };
}
optional<std::pair<Response, uint64_t>> OfflineDatabase::getResource(const Resource& resource) {
// Update accessed timestamp used for LRU eviction.
{
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();
}
// 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);
if (!query.run()) {
return {};
}
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();
}
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()) {
return {};
}
return query.get<optional<int64_t>>(0);
}
bool OfflineDatabase::putResource(const Resource& resource,
const Response& response,
const std::string& data,
bool compressed) {
if (response.notModified) {
// 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;
}
// We can't use REPLACE because it would change the id value.
// 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
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);
}
updateQuery.run();
if (updateQuery.changes() != 0) {
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);
}
insertQuery.run();
return true;
}
optional<std::pair<Response, uint64_t>> OfflineDatabase::getTile(const Resource::TileData& tile) {
{
// clang-format off
mapbox::sqlite::Query accessedQuery{ getStatement(
"UPDATE tiles "
"SET accessed = ?1 "
"WHERE url_template = ?2 "
" AND pixel_ratio = ?3 "
" AND x = ?4 "
" AND y = ?5 "
" AND z = ?6 ") };
// clang-format on
accessedQuery.bind(1, util::now());
accessedQuery.bind(2, tile.urlTemplate);
accessedQuery.bind(3, tile.pixelRatio);
accessedQuery.bind(4, tile.x);
accessedQuery.bind(5, tile.y);
accessedQuery.bind(6, tile.z);
accessedQuery.run();
}
// 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);
if (!query.run()) {
return {};
}
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();
}
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
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 {};
}
return size.get<optional<int64_t>>(0);
}
bool OfflineDatabase::putTile(const Resource::TileData& tile,
const Response& response,
const std::string& data,
bool compressed) {
if (response.notModified) {
// 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;
}
// We can't use REPLACE because it would change the id value.
// 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
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);
}
updateQuery.run();
if (updateQuery.changes() != 0) {
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);
}
insertQuery.run();
return true;
}
std::vector<OfflineRegion> OfflineDatabase::listRegions() {
mapbox::sqlite::Query query{ getStatement("SELECT id, definition, description FROM regions") };
std::vector<OfflineRegion> result;
while (query.run()) {
result.push_back(OfflineRegion(
query.get<int64_t>(0),
decodeOfflineRegionDefinition(query.get<std::string>(1)),
query.get<std::vector<uint8_t>>(2)));
}
return result;
}
OfflineRegion OfflineDatabase::createRegion(const OfflineRegionDefinition& definition,
const OfflineRegionMetadata& metadata) {
// clang-format off
mapbox::sqlite::Query query{ getStatement(
"INSERT INTO regions (definition, description) "
"VALUES (?1, ?2) ") };
// clang-format on
query.bind(1, encodeOfflineRegionDefinition(definition));
query.bindBlob(2, metadata);
query.run();
return OfflineRegion(query.lastInsertRowId(), definition, metadata);
}
OfflineRegionMetadata OfflineDatabase::updateMetadata(const int64_t regionID, const OfflineRegionMetadata& metadata) {
// clang-format off
mapbox::sqlite::Query query{ getStatement(
"UPDATE regions SET description = ?1 "
"WHERE id = ?2") };
// clang-format on
query.bindBlob(1, metadata);
query.bind(2, regionID);
query.run();
return metadata;
}
void OfflineDatabase::deleteRegion(OfflineRegion&& region) {
{
mapbox::sqlite::Query query{ getStatement("DELETE FROM regions WHERE id = ?") };
query.bind(1, region.getID());
query.run();
}
evict(0);
db->exec("PRAGMA incremental_vacuum");
// Ensure that the cached offlineTileCount value is recalculated.
offlineMapboxTileCount = {};
}
optional<std::pair<Response, uint64_t>> OfflineDatabase::getRegionResource(int64_t regionID, const Resource& resource) {
auto response = getInternal(resource);
if (response) {
markUsed(regionID, resource);
}
return response;
}
optional<int64_t> OfflineDatabase::hasRegionResource(int64_t regionID, const Resource& resource) {
auto response = hasInternal(resource);
if (response) {
markUsed(regionID, resource);
}
return response;
}
uint64_t OfflineDatabase::putRegionResource(int64_t regionID, const Resource& resource, const Response& response) {
mapbox::sqlite::Transaction transaction(*db);
auto size = putRegionResourceInternal(regionID, resource, response);
transaction.commit();
return size;
}
void OfflineDatabase::putRegionResources(int64_t regionID, const std::list<std::tuple<Resource, Response>>& resources, OfflineRegionStatus& status) {
mapbox::sqlite::Transaction transaction(*db);
for (const auto& elem : resources) {
const auto& resource = std::get<0>(elem);
const auto& response = std::get<1>(elem);
try {
uint64_t resourceSize = putRegionResourceInternal(regionID, resource, response);
status.completedResourceCount++;
status.completedResourceSize += resourceSize;
if (resource.kind == Resource::Kind::Tile) {
status.completedTileCount += 1;
status.completedTileSize += resourceSize;
}
} catch (MapboxTileLimitExceededException) {
// Commit the rest of the batch and retrow
transaction.commit();
throw;
}
}
// Commit the completed batch
transaction.commit();
}
uint64_t OfflineDatabase::putRegionResourceInternal(int64_t regionID, const Resource& resource, const Response& response) {
if (exceedsOfflineMapboxTileCountLimit(resource)) {
throw MapboxTileLimitExceededException();
}
uint64_t size = putInternal(resource, response, false).second;
bool previouslyUnused = markUsed(regionID, resource);
if (offlineMapboxTileCount
&& resource.kind == Resource::Kind::Tile
&& util::mapbox::isMapboxURL(resource.url)
&& previouslyUnused) {
*offlineMapboxTileCount += 1;
}
return size;
}
bool OfflineDatabase::markUsed(int64_t regionID, const Resource& resource) {
if (resource.kind == Resource::Kind::Tile) {
// clang-format off
mapbox::sqlite::Query insertQuery{ getStatement(
"INSERT OR IGNORE INTO region_tiles (region_id, tile_id) "
"SELECT ?1, tiles.id "
"FROM tiles "
"WHERE url_template = ?2 "
" AND pixel_ratio = ?3 "
" AND x = ?4 "
" AND y = ?5 "
" AND z = ?6 ") };
// clang-format on
const Resource::TileData& tile = *resource.tileData;
insertQuery.bind(1, regionID);
insertQuery.bind(2, tile.urlTemplate);
insertQuery.bind(3, tile.pixelRatio);
insertQuery.bind(4, tile.x);
insertQuery.bind(5, tile.y);
insertQuery.bind(6, tile.z);
insertQuery.run();
if (insertQuery.changes() == 0) {
return false;
}
// clang-format off
mapbox::sqlite::Query selectQuery{ getStatement(
"SELECT region_id "
"FROM region_tiles, tiles "
"WHERE region_id != ?1 "
" AND url_template = ?2 "
" AND pixel_ratio = ?3 "
" AND x = ?4 "
" AND y = ?5 "
" AND z = ?6 "
"LIMIT 1 ") };
// clang-format on
selectQuery.bind(1, regionID);
selectQuery.bind(2, tile.urlTemplate);
selectQuery.bind(3, tile.pixelRatio);
selectQuery.bind(4, tile.x);
selectQuery.bind(5, tile.y);
selectQuery.bind(6, tile.z);
return !selectQuery.run();
} else {
// clang-format off
mapbox::sqlite::Query insertQuery{ getStatement(
"INSERT OR IGNORE INTO region_resources (region_id, resource_id) "
"SELECT ?1, resources.id "
"FROM resources "
"WHERE resources.url = ?2 ") };
// clang-format on
insertQuery.bind(1, regionID);
insertQuery.bind(2, resource.url);
insertQuery.run();
if (insertQuery.changes() == 0) {
return false;
}
// clang-format off
mapbox::sqlite::Query selectQuery{ getStatement(
"SELECT region_id "
"FROM region_resources, resources "
"WHERE region_id != ?1 "
" AND resources.url = ?2 "
"LIMIT 1 ") };
// clang-format on
selectQuery.bind(1, regionID);
selectQuery.bind(2, resource.url);
return !selectQuery.run();
}
}
OfflineRegionDefinition OfflineDatabase::getRegionDefinition(int64_t regionID) {
mapbox::sqlite::Query query{ getStatement("SELECT definition FROM regions WHERE id = ?1") };
query.bind(1, regionID);
query.run();
return decodeOfflineRegionDefinition(query.get<std::string>(0));
}
OfflineRegionStatus OfflineDatabase::getRegionCompletedStatus(int64_t regionID) {
OfflineRegionStatus result;
std::tie(result.completedResourceCount, result.completedResourceSize)
= getCompletedResourceCountAndSize(regionID);
std::tie(result.completedTileCount, result.completedTileSize)
= getCompletedTileCountAndSize(regionID);
result.completedResourceCount += result.completedTileCount;
result.completedResourceSize += result.completedTileSize;
return result;
}
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) };
}
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) };
}
template <class T>
T OfflineDatabase::getPragma(const char* sql) {
mapbox::sqlite::Query query{ getStatement(sql) };
query.run();
return query.get<T>(0);
}
// Remove least-recently used resources and tiles until the used database size,
// as calculated by multiplying the number of in-use pages by the page size, is
// less than the maximum cache size. Returns false if this condition cannot be
// satisfied.
//
// SQLite database never shrinks in size unless we call VACCUM. We here
// are monitoring the soft limit (i.e. number of free pages in the file)
// and as it approaches to the hard limit (i.e. the actual file size) we
// 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"));
};
// 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;
}
}
return true;
}
void OfflineDatabase::setOfflineMapboxTileCountLimit(uint64_t limit) {
offlineMapboxTileCountLimit = limit;
}
uint64_t OfflineDatabase::getOfflineMapboxTileCountLimit() {
return offlineMapboxTileCountLimit;
}
bool OfflineDatabase::offlineMapboxTileCountLimitExceeded() {
return getOfflineMapboxTileCount() >= offlineMapboxTileCountLimit;
}
uint64_t OfflineDatabase::getOfflineMapboxTileCount() {
// Calculating this on every call would be much simpler than caching and
// manually updating the value, but it would make offline downloads an O(n²)
// operation, because the database query below involves an index scan of
// region_tiles.
if (offlineMapboxTileCount) {
return *offlineMapboxTileCount;
}
// clang-format off
mapbox::sqlite::Query query{ getStatement(
"SELECT COUNT(DISTINCT id) "
"FROM region_tiles, tiles "
"WHERE tile_id = tiles.id "
"AND url_template LIKE 'mapbox://%' ") };
// clang-format on
query.run();
offlineMapboxTileCount = query.get<int64_t>(0);
return *offlineMapboxTileCount;
}
bool OfflineDatabase::exceedsOfflineMapboxTileCountLimit(const Resource& resource) {
return resource.kind == Resource::Kind::Tile
&& util::mapbox::isMapboxURL(resource.url)
&& offlineMapboxTileCountLimitExceeded();
}
} // namespace mbgl