flusql/src/cli.rs

1820 lines
85 KiB
Rust
Raw Normal View History

2026-01-14 20:58:37 +00:00
// cli.rs - модуль реализующий REPL-интерфейс в проекте fluSQL
// Инициализирует два режима работв: 'lua' и 'sql'
use std::collections::VecDeque;
use std::fs::File;
use std::io::{self, Write, Read, BufReader, BufWriter, BufRead};
use std::path::Path;
use unicode_width::UnicodeWidthStr;
use crate::cluster::ClusterManager;
use crate::history::CommandHistory;
use crate::lua::LuaInterpreter;
use crate::lua_mode::LuaModeContext;
use crate::plugins::{PluginManager, PluginConfig};
use crate::utils::config::Config;
use std::sync::Arc;
/// Проверка поддержки цветного вывода в терминале
fn supports_color() -> bool {
// Проверяем переменную окружения TERM
if let Ok(term) = std::env::var("TERM") {
// Поддерживаемые терминалы для всех дистрибутивов Linux
let supported_terms = [
"xterm", "xterm-256color", "screen", "screen-256color", "tmux", "tmux-256color",
"rxvt", "rxvt-unicode", "linux", "vt100", "vt220", "ansi", "dumb"
];
for supported in &supported_terms {
if term.contains(supported) {
return true;
}
}
}
// Проверяем наличие переменной COLORTERM (используется в современных дистрибутивах)
if std::env::var("COLORTERM").is_ok() {
return true;
}
// Проверяем, является ли stdout tty
if atty::is(atty::Stream::Stdout) {
// Дополнительные проверки для разных дистрибутивов
if cfg!(target_os = "linux") {
// Проверяем наличие известных окружений рабочего стола
let desktop_env = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
let session = std::env::var("DESKTOP_SESSION").unwrap_or_default();
// Поддерживаемые окружения
let supported_desktops = [
"GNOME", "KDE", "XFCE", "LXDE", "LXQt", "MATE", "Cinnamon", "Pantheon",
"Deepin", "Budgie", "Unity", "i3", "awesome", "dwm", "bspwm", "herbstluftwm"
];
for desktop in &supported_desktops {
if desktop_env.contains(desktop) || session.contains(desktop) {
return true;
}
}
}
}
false
}
/// Получение кодов цвета для текущего терминала
fn get_color_codes() -> ColorCodes {
if supports_color() {
ColorCodes {
reset: "\x1b[0m",
bold: "\x1b[1m",
faint: "\x1b[2m",
italic: "\x1b[3m",
underline: "\x1b[4m",
blink: "\x1b[5m",
// Основные цвета
black: "\x1b[30m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
// Яркие цвета
bright_black: "\x1b[90m",
bright_red: "\x1b[91m",
bright_green: "\x1b[92m",
bright_yellow: "\x1b[93m",
bright_blue: "\x1b[94m",
bright_magenta: "\x1b[95m",
bright_cyan: "\x1b[96m",
bright_white: "\x1b[97m",
// Фоны
bg_black: "\x1b[40m",
bg_red: "\x1b[41m",
bg_green: "\x1b[42m",
bg_yellow: "\x1b[43m",
bg_blue: "\x1b[44m",
bg_magenta: "\x1b[45m",
bg_cyan: "\x1b[46m",
bg_white: "\x1b[47m",
// Яркие фоны
bg_bright_black: "\x1b[100m",
bg_bright_red: "\x1b[101m",
bg_bright_green: "\x1b[102m",
bg_bright_yellow: "\x1b[103m",
bg_bright_blue: "\x1b[104m",
bg_bright_magenta: "\x1b[105m",
bg_bright_cyan: "\x1b[106m",
bg_bright_white: "\x1b[107m",
// ОСНОВНОЙ ЦВЕТ #00bfff для всего интерфейса flusql
// True color: RGB(0, 191, 255) - яркий голубой/синий
// 256-color: код 39 (яркий голубой) - ближайший к #00bfff
header_color: "\x1b[38;5;39m", // #00bfff для 256-цветных терминалов
header_color_true: "\x1b[38;2;0;191;255m", // #00bfff для true color терминалов
success_color: "\x1b[38;5;39m", // #00bfff для успешных операций
warning_color: "\x1b[38;5;214m", // Оранжевый
error_color: "\x1b[38;5;196m", // Красный
info_color: "\x1b[38;5;39m", // #00bfff для информационных сообщений
accent_color: "\x1b[38;5;39m", // #00bfff как акцентный цвет
// Специальные коды для отладки
test_true_color: "\x1b[38;2;0;191;255m",
test_256_color_39: "\x1b[38;5;39m",
test_256_color_38: "\x1b[38;5;38m",
test_256_color_33: "\x1b[38;5;33m",
}
} else {
ColorCodes::no_color()
}
}
/// Коды цветов для терминала
#[derive(Clone)]
struct ColorCodes {
reset: &'static str,
bold: &'static str,
faint: &'static str,
italic: &'static str,
underline: &'static str,
blink: &'static str,
black: &'static str,
red: &'static str,
green: &'static str,
yellow: &'static str,
blue: &'static str,
magenta: &'static str,
cyan: &'static str,
white: &'static str,
bright_black: &'static str,
bright_red: &'static str,
bright_green: &'static str,
bright_yellow: &'static str,
bright_blue: &'static str,
bright_magenta: &'static str,
bright_cyan: &'static str,
bright_white: &'static str,
bg_black: &'static str,
bg_red: &'static str,
bg_green: &'static str,
bg_yellow: &'static str,
bg_blue: &'static str,
bg_magenta: &'static str,
bg_cyan: &'static str,
bg_white: &'static str,
bg_bright_black: &'static str,
bg_bright_red: &'static str,
bg_bright_green: &'static str,
bg_bright_yellow: &'static str,
bg_bright_blue: &'static str,
bg_bright_magenta: &'static str,
bg_bright_cyan: &'static str,
bg_bright_white: &'static str,
// Основной цвет проекта flusql - #00bfff (яркий голубой/синий)
header_color: &'static str,
header_color_true: &'static str,
success_color: &'static str,
warning_color: &'static str,
error_color: &'static str,
info_color: &'static str,
accent_color: &'static str,
// Коды для тестирования
test_true_color: &'static str,
test_256_color_39: &'static str,
test_256_color_38: &'static str,
test_256_color_33: &'static str,
}
impl ColorCodes {
fn no_color() -> Self {
Self {
reset: "",
bold: "",
faint: "",
italic: "",
underline: "",
blink: "",
black: "",
red: "",
green: "",
yellow: "",
blue: "",
magenta: "",
cyan: "",
white: "",
bright_black: "",
bright_red: "",
bright_green: "",
bright_yellow: "",
bright_blue: "",
bright_magenta: "",
bright_cyan: "",
bright_white: "",
bg_black: "",
bg_red: "",
bg_green: "",
bg_yellow: "",
bg_blue: "",
bg_magenta: "",
bg_cyan: "",
bg_white: "",
bg_bright_black: "",
bg_bright_red: "",
bg_bright_green: "",
bg_bright_yellow: "",
bg_bright_blue: "",
bg_bright_magenta: "",
bg_bright_cyan: "",
bg_bright_white: "",
header_color: "",
header_color_true: "",
success_color: "",
warning_color: "",
error_color: "",
info_color: "",
accent_color: "",
test_true_color: "",
test_256_color_39: "",
test_256_color_38: "",
test_256_color_33: "",
}
}
/// Прямая раскраска текста указанным кодом
fn colorize(&self, text: &str, color: &str) -> String {
if color.is_empty() {
text.to_string()
} else {
format!("{}{}{}", color, text, self.reset)
}
}
/// Основной цвет flusql - #00bfff для заголовков
fn header(&self, text: &str) -> String {
self.apply_primary_color(text, self.header_color_true, self.header_color)
}
/// Успешные операций - цвет #00bfff
fn success(&self, text: &str) -> String {
self.apply_primary_color(text, self.header_color_true, self.success_color)
}
/// Предупреждения - оранжевый
fn warning(&self, text: &str) -> String {
self.colorize(text, self.warning_color)
}
/// Ошибки - красный
fn error(&self, text: &str) -> String {
self.colorize(text, self.error_color)
}
/// Информационные сообщения - цвет #00bfff (заменяет cyan)
fn info(&self, text: &str) -> String {
self.apply_primary_color(text, self.header_color_true, self.info_color)
}
/// Акцентный цвет - #00bfff (заменяет cyan)
fn accent(&self, text: &str) -> String {
self.apply_primary_color(text, self.header_color_true, self.accent_color)
}
/// Жирный текст
fn bold(&self, text: &str) -> String {
self.colorize(text, self.bold)
}
/// Подчеркнутый текст
fn underline(&self, text: &str) -> String {
self.colorize(text, self.underline)
}
/// Основной метод применения цвета #00bfff
fn apply_primary_color(&self, text: &str, true_color: &str, fallback_256: &str) -> String {
if !supports_color() {
return text.to_string();
}
if self.supports_true_color() {
self.colorize(text, true_color)
} else if self.supports_256_colors() {
self.colorize(text, fallback_256)
} else {
// Для 8-цветных терминалов используем стандартный cyan (ближайший к #00bfff)
self.colorize(text, self.cyan)
}
}
/// Проверка поддержки true color (24-bit цветов)
fn supports_true_color(&self) -> bool {
if !supports_color() {
return false;
}
// Проверяем переменные окружения для поддержки true color
if let Ok(colorterm) = std::env::var("COLORTERM") {
if colorterm.contains("truecolor") || colorterm.contains("24bit") {
return true;
}
}
// Проверяем переменную TERM для современных терминалов
if let Ok(term) = std::env::var("TERM") {
let truecolor_terms = [
"xterm-truecolor",
"xterm-256color", // Многие современные терминалы с 256 цветами поддерживают true color
"screen-truecolor",
"screen-256color",
"tmux-truecolor",
"tmux-256color",
"rxvt-unicode-256color",
"alacritty",
"kitty",
"wezterm",
"foot",
"konsole",
"gnome-terminal",
"terminology",
];
for truecolor_term in &truecolor_terms {
if term.contains(truecolor_term) {
return true;
}
}
}
false
}
/// Проверка поддержки 256 цветов
fn supports_256_colors(&self) -> bool {
if !supports_color() {
return false;
}
if let Ok(term) = std::env::var("TERM") {
term.contains("256color")
} else {
false
}
}
/// Тестирование цветов (для отладки)
fn test_colors(&self) -> String {
let mut result = String::new();
result.push_str("\n=== ТЕСТИРОВАНИЕ ЦВЕТОВ flusql ===\n");
result.push_str(&format!("True color #00bfff: {}Это цвет #00bfff (RGB: 0, 191, 255){}\n",
self.test_true_color, self.reset));
result.push_str(&format!("256-color код 39: {}Это код 39 (яркий голубой){}\n",
self.test_256_color_39, self.reset));
result.push_str(&format!("256-color код 38: {}Это код 38 (голубой){}\n",
self.test_256_color_38, self.reset));
result.push_str(&format!("256-color код 33: {}Это код 33 (темно-голубой){}\n",
self.test_256_color_33, self.reset));
result.push_str(&format!("Стандартный cyan: {}Это стандартный cyan (ближайший к #00bfff){}\n",
self.cyan, self.reset));
result.push_str(&format!("Стандартный синий: {}Это стандартный синий{}\n",
self.blue, self.reset));
result.push_str("===================================\n");
result
}
}
/// Получить ширину строки в терминале (учитывает Unicode символы)
fn display_width(s: &str) -> usize {
UnicodeWidthStr::width(s)
}
/// Форматирование таблицы с использованием непрерывных линий
fn format_table(headers: &[String], rows: &[Vec<String>]) -> String {
if rows.is_empty() {
return String::from("Нет данных");
}
let colors = get_color_codes();
// Рассчитываем ширину колонок с учетом отображения Unicode
let mut col_widths: Vec<usize> = headers.iter().map(|h| display_width(h)).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
if i < col_widths.len() {
col_widths[i] = col_widths[i].max(display_width(cell));
}
}
}
// Добавляем отступы
for width in &mut col_widths {
*width += 2; // По 1 пробелу с каждой стороны
}
let mut result = String::new();
// Верхняя граница
result.push('┌');
for (i, &width) in col_widths.iter().enumerate() {
for _ in 0..width {
result.push('─');
}
if i < col_widths.len() - 1 {
result.push('┬');
}
}
result.push_str("\n");
// Заголовки
result.push('│');
for (i, (header, &width)) in headers.iter().zip(col_widths.iter()).enumerate() {
let header_width = display_width(header);
let padding = width - header_width;
let left_pad = padding / 2;
let right_pad = padding - left_pad;
result.push_str(&format!("{}{}{}",
" ".repeat(left_pad),
colors.bold(header),
" ".repeat(right_pad)));
result.push('│');
}
result.push('\n');
// Разделитель заголовков
result.push('├');
for (i, &width) in col_widths.iter().enumerate() {
for _ in 0..width {
result.push('─');
}
if i < col_widths.len() - 1 {
result.push('┼');
}
}
result.push_str("\n");
// Данные
for (row_idx, row) in rows.iter().enumerate() {
result.push('│');
for (i, (cell, &width)) in row.iter().zip(col_widths.iter()).enumerate() {
let cell_width = display_width(cell);
let padding = width - cell_width;
// Для первой колонки - выравнивание по левому краю, для остальных - по центру
if i == 0 {
result.push_str(&format!(" {}{}{}",
cell,
" ".repeat(padding - 1),
""));
} else {
let left_pad = padding / 2;
let right_pad = padding - left_pad;
result.push_str(&format!("{}{}{}",
" ".repeat(left_pad),
cell,
" ".repeat(right_pad)));
}
result.push('│');
}
result.push('\n');
// Добавляем горизонтальные линии между строками (кроме последней)
if row_idx < rows.len() - 1 {
result.push('├');
for (i, &width) in col_widths.iter().enumerate() {
for _ in 0..width {
result.push('─');
}
if i < col_widths.len() - 1 {
result.push('┼');
}
}
result.push_str("\n");
}
}
// Нижняя граница
result.push('└');
for (i, &width) in col_widths.iter().enumerate() {
for _ in 0..width {
result.push('─');
}
if i < col_widths.len() - 1 {
result.push('┴');
}
}
result.push('┘');
result
}
/// Форматирование таблицы с одной колонкой
fn format_single_column_table(items: &[String]) -> String {
if items.is_empty() {
return String::new();
}
let colors = get_color_codes();
// Находим максимальную ширину строки с учетом Unicode
let max_width = items.iter().map(|item| display_width(item)).max().unwrap_or(0);
// Ширина колонки
let col_width = max_width + 2; // +2 для пробелов с каждой стороны
let mut result = String::new();
// Верхняя граница
result.push('┌');
for _ in 0..col_width {
result.push('─');
}
result.push('┐');
result.push('\n');
// Элементы
for (item_idx, item) in items.iter().enumerate() {
result.push('│');
result.push(' ');
// Разделяем на маркер и текст
let (marker, text) = if let Some(stripped) = item.strip_prefix("") {
("", stripped)
} else {
("", item.as_str())
};
// Используем цвет #00bfff для всех информационных сообщений (заменяет cyan)
result.push_str(&colors.info(marker));
result.push_str(text);
result.push_str(colors.reset);
// Заполняем оставшееся пространство пробелами с учетом ширины в символы
let item_width = display_width(item);
let spaces_needed = col_width - item_width - 1; // -1 для пробела в начале
if spaces_needed > 0 {
result.push_str(&" ".repeat(spaces_needed));
}
result.push('│');
result.push('\n');
// Добавляем горизонтальные линии между строками (кроме последней)
if item_idx < items.len() - 1 {
result.push('├');
for _ in 0..col_width {
result.push('─');
}
result.push('┤');
result.push('\n');
}
}
// Нижняя граница
result.push('└');
for _ in 0..col_width {
result.push('─');
}
result.push('┘');
result.push('\n');
result
}
/// Показать справку в виде таблицы с горизонтальными линиями
fn show_help_table() -> String {
let colors = get_color_codes();
let mut output = String::new();
// Таблица команд
let headers = vec!["Команда".to_string(), "Описание".to_string(), "Пример".to_string()];
let rows = vec![
vec!["CREATE DATABASE".to_string(), "Создать базу данных".to_string(), "CREATE DATABASE mydb;".to_string()],
vec!["DROP DATABASE".to_string(), "Удалить базу данных".to_string(), "DROP DATABASE mydb;".to_string()],
vec!["SHOW DATABASES".to_string(), "Показать список баз данных".to_string(), "SHOW DATABASES;".to_string()],
vec!["\\dt".to_string(), "Показать таблицы в текущей БД".to_string(), "\\dt;".to_string()],
vec!["\\d таблица".to_string(), "Описание структуры таблицы".to_string(), "\\d users;".to_string()],
vec!["USE".to_string(), "Использовать базу данных".to_string(), "USE mydb;".to_string()],
vec!["CREATE TABLE".to_string(), "Создать таблицу".to_string(), "CREATE TABLE users (id INT, name VARCHAR(255));".to_string()],
vec!["ALTER TABLE".to_string(), "Изменить таблицу".to_string(), "ALTER TABLE users ADD COLUMN age INT;".to_string()],
vec!["DROP TABLE".to_string(), "Удалить таблицу".to_string(), "DROP TABLE users;".to_string()],
vec!["DROP TABLE CASCADE".to_string(), "Удалить таблицу с зависимостями".to_string(), "DROP TABLE users CASCADE;".to_string()],
vec!["SELECT".to_string(), "Выбрать данные".to_string(), "SELECT * FROM users;".to_string()],
vec!["INSERT".to_string(), "Вставить данные".to_string(), "INSERT INTO users (id, name) VALUES (1, 'Alice');".to_string()],
vec!["UPDATE".to_string(), "Обновить данные".to_string(), "UPDATE users SET age = 30 WHERE id = 1;".to_string()],
vec!["DELETE".to_string(), "Удалить данные".to_string(), "DELETE FROM users WHERE id = 1;".to_string()],
vec!["CREATE INDEX".to_string(), "Создать индекс".to_string(), "CREATE INDEX idx_users_id ON users (id);".to_string()],
vec!["DROP INDEX".to_string(), "Удалить индекс".to_string(), "DROP INDEX idx_users_id;".to_string()],
vec!["CREATE TRIGGER".to_string(), "Создать триггер".to_string(), "CREATE TRIGGER check_age BEFORE INSERT ON users FOR EACH ROW EXECUTE FUNCTION check_age();".to_string()],
vec!["DROP TRIGGER".to_string(), "Удалить триггер".to_string(), "DROP TRIGGER check_age ON users;".to_string()],
vec!["EXPLAIN".to_string(), "Объяснить план выполнения".to_string(), "EXPLAIN SELECT * FROM users;".to_string()],
vec!["EXPLAIN ANALYZE".to_string(), "Выполнить и объяснить запрос".to_string(), "EXPLAIN ANALYZE SELECT * FROM users;".to_string()],
vec!["BEGIN".to_string(), "Начать транзакцию".to_string(), "BEGIN;".to_string()],
vec!["COMMIT".to_string(), "Зафиксировать транзакцию".to_string(), "COMMIT;".to_string()],
vec!["ROLLBACK".to_string(), "Откатить транзакцию".to_string(), "ROLLBACK;".to_string()],
vec!["COPY TO".to_string(), "Экспорт в CSV".to_string(), "COPY users TO 'users.csv' WITH CSV HEADER;".to_string()],
vec!["COPY FROM".to_string(), "Импорт из CSV".to_string(), "COPY users FROM 'users.csv' WITH CSV HEADER;".to_string()],
vec!["CREATE SEQUENCE".to_string(), "Создать последовательность".to_string(), "CREATE SEQUENCE users_id_seq;".to_string()],
vec!["CREATE TYPE".to_string(), "Создать составной тип".to_string(), "CREATE TYPE address AS (street TEXT, city TEXT);".to_string()],
vec!["CREATE VIEW".to_string(), "Создать представление".to_string(), "CREATE VIEW user_emails AS SELECT id, email FROM users;".to_string()],
vec!["HISTORY".to_string(), "Показать историю команд".to_string(), "HISTORY;".to_string()],
vec!["HISTORY CLEAR".to_string(), "Очистить историю команд".to_string(), "HISTORY CLEAR;".to_string()],
vec!["HISTORY EXPORT".to_string(), "Экспорт истории в файл".to_string(), "HISTORY EXPORT 'history.txt';".to_string()],
vec!["!!".to_string(), "Повторить последнюю команду".to_string(), "!!".to_string()],
vec!["!n".to_string(), "Выполнить команду из истории по номеру".to_string(), "!3".to_string()],
vec!["HELP".to_string(), "Показать справку".to_string(), "HELP;".to_string()],
vec!["EXIT".to_string(), "Выйти из программы".to_string(), "EXIT;".to_string()],
vec!["QUIT".to_string(), "Выйти из программы".to_string(), "QUIT;".to_string()],
vec!["lua.mode".to_string(), "Войти в Lua режим".to_string(), "lua.mode".to_string()],
vec!["plugins list".to_string(), "Список загруженных плагинов".to_string(), "plugins list".to_string()],
vec!["plugin load".to_string(), "Загрузить плагин".to_string(), "plugin load './plugins/example.lua'".to_string()],
vec!["plugin unload".to_string(), "Выгрузить плагин".to_string(), "plugin unload 'plugin_id'".to_string()],
vec!["plugins reload".to_string(), "Перезагрузить все плагины".to_string(), "plugins reload".to_string()],
];
// Команды управления кластером в Lua режиме
let cluster_headers = vec!["Функция Lua".to_string(), "Описание".to_string()];
let cluster_rows = vec![
vec!["cluster.get_status()".to_string(), "Получить полный статус кластера".to_string()],
vec!["cluster.coord_status()".to_string(), "Показать IP-адрес текущего координатора".to_string()],
vec!["cluster.add_node(node_id, address)".to_string(), "Добавить узел".to_string()],
vec!["cluster.evict(node_id)".to_string(), "Исключить узел из кластера".to_string()],
vec!["cluster.elect_coordinator()".to_string(), "Выбрать нового координатора".to_string()],
vec!["cluster.rebalance()".to_string(), "Ребалансировать кластер".to_string()],
vec!["cluster.add_shard(shard_id, master, slaves)".to_string(), "Добавить шард".to_string()],
vec!["cluster.remove_shard(shard_id)".to_string(), "Удалить шард".to_string()],
vec!["cluster.start_replication(source, target)".to_string(), "Начать репликацию".to_string()],
];
// Команды плагинов в Lua режиме
let plugin_headers = vec!["Функция Lua".to_string(), "Описание".to_string()];
let plugin_rows = vec![
vec!["plugins.list()".to_string(), "Список загруженных плагинов".to_string()],
vec!["plugins.get(plugin_id)".to_string(), "Получить информацию о плагине".to_string()],
vec!["plugins.reload()".to_string(), "Перезагрузить все плагины".to_string()],
vec!["plugins.emit_event(event_name, data)".to_string(), "Отправить событие плагинам".to_string()],
];
// Тестирование цветов (можно отключить)
// output.push_str(&colors.test_colors());
// Форматируем основную таблицу команд
output.push_str(&format!("\n{}\n", colors.header("ОСНОВНЫЕ КОМАНДЫ:")));
output.push_str(&format_table(&headers, &rows));
// Команды управления кластера в Lua режиме
output.push_str(&format!("\n{}\n", colors.header("КОМАНДЫ УПРАВЛЕНИЯ КЛАСТЕРОМ (только в Lua режиме):")));
output.push_str(&format!("{}\n", colors.info("1. Введите команду 'lua.mode' для входа в Lua режим")));
output.push_str(&format!("{}\n", colors.info("2. В Lua режиме используйте функции:")));
output.push_str(&format_table(&cluster_headers, &cluster_rows));
// Команды плагинов в Lua режиме
output.push_str(&format!("\n{}\n", colors.header("ПЛАГИНЫ (только в Lua режиме):")));
output.push_str(&format!("{}\n", colors.info("1. Введите команду 'lua.mode' для входа в Lua режим")));
output.push_str(&format!("{}\n", colors.info("2. В Lua режиме используйте функции:")));
output.push_str(&format_table(&plugin_headers, &plugin_rows));
// Автоматические перевыборы координатора
output.push_str(&format!("\n{}\n", colors.header("АВТОМАТИЧЕСКИЕ ПЕРЕВЫБОРЫ КООРДИНАТОРА:")));
output.push_str(&format_single_column_table(&[
"• Координатор автоматически перевыбирается при его отказе".to_string(),
"• Новый координатор выбирается среди онлайн узлов".to_string(),
"• Критерий выбора: узел с наибольшим количеством шардов".to_string(),
"• Heartbeat проверки каждые 15 секунд".to_string(),
"• Timeout координатора: 30 секунд без heartbeat".to_string(),
]));
// История команд
output.push_str(&format!("\n{}\n", colors.header("ИСТОРИЯ КОМАНД:")));
output.push_str(&format_single_column_table(&[
"• История автоматически сохраняется между сессиями".to_string(),
"• Используйте стрелки вверх/вниз для навигации по истории".to_string(),
"• !! - повторить последнюю команду".to_string(),
"• !n - выполнить команду с номером n из истории".to_string(),
"• HISTORY - показать всю историю команд".to_string(),
"• HISTORY CLEAR - очистить историю".to_string(),
"• HISTORY EXPORT 'file' - экспортировать историю в файл".to_string(),
]));
// Система плагинов
output.push_str(&format!("\n{}\n", colors.header("СИСТЕМА ПЛАГИНОВ:")));
output.push_str(&format_single_column_table(&[
"• Плагины пишутся на Lua и загружаются из директории 'plugins'".to_string(),
"• Поддержка изоляции (sandbox) для безопасности".to_string(),
"• Система событий и триггеров".to_string(),
"• Зависимости между плагинами".to_string(),
"• plugins list - список плагинов".to_string(),
"• plugin load 'path' - загрузить плагин".to_string(),
"• plugin unload 'id' - выгрузить плагин".to_string(),
"• plugins reload - перезагрузить все плагины".to_string(),
]));
// Конфигурация
output.push_str(&format!("\n{}\n", colors.header("КОНФИГУРАЦИЯ:")));
output.push_str(&format_single_column_table(&[
"• Конфигурация загружается из config.toml".to_string(),
"• Настройки сервера: порт, хост, пул потоков".to_string(),
"• Настройки БД: размер страницы, кэш, WAL, MVCC".to_string(),
"• Настройки плагинов: директория, sandbox, таймауты".to_string(),
"• Настройки сети и безопасности".to_string(),
"• Переменная окружения FLUSQL_CONFIG для указания пути к конфигу".to_string(),
]));
// Информация о поддержке цветов
if supports_color() {
if get_color_codes().supports_true_color() {
output.push_str(&format!("\n{} {} {}\n", colors.header("ЦВЕТНОЙ ВЫВОД:"), colors.success("активирован (true color)"), colors.reset));
} else if get_color_codes().supports_256_colors() {
output.push_str(&format!("\n{} {} {}\n", colors.header("ЦВЕТНОЙ ВЫВОД:"), colors.success("активирован (256 цветов)"), colors.reset));
} else {
output.push_str(&format!("\n{} {} {}\n", colors.header("ЦВЕТНОЙ ВЫВОД:"), colors.success("активирован (8 цветов)"), colors.reset));
}
} else {
output.push_str(&format!("\n{} {} {}\n", colors.header("ЦВЕТНОЙ ВЫВОД:"), colors.warning("недоступен"), colors.reset));
}
output
}
/// Показать справку для Lua режима
fn show_lua_help_table() -> String {
let colors = get_color_codes();
let mut output = String::new();
output.push_str(&format!("\n{}\n", colors.header("КОМАНДЫ LUA РЕЖИМА:")));
// Основные команды Lua режима
let main_headers = vec!["Команда".to_string(), "Описание".to_string()];
let main_rows = vec![
vec!["sql.mode".to_string(), "Вернуться в SQL режим".to_string()],
vec!["exit / quit".to_string(), "Выйти из Lua режима".to_string()],
vec!["help".to_string(), "Показать эту справку".to_string()],
vec!["history".to_string(), "Показать историю Lua команд".to_string()],
vec!["clear-history".to_string(), "Очистить историю Lua команд".to_string()],
];
output.push_str(&format_table(&main_headers, &main_rows));
// Функции кластера
output.push_str(&format!("\n{}\n", colors.header("ФУНКЦИИ КЛАСТЕРА:")));
let cluster_headers = vec!["Функция Lua".to_string(), "Описание".to_string()];
let cluster_rows = vec![
vec!["cluster.get_status()".to_string(), "Получить полный статус кластера".to_string()],
vec!["cluster.coord_status()".to_string(), "Показать IP-адрес текущего координатора".to_string()],
vec!["cluster.add_node(node_id, address)".to_string(), "Добавить узел".to_string()],
vec!["cluster.evict(node_id)".to_string(), "Исключить узел из кластера".to_string()],
vec!["cluster.elect_coordinator()".to_string(), "Выбрать нового координатора".to_string()],
vec!["cluster.rebalance()".to_string(), "Ребалансировать кластер".to_string()],
vec!["cluster.add_shard(shard_id, master, slaves)".to_string(), "Добавить шард".to_string()],
vec!["cluster.remove_shard(shard_id)".to_string(), "Удалить шард".to_string()],
vec!["cluster.start_replication(source, target)".to_string(), "Начать репликации".to_string()],
];
output.push_str(&format_table(&cluster_headers, &cluster_rows));
// Функции плагинов
output.push_str(&format!("\n{}\n", colors.header("ФУНКЦИИ ПЛАГИНОВ:")));
let plugin_headers = vec!["Функция Lua".to_string(), "Описание".to_string()];
let plugin_rows = vec![
vec!["plugins.list()".to_string(), "Список загруженных плагинов".to_string()],
vec!["plugins.get(plugin_id)".to_string(), "Получить информацию о плагине".to_string()],
vec!["plugins.reload()".to_string(), "Перезагрузить все плагины".to_string()],
vec!["plugins.emit_event(event_name, data)".to_string(), "Отправить событие плагинам".to_string()],
];
output.push_str(&format_table(&plugin_headers, &plugin_rows));
// Примеры Lua кода
output.push_str(&format!("\n{}\n", colors.header("ПРИМЕРЫ LUA КОДА:")));
output.push_str(&format_single_column_table(&[
"• print('Hello from Lua!')".to_string(),
"• for i=1,5 do print('Number: ' .. i) end".to_string(),
"• status = cluster.get_status()".to_string(),
"• print('Coordinator: ' .. status.coordinator_id)".to_string(),
"• plugins.list()".to_string(),
"• plugins.emit_event('custom.event', '{ \"data\": \"value\" }')".to_string(),
]));
// Встроенные функции Lua
output.push_str(&format!("\n{}\n", colors.header("ВСТРОЕННЫЕ ФУНКЦИИ LUA:")));
output.push_str(&format_single_column_table(&[
"Все стандартные функции Lua доступны".to_string(),
"• math, string, table, os, io - стандартные библиотеки".to_string(),
"• print() - вывод в консоль".to_string(),
"• type() - определение типа переменной".to_string(),
"• pairs() и ipairs() - итерация по таблицам".to_string(),
]));
output
}
/// Структура для управления историей команд в реальном времени
struct RealtimeHistory {
commands: VecDeque<String>,
current_index: isize,
max_size: usize,
}
impl RealtimeHistory {
fn new(max_size: usize) -> Self {
Self {
commands: VecDeque::with_capacity(max_size),
current_index: -1,
max_size,
}
}
fn add(&mut self, command: String) {
// Удаляем дубликаты
if let Some(pos) = self.commands.iter().position(|c| c == &command) {
self.commands.remove(pos);
}
self.commands.push_back(command);
while self.commands.len() > self.max_size {
self.commands.pop_front();
}
// Сбрасываем индекс
self.current_index = -1;
}
fn get_previous(&mut self) -> Option<String> {
if self.commands.is_empty() {
return None;
}
if self.current_index < 0 {
// Начинаем с последней команды
self.current_index = self.commands.len() as isize - 1;
} else if self.current_index > 0 {
self.current_index -= 1;
} else {
// Уже на первой команде, возвращаем ее
return self.commands.get(0).cloned();
}
self.commands.get(self.current_index as usize).cloned()
}
fn get_next(&mut self) -> Option<String> {
if self.commands.is_empty() || self.current_index < 0 {
return None;
}
if self.current_index < self.commands.len() as isize - 1 {
self.current_index += 1;
self.commands.get(self.current_index as usize).cloned()
} else {
// Достигли конца, сбрасываем индекс и возвращаем пустую строку
self.current_index = -1;
Some(String::new())
}
}
fn reset_index(&mut self) {
self.current_index = -1;
}
fn get_all(&self) -> Vec<String> {
self.commands.iter().cloned().collect()
}
fn clear(&mut self) {
self.commands.clear();
self.current_index = -1;
}
}
/// Функция для чтения ввода с поддержкой истории (аналог readline)
fn read_line_with_history(prompt: &str, history: &mut RealtimeHistory) -> Result<String, std::io::Error> {
let colors = get_color_codes();
print!("{}", colors.header(prompt));
io::stdout().flush()?;
let mut input = String::new();
let mut cursor_pos = 0;
let mut saved_input = String::new();
// Устанавливаем raw режим для терминала
let mut termios = libc::termios { c_iflag: 0, c_oflag: 0, c_cflag: 0, c_lflag: 0, c_line: 0, c_cc: [0; 32], c_ispeed: 0, c_ospeed: 0 };
unsafe {
// Получаем текущие настройки терминала
if libc::tcgetattr(0, &mut termios) != 0 {
// Если не удалось получить настройки терминала, используем простой ввод
return read_line_simple(prompt);
}
let mut raw_termios = termios;
// Отключаем канонический режим и эхо
raw_termios.c_lflag &= !(libc::ICANON | libc::ECHO);
// Устанавливаем минимальное количество символов для чтения и timeout
raw_termios.c_cc[libc::VMIN] = 1;
raw_termios.c_cc[libc::VTIME] = 0;
// Применяем raw режим
if libc::tcsetattr(0, libc::TCSANOW, &raw_termios) != 0 {
// Если не удалось установить raw режим, используем простой ввод
return read_line_simple(prompt);
}
}
// Функция восстановления настроек терминала
let restore_termios = move || {
unsafe {
libc::tcsetattr(0, libc::TCSANOW, &termios);
}
};
loop {
let mut buffer = [0; 1];
// Читаем один символ
let bytes_read = match io::stdin().read(&mut buffer) {
Ok(bytes) => bytes,
Err(e) => {
restore_termios();
return Err(e);
}
};
if bytes_read == 0 {
restore_termios();
return Ok(input);
}
let c = buffer[0];
match c {
// Enter (13 или 10)
13 | 10 => {
restore_termios();
println!();
let trimmed = input.trim().to_string();
if !trimmed.is_empty() {
history.add(trimmed.clone());
}
return Ok(trimmed);
}
// Backspace или Delete
8 | 127 => {
if cursor_pos > 0 {
input.remove(cursor_pos - 1);
cursor_pos -= 1;
// Очищаем строку и перерисовываем
print!("\r{}", " ".repeat(prompt.len() + input.len() + 2));
print!("\r{}{}", colors.header(prompt), input);
io::stdout().flush()?;
}
}
// Escape последовательности (стрелки и т.д.)
27 => {
// Читаем следующие 2 символа для определения escape последовательности
let mut next_buffer = [0; 2];
if io::stdin().read(&mut next_buffer[0..1]).is_ok() && next_buffer[0] == 91 {
if io::stdin().read(&mut next_buffer[1..2]).is_ok() {
match next_buffer[1] {
// Стрелка вверх
65 => {
if let Some(prev_command) = history.get_previous() {
// Сохраняем текущий ввод
if history.current_index == history.commands.len() as isize - 1 {
saved_input = input.clone();
}
input = prev_command.clone();
cursor_pos = input.len();
print!("\r{}", " ".repeat(prompt.len() + input.len() + 2));
print!("\r{}{}", colors.header(prompt), input);
io::stdout().flush()?;
}
}
// Стрелка вниз
66 => {
if let Some(next_command) = history.get_next() {
input = next_command.clone();
if input.is_empty() && !saved_input.is_empty() {
input = saved_input.clone();
saved_input.clear();
}
cursor_pos = input.len();
print!("\r{}", " ".repeat(prompt.len() + input.len() + 2));
print!("\r{}{}", colors.header(prompt), input);
io::stdout().flush()?;
}
}
// Стрелка вправо
67 => {
if cursor_pos < input.len() {
cursor_pos += 1;
// Показываем позицию курсора
print!("\r{}{}", colors.header(prompt), input);
// Позиционируем курсор
print!("\x1b[{}C", cursor_pos + prompt.len() - input.len());
io::stdout().flush()?;
}
}
// Стрелка влево
68 => {
if cursor_pos > 0 {
cursor_pos -= 1;
// Показываем позицию курсора
print!("\r{}{}", colors.header(prompt), input);
// Позиционируем курсор
print!("\x1b[{}C", cursor_pos + prompt.len() - input.len());
io::stdout().flush()?;
}
}
// Home (часто 72 или 1~)
72 | 49 => {
cursor_pos = 0;
print!("\r{}{}", colors.header(prompt), input);
print!("\x1b[{}C", prompt.len());
io::stdout().flush()?;
}
// End (часто 70 или 4~)
70 | 52 => {
cursor_pos = input.len();
print!("\r{}{}", colors.header(prompt), input);
io::stdout().flush()?;
}
// Delete (51~)
51 => {
if cursor_pos < input.len() {
let _ = input.remove(cursor_pos);
print!("\r{}", " ".repeat(prompt.len() + input.len() + 2));
print!("\r{}{}", colors.header(prompt), input);
io::stdout().flush()?;
}
}
_ => {}
}
}
}
}
// Ctrl+C (3) - прерывание
3 => {
restore_termios();
println!("^C");
return Err(io::Error::new(io::ErrorKind::Interrupted, "Ctrl+C pressed"));
}
// Ctrl+D (4) - EOF
4 => {
restore_termios();
return Ok(String::new());
}
// Обычные символы
_ if c >= 32 => {
// Безопасное преобразование u8 в char
let ch = char::from_u32(c as u32).unwrap_or('?');
input.insert(cursor_pos, ch);
cursor_pos += 1;
// Очищаем строку и перерисовываем
print!("\r{}", " ".repeat(prompt.len() + input.len() + 2));
print!("\r{}{}", colors.header(prompt), input);
io::stdout().flush()?;
}
_ => {}
}
}
}
/// Простой ввод строки без поддержки стрелок (fallback)
fn read_line_simple(prompt: &str) -> Result<String, std::io::Error> {
let colors = get_color_codes();
print!("{}", colors.header(prompt));
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
Ok(input.trim().to_string())
}
/// Запуск REPL интерфейса
pub async fn start_repl() -> Result<(), Box<dyn std::error::Error>> {
// Инициализация истории команд
let history = CommandHistory::new(1000, ".flusql_history");
let current_session = "default".to_string();
let colors = get_color_codes();
println!();
println!("{}", colors.header("Distributed wide-column store"));
println!("{}", colors.header(&format!("version {}", crate::VERSION)));
// Загрузка конфигурации
let config = Config::from_env_or_file("config.toml").unwrap_or_default();
// Инициализация менеджера кластера (из конфигурации)
let cluster_manager = Arc::new(ClusterManager::new(
&config.cluster.node_id,
&config.cluster.node_address
));
// Инициализация Lua интерпретатора
let mut lua_interpreter = LuaInterpreter::new();
// Регистрируем функции кластера в Lua
if let Err(e) = lua_interpreter.register_cluster_functions(Arc::clone(&cluster_manager)) {
println!("{}", colors.warning(&format!("Failed to register cluster functions: {}", e)));
}
// Инициализация менеджера плагинов
let plugins_config = PluginConfig {
enabled: config.plugins.enabled,
plugins_dir: config.plugins.plugins_dir.clone(),
auto_load: config.plugins.auto_load,
};
let mut plugin_manager = PluginManager::new(plugins_config);
if config.plugins.enabled {
match plugin_manager.initialize().await {
Ok(_) => {
println!("{}", colors.success("Plugin system initialized"));
// Загружаем плагины если авто-загрузка включена
if config.plugins.auto_load {
match plugin_manager.load_all_plugins().await {
Ok(loaded_plugins) => {
let count: usize = loaded_plugins.len();
println!("{}", colors.success(&format!("Loaded {} plugins", count)));
}
Err(e) => {
println!("{}", colors.warning(&format!("Failed to load plugins: {}", e)));
}
}
}
}
Err(e) => {
println!("{}", colors.warning(&format!("Failed to initialize plugin system: {}", e)));
}
}
}
// Регистрируем функции плагинов в Lua
if let Err(e) = lua_interpreter.register_plugin_functions(Arc::new(plugin_manager.clone())) {
println!("{}", colors.warning(&format!("Failed to register plugin functions: {}", e)));
}
// Инициализация Lua утилит
if let Err(e) = lua_interpreter.register_utilities() {
println!("{}", colors.warning(&format!("Failed to register Lua utilities: {}", e)));
}
// Информация о системе
if cfg!(target_os = "linux") {
if let Ok(os_release) = std::fs::read_to_string("/etc/os-release") {
for line in os_release.lines() {
if line.starts_with("PRETTY_NAME=") {
let distro_name = line.trim_start_matches("PRETTY_NAME=").trim_matches('"');
println!("{}", colors.info(&format!("Running on: {}", distro_name)));
break;
}
}
}
}
// Информация о конфигурации
println!("{}", colors.info(&format!("Data directory: {}", config.database.data_dir)));
println!("{}", colors.info(&format!("Page size: {} bytes", config.database.page_size)));
println!("{}", colors.info(&format!("Server: {}:{}", config.server.host, config.server.port)));
println!("{}", colors.info(&format!("Plugins: {}", if config.plugins.enabled { "enabled" } else { "disabled" })));
println!("{}", colors.info(&format!("Max connections: {}", config.server.max_connections)));
println!("{}", colors.info(&format!("Cache size: {} MB", config.database.cache_size_mb)));
if supports_color() {
if colors.supports_true_color() {
println!("{}", colors.success("Цветной вывод активирован (true color)"));
} else if colors.supports_256_colors() {
println!("{}", colors.success("Цветной вывод активирован (256 цветов)"));
} else {
println!("{}", colors.success("Цветной вывод активирован (8 цветов)"));
}
} else {
println!("{}", colors.warning("Цветной вывод недоступен"));
}
println!();
// Основной цикл REPL
let mut last_command = String::new();
let mut command_history: VecDeque<String> = VecDeque::with_capacity(1000);
let mut lua_mode = false; // Флаг режима Lua
let mut lua_mode_context = LuaModeContext::new(); // Контекст Lua режима
// Создаем отдельные истории для SQL и Lua режимов
let mut sql_history = RealtimeHistory::new(1000);
let mut lua_history = RealtimeHistory::new(1000);
loop {
// Выбор приглашения в зависимости от режима
let prompt = if lua_mode { "flusql(lua)€ " } else { "flusql> " };
// Используем соответствующую историю в зависимости от режима
let input_result = if lua_mode {
read_line_with_history(prompt, &mut lua_history)
} else {
read_line_with_history(prompt, &mut sql_history)
};
let input = match input_result {
Ok(input) => input,
Err(e) => {
if e.kind() == io::ErrorKind::Interrupted {
// Ctrl+C - очищаем строку и продолжаем
println!();
continue;
}
println!("{}", colors.error(&format!("Ошибка ввода: {}", e)));
continue;
}
};
if input.is_empty() {
continue;
}
// Обработка специальных команд истории
let processed_input = if input == "!!" {
if last_command.is_empty() {
println!("{}", colors.error("No previous command"));
continue;
}
last_command.clone()
} else if input.starts_with('!') && input.len() > 1 {
// Команда вида !n - выполнить команду из истории по номеру
if let Ok(n) = input[1..].parse::<usize>() {
if n > 0 && n <= command_history.len() {
command_history[n - 1].clone()
} else {
println!("{}", colors.error(&format!("Invalid history number: {}", n)));
continue;
}
} else {
// Поиск по префиксу
let prefix = &input[1..];
let matches: Vec<String> = command_history.iter()
.filter(|cmd| cmd.starts_with(prefix))
.cloned()
.collect();
if matches.is_empty() {
println!("{}", colors.error(&format!("No matching command in history for: {}", prefix)));
continue;
} else if matches.len() == 1 {
matches[0].clone()
} else {
println!("{}", colors.error("Multiple matches found:"));
for (i, cmd) in matches.iter().enumerate() {
println!(" {}: {}", i + 1, cmd);
}
continue;
}
}
} else {
input.to_string()
};
// Сохраняем команду в истории (кроме переключения режимов)
if processed_input != "lua.mode" && processed_input != "sql.mode" {
history.add(&current_session, &processed_input);
// Добавляем в локальную историю для навигации стрелками
command_history.push_back(processed_input.clone());
while command_history.len() > 1000 {
command_history.pop_front();
}
last_command = processed_input.clone();
}
// Сбрасываем индекс истории для реального времени
if lua_mode {
lua_history.reset_index();
} else {
sql_history.reset_index();
}
// Обработка команд в зависимости от режима
if lua_mode {
// Lua режим
// Проверка и преобразование команд REPL в вызовы Lua функций
let lua_command = transform_repl_command_to_lua(&processed_input);
match lua_command.to_lowercase().as_str() {
"sql.mode" | "exit" | "quit" => {
// Выход из Lua режима
println!("{}", colors.info("Exiting Lua mode, returning to SQL mode"));
lua_mode = false;
}
"help" => {
println!("{}", show_lua_help_table());
}
"history" => {
// Показать историю команд Lua
println!("{}", colors.info("Lua Command History:"));
let lua_history_all = lua_mode_context.history.get_all_history();
for (i, cmd) in lua_history_all.iter().enumerate() {
println!(" {}: {}", i + 1, cmd);
}
}
"clear-history" | "history clear" => {
// Очистить историю команд Lua
lua_mode_context.history.clear_history();
lua_history.clear();
println!("{}", colors.info("Lua history cleared"));
}
_ => {
// Добавляем команду в историю Lua
lua_mode_context.history.add_command(processed_input.clone());
// Выполнение Lua кода
match lua_interpreter.execute(&lua_command) {
Ok(result) => {
// Выводим результат только если он не пустой (не "nil")
if !result.is_empty() {
println!("{}", result);
}
}
Err(e) => {
println!("{}", colors.error(&format!("Lua Error: {}", e)));
}
}
}
}
} else {
// SQL режим
match processed_input.to_lowercase().as_str() {
"exit" | "quit" => {
// Сохраняем историю перед выходом
if let Err(e) = history.save_to_file().await {
println!("{}", colors.error(&format!("Warning: Failed to save command history: {}", e)));
}
// Останавливаем систему плагинов
if config.plugins.enabled {
println!("{}", colors.info("Stopping plugin system..."));
// Здесь должна быть логика остановки плагинов
}
println!("{}", colors.header("closed flusql..."));
break;
}
"help" => {
println!("{}", show_help_table());
}
"history" => {
// Показать историю команд
println!("{}", colors.info("Command History:"));
println!("{}", colors.info(&format!("Session: {}", current_session)));
let history_list = history.get_history(&current_session);
for (i, cmd) in history_list.iter().enumerate() {
println!(" {}: {}", i + 1, cmd);
}
}
"history clear" => {
// Очистить историю
history.clear(&current_session);
command_history.clear();
sql_history.clear();
println!("{}", colors.info("History cleared"));
}
_ if processed_input.starts_with("history export ") => {
// Экспорт истории в файл
let export_path = processed_input.trim_start_matches("history export ").trim();
if export_path.is_empty() {
println!("{}", colors.error("Usage: HISTORY EXPORT 'file_path';"));
} else {
let export_path = export_path.trim_matches(|c| c == '\'' || c == '"');
match history.export_history(export_path).await {
Ok(_) => println!("{}", colors.success(&format!("History exported to {}", export_path))),
Err(e) => println!("{}", colors.error(&format!("Error exporting history: {}", e))),
}
}
}
"plugins list" => {
if !config.plugins.enabled {
println!("{}", colors.warning("Plugin system is disabled"));
continue;
}
let plugins = plugin_manager.list_plugins();
println!("{}", colors.info(&format!("Loaded plugins: {}", plugins.len())));
for plugin in plugins {
println!(" {} v{} - {}",
colors.accent(&plugin.name),
plugin.version,
plugin.description);
println!(" State: {:?}, Author: {}", plugin.state, plugin.author);
if !plugin.hooks.is_empty() {
println!(" Hooks: {}", plugin.hooks.len());
}
}
}
_ if processed_input.starts_with("plugin load ") => {
if !config.plugins.enabled {
println!("{}", colors.warning("Plugin system is disabled"));
continue;
}
let plugin_path = processed_input.trim_start_matches("plugin load ").trim();
if plugin_path.is_empty() {
println!("{}", colors.error("Usage: PLUGIN LOAD 'path/to/plugin.lua';"));
continue;
}
let plugin_path = plugin_path.trim_matches(|c| c == '\'' || c == '"');
println!("{}", colors.info(&format!("Loading plugin from: {}", plugin_path)));
match plugin_manager.load_plugin(Path::new(plugin_path)).await {
Ok(plugin_id) => {
println!("{}", colors.success(&format!("Plugin loaded with ID: {}", plugin_id)));
// Инициализируем плагин после загрузки
if let Err(e) = plugin_manager.initialize_plugin(&plugin_id).await {
println!("{}", colors.warning(&format!("Failed to initialize plugin: {}", e)));
}
}
Err(e) => {
println!("{}", colors.error(&format!("Failed to load plugin: {}", e)));
}
}
}
_ if processed_input.starts_with("plugin unload ") => {
if !config.plugins.enabled {
println!("{}", colors.warning("Plugin system is disabled"));
continue;
}
let plugin_id = processed_input.trim_start_matches("plugin unload ").trim();
if plugin_id.is_empty() {
println!("{}", colors.error("Usage: PLUGIN UNLOAD 'plugin_id';"));
continue;
}
let plugin_id = plugin_id.trim_matches(|c| c == '\'' || c == '"');
println!("{}", colors.info(&format!("Unloading plugin: {}", plugin_id)));
match plugin_manager.unload_plugin(plugin_id).await {
Ok(_) => {
println!("{}", colors.success("Plugin unloaded successfully"));
}
Err(e) => {
println!("{}", colors.error(&format!("Failed to unload plugin: {}", e)));
}
}
}
"plugins reload" => {
if !config.plugins.enabled {
println!("{}", colors.warning("Plugin system is disabled"));
continue;
}
println!("{}", colors.info("Reloading all plugins..."));
match plugin_manager.load_all_plugins().await {
Ok(loaded_plugins) => {
let count: usize = loaded_plugins.len();
println!("{}", colors.success(&format!("Reloaded {} plugins", count)));
}
Err(e) => {
println!("{}", colors.error(&format!("Failed to reload plugins: {}", e)));
}
}
}
"config show" => {
println!("{}", colors.info("Current configuration:"));
println!(" Server: {}:{}", config.server.host, config.server.port);
println!(" Data directory: {}", config.database.data_dir);
println!(" Page size: {} bytes", config.database.page_size);
println!(" Cache size: {} MB", config.database.cache_size_mb);
println!(" Max connections: {}", config.server.max_connections);
println!(" Plugins enabled: {}", config.plugins.enabled);
println!(" Plugins directory: {}", config.plugins.plugins_dir);
}
"lua.mode" => {
println!();
println!("{}", colors.info("Type 'help' to see available Lua functions"));
println!("{}", colors.info("Type 'sql.mode' to return to SQL mode"));
println!("{}", colors.info("Try: print('Hello from Lua!') or plugins.list()"));
println!(); // Добавленная пустая строка
lua_mode = true;
}
_ => {
// Проверяем команды экспорта/импорта CSV
if processed_input.to_lowercase().starts_with("copy to") {
// Экспорт данных в CSV
match parse_copy_command(&processed_input) {
Ok((table_name, file_path, with_header)) => {
println!("{}", colors.info(&format!("Exporting table '{}' to CSV file: {}", table_name, file_path)));
// Здесь должна быть реальная логика экспорта
match export_to_csv(&table_name, &file_path, with_header) {
Ok(rows_exported) => {
println!("{}", colors.success(&format!("Successfully exported {} rows to {}", rows_exported, file_path)));
}
Err(e) => {
println!("{}", colors.error(&format!("Export error: {}", e)));
}
}
}
Err(e) => {
println!("{}", colors.error(&format!("COPY TO parse error: {}", e)));
}
}
} else if processed_input.to_lowercase().starts_with("copy from") {
// Импорт данных из CSV
match parse_copy_command(&processed_input) {
Ok((table_name, file_path, with_header)) => {
println!("{}", colors.info(&format!("Importing CSV file '{}' to table: {}", file_path, table_name)));
// Здесь должна быть реальная логика импорта
match import_from_csv(&table_name, &file_path, with_header) {
Ok(rows_imported) => {
println!("{}", colors.success(&format!("Successfully imported {} rows from {}", rows_imported, file_path)));
}
Err(e) => {
println!("{}", colors.error(&format!("Import error: {}", e)));
}
}
}
Err(e) => {
println!("{}", colors.error(&format!("COPY FROM parse error: {}", e)));
}
}
} else {
// Парсинг и выполнение SQL команды
match crate::parser::sql::SqlParser::new().parse(&processed_input) {
Ok(sql_query) => {
println!("{}", colors.success("Parsed SQL query successfully"));
println!("{}", colors.info(&format!("Query: {:?}", sql_query)));
// Обработка новых команд
match sql_query {
crate::parser::sql::SqlQuery::Explain { query, analyze, .. } => {
println!("{}", colors.info(&format!("Explain {}query:", if analyze { "Analyze " } else { "" })));
println!(" {:?}", query);
println!("{}", colors.info("Explain functionality not fully implemented yet"));
}
crate::parser::sql::SqlQuery::CreateTrigger { name, table, .. } => {
println!("{}", colors.success(&format!("Creating trigger '{}' on table '{}'", name, table)));
println!("{}", colors.info("Trigger creation not fully implemented yet"));
}
crate::parser::sql::SqlQuery::DropTrigger { name, table, .. } => {
println!("{}", colors.success(&format!("Dropping trigger '{}' from table '{}'", name, table)));
println!("{}", colors.info("Trigger deletion not fully implemented yet"));
}
crate::parser::sql::SqlQuery::ShowTables => {
println!("{}", colors.info("Showing tables (functionality not implemented yet)"));
}
crate::parser::sql::SqlQuery::DescribeTable { table_name } => {
println!("{}", colors.info(&format!("Describing table '{}' (functionality not implemented yet)", table_name)));
}
crate::parser::sql::SqlQuery::CopyTo { table, file_path, format, header, delimiter, .. } => {
let delimiter_str = delimiter.as_ref()
.map(|d| d.as_str())
.unwrap_or("default");
println!("{}", colors.info(&format!("Copying table '{}' to '{}' (format: {}, header: {}, delimiter: '{}')",
table, file_path, format, header, delimiter_str)));
// Здесь должен быть вызов функции экспорта
}
crate::parser::sql::SqlQuery::CopyFrom { table, file_path, format, header, delimiter, .. } => {
let delimiter_str = delimiter.as_ref()
.map(|d| d.as_str())
.unwrap_or("default");
println!("{}", colors.info(&format!("Copying from '{}' to table '{}' (format: {}, header: {}, delimiter: '{}')",
file_path, table, format, header, delimiter_str)));
// Здесь должен быть вызов функции импорта
}
_ => {
println!("{}", colors.warning(&format!("Command not fully implemented yet: {}", processed_input)));
}
}
}
Err(e) => {
println!("{}", colors.error(&format!("SQL Parse Error: {}", e)));
}
}
}
}
}
}
}
Ok(())
}
/// Парсинг команд COPY TO/COPY FROM
fn parse_copy_command(input: &str) -> Result<(String, String, bool), String> {
let input_lower = input.to_lowercase();
if input_lower.starts_with("copy to ") || input_lower.starts_with("copy from ") {
let parts: Vec<&str> = input.split_whitespace().collect();
if parts.len() < 5 {
return Err("Invalid COPY command format".to_string());
}
let direction = parts[0].to_lowercase(); // "to" или "from"
let table_name = parts[1].to_string();
let keyword = parts[2].to_lowercase(); // должно быть "to" или "from"
// Проверяем правильность формата
if (direction == "copy" && keyword == "to" && parts[0].to_lowercase() == "copy") ||
(direction == "copy" && keyword == "from" && parts[0].to_lowercase() == "copy") {
// Извлекаем путь к файлу (может быть в кавычках)
let file_path_part = parts[3];
let file_path = file_path_part.trim_matches(|c| c == '\'' || c == '"').to_string();
// Проверяем, есть ли опции
let mut with_header = false;
let mut i = 4;
while i < parts.len() {
if parts[i].to_lowercase() == "with" && i + 2 < parts.len() {
if parts[i+1].to_lowercase() == "csv" && parts[i+2].to_lowercase() == "header" {
with_header = true;
i += 3;
} else {
i += 1;
}
} else {
i += 1;
}
}
return Ok((table_name, file_path, with_header));
}
}
Err("Failed to parse COPY command".to_string())
}
/// Экспорт данных в CSV (заглушка для демонстрации)
fn export_to_csv(table_name: &str, file_path: &str, with_header: bool) -> Result<usize, String> {
println!("Exporting table '{}' to CSV file '{}' (header: {})",
table_name, file_path, with_header);
// В реальной реализации здесь будет логика экспорта данных из БД в CSV
// Для демонстрации создаем пустой файл
match File::create(file_path) {
Ok(_) => {
println!("CSV file created: {}", file_path);
Ok(0) // Возвращаем 0 экспортированных строк (заглушка)
}
Err(e) => Err(format!("Failed to create CSV file: {}", e))
}
}
/// Импорт данных из CSV (заглушка для демонстрации)
fn import_from_csv(table_name: &str, file_path: &str, with_header: bool) -> Result<usize, String> {
println!("Importing CSV file '{}' to table '{}' (header: {})",
file_path, table_name, with_header);
// В реальной реализации здесь будет логика импорта данных из CSV в БД
// Для демонстрации проверяем существование файла
match File::open(file_path) {
Ok(file) => {
let reader = BufReader::new(file);
let mut line_count = 0;
// Подсчитываем строки в файле
for line in reader.lines() {
match line {
Ok(_) => line_count += 1,
Err(e) => return Err(format!("Error reading CSV file: {}", e))
}
}
// Если есть заголовок, уменьшаем счетчик на 1
let data_rows = if with_header && line_count > 0 {
line_count - 1
} else {
line_count
};
println!("Found {} rows in CSV file", data_rows);
Ok(data_rows) // Возвращаем количество импортированных строк
}
Err(e) => Err(format!("Failed to open CSV file: {}", e))
}
}
/// Преобразование команд REPL в вызовы Lua функций
/// Это позволяет использовать знакомые команды в Lua режиме
fn transform_repl_command_to_lua(command: &str) -> String {
let trimmed = command.trim();
// Обрабатываем команды плагинов
if trimmed.starts_with("plugins ") {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 2 {
match parts[1] {
"list" => return "plugins.list()".to_string(),
"reload" => return "plugins.reload()".to_string(),
_ => {}
}
}
} else if trimmed.starts_with("plugin ") {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 3 {
match parts[1] {
"load" if parts.len() >= 3 => {
return format!("plugins.load_plugin(\"{}\")", parts[2]);
}
"unload" if parts.len() >= 3 => {
return format!("plugins.unload_plugin(\"{}\")", parts[2]);
}
_ => {}
}
}
}
// Обрабатываем команды кластера
else if trimmed.starts_with("cluster ") {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 2 {
match parts[1] {
"status" | "get_status" => return "cluster.get_status()".to_string(),
"coord_status" => return "cluster.coord_status()".to_string(),
"add_node" if parts.len() >= 4 => {
return format!("cluster.add_node(\"{}\", \"{}\")", parts[2], parts[3]);
}
"evict" if parts.len() >= 3 => {
return format!("cluster.evict(\"{}\")", parts[2]);
}
"elect_coordinator" | "elect" => return "cluster.elect_coordinator()".to_string(),
"rebalance" => return "cluster.rebalance()".to_string(),
"add_shard" if parts.len() >= 5 => {
// Упрощенный формат: cluster add_shard shard_id master_node slave1,slave2
let shard_id = parts[2];
let master = parts[3];
let slaves_str = if parts.len() > 4 { parts[4] } else { "" };
let slaves: Vec<&str> = slaves_str.split(',').filter(|s| !s.is_empty()).collect();
if slaves.is_empty() {
return format!("cluster.add_shard(\"{}\", \"{}\", {{}})", shard_id, master);
} else {
let slaves_lua = slaves.iter()
.enumerate()
.map(|(i, slave)| format!("[{}] = \"{}\"", i + 1, slave))
.collect::<Vec<String>>()
.join(", ");
return format!("cluster.add_shard(\"{}\", \"{}\", {{{}}})", shard_id, master, slaves_lua);
}
}
"remove_shard" if parts.len() >= 3 => {
return format!("cluster.remove_shard(\"{}\")", parts[2]);
}
"start_replication" if parts.len() >= 4 => {
return format!("cluster.start_replication(\"{}\", \"{}\")", parts[2], parts[3]);
}
_ => {}
}
}
}
// Обработка команд конфигурации
else if trimmed.starts_with("config ") {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 2 {
match parts[1] {
"show" => return "print('Use config.toml file for configuration')".to_string(),
_ => {}
}
}
}
// Если команда уже выглядит как вызов Lua функции, оставляем как есть
// Проверяем на наличие скобок
if trimmed.contains('(') && trimmed.contains(')') {
return trimmed.to_string();
}
// Возвращаем как есть (будет обработано как Lua код)
trimmed.to_string()
}