Files
futriix/internal/repl/repl.go

3627 lines
128 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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)
// - Миграция (migration)
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["migration"] = &Command{
Name: "migration",
Description: "Cross-datacenter migration commands",
Handler: r.handleMigration,
}
// -------------------------------------------------------------------------
// Системные команды
// -------------------------------------------------------------------------
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 <name>".
// Создаёт новую базу данных (slice) с указанным именем.
//
// Аргументы:
// - args[0]: Имя создаваемой базы данных
//
// Возвращает:
// - error: Ошибка при создании или отсутствии аргументов
func (r *Repl) handleCreateSlice(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: create slice <name>")
}
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 <name>".
// Удаляет существующую базу данных. Если текущая БД удаляется,
// сбрасывает currentDB.
//
// Аргументы:
// - args[0]: Имя удаляемой базы данных
//
// Возвращает:
// - error: Ошибка при удалении или отсутствии аргументов
func (r *Repl) handleDropDatabase(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: drop database <name>")
}
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 <database>".
// Переключает текущую сессию на указанную базу данных.
// Проверяет существование БД перед переключением.
//
// Аргументы:
// - args[0]: Имя базы данных для использования
//
// Возвращает:
// - error: Ошибка при отсутствии БД или аргументов
func (r *Repl) handleUseDatabase(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: use <database>")
}
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 <name>".
// Создаёт новую коллекцию в текущей базе данных.
// Требует выбранной базы данных (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>")
}
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 <name>".
// Удаляет коллекцию из текущей базы данных.
// Требует выбранной базы данных.
//
// Аргументы:
// - 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>")
}
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 <collection> <json>".
// Вставляет новый документ в указанную коллекцию.
// Поддерживает два формата:
// 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 <collection> <json>")
}
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 <collection> <id>".
// Находит документ по 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 <collection> <id>")
}
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 <collection> <index> <value>".
// Выполняет поиск документов по указанному индексу.
// Использует индекс для быстрого поиска.
//
// Аргументы:
// - 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 <collection> <index> <value>")
}
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 <collection> <from_date> <to_date>".
// Находит документы по временному диапазону (поле 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 <collection> <from_date> <to_date>\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 <collection> <id> <field=value>...".
// Обновляет поля существующего документа.
//
// Аргументы:
// - 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 <collection> <id> <field=value>...")
}
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 <collection> <id>".
// Выполняет мягкое удаление документа (если включено).
// Документ помечается как удалённый (устанавливается 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 <collection> <id>")
}
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 <collection> <id>".
// Окончательно удаляет ранее мягко удалённый документ.
// Документ полностью удаляется из хранилища.
//
// Аргументы:
// - 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 <collection> <id>")
}
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 <collection> <id>".
// Восстанавливает ранее мягко удалённый документ.
// Сбрасывает 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 <collection> <id>")
}
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 <collection>".
// Показывает все мягко удалённые документы в коллекции.
//
// Аргументы:
// - 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 <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.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 <collection>".
// Показывает статистику коллекции: количество активных, удалённых и всех документов.
//
// Аргументы:
// - 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 <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
}
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 <collection> <id>".
// Показывает все временные метки документа: 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 <collection> <id>")
}
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 <collection>".
// Вычисляет и показывает статистику временных меток для всех документов коллекции.
// Включает: минимальную, максимальную и среднюю дату для 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 <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.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 <data_type> <operation>".
// Фильтрует и показывает записи аудита по типу данных и операции.
//
// Аргументы:
// - 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 <data_type> <operation>\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 <collection> <name> <fields> [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 <collection> <name> <fields> [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 <collection> <name>".
// Удаляет существующий индекс из коллекции.
//
// Аргументы:
// - 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 <collection> <name>")
}
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 <collection>".
// Показывает все индексы коллекции с их свойствами.
//
// Аргументы:
// - 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 <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
}
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 <collection> <field>".
// Добавляет ограничение "обязательное поле" для указанной коллекции.
// Документы без этого поля не будут вставляться.
//
// Аргументы:
// - 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 <collection> <field>")
}
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 <collection> <field>".
// Добавляет ограничение уникальности для указанного поля.
// Значения поля должны быть уникальны в коллекции.
//
// Аргументы:
// - 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 <collection> <field>")
}
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 <collection> <field> <value>".
// Добавляет ограничение минимального значения для числового поля.
//
// Аргументы:
// - 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 <collection> <field> <value>")
}
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 <collection> <field> <value>".
// Добавляет ограничение максимального значения для числового поля.
//
// Аргументы:
// - 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 <collection> <field> <value>")
}
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 <collection> <field> <values...>".
// Добавляет ограничение на допустимые значения (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 <collection> <field> <values...>")
}
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 <name> <filepath>".
// Загружает плагин из файла.
//
// Аргументы:
// - 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> <filepath>")
}
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 <name>".
// Выгружает плагин.
//
// Аргументы:
// - 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>")
}
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 <name>".
// Запускает загруженный плагин.
//
// Аргументы:
// - 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>")
}
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 <name>".
// Останавливает запущенный плагин.
//
// Аргументы:
// - 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>")
}
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 <plugin> <function> [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 <plugin> <function> [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 <database> <filename>".
// Экспортирует базу данных в файл в формате MessagePack.
//
// Аргументы:
// - args[0]: Имя базы данных
// - args[1]: Имя файла для экспорта
//
// Возвращает:
// - error: Ошибка при экспорте или отсутствии аргументов
func (r *Repl) handleExport(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: export <database> <filename>")
}
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 <database> <filename>".
// Импортирует базу данных из файла в формате MessagePack.
//
// Аргументы:
// - args[0]: Имя базы данных
// - args[1]: Имя файла для импорта
//
// Возвращает:
// - error: Ошибка при импорте или отсутствии аргументов
func (r *Repl) handleImport(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: import <database> <filename>")
}
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 <username> <password>".
// Аутентифицирует пользователя по имени и паролю.
//
// Аргументы:
// - args[0]: Имя пользователя
// - args[1]: Пароль
//
// Возвращает:
// - error: Ошибка аутентификации или отсутствии аргументов
func (r *Repl) handleACLLogin(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: acl login <username> <password>")
}
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 <collection> <role> <permissions>".
// Назначает разрешения роли на коллекцию.
// Разрешения: 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 <collection> <role> <permissions>\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 <name>".
// Вручную сжимает все документы в указанной коллекции.
//
// Аргументы:
// - 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 <name>")
}
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 <collection> <id>".
// Показывает информацию о сжатии для конкретного документа.
//
// Аргументы:
// - 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 <collection> <id>")
}
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 <collection> <name> <event> <action> [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 <text>, --set <field> <value>, --inc <field> <value>, --currentDate <field>, --condition <field> <op> <value>")
}
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 <collection> <event> <name>".
// Удаляет триггер из коллекции.
//
// Аргументы:
// - 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 <collection> <event> <name>")
}
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 <collection>".
// Показывает все триггеры коллекции с их свойствами.
//
// Аргументы:
// - 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 <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
}
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 <collection> <event> <name>".
// Включает отключённый триггер.
//
// Аргументы:
// - 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 <collection> <event> <name>")
}
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 <collection> <event> <name>".
// Отключает триггер без его удаления.
//
// Аргументы:
// - 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 <collection> <event> <name>")
}
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
}
// =============================================================================
// ОБРАБОТЧИКИ КОМАНД МИГРАЦИИ (КРОСС-ДАТАЦЕНТРОВАЯ МИГРАЦИЯ)
// =============================================================================
// handleMigration обрабатывает команды миграции
// Формат: migration <subcommand> [options]
func (r *Repl) handleMigration(args []string) error {
if len(args) == 0 {
return fmt.Errorf("usage: migration <subcommand>\n"+
" Subcommands:\n"+
" start <source_dc> <target_dc> [database] [collection] - Start a new migration\n"+
" status [task_id] - Show migration status\n"+
" list - List all migration tasks\n"+
" pause <task_id> - Pause a running migration\n"+
" resume <task_id> - Resume a paused migration\n"+
" cancel <task_id> - Cancel a migration\n"+
" stats - Show migration statistics\n"+
" config - Show migration configuration\n"+
" queue - Show change queue status")
}
// Проверяем наличие координатора и мигратора
if r.coordinator == nil {
return fmt.Errorf("cluster coordinator not available")
}
migrator := r.coordinator.GetCrossDCMigrator()
if migrator == nil {
return fmt.Errorf("migration system not initialized (check config: migration.enabled = true)")
}
// Импортируем команды миграции
// Используем прямое выполнение через обработчик
subcommand := args[0]
switch subcommand {
case "start":
if len(args) < 3 {
return fmt.Errorf("usage: migration start <source_dc> <target_dc> [database] [collection]")
}
return r.handleMigrationStart(migrator, args[1:])
case "status":
return r.handleMigrationStatus(migrator, args[1:])
case "list":
return r.handleMigrationList(migrator)
case "pause":
if len(args) < 2 {
return fmt.Errorf("usage: migration pause <task_id>")
}
return r.handleMigrationPause(migrator, args[1])
case "resume":
if len(args) < 2 {
return fmt.Errorf("usage: migration resume <task_id>")
}
return r.handleMigrationResume(migrator, args[1])
case "cancel":
if len(args) < 2 {
return fmt.Errorf("usage: migration cancel <task_id>")
}
return r.handleMigrationCancel(migrator, args[1])
case "stats":
return r.handleMigrationStats(migrator)
case "config":
return r.handleMigrationConfig(migrator)
case "queue":
return r.handleMigrationQueue(migrator)
default:
return fmt.Errorf("unknown migration subcommand: %s", subcommand)
}
}
// handleMigrationStart обрабатывает команду migration start
func (r *Repl) handleMigrationStart(migrator *cluster.CrossDCMigrator, args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: migration start <source_dc> <target_dc> [database] [collection]")
}
sourceDC := args[0]
targetDC := args[1]
var databases []string
var collections map[string][]string
if len(args) > 2 {
databases = []string{args[2]}
if len(args) > 3 {
collections = map[string][]string{
args[2]: {args[3]},
}
}
}
utils.PrintInfo(fmt.Sprintf("Starting migration from %s to %s at %s...",
sourceDC, targetDC, time.Now().Format("2006-01-02 15:04:05")))
task, err := migrator.StartMigration(sourceDC, targetDC, databases, collections)
if err != nil {
return fmt.Errorf("failed to start migration: %v", err)
}
utils.PrintSuccess(fmt.Sprintf("Migration started: %s", task.ID))
utils.PrintInfo(fmt.Sprintf(" Source: %s", task.SourceDC))
utils.PrintInfo(fmt.Sprintf(" Target: %s", task.TargetDC))
utils.PrintInfo(fmt.Sprintf(" Total documents: %d", task.TotalDocuments))
utils.PrintInfo(fmt.Sprintf(" Status: %s", task.Status))
return nil
}
// handleMigrationStatus обрабатывает команду migration status
func (r *Repl) handleMigrationStatus(migrator *cluster.CrossDCMigrator, args []string) error {
var taskID string
if len(args) > 0 {
taskID = args[0]
} else {
taskID = migrator.GetCurrentTaskID()
if taskID == "" {
return fmt.Errorf("no active migration task")
}
}
task, err := migrator.GetMigrationStatus(taskID)
if err != nil {
return err
}
// Выводим статус задачи, используя публичные поля
utils.PrintHeader(fmt.Sprintf("Migration Task: %s", task.ID))
utils.PrintInfo(fmt.Sprintf(" Source DC: %s", task.SourceDC))
utils.PrintInfo(fmt.Sprintf(" Target DC: %s", task.TargetDC))
utils.PrintInfo(fmt.Sprintf(" Status: %s", r.colorize(string(task.Status), r.getMigrationStatusColor(string(task.Status)))))
utils.PrintInfo(fmt.Sprintf(" Progress: %.1f%%", task.ProgressPercent))
utils.PrintInfo(fmt.Sprintf(" Total Docs: %d", task.TotalDocuments))
utils.PrintInfo(fmt.Sprintf(" Migrated: %d", task.MigratedDocs))
utils.PrintInfo(fmt.Sprintf(" Failed: %d", task.FailedDocs))
utils.PrintInfo(fmt.Sprintf(" Skipped: %d", task.SkippedDocs))
if task.StartTime > 0 {
startTime := time.UnixMilli(task.StartTime).Format("2006-01-02 15:04:05")
utils.PrintInfo(fmt.Sprintf(" Started: %s", startTime))
}
if task.EndTime > 0 {
endTime := time.UnixMilli(task.EndTime).Format("2006-01-02 15:04:05")
utils.PrintInfo(fmt.Sprintf(" Completed: %s", endTime))
}
if task.Error != "" {
utils.PrintError(fmt.Sprintf(" Error: %s", task.Error))
}
if len(task.Databases) > 0 {
utils.PrintInfo(fmt.Sprintf(" Databases: %s", strings.Join(task.Databases, ", ")))
}
if len(task.Collections) > 0 {
utils.PrintInfo(" Collections:")
for db, colls := range task.Collections {
utils.PrintInfo(fmt.Sprintf(" %s: %s", db, strings.Join(colls, ", ")))
}
}
return nil
}
// handleMigrationList обрабатывает команду migration list
func (r *Repl) handleMigrationList(migrator *cluster.CrossDCMigrator) error {
tasks := migrator.ListTasks()
if len(tasks) == 0 {
utils.PrintInfo("No migration tasks found")
return nil
}
utils.PrintHeader("Migration Tasks")
utils.PrintInfo(" ID STATUS PROGRESS SOURCE -> TARGET")
utils.Println(" ---------------------------------------------------------------------------------")
for _, task := range tasks {
id := task.ID
if len(id) > 20 {
id = id[:17] + "..."
}
statusColor := r.getMigrationStatusColor(string(task.Status))
statusStr := r.colorize(string(task.Status), statusColor)
progress := fmt.Sprintf("%.1f%%", task.ProgressPercent)
if task.Status == cluster.MigrationStatusCompleted {
progress = "100%"
}
utils.PrintInfo(fmt.Sprintf(" %-36s %-12s %-10s %s -> %s",
id, statusStr, progress, task.SourceDC, task.TargetDC))
}
return nil
}
// handleMigrationPause обрабатывает команду migration pause
func (r *Repl) handleMigrationPause(migrator *cluster.CrossDCMigrator, taskID string) error {
if err := migrator.PauseMigration(taskID); err != nil {
return err
}
utils.PrintSuccess(fmt.Sprintf("Migration %s paused", taskID))
return nil
}
// handleMigrationResume обрабатывает команду migration resume
func (r *Repl) handleMigrationResume(migrator *cluster.CrossDCMigrator, taskID string) error {
if err := migrator.ResumeMigration(taskID); err != nil {
return err
}
utils.PrintSuccess(fmt.Sprintf("Migration %s resumed", taskID))
return nil
}
// handleMigrationCancel обрабатывает команду migration cancel
func (r *Repl) handleMigrationCancel(migrator *cluster.CrossDCMigrator, taskID string) error {
if err := migrator.CancelMigration(taskID); err != nil {
return err
}
utils.PrintSuccess(fmt.Sprintf("Migration %s cancelled", taskID))
return nil
}
// handleMigrationStats обрабатывает команду migration stats
func (r *Repl) handleMigrationStats(migrator *cluster.CrossDCMigrator) error {
stats := migrator.GetMigrationStats()
utils.PrintHeader("Migration Statistics")
utils.PrintInfo(fmt.Sprintf(" Total Changes: %d", stats.TotalChanges))
utils.PrintInfo(fmt.Sprintf(" Applied Changes: %d", stats.AppliedChanges))
utils.PrintInfo(fmt.Sprintf(" Failed Changes: %d", stats.FailedChanges))
utils.PrintInfo(fmt.Sprintf(" Skipped Changes: %d", stats.SkippedChanges))
utils.PrintInfo(fmt.Sprintf(" Avg Latency: %d ms", stats.LatencyAvg))
utils.PrintInfo(fmt.Sprintf(" Max Latency: %d ms", stats.LatencyMax))
utils.PrintInfo(fmt.Sprintf(" Throughput: %d docs/sec", stats.Throughput))
return nil
}
// handleMigrationConfig обрабатывает команду migration config
func (r *Repl) handleMigrationConfig(migrator *cluster.CrossDCMigrator) error {
// Используем публичные методы для получения конфигурации
cfg := migrator.GetConfig()
utils.PrintHeader("Migration Configuration")
utils.PrintInfo(fmt.Sprintf(" Enabled: %v", cfg.Enabled))
utils.PrintInfo(fmt.Sprintf(" Mode: %s", cfg.Mode))
if cfg.Source != nil {
utils.PrintInfo(fmt.Sprintf(" Source DC: %s (%s)", cfg.Source.Name, cfg.Source.Endpoint))
}
if cfg.Target != nil {
utils.PrintInfo(fmt.Sprintf(" Target DC: %s (%s)", cfg.Target.Name, cfg.Target.Endpoint))
}
if cfg.Settings != nil {
utils.PrintInfo(fmt.Sprintf(" Batch Size: %d", cfg.Settings.BatchSize))
utils.PrintInfo(fmt.Sprintf(" Workers: %d", cfg.Settings.Workers))
utils.PrintInfo(fmt.Sprintf(" Compression: %s", cfg.Settings.Compression))
utils.PrintInfo(fmt.Sprintf(" Resume Enabled: %v", cfg.Settings.ResumeEnabled))
utils.PrintInfo(fmt.Sprintf(" Max Retries: %d", cfg.Settings.MaxRetries))
}
if cfg.Delta != nil {
utils.PrintInfo(fmt.Sprintf(" Delta Sync: %v", cfg.Delta.Enabled))
utils.PrintInfo(fmt.Sprintf(" Delta Interval: %d sec", cfg.Delta.IntervalSec))
}
if cfg.Validation != nil {
utils.PrintInfo(fmt.Sprintf(" Validation: %v", cfg.Validation.Enabled))
utils.PrintInfo(fmt.Sprintf(" Sample Percent: %d%%", cfg.Validation.SamplePercent))
}
return nil
}
// handleMigrationQueue обрабатывает команду migration queue
func (r *Repl) handleMigrationQueue(migrator *cluster.CrossDCMigrator) error {
stats := migrator.GetQueueStats()
utils.PrintHeader("Migration Queue Status")
utils.PrintInfo(fmt.Sprintf(" Queue Size: %d", stats["size"]))
utils.PrintInfo(fmt.Sprintf(" Last LSN: %d", stats["last_lsn"]))
utils.PrintInfo(fmt.Sprintf(" Max Size: %d", stats["max_size"]))
utils.PrintInfo(fmt.Sprintf(" File Path: %s", stats["file_path"]))
return nil
}
// getMigrationStatusColor возвращает цвет для статуса миграции
func (r *Repl) getMigrationStatusColor(status string) string {
switch status {
case "idle":
return "cyan"
case "preparing":
return "blue"
case "migrating":
return "yellow"
case "delta_sync":
return "yellow"
case "validating":
return "cyan"
case "completed":
return "green"
case "failed":
return "red"
case "paused":
return "yellow"
default:
return "white"
}
}
// =============================================================================
// ВСПОМОГАТЕЛЬНЫЙ МЕТОД ДЛЯ ЦВЕТНОГО ВЫВОДА
// =============================================================================
// 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 <name>", "Create a new database (slice)"},
{"drop database <name>", "Delete an existing database"},
{"use <database>", "Switch to a specific database"},
{"show databases", "List all available databases"},
},
"Collection Management": {
{"create collection <name>", "Create a new collection in current database"},
{"drop collection <name>", "Delete a collection from current database"},
{"show collections", "List all collections in current database"},
},
"Document Operations": {
{"insert <collection> <json>", "Insert a new document (JSON format: key=value,key2=value2)"},
{"find <collection> <id>", "Find a document by its ID"},
{"findbyindex <collection> <index> <value>", "Find documents using an index"},
{"findbytime <collection> <from_date> <to_date>", "Find documents by time range"},
{"update <collection> <id> <field=value>...", "Update fields of an existing document"},
{"delete <collection> <id>", "Delete a document (soft delete if enabled)"},
{"permanent delete <collection> <id>", "Permanently delete a soft-deleted document"},
{"restore <collection> <id>", "Restore a soft-deleted document"},
{"show deleted <collection>", "Show soft-deleted documents in a collection"},
{"count <collection>", "Count total documents in a collection"},
},
"Timestamp Management": {
{"show timestamps <collection> <id>", "Show timestamps for a document"},
{"stats timestamps <collection>", "Show timestamp statistics for a collection"},
{"audit log", "Show audit log"},
{"audit filter <type> <op>", "Filter audit log by type and operation"},
},
"Index Management": {
{"create index <collection> <name> <fields> [unique]", "Create a new index on specified fields"},
{"drop index <collection> <name>", "Remove an existing index"},
{"show indexes <collection>", "List all indexes on a collection"},
},
"Constraints": {
{"add required <collection> <field>", "Add a required field constraint"},
{"add unique <collection> <field>", "Add a unique constraint on a field"},
{"add min <collection> <field> <value>", "Add a minimum value constraint for numeric fields"},
{"add max <collection> <field> <value>", "Add a maximum value constraint for numeric fields"},
{"add enum <collection> <field> <values...>", "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 <collection> <name> <event> <action> [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 <collection> <event> <name>", "Remove a trigger from collection"},
{"show triggers <collection>", "List all triggers on a collection"},
{"enable trigger <collection> <event> <name>", "Enable a disabled trigger"},
{"disable trigger <collection> <event> <name>", "Disable a trigger without removing it"},
{"trigger log", "Show trigger execution history"},
},
"Plugins": {
{"plugin list", "List all loaded plugins"},
{"plugin load <name> <filepath>", "Load a plugin from Lua file"},
{"plugin unload <name>", "Unload a plugin"},
{"plugin start <name>", "Start a loaded plugin"},
{"plugin stop <name>", "Stop a running plugin"},
{"plugin exec <plugin> <function> [args...]", "Execute a plugin function"},
},
"Import/Export": {
{"export <database> <filename>", "Export entire database to file"},
{"import <database> <filename>", "Import database from file"},
},
"Compression": {
{"compression stats", "Show compression statistics for current database"},
{"compression config", "Display current compression settings"},
{"compress collection <name>", "Manually compress all documents in a collection"},
{"doc compression <collection> <id>", "Show compression info for a specific document"},
},
"Access Control": {
{"acl login <username> <password>", "Authenticate with username and password"},
{"acl logout", "Logout current user session"},
{"acl grant <collection> <role> <permissions>", "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"},
},
"Migration": {
{"migration start <source_dc> <target_dc> [db] [coll]", "Start cross-datacenter migration"},
{"migration status [task_id]", "Show migration status"},
{"migration list", "List all migration tasks"},
{"migration pause <task_id>", "Pause a running migration"},
{"migration resume <task_id>", "Resume a paused migration"},
{"migration cancel <task_id>", "Cancel a migration"},
{"migration stats", "Show migration statistics"},
{"migration config", "Show migration configuration"},
{"migration queue", "Show change queue status"},
},
"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("")
utils.Println("Migration examples:")
utils.Println(" migration start dc-primary dc-secondary")
utils.Println(" migration start dc-primary dc-secondary mydb users")
utils.Println(" migration status")
utils.Println(" migration list")
utils.Println(" migration stats")
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
}