From c2e64364996eeda0aa32ac960240c5686271cf45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=93=D1=80=D0=B8=D0=B3=D0=BE=D1=80=D0=B8=D0=B9=20=D0=A1?= =?UTF-8?q?=D0=B0=D1=84=D1=80=D0=BE=D0=BD=D0=BE=D0=B2?= Date: Mon, 20 Jul 2026 21:30:43 +0000 Subject: [PATCH] Delete internal/repl/repl.go --- internal/repl/repl.go | 3285 ----------------------------------------- 1 file changed, 3285 deletions(-) delete mode 100644 internal/repl/repl.go diff --git a/internal/repl/repl.go b/internal/repl/repl.go deleted file mode 100644 index a677f36..0000000 --- a/internal/repl/repl.go +++ /dev/null @@ -1,3285 +0,0 @@ -/* - * 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 - */ - -// ============================================================================= -// Пакет: repl (Read-Eval-Print Loop) -// ============================================================================= -// Назначение: Предоставляет интерактивный интерфейс командной строки (REPL) -// для взаимодействия с СУБД futriix. Поддерживает автодополнение команд, -// историю ввода, цветной вывод, а также все операции с данными. -// ============================================================================= -// Основные компоненты: -// - Repl: Основная структура, управляющая состоянием REPL -// - Command: Структура, описывающая команду и её обработчик -// - History: Управление историей команд с сохранением в файл -// ============================================================================= -// Использование: -// - Запуск: repl.Run() запускает основной цикл обработки команд -// - Команды: Регистрируются через registerCommands() -// - Обработчики: Каждая команда имеет свой метод-обработчик (handleXXX) -// ============================================================================= - -package repl - -import ( - "bufio" - "fmt" - "os" - "strings" - "time" - - "futriis/internal/acl" - "futriis/internal/cluster" - "futriis/internal/compression" - "futriis/internal/config" - "futriis/internal/log" - "futriis/internal/plugin" - "futriis/internal/storage" - "futriis/pkg/utils" - - "github.com/fatih/color" -) - -// ============================================================================= -// ТИПЫ ДАННЫХ -// ============================================================================= - -// Repl представляет основную структуру REPL, которая управляет всеми аспектами -// интерактивной сессии: состоянием подключения к БД, пользователем, историей, -// зарегистрированными командами и их обработчиками. -// -// Поля: -// - store: Ссылка на основное хранилище данных (Storage) -// - coordinator: Координатор кластера (Raft) для распределённых операций -// - logger: Логгер для записи событий и ошибок -// - config: Конфигурация приложения -// - aclManager: Менеджер контроля доступа (ACL) -// - pluginManager: Менеджер плагинов -// - reader: Буферизированный читатель из stdin -// - currentDB: Имя текущей выбранной базы данных -// - currentUser: Имя текущего аутентифицированного пользователя -// - currentRole: Роль текущего пользователя -// - authenticated: Флаг аутентификации пользователя -// - sessionID: ID текущей сессии -// - commands: Карта зарегистрированных команд (имя -> *Command) -// - history: Список команд в истории -// - historyPos: Текущая позиция в истории (для навигации) -// - historyFile: Менеджер истории для сохранения/загрузки из файла -type Repl struct { - store *storage.Storage - coordinator *cluster.RaftCoordinator - logger *log.Logger - config *config.Config - aclManager *acl.ACLManager - pluginManager *plugin.PluginManager - reader *bufio.Reader - currentDB string - currentUser string - currentRole string - authenticated bool - sessionID string - commands map[string]*Command - history []string - historyPos int - historyFile *History -} - -// Command представляет отдельную команду REPL. -// Содержит имя, описание и функцию-обработчик, которая вызывается при выполнении. -// -// Поля: -// - Name: Уникальное имя команды (используется для поиска) -// - Description: Краткое описание команды для справки -// - Handler: Функция, принимающая список аргументов и возвращающая ошибку -type Command struct { - Name string - Description string - Handler func(args []string) error -} - -// ============================================================================= -// КОНСТРУКТОРЫ -// ============================================================================= - -// NewRepl создаёт новый экземпляр REPL с заданными зависимостями. -// Инициализирует историю команд, загружает сохранённую историю из файла -// и регистрирует все доступные команды. -// -// Параметры: -// - store: Указатель на хранилище данных -// - coordinator: Указатель на координатор кластера -// - logger: Указатель на логгер -// - cfg: Указатель на конфигурацию -// - aclManager: Указатель на менеджер ACL -// - pluginManager: Указатель на менеджер плагинов -// -// Возвращает: -// - *Repl: Инициализированный экземпляр REPL -func NewRepl(store *storage.Storage, coordinator *cluster.RaftCoordinator, logger *log.Logger, cfg *config.Config, aclManager *acl.ACLManager, pluginManager *plugin.PluginManager) *Repl { - r := &Repl{ - store: store, - coordinator: coordinator, - logger: logger, - config: cfg, - aclManager: aclManager, - pluginManager: pluginManager, - reader: bufio.NewReader(os.Stdin), - currentDB: "", - currentUser: "", - currentRole: "anonymous", - authenticated: false, - sessionID: "", - commands: make(map[string]*Command), - history: make([]string, 0, cfg.Repl.HistorySize), - historyPos: -1, - historyFile: NewHistory(cfg.Repl.HistorySize), - } - - // Загружаем историю из файла, если она существует - if err := r.historyFile.Load(); err == nil { - r.history = r.historyFile.GetEntries() - r.historyPos = len(r.history) - } - - r.registerCommands() - return r -} - -// ============================================================================= -// РЕГИСТРАЦИЯ КОМАНД -// ============================================================================= - -// registerCommands регистрирует все доступные команды REPL. -// Команды сгруппированы по функциональным областям: -// - Управление базами данных (create slice, drop database, use, show databases) -// - Управление коллекциями (create collection, drop collection, show collections) -// - Работа с документами (insert, find, update, delete, restore, etc.) -// - Управление индексами (create index, drop index, show indexes) -// - Ограничения (add required, add unique, add min, add max, add enum) -// - Триггеры (MongoDB-like syntax) -// - Транзакции (begin transaction, commit, rollback, show transactions) -// - Плагины (plugin list, load, unload, start, stop, exec) -// - Импорт/экспорт (export, import) -// - Управление доступом (acl login, logout, grant, users, roles) -// - Сжатие (compression stats, compress collection, doc compression) -// - Аудит (audit log, audit filter) -// - Кластер (status, nodes) -// - Системные (help, clear, quit, exit) -func (r *Repl) registerCommands() { - // ------------------------------------------------------------------------- - // Команды управления базами данных - // ------------------------------------------------------------------------- - - r.commands["create slice"] = &Command{ - Name: "create slice", - Description: "Create a new database (slice)", - Handler: r.handleCreateSlice, - } - - 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["findbytime"] = &Command{ - Name: "findbytime", - Description: "Find documents by time range (created_at)", - Handler: r.handleFindByTime, - } - - r.commands["update"] = &Command{ - Name: "update", - Description: "Update a document", - Handler: r.handleUpdate, - } - - r.commands["delete"] = &Command{ - Name: "delete", - Description: "Delete a document (soft delete if enabled)", - Handler: r.handleDelete, - } - - r.commands["permanent delete"] = &Command{ - Name: "permanent delete", - Description: "Permanently delete a soft-deleted document", - Handler: r.handlePermanentDelete, - } - - r.commands["restore"] = &Command{ - Name: "restore", - Description: "Restore a soft-deleted document", - Handler: r.handleRestore, - } - - r.commands["show deleted"] = &Command{ - Name: "show deleted", - Description: "Show soft-deleted documents in a collection", - Handler: r.handleShowDeleted, - } - - r.commands["count"] = &Command{ - Name: "count", - Description: "Count documents in a collection", - Handler: r.handleCount, - } - - r.commands["show timestamps"] = &Command{ - Name: "show timestamps", - Description: "Show timestamps for a document", - Handler: r.handleShowTimestamps, - } - - r.commands["stats timestamps"] = &Command{ - Name: "stats timestamps", - Description: "Show timestamp statistics for a collection", - Handler: r.handleStatsTimestamps, - } - - // ------------------------------------------------------------------------- - // Команды управления индексами - // ------------------------------------------------------------------------- - - 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["begin transaction"] = &Command{ - Name: "begin transaction", - Description: "Start a new transaction", - Handler: r.handleBeginTransaction, - } - - r.commands["commit"] = &Command{ - Name: "commit", - Description: "Commit current transaction", - Handler: r.handleCommitTransaction, - } - - r.commands["rollback"] = &Command{ - Name: "rollback", - Description: "Rollback current transaction", - Handler: r.handleRollbackTransaction, - } - - r.commands["show transactions"] = &Command{ - Name: "show transactions", - Description: "Show active transactions", - Handler: r.handleShowTransactions, - } - - // ------------------------------------------------------------------------- - // Команды плагинов - // ------------------------------------------------------------------------- - - r.commands["plugin list"] = &Command{ - Name: "plugin list", - Description: "List all loaded plugins", - Handler: r.handlePluginList, - } - - r.commands["plugin load"] = &Command{ - Name: "plugin load", - Description: "Load a plugin from file", - Handler: r.handlePluginLoad, - } - - r.commands["plugin unload"] = &Command{ - Name: "plugin unload", - Description: "Unload a plugin", - Handler: r.handlePluginUnload, - } - - r.commands["plugin start"] = &Command{ - Name: "plugin start", - Description: "Start a plugin", - Handler: r.handlePluginStart, - } - - r.commands["plugin stop"] = &Command{ - Name: "plugin stop", - Description: "Stop a plugin", - Handler: r.handlePluginStop, - } - - r.commands["plugin exec"] = &Command{ - Name: "plugin exec", - Description: "Execute a plugin function", - Handler: r.handlePluginExec, - } - - // ------------------------------------------------------------------------- - // Команды импорта/экспорта - // ------------------------------------------------------------------------- - - 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 (Access Control List) - // ------------------------------------------------------------------------- - - 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["acl users"] = &Command{ - Name: "acl users", - Description: "List all users", - Handler: r.handleACLUsers, - } - - r.commands["acl roles"] = &Command{ - Name: "acl roles", - Description: "List all roles", - Handler: r.handleACLRoles, - } - - // ------------------------------------------------------------------------- - // Команды сжатия - // ------------------------------------------------------------------------- - - 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["audit log"] = &Command{ - Name: "audit log", - Description: "Show audit log", - Handler: r.handleAuditLog, - } - - r.commands["audit filter"] = &Command{ - Name: "audit filter", - Description: "Filter audit log by type and operation", - Handler: r.handleAuditFilter, - } - - // ------------------------------------------------------------------------- - // Команды кластера - // ------------------------------------------------------------------------- - - 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, - } -} - -// ============================================================================= -// ОСНОВНОЙ ЦИКЛ REPL -// ============================================================================= - -// Run запускает основной цикл REPL. -// В цикле: -// 1. Формируется приглашение к вводу с учётом текущего состояния -// 2. Считывается ввод пользователя -// 3. Пустые строки игнорируются -// 4. Команды сохраняются в историю -// 5. Выполняется парсинг и обработка команды -// 6. Ошибки выводятся в цветном формате -// -// Цикл продолжается до получения команды quit/exit или EOF. -// Возвращает ошибку только при критической ошибке ввода-вывода. -func (r *Repl) Run() error { - // Выводим приветственное сообщение - utils.Println("") - utils.PrintInfo("Type 'help' for available commands") - utils.Println("") - - for { - // Формируем приглашение к вводу (prompt) с учётом текущего контекста - prompt := r.buildPrompt() - - // Читаем ввод пользователя - fmt.Print(prompt) - input, err := r.reader.ReadString('\n') - if err != nil { - // Обрабатываем EOF (Ctrl+D) как корректный выход - 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()) - if r.logger != nil { - r.logger.Error("REPL command error: " + err.Error()) - } - } - } -} - -// ============================================================================= -// ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ REPL -// ============================================================================= - -// buildPrompt формирует строку приглашения к вводу. -// Формат: futriiX[:database] (username):~> -// Цвета: -// - futriiX: Голубой (HiCyan) -// - :database: Жёлтый (HiYellow) -// - (username): Зелёный (HiGreen) для аутентифицированных пользователей -func (r *Repl) buildPrompt() string { - prompt := color.New(color.FgHiCyan).Sprint("futriiX") - - if r.currentDB != "" { - prompt += color.New(color.FgHiYellow).Sprint(":" + r.currentDB) - } - - if r.authenticated && r.currentUser != "" { - prompt += color.New(color.FgHiGreen).Sprint(" (" + r.currentUser + ")") - } - - prompt += color.New(color.FgHiCyan).Sprint(":~> ") - return prompt -} - -// executeCommand выполняет введённую команду. -// Алгоритм: -// 1. Разбивает ввод на части (поля) -// 2. Проверяет, начинается ли ввод с зарегистрированной команды -// 3. Извлекает аргументы команды (остаток строки после имени команды) -// 4. Вызывает обработчик команды с аргументами -// -// Параметры: -// - input: Полная строка ввода пользователя -// -// Возвращает: -// - error: Ошибка выполнения команды или "unknown command" -func (r *Repl) executeCommand(input string) error { - parts := strings.Fields(input) - if len(parts) == 0 { - return nil - } - - // Ищем команду по префиксу (поддерживаем команды с пробелами, например "create slice") - for cmdName, cmd := range r.commands { - if strings.HasPrefix(input, cmdName) { - // Извлекаем аргументы (всё, что после имени команды) - args := strings.TrimPrefix(input, cmdName) - args = strings.TrimSpace(args) - argList := strings.Fields(args) - return cmd.Handler(argList) - } - } - - return fmt.Errorf("unknown command: %s", parts[0]) -} - -// addToHistory добавляет команду в историю с защитой от дублирования. -// Если команда совпадает с последней в истории, она не добавляется повторно. -// Также автоматически сохраняет историю в файл. -// -// Параметры: -// - cmd: Строка команды для добавления -func (r *Repl) addToHistory(cmd string) { - // Защита от дублирования последней команды - if len(r.history) > 0 && r.history[len(r.history)-1] == cmd { - return - } - - // Ограничиваем размер истории - if len(r.history) >= r.config.Repl.HistorySize { - r.history = r.history[1:] - } - r.history = append(r.history, cmd) - r.historyPos = len(r.history) - - // Сохраняем в файл (асинхронно через History.Add + Save) - r.historyFile.Add(cmd) - r.historyFile.Save() -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД УПРАВЛЕНИЯ БАЗАМИ ДАННЫХ -// ============================================================================= - -// handleCreateSlice — обработчик команды "create slice ". -// Создаёт новую базу данных (slice) с указанным именем. -// -// Аргументы: -// - args[0]: Имя создаваемой базы данных -// -// Возвращает: -// - error: Ошибка при создании или отсутствии аргументов -func (r *Repl) handleCreateSlice(args []string) error { - if len(args) < 1 { - return fmt.Errorf("usage: create slice ") - } - - name := args[0] - if err := r.store.CreateDatabase(name); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Slice '%s' created at %s", name, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleDropDatabase — обработчик команды "drop database ". -// Удаляет существующую базу данных. Если текущая БД удаляется, -// сбрасывает currentDB. -// -// Аргументы: -// - args[0]: Имя удаляемой базы данных -// -// Возвращает: -// - error: Ошибка при удалении или отсутствии аргументов -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 at %s", name, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleUseDatabase — обработчик команды "use ". -// Переключает текущую сессию на указанную базу данных. -// Проверяет существование БД перед переключением. -// -// Аргументы: -// - args[0]: Имя базы данных для использования -// -// Возвращает: -// - error: Ошибка при отсутствии БД или аргументов -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 -} - -// handleShowDatabases — обработчик команды "show databases". -// Выводит список всех существующих баз данных. -// Текущая база данных отмечается звёздочкой (*). -func (r *Repl) handleShowDatabases(args []string) error { - databases := r.store.ListDatabases() - if len(databases) == 0 { - utils.PrintInfo("No databases found") - return nil - } - - utils.PrintHeader("Databases") - for _, db := range databases { - prefix := " " - if db == r.currentDB { - prefix = " *" - } - utils.Println(fmt.Sprintf("%s %s", prefix, db)) - } - return nil -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД УПРАВЛЕНИЯ КОЛЛЕКЦИЯМИ -// ============================================================================= - -// handleCreateCollection — обработчик команды "create collection ". -// Создаёт новую коллекцию в текущей базе данных. -// Требует выбранной базы данных (use). -// -// Аргументы: -// - args[0]: Имя создаваемой коллекции -// -// Возвращает: -// - error: Ошибка при создании, отсутствии аргументов или выборе БД -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' at %s", name, r.currentDB, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleDropCollection — обработчик команды "drop collection ". -// Удаляет коллекцию из текущей базы данных. -// Требует выбранной базы данных. -// -// Аргументы: -// - args[0]: Имя удаляемой коллекции -// -// Возвращает: -// - error: Ошибка при удалении, отсутствии аргументов или выборе БД -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 -} - -// handleShowCollections — обработчик команды "show collections". -// Выводит список всех коллекций в текущей базе данных. -// Требует выбранной базы данных. -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 -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД РАБОТЫ С ДОКУМЕНТАМИ -// ============================================================================= - -// handleInsert — обработчик команды "insert ". -// Вставляет новый документ в указанную коллекцию. -// Поддерживает два формата: -// 1. key=value,key2=value2 (простой формат) -// 2. {key=value,key2=value2} (JSON-подобный формат) -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1:]: Пары ключ=значение -// -// Возвращает: -// - error: Ошибка при вставке, отсутствии аргументов или выборе БД -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 - } - - doc := storage.NewDocument() - - // Парсим входные данные: поддерживаем как JSON, так и простой key=value - if strings.Contains(jsonStr, "{") { - // JSON-подобный формат: извлекаем пары из фигурных скобок - pairs := strings.Split(jsonStr, ",") - for _, pair := range pairs { - pair = strings.TrimSpace(pair) - pair = strings.Trim(pair, "{}") - kv := strings.SplitN(pair, "=", 2) - if len(kv) == 2 { - doc.SetField(kv[0], kv[1]) - } - } - } else { - // Простой формат: key=value,key2=value2 - pairs := strings.Split(jsonStr, ",") - for _, pair := range pairs { - pair = strings.TrimSpace(pair) - 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 (created at: %s)", - doc.ID, time.UnixMilli(doc.CreatedAt).Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleFind — обработчик команды "find ". -// Находит документ по ID в указанной коллекции и выводит его содержимое -// вместе с временными метками. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: ID документа -// -// Возвращает: -// - error: Ошибка при поиске, отсутствии аргументов или выборе БД -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()) - - // Выводим все временные метки документа - utils.PrintInfo(fmt.Sprintf(" created_at: %s", time.UnixMilli(doc.CreatedAt).Format("2006-01-02 15:04:05.000"))) - utils.PrintInfo(fmt.Sprintf(" updated_at: %s", time.UnixMilli(doc.UpdatedAt).Format("2006-01-02 15:04:05.000"))) - if doc.DeletedAt > 0 { - utils.PrintWarning(fmt.Sprintf(" deleted_at: %s", time.UnixMilli(doc.DeletedAt).Format("2006-01-02 15:04:05.000"))) - } - return nil -} - -// handleFindByIndex — обработчик команды "findbyindex ". -// Выполняет поиск документов по указанному индексу. -// Использует индекс для быстрого поиска. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя индекса -// - args[2]: Значение для поиска -// -// Возвращает: -// - error: Ошибка при поиске, отсутствии аргументов или выборе БД -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 (updated: %s)", i+1, doc.ID, - time.UnixMilli(doc.UpdatedAt).Format("15:04:05.000"))) - utils.PrintJSON(doc.GetFields()) - } - return nil -} - -// handleFindByTime — обработчик команды "findbytime ". -// Находит документы по временному диапазону (поле created_at). -// Поддерживает форматы дат: -// - YYYY-MM-DD -// - YYYY-MM-DD HH:MM:SS -// - YYYY-MM-DDTHH:MM:SS -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Начальная дата (from_date) -// - args[2]: Конечная дата (to_date) -// -// Возвращает: -// - error: Ошибка при парсинге дат, поиске или отсутствии аргументов -func (r *Repl) handleFindByTime(args []string) error { - if r.currentDB == "" { - return fmt.Errorf("no database selected") - } - - if len(args) < 3 { - return fmt.Errorf("usage: findbytime \n"+ - " Date formats: YYYY-MM-DD or YYYY-MM-DD HH:MM:SS") - } - - collName := args[0] - fromStr := args[1] - toStr := args[2] - - // Парсим даты с попыткой нескольких форматов - var fromTime, toTime time.Time - var err error - - formats := []string{ - "2006-01-02", - "2006-01-02 15:04:05", - "2006-01-02T15:04:05", - } - - for _, format := range formats { - fromTime, err = time.Parse(format, fromStr) - if err == nil { - break - } - } - if err != nil { - return fmt.Errorf("invalid from_date format: %s", fromStr) - } - - for _, format := range formats { - toTime, err = time.Parse(format, toStr) - if err == nil { - break - } - } - if err != nil { - return fmt.Errorf("invalid to_date format: %s", toStr) - } - - fromMs := fromTime.UnixMilli() - toMs := toTime.UnixMilli() - - db, err := r.store.GetDatabase(r.currentDB) - if err != nil { - return err - } - - coll, err := db.GetCollection(collName) - if err != nil { - return err - } - - // ИСПРАВЛЕНО: корректное использование FindByFilter с функцией фильтрации - docs := coll.FindByFilter(func(doc *storage.Document) bool { - return doc.CreatedAt >= fromMs && doc.CreatedAt <= toMs - }) - - if len(docs) == 0 { - utils.PrintInfo("No documents found in the specified time range") - return nil - } - - utils.PrintHeader(fmt.Sprintf("Documents created between %s and %s:", - fromTime.Format("2006-01-02 15:04:05"), toTime.Format("2006-01-02 15:04:05"))) - - for i, doc := range docs { - utils.PrintInfo(fmt.Sprintf(" [%d] ID: %s (created: %s)", - i+1, doc.ID, time.UnixMilli(doc.CreatedAt).Format("2006-01-02 15:04:05.000"))) - } - - return nil -} - -// handleUpdate — обработчик команды "update ...". -// Обновляет поля существующего документа. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: ID документа -// - args[2:]: Пары field=value для обновления -// -// Возвращает: -// - error: Ошибка при обновлении, отсутствии аргументов или выборе БД -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 at %s", docID, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleDelete — обработчик команды "delete ". -// Выполняет мягкое удаление документа (если включено). -// Документ помечается как удалённый (устанавливается DeletedAt). -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: ID документа -// -// Возвращает: -// - error: Ошибка при удалении, отсутствии аргументов или выборе БД -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 at %s", docID, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handlePermanentDelete — обработчик команды "permanent delete ". -// Окончательно удаляет ранее мягко удалённый документ. -// Документ полностью удаляется из хранилища. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: ID документа -// -// Возвращает: -// - error: Ошибка при удалении, отсутствии аргументов или выборе БД -func (r *Repl) handlePermanentDelete(args []string) error { - if r.currentDB == "" { - return fmt.Errorf("no database selected") - } - - if len(args) < 2 { - return fmt.Errorf("usage: permanent 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.PermanentDelete(docID); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Document '%s' permanently deleted", docID)) - return nil -} - -// handleRestore — обработчик команды "restore ". -// Восстанавливает ранее мягко удалённый документ. -// Сбрасывает DeletedAt в 0. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: ID документа -// -// Возвращает: -// - error: Ошибка при восстановлении, отсутствии аргументов или выборе БД -func (r *Repl) handleRestore(args []string) error { - if r.currentDB == "" { - return fmt.Errorf("no database selected") - } - - if len(args) < 2 { - return fmt.Errorf("usage: restore ") - } - - 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.RestoreDeleted(docID); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Document '%s' restored at %s", docID, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleShowDeleted — обработчик команды "show deleted ". -// Показывает все мягко удалённые документы в коллекции. -// -// Аргументы: -// - args[0]: Имя коллекции -// -// Возвращает: -// - error: Ошибка при получении списка, отсутствии аргументов или выборе БД -func (r *Repl) handleShowDeleted(args []string) error { - if r.currentDB == "" { - return fmt.Errorf("no database selected") - } - - if len(args) < 1 { - return fmt.Errorf("usage: show deleted ") - } - - 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.GetAllDocumentsIncludingDeleted() - deleted := make([]*storage.Document, 0) - - for _, doc := range docs { - if doc.IsDeleted() { - deleted = append(deleted, doc) - } - } - - if len(deleted) == 0 { - utils.PrintInfo("No deleted documents found") - return nil - } - - utils.PrintHeader(fmt.Sprintf("Deleted documents in collection '%s':", collName)) - for i, doc := range deleted { - utils.PrintInfo(fmt.Sprintf(" [%d] ID: %s (deleted: %s)", - i+1, doc.ID, time.UnixMilli(doc.DeletedAt).Format("2006-01-02 15:04:05.000"))) - } - - return nil -} - -// handleCount — обработчик команды "count ". -// Показывает статистику коллекции: количество активных, удалённых и всех документов. -// -// Аргументы: -// - args[0]: Имя коллекции -// -// Возвращает: -// - error: Ошибка при подсчёте, отсутствии аргументов или выборе БД -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 - } - - active := coll.Count() - deleted := coll.CountDeleted() - total := coll.CountAll() - - utils.PrintHeader(fmt.Sprintf("Collection '%s' statistics:", collName)) - utils.PrintInfo(fmt.Sprintf(" Active documents: %d", active)) - if deleted > 0 { - utils.PrintWarning(fmt.Sprintf(" Deleted documents: %d", deleted)) - } - utils.PrintInfo(fmt.Sprintf(" Total documents: %d", total)) - - return nil -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД УПРАВЛЕНИЯ ВРЕМЕННЫМИ МЕТКАМИ -// ============================================================================= - -// handleShowTimestamps — обработчик команды "show timestamps ". -// Показывает все временные метки документа: created_at, updated_at, deleted_at и версию. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: ID документа -// -// Возвращает: -// - error: Ошибка при поиске, отсутствии аргументов или выборе БД -func (r *Repl) handleShowTimestamps(args []string) error { - if r.currentDB == "" { - return fmt.Errorf("no database selected") - } - - if len(args) < 2 { - return fmt.Errorf("usage: show timestamps ") - } - - 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 - } - - // Ищем документ, включая удалённые (чтобы показать deleted_at) - doc, err := coll.FindIncludingDeleted(docID) - if err != nil { - return err - } - - utils.PrintHeader(fmt.Sprintf("Timestamps for document: %s", docID)) - utils.PrintInfo(fmt.Sprintf(" Created: %s (%d)", - time.UnixMilli(doc.CreatedAt).Format("2006-01-02 15:04:05.000"), - doc.CreatedAt)) - utils.PrintInfo(fmt.Sprintf(" Updated: %s (%d)", - time.UnixMilli(doc.UpdatedAt).Format("2006-01-02 15:04:05.000"), - doc.UpdatedAt)) - - if doc.DeletedAt > 0 { - utils.PrintWarning(fmt.Sprintf(" Deleted: %s (%d)", - time.UnixMilli(doc.DeletedAt).Format("2006-01-02 15:04:05.000"), - doc.DeletedAt)) - } else { - utils.PrintInfo(" Deleted: not deleted") - } - - utils.PrintInfo(fmt.Sprintf(" Version: %d", doc.Version)) - - return nil -} - -// handleStatsTimestamps — обработчик команды "stats timestamps ". -// Вычисляет и показывает статистику временных меток для всех документов коллекции. -// Включает: минимальную, максимальную и среднюю дату для created_at и updated_at. -// -// Аргументы: -// - args[0]: Имя коллекции -// -// Возвращает: -// - error: Ошибка при получении статистики, отсутствии аргументов или выборе БД -func (r *Repl) handleStatsTimestamps(args []string) error { - if r.currentDB == "" { - return fmt.Errorf("no database selected") - } - - if len(args) < 1 { - return fmt.Errorf("usage: stats timestamps ") - } - - 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.GetAllDocumentsIncludingDeleted() - - if len(docs) == 0 { - utils.PrintInfo("No documents in collection") - return nil - } - - // Вычисляем статистику - var minCreated, maxCreated int64 = 1<<63 - 1, 0 - var minUpdated, maxUpdated int64 = 1<<63 - 1, 0 - var totalCreated, totalUpdated int64 = 0, 0 - - for _, doc := range docs { - if doc.CreatedAt < minCreated { - minCreated = doc.CreatedAt - } - if doc.CreatedAt > maxCreated { - maxCreated = doc.CreatedAt - } - if doc.UpdatedAt < minUpdated { - minUpdated = doc.UpdatedAt - } - if doc.UpdatedAt > maxUpdated { - maxUpdated = doc.UpdatedAt - } - totalCreated += doc.CreatedAt - totalUpdated += doc.UpdatedAt - } - - avgCreated := totalCreated / int64(len(docs)) - avgUpdated := totalUpdated / int64(len(docs)) - - utils.PrintHeader(fmt.Sprintf("Timestamp statistics for collection '%s':", collName)) - utils.PrintInfo(fmt.Sprintf(" Documents count: %d", len(docs))) - utils.Println("") - utils.PrintInfo(" Created timestamps:") - utils.PrintInfo(fmt.Sprintf(" Earliest: %s", time.UnixMilli(minCreated).Format("2006-01-02 15:04:05.000"))) - utils.PrintInfo(fmt.Sprintf(" Latest: %s", time.UnixMilli(maxCreated).Format("2006-01-02 15:04:05.000"))) - utils.PrintInfo(fmt.Sprintf(" Average: %s", time.UnixMilli(avgCreated).Format("2006-01-02 15:04:05.000"))) - utils.Println("") - utils.PrintInfo(" Updated timestamps:") - utils.PrintInfo(fmt.Sprintf(" Earliest: %s", time.UnixMilli(minUpdated).Format("2006-01-02 15:04:05.000"))) - utils.PrintInfo(fmt.Sprintf(" Latest: %s", time.UnixMilli(maxUpdated).Format("2006-01-02 15:04:05.000"))) - utils.PrintInfo(fmt.Sprintf(" Average: %s", time.UnixMilli(avgUpdated).Format("2006-01-02 15:04:05.000"))) - - return nil -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД АУДИТА -// ============================================================================= - -// handleAuditLog — обработчик команды "audit log". -// Показывает последние 50 записей аудита в обратном порядке. -// Записи подсвечиваются цветами: -// - Зелёный: CREATE/INSERT -// - Жёлтый: DELETE/SOFT_DELETE -// - Белый: остальные операции -func (r *Repl) handleAuditLog(args []string) error { - entries := storage.GetAuditLog() - - if len(entries) == 0 { - utils.PrintInfo("No audit log entries") - return nil - } - - // Показываем последние 50 записей - start := 0 - if len(entries) > 50 { - start = len(entries) - 50 - } - - utils.PrintHeader("Audit Log (last 50 entries)") - for i := len(entries) - 1; i >= start; i-- { - entry := entries[i] - colorFunc := utils.PrintInfo - if entry.Operation == "DELETE" || entry.Operation == "SOFT_DELETE" { - colorFunc = utils.PrintWarning - } else if entry.Operation == "CREATE" || entry.Operation == "INSERT" { - colorFunc = utils.PrintSuccess - } - - colorFunc(fmt.Sprintf(" [%s] %s - %s: %s", - entry.TimestampStr, entry.Operation, entry.DataType, entry.Name)) - } - - return nil -} - -// handleAuditFilter — обработчик команды "audit filter ". -// Фильтрует и показывает записи аудита по типу данных и операции. -// -// Аргументы: -// - args[0]: Тип данных (DATABASE, COLLECTION, DOCUMENT, FIELD, INDEX, TRANSACTION) -// - args[1]: Операция (CREATE, INSERT, UPDATE, DELETE, SOFT_DELETE, RESTORE) -// -// Возвращает: -// - error: Ошибка при фильтрации или отсутствии аргументов -func (r *Repl) handleAuditFilter(args []string) error { - if len(args) < 2 { - return fmt.Errorf("usage: audit filter \n"+ - " data_type: DATABASE, COLLECTION, DOCUMENT, FIELD, INDEX, TRANSACTION\n"+ - " operation: CREATE, INSERT, UPDATE, DELETE, SOFT_DELETE, RESTORE") - } - - dataType := strings.ToUpper(args[0]) - operation := strings.ToUpper(args[1]) - - entries := storage.GetAuditLogFiltered(dataType, operation, 0, 0) - - if len(entries) == 0 { - utils.PrintInfo(fmt.Sprintf("No audit log entries found for %s/%s", dataType, operation)) - return nil - } - - utils.PrintHeader(fmt.Sprintf("Audit Log Filtered: %s / %s", dataType, operation)) - for _, entry := range entries { - utils.PrintInfo(fmt.Sprintf(" [%s] %s: %s", entry.TimestampStr, entry.Operation, entry.Name)) - } - - return nil -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД УПРАВЛЕНИЯ ИНДЕКСАМИ -// ============================================================================= - -// handleCreateIndex — обработчик команды "create index [unique]". -// Создаёт новый индекс на указанных полях коллекции. -// Если указан флаг "unique", индекс будет уникальным. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя индекса -// - args[2]: Список полей через запятую -// - args[3]: (опционально) "unique" для уникального индекса -// -// Возвращает: -// - error: Ошибка при создании, отсутствии аргументов или выборе БД -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' at %s", indexName, collName, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleDropIndex — обработчик команды "drop index ". -// Удаляет существующий индекс из коллекции. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя индекса -// -// Возвращает: -// - error: Ошибка при удалении, отсутствии аргументов или выборе БД -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 -} - -// handleShowIndexes — обработчик команды "show indexes ". -// Показывает все индексы коллекции с их свойствами. -// -// Аргументы: -// - args[0]: Имя коллекции -// -// Возвращает: -// - error: Ошибка при получении списка, отсутствии аргументов или выборе БД -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.GetIndexesInfo() - 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 { - uniqueStr := "" - if idx["unique"].(bool) { - uniqueStr = " (unique)" - } - createdAt := time.UnixMilli(idx["created_at"].(int64)).Format("2006-01-02 15:04:05") - utils.Println(fmt.Sprintf(" - %s: %v%s (created: %s)", idx["name"], idx["fields"], uniqueStr, createdAt)) - } - return nil -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД ОГРАНИЧЕНИЙ -// ============================================================================= - -// handleAddRequired — обработчик команды "add required ". -// Добавляет ограничение "обязательное поле" для указанной коллекции. -// Документы без этого поля не будут вставляться. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя поля -// -// Возвращает: -// - error: Ошибка при добавлении, отсутствии аргументов или выборе БД -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 -} - -// handleAddUnique — обработчик команды "add unique ". -// Добавляет ограничение уникальности для указанного поля. -// Значения поля должны быть уникальны в коллекции. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя поля -// -// Возвращает: -// - error: Ошибка при добавлении, отсутствии аргументов или выборе БД -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 -} - -// handleAddMin — обработчик команды "add min ". -// Добавляет ограничение минимального значения для числового поля. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя поля -// - args[2]: Минимальное значение (число) -// -// Возвращает: -// - error: Ошибка при добавлении, отсутствии аргументов или выборе БД -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 -} - -// handleAddMax — обработчик команды "add max ". -// Добавляет ограничение максимального значения для числового поля. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя поля -// - args[2]: Максимальное значение (число) -// -// Возвращает: -// - error: Ошибка при добавлении, отсутствии аргументов или выборе БД -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 -} - -// handleAddEnum — обработчик команды "add enum ". -// Добавляет ограничение на допустимые значения (enum) для поля. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя поля -// - args[2:]: Список допустимых значений -// -// Возвращает: -// - error: Ошибка при добавлении, отсутствии аргументов или выборе БД -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 -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД ТРАНЗАКЦИЙ -// ============================================================================= - -// handleBeginTransaction — обработчик команды "begin transaction". -// Начинает новую транзакцию. -// -// Возвращает: -// - error: Ошибка при создании транзакции или отсутствии выбора БД -func (r *Repl) handleBeginTransaction(args []string) error { - if r.currentDB == "" { - return fmt.Errorf("no database selected") - } - - tx := storage.BeginTransaction() - if tx == nil { - return fmt.Errorf("failed to begin transaction") - } - - utils.PrintSuccess(fmt.Sprintf("Transaction %d started at %s", tx.ID, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleCommitTransaction — обработчик команды "commit". -// Фиксирует (коммитит) текущую транзакцию. -// -// Возвращает: -// - error: Ошибка при коммите или отсутствии активной транзакции -func (r *Repl) handleCommitTransaction(args []string) error { - if !storage.HasActiveTransaction() { - return fmt.Errorf("no active transaction to commit") - } - - if err := storage.CommitCurrentTransaction(); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Transaction committed successfully at %s", time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleRollbackTransaction — обработчик команды "rollback". -// Откатывает (отменяет) текущую транзакцию. -// -// Возвращает: -// - error: Ошибка при откате или отсутствии активной транзакции -func (r *Repl) handleRollbackTransaction(args []string) error { - if !storage.HasActiveTransaction() { - return fmt.Errorf("no active transaction to rollback") - } - - if err := storage.AbortCurrentTransaction(); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Transaction rolled back at %s", time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleShowTransactions — обработчик команды "show transactions". -// Показывает все активные транзакции и их операции. -func (r *Repl) handleShowTransactions(args []string) error { - transactions := storage.GetActiveTransactions() - - if len(transactions) == 0 { - utils.PrintInfo("No active transactions") - return nil - } - - utils.PrintHeader("Active Transactions") - for _, tx := range transactions { - utils.PrintInfo(fmt.Sprintf(" ID: %s, Status: %s, Operations: %d, Started: %s", - tx.ID, tx.Status, tx.OperationCount, tx.StartTime)) - } - return nil -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД ПЛАГИНОВ -// ============================================================================= - -// handlePluginList — обработчик команды "plugin list". -// Показывает все загруженные плагины с их статусом. -func (r *Repl) handlePluginList(args []string) error { - if r.pluginManager == nil || !r.pluginManager.IsEnabled() { - return fmt.Errorf("plugin system is disabled") - } - - plugins := r.pluginManager.ListPlugins() - - if len(plugins) == 0 { - utils.PrintInfo("No plugins loaded") - utils.PrintInfo(fmt.Sprintf("Plugins directory: %s", r.pluginManager.GetPluginsDir())) - return nil - } - - utils.PrintHeader("Loaded Plugins") - for _, p := range plugins { - status := "loaded" - switch p.Status.Load() { - case 1: - status = "running" - case 2: - status = "stopped" - case 3: - status = "error" - } - utils.PrintInfo(fmt.Sprintf(" %s v%s - %s [%s]", p.Name, p.Version(), status, p.Description())) - utils.Println(fmt.Sprintf(" Author: %s, Loaded: %s", p.Author(), p.LoadedAt().Format("2006-01-02 15:04:05"))) - } - return nil -} - -// handlePluginLoad — обработчик команды "plugin load ". -// Загружает плагин из файла. -// -// Аргументы: -// - args[0]: Имя плагина -// - args[1]: Путь к файлу плагина -// -// Возвращает: -// - error: Ошибка при загрузке или отсутствии аргументов -func (r *Repl) handlePluginLoad(args []string) error { - if r.pluginManager == nil || !r.pluginManager.IsEnabled() { - return fmt.Errorf("plugin system is disabled") - } - - if len(args) < 2 { - return fmt.Errorf("usage: plugin load ") - } - - name := args[0] - filepath := args[1] - - if err := r.pluginManager.LoadPlugin(name, filepath); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Plugin '%s' loaded from %s at %s", name, filepath, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handlePluginUnload — обработчик команды "plugin unload ". -// Выгружает плагин. -// -// Аргументы: -// - args[0]: Имя плагина -// -// Возвращает: -// - error: Ошибка при выгрузке или отсутствии аргументов -func (r *Repl) handlePluginUnload(args []string) error { - if r.pluginManager == nil || !r.pluginManager.IsEnabled() { - return fmt.Errorf("plugin system is disabled") - } - - if len(args) < 1 { - return fmt.Errorf("usage: plugin unload ") - } - - name := args[0] - - if err := r.pluginManager.UnloadPlugin(name); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Plugin '%s' unloaded", name)) - return nil -} - -// handlePluginStart — обработчик команды "plugin start ". -// Запускает загруженный плагин. -// -// Аргументы: -// - args[0]: Имя плагина -// -// Возвращает: -// - error: Ошибка при запуске или отсутствии аргументов -func (r *Repl) handlePluginStart(args []string) error { - if r.pluginManager == nil || !r.pluginManager.IsEnabled() { - return fmt.Errorf("plugin system is disabled") - } - - if len(args) < 1 { - return fmt.Errorf("usage: plugin start ") - } - - name := args[0] - - if err := r.pluginManager.StartPlugin(name); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Plugin '%s' started at %s", name, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handlePluginStop — обработчик команды "plugin stop ". -// Останавливает запущенный плагин. -// -// Аргументы: -// - args[0]: Имя плагина -// -// Возвращает: -// - error: Ошибка при остановке или отсутствии аргументов -func (r *Repl) handlePluginStop(args []string) error { - if r.pluginManager == nil || !r.pluginManager.IsEnabled() { - return fmt.Errorf("plugin system is disabled") - } - - if len(args) < 1 { - return fmt.Errorf("usage: plugin stop ") - } - - name := args[0] - - if err := r.pluginManager.StopPlugin(name); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Plugin '%s' stopped", name)) - return nil -} - -// handlePluginExec — обработчик команды "plugin exec [args...]". -// Выполняет функцию плагина с переданными аргументами. -// -// Аргументы: -// - args[0]: Имя плагина -// - args[1]: Имя функции -// - args[2:]: Аргументы для функции -// -// Возвращает: -// - error: Ошибка при выполнении или отсутствии аргументов -func (r *Repl) handlePluginExec(args []string) error { - if r.pluginManager == nil || !r.pluginManager.IsEnabled() { - return fmt.Errorf("plugin system is disabled") - } - - if len(args) < 2 { - return fmt.Errorf("usage: plugin exec [args...]") - } - - pluginName := args[0] - funcName := args[1] - - var execArgs []interface{} - for _, arg := range args[2:] { - execArgs = append(execArgs, arg) - } - - result, err := r.pluginManager.ExecutePlugin(pluginName, funcName, execArgs...) - if err != nil { - return err - } - - utils.PrintSuccess("Result:") - utils.PrintJSON(result) - return nil -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД ИМПОРТА/ЭКСПОРТА -// ============================================================================= - -// handleExport — обработчик команды "export ". -// Экспортирует базу данных в файл в формате MessagePack. -// -// Аргументы: -// - args[0]: Имя базы данных -// - args[1]: Имя файла для экспорта -// -// Возвращает: -// - error: Ошибка при экспорте или отсутствии аргументов -func (r *Repl) handleExport(args []string) error { - if len(args) < 2 { - return fmt.Errorf("usage: export ") - } - - dbName := args[0] - fileName := args[1] - - if !r.store.ExistsDatabase(dbName) { - return fmt.Errorf("database '%s' does not exist", dbName) - } - - utils.PrintInfo(fmt.Sprintf("Exporting database '%s' to %s at %s...", dbName, fileName, time.Now().Format("2006-01-02 15:04:05.000"))) - - // ИСПРАВЛЕНО: используем метод SerializeDatabase через Database - db, err := r.store.GetDatabase(dbName) - if err != nil { - return err - } - - data, err := db.SerializeDatabase() - if err != nil { - return fmt.Errorf("failed to serialize database: %v", err) - } - - if err := os.WriteFile(fileName, data, 0644); err != nil { - return fmt.Errorf("failed to write export file: %v", err) - } - - utils.PrintSuccess(fmt.Sprintf("Database '%s' exported to %s", dbName, fileName)) - return nil -} - -// handleImport — обработчик команды "import ". -// Импортирует базу данных из файла в формате MessagePack. -// -// Аргументы: -// - args[0]: Имя базы данных -// - args[1]: Имя файла для импорта -// -// Возвращает: -// - error: Ошибка при импорте или отсутствии аргументов -func (r *Repl) handleImport(args []string) error { - if len(args) < 2 { - return fmt.Errorf("usage: import ") - } - - dbName := args[0] - fileName := args[1] - - utils.PrintInfo(fmt.Sprintf("Importing data from %s to database '%s' at %s...", fileName, dbName, time.Now().Format("2006-01-02 15:04:05.000"))) - - data, err := os.ReadFile(fileName) - if err != nil { - return fmt.Errorf("failed to read import file: %v", err) - } - - // Создаём базу данных, если она не существует - if !r.store.ExistsDatabase(dbName) { - if err := r.store.CreateDatabase(dbName); err != nil { - return err - } - } - - db, err := r.store.GetDatabase(dbName) - if err != nil { - return err - } - - // ИСПРАВЛЕНО: используем метод DeserializeDatabase через Database - if err := db.DeserializeDatabase(data); err != nil { - return fmt.Errorf("failed to deserialize database: %v", err) - } - - utils.PrintSuccess(fmt.Sprintf("Data imported to database '%s' from %s", dbName, fileName)) - return nil -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД ACL -// ============================================================================= - -// handleACLLogin — обработчик команды "acl login ". -// Аутентифицирует пользователя по имени и паролю. -// -// Аргументы: -// - args[0]: Имя пользователя -// - args[1]: Пароль -// -// Возвращает: -// - error: Ошибка аутентификации или отсутствии аргументов -func (r *Repl) handleACLLogin(args []string) error { - if len(args) < 2 { - return fmt.Errorf("usage: acl login ") - } - - username := args[0] - password := args[1] - - if r.aclManager == nil { - return fmt.Errorf("ACL manager not initialized") - } - - sessionID, err := r.aclManager.Authenticate(username, password) - if err != nil { - return err - } - - r.authenticated = true - r.currentUser = username - r.sessionID = sessionID - - // Получаем роли пользователя - roles := r.aclManager.GetUserRoles(sessionID) - if len(roles) > 0 { - r.currentRole = roles[0] - } - - utils.PrintSuccess(fmt.Sprintf("Logged in as '%s' with role '%s' at %s", username, r.currentRole, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleACLLogout — обработчик команды "acl logout". -// Завершает сессию текущего пользователя. -func (r *Repl) handleACLLogout(args []string) error { - if r.sessionID != "" && r.aclManager != nil { - r.aclManager.Logout(r.sessionID) - } - - r.authenticated = false - r.currentUser = "" - r.currentRole = "anonymous" - r.sessionID = "" - - utils.PrintSuccess(fmt.Sprintf("Logged out at %s", time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleACLGrant — обработчик команды "acl grant ". -// Назначает разрешения роли на коллекцию. -// Разрешения: r=read, w=write, d=delete, a=admin. -// Требует прав администратора. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя роли -// - args[2]: Строка разрешений (например, "rwa") -// -// Возвращает: -// - error: Ошибка при назначении разрешений или отсутствии прав -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 \n"+ - " Permissions: r=read, w=write, d=delete, a=admin\n"+ - " Example: acl grant users admin rwa") - } - - 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' at %s", - perms, role, collName, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleACLUsers — обработчик команды "acl users". -// Показывает список всех пользователей и их роли. -func (r *Repl) handleACLUsers(args []string) error { - if r.aclManager == nil { - return fmt.Errorf("ACL manager not initialized") - } - - users := r.aclManager.ListUsers() - - if len(users) == 0 { - utils.PrintInfo("No users found") - return nil - } - - utils.PrintHeader("Users") - for _, username := range users { - userInfo, err := r.aclManager.GetUserInfo(username) - if err != nil { - continue - } - status := "active" - if !userInfo.Active { - status = "disabled" - } - utils.PrintInfo(fmt.Sprintf(" %s - Roles: %v [%s]", username, userInfo.Roles, status)) - } - return nil -} - -// handleACLRoles — обработчик команды "acl roles". -// Показывает список всех ролей и их разрешения. -func (r *Repl) handleACLRoles(args []string) error { - if r.aclManager == nil { - return fmt.Errorf("ACL manager not initialized") - } - - roles := r.aclManager.ListRoles() - - if len(roles) == 0 { - utils.PrintInfo("No roles found") - return nil - } - - utils.PrintHeader("Roles") - for _, roleName := range roles { - perms, err := r.aclManager.GetRolePermissions(roleName) - if err != nil { - continue - } - utils.PrintInfo(fmt.Sprintf(" %s - Permissions: %v", roleName, perms)) - } - return nil -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД СЖАТИЯ -// ============================================================================= - -// handleCompressionStats — обработчик команды "compression stats". -// Показывает статистику сжатия для текущей базы данных. -// Включает: количество сжатых документов, коэффициент сжатия, размеры. -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 -} - -// handleCompressCollection — обработчик команды "compress collection ". -// Вручную сжимает все документы в указанной коллекции. -// -// Аргументы: -// - args[0]: Имя коллекции -// -// Возвращает: -// - error: Ошибка при сжатии, отсутствии аргументов или выборе БД -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' at %s...", collName, time.Now().Format("2006-01-02 15:04:05.000"))) - - 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 -} - -// handleDocCompression — обработчик команды "doc compression ". -// Показывает информацию о сжатии для конкретного документа. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: ID документа -// -// Возвращает: -// - error: Ошибка при получении информации, отсутствии аргументов или выборе БД -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 -} - -// handleCompressionConfig — обработчик команды "compression config". -// Показывает текущую конфигурацию сжатия и доступные алгоритмы. -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 -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД ТРИГГЕРОВ (MongoDB-LIKE) -// ============================================================================= - -// handleCreateTrigger — обработчик команды "create trigger". -// Создаёт триггер на коллекции с указанным событием и действием. -// Поддерживает MongoDB-подобный синтаксис с опциями. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Имя триггера -// - args[2]: Событие (BEFORE_INSERT, AFTER_INSERT, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE, AFTER_DELETE) -// - args[3]: Действие (abort, skip, modify, log, notify) -// - args[4:]: Опции (--description, --set, --inc, --currentDate, --condition) -// -// Возвращает: -// - error: Ошибка при создании, отсутствии аргументов или выборе БД -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 [options]\n"+ - " Events: BEFORE_INSERT, AFTER_INSERT, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE, AFTER_DELETE\n"+ - " Actions: abort, skip, modify, log, notify\n"+ - " Options: --description , --set , --inc , --currentDate , --condition ") - } - - collName := args[0] - triggerName := args[1] - event := args[2] - action := args[3] - - // Получаем коллекцию - db, err := r.store.GetDatabase(r.currentDB) - if err != nil { - return err - } - - coll, err := db.GetCollection(collName) - if err != nil { - return err - } - - // Создаём триггер - trigger := &storage.Trigger{ - Name: triggerName, - Event: event, - Action: action, - Enabled: true, - Description: "", - CreatedAt: time.Now().UnixMilli(), - UpdatedAt: time.Now().UnixMilli(), - } - - // Парсим опции - for i := 4; i < len(args); i++ { - switch args[i] { - case "--description": - if i+1 < len(args) { - trigger.Description = args[i+1] - i++ - } - case "--set": - if i+2 < len(args) { - // Для простоты сохраняем в отдельную структуру - // В реальной реализации здесь должна быть логика обработки - i += 2 - } - case "--inc": - if i+2 < len(args) { - i += 2 - } - case "--currentDate": - if i+1 < len(args) { - i++ - } - case "--condition": - if i+3 < len(args) { - i += 3 - } - } - } - - // Добавляем триггер в коллекцию - if err := coll.AddTrigger(trigger); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Trigger '%s' created on collection '%s' for event %s at %s", - triggerName, collName, event, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleDropTrigger — обработчик команды "drop trigger ". -// Удаляет триггер из коллекции. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Событие -// - args[2]: Имя триггера -// -// Возвращает: -// - error: Ошибка при удалении, отсутствии аргументов или выборе БД -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] - triggerName := args[2] - - 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.DropTrigger(triggerName); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Trigger '%s' dropped from collection '%s'", triggerName, collName)) - return nil -} - -// handleShowTriggers — обработчик команды "show triggers ". -// Показывает все триггеры коллекции с их свойствами. -// -// Аргументы: -// - args[0]: Имя коллекции -// -// Возвращает: -// - error: Ошибка при получении списка, отсутствии аргументов или выборе БД -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] - - db, err := r.store.GetDatabase(r.currentDB) - if err != nil { - return err - } - - coll, err := db.GetCollection(collName) - if err != nil { - return err - } - - triggers := coll.ListTriggers() - - 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] (created: %s)", - t.Name, t.Event, status, t.Action, time.UnixMilli(t.CreatedAt).Format("2006-01-02 15:04:05"))) - if t.Description != "" { - utils.Println(fmt.Sprintf(" Description: %s", t.Description)) - } - } - return nil -} - -// handleEnableTrigger — обработчик команды "enable trigger ". -// Включает отключённый триггер. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Событие -// - args[2]: Имя триггера -// -// Возвращает: -// - error: Ошибка при включении, отсутствии аргументов или выборе БД -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] - triggerName := args[2] - - 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.EnableTrigger(triggerName); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Trigger '%s' enabled at %s", triggerName, time.Now().Format("2006-01-02 15:04:05.000"))) - return nil -} - -// handleDisableTrigger — обработчик команды "disable trigger ". -// Отключает триггер без его удаления. -// -// Аргументы: -// - args[0]: Имя коллекции -// - args[1]: Событие -// - args[2]: Имя триггера -// -// Возвращает: -// - error: Ошибка при отключении, отсутствии аргументов или выборе БД -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] - triggerName := args[2] - - 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.DisableTrigger(triggerName); err != nil { - return err - } - - utils.PrintSuccess(fmt.Sprintf("Trigger '%s' disabled", triggerName)) - return nil -} - -// handleTriggerLog — обработчик команды "trigger log". -// Показывает историю выполнения триггеров. -func (r *Repl) handleTriggerLog(args []string) error { - logs := storage.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 -} - -// ============================================================================= -// ОБРАБОТЧИКИ КОМАНД КЛАСТЕРА -// ============================================================================= - -// handleStatus — обработчик команды "status". -// Показывает статус кластера: роль узла (leader/follower), лидер, количество узлов. -func (r *Repl) handleStatus(args []string) error { - utils.PrintHeader("Cluster Status") - - if r.coordinator == nil { - utils.PrintWarning(" Cluster coordinator not available") - return nil - } - - isLeader := r.coordinator.IsLeader() - if isLeader { - utils.PrintSuccess(" Role: LEADER") - } else { - utils.PrintWarning(" Role: FOLLOWER") - } - - leader := r.coordinator.GetLeader() - if leader != nil { - // ИСПРАВЛЕНО: корректное обращение к полям структуры NodeInfo - leaderIP := "unknown" - leaderPort := 0 - if leader.IP != "" { - leaderIP = leader.IP - } - if leader.Port > 0 { - leaderPort = leader.Port - } - utils.PrintInfo(fmt.Sprintf(" Leader: %s:%d", leaderIP, leaderPort)) - } else { - utils.PrintInfo(" Leader: none") - } - - status := r.coordinator.GetClusterStatus() - utils.PrintInfo(fmt.Sprintf(" Cluster Name: %s", status.Name)) - utils.PrintInfo(fmt.Sprintf(" Total Nodes: %d", status.TotalNodes)) - utils.PrintInfo(fmt.Sprintf(" Active Nodes: %d", status.ActiveNodes)) - utils.PrintInfo(fmt.Sprintf(" Health: %s", status.Health)) - utils.PrintInfo(fmt.Sprintf(" Replication Factor: %d", status.ReplicationFactor)) - - return nil -} - -// handleNodes — обработчик команды "nodes". -// Показывает список всех узлов кластера с их статусом. -// Лидер отмечается звёздочкой (*). -func (r *Repl) handleNodes(args []string) error { - if r.coordinator == nil { - return fmt.Errorf("cluster coordinator not available") - } - - nodes := r.coordinator.GetAllNodes() - - if len(nodes) == 0 { - utils.PrintInfo("No nodes in cluster") - return nil - } - - utils.PrintHeader("Cluster Nodes") - leader := r.coordinator.GetLeader() - leaderID := "" - if leader != nil { - leaderID = leader.ID - } - - for _, node := range nodes { - prefix := " " - if node.ID == leaderID { - prefix = " *" - } - statusColor := "green" - if node.Status != "active" { - statusColor = "yellow" - } - // ИСПРАВЛЕНО: корректное форматирование с IP и Port - nodeIP := "unknown" - nodePort := 0 - if node.IP != "" { - nodeIP = node.IP - } - if node.Port > 0 { - nodePort = node.Port - } - lastSeen := time.UnixMilli(node.LastSeen).Format("15:04:05") - // ИСПРАВЛЕНО: используем colorize вместо utils.Colorize - statusText := r.colorize(node.Status, statusColor) - utils.Println(fmt.Sprintf("%s %s:%d [%s] (last seen: %s)", - prefix, nodeIP, nodePort, statusText, lastSeen)) - } - - return nil -} - -// ============================================================================= -// ВСПОМОГАТЕЛЬНЫЙ МЕТОД ДЛЯ ЦВЕТНОГО ВЫВОДА -// ============================================================================= - -// colorize возвращает цветной текст с использованием ANSI-кодов. -// ИСПРАВЛЕНО: добавлен метод вместо несуществующего utils.Colorize -func (r *Repl) colorize(text, color string) string { - switch color { - case "green": - return "\033[32m" + text + "\033[0m" - case "red": - return "\033[31m" + text + "\033[0m" - case "yellow": - return "\033[33m" + text + "\033[0m" - case "blue": - return "\033[34m" + text + "\033[0m" - case "cyan": - return "\033[36m" + text + "\033[0m" - case "white": - return "\033[37m" + text + "\033[0m" - default: - return text - } -} - -// ============================================================================= -// СИСТЕМНЫЕ ОБРАБОТЧИКИ КОМАНД -// ============================================================================= - -// handleHelp — обработчик команды "help". -// Показывает подробную справку по всем доступным командам, -// сгруппированную по категориям. -func (r *Repl) handleHelp(args []string) error { - utils.Println("") - fmt.Println(color.New(color.FgHiCyan).Sprint("\n=== Available Commands ===")) - - // Определяем категории команд для отображения в справке - categories := map[string][]struct { - cmd string - description string - }{ - "Database Management": { - {"create slice ", "Create a new database (slice)"}, - {"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"}, - {"findbytime ", "Find documents by time range"}, - {"update ...", "Update fields of an existing document"}, - {"delete ", "Delete a document (soft delete if enabled)"}, - {"permanent delete ", "Permanently delete a soft-deleted document"}, - {"restore ", "Restore a soft-deleted document"}, - {"show deleted ", "Show soft-deleted documents in a collection"}, - {"count ", "Count total documents in a collection"}, - }, - "Timestamp Management": { - {"show timestamps ", "Show timestamps for a document"}, - {"stats timestamps ", "Show timestamp statistics for a collection"}, - {"audit log", "Show audit log"}, - {"audit filter ", "Filter audit log by type and operation"}, - }, - "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)"}, - }, - "Transactions": { - {"begin transaction", "Start a new transaction"}, - {"commit", "Commit the current transaction"}, - {"rollback", "Rollback the current transaction"}, - {"show transactions", "List all active transactions"}, - }, - "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"}, - }, - "Plugins": { - {"plugin list", "List all loaded plugins"}, - {"plugin load ", "Load a plugin from Lua file"}, - {"plugin unload ", "Unload a plugin"}, - {"plugin start ", "Start a loaded plugin"}, - {"plugin stop ", "Stop a running plugin"}, - {"plugin exec [args...]", "Execute a plugin function"}, - }, - "Import/Export": { - {"export ", "Export entire database to file"}, - {"import ", "Import database from 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)"}, - {"acl users", "List all users"}, - {"acl roles", "List all roles"}, - }, - "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 futriix database REPL"}, - {"exit", "Exit the futriix database REPL (alias for quit)"}, - }, - } - - // Выводим команды по категориям - for category, commands := range categories { - 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.Println("") - utils.PrintInfo("Examples:") - utils.Println(" create slice test") - utils.Println(" use test") - utils.Println(" create collection users") - utils.Println(" insert users name=John,age=30") - utils.Println(" find users john123") - utils.Println(" show timestamps users john123") - utils.Println(" stats timestamps users") - utils.Println(" findbytime users 2026-01-01 2026-01-31") - utils.Println(" create index users idx_name name") - utils.Println(" begin transaction") - utils.Println(" update users john123 age=31") - utils.Println(" commit") - utils.Println(" acl login admin admin") - utils.Println(" status") - utils.Println("") - - return nil -} - -// handleClear — обработчик команды "clear". -// Очищает экран терминала с помощью ANSI-кода. -func (r *Repl) handleClear(args []string) error { - fmt.Print("\033[2J\033[H") - return nil -} - -// handleQuit — обработчик команды "quit" и "exit". -// Завершает работу REPL и выходит из программы. -func (r *Repl) handleQuit(args []string) error { - utils.PrintInfo(fmt.Sprintf("Session ended at %s", time.Now().Format("2006-01-02 15:04:05.000"))) - os.Exit(0) - return nil -} - -// ============================================================================= -// ЗАКРЫТИЕ REPL -// ============================================================================= - -// Close закрывает REPL и сохраняет историю команд в файл. -// Должен вызываться при завершении работы программы. -func (r *Repl) Close() error { - if r.historyFile != nil { - r.historyFile.Save() - } - return nil -}