Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 89 additions & 82 deletions crates/bindings-cpp/include/spacetimedb/index_iterator.h
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,59 @@ class IndexIterator {
return writer.take_buffer();
}

template<typename... FieldTypes>
void init_prefix_range_scan(IndexId index_id, const std::tuple<FieldTypes...>& values) {
constexpr std::size_t N = sizeof...(FieldTypes);
constexpr std::size_t prefix_count = N - 1;

SpacetimeDB::bsatn::Writer prefix_writer;
serialize_tuple_prefix(prefix_writer, values, std::make_index_sequence<prefix_count>{});
auto prefix_buffer = prefix_writer.take_buffer();

const auto& range = std::get<N - 1>(values);
auto rstart_buffer = serialize_range_start(range);
auto rend_buffer = serialize_range_end(range);

Status status = FFI::datastore_index_scan_range_bsatn(
index_id,
prefix_buffer.data(), prefix_buffer.size(), ColId{static_cast<uint16_t>(prefix_count)},
rstart_buffer.data(), rstart_buffer.size(),
rend_buffer.data(), rend_buffer.size(),
&iter_handle_
);

if (status != StatusCode::OK) {
std::abort(); // IndexIterator: multi-column prefix+range scan failed
}
}

template<typename... FieldTypes>
void init_exact_prefix_scan(IndexId index_id, const std::tuple<FieldTypes...>& values) {
constexpr std::size_t N = sizeof...(FieldTypes);
constexpr std::size_t prefix_count = N - 1;

SpacetimeDB::bsatn::Writer prefix_writer;
serialize_tuple_prefix(prefix_writer, values, std::make_index_sequence<prefix_count>{});
auto prefix_buffer = prefix_writer.take_buffer();

SpacetimeDB::bsatn::Writer bound_writer;
bound_writer.write_u8(0); // Bound::Inclusive
serialize(bound_writer, std::get<N - 1>(values));
auto bound_buffer = bound_writer.take_buffer();

Status status = FFI::datastore_index_scan_range_bsatn(
index_id,
prefix_buffer.data(), prefix_buffer.size(), ColId{static_cast<uint16_t>(prefix_count)},
bound_buffer.data(), bound_buffer.size(),
bound_buffer.data(), bound_buffer.size(),
&iter_handle_
);

if (status != StatusCode::OK) {
std::abort(); // IndexIterator: multi-column exact prefix scan failed
}
}

/**
* @brief Create iterator for prefix-only match (N-1 columns specified)
*
Expand Down Expand Up @@ -245,97 +298,45 @@ class IndexIterator {
}

/**
* @brief Create iterator for prefix match with range on last column
*
* Finds all rows where the first N-1 columns match exactly and the last column
* falls within the specified range.
*
* @example Range on last column - find scores for a player at specific levels:
* @code
* FIELD_NamedMultiColumnIndex(score, by_player_and_level, player_id, level)
*
* // Find scores for player 123 at levels 1-10
* auto scores = ctx.db[score_by_player_and_level].filter(
* std::make_tuple(uint32_t(123), range_inclusive(1u, 10u))
* );
* @endcode
*/
template<typename PrefixType, typename RangeType>
IndexIterator(IndexId index_id, const std::tuple<PrefixType, RangeType>& values)
requires (is_range_v<RangeType>)
{
// Serialize prefix value
SpacetimeDB::bsatn::Writer prefix_writer;
serialize(prefix_writer, std::get<0>(values));
auto prefix_buffer = prefix_writer.take_buffer();

// Serialize range as start/end bounds
const auto& range = std::get<1>(values);
auto rstart_buffer = serialize_range_start(range);
auto rend_buffer = serialize_range_end(range);

// Call FFI with prefix_elems = 1 (only the prefix column)
Status status = FFI::datastore_index_scan_range_bsatn(
index_id,
prefix_buffer.data(), prefix_buffer.size(), ColId{1},
rstart_buffer.data(), rstart_buffer.size(),
rend_buffer.data(), rend_buffer.size(),
&iter_handle_
);

if (status != StatusCode::OK) {
std::abort(); // IndexIterator: prefix+range match failed
}
advance();
}

