Files
futriis/internal/repl/repl.go
2026-04-08 21:43:35 +03:00

1327 lines
37 KiB
Go
Raw Permalink 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.
// Файл: 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
}