c++filterodb

Is there an easy way to implement C++ queries like the ones from ODB?


I am working with filters in C++ and I would like to create a query like ODB does:

db->query (query::age > 30)

I don't know what kind of data is being passed there. I have been reading about filters in C++ (with functors, lambda expressions, ...) but it is not as simple and compact as ODB queries. In addition, these query conditions can be concatenated.

db->query_one (query::first == "Joe" && query::last == "Dirt")

I have been looking what I think that the query class code is, but I have not been able to identify how it is achieved.

I need to filter a list of instances of Systems:

class Model
{
  int id;
  QString name;
};
class System
{
  int id;
  int user_id;
  Model model;
};

I would like to have something like this:

Filter<System> system_filter; 
Query<System> system_query(System::id == 1 && System::Model::name == "MyModelName")
system_filter.filter(system_list, system_query);

Currently I have this code (similar to an example of the book Design Patterns in Modern C++):

template <typename T>
struct Specification
{
  virtual ~Specification(){}
  virtual bool is_specified(T* item) = 0;
};

template <typename T>
struct Filter
{
  virtual QList<T*> filter(QList<T*> items, Specification<T>& spec)
  {
    QList<T*> result;
    for (auto& item:items)
    {
      if (spec.is_specified(item))
      {
        result.push_back(item);
      }
    }
    return result;
  }
};
template <typename T>
struct IdSpecification: common::Specification<T>
{
  int id;

  IdSpecification(int id) : id(id){}

public:
  bool is_specified(T* item) override
  {
    return item->getId() == id;
  }
};
template <typename T>
struct NameSpecification: common::Specification<T>
{
  QRegularExpression regex;

  NameSpecification(const QRegularExpression& regex) : regex(regex){}

public:
  bool is_specified(T* item) override
  {
    QRegularExpressionMatch match = regex.match(item->getName());
    return regex.match(item->getName()).hasMatch();
  }
};
template <typename T, typename B>
template <typename T>
struct AndSpecification: Specification<T>
{
  Specification<T>& first;
  Specification<T>& second;

  AndSpecification(Specification<T>& first, Specification<T>& second): first(first), second(second){}

public:
  bool is_specified(T* item) override
  {
    return first.is_specified(item) && second.is_specified(item);
  }
};

So now I can do something like this:

IdSpecification<System> system_specification(3);
NameSpecification<System> name_specification("SytemName");
AndSpecification<System> and_specification(system_specification, type_specification);
auto system_filtered = system_filter.filter(system_list, and_specification);

Thanks!


Solution

  • You want expression trees.

    You might start with:

    template<auto Member>
    struct member_query_t;
    
    template<class T, class V>
    struct member_query_t< V T::* M > {
      V const& operator()(T const& t) const { return t.*M; }
    };
    template<auto Member>
    constexpr member_query_t<Member> member_q = {};
    

    now member_q<&Model::id> is a C++ object that knows both what class it is operating on, and how to access the member id.

    We can now build something called exprssion trees. Here, we make some types and overload some operators to return a compile-time type with the information about the expression encoded in it.

    We can augment this with an ordering, a hash, a comparison, and the like, so our store can see we are being queried for Member and store fast lookup information inside itself for Member.

    template<class T, class V>
    struct member_query_t< V T::* M > {
      V const& operator()(T const& t) const { return t.*M; }
    
      auto hasher() const {
        return std::hash<T>{};
      }
      auto ordering() const {
        return std::less<T>{};
      }
      auto comparison() const {
        return std::equal_to<T>{};
      }
    };
    

    you can get fancy to avoid requiring full specialization for those traits. This kind of syntax would be possible:

    auto system_query(
      member_query<&System::id> == 1 &&  
      member_query<&System::model>[member_query<&Model::name>] == "MyModelName"
    );
    

    where system_query has the entire process of how a lookup works stored its type.

    We can then do a completely different advanced technique called type erasure in C++ to make this into a Query<System>.

    Each of these is easily 100s of lines of relatively dense template code to get working right.

    What more, even after you do that, writing Filter<System> to work requires delving into RTTI and writing an efficient generic database system that can handle nearly arbitrary lookup mechanisms and compose them together on request.

    So you have like 3 hard challenges to do what you want to describe, any one of which would require me multiple iterated versions to get right.