From b53667d5551e53e8bde4d17d9e6b2cf0e4e52c18 Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Mon, 26 Apr 2021 23:20:16 +0200 Subject: [PATCH] Queries. Data structures and lua bindings. --- apps/openmw_test_suite/CMakeLists.txt | 1 + .../lua/test_querypackage.cpp | 29 +++ components/CMakeLists.txt | 4 + components/queries/luabindings.cpp | 118 +++++++++++ components/queries/luabindings.hpp | 8 + components/queries/query.cpp | 185 ++++++++++++++++++ components/queries/query.hpp | 99 ++++++++++ 7 files changed, 444 insertions(+) create mode 100644 apps/openmw_test_suite/lua/test_querypackage.cpp create mode 100644 components/queries/luabindings.cpp create mode 100644 components/queries/luabindings.hpp create mode 100644 components/queries/query.cpp create mode 100644 components/queries/query.hpp diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/openmw_test_suite/CMakeLists.txt index f9d0e8140f..bd8b032378 100644 --- a/apps/openmw_test_suite/CMakeLists.txt +++ b/apps/openmw_test_suite/CMakeLists.txt @@ -19,6 +19,7 @@ if (GTEST_FOUND AND GMOCK_FOUND) lua/test_scriptscontainer.cpp lua/test_utilpackage.cpp lua/test_serialization.cpp + lua/test_querypackage.cpp misc/test_stringops.cpp misc/test_endianness.cpp diff --git a/apps/openmw_test_suite/lua/test_querypackage.cpp b/apps/openmw_test_suite/lua/test_querypackage.cpp new file mode 100644 index 0000000000..aeaf992db0 --- /dev/null +++ b/apps/openmw_test_suite/lua/test_querypackage.cpp @@ -0,0 +1,29 @@ +#include "gmock/gmock.h" +#include + +#include + +namespace +{ + using namespace testing; + + TEST(LuaQueryPackageTest, basic) + { + sol::state lua; + lua.open_libraries(sol::lib::base, sol::lib::string); + Queries::registerQueryBindings(lua); + lua["query"] = Queries::Query("test"); + lua["fieldX"] = Queries::Field({ "x" }, typeid(std::string)); + lua["fieldY"] = Queries::Field({ "y" }, typeid(int)); + lua.safe_script("t = query:where(fieldX:eq('abc') + fieldX:like('%abcd%'))"); + lua.safe_script("t = t:where(fieldY:gt(5))"); + lua.safe_script("t = t:orderBy(fieldX)"); + lua.safe_script("t = t:orderByDesc(fieldY)"); + lua.safe_script("t = t:groupBy(fieldY)"); + lua.safe_script("t = t:limit(10):offset(5)"); + EXPECT_EQ( + lua.safe_script("return tostring(t)").get(), + "SELECT test WHERE ((x == \"abc\") OR (x LIKE \"%abcd%\")) AND (y > 5) ORDER BY x, y DESC GROUP BY y LIMIT 10 OFFSET 5"); + } +} + diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 8b66baab28..7629c9798f 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -156,6 +156,10 @@ add_component_dir (fallback fallback validate ) +add_component_dir (queries + query luabindings + ) + if(WIN32) add_component_dir (crashcatcher windows_crashcatcher diff --git a/components/queries/luabindings.cpp b/components/queries/luabindings.cpp new file mode 100644 index 0000000000..c830a140f7 --- /dev/null +++ b/components/queries/luabindings.cpp @@ -0,0 +1,118 @@ +#include "luabindings.hpp" + +namespace sol +{ + template <> + struct is_automagical : std::false_type {}; + + template <> + struct is_automagical : std::false_type {}; + + template <> + struct is_automagical : std::false_type {}; +} + +namespace Queries +{ + template + struct CondBuilder + { + Filter operator()(const Field& field, const sol::object& o) + { + FieldValue value; + if (field.type() == typeid(bool) && o.is()) + value = o.as(); + else if (field.type() == typeid(int32_t) && o.is()) + value = o.as(); + else if (field.type() == typeid(int64_t) && o.is()) + value = o.as(); + else if (field.type() == typeid(float) && o.is()) + value = o.as(); + else if (field.type() == typeid(double) && o.is()) + value = o.as(); + else if (field.type() == typeid(std::string) && o.is()) + value = o.as(); + else + throw std::logic_error("Invalid value for field " + field.toString()); + Filter filter; + filter.add({&field, type, value}); + return filter; + } + }; + + void registerQueryBindings(sol::state& lua) + { + sol::usertype field = lua.new_usertype("QueryField"); + sol::usertype filter = lua.new_usertype("QueryFilter"); + sol::usertype query = lua.new_usertype("Query"); + + field[sol::meta_function::to_string] = [](const Field& f) { return f.toString(); }; + field["eq"] = CondBuilder(); + field["neq"] = CondBuilder(); + field["lt"] = CondBuilder(); + field["lte"] = CondBuilder(); + field["gt"] = CondBuilder(); + field["gte"] = CondBuilder(); + field["like"] = CondBuilder(); + + filter[sol::meta_function::to_string] = [](const Filter& filter) { return filter.toString(); }; + filter[sol::meta_function::multiplication] = [](const Filter& a, const Filter& b) + { + Filter res = a; + res.add(b, Operation::AND); + return res; + }; + filter[sol::meta_function::addition] = [](const Filter& a, const Filter& b) + { + Filter res = a; + res.add(b, Operation::OR); + return res; + }; + filter[sol::meta_function::unary_minus] = [](const Filter& a) + { + Filter res = a; + if (!a.mConditions.empty()) + res.mOperations.push_back({Operation::NOT, 0}); + return res; + }; + + query[sol::meta_function::to_string] = [](const Query& q) { return q.toString(); }; + query["where"] = [](const Query& q, const Filter& filter) + { + Query res = q; + res.mFilter.add(filter, Operation::AND); + return res; + }; + query["orderBy"] = [](const Query& q, const Field& field) + { + Query res = q; + res.mOrderBy.push_back({&field, false}); + return res; + }; + query["orderByDesc"] = [](const Query& q, const Field& field) + { + Query res = q; + res.mOrderBy.push_back({&field, true}); + return res; + }; + query["groupBy"] = [](const Query& q, const Field& field) + { + Query res = q; + res.mGroupBy.push_back(&field); + return res; + }; + query["offset"] = [](const Query& q, int64_t offset) + { + Query res = q; + res.mOffset = offset; + return res; + }; + query["limit"] = [](const Query& q, int64_t limit) + { + Query res = q; + res.mLimit = limit; + return res; + }; + } +} + diff --git a/components/queries/luabindings.hpp b/components/queries/luabindings.hpp new file mode 100644 index 0000000000..a23dfa932b --- /dev/null +++ b/components/queries/luabindings.hpp @@ -0,0 +1,8 @@ +#include + +#include "query.hpp" + +namespace Queries +{ + void registerQueryBindings(sol::state& lua); +} diff --git a/components/queries/query.cpp b/components/queries/query.cpp new file mode 100644 index 0000000000..3c7f1517ee --- /dev/null +++ b/components/queries/query.cpp @@ -0,0 +1,185 @@ +#include "query.hpp" + +#include +#include + +namespace Queries +{ + Field::Field(std::vector path, std::type_index type) + : mPath(std::move(path)) + , mType(type) {} + + std::string Field::toString() const + { + std::string result; + for (const std::string& segment : mPath) + { + if (!result.empty()) + result += "."; + result += segment; + } + return result; + } + + std::string toString(const FieldValue& value) + { + return std::visit([](auto&& arg) -> std::string + { + using T = std::decay_t; + if constexpr (std::is_same_v) + { + std::ostringstream oss; + oss << std::quoted(arg); + return oss.str(); + } + else if constexpr (std::is_same_v) + return arg ? "true" : "false"; + else + return std::to_string(arg); + }, value); + } + + std::string Condition::toString() const + { + std::string res; + res += mField->toString(); + switch (mType) + { + case Condition::EQUAL: res += " == "; break; + case Condition::NOT_EQUAL: res += " != "; break; + case Condition::LESSER: res += " < "; break; + case Condition::LESSER_OR_EQUAL: res += " <= "; break; + case Condition::GREATER: res += " > "; break; + case Condition::GREATER_OR_EQUAL: res += " >= "; break; + case Condition::LIKE: res += " LIKE "; break; + } + res += Queries::toString(mValue); + return res; + } + + void Filter::add(const Condition& c, Operation::Type op) + { + mOperations.push_back({Operation::PUSH, mConditions.size()}); + mConditions.push_back(c); + if (mConditions.size() > 1) + mOperations.push_back({op, 0}); + } + + void Filter::add(const Filter& f, Operation::Type op) + { + size_t conditionOffset = mConditions.size(); + size_t operationsBefore = mOperations.size(); + mConditions.insert(mConditions.end(), f.mConditions.begin(), f.mConditions.end()); + mOperations.insert(mOperations.end(), f.mOperations.begin(), f.mOperations.end()); + for (size_t i = operationsBefore; i < mOperations.size(); ++i) + mOperations[i].mConditionIndex += conditionOffset; + if (conditionOffset > 0 && !f.mConditions.empty()) + mOperations.push_back({op, 0}); + } + + std::string Filter::toString() const + { + if(mOperations.empty()) + return ""; + std::vector stack; + auto pop = [&stack](){ auto v = stack.back(); stack.pop_back(); return v; }; + auto push = [&stack](const std::string& s) { stack.push_back(s); }; + for (const Operation& op : mOperations) + { + if(op.mType == Operation::PUSH) + push(mConditions[op.mConditionIndex].toString()); + else if(op.mType == Operation::AND) + { + auto rhs = pop(); + auto lhs = pop(); + std::string res; + res += "("; + res += lhs; + res += ") AND ("; + res += rhs; + res += ")"; + push(res); + } + else if (op.mType == Operation::OR) + { + auto rhs = pop(); + auto lhs = pop(); + std::string res; + res += "("; + res += lhs; + res += ") OR ("; + res += rhs; + res += ")"; + push(res); + } + else if (op.mType == Operation::NOT) + { + std::string res; + res += "NOT ("; + res += pop(); + res += ")"; + push(res); + } + else + throw std::logic_error("Unknown operation type!"); + } + return pop(); + } + + std::string Query::toString() const + { + std::string res; + res += "SELECT "; + res += mQueryType; + + std::string filter = mFilter.toString(); + if(!filter.empty()) + { + res += " WHERE "; + res += filter; + } + + std::string order; + for(const OrderBy& ord : mOrderBy) + { + if(!order.empty()) + order += ", "; + order += ord.mField->toString(); + if(ord.mDescending) + order += " DESC"; + } + if (!order.empty()) + { + res += " ORDER BY "; + res += order; + } + + std::string group; + for (const Field* f: mGroupBy) + { + if (!group.empty()) + group += " ,"; + group += f->toString(); + } + if (!group.empty()) + { + res += " GROUP BY "; + res += group; + } + + if (mLimit != sNoLimit) + { + res += " LIMIT "; + res += std::to_string(mLimit); + } + + if (mOffset != 0) + { + res += " OFFSET "; + res += std::to_string(mOffset); + } + + return res; + } +} + diff --git a/components/queries/query.hpp b/components/queries/query.hpp new file mode 100644 index 0000000000..45144fed62 --- /dev/null +++ b/components/queries/query.hpp @@ -0,0 +1,99 @@ +#ifndef COMPONENTS_QUERIES_QUERY +#define COMPONENTS_QUERIES_QUERY + +#include +#include +#include +#include +#include + +namespace Queries +{ + class Field + { + public: + Field(std::vector path, std::type_index type); + + const std::vector& path() const { return mPath; } + const std::type_index type() const { return mType; } + + std::string toString() const; + + private: + std::vector mPath; + std::type_index mType; + }; + + struct OrderBy + { + const Field* mField; + bool mDescending; + }; + + using FieldValue = std::variant; + std::string toString(const FieldValue& value); + + struct Condition + { + enum Type + { + EQUAL = 0, + NOT_EQUAL = 1, + GREATER = 2, + GREATER_OR_EQUAL = 3, + LESSER = 4, + LESSER_OR_EQUAL = 5, + LIKE = 6, + }; + + std::string toString() const; + + const Field* mField; + Type mType; + FieldValue mValue; + }; + + struct Operation + { + enum Type + { + PUSH = 0, // push condition on stack + NOT = 1, // invert top condition on stack + AND = 2, // logic AND for two top conditions + OR = 3, // logic OR for two top conditions + }; + + Type mType; + size_t mConditionIndex; // used only if mType == PUSH + }; + + struct Filter + { + std::string toString() const; + + // combines with given condition or filter using operation `AND` or `OR`. + void add(const Condition& c, Operation::Type op = Operation::AND); + void add(const Filter& f, Operation::Type op = Operation::AND); + + std::vector mConditions; + std::vector mOperations; // operations on conditions in reverse polish notation + }; + + struct Query + { + static constexpr int64_t sNoLimit = -1; + + Query(std::string type) : mQueryType(std::move(type)) {} + std::string toString() const; + + std::string mQueryType; + Filter mFilter; + std::vector mOrderBy; + std::vector mGroupBy; + int64_t mOffset = 0; + int64_t mLimit = sNoLimit; + }; +} + +#endif // !COMPONENTS_QUERIES_QUERY +