/**
* @brief Create iterator for multi-column exact match
* @brief Create iterator for multi-column exact prefix or prefix+range scan
*
* Efficiently finds all rows where all indexed columns exactly match the tuple values.
* Scans rows where all supplied prefix columns match exactly. If the final
* supplied tuple element is a Range<T>, that range is applied to the final
* supplied column.
*
* @tparam FieldTypes The types of the indexed fields
* @param index_id The multi-column index to scan
* @param values Tuple of values to match (one per column)
* @param values Tuple of exact values optionally terminated by Range<T>
*
* @example Multi-column exact match:
* @example Multi-column exact and range matches:
* @code
* FIELD_NamedMultiColumnIndex(score, by_player_and_level, player_id, level)
*
* // Find exact score for player 123 at level 5
* auto iter = IndexIterator<Score>(index_id, std::make_tuple(uint32_t(123), uint32_t(5)));
*
* // Find scores for player 123 at levels 1-10
* auto scores = IndexIterator<Score>(
* index_id,
* std::make_tuple(uint32_t(123), range_inclusive(1u, 10u))
* );
* @endcode
*/
template<typename... FieldTypes>
IndexIterator(IndexId index_id, const std::tuple<FieldTypes...>& values)
requires (sizeof...(FieldTypes) > 1 && sizeof...(FieldTypes) <= 6)
requires (sizeof...(FieldTypes) > 0 && sizeof...(FieldTypes) <= 6)
{
constexpr std::size_t N = sizeof...(FieldTypes);
constexpr std::size_t prefix_count = N - 1;

// Serialize first N-1 elements into prefix buffer
SpacetimeDB::bsatn::Writer prefix_writer;
serialize_tuple_prefix(prefix_writer, values, std::make_index_sequence<prefix_count>{});
auto prefix_buffer = prefix_writer.take_buffer();

// Serialize the last element as both start and end bounds (exact match)
SpacetimeDB::bsatn::Writer bound_writer;
bound_writer.write_u8(0); // Bound::Included
serialize(bound_writer, std::get<N - 1>(values)); // Last element only
auto bound_buffer = bound_writer.take_buffer();

// Call FFI with prefix_elems = N-1 (as per C# pattern)
Status status = FFI::datastore_index_scan_range_bsatn(
index_id,
prefix_buffer.data(), prefix_buffer.size(), ColId{static_cast<uint16_t>(prefix_count)},
bound_buffer.data(), bound_buffer.size(), // Last value as start
bound_buffer.data(), bound_buffer.size(), // Last value as end
&iter_handle_
static_assert(
detail::valid_index_scan_tuple_v<FieldTypes...>,
"Range<T> in a multi-column index filter must be the final supplied element"
);

if (status != StatusCode::OK) {
std::abort(); // IndexIterator: multi-column exact match failed

if constexpr (detail::last_is_range_arg_v<FieldTypes...>) {
init_prefix_range_scan(index_id, values);
} else {
init_exact_prefix_scan(index_id, values);
}

advance();
}

Expand Down Expand Up @@ -521,12 +522,12 @@ class IndexIterator {
// For now we rely on btree scan to handle bounds correctly

// Constants for performance tuning
static constexpr size_t INITIAL_ROW_BUFFER_SIZE = 4096;
static constexpr size_t INITIAL_ROW_BUFFER_SIZE = 128 * 1024;
static constexpr size_t MAX_ROW_BUFFER_SIZE = 1024 * 1024;
static constexpr size_t TYPICAL_BATCH_SIZE = 32;
static constexpr int16_t ITER_EXHAUSTED = -1;
static constexpr int16_t ITER_OK = 0;
static constexpr uint16_t ERROR_BUFFER_TOO_SMALL = 3;
static constexpr int16_t ERROR_BUFFER_TOO_SMALL = static_cast<int16_t>(StatusCode::BUFFER_TOO_SMALL.inner);

// Helper to serialize tuple elements without treating tuple as a type
template<typename... FieldTypes, std::size_t... Is>
Expand Down Expand Up @@ -586,13 +587,19 @@ class IndexIterator {
return;
}

if (ret == ERROR_BUFFER_TOO_SMALL) {
if (buffer_len > MAX_ROW_BUFFER_SIZE) {
while (ret == ERROR_BUFFER_TOO_SMALL) {
const size_t requested_size = buffer_len;
size_t new_size = requested_size;
if (new_size <= row_buffer_.size()) {
new_size = row_buffer_.size() * 2;
}
if (new_size > MAX_ROW_BUFFER_SIZE) {
std::abort(); // Buffer size exceeds maximum
}
row_buffer_.resize(buffer_len);
ret = FFI::row_iter_bsatn_advance(iter_handle_,
row_buffer_.data(),
row_buffer_.resize(new_size);
buffer_len = row_buffer_.size();
ret = FFI::row_iter_bsatn_advance(iter_handle_,
row_buffer_.data(),
&buffer_len);
}

Expand Down Expand Up @@ -710,4 +717,4 @@ class IndexIteratorRange {
} // namespace SpacetimeDB


#endif // SPACETIMEDB_INDEX_ITERATOR_H
#endif // SPACETIMEDB_INDEX_ITERATOR_H
28 changes: 27 additions & 1 deletion crates/bindings-cpp/include/spacetimedb/range_queries.h
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,32 @@ constexpr bool is_range_v = is_range<T>::value;
template<typename T>
concept RangeType = is_range_v<T>;

namespace detail {

template<typename T>
inline constexpr bool is_range_arg_v = is_range_v<std::remove_cvref_t<T>>;

template<typename... Ts>
inline constexpr bool contains_range_arg_v = (is_range_arg_v<Ts> || ...);

template<typename... Ts>
struct last_is_range_arg : std::false_type {};

template<typename T, typename... Rest>
struct last_is_range_arg<T, Rest...> : last_is_range_arg<Rest...> {};

template<typename T>
struct last_is_range_arg<T> : std::bool_constant<is_range_arg_v<T>> {};

template<typename... Ts>
inline constexpr bool last_is_range_arg_v = last_is_range_arg<Ts...>::value;

template<typename... Ts>
inline constexpr bool valid_index_scan_tuple_v =
!contains_range_arg_v<Ts...> || last_is_range_arg_v<Ts...>;

} // namespace detail

// =============================================================================
// Integration with field accessors
// =============================================================================
Expand Down Expand Up @@ -209,4 +235,4 @@ SPACETIMEDB_REDUCER(test_range_queries, ReducerContext ctx) {
// Count people in range
size_t count = ctx.db[person_age].count_range(range_from(50));
}
*/
*/
26 changes: 12 additions & 14 deletions crates/bindings-cpp/include/spacetimedb/table_with_constraints.h
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ class TypedMultiColumnIndexAccessor : public TableAccessor<TableType> {
index_name_(index_name),
column_list_(column_list) {}

// Exact match on all columns (template method - types deduced from call)
// Exact supplied prefix, optionally terminated by a range.
template<typename... FieldTypes>
IndexIteratorRange<TableType> filter(const std::tuple<FieldTypes...>& values) const
requires (sizeof...(FieldTypes) > 0 && sizeof...(FieldTypes) <= 6)
Expand All @@ -652,34 +652,32 @@ class TypedMultiColumnIndexAccessor : public TableAccessor<TableType> {

return IndexIteratorRange<TableType>(IndexIterator<TableType>(id, values));
}

// Prefix-only match: find all rows where first N-1 columns match
template<typename FirstColType>
IndexIteratorRange<TableType> filter(const FirstColType& prefix_value) const
requires (!is_tuple_v<FirstColType>)
{

// Range on the first indexed column.
template<typename FieldType>
IndexIteratorRange<TableType> filter(const Range<FieldType>& range) const {
IndexId id = resolve_index_id();

if (id.inner == 0) {
return IndexIteratorRange<TableType>(IndexIterator<TableType>());
}

// Use prefix_match_tag to disambiguate constructor
return IndexIteratorRange<TableType>(IndexIterator<TableType>(prefix_match_tag{}, id, prefix_value));
return IndexIteratorRange<TableType>(IndexIterator<TableType>(id, range));
}

// Prefix + range match: find rows where first N-1 columns match and last is in range
template<typename FirstColType, typename RangeType>
IndexIteratorRange<TableType> filter(const std::tuple<FirstColType, RangeType>& values) const
requires (is_range_v<RangeType>)
// Prefix-only match: find all rows where the first indexed column matches.
template<typename FirstColType>
IndexIteratorRange<TableType> filter(const FirstColType& prefix_value) const
requires (!is_tuple_v<FirstColType> && !is_range_v<FirstColType>)
{
IndexId id = resolve_index_id();

if (id.inner == 0) {
return IndexIteratorRange<TableType>(IndexIterator<TableType>());
}

return IndexIteratorRange<TableType>(IndexIterator<TableType>(id, values));
// Use prefix_match_tag to disambiguate constructor
return IndexIteratorRange<TableType>(IndexIterator<TableType>(prefix_match_tag{}, id, prefix_value));
}
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#include "spacetimedb.h"

#include <cstdint>
#include <tuple>

using namespace SpacetimeDB;

struct InvalidRangeRow {
uint32_t a;
uint32_t b;
uint32_t c;
};

SPACETIMEDB_STRUCT(InvalidRangeRow, a, b, c)
SPACETIMEDB_TABLE(InvalidRangeRow, invalid_range_row, Public)
FIELD_NamedMultiColumnIndex(invalid_range_row, by_all, a, b, c)

SPACETIMEDB_REDUCER(check_invalid_middle_range, ReducerContext ctx) {
auto rows = ctx.db[invalid_range_row_by_all].filter(
std::make_tuple(uint32_t(1), range_from(uint32_t(2)), uint32_t(3)));
(void)rows;
return Ok();
}
Loading
Loading