diff --git a/components/core/CMakeLists.txt b/components/core/CMakeLists.txt index be13b3bdb..7c48a3d39 100644 --- a/components/core/CMakeLists.txt +++ b/components/core/CMakeLists.txt @@ -605,6 +605,7 @@ set(SOURCE_FILES_unitTest tests/test-Stopwatch.cpp tests/test-StreamingCompression.cpp tests/test-string_utils.cpp + tests/test-sql.cpp tests/test-TimestampPattern.cpp tests/test-utf8_utils.cpp tests/test-Utils.cpp @@ -627,6 +628,7 @@ target_link_libraries(unitTest ${MONGOCXX_TARGET} simdjson spdlog::spdlog + sql OpenSSL::Crypto ${sqlite_LIBRARY_DEPENDENCIES} ${STD_FS_LIBS} diff --git a/components/core/src/clp_s/CMakeLists.txt b/components/core/src/clp_s/CMakeLists.txt index 997a03488..2b98b69f9 100644 --- a/components/core/src/clp_s/CMakeLists.txt +++ b/components/core/src/clp_s/CMakeLists.txt @@ -1,4 +1,5 @@ add_subdirectory(search/kql) +add_subdirectory(search/sql) set( CLP_SOURCES diff --git a/components/core/src/clp_s/search/antlr_common/ErrorListener.hpp b/components/core/src/clp_s/search/antlr_common/ErrorListener.hpp new file mode 100644 index 000000000..c470abc0a --- /dev/null +++ b/components/core/src/clp_s/search/antlr_common/ErrorListener.hpp @@ -0,0 +1,36 @@ +#ifndef CLP_S_SEARCH_ANTLRCOMMON_ERRORLISTENER_HPP +#define CLP_S_SEARCH_ANTLRCOMMON_ERRORLISTENER_HPP + +#include +#include +#include +#include + +#include + +namespace clp_s::search::antlr_common { +class ErrorListener : public antlr4::BaseErrorListener { +public: + auto syntaxError( + [[maybe_unused]] antlr4::Recognizer* recognizer, + [[maybe_unused]] antlr4::Token* offending_symbol, + [[maybe_unused]] size_t line, + [[maybe_unused]] size_t char_position_in_line, + std::string const& msg, + [[maybe_unused]] std::exception_ptr e + ) -> void override { + m_error = true; + m_error_message = msg; + } + + [[nodiscard]] auto error() const -> bool { return m_error; } + + [[nodiscard]] auto message() const -> std::string_view { return m_error_message; } + +private: + bool m_error{false}; + std::string m_error_message; +}; +} // namespace clp_s::search::antlr_common + +#endif // CLP_S_SEARCH_ANTLRCOMMON_ERRORLISTENER_HPP diff --git a/components/core/src/clp_s/search/kql/CMakeLists.txt b/components/core/src/clp_s/search/kql/CMakeLists.txt index 9dba44a4b..b15990180 100644 --- a/components/core/src/clp_s/search/kql/CMakeLists.txt +++ b/components/core/src/clp_s/search/kql/CMakeLists.txt @@ -8,6 +8,7 @@ ANTLR_TARGET( add_library( kql ../../Utils.hpp + ../antlr_common/ErrorListener.hpp ../AndExpr.hpp ../BooleanLiteral.hpp ../ColumnDescriptor.hpp @@ -26,4 +27,3 @@ add_library( target_compile_features(kql PRIVATE cxx_std_20) target_include_directories(kql PRIVATE ${ANTLR_KqlParser_OUTPUT_DIR}) target_link_libraries(kql PRIVATE antlr4_static Boost::filesystem) - diff --git a/components/core/src/clp_s/search/kql/kql.cpp b/components/core/src/clp_s/search/kql/kql.cpp index 098886671..19da5a1b2 100644 --- a/components/core/src/clp_s/search/kql/kql.cpp +++ b/components/core/src/clp_s/search/kql/kql.cpp @@ -6,14 +6,9 @@ #include #include -#include "KqlBaseVisitor.h" -#include "KqlLexer.h" -#include "KqlParser.h" -// If redlining may want to add ${workspaceFolder}/build/** -// to include path for vscode C/C++ utils - #include "../../Utils.hpp" #include "../AndExpr.hpp" +#include "../antlr_common/ErrorListener.hpp" #include "../BooleanLiteral.hpp" #include "../ColumnDescriptor.hpp" #include "../DateLiteral.hpp" @@ -23,34 +18,16 @@ #include "../NullLiteral.hpp" #include "../OrExpr.hpp" #include "../StringLiteral.hpp" +#include "KqlBaseVisitor.h" +#include "KqlLexer.h" +#include "KqlParser.h" using namespace antlr4; using namespace kql; +using clp_s::search::antlr_common::ErrorListener; namespace clp_s::search::kql { -class ErrorListener : public BaseErrorListener { -public: - void syntaxError( - Recognizer* recognizer, - Token* offending_symbol, - size_t line, - size_t char_position_in_line, - std::string const& msg, - std::exception_ptr e - ) override { - m_error = true; - m_error_message = msg; - } - - bool error() const { return m_error; } - - std::string const& message() const { return m_error_message; } - -private: - bool m_error{false}; - std::string m_error_message; -}; - +namespace { class ParseTreeVisitor : public KqlBaseVisitor { private: static void @@ -236,6 +213,7 @@ class ParseTreeVisitor : public KqlBaseVisitor { return base; } }; +} // namespace std::shared_ptr parse_kql_expression(std::istream& in) { ErrorListener lexer_error_listener; diff --git a/components/core/src/clp_s/search/sql/CMakeLists.txt b/components/core/src/clp_s/search/sql/CMakeLists.txt new file mode 100644 index 000000000..dde7f27cc --- /dev/null +++ b/components/core/src/clp_s/search/sql/CMakeLists.txt @@ -0,0 +1,29 @@ +ANTLR_TARGET( + SqlParser + Sql.g4 + LEXER PARSER VISITOR + PACKAGE sql +) + +add_library( + sql + ../../Utils.hpp + ../antlr_common/ErrorListener.hpp + ../AndExpr.hpp + ../BooleanLiteral.hpp + ../ColumnDescriptor.hpp + ../DateLiteral.hpp + ../EmptyExpr.hpp + ../Expression.hpp + ../FilterExpr.hpp + ../Integral.hpp + ../NullLiteral.hpp + ../OrExpr.hpp + ../StringLiteral.hpp + ${ANTLR_SqlParser_CXX_OUTPUTS} + sql.cpp + sql.hpp +) +target_compile_features(sql PRIVATE cxx_std_20) +target_include_directories(sql PRIVATE ${ANTLR_SqlParser_OUTPUT_DIR}) +target_link_libraries(sql PRIVATE antlr4_static Boost::filesystem) diff --git a/components/core/src/clp_s/search/sql/Sql.g4 b/components/core/src/clp_s/search/sql/Sql.g4 new file mode 100644 index 000000000..13abd9146 --- /dev/null +++ b/components/core/src/clp_s/search/sql/Sql.g4 @@ -0,0 +1,6 @@ +// Boilerplate for work in progress SQL grammar. +grammar Sql; + +start: EOF ; + +SPACE: [ \t\r\n] -> skip ; diff --git a/components/core/src/clp_s/search/sql/sql.cpp b/components/core/src/clp_s/search/sql/sql.cpp new file mode 100644 index 000000000..071e57d56 --- /dev/null +++ b/components/core/src/clp_s/search/sql/sql.cpp @@ -0,0 +1,63 @@ +#include +#include +#include + +#include +#include + +#include "../antlr_common/ErrorListener.hpp" +#include "../EmptyExpr.hpp" +#include "../Expression.hpp" +#include "SqlBaseVisitor.h" +#include "SqlLexer.h" +#include "SqlParser.h" + +using antlr4::ANTLRInputStream; +using antlr4::CommonTokenStream; +using clp_s::search::antlr_common::ErrorListener; +using sql::SqlBaseVisitor; +using sql::SqlLexer; +using sql::SqlParser; + +namespace clp_s::search::sql { +namespace { +class ParseTreeVisitor : public SqlBaseVisitor { +public: + [[nodiscard]] auto visitStart([[maybe_unused]] SqlParser::StartContext* ctx) + -> std::any override { + return EmptyExpr::create(); + } +}; +} // namespace + +auto parse_sql_expression(std::istream& in) -> std::shared_ptr { + ErrorListener lexer_error_listener; + ErrorListener parser_error_listener; + + ANTLRInputStream input{in}; + SqlLexer lexer{&input}; + lexer.removeErrorListeners(); + lexer.addErrorListener(&lexer_error_listener); + CommonTokenStream tokens{&lexer}; + SqlParser parser(&tokens); + parser.removeErrorListeners(); + parser.addErrorListener(&parser_error_listener); + SqlParser::StartContext* tree{parser.start()}; + + if (lexer_error_listener.error()) { + SPDLOG_ERROR("Lexer error: {}", lexer_error_listener.message()); + return nullptr; + } + if (parser_error_listener.error()) { + SPDLOG_ERROR("Parser error: {}", parser_error_listener.message()); + return nullptr; + } + + ParseTreeVisitor visitor; + try { + return std::any_cast>(visitor.visitStart(tree)); + } catch (std::exception const& e) { + return nullptr; + } +} +} // namespace clp_s::search::sql diff --git a/components/core/src/clp_s/search/sql/sql.hpp b/components/core/src/clp_s/search/sql/sql.hpp new file mode 100644 index 000000000..2c0045a23 --- /dev/null +++ b/components/core/src/clp_s/search/sql/sql.hpp @@ -0,0 +1,18 @@ +#ifndef CLP_S_SEARCH_SQL_SQL_HPP +#define CLP_S_SEARCH_SQL_SQL_HPP + +#include +#include + +#include "../Expression.hpp" + +namespace clp_s::search::sql { +/** + * Parses an SQL expression from the given stream to generate a search AST. + * @param in Input stream containing an SQL expression followed by EOF + * @return a search AST on success, nullptr otherwise + */ +[[nodiscard]] auto parse_sql_expression(std::istream& in) -> std::shared_ptr; +} // namespace clp_s::search::sql + +#endif // CLP_S_SEARCH_SQL_SQL_HPP diff --git a/components/core/tests/LogSuppressor.hpp b/components/core/tests/LogSuppressor.hpp index adb1c00dd..db20cca26 100644 --- a/components/core/tests/LogSuppressor.hpp +++ b/components/core/tests/LogSuppressor.hpp @@ -1,3 +1,8 @@ +#ifndef TESTS_LOGSUPPRESSOR_HPP +#define TESTS_LOGSUPPRESSOR_HPP + +#include + /** * A class that suppresses logs so long as it's instantiated. */ @@ -19,3 +24,5 @@ class LogSuppressor { private: spdlog::level::level_enum m_previous_logging_level; }; + +#endif // TESTS_LOGSUPPRESSOR_HPP diff --git a/components/core/tests/test-sql.cpp b/components/core/tests/test-sql.cpp new file mode 100644 index 000000000..46a9a23a9 --- /dev/null +++ b/components/core/tests/test-sql.cpp @@ -0,0 +1,23 @@ +#include +#include + +#include + +#include "../src/clp_s/search/EmptyExpr.hpp" +#include "../src/clp_s/search/sql/sql.hpp" +#include "LogSuppressor.hpp" + +using clp_s::search::EmptyExpr; +using clp_s::search::sql::parse_sql_expression; +using std::stringstream; + +TEST_CASE("Test parsing SQL", "[SQL]") { + // Suppress logging + LogSuppressor const suppressor; + + SECTION("Stub accepts empty string") { + stringstream empty_string{""}; + auto filter = std::dynamic_pointer_cast(parse_sql_expression(empty_string)); + REQUIRE((nullptr != filter)); + } +}