firefly/src/HashOperations.cs
Jacob Schmidt 2944eac2f8
Some checks failed
Build / build (push) Failing after 1m20s
feat: Implement automatic backup on data modification
Added automatic backup system that triggers saves when data is modified. Changes include:

- Added tracking of data modifications in BackupSystem.cs

- Added debounce timer (5s) to prevent excessive backups during rapid changes

- Modified string, list, and hash operations to track data changes

- Ensures data is saved shortly after modifications without impacting performance
2025-05-03 22:57:13 -05:00

276 lines
10 KiB
C#

using System.Text;
using System.Collections.Concurrent;
namespace Firefly
{
public partial class Firefly
{
#region Hash Operations
/// <summary>
/// Handles the HSET command which sets a field in a hash.
/// </summary>
/// <param name="args">Command arguments in format: "key field value"</param>
/// <returns>1 if the field was added, 0 if it was updated</returns>
static byte[] HandleHSetCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hset' command\r\n");
}
string key = parts[0];
string field = parts[1];
string value = parts.Length == 3 ? parts[2] : string.Join(" ", parts.Skip(2));
try
{
var hash = GetOrCreateHash(key);
bool isNewField = hash.TryAdd(field, value);
if (!isNewField)
hash[field] = value;
MarkDataAsModified();
return Encoding.UTF8.GetBytes($":{(isNewField ? 1 : 0)}\r\n");
}
catch (InvalidOperationException ex)
{
// Handle the case where the key already exists with a different type
string? existingType = GetKeyType(key);
if (existingType != null)
{
return Encoding.UTF8.GetBytes($"-ERR key '{key}' already exists as type '{existingType}'\r\n");
}
return Encoding.UTF8.GetBytes($"-ERR {ex.Message}\r\n");
}
}
/// <summary>
/// Handles the HGET command which retrieves the value of a field in a hash.
/// </summary>
/// <param name="args">Command arguments in format: "key field"</param>
/// <returns>The value of the field, or nil if the field doesn't exist</returns>
static byte[] HandleHGetCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hget' command\r\n");
}
string key = parts[0];
string field = parts[1];
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) &&
hash != null && hash.TryGetValue(field, out string? fieldValue))
{
return Encoding.UTF8.GetBytes($"+{fieldValue}\r\n");
}
return Encoding.UTF8.GetBytes("$-1\r\n");
}
/// <summary>
/// Handles the HDEL command which removes a field from a hash.
/// </summary>
/// <param name="args">Command arguments in format: "key field"</param>
/// <returns>1 if the field was removed, 0 if it didn't exist</returns>
static byte[] HandleHDelCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hdel' command\r\n");
}
string key = parts[0];
string field = parts[1];
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) && hash != null)
{
bool removed = hash.TryRemove(field, out _);
if (removed)
MarkDataAsModified();
if (hash.IsEmpty)
HashStoreRemove(key);
return Encoding.UTF8.GetBytes($":{(removed ? 1 : 0)}\r\n");
}
return Encoding.UTF8.GetBytes(":0\r\n");
}
/// <summary>
/// Handles the HEXISTS command which checks if a field exists in a hash.
/// </summary>
/// <param name="args">Command arguments in format: "key field"</param>
/// <returns>1 if the field exists, 0 if it doesn't</returns>
static byte[] HandleHExistsCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hexists' command\r\n");
}
string key = parts[0];
string field = parts[1];
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) &&
hash != null && hash.TryGetValue(field, out _))
{
return Encoding.UTF8.GetBytes(":1\r\n");
}
return Encoding.UTF8.GetBytes(":0\r\n");
}
/// <summary>
/// Handles the HGETALL command which retrieves all fields and values in a hash.
/// </summary>
/// <param name="args">Command arguments in format: "key"</param>
/// <returns>All fields and values in the hash, or nil if the hash doesn't exist</returns>
static byte[] HandleHGetAllCommand(string args)
{
if (string.IsNullOrWhiteSpace(args))
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hgetall' command\r\n");
}
string key = args.Trim();
if (HashStoreGet(key, out ConcurrentDictionary<string, string>? hash) &&
hash != null && !hash.IsEmpty)
{
var hashSnapshot = hash.ToArray();
StringBuilder response = new();
response.Append("*\r\n");
foreach (var kvp in hashSnapshot)
{
response.Append($"+{kvp.Key}\r\n");
response.Append($"+{kvp.Value}\r\n");
}
return Encoding.UTF8.GetBytes(response.ToString());
}
return Encoding.UTF8.GetBytes("*\r\n");
}
/// <summary>
/// Handles the HMSET command which sets multiple fields in a hash.
/// </summary>
/// <param name="args">Command arguments in format: "key field value [field value ...]"</param>
/// <returns>OK on success, error if arguments are invalid</returns>
static byte[] HandleHMSetCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3 || (parts.Length - 1) % 2 != 0)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'hmset' command\r\n");
}
string key = parts[0];
var hash = GetOrCreateHash(key);
try
{
for (int i = 1; i < parts.Length; i += 2)
{
string field = parts[i];
if (i + 1 < parts.Length)
{
string value = parts[i + 1];
if (value.StartsWith('"') && value.EndsWith('"') && value.Length >= 2)
{
value = value[1..^1];
}
hash[field] = value;
}
}
}
catch (Exception ex)
{
return Encoding.UTF8.GetBytes($"-ERR internal error: {ex.Message}\r\n");
}
MarkDataAsModified();
return Encoding.UTF8.GetBytes("+OK\r\n");
}
#region Hash Store Helpers
/// <summary>
/// Gets or creates a hash for a given key.
/// </summary>
/// <param name="key">The key to get or create the hash for</param>
/// <returns>The hash</returns>
private static ConcurrentDictionary<string, string> GetOrCreateHash(string key)
{
int shardIndex = GetShardIndex(key);
// If the key doesn't exist in this shard, check if it exists in any other store
if (!hashStoreShards[shardIndex].ContainsKey(key))
{
// Check if key exists in any other store
if (!EnsureKeyDoesNotExist(key, "hash"))
{
throw new InvalidOperationException($"Key '{key}' already exists with a different type");
}
}
var hash = hashStoreShards[shardIndex].GetOrAdd(key, _ => new ConcurrentDictionary<string, string>());
if (hash.IsEmpty)
MarkDataAsModified();
return hash;
}
/// <summary>
/// Checks if a hash exists for a given key.
/// </summary>
/// <param name="key">The key to check the hash for</param>
/// <returns>True if the hash exists, false otherwise</returns>
private static bool HashStoreExists(string key)
{
int shardIndex = GetShardIndex(key);
return hashStoreShards[shardIndex].ContainsKey(key);
}
/// <summary>
/// Gets a hash for a given key.
/// </summary>
/// <param name="key">The key to get the hash for</param>
/// <param name="value">The hash</param>
/// <returns>True if the hash was found, false otherwise</returns>
private static bool HashStoreGet(string key, out ConcurrentDictionary<string, string>? value)
{
int shardIndex = GetShardIndex(key);
return hashStoreShards[shardIndex].TryGetValue(key, out value);
}
/// <summary>
/// Removes a hash for a given key.
/// </summary>
/// <param name="key">The key to remove the hash for</param>
/// <returns>True if the hash was removed, false otherwise</returns>
private static bool HashStoreRemove(string key)
{
int shardIndex = GetShardIndex(key);
bool result = hashStoreShards[shardIndex].TryRemove(key, out _);
if (result)
MarkDataAsModified();
return result;
}
#endregion
#endregion
}
}