|  |  | @ -7,6 +7,7 @@ | 
			
		
	
		
		
			
				
					
					|  |  |  | #include "database/database.hpp" |  |  |  | #include "database/database.hpp" | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | #include <bits/ranges_algo.h> |  |  |  | #include <bits/ranges_algo.h> | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | #include <stdint.h> | 
			
		
	
		
		
			
				
					
					|  |  |  | #include <algorithm> |  |  |  | #include <algorithm> | 
			
		
	
		
		
			
				
					
					|  |  |  | #include <cctype> |  |  |  | #include <cctype> | 
			
		
	
		
		
			
				
					
					|  |  |  | #include <cstdint> |  |  |  | #include <cstdint> | 
			
		
	
	
		
		
			
				
					|  |  | @ -21,6 +22,7 @@ | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | #include "cppbor.h" |  |  |  | #include "cppbor.h" | 
			
		
	
		
		
			
				
					
					|  |  |  | #include "cppbor_parse.h" |  |  |  | #include "cppbor_parse.h" | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | #include "debug.hpp" | 
			
		
	
		
		
			
				
					
					|  |  |  | #include "esp_log.h" |  |  |  | #include "esp_log.h" | 
			
		
	
		
		
			
				
					
					|  |  |  | #include "esp_timer.h" |  |  |  | #include "esp_timer.h" | 
			
		
	
		
		
			
				
					
					|  |  |  | #include "ff.h" |  |  |  | #include "ff.h" | 
			
		
	
	
		
		
			
				
					|  |  | @ -66,8 +68,8 @@ static std::atomic<bool> sIsDbOpen(false); | 
			
		
	
		
		
			
				
					
					|  |  |  | using std::placeholders::_1; |  |  |  | using std::placeholders::_1; | 
			
		
	
		
		
			
				
					
					|  |  |  | using std::placeholders::_2; |  |  |  | using std::placeholders::_2; | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | static auto CreateNewDatabase(leveldb::Options& options, locale::ICollator& col) |  |  |  | static auto CreateNewDatabase(leveldb::Options& options, | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     -> leveldb::DB* { |  |  |  |                               locale::ICollator& col) -> leveldb::DB* { | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   Database::Destroy(); |  |  |  |   Database::Destroy(); | 
			
		
	
		
		
			
				
					
					|  |  |  |   leveldb::DB* db; |  |  |  |   leveldb::DB* db; | 
			
		
	
		
		
			
				
					
					|  |  |  |   options.create_if_missing = true; |  |  |  |   options.create_if_missing = true; | 
			
		
	
	
		
		
			
				
					|  |  | @ -348,6 +350,8 @@ auto Database::updateIndexes() -> void { | 
			
		
	
		
		
			
				
					
					|  |  |  |   } |  |  |  |   } | 
			
		
	
		
		
			
				
					
					|  |  |  |   update_tracker_ = std::make_unique<UpdateTracker>(); |  |  |  |   update_tracker_ = std::make_unique<UpdateTracker>(); | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |   tag_parser_.ClearCaches(); | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   leveldb::ReadOptions read_options; |  |  |  |   leveldb::ReadOptions read_options; | 
			
		
	
		
		
			
				
					
					|  |  |  |   read_options.fill_cache = false; |  |  |  |   read_options.fill_cache = false; | 
			
		
	
		
		
			
				
					
					|  |  |  |   read_options.verify_checksums = true; |  |  |  |   read_options.verify_checksums = true; | 
			
		
	
	
		
		
			
				
					|  |  | @ -373,21 +377,24 @@ auto Database::updateIndexes() -> void { | 
			
		
	
		
		
			
				
					
					|  |  |  |         continue; |  |  |  |         continue; | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       std::shared_ptr<TrackTags> tags; | 
			
		
	
		
		
			
				
					
					|  |  |  |       FILINFO info; |  |  |  |       FILINFO info; | 
			
		
	
		
		
			
				
					
					|  |  |  |       FRESULT res = f_stat(track->filepath.c_str(), &info); |  |  |  |       FRESULT res = f_stat(track->filepath.c_str(), &info); | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       std::pair<uint16_t, uint16_t> modified_at{0, 0}; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (res == FR_OK) { |  |  |  |       if (res == FR_OK) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         modified_at = {info.fdate, info.ftime}; |  |  |  |         std::pair<uint16_t, uint16_t> modified_at{info.fdate, info.ftime}; | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |         if (modified_at == track->modified_at) { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       if (modified_at == track->modified_at) { |  |  |  |           continue; | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         continue; |  |  |  |         } else { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       } else { |  |  |  |           // Will be written out later; we make sure we update the track's
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         track->modified_at = modified_at; |  |  |  |           // modification time and tags in the same batch so that interrupted
 | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           // reindexes don't cause a big mess.
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           track->modified_at = modified_at; | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         tags = tag_parser_.ReadAndParseTags( | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |             {track->filepath.data(), track->filepath.size()}); | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       std::shared_ptr<TrackTags> tags = tag_parser_.ReadAndParseTags( |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |           {track->filepath.data(), track->filepath.size()}); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (!tags || tags->encoding() == Container::kUnsupported) { |  |  |  |       if (!tags || tags->encoding() == Container::kUnsupported) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         // We couldn't read the tags for this track. Either they were
 |  |  |  |         // We couldn't read the tags for this track. Either they were
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         // malformed, or perhaps the file is missing. Either way, tombstone
 |  |  |  |         // malformed, or perhaps the file is missing. Either way, tombstone
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -411,30 +418,24 @@ auto Database::updateIndexes() -> void { | 
			
		
	
		
		
			
				
					
					|  |  |  |       // At this point, we know that the track still exists in its original
 |  |  |  |       // 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.
 |  |  |  |       // location. All that's left to do is update any metadata about it.
 | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       auto new_type = calculateMediaType(*tags, track->filepath); |  |  |  |       // Remove the old index records first so has to avoid dangling records.
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       uint64_t new_hash = tags->Hash(); |  |  |  |       dbRemoveIndexes(track); | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       if (new_hash != track->tags_hash || new_type != track->type) { |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         // 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); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         // Again, we remove the old index records first so has to avoid
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         // dangling references.
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         dbRemoveIndexes(track); |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         // Atomically correct the hash + create the new index records.
 |  |  |  |       // Atomically correct the hash + create the new index records.
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         leveldb::WriteBatch batch; |  |  |  |       leveldb::WriteBatch batch; | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         track->tags_hash = new_hash; |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |         dbIngestTagHashes(*tags, track->individual_tag_hashes, batch); |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |         track->type = new_type; |  |  |  |       uint64_t new_hash = tags->Hash(); | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         dbCreateIndexesForTrack(*track, *tags, batch); |  |  |  |       if (track->tags_hash != new_hash) { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |         batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); |  |  |  |         track->tags_hash = new_hash; | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |         batch.Put(EncodeHashKey(new_hash), EncodeHashValue(track->id)); |  |  |  |         batch.Put(EncodeHashKey(new_hash), EncodeHashValue(track->id)); | 
			
		
	
		
		
			
				
					
					|  |  |  |         db_->Write(leveldb::WriteOptions(), &batch); |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       track->type = calculateMediaType(*tags, track->filepath); | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       batch.Put(EncodeDataKey(track->id), EncodeDataValue(*track)); | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       dbIngestTagHashes(*tags, track->individual_tag_hashes, batch); | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       dbCreateIndexesForTrack(*track, *tags, batch); | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       db_->Write(leveldb::WriteOptions(), &batch); | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |   } |  |  |  |   } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -445,8 +446,8 @@ auto Database::updateIndexes() -> void { | 
			
		
	
		
		
			
				
					
					|  |  |  |   track_finder_.launch(""); |  |  |  |   track_finder_.launch(""); | 
			
		
	
		
		
			
				
					
					|  |  |  | }; |  |  |  | }; | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | auto Database::processCandidateCallback(FILINFO& info, std::string_view path) |  |  |  | auto Database::processCandidateCallback(FILINFO& info, | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     -> void { |  |  |  |                                         std::string_view path) -> void { | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   leveldb::ReadOptions read_options; |  |  |  |   leveldb::ReadOptions read_options; | 
			
		
	
		
		
			
				
					
					|  |  |  |   read_options.fill_cache = true; |  |  |  |   read_options.fill_cache = true; | 
			
		
	
		
		
			
				
					
					|  |  |  |   read_options.verify_checksums = false; |  |  |  |   read_options.verify_checksums = false; | 
			
		
	
	
		
		
			
				
					|  |  | @ -530,9 +531,8 @@ static constexpr char kMusicMediaPath[] = "/Music/"; | 
			
		
	
		
		
			
				
					
					|  |  |  | static constexpr char kPodcastMediaPath[] = "/Podcasts/"; |  |  |  | static constexpr char kPodcastMediaPath[] = "/Podcasts/"; | 
			
		
	
		
		
			
				
					
					|  |  |  | static constexpr char kAudiobookMediaPath[] = "/Audiobooks/"; |  |  |  | static constexpr char kAudiobookMediaPath[] = "/Audiobooks/"; | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | auto Database::calculateMediaType(TrackTags& tags, std::string_view path) |  |  |  | auto Database::calculateMediaType(TrackTags& tags, | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     -> MediaType { |  |  |  |                                   std::string_view path) -> MediaType { | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   auto equalsIgnoreCase = [&](char lhs, char rhs) { |  |  |  |   auto equalsIgnoreCase = [&](char lhs, char rhs) { | 
			
		
	
		
		
			
				
					
					|  |  |  |     return std::tolower(lhs) == std::tolower(rhs); |  |  |  |     return std::tolower(lhs) == std::tolower(rhs); | 
			
		
	
		
		
			
				
					
					|  |  |  |   }; |  |  |  |   }; | 
			
		
	
	
		
		
			
				
					|  |  | @ -540,7 +540,8 @@ auto Database::calculateMediaType(TrackTags& tags, std::string_view path) | 
			
		
	
		
		
			
				
					
					|  |  |  |   // Use the filepath first, since it's the most explicit way for the user to
 |  |  |  |   // Use the filepath first, since it's the most explicit way for the user to
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   // tell us what this track is.
 |  |  |  |   // tell us what this track is.
 | 
			
		
	
		
		
			
				
					
					|  |  |  |   auto checkPathPrefix = [&](std::string_view path, std::string prefix) { |  |  |  |   auto checkPathPrefix = [&](std::string_view path, std::string prefix) { | 
			
		
	
		
		
			
				
					
					|  |  |  |     auto res = std::mismatch(prefix.begin(), prefix.end(), path.begin(), path.end(), equalsIgnoreCase); |  |  |  |     auto res = std::mismatch(prefix.begin(), prefix.end(), path.begin(), | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |                              path.end(), equalsIgnoreCase); | 
			
		
	
		
		
			
				
					
					|  |  |  |     return res.first == prefix.end(); |  |  |  |     return res.first == prefix.end(); | 
			
		
	
		
		
			
				
					
					|  |  |  |   }; |  |  |  |   }; | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
	
		
		
			
				
					|  |  | @ -634,8 +635,8 @@ auto Database::dbMintNewTrackId() -> TrackId { | 
			
		
	
		
		
			
				
					
					|  |  |  |   return next_track_id_++; |  |  |  |   return next_track_id_++; | 
			
		
	
		
		
			
				
					
					|  |  |  | } |  |  |  | } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | auto Database::dbGetTrackData(leveldb::ReadOptions options, TrackId id) |  |  |  | auto Database::dbGetTrackData(leveldb::ReadOptions options, | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |     -> std::shared_ptr<TrackData> { |  |  |  |                               TrackId id) -> std::shared_ptr<TrackData> { | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |   std::string key = EncodeDataKey(id); |  |  |  |   std::string key = EncodeDataKey(id); | 
			
		
	
		
		
			
				
					
					|  |  |  |   std::string raw_val; |  |  |  |   std::string raw_val; | 
			
		
	
		
		
			
				
					
					|  |  |  |   if (!db_->Get(options, key, &raw_val).ok()) { |  |  |  |   if (!db_->Get(options, key, &raw_val).ok()) { | 
			
		
	
	
		
		
			
				
					|  |  | @ -668,26 +669,55 @@ auto Database::dbRemoveIndexes(std::shared_ptr<TrackData> data) -> void { | 
			
		
	
		
		
			
				
					
					|  |  |  |   } |  |  |  |   } | 
			
		
	
		
		
			
				
					
					|  |  |  |   for (const IndexInfo& index : getIndexes()) { |  |  |  |   for (const IndexInfo& index : getIndexes()) { | 
			
		
	
		
		
			
				
					
					|  |  |  |     auto entries = Index(collator_, index, *data, *tags); |  |  |  |     auto entries = Index(collator_, index, *data, *tags); | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     std::optional<uint8_t> preserve_depth{}; | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     // Iterate through the index records backwards, so that we start deleting
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     // records from the highest depth. This allows us to work out what depth to
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     // stop at as we go.
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     // e.g. if the last track of an album is being deleted, we need to delete
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     // the album index record at n-1 depth. But if there are still other tracks
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |     // in the album, then we should only clear records at depth n.
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     for (auto it = entries.rbegin(); it != entries.rend(); it++) { |  |  |  |     for (auto it = entries.rbegin(); it != entries.rend(); it++) { | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       if (preserve_depth) { | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         if (it->first.header.components_hash.size() < *preserve_depth) { | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           // Some records deeper than this were not removed, so don't try to
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           // remove this one.
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |           continue; | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         // A record weren't removed, but the current record is either a
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         // sibling of that record, or a leaf belonging to a sibling of that
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         // record. Either way, we can start deleting records again.
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         // preserve_depth will be set to a new value if we start encountering
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         // siblings again.
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         preserve_depth.reset(); | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       auto key = EncodeIndexKey(it->first); |  |  |  |       auto key = EncodeIndexKey(it->first); | 
			
		
	
		
		
			
				
					
					|  |  |  |       auto status = db_->Delete(leveldb::WriteOptions{}, key); |  |  |  |       auto status = db_->Delete(leveldb::WriteOptions{}, key); | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (!status.ok()) { |  |  |  |       if (!status.ok()) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         return; |  |  |  |         return; | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       // Are any sibling records left at this depth? If two index records have
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       // matching headers, they are siblings (same depth, same selections at
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       // lower depths)
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       std::unique_ptr<leveldb::Iterator> cursor{db_->NewIterator({})}; |  |  |  |       std::unique_ptr<leveldb::Iterator> cursor{db_->NewIterator({})}; | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       // Check the record before the one we just deleted.
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       cursor->Seek(key); |  |  |  |       cursor->Seek(key); | 
			
		
	
		
		
			
				
					
					|  |  |  |       cursor->Prev(); |  |  |  |       cursor->Prev(); | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  |  | 
			
		
	
		
		
			
				
					
					|  |  |  |       auto prev_key = ParseIndexKey(cursor->key()); |  |  |  |       auto prev_key = ParseIndexKey(cursor->key()); | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (prev_key && prev_key->header == it->first.header) { |  |  |  |       if (prev_key && prev_key->header == it->first.header) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         break; |  |  |  |         preserve_depth = it->first.header.components_hash.size(); | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         continue; | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |       // Check the record after.
 | 
			
		
	
		
		
			
				
					
					|  |  |  |       cursor->Next(); |  |  |  |       cursor->Next(); | 
			
		
	
		
		
			
				
					
					|  |  |  |       auto next_key = ParseIndexKey(cursor->key()); |  |  |  |       auto next_key = ParseIndexKey(cursor->key()); | 
			
		
	
		
		
			
				
					
					|  |  |  |       if (next_key && next_key->header == it->first.header) { |  |  |  |       if (next_key && next_key->header == it->first.header) { | 
			
		
	
		
		
			
				
					
					|  |  |  |         break; |  |  |  |         preserve_depth = it->first.header.components_hash.size(); | 
			
				
				
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |  |  |  |  |         continue; | 
			
		
	
		
		
			
				
					
					|  |  |  |       } |  |  |  |       } | 
			
		
	
		
		
			
				
					
					|  |  |  |     } |  |  |  |     } | 
			
		
	
		
		
			
				
					
					|  |  |  |   } |  |  |  |   } | 
			
		
	
	
		
		
			
				
					|  |  | @ -809,8 +839,7 @@ Iterator::Iterator(std::shared_ptr<Database> db, IndexId idx) | 
			
		
	
		
		
			
				
					
					|  |  |  |     : Iterator(db, |  |  |  |     : Iterator(db, | 
			
		
	
		
		
			
				
					
					|  |  |  |                IndexKey::Header{ |  |  |  |                IndexKey::Header{ | 
			
		
	
		
		
			
				
					
					|  |  |  |                    .id = idx, |  |  |  |                    .id = idx, | 
			
		
	
		
		
			
				
					
					|  |  |  |                    .depth = 0, |  |  |  |                    .components_hash = {}, | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |                    .components_hash = 0, |  |  |  |  | 
			
		
	
		
		
	
		
		
			
				
					
					|  |  |  |                }) {} |  |  |  |                }) {} | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  | Iterator::Iterator(std::shared_ptr<Database> db, const IndexKey::Header& header) |  |  |  | Iterator::Iterator(std::shared_ptr<Database> db, const IndexKey::Header& header) | 
			
		
	
	
		
		
			
				
					|  |  | @ -883,8 +912,8 @@ auto TrackIterator::next() -> void { | 
			
		
	
		
		
			
				
					
					|  |  |  | 
 |  |  |  | 
 | 
			
		
	
		
		
			
				
					
					|  |  |  |     auto& cur = levels_.back().value(); |  |  |  |     auto& cur = levels_.back().value(); | 
			
		
	
		
		
			
				
					
					|  |  |  |     if (!cur) { |  |  |  |     if (!cur) { | 
			
		
	
		
		
			
				
					
					|  |  |  |       // The current top iterator is out of tracks. Pop it, and move the parent
 |  |  |  |       // The current top iterator is out of tracks. Pop it, and move the
 | 
			
				
				
			
		
	
		
		
			
				
					
					|  |  |  |       // to the next item.
 |  |  |  |       // parent to the next item.
 | 
			
				
				
			
		
	
		
		
	
		
		
	
		
		
			
				
					
					|  |  |  |       levels_.pop_back(); |  |  |  |       levels_.pop_back(); | 
			
		
	
		
		
			
				
					
					|  |  |  |     } else if (std::holds_alternative<IndexKey::Header>(cur->contents())) { |  |  |  |     } else if (std::holds_alternative<IndexKey::Header>(cur->contents())) { | 
			
		
	
		
		
			
				
					
					|  |  |  |       // This record is a branch. Push a new iterator.
 |  |  |  |       // This record is a branch. Push a new iterator.
 | 
			
		
	
	
		
		
			
				
					|  |  | 
 |