spacetimedb_lib/
filterable_value.rs

1use crate::{ConnectionId, Identity};
2use core::ops;
3use spacetimedb_sats::bsatn;
4use spacetimedb_sats::{hash::Hash, i256, u256, Serialize};
5
6/// Types which can appear as an argument to an index filtering operation
7/// for a column of type `Column`.
8///
9/// Types which can appear specifically as a terminating bound in a BTree index,
10/// which may be a range, instead use [`IndexScanRangeBoundsTerminator`].
11/// Because SpacetimeDB supports a only restricted set of types as index keys,
12/// only a small set of `Column` types have corresponding `FilterableValue` implementations.
13/// Specifically, these types are:
14/// - Signed and unsigned integers of various widths.
15/// - [`bool`].
16/// - [`String`], which is also filterable with `&str`.
17/// - [`Identity`].
18/// - [`ConnectionId`].
19/// - [`Hash`](struct@Hash).
20/// - No-payload enums annotated with `#[derive(SpacetimeType)]`.
21///   No-payload enums are sometimes called "plain," "simple" or "C-style."
22///   They are enums where no variant has any payload data.
23//
24// General rules for implementors of this type:
25// - It should only be implemented for types that have
26//   simple-to-implement consistent total equality and ordering
27//   on all languages SpacetimeDB supports in both client and module SDKs.
28//   This means that user-defined compound types other than C-style enums,
29//   and arrays thereof,
30//   should not implement it, as C# and TypeScript use reference equality for those types.
31// - It should only be implemented for owned values if those values are `Copy`.
32//   Otherwise it should only be implemented for references.
33//   This is so that rustc and IDEs will recommend rewriting `x` to `&x` rather than `x.clone()`.
34// - `Arg: FilterableValue<Column = Col>`
35//   for any pair of types `(Arg, Col)` which meet the above criteria
36//   is desirable if `Arg` and `Col` have the same BSATN layout.
37//   E.g. `&str: FilterableValue<Column = String>` is desirable.
38#[diagnostic::on_unimplemented(
39    message = "`{Self}` cannot appear as an argument to an index filtering operation",
40    label = "should be an integer type, `bool`, `String`, `&str`, `Identity`, `ConnectionId`, `Hash` or a no-payload enum which derives `SpacetimeType`, not `{Self}`",
41    note = "The allowed set of types are limited to integers, bool, strings, `Identity`, `ConnectionId`, `Hash` and no-payload enums which derive `SpacetimeType`,"
42)]
43pub trait FilterableValue: Serialize + Private {
44    type Column;
45}
46
47/// Hidden supertrait for [`FilterableValue`],
48/// to discourage users from hand-writing implementations.
49///
50/// We want to expose [`FilterableValue`] in the docs, but to prevent users from implementing it.
51/// Normally, we would just make this `Private` trait inaccessible,
52/// but we need to macro-generate implementations, so it must be `pub`.
53/// We mark it `doc(hidden)` to discourage use.
54#[doc(hidden)]
55pub trait Private {}
56
57macro_rules! impl_filterable_value {
58    (@one $arg:ty => $col:ty) => {
59        impl Private for $arg {}
60        impl FilterableValue for $arg {
61            type Column = $col;
62        }
63    };
64    (@one $arg:ty: Copy) => {
65        impl_filterable_value!(@one $arg => $arg);
66        impl_filterable_value!(@one &$arg => $arg);
67    };
68    (@one $arg:ty) => {
69        impl_filterable_value!(@one &$arg => $arg);
70    };
71    ($($arg:ty $(: $copy:ident)? $(=> $col:ty)?),* $(,)?) => {
72        $(impl_filterable_value!(@one $arg $(: $copy)? $(=> $col)?);)*
73    };
74}
75
76impl_filterable_value! {
77    u8: Copy,
78    u16: Copy,
79    u32: Copy,
80    u64: Copy,
81    u128: Copy,
82    u256: Copy,
83
84    i8: Copy,
85    i16: Copy,
86    i32: Copy,
87    i64: Copy,
88    i128: Copy,
89    i256: Copy,
90
91    bool: Copy,
92
93    String,
94    &str => String,
95
96    Identity: Copy,
97    ConnectionId: Copy,
98    Hash: Copy,
99
100    // Some day we will likely also want to support `Vec<u8>` and `[u8]`,
101    // as they have trivial portable equality and ordering,
102    // but @RReverser's proposed filtering rules do not include them.
103    // Vec<u8>,
104    // &[u8] => Vec<u8>,
105}
106
107pub enum TermBound<T> {
108    Single(ops::Bound<T>),
109    Range(ops::Bound<T>, ops::Bound<T>),
110}
111impl<Bound: FilterableValue> TermBound<&Bound> {
112    #[inline]
113    /// If `self` is [`TermBound::Range`], returns the `rend_idx` value for `IndexScanRangeArgs`,
114    /// i.e. the index in `buf` of the first byte in the end range
115    pub fn serialize_into(&self, buf: &mut Vec<u8>) -> Option<usize> {
116        let (start, end) = match self {
117            TermBound::Single(elem) => (elem, None),
118            TermBound::Range(start, end) => (start, Some(end)),
119        };
120        bsatn::to_writer(buf, start).unwrap();
121        end.map(|end| {
122            let rend_idx = buf.len();
123            bsatn::to_writer(buf, end).unwrap();
124            rend_idx
125        })
126    }
127}
128pub trait IndexScanRangeBoundsTerminator {
129    type Arg;
130    fn bounds(&self) -> TermBound<&Self::Arg>;
131}
132
133impl<Col, Arg: FilterableValue<Column = Col>> IndexScanRangeBoundsTerminator for Arg {
134    type Arg = Arg;
135    fn bounds(&self) -> TermBound<&Arg> {
136        TermBound::Single(ops::Bound::Included(self))
137    }
138}
139
140macro_rules! impl_terminator {
141    ($($range:ty),* $(,)?) => {
142        $(impl<T: FilterableValue> IndexScanRangeBoundsTerminator for $range {
143            type Arg = T;
144            fn bounds(&self) -> TermBound<&T> {
145                TermBound::Range(
146                    ops::RangeBounds::start_bound(self),
147                    ops::RangeBounds::end_bound(self),
148                )
149            }
150        })*
151    };
152}
153
154impl_terminator!(
155    ops::Range<T>,
156    ops::RangeFrom<T>,
157    ops::RangeInclusive<T>,
158    ops::RangeTo<T>,
159    ops::RangeToInclusive<T>,
160    (ops::Bound<T>, ops::Bound<T>),
161);