mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-07-04 03:32:57 -04:00
346e2b2514
# Description of Changes - Added a query builder for C++ module bindings - Added query-builder table/filter/join types - Added semijoin support with compile-time checks for lookup-table and indexed-field usage - Added support for returning query-builder queries from C++ views - Hooked query-builder metadata into the C++ table/view macros and V10 module-def path - Added test coverage for the new C++ query-builder behavior - Compile tests for pass/fail cases - SQL tests for generated query output - Added a C++ test module for view primary key coverage - **Update:** Switched the core to pass the columns and index-columns metadata with the table source for better client-side codegen to have some shared code between server + client. # API and ABI breaking changes - No intended API or ABI breaking changes - Adds a new public query-builder API to the C++ bindings - C++ views can now return query-builder query types in addition to materialized row results # Expected complexity level and risk 3 - Mostly contained to C++ bindings, but it touches macros, view registration/serialization, and module-def generation, so there are a few places where the pieces need to stay in sync. # Testing I've done end to end testing of I think every type as well as built some tests to confirm the SQL output. - [x] Run the C++ query-builder SQL tests [crates/bindings-cpp/tests/query-builder-compile/run_query_builder_compile_tests.sh] - [x] Smoke test a generated C++ module using query-builder views --------- Signed-off-by: Jason Larabie <jason@clockworklabs.io> Co-authored-by: Ryan <r.ekhoff@clockworklabs.io> Co-authored-by: Zeke Foppa <196249+bfops@users.noreply.github.com>
332 lines
12 KiB
C++
332 lines
12 KiB
C++
#pragma once
|
|
|
|
#include "spacetimedb/bsatn/traits.h"
|
|
#include "spacetimedb/query_builder/expr.h"
|
|
#include <cstdio>
|
|
#include <concepts>
|
|
#include <string>
|
|
#include <type_traits>
|
|
#include <utility>
|
|
|
|
#ifndef SPACETIMEDB_QUERY_BUILDER_ENABLE_BSATN
|
|
#define SPACETIMEDB_QUERY_BUILDER_ENABLE_BSATN 1
|
|
#endif
|
|
|
|
#ifndef SPACETIMEDB_QUERY_BUILDER_ENABLE_INDEXED_WHERE
|
|
#define SPACETIMEDB_QUERY_BUILDER_ENABLE_INDEXED_WHERE 0
|
|
#endif
|
|
|
|
namespace SpacetimeDB::query_builder {
|
|
|
|
template<typename T>
|
|
struct query_row_type;
|
|
|
|
template<typename TRow, typename TCols, typename TIxCols>
|
|
class Table;
|
|
|
|
template<typename TRow, typename TCols, typename TIxCols>
|
|
class FromWhere;
|
|
|
|
template<typename T>
|
|
struct HasCols;
|
|
|
|
template<typename T>
|
|
struct HasIxCols;
|
|
|
|
template<typename T>
|
|
struct CanBeLookupTable : std::false_type {};
|
|
|
|
template<typename T>
|
|
using query_row_type_t = typename query_row_type<std::remove_cvref_t<T>>::type;
|
|
|
|
template<typename TRow>
|
|
class RawQuery {
|
|
public:
|
|
using row_type = TRow;
|
|
|
|
explicit RawQuery(std::string sql)
|
|
: sql_(std::move(sql)) {}
|
|
|
|
template<typename TQuery>
|
|
requires(!std::same_as<std::remove_cvref_t<TQuery>, RawQuery> &&
|
|
requires { typename query_row_type_t<TQuery>; } &&
|
|
std::same_as<query_row_type_t<TQuery>, TRow> &&
|
|
requires(TQuery&& query) { { std::forward<TQuery>(query).into_sql() } -> std::convertible_to<std::string>; })
|
|
RawQuery(TQuery&& query)
|
|
: sql_(std::forward<TQuery>(query).into_sql()) {}
|
|
|
|
[[nodiscard]] const std::string& sql() const { return sql_; }
|
|
[[nodiscard]] std::string into_sql() const { return sql_; }
|
|
|
|
private:
|
|
std::string sql_;
|
|
};
|
|
|
|
template<typename T>
|
|
concept QueryLike = requires(const T& query) {
|
|
{ query.into_sql() } -> std::convertible_to<std::string>;
|
|
};
|
|
|
|
template<typename T>
|
|
concept QueryBuilderReturn = requires {
|
|
typename query_row_type_t<T>;
|
|
} && QueryLike<std::remove_cvref_t<T>>;
|
|
|
|
namespace detail {
|
|
|
|
template<typename TRow>
|
|
struct row_tag {};
|
|
|
|
template<typename TFn, typename TCols>
|
|
constexpr void assert_where_predicate_is_column_only() {
|
|
static_assert(
|
|
std::is_invocable_v<TFn, const TCols&>,
|
|
"where() predicates must accept only table columns. Indexed columns are only available in semijoin predicates.");
|
|
}
|
|
|
|
inline std::false_type lookup_table_allowed(...);
|
|
|
|
template<typename TRow>
|
|
auto adl_lookup_table_allowed(int) -> decltype(lookup_table_allowed(row_tag<TRow>{}));
|
|
|
|
template<typename TRow>
|
|
std::false_type adl_lookup_table_allowed(...);
|
|
|
|
} // namespace detail
|
|
|
|
template<typename TRow>
|
|
inline constexpr bool can_be_lookup_row_v =
|
|
CanBeLookupTable<TRow>::value || decltype(detail::adl_lookup_table_allowed<TRow>(0))::value;
|
|
|
|
template<typename T>
|
|
inline constexpr bool can_be_lookup_table_v = CanBeLookupTable<std::remove_cvref_t<T>>::value;
|
|
|
|
template<typename TRow, typename TCols, typename TIxCols>
|
|
struct CanBeLookupTable<Table<TRow, TCols, TIxCols>> : std::bool_constant<can_be_lookup_row_v<TRow>> {};
|
|
|
|
template<typename TRow, typename TCols, typename TIxCols>
|
|
class Table {
|
|
public:
|
|
using row_type = TRow;
|
|
using cols_type = TCols;
|
|
using ix_cols_type = TIxCols;
|
|
|
|
constexpr Table(const char* table_name, TCols cols, TIxCols ix_cols)
|
|
: table_name_(table_name), cols_(std::move(cols)), ix_cols_(std::move(ix_cols)) {}
|
|
|
|
[[nodiscard]] constexpr const char* name() const { return table_name_; }
|
|
[[nodiscard]] constexpr const TCols& cols() const { return cols_; }
|
|
[[nodiscard]] constexpr const TIxCols& ix_cols() const { return ix_cols_; }
|
|
|
|
[[nodiscard]] RawQuery<TRow> build() const {
|
|
std::string sql;
|
|
sql.reserve(16 + std::char_traits<char>::length(table_name_));
|
|
sql += "SELECT * FROM \"";
|
|
sql += table_name_;
|
|
sql += "\"";
|
|
return RawQuery<TRow>(std::move(sql));
|
|
}
|
|
|
|
[[nodiscard]] std::string into_sql() const { return build().into_sql(); }
|
|
|
|
// `where` is the ergonomic entry point. Normal C++ predicates receive only
|
|
// columns; indexed columns are reserved for joins unless explicitly enabled.
|
|
template<typename TFn>
|
|
[[nodiscard]] auto where(TFn&& predicate) const {
|
|
if constexpr (SPACETIMEDB_QUERY_BUILDER_ENABLE_INDEXED_WHERE && std::is_invocable_v<TFn, const TCols&, const TIxCols&>) {
|
|
return where_ix(std::forward<TFn>(predicate));
|
|
} else {
|
|
detail::assert_where_predicate_is_column_only<TFn, TCols>();
|
|
return where_col(std::forward<TFn>(predicate));
|
|
}
|
|
}
|
|
|
|
template<typename TFn>
|
|
[[nodiscard]] auto Where(TFn&& predicate) const {
|
|
return where(std::forward<TFn>(predicate));
|
|
}
|
|
|
|
template<typename TFn>
|
|
[[nodiscard]] auto filter(TFn&& predicate) const {
|
|
return where(std::forward<TFn>(predicate));
|
|
}
|
|
|
|
template<typename TFn>
|
|
[[nodiscard]] auto Filter(TFn&& predicate) const {
|
|
return where(std::forward<TFn>(predicate));
|
|
}
|
|
|
|
template<typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
|
|
[[nodiscard]] auto left_semijoin(const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) const;
|
|
|
|
template<typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
|
|
[[nodiscard]] auto LeftSemijoin(const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) const {
|
|
return left_semijoin(right, std::forward<TFn>(predicate));
|
|
}
|
|
|
|
template<typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
|
|
[[nodiscard]] auto right_semijoin(const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) const;
|
|
|
|
template<typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
|
|
[[nodiscard]] auto RightSemijoin(const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) const {
|
|
return right_semijoin(right, std::forward<TFn>(predicate));
|
|
}
|
|
|
|
private:
|
|
template<typename TFn>
|
|
[[nodiscard]] auto where_col(TFn&& predicate) const {
|
|
auto expr = detail::make_bool_expr<TRow>(std::forward<TFn>(predicate)(cols_));
|
|
return FromWhere<TRow, TCols, TIxCols>(*this, std::move(expr));
|
|
}
|
|
|
|
template<typename TFn>
|
|
[[nodiscard]] auto where_ix(TFn&& predicate) const {
|
|
auto expr = detail::make_bool_expr<TRow>(std::forward<TFn>(predicate)(cols_, ix_cols_));
|
|
return FromWhere<TRow, TCols, TIxCols>(*this, std::move(expr));
|
|
}
|
|
|
|
const char* table_name_;
|
|
TCols cols_;
|
|
TIxCols ix_cols_;
|
|
};
|
|
|
|
template<typename TRow, typename TCols, typename TIxCols>
|
|
class FromWhere {
|
|
public:
|
|
using row_type = TRow;
|
|
using cols_type = TCols;
|
|
using ix_cols_type = TIxCols;
|
|
|
|
constexpr FromWhere(Table<TRow, TCols, TIxCols> table, BoolExpr<TRow> expr)
|
|
: table_(std::move(table)), expr_(std::move(expr)) {}
|
|
|
|
[[nodiscard]] constexpr const char* table_name() const { return table_.name(); }
|
|
[[nodiscard]] constexpr const Table<TRow, TCols, TIxCols>& table() const { return table_; }
|
|
[[nodiscard]] const BoolExpr<TRow>& expr() const { return expr_; }
|
|
|
|
[[nodiscard]] RawQuery<TRow> build() const {
|
|
std::string predicate = expr_.format();
|
|
std::string sql;
|
|
sql.reserve(24 + std::char_traits<char>::length(table_.name()) + predicate.size());
|
|
sql += "SELECT * FROM \"";
|
|
sql += table_.name();
|
|
sql += "\" WHERE ";
|
|
sql += predicate;
|
|
return RawQuery<TRow>(std::move(sql));
|
|
}
|
|
|
|
[[nodiscard]] std::string into_sql() const { return build().into_sql(); }
|
|
|
|
// `where` is the ergonomic entry point. Normal C++ predicates receive only
|
|
// columns; indexed columns are reserved for joins unless explicitly enabled.
|
|
template<typename TFn>
|
|
[[nodiscard]] FromWhere where(TFn&& predicate) const {
|
|
if constexpr (SPACETIMEDB_QUERY_BUILDER_ENABLE_INDEXED_WHERE && std::is_invocable_v<TFn, const TCols&, const TIxCols&>) {
|
|
return where_ix(std::forward<TFn>(predicate));
|
|
} else {
|
|
detail::assert_where_predicate_is_column_only<TFn, TCols>();
|
|
return where_col(std::forward<TFn>(predicate));
|
|
}
|
|
}
|
|
|
|
template<typename TFn>
|
|
[[nodiscard]] FromWhere Where(TFn&& predicate) const {
|
|
return where(std::forward<TFn>(predicate));
|
|
}
|
|
|
|
template<typename TFn>
|
|
[[nodiscard]] FromWhere filter(TFn&& predicate) const {
|
|
return where(std::forward<TFn>(predicate));
|
|
}
|
|
|
|
template<typename TFn>
|
|
[[nodiscard]] FromWhere Filter(TFn&& predicate) const {
|
|
return where(std::forward<TFn>(predicate));
|
|
}
|
|
|
|
template<typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
|
|
[[nodiscard]] auto left_semijoin(const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) const;
|
|
|
|
template<typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
|
|
[[nodiscard]] auto LeftSemijoin(const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) const {
|
|
return left_semijoin(right, std::forward<TFn>(predicate));
|
|
}
|
|
|
|
template<typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
|
|
[[nodiscard]] auto right_semijoin(const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) const;
|
|
|
|
template<typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
|
|
[[nodiscard]] auto RightSemijoin(const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) const {
|
|
return right_semijoin(right, std::forward<TFn>(predicate));
|
|
}
|
|
|
|
private:
|
|
template<typename TFn>
|
|
[[nodiscard]] FromWhere where_col(TFn&& predicate) const {
|
|
auto extra = detail::make_bool_expr<TRow>(std::forward<TFn>(predicate)(table_.cols()));
|
|
return FromWhere(table_, expr_.and_(extra));
|
|
}
|
|
|
|
template<typename TFn>
|
|
[[nodiscard]] FromWhere where_ix(TFn&& predicate) const {
|
|
auto extra = detail::make_bool_expr<TRow>(std::forward<TFn>(predicate)(table_.cols(), table_.ix_cols()));
|
|
return FromWhere(table_, expr_.and_(extra));
|
|
}
|
|
|
|
Table<TRow, TCols, TIxCols> table_;
|
|
BoolExpr<TRow> expr_;
|
|
};
|
|
|
|
template<typename TRow>
|
|
struct query_row_type<RawQuery<TRow>> {
|
|
using type = TRow;
|
|
};
|
|
|
|
template<typename TRow, typename TCols, typename TIxCols>
|
|
struct query_row_type<Table<TRow, TCols, TIxCols>> {
|
|
using type = TRow;
|
|
};
|
|
|
|
template<typename TRow, typename TCols, typename TIxCols>
|
|
struct query_row_type<FromWhere<TRow, TCols, TIxCols>> {
|
|
using type = TRow;
|
|
};
|
|
|
|
} // namespace SpacetimeDB::query_builder
|
|
|
|
#if SPACETIMEDB_QUERY_BUILDER_ENABLE_BSATN
|
|
namespace SpacetimeDB::bsatn {
|
|
|
|
template<typename TRow>
|
|
struct algebraic_type_of<::SpacetimeDB::query_builder::RawQuery<TRow>> {
|
|
static AlgebraicType get() {
|
|
std::vector<ProductTypeElement> elements;
|
|
elements.emplace_back(std::string("__query__"), algebraic_type_of<TRow>::get());
|
|
return AlgebraicType::make_product(std::make_unique<ProductType>(std::move(elements)));
|
|
}
|
|
};
|
|
|
|
template<typename TRow>
|
|
struct bsatn_traits<::SpacetimeDB::query_builder::RawQuery<TRow>> {
|
|
static void serialize(Writer&, const ::SpacetimeDB::query_builder::RawQuery<TRow>&) {
|
|
std::fputs("SpacetimeDB bindings-cpp internal error: attempted to BSATN-serialize query_builder::RawQuery. "
|
|
"RawQuery is only valid as a view return type and should not be serialized directly.\n",
|
|
stderr);
|
|
std::abort();
|
|
}
|
|
|
|
static ::SpacetimeDB::query_builder::RawQuery<TRow> deserialize(Reader&) {
|
|
std::fputs("SpacetimeDB bindings-cpp internal error: attempted to BSATN-deserialize query_builder::RawQuery. "
|
|
"RawQuery should only appear in query-view metadata handling.\n",
|
|
stderr);
|
|
std::abort();
|
|
}
|
|
|
|
static AlgebraicType algebraic_type() {
|
|
return algebraic_type_of<::SpacetimeDB::query_builder::RawQuery<TRow>>::get();
|
|
}
|
|
};
|
|
|
|
} // namespace SpacetimeDB::bsatn
|
|
#endif
|