Sfoglia il codice sorgente

SQL Utility: Redesigned the input loop

The existing input loop called the `read_sql` method recursively. This
lead to strange behaviour in the event loop. This is solved by
encapsulating the REPL in an object and ensuring the `read_sql` method
is not called recursively. The method now returns after the first
recognized SQL statement or command.
Jan de Visser 3 anni fa
parent
commit
89835ec83c
1 ha cambiato i file con 216 aggiunte e 202 eliminazioni
  1. 216 202
      Userland/Utilities/sql.cpp

+ 216 - 202
Userland/Utilities/sql.cpp

@@ -15,244 +15,258 @@
 #include <LibSQL/SQLClient.h>
 #include <unistd.h>
 
-namespace {
-
-String s_history_path = String::formatted("{}/.sql-history", Core::StandardPaths::home_directory());
-RefPtr<Line::Editor> s_editor;
-int s_repl_line_level = 0;
-bool s_keep_running = true;
-String s_pending_database = "";
-String s_current_database = "";
-AK::RefPtr<SQL::SQLClient> s_sql_client;
-int s_connection_id = 0;
-
-String prompt_for_level(int level)
-{
-    static StringBuilder prompt_builder;
-    prompt_builder.clear();
-    prompt_builder.append("> ");
-
-    for (auto i = 0; i < level; ++i)
-        prompt_builder.append("    ");
+class SQLRepl {
+public:
+    explicit SQLRepl(String const& database_name)
+        : m_loop()
+    {
+        m_editor = Line::Editor::construct();
+        m_editor->load_history(m_history_path);
+
+        m_editor->on_display_refresh = [this](Line::Editor& editor) {
+            editor.strip_styles();
+
+            int open_indents = m_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 length = token.value().length();
+                auto start = token.start_position().column - 1;
+                auto end = start + length;
+
+                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;
+                }
+            }
 
-    return prompt_builder.build();
-}
+            m_editor->set_prompt(prompt_for_level(open_indents));
+        };
 
-String read_next_piece()
-{
-    StringBuilder piece;
+        m_sql_client = SQL::SQLClient::construct();
 
-    do {
-        if (!piece.is_empty())
-            piece.append('\n');
+        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();
+        };
 
-        auto line_result = s_editor->get_line(prompt_for_level(s_repl_line_level));
+        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();
+            }
+        };
 
-        if (line_result.is_error()) {
-            s_keep_running = false;
-            return {};
-        }
+        m_sql_client->on_next_result = [](int, Vector<String> const& row) {
+            StringBuilder builder;
+            builder.join(", ", row);
+            outln(builder.build());
+        };
 
-        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;
-            }
+        m_sql_client->on_results_exhausted = [this](int, int total_rows) {
+            outln("{} row(s)", total_rows);
+            read_sql();
+        };
 
-            is_first_token = false;
-        }
+        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);
+        };
 
-        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());
+        m_sql_client->on_execution_error = [this](int, int, String const& message) {
+            outln("\033[33;1mExecution error:\033[0m {}", message);
+            read_sql();
+        };
 
-    return piece.to_string();
-}
+        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);
+            }
+        };
 
-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);
+        if (!database_name.is_empty())
+            connect(database_name);
     }
-}
 
-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 <database name>\033[0m {}", command);
-    } else {
-        outln("\033[33;1mUnrecognized command:\033[0m {}", command);
+    ~SQLRepl()
+    {
+        m_editor->save_history(m_history_path);
     }
-}
-
-}
-
-int main(int argc, char** argv)
-{
-    String database_name(getlogin());
-
-    Core::ArgsParser args_parser;
-    args_parser.set_general_help("This is a client for the SerenitySQL database server.");
-    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);
+    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);
+        }
+    }
 
-    s_editor->on_display_refresh = [](Line::Editor& editor) {
-        editor.strip_styles();
+    auto run()
+    {
+        return m_loop.exec();
+    }
 
-        size_t open_indents = s_repl_line_level;
+private:
+    String m_history_path { String::formatted("{}/.sql-history", Core::StandardPaths::home_directory()) };
+    RefPtr<Line::Editor> m_editor { nullptr };
+    int m_repl_line_level { 0 };
+    bool m_keep_running { true };
+    String m_pending_database {};
+    String m_current_database {};
+    AK::RefPtr<SQL::SQLClient> m_sql_client { nullptr };
+    int m_connection_id { 0 };
+    Core::EventLoop m_loop;
+
+    String read_next_piece()
+    {
+        StringBuilder piece;
 
-        auto line = editor.line();
-        SQL::AST::Lexer lexer(line);
+        do {
+            if (!piece.is_empty())
+                piece.append('\n');
 
-        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;
+            auto line_result = m_editor->get_line(prompt_for_level(m_repl_line_level));
 
-            if (indenters_starting_line) {
-                if (token.type() != SQL::AST::TokenType::ParenClose)
-                    indenters_starting_line = false;
-                else
-                    --open_indents;
+            if (line_result.is_error()) {
+                m_keep_running = false;
+                return {};
             }
 
-            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;
+            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;
             }
-        }
-
-        editor.set_prompt(prompt_for_level(open_indents));
-    };
 
-    Core::EventLoop loop;
-    s_sql_client = SQL::SQLClient::construct();
-    int the_connection_id;
+            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());
 
-    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);
-    };
+        return piece.to_string();
+    }
 
-    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();
-    };
+    void read_sql()
+    {
+        String piece = read_next_piece();
+        if (piece.is_empty())
+            return;
 
-    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();
+        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_sql_client->on_next_result = [&](int, Vector<String> const& row) {
-        StringBuilder builder;
-        builder.join(", ", row);
-        outln("{}", builder.build());
+        if (!m_keep_running) {
+            m_sql_client->async_disconnect(m_connection_id);
+            return;
+        }
     };
 
-    s_sql_client->on_results_exhausted = [&](int, int total_rows) {
-        outln("{} row(s)", total_rows);
-        read_sql();
-    };
+    static String prompt_for_level(int level)
+    {
+        static StringBuilder prompt_builder;
+        prompt_builder.clear();
+        prompt_builder.append("> ");
 
-    s_sql_client->on_connection_error = [&](int, int code, String const& message) {
-        outln("\033[33;1mConnection error:\033[0m {}", message);
-        loop.quit(code);
-    };
+        for (auto i = 0; i < level; ++i)
+            prompt_builder.append("    ");
 
-    s_sql_client->on_execution_error = [&](int, int, String const& message) {
-        outln("\033[33;1mExecution error:\033[0m {}", message);
-        read_sql();
-    };
+        return prompt_builder.build();
+    }
 
-    s_sql_client->on_disconnected = [&](int) {
-        if (s_pending_database.is_empty()) {
-            loop.quit(0);
+    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 <database name>\033[0m {}", command);
         } else {
-            outln("** Disconnected from {} **", s_current_database);
-            s_current_database = "";
-            s_sql_client->connect(s_pending_database);
+            outln("\033[33;1mUnrecognized command:\033[0m {}", command);
         }
-    };
+    }
+};
 
-    connect(database_name);
-    auto rc = loop.exec();
+int main(int argc, char** argv)
+{
+    String database_name(getlogin());
 
-    s_editor->save_history(s_history_path);
+    Core::ArgsParser args_parser;
+    args_parser.set_general_help("This is a client for the SerenitySQL database server.");
+    args_parser.add_option(database_name, "Database to connect to", "database", 'd', "database");
+    args_parser.parse(argc, argv);
 
-    return rc;
+    SQLRepl repl(database_name);
+    return repl.run();
 }