Updated ChangeStatistics to use Microsoft.Data.Sqlite and be async
This commit is contained in:
@@ -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
|
||||
/// <param name="results">The results object to update with the statistics</param>
|
||||
/// <param name="currentFilesetId">The ID of the current fileset</param>
|
||||
/// <param name="previousFilesetId">The ID of the previous fileset</param>
|
||||
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
|
||||
/// <param name="blocksetId">The ID of the blockset to count, or null for all</param>
|
||||
/// <param name="excludeBlocksets">The blocksets to exclude from the count</param>
|
||||
/// <returns>The number of added files or folders</returns>
|
||||
private static long CountAdded(IDbCommand cmd, string tmpName, long? blocksetId, long[]? excludeBlocksets = null)
|
||||
private static async Task<long> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -124,16 +158,21 @@ public static class ChangeStatistics
|
||||
/// <param name="blocksetId">The ID of the blockset to count, or null for all</param>
|
||||
/// <param name="excludeBlocksets">The blocksets to exclude from the count</param>
|
||||
/// <returns>The number of deleted files or folders</returns>
|
||||
private static long CountDeleted(IDbCommand cmd, string tmpName, long? blocksetId, long[]? excludeBlocksets = null)
|
||||
private static async Task<long> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -145,23 +184,32 @@ public static class ChangeStatistics
|
||||
/// <param name="hash1">The hash column to compare</param>
|
||||
/// <param name="excludeBlocksets">The blocksets to exclude from the count</param>
|
||||
/// <returns>The number of modified files or folders</returns>
|
||||
private static long CountModified(IDbCommand cmd, string tmpName, long? blocksetId, long[]? excludeBlocksets = null)
|
||||
private static async Task<long> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -187,15 +239,21 @@ public static class ChangeStatistics
|
||||
/// <param name="blocksetId">The ID of the blockset to count, or null for all</param>
|
||||
/// <param name="excludeBlocksets">The blocksets to exclude from the count</param>
|
||||
/// <returns>The number of files or folders that match the condition</returns>
|
||||
private static long CountWithCondition(IDbCommand cmd, string tmpName, string alias, string baseCondition, long? blocksetId, long[]? excludeBlocksets)
|
||||
private static async Task<long> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user