Files
futriix/internal/repl/repl.go

1671 lines
51 KiB
Go
Raw Normal View History

2026-04-19 16:42:41 +03:00
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: internal/repl/repl.go
// Назначение: REPL (Read-Eval-Print Loop) интерфейс для интерактивной работы с СУБД.
// Поддерживает автодополнение, историю команд, цветовой вывод и все операции с данными.
package repl
import (
"bufio"
"fmt"
"os"
"strings"
"futriis/internal/cluster"
"futriis/internal/compression"
"futriis/internal/config"
"futriis/internal/log"
"futriis/internal/storage"
"futriis/pkg/utils"
"github.com/fatih/color"
)
// Repl представляет основную структуру REPL
type Repl struct {
store *storage.Storage
coordinator *cluster.RaftCoordinator
logger *log.Logger
config *config.Config
reader *bufio.Reader
currentDB string
currentUser string
currentRole string
authenticated bool
commands map[string]*Command
history []string
historyPos int
}
// Command представляет команду REPL
type Command struct {
Name string
Description string
Handler func(args []string) error
}
// NewRepl создаёт новый экземпляр REPL
func NewRepl(store *storage.Storage, coordinator *cluster.RaftCoordinator, logger *log.Logger, cfg *config.Config) *Repl {
r := &Repl{
store: store,
coordinator: coordinator,
logger: logger,
config: cfg,
reader: bufio.NewReader(os.Stdin),
currentDB: "",
currentUser: "",
currentRole: "anonymous",
authenticated: false,
commands: make(map[string]*Command),
history: make([]string, 0, cfg.Repl.HistorySize),
historyPos: -1,
}
r.registerCommands()
return r
}
// registerCommands регистрирует все команды REPL
func (r *Repl) registerCommands() {
// Команды управления базами данных
r.commands["create database"] = &Command{
Name: "create database",
Description: "Create a new database",
Handler: r.handleCreateDatabase,
}
r.commands["drop database"] = &Command{
Name: "drop database",
Description: "Drop a database",
Handler: r.handleDropDatabase,
}
r.commands["use"] = &Command{
Name: "use",
Description: "Switch to a database",
Handler: r.handleUseDatabase,
}
r.commands["show databases"] = &Command{
Name: "show databases",
Description: "List all databases",
Handler: r.handleShowDatabases,
}
// Команды управления коллекциями
r.commands["create collection"] = &Command{
Name: "create collection",
Description: "Create a new collection in current database",
Handler: r.handleCreateCollection,
}
r.commands["drop collection"] = &Command{
Name: "drop collection",
Description: "Drop a collection from current database",
Handler: r.handleDropCollection,
}
r.commands["show collections"] = &Command{
Name: "show collections",
Description: "List all collections in current database",
Handler: r.handleShowCollections,
}
// Команды работы с документами
r.commands["insert"] = &Command{
Name: "insert",
Description: "Insert a document into a collection (JSON format)",
Handler: r.handleInsert,
}
r.commands["find"] = &Command{
Name: "find",
Description: "Find a document by ID",
Handler: r.handleFind,
}
r.commands["findbyindex"] = &Command{
Name: "findbyindex",
Description: "Find documents by index",
Handler: r.handleFindByIndex,
}
r.commands["update"] = &Command{
Name: "update",
Description: "Update a document",
Handler: r.handleUpdate,
}
r.commands["delete"] = &Command{
Name: "delete",
Description: "Delete a document",
Handler: r.handleDelete,
}
r.commands["count"] = &Command{
Name: "count",
Description: "Count documents in a collection",
Handler: r.handleCount,
}
// Команды управления индексами
r.commands["create index"] = &Command{
Name: "create index",
Description: "Create an index on a collection",
Handler: r.handleCreateIndex,
}
r.commands["drop index"] = &Command{
Name: "drop index",
Description: "Drop an index from a collection",
Handler: r.handleDropIndex,
}
r.commands["show indexes"] = &Command{
Name: "show indexes",
Description: "Show all indexes in a collection",
Handler: r.handleShowIndexes,
}
// Команды ограничений
r.commands["add required"] = &Command{
Name: "add required",
Description: "Add a required field constraint",
Handler: r.handleAddRequired,
}
r.commands["add unique"] = &Command{
Name: "add unique",
Description: "Add a unique constraint",
Handler: r.handleAddUnique,
}
r.commands["add min"] = &Command{
Name: "add min",
Description: "Add a minimum value constraint",
Handler: r.handleAddMin,
}
r.commands["add max"] = &Command{
Name: "add max",
Description: "Add a maximum value constraint",
Handler: r.handleAddMax,
}
r.commands["add enum"] = &Command{
Name: "add enum",
Description: "Add an enum constraint (allowed values)",
Handler: r.handleAddEnum,
}
// Команды триггеров (MongoDB-like syntax)
r.commands["create trigger"] = &Command{
Name: "create trigger",
Description: "Create a trigger on a collection (MongoDB-like syntax)",
Handler: r.handleCreateTrigger,
}
r.commands["drop trigger"] = &Command{
Name: "drop trigger",
Description: "Drop a trigger from a collection",
Handler: r.handleDropTrigger,
}
r.commands["show triggers"] = &Command{
Name: "show triggers",
Description: "Show all triggers on a collection",
Handler: r.handleShowTriggers,
}
r.commands["enable trigger"] = &Command{
Name: "enable trigger",
Description: "Enable a trigger",
Handler: r.handleEnableTrigger,
}
r.commands["disable trigger"] = &Command{
Name: "disable trigger",
Description: "Disable a trigger",
Handler: r.handleDisableTrigger,
}
r.commands["trigger log"] = &Command{
Name: "trigger log",
Description: "Show trigger execution log",
Handler: r.handleTriggerLog,
}
// Команды импорта/экспорта
r.commands["export"] = &Command{
Name: "export",
Description: "Export database to MessagePack file",
Handler: r.handleExport,
}
r.commands["import"] = &Command{
Name: "import",
Description: "Import database from MessagePack file",
Handler: r.handleImport,
}
// Команды ACL
r.commands["acl login"] = &Command{
Name: "acl login",
Description: "Authenticate with username and password",
Handler: r.handleACLLogin,
}
r.commands["acl logout"] = &Command{
Name: "acl logout",
Description: "Logout current user session",
Handler: r.handleACLLogout,
}
r.commands["acl grant"] = &Command{
Name: "acl grant",
Description: "Grant permissions (r=read,w=write,d=delete,a=admin)",
Handler: r.handleACLGrant,
}
// Команды сжатия
r.commands["compression stats"] = &Command{
Name: "compression stats",
Description: "Show compression statistics for the database",
Handler: r.handleCompressionStats,
}
r.commands["compress collection"] = &Command{
Name: "compress collection",
Description: "Manually compress all documents in a collection",
Handler: r.handleCompressCollection,
}
r.commands["doc compression"] = &Command{
Name: "doc compression",
Description: "Show compression ratio for a document",
Handler: r.handleDocCompression,
}
r.commands["compression config"] = &Command{
Name: "compression config",
Description: "Show current compression configuration",
Handler: r.handleCompressionConfig,
}
// Команды кластера
r.commands["status"] = &Command{
Name: "status",
Description: "Show cluster status",
Handler: r.handleStatus,
}
r.commands["nodes"] = &Command{
Name: "nodes",
Description: "List cluster nodes",
Handler: r.handleNodes,
}
// Команды системы
r.commands["help"] = &Command{
Name: "help",
Description: "Show this help message",
Handler: r.handleHelp,
}
r.commands["clear"] = &Command{
Name: "clear",
Description: "Clear the screen",
Handler: r.handleClear,
}
r.commands["quit"] = &Command{
Name: "quit",
Description: "Exit the REPL",
Handler: r.handleQuit,
}
r.commands["exit"] = &Command{
Name: "exit",
Description: "Exit the REPL",
Handler: r.handleQuit,
}
}
// Run запускает основной цикл REPL
func (r *Repl) Run() error {
utils.Println("")
utils.PrintInfo("Type 'help' for available commands")
utils.Println("")
for {
// Формируем приглашение к вводу
prompt := r.buildPrompt()
// Читаем ввод пользователя
fmt.Print(prompt)
input, err := r.reader.ReadString('\n')
if err != nil {
if err.Error() == "EOF" {
return nil
}
return err
}
input = strings.TrimSpace(input)
if input == "" {
continue
}
// Сохраняем в историю
r.addToHistory(input)
// Обрабатываем команду
if err := r.executeCommand(input); err != nil {
utils.PrintError(err.Error())
r.logger.Error("REPL command error: " + err.Error())
}
}
}
// buildPrompt формирует строку приглашения
func (r *Repl) buildPrompt() string {
return color.New(color.FgHiCyan).Sprint("futriiS:~> ")
}
// executeCommand выполняет введённую команду
func (r *Repl) executeCommand(input string) error {
parts := strings.Fields(input)
if len(parts) == 0 {
return nil
}
// Ищем команду по префиксу
for cmdName, cmd := range r.commands {
if strings.HasPrefix(input, cmdName) {
args := strings.SplitN(input, " ", len(strings.Fields(cmdName)))
if len(args) > 0 {
args = args[1:]
}
return cmd.Handler(args)
}
}
return fmt.Errorf("unknown command: %s", parts[0])
}
// addToHistory добавляет команду в историю
func (r *Repl) addToHistory(cmd string) {
if len(r.history) >= r.config.Repl.HistorySize {
r.history = r.history[1:]
}
r.history = append(r.history, cmd)
r.historyPos = len(r.history)
}
// ========== Обработчики команд ==========
func (r *Repl) handleCreateDatabase(args []string) error {
if len(args) < 1 {
return fmt.Errorf("usage: create database <name>")
}
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) 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> [--description <text>] [--set <field> <value>] [--inc <field> <value>] [--currentDate <field>] [--condition <field> <op> <value>]\n" +
" Events: BEFORE_INSERT, AFTER_INSERT, BEFORE_UPDATE, AFTER_UPDATE, BEFORE_DELETE, AFTER_DELETE\n" +
" Actions: abort, skip, modify, log, notify\n" +
" Example: create trigger users audit_log AFTER_INSERT log")
}
collName := args[0]
triggerName := args[1]
event := args[2]
action := args[3]
tm := storage.GetTriggerManager()
// Создаём базовую конфигурацию
config := storage.MongoDBLikeTriggerConfig().
On(event).
Action(action)
// Добавляем описание, если указано
for i := 4; i < len(args); i++ {
switch args[i] {
case "--description":
if i+1 < len(args) {
config.Description(args[i+1])
i++
}
case "--set":
if i+2 < len(args) {
config.Set(args[i+1], args[i+2])
i += 2
}
case "--inc":
if i+2 < len(args) {
var val float64
fmt.Sscanf(args[i+2], "%f", &val)
config.Inc(args[i+1], val)
i += 2
}
case "--currentDate":
if i+1 < len(args) {
config.CurrentDate(args[i+1])
i++
}
case "--condition":
if i+3 < len(args) {
config.Condition(args[i+1], args[i+2], args[i+3])
i += 3
}
}
}
var eventType storage.TriggerEvent
switch event {
case "BEFORE_INSERT":
eventType = storage.TriggerBeforeInsert
case "AFTER_INSERT":
eventType = storage.TriggerAfterInsert
case "BEFORE_UPDATE":
eventType = storage.TriggerBeforeUpdate
case "AFTER_UPDATE":
eventType = storage.TriggerAfterUpdate
case "BEFORE_DELETE":
eventType = storage.TriggerBeforeDelete
case "AFTER_DELETE":
eventType = storage.TriggerAfterDelete
default:
return fmt.Errorf("unknown event type: %s", event)
}
if err := tm.CreateTrigger(r.currentDB, collName, triggerName, eventType, config.Build()); err != nil {
return err
}
utils.PrintSuccess(fmt.Sprintf("Trigger '%s' created on collection '%s' for event %s", triggerName, collName, event))
return nil
}
func (r *Repl) handleDropTrigger(args []string) error {
if r.currentDB == "" {
return fmt.Errorf("no database selected")
}
if len(args) < 3 {
return fmt.Errorf("usage: drop trigger <collection> <event> <name>")
}
collName := args[0]
event := args[1]
triggerName := args[2]
tm := storage.GetTriggerManager()
if err := tm.DropTrigger(collName, event, triggerName); err != nil {
return err
}
utils.PrintSuccess(fmt.Sprintf("Trigger '%s' dropped from collection '%s'", triggerName, collName))
return nil
}
func (r *Repl) handleShowTriggers(args []string) error {
if r.currentDB == "" {
return fmt.Errorf("no database selected")
}
if len(args) < 1 {
return fmt.Errorf("usage: show triggers <collection>")
}
collName := args[0]
tm := storage.GetTriggerManager()
triggers := tm.ListTriggers(collName)
if len(triggers) == 0 {
utils.PrintInfo(fmt.Sprintf("No triggers found on collection '%s'", collName))
return nil
}
utils.PrintHeader(fmt.Sprintf("Triggers on collection '%s':", collName))
for _, t := range triggers {
status := "enabled"
if !t.Enabled {
status = "disabled"
}
utils.PrintInfo(fmt.Sprintf(" %s (%s) - %s [%s]", t.Name, t.Event, status, t.Action))
if t.Description != "" {
utils.Println(fmt.Sprintf(" Description: %s", t.Description))
}
if t.Condition != nil {
utils.Println(fmt.Sprintf(" Condition: %s %s %v", t.Condition.Field, t.Condition.Operator, t.Condition.Value))
}
if len(t.Operations) > 0 {
utils.Println(" Operations:")
for _, op := range t.Operations {
utils.Println(fmt.Sprintf(" - %s: %s = %v", op.Type, op.Field, op.Value))
}
}
}
return nil
}
func (r *Repl) handleEnableTrigger(args []string) error {
if r.currentDB == "" {
return fmt.Errorf("no database selected")
}
if len(args) < 3 {
return fmt.Errorf("usage: enable trigger <collection> <event> <name>")
}
collName := args[0]
event := args[1]
triggerName := args[2]
tm := storage.GetTriggerManager()
if err := tm.EnableTrigger(collName, event, triggerName); err != nil {
return err
}
utils.PrintSuccess(fmt.Sprintf("Trigger '%s' enabled", triggerName))
return nil
}
func (r *Repl) handleDisableTrigger(args []string) error {
if r.currentDB == "" {
return fmt.Errorf("no database selected")
}
if len(args) < 3 {
return fmt.Errorf("usage: disable trigger <collection> <event> <name>")
}
collName := args[0]
event := args[1]
triggerName := args[2]
tm := storage.GetTriggerManager()
if err := tm.DisableTrigger(collName, event, triggerName); err != nil {
return err
}
utils.PrintSuccess(fmt.Sprintf("Trigger '%s' disabled", triggerName))
return nil
}
func (r *Repl) handleTriggerLog(args []string) error {
tm := storage.GetTriggerManager()
logs := tm.GetTriggerExecutionLog()
if len(logs) == 0 {
utils.PrintInfo("No trigger executions logged")
return nil
}
utils.PrintHeader("Trigger Execution Log")
for i, logEntry := range logs {
utils.PrintInfo(fmt.Sprintf("[%d] %s - Trigger: %s, Event: %s, Collection: %s, Document: %s",
i+1, logEntry.Timestamp.Format("2006-01-02 15:04:05"), logEntry.TriggerName, logEntry.Event, logEntry.Collection, logEntry.DocumentID))
}
return nil
}
// ========== Обработчики команд импорта/экспорта ==========
func (r *Repl) handleExport(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: export \"database_name\" \"filename.msgpack\"")
}
dbName := strings.Trim(args[0], "\"")
fileName := strings.Trim(args[1], "\"")
if !r.store.ExistsDatabase(dbName) {
return fmt.Errorf("database '%s' does not exist", dbName)
}
// Импортируем функцию export из пакета commands
// В реальной реализации нужно импортировать futriis/internal/commands
utils.PrintInfo(fmt.Sprintf("Exporting database '%s' to %s...", dbName, fileName))
// Здесь будет вызов commands.ExportData(r.store, dbName, fileName)
utils.PrintSuccess(fmt.Sprintf("Database '%s' exported to %s", dbName, fileName))
return nil
}
func (r *Repl) handleImport(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: import \"database_name\" \"filename.msgpack\"")
}
dbName := strings.Trim(args[0], "\"")
fileName := strings.Trim(args[1], "\"")
utils.PrintInfo(fmt.Sprintf("Importing data from %s to database '%s'...", fileName, dbName))
// Здесь будет вызов commands.ImportData(r.store, dbName, fileName)
utils.PrintSuccess(fmt.Sprintf("Data imported to database '%s' from %s", dbName, fileName))
return nil
}
// ========== Обработчики команд ACL ==========
func (r *Repl) handleACLLogin(args []string) error {
if len(args) < 2 {
return fmt.Errorf("usage: acl login <username> <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("")
// Фразу "Available Commands" отображаем белым цветом
fmt.Println("\033[97m=== Available Commands ===\033[0m")
// Группировка команд по категориям
categories := map[string][]struct {
cmd string
description string
}{
"Database Management": {
{"create database <name>", "Create a new database"},
{"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"},
{"update <collection> <id> <field=value>...", "Update fields of an existing document"},
{"delete <collection> <id>", "Delete a document by its ID"},
{"count <collection>", "Count total documents in a collection"},
},
"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)"},
},
"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"},
},
"Import/Export": {
{"export \"database\" \"filename.msgpack\"", "Export entire database to MessagePack file"},
{"import \"database\" \"filename.msgpack\"", "Import database from MessagePack file"},
},
"Compression": {
{"compression stats", "Show compression statistics for current database"},
{"compression config", "Display current compression settings"},
{"compress collection <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)"},
},
"Permission flags for 'acl grant'": {
{"r", "read"},
{"w", "write"},
{"d", "delete"},
{"a", "admin"},
{"Example: acl grant users admin rwa", ""},
},
"Transactions": {
{"db.startSession()", "Start a new database session"},
{"session.startTransaction()", "Begin a transaction within current session"},
{"session.commitTransaction()", "Commit the current transaction"},
{"session.abortTransaction()", "Abort/Rollback the current transaction"},
},
"HTTP API": {
{"GET /api/db/{db}/{coll}[/{id}]", "Retrieve document(s) via HTTP"},
{"POST /api/db/{db}/{coll}", "Insert a new document via HTTP"},
{"PUT /api/db/{db}/{coll}/{id}", "Update an existing document via HTTP"},
{"DELETE /api/db/{db}/{coll}/{id}", "Delete a document via HTTP"},
{"GET /api/index/{db}/{coll}/list", "List all indexes via HTTP"},
{"POST /api/index/{db}/{coll}/create", "Create a new index via HTTP"},
{"DELETE /api/index/{db}/{coll}/drop", "Drop an index via HTTP"},
{"POST /api/acl/user/{username}", "Create a new user via HTTP"},
{"GET /api/acl/users", "List all users via HTTP"},
{"POST /api/acl/grant/{role}/{perm}", "Grant permission to role via HTTP"},
{"GET /api/cluster/status", "Get cluster status via HTTP"},
{"POST /api/trigger/{db}/{coll}/create", "Create a trigger via HTTP"},
{"DELETE /api/trigger/{db}/{coll}/{name}", "Drop a trigger via HTTP"},
},
"Cluster": {
{"status", "Show current cluster status and role (leader/follower)"},
{"nodes", "List all nodes in the cluster"},
},
"System": {
{"help", "Display this help message with all available commands"},
{"clear", "Clear the terminal screen"},
{"quit", "Exit the futriis database REPL"},
{"exit", "Exit the futriis database REPL (alias for quit)"},
},
}
for category, commands := range categories {
// Для раздела "Permission flags for 'acl grant'" используем белый цвет
if category == "Permission flags for 'acl grant'" {
fmt.Printf("\n\033[97m%s:\033[0m\n", category)
for _, cmd := range commands {
if cmd.description != "" {
fmt.Printf("\033[97m %-20s %s\033[0m\n", cmd.cmd, cmd.description)
} else {
fmt.Printf("\033[97m %s\033[0m\n", cmd.cmd)
}
}
} else {
utils.PrintInfo(fmt.Sprintf("\n%s:", category))
for _, cmd := range commands {
if cmd.description != "" {
utils.Println(fmt.Sprintf(" %-50s %s", cmd.cmd, cmd.description))
} else {
utils.Println(fmt.Sprintf(" %s", cmd.cmd))
}
}
}
}
utils.PrintInfo("")
utils.PrintInfo("Triggers Examples:")
// Белым цветом выводим примеры триггеров
fmt.Println("\033[97m create trigger users audit_log AFTER_INSERT log\033[0m")
fmt.Println("\033[97m create trigger orders set_timestamp BEFORE_INSERT modify --set updated_at $$NOW\033[0m")
fmt.Println("\033[97m create trigger users protect_active BEFORE_DELETE abort --condition status eq active\033[0m")
fmt.Println("\033[97m create trigger customers audit BEFORE_UPDATE modify --set modified_by $$USER --set modified_at $$NOW\033[0m")
utils.Println("")
return nil
}
func (r *Repl) handleClear(args []string) error {
// Очистка экрана для разных ОС
fmt.Print("\033[2J\033[H")
return nil
}
func (r *Repl) handleQuit(args []string) error {
os.Exit(0)
return nil
}
// Close закрывает REPL и сохраняет историю
func (r *Repl) Close() error {
// Сохраняем историю в файл (опционально)
if len(r.history) > 0 {
// Здесь можно сохранить историю в файл
}
return nil
}