// Copyright (C) 2025, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. #nullable enable using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Duplicati.Library.Utility; using Microsoft.Data.Sqlite; namespace Duplicati.Library.Main.Database { /// /// This class is used to recreate a local database from a backup. /// internal class LocalRecreateDatabase : LocalRestoreDatabase { /// /// The tag used for logging. /// private static readonly string LOGTAG = Logging.Log.LogTagFromType(typeof(LocalRecreateDatabase)); /// /// Keeps track of the path entries in a sorted manner. /// This is used to ensure that the entries are unique and sorted by their keys. /// private class PathEntryKeeper { /// /// Comparer for KeyValuePair to sort by key and value. /// private struct KeyValueComparer : IComparer> { public readonly int Compare(KeyValuePair x, KeyValuePair y) { return x.Key == y.Key ? (x.Value == y.Value ? 0 : (x.Value < y.Value ? -1 : 1)) : (x.Key < y.Key ? -1 : 1); } } } /// /// Inserts a new file entry into the FileLookup table and returns the new row ID. /// private SqliteCommand m_insertFileCommand = null!; /// /// Inserts a new entry into the FilesetEntry table linking a fileset to a file with a last-modified timestamp. /// private SqliteCommand m_insertFilesetEntryCommand = null!; /// /// Inserts a new row into the Metadataset table for a given blockset and returns the new row ID. /// private SqliteCommand m_insertMetadatasetCommand = null!; /// /// Inserts a new blockset with its length and full hash into the Blockset table and returns the new row ID. /// private SqliteCommand m_insertBlocksetCommand = null!; /// /// Inserts a new blocklist hash entry into the BlocklistHash table for a given blockset and index. /// private SqliteCommand m_insertBlocklistHashCommand = null!; /// /// Updates the VolumeID of a block in the Block table matching a specific hash and size. /// private SqliteCommand m_updateBlockVolumeCommand = null!; /// /// Inserts a blocklist hash, block hash, and index into the temporary blocklist table. /// private SqliteCommand m_insertTempBlockListHash = null!; /// /// Inserts or ignores a file hash, block hash, and block size into the temporary small blockset table. /// private SqliteCommand m_insertSmallBlockset = null!; /// /// Selects the ID of a blockset from the Blockset table matching a given length and full hash. /// private SqliteCommand m_findBlocksetCommand = null!; /// /// Selects the ID of a Metadataset whose blockset matches a given full hash and length. /// private SqliteCommand m_findMetadatasetCommand = null!; /// /// Selects the ID of a file from the FileLookup table matching a given prefix, path, blockset, and metadata. /// private SqliteCommand m_findFilesetCommand = null!; /// /// Selects distinct blocklist hashes from the temporary blocklist table matching a given hash. /// private SqliteCommand m_findTempBlockListHashCommand = null!; /// /// Selects the VolumeID of a block in the Block table matching a specific hash and size. /// private SqliteCommand m_findHashBlockCommand = null!; /// /// Inserts a new block with hash, size, and volume ID into the Block table. /// private SqliteCommand m_insertBlockCommand = null!; /// /// Inserts or ignores a duplicate block entry linking a block and a volume into the DuplicateBlock table. /// private SqliteCommand m_insertDuplicateBlockCommand = null!; /// /// Temporary table name for blocklist hashes and their indices. /// private string m_tempblocklist = null!; /// /// Temporary table name for small blocksets, containing file hashes, block hashes, and block sizes. /// private string m_tempsmalllist = null!; /// /// A lookup table that prevents multiple downloads of the same volume. /// private readonly Dictionary m_proccessedVolumes = new Dictionary(); /// /// SQL that finds index and block size for all blocklist hashes, based on the temporary hash list. /// /// The size of the blocks in the blocklist. /// The size of the block hashes. /// The name of the temporary table containing blocklist hashes and indices. /// The number of blocks in the full block list, used to calculate the full index. /// A SQL query string that selects blocklist entries with their full index and block size. private static string SELECT_BLOCKLIST_ENTRIES(long blocksize, long blockhashsize, string temptable, long fullBlockListBlockCount) { var str_blocksize = Library.Utility.Utility.FormatInvariantValue(blocksize); var str_blockhashsize = Library.Utility.Utility.FormatInvariantValue(blockhashsize); var str_fullBlockListBlockCount = Library.Utility.Utility.FormatInvariantValue(fullBlockListBlockCount); return $@" SELECT ""E"".""BlocksetID"", ""F"".""Index"" + (""E"".""BlocklistIndex"" * {str_fullBlockListBlockCount}) AS ""FullIndex"", ""F"".""BlockHash"", MIN({str_blocksize}, ""E"".""Length"" - ((""F"".""Index"" + (""E"".""BlocklistIndex"" * {str_fullBlockListBlockCount})) * {str_blocksize})) AS ""BlockSize"", ""E"".""Hash"", ""E"".""BlocklistSize"", ""E"".""BlocklistHash"" FROM ( SELECT * FROM ( SELECT ""A"".""BlocksetID"", ""A"".""Index"" AS ""BlocklistIndex"", MIN({str_fullBlockListBlockCount} * {str_blockhashsize}, (((""B"".""Length"" + {str_blocksize} - 1) / {str_blocksize}) - (""A"".""Index"" * ({str_fullBlockListBlockCount}))) * {str_blockhashsize}) AS ""BlocklistSize"", ""A"".""Hash"" AS ""BlocklistHash"", ""B"".""Length"" FROM ""BlocklistHash"" ""A"", ""Blockset"" ""B"" WHERE ""B"".""ID"" = ""A"".""BlocksetID"" ) ""C"", ""Block"" ""D"" WHERE ""C"".""BlocklistHash"" = ""D"".""Hash"" AND ""C"".""BlocklistSize"" = ""D"".""Size"" ) ""E"", ""{temptable}"" ""F"" WHERE ""F"".""BlocklistHash"" = ""E"".""Hash"" ORDER BY ""E"".""BlocksetID"", ""FullIndex"" "; } /// /// Asynchronously creates a new instance of the class. /// /// The parent database from which to restore. /// The options to use for the database creation. /// An optional existing instance of to use; if null, a new instance will be created. /// A cancellation token to observe while waiting for the task to complete. /// A task that represents the asynchronous operation, containing the newly created instance. public static async Task CreateAsync(LocalDatabase parentdb, Options options, LocalRecreateDatabase? dbnew, CancellationToken token) { dbnew ??= new LocalRecreateDatabase(); dbnew = (LocalRecreateDatabase) await LocalRestoreDatabase.CreateAsync(parentdb, dbnew, token) .ConfigureAwait(false); dbnew.m_tempblocklist = $"TempBlocklist_{Library.Utility.Utility.GetHexGuid()}"; dbnew.m_tempsmalllist = $"TempSmalllist_{Library.Utility.Utility.GetHexGuid()}"; if (dbnew.m_connection == null) throw new Exception("Connection is null"); await using var cmd = dbnew.m_connection.CreateCommand(); await cmd.ExecuteNonQueryAsync($@" CREATE TEMPORARY TABLE ""{dbnew.m_tempblocklist}"" ( ""BlockListHash"" TEXT NOT NULL, ""BlockHash"" TEXT NOT NULL, ""Index"" INTEGER NOT NULL ) ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync($@" CREATE INDEX ""Index_{dbnew.m_tempblocklist}"" ON ""{dbnew.m_tempblocklist}"" (""BlockListHash""); ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync($@" CREATE TEMPORARY TABLE ""{dbnew.m_tempsmalllist}"" ( ""FileHash"" TEXT NOT NULL, ""BlockHash"" TEXT NOT NULL, ""BlockSize"" INTEGER NOT NULL ) ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync($@" CREATE UNIQUE INDEX ""Index_File_{dbnew.m_tempsmalllist}"" ON ""{dbnew.m_tempsmalllist}"" ( ""FileHash"", ""BlockSize"" ); ", token) .ConfigureAwait(false); await cmd.ExecuteNonQueryAsync($@" CREATE UNIQUE INDEX ""Index_Block_{dbnew.m_tempsmalllist}"" ON ""{dbnew.m_tempsmalllist}"" ( ""BlockHash"", ""BlockSize"" ); ", token) .ConfigureAwait(false); dbnew.m_insertFileCommand = await dbnew.m_connection.CreateCommandAsync(@" INSERT INTO ""FileLookup"" ( ""PrefixID"", ""Path"", ""BlocksetID"", ""MetadataID"" ) VALUES ( @PrefixId, @Path, @BlocksetId, @MetadataId ); SELECT last_insert_rowid(); ", token) .ConfigureAwait(false); dbnew.m_insertFilesetEntryCommand = await dbnew.m_connection.CreateCommandAsync(@" INSERT INTO ""FilesetEntry"" ( ""FilesetID"", ""FileID"", ""Lastmodified"" ) VALUES ( @FilesetId, @FileId, @LastModified ) ", token) .ConfigureAwait(false); dbnew.m_insertMetadatasetCommand = await dbnew.m_connection.CreateCommandAsync(@" INSERT INTO ""Metadataset"" (""BlocksetID"") VALUES (@BlocksetId); SELECT last_insert_rowid(); ", token) .ConfigureAwait(false); dbnew.m_insertBlocksetCommand = await dbnew.m_connection.CreateCommandAsync(@" INSERT INTO ""Blockset"" ( ""Length"", ""FullHash"" ) VALUES ( @Length, @FullHash ); SELECT last_insert_rowid(); ", token) .ConfigureAwait(false); dbnew.m_insertBlocklistHashCommand = await dbnew.m_connection.CreateCommandAsync(@" INSERT INTO ""BlocklistHash"" ( ""BlocksetID"", ""Index"", ""Hash"" ) VALUES ( @BlocksetId, @Index, @Hash ) ", token) .ConfigureAwait(false); dbnew.m_updateBlockVolumeCommand = await dbnew.m_connection.CreateCommandAsync(@" UPDATE ""Block"" SET ""VolumeID"" = @VolumeId WHERE ""Hash"" = @Hash AND ""Size"" = @Size ", token) .ConfigureAwait(false); dbnew.m_insertTempBlockListHash = await dbnew.m_connection.CreateCommandAsync($@" INSERT INTO ""{dbnew.m_tempblocklist}"" ( ""BlocklistHash"", ""BlockHash"", ""Index"" ) VALUES ( @BlocklistHash, @BlockHash, @Index ) ", token) .ConfigureAwait(false); dbnew.m_insertSmallBlockset = await dbnew.m_connection.CreateCommandAsync($@" INSERT OR IGNORE INTO ""{dbnew.m_tempsmalllist}"" ( ""FileHash"", ""BlockHash"", ""BlockSize"" ) VALUES ( @FileHash, @BlockHash, @BlockSize ) ", token) .ConfigureAwait(false); dbnew.m_findBlocksetCommand = await dbnew.m_connection.CreateCommandAsync(@" SELECT ""ID"" FROM ""Blockset"" WHERE ""Length"" = @Length AND ""FullHash"" = @FullHash ", token) .ConfigureAwait(false); dbnew.m_findMetadatasetCommand = await dbnew.m_connection.CreateCommandAsync(@" SELECT ""Metadataset"".""ID"" FROM ""Metadataset"", ""Blockset"" WHERE ""Metadataset"".""BlocksetID"" = ""Blockset"".""ID"" AND ""Blockset"".""FullHash"" = @FullHash AND ""Blockset"".""Length"" = @Length ", token) .ConfigureAwait(false); dbnew.m_findFilesetCommand = await dbnew.m_connection.CreateCommandAsync(@" SELECT ""ID"" FROM ""FileLookup"" WHERE ""PrefixID"" = @PrefixId AND ""Path"" = @Path AND ""BlocksetID"" = @BlocksetId AND ""MetadataID"" = @MetadataId ", token) .ConfigureAwait(false); dbnew.m_findTempBlockListHashCommand = await dbnew.m_connection.CreateCommandAsync($@" SELECT DISTINCT ""BlockListHash"" FROM ""{dbnew.m_tempblocklist}"" WHERE ""BlockListHash"" = @BlocklistHash ", token) .ConfigureAwait(false); dbnew.m_findHashBlockCommand = await dbnew.m_connection.CreateCommandAsync(@" SELECT ""VolumeID"" FROM ""Block"" WHERE ""Hash"" = @Hash AND ""Size"" = @Size ", token) .ConfigureAwait(false); dbnew.m_insertBlockCommand = await dbnew.m_connection.CreateCommandAsync(@" INSERT INTO ""Block"" ( ""Hash"", ""Size"", ""VolumeID"" ) VALUES ( @Hash, @Size, @VolumeId ) ", token) .ConfigureAwait(false); dbnew.m_insertDuplicateBlockCommand = await dbnew.m_connection.CreateCommandAsync(@" INSERT OR IGNORE INTO ""DuplicateBlock"" ( ""BlockID"", ""VolumeID"" ) VALUES ( ( SELECT ""ID"" FROM ""Block"" WHERE ""Hash"" = @Hash AND ""Size"" = @Size ), @VolumeId ) ", token) .ConfigureAwait(false); return dbnew; } /// /// Identifies and inserts any missing blocks and blockset entries into the database by comparing temporary tables of blocklist and small blockset data with the main tables, ensuring that all required block and blockset relationships are present for database consistency. /// /// The size of the hashes in the blocklist. /// The size of the blocks in the blocklist. /// A cancellation token to observe while waiting for the task to complete. /// A task that completes when the operation is finished. public async Task FindMissingBlocklistHashes(long hashsize, long blocksize, CancellationToken token) { await using var cmd = m_connection.CreateCommand(m_rtr); //Update all small blocklists and matching blocks var selectSmallBlocks = $@" SELECT ""BlockHash"", ""BlockSize"" FROM ""{m_tempsmalllist}"" "; var selectBlockHashes = $@" SELECT ""BlockHash"" AS ""FullHash"", ""BlockSize"" AS ""Length"" FROM ( {SELECT_BLOCKLIST_ENTRIES(blocksize, hashsize, m_tempblocklist, blocksize / hashsize)} ) "; var selectAllBlocks = @$" SELECT DISTINCT ""FullHash"", ""Length"" FROM ( {selectBlockHashes} UNION {selectSmallBlocks} ) "; var selectNewBlocks = $@" SELECT ""FullHash"" AS ""Hash"", ""Length"" AS ""Size"", -1 AS ""VolumeID"" FROM ( SELECT ""A"".""FullHash"", ""A"".""Length"", CASE WHEN ""B"".""Hash"" IS NULL THEN '' ELSE ""B"".""Hash"" END AS ""Hash"", CASE WHEN ""B"".""Size"" is NULL THEN -1 ELSE ""B"".""Size"" END AS ""Size"" FROM ({selectAllBlocks}) ""A"" LEFT OUTER JOIN ""Block"" ""B"" ON ""B"".""Hash"" = ""A"".""FullHash"" AND ""B"".""Size"" = ""A"".""Length"" ) WHERE ""FullHash"" != ""Hash"" AND ""Length"" != ""Size"" "; var insertBlocksCommand = @$" INSERT INTO ""Block"" ( ""Hash"", ""Size"", ""VolumeID"" ) {selectNewBlocks} "; // Insert all known blocks into block table with volumeid = -1 await cmd.ExecuteNonQueryAsync(insertBlocksCommand, token) .ConfigureAwait(false); // TODO: The BlocklistHash join seems to be unnecessary, but the join might be required to work around a from really old versions of Duplicati // this could be used instead //@") D, ""Block"" WHERE ""BlockQuery"".""BlockHash"" = ""Block"".""Hash"" AND ""BlockQuery"".""BlockSize"" = ""Block"".""Size"" "; var selectBlocklistBlocksetEntries = $@" SELECT ""E"".""BlocksetID"" AS ""BlocksetID"", ""D"".""FullIndex"" AS ""Index"", ""F"".""ID"" AS ""BlockID"" FROM ({SELECT_BLOCKLIST_ENTRIES(blocksize, hashsize, m_tempblocklist, blocksize / hashsize)}) ""D"", ""BlocklistHash"" ""E"", ""Block"" ""F"", ""Block"" ""G"" WHERE ""D"".""BlocksetID"" = ""E"".""BlocksetID"" AND ""D"".""BlocklistHash"" = ""E"".""Hash"" AND ""D"".""BlocklistSize"" = ""G"".""Size"" AND ""D"".""BlocklistHash"" = ""G"".""Hash"" AND ""D"".""Blockhash"" = ""F"".""Hash"" AND ""D"".""BlockSize"" = ""F"".""Size"" "; var selectBlocksetEntries = $@" SELECT ""Blockset"".""ID"" AS ""BlocksetID"", 0 AS ""Index"", ""Block"".""ID"" AS ""BlockID"" FROM ""Blockset"", ""Block"", ""{m_tempsmalllist}"" ""S"" WHERE ""Blockset"".""Fullhash"" = ""S"".""FileHash"" AND ""S"".""BlockHash"" = ""Block"".""Hash"" AND ""S"".""BlockSize"" = ""Block"".""Size"" AND ""Blockset"".""Length"" = ""S"".""BlockSize"" AND ""Blockset"".""Length"" <= {Library.Utility.Utility.FormatInvariantValue(blocksize)} "; var selectAllBlocksetEntries = @$" {selectBlocklistBlocksetEntries} UNION {selectBlocksetEntries} "; var selectFiltered = @$" SELECT DISTINCT ""H"".""BlocksetID"", ""H"".""Index"", ""H"".""BlockID"" FROM ({selectAllBlocksetEntries}) H WHERE ( ""H"".""BlocksetID"" || ':' || ""H"".""Index"" ) NOT IN ( SELECT ( ""ExistingBlocksetEntries"".""BlocksetID"" || ':' || ""ExistingBlocksetEntries"".""Index"" ) FROM ""BlocksetEntry"" ""ExistingBlocksetEntries"" ) "; var insertBlocksetEntriesCommand = @$" INSERT INTO ""BlocksetEntry"" ( ""BlocksetID"", ""Index"", ""BlockID"" ) {selectFiltered} "; try { await cmd.ExecuteNonQueryAsync(insertBlocksetEntriesCommand, token) .ConfigureAwait(false); } catch (Exception ex) { Logging.Log.WriteErrorMessage(LOGTAG, "BlocksetInsertFailed", ex, "Blockset insert failed, committing temporary tables for trace purposes"); await using (var fixcmd = m_connection.CreateCommand(m_rtr)) { await fixcmd.ExecuteNonQueryAsync($@" CREATE TABLE ""{m_tempblocklist}-Failure"" AS SELECT * FROM ""{m_tempblocklist}"" ", token) .ConfigureAwait(false); await fixcmd.ExecuteNonQueryAsync($@" CREATE TABLE ""{m_tempsmalllist}-Failure"" AS SELECT * FROM ""{m_tempsmalllist}"" ", token) .ConfigureAwait(false); } throw new Exception("The recreate failed, please create a bug-report from this database and send it to the developers for further analysis"); } await m_rtr.CommitAsync(token: token).ConfigureAwait(false); } /// /// From the temporary tables 1) insert new blocks into Block (VolumeID to be set at a later stage) /// and 2) add missing BlocksetEntry lines /// /// hashsize and blocksize: global database parameters /// hashOnly: do not take in account small blocks - these have been added at the /// end of the index handling and are not changed in the dblock handling so we can ignore them /// /// Notes: /// /// temp block list structure: blocklist hash, block hash, index relative to the /// beginning of the blocklist hash (NOT the file) /// /// temp small list structure: filehash, blockhash, blocksize: as the small files are defined /// by the fact that they are contained in a single block, blockhash is the same as the filehash, /// and blocksize can vary from 0 to the configured block size for the backup /// The size of the hashes in the blocklist. /// The size of the blocks in the blocklist. /// If true, only hash entries are processed, ignoring small blocks. /// A cancellation token to observe while waiting for the task to complete. /// A task that completes when the operation is finished. public async Task AddBlockAndBlockSetEntryFromTemp(long hashsize, long blocksize, bool hashOnly, CancellationToken token) { // TODO should values be parameters, rather than hardcoded into the SQL queries? await using var cmd = m_connection.CreateCommand(m_rtr); var extra = hashOnly ? "" : $@" UNION SELECT ""TS"".""BlockHash"", ""TS"".""BlockSize"" FROM ""{m_tempsmalllist}"" ""TS"" WHERE NOT EXISTS ( SELECT ""X"" FROM Block AS ""B"" WHERE ""B"".""Hash"" = ""TS"".""BlockHash"" AND ""B"".""Size"" = ""TS"".""BlockSize"" ) "; var str_blocksize = Library.Utility.Utility.FormatInvariantValue(blocksize); var str_blocksize_per_hashsize = Library.Utility.Utility.FormatInvariantValue(blocksize / hashsize); var insertBlocksCommand = $@" INSERT INTO ""Block"" ( ""Hash"", ""Size"", ""VolumeID"" ) SELECT DISTINCT ""BlockHash"" AS ""Hash"", ""BlockSize"" AS ""Size"", -1 AS ""VolumeID"" FROM ( SELECT ""NB"".""BlockHash"", MIN({str_blocksize}, ""BS"".""Length"" - ((""NB"".""Index"" + (BH.""Index"" * {str_blocksize_per_hashsize})) * {str_blocksize})) AS ""BlockSize"" FROM ( SELECT ""TBL"".""BlockListHash"", ""TBL"".""BlockHash"", ""TBL"".""Index"" FROM ""{m_tempblocklist}"" ""TBL"" LEFT OUTER JOIN ""Block"" ""B"" ON (""B"".""Hash"" = ""TBL"".""BlockHash"") WHERE ""B"".""Hash"" IS NULL ) ""NB"" JOIN ""BlocklistHash"" ""BH"" ON (""BH"".""Hash"" = ""NB"".""BlocklistHash"") JOIN ""Blockset"" ""BS"" ON (""BS"".""ID"" = ""BH"".""Blocksetid"") {extra} ) "; extra = hashOnly ? "" : $@" UNION SELECT ""BS"".""ID"" AS ""BlocksetID"", 0 AS ""Index"", ""BL"".""ID"" AS ""BlockID"" FROM ""{m_tempsmalllist}"" ""TS"" JOIN ""Blockset"" ""BS"" ON ( ""BS"".""FullHash"" = ""TS"".""FileHash"" AND ""BS"".""Length"" = ""TS"".""BlockSize"" AND ""BS"".""Length"" <= {str_blocksize} ) JOIN ""Block"" ""BL"" ON ( ""BL"".""Hash"" = ""TS"".""BlockHash"" AND ""BL"".""Size"" = ""TS"".""BlockSize"" ) LEFT OUTER JOIN ""BlocksetEntry"" ""BE"" ON ( ""BE"".""BlocksetID"" = ""BS"".""ID"" AND ""BE"".""Index"" = 0 ) WHERE ""BE"".""BlocksetID"" IS NULL "; var insertBlocksetEntriesCommand = $@" INSERT INTO ""BlocksetEntry"" ( ""BlocksetID"", ""Index"", ""BlockID"" ) SELECT DISTINCT ""BH"".""blocksetid"", (""BH"".""Index"" * {str_blocksize_per_hashsize}) + ""TBL"".""Index"" as ""FullIndex"", ""BK"".""ID"" AS ""BlockID"" FROM ""{m_tempblocklist}"" ""TBL"" JOIN ""blocklisthash"" ""BH"" ON (""BH"".""Hash"" = ""TBL"".""blocklisthash"") JOIN ""Block"" ""BK"" ON (""BK"".""Hash"" = ""TBL"".""BlockHash"") LEFT OUTER JOIN ""BlocksetEntry"" ""BE"" ON ( BE.""BlockSetID"" = ""BH"".""BlocksetID"" AND BE.""Index"" = (""BH"".""Index"" * {str_blocksize_per_hashsize})+""TBL"".""Index"" ) WHERE ""BE"".""BlockSetID"" IS NULL {extra} "; try { // Insert discovered new blocks into block table with volumeid = -1 await cmd.ExecuteNonQueryAsync(insertBlocksCommand, token) .ConfigureAwait(false); // Insert corresponding entries into blockset await cmd.ExecuteNonQueryAsync(insertBlocksetEntriesCommand, token) .ConfigureAwait(false); } catch (Exception ex) { Logging.Log.WriteErrorMessage(LOGTAG, "BlockOrBlocksetInsertFailed", ex, "Block or Blockset insert failed, committing temporary tables for trace purposes"); await using (var fixcmd = m_connection.CreateCommand(m_rtr)) { await fixcmd.ExecuteNonQueryAsync($@" CREATE TABLE ""{m_tempblocklist}_Failure"" AS SELECT * FROM ""{m_tempblocklist}"" ", token) .ConfigureAwait(false); await fixcmd.ExecuteNonQueryAsync($@" CREATE TABLE ""{m_tempsmalllist}_Failure"" AS SELECT * FROM ""{m_tempsmalllist}"" ", token) .ConfigureAwait(false); } throw new Exception("The recreate failed, please create a bug-report from this database and send it to the developers for further analysis"); } } /// /// Adds a directory entry to the FilesetEntry table, linking it to a fileset and a path prefix. /// /// The ID of the fileset to which the entry belongs. /// The ID of the path prefix for the entry. /// The path of the directory entry. /// The last modified time of the directory entry. /// The ID of the metadata associated with the directory entry. /// A cancellation token to observe while waiting for the task to complete. /// A task that when completed, indicates that the directory entry has been added. public async Task AddDirectoryEntry(long filesetid, long pathprefixid, string path, DateTime time, long metadataid, CancellationToken token) { await AddEntry(filesetid, pathprefixid, path, time, FOLDER_BLOCKSET_ID, metadataid, token) .ConfigureAwait(false); } /// /// Adds a symlink entry to the FilesetEntry table, linking it to a fileset and a path prefix. /// /// The ID of the fileset to which the entry belongs. /// The ID of the path prefix for the entry. /// The path of the symlink entry. /// The last modified time of the symlink entry. /// The ID of the metadata associated with the symlink entry. /// A cancellation token to observe while waiting for the task to complete. /// A task that when completed, indicates that the symlink entry has been added. public async Task AddSymlinkEntry(long filesetid, long pathprefixid, string path, DateTime time, long metadataid, CancellationToken token) { await AddEntry(filesetid, pathprefixid, path, time, SYMLINK_BLOCKSET_ID, metadataid, token) .ConfigureAwait(false); } /// /// Adds a file entry to the FilesetEntry table, linking it to a fileset and a path prefix. /// /// The ID of the fileset to which the entry belongs. /// The ID of the path prefix for the entry. /// The path of the file entry. /// The last modified time of the file entry. /// The ID of the blockset associated with the file entry. /// The ID of the metadata associated with the file entry. /// A cancellation token to observe while waiting for the task to complete. /// A task that when completed, indicates that the file entry has been added. public async Task AddFileEntry(long filesetid, long pathprefixid, string path, DateTime time, long blocksetid, long metadataid, CancellationToken token) { await AddEntry(filesetid, pathprefixid, path, time, blocksetid, metadataid, token) .ConfigureAwait(false); } /// /// Adds a file and fileset entry to the FilesetEntry table, linking it to a fileset and a path prefix. /// If the file already exists, it retrieves its ID; otherwise, it inserts a new file entry. /// Then, it inserts the fileset entry linking the file to the fileset. /// /// The ID of the fileset to which the entry belongs. /// The ID of the path prefix for the entry. /// The path of the file entry. /// The last modified time of the file entry. /// The ID of the blockset associated with the file entry. /// The ID of the metadata associated with the file entry. /// A cancellation token to observe while waiting for the task to complete. /// A task that when completed, indicates that the file and fileset entry has been added. private async Task AddEntry(long filesetid, long pathprefixid, string path, DateTime time, long blocksetid, long metadataid, CancellationToken token) { var fileid = await m_findFilesetCommand .SetTransaction(m_rtr) .SetParameterValue("@PrefixId", pathprefixid) .SetParameterValue("@Path", path) .SetParameterValue("@BlocksetId", blocksetid) .SetParameterValue("@MetadataId", metadataid) .ExecuteScalarInt64Async(-1, token) .ConfigureAwait(false); if (fileid < 0) { fileid = await m_insertFileCommand .SetTransaction(m_rtr) .SetParameterValue("@PrefixId", pathprefixid) .SetParameterValue("@Path", path) .SetParameterValue("@BlocksetId", blocksetid) .SetParameterValue("@MetadataId", metadataid) .ExecuteScalarInt64Async(-1, token) .ConfigureAwait(false); } await m_insertFilesetEntryCommand .SetTransaction(m_rtr) .SetParameterValue("@FilesetId", filesetid) .SetParameterValue("@FileId", fileid) .SetParameterValue("@LastModified", time.ToUniversalTime().Ticks) .ExecuteNonQueryAsync(token) .ConfigureAwait(false); } /// /// Adds a new metadataset to the database, creating a blockset if it does not already exist. /// If the metadataset already exists, it returns its ID. /// /// The full hash of the metadataset. /// The size of the metadataset in bytes. /// A collection of blocklist hashes associated with the metadataset. /// The expected number of blocklist hashes for the metadataset. /// A cancellation token to observe while waiting for the task to complete. /// A task that returns the ID of the added or existing metadataset. public async Task AddMetadataset(string metahash, long metahashsize, IEnumerable metablocklisthashes, long expectedmetablocklisthashes, CancellationToken token) { var metadataid = -1L; if (metahash == null) return metadataid; metadataid = await m_findMetadatasetCommand .SetTransaction(m_rtr) .SetParameterValue("@FullHash", metahash) .SetParameterValue("@Length", metahashsize) .ExecuteScalarInt64Async(-1, token) .ConfigureAwait(false); if (metadataid != -1) return metadataid; var blocksetid = await AddBlockset(metahash, metahashsize, metablocklisthashes, expectedmetablocklisthashes, token) .ConfigureAwait(false); metadataid = await m_insertMetadatasetCommand .SetTransaction(m_rtr) .SetParameterValue("@BlocksetId", blocksetid) .ExecuteScalarInt64Async(-1, token) .ConfigureAwait(false); return metadataid; } /// /// Adds a new blockset to the database, creating it if it does not already exist. /// If the blockset already exists, it returns its ID. /// /// The full hash of the blockset. /// The size of the blockset in bytes. /// A collection of blocklist hashes associated with the blockset. /// The expected number of blocklist hashes for the blockset. /// A cancellation token to observe while waiting for the task to complete. /// A task that returns the ID of the added or existing blockset. public async Task AddBlockset(string fullhash, long size, IEnumerable blocklisthashes, long expectedblocklisthashes, CancellationToken token) { var blocksetid = await m_findBlocksetCommand .SetTransaction(m_rtr) .SetParameterValue("@Length", size) .SetParameterValue("@FullHash", fullhash) .ExecuteScalarInt64Async(-1, token) .ConfigureAwait(false); if (blocksetid != -1) return blocksetid; blocksetid = await m_insertBlocksetCommand .SetTransaction(m_rtr) .SetParameterValue("@Length", size) .SetParameterValue("@FullHash", fullhash) .ExecuteScalarInt64Async(-1, token) .ConfigureAwait(false); long c = 0; if (blocklisthashes != null) { var index = 0L; m_insertBlocklistHashCommand .SetTransaction(m_rtr) .SetParameterValue("@BlocksetId", blocksetid); foreach (var hash in blocklisthashes) { if (!string.IsNullOrEmpty(hash)) { c++; if (c <= expectedblocklisthashes) { await m_insertBlocklistHashCommand .SetParameterValue("@Index", index++) .SetParameterValue("@Hash", hash) .ExecuteNonQueryAsync(token) .ConfigureAwait(false); } } } } if (c != expectedblocklisthashes) Logging.Log.WriteWarningMessage(LOGTAG, "MismatchInBlocklistHashCount", null, "Mismatching number of blocklist hashes detected on blockset {2}. Expected {0} blocklist hashes, but found {1}", expectedblocklisthashes, c, blocksetid); return blocksetid; } /// /// Updates the block information in the database, inserting a new block if it does not exist, /// updating the volume ID if it does, or inserting a duplicate block entry if the block already exists with a different volume ID. /// /// The hash of the block to update. /// The size of the block in bytes. /// The ID of the volume to which the block belongs. /// A cancellation token to observe while waiting for the task to complete. /// A task that returns a tuple indicating whether any changes were made and whether the block was newly inserted. public async Task<(bool, bool)> UpdateBlock(string hash, long size, long volumeID, CancellationToken token) { var anyChange = false; var currentVolumeId = await m_findHashBlockCommand .SetTransaction(m_rtr) .SetParameterValue("@Hash", hash) .SetParameterValue("@Size", size) .ExecuteScalarInt64Async(-2, token) .ConfigureAwait(false); if (currentVolumeId == volumeID) return (anyChange, false); anyChange = true; if (currentVolumeId == -2) { //Insert await m_insertBlockCommand .SetTransaction(m_rtr) .SetParameterValue("@Hash", hash) .SetParameterValue("@Size", size) .SetParameterValue("@VolumeId", volumeID) .ExecuteNonQueryAsync(token) .ConfigureAwait(false); return (anyChange, true); } else if (currentVolumeId == -1) { //Update var c = await m_updateBlockVolumeCommand .SetTransaction(m_rtr) .SetParameterValue("@VolumeId", volumeID) .SetParameterValue("@Hash", hash) .SetParameterValue("@Size", size) .ExecuteNonQueryAsync(token) .ConfigureAwait(false); if (c != 1) throw new Exception($"Failed to update table, found {c} entries for key {hash} with size {size}"); return (anyChange, true); } else { await m_insertDuplicateBlockCommand .SetTransaction(m_rtr) .SetParameterValue("@Hash", hash) .SetParameterValue("@Size", size) .SetParameterValue("@VolumeId", volumeID) .ExecuteNonQueryAsync(token) .ConfigureAwait(false); return (anyChange, false); } } /// /// Adds a link between a small blockset and a block in the database. /// This method is used to track small blocks that are part of a larger blockset, allowing for efficient storage and retrieval of small files. /// /// The hash of the file that contains the small block. /// The hash of the small block. /// The size of the small block in bytes. /// A cancellation token to observe while waiting for the task to complete. /// A task that completes when the small blockset link has been added. public async Task AddSmallBlocksetLink(string filehash, string blockhash, long blocksize, CancellationToken token) { await m_insertSmallBlockset .SetTransaction(m_rtr) .SetParameterValue("@FileHash", filehash) .SetParameterValue("@BlockHash", blockhash) .SetParameterValue("@BlockSize", blocksize) .ExecuteNonQueryAsync(token) .ConfigureAwait(false); } /// /// Adds a temporary blocklist hash to the database, along with its associated block hashes. /// This method is used to track temporary blocklists that are being processed, allowing for efficient management of blocklist data. /// /// The hash of the temporary blocklist. /// A collection of block hashes associated with the temporary blocklist. /// A cancellation token to observe while waiting for the task to complete. /// A task that returns a boolean indicating whether the temporary blocklist hash was successfully added. public async Task AddTempBlockListHash(string hash, IEnumerable blocklisthashes, CancellationToken token) { var r = await m_findTempBlockListHashCommand .SetTransaction(m_rtr) .SetParameterValue("@BlocklistHash", hash) .ExecuteScalarAsync(token) .ConfigureAwait(false); if (r != null && r != DBNull.Value) return false; m_insertTempBlockListHash .SetTransaction(m_rtr) .SetParameterValue("@BlocklistHash", hash); var index = 0L; foreach (var s in blocklisthashes) { await m_insertTempBlockListHash .SetParameterValue("@BlockHash", s) .SetParameterValue("@Index", index++) .ExecuteNonQueryAsync(token) .ConfigureAwait(false); } return true; } /// /// Retrieves a list of block hashes associated with a specific volume ID from the database. /// This method is used to obtain the block hashes for a given volume, allowing for efficient retrieval of block data. /// /// The ID of the volume for which to retrieve block hashes. /// A cancellation token to observe while waiting for the task to complete. /// An asynchronous enumerable collection of block hashes associated with the specified volume ID. public async IAsyncEnumerable GetBlockLists(long volumeid, [EnumeratorCancellation] CancellationToken token) { await using var cmd = m_connection.CreateCommand(@" SELECT DISTINCT ""BlocklistHash"".""Hash"" FROM ""BlocklistHash"", ""Block"" WHERE ""Block"".""Hash"" = ""BlocklistHash"".""Hash"" AND ""Block"".""VolumeID"" = @VolumeId ") .SetTransaction(m_rtr) .SetParameterValue("@VolumeId", volumeid); await using var rd = await cmd.ExecuteReaderAsync(token).ConfigureAwait(false); while (await rd.ReadAsync(token).ConfigureAwait(false)) yield return rd.ConvertValueToString(0) ?? ""; } /// /// Retrieves a list of remote volumes that are missing blocks based on the specified parameters. /// This method is used to identify volumes that need to be processed for missing blocks, allowing for efficient management of remote storage. /// /// The current pass number, used to determine the state of processing. /// The size of the blocks in the blocklist. /// The size of the hashes in the blocklist. /// A boolean indicating whether to force the use of blocks, even if they are not missing. /// A cancellation token to observe while waiting for the task to complete. /// An asynchronous enumerable collection of remote volumes that are missing blocks. public async IAsyncEnumerable GetMissingBlockListVolumes(int passNo, long blocksize, long hashsize, bool forceBlockUse, [EnumeratorCancellation] CancellationToken token) { await using var cmd = m_connection.CreateCommand(m_rtr); var selectCommand = @" SELECT DISTINCT ""RemoteVolume"".""Name"", ""RemoteVolume"".""Hash"", ""RemoteVolume"".""Size"", ""RemoteVolume"".""ID"" FROM ""RemoteVolume"" "; var missingBlocklistEntries = $@" SELECT ""BlocklistHash"".""Hash"" FROM ""BlocklistHash"" LEFT OUTER JOIN ""BlocksetEntry"" ON ""BlocksetEntry"".""Index"" = (""BlocklistHash"".""Index"" * {Library.Utility.Utility.FormatInvariantValue(blocksize / hashsize)}) AND ""BlocksetEntry"".""BlocksetID"" = ""BlocklistHash"".""BlocksetID"" WHERE ""BlocksetEntry"".""BlocksetID"" IS NULL "; var missingBlockInfo = @" SELECT ""VolumeID"" FROM ""Block"" WHERE ""VolumeID"" < 0 AND ""Size"" > 0 "; var missingBlocklistVolumes = $@" SELECT ""VolumeID"" FROM ""Block"", ({missingBlocklistEntries}) ""A"" WHERE ""A"".""Hash"" = ""Block"".""Hash"" "; var countMissingInformation = $@" SELECT COUNT(*) FROM ( SELECT DISTINCT ""VolumeID"" FROM ( {missingBlockInfo} UNION {missingBlocklistVolumes} ) ) "; if (passNo == 0) { // On the first pass, we select all the volumes we know we need, // which may be an empty list cmd.SetCommandAndParameters($@" {selectCommand} WHERE ""ID"" IN ({missingBlocklistVolumes}) "); // Reset the list m_proccessedVolumes.Clear(); } else { //On anything but the first pass, we check if we are done var r = await cmd .SetCommandAndParameters(countMissingInformation) .ExecuteScalarInt64Async(0, token) .ConfigureAwait(false); if (r == 0 && !forceBlockUse) yield break; if (passNo == 1) { // On the second pass, we select all volumes that are not mentioned in the db var mentionedVolumes = @" SELECT DISTINCT ""VolumeID"" FROM ""Block"" "; cmd .SetCommandAndParameters($@" {selectCommand} WHERE ""ID"" NOT IN ({mentionedVolumes}) AND ""Type"" = @Type ") .SetParameterValue("@Type", RemoteVolumeType.Blocks.ToString()); } else { // On the final pass, we select all volumes // the filter will ensure that we do not download anything twice cmd .SetCommandAndParameters($@" {selectCommand} WHERE ""Type"" = @Type ") .SetParameterValue("@Type", RemoteVolumeType.Blocks.ToString()); } } await using var rd = await cmd .ExecuteReaderAsync(token) .ConfigureAwait(false); while (await rd.ReadAsync(token).ConfigureAwait(false)) { var volumeID = rd.ConvertValueToInt64(3); // Guard against multiple downloads of the same file if (m_proccessedVolumes.TryAdd(volumeID, volumeID)) { yield return new RemoteVolume( rd.ConvertValueToString(0), rd.ConvertValueToString(1), rd.ConvertValueToInt64(2, -1) ); } } } /// /// Cleans up missing volumes by replacing blocks for non-present remote files with existing ones, /// removing references to non-present remote files, and marking index files for later removal. /// /// A cancellation token to observe while waiting for the task to complete. /// A task that completes when the cleanup operation is finished. public async Task CleanupMissingVolumes(CancellationToken token) { var tablename = "SwapBlocks-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray()); // TODO: either hardcode all string constants or none // The first part of this query swaps out blocks for non-present remote files with // existing ones (as recorded in the DuplicateBlock table) // The second part removes references to the non-present remote files, // and marks the index files that pointed to them, such that they will be removed later on var sql = $@" CREATE TEMPORARY TABLE ""{tablename}"" AS SELECT ""A"".""ID"" AS ""BlockID"", ""A"".""VolumeID"" AS ""SourceVolumeID"", ""A"".""State"" AS ""SourceVolumeState"", ""B"".""VolumeID"" AS ""TargetVolumeID"", ""B"".""State"" AS ""TargetVolumeState"" FROM ( SELECT ""Block"".""ID"", ""Block"".""VolumeID"", ""Remotevolume"".""State"" FROM ""Block"", ""Remotevolume"" WHERE ""Block"".""VolumeID"" = ""Remotevolume"".""ID"" AND ""Remotevolume"".""State"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeState.Temporary)}' ) A, ( SELECT ""DuplicateBlock"".""BlockID"", MIN(""DuplicateBlock"".""VolumeID"") AS ""VolumeID"", ""Remotevolume"".""State"" FROM ""DuplicateBlock"", ""Remotevolume"" WHERE ""DuplicateBlock"".""VolumeID"" = ""Remotevolume"".""ID"" AND ""Remotevolume"".""State"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeState.Verified)}' GROUP BY ""DuplicateBlock"".""BlockID"", ""Remotevolume"".""State"" ) B WHERE ""A"".""ID"" = ""B"".""BlockID""; UPDATE ""Block"" SET ""VolumeID"" = ( SELECT ""TargetVolumeID"" FROM ""{tablename}"" WHERE ""Block"".""ID"" = ""{tablename}"".""BlockID"" ) WHERE ""Block"".""ID"" IN ( SELECT ""BlockID"" FROM ""{tablename}"" ); UPDATE ""DuplicateBlock"" SET ""VolumeID"" = ( SELECT ""SourceVolumeID"" FROM ""{tablename}"" WHERE ""DuplicateBlock"".""BlockID"" = ""{tablename}"".""BlockID"" ) WHERE ( ""DuplicateBlock"".""BlockID"", ""DuplicateBlock"".""VolumeID"" ) IN ( SELECT ""BlockID"", ""TargetVolumeID"" FROM ""{tablename}"" ); DROP TABLE ""{tablename}""; DELETE FROM ""IndexBlockLink"" WHERE ""BlockVolumeID"" IN ( SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeType.Blocks)}' AND ""State"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeState.Temporary)}' AND ""ID"" NOT IN ( SELECT DISTINCT ""VolumeID"" FROM ""Block"" ) ); DELETE FROM ""DuplicateBlock"" WHERE ""VolumeID"" IN ( SELECT ""ID"" FROM ""RemoteVolume"" WHERE ""Type"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeType.Blocks)}' AND ""State"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeState.Temporary)}' AND ""ID"" NOT IN ( SELECT DISTINCT ""VolumeID"" FROM ""Block"" ) ); DELETE FROM ""RemoteVolume"" WHERE ""Type"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeType.Blocks)}' AND ""State"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeState.Temporary)}' AND ""ID"" NOT IN ( SELECT DISTINCT ""VolumeID"" FROM ""Block"" ); "; // We could delete these, but we don't have to, so we keep them around until the next compact is done // UPDATE ""RemoteVolume"" SET ""State"" = ""{3}"" WHERE ""Type"" = ""{5}"" AND ""ID"" NOT IN (SELECT ""IndexVolumeID"" FROM ""IndexBlockLink""); var countsql = $@" SELECT COUNT(*) FROM ""RemoteVolume"" WHERE ""State"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeState.Temporary)}' AND ""Type"" = '{Library.Utility.Utility.FormatInvariantValue(RemoteVolumeType.Blocks)}' "; await using var cmd = m_connection.CreateCommand(m_rtr); var cnt = await cmd.ExecuteScalarInt64Async(countsql, token) .ConfigureAwait(false); if (cnt > 0) { try { await cmd.ExecuteNonQueryAsync(sql, token) .ConfigureAwait(false); var cnt2 = await cmd.ExecuteScalarInt64Async(countsql, token) .ConfigureAwait(false); Logging.Log.WriteWarningMessage(LOGTAG, "MissingVolumesDetected", null, "Replaced blocks for {0} missing volumes; there are now {1} missing volumes", cnt, cnt2); } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "MissingVolumesDetected", ex, "Found {0} missing volumes; failed while attempting to replace blocks from existing volumes", cnt); throw; } } } /// /// Move blocks that are not referenced by any files to DeletedBlock table. /// Needs to be called after the last FindMissingBlocklistHashes, otherwise the tables are not up to date. /// /// A cancellation token to observe while waiting for the task to complete. /// A task that completes when the cleanup operation is finished. public async Task CleanupDeletedBlocks(CancellationToken token) { // Find out which blocks are deleted and move them into DeletedBlock, so that compact notices these blocks are empty // Deleted blocks do not appear in the BlocksetEntry and not in the BlocklistHash table var tmptablename = "DeletedBlocks-" + Library.Utility.Utility.ByteArrayAsHexString(Guid.NewGuid().ToByteArray()); await using var cmd = m_connection.CreateCommand(m_rtr); // 1. Select blocks not used by any file and not as a blocklist into temporary table await cmd.ExecuteNonQueryAsync($@" CREATE TEMPORARY TABLE ""{tmptablename}"" AS SELECT ""Block"".""ID"", ""Block"".""Hash"", ""Block"".""Size"", ""Block"".""VolumeID"" FROM ""Block"" WHERE ""Block"".""ID"" NOT IN ( SELECT ""BlocksetEntry"".""BlockID"" FROM ""BlocksetEntry"" ) AND ""Block"".""Hash"" NOT IN ( SELECT ""BlocklistHash"".""Hash"" FROM ""BlocklistHash"" ) ", token) .ConfigureAwait(false); // 2. Insert blocks into DeletedBlock table await cmd.ExecuteNonQueryAsync($@" INSERT INTO ""DeletedBlock"" ( ""Hash"", ""Size"", ""VolumeID"" ) SELECT ""Hash"", ""Size"", ""VolumeID"" FROM ""{tmptablename}"" ", token) .ConfigureAwait(false); // 3. Remove blocks from Block table await cmd.ExecuteNonQueryAsync($@" DELETE FROM ""Block"" WHERE ""ID"" IN ( SELECT ""ID"" FROM ""{tmptablename}"" ) ", token) .ConfigureAwait(false); await cmd .ExecuteNonQueryAsync($@"DROP TABLE IF EXISTS ""{tmptablename}""", token) .ConfigureAwait(false); await m_rtr.CommitAsync(token: token).ConfigureAwait(false); } public override void Dispose() { DisposeAsync().AsTask().Await(); } public async override ValueTask DisposeAsync() { await using (var cmd = m_connection.CreateCommand(m_rtr)) { if (m_tempblocklist != null) try { await cmd.ExecuteNonQueryAsync(@$"DROP TABLE IF EXISTS ""{m_tempblocklist}""", default) .ConfigureAwait(false); } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "FailedToDropTempBlocklist", ex, "Failed to drop temporary blocklist table {0}", m_tempblocklist); } finally { m_tempblocklist = null!; } if (m_tempsmalllist != null) try { await cmd.ExecuteNonQueryAsync(@$"DROP TABLE IF EXISTS ""{m_tempsmalllist}""", default) .ConfigureAwait(false); } catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "FailedToDropTempSmalllist", ex, "Failed to drop temporary smalllist table {0}", m_tempsmalllist); } finally { m_tempsmalllist = null!; } } await base.DisposeAsync().ConfigureAwait(false); } } }