//! Redis string commands //! //! Strings are the most basic Redis value type. Despite the name, they can hold //! any binary data up to 512MB. Common uses: //! - Caching (SET/GET with TTL) //! - Counters (INCR/DECR) //! - Bit operations (SETBIT/GETBIT) //! - Simple key-value storage //! //! ## Examples //! //! ```zig //! try client.set("name", "alice"); //! const name = try client.get("name"); // "alice" //! //! try client.setEx("session", "data", 3600); // expires in 1 hour //! //! try client.set("counter", "0"); //! const n = try client.incr("counter"); // 1 //! const m = try client.incrBy("counter", 10); // 11 //! ``` const std = @import("std"); const Client = @import("../client.zig").Client; const Value = @import("../resp.zig").Value; const CommandError = @import("../resp.zig").CommandError; const ClientError = @import("../resp.zig").ClientError; /// String command implementations. /// These are implemented as methods that take a Client pointer. pub const StringCommands = struct { client: *Client, pub fn init(client: *Client) StringCommands { return .{ .client = client }; } // ======================================================================== // Basic Operations // ======================================================================== /// GET key - retrieve string value /// Returns null if key doesn't exist. pub fn get(self: *StringCommands, key: []const u8) ClientError!?[]const u8 { const result = try self.client.sendCommand(&.{ "GET", key }); return switch (result) { .bulk => |b| b, .nil => null, .err => return CommandError.RedisError, else => null, }; } /// SET key value - store string value pub fn set(self: *StringCommands, key: []const u8, value: []const u8) ClientError!void { const result = try self.client.sendCommand(&.{ "SET", key, value }); if (result.isError()) return CommandError.RedisError; } /// SET key value NX - set only if key doesn't exist /// Returns true if set, false if key existed. pub fn setNx(self: *StringCommands, key: []const u8, value: []const u8) ClientError!bool { const result = try self.client.sendCommand(&.{ "SET", key, value, "NX" }); return switch (result) { .string => true, .nil => false, .err => return CommandError.RedisError, else => false, }; } /// SET key value XX - set only if key exists /// Returns true if set, false if key didn't exist. pub fn setXx(self: *StringCommands, key: []const u8, value: []const u8) ClientError!bool { const result = try self.client.sendCommand(&.{ "SET", key, value, "XX" }); return switch (result) { .string => true, .nil => false, .err => return CommandError.RedisError, else => false, }; } /// SET key value EX seconds - set with TTL in seconds pub fn setEx(self: *StringCommands, key: []const u8, value: []const u8, seconds: u32) ClientError!void { var buf: [16]u8 = undefined; const sec_str = std.fmt.bufPrint(&buf, "{d}", .{seconds}) catch unreachable; const result = try self.client.sendCommand(&.{ "SET", key, value, "EX", sec_str }); if (result.isError()) return CommandError.RedisError; } /// SET key value PX milliseconds - set with TTL in milliseconds pub fn setPx(self: *StringCommands, key: []const u8, value: []const u8, millis: u64) ClientError!void { var buf: [24]u8 = undefined; const ms_str = std.fmt.bufPrint(&buf, "{d}", .{millis}) catch unreachable; const result = try self.client.sendCommand(&.{ "SET", key, value, "PX", ms_str }); if (result.isError()) return CommandError.RedisError; } /// GETSET key value - set new value and return old value (deprecated, use GETEX) pub fn getSet(self: *StringCommands, key: []const u8, value: []const u8) ClientError!?[]const u8 { const result = try self.client.sendCommand(&.{ "GETSET", key, value }); return switch (result) { .bulk => |b| b, .nil => null, .err => return CommandError.RedisError, else => null, }; } /// MGET key [key ...] - get multiple values /// Returns array of values (null for missing keys). pub fn mget(self: *StringCommands, keys: []const []const u8) ClientError![]const Value { var args = try self.client.allocator.alloc([]const u8, keys.len + 1); defer self.client.allocator.free(args); args[0] = "MGET"; @memcpy(args[1..], keys); const result = try self.client.sendCommand(args); return switch (result) { .array => |a| a, else => &.{}, }; } /// MSET key value [key value ...] - set multiple key-value pairs atomically pub fn mset(self: *StringCommands, pairs: []const [2][]const u8) ClientError!void { const arg_count = 1 + pairs.len * 2; var args = try self.client.allocator.alloc([]const u8, arg_count); defer self.client.allocator.free(args); args[0] = "MSET"; for (pairs, 0..) |pair, i| { args[1 + i * 2] = pair[0]; args[1 + i * 2 + 1] = pair[1]; } const result = try self.client.sendCommand(args); if (result.isError()) return CommandError.RedisError; } /// MSETNX key value [key value ...] - set multiple only if none exist /// Returns true if all were set, false if any existed. pub fn msetNx(self: *StringCommands, pairs: []const [2][]const u8) ClientError!bool { const arg_count = 1 + pairs.len * 2; var args = try self.client.allocator.alloc([]const u8, arg_count); defer self.client.allocator.free(args); args[0] = "MSETNX"; for (pairs, 0..) |pair, i| { args[1 + i * 2] = pair[0]; args[1 + i * 2 + 1] = pair[1]; } const result = try self.client.sendCommand(args); return switch (result) { .integer => |i| i == 1, else => false, }; } // ======================================================================== // Numeric Operations // ======================================================================== /// INCR key - increment integer value by 1 pub fn incr(self: *StringCommands, key: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "INCR", key }); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => return CommandError.RedisError, }; } /// INCRBY key increment - increment by specific amount pub fn incrBy(self: *StringCommands, key: []const u8, increment: i64) ClientError!i64 { var buf: [24]u8 = undefined; const inc_str = std.fmt.bufPrint(&buf, "{d}", .{increment}) catch unreachable; const result = try self.client.sendCommand(&.{ "INCRBY", key, inc_str }); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => return CommandError.RedisError, }; } /// INCRBYFLOAT key increment - increment by float pub fn incrByFloat(self: *StringCommands, key: []const u8, increment: f64) ClientError!f64 { var buf: [32]u8 = undefined; const inc_str = std.fmt.bufPrint(&buf, "{d}", .{increment}) catch unreachable; const result = try self.client.sendCommand(&.{ "INCRBYFLOAT", key, inc_str }); return switch (result) { .bulk => |b| if (b) |s| std.fmt.parseFloat(f64, s) catch return CommandError.RedisError else return CommandError.RedisError, .err => return CommandError.WrongType, else => return CommandError.RedisError, }; } /// DECR key - decrement integer value by 1 pub fn decr(self: *StringCommands, key: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "DECR", key }); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => return CommandError.RedisError, }; } /// DECRBY key decrement - decrement by specific amount pub fn decrBy(self: *StringCommands, key: []const u8, decrement: i64) ClientError!i64 { var buf: [24]u8 = undefined; const dec_str = std.fmt.bufPrint(&buf, "{d}", .{decrement}) catch unreachable; const result = try self.client.sendCommand(&.{ "DECRBY", key, dec_str }); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => return CommandError.RedisError, }; } // ======================================================================== // String Manipulation // ======================================================================== /// APPEND key value - append to string /// Returns new length of string. pub fn append(self: *StringCommands, key: []const u8, value: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "APPEND", key, value }); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => 0, }; } /// STRLEN key - get string length pub fn strlen(self: *StringCommands, key: []const u8) ClientError!i64 { const result = try self.client.sendCommand(&.{ "STRLEN", key }); return switch (result) { .integer => |i| i, else => 0, }; } /// GETRANGE key start end - get substring pub fn getRange(self: *StringCommands, key: []const u8, start: i64, end: i64) ClientError!?[]const u8 { var start_buf: [24]u8 = undefined; var end_buf: [24]u8 = undefined; const start_str = std.fmt.bufPrint(&start_buf, "{d}", .{start}) catch unreachable; const end_str = std.fmt.bufPrint(&end_buf, "{d}", .{end}) catch unreachable; const result = try self.client.sendCommand(&.{ "GETRANGE", key, start_str, end_str }); return switch (result) { .bulk => |b| b, else => null, }; } /// SETRANGE key offset value - overwrite part of string /// Returns new length of string. pub fn setRange(self: *StringCommands, key: []const u8, offset: usize, value: []const u8) ClientError!i64 { var buf: [24]u8 = undefined; const offset_str = std.fmt.bufPrint(&buf, "{d}", .{offset}) catch unreachable; const result = try self.client.sendCommand(&.{ "SETRANGE", key, offset_str, value }); return switch (result) { .integer => |i| i, .err => return CommandError.WrongType, else => 0, }; } }; // ============================================================================ // Client Extension Methods // ============================================================================ /// Extend Client with string commands. /// Usage: `try client.strings().get("key")` pub fn strings(client: *Client) StringCommands { return StringCommands.init(client); } // Direct convenience methods on Client would require modifying client.zig // For now, users can call: client.strings().get("key") // Or we can add these as pub fn in the main redis.zig module