diff --git a/Duplicati/Library/Main/Database/ChangeStatistics.cs b/Duplicati/Library/Main/Database/ChangeStatistics.cs index 49f69ec11..67a8e3ff3 100644 --- a/Duplicati/Library/Main/Database/ChangeStatistics.cs +++ b/Duplicati/Library/Main/Database/ChangeStatistics.cs @@ -1,27 +1,28 @@ // 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 +// +// 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 +// +// 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 +// +// 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.Data; +using System.Threading.Tasks; +using Microsoft.Data.Sqlite; namespace Duplicati.Library.Main.Database; @@ -42,57 +43,85 @@ public static class ChangeStatistics /// The results object to update with the statistics /// The ID of the current fileset /// The ID of the previous fileset - internal static void UpdateChangeStatistics(IDbCommand cmd, BackupResults results, long currentFilesetId, long previousFilesetId) + internal static async Task UpdateChangeStatistics(SqliteCommand cmd, BackupResults results, long currentFilesetId, long previousFilesetId) { var tmpName = $"TmpFileState_{Guid.NewGuid():N}"; try { // Create temp table - cmd.SetCommandAndParameters(LocalDatabase.FormatInvariant($@" - CREATE TEMP TABLE ""{tmpName}"" AS - SELECT - FL.""PrefixID"", - FL.""Path"", - FL.""BlocksetID"", - BS_Meta.""Fullhash"" AS ""Metahash"", - CASE FE.""FilesetID"" - WHEN @LastFilesetId THEN 0 - WHEN @CurrentFilesetId THEN 1 - END AS Source - FROM ""FileLookup"" FL - JOIN ""FilesetEntry"" FE ON FL.""ID"" = FE.""FileID"" - LEFT JOIN ""Blockset"" BS_Data ON FL.""BlocksetID"" = BS_Data.""ID"" - LEFT JOIN ""Metadataset"" M ON FL.""MetadataID"" = M.""ID"" - LEFT JOIN ""Blockset"" BS_Meta ON M.""BlocksetID"" = BS_Meta.""ID"" - WHERE FE.""FilesetID"" IN (@LastFilesetId, @CurrentFilesetId); -")) -.SetParameterValue("@LastFilesetId", previousFilesetId) -.SetParameterValue("@CurrentFilesetId", currentFilesetId) -.ExecuteNonQuery(); + await cmd.SetCommandAndParameters($@" + CREATE TEMP TABLE ""{tmpName}"" AS + SELECT + FL.""PrefixID"", + FL.""Path"", + FL.""BlocksetID"", + BS_Meta.""Fullhash"" AS ""Metahash"", + CASE FE.""FilesetID"" + WHEN @LastFilesetId THEN 0 + WHEN @CurrentFilesetId THEN 1 + END AS Source + FROM ""FileLookup"" FL + JOIN ""FilesetEntry"" FE + ON FL.""ID"" = FE.""FileID"" + LEFT JOIN ""Blockset"" BS_Data + ON FL.""BlocksetID"" = BS_Data.""ID"" + LEFT JOIN ""Metadataset"" M + ON FL.""MetadataID"" = M.""ID"" + LEFT JOIN ""Blockset"" BS_Meta + ON M.""BlocksetID"" = BS_Meta.""ID"" + WHERE FE.""FilesetID"" IN ( + @LastFilesetId, + @CurrentFilesetId + ); + ") + .SetParameterValue("@LastFilesetId", previousFilesetId) + .SetParameterValue("@CurrentFilesetId", currentFilesetId) + .ExecuteNonQueryAsync(); // Index for fast comparison - cmd.ExecuteNonQuery(LocalDatabase.FormatInvariant($@"CREATE INDEX ""idx_{tmpName}"" ON ""{tmpName}"" (""PrefixID"", ""Path"")")); + await cmd.ExecuteNonQueryAsync($@" + CREATE INDEX ""idx_{tmpName}"" + ON ""{tmpName}"" ( + ""PrefixID"", + ""Path"" + ) + "); // Added - results.AddedFolders = CountAdded(cmd, tmpName, LocalDatabase.FOLDER_BLOCKSET_ID); - results.AddedSymlinks = CountAdded(cmd, tmpName, LocalDatabase.SYMLINK_BLOCKSET_ID); - results.AddedFiles = CountAdded(cmd, tmpName, null, new[] { LocalDatabase.FOLDER_BLOCKSET_ID, LocalDatabase.SYMLINK_BLOCKSET_ID }); + results.AddedFolders = await CountAdded(cmd, tmpName, LocalDatabase.FOLDER_BLOCKSET_ID); + results.AddedSymlinks = await CountAdded(cmd, tmpName, LocalDatabase.SYMLINK_BLOCKSET_ID); + results.AddedFiles = await CountAdded(cmd, tmpName, null, [ + LocalDatabase.FOLDER_BLOCKSET_ID, + LocalDatabase.SYMLINK_BLOCKSET_ID + ]); // Deleted - results.DeletedFolders = CountDeleted(cmd, tmpName, LocalDatabase.FOLDER_BLOCKSET_ID); - results.DeletedSymlinks = CountDeleted(cmd, tmpName, LocalDatabase.SYMLINK_BLOCKSET_ID); - results.DeletedFiles = CountDeleted(cmd, tmpName, null, new[] { LocalDatabase.FOLDER_BLOCKSET_ID, LocalDatabase.SYMLINK_BLOCKSET_ID }); + results.DeletedFolders = await CountDeleted(cmd, tmpName, LocalDatabase.FOLDER_BLOCKSET_ID); + results.DeletedSymlinks = await CountDeleted(cmd, tmpName, LocalDatabase.SYMLINK_BLOCKSET_ID); + results.DeletedFiles = await CountDeleted(cmd, tmpName, null, [ + LocalDatabase.FOLDER_BLOCKSET_ID, + LocalDatabase.SYMLINK_BLOCKSET_ID + ]); // Modified - results.ModifiedFolders = CountModified(cmd, tmpName, LocalDatabase.FOLDER_BLOCKSET_ID); - results.ModifiedSymlinks = CountModified(cmd, tmpName, LocalDatabase.SYMLINK_BLOCKSET_ID); - results.ModifiedFiles = CountModified(cmd, tmpName, null, new[] { LocalDatabase.FOLDER_BLOCKSET_ID, LocalDatabase.SYMLINK_BLOCKSET_ID }); + results.ModifiedFolders = await CountModified(cmd, tmpName, LocalDatabase.FOLDER_BLOCKSET_ID); + results.ModifiedSymlinks = await CountModified(cmd, tmpName, LocalDatabase.SYMLINK_BLOCKSET_ID); + results.ModifiedFiles = await CountModified(cmd, tmpName, null, [ + LocalDatabase.FOLDER_BLOCKSET_ID, + LocalDatabase.SYMLINK_BLOCKSET_ID + ]); } finally { - try { cmd.ExecuteNonQuery(LocalDatabase.FormatInvariant($@"DROP TABLE IF EXISTS ""{tmpName}"";")); } - catch (Exception ex) { Logging.Log.WriteWarningMessage(LOGTAG, "DropTemp", ex, $"Failed to drop {tmpName}"); } + try + { + await cmd.ExecuteNonQueryAsync($@"DROP TABLE IF EXISTS ""{tmpName}"";"); + } + catch (Exception ex) + { + Logging.Log.WriteWarningMessage(LOGTAG, "DropTemp", ex, $"Failed to drop {tmpName}"); + } } } @@ -104,16 +133,21 @@ public static class ChangeStatistics /// The ID of the blockset to count, or null for all /// The blocksets to exclude from the count /// The number of added files or folders - private static long CountAdded(IDbCommand cmd, string tmpName, long? blocksetId, long[]? excludeBlocksets = null) + private static async Task CountAdded(SqliteCommand cmd, string tmpName, long? blocksetId, long[]? excludeBlocksets = null) { - var conditions = LocalDatabase.FormatInvariant($@"Source = 1 AND NOT EXISTS ( - SELECT 1 FROM ""{tmpName}"" B - WHERE B.Source = 0 - AND B.PrefixID = A.PrefixID - AND B.Path = A.Path - )"); + var conditions = $@" + Source = 1 + AND NOT EXISTS ( + SELECT 1 + FROM ""{tmpName}"" B + WHERE + B.Source = 0 + AND B.PrefixID = A.PrefixID + AND B.Path = A.Path + ) + "; - return CountWithCondition(cmd, tmpName, "A", conditions, blocksetId, excludeBlocksets); + return await CountWithCondition(cmd, tmpName, "A", conditions, blocksetId, excludeBlocksets); } /// @@ -124,16 +158,21 @@ public static class ChangeStatistics /// The ID of the blockset to count, or null for all /// The blocksets to exclude from the count /// The number of deleted files or folders - private static long CountDeleted(IDbCommand cmd, string tmpName, long? blocksetId, long[]? excludeBlocksets = null) + private static async Task CountDeleted(SqliteCommand cmd, string tmpName, long? blocksetId, long[]? excludeBlocksets = null) { - var conditions = @"Source = 0 AND NOT EXISTS ( - SELECT 1 FROM """ + tmpName + @""" B - WHERE B.Source = 1 - AND B.PrefixID = A.PrefixID - AND B.Path = A.Path - )"; + var conditions = @$" + Source = 0 + AND NOT EXISTS ( + SELECT 1 + FROM ""{tmpName}"" B + WHERE + B.Source = 1 + AND B.PrefixID = A.PrefixID + AND B.Path = A.Path + ) + "; - return CountWithCondition(cmd, tmpName, "A", conditions, blocksetId, excludeBlocksets); + return await CountWithCondition(cmd, tmpName, "A", conditions, blocksetId, excludeBlocksets); } /// @@ -145,23 +184,32 @@ public static class ChangeStatistics /// The hash column to compare /// The blocksets to exclude from the count /// The number of modified files or folders - private static long CountModified(IDbCommand cmd, string tmpName, long? blocksetId, long[]? excludeBlocksets = null) + private static async Task CountModified(SqliteCommand cmd, string tmpName, long? blocksetId, long[]? excludeBlocksets = null) { string conditions; if (blocksetId == LocalDatabase.FOLDER_BLOCKSET_ID || blocksetId == LocalDatabase.SYMLINK_BLOCKSET_ID) { conditions = @" - A.Source = 0 AND B.Source = 1 - AND A.PrefixID = B.PrefixID AND A.Path = B.Path - AND A.Metahash IS NOT B.Metahash"; + A.Source = 0 + AND B.Source = 1 + AND A.PrefixID = B.PrefixID + AND A.Path = B.Path + AND A.Metahash IS NOT B.Metahash + "; } else { conditions = @" - A.Source = 0 AND B.Source = 1 - AND A.PrefixID = B.PrefixID AND A.Path = B.Path - AND (A.BlocksetID IS NOT B.BlocksetID OR A.Metahash IS NOT B.Metahash)"; + A.Source = 0 + AND B.Source = 1 + AND A.PrefixID = B.PrefixID + AND A.Path = B.Path + AND ( + A.BlocksetID IS NOT B.BlocksetID + OR A.Metahash IS NOT B.Metahash + ) + "; } string blocksetCondition = GetBlocksetCondition("A", blocksetId, excludeBlocksets); @@ -169,12 +217,16 @@ public static class ChangeStatistics conditions += $" AND {blocksetCondition}"; string sql = $@" - SELECT COUNT(*) - FROM ""{tmpName}"" A - JOIN ""{tmpName}"" B ON A.PrefixID = B.PrefixID AND A.Path = B.Path - WHERE {conditions}"; + SELECT COUNT(*) + FROM ""{tmpName}"" A + JOIN ""{tmpName}"" B + ON A.PrefixID = B.PrefixID + AND A.Path = B.Path + WHERE {conditions} + "; - return cmd.SetCommandAndParameters(sql).ExecuteScalarInt64(0); + return await cmd.SetCommandAndParameters(sql) + .ExecuteScalarInt64Async(0); } /// @@ -187,15 +239,21 @@ public static class ChangeStatistics /// The ID of the blockset to count, or null for all /// The blocksets to exclude from the count /// The number of files or folders that match the condition - private static long CountWithCondition(IDbCommand cmd, string tmpName, string alias, string baseCondition, long? blocksetId, long[]? excludeBlocksets) + private static async Task CountWithCondition(SqliteCommand cmd, string tmpName, string alias, string baseCondition, long? blocksetId, long[]? excludeBlocksets) { var fullCondition = baseCondition; var blocksetCondition = GetBlocksetCondition(alias, blocksetId, excludeBlocksets); if (!string.IsNullOrEmpty(blocksetCondition)) fullCondition += " AND " + blocksetCondition; - var sql = LocalDatabase.FormatInvariant($@"SELECT COUNT(*) FROM ""{tmpName}"" {alias} WHERE {fullCondition}"); - return cmd.SetCommandAndParameters(sql).ExecuteScalarInt64(0); + var sql = $@" + SELECT COUNT(*) + FROM ""{tmpName}"" {alias} + WHERE {fullCondition} + "; + + return await cmd.SetCommandAndParameters(sql) + .ExecuteScalarInt64Async(0); } /// @@ -208,9 +266,9 @@ public static class ChangeStatistics private static string GetBlocksetCondition(string alias, long? blocksetId, long[]? exclude) { if (blocksetId.HasValue) - return LocalDatabase.FormatInvariant($"{alias}.\"BlocksetID\" = {blocksetId.Value}"); + return @$"{alias}.""BlocksetID"" = {blocksetId.Value}"; if (exclude?.Length > 0) - return LocalDatabase.FormatInvariant($"{alias}.\"BlocksetID\" NOT IN ({string.Join(",", exclude)})"); + return @$"{alias}.""BlocksetID"" NOT IN ({string.Join(",", exclude)})"; return string.Empty; } } \ No newline at end of file