Expand negative testing and fixed issues around brittle checks

This commit is contained in:
Jason Larabie
2026-04-30 13:26:36 -07:00
parent cea807bf08
commit 00fb728388
7 changed files with 344 additions and 4 deletions
@@ -21,6 +21,14 @@ namespace SpacetimeDB::query_builder {
template<typename TRow, typename TValue>
class Col;
template<typename T>
struct is_col : std::false_type {};
template<typename TValue, typename TRhs>
inline constexpr bool is_rhs_for_value_v =
std::is_same_v<TValue, std::remove_cvref_t<TRhs>> ||
(std::is_same_v<TValue, std::string> && std::is_convertible_v<TRhs, std::string_view>);
template<typename TRow>
class ColumnRef {
public:
@@ -314,30 +322,194 @@ public:
: column_(table_name, column_name) {}
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> eq(const TRhs& rhs) const { return compare(BoolExpr<TRow>::Kind::Eq, rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> Eq(const TRhs& rhs) const { return eq(rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> ne(const TRhs& rhs) const { return compare(BoolExpr<TRow>::Kind::Ne, rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> Ne(const TRhs& rhs) const { return ne(rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> gt(const TRhs& rhs) const { return compare(BoolExpr<TRow>::Kind::Gt, rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> Gt(const TRhs& rhs) const { return gt(rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> lt(const TRhs& rhs) const { return compare(BoolExpr<TRow>::Kind::Lt, rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> Lt(const TRhs& rhs) const { return lt(rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> gte(const TRhs& rhs) const { return compare(BoolExpr<TRow>::Kind::Gte, rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> Gte(const TRhs& rhs) const { return gte(rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> lte(const TRhs& rhs) const { return compare(BoolExpr<TRow>::Kind::Lte, rhs); }
template<typename TRhs>
requires(is_rhs_for_value_v<TValue, TRhs>)
[[nodiscard]] BoolExpr<TRow> 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<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto eq(const TRhs&) const {
static_assert(is_rhs_for_value_v<TValue, TRhs>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto Eq(const TRhs& rhs) const { return eq(rhs); }
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto ne(const TRhs&) const {
static_assert(is_rhs_for_value_v<TValue, TRhs>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto Ne(const TRhs& rhs) const { return ne(rhs); }
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto gt(const TRhs&) const {
static_assert(is_rhs_for_value_v<TValue, TRhs>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto Gt(const TRhs& rhs) const { return gt(rhs); }
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto lt(const TRhs&) const {
static_assert(is_rhs_for_value_v<TValue, TRhs>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto Lt(const TRhs& rhs) const { return lt(rhs); }
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto gte(const TRhs&) const {
static_assert(is_rhs_for_value_v<TValue, TRhs>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto Gte(const TRhs& rhs) const { return gte(rhs); }
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto lte(const TRhs&) const {
static_assert(is_rhs_for_value_v<TValue, TRhs>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TRhs>
requires(!is_rhs_for_value_v<TValue, TRhs> && !is_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] auto Lte(const TRhs& rhs) const { return lte(rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> eq(const Col<TRow, TOtherValue>& rhs) const { return compare(BoolExpr<TRow>::Kind::Eq, rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> Eq(const Col<TRow, TOtherValue>& rhs) const { return eq(rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> ne(const Col<TRow, TOtherValue>& rhs) const { return compare(BoolExpr<TRow>::Kind::Ne, rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> Ne(const Col<TRow, TOtherValue>& rhs) const { return ne(rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> gt(const Col<TRow, TOtherValue>& rhs) const { return compare(BoolExpr<TRow>::Kind::Gt, rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> Gt(const Col<TRow, TOtherValue>& rhs) const { return gt(rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> lt(const Col<TRow, TOtherValue>& rhs) const { return compare(BoolExpr<TRow>::Kind::Lt, rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> Lt(const Col<TRow, TOtherValue>& rhs) const { return lt(rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> gte(const Col<TRow, TOtherValue>& rhs) const { return compare(BoolExpr<TRow>::Kind::Gte, rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> Gte(const Col<TRow, TOtherValue>& rhs) const { return gte(rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> lte(const Col<TRow, TOtherValue>& rhs) const { return compare(BoolExpr<TRow>::Kind::Lte, rhs); }
template<typename TOtherValue>
requires(std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] BoolExpr<TRow> Lte(const Col<TRow, TOtherValue>& 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<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto eq(const Col<TRow, TOtherValue>&) const {
static_assert(std::is_same_v<TValue, TOtherValue>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto Eq(const Col<TRow, TOtherValue>& rhs) const { return eq(rhs); }
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto ne(const Col<TRow, TOtherValue>&) const {
static_assert(std::is_same_v<TValue, TOtherValue>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto Ne(const Col<TRow, TOtherValue>& rhs) const { return ne(rhs); }
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto gt(const Col<TRow, TOtherValue>&) const {
static_assert(std::is_same_v<TValue, TOtherValue>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto Gt(const Col<TRow, TOtherValue>& rhs) const { return gt(rhs); }
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto lt(const Col<TRow, TOtherValue>&) const {
static_assert(std::is_same_v<TValue, TOtherValue>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto Lt(const Col<TRow, TOtherValue>& rhs) const { return lt(rhs); }
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto gte(const Col<TRow, TOtherValue>&) const {
static_assert(std::is_same_v<TValue, TOtherValue>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto Gte(const Col<TRow, TOtherValue>& rhs) const { return gte(rhs); }
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto lte(const Col<TRow, TOtherValue>&) const {
static_assert(std::is_same_v<TValue, TOtherValue>, "Column comparison requires both sides to have the same value type.");
return BoolExpr<TRow>::always(false);
}
template<typename TOtherValue>
requires(!std::is_same_v<TValue, TOtherValue>)
[[nodiscard]] auto Lte(const Col<TRow, TOtherValue>& rhs) const { return lte(rhs); }
[[nodiscard]] constexpr const ColumnRef<TRow>& column_ref() const { return column_; }
private:
@@ -349,6 +521,9 @@ private:
ColumnRef<TRow> column_;
};
template<typename TRow, typename TValue>
struct is_col<Col<TRow, TValue>> : std::true_type {};
namespace detail {
template<typename TRow, typename TValue>
@@ -13,6 +13,12 @@ struct IxJoinEq;
template<typename TRow, auto MemberPtr>
struct member_tag {};
template<typename T>
struct is_ix_col : std::false_type {};
template<typename T>
struct is_ix_join_eq : std::false_type {};
inline std::false_type indexed_member_lookup(...);
template<typename TRow, auto MemberPtr>
@@ -36,12 +42,27 @@ public:
return eq(rhs);
}
// Keep mismatched indexed-column comparisons on a dedicated overload so they
// fail here with a clear diagnostic instead of falling through to BoolExpr.
template<typename TOtherRow, typename TOtherValue>
[[nodiscard]] auto eq(const IxCol<TOtherRow, TOtherValue>&) const {
static_assert(std::is_same_v<TValue, TOtherValue>, "Semijoin indexed equality requires both sides to have the same value type.");
return IxJoinEq<TRow, TOtherRow, TValue>{};
}
template<typename TOtherRow, typename TOtherValue>
[[nodiscard]] auto Eq(const IxCol<TOtherRow, TOtherValue>& rhs) const {
return eq(rhs);
}
template<typename TRhs>
requires(!is_ix_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] BoolExpr<TRow> eq(const TRhs& rhs) const {
return compare(BoolExpr<TRow>::Kind::Eq, rhs);
}
template<typename TRhs>
requires(!is_ix_col<std::remove_cvref_t<TRhs>>::value)
[[nodiscard]] BoolExpr<TRow> Eq(const TRhs& rhs) const { return eq(rhs); }
[[nodiscard]] constexpr const ColumnRef<TRow>& column_ref() const { return column_; }
@@ -58,6 +79,9 @@ private:
friend class IxCol;
};
template<typename TRow, typename TValue>
struct is_ix_col<IxCol<TRow, TValue>> : std::true_type {};
namespace detail {
template<typename, typename TRow, auto MemberPtr>
@@ -71,6 +95,9 @@ struct IxJoinEq {
ColumnRef<TRightRow> rhs;
};
template<typename TLeftRow, typename TRightRow, typename TValue>
struct is_ix_join_eq<IxJoinEq<TLeftRow, TRightRow, TValue>> : std::true_type {};
template<typename TLeftRow, typename TLeftCols, typename TLeftIxCols, typename TRightRow, typename TRightCols, typename TRightIxCols>
class LeftSemiJoin {
public:
@@ -269,28 +296,52 @@ template<typename TLeftRow, typename TLeftCols, typename TLeftIxCols, typename T
[[nodiscard]] auto left_semijoin_impl(const Table<TLeftRow, TLeftCols, TLeftIxCols>& left, const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) {
static_assert(can_be_lookup_table_v<Table<TRightRow, TRightCols, TRightIxCols>>, "Lookup side of a semijoin must opt in via CanBeLookupTable.");
const auto join = std::forward<TFn>(predicate)(left.ix_cols(), right.ix_cols());
return LeftSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left, right, join.lhs, join.rhs);
using TJoin = std::remove_cvref_t<decltype(join)>;
if constexpr (is_ix_join_eq<TJoin>::value) {
return LeftSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left, right, join.lhs, join.rhs);
} else {
static_assert(is_ix_join_eq<TJoin>::value, "Semijoin predicate must compare two indexed columns with eq().");
return LeftSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left, right, {}, {});
}
}
template<typename TLeftRow, typename TLeftCols, typename TLeftIxCols, typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
[[nodiscard]] auto left_semijoin_impl(const FromWhere<TLeftRow, TLeftCols, TLeftIxCols>& left, const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) {
static_assert(can_be_lookup_table_v<Table<TRightRow, TRightCols, TRightIxCols>>, "Lookup side of a semijoin must opt in via CanBeLookupTable.");
const auto join = std::forward<TFn>(predicate)(left.table().ix_cols(), right.ix_cols());
return LeftSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left.table(), right, join.lhs, join.rhs, left.expr());
using TJoin = std::remove_cvref_t<decltype(join)>;
if constexpr (is_ix_join_eq<TJoin>::value) {
return LeftSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left.table(), right, join.lhs, join.rhs, left.expr());
} else {
static_assert(is_ix_join_eq<TJoin>::value, "Semijoin predicate must compare two indexed columns with eq().");
return LeftSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left.table(), right, {}, {}, left.expr());
}
}
template<typename TLeftRow, typename TLeftCols, typename TLeftIxCols, typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
[[nodiscard]] auto right_semijoin_impl(const Table<TLeftRow, TLeftCols, TLeftIxCols>& left, const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) {
static_assert(can_be_lookup_table_v<Table<TRightRow, TRightCols, TRightIxCols>>, "Lookup side of a semijoin must opt in via CanBeLookupTable.");
const auto join = std::forward<TFn>(predicate)(left.ix_cols(), right.ix_cols());
return RightSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left, right, join.lhs, join.rhs);
using TJoin = std::remove_cvref_t<decltype(join)>;
if constexpr (is_ix_join_eq<TJoin>::value) {
return RightSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left, right, join.lhs, join.rhs);
} else {
static_assert(is_ix_join_eq<TJoin>::value, "Semijoin predicate must compare two indexed columns with eq().");
return RightSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left, right, {}, {});
}
}
template<typename TLeftRow, typename TLeftCols, typename TLeftIxCols, typename TRightRow, typename TRightCols, typename TRightIxCols, typename TFn>
[[nodiscard]] auto right_semijoin_impl(const FromWhere<TLeftRow, TLeftCols, TLeftIxCols>& left, const Table<TRightRow, TRightCols, TRightIxCols>& right, TFn&& predicate) {
static_assert(can_be_lookup_table_v<Table<TRightRow, TRightCols, TRightIxCols>>, "Lookup side of a semijoin must opt in via CanBeLookupTable.");
const auto join = std::forward<TFn>(predicate)(left.table().ix_cols(), right.ix_cols());
return RightSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left.table(), right, join.lhs, join.rhs, left.expr());
using TJoin = std::remove_cvref_t<decltype(join)>;
if constexpr (is_ix_join_eq<TJoin>::value) {
return RightSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left.table(), right, join.lhs, join.rhs, left.expr());
} else {
static_assert(is_ix_join_eq<TJoin>::value, "Semijoin predicate must compare two indexed columns with eq().");
return RightSemiJoin<TLeftRow, TLeftCols, TLeftIxCols, TRightRow, TRightCols, TRightIxCols>(left.table(), right, {}, {}, left.expr());
}
}
} // namespace detail
@@ -0,0 +1,21 @@
#include <spacetimedb.h>
using namespace SpacetimeDB;
template<typename TRow>
auto TableFor(const char* table_name) {
return QueryBuilder{}.table<TRow>(
table_name,
query_builder::HasCols<TRow>::get(table_name),
query_builder::HasIxCols<TRow>::get(table_name));
}
struct PlayerInfo {
uint8_t age;
};
SPACETIMEDB_STRUCT(PlayerInfo, age)
SPACETIMEDB_TABLE(PlayerInfo, player_info, Public)
auto invalid_filter = TableFor<PlayerInfo>("player_info").where([](const auto& players) {
return players.age.eq(4200);
});
@@ -0,0 +1,33 @@
#include <spacetimedb.h>
using namespace SpacetimeDB;
template<typename TRow>
auto TableFor(const char* table_name) {
return QueryBuilder{}.table<TRow>(
table_name,
query_builder::HasCols<TRow>::get(table_name),
query_builder::HasIxCols<TRow>::get(table_name));
}
struct User {
Identity identity;
};
SPACETIMEDB_STRUCT(User, identity)
SPACETIMEDB_TABLE(User, user, Public)
FIELD_PrimaryKey(user, identity)
struct Membership {
Identity membership_identity;
uint64_t tenant_id;
};
SPACETIMEDB_STRUCT(Membership, membership_identity, tenant_id)
SPACETIMEDB_TABLE(Membership, membership, Public)
FIELD_PrimaryKey(membership, membership_identity)
FIELD_Index(membership, tenant_id)
auto invalid_join = TableFor<User>("user").right_semijoin(
TableFor<Membership>("membership"),
[](const auto& users, const auto& memberships) {
return users.identity.eq(memberships.tenant_id);
});
@@ -0,0 +1,23 @@
#include <spacetimedb.h>
using namespace SpacetimeDB;
template<typename TRow>
auto TableFor(const char* table_name) {
return QueryBuilder{}.table<TRow>(
table_name,
query_builder::HasCols<TRow>::get(table_name),
query_builder::HasIxCols<TRow>::get(table_name));
}
struct User {
Identity identity;
uint64_t tenant_id;
};
SPACETIMEDB_STRUCT(User, identity, tenant_id)
SPACETIMEDB_TABLE(User, user, Public)
FIELD_PrimaryKey(user, identity)
auto invalid_filter = TableFor<User>("user").where([](const auto& users) {
return users.identity.eq(users.tenant_id);
});
@@ -0,0 +1,33 @@
#include <spacetimedb.h>
using namespace SpacetimeDB;
template<typename TRow>
auto TableFor(const char* table_name) {
return QueryBuilder{}.table<TRow>(
table_name,
query_builder::HasCols<TRow>::get(table_name),
query_builder::HasIxCols<TRow>::get(table_name));
}
struct User {
uint64_t id;
};
SPACETIMEDB_STRUCT(User, id)
SPACETIMEDB_TABLE(User, user, Public)
FIELD_PrimaryKey(user, id)
struct Membership {
uint64_t id;
uint64_t user_id;
};
SPACETIMEDB_STRUCT(Membership, id, user_id)
SPACETIMEDB_TABLE(Membership, membership, Public)
FIELD_PrimaryKey(membership, id)
FIELD_Index(membership, user_id)
auto invalid_join = TableFor<User>("user").right_semijoin(
TableFor<Membership>("membership"),
[](const auto& users, const auto& memberships) {
return users.id.eq(1ULL);
});
@@ -95,7 +95,11 @@ compile_should_fail() {
}
compile_should_pass "$SCRIPT_DIR/pass_query_integration.cpp"
compile_should_fail "$SCRIPT_DIR/fail_invalid_join_predicate.cpp" "Semijoin predicate must compare two indexed columns with eq()."
compile_should_fail "$SCRIPT_DIR/fail_incompatible_where_types.cpp" "Column comparison requires both sides to have the same value type."
compile_should_fail "$SCRIPT_DIR/fail_implicit_numeric_where_types.cpp" "Column comparison requires both sides to have the same value type."
compile_should_fail "$SCRIPT_DIR/fail_non_index_join.cpp" "no member named 'tenant_id'"
compile_should_fail "$SCRIPT_DIR/fail_incompatible_join_types.cpp" "Semijoin indexed equality requires both sides to have the same value type."
compile_should_fail "$SCRIPT_DIR/fail_event_lookup.cpp" "Lookup side of a semijoin must opt in via CanBeLookupTable."
echo "All query-builder compile tests passed"