#include #include #include #include #include #include #include #include "offline_schema.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()) { const auto& ex = result.get(); 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()); db = std::make_unique(std::move(result.get())); 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 { // 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::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(offlineDatabaseSchema); 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(getPragma("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(*db, sql)).first; } return *it->second; } optional OfflineDatabase::get(const Resource& resource) { auto result = getInternal(resource); return result ? result->first : optional(); } optional> OfflineDatabase::getInternal(const Resource& resource) { if (resource.kind == Resource::Kind::Tile) { assert(resource.tileData); return getTile(*resource.tileData); } else { return getResource(resource); } } optional OfflineDatabase::hasInternal(const Resource& resource) { if (resource.kind == Resource::Kind::Tile) { assert(resource.tileData); return hasTile(*resource.tileData); } else { return hasResource(resource); } } std::pair 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 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> 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>(0); response.expires = query.get>(1); response.mustRevalidate = query.get(2); response.modified = query.get>(3); auto data = query.get>(4); if (!data) { response.noContent = true; } else if (query.get(5)) { response.data = std::make_shared(util::decompress(*data)); size = data->length(); } else { response.data = std::make_shared(*data); size = data->length(); } return std::make_pair(response, size); } optional 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>(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> 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>(0); response.expires = query.get>(1); response.mustRevalidate = query.get(2); response.modified = query.get>(3); optional data = query.get>(4); if (!data) { response.noContent = true; } else if (query.get(5)) { response.data = std::make_shared(util::decompress(*data)); size = data->length(); } else { response.data = std::make_shared(*data); size = data->length(); } return std::make_pair(response, size); } optional 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>(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 OfflineDatabase::listRegions() { mapbox::sqlite::Query query{ getStatement("SELECT id, definition, description FROM regions") }; std::vector result; while (query.run()) { result.push_back(OfflineRegion( query.get(0), decodeOfflineRegionDefinition(query.get(1)), query.get>(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> OfflineDatabase::getRegionResource(int64_t regionID, const Resource& resource) { auto response = getInternal(resource); if (response) { markUsed(regionID, resource); } return response; } optional 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>& 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 (const 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(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 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(0), query.get(1) }; } std::pair 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(0), query.get(1) }; } template T OfflineDatabase::getPragma(const char* sql) { mapbox::sqlite::Query query{ getStatement(sql) }; query.run(); return query.get(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("PRAGMA page_size"); uint64_t pageCount = getPragma("PRAGMA page_count"); auto usedSize = [&] { return pageSize * (pageCount - getPragma("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(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(0); return *offlineMapboxTileCount; } bool OfflineDatabase::exceedsOfflineMapboxTileCountLimit(const Resource& resource) { return resource.kind == Resource::Kind::Tile && util::mapbox::isMapboxURL(resource.url) && offlineMapboxTileCountLimitExceeded(); } } // namespace mbgl