using System;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using System.Diagnostics;
using System.Data;
namespace SpacetimeDB
{
///
/// A dictionary that may have multiple copies of a key-value pair.
/// Note that a particular key only maps to one value -- it is a logical error
/// to insert the same key with different values.
///
/// You MUST use the MultiDictionary(IEqualityComparer keyComparer, IEqualityComparer valueComparer)
/// constructor to construct this -- it is a struct for performance reasons, but the default constructor creates an invalid state.
///
///
///
internal struct MultiDictionary : IEquatable>
{
// The actual data.
readonly Dictionary RawDict;
readonly IEqualityComparer ValueComparer;
///
/// Construct a MultiDictionary.
///
/// This is the only valid constructor for a Multidictionary - using the parameterless constructor
/// will result in null pointer errors. But we can't enforce this because of Unity.
///
///
public MultiDictionary(IEqualityComparer keyComparer, IEqualityComparer valueComparer)
{
RawDict = new(keyComparer);
ValueComparer = valueComparer;
}
public static MultiDictionary FromEnumerable(IEnumerable> enumerable, IEqualityComparer keyComparer, IEqualityComparer valueComparer)
{
var result = new MultiDictionary(keyComparer, valueComparer);
foreach (var item in enumerable)
{
result.Add(item.Key, item.Value);
}
return result;
}
///
/// Return the count WITHOUT multiplicities.
/// This is mathematically unnatural, but cheap.
///
public readonly uint CountDistinct => (uint)RawDict.Count;
///
/// Return the count WITH multiplicities.
///
public readonly uint Count => RawDict.Select(item => item.Value.Multiplicity).Aggregate(0u, (a, b) => a + b);
///
/// Add a key-value-pair to the multidictionary.
/// If the key is already present, its associated value must satisfy
/// keyComparer.Equals(value, item.Value).
///
///
/// Whether the key is entirely new to the dictionary. If it was already present, we assert that the old value is equal to the new value.
public bool Add(TKey key, TValue value)
{
if (value == null)
{
throw new NullReferenceException("Null values are forbidden in multidictionary");
}
Debug.Assert(RawDict != null);
Debug.Assert(key != null);
if (RawDict.TryGetValue(key, out var result))
{
Debug.Assert(ValueComparer.Equals(value, result.Value));
RawDict[key] = (value, result.Multiplicity + 1);
return false;
}
else
{
RawDict[key] = (value, 1);
return true;
}
}
///
/// Completely clear the multidictionary.
///
public void Clear()
{
RawDict.Clear();
}
///
/// Whether the multidictionary contains any copies of an item.
///
///
///
public bool Contains(KeyValuePair item)
{
if (RawDict.TryGetValue(item.Key, out var result))
{
return ValueComparer.Equals(item.Value, result.Value);
}
return false;
}
///
/// The number of times a key occurs in the multidictionary.
/// All of these occurrences must map to the same value.
///
///
///
public uint Multiplicity(TKey key) => RawDict.ContainsKey(key) ? RawDict[key].Multiplicity : 0;
///
/// The value associated with a key.
///
/// Throws if the key is not present.
///
///
///
public TValue this[TKey key] => RawDict[key].Value;
///
/// Remove a key from the dictionary.
///
///
/// Whether the last copy of the key was removed.
public bool Remove(TKey key, out TValue row)
{
if (RawDict.TryGetValue(key, out var result))
{
row = result.Value;
if (result.Multiplicity == 1)
{
RawDict.Remove(key);
return true;
}
else
{
RawDict[key] = (result.Value, result.Multiplicity - 1);
return false;
}
}
row = default!; // uhh, this might be null. Good thing it's an internal method?
return false;
}
public bool Equals(MultiDictionary other)
{
foreach (var item in RawDict)
{
var (key, (value, multiplicity)) = item;
if (other.RawDict.TryGetValue(key, out var otherVM))
{
var (otherValue, otherMultiplicity) = otherVM;
if (!(ValueComparer.Equals(value, otherValue) && multiplicity == otherMultiplicity))
{
return false;
}
}
}
return true;
}
public readonly IEnumerable Values
{
get
{
return RawDict.Select(item => item.Value.Value);
}
}
public readonly IEnumerable> Entries
{
get
{
return RawDict.Select(item => new KeyValuePair(item.Key, item.Value.Value));
}
}
///
/// Iterate the rows that will be removed when `delta` is applied.
///
///
///
public readonly IEnumerable> WillRemove(MultiDictionaryDelta delta)
{
var self = this;
return delta.Entries.Where(their =>
{
if (their.Value.IsValueChange)
{
// Value changes are translated to Updates, not removals.
return false;
}
var theirNonValueChange = their.Value.NonValueChange;
if (theirNonValueChange.Delta >= 0)
{
// Adds can't result in removals.
return false;
}
if (self.RawDict.TryGetValue(their.Key, out var mine))
{
var resultMultiplicity = (int)mine.Multiplicity + theirNonValueChange.Delta;
return resultMultiplicity <= 0; // if < 0, we have a problem, but that's caught in Apply.
}
else
{
Log.Warn($"Want to remove row with key {their.Key}, but it doesn't exist!");
return false;
}
}).Select(entry => new KeyValuePair(entry.Key, entry.Value.NonValueChange.Value));
}
///
/// Apply a collection of changes to a multidictionary.
///
/// The changes to apply.
/// Will be populated with inserted KVPs.
/// Will be populated with updated KVPs.
/// Will be populated with removed KVPs.
public void Apply(MultiDictionaryDelta delta, List> wasInserted, List<(TKey Key, TValue OldValue, TValue NewValue)> wasUpdated, List> wasRemoved)
{
foreach (var (key, their) in delta.Entries)
{
if (RawDict.TryGetValue(key, out var my))
{
if (their.IsValueChange)
{
// Their update expects to change the value associated with this key.
var (before, after) = their.ValueChange;
Debug.Assert(ValueComparer.Equals(my.Value, before.Value));
var reducedMultiplicity = (int)my.Multiplicity + before.Delta;
if (reducedMultiplicity != 0)
{
PseudoThrow($"Attempted to apply {their} to {my}, but this resulted in a multiplicity of {reducedMultiplicity}, failing to correctly remove the row before applying the update");
}
RawDict[key] = (after.Value, (uint)after.Delta);
wasUpdated.Add((key, my.Value, after.Value));
}
else
{ // !their.IsValueChange
// Their update does not expect to change the value associated with this key.
// However, it may remove the key-value pair entirely.
var theirDelta = their.NonValueChange;
Debug.Assert(ValueComparer.Equals(my.Value, theirDelta.Value));
var newMultiplicity = (int)my.Multiplicity + theirDelta.Delta;
if (newMultiplicity > 0)
{
// Update the count, NOT dispatching an update event.
// It sort of matters if we use my.Value or their.Value here:
// They may satisfy `Equals` but not actually have equal pointers.
// We'd prefer to keep pointers stable if they don't need to change.
// So even though my.Value and theirValue are "equal", prefer using my.Value.
RawDict[key] = (my.Value, (uint)newMultiplicity);
}
else // if (newMultiplicity <= 0)
{
// This is a removal.
if (newMultiplicity < 0)
{
PseudoThrow($"Internal error: Removing row with key {key} {-theirDelta.Delta} times, but it is only present {my.Multiplicity} times.");
}
RawDict.Remove(key);
wasRemoved.Add(new(key, theirDelta.Value));
}
}
}
else
{
// Key is not present in map.
if (their.IsValueChange)
{
PseudoThrow($"Internal error: Can't perform a value change on a nonexistent key {key} (change: {their}).");
}
else
{
var theirDelta = their.NonValueChange;
if (theirDelta.Delta == 0)
{
// Hmm.
// This is not actually a problem.
// Do nothing.
}
else if (theirDelta.Delta < 0)
{
PseudoThrow($"Internal error: Can't remove nonexistent key {theirDelta.Value}");
}
else
{
RawDict[key] = (theirDelta.Value, (uint)theirDelta.Delta);
wasInserted.Add(new(key, theirDelta.Value));
}
}
}
}
}
///
/// Raise a debug assertion failure in debug mode, otherwise just warn and keep going.
///
///
private void PseudoThrow(string message)
{
Log.Warn(message);
Debug.Assert(false, message);
}
public override string ToString()
{
StringBuilder result = new();
result.Append("SpacetimeDB.MultiDictionary { ");
foreach (var item in RawDict)
{
result.Append($"({item.Key}: {item.Value.Value}) x {item.Value.Multiplicity}, ");
}
result.Append("}");
return result.ToString();
}
}
///
/// A bulk change to a multidictionary. Allows both adding and removing rows.
///
/// You MUST use the MultiDictionaryDelta(IEqualityComparer keyComparer, IEqualityComparer valueComparer)
/// to construct this -- it is a struct for performance reasons, but the default constructor creates an invalid collection!
///
/// Can be applied to a multidictionary, and also inspected before application to see
/// what rows will be deleted. (This is used for OnBeforeDelete.)
///
/// The order of operations applied to a MultiDictionaryDelta does not matter.
/// No matter the order of Add and Remove calls on a delta, when the Delta is applied,
/// the result will be the same, as long as the Add and Remove *counts* for each KeyValuePair are
/// the same.
/// (This means that this is a "conflict-free replicated data type", unlike MultiDictionary.)
/// (MultiDictionary would also be "conflict-free" if it didn't support Remove.)
///
/// The delta may include value updates.
/// When applied, the delta must maintain the invariant of MultiDictionary that each key maps to exactly one value.
/// For example, if the target dictionary has the state:
/// (k1: v1) (k1: v1)
/// Then a delta must remove both of these key-value pairs if it wishes to assign a new value to k1.
///
/// Each key can be associated with at most two values in a MultiDictionaryDelta.
/// For example, -(k1: v1) +(k1: v2) -(k1: v2) +(k1: v3) is NOT a valid MultiDictionaryDelta.
///
/// When removing a row for an update, it is legal for the passed value to be equal to EITHER the old value or the new value.
/// (This is because I'm not sure what SpacetimeDB core does.)
///
///
///
internal struct MultiDictionaryDelta : IEquatable>
{
///
/// A change to an individual value associated to a key.
///
public struct ValueDelta
{
///
/// The value stored.
///
public TValue Value;
///
/// The change in multiplicity of the value.
///
public int Delta;
public ValueDelta(TValue Value, int Delta)
{
this.Value = Value;
this.Delta = Delta;
}
public override string ToString()
{
return $"{Value} x ({Delta})";
}
public bool Equals(ValueDelta other, IEqualityComparer equalityComparer) =>
equalityComparer.Equals(Value, other.Value) && Delta == other.Delta;
}
///
/// A change to a key-value pair.
///
/// - If the value associated to the key does not change, then .IsValueChange == false;
/// the key-value pair can only have multiplicity changes.
/// Use .NonValueChange to get at this multiplicity information.
///
/// - If the value associated to the key does change, then .IsValueChange == true;
/// use .ValueChange to get the values before and after the change, together with multiplicity information.
///
public struct KeyDelta
{
// The (one | two) ValueDeltas associated with this key.
//
// You can think of this struct as a HashSet with signed multiplicities that stores at most two values.
//
// While we are accumulating changes to this key, we don't know which of the values we've
// seen so far is the old value, and which is the new value.
// (We can't look incoming values up in the existing dictionary for thread-safety reasons.)
//
// Once all changes are accumulated, we expect either:
// - There is only one value (NonValueChange)
// - There are two values:
// - One with (-), one with (+) delta (ValueChange)
// - One with 0, one with non-0 delta (This is odd, but we treat it as NonValueChange.)
// - Both with the same delta sign. This represents an unrecoverable error, so we throw an exception.
//
// Invariant: If D2 is present, its (signed) Delta should be >= the Delta for D1.
// This is enforced by the Normalize method, which should be called after
// any other method that modifies this struct. This ensures that D1 holds the old value,
// and D2 holds the new value. But this guarantee is only present after all changes have been accumulated.
ValueDelta D1;
ValueDelta? D2;
private void Normalize()
{
if (D2 != null && D2.Value.Delta < D1.Delta)
{
var tmp = D2.Value;
D2 = D1;
D1 = tmp;
}
}
///
/// Construct a KeyDelta from a single ValueDelta.
///
///
public KeyDelta(ValueDelta delta)
{
D1 = delta;
D2 = null;
}
///
/// If this KeyDelta is a value change -- that is, it removes one value some number of times and adds another some number of times.
/// (If it isn't a value change, it just will consist of adds or removes for a single value).
///
public bool IsValueChange
{
get => D2 != null && D1.Delta < 0 && D2.Value.Delta > 0;
}
///
/// The deltas in the case of this KeyDelta being a value change.
/// Guarantees Before.Delta < 0 && After.Delta > 0.
///
public (ValueDelta Before, ValueDelta After) ValueChange
{
get
{
if (!IsValueChange)
{
throw new InvalidOperationException($"KeyDelta {this} is not a ValueChange");
}
return (D1, D2!.Value);
}
}
///
/// If !IsUpdate, this gives you the single relevant ValueDelta for this key.
///
public ValueDelta NonValueChange
{
get
{
if (IsValueChange)
{
throw new Exception($"KeyDelta {this} is a ValueChange");
}
if (D2 == null)
{
return D1;
}
else
{
// Now we're in a weird place.
// We're not an update, but D2 is initialized.
// This means that at least one of D1 or D2 has Delta == 0.
// If exactly one of them has Delta == 0, we're okay:
if (D1.Delta == 0 && D2.Value.Delta != 0)
{
return D2.Value;
}
else if (D1.Delta != 0 && D2.Value.Delta == 0)
{
return D1;
}
else
{
// In this case, something strange is going on: both values have the same sign.
// There's nothing sensible to do here, and this represents a server-side error, so just throw.
throw new InvalidOperationException($"Called NonValueChange on a ValueDelta in an ambiguous state: {this}");
}
}
}
}
///
/// Add a copy of this Value to this KeyDelta.
///
///
///
public void Add(TValue value, IEqualityComparer equalityComparer)
{
if (equalityComparer.Equals(value, D1.Value))
{
D1.Delta += 1;
}
else if (D2 == null)
{
D2 = new(value, +1);
}
else
{
var d2 = D2.Value;
Debug.Assert(equalityComparer.Equals(value, d2.Value));
// In release mode, the above assert won't fire.
// This means that if we get more than two values, the first two distinct values seen
// will be the ones stored in the KeyDelta.
d2.Delta += 1;
D2 = d2;
}
Normalize();
}
///
/// Remove a copy of this Value from this KeyDelta.
///
///
///
public void Remove(TValue value, IEqualityComparer equalityComparer)
{
if (equalityComparer.Equals(value, D1.Value))
{
D1.Delta -= 1;
}
else if (D2 == null)
{
D2 = new(value, -1);
}
else
{
var newD2 = D2.Value;
Debug.Assert(equalityComparer.Equals(value, newD2.Value));
// In release mode, the above assert won't fire.
// This means that if we get more than two values, the first two distinct values seen
// will be the ones stored in the KeyDelta.
newD2.Delta -= 1;
D2 = newD2;
}
Normalize();
}
///
/// Check if two KeyDeltas are equal.
///
/// If either is in an invalid state, this throws.
/// That means you should avoid comparing KeyDeltas
/// (and therefore MultiDictionaryDeltas) unless you are sure they are valid, i.e. until they have
/// absorbed all the necessary Adds and Removes.
///
///
///
///
public bool Equals(KeyDelta other, IEqualityComparer equalityComparer)
{
if (IsValueChange != other.IsValueChange) return false;
if (IsValueChange)
{
var asUpdate = ValueChange;
var otherAsUpdate = other.ValueChange;
return asUpdate.Before.Equals(otherAsUpdate.Before, equalityComparer) &&
asUpdate.After.Equals(otherAsUpdate.After, equalityComparer);
}
else
{
return NonValueChange.Equals(other.NonValueChange, equalityComparer);
}
}
public override string ToString()
{
if (D2 == null)
{
return D1.ToString();
}
else
{
return $"({D1}, {D2})";
}
}
}
///
/// For each key, track its value (or its old and new values).
/// Also track the deltas associated to the values for this key.
///
readonly Dictionary RawDict;
readonly IEqualityComparer ValueComparer;
///
/// Construct a MultiDictionaryDelta.
///
/// This is the ONLY valid constructor for a MultiDictionaryDelta - using the parameterless constructor
/// will result in null pointer errors. But we can't enforce this because of Unity.
///
///
public MultiDictionaryDelta(IEqualityComparer keyComparer, IEqualityComparer valueComparer)
{
RawDict = new(keyComparer);
ValueComparer = valueComparer;
}
///
/// Add a key-value-pair to the multidictionary.
/// If the key is already present, its associated value must satisfy
/// keyComparer.Equals(value, item.Value).
///
///
public void Add(TKey key, TValue value)
{
if (value == null)
{
throw new NullReferenceException("Null values are forbidden in multidictionary");
}
Debug.Assert(RawDict != null);
Debug.Assert(key != null);
KeyDelta result;
if (RawDict.TryGetValue(key, out result))
{
result.Add(value, ValueComparer);
}
else
{
result = new(new(value, +1));
}
RawDict[key] = result;
}
///
/// Completely clear the multidictionary.
///
public void Clear()
{
RawDict.Clear();
}
///
/// Remove a key from the dictionary.
///
///
public void Remove(TKey key, TValue value)
{
KeyDelta result;
if (RawDict.TryGetValue(key, out result))
{
result.Remove(value, ValueComparer);
}
else
{
result = new(new(value, -1));
}
RawDict[key] = result;
}
public override string ToString()
{
StringBuilder result = new();
result.Append("SpacetimeDB.MultiDictionaryDelta { ");
foreach (var item in RawDict)
{
result.Append($"({item.Key}: {item.Value}, ");
}
result.Append("}");
return result.ToString();
}
public bool Equals(MultiDictionaryDelta other)
{
foreach (var item in RawDict)
{
var (key, my) = item;
if (other.RawDict.TryGetValue(key, out var their))
{
if (!their.Equals(my, ValueComparer))
{
return false;
}
}
else
{
return false;
}
}
return true;
}
public readonly IEnumerable> Entries
{
get
{
return RawDict;
}
}
}
}