firefly/src/ListOperations.cs
Jacob Schmidt 44f6b40e79
Some checks failed
Build / build (push) Failing after 1m1s
feat: Implement LLEN command to get list length
This commit introduces the LLEN command, which allows users to retrieve the length of a list stored in the database.

- Added `HandleLLengthCommand` function to `ListOperations.cs` to handle the LLEN command logic. This function retrieves the list associated with the given key and returns its length. If the key does not exist, it returns 0.
- Updated `Firefly.cs` to include the LLEN command in the command processing logic, mapping the "LLEN" command to the `HandleLLengthCommand` function.
2025-04-13 16:49:05 -05:00

648 lines
23 KiB
C#

using System.Text;
namespace Firefly
{
public partial class Firefly
{
#region List Operations
/// <summary>
/// Handles the LPUSH command which adds an element to the left of a list.
/// </summary>
/// <param name="args">Command arguments in format: "key value"</param>
/// <returns>The length of the list after the push operation</returns>
static byte[] HandleLPushCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lpush' command\r\n");
}
string key = parts[0];
string value = parts[1];
try
{
// Use helper function to safely modify the list with write lock
int newLength = 0;
ListStoreWithWriteLock(key, list =>
{
list.Insert(0, value);
newLength = list.Count;
});
return Encoding.UTF8.GetBytes($":{newLength}\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 RPUSH command which adds values to the tail of a list.
/// </summary>
/// <param name="args">Command arguments in format: "key value1 [value2 ...]"</param>
/// <returns>Response indicating the new length of the list</returns>
static byte[] HandleRPushCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'rpush' command\r\n");
}
string key = parts[0];
List<string> result = [];
// Use helper function to safely modify the list with write lock
ListStoreWithWriteLock(key, list =>
{
// Add all values to the end of the list
for (int i = 1; i < parts.Length; i++)
{
list.Add(parts[i]);
}
result.Add(list.Count.ToString());
});
return Encoding.UTF8.GetBytes($":{result[0]}\r\n");
}
/// <summary>
/// Handles the LPOP command which removes and returns the first element of a list.
/// </summary>
/// <param name="args">Command arguments containing the key</param>
/// <returns>The popped value or nil if the list is empty</returns>
static byte[] HandleLPopCommand(string args)
{
if (string.IsNullOrWhiteSpace(args))
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lpop' command\r\n");
}
string key = args.Trim();
List<string> result = [];
// Use helper function to safely modify the list with write lock
ListStoreWithWriteLock(key, list =>
{
if (list.Count > 0)
{
// Remove and store the first element
string value = list[0];
list.RemoveAt(0);
result.Add(value);
// Clean up empty lists
if (list.Count == 0)
{
ListStoreRemove(key);
}
}
});
return result.Count > 0
? Encoding.UTF8.GetBytes($"+{result[0]}\r\n")
: Encoding.UTF8.GetBytes("$-1\r\n");
}
/// <summary>
/// Handles the RPOP command which removes and returns the last element of a list.
/// </summary>
/// <param name="args">Command arguments containing the key</param>
/// <returns>The popped value or nil if the list is empty</returns>
static byte[] HandleRPopCommand(string args)
{
if (string.IsNullOrWhiteSpace(args))
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'rpop' command\r\n");
}
string key = args.Trim();
List<string> result = [];
// Use helper function to safely modify the list with write lock
ListStoreWithWriteLock(key, list =>
{
if (list.Count > 0)
{
// Remove and store the last element
int lastIndex = list.Count - 1;
string value = list[lastIndex];
list.RemoveAt(lastIndex);
result.Add(value);
// Clean up empty lists
if (list.Count == 0)
{
ListStoreRemove(key);
}
}
});
return result.Count > 0
? Encoding.UTF8.GetBytes($"+{result[0]}\r\n")
: Encoding.UTF8.GetBytes("$-1\r\n");
}
/// <summary>
/// Handles the LLEN command which returns the length of a list.
/// </summary>
/// <param name="args">Command arguments containing the key</param>
/// <returns>The length of the list or 0 if the key does not exist</returns>
static byte[] HandleLLengthCommand(string args)
{
if (string.IsNullOrWhiteSpace(args))
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'llen' command\r\n");
}
string key = args.Trim();
int length = 0;
// Use helper function to safely read the list with read lock
ListStoreWithReadLock(key, list =>
{
length = list.Count;
});
return Encoding.UTF8.GetBytes($":{length}\r\n");
}
/// <summary>
/// Handles the LRANGE command which returns a range of elements from a list.
/// </summary>
/// <param name="args">Command arguments in format: "key start stop"</param>
/// <returns>Array of elements in the specified range</returns>
static byte[] HandleLRangeCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lrange' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int start) || !int.TryParse(parts[2], out int stop))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
// Use helper function to safely read the list with read lock
return ListStoreWithReadLock(key, list =>
{
if (list.Count == 0)
{
return Encoding.UTF8.GetBytes("*\r\n");
}
// Handle negative indices (counting from the end)
if (start < 0) start = list.Count + start;
if (stop < 0) stop = list.Count + stop;
// Ensure indices are within bounds
start = Math.Max(start, 0);
stop = Math.Min(stop, list.Count - 1);
if (start > stop)
{
return Encoding.UTF8.GetBytes("*\r\n");
}
// Build response with all elements in range
StringBuilder response = new();
response.Append("*\r\n");
for (int i = start; i <= stop; i++)
{
response.Append($"+{list[i]}\r\n");
}
return Encoding.UTF8.GetBytes(response.ToString());
}) ?? Encoding.UTF8.GetBytes("*\r\n");
}
/// <summary>
/// Handles the LINDEX command which returns an element from a list by its index.
/// </summary>
/// <param name="args">Command arguments in format: "key index"</param>
/// <returns>The element at the specified index or nil if not found</returns>
static byte[] HandleLIndexCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lindex' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int index))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
// Use helper function to safely read the list with read lock
return ListStoreWithReadLock(key, list =>
{
if (list.Count == 0)
{
return Encoding.UTF8.GetBytes("$-1\r\n");
}
// Handle negative index (counting from the end)
if (index < 0)
{
index = list.Count + index;
}
// Check if index is within bounds
if (index < 0 || index >= list.Count)
{
return Encoding.UTF8.GetBytes("$-1\r\n");
}
return Encoding.UTF8.GetBytes($"+{list[index]}\r\n");
}) ?? Encoding.UTF8.GetBytes("$-1\r\n");
}
/// <summary>
/// Handles the LSET command which sets the value of an element in a list by its index.
/// </summary>
/// <param name="args">Command arguments in format: "key index value"</param>
/// <returns>OK on success, error if index is out of range</returns>
static byte[] HandleLSetCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lset' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int index))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
string value = parts[2];
bool success = false;
// Use helper function to safely modify the list with write lock
ListStoreWithWriteLock(key, list =>
{
if (list.Count == 0)
{
return;
}
// Handle negative index (counting from the end)
if (index < 0)
{
index = list.Count + index;
}
// Check if index is within bounds
if (index < 0 || index >= list.Count)
{
return;
}
list[index] = value;
success = true;
});
return success
? Encoding.UTF8.GetBytes("+OK\r\n")
: Encoding.UTF8.GetBytes("-ERR index out of range\r\n");
}
/// <summary>
/// Handles the LPOS command which returns the position of an element in a list.
/// </summary>
/// <param name="args">Command arguments in format: "key element [RANK rank] [MAXLEN len]"</param>
/// <returns>The position of the element or nil if not found</returns>
static byte[] HandleLPosCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 2)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lpos' command\r\n");
}
string key = parts[0];
string element = parts[1];
// Optional parameters
int rank = 1; // Default to first occurrence
int maxlen = 0; // 0 means no limit
// Parse optional parameters
for (int i = 2; i < parts.Length; i++)
{
if (parts[i].Equals("RANK", StringComparison.OrdinalIgnoreCase) && i + 1 < parts.Length)
{
if (int.TryParse(parts[++i], out int parsedRank))
{
rank = parsedRank;
}
}
else if (parts[i].Equals("MAXLEN", StringComparison.OrdinalIgnoreCase) && i + 1 < parts.Length)
{
if (int.TryParse(parts[++i], out int parsedMaxlen))
{
maxlen = parsedMaxlen;
}
}
}
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterReadLock();
try
{
if (!listStoreShards[shardIndex].TryGetValue(key, out List<string>? list) || list == null)
{
return Encoding.UTF8.GetBytes("$-1\r\n"); // Key doesn't exist
}
int found = 0;
for (int i = 0; i < list.Count; i++)
{
if (maxlen > 0 && i >= maxlen)
{
break;
}
if (list[i] == element)
{
found++;
if (found == Math.Abs(rank))
{
return Encoding.UTF8.GetBytes($":{i}\r\n");
}
}
}
return Encoding.UTF8.GetBytes("$-1\r\n"); // Element not found
}
finally
{
listStoreLocks[shardIndex].ExitReadLock();
}
}
/// <summary>
/// Handles the LTRIM command which trims a list to the specified range.
/// </summary>
/// <param name="args">Command arguments in format: "key start stop"</param>
/// <returns>OK on success, error if arguments are invalid</returns>
static byte[] HandleLTrimCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'ltrim' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int start) || !int.TryParse(parts[2], out int stop))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterWriteLock();
try
{
if (!listStoreShards[shardIndex].TryGetValue(key, out List<string>? list) || list == null)
{
return Encoding.UTF8.GetBytes("+OK\r\n"); // Non-existent key is treated as empty list
}
// Handle negative indices
if (start < 0) start = list.Count + start;
if (stop < 0) stop = list.Count + stop;
// Normalize boundaries
start = Math.Max(start, 0);
stop = Math.Min(stop, list.Count - 1);
if (start > stop || start >= list.Count)
{
// Clear the list if range is invalid
list.Clear();
listStoreShards[shardIndex].TryRemove(key, out _);
}
else
{
// Calculate the new range
int newLength = stop - start + 1;
if (start > 0 || stop < list.Count - 1)
{
var trimmed = list.GetRange(start, newLength);
list.Clear();
list.AddRange(trimmed);
}
}
return Encoding.UTF8.GetBytes("+OK\r\n");
}
finally
{
listStoreLocks[shardIndex].ExitWriteLock();
}
}
/// <summary>
/// Handles the LREM command which removes elements equal to the given value from a list.
/// </summary>
/// <param name="args">Command arguments in format: "key count element"</param>
/// <returns>The number of removed elements</returns>
static byte[] HandleLRemCommand(string args)
{
string[] parts = SplitRespectingQuotes(args);
if (parts.Length < 3)
{
return Encoding.UTF8.GetBytes("-ERR wrong number of arguments for 'lrem' command\r\n");
}
string key = parts[0];
if (!int.TryParse(parts[1], out int count))
{
return Encoding.UTF8.GetBytes("-ERR value is not an integer or out of range\r\n");
}
string element = parts[2];
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterWriteLock();
try
{
if (!listStoreShards[shardIndex].TryGetValue(key, out List<string>? list) || list == null)
{
return Encoding.UTF8.GetBytes(":0\r\n"); // Key doesn't exist
}
int removed = 0;
if (count > 0)
{
// Remove count occurrences from head to tail
for (int i = 0; i < list.Count && removed < count; i++)
{
if (list[i] == element)
{
list.RemoveAt(i);
removed++;
i--; // Adjust index after removal
}
}
}
else if (count < 0)
{
// Remove |count| occurrences from tail to head
for (int i = list.Count - 1; i >= 0 && removed < -count; i--)
{
if (list[i] == element)
{
list.RemoveAt(i);
removed++;
}
}
}
else // count == 0
{
// Remove all occurrences
removed = list.RemoveAll(x => x == element);
}
// Remove the key if the list is empty
if (list.Count == 0)
{
listStoreShards[shardIndex].TryRemove(key, out _);
}
return Encoding.UTF8.GetBytes($":{removed}\r\n");
}
finally
{
listStoreLocks[shardIndex].ExitWriteLock();
}
}
#region List Store Helpers
/// <summary>
/// Gets or creates a list for a given key.
/// </summary>
/// <param name="key">The key to get or create the list for</param>
/// <returns>The list</returns>
private static List<string> GetOrCreateList(string key)
{
int shardIndex = GetShardIndex(key);
// If the key doesn't exist in this shard, check if it exists in any other store
if (!listStoreShards[shardIndex].ContainsKey(key))
{
// Check if key exists in any other store
if (!EnsureKeyDoesNotExist(key, "list"))
{
throw new InvalidOperationException($"Key '{key}' already exists with a different type");
}
}
return listStoreShards[shardIndex].GetOrAdd(key, _ => []);
}
/// <summary>
/// Checks if a list exists for a given key.
/// </summary>
/// <param name="key">The key to check the list for</param>
/// <returns>True if the list exists, false otherwise</returns>
private static bool ListStoreExists(string key)
{
int shardIndex = GetShardIndex(key);
return listStoreShards[shardIndex].ContainsKey(key);
}
/// <summary>
/// Gets a list for a given key.
/// </summary>
/// <param name="key">The key to get the list for</param>
/// <param name="value">The list</param>
/// <returns>True if the list was found, false otherwise</returns>
private static bool ListStoreGet(string key, out List<string>? value)
{
int shardIndex = GetShardIndex(key);
return listStoreShards[shardIndex].TryGetValue(key, out value);
}
/// <summary>
/// Removes a list for a given key.
/// </summary>
/// <param name="key">The key to remove the list for</param>
/// <returns>True if the list was removed, false otherwise</returns>
private static bool ListStoreRemove(string key)
{
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterWriteLock();
try
{
return listStoreShards[shardIndex].TryRemove(key, out _);
}
finally
{
listStoreLocks[shardIndex].ExitWriteLock();
}
}
/// <summary>
/// Executes an action with a write lock on a list for a given key.
/// </summary>
/// <param name="key">The key to execute the action on</param>
/// <param name="action">The action to execute</param>
private static void ListStoreWithWriteLock(string key, Action<List<string>> action)
{
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterWriteLock();
try
{
var list = GetOrCreateList(key);
action(list);
}
finally
{
listStoreLocks[shardIndex].ExitWriteLock();
}
}
/// <summary>
/// Executes an action with a read lock on a list for a given key.
/// </summary>
/// <typeparam name="T">The type of the result</typeparam>
/// <param name="key">The key to execute the action on</param>
/// <param name="action">The action to execute</param>
private static T ListStoreWithReadLock<T>(string key, Func<List<string>, T> action)
{
int shardIndex = GetShardIndex(key);
listStoreLocks[shardIndex].EnterReadLock();
try
{
if (ListStoreGet(key, out List<string>? list) && list != null)
{
return action(list);
}
return default!;
}
finally
{
listStoreLocks[shardIndex].ExitReadLock();
}
}
#endregion
#endregion
}
}