summaryrefslogtreecommitdiff
path: root/src/database/database.cpp
diff options
context:
space:
mode:
authorjacqueline <me@jacqueline.id.au>2024-05-02 19:12:26 +1000
committerjacqueline <me@jacqueline.id.au>2024-05-02 19:12:26 +1000
commit1573a8c4cde1cd9528b422b2dcc598e37ffe94a7 (patch)
treed162822b8fd7054f81bace0c7a65ab4d5e6f93ef /src/database/database.cpp
parenta231fd1c8afedbeb14b0bc77d76bad61db986059 (diff)
downloadtangara-fw-1573a8c4cde1cd9528b422b2dcc598e37ffe94a7.tar.gz
WIP merge cyclically dependent components into one big component
Diffstat (limited to 'src/database/database.cpp')
-rw-r--r--src/database/database.cpp820
1 files changed, 0 insertions, 820 deletions
diff --git a/src/database/database.cpp b/src/database/database.cpp
deleted file mode 100644
index 48fb0c63..00000000
--- a/src/database/database.cpp
+++ /dev/null
@@ -1,820 +0,0 @@
-/*
- * Copyright 2023 jacqueline <me@jacqueline.id.au>
- *
- * SPDX-License-Identifier: GPL-3.0-only
- */
-
-#include "database.hpp"
-
-#include <stdint.h>
-#include <sys/_stdint.h>
-
-#include <algorithm>
-#include <cstdint>
-#include <functional>
-#include <iomanip>
-#include <iostream>
-#include <memory>
-#include <optional>
-#include <sstream>
-#include <string>
-#include <variant>
-
-#include "collation.hpp"
-#include "cppbor.h"
-#include "cppbor_parse.h"
-#include "esp_log.h"
-#include "ff.h"
-#include "freertos/projdefs.h"
-#include "index.hpp"
-#include "komihash.h"
-#include "leveldb/cache.h"
-#include "leveldb/db.h"
-#include "leveldb/iterator.h"
-#include "leveldb/options.h"
-#include "leveldb/slice.h"
-#include "leveldb/status.h"
-#include "leveldb/write_batch.h"
-
-#include "db_events.hpp"
-#include "env_esp.hpp"
-#include "event_queue.hpp"
-#include "file_gatherer.hpp"
-#include "memory_resource.hpp"
-#include "records.hpp"
-#include "result.hpp"
-#include "spi.hpp"
-#include "tag_parser.hpp"
-#include "tasks.hpp"
-#include "track.hpp"
-
-namespace database {
-
-static SingletonEnv<leveldb::EspEnv> sEnv;
-[[maybe_unused]] static const char* kTag = "DB";
-
-static const char kDbPath[] = "/.tangara-db";
-
-static const char kKeyDbVersion[] = "schema_version";
-
-static const char kKeyCustom[] = "U\0";
-static const char kKeyCollator[] = "collator";
-static const char kKeyTrackId[] = "next_track_id";
-
-static std::atomic<bool> sIsDbOpen(false);
-
-static auto CreateNewDatabase(leveldb::Options& options, locale::ICollator& col)
- -> leveldb::DB* {
- Database::Destroy();
- leveldb::DB* db;
- options.create_if_missing = true;
- auto status = leveldb::DB::Open(options, kDbPath, &db);
- if (!status.ok()) {
- ESP_LOGE(kTag, "failed to open db, status %s", status.ToString().c_str());
- return nullptr;
- }
- auto version_str = std::to_string(kCurrentDbVersion);
- status = db->Put(leveldb::WriteOptions{}, kKeyDbVersion, version_str);
- if (!status.ok()) {
- delete db;
- return nullptr;
- }
- ESP_LOGI(kTag, "opening db with collator %s",
- col.Describe().value_or("NULL").c_str());
- status = db->Put(leveldb::WriteOptions{}, kKeyCollator,
- col.Describe().value_or(""));
- if (!status.ok()) {
- delete db;
- return nullptr;
- }
- return db;
-}
-
-static auto CheckDatabase(leveldb::DB& db, locale::ICollator& col) -> bool {
- leveldb::Status status;
-
- std::string raw_version;
- std::optional<uint8_t> version{};
- status = db.Get(leveldb::ReadOptions{}, kKeyDbVersion, &raw_version);
- if (status.ok()) {
- version = std::stoi(raw_version);
- }
- if (!version || *version != kCurrentDbVersion) {
- ESP_LOGW(kTag, "db version missing or incorrect");
- return false;
- }
-
- std::string collator;
- status = db.Get(leveldb::ReadOptions{}, kKeyCollator, &collator);
- if (!status.ok()) {
- ESP_LOGW(kTag, "db collator is unknown");
- return false;
- }
- auto needed = col.Describe();
-
- if ((needed && needed.value() != collator) ||
- (!needed && !collator.empty())) {
- ESP_LOGW(kTag, "db collator is mismatched");
- return false;
- }
-
- return true;
-}
-
-auto Database::Open(IFileGatherer& gatherer,
- ITagParser& parser,
- locale::ICollator& collator,
- tasks::WorkerPool& bg_worker)
- -> cpp::result<Database*, DatabaseError> {
- if (sIsDbOpen.exchange(true)) {
- return cpp::fail(DatabaseError::ALREADY_OPEN);
- }
-
- if (!leveldb::sBackgroundThread) {
- leveldb::sBackgroundThread = &bg_worker;
- }
-
- return bg_worker
- .Dispatch<cpp::result<Database*, DatabaseError>>(
- [&]() -> cpp::result<Database*, DatabaseError> {
- leveldb::DB* db;
- std::unique_ptr<leveldb::Cache> cache{
- leveldb::NewLRUCache(256 * 1024)};
-
- leveldb::Options options;
- options.env = sEnv.env();
- options.write_buffer_size = 4 * 1024;
- options.max_file_size = 16 * 1024;
- options.block_cache = cache.get();
- options.block_size = 2048;
-
- auto status = leveldb::DB::Open(options, kDbPath, &db);
- if (!status.ok()) {
- ESP_LOGI(kTag, "opening db failed. recreating.");
- db = CreateNewDatabase(options, collator);
- if (db == nullptr) {
- return cpp::fail(FAILED_TO_OPEN);
- }
- }
-
- if (!CheckDatabase(*db, collator)) {
- ESP_LOGI(kTag, "db incompatible. recreating.");
- delete db;
- db = CreateNewDatabase(options, collator);
- if (db == nullptr) {
- return cpp::fail(FAILED_TO_OPEN);
- }
- }
-
- ESP_LOGI(kTag, "Database opened successfully");
- return new Database(db, cache.release(), gatherer, parser,
- collator);
- })
- .get();
-}
-
-auto Database::Destroy() -> void {
- leveldb::Options options;
- options.env = sEnv.env();
- leveldb::DestroyDB(kDbPath, options);
-}
-
-Database::Database(leveldb::DB* db,
- leveldb::Cache* cache,
- IFileGatherer& file_gatherer,
- ITagParser& tag_parser,
- locale::ICollator& collator)
- : db_(db),
- cache_(cache),
- file_gatherer_(file_gatherer),
- tag_parser_(tag_parser),
- collator_(collator),
- is_updating_(false) {}
-
-Database::~Database() {
- // Delete db_ first so that any outstanding background work finishes before
- // the background task is killed.
- delete db_;
- delete cache_;
-
- sIsDbOpen.store(false);
-}
-
-auto Database::schemaVersion() -> std::string {
- // If the database is open, then it must have the current schema.
- return std::to_string(kCurrentDbVersion);
-}
-
-auto Database::sizeOnDiskBytes() -> size_t {
- auto lock = drivers::acquire_spi();
-
- FF_DIR dir;
- FRESULT res = f_opendir(&dir, kDbPath);
- if (res != FR_OK) {
- return 0;
- }
-
- size_t total_size = 0;
- for (;;) {
- FILINFO info;
- res = f_readdir(&dir, &info);
- if (res != FR_OK || info.fname[0] == 0) {
- break;
- }
- total_size += info.fsize;
- }
-
- return total_size;
-}
-
-auto Database::put(const std::string& key, const std::string& val) -> void {
- if (val.empty()) {
- db_->Delete(leveldb::WriteOptions{}, kKeyCustom + key);
- } else {
- db_->Put(leveldb::WriteOptions{}, kKeyCustom + key, val);
- }
-}
-
-auto Database::get(const std::string& key) -> std::optional<std::string> {
- std::string val;
- auto res = db_->Get(leveldb::ReadOptions{}, kKeyCustom + key, &val);
- if (!res.ok() || val.empty()) {
- return {};
- }
- return val;
-}
-
-auto Database::getTrackPath(TrackId id) -> std::optional<std::string> {
- auto track_data = dbGetTrackData(id);
- if (!track_data) {
- return {};
- }
- return std::string{track_data->filepath.data(), track_data->filepath.size()};
-}
-
-auto Database::getTrack(TrackId id) -> std::shared_ptr<Track> {
- std::shared_ptr<TrackData> data = dbGetTrackData(id);
- if (!data || data->is_tombstoned) {
- return {};
- }
- std::shared_ptr<TrackTags> tags = tag_parser_.ReadAndParseTags(
- {data->filepath.data(), data->filepath.size()});
- if (!tags) {
- return {};
- }
- return std::make_shared<Track>(data, tags);
-}
-
-auto Database::getIndexes() -> std::vector<IndexInfo> {
- // TODO(jacqueline): This probably needs to be async? When we have runtime
- // configurable indexes, they will need to come from somewhere.
- return {
- kAllTracks,
- kAllAlbums,
- kAlbumsByArtist,
- kTracksByGenre,
- };
-}
-
-class UpdateNotifier {
- public:
- UpdateNotifier(std::atomic<bool>& is_updating) : is_updating_(is_updating) {
- events::Ui().Dispatch(event::UpdateStarted{});
- events::System().Dispatch(event::UpdateStarted{});
- }
- ~UpdateNotifier() {
- is_updating_ = false;
- events::Ui().Dispatch(event::UpdateFinished{});
- events::System().Dispatch(event::UpdateFinished{});
- }
-
- private:
- std::atomic<bool>& is_updating_;
-};
-
-auto Database::updateIndexes() -> void {
- if (is_updating_.exchange(true)) {
- return;
- }
- UpdateNotifier notifier{is_updating_};
-
- leveldb::ReadOptions read_options;
- read_options.fill_cache = true;
-
- // Stage 1: verify all existing tracks are still valid.
- ESP_LOGI(kTag, "verifying existing tracks");
- {
- uint64_t num_processed = 0;
- std::unique_ptr<leveldb::Iterator> it{db_->NewIterator(read_options)};
- std::string prefix = EncodeDataPrefix();
- for (it->Seek(prefix); it->Valid() && it->key().starts_with(prefix);
- it->Next()) {
- num_processed++;
- events::Ui().Dispatch(event::UpdateProgress{
- .stage = event::UpdateProgress::Stage::kVerifyingExistingTracks,
- .val = num_processed,
- });
-
- std::shared_ptr<TrackData> track = ParseDataValue(it->value());
- if (!track) {
- // The value was malformed. Drop this record.
- ESP_LOGW(kTag, "dropping malformed metadata");
- db_->Delete(leveldb::WriteOptions(), it->key());
- continue;
- }
-
- if (track->is_tombstoned) {
- ESP_LOGW(kTag, "skipping tombstoned %lx", track->id);
- continue;
- }
-
- FRESULT res;
- FILINFO info;
- {
- auto lock = drivers::acquire_spi();
- res = f_stat(track->filepath.c_str(), &info);
- }
-
- std::pair<uint16_t, uint16_t> modified_at{0, 0};
- if (res == FR_OK) {
- modified_at = {info.fdate, info.ftime};
- }
- if (modified_at == track->modified_at) {
- continue;
- } else {
- track->modified_at = modified_at;
- }
-
- std::shared_ptr<TrackTags> tags = tag_parser_.ReadAndParseTags(
- {track->filepath.data(), track->filepath.size()});
- if (!tags || tags->encoding() == Container::kUnsupported) {
- // We couldn't read the tags for this track. Either they were
- // malformed, or perhaps the file is missing. Either way, tombstone
- // this record.
- ESP_LOGW(kTag, "entombing missing #%lx", track->id);
- dbRemoveIndexes(track);
- track->is_tombstoned = true;
- dbPutTrackData(*track);
- db_->Delete(leveldb::WriteOptions{}, EncodePathKey(track->filepath));
- continue;
- }
-
- // At this point, we know that the track still exists in its original
- // location. All that's left to do is update any metadata about it.
-
- uint64_t new_hash = tags->Hash();
- if (new_hash != track->tags_hash) {
- // This track's tags have changed. Since the filepath is exactly the
- // same, we assume this is a legitimate correction. Update the
- // database.
- ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash,
- new_hash);
- dbRemoveIndexes(track);
-
- track->tags_hash = new_hash;
- dbIngestTagHashes(*tags, track->individual_tag_hashes);
- dbPutTrackData(*track);
- dbPutHash(new_hash, track->id);
- }
- }
- }
-
- // Stage 2: search for newly added files.
- ESP_LOGI(kTag, "scanning for new tracks");
- uint64_t num_processed = 0;
- file_gatherer_.FindFiles("", [&](std::string_view path, const FILINFO& info) {
- num_processed++;
- events::Ui().Dispatch(event::UpdateProgress{
- .stage = event::UpdateProgress::Stage::kScanningForNewTracks,
- .val = num_processed,
- });
-
- std::string unused;
- if (db_->Get(read_options, EncodePathKey(path), &unused).ok()) {
- // This file is already in the database; skip it.
- return;
- }
-
- std::shared_ptr<TrackTags> tags = tag_parser_.ReadAndParseTags(path);
- if (!tags || tags->encoding() == Container::kUnsupported) {
- // No parseable tags; skip this fiile.
- return;
- }
-
- // Check for any existing record with the same hash.
- uint64_t hash = tags->Hash();
- std::string key = EncodeHashKey(hash);
- std::optional<TrackId> existing_hash;
- std::string raw_entry;
- if (db_->Get(leveldb::ReadOptions(), key, &raw_entry).ok()) {
- existing_hash = ParseHashValue(raw_entry);
- }
-
- std::pair<uint16_t, uint16_t> modified{info.fdate, info.ftime};
- if (!existing_hash) {
- // We've never met this track before! Or we have, but the entry is
- // malformed. Either way, record this as a new track.
- TrackId id = dbMintNewTrackId();
- ESP_LOGI(kTag, "recording new 0x%lx", id);
-
- auto data = std::make_shared<TrackData>();
- data->id = id;
- data->filepath = path;
- data->tags_hash = hash;
- data->modified_at = modified;
- dbIngestTagHashes(*tags, data->individual_tag_hashes);
-
- dbPutTrackData(*data);
- dbPutHash(hash, id);
- auto t = std::make_shared<Track>(data, tags);
- dbCreateIndexesForTrack(*t);
- db_->Put(leveldb::WriteOptions{}, EncodePathKey(path),
- TrackIdToBytes(id));
- return;
- }
-
- std::shared_ptr<TrackData> existing_data = dbGetTrackData(*existing_hash);
- if (!existing_data) {
- // We found a hash that matches, but there's no data record? Weird.
- auto new_data = std::make_shared<TrackData>();
- new_data->id = dbMintNewTrackId();
- new_data->filepath = path;
- new_data->tags_hash = hash;
- new_data->modified_at = modified;
- dbIngestTagHashes(*tags, new_data->individual_tag_hashes);
- dbPutTrackData(*new_data);
- auto t = std::make_shared<Track>(new_data, tags);
- dbCreateIndexesForTrack(*t);
- db_->Put(leveldb::WriteOptions{}, EncodePathKey(path),
- TrackIdToBytes(new_data->id));
- return;
- }
-
- if (existing_data->is_tombstoned) {
- ESP_LOGI(kTag, "exhuming track %lu", existing_data->id);
- existing_data->is_tombstoned = false;
- existing_data->modified_at = modified;
- dbPutTrackData(*existing_data);
- auto t = std::make_shared<Track>(existing_data, tags);
- dbCreateIndexesForTrack(*t);
- db_->Put(leveldb::WriteOptions{}, EncodePathKey(path),
- TrackIdToBytes(existing_data->id));
- } else if (existing_data->filepath !=
- std::pmr::string{path.data(), path.size()}) {
- ESP_LOGW(kTag, "hash collision: %s, %s, %s",
- tags->title().value_or("no title").c_str(),
- tags->artist().value_or("no artist").c_str(),
- tags->album().value_or("no album").c_str());
- }
- });
-}
-
-auto Database::isUpdating() -> bool {
- return is_updating_;
-}
-
-auto Database::dbMintNewTrackId() -> TrackId {
- TrackId next_id = 1;
- std::string val;
- auto status = db_->Get(leveldb::ReadOptions(), kKeyTrackId, &val);
- if (status.ok()) {
- next_id = BytesToTrackId(val).value_or(next_id);
- } else if (!status.IsNotFound()) {
- // TODO(jacqueline): Handle this more.
- ESP_LOGE(kTag, "failed to get next track id");
- }
-
- if (!db_->Put(leveldb::WriteOptions(), kKeyTrackId,
- TrackIdToBytes(next_id + 1))
- .ok()) {
- ESP_LOGE(kTag, "failed to write next track id");
- }
-
- return next_id;
-}
-
-auto Database::dbEntomb(TrackId id, uint64_t hash) -> void {
- std::string key = EncodeHashKey(hash);
- std::string val = EncodeHashValue(id);
- if (!db_->Put(leveldb::WriteOptions(), key, val).ok()) {
- ESP_LOGE(kTag, "failed to entomb #%llx (id #%lx)", hash, id);
- }
-}
-
-auto Database::dbPutTrackData(const TrackData& s) -> void {
- std::string key = EncodeDataKey(s.id);
- std::string val = EncodeDataValue(s);
- if (!db_->Put(leveldb::WriteOptions(), key, val).ok()) {
- ESP_LOGE(kTag, "failed to write data for #%lx", s.id);
- }
-}
-
-auto Database::dbGetTrackData(TrackId id) -> std::shared_ptr<TrackData> {
- std::string key = EncodeDataKey(id);
- std::string raw_val;
- if (!db_->Get(leveldb::ReadOptions(), key, &raw_val).ok()) {
- ESP_LOGW(kTag, "no key found for #%lx", id);
- return {};
- }
- return ParseDataValue(raw_val);
-}
-
-auto Database::dbPutHash(const uint64_t& hash, TrackId i) -> void {
- std::string key = EncodeHashKey(hash);
- std::string val = EncodeHashValue(i);
- if (!db_->Put(leveldb::WriteOptions(), key, val).ok()) {
- ESP_LOGE(kTag, "failed to write hash for #%lx", i);
- }
-}
-
-auto Database::dbGetHash(const uint64_t& hash) -> std::optional<TrackId> {
- std::string key = EncodeHashKey(hash);
- std::string raw_val;
- if (!db_->Get(leveldb::ReadOptions(), key, &raw_val).ok()) {
- ESP_LOGW(kTag, "no key found for hash #%llx", hash);
- return {};
- }
- return ParseHashValue(raw_val);
-}
-
-auto Database::dbCreateIndexesForTrack(const Track& track) -> void {
- for (const IndexInfo& index : getIndexes()) {
- leveldb::WriteBatch writes;
- auto entries = Index(collator_, index, track);
- for (const auto& it : entries) {
- writes.Put(EncodeIndexKey(it.first),
- {it.second.data(), it.second.size()});
- }
- db_->Write(leveldb::WriteOptions(), &writes);
- }
-}
-
-auto Database::dbRemoveIndexes(std::shared_ptr<TrackData> data) -> void {
- auto tags = dbRecoverTagsFromHashes(data->individual_tag_hashes);
- if (!tags) {
- return;
- }
- Track track{data, tags};
- for (const IndexInfo& index : getIndexes()) {
- auto entries = Index(collator_, index, track);
- for (auto it = entries.rbegin(); it != entries.rend(); it++) {
- auto key = EncodeIndexKey(it->first);
- auto status = db_->Delete(leveldb::WriteOptions{}, key);
- if (!status.ok()) {
- return;
- }
-
- std::unique_ptr<leveldb::Iterator> cursor{db_->NewIterator({})};
- cursor->Seek(key);
- cursor->Prev();
-
- auto prev_key = ParseIndexKey(cursor->key());
- if (prev_key && prev_key->header == it->first.header) {
- break;
- }
-
- cursor->Next();
- auto next_key = ParseIndexKey(cursor->key());
- if (next_key && next_key->header == it->first.header) {
- break;
- }
- }
- }
-}
-
-auto Database::dbIngestTagHashes(const TrackTags& tags,
- std::pmr::unordered_map<Tag, uint64_t>& out)
- -> void {
- leveldb::WriteBatch batch{};
- for (const auto& tag : tags.allPresent()) {
- auto val = tags.get(tag);
- auto hash = tagHash(val);
- batch.Put(EncodeTagHashKey(hash), tagToString(val));
- out[tag] = hash;
- }
- db_->Write(leveldb::WriteOptions{}, &batch);
-}
-
-auto Database::dbRecoverTagsFromHashes(
- const std::pmr::unordered_map<Tag, uint64_t>& hashes)
- -> std::shared_ptr<TrackTags> {
- auto out = std::make_shared<TrackTags>();
- for (const auto& entry : hashes) {
- std::string value;
- auto res = db_->Get(leveldb::ReadOptions{}, EncodeTagHashKey(entry.second),
- &value);
- if (!res.ok()) {
- ESP_LOGI(kTag, "failed to retrieve tag!");
- continue;
- }
- out->set(entry.first, {value.data(), value.size()});
- }
- return out;
-}
-
-auto seekToOffset(leveldb::Iterator* it, int offset) {
- while (it->Valid() && offset != 0) {
- if (offset < 0) {
- it->Prev();
- offset++;
- } else {
- it->Next();
- offset--;
- }
- }
-}
-
-auto Database::getRecord(const SearchKey& c)
- -> std::optional<std::pair<std::pmr::string, Record>> {
- std::unique_ptr<leveldb::Iterator> it{
- db_->NewIterator(leveldb::ReadOptions{})};
-
- it->Seek(c.startKey());
- seekToOffset(it.get(), c.offset);
- if (!it->Valid() || !it->key().starts_with(std::string_view{c.prefix})) {
- return {};
- }
-
- std::optional<IndexKey> key = ParseIndexKey(it->key());
- if (!key) {
- ESP_LOGW(kTag, "parsing index key failed");
- return {};
- }
-
- return std::make_pair(std::pmr::string{it->key().data(), it->key().size(),
- &memory::kSpiRamResource},
- Record{*key, it->value()});
-}
-
-auto Database::countRecords(const SearchKey& c) -> size_t {
- std::unique_ptr<leveldb::Iterator> it{
- db_->NewIterator(leveldb::ReadOptions{})};
-
- it->Seek(c.startKey());
- seekToOffset(it.get(), c.offset);
- if (!it->Valid() || !it->key().starts_with(std::string_view{c.prefix})) {
- return {};
- }
-
- size_t count = 0;
- while (it->Valid() && it->key().starts_with(std::string_view{c.prefix})) {
- it->Next();
- count++;
- }
-
- return count;
-}
-
-auto SearchKey::startKey() const -> std::string_view {
- if (key) {
- return *key;
- }
- return prefix;
-}
-
-Record::Record(const IndexKey& key, const leveldb::Slice& t)
- : text_(t.data(), t.size(), &memory::kSpiRamResource) {
- if (key.track) {
- contents_ = *key.track;
- } else {
- contents_ = ExpandHeader(key.header, key.item);
- }
-}
-
-auto Record::text() const -> std::string_view {
- return text_;
-}
-
-auto Record::contents() const
- -> const std::variant<TrackId, IndexKey::Header>& {
- return contents_;
-}
-
-Iterator::Iterator(std::shared_ptr<Database> db, IndexId idx)
- : Iterator(db,
- IndexKey::Header{
- .id = idx,
- .depth = 0,
- .components_hash = 0,
- }) {}
-
-Iterator::Iterator(std::shared_ptr<Database> db, const IndexKey::Header& header)
- : db_(db), key_{}, current_() {
- std::string prefix = EncodeIndexPrefix(header);
- key_ = {
- .prefix = {prefix.data(), prefix.size(), &memory::kSpiRamResource},
- .key = {},
- .offset = 0,
- };
- iterate(key_);
-}
-
-auto Iterator::value() const -> const std::optional<Record>& {
- return current_;
-}
-
-auto Iterator::next() -> void {
- SearchKey new_key = key_;
- new_key.offset = 1;
- iterate(new_key);
-}
-
-auto Iterator::prev() -> void {
- SearchKey new_key = key_;
- new_key.offset = -1;
- iterate(new_key);
-}
-
-auto Iterator::iterate(const SearchKey& key) -> void {
- auto db = db_.lock();
- if (!db) {
- ESP_LOGW(kTag, "iterate with dead db");
- return;
- }
- auto res = db->getRecord(key);
- if (res) {
- key_ = {
- .prefix = key_.prefix,
- .key = res->first,
- .offset = 0,
- };
- current_ = res->second;
- } else {
- key_ = key;
- current_.reset();
- }
-}
-
-auto Iterator::count() const -> size_t {
- auto db = db_.lock();
- if (!db) {
- ESP_LOGW(kTag, "count with dead db");
- return 0;
- }
- return db->countRecords(key_);
-}
-
-TrackIterator::TrackIterator(const Iterator& it) : db_(it.db_), levels_() {
- levels_.push_back(it);
- next(false);
-}
-
-auto TrackIterator::next() -> void {
- next(true);
-}
-
-auto TrackIterator::next(bool advance) -> void {
- while (!levels_.empty()) {
- if (advance) {
- levels_.back().next();
- }
-
- auto& cur = levels_.back().value();
- if (!cur) {
- // The current top iterator is out of tracks. Pop it, and move the parent
- // to the next item.
- levels_.pop_back();
- advance = true;
- } else if (std::holds_alternative<IndexKey::Header>(cur->contents())) {
- // This record is a branch. Push a new iterator.
- auto key = std::get<IndexKey::Header>(cur->contents());
- auto db = db_.lock();
- if (!db) {
- return;
- }
- levels_.emplace_back(db, key);
- // Don't skip the first value of the new level.
- advance = false;
- } else if (std::holds_alternative<TrackId>(cur->contents())) {
- // New record is a leaf.
- break;
- }
- }
-}
-
-auto TrackIterator::value() const -> std::optional<TrackId> {
- if (levels_.empty()) {
- return {};
- }
- auto cur = levels_.back().value();
- if (!cur) {
- return {};
- }
- if (std::holds_alternative<TrackId>(cur->contents())) {
- return std::get<TrackId>(cur->contents());
- }
- return {};
-}
-
-auto TrackIterator::count() const -> size_t {
- size_t size = 0;
- TrackIterator copy{*this};
- while (!copy.levels_.empty()) {
- size += copy.levels_.back().count();
- copy.levels_.pop_back();
- copy.next();
- }
- return size;
-}
-
-} // namespace database