#pragma once #include "spacetimedb/bsatn/timestamp.h" #include "spacetimedb/bsatn/types.h" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace SpacetimeDB::query_builder { template class Col; template struct is_col : std::false_type {}; template inline constexpr bool is_rhs_for_value_v = std::is_same_v> || (std::is_same_v && std::is_convertible_v); template class ColumnRef { public: constexpr ColumnRef() : table_name_(""), column_name_("") {} constexpr ColumnRef(const char* table_name, const char* column_name) : table_name_(table_name), column_name_(column_name) {} [[nodiscard]] std::string format() const { return "\"" + std::string(table_name_) + "\".\"" + std::string(column_name_) + "\""; } [[nodiscard]] constexpr const char* table_name() const { return table_name_; } [[nodiscard]] constexpr const char* column_name() const { return column_name_; } private: const char* table_name_; const char* column_name_; }; namespace detail { inline std::string quote_string(std::string_view value) { std::string escaped; escaped.reserve(value.size() + 2); escaped.push_back('\''); for (char ch : value) { escaped.push_back(ch); if (ch == '\'') { escaped.push_back('\''); } } escaped.push_back('\''); return escaped; } inline std::string trim_timestamp_fraction(std::string value) { // Keep this in sync with the current Timestamp::to_string() UTC form. // If that representation changes away from a +00:00 / Z suffix, revisit this trimming logic. const std::size_t plus = value.rfind("+00:00"); const std::size_t z = value.rfind('Z'); const std::size_t dot = value.find('.'); const std::size_t suffix = plus != std::string::npos ? plus : z; if (suffix == std::string::npos || dot == std::string::npos || dot > suffix) { return value; } std::size_t trim = suffix; while (trim > dot + 1 && value[trim - 1] == '0') { --trim; } if (trim == dot + 1) { value.erase(dot, suffix - dot); } else { value.erase(trim, suffix - trim); } return value; } inline std::string literal_sql(const std::string& value) { return quote_string(value); } inline std::string literal_sql(std::string_view value) { return quote_string(value); } inline std::string literal_sql(const char* value) { return quote_string(value == nullptr ? "" : value); } inline std::string literal_sql(bool value) { return value ? "TRUE" : "FALSE"; } inline std::string literal_sql(const Identity& value) { return "0x" + value.to_hex_string(); } inline std::string literal_sql(const ConnectionId& value) { return "0x" + value.to_string(); } inline std::string literal_sql(const Timestamp& value) { return quote_string(trim_timestamp_fraction(value.to_string())); } inline std::string literal_sql(const TimeDuration&) = delete; inline std::string literal_sql(const std::vector& value) { std::ostringstream out; out << "0x" << std::hex << std::setfill('0'); for (uint8_t byte : value) { out << std::setw(2) << static_cast(byte); } return out.str(); } inline std::string literal_sql(const u128& value) { return value.to_string(); } inline std::string literal_sql(const i128& value) { return value.to_string(); } inline std::string literal_sql(const u256& value) { return value.to_string(); } inline std::string literal_sql(const i256& value) { return value.to_string(); } template inline std::string format_floating_point(TFloat value) { char buffer[64]; const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value, std::chars_format::general); if (result.ec == std::errc{}) { return std::string(buffer, result.ptr); } std::ostringstream out; out.imbue(std::locale::classic()); out << std::setprecision(std::numeric_limits::max_digits10); out << value; return out.str(); } inline std::string literal_sql(float value) { return format_floating_point(value); } inline std::string literal_sql(double value) { return format_floating_point(value); } template std::string literal_sql(const TValue& value) requires(std::is_integral_v && !std::is_same_v, bool>) { return std::to_string(value); } template class Operand { public: static Operand column(ColumnRef column) { return Operand(std::move(column)); } static Operand literal(std::string sql) { return Operand(std::move(sql)); } [[nodiscard]] std::string format() const { return std::holds_alternative>(value_) ? std::get>(value_).format() : std::get(value_); } private: explicit Operand(ColumnRef column) : value_(std::move(column)) {} explicit Operand(std::string sql) : value_(std::move(sql)) {} std::variant, std::string> value_; }; template Operand to_operand(const Col& column); template Operand to_operand(const TValue& value) { return Operand::literal(literal_sql(value)); } } // namespace detail template class BoolExpr { public: enum class Kind { Eq, Ne, Gt, Lt, Gte, Lte, And, Or, Not, }; static BoolExpr compare(Kind kind, detail::Operand lhs, detail::Operand rhs) { return BoolExpr(std::make_shared(kind, std::move(lhs), std::move(rhs))); } static BoolExpr always(bool value) { return compare( Kind::Eq, detail::Operand::literal(value ? "TRUE" : "FALSE"), detail::Operand::literal("TRUE")); } [[nodiscard]] std::string format() const { return format_node(root_); } [[nodiscard]] BoolExpr and_(const BoolExpr& other) const { return BoolExpr(std::make_shared(Kind::And, root_, other.root_)); } [[nodiscard]] BoolExpr And(const BoolExpr& other) const { return and_(other); } [[nodiscard]] BoolExpr or_(const BoolExpr& other) const { return BoolExpr(std::make_shared(Kind::Or, root_, other.root_)); } [[nodiscard]] BoolExpr Or(const BoolExpr& other) const { return or_(other); } [[nodiscard]] BoolExpr not_() const { return BoolExpr(std::make_shared(Kind::Not, root_, nullptr)); } [[nodiscard]] BoolExpr Not() const { return not_(); } private: struct Node; struct CompareData { detail::Operand lhs; detail::Operand rhs; }; struct LogicData { std::shared_ptr left; std::shared_ptr right; }; struct NotData { std::shared_ptr child; }; struct Node { Node(Kind kind_in, detail::Operand lhs_in, detail::Operand rhs_in) : kind(kind_in), data(CompareData{std::move(lhs_in), std::move(rhs_in)}) {} Node(Kind kind_in, std::shared_ptr left_in, std::shared_ptr right_in) : kind(kind_in), data(kind_in == Kind::Not ? NodeData(NotData{std::move(left_in)}) : NodeData(LogicData{std::move(left_in), std::move(right_in)})) {} Kind kind; using NodeData = std::variant; NodeData data; }; explicit BoolExpr(std::shared_ptr root) : root_(std::move(root)) {} static std::string format_node(const std::shared_ptr& node) { switch (node->kind) { case Kind::Eq: { const auto& compare = std::get(node->data); return "(" + compare.lhs.format() + " = " + compare.rhs.format() + ")"; } case Kind::Ne: { const auto& compare = std::get(node->data); return "(" + compare.lhs.format() + " <> " + compare.rhs.format() + ")"; } case Kind::Gt: { const auto& compare = std::get(node->data); return "(" + compare.lhs.format() + " > " + compare.rhs.format() + ")"; } case Kind::Lt: { const auto& compare = std::get(node->data); return "(" + compare.lhs.format() + " < " + compare.rhs.format() + ")"; } case Kind::Gte: { const auto& compare = std::get(node->data); return "(" + compare.lhs.format() + " >= " + compare.rhs.format() + ")"; } case Kind::Lte: { const auto& compare = std::get(node->data); return "(" + compare.lhs.format() + " <= " + compare.rhs.format() + ")"; } case Kind::And: { const auto& logic = std::get(node->data); return "(" + format_node(logic.left) + " AND " + format_node(logic.right) + ")"; } case Kind::Or: { const auto& logic = std::get(node->data); return "(" + format_node(logic.left) + " OR " + format_node(logic.right) + ")"; } case Kind::Not: { const auto& not_data = std::get(node->data); return "(NOT " + format_node(not_data.child) + ")"; } } return {}; } std::shared_ptr root_; }; namespace detail { template BoolExpr make_bool_expr(BoolExpr expr) { return expr; } template BoolExpr make_bool_expr(bool value) { return BoolExpr::always(value); } template BoolExpr make_bool_expr(const Col& column) { return column.eq(true); } } // namespace detail template class Col { public: constexpr Col() = default; constexpr Col(const char* table_name, const char* column_name) : column_(table_name, column_name) {} template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr eq(const TRhs& rhs) const { return compare(BoolExpr::Kind::Eq, rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr Eq(const TRhs& rhs) const { return eq(rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr ne(const TRhs& rhs) const { return compare(BoolExpr::Kind::Ne, rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr Ne(const TRhs& rhs) const { return ne(rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr gt(const TRhs& rhs) const { return compare(BoolExpr::Kind::Gt, rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr Gt(const TRhs& rhs) const { return gt(rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr lt(const TRhs& rhs) const { return compare(BoolExpr::Kind::Lt, rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr Lt(const TRhs& rhs) const { return lt(rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr gte(const TRhs& rhs) const { return compare(BoolExpr::Kind::Gte, rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr Gte(const TRhs& rhs) const { return gte(rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr lte(const TRhs& rhs) const { return compare(BoolExpr::Kind::Lte, rhs); } template requires(is_rhs_for_value_v) [[nodiscard]] BoolExpr Lte(const TRhs& rhs) const { return lte(rhs); } // Keep incompatible non-column RHS values on a dedicated overload so they // fail with the same diagnostic shape as mismatched column comparisons. template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto eq(const TRhs&) const { static_assert(is_rhs_for_value_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto Eq(const TRhs& rhs) const { return eq(rhs); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto ne(const TRhs&) const { static_assert(is_rhs_for_value_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto Ne(const TRhs& rhs) const { return ne(rhs); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto gt(const TRhs&) const { static_assert(is_rhs_for_value_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto Gt(const TRhs& rhs) const { return gt(rhs); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto lt(const TRhs&) const { static_assert(is_rhs_for_value_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto Lt(const TRhs& rhs) const { return lt(rhs); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto gte(const TRhs&) const { static_assert(is_rhs_for_value_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto Gte(const TRhs& rhs) const { return gte(rhs); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto lte(const TRhs&) const { static_assert(is_rhs_for_value_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!is_rhs_for_value_v && !is_col>::value) [[nodiscard]] auto Lte(const TRhs& rhs) const { return lte(rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr eq(const Col& rhs) const { return compare(BoolExpr::Kind::Eq, rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr Eq(const Col& rhs) const { return eq(rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr ne(const Col& rhs) const { return compare(BoolExpr::Kind::Ne, rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr Ne(const Col& rhs) const { return ne(rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr gt(const Col& rhs) const { return compare(BoolExpr::Kind::Gt, rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr Gt(const Col& rhs) const { return gt(rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr lt(const Col& rhs) const { return compare(BoolExpr::Kind::Lt, rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr Lt(const Col& rhs) const { return lt(rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr gte(const Col& rhs) const { return compare(BoolExpr::Kind::Gte, rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr Gte(const Col& rhs) const { return gte(rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr lte(const Col& rhs) const { return compare(BoolExpr::Kind::Lte, rhs); } template requires(std::is_same_v) [[nodiscard]] BoolExpr Lte(const Col& rhs) const { return lte(rhs); } // Keep mismatched column-to-column comparisons on a dedicated overload so // they fail here with a clear diagnostic instead of disappearing into the // generic operand-conversion path. template requires(!std::is_same_v) [[nodiscard]] auto eq(const Col&) const { static_assert(std::is_same_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!std::is_same_v) [[nodiscard]] auto Eq(const Col& rhs) const { return eq(rhs); } template requires(!std::is_same_v) [[nodiscard]] auto ne(const Col&) const { static_assert(std::is_same_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!std::is_same_v) [[nodiscard]] auto Ne(const Col& rhs) const { return ne(rhs); } template requires(!std::is_same_v) [[nodiscard]] auto gt(const Col&) const { static_assert(std::is_same_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!std::is_same_v) [[nodiscard]] auto Gt(const Col& rhs) const { return gt(rhs); } template requires(!std::is_same_v) [[nodiscard]] auto lt(const Col&) const { static_assert(std::is_same_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!std::is_same_v) [[nodiscard]] auto Lt(const Col& rhs) const { return lt(rhs); } template requires(!std::is_same_v) [[nodiscard]] auto gte(const Col&) const { static_assert(std::is_same_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!std::is_same_v) [[nodiscard]] auto Gte(const Col& rhs) const { return gte(rhs); } template requires(!std::is_same_v) [[nodiscard]] auto lte(const Col&) const { static_assert(std::is_same_v, "Column comparison requires both sides to have the same value type."); return BoolExpr::always(false); } template requires(!std::is_same_v) [[nodiscard]] auto Lte(const Col& rhs) const { return lte(rhs); } [[nodiscard]] constexpr const ColumnRef& column_ref() const { return column_; } private: template [[nodiscard]] BoolExpr compare(typename BoolExpr::Kind kind, const TRhs& rhs) const { return BoolExpr::compare(kind, detail::to_operand(*this), detail::to_operand(rhs)); } ColumnRef column_; }; template struct is_col> : std::true_type {}; namespace detail { template Operand to_operand(const Col& column) { return Operand::column(column.column_ref()); } } // namespace detail } // namespace SpacetimeDB::query_builder