From 2b485b2c78a5756e67223ec15b3e4f898ad25f69 Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Tue, 23 Dec 2025 13:55:00 +0200 Subject: [PATCH 1/2] Use System.HashCode instead of JSONConvert.SerializeObject for GetHashCode and Equals --- .../Sync/Bucket/BucketStorageAdapter.cs | 13 +++-- .../PowerSync.Common/DB/Crud/CrudEntry.cs | 20 ++++++-- .../PowerSync.Common/PowerSync.Common.csproj | 11 ++++- .../PowerSync.Common/Utils/CompareUtils.cs | 43 +++++++++++++++++ PowerSync/PowerSync.Common/Utils/HashUtils.cs | 47 +++++++++++++++++++ 5 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 PowerSync/PowerSync.Common/Utils/CompareUtils.cs create mode 100644 PowerSync/PowerSync.Common/Utils/HashUtils.cs diff --git a/PowerSync/PowerSync.Common/Client/Sync/Bucket/BucketStorageAdapter.cs b/PowerSync/PowerSync.Common/Client/Sync/Bucket/BucketStorageAdapter.cs index 4611bb2..dd590b2 100644 --- a/PowerSync/PowerSync.Common/Client/Sync/Bucket/BucketStorageAdapter.cs +++ b/PowerSync/PowerSync.Common/Client/Sync/Bucket/BucketStorageAdapter.cs @@ -2,6 +2,7 @@ namespace PowerSync.Common.Client.Sync.Bucket; using System; +using System.Linq; using System.Threading.Tasks; using PowerSync.Common.DB.Crud; @@ -54,12 +55,18 @@ public class SyncLocalDatabaseResult public override bool Equals(object? obj) { if (obj is not SyncLocalDatabaseResult other) return false; - return JsonConvert.SerializeObject(this) == JsonConvert.SerializeObject(other); + return Ready == other.Ready + && CheckpointValid == other.CheckpointValid + && CompareUtils.ArraysEqual(CheckpointFailures, other.CheckpointFailures); } public override int GetHashCode() { - return JsonConvert.SerializeObject(this).GetHashCode(); + return HashCode.Combine( + Ready, + CheckpointValid, + HashUtils.GetHashCodeArray(CheckpointFailures) + ); } } @@ -129,4 +136,4 @@ public interface IBucketStorageAdapter : IEventStream /// Invokes the `powersync_control` function for the sync client. /// Task Control(string op, object? payload); -} \ No newline at end of file +} diff --git a/PowerSync/PowerSync.Common/DB/Crud/CrudEntry.cs b/PowerSync/PowerSync.Common/DB/Crud/CrudEntry.cs index 5fe8f02..bea01b2 100644 --- a/PowerSync/PowerSync.Common/DB/Crud/CrudEntry.cs +++ b/PowerSync/PowerSync.Common/DB/Crud/CrudEntry.cs @@ -3,6 +3,8 @@ namespace PowerSync.Common.DB.Crud; using System.Collections.Generic; using Newtonsoft.Json; +using PowerSync.Common.Utils; + public enum UpdateType { [JsonProperty("PUT")] @@ -90,11 +92,23 @@ public static CrudEntry FromRow(CrudEntryJSON dbRow) public override bool Equals(object? obj) { if (obj is not CrudEntry other) return false; - return JsonConvert.SerializeObject(this) == JsonConvert.SerializeObject(other); + return ClientId == other.ClientId + && Id == other.Id + && Op == other.Op + && TransactionId == other.TransactionId + && Table == other.Table + && CompareUtils.DictionariesEqual(OpData, other.OpData); } public override int GetHashCode() { - return JsonConvert.SerializeObject(this).GetHashCode(); + return HashCode.Combine( + ClientId, + Id, + Op, + Table, + TransactionId, + HashUtils.GetHashCodeDictionary(OpData) + ); } -} \ No newline at end of file +} diff --git a/PowerSync/PowerSync.Common/PowerSync.Common.csproj b/PowerSync/PowerSync.Common/PowerSync.Common.csproj index abb53de..64dd646 100644 --- a/PowerSync/PowerSync.Common/PowerSync.Common.csproj +++ b/PowerSync/PowerSync.Common/PowerSync.Common.csproj @@ -31,6 +31,13 @@ + + + + 6.0.0 + + + @@ -50,7 +57,7 @@ - - + + diff --git a/PowerSync/PowerSync.Common/Utils/CompareUtils.cs b/PowerSync/PowerSync.Common/Utils/CompareUtils.cs new file mode 100644 index 0000000..0dfe36e --- /dev/null +++ b/PowerSync/PowerSync.Common/Utils/CompareUtils.cs @@ -0,0 +1,43 @@ +namespace PowerSync.Common.Utils; + +using System.Collections.Generic; +using System.Linq; + +public static class CompareUtils +{ + /// + /// Compare two dictionaries by value. Checks if both dictionaries have both the same + /// number of keys, as well as the same keys pointing to the same values. + /// + public static bool DictionariesEqual(Dictionary? dict1, Dictionary? dict2) + { + if (ReferenceEquals(dict1, dict2)) return true; + if (dict1 == null || dict2 == null) return false; + if (dict1.Count != dict2.Count) return false; + + var comparer = EqualityComparer.Default; + + foreach (var keyValuePair in dict1) + { + if (!dict2.TryGetValue(keyValuePair.Key, out TValue secondValue) || + !comparer.Equals(keyValuePair.Value, secondValue)) + { + return false; + } + } + + return true; + } + + /// + /// Check if two (maybe null) arrays contain the same elements in the same order. + /// Effectively arr1.SequenceEqual(arr2), but with null checks. + /// + public static bool ArraysEqual(TValue[]? arr1, TValue[]? arr2) + { + if (ReferenceEquals(arr1, arr2)) return true; + if (arr1 == null || arr2 == null) return false; + return arr1.SequenceEqual(arr2); + } +} + diff --git a/PowerSync/PowerSync.Common/Utils/HashUtils.cs b/PowerSync/PowerSync.Common/Utils/HashUtils.cs new file mode 100644 index 0000000..ba258e9 --- /dev/null +++ b/PowerSync/PowerSync.Common/Utils/HashUtils.cs @@ -0,0 +1,47 @@ +namespace PowerSync.Common.Utils; + +using System; +using System.Collections.Generic; + +public static class HashUtils +{ + /// + /// Create a hash from the key-value pairs in a dictionary. The hash does not depend + /// on the internal pair ordering of the dictionary. + /// + public static int GetHashCodeDictionary(Dictionary? dict) + { + if (dict == null) + { + return 0; + } + + // Use integer hash because order matters with System.HashCode + int hash = 0; + foreach (var kvp in dict) + { + // Combine hashes with XOR so that order doesn't matter + hash ^= HashCode.Combine(kvp.Key, kvp.Value); + } + return hash; + } + + /// + /// Create a hash from an array of values. The hash depends on the order of + /// elements in the array. + /// + public static int GetHashCodeArray(TValue[]? values) + { + if (values == null) + { + return 0; + } + + HashCode hash = new HashCode(); + foreach (var value in values) + { + hash.Add(value); + } + return hash.ToHashCode(); + } +} From aeeba6a3963c5bf5289094248f972dcdf0026705 Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Mon, 5 Jan 2026 12:14:21 +0200 Subject: [PATCH 2/2] Add null constraints to satisfy linter --- PowerSync/PowerSync.Common/Utils/CompareUtils.cs | 4 ++-- PowerSync/PowerSync.Common/Utils/HashUtils.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PowerSync/PowerSync.Common/Utils/CompareUtils.cs b/PowerSync/PowerSync.Common/Utils/CompareUtils.cs index 0dfe36e..4bfa8c2 100644 --- a/PowerSync/PowerSync.Common/Utils/CompareUtils.cs +++ b/PowerSync/PowerSync.Common/Utils/CompareUtils.cs @@ -9,7 +9,7 @@ public static class CompareUtils /// Compare two dictionaries by value. Checks if both dictionaries have both the same /// number of keys, as well as the same keys pointing to the same values. /// - public static bool DictionariesEqual(Dictionary? dict1, Dictionary? dict2) + public static bool DictionariesEqual(Dictionary? dict1, Dictionary? dict2) where TKey : notnull { if (ReferenceEquals(dict1, dict2)) return true; if (dict1 == null || dict2 == null) return false; @@ -19,7 +19,7 @@ public static bool DictionariesEqual(Dictionary? dic foreach (var keyValuePair in dict1) { - if (!dict2.TryGetValue(keyValuePair.Key, out TValue secondValue) || + if (!dict2.TryGetValue(keyValuePair.Key, out TValue? secondValue) || !comparer.Equals(keyValuePair.Value, secondValue)) { return false; diff --git a/PowerSync/PowerSync.Common/Utils/HashUtils.cs b/PowerSync/PowerSync.Common/Utils/HashUtils.cs index ba258e9..0e31fb1 100644 --- a/PowerSync/PowerSync.Common/Utils/HashUtils.cs +++ b/PowerSync/PowerSync.Common/Utils/HashUtils.cs @@ -9,7 +9,7 @@ public static class HashUtils /// Create a hash from the key-value pairs in a dictionary. The hash does not depend /// on the internal pair ordering of the dictionary. /// - public static int GetHashCodeDictionary(Dictionary? dict) + public static int GetHashCodeDictionary(Dictionary? dict) where TKey : notnull { if (dict == null) {