From fbe2f1af0792eef14b8bc5810c5cf4159faf5cda Mon Sep 17 00:00:00 2001 From: Cyrille Berger <cyrille.berger@liu.se> Date: Fri, 5 May 2023 11:44:01 +0200 Subject: [PATCH] add a simple management query libary --- .vscode/launch.json | 26 ++++ docs/doxygen/pages/kDBSMQuery.dox | 19 +++ docs/doxygen/pages/mainpage.dox | 5 + kDB/CMakeLists.txt | 1 + kDB/Forward.h | 8 ++ kDB/SMQuery/CMakeLists.txt | 24 ++++ kDB/SMQuery/Engine.cpp | 46 +++++++ kDB/SMQuery/Engine.h | 31 +++++ kDB/SMQuery/Forward.h | 0 kDB/SMQuery/Interfaces/Action.h | 19 +++ kDB/SMQuery/Interfaces/Interfaces.cpp | 6 + kDB/SMQuery/Parser.cpp | 167 ++++++++++++++++++++++++++ kDB/SMQuery/Parser.h | 18 +++ kDB/SMQuery/tests/CMakeLists.txt | 3 + kDB/SMQuery/tests/TestEngine.cpp | 70 +++++++++++ kDB/SMQuery/tests/TestEngine.h | 10 ++ kDBConfig.cmake.in | 1 + 17 files changed, 454 insertions(+) create mode 100644 docs/doxygen/pages/kDBSMQuery.dox create mode 100644 kDB/SMQuery/CMakeLists.txt create mode 100644 kDB/SMQuery/Engine.cpp create mode 100644 kDB/SMQuery/Engine.h create mode 100644 kDB/SMQuery/Forward.h create mode 100644 kDB/SMQuery/Interfaces/Action.h create mode 100644 kDB/SMQuery/Interfaces/Interfaces.cpp create mode 100644 kDB/SMQuery/Parser.cpp create mode 100644 kDB/SMQuery/Parser.h create mode 100644 kDB/SMQuery/tests/CMakeLists.txt create mode 100644 kDB/SMQuery/tests/TestEngine.cpp create mode 100644 kDB/SMQuery/tests/TestEngine.h diff --git a/.vscode/launch.json b/.vscode/launch.json index a12ffb32..4c467e7a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -291,6 +291,32 @@ "visualizerFile": "${env:LRS_PKG_ROOT}/src/qt-natvis/qt5.natvis.xml", "showDisplayString": true }, + { + "name": "TestEngine (kDB::SMQuery) (gdb)", + "type": "cppdbg", + "request": "launch", + "program": "${env:LRS_PKG_ROOT}/host-build-debug/kDB/kDB/SMQuery/tests/TestEngine", + "args": [], + "stopAtEntry": false, + "cwd": "${env:LRS_PKG_ROOT}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ], + "visualizerFile": "${env:LRS_PKG_ROOT}/src/qt-natvis/qt5.natvis.xml", + "showDisplayString": true + }, ] } diff --git a/docs/doxygen/pages/kDBSMQuery.dox b/docs/doxygen/pages/kDBSMQuery.dox new file mode 100644 index 00000000..b5c58212 --- /dev/null +++ b/docs/doxygen/pages/kDBSMQuery.dox @@ -0,0 +1,19 @@ +/** +\page kDB_SMQuery kDB::SMQuery Library +\defgroup kDB_SMQuery + +kDB::SMQuery is a kDB library which allows to execute Simple Management Query. + +The idea is for the engine to be able to execute query of the form + +@code +SOME KEY name1: value1 name2: value2 ... +@endcode + +For instance: + +@code +CREATE DATASETS uri: <uri_of_dataset> +@endcode + +*/ \ No newline at end of file diff --git a/docs/doxygen/pages/mainpage.dox b/docs/doxygen/pages/mainpage.dox index 49d3b8f7..09e4d0e1 100644 --- a/docs/doxygen/pages/mainpage.dox +++ b/docs/doxygen/pages/mainpage.dox @@ -1,7 +1,12 @@ /** \mainpage kDB: Knowledge DataBase \section kdb_overview General overview + +\section kDB_core Core Libraries + - \ref kDB_SMQuery Simple Management Query + \section kdb_extensions Extensions + - \ref kDBDatasets extension to manage datasets - \ref kDBQueries extension to handle query templates in C++/Python */ diff --git a/kDB/CMakeLists.txt b/kDB/CMakeLists.txt index df5f0291..178d975f 100644 --- a/kDB/CMakeLists.txt +++ b/kDB/CMakeLists.txt @@ -1,6 +1,7 @@ add_subdirectory(RDFDB) add_subdirectory(Repository) add_subdirectory(RDFView) +add_subdirectory(SMQuery) add_subdirectory(SPARQL) add_subdirectory(Utils) diff --git a/kDB/Forward.h b/kDB/Forward.h index bd61d9b6..11d12169 100644 --- a/kDB/Forward.h +++ b/kDB/Forward.h @@ -69,4 +69,12 @@ namespace kDB { class Database; } + namespace SMQuery + { + class Engine; + namespace Interfaces + { + class Action; + } + } } diff --git a/kDB/SMQuery/CMakeLists.txt b/kDB/SMQuery/CMakeLists.txt new file mode 100644 index 00000000..33c98b59 --- /dev/null +++ b/kDB/SMQuery/CMakeLists.txt @@ -0,0 +1,24 @@ +add_subdirectory(tests) + +# Create library + +set(KDBSMQUERY_SRCS + Engine.cpp + Parser.cpp + Interfaces/Interfaces.cpp + ) + +add_library(kDBSMQuery SHARED ${KDBSMQUERY_SRCS}) +target_link_libraries(kDBSMQuery PUBLIC knowCore ) + +# Install +install(TARGETS kDBSMQuery EXPORT kDBTargets ${INSTALL_TARGETS_DEFAULT_ARGS} ) +set(PROJECT_EXPORTED_TARGETS kDBSMQuery ${PROJECT_EXPORTED_TARGETS} CACHE INTERNAL "") + +install( FILES + Engine.h + DESTINATION ${INSTALL_INCLUDE_DIR}/kDBSMQuery ) + + install( FILES + Interfaces/Action.h + DESTINATION ${INSTALL_INCLUDE_DIR}/kDBSMQuery/Interfaces ) diff --git a/kDB/SMQuery/Engine.cpp b/kDB/SMQuery/Engine.cpp new file mode 100644 index 00000000..8a923092 --- /dev/null +++ b/kDB/SMQuery/Engine.cpp @@ -0,0 +1,46 @@ +#include "Engine.h" + +#include <QHash> + +#include "Parser.h" +#include "Interfaces/Action.h" + +using namespace kDB::SMQuery; + +struct Engine::Private +{ + QHash<QString, Interfaces::Action*> key2action; + QList<Interfaces::Action*> actions; +}; + +Engine::Engine() : d(new Private) +{ + +} + +Engine::~Engine() +{ + qDeleteAll(d->actions); + delete d; +} + +void Engine::add(const QStringList& _keys, Interfaces::Action* _action) +{ + d->actions.append(_action); + for(const QString& key : _keys) + { + d->key2action[key.toUpper()] = _action; + } +} + +knowCore::ReturnVoid Engine::execute(const QString& _text) +{ + KNOWCORE_RETURN_VALUE_TRY(query, Parser::parse(_text)); + QString key = query.key.join(" ").toUpper(); + if(d->key2action.contains(key)) + { + return d->key2action.value(key)->execute(key, query.parameters); + } else { + return kCrvError("No action for '{}'", key); + } +} diff --git a/kDB/SMQuery/Engine.h b/kDB/SMQuery/Engine.h new file mode 100644 index 00000000..86c390e2 --- /dev/null +++ b/kDB/SMQuery/Engine.h @@ -0,0 +1,31 @@ +#include <QStringList> + +#include <kDB/Forward.h> + +namespace kDB::SMQuery +{ + /** + * @ingroup kDB_SMQuery + * + * Execution engine for Simple Management Query + */ + class Engine + { + public: + Engine(); + ~Engine(); + /** + * Add an \ref _action for the list of \ref _keys. + * + * _keys take the form of "SOME ACTION" and will react to query with "SOME ACTION". + */ + void add(const QStringList& _keys, Interfaces::Action* _action); + /** + * Execute a query. + */ + knowCore::ReturnVoid execute(const QString& _text); + private: + struct Private; + Private* const d; + }; +} \ No newline at end of file diff --git a/kDB/SMQuery/Forward.h b/kDB/SMQuery/Forward.h new file mode 100644 index 00000000..e69de29b diff --git a/kDB/SMQuery/Interfaces/Action.h b/kDB/SMQuery/Interfaces/Action.h new file mode 100644 index 00000000..e5ca21a1 --- /dev/null +++ b/kDB/SMQuery/Interfaces/Action.h @@ -0,0 +1,19 @@ +#include <knowCore/Forward.h> + +namespace kDB::SMQuery::Interfaces +{ + /** + * @ingroup kDB_SMQuery + * + * Define an action to be executed by the SMQuery engine + */ + class Action + { + public: + virtual ~Action(); + /** + * Execute the action with the given \ref _key and \ref _parameters + */ + virtual knowCore::ReturnVoid execute(const QString& _key, const knowCore::ValueHash& _parameters) = 0; + }; +} diff --git a/kDB/SMQuery/Interfaces/Interfaces.cpp b/kDB/SMQuery/Interfaces/Interfaces.cpp new file mode 100644 index 00000000..c5f1cfda --- /dev/null +++ b/kDB/SMQuery/Interfaces/Interfaces.cpp @@ -0,0 +1,6 @@ +#include "Action.h" + +using namespace kDB::SMQuery::Interfaces; + +Action::~Action() +{} diff --git a/kDB/SMQuery/Parser.cpp b/kDB/SMQuery/Parser.cpp new file mode 100644 index 00000000..cdc69825 --- /dev/null +++ b/kDB/SMQuery/Parser.cpp @@ -0,0 +1,167 @@ +#include "Parser.h" + +#include <knowCore/BigNumber.h> +#include <knowCore/LexerTextStream.h> + +using namespace kDB::SMQuery; + +struct Parser::Private +{ + struct Token + { + enum class Type { + END_OF_FILE, + IDENTIFIER, + COLON, + VALUE + }; + Type type; + QString text; + knowCore::Value value; + }; + QString getIdentifier(knowCore::LexerTextStream::Element lastChar); + knowCore::ReturnValue<Token> nextToken(); + + knowCore::LexerTextStream stream; +}; + +QString Parser::Private::getIdentifier(knowCore::LexerTextStream::Element lastChar) +{ + QString identifierStr; + if( lastChar.content != 0) { + identifierStr = lastChar.content; + } + while (not stream.eof() ) + { + lastChar = stream.getNextChar(); + if( lastChar.isLetter() or lastChar == '_') + { + identifierStr += lastChar.content; + } else { + stream.unget(lastChar); + break; + } + } + return identifierStr; +} + +knowCore::ReturnValue<Parser::Private::Token> Parser::Private::nextToken() +{ + using Token = Private::Token; + knowCore::LexerTextStream::Element lastChar = stream.getNextNonSeparatorChar(); + const knowCore::LexerTextStream::Element firstChar = lastChar; + if( lastChar == EOF ) return kCrvSuccess(Token{Token::Type::END_OF_FILE, QString(), knowCore::Value()}); + + KNOWCORE_ASSERT(lastChar.content.size() == 1); + switch(lastChar.content.at(0).toLatin1()) + { + case '<': + { // This an URI + auto [string, finished] = stream.getString(">"); + if(finished) + { + return kCrvSuccess(Token{Token::Type::VALUE, QString(), knowCore::Value::fromValue(knowCore::Uri(string.content))}); + } else { + return kCrvError("Unfinished uri: {}", string.content); + } + } + case '"': + { // This a String + auto [string, finished] = stream.getString("\""); + if(finished) + { + return kCrvSuccess(Token{Token::Type::VALUE, QString(), knowCore::Value::fromValue(string.content)}); + } else { + return kCrvError("Unfinished uri: {}", string.content); + } + } + case ':': + return kCrvSuccess(Token{Token::Type::COLON, QString(), knowCore::Value()}); + default: + { + if(lastChar.isLetter()) + { + return kCrvSuccess(Token{Token::Type::IDENTIFIER, getIdentifier(lastChar), knowCore::Value()}); + } else if(lastChar.isDigit()) + { + auto [number, isinteger] = stream.getDigit(lastChar); + KNOWCORE_RETURN_VALUE_TRY(bn, knowCore::BigNumber::fromString(number.content)); + return kCrvSuccess(Token{Token::Type::VALUE, QString(), bn.toValue()}); + } else { + return kCrvError("Invalid character: {}", lastChar.content); + } + } + } + + +} + +knowCore::ReturnValue<Parser::Result> Parser::parse(const QString& _string) +{ + using Token = Private::Token; + Private d; + d.stream.setString(_string); + Result r; + bool parsingKey = true; + while(parsingKey) + { + KNOWCORE_RETURN_VALUE_TRY(token, d.nextToken()); + switch(token.type) + { + case Token::Type::END_OF_FILE: + return kCrvSuccess(r); + case Token::Type::IDENTIFIER: + { + r.key.append(token.text); + break; + } + case Token::Type::COLON: + { + if(r.key.isEmpty()) + { + return kCrvError("Unexpected ':'"); + } + QString name = r.key.takeLast(); + KNOWCORE_RETURN_VALUE_TRY(token, d.nextToken()); + if(token.type != Token::Type::VALUE) + { + return kCrvError("Expected value after '{}:'", name); + } + r.parameters.add(name, token.value); + parsingKey = false; + break; + } + default: + return kCrvError("Unexpected token, expecting identifier, ':' or end of file."); + } + } + while(true) + { + KNOWCORE_RETURN_VALUE_TRY(token, d.nextToken()); + switch(token.type) + { + case Token::Type::END_OF_FILE: + return kCrvSuccess(r); + case Token::Type::IDENTIFIER: + { + QString name = token.text; + KNOWCORE_RETURN_VALUE_TRY(token_colon, d.nextToken()); + if(token_colon.type != Token::Type::COLON) + { + return kCrvError("Expected ':' after '{}'", name); + } + KNOWCORE_RETURN_VALUE_TRY(token_value, d.nextToken()); + if(token_value.type != Token::Type::VALUE) + { + return kCrvError("Expected value after '{}:'", name); + } + r.parameters.add(name, token_value.value); + break; + } + default: + return kCrvError("Unexpected token, expecting identifier or end of file."); + } + } + +} + diff --git a/kDB/SMQuery/Parser.h b/kDB/SMQuery/Parser.h new file mode 100644 index 00000000..a2f04fb4 --- /dev/null +++ b/kDB/SMQuery/Parser.h @@ -0,0 +1,18 @@ +#include <knowCore/ValueHash.h> + +namespace kDB::SMQuery +{ + class Parser + { + public: + struct Result + { + QStringList key; + knowCore::ValueHash parameters; + }; + public: + static knowCore::ReturnValue<Result> parse(const QString& _query); + private: + struct Private; + }; +} diff --git a/kDB/SMQuery/tests/CMakeLists.txt b/kDB/SMQuery/tests/CMakeLists.txt new file mode 100644 index 00000000..233d8da7 --- /dev/null +++ b/kDB/SMQuery/tests/CMakeLists.txt @@ -0,0 +1,3 @@ +add_executable(TestEngine TestEngine.cpp ) +target_link_libraries(TestEngine PRIVATE kDBSMQuery Qt5::Test) +add_test(NAME TEST-SMQuery-Engine COMMAND TestEngine) diff --git a/kDB/SMQuery/tests/TestEngine.cpp b/kDB/SMQuery/tests/TestEngine.cpp new file mode 100644 index 00000000..568edae8 --- /dev/null +++ b/kDB/SMQuery/tests/TestEngine.cpp @@ -0,0 +1,70 @@ +#include "TestEngine.h" + +#include "../Interfaces/Action.h" +#include "../Engine.h" + +#include <knowCore/TypeDefinitions.h> +#include <knowCore/Test.h> +#include <knowCore/ValueHash.h> + +class TestFailAction : public kDB::SMQuery::Interfaces::Action +{ +public: + virtual ~TestFailAction() {} + knowCore::ReturnVoid execute(const QString&, const knowCore::ValueHash&) override + { + return kCrvError("Expected failure."); + } + +}; + +class TestAction : public kDB::SMQuery::Interfaces::Action +{ +public: + virtual ~TestAction() {} + knowCore::ReturnVoid execute(const QString& _key, const knowCore::ValueHash& _parameters) override + { + if(_key == "ALSO FAIL") + { + return kCrvError("Expected failure."); + } else if(_key == "DO SUCCEED") + { + if(_parameters.contains("uri") and _parameters.value("uri") != knowCore::Value::fromValue("hello world"_kCu)) + { + return kCrvError("Invalid uri."); + } + if(_parameters.contains("value") and _parameters.value("value") != knowCore::Value::fromValue(12)) + { + return kCrvError("Invalid value."); + } + if(_parameters.contains("text") and _parameters.value("text") != knowCore::Value::fromValue("some text"_kCs)) + { + return kCrvError("Invalid text."); + } + return kCrvSuccess(); + } else { + return kCrvError("Should not happen."); + } + } + +}; +void TestEngine::testQuery() +{ + kDB::SMQuery::Engine e; + e.add({"TRIGGER FAIL"}, new TestFailAction); + e.add({"ALSO FAIL", "DO SUCCEED"}, new TestAction); + + KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("RANDOM ACTION")); + KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("TRIGGER FAIL")); + KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("ALSO FAIL")); + KNOWCORE_TEST_VERIFY_SUCCESS_VOID(e.execute("DO SUCCEED uri: <hello world>")); + KNOWCORE_TEST_VERIFY_SUCCESS_VOID(e.execute("DO SUCCEED uri: <hello world> value: 12")); + KNOWCORE_TEST_VERIFY_SUCCESS_VOID(e.execute("DO SUCCEED uri: <hello world> value: 12 text: \"some text\"")); + + KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("DO SUCCEED uri: <hello>")); + KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("DO SUCCEED value: 121")); + KNOWCORE_TEST_VERIFY_FAILURE_VOID(e.execute("DO SUCCEED text: \"some wrong text\"")); +} + +QTEST_MAIN(TestEngine) + diff --git a/kDB/SMQuery/tests/TestEngine.h b/kDB/SMQuery/tests/TestEngine.h new file mode 100644 index 00000000..a9d04ea6 --- /dev/null +++ b/kDB/SMQuery/tests/TestEngine.h @@ -0,0 +1,10 @@ + +#include <QtTest/QtTest> + +class TestEngine : public QObject +{ + Q_OBJECT +private slots: + void testQuery(); +}; + diff --git a/kDBConfig.cmake.in b/kDBConfig.cmake.in index 5c754833..ce09787c 100644 --- a/kDBConfig.cmake.in +++ b/kDBConfig.cmake.in @@ -59,6 +59,7 @@ macro(__kdb_add_nice_target_core_library CV KCF name) endmacro() __kdb_add_nice_target_core_library(TRUE __KDB_COMPONENTS_FINDABLE__ Repository) +__kdb_add_nice_target_core_library(TRUE __KDB_COMPONENTS_FINDABLE__ SMQuery) macro(__kdb_add_nice_target_components CV KE KCF name) if(${CV}) -- GitLab