diff --git a/CMakeLists.txt b/CMakeLists.txt index 27dd1bf..4672401 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,6 +6,7 @@ set(CMAKE_CXX_STANDARD 17) include_directories(${PROJECT_SOURCE_DIR}/include) add_subdirectory(src) +add_subdirectory(test) add_executable(query main.cpp) target_link_libraries(query matador) diff --git a/include/matador/sql/basic_condition.hpp b/include/matador/sql/basic_condition.hpp new file mode 100644 index 0000000..1a13df1 --- /dev/null +++ b/include/matador/sql/basic_condition.hpp @@ -0,0 +1,63 @@ +#ifndef QUERY_BASIC_CONDITION_HPP +#define QUERY_BASIC_CONDITION_HPP + +#include "matador/sql/column.hpp" + +#include +#include +#include + +namespace matador::sql { + +class dialect; + +class basic_condition +{ +public: + basic_condition() = default; + virtual ~basic_condition() = default; + + enum class operand_t : uint8_t + { + EQUAL = 0, + NOT_EQUAL, + LESS, + LESS_EQUAL, + GREATER, + GREATER_EQUAL, + OR, + AND, + NOT, + IN_LIST, + LIKE + }; + + virtual std::string evaluate(dialect &dialect) const = 0; + + static std::unordered_map operands; +}; + +class basic_column_condition : public basic_condition +{ +public: + column field_; + std::string operand; + + basic_column_condition(column fld, basic_condition::operand_t op); +}; + +class basic_in_condition : public basic_condition +{ +public: + column field_; + + explicit basic_in_condition(column fld); + + [[nodiscard]] virtual size_t size() const = 0; +}; + +/// @endcond + +} + +#endif //QUERY_BASIC_CONDITION_HPP diff --git a/include/matador/sql/column.hpp b/include/matador/sql/column.hpp index c1ab806..391a1ab 100644 --- a/include/matador/sql/column.hpp +++ b/include/matador/sql/column.hpp @@ -11,6 +11,10 @@ namespace matador::sql { class column { public: + explicit column(std::string name) + : name_(std::move(name)) + , attributes_(utils::null_attributes) {} + template explicit column(std::string name, utils::field_attributes attr = utils::null_attributes) : column(std::move(name), data_type_traits::data_type(), attr) @@ -27,5 +31,42 @@ private: data_type_t type_{}; std::any value_; }; + +/** + * User defined literal to have a shortcut creating a column object + * @param name Name of the column + * @param len Length of the column name + * @return A column object with given name + */ +column operator "" _col(const char *name, size_t len); + +column make_column(const std::string &name, data_type_t type, utils::field_attributes attr = utils::null_attributes); + +template < typename Type > +column make_column(const std::string &name, utils::field_attributes attr = utils::null_attributes) +{ + return make_column(name, data_type_traits::builtin_type(0), attr); +} +template <> +column make_column(const std::string &name, utils::field_attributes attr); + +template < typename Type > +column make_pk_column(const std::string &name, size_t size = 0) +{ + return make_column(name, { size, utils::constraints::PRIMARY_KEY | utils::constraints::NOT_NULL}); +} + +template <> +column make_pk_column(const std::string &name, size_t size); + +template < typename Type > +column make_fk_column(const std::string &name, size_t size = 0) +{ + return make_column(name, { size, utils::constraints::FOREIGN_KEY }); +} + +template <> +column make_fk_column(const std::string &name, size_t size); + } #endif //QUERY_COLUMN_HPP diff --git a/include/matador/sql/condition.hpp b/include/matador/sql/condition.hpp new file mode 100644 index 0000000..b605d33 --- /dev/null +++ b/include/matador/sql/condition.hpp @@ -0,0 +1,649 @@ +#ifndef QUERY_CONDITION_HPP +#define QUERY_CONDITION_HPP + +#include "matador/sql/basic_condition.hpp" +#include "matador/sql/dialect.hpp" + +#include +#include + +namespace matador::sql { + +/** + * @class condition + * @brief Represents a sql query condition + * + * @tparam L Left hand operator type + * @tparam R Right hand operator type + * This class represents a condition part + * of a sql query or update statement. + * Each compare method returns a reference to + * the condition itself. That way one can + * concatenate conditions together. + */ + +/// @cond MATADOR_DEV + +class query; + +template +class condition; + +template +class condition::value && + !std::is_same::value && + !std::is_same::value>::type> : public basic_column_condition +{ +public: + condition(const column &fld, basic_condition::operand_t op, T val) + : basic_column_condition(fld, op) + , value(val) + { } + + T value; + + std::string evaluate(dialect &d) const override + { + d.add_host_var(field_.name()); + std::stringstream str; + if (d.compile_type() == dialect::compile_type_t::DIRECT) { + str << d.prepare_identifier(field_.name()) << " " << operand << " " << value; + } else { + str << d.prepare_identifier(field_.name()) << " " << operand << " " << d.next_placeholder(); + } + return str.str(); + } +}; + +template +class condition::value || + std::is_same::value>::type> : public basic_column_condition +{ +public: + condition(const column &fld, basic_condition::operand_t op, T val) + : basic_column_condition(fld, op) + ,value(val) + { } + + T value; + + std::string evaluate(dialect &d) const override + { + d.add_host_var(field_.name()); + std::stringstream str; + if (d.compile_type() == dialect::compile_type_t::DIRECT) { + str << d.prepare_identifier(field_.name()) << " " << operand << " '" << value << "'"; + } else { + str << d.prepare_identifier(field_.name()) << " " << operand << " " << d.next_placeholder(); + } + return str.str(); + } +}; + +template +class condition::value && + !std::is_same::value && + !std::is_same::value>::type> : public basic_column_condition +{ +public: + condition(T val, basic_condition::operand_t op, const column &fld) + : basic_column_condition(fld, op) + , value(val) + { } + + T value; + + std::string evaluate(dialect &d) const override + { + std::stringstream str; + str << value << " " << operand << " " << d.prepare_identifier(field_.name()); + return str.str(); + } +}; + +template +class condition::value || + std::is_same::value>::type> : public basic_column_condition +{ +public: + condition(T val, basic_condition::operand_t op, const column &fld) + : basic_column_condition(fld, op) + , value(val) + { } + + T value; + + std::string evaluate(dialect &d) const override + { + std::stringstream str; + str << "'" << value << "' " << operand << " " << d.prepare_identifier(field_.name()); + return str.str(); + } +}; + +/// @endcond + +/** + * @brief Condition class representing an IN condition + * + * This class represents an query IN condition and evaluates to + * this condition based on the current database d + * + * @code + * WHERE age IN (29,34,56) + * @endcode + */ +template < class V > +class condition> : public basic_in_condition +{ +public: +/** + * @brief Creates an IN condition + * + * Creates an IN condition for the given column and + * the given list of arguments. + * + * @param col Column for the IN condition + * @param args List of arguments + */ +condition(const column &col, const std::initializer_list &args) + : basic_in_condition(col) + , args_(args) +{} + +/** + * @brief Evaluates the condition + * + * Evaluates the condition to a part of the + * query string based on the given compile type + * + * @param d The d used to evaluate + * @return A condition IN part of the query + */ +std::string evaluate(dialect &d) const override +{ + auto count = size(); + for (size_t i = 0; i < count; ++i) { + d.add_host_var(field_.name()); + } + std::stringstream str; + str << d.prepare_identifier(field_.name()) << " IN ("; + if (args_.size() > 1) { + auto first = args_.begin(); + auto last = args_.end() - 1; + while (first != last) { + if (d.compile_type() == dialect::compile_type_t::DIRECT) { + str << *first++ << ","; + } else { + ++first; + str << d.next_placeholder() << ","; + } + } + } + if (!args_.empty()) { + if (d.compile_type() == dialect::compile_type_t::DIRECT) { + str << args_.back(); + } else { + str << d.next_placeholder(); + } + } + str << ")"; + return str.str(); +} + +/** + * @brief Returns the number of arguments in the list + * @return The number of arguments in the list + */ +size_t size() const override +{ +return args_.size(); +} + +private: +std::vector args_; +}; + +/** + * @brief Condition class representing an IN condition + * + * This class represents an query IN condition and evaluates to + * this condition based on the current database d + * + * @code + * WHERE age IN (select age_value from ) + * @endcode + */ +template <> +class condition : public basic_column_condition +{ +public: + /** + * @brief Create a query IN condition + * + * Create an IN condition where the argument values come from + * the given query. To evaluate the query a sql d must be + * given. + * + * @param col Column for the IN condition + * @param op Operand of the condition + * @param q The query to be evaluated to the IN arguments + */ + condition(column col, basic_condition::operand_t op, query &q); + + /** + * @brief Evaluates the condition + * + * Evaluates the condition to a part of the + * query string based on the given compile type + * + * @param d The d used to evaluate + * @return A condition IN part of the query + */ + std::string evaluate(dialect &d) const override + { + std::string result(d.prepare_identifier(field_.name()) + " " + operand + " ("); +// result += d.continue_build(const_cast(query_), d.compile_type()); + result += (")"); + return result; + } + +private: + query &query_; +}; + +condition::condition(column col, basic_condition::operand_t op, query &q) + : basic_column_condition(std::move(col), op) + , query_(q) +{} + +/** + * @brief Between condition. + * + * The condition represents a between where clause + * part. + * + * @tparam T The type of the boundary values + */ +template < class T > +class condition> : public basic_condition +{ +public: +/** + * @brief Create a new between condition + * + * @param col The column for the range check + * @param range The boundary values defining the range + */ +condition(column col, const std::pair &range) + : field_(std::move(col)), range_(range) { } + +/** + * @brief Evaluates the condition + * + * Evaluates the condition to a between part + * based on the given compile type + * + * @param d The d used to evaluate + * @return A condition BETWEEN part of the query + */ +std::string evaluate(dialect &d) const override +{ +d.add_host_var(field_.name()); +d.add_host_var(field_.name()); +std::stringstream str; +if (d.compile_type() == dialect::compile_type_t::DIRECT) { +str << d.prepare_identifier(field_.name()) << " BETWEEN " << range_.first << " AND " << range_.second; +} else { +str << d.prepare_identifier(field_.name()) << " BETWEEN " << d.next_placeholder() << " AND " << d.next_placeholder(); +} +return str.str(); +} + +private: +column field_; +std::pair range_; +}; + +/** + * @brief Logical binary condition + * + * This class represents a logical binary condition + * - AND + * - OR + * + * @tparam L1 The left hand type of the left operator + * @tparam R1 The right hand type of the left operator + * @tparam L2 The left hand type of the right operator + * @tparam R2 The right hand type of the right operator + */ +template +class condition, condition> : public basic_condition +{ +public: + /** + * @brief Create a binary logical condition + * @param l Left hand operator of the condition + * @param r right hand operator of the condition + * @param op The operand (AND or OR) + */ +// condition(const condition &l, const condition &r, basic_condition::operand_t op) + condition(condition &&l, condition &&r, basic_condition::operand_t op) + : left(std::move(l)), right(std::move(r)), operand(op) { } + + /** + * @brief Evaluates the condition + * + * @param d The d used to evaluate + * @return The evaluated string based on the compile type + */ + std::string evaluate(dialect &d) const override + { + std::stringstream str; + // ensure the numbering order for host vars + auto cl = left.evaluate(d); + auto cr = right.evaluate(d); + if (operand == basic_condition::operand_t::AND) { + str << "(" << cl << " " << basic_condition::operands[operand] << " " << cr << ")"; + } else { + str << cl << " " << basic_condition::operands[operand] << " " << cr; + } + return str.str(); + } + +private: + condition left; + condition right; + basic_condition::operand_t operand; +}; + +/** + * @brief Logical unary condition + * + * This class represents a logical unary NOT condition + * + * @tparam L Left hand type of the condition to be negated + * @tparam R Right hand type of the condition to be negated + */ +template +class condition, void> : public basic_condition +{ +public: + /** + * @brief Create a logical unary condition + * @param c The condition to be negated + */ + condition(const condition &c) // NOLINT(*-explicit-constructor) + : cond(c), operand(basic_condition::operands[basic_condition::operand_t::NOT]) { } + + /** + * @brief Evaluates the condition + * + * @param d The d used to evaluate + * @return The evaluated string based on the compile type + */ + std::string evaluate(dialect &d) const override + { + std::stringstream str; + str << operand << " (" << cond.evaluate(d) << ")"; + return str.str(); + } + +private: + condition cond; + std::string operand; +}; + +/** + * @file condition.hpp + * @brief Contains functions to create query conditions + * + * This file contains some functions to create + * query conditions. These functions wrap the + * constructing of a concrete condition and allows + * expression like condition programming. + * + * @code + * cond1 == cond2 + * cond1 < cond2 + * cond2 != cond1 && cond3 < cond4 + * @endcode + */ + +/** + * @brief Creates an IN condition for a given column and a list of values + * + * @tparam V The type of the list arguments + * @param col The column to compare + * @param args The list of values + * @return The condition object + */ +template < class V > +condition> in(const column &col, std::initializer_list args) +{ + return condition>(col, args); +} + +/** + * @brief Creates an IN condition for a given column and a query to be executed + * + * @param col The column to compare + * @param q The query to be executes as sub select + * @return The condition object + */ +condition in(const column &col, query &&q); + +/** + * @brief Creates a between condition. + * + * Creates a between condition for the given column with + * the given range consisting of a low and high value + * + * @tparam T The type of the column and range + * @param col The column for the between condition + * @param low The low value of the range + * @param high The high value of the range + * @return The condition object + */ +template +condition> between(const column &col, T low, T high) +{ + return condition>(col, std::make_pair(low, high)); +} + +/** + * @brief Creates a like condition + * + * Creates a like condition for the given column + * and the given value string. + * + * @param col The column for the like condition + * @param val The value to the like operator + * @return The like condition object + */ +condition like(const column &col, const std::string &val); +//OOS_SQL_API condition like(const matador::column &col, const std::string &val); + +/** + * @brief Condition equality operator for a column and a value + * + * Creates a condition object of a column and a value + * checked on equality. + * + * @tparam T The type of the value + * @param col The column object + * @param val The value to compare with + * @return The condition object representing the equality operation + */ +template +condition operator==(const column &col, T val) +{ + return condition(col, basic_condition::operand_t::EQUAL, val); +} + +/** + * @brief Condition equality method for a column and a query + * + * Creates a condition object of a column and a query + * checked on equality. + * + * @param col The column object + * @param q The query to compare with + * @return The condition object representing the equality operation + */ +condition equals(const column &col, query &q); + +/** + * @brief Condition inequality operator for a column and a value + * + * Creates a condition condition object of a column and a value + * checked on inequality. + * + * @tparam T The type of the value + * @param col The column object + * @param val The value to compare with + * @return The condition object representing the inequality operation + */ +template +condition operator!=(const column &col, T val) +{ + return condition(col, basic_condition::operand_t::NOT_EQUAL, val); +} + +/** + * @brief Condition less operator for a column and a value + * + * Creates a condition object checking if the value of the given column + * is less than the given value. + * + * @tparam T The type of the value + * @param col The column object + * @param val The value to compare with + * @return The condition object representing the less operation + */ +template +condition operator<(const column &col, T val) +{ + return condition(col, basic_condition::operand_t::LESS, val); +} + +/** + * @brief Condition less or equal operator for a column and a value + * + * Creates a condition object checking if the value of the given column + * is less or equal than the given value. + * + * @tparam T The type of the value + * @param col The column object + * @param val The value to compare with + * @return The condition object representing the less or equal operation + */ +template +condition operator<=(const column &col, T val) +{ + return condition(col, basic_condition::operand_t::LESS_EQUAL, val); +} + +/** + * @brief Condition greater operator for a column and a value + * + * Creates a condition object checking if the value of the given column + * is greater than the given value. + * + * @tparam T The type of the value + * @param col The column object + * @param val The value to compare with + * @return The condition object representing the greater operation + */ +template +condition operator>(const column &col, T val) +{ + return condition(col, basic_condition::operand_t::GREATER, val); +} + +/** + * @brief Condition greater or equal operator for a column and a value + * + * Creates a condition object checking if the value of the given column + * is greater or equal than the given value. + * + * @tparam T The type of the value + * @param col The column object + * @param val The value to compare with + * @return The condition object representing the greater or equal operation + */ +template +condition operator>=(const column &col, T val) +{ + return condition(col, basic_condition::operand_t::GREATER_EQUAL, val); +} + +/** + * @brief AND operation condition consisting of a left and right hand condition + * + * @tparam L1 Left hand type of left hand condition + * @tparam R1 Right hand type of left hand condition + * @tparam L2 Left hand type of right hand condition + * @tparam R2 Right hand type of right hand condition + * @param l Left hand condition + * @param r Right hand condition + * @return An condition object representing the AND operation + */ +template +condition, condition> operator&&(condition l, condition r) +{ + return condition, condition>(std::move(l), std::move(r), basic_condition::operand_t::AND); +} + +/** + * @brief OR operation condition consisting of a left and right hand condition + * + * @tparam L1 Left hand type of left hand condition + * @tparam R1 Right hand type of left hand condition + * @tparam L2 Left hand type of right hand condition + * @tparam R2 Right hand type of right hand condition + * @param l Left hand condition + * @param r Right hand condition + * @return An condition object representing the OR operation + */ +template +condition, condition> operator||(condition l, condition r) +{ + return condition, condition>(std::move(l), std::move(r), basic_condition::operand_t::OR); +} + +/** + * @brief Negates a given condition. + * + * @tparam L The left hand type of the condition + * @tparam R The right hand type of the condition + * @param c The condition to negated + * @return An condition object representing the NOT operation + */ +template +condition, void> operator!(condition c) +{ + return condition, void>(std::move(c)); +} + +/** + * @brief Creates a unique condition from a given condition object + * + * @tparam L The left hand type of the condition + * @tparam R The right hand type of the condition + * @param cond The condition to be copied + * @return A unique condition pointer representing the given condition + */ +template +std::unique_ptr make_condition(const condition &cond) +{ + return std::make_unique>(cond); +} + +} + +#endif //QUERY_CONDITION_HPP diff --git a/include/matador/sql/dialect.hpp b/include/matador/sql/dialect.hpp index 54026bf..5000702 100644 --- a/include/matador/sql/dialect.hpp +++ b/include/matador/sql/dialect.hpp @@ -6,10 +6,11 @@ #include #include #include +#include namespace matador::sql { -class dialect +class [[nodiscard]] dialect { public: enum class token_t : uint8_t @@ -52,6 +53,14 @@ public: NONE }; + /** + * @brief Enum representing the compile type + */ + enum class compile_type_t : uint8_t { + PREPARED, /**< Compile type for prepared statements */ + DIRECT /**< Compile type for direct execute statements */ + }; + /** * Holding enums concerning escaping identifiers */ @@ -110,9 +119,28 @@ public: */ virtual escape_identifier_t identifier_escape_type() const; + /** + * Generates a next placeholder string. default is + * question mark '?' + * + * @return Placeholder string + */ + virtual std::string next_placeholder() const; + + compile_type_t compile_type() const; + void add_host_var(const std::string &host_var); + void add_column(const std::string &column); + + const std::vector& host_vars() const; + const std::vector& columns() const; + private: using token_to_string_map = std::unordered_map; + compile_type_t compile_type_ = compile_type_t::DIRECT; + std::vector host_vars_; + std::vector columns_; + token_to_string_map tokens_ { {token_t::CREATE, "CREATE"}, {token_t::DROP, "DROP"}, diff --git a/include/matador/sql/key_value_pair.hpp b/include/matador/sql/key_value_pair.hpp new file mode 100644 index 0000000..6d55761 --- /dev/null +++ b/include/matador/sql/key_value_pair.hpp @@ -0,0 +1,33 @@ +#ifndef QUERY_KEY_VALUE_PAIR_HPP +#define QUERY_KEY_VALUE_PAIR_HPP + +#include +#include +#include + +namespace matador::sql { + +using any_type = std::variant< + char, short, int, long, long long, + unsigned char, unsigned short, unsigned int, unsigned long, unsigned long long, + bool, + float, double, + const char*, + std::string>; + +class key_value_pair +{ +public: + key_value_pair(const std::string &name, any_type value); + key_value_pair(const char *name, any_type value); + + const std::string& name() const; + const any_type& value() const; + +private: + std::string name_; + any_type value_; +}; + +} +#endif //QUERY_KEY_VALUE_PAIR_HPP diff --git a/include/matador/sql/query_builder.hpp b/include/matador/sql/query_builder.hpp index 05b75a3..a9abc97 100644 --- a/include/matador/sql/query_builder.hpp +++ b/include/matador/sql/query_builder.hpp @@ -1,7 +1,9 @@ #ifndef QUERY_QUERY_BUILDER_HPP #define QUERY_QUERY_BUILDER_HPP +#include "matador/sql/basic_condition.hpp" #include "matador/sql/column.hpp" +#include "matador/sql/key_value_pair.hpp" #include #include @@ -12,13 +14,49 @@ namespace matador::sql { class dialect; +namespace detail { +struct any_type_to_string_visitor +{ + any_type_to_string_visitor(const dialect &d) : d(d) {} + + void operator()(char &x) { to_string(x); } + void operator()(short &x) { to_string(x); } + void operator()(int &x) { to_string(x); } + void operator()(long &x) { to_string(x); } + void operator()(long long &x) { to_string(x); } + void operator()(unsigned char &x) { to_string(x); } + void operator()(unsigned short &x) { to_string(x); } + void operator()(unsigned int &x) { to_string(x); } + void operator()(unsigned long &x) { to_string(x); } + void operator()(unsigned long long &x) { to_string(x); } + void operator()(bool &x) { to_string(x); } + void operator()(float &x) { to_string(x); } + void operator()(double &x) { to_string(x); } + void operator()(const char *x) { to_string(x); } + void operator()(std::string &x) { to_string(x); } + + template + void to_string(Type &val) + { + result = std::to_string(val); + } + void to_string(const char *val); + void to_string(std::string &val); + + const dialect &d; + std::string result; +}; + +} + class query_builder { private: enum class state_t { QUERY_INIT, QUERY_CREATE, - QUERY_TABLE, + QUERY_TABLE_CREATE, + QUERY_TABLE_DROP, QUERY_DROP, QUERY_SELECT, QUERY_INSERT, @@ -49,12 +87,20 @@ private: public: explicit query_builder(const dialect &d); - query_builder& create(); + query_builder create(); + query_builder drop(); + query_builder select(std::initializer_list column_names); + query_builder insert(); + query_builder update(const std::string &table); + query_builder remove(); + query_builder& table(const std::string &table, std::initializer_list columns); - - query_builder& select(std::initializer_list column_names); - + query_builder& table(const std::string &table); + query_builder& into(const std::string &table, std::initializer_list column_names); + query_builder& values(std::initializer_list values); query_builder& from(const std::string &table, const std::string &as = ""); + query_builder& set(std::initializer_list key_values); + query_builder& where(const basic_condition &cond); std::string compile(); @@ -70,6 +116,8 @@ private: std::vector query_parts_; + detail::any_type_to_string_visitor value_to_string_; + using query_state_set = std::unordered_set; using query_state_transition_map = std::unordered_map; using query_state_to_string_map = std::unordered_map; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8f92446..57e4162 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,13 +1,18 @@ set(SQL_SOURCES sql/dialect.cpp sql/query_builder.cpp - sql/column.cpp) + sql/column.cpp + sql/key_value_pair.cpp + sql/basic_condition.cpp) set(SQL_HEADER ../include/matador/sql/dialect.hpp ../include/matador/sql/query_builder.hpp ../include/matador/sql/column.hpp - ../include/matador/sql/types.hpp) + ../include/matador/sql/types.hpp + ../include/matador/sql/key_value_pair.hpp + ../include/matador/sql/basic_condition.hpp + ../include/matador/sql/condition.hpp) set(UTILS_HEADER ../include/matador/utils/field_attributes.hpp diff --git a/src/sql/basic_condition.cpp b/src/sql/basic_condition.cpp new file mode 100644 index 0000000..bd46990 --- /dev/null +++ b/src/sql/basic_condition.cpp @@ -0,0 +1,26 @@ +#include "matador/sql/basic_condition.hpp" + +namespace matador::sql { + +std::unordered_map basic_condition::operands{ + {operand_t::EQUAL, "="}, + {operand_t::NOT_EQUAL, "<>"}, + {operand_t::LESS, "<"}, + {operand_t::LESS_EQUAL, "<="}, + {operand_t::GREATER, ">"}, + {operand_t::GREATER_EQUAL, ">="}, + {operand_t::OR, "OR"}, + {operand_t::AND, "AND"}, + {operand_t::NOT, "NOT"}, + {operand_t::IN_LIST, "IN"}, + {operand_t::LIKE, "LIKE"} +}; + +basic_column_condition::basic_column_condition(column fld, basic_condition::operand_t op) + : field_(std::move(fld)), operand(basic_condition::operands[op]) +{ } + +basic_in_condition::basic_in_condition(column fld) + : field_(std::move(fld)) +{ } +} \ No newline at end of file diff --git a/src/sql/column.cpp b/src/sql/column.cpp index e45356e..3e2779f 100644 --- a/src/sql/column.cpp +++ b/src/sql/column.cpp @@ -20,4 +20,28 @@ data_type_t column::type() const { return type_; } +column operator "" _col(const char *name, size_t len) +{ + return column(std::string(name, len)); +} + +column make_column( const std::string& name, data_type_t type, utils::field_attributes attr ) +{ + return {name, type, attr}; +} + +template<> +column make_column( const std::string& name, utils::field_attributes attr ) { + return make_column(name, data_type_traits::builtin_type(attr.size()), attr); +} + +template<> +column make_fk_column( const std::string& name, size_t size ) { + return make_column(name, { size, utils::constraints::FOREIGN_KEY }); +} + +template<> +column make_pk_column( const std::string& name, size_t size ) { + return make_column(name, { size, utils::constraints::PRIMARY_KEY | utils::constraints::NOT_NULL}); +} } \ No newline at end of file diff --git a/src/sql/dialect.cpp b/src/sql/dialect.cpp index a850d34..0164ae3 100644 --- a/src/sql/dialect.cpp +++ b/src/sql/dialect.cpp @@ -52,8 +52,39 @@ void dialect::escape_quotes_in_literals(std::string &str) const utils::replace_all(str, single_quote, double_quote); } -dialect::escape_identifier_t dialect::identifier_escape_type() const { +dialect::escape_identifier_t dialect::identifier_escape_type() const +{ return dialect::escape_identifier_t::ESCAPE_BOTH_SAME; } +std::string dialect::next_placeholder() const +{ + return "?"; +} + +dialect::compile_type_t dialect::compile_type() const +{ + return compile_type_; +} + +void dialect::add_host_var(const std::string &host_var) +{ + host_vars_.emplace_back(host_var); +} + +void dialect::add_column(const std::string &column) +{ + columns_.emplace_back(column); +} + +const std::vector &dialect::host_vars() const +{ + return host_vars_; +} + +const std::vector &dialect::columns() const +{ + return columns_; +} + } \ No newline at end of file diff --git a/src/sql/key_value_pair.cpp b/src/sql/key_value_pair.cpp new file mode 100644 index 0000000..d908998 --- /dev/null +++ b/src/sql/key_value_pair.cpp @@ -0,0 +1,21 @@ +#include "matador/sql/key_value_pair.hpp" + +namespace matador::sql { +key_value_pair::key_value_pair(const std::string &name, any_type value) +: name_(name) +, value_(std::move(value)) { +} + +key_value_pair::key_value_pair(const char *name, any_type value) + : name_(name) + , value_(std::move(value)) { +} + +const std::string &key_value_pair::name() const { + return name_; +} + +const any_type& key_value_pair::value() const { + return value_; +} +} \ No newline at end of file diff --git a/src/sql/query_builder.cpp b/src/sql/query_builder.cpp index f3b6e34..d440570 100644 --- a/src/sql/query_builder.cpp +++ b/src/sql/query_builder.cpp @@ -5,17 +5,33 @@ namespace matador::sql { +namespace detail { + +void any_type_to_string_visitor::to_string(const char *val) +{ + result = "'" + d.prepare_literal(val) + "'"; +} + +void any_type_to_string_visitor::to_string(std::string &val) +{ + result = "'" + d.prepare_literal(val) + "'"; +} + +} +std::string build_create_column(const column &col, const dialect &d); + // poor mens state machine // but does the job for query query_builder::query_state_transition_map query_builder::transitions_{ {state_t::QUERY_INIT, {state_t::QUERY_CREATE, state_t::QUERY_DROP, state_t::QUERY_SELECT, state_t::QUERY_INSERT, state_t::QUERY_UPDATE, state_t::QUERY_DELETE}}, - {state_t::QUERY_CREATE, {state_t::QUERY_TABLE}}, - {state_t::QUERY_DROP, {state_t::QUERY_TABLE}}, + {state_t::QUERY_CREATE, {state_t::QUERY_TABLE_CREATE}}, + {state_t::QUERY_DROP, {state_t::QUERY_TABLE_DROP}}, {state_t::QUERY_SELECT, {state_t::QUERY_FROM}}, {state_t::QUERY_INSERT, {state_t::QUERY_INTO}}, {state_t::QUERY_UPDATE, {state_t::QUERY_SET}}, {state_t::QUERY_DELETE, {state_t::QUERY_FROM}}, - {state_t::QUERY_TABLE, {state_t::QUERY_FINISH}}, + {state_t::QUERY_TABLE_CREATE, {state_t::QUERY_FINISH}}, + {state_t::QUERY_TABLE_DROP, {state_t::QUERY_FINISH}}, {state_t::QUERY_FROM, {state_t::QUERY_WHERE, state_t::QUERY_ORDER_BY, state_t::QUERY_GROUP_BY, state_t::QUERY_FINISH}}, {state_t::QUERY_SET, {state_t::QUERY_WHERE, state_t::QUERY_FINISH}}, {state_t::QUERY_INTO, {state_t::QUERY_VALUES}}, @@ -36,7 +52,8 @@ query_builder::query_state_to_string_map query_builder::state_strings_ { { state_t::QUERY_INSERT, "insert" }, { state_t::QUERY_UPDATE, "update" }, { state_t::QUERY_DELETE, "delete" }, - { state_t::QUERY_TABLE, "table" }, + { state_t::QUERY_TABLE_CREATE, "table" }, + { state_t::QUERY_TABLE_DROP, "table" }, { state_t::QUERY_FROM, "from" }, { state_t::QUERY_SET, "set" }, { state_t::QUERY_INTO, "into" }, @@ -58,35 +75,90 @@ query_builder::query_command_to_string_map query_builder::command_strings_ { }; query_builder::query_builder(const dialect &d) -: dialect_(d) {} +: dialect_(d) +, value_to_string_(d) {} -query_builder &query_builder::create() { +query_builder query_builder::create() { initialize(command_t::CREATE, state_t::QUERY_CREATE); - query_parts_.emplace_back(dialect_.token_at(dialect::token_t::CREATE) + " "); + query_parts_.emplace_back(dialect_.token_at(dialect::token_t::CREATE)); + + return *this; +} + +query_builder query_builder::drop() { + initialize(command_t::DROP, state_t::QUERY_DROP); + + query_parts_.emplace_back(dialect_.token_at(dialect::token_t::DROP)); + + return *this; +} + +query_builder query_builder::select(std::initializer_list column_names) { + initialize(command_t::SELECT, state_t::QUERY_SELECT); + + query_parts_.emplace_back(dialect_.token_at(dialect::token_t::SELECT) + " "); + + std::string result; + if (column_names.size() < 2) { + for (const auto &col : column_names) { + result.append(dialect_.prepare_identifier(col)); + } + } else { + auto it = column_names.begin(); + result.append(dialect_.prepare_identifier(*it++)); + for (it; it != column_names.end(); ++it) { + result.append(", "); + result.append(dialect_.prepare_identifier(*it)); + } + } + + query_parts_.emplace_back(result); + return *this; +} + +query_builder query_builder::insert() { + initialize(command_t::INSERT, state_t::QUERY_INSERT); + + query_parts_.emplace_back(dialect_.token_at(dialect::token_t::INSERT)); + + return *this; +} + +query_builder query_builder::update(const std::string &table) { + initialize(command_t::UPDATE, state_t::QUERY_UPDATE); + + query_parts_.emplace_back(dialect_.token_at(dialect::token_t::UPDATE) + " " + dialect_.prepare_identifier(table)); + + return *this; +} + +query_builder query_builder::remove() { + initialize(command_t::REMOVE, state_t::QUERY_DELETE); + + query_parts_.emplace_back(dialect_.token_at(dialect::token_t::REMOVE)); return *this; } query_builder &query_builder::table(const std::string &table, std::initializer_list columns) { - transition_to(state_t::QUERY_TABLE); + transition_to(state_t::QUERY_TABLE_CREATE); - query_parts_.emplace_back(dialect_.token_at(dialect::token_t::TABLE) + " " + dialect_.prepare_identifier(table) + " "); + query_parts_.emplace_back(" " + dialect_.token_at(dialect::token_t::TABLE) + " " + dialect_.prepare_identifier(table) + " "); std::string result = "("; - for (const auto &col : columns) { - result.append(dialect_.prepare_identifier(col.name()) + " " + dialect_.data_type_at(col.type())); - if (col.attributes().size() > 0) { - result.append("(" + std::to_string(col.attributes().size()) +")"); - } - if (is_constraint_set(col.attributes().options(), utils::constraints::NOT_NULL)) { - result.append(" NOT NULL"); - } - if (is_constraint_set(col.attributes().options(), utils::constraints::PRIMARY_KEY)) { - result.append(" PRIMARY KEY"); - } - result.append(", "); + if (columns.size() < 2) { + for (const auto &col : columns) { + result.append(build_create_column(col, dialect_)); + } + } else { + auto it = columns.begin(); + result.append(build_create_column(*it++, dialect_)); + for (it; it != columns.end(); ++it) { + result.append(", "); + result.append(build_create_column(*it, dialect_)); + } } result += ")"; @@ -94,22 +166,117 @@ query_builder &query_builder::table(const std::string &table, std::initializer_l return *this; } -query_builder &query_builder::select(std::initializer_list column_names) { - initialize(command_t::SELECT, state_t::QUERY_SELECT); +query_builder &query_builder::table(const std::string &table) { + transition_to(state_t::QUERY_TABLE_DROP); - query_parts_.emplace_back(dialect_.token_at(dialect::token_t::SELECT) + " "); + query_parts_.emplace_back(" " + dialect_.token_at(dialect::token_t::TABLE) + " " + dialect_.prepare_identifier(table)); return *this; } +query_builder &query_builder::into(const std::string &table, std::initializer_list column_names) { + transition_to(state_t::QUERY_INTO); + + query_parts_.emplace_back(" " + dialect_.token_at(dialect::token_t::INTO) + " " + dialect_.prepare_identifier(table) + " "); + + std::string result{"("}; + if (column_names.size() < 2) { + for (const auto &col : column_names) { + result.append(dialect_.prepare_identifier(col)); + } + } else { + auto it = column_names.begin(); + result.append(dialect_.prepare_identifier(*it++)); + for (it; it != column_names.end(); ++it) { + result.append(", "); + result.append(dialect_.prepare_identifier(*it)); + } + } + result += (")"); + + query_parts_.emplace_back(result); + return *this; +} + +query_builder &query_builder::values(std::initializer_list values) { + transition_to(state_t::QUERY_VALUES); + + query_parts_.emplace_back(" " + dialect_.token_at(dialect::token_t::VALUES) + " "); + + std::string result{"("}; + if (values.size() < 2) { + for (auto val : values) { + std::visit(value_to_string_, val); + result.append(value_to_string_.result); + } + } else { + auto it = values.begin(); + auto val = *it++; + std::visit(value_to_string_, val); + result.append(value_to_string_.result); + for (; it != values.end(); ++it) { + result.append(", "); + val = *it; + std::visit(value_to_string_, val); + result.append(value_to_string_.result); + } + } + result += (")"); + + query_parts_.emplace_back(result); + return *this; +} + query_builder &query_builder::from(const std::string &table, const std::string &as) { transition_to(state_t::QUERY_FROM); - query_parts_.emplace_back(dialect_.token_at(dialect::token_t::TABLE) + " " + (as.empty() ? "" : as + " ")); + query_parts_.emplace_back(" " + dialect_.token_at(dialect::token_t::FROM) + " " + dialect_.prepare_identifier(table) + (as.empty() ? "" : " " + as)); return *this; } +query_builder &query_builder::set(std::initializer_list key_values) { + transition_to(state_t::QUERY_SET); + + query_parts_.emplace_back(" " + dialect_.token_at(dialect::token_t::SET) + " "); + + std::string result; + if (key_values.size() < 2) { + for (const auto &col : key_values) { + result.append(dialect_.prepare_identifier(col.name()) + "="); + auto var = col.value(); + std::visit(value_to_string_, var); + result.append(value_to_string_.result); + } + } else { + auto it = key_values.begin(); + result.append(dialect_.prepare_identifier(it->name()) + "="); + auto var = (it++)->value(); + std::visit(value_to_string_, var); + result.append(value_to_string_.result); + for (; it != key_values.end(); ++it) { + result.append(", "); + result.append(dialect_.prepare_identifier((*it).name()) + "="); + var = it->value(); + std::visit(value_to_string_, var); + result.append(value_to_string_.result); + } + } + + query_parts_.emplace_back(result); + return *this; +} + +query_builder &query_builder::where(const basic_condition &cond) { + transition_to(state_t::QUERY_WHERE); + + query_parts_.emplace_back(" " + dialect_.token_at(dialect::token_t::WHERE) + " "); + query_parts_.emplace_back(cond.evaluate(const_cast(dialect_))); + + return *this; +} + + std::string query_builder::compile() { std::string result; for (const auto &part : query_parts_) { @@ -133,4 +300,19 @@ void query_builder::initialize(query_builder::command_t cmd, query_builder::stat query_parts_.clear(); } +std::string build_create_column(const column &col, const dialect &d) { + std::string result = d.prepare_identifier(col.name()) + " " + d.data_type_at(col.type()); + if (col.attributes().size() > 0) { + result.append("(" + std::to_string(col.attributes().size()) +")"); + } + if (is_constraint_set(col.attributes().options(), utils::constraints::NOT_NULL)) { + result.append(" NOT NULL"); + } + if (is_constraint_set(col.attributes().options(), utils::constraints::PRIMARY_KEY)) { + result.append(" PRIMARY KEY"); + } + + return result; +} + } \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..a910c6c --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,13 @@ +Include(FetchContent) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.4.0 # or a later release +) + +FetchContent_MakeAvailable(Catch2) + +add_executable(tests builder.cpp) +target_link_libraries(tests PRIVATE Catch2::Catch2WithMain matador) +target_include_directories(tests PUBLIC $/include) \ No newline at end of file diff --git a/test/builder.cpp b/test/builder.cpp new file mode 100644 index 0000000..9efe6e7 --- /dev/null +++ b/test/builder.cpp @@ -0,0 +1,77 @@ +#include + +#include +#include +#include +#include + +using namespace matador::sql; + +TEST_CASE("Create table", "[query]") { + dialect d; + query_builder query(d); + const auto sql = query.create().table("person", { + make_pk_column("id"), + make_column("name", 255), + make_column("age") + }).compile(); + + REQUIRE(sql == R"##(CREATE TABLE "person" ("id" BIGINT NOT NULL PRIMARY KEY, "name" VARCHAR(255), "age" INTEGER))##"); +} + +TEST_CASE("Drop table", "[query]") { + dialect d; + query_builder query(d); + const auto sql = query.drop().table("person").compile(); + + REQUIRE(sql == R"(DROP TABLE "person")"); +} + +TEST_CASE("Select", "[query]") { + dialect d; + query_builder query(d); + const auto sql = query.select({"id", "name", "age"}).from("person").compile(); + + REQUIRE(sql == R"(SELECT "id", "name", "age" FROM "person")"); +} + +TEST_CASE("Insert", "[query]") { + dialect d; + query_builder query(d); + const auto sql = query.insert().into("person", { + "id", "name", "age" + }).values({7UL, "george", 65U}).compile(); + + REQUIRE(sql == R"(INSERT INTO "person" ("id", "name", "age") VALUES (7, 'george', 65))"); +} + +TEST_CASE("Update", "[query]") { + dialect d; + query_builder query(d); + const auto sql = query.update("person").set({ + {"id", 7UL}, + {"name", "george"}, + {"age", 65U} + }).compile(); + + REQUIRE(sql == R"(UPDATE "person" SET "id"=7, "name"='george', "age"=65)"); +} + +TEST_CASE("Delete", "[query]") { + dialect d; + query_builder query(d); + const auto sql = query.remove().from("person").compile(); + + REQUIRE(sql == R"(DELETE FROM "person")"); +} + +TEST_CASE("Where", "[query]") { + dialect d; + query_builder query(d); + const auto sql = query.select({"id", "name", "age"}) + .from("person") + .where("id"_col == 8) + .compile(); + + REQUIRE(sql == R"(SELECT "id", "name", "age" FROM "person" WHERE "id" = 8)"); +} \ No newline at end of file