1820 lines
85 KiB
Rust
1820 lines
85 KiB
Rust
// 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(¤t_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(¤t_session);
|
||
for (i, cmd) in history_list.iter().enumerate() {
|
||
println!(" {}: {}", i + 1, cmd);
|
||
}
|
||
}
|
||
"history clear" => {
|
||
// Очистить историю
|
||
history.clear(¤t_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()
|
||
}
|