1327 lines
37 KiB
Go
1327 lines
37 KiB
Go
|
|
// Файл: internal/repl/repl.go
|
|||
|
|
// Назначение: REPL (Read-Eval-Print Loop) интерфейс для интерактивной работы с СУБД.
|
|||
|
|
// Поддерживает автодополнение, историю команд, цветовой вывод и все операции с данными.
|
|||
|
|
|
|||
|
|
package repl
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"bufio"
|
|||
|
|
"fmt"
|
|||
|
|
"os"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"futriis/internal/cluster"
|
|||
|
|
"futriis/internal/compression"
|
|||
|
|
"futriis/internal/config"
|
|||
|
|
"futriis/internal/log"
|
|||
|
|
"futriis/internal/storage"
|
|||
|
|
"futriis/pkg/utils"
|
|||
|
|
|
|||
|
|
"github.com/fatih/color"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Repl представляет основную структуру REPL
|
|||
|
|
type Repl struct {
|
|||
|
|
store *storage.Storage
|
|||
|
|
coordinator *cluster.RaftCoordinator
|
|||
|
|
logger *log.Logger
|
|||
|
|
config *config.Config
|
|||
|
|
reader *bufio.Reader
|
|||
|
|
currentDB string
|
|||
|
|
currentUser string
|
|||
|
|
currentRole string
|
|||
|
|
authenticated bool
|
|||
|
|
commands map[string]*Command
|
|||
|
|
history []string
|
|||
|
|
historyPos int
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Command представляет команду REPL
|
|||
|
|
type Command struct {
|
|||
|
|
Name string
|
|||
|
|
Description string
|
|||
|
|
Handler func(args []string) error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewRepl создаёт новый экземпляр REPL
|
|||
|
|
func NewRepl(store *storage.Storage, coordinator *cluster.RaftCoordinator, logger *log.Logger, cfg *config.Config) *Repl {
|
|||
|
|
r := &Repl{
|
|||
|
|
store: store,
|
|||
|
|
coordinator: coordinator,
|
|||
|
|
logger: logger,
|
|||
|
|
config: cfg,
|
|||
|
|
reader: bufio.NewReader(os.Stdin),
|
|||
|
|
currentDB: "",
|
|||
|
|
currentUser: "",
|
|||
|
|
currentRole: "anonymous",
|
|||
|
|
authenticated: false,
|
|||
|
|
commands: make(map[string]*Command),
|
|||
|
|
history: make([]string, 0, cfg.Repl.HistorySize),
|
|||
|
|
historyPos: -1,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.registerCommands()
|
|||
|
|
return r
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// registerCommands регистрирует все команды REPL
|
|||
|
|
func (r *Repl) registerCommands() {
|
|||
|
|
// Команды управления базами данных
|
|||
|
|
r.commands["create database"] = &Command{
|
|||
|
|
Name: "create database",
|
|||
|
|
Description: "Create a new database",
|
|||
|
|
Handler: r.handleCreateDatabase,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["drop database"] = &Command{
|
|||
|
|
Name: "drop database",
|
|||
|
|
Description: "Drop a database",
|
|||
|
|
Handler: r.handleDropDatabase,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["use"] = &Command{
|
|||
|
|
Name: "use",
|
|||
|
|
Description: "Switch to a database",
|
|||
|
|
Handler: r.handleUseDatabase,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["show databases"] = &Command{
|
|||
|
|
Name: "show databases",
|
|||
|
|
Description: "List all databases",
|
|||
|
|
Handler: r.handleShowDatabases,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Команды управления коллекциями
|
|||
|
|
r.commands["create collection"] = &Command{
|
|||
|
|
Name: "create collection",
|
|||
|
|
Description: "Create a new collection in current database",
|
|||
|
|
Handler: r.handleCreateCollection,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["drop collection"] = &Command{
|
|||
|
|
Name: "drop collection",
|
|||
|
|
Description: "Drop a collection from current database",
|
|||
|
|
Handler: r.handleDropCollection,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["show collections"] = &Command{
|
|||
|
|
Name: "show collections",
|
|||
|
|
Description: "List all collections in current database",
|
|||
|
|
Handler: r.handleShowCollections,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Команды работы с документами
|
|||
|
|
r.commands["insert"] = &Command{
|
|||
|
|
Name: "insert",
|
|||
|
|
Description: "Insert a document into a collection (JSON format)",
|
|||
|
|
Handler: r.handleInsert,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["find"] = &Command{
|
|||
|
|
Name: "find",
|
|||
|
|
Description: "Find a document by ID",
|
|||
|
|
Handler: r.handleFind,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["findbyindex"] = &Command{
|
|||
|
|
Name: "findbyindex",
|
|||
|
|
Description: "Find documents by index",
|
|||
|
|
Handler: r.handleFindByIndex,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["update"] = &Command{
|
|||
|
|
Name: "update",
|
|||
|
|
Description: "Update a document",
|
|||
|
|
Handler: r.handleUpdate,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["delete"] = &Command{
|
|||
|
|
Name: "delete",
|
|||
|
|
Description: "Delete a document",
|
|||
|
|
Handler: r.handleDelete,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["count"] = &Command{
|
|||
|
|
Name: "count",
|
|||
|
|
Description: "Count documents in a collection",
|
|||
|
|
Handler: r.handleCount,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Команды управления индексами
|
|||
|
|
r.commands["create index"] = &Command{
|
|||
|
|
Name: "create index",
|
|||
|
|
Description: "Create an index on a collection",
|
|||
|
|
Handler: r.handleCreateIndex,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["drop index"] = &Command{
|
|||
|
|
Name: "drop index",
|
|||
|
|
Description: "Drop an index from a collection",
|
|||
|
|
Handler: r.handleDropIndex,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["show indexes"] = &Command{
|
|||
|
|
Name: "show indexes",
|
|||
|
|
Description: "Show all indexes in a collection",
|
|||
|
|
Handler: r.handleShowIndexes,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Команды ограничений
|
|||
|
|
r.commands["add required"] = &Command{
|
|||
|
|
Name: "add required",
|
|||
|
|
Description: "Add a required field constraint",
|
|||
|
|
Handler: r.handleAddRequired,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["add unique"] = &Command{
|
|||
|
|
Name: "add unique",
|
|||
|
|
Description: "Add a unique constraint",
|
|||
|
|
Handler: r.handleAddUnique,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["add min"] = &Command{
|
|||
|
|
Name: "add min",
|
|||
|
|
Description: "Add a minimum value constraint",
|
|||
|
|
Handler: r.handleAddMin,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["add max"] = &Command{
|
|||
|
|
Name: "add max",
|
|||
|
|
Description: "Add a maximum value constraint",
|
|||
|
|
Handler: r.handleAddMax,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["add enum"] = &Command{
|
|||
|
|
Name: "add enum",
|
|||
|
|
Description: "Add an enum constraint (allowed values)",
|
|||
|
|
Handler: r.handleAddEnum,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Команды ACL
|
|||
|
|
r.commands["acl login"] = &Command{
|
|||
|
|
Name: "acl login",
|
|||
|
|
Description: "Login with username and password",
|
|||
|
|
Handler: r.handleACLLogin,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["acl logout"] = &Command{
|
|||
|
|
Name: "acl logout",
|
|||
|
|
Description: "Logout current user",
|
|||
|
|
Handler: r.handleACLLogout,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["acl grant"] = &Command{
|
|||
|
|
Name: "acl grant",
|
|||
|
|
Description: "Grant permissions to a role",
|
|||
|
|
Handler: r.handleACLGrant,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Команды сжатия
|
|||
|
|
r.commands["compression stats"] = &Command{
|
|||
|
|
Name: "compression stats",
|
|||
|
|
Description: "Show compression statistics for the database",
|
|||
|
|
Handler: r.handleCompressionStats,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["compress collection"] = &Command{
|
|||
|
|
Name: "compress collection",
|
|||
|
|
Description: "Manually compress all documents in a collection",
|
|||
|
|
Handler: r.handleCompressCollection,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["doc compression"] = &Command{
|
|||
|
|
Name: "doc compression",
|
|||
|
|
Description: "Show compression ratio for a document",
|
|||
|
|
Handler: r.handleDocCompression,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["compression config"] = &Command{
|
|||
|
|
Name: "compression config",
|
|||
|
|
Description: "Show current compression configuration",
|
|||
|
|
Handler: r.handleCompressionConfig,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Команды кластера
|
|||
|
|
r.commands["status"] = &Command{
|
|||
|
|
Name: "status",
|
|||
|
|
Description: "Show cluster status",
|
|||
|
|
Handler: r.handleStatus,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["nodes"] = &Command{
|
|||
|
|
Name: "nodes",
|
|||
|
|
Description: "List cluster nodes",
|
|||
|
|
Handler: r.handleNodes,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Команды системы
|
|||
|
|
r.commands["help"] = &Command{
|
|||
|
|
Name: "help",
|
|||
|
|
Description: "Show this help message",
|
|||
|
|
Handler: r.handleHelp,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["clear"] = &Command{
|
|||
|
|
Name: "clear",
|
|||
|
|
Description: "Clear the screen",
|
|||
|
|
Handler: r.handleClear,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["quit"] = &Command{
|
|||
|
|
Name: "quit",
|
|||
|
|
Description: "Exit the REPL",
|
|||
|
|
Handler: r.handleQuit,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
r.commands["exit"] = &Command{
|
|||
|
|
Name: "exit",
|
|||
|
|
Description: "Exit the REPL",
|
|||
|
|
Handler: r.handleQuit,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Run запускает основной цикл REPL
|
|||
|
|
func (r *Repl) Run() error {
|
|||
|
|
utils.Println("")
|
|||
|
|
utils.PrintInfo("Type 'help' for available commands")
|
|||
|
|
utils.Println("")
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
// Формируем приглашение к вводу
|
|||
|
|
prompt := r.buildPrompt()
|
|||
|
|
|
|||
|
|
// Читаем ввод пользователя
|
|||
|
|
fmt.Print(prompt)
|
|||
|
|
input, err := r.reader.ReadString('\n')
|
|||
|
|
if err != nil {
|
|||
|
|
if err.Error() == "EOF" {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
input = strings.TrimSpace(input)
|
|||
|
|
if input == "" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Сохраняем в историю
|
|||
|
|
r.addToHistory(input)
|
|||
|
|
|
|||
|
|
// Обрабатываем команду
|
|||
|
|
if err := r.executeCommand(input); err != nil {
|
|||
|
|
utils.PrintError(err.Error())
|
|||
|
|
r.logger.Error("REPL command error: " + err.Error())
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// buildPrompt формирует строку приглашения
|
|||
|
|
func (r *Repl) buildPrompt() string {
|
|||
|
|
return color.New(color.FgHiCyan).Sprint("futriiS:~> ")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// executeCommand выполняет введённую команду
|
|||
|
|
func (r *Repl) executeCommand(input string) error {
|
|||
|
|
parts := strings.Fields(input)
|
|||
|
|
if len(parts) == 0 {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Ищем команду по префиксу
|
|||
|
|
for cmdName, cmd := range r.commands {
|
|||
|
|
if strings.HasPrefix(input, cmdName) {
|
|||
|
|
args := strings.SplitN(input, " ", len(strings.Fields(cmdName)))
|
|||
|
|
if len(args) > 0 {
|
|||
|
|
args = args[1:]
|
|||
|
|
}
|
|||
|
|
return cmd.Handler(args)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return fmt.Errorf("unknown command: %s", parts[0])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// addToHistory добавляет команду в историю
|
|||
|
|
func (r *Repl) addToHistory(cmd string) {
|
|||
|
|
if len(r.history) >= r.config.Repl.HistorySize {
|
|||
|
|
r.history = r.history[1:]
|
|||
|
|
}
|
|||
|
|
r.history = append(r.history, cmd)
|
|||
|
|
r.historyPos = len(r.history)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Обработчики команд ==========
|
|||
|
|
|
|||
|
|
func (r *Repl) handleCreateDatabase(args []string) error {
|
|||
|
|
if len(args) < 1 {
|
|||
|
|
return fmt.Errorf("usage: create database <name>")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
name := args[0]
|
|||
|
|
if err := r.store.CreateDatabase(name); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintSuccess(fmt.Sprintf("Database '%s' created", name))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleDropDatabase(args []string) error {
|
|||
|
|
if len(args) < 1 {
|
|||
|
|
return fmt.Errorf("usage: drop database <name>")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
name := args[0]
|
|||
|
|
if err := r.store.DropDatabase(name); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if r.currentDB == name {
|
|||
|
|
r.currentDB = ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintSuccess(fmt.Sprintf("Database '%s' dropped", name))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleUseDatabase(args []string) error {
|
|||
|
|
if len(args) < 1 {
|
|||
|
|
return fmt.Errorf("usage: use <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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleShowDatabases(args []string) error {
|
|||
|
|
databases := r.store.ListDatabases()
|
|||
|
|
if len(databases) == 0 {
|
|||
|
|
utils.PrintInfo("No databases found")
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintInfo("Databases:")
|
|||
|
|
for _, db := range databases {
|
|||
|
|
prefix := " "
|
|||
|
|
if db == r.currentDB {
|
|||
|
|
prefix = " *"
|
|||
|
|
}
|
|||
|
|
utils.Println(fmt.Sprintf("%s %s", prefix, db))
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleCreateCollection(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 1 {
|
|||
|
|
return fmt.Errorf("usage: create collection <name>")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
name := args[0]
|
|||
|
|
db, err := r.store.GetDatabase(r.currentDB)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := db.CreateCollection(name); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintSuccess(fmt.Sprintf("Collection '%s' created in database '%s'", name, r.currentDB))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleDropCollection(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 1 {
|
|||
|
|
return fmt.Errorf("usage: drop collection <name>")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
name := args[0]
|
|||
|
|
db, err := r.store.GetDatabase(r.currentDB)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := db.DropCollection(name); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintSuccess(fmt.Sprintf("Collection '%s' dropped from database '%s'", name, r.currentDB))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleShowCollections(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
db, err := r.store.GetDatabase(r.currentDB)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
collections := db.ListCollections()
|
|||
|
|
if len(collections) == 0 {
|
|||
|
|
utils.PrintInfo("No collections found")
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintInfo(fmt.Sprintf("Collections in database '%s':", r.currentDB))
|
|||
|
|
for _, coll := range collections {
|
|||
|
|
utils.Println(fmt.Sprintf(" - %s", coll))
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleInsert(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 2 {
|
|||
|
|
return fmt.Errorf("usage: insert <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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Простой парсинг JSON (можно улучшить)
|
|||
|
|
// Для примера, создаём документ из map
|
|||
|
|
doc := storage.NewDocument()
|
|||
|
|
|
|||
|
|
// Упрощённый парсинг: ожидаем формат key=value,key2=value2
|
|||
|
|
pairs := strings.Split(jsonStr, ",")
|
|||
|
|
for _, pair := range pairs {
|
|||
|
|
kv := strings.SplitN(pair, "=", 2)
|
|||
|
|
if len(kv) == 2 {
|
|||
|
|
doc.SetField(kv[0], kv[1])
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if err := coll.Insert(doc); err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintSuccess(fmt.Sprintf("Document inserted with ID: %s", doc.ID))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleFind(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 2 {
|
|||
|
|
return fmt.Errorf("usage: find <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())
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleFindByIndex(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 3 {
|
|||
|
|
return fmt.Errorf("usage: findbyindex <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", i+1, doc.ID))
|
|||
|
|
utils.PrintJSON(doc.GetFields())
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleUpdate(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 3 {
|
|||
|
|
return fmt.Errorf("usage: update <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", docID))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleDelete(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 2 {
|
|||
|
|
return fmt.Errorf("usage: delete <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", docID))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleCount(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 1 {
|
|||
|
|
return fmt.Errorf("usage: count <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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
count := coll.Count()
|
|||
|
|
utils.PrintInfo(fmt.Sprintf("Collection '%s' has %d document(s)", collName, count))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleCreateIndex(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 3 {
|
|||
|
|
return fmt.Errorf("usage: create index <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'", indexName, collName))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleDropIndex(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 2 {
|
|||
|
|
return fmt.Errorf("usage: drop index <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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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.GetIndexes()
|
|||
|
|
if len(indexes) == 0 {
|
|||
|
|
utils.PrintInfo(fmt.Sprintf("No indexes found on collection '%s'", collName))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintInfo(fmt.Sprintf("Indexes on collection '%s':", collName))
|
|||
|
|
for _, idx := range indexes {
|
|||
|
|
utils.Println(fmt.Sprintf(" - %s", idx))
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleAddRequired(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 2 {
|
|||
|
|
return fmt.Errorf("usage: add required <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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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 username == "admin" && password == "admin" {
|
|||
|
|
r.authenticated = true
|
|||
|
|
r.currentUser = username
|
|||
|
|
r.currentRole = "admin"
|
|||
|
|
utils.PrintSuccess(fmt.Sprintf("Logged in as '%s' with role '%s'", username, r.currentRole))
|
|||
|
|
} else {
|
|||
|
|
return fmt.Errorf("invalid username or password")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleACLLogout(args []string) error {
|
|||
|
|
r.authenticated = false
|
|||
|
|
r.currentUser = ""
|
|||
|
|
r.currentRole = "anonymous"
|
|||
|
|
utils.PrintSuccess("Logged out")
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleACLGrant(args []string) error {
|
|||
|
|
if !r.authenticated || r.currentRole != "admin" {
|
|||
|
|
return fmt.Errorf("permission denied: admin access required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 3 {
|
|||
|
|
return fmt.Errorf("usage: acl grant <collection> <role> <permissions>")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
collName := args[0]
|
|||
|
|
role := args[1]
|
|||
|
|
perms := args[2]
|
|||
|
|
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
db, err := r.store.GetDatabase(r.currentDB)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
coll, err := db.GetCollection(collName)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
canRead := strings.Contains(perms, "r")
|
|||
|
|
canWrite := strings.Contains(perms, "w")
|
|||
|
|
canDelete := strings.Contains(perms, "d")
|
|||
|
|
isAdmin := strings.Contains(perms, "a")
|
|||
|
|
|
|||
|
|
coll.SetACL(role, canRead, canWrite, canDelete, isAdmin)
|
|||
|
|
utils.PrintSuccess(fmt.Sprintf("Permissions '%s' granted to role '%s' on collection '%s'", perms, role, collName))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Обработчики команд сжатия ==========
|
|||
|
|
|
|||
|
|
func (r *Repl) handleCompressionStats(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
db, err := r.store.GetDatabase(r.currentDB)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
collections := db.ListCollections()
|
|||
|
|
totalDocs := int64(0)
|
|||
|
|
compressedDocs := int64(0)
|
|||
|
|
totalOriginalSize := int64(0)
|
|||
|
|
totalCompressedSize := int64(0)
|
|||
|
|
|
|||
|
|
for _, collName := range collections {
|
|||
|
|
coll, err := db.GetCollection(collName)
|
|||
|
|
if err != nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
docs := coll.GetAllDocuments()
|
|||
|
|
for _, doc := range docs {
|
|||
|
|
totalDocs++
|
|||
|
|
if doc.Compressed {
|
|||
|
|
compressedDocs++
|
|||
|
|
totalOriginalSize += doc.OriginalSize
|
|||
|
|
// Оцениваем сжатый размер (для статистики)
|
|||
|
|
if data, err := doc.Serialize(); err == nil {
|
|||
|
|
totalCompressedSize += int64(len(data))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintHeader("Compression Statistics")
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Total Documents: %d", totalDocs))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Compressed Documents: %d", compressedDocs))
|
|||
|
|
if totalDocs > 0 {
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Compression Rate: %.2f%%", float64(compressedDocs)/float64(totalDocs)*100))
|
|||
|
|
}
|
|||
|
|
if totalOriginalSize > 0 {
|
|||
|
|
ratio := float64(totalCompressedSize) / float64(totalOriginalSize)
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Size Reduction: %.2f%%", (1-ratio)*100))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Original Size: %s", utils.FormatBytes(totalOriginalSize)))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Compressed Size: %s", utils.FormatBytes(totalCompressedSize)))
|
|||
|
|
}
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Algorithm: %s", r.config.Compression.Algorithm))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Compression Level: %d", r.config.Compression.Level))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Min Size Threshold: %s", utils.FormatBytes(int64(r.config.Compression.MinSize))))
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleCompressCollection(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 1 {
|
|||
|
|
return fmt.Errorf("usage: compress collection <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'...", collName))
|
|||
|
|
|
|||
|
|
for _, doc := range docs {
|
|||
|
|
if !doc.Compressed {
|
|||
|
|
if err := doc.Compress(&compression.Config{
|
|||
|
|
Enabled: r.config.Compression.Enabled,
|
|||
|
|
Algorithm: r.config.Compression.Algorithm,
|
|||
|
|
Level: r.config.Compression.Level,
|
|||
|
|
MinSize: r.config.Compression.MinSize,
|
|||
|
|
}); err == nil {
|
|||
|
|
compressed++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintSuccess(fmt.Sprintf("Compressed %d documents in collection '%s'", compressed, collName))
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleDocCompression(args []string) error {
|
|||
|
|
if r.currentDB == "" {
|
|||
|
|
return fmt.Errorf("no database selected")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(args) < 2 {
|
|||
|
|
return fmt.Errorf("usage: doc compression <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
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleCompressionConfig(args []string) error {
|
|||
|
|
utils.PrintHeader("Compression Configuration")
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Enabled: %v", r.config.Compression.Enabled))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Algorithm: %s", r.config.Compression.Algorithm))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Level: %d", r.config.Compression.Level))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Min Size: %s", utils.FormatBytes(int64(r.config.Compression.MinSize))))
|
|||
|
|
|
|||
|
|
// Выводим информацию об алгоритмах
|
|||
|
|
utils.PrintInfo("")
|
|||
|
|
utils.PrintInfo("Available Algorithms:")
|
|||
|
|
utils.PrintInfo(" snappy - Fast compression/decompression, good balance (default)")
|
|||
|
|
utils.PrintInfo(" lz4 - Extremely fast, lower compression ratio")
|
|||
|
|
utils.PrintInfo(" zstd - High compression ratio, slower")
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Обработчики команд кластера ==========
|
|||
|
|
|
|||
|
|
func (r *Repl) handleStatus(args []string) error {
|
|||
|
|
utils.PrintHeader("Cluster Status")
|
|||
|
|
|
|||
|
|
isLeader := r.coordinator.IsLeader()
|
|||
|
|
if isLeader {
|
|||
|
|
utils.PrintSuccess(" Role: LEADER")
|
|||
|
|
} else {
|
|||
|
|
utils.PrintWarning(" Role: FOLLOWER")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Cluster Name: %s", r.config.Cluster.Name))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Node: %s:%d", r.config.Cluster.NodeIP, r.config.Cluster.NodePort))
|
|||
|
|
utils.PrintInfo(fmt.Sprintf(" Raft Port: %d", r.config.Cluster.RaftPort))
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleNodes(args []string) error {
|
|||
|
|
utils.PrintHeader("Cluster Nodes")
|
|||
|
|
|
|||
|
|
for i, node := range r.config.Cluster.Nodes {
|
|||
|
|
prefix := " "
|
|||
|
|
if i == 0 {
|
|||
|
|
prefix = " *"
|
|||
|
|
}
|
|||
|
|
utils.Println(fmt.Sprintf("%s %s", prefix, node))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ========== Системные обработчики ==========
|
|||
|
|
|
|||
|
|
func (r *Repl) handleHelp(args []string) error {
|
|||
|
|
utils.Println("")
|
|||
|
|
utils.PrintHeader("Available Commands")
|
|||
|
|
|
|||
|
|
// Группировка команд по категориям
|
|||
|
|
categories := map[string][]string{
|
|||
|
|
"Database Management": {
|
|||
|
|
"create database <name>",
|
|||
|
|
"drop database <name>",
|
|||
|
|
"use <database>",
|
|||
|
|
"show databases",
|
|||
|
|
},
|
|||
|
|
"Collection Management": {
|
|||
|
|
"create collection <name>",
|
|||
|
|
"drop collection <name>",
|
|||
|
|
"show collections",
|
|||
|
|
},
|
|||
|
|
"Document Operations": {
|
|||
|
|
"insert <collection> <json>",
|
|||
|
|
"find <collection> <id>",
|
|||
|
|
"findbyindex <collection> <index> <value>",
|
|||
|
|
"update <collection> <id> <field=value>...",
|
|||
|
|
"delete <collection> <id>",
|
|||
|
|
"count <collection>",
|
|||
|
|
},
|
|||
|
|
"Index Management": {
|
|||
|
|
"create index <collection> <name> <fields> [unique]",
|
|||
|
|
"drop index <collection> <name>",
|
|||
|
|
"show indexes <collection>",
|
|||
|
|
},
|
|||
|
|
"Constraints": {
|
|||
|
|
"add required <collection> <field>",
|
|||
|
|
"add unique <collection> <field>",
|
|||
|
|
"add min <collection> <field> <value>",
|
|||
|
|
"add max <collection> <field> <value>",
|
|||
|
|
"add enum <collection> <field> <values...>",
|
|||
|
|
},
|
|||
|
|
"Compression": {
|
|||
|
|
"compression stats",
|
|||
|
|
"compression config",
|
|||
|
|
"compress collection <name>",
|
|||
|
|
"doc compression <collection> <id>",
|
|||
|
|
},
|
|||
|
|
"Access Control": {
|
|||
|
|
"acl login <username> <password>",
|
|||
|
|
"acl logout",
|
|||
|
|
"acl grant <collection> <role> <permissions>",
|
|||
|
|
},
|
|||
|
|
"Transactions": {
|
|||
|
|
"db.startSession() - Start a new session",
|
|||
|
|
"session.startTransaction() - Begin a transaction",
|
|||
|
|
"session.commitTransaction() - Commit current transaction",
|
|||
|
|
"session.abortTransaction() - Abort/Rollback current transaction",
|
|||
|
|
},
|
|||
|
|
"HTTP API": {
|
|||
|
|
"GET /api/db/{db}/{coll}[/{id}] - Get document(s)",
|
|||
|
|
"POST /api/db/{db}/{coll} - Insert document",
|
|||
|
|
"PUT /api/db/{db}/{coll}/{id} - Update document",
|
|||
|
|
"DELETE /api/db/{db}/{coll}/{id} - Delete document",
|
|||
|
|
"GET /api/index/{db}/{coll}/list - List indexes",
|
|||
|
|
"POST /api/index/{db}/{coll}/create - Create index",
|
|||
|
|
"DELETE /api/index/{db}/{coll}/drop - Drop index",
|
|||
|
|
"POST /api/acl/user/{username} - Create user",
|
|||
|
|
"GET /api/acl/users - List users",
|
|||
|
|
"POST /api/acl/grant/{role}/{perm} - Grant permission",
|
|||
|
|
"GET /api/cluster/status - Cluster status",
|
|||
|
|
},
|
|||
|
|
"Cluster": {
|
|||
|
|
"status",
|
|||
|
|
"nodes",
|
|||
|
|
},
|
|||
|
|
"System": {
|
|||
|
|
"help",
|
|||
|
|
"clear",
|
|||
|
|
"quit",
|
|||
|
|
"exit",
|
|||
|
|
},
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for category, commands := range categories {
|
|||
|
|
utils.PrintInfo(fmt.Sprintf("\n%s:", category))
|
|||
|
|
for _, cmd := range commands {
|
|||
|
|
utils.Println(fmt.Sprintf(" %-40s %s", cmd, r.getCommandDescription(cmd)))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
utils.PrintInfo("")
|
|||
|
|
utils.PrintInfo("Permission flags for 'acl grant':")
|
|||
|
|
utils.PrintInfo(" r - read")
|
|||
|
|
utils.PrintInfo(" w - write")
|
|||
|
|
utils.PrintInfo(" d - delete")
|
|||
|
|
utils.PrintInfo(" a - admin")
|
|||
|
|
utils.PrintInfo(" Example: acl grant users admin rwa")
|
|||
|
|
utils.PrintInfo("")
|
|||
|
|
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) getCommandDescription(cmd string) string {
|
|||
|
|
// Ищем полное имя команды
|
|||
|
|
for name, command := range r.commands {
|
|||
|
|
if strings.HasPrefix(cmd, name) {
|
|||
|
|
return command.Description
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleClear(args []string) error {
|
|||
|
|
// Очистка экрана для разных ОС
|
|||
|
|
fmt.Print("\033[2J\033[H")
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (r *Repl) handleQuit(args []string) error {
|
|||
|
|
os.Exit(0)
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Close закрывает REPL и сохраняет историю
|
|||
|
|
func (r *Repl) Close() error {
|
|||
|
|
// Сохраняем историю в файл (опционально)
|
|||
|
|
if len(r.history) > 0 {
|
|||
|
|
// Здесь можно сохранить историю в файл
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|