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