diff --git a/Userland/Utilities/sql.cpp b/Userland/Utilities/sql.cpp index 1be7bd4dd7a..7f573f0fc4d 100644 --- a/Userland/Utilities/sql.cpp +++ b/Userland/Utilities/sql.cpp @@ -15,109 +15,248 @@ #include #include -namespace { +class SQLRepl { +public: + explicit SQLRepl(String const& database_name) + : m_loop() + { + m_editor = Line::Editor::construct(); + m_editor->load_history(m_history_path); -String s_history_path = String::formatted("{}/.sql-history", Core::StandardPaths::home_directory()); -RefPtr s_editor; -int s_repl_line_level = 0; -bool s_keep_running = true; -String s_pending_database = ""; -String s_current_database = ""; -AK::RefPtr s_sql_client; -int s_connection_id = 0; + m_editor->on_display_refresh = [this](Line::Editor& editor) { + editor.strip_styles(); -String prompt_for_level(int level) -{ - static StringBuilder prompt_builder; - prompt_builder.clear(); - prompt_builder.append("> "); + int open_indents = m_repl_line_level; - for (auto i = 0; i < level; ++i) - prompt_builder.append(" "); + auto line = editor.line(); + SQL::AST::Lexer lexer(line); - return prompt_builder.build(); -} + bool indenters_starting_line = true; + for (SQL::AST::Token token = lexer.next(); token.type() != SQL::AST::TokenType::Eof; token = lexer.next()) { + auto length = token.value().length(); + auto start = token.start_position().column - 1; + auto end = start + length; -String read_next_piece() -{ - StringBuilder piece; + if (indenters_starting_line) { + if (token.type() != SQL::AST::TokenType::ParenClose) + indenters_starting_line = false; + else + --open_indents; + } - do { - if (!piece.is_empty()) - piece.append('\n'); - - auto line_result = s_editor->get_line(prompt_for_level(s_repl_line_level)); - - if (line_result.is_error()) { - s_keep_running = false; - return {}; - } - - auto& line = line_result.value(); - auto lexer = SQL::AST::Lexer(line); - - s_editor->add_to_history(line); - piece.append(line); - - bool is_first_token = true; - bool is_command = false; - bool last_token_ended_statement = false; - - for (SQL::AST::Token token = lexer.next(); token.type() != SQL::AST::TokenType::Eof; token = lexer.next()) { - switch (token.type()) { - case SQL::AST::TokenType::ParenOpen: - ++s_repl_line_level; - break; - case SQL::AST::TokenType::ParenClose: - --s_repl_line_level; - break; - case SQL::AST::TokenType::SemiColon: - last_token_ended_statement = true; - break; - case SQL::AST::TokenType::Period: - if (is_first_token) - is_command = true; - break; - default: - last_token_ended_statement = is_command; - break; + switch (token.category()) { + case SQL::AST::TokenCategory::Invalid: + editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Red), Line::Style::Underline }); + break; + case SQL::AST::TokenCategory::Number: + editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Magenta) }); + break; + case SQL::AST::TokenCategory::String: + editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Green), Line::Style::Bold }); + break; + case SQL::AST::TokenCategory::Blob: + editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Magenta), Line::Style::Bold }); + break; + case SQL::AST::TokenCategory::Keyword: + editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Blue), Line::Style::Bold }); + break; + case SQL::AST::TokenCategory::Identifier: + editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::White), Line::Style::Bold }); + break; + default: + break; + } } - is_first_token = false; + m_editor->set_prompt(prompt_for_level(open_indents)); + }; + + m_sql_client = SQL::SQLClient::construct(); + + m_sql_client->on_connected = [this](int connection_id, String const& connected_to_database) { + outln("Connected to \033[33;1m{}\033[0m", connected_to_database); + m_current_database = connected_to_database; + m_pending_database = ""; + m_connection_id = connection_id; + read_sql(); + }; + + m_sql_client->on_execution_success = [this](int, bool has_results, int updated, int created, int deleted) { + if (updated != 0 || created != 0 || deleted != 0) { + outln("{} row(s) updated, {} created, {} deleted", updated, created, deleted); + } + if (!has_results) { + read_sql(); + } + }; + + m_sql_client->on_next_result = [](int, Vector const& row) { + StringBuilder builder; + builder.join(", ", row); + outln(builder.build()); + }; + + m_sql_client->on_results_exhausted = [this](int, int total_rows) { + outln("{} row(s)", total_rows); + read_sql(); + }; + + m_sql_client->on_connection_error = [this](int, int code, String const& message) { + outln("\033[33;1mConnection error:\033[0m {}", message); + m_loop.quit(code); + }; + + m_sql_client->on_execution_error = [this](int, int, String const& message) { + outln("\033[33;1mExecution error:\033[0m {}", message); + read_sql(); + }; + + m_sql_client->on_disconnected = [this](int) { + if (m_pending_database.is_empty()) { + outln("Disconnected from \033[33;1m{}\033[0m and terminating", m_current_database); + m_loop.quit(0); + } else { + outln("Disconnected from \033[33;1m{}\033[0m", m_current_database); + m_current_database = ""; + m_sql_client->connect(m_pending_database); + } + }; + + if (!database_name.is_empty()) + connect(database_name); + } + + ~SQLRepl() + { + m_editor->save_history(m_history_path); + } + + void connect(String const& database_name) + { + if (m_current_database.is_empty()) { + m_sql_client->connect(database_name); + } else { + m_pending_database = database_name; + m_sql_client->async_disconnect(m_connection_id); + } + } + + auto run() + { + return m_loop.exec(); + } + +private: + String m_history_path { String::formatted("{}/.sql-history", Core::StandardPaths::home_directory()) }; + RefPtr m_editor { nullptr }; + int m_repl_line_level { 0 }; + bool m_keep_running { true }; + String m_pending_database {}; + String m_current_database {}; + AK::RefPtr m_sql_client { nullptr }; + int m_connection_id { 0 }; + Core::EventLoop m_loop; + + String read_next_piece() + { + StringBuilder piece; + + do { + if (!piece.is_empty()) + piece.append('\n'); + + auto line_result = m_editor->get_line(prompt_for_level(m_repl_line_level)); + + if (line_result.is_error()) { + m_keep_running = false; + return {}; + } + + auto& line = line_result.value(); + auto lexer = SQL::AST::Lexer(line); + + m_editor->add_to_history(line); + piece.append(line); + + bool is_first_token = true; + bool is_command = false; + bool last_token_ended_statement = false; + + for (SQL::AST::Token token = lexer.next(); token.type() != SQL::AST::TokenType::Eof; token = lexer.next()) { + switch (token.type()) { + case SQL::AST::TokenType::ParenOpen: + ++m_repl_line_level; + break; + case SQL::AST::TokenType::ParenClose: + --m_repl_line_level; + break; + case SQL::AST::TokenType::SemiColon: + last_token_ended_statement = true; + break; + case SQL::AST::TokenType::Period: + if (is_first_token) + is_command = true; + break; + default: + last_token_ended_statement = is_command; + break; + } + + is_first_token = false; + } + + m_repl_line_level = last_token_ended_statement ? 0 : (m_repl_line_level > 0 ? m_repl_line_level : 1); + } while ((m_repl_line_level > 0) || piece.is_empty()); + + return piece.to_string(); + } + + void read_sql() + { + String piece = read_next_piece(); + if (piece.is_empty()) + return; + + if (piece.starts_with('.')) { + handle_command(piece); + } else { + auto statement_id = m_sql_client->sql_statement(m_connection_id, piece); + m_sql_client->async_statement_execute(statement_id); } - s_repl_line_level = last_token_ended_statement ? 0 : (s_repl_line_level > 0 ? s_repl_line_level : 1); - } while ((s_repl_line_level > 0) || piece.is_empty()); + if (!m_keep_running) { + m_sql_client->async_disconnect(m_connection_id); + return; + } + }; - return piece.to_string(); -} + static String prompt_for_level(int level) + { + static StringBuilder prompt_builder; + prompt_builder.clear(); + prompt_builder.append("> "); -void connect(String const& database_name) -{ - if (s_current_database.is_empty()) { - s_sql_client->connect(database_name); - } else { - s_pending_database = database_name; - s_sql_client->async_disconnect(s_connection_id); + for (auto i = 0; i < level; ++i) + prompt_builder.append(" "); + + return prompt_builder.build(); } -} -void handle_command(StringView command) -{ - if (command == ".exit" || command == ".quit") { - s_keep_running = false; - } else if (command.starts_with(".connect ")) { - auto parts = command.split_view(' '); - if (parts.size() == 2) - connect(parts[1]); - else - outln("\033[33;1mUsage: .connect \033[0m {}", command); - } else { - outln("\033[33;1mUnrecognized command:\033[0m {}", command); + void handle_command(StringView command) + { + if (command == ".exit" || command == ".quit") { + m_keep_running = false; + } else if (command.starts_with(".connect ")) { + auto parts = command.split_view(' '); + if (parts.size() == 2) + connect(parts[1]); + else + outln("\033[33;1mUsage: .connect \033[0m {}", command); + } else { + outln("\033[33;1mUnrecognized command:\033[0m {}", command); + } } -} - -} +}; int main(int argc, char** argv) { @@ -128,131 +267,6 @@ int main(int argc, char** argv) args_parser.add_option(database_name, "Database to connect to", "database", 'd', "database"); args_parser.parse(argc, argv); - s_editor = Line::Editor::construct(); - s_editor->load_history(s_history_path); - - s_editor->on_display_refresh = [](Line::Editor& editor) { - editor.strip_styles(); - - size_t open_indents = s_repl_line_level; - - auto line = editor.line(); - SQL::AST::Lexer lexer(line); - - bool indenters_starting_line = true; - for (SQL::AST::Token token = lexer.next(); token.type() != SQL::AST::TokenType::Eof; token = lexer.next()) { - auto start = token.start_position().column - 1; - auto end = token.end_position().column - 1; - - if (indenters_starting_line) { - if (token.type() != SQL::AST::TokenType::ParenClose) - indenters_starting_line = false; - else - --open_indents; - } - - switch (token.category()) { - case SQL::AST::TokenCategory::Invalid: - editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Red), Line::Style::Underline }); - break; - case SQL::AST::TokenCategory::Number: - editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Magenta) }); - break; - case SQL::AST::TokenCategory::String: - editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Green), Line::Style::Bold }); - break; - case SQL::AST::TokenCategory::Blob: - editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Magenta), Line::Style::Bold }); - break; - case SQL::AST::TokenCategory::Keyword: - editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::Blue), Line::Style::Bold }); - break; - case SQL::AST::TokenCategory::Identifier: - editor.stylize({ start, end }, { Line::Style::Foreground(Line::Style::XtermColor::White), Line::Style::Bold }); - break; - default: - break; - } - } - - editor.set_prompt(prompt_for_level(open_indents)); - }; - - Core::EventLoop loop; - s_sql_client = SQL::SQLClient::construct(); - int the_connection_id; - - auto read_sql = [&]() { - do { - String piece = read_next_piece(); - if (!s_keep_running) - break; - if (piece.is_empty()) - continue; - - if (piece.starts_with('.')) { - handle_command(piece); - } else { - auto statement_id = s_sql_client->sql_statement(the_connection_id, piece); - s_sql_client->async_statement_execute(statement_id); - return; - } - } while (s_keep_running); - s_sql_client->async_disconnect(the_connection_id); - }; - - s_sql_client->on_connected = [&](int connection_id, String const& connected_to_database) { - outln("** Connected to {} **", connected_to_database); - s_current_database = connected_to_database; - s_pending_database = ""; - s_connection_id = connection_id; - read_sql(); - }; - - s_sql_client->on_execution_success = [&](int, bool has_results, int updated, int created, int deleted) { - if (updated != 0 || created != 0 || deleted != 0) { - outln("{} row(s) updated, {} created, {} deleted", updated, created, deleted); - } - if (!has_results) { - read_sql(); - } - }; - - s_sql_client->on_next_result = [&](int, Vector const& row) { - StringBuilder builder; - builder.join(", ", row); - outln("{}", builder.build()); - }; - - s_sql_client->on_results_exhausted = [&](int, int total_rows) { - outln("{} row(s)", total_rows); - read_sql(); - }; - - s_sql_client->on_connection_error = [&](int, int code, String const& message) { - outln("\033[33;1mConnection error:\033[0m {}", message); - loop.quit(code); - }; - - s_sql_client->on_execution_error = [&](int, int, String const& message) { - outln("\033[33;1mExecution error:\033[0m {}", message); - read_sql(); - }; - - s_sql_client->on_disconnected = [&](int) { - if (s_pending_database.is_empty()) { - loop.quit(0); - } else { - outln("** Disconnected from {} **", s_current_database); - s_current_database = ""; - s_sql_client->connect(s_pending_database); - } - }; - - connect(database_name); - auto rc = loop.exec(); - - s_editor->save_history(s_history_path); - - return rc; + SQLRepl repl(database_name); + return repl.run(); }