
Some checks failed
Build / build (push) Failing after 1m1s
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.
648 lines
23 KiB
C#
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
|
|
}
|
|
} |