Files
futriix/internal/repl/repl.go
2026-04-19 16:42:41 +03:00

1671 lines
51 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* Copyright 2026 Safronov Grigorii
*
* Licensed under the CDDL, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
*
* You may obtain a copy of the License at
* https://opensource.org/licenses/CDDL-1.0
*/
// Файл: 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
}