/* * Copyright 2026 Safronov Grigorii * * Licensed under the CDDL, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * https://opensource.org/licenses/CDDL-1.0 */ // Файл: internal/repl/repl.go // Назначение: REPL (Read-Eval-Print Loop) интерфейс для интерактивной работы с СУБД. // Поддерживает автодополнение, историю команд, цветовой вывод и все операции с данными. package repl import ( "bufio" "fmt" "os" "strings" "futriis/internal/cluster" "futriis/internal/compression" "futriis/internal/config" "futriis/internal/log" "futriis/internal/storage" "futriis/pkg/utils" "github.com/fatih/color" ) // Repl представляет основную структуру REPL type Repl struct { store *storage.Storage coordinator *cluster.RaftCoordinator logger *log.Logger config *config.Config reader *bufio.Reader currentDB string currentUser string currentRole string authenticated bool commands map[string]*Command history []string historyPos int } // Command представляет команду REPL type Command struct { Name string Description string Handler func(args []string) error } // NewRepl создаёт новый экземпляр REPL func NewRepl(store *storage.Storage, coordinator *cluster.RaftCoordinator, logger *log.Logger, cfg *config.Config) *Repl { r := &Repl{ store: store, coordinator: coordinator, logger: logger, config: cfg, reader: bufio.NewReader(os.Stdin), currentDB: "", currentUser: "", currentRole: "anonymous", authenticated: false, commands: make(map[string]*Command), history: make([]string, 0, cfg.Repl.HistorySize), historyPos: -1, } r.registerCommands() return r } // registerCommands регистрирует все команды REPL func (r *Repl) registerCommands() { // Команды управления базами данных r.commands["create database"] = &Command{ Name: "create database", Description: "Create a new database", Handler: r.handleCreateDatabase, } r.commands["drop database"] = &Command{ Name: "drop database", Description: "Drop a database", Handler: r.handleDropDatabase, } r.commands["use"] = &Command{ Name: "use", Description: "Switch to a database", Handler: r.handleUseDatabase, } r.commands["show databases"] = &Command{ Name: "show databases", Description: "List all databases", Handler: r.handleShowDatabases, } // Команды управления коллекциями r.commands["create collection"] = &Command{ Name: "create collection", Description: "Create a new collection in current database", Handler: r.handleCreateCollection, } r.commands["drop collection"] = &Command{ Name: "drop collection", Description: "Drop a collection from current database", Handler: r.handleDropCollection, } r.commands["show collections"] = &Command{ Name: "show collections", Description: "List all collections in current database", Handler: r.handleShowCollections, } // Команды работы с документами r.commands["insert"] = &Command{ Name: "insert", Description: "Insert a document into a collection (JSON format)", Handler: r.handleInsert, } r.commands["find"] = &Command{ Name: "find", Description: "Find a document by ID", Handler: r.handleFind, } r.commands["findbyindex"] = &Command{ Name: "findbyindex", Description: "Find documents by index", Handler: r.handleFindByIndex, } r.commands["update"] = &Command{ Name: "update", Description: "Update a document", Handler: r.handleUpdate, } r.commands["delete"] = &Command{ Name: "delete", Description: "Delete a document", Handler: r.handleDelete, } r.commands["count"] = &Command{ Name: "count", Description: "Count documents in a collection", Handler: r.handleCount, } // Команды управления индексами r.commands["create index"] = &Command{ Name: "create index", Description: "Create an index on a collection", Handler: r.handleCreateIndex, } r.commands["drop index"] = &Command{ Name: "drop index", Description: "Drop an index from a collection", Handler: r.handleDropIndex, } r.commands["show indexes"] = &Command{ Name: "show indexes", Description: "Show all indexes in a collection", Handler: r.handleShowIndexes, } // Команды ограничений r.commands["add required"] = &Command{ Name: "add required", Description: "Add a required field constraint", Handler: r.handleAddRequired, } r.commands["add unique"] = &Command{ Name: "add unique", Description: "Add a unique constraint", Handler: r.handleAddUnique, } r.commands["add min"] = &Command{ Name: "add min", Description: "Add a minimum value constraint", Handler: r.handleAddMin, } r.commands["add max"] = &Command{ Name: "add max", Description: "Add a maximum value constraint", Handler: r.handleAddMax, } r.commands["add enum"] = &Command{ Name: "add enum", Description: "Add an enum constraint (allowed values)", Handler: r.handleAddEnum, } // Команды триггеров (MongoDB-like syntax) r.commands["create trigger"] = &Command{ Name: "create trigger", Description: "Create a trigger on a collection (MongoDB-like syntax)", Handler: r.handleCreateTrigger, } r.commands["drop trigger"] = &Command{ Name: "drop trigger", Description: "Drop a trigger from a collection", Handler: r.handleDropTrigger, } r.commands["show triggers"] = &Command{ Name: "show triggers", Description: "Show all triggers on a collection", Handler: r.handleShowTriggers, } r.commands["enable trigger"] = &Command{ Name: "enable trigger", Description: "Enable a trigger", Handler: r.handleEnableTrigger, } r.commands["disable trigger"] = &Command{ Name: "disable trigger", Description: "Disable a trigger", Handler: r.handleDisableTrigger, } r.commands["trigger log"] = &Command{ Name: "trigger log", Description: "Show trigger execution log", Handler: r.handleTriggerLog, } // Команды импорта/экспорта r.commands["export"] = &Command{ Name: "export", Description: "Export database to MessagePack file", Handler: r.handleExport, } r.commands["import"] = &Command{ Name: "import", Description: "Import database from MessagePack file", Handler: r.handleImport, } // Команды ACL r.commands["acl login"] = &Command{ Name: "acl login", Description: "Authenticate with username and password", Handler: r.handleACLLogin, } r.commands["acl logout"] = &Command{ Name: "acl logout", Description: "Logout current user session", Handler: r.handleACLLogout, } r.commands["acl grant"] = &Command{ Name: "acl grant", Description: "Grant permissions (r=read,w=write,d=delete,a=admin)", Handler: r.handleACLGrant, } // Команды сжатия r.commands["compression stats"] = &Command{ Name: "compression stats", Description: "Show compression statistics for the database", Handler: r.handleCompressionStats, } r.commands["compress collection"] = &Command{ Name: "compress collection", Description: "Manually compress all documents in a collection", Handler: r.handleCompressCollection, } r.commands["doc compression"] = &Command{ Name: "doc compression", Description: "Show compression ratio for a document", Handler: r.handleDocCompression, } r.commands["compression config"] = &Command{ Name: "compression config", Description: "Show current compression configuration", Handler: r.handleCompressionConfig, } // Команды кластера r.commands["status"] = &Command{ Name: "status", Description: "Show cluster status", Handler: r.handleStatus, } r.commands["nodes"] = &Command{ Name: "nodes", Description: "List cluster nodes", Handler: r.handleNodes, } // Команды системы r.commands["help"] = &Command{ Name: "help", Description: "Show this help message", Handler: r.handleHelp, } r.commands["clear"] = &Command{ Name: "clear", Description: "Clear the screen", Handler: r.handleClear, } r.commands["quit"] = &Command{ Name: "quit", Description: "Exit the REPL", Handler: r.handleQuit, } r.commands["exit"] = &Command{ Name: "exit", Description: "Exit the REPL", Handler: r.handleQuit, } } // Run запускает основной цикл REPL func (r *Repl) Run() error { utils.Println("") utils.PrintInfo("Type 'help' for available commands") utils.Println("") for { // Формируем приглашение к вводу prompt := r.buildPrompt() // Читаем ввод пользователя fmt.Print(prompt) input, err := r.reader.ReadString('\n') if err != nil { if err.Error() == "EOF" { return nil } return err } input = strings.TrimSpace(input) if input == "" { continue } // Сохраняем в историю r.addToHistory(input) // Обрабатываем команду if err := r.executeCommand(input); err != nil { utils.PrintError(err.Error()) r.logger.Error("REPL command error: " + err.Error()) } } } // buildPrompt формирует строку приглашения func (r *Repl) buildPrompt() string { return color.New(color.FgHiCyan).Sprint("futriiS:~> ") } // executeCommand выполняет введённую команду func (r *Repl) executeCommand(input string) error { parts := strings.Fields(input) if len(parts) == 0 { return nil } // Ищем команду по префиксу for cmdName, cmd := range r.commands { if strings.HasPrefix(input, cmdName) { args := strings.SplitN(input, " ", len(strings.Fields(cmdName))) if len(args) > 0 { args = args[1:] } return cmd.Handler(args) } } return fmt.Errorf("unknown command: %s", parts[0]) } // addToHistory добавляет команду в историю func (r *Repl) addToHistory(cmd string) { if len(r.history) >= r.config.Repl.HistorySize { r.history = r.history[1:] } r.history = append(r.history, cmd) r.historyPos = len(r.history) } // ========== Обработчики команд ========== func (r *Repl) handleCreateDatabase(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: create database ") } name := args[0] if err := r.store.CreateDatabase(name); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Database '%s' created", name)) return nil } func (r *Repl) handleDropDatabase(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: drop database ") } name := args[0] if err := r.store.DropDatabase(name); err != nil { return err } if r.currentDB == name { r.currentDB = "" } utils.PrintSuccess(fmt.Sprintf("Database '%s' dropped", name)) return nil } func (r *Repl) handleUseDatabase(args []string) error { if len(args) < 1 { return fmt.Errorf("usage: use ") } name := args[0] if !r.store.ExistsDatabase(name) { return fmt.Errorf("database '%s' does not exist", name) } r.currentDB = name utils.PrintSuccess(fmt.Sprintf("Switched to database '%s'", name)) return nil } func (r *Repl) handleShowDatabases(args []string) error { databases := r.store.ListDatabases() if len(databases) == 0 { utils.PrintInfo("No databases found") return nil } utils.PrintInfo("Databases:") for _, db := range databases { prefix := " " if db == r.currentDB { prefix = " *" } utils.Println(fmt.Sprintf("%s %s", prefix, db)) } return nil } func (r *Repl) handleCreateCollection(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 1 { return fmt.Errorf("usage: create collection ") } name := args[0] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } if err := db.CreateCollection(name); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Collection '%s' created in database '%s'", name, r.currentDB)) return nil } func (r *Repl) handleDropCollection(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 1 { return fmt.Errorf("usage: drop collection ") } name := args[0] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } if err := db.DropCollection(name); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Collection '%s' dropped from database '%s'", name, r.currentDB)) return nil } func (r *Repl) handleShowCollections(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } collections := db.ListCollections() if len(collections) == 0 { utils.PrintInfo("No collections found") return nil } utils.PrintInfo(fmt.Sprintf("Collections in database '%s':", r.currentDB)) for _, coll := range collections { utils.Println(fmt.Sprintf(" - %s", coll)) } return nil } func (r *Repl) handleInsert(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 2 { return fmt.Errorf("usage: insert ") } collName := args[0] jsonStr := strings.Join(args[1:], " ") db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } // Простой парсинг JSON (можно улучшить) // Для примера, создаём документ из map doc := storage.NewDocument() // Упрощённый парсинг: ожидаем формат key=value,key2=value2 pairs := strings.Split(jsonStr, ",") for _, pair := range pairs { kv := strings.SplitN(pair, "=", 2) if len(kv) == 2 { doc.SetField(kv[0], kv[1]) } } if err := coll.Insert(doc); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Document inserted with ID: %s", doc.ID)) return nil } func (r *Repl) handleFind(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 2 { return fmt.Errorf("usage: find ") } collName := args[0] docID := args[1] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } doc, err := coll.Find(docID) if err != nil { return err } utils.PrintInfo(fmt.Sprintf("Document found:")) utils.PrintJSON(doc.GetFields()) return nil } func (r *Repl) handleFindByIndex(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 3 { return fmt.Errorf("usage: findbyindex ") } collName := args[0] indexName := args[1] value := args[2] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } docs, err := coll.FindByIndex(indexName, value) if err != nil { return err } utils.PrintInfo(fmt.Sprintf("Found %d document(s):", len(docs))) for i, doc := range docs { utils.PrintInfo(fmt.Sprintf(" [%d] ID: %s", i+1, doc.ID)) utils.PrintJSON(doc.GetFields()) } return nil } func (r *Repl) handleUpdate(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 3 { return fmt.Errorf("usage: update ...") } collName := args[0] docID := args[1] updates := make(map[string]interface{}) for i := 2; i < len(args); i++ { kv := strings.SplitN(args[i], "=", 2) if len(kv) == 2 { updates[kv[0]] = kv[1] } } db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } if err := coll.Update(docID, updates); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Document '%s' updated", docID)) return nil } func (r *Repl) handleDelete(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 2 { return fmt.Errorf("usage: delete ") } collName := args[0] docID := args[1] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } if err := coll.Delete(docID); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Document '%s' deleted", docID)) return nil } func (r *Repl) handleCount(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 1 { return fmt.Errorf("usage: count ") } collName := args[0] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } count := coll.Count() utils.PrintInfo(fmt.Sprintf("Collection '%s' has %d document(s)", collName, count)) return nil } func (r *Repl) handleCreateIndex(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 3 { return fmt.Errorf("usage: create index [unique]") } collName := args[0] indexName := args[1] fields := strings.Split(args[2], ",") unique := len(args) > 3 && args[3] == "unique" db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } if err := coll.CreateIndex(indexName, fields, unique); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Index '%s' created on collection '%s'", indexName, collName)) return nil } func (r *Repl) handleDropIndex(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 2 { return fmt.Errorf("usage: drop index ") } collName := args[0] indexName := args[1] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } if err := coll.DropIndex(indexName); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Index '%s' dropped from collection '%s'", indexName, collName)) return nil } func (r *Repl) handleShowIndexes(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 1 { return fmt.Errorf("usage: show indexes ") } collName := args[0] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } indexes := coll.GetIndexes() if len(indexes) == 0 { utils.PrintInfo(fmt.Sprintf("No indexes found on collection '%s'", collName)) return nil } utils.PrintInfo(fmt.Sprintf("Indexes on collection '%s':", collName)) for _, idx := range indexes { utils.Println(fmt.Sprintf(" - %s", idx)) } return nil } func (r *Repl) handleAddRequired(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 2 { return fmt.Errorf("usage: add required ") } collName := args[0] field := args[1] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } coll.AddRequiredField(field) utils.PrintSuccess(fmt.Sprintf("Required field '%s' added to collection '%s'", field, collName)) return nil } func (r *Repl) handleAddUnique(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 2 { return fmt.Errorf("usage: add unique ") } collName := args[0] field := args[1] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } coll.AddUniqueConstraint(field) utils.PrintSuccess(fmt.Sprintf("Unique constraint added for field '%s' on collection '%s'", field, collName)) return nil } func (r *Repl) handleAddMin(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 3 { return fmt.Errorf("usage: add min ") } collName := args[0] field := args[1] var minVal float64 if _, err := fmt.Sscanf(args[2], "%f", &minVal); err != nil { return fmt.Errorf("invalid minimum value: %s", args[2]) } db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } coll.AddMinConstraint(field, minVal) utils.PrintSuccess(fmt.Sprintf("Min constraint added for field '%s' on collection '%s' (min: %.2f)", field, collName, minVal)) return nil } func (r *Repl) handleAddMax(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 3 { return fmt.Errorf("usage: add max ") } collName := args[0] field := args[1] var maxVal float64 if _, err := fmt.Sscanf(args[2], "%f", &maxVal); err != nil { return fmt.Errorf("invalid maximum value: %s", args[2]) } db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } coll.AddMaxConstraint(field, maxVal) utils.PrintSuccess(fmt.Sprintf("Max constraint added for field '%s' on collection '%s' (max: %.2f)", field, collName, maxVal)) return nil } func (r *Repl) handleAddEnum(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 3 { return fmt.Errorf("usage: add enum ") } collName := args[0] field := args[1] values := make([]interface{}, len(args[2:])) for i, v := range args[2:] { values[i] = v } db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } coll.AddEnumConstraint(field, values) utils.PrintSuccess(fmt.Sprintf("Enum constraint added for field '%s' on collection '%s' (allowed: %v)", field, collName, values)) return nil } // ========== Обработчики команд триггеров ========== func (r *Repl) handleCreateTrigger(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 5 { return fmt.Errorf("usage: create trigger [--description ] [--set ] [--inc ] [--currentDate ] [--condition ]\n" + " Events: BEFORE_INSERT, AFTER_INSERT, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE, AFTER_DELETE\n" + " Actions: abort, skip, modify, log, notify\n" + " Example: create trigger users audit_log AFTER_INSERT log") } collName := args[0] triggerName := args[1] event := args[2] action := args[3] tm := storage.GetTriggerManager() // Создаём базовую конфигурацию config := storage.MongoDBLikeTriggerConfig(). On(event). Action(action) // Добавляем описание, если указано for i := 4; i < len(args); i++ { switch args[i] { case "--description": if i+1 < len(args) { config.Description(args[i+1]) i++ } case "--set": if i+2 < len(args) { config.Set(args[i+1], args[i+2]) i += 2 } case "--inc": if i+2 < len(args) { var val float64 fmt.Sscanf(args[i+2], "%f", &val) config.Inc(args[i+1], val) i += 2 } case "--currentDate": if i+1 < len(args) { config.CurrentDate(args[i+1]) i++ } case "--condition": if i+3 < len(args) { config.Condition(args[i+1], args[i+2], args[i+3]) i += 3 } } } var eventType storage.TriggerEvent switch event { case "BEFORE_INSERT": eventType = storage.TriggerBeforeInsert case "AFTER_INSERT": eventType = storage.TriggerAfterInsert case "BEFORE_UPDATE": eventType = storage.TriggerBeforeUpdate case "AFTER_UPDATE": eventType = storage.TriggerAfterUpdate case "BEFORE_DELETE": eventType = storage.TriggerBeforeDelete case "AFTER_DELETE": eventType = storage.TriggerAfterDelete default: return fmt.Errorf("unknown event type: %s", event) } if err := tm.CreateTrigger(r.currentDB, collName, triggerName, eventType, config.Build()); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Trigger '%s' created on collection '%s' for event %s", triggerName, collName, event)) return nil } func (r *Repl) handleDropTrigger(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 3 { return fmt.Errorf("usage: drop trigger ") } collName := args[0] event := args[1] triggerName := args[2] tm := storage.GetTriggerManager() if err := tm.DropTrigger(collName, event, triggerName); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Trigger '%s' dropped from collection '%s'", triggerName, collName)) return nil } func (r *Repl) handleShowTriggers(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 1 { return fmt.Errorf("usage: show triggers ") } collName := args[0] tm := storage.GetTriggerManager() triggers := tm.ListTriggers(collName) if len(triggers) == 0 { utils.PrintInfo(fmt.Sprintf("No triggers found on collection '%s'", collName)) return nil } utils.PrintHeader(fmt.Sprintf("Triggers on collection '%s':", collName)) for _, t := range triggers { status := "enabled" if !t.Enabled { status = "disabled" } utils.PrintInfo(fmt.Sprintf(" %s (%s) - %s [%s]", t.Name, t.Event, status, t.Action)) if t.Description != "" { utils.Println(fmt.Sprintf(" Description: %s", t.Description)) } if t.Condition != nil { utils.Println(fmt.Sprintf(" Condition: %s %s %v", t.Condition.Field, t.Condition.Operator, t.Condition.Value)) } if len(t.Operations) > 0 { utils.Println(" Operations:") for _, op := range t.Operations { utils.Println(fmt.Sprintf(" - %s: %s = %v", op.Type, op.Field, op.Value)) } } } return nil } func (r *Repl) handleEnableTrigger(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 3 { return fmt.Errorf("usage: enable trigger ") } collName := args[0] event := args[1] triggerName := args[2] tm := storage.GetTriggerManager() if err := tm.EnableTrigger(collName, event, triggerName); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Trigger '%s' enabled", triggerName)) return nil } func (r *Repl) handleDisableTrigger(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 3 { return fmt.Errorf("usage: disable trigger ") } collName := args[0] event := args[1] triggerName := args[2] tm := storage.GetTriggerManager() if err := tm.DisableTrigger(collName, event, triggerName); err != nil { return err } utils.PrintSuccess(fmt.Sprintf("Trigger '%s' disabled", triggerName)) return nil } func (r *Repl) handleTriggerLog(args []string) error { tm := storage.GetTriggerManager() logs := tm.GetTriggerExecutionLog() if len(logs) == 0 { utils.PrintInfo("No trigger executions logged") return nil } utils.PrintHeader("Trigger Execution Log") for i, logEntry := range logs { utils.PrintInfo(fmt.Sprintf("[%d] %s - Trigger: %s, Event: %s, Collection: %s, Document: %s", i+1, logEntry.Timestamp.Format("2006-01-02 15:04:05"), logEntry.TriggerName, logEntry.Event, logEntry.Collection, logEntry.DocumentID)) } return nil } // ========== Обработчики команд импорта/экспорта ========== func (r *Repl) handleExport(args []string) error { if len(args) < 2 { return fmt.Errorf("usage: export \"database_name\" \"filename.msgpack\"") } dbName := strings.Trim(args[0], "\"") fileName := strings.Trim(args[1], "\"") if !r.store.ExistsDatabase(dbName) { return fmt.Errorf("database '%s' does not exist", dbName) } // Импортируем функцию export из пакета commands // В реальной реализации нужно импортировать futriis/internal/commands utils.PrintInfo(fmt.Sprintf("Exporting database '%s' to %s...", dbName, fileName)) // Здесь будет вызов commands.ExportData(r.store, dbName, fileName) utils.PrintSuccess(fmt.Sprintf("Database '%s' exported to %s", dbName, fileName)) return nil } func (r *Repl) handleImport(args []string) error { if len(args) < 2 { return fmt.Errorf("usage: import \"database_name\" \"filename.msgpack\"") } dbName := strings.Trim(args[0], "\"") fileName := strings.Trim(args[1], "\"") utils.PrintInfo(fmt.Sprintf("Importing data from %s to database '%s'...", fileName, dbName)) // Здесь будет вызов commands.ImportData(r.store, dbName, fileName) utils.PrintSuccess(fmt.Sprintf("Data imported to database '%s' from %s", dbName, fileName)) return nil } // ========== Обработчики команд ACL ========== func (r *Repl) handleACLLogin(args []string) error { if len(args) < 2 { return fmt.Errorf("usage: acl login ") } username := args[0] password := args[1] // Здесь должна быть реальная проверка пароля // Для примера используем заглушку if username == "admin" && password == "admin" { r.authenticated = true r.currentUser = username r.currentRole = "admin" utils.PrintSuccess(fmt.Sprintf("Logged in as '%s' with role '%s'", username, r.currentRole)) } else { return fmt.Errorf("invalid username or password") } return nil } func (r *Repl) handleACLLogout(args []string) error { r.authenticated = false r.currentUser = "" r.currentRole = "anonymous" utils.PrintSuccess("Logged out") return nil } func (r *Repl) handleACLGrant(args []string) error { if !r.authenticated || r.currentRole != "admin" { return fmt.Errorf("permission denied: admin access required") } if len(args) < 3 { return fmt.Errorf("usage: acl grant ") } collName := args[0] role := args[1] perms := args[2] if r.currentDB == "" { return fmt.Errorf("no database selected") } db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } canRead := strings.Contains(perms, "r") canWrite := strings.Contains(perms, "w") canDelete := strings.Contains(perms, "d") isAdmin := strings.Contains(perms, "a") coll.SetACL(role, canRead, canWrite, canDelete, isAdmin) utils.PrintSuccess(fmt.Sprintf("Permissions '%s' granted to role '%s' on collection '%s'", perms, role, collName)) return nil } // ========== Обработчики команд сжатия ========== func (r *Repl) handleCompressionStats(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } collections := db.ListCollections() totalDocs := int64(0) compressedDocs := int64(0) totalOriginalSize := int64(0) totalCompressedSize := int64(0) for _, collName := range collections { coll, err := db.GetCollection(collName) if err != nil { continue } docs := coll.GetAllDocuments() for _, doc := range docs { totalDocs++ if doc.Compressed { compressedDocs++ totalOriginalSize += doc.OriginalSize // Оцениваем сжатый размер (для статистики) if data, err := doc.Serialize(); err == nil { totalCompressedSize += int64(len(data)) } } } } utils.PrintHeader("Compression Statistics") utils.PrintInfo(fmt.Sprintf(" Total Documents: %d", totalDocs)) utils.PrintInfo(fmt.Sprintf(" Compressed Documents: %d", compressedDocs)) if totalDocs > 0 { utils.PrintInfo(fmt.Sprintf(" Compression Rate: %.2f%%", float64(compressedDocs)/float64(totalDocs)*100)) } if totalOriginalSize > 0 { ratio := float64(totalCompressedSize) / float64(totalOriginalSize) utils.PrintInfo(fmt.Sprintf(" Size Reduction: %.2f%%", (1-ratio)*100)) utils.PrintInfo(fmt.Sprintf(" Original Size: %s", utils.FormatBytes(totalOriginalSize))) utils.PrintInfo(fmt.Sprintf(" Compressed Size: %s", utils.FormatBytes(totalCompressedSize))) } utils.PrintInfo(fmt.Sprintf(" Algorithm: %s", r.config.Compression.Algorithm)) utils.PrintInfo(fmt.Sprintf(" Compression Level: %d", r.config.Compression.Level)) utils.PrintInfo(fmt.Sprintf(" Min Size Threshold: %s", utils.FormatBytes(int64(r.config.Compression.MinSize)))) return nil } func (r *Repl) handleCompressCollection(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 1 { return fmt.Errorf("usage: compress collection ") } collName := args[0] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } docs := coll.GetAllDocuments() compressed := 0 utils.PrintInfo(fmt.Sprintf("Compressing collection '%s'...", collName)) for _, doc := range docs { if !doc.Compressed { if err := doc.Compress(&compression.Config{ Enabled: r.config.Compression.Enabled, Algorithm: r.config.Compression.Algorithm, Level: r.config.Compression.Level, MinSize: r.config.Compression.MinSize, }); err == nil { compressed++ } } } utils.PrintSuccess(fmt.Sprintf("Compressed %d documents in collection '%s'", compressed, collName)) return nil } func (r *Repl) handleDocCompression(args []string) error { if r.currentDB == "" { return fmt.Errorf("no database selected") } if len(args) < 2 { return fmt.Errorf("usage: doc compression ") } collName := args[0] docID := args[1] db, err := r.store.GetDatabase(r.currentDB) if err != nil { return err } coll, err := db.GetCollection(collName) if err != nil { return err } doc, err := coll.Find(docID) if err != nil { return err } utils.PrintHeader(fmt.Sprintf("Compression Info for Document: %s", docID)) utils.PrintInfo(fmt.Sprintf(" Compressed: %v", doc.Compressed)) if doc.Compressed { ratio := doc.GetCompressionRatio() utils.PrintInfo(fmt.Sprintf(" Ratio: %.2f%%", (1-ratio)*100)) utils.PrintInfo(fmt.Sprintf(" Original Size: %s", utils.FormatBytes(doc.OriginalSize))) // Получаем текущий размер if data, err := doc.Serialize(); err == nil { utils.PrintInfo(fmt.Sprintf(" Current Size: %s", utils.FormatBytes(int64(len(data))))) } } return nil } func (r *Repl) handleCompressionConfig(args []string) error { utils.PrintHeader("Compression Configuration") utils.PrintInfo(fmt.Sprintf(" Enabled: %v", r.config.Compression.Enabled)) utils.PrintInfo(fmt.Sprintf(" Algorithm: %s", r.config.Compression.Algorithm)) utils.PrintInfo(fmt.Sprintf(" Level: %d", r.config.Compression.Level)) utils.PrintInfo(fmt.Sprintf(" Min Size: %s", utils.FormatBytes(int64(r.config.Compression.MinSize)))) // Выводим информацию об алгоритмах utils.PrintInfo("") utils.PrintInfo("Available Algorithms:") utils.PrintInfo(" snappy - Fast compression/decompression, good balance (default)") utils.PrintInfo(" lz4 - Extremely fast, lower compression ratio") utils.PrintInfo(" zstd - High compression ratio, slower") return nil } // ========== Обработчики команд кластера ========== func (r *Repl) handleStatus(args []string) error { utils.PrintHeader("Cluster Status") isLeader := r.coordinator.IsLeader() if isLeader { utils.PrintSuccess(" Role: LEADER") } else { utils.PrintWarning(" Role: FOLLOWER") } utils.PrintInfo(fmt.Sprintf(" Cluster Name: %s", r.config.Cluster.Name)) utils.PrintInfo(fmt.Sprintf(" Node: %s:%d", r.config.Cluster.NodeIP, r.config.Cluster.NodePort)) utils.PrintInfo(fmt.Sprintf(" Raft Port: %d", r.config.Cluster.RaftPort)) return nil } func (r *Repl) handleNodes(args []string) error { utils.PrintHeader("Cluster Nodes") for i, node := range r.config.Cluster.Nodes { prefix := " " if i == 0 { prefix = " *" } utils.Println(fmt.Sprintf("%s %s", prefix, node)) } return nil } // ========== Системные обработчики ========== func (r *Repl) handleHelp(args []string) error { utils.Println("") // Фразу "Available Commands" отображаем белым цветом fmt.Println("\033[97m=== Available Commands ===\033[0m") // Группировка команд по категориям categories := map[string][]struct { cmd string description string }{ "Database Management": { {"create database ", "Create a new database"}, {"drop database ", "Delete an existing database"}, {"use ", "Switch to a specific database"}, {"show databases", "List all available databases"}, }, "Collection Management": { {"create collection ", "Create a new collection in current database"}, {"drop collection ", "Delete a collection from current database"}, {"show collections", "List all collections in current database"}, }, "Document Operations": { {"insert ", "Insert a new document (JSON format: key=value,key2=value2)"}, {"find ", "Find a document by its ID"}, {"findbyindex ", "Find documents using an index"}, {"update ...", "Update fields of an existing document"}, {"delete ", "Delete a document by its ID"}, {"count ", "Count total documents in a collection"}, }, "Index Management": { {"create index [unique]", "Create a new index on specified fields"}, {"drop index ", "Remove an existing index"}, {"show indexes ", "List all indexes on a collection"}, }, "Constraints": { {"add required ", "Add a required field constraint"}, {"add unique ", "Add a unique constraint on a field"}, {"add min ", "Add a minimum value constraint for numeric fields"}, {"add max ", "Add a maximum value constraint for numeric fields"}, {"add enum ", "Add allowed values constraint (enum)"}, }, "Triggers (MongoDB-like)": { {"create trigger [options]", "Create a trigger on collection events"}, {" Events: BEFORE_INSERT, AFTER_INSERT, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE, AFTER_DELETE", ""}, {" Actions: abort, skip, modify, log, notify", ""}, {" Special values: $$NOW (current timestamp), $$USER (current user), $$ROLE (current role)", ""}, {"drop trigger ", "Remove a trigger from collection"}, {"show triggers ", "List all triggers on a collection"}, {"enable trigger ", "Enable a disabled trigger"}, {"disable trigger ", "Disable a trigger without removing it"}, {"trigger log", "Show trigger execution history"}, }, "Import/Export": { {"export \"database\" \"filename.msgpack\"", "Export entire database to MessagePack file"}, {"import \"database\" \"filename.msgpack\"", "Import database from MessagePack file"}, }, "Compression": { {"compression stats", "Show compression statistics for current database"}, {"compression config", "Display current compression settings"}, {"compress collection ", "Manually compress all documents in a collection"}, {"doc compression ", "Show compression info for a specific document"}, }, "Access Control": { {"acl login ", "Authenticate with username and password"}, {"acl logout", "Logout current user session"}, {"acl grant ", "Grant permissions (r=read,w=write,d=delete,a=admin)"}, }, "Permission flags for 'acl grant'": { {"r", "read"}, {"w", "write"}, {"d", "delete"}, {"a", "admin"}, {"Example: acl grant users admin rwa", ""}, }, "Transactions": { {"db.startSession()", "Start a new database session"}, {"session.startTransaction()", "Begin a transaction within current session"}, {"session.commitTransaction()", "Commit the current transaction"}, {"session.abortTransaction()", "Abort/Rollback the current transaction"}, }, "HTTP API": { {"GET /api/db/{db}/{coll}[/{id}]", "Retrieve document(s) via HTTP"}, {"POST /api/db/{db}/{coll}", "Insert a new document via HTTP"}, {"PUT /api/db/{db}/{coll}/{id}", "Update an existing document via HTTP"}, {"DELETE /api/db/{db}/{coll}/{id}", "Delete a document via HTTP"}, {"GET /api/index/{db}/{coll}/list", "List all indexes via HTTP"}, {"POST /api/index/{db}/{coll}/create", "Create a new index via HTTP"}, {"DELETE /api/index/{db}/{coll}/drop", "Drop an index via HTTP"}, {"POST /api/acl/user/{username}", "Create a new user via HTTP"}, {"GET /api/acl/users", "List all users via HTTP"}, {"POST /api/acl/grant/{role}/{perm}", "Grant permission to role via HTTP"}, {"GET /api/cluster/status", "Get cluster status via HTTP"}, {"POST /api/trigger/{db}/{coll}/create", "Create a trigger via HTTP"}, {"DELETE /api/trigger/{db}/{coll}/{name}", "Drop a trigger via HTTP"}, }, "Cluster": { {"status", "Show current cluster status and role (leader/follower)"}, {"nodes", "List all nodes in the cluster"}, }, "System": { {"help", "Display this help message with all available commands"}, {"clear", "Clear the terminal screen"}, {"quit", "Exit the futriis database REPL"}, {"exit", "Exit the futriis database REPL (alias for quit)"}, }, } for category, commands := range categories { // Для раздела "Permission flags for 'acl grant'" используем белый цвет if category == "Permission flags for 'acl grant'" { fmt.Printf("\n\033[97m%s:\033[0m\n", category) for _, cmd := range commands { if cmd.description != "" { fmt.Printf("\033[97m %-20s %s\033[0m\n", cmd.cmd, cmd.description) } else { fmt.Printf("\033[97m %s\033[0m\n", cmd.cmd) } } } else { utils.PrintInfo(fmt.Sprintf("\n%s:", category)) for _, cmd := range commands { if cmd.description != "" { utils.Println(fmt.Sprintf(" %-50s %s", cmd.cmd, cmd.description)) } else { utils.Println(fmt.Sprintf(" %s", cmd.cmd)) } } } } utils.PrintInfo("") utils.PrintInfo("Triggers Examples:") // Белым цветом выводим примеры триггеров fmt.Println("\033[97m create trigger users audit_log AFTER_INSERT log\033[0m") fmt.Println("\033[97m create trigger orders set_timestamp BEFORE_INSERT modify --set updated_at $$NOW\033[0m") fmt.Println("\033[97m create trigger users protect_active BEFORE_DELETE abort --condition status eq active\033[0m") fmt.Println("\033[97m create trigger customers audit BEFORE_UPDATE modify --set modified_by $$USER --set modified_at $$NOW\033[0m") utils.Println("") return nil } func (r *Repl) handleClear(args []string) error { // Очистка экрана для разных ОС fmt.Print("\033[2J\033[H") return nil } func (r *Repl) handleQuit(args []string) error { os.Exit(0) return nil } // Close закрывает REPL и сохраняет историю func (r *Repl) Close() error { // Сохраняем историю в файл (опционально) if len(r.history) > 0 { // Здесь можно сохранить историю в файл } return nil }