package main import ( "bufio" "fmt" "os" "strings" "time" "git.canoozie.net/jer/go-storage/pkg/common/iterator" "git.canoozie.net/jer/go-storage/pkg/engine" // Import transaction package to register the transaction creator _ "git.canoozie.net/jer/go-storage/pkg/transaction" ) const helpText = ` Go Storage (gs) - SQLite-like interface for the storage engine Usage: gs [database_path] - Start with an optional database path Commands: .help - Show this help message .open PATH - Open a database at PATH .close - Close the current database .exit - Exit the program .stats - Show database statistics .flush - Force flush memtables to disk BEGIN [TRANSACTION] - Begin a transaction (default: read-write) BEGIN READONLY - Begin a read-only transaction COMMIT - Commit the current transaction ROLLBACK - Rollback the current transaction PUT key value - Store a key-value pair GET key - Retrieve a value by key DELETE key - Delete a key-value pair SCAN - Scan all key-value pairs SCAN prefix - Scan key-value pairs with given prefix SCAN RANGE start end - Scan key-value pairs in range [start, end) - Note: start and end are treated as string keys, not numeric indices ` func main() { fmt.Println("Go Storage (gs) version 1.0.0") fmt.Println("Enter .help for usage hints.") // Initialize variables var eng *engine.Engine var tx engine.Transaction var err error var dbPath string // Check if a database path was provided as an argument if len(os.Args) > 1 { dbPath = os.Args[1] fmt.Printf("Opening database at %s\n", dbPath) eng, err = engine.NewEngine(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "Error opening database: %s\n", err) os.Exit(1) } } reader := bufio.NewReader(os.Stdin) for { // Print prompt with database path if open if tx != nil { if tx.IsReadOnly() { if dbPath != "" { fmt.Printf("gs:%s[RO]> ", dbPath) } else { fmt.Print("gs[RO]> ") } } else { if dbPath != "" { fmt.Printf("gs:%s[RW]> ", dbPath) } else { fmt.Print("gs[RW]> ") } } } else { if dbPath != "" { fmt.Printf("gs:%s> ", dbPath) } else { fmt.Print("gs> ") } } // Read command line, readErr := reader.ReadString('\n') if readErr != nil { fmt.Fprintf(os.Stderr, "Error reading input: %s\n", readErr) continue } // Trim whitespace line = strings.TrimSpace(line) if line == "" { continue } // Process command parts := strings.Fields(line) cmd := strings.ToUpper(parts[0]) // Special dot commands if strings.HasPrefix(cmd, ".") { cmd = strings.ToLower(cmd) switch cmd { case ".help": fmt.Print(helpText) case ".open": if len(parts) < 2 { fmt.Println("Error: Missing path argument") continue } // Close any existing engine if eng != nil { eng.Close() } // Open the database dbPath = parts[1] eng, err = engine.NewEngine(dbPath) if err != nil { fmt.Fprintf(os.Stderr, "Error opening database: %s\n", err) dbPath = "" continue } fmt.Printf("Database opened at %s\n", dbPath) case ".close": if eng == nil { fmt.Println("No database open") continue } // Close any active transaction if tx != nil { tx.Rollback() tx = nil } // Close the engine err = eng.Close() if err != nil { fmt.Fprintf(os.Stderr, "Error closing database: %s\n", err) } else { fmt.Printf("Database %s closed\n", dbPath) eng = nil dbPath = "" } case ".exit": // Close any active transaction if tx != nil { tx.Rollback() } // Close the engine if eng != nil { eng.Close() } fmt.Println("Goodbye!") return case ".stats": if eng == nil { fmt.Println("No database open") continue } // Print statistics stats := eng.GetStats() fmt.Println("Database Statistics:") fmt.Printf(" Operations: %d puts, %d gets (%d hits, %d misses), %d deletes\n", stats["put_ops"], stats["get_ops"], stats["get_hits"], stats["get_misses"], stats["delete_ops"]) fmt.Printf(" Transactions: %d started, %d committed, %d aborted\n", stats["tx_started"], stats["tx_completed"], stats["tx_aborted"]) fmt.Printf(" Storage: %d bytes read, %d bytes written, %d flushes\n", stats["total_bytes_read"], stats["total_bytes_written"], stats["flush_count"]) fmt.Printf(" Tables: %d sstables, %d immutable memtables\n", stats["sstable_count"], stats["immutable_memtable_count"]) case ".flush": if eng == nil { fmt.Println("No database open") continue } // Flush all memtables err = eng.FlushImMemTables() if err != nil { fmt.Fprintf(os.Stderr, "Error flushing memtables: %s\n", err) } else { fmt.Println("Memtables flushed to disk") } default: fmt.Printf("Unknown command: %s\n", cmd) } continue } // Regular commands switch cmd { case "BEGIN": if eng == nil { fmt.Println("Error: No database open") continue } // Check if we already have a transaction if tx != nil { fmt.Println("Error: Transaction already in progress") continue } // Check if readonly readOnly := false if len(parts) >= 2 && strings.ToUpper(parts[1]) == "READONLY" { readOnly = true } // Begin transaction tx, err = eng.BeginTransaction(readOnly) if err != nil { fmt.Fprintf(os.Stderr, "Error beginning transaction: %s\n", err) continue } if readOnly { fmt.Println("Started read-only transaction") } else { fmt.Println("Started read-write transaction") } case "COMMIT": if tx == nil { fmt.Println("Error: No transaction in progress") continue } // Commit transaction startTime := time.Now() err = tx.Commit() if err != nil { fmt.Fprintf(os.Stderr, "Error committing transaction: %s\n", err) } else { fmt.Printf("Transaction committed (%.2f ms)\n", float64(time.Since(startTime).Microseconds())/1000.0) tx = nil } case "ROLLBACK": if tx == nil { fmt.Println("Error: No transaction in progress") continue } // Rollback transaction err = tx.Rollback() if err != nil { fmt.Fprintf(os.Stderr, "Error rolling back transaction: %s\n", err) } else { fmt.Println("Transaction rolled back") tx = nil } case "PUT": if len(parts) < 3 { fmt.Println("Error: PUT requires key and value arguments") continue } // Check if we're in a transaction if tx != nil { // Check if read-only if tx.IsReadOnly() { fmt.Println("Error: Cannot PUT in a read-only transaction") continue } // Use transaction PUT err = tx.Put([]byte(parts[1]), []byte(strings.Join(parts[2:], " "))) if err != nil { fmt.Fprintf(os.Stderr, "Error putting value: %s\n", err) } else { fmt.Println("Value stored in transaction (will be visible after commit)") } } else { // Check if database is open if eng == nil { fmt.Println("Error: No database open") continue } // Use direct PUT err = eng.Put([]byte(parts[1]), []byte(strings.Join(parts[2:], " "))) if err != nil { fmt.Fprintf(os.Stderr, "Error putting value: %s\n", err) } else { fmt.Println("Value stored") } } case "GET": if len(parts) < 2 { fmt.Println("Error: GET requires a key argument") continue } // Check if we're in a transaction if tx != nil { // Use transaction GET val, err := tx.Get([]byte(parts[1])) if err != nil { if err == engine.ErrKeyNotFound { fmt.Println("Key not found") } else { fmt.Fprintf(os.Stderr, "Error getting value: %s\n", err) } } else { fmt.Printf("%s\n", val) } } else { // Check if database is open if eng == nil { fmt.Println("Error: No database open") continue } // Use direct GET val, err := eng.Get([]byte(parts[1])) if err != nil { if err == engine.ErrKeyNotFound { fmt.Println("Key not found") } else { fmt.Fprintf(os.Stderr, "Error getting value: %s\n", err) } } else { fmt.Printf("%s\n", val) } } case "DELETE": if len(parts) < 2 { fmt.Println("Error: DELETE requires a key argument") continue } // Check if we're in a transaction if tx != nil { // Check if read-only if tx.IsReadOnly() { fmt.Println("Error: Cannot DELETE in a read-only transaction") continue } // Use transaction DELETE err = tx.Delete([]byte(parts[1])) if err != nil { fmt.Fprintf(os.Stderr, "Error deleting key: %s\n", err) } else { fmt.Println("Key deleted in transaction (will be applied after commit)") } } else { // Check if database is open if eng == nil { fmt.Println("Error: No database open") continue } // Use direct DELETE err = eng.Delete([]byte(parts[1])) if err != nil { fmt.Fprintf(os.Stderr, "Error deleting key: %s\n", err) } else { fmt.Println("Key deleted") } } case "SCAN": var iter iterator.Iterator // Check if we're in a transaction if tx != nil { if len(parts) == 1 { // Full scan iter = tx.NewIterator() } else if len(parts) == 2 { // Prefix scan prefix := []byte(parts[1]) prefixEnd := makeKeySuccessor(prefix) iter = tx.NewRangeIterator(prefix, prefixEnd) } else if len(parts) == 3 && strings.ToUpper(parts[1]) == "RANGE" { // Syntax error fmt.Println("Error: SCAN RANGE requires start and end keys") continue } else if len(parts) == 4 && strings.ToUpper(parts[1]) == "RANGE" { // Range scan with explicit RANGE keyword iter = tx.NewRangeIterator([]byte(parts[2]), []byte(parts[3])) } else if len(parts) == 3 { // Old style range scan fmt.Println("Warning: Using deprecated range syntax. Use 'SCAN RANGE start end' instead.") iter = tx.NewRangeIterator([]byte(parts[1]), []byte(parts[2])) } else { fmt.Println("Error: Invalid SCAN syntax. See .help for usage") continue } } else { // Check if database is open if eng == nil { fmt.Println("Error: No database open") continue } // Use engine iterators var iterErr error if len(parts) == 1 { // Full scan iter, iterErr = eng.GetIterator() } else if len(parts) == 2 { // Prefix scan prefix := []byte(parts[1]) prefixEnd := makeKeySuccessor(prefix) iter, iterErr = eng.GetRangeIterator(prefix, prefixEnd) } else if len(parts) == 3 && strings.ToUpper(parts[1]) == "RANGE" { // Syntax error fmt.Println("Error: SCAN RANGE requires start and end keys") continue } else if len(parts) == 4 && strings.ToUpper(parts[1]) == "RANGE" { // Range scan with explicit RANGE keyword iter, iterErr = eng.GetRangeIterator([]byte(parts[2]), []byte(parts[3])) } else if len(parts) == 3 { // Old style range scan fmt.Println("Warning: Using deprecated range syntax. Use 'SCAN RANGE start end' instead.") iter, iterErr = eng.GetRangeIterator([]byte(parts[1]), []byte(parts[2])) } else { fmt.Println("Error: Invalid SCAN syntax. See .help for usage") continue } if iterErr != nil { fmt.Fprintf(os.Stderr, "Error creating iterator: %s\n", iterErr) continue } } // Perform the scan count := 0 for iter.SeekToFirst(); iter.Valid(); iter.Next() { // Check if this key exists in the engine via Get to ensure consistency var keyExists bool var keyValue []byte if tx != nil { // Use transaction Get keyValue, err = tx.Get(iter.Key()) keyExists = (err == nil) } else { // Use engine Get keyValue, err = eng.Get(iter.Key()) keyExists = (err == nil) } // Only display key if it actually exists if keyExists { fmt.Printf("%s: %s\n", iter.Key(), keyValue) count++ } } fmt.Printf("%d entries found\n", count) default: fmt.Printf("Unknown command: %s\n", cmd) } } } // makeKeySuccessor creates the successor key for a prefix scan // by adding a 0xFF byte to the end of the prefix func makeKeySuccessor(prefix []byte) []byte { successor := make([]byte, len(prefix)+1) copy(successor, prefix) successor[len(prefix)] = 0xFF return successor }