From 673a1dfa92ffcd6c3125fe5aa6a7586d77698964 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Fri, 20 Jun 2025 20:35:39 +0600 Subject: [PATCH 1/8] Update README to enhance the getting started section with detailed steps for cloning the repository, building, running the server, using the REPL, and running tests. Add instructions for using release binaries and the CLI client. --- readme.md | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/readme.md b/readme.md index 5c527e7..9e2e622 100644 --- a/readme.md +++ b/readme.md @@ -197,29 +197,49 @@ GET foo ## 🚀 Getting Started -### Build Server +### 1. **Clone the Repository** +```bash +git clone https://github.com/itsfuad/FurrDB.git +cd FurrDB +``` +### 2. **Build the Server** ```bash go build -o furrdb ./cmd/furrdb ``` -### Run Server - +### 3. **Run the Server** ```bash ./furrdb ``` -Server runs on `localhost:7070` by default. +The server will start on `localhost:7070` by default. -### Use Client +### 4. **Use the REPL (Interactive Shell)** +```bash +go run ./cmd/furrdb --repl +``` +Type commands directly, or use `HELP` for a list of commands. +### 5. **Use the CLI Client to Run Scripts** ```bash -go run ./client +go run ./cmd/furrdbcli scripts/all_commands.txt ``` -Or connect manually: +Or connect manually with telnet: ```bash telnet localhost 7070 ``` +### 6. **Using Release Binaries** +- Download the latest release from the [GitHub Releases page](https://github.com/itsfuad/FurrDB/releases). +- Extract and run the binary for your OS: + - `furrdb` (server) + - `furrdbcli` (CLI client) + +### 7. **Run Tests** +```bash +go test ./... +``` + --- ## ⚙️ Configuration From fbaad69ee964d03b9d706c30bade2e7d196c0eea Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Fri, 20 Jun 2025 20:48:38 +0600 Subject: [PATCH 2/8] .. --- scripts/all_commands.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/all_commands.txt b/scripts/all_commands.txt index eaf283a..a13854e 100644 --- a/scripts/all_commands.txt +++ b/scripts/all_commands.txt @@ -25,8 +25,6 @@ SMEMBERS myset # returns x,z KEYS INFO FLUSHDB -KEYS -INFO # Ping PING From 98ef31d72b9b51adbd78ddbe263b201326b9673d Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Fri, 20 Jun 2025 21:21:54 +0600 Subject: [PATCH 3/8] Refactor script evaluation and command processing; simplify argument handling and improve readability --- internal/db/db_test.go | 10 +- internal/engine/test_aof.log | 2 + internal/handlers/script_handlers.go | 2 +- internal/repl/repl.go | 42 +++--- internal/script/script.go | 187 +++++++++++++++++---------- internal/script/script_test.go | 14 +- internal/server/server.go | 54 ++++---- 7 files changed, 188 insertions(+), 123 deletions(-) diff --git a/internal/db/db_test.go b/internal/db/db_test.go index 6b3c84b..deacaef 100644 --- a/internal/db/db_test.go +++ b/internal/db/db_test.go @@ -7,6 +7,10 @@ import ( "time" ) +const ( + TEST_FILE = "test_dump.rdb" +) + func TestStoreBasicOps(t *testing.T) { db := NewStore() db.mu.Lock() @@ -127,7 +131,7 @@ func TestSnapshotSaveLoad(t *testing.T) { _, _ = saddHandler([]string{"snapset", "a", "b"}) _, _ = expireHandler([]string{"snapkey", "10"}) - err := SaveSnapshot("test_dump.rdb") + err := SaveSnapshot(TEST_FILE) if err != nil { t.Fatalf("SaveSnapshot failed: %v", err) } @@ -139,7 +143,7 @@ func TestSnapshotSaveLoad(t *testing.T) { t.Errorf("expected empty after clear, got %s", val) } - err = LoadSnapshot("test_dump.rdb") + err = LoadSnapshot(TEST_FILE) if err != nil { t.Fatalf("LoadSnapshot failed: %v", err) } @@ -151,5 +155,5 @@ func TestSnapshotSaveLoad(t *testing.T) { if !(strings.Contains(members, "a") && strings.Contains(members, "b")) { t.Errorf("expected set members a and b, got %s", members) } - _ = os.Remove("test_dump.rdb") + _ = os.Remove(TEST_FILE) } diff --git a/internal/engine/test_aof.log b/internal/engine/test_aof.log index f0cddba..6158aed 100644 --- a/internal/engine/test_aof.log +++ b/internal/engine/test_aof.log @@ -9,3 +9,5 @@ SET testkey testval SET testkey testval SET testkey testval SET testkey testval +SET testkey testval +SET testkey testval diff --git a/internal/handlers/script_handlers.go b/internal/handlers/script_handlers.go index 81cb318..6d23f66 100644 --- a/internal/handlers/script_handlers.go +++ b/internal/handlers/script_handlers.go @@ -30,7 +30,7 @@ func evalHandler(args []string) (string, error) { return "", fmt.Errorf("missing argument for EVAL") } scriptStr := strings.Join(args, " ") - return script.EvalScript(scriptStr, nil) + return script.EvalScript(scriptStr) } func init() { diff --git a/internal/repl/repl.go b/internal/repl/repl.go index 0f938fd..85ff378 100644 --- a/internal/repl/repl.go +++ b/internal/repl/repl.go @@ -4,7 +4,6 @@ import ( "bufio" "fmt" "os" - "runtime" "strings" "furr/internal/db" @@ -30,39 +29,32 @@ func Start() { } cmd := strings.ToUpper(tokens[0]) args := tokens[1:] - if cmd == "EXIT" { + switch cmd { + case "EXIT": fmt.Println("bye!") return - } - if cmd == "HELP" { + case "HELP": printHelp() - continue - } - if cmd == "CLEAR" { + case "CLEAR": clearScreen() - continue - } - handler, ok := db.Commands[cmd] - if !ok { - fmt.Println("ERR unknown command") - continue + default: + handler, ok := db.Commands[cmd] + if !ok { + fmt.Println("ERR unknown command") + continue + } + result, err := handler(args) + if err != nil { + fmt.Println("ERR", err) + continue + } + fmt.Println(result) } - result, err := handler(args) - if err != nil { - fmt.Println("ERR", err) - continue - } - fmt.Println(result) } } func clearScreen() { - if runtime.GOOS == "windows" { - fmt.Print("\033[2J\033[H") // ANSI clear for modern Windows terminals - // Optionally, use syscall for legacy cmd.exe, but most support ANSI now - } else { - fmt.Print("\033[2J\033[H") // ANSI escape codes for Unix - } + fmt.Print("\033[2J\033[H") // ANSI escape code } func printHelp() { diff --git a/internal/script/script.go b/internal/script/script.go index 9022a17..c6a019e 100644 --- a/internal/script/script.go +++ b/internal/script/script.go @@ -25,100 +25,157 @@ func RunScript(hash string, args []string) (string, error) { if !ok { return "", nil } - return evalScriptLines(script, args) + return evalScriptLines(script) } // EvalScript evaluates a script string without storing (stub) -func EvalScript(script string, args []string) (string, error) { - return evalScriptLines(script, args) +func EvalScript(script string) (string, error) { + return evalScriptLines(script) } -func evalScriptLines(script string, args []string) (string, error) { +// getWhitelist returns the allowed commands +func getWhitelist() map[string]bool { + return map[string]bool{ + "SET": true, "GET": true, "DEL": true, "EXISTS": true, + "LPUSH": true, "RPUSH": true, "LPOP": true, "RPOP": true, "LRANGE": true, + "SADD": true, "SREM": true, "SMEMBERS": true, + } +} + +// handleSkipping processes lines when in a skipping state +func handleSkipping(line string, skipToEnd int) int { + if strings.HasPrefix(line, "IF ") { + return skipToEnd + 1 + } else if line == "END" { + return skipToEnd - 1 + } + return skipToEnd +} + +// handleLetStatement processes a LET statement +func handleLetStatement(line string, lineNum int, whitelist map[string]bool, vars map[string]string) (string, error) { + parts := strings.Fields(line) + if len(parts) < 5 || parts[2] != "=" { + return "", fmt.Errorf("ERR invalid LET syntax on line %d", lineNum) + } + + varName := parts[1] + cmd := strings.ToUpper(parts[3]) + cmdArgs := parts[4:] + + if !whitelist[cmd] { + return "", fmt.Errorf("ERR command %s not allowed in LET on line %d", cmd, lineNum) + } + + handler, ok := db.Commands[cmd] + if !ok { + return "", fmt.Errorf("ERR unknown command '%s' in LET on line %d", cmd, lineNum) + } + + result, err := handler(cmdArgs) + if err != nil { + return "", fmt.Errorf("ERR %v on line %d", err, lineNum) + } + + vars[varName] = result + return result, nil +} + +// handleIfStatement processes an IF statement +func handleIfStatement(line string, lineNum int, vars map[string]string) (int, error) { + parts := strings.Fields(line) + if len(parts) != 4 || parts[2] != "==" { + return 0, fmt.Errorf("ERR invalid IF syntax on line %d", lineNum) + } + + varName := parts[1] + expected := parts[3] + + if vars[varName] != expected { + return 1, nil + } + return 0, nil +} + +// executeCommand executes a normal DB command +func executeCommand(line string, lineNum int, whitelist map[string]bool) (string, error) { + tokens := strings.Fields(line) + if len(tokens) == 0 { + return "", nil + } + + cmd := strings.ToUpper(tokens[0]) + params := tokens[1:] + + if !whitelist[cmd] { + return "", fmt.Errorf("ERR command %s not allowed in script on line %d", cmd, lineNum) + } + + handler, ok := db.Commands[cmd] + if !ok { + return "", fmt.Errorf("ERR unknown command '%s' on line %d", cmd, lineNum) + } + + result, err := handler(params) + if err != nil { + return "", fmt.Errorf("ERR %v on line %d", err, lineNum) + } + + return result, nil +} + +func evalScriptLines(script string) (string, error) { lines := strings.Split(script, ";") vars := make(map[string]string) var last string + maxLines := 100 if len(lines) > maxLines { return "", fmt.Errorf("ERR script too long (max %d lines)", maxLines) } - whitelist := map[string]bool{ - "SET": true, "GET": true, "DEL": true, "EXISTS": true, - "LPUSH": true, "RPUSH": true, "LPOP": true, "RPOP": true, "LRANGE": true, - "SADD": true, "SREM": true, "SMEMBERS": true, - } + + whitelist := getWhitelist() var skipToEnd int // 0=not skipping, >0=inside nested IFs + for i, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } + if skipToEnd > 0 { - if strings.HasPrefix(line, "IF ") { - skipToEnd++ - } else if line == "END" { - skipToEnd-- - } + skipToEnd = handleSkipping(line, skipToEnd) continue } + if strings.HasPrefix(line, "LET ") { - // LET x = CMD args - parts := strings.Fields(line) - if len(parts) < 5 || parts[2] != "=" { - return "", fmt.Errorf("ERR invalid LET syntax on line %d", i+1) - } - varName := parts[1] - cmd := strings.ToUpper(parts[3]) - cmdArgs := parts[4:] - if !whitelist[cmd] { - return "", fmt.Errorf("ERR command %s not allowed in LET on line %d", cmd, i+1) - } - handler, ok := db.Commands[cmd] - if !ok { - return "", fmt.Errorf("ERR unknown command '%s' in LET on line %d", cmd, i+1) - } - result, err := handler(cmdArgs) + result, err := handleLetStatement(line, i+1, whitelist, vars) if err != nil { - return "", fmt.Errorf("ERR %v on line %d", err, i+1) + return "", err } - vars[varName] = result last = result + + } else if strings.HasPrefix(line, "IF ") { + skip, err := handleIfStatement(line, i+1, vars) + if err != nil { + return "", err + } + skipToEnd = skip + + } else if line == "END" { continue - } - if strings.HasPrefix(line, "IF ") { - // IF var == value - parts := strings.Fields(line) - if len(parts) != 4 || parts[2] != "==" { - return "", fmt.Errorf("ERR invalid IF syntax on line %d", i+1) + + } else { + // Normal DB command + result, err := executeCommand(line, i+1, whitelist) + if err != nil { + return "", err } - varName := parts[1] - expected := parts[3] - if vars[varName] != expected { - skipToEnd = 1 + if result != "" { + last = result } - continue - } - if line == "END" { - continue - } - // Normal DB command - tokens := strings.Fields(line) - if len(tokens) == 0 { - continue - } - cmd := strings.ToUpper(tokens[0]) - params := tokens[1:] - if !whitelist[cmd] { - return "", fmt.Errorf("ERR command %s not allowed in script on line %d", cmd, i+1) - } - handler, ok := db.Commands[cmd] - if !ok { - return "", fmt.Errorf("ERR unknown command '%s' on line %d", cmd, i+1) - } - result, err := handler(params) - if err != nil { - return "", fmt.Errorf("ERR %v on line %d", err, i+1) } - last = result } + return last, nil } diff --git a/internal/script/script_test.go b/internal/script/script_test.go index 10faf43..453c572 100644 --- a/internal/script/script_test.go +++ b/internal/script/script_test.go @@ -20,7 +20,7 @@ func TestRegisterAndRunScript(t *testing.T) { } func TestEvalScript(t *testing.T) { - res, err := EvalScript("SET baz qux; GET baz", nil) + res, err := EvalScript("SET baz qux; GET baz") if err != nil { t.Fatal(err) } @@ -29,10 +29,10 @@ func TestEvalScript(t *testing.T) { } } -func TestScriptDSL_LetIfEnd(t *testing.T) { +func TestScriptDSLLetIfEnd(t *testing.T) { scriptStr := `LET x = GET foo; IF x == bar; SET foo baz; END; GET foo` _, _ = db.Commands["SET"]([]string{"foo", "bar"}) - res, err := EvalScript(scriptStr, nil) + res, err := EvalScript(scriptStr) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -41,18 +41,18 @@ func TestScriptDSL_LetIfEnd(t *testing.T) { } } -func TestScriptDSL_Sandbox(t *testing.T) { +func TestScriptDSLSandbox(t *testing.T) { scriptStr := `FLUSHDB; GET foo` _, _ = db.Commands["SET"]([]string{"foo", "bar"}) - _, err := EvalScript(scriptStr, nil) + _, err := EvalScript(scriptStr) if err == nil || err.Error() != "ERR command FLUSHDB not allowed in script on line 1" { t.Errorf("expected sandbox error, got %v", err) } } -func TestScriptDSL_LetSyntaxError(t *testing.T) { +func TestScriptDSLLetSyntaxError(t *testing.T) { scriptStr := `LET x GET foo` - _, err := EvalScript(scriptStr, nil) + _, err := EvalScript(scriptStr) if err == nil || err.Error() != "ERR invalid LET syntax on line 1" { t.Errorf("expected LET syntax error, got %v", err) } diff --git a/internal/server/server.go b/internal/server/server.go index 577cc1d..0ba6ab2 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -35,38 +35,48 @@ func handleConn(conn net.Conn) { if err != nil { return } - line = strings.TrimSpace(line) - if line == "" { - continue - } - tokens := strings.Fields(line) + + tokens := parseInput(line) if len(tokens) == 0 { continue } + cmd := strings.ToUpper(tokens[0]) args := tokens[1:] - var resp string - switch cmd { - case "PING": - resp = "PONG" - case "EXIT": + + if cmd == "EXIT" { w.WriteString("BYE\n") w.Flush() return - default: - handler, ok := db.Commands[cmd] - if !ok { - resp = "ERR unknown command" - } else { - result, err := handler(args) - if err != nil { - resp = "ERR " + err.Error() - } else { - resp = result - } - } } + + resp := processCommand(cmd, args) w.WriteString(resp + "\n") w.Flush() } } + +func parseInput(line string) []string { + line = strings.TrimSpace(line) + if line == "" { + return nil + } + return strings.Fields(line) +} + +func processCommand(cmd string, args []string) string { + if cmd == "PING" { + return "PONG" + } + + handler, ok := db.Commands[cmd] + if !ok { + return "ERR unknown command" + } + + result, err := handler(args) + if err != nil { + return "ERR " + err.Error() + } + return result +} From c577c20dcd18aa851552cc76f4378432075a11a7 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Fri, 20 Jun 2025 21:28:59 +0600 Subject: [PATCH 4/8] Refactor script handling; streamline LET and IF statement processing, and consolidate command execution logic --- internal/engine/test_aof.log | 2 + internal/script/script.go | 83 +++++++++++++++++------------------- 2 files changed, 41 insertions(+), 44 deletions(-) diff --git a/internal/engine/test_aof.log b/internal/engine/test_aof.log index 6158aed..833fff4 100644 --- a/internal/engine/test_aof.log +++ b/internal/engine/test_aof.log @@ -11,3 +11,5 @@ SET testkey testval SET testkey testval SET testkey testval SET testkey testval +SET testkey testval +SET testkey testval diff --git a/internal/script/script.go b/internal/script/script.go index c6a019e..9b93a71 100644 --- a/internal/script/script.go +++ b/internal/script/script.go @@ -58,25 +58,25 @@ func handleLetStatement(line string, lineNum int, whitelist map[string]bool, var if len(parts) < 5 || parts[2] != "=" { return "", fmt.Errorf("ERR invalid LET syntax on line %d", lineNum) } - + varName := parts[1] cmd := strings.ToUpper(parts[3]) cmdArgs := parts[4:] - + if !whitelist[cmd] { return "", fmt.Errorf("ERR command %s not allowed in LET on line %d", cmd, lineNum) } - + handler, ok := db.Commands[cmd] if !ok { return "", fmt.Errorf("ERR unknown command '%s' in LET on line %d", cmd, lineNum) } - + result, err := handler(cmdArgs) if err != nil { return "", fmt.Errorf("ERR %v on line %d", err, lineNum) } - + vars[varName] = result return result, nil } @@ -87,10 +87,10 @@ func handleIfStatement(line string, lineNum int, vars map[string]string) (int, e if len(parts) != 4 || parts[2] != "==" { return 0, fmt.Errorf("ERR invalid IF syntax on line %d", lineNum) } - + varName := parts[1] expected := parts[3] - + if vars[varName] != expected { return 1, nil } @@ -103,24 +103,24 @@ func executeCommand(line string, lineNum int, whitelist map[string]bool) (string if len(tokens) == 0 { return "", nil } - + cmd := strings.ToUpper(tokens[0]) params := tokens[1:] - + if !whitelist[cmd] { return "", fmt.Errorf("ERR command %s not allowed in script on line %d", cmd, lineNum) } - + handler, ok := db.Commands[cmd] if !ok { return "", fmt.Errorf("ERR unknown command '%s' on line %d", cmd, lineNum) } - + result, err := handler(params) if err != nil { return "", fmt.Errorf("ERR %v on line %d", err, lineNum) } - + return result, nil } @@ -128,54 +128,49 @@ func evalScriptLines(script string) (string, error) { lines := strings.Split(script, ";") vars := make(map[string]string) var last string - - maxLines := 100 + + const maxLines = 100 if len(lines) > maxLines { return "", fmt.Errorf("ERR script too long (max %d lines)", maxLines) } - + whitelist := getWhitelist() - var skipToEnd int // 0=not skipping, >0=inside nested IFs - + var skipToEnd int + for i, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } - if skipToEnd > 0 { skipToEnd = handleSkipping(line, skipToEnd) continue } - - if strings.HasPrefix(line, "LET ") { - result, err := handleLetStatement(line, i+1, whitelist, vars) - if err != nil { - return "", err - } - last = result - - } else if strings.HasPrefix(line, "IF ") { - skip, err := handleIfStatement(line, i+1, vars) - if err != nil { - return "", err - } + result, skip, err := evalScriptLine(line, i+1, whitelist, vars) + if err != nil { + return "", err + } + if skip > 0 { skipToEnd = skip - - } else if line == "END" { continue - - } else { - // Normal DB command - result, err := executeCommand(line, i+1, whitelist) - if err != nil { - return "", err - } - if result != "" { - last = result - } + } + if result != "" { + last = result } } - return last, nil } + +func evalScriptLine(line string, lineNum int, whitelist map[string]bool, vars map[string]string) (result string, skip int, err error) { + switch { + case strings.HasPrefix(line, "LET "): + result, err = handleLetStatement(line, lineNum, whitelist, vars) + case strings.HasPrefix(line, "IF "): + skip, err = handleIfStatement(line, lineNum, vars) + case line == "END": + // nothing to do + default: + result, err = executeCommand(line, lineNum, whitelist) + } + return +} From 20f9dccc91989625acd6e9eba7b143acaa969184 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Fri, 20 Jun 2025 21:42:36 +0600 Subject: [PATCH 5/8] .. --- internal/server/server.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 0ba6ab2..8a6266d 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -35,21 +35,21 @@ func handleConn(conn net.Conn) { if err != nil { return } - + tokens := parseInput(line) if len(tokens) == 0 { continue } - + cmd := strings.ToUpper(tokens[0]) args := tokens[1:] - + if cmd == "EXIT" { w.WriteString("BYE\n") w.Flush() return } - + resp := processCommand(cmd, args) w.WriteString(resp + "\n") w.Flush() @@ -68,12 +68,12 @@ func processCommand(cmd string, args []string) string { if cmd == "PING" { return "PONG" } - + handler, ok := db.Commands[cmd] if !ok { return "ERR unknown command" } - + result, err := handler(args) if err != nil { return "ERR " + err.Error() From a9e0442864f1d671b249591aacabe17aa2701c0d Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Fri, 20 Jun 2025 23:17:57 +0600 Subject: [PATCH 6/8] Remove outdated project structure section from README and delete all_commands demo script from scripts directory. --- .vscode/settings.json | 3 ++ {scripts => examples}/all_commands.txt | 0 examples/crud_demo.go | 58 +++++++++++++++++++++++++ examples/crud_demo.py | 49 +++++++++++++++++++++ examples/crud_demo.ts | 59 ++++++++++++++++++++++++++ readme.md | 20 --------- 6 files changed, 169 insertions(+), 20 deletions(-) create mode 100644 .vscode/settings.json rename {scripts => examples}/all_commands.txt (100%) create mode 100644 examples/crud_demo.go create mode 100644 examples/crud_demo.py create mode 100644 examples/crud_demo.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b943dbc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} \ No newline at end of file diff --git a/scripts/all_commands.txt b/examples/all_commands.txt similarity index 100% rename from scripts/all_commands.txt rename to examples/all_commands.txt diff --git a/examples/crud_demo.go b/examples/crud_demo.go new file mode 100644 index 0000000..e206c22 --- /dev/null +++ b/examples/crud_demo.go @@ -0,0 +1,58 @@ +package main + +import ( + "bufio" + "fmt" + "net" + "strings" +) + +func sendCommand(conn net.Conn, cmd string) string { + fmt.Fprintf(conn, "%s\n", cmd) + reader := bufio.NewReader(conn) + resp, _ := reader.ReadString('\n') + return strings.TrimSpace(resp) +} + +func main() { + conn, err := net.Dial("tcp", "127.0.0.1:7070") + if err != nil { + fmt.Println("Error connecting:", err) + return + } + defer conn.Close() + + fmt.Println("Connected to FurrDB") + + // CREATE + fmt.Println("[CREATE] Set user:1 name and email") + fmt.Println(sendCommand(conn, "SET user:1:name Alice")) + fmt.Println(sendCommand(conn, "SET user:1:email alice@example.com")) + + // READ + fmt.Println("\n[READ] Get user:1 name and email") + fmt.Println("Name:", sendCommand(conn, "GET user:1:name")) + fmt.Println("Email:", sendCommand(conn, "GET user:1:email")) + + // UPDATE + fmt.Println("\n[UPDATE] Update user:1 name") + fmt.Println(sendCommand(conn, "SET user:1:name Alicia")) + fmt.Println("Updated Name:", sendCommand(conn, "GET user:1:name")) + + // EXISTS + fmt.Println("\n[EXISTS] Check if user:1:email exists") + fmt.Println("Exists:", sendCommand(conn, "EXISTS user:1:email")) + + // DELETE + fmt.Println("\n[DELETE] Delete user:1:email") + fmt.Println(sendCommand(conn, "DEL user:1:email")) + fmt.Println("Email after delete:", sendCommand(conn, "GET user:1:email")) + + // KEYS + fmt.Println("\n[KEYS] List all keys") + fmt.Println(sendCommand(conn, "KEYS")) + + // EXIT + sendCommand(conn, "EXIT") + fmt.Println("\nConnection closed") +} diff --git a/examples/crud_demo.py b/examples/crud_demo.py new file mode 100644 index 0000000..b2fb7cc --- /dev/null +++ b/examples/crud_demo.py @@ -0,0 +1,49 @@ +import socket + +HOST = '127.0.0.1' +PORT = 7070 + +def send_command(sock, cmd): + sock.sendall((cmd + '\n').encode()) + resp = b'' + while not resp.endswith(b'\n'): + chunk = sock.recv(4096) + if not chunk: + break + resp += chunk + return resp.decode().strip() + +with socket.create_connection((HOST, PORT)) as s: + print('Connected to FurrDB\n') + + # CREATE + print('[CREATE] Set user:1 name and email') + print(send_command(s, 'SET user:1:name Alice')) + print(send_command(s, 'SET user:1:email alice@example.com')) + + # READ + print('\n[READ] Get user:1 name and email') + print('Name:', send_command(s, 'GET user:1:name')) + print('Email:', send_command(s, 'GET user:1:email')) + + # UPDATE + print('\n[UPDATE] Update user:1 name') + print(send_command(s, 'SET user:1:name Alicia')) + print('Updated Name:', send_command(s, 'GET user:1:name')) + + # EXISTS + print('\n[EXISTS] Check if user:1:email exists') + print('Exists:', send_command(s, 'EXISTS user:1:email')) + + # DELETE + print('\n[DELETE] Delete user:1:email') + print(send_command(s, 'DEL user:1:email')) + print('Email after delete:', send_command(s, 'GET user:1:email')) + + # KEYS + print('\n[KEYS] List all keys') + print(send_command(s, 'KEYS')) + + # EXIT + send_command(s, 'EXIT') + print('\nConnection closed') \ No newline at end of file diff --git a/examples/crud_demo.ts b/examples/crud_demo.ts new file mode 100644 index 0000000..5213f74 --- /dev/null +++ b/examples/crud_demo.ts @@ -0,0 +1,59 @@ +// FurrDB CRUD Demo (Deno TypeScript) +// Demonstrates basic CRUD operations for a user profile + +const HOST = "127.0.0.1"; +const PORT = 7070; + +// Helper to send a command and receive a response +async function sendCommand(conn: Deno.Conn, cmd: string): Promise { + const encoder = new TextEncoder(); + const decoder = new TextDecoder(); + await conn.write(encoder.encode(cmd + "\n")); + let resp = ""; + while (!resp.endsWith("\n")) { + const buf = new Uint8Array(1024); + const n = await conn.read(buf); + if (n === null) break; + resp += decoder.decode(buf.subarray(0, n)); + } + return resp.trim(); +} + +// Main demo logic +const conn = await Deno.connect({ hostname: HOST, port: PORT }); +console.log("Connected to FurrDB\n"); + +// Create (SET user:1 name and email) +console.log("[CREATE] Set user:1 name and email"); +console.log(await sendCommand(conn, 'SET user:1:name Alice')); +console.log(await sendCommand(conn, 'SET user:1:email alice@example.com')); + +// Read (GET user:1 name and email) +console.log("\n[READ] Get user:1 name and email"); +console.log('Name:', await sendCommand(conn, 'GET user:1:name')); +console.log('Email:', await sendCommand(conn, 'GET user:1:email')); + +// Update (SET user:1 name) +console.log("\n[UPDATE] Update user:1 name"); +console.log(await sendCommand(conn, 'SET user:1:name Alicia')); +console.log('Updated Name:', await sendCommand(conn, 'GET user:1:name')); + +// Exists +console.log("\n[EXISTS] Check if user:1:email exists"); +console.log('Exists:', await sendCommand(conn, 'EXISTS user:1:email')); + +// Delete (DEL user:1 email) +console.log("\n[DELETE] Delete user:1:email"); +console.log(await sendCommand(conn, 'DEL user:1:email')); +console.log('Email after delete:', await sendCommand(conn, 'GET user:1:email')); + +// List all keys +console.log("\n[KEYS] List all keys"); +console.log(await sendCommand(conn, 'KEYS')); + +// Exit +await sendCommand(conn, 'EXIT'); +conn.close(); +console.log("\nConnection closed"); + +export {}; \ No newline at end of file diff --git a/readme.md b/readme.md index 9e2e622..2cafc8d 100644 --- a/readme.md +++ b/readme.md @@ -28,26 +28,6 @@ --- -## 🏗️ Project Structure - -``` -furrdb/ -├── cmd/ -│ └── furrdb/ # Main server entrypoint -├── client/ # CLI client (minidb-cli) -├── internal/ -│ ├── db/ # In-memory data store and command handlers -│ ├── engine/ # Persistence engine (AOF-based) -│ ├── server/ # TCP listener and protocol parser -│ ├── script/ # Script registration, hashing, execution -│ ├── repl/ # Optional local REPL shell -│ └── utils/ # Logging, hashing, and helper functions -├── scripts/ # Sample scripts for testing -├── testdata/ # Persistence and input test files -├── go.mod -└── README.md -``` - --- ## 🧠 Architecture Overview From 74d46a864c509af1072611bc41d588b760e53877 Mon Sep 17 00:00:00 2001 From: Fuad Hasan Date: Sun, 22 Jun 2025 20:17:51 +0600 Subject: [PATCH 7/8] Fix command mapping by renaming SNAPSHOT to SAVE in Commands --- dump.rdb | Bin 0 -> 191 bytes internal/db/db.go | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 dump.rdb diff --git a/dump.rdb b/dump.rdb new file mode 100644 index 0000000000000000000000000000000000000000..f6613a3cb1ae7bdcb84b9da6e6cf56509f4336a1 GIT binary patch literal 191 zcmdO_XJ%yj-@w4g%*f)BSdz&2zlnj7HKej2wV3gL3s5*D#E0>J8v_G_`u|22Mn>`6 z#DeJJlA_GK^w`Y2lGLKK#NRPZq9!3F>x@I(WDM@-|i8-aIAXCAH@-VV6Fev Date: Sun, 22 Jun 2025 20:20:13 +0600 Subject: [PATCH 8/8] ... --- internal/db/db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/db.go b/internal/db/db.go index 30b85be..c87c8a2 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -57,7 +57,7 @@ var Commands = map[string]HandlerFunc{ "INFO": infoHandler, "EXPIRE": expireHandler, "TTL": ttlHandler, - "SAVE": snapshotHandler, + "SAVE": snapshotHandler, } func (s *Store) ttlCleaner() {