mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-02-09 09:39:53 +00:00
Queries. Data structures and lua bindings.
This commit is contained in:
parent
479856f812
commit
b53667d555
@ -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
|
||||
|
29
apps/openmw_test_suite/lua/test_querypackage.cpp
Normal file
29
apps/openmw_test_suite/lua/test_querypackage.cpp
Normal file
@ -0,0 +1,29 @@
|
||||
#include "gmock/gmock.h"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <components/queries/luabindings.hpp>
|
||||
|
||||
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<std::string>(),
|
||||
"SELECT test WHERE ((x == \"abc\") OR (x LIKE \"%abcd%\")) AND (y > 5) ORDER BY x, y DESC GROUP BY y LIMIT 10 OFFSET 5");
|
||||
}
|
||||
}
|
||||
|
@ -156,6 +156,10 @@ add_component_dir (fallback
|
||||
fallback validate
|
||||
)
|
||||
|
||||
add_component_dir (queries
|
||||
query luabindings
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
add_component_dir (crashcatcher
|
||||
windows_crashcatcher
|
||||
|
118
components/queries/luabindings.cpp
Normal file
118
components/queries/luabindings.cpp
Normal file
@ -0,0 +1,118 @@
|
||||
#include "luabindings.hpp"
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<Queries::Field> : std::false_type {};
|
||||
|
||||
template <>
|
||||
struct is_automagical<Queries::Filter> : std::false_type {};
|
||||
|
||||
template <>
|
||||
struct is_automagical<Queries::Query> : std::false_type {};
|
||||
}
|
||||
|
||||
namespace Queries
|
||||
{
|
||||
template <Condition::Type type>
|
||||
struct CondBuilder
|
||||
{
|
||||
Filter operator()(const Field& field, const sol::object& o)
|
||||
{
|
||||
FieldValue value;
|
||||
if (field.type() == typeid(bool) && o.is<bool>())
|
||||
value = o.as<bool>();
|
||||
else if (field.type() == typeid(int32_t) && o.is<int32_t>())
|
||||
value = o.as<int32_t>();
|
||||
else if (field.type() == typeid(int64_t) && o.is<int64_t>())
|
||||
value = o.as<int64_t>();
|
||||
else if (field.type() == typeid(float) && o.is<float>())
|
||||
value = o.as<float>();
|
||||
else if (field.type() == typeid(double) && o.is<double>())
|
||||
value = o.as<double>();
|
||||
else if (field.type() == typeid(std::string) && o.is<std::string>())
|
||||
value = o.as<std::string>();
|
||||
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> field = lua.new_usertype<Field>("QueryField");
|
||||
sol::usertype<Filter> filter = lua.new_usertype<Filter>("QueryFilter");
|
||||
sol::usertype<Query> query = lua.new_usertype<Query>("Query");
|
||||
|
||||
field[sol::meta_function::to_string] = [](const Field& f) { return f.toString(); };
|
||||
field["eq"] = CondBuilder<Condition::EQUAL>();
|
||||
field["neq"] = CondBuilder<Condition::NOT_EQUAL>();
|
||||
field["lt"] = CondBuilder<Condition::LESSER>();
|
||||
field["lte"] = CondBuilder<Condition::LESSER_OR_EQUAL>();
|
||||
field["gt"] = CondBuilder<Condition::GREATER>();
|
||||
field["gte"] = CondBuilder<Condition::GREATER_OR_EQUAL>();
|
||||
field["like"] = CondBuilder<Condition::LIKE>();
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
8
components/queries/luabindings.hpp
Normal file
8
components/queries/luabindings.hpp
Normal file
@ -0,0 +1,8 @@
|
||||
#include <sol/sol.hpp>
|
||||
|
||||
#include "query.hpp"
|
||||
|
||||
namespace Queries
|
||||
{
|
||||
void registerQueryBindings(sol::state& lua);
|
||||
}
|
185
components/queries/query.cpp
Normal file
185
components/queries/query.cpp
Normal file
@ -0,0 +1,185 @@
|
||||
#include "query.hpp"
|
||||
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
|
||||
namespace Queries
|
||||
{
|
||||
Field::Field(std::vector<std::string> 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<decltype(arg)>;
|
||||
if constexpr (std::is_same_v<T, std::string>)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << std::quoted(arg);
|
||||
return oss.str();
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, bool>)
|
||||
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<std::string> 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;
|
||||
}
|
||||
}
|
||||
|
99
components/queries/query.hpp
Normal file
99
components/queries/query.hpp
Normal file
@ -0,0 +1,99 @@
|
||||
#ifndef COMPONENTS_QUERIES_QUERY
|
||||
#define COMPONENTS_QUERIES_QUERY
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <typeindex>
|
||||
#include <variant>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Queries
|
||||
{
|
||||
class Field
|
||||
{
|
||||
public:
|
||||
Field(std::vector<std::string> path, std::type_index type);
|
||||
|
||||
const std::vector<std::string>& path() const { return mPath; }
|
||||
const std::type_index type() const { return mType; }
|
||||
|
||||
std::string toString() const;
|
||||
|
||||
private:
|
||||
std::vector<std::string> mPath;
|
||||
std::type_index mType;
|
||||
};
|
||||
|
||||
struct OrderBy
|
||||
{
|
||||
const Field* mField;
|
||||
bool mDescending;
|
||||
};
|
||||
|
||||
using FieldValue = std::variant<bool, int32_t, int64_t, float, double, std::string>;
|
||||
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<Condition> mConditions;
|
||||
std::vector<Operation> 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<OrderBy> mOrderBy;
|
||||
std::vector<const Field*> mGroupBy;
|
||||
int64_t mOffset = 0;
|
||||
int64_t mLimit = sNoLimit;
|
||||
};
|
||||
}
|
||||
|
||||
#endif // !COMPONENTS_QUERIES_QUERY
|
||||
|
Loading…
x
Reference in New Issue
Block a user