spacetimedb_lib/filterable_value.rs
1use crate::{ConnectionId, Identity, Uuid};
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///
12/// Because SpacetimeDB supports a only restricted set of types as index keys,
13/// only a small set of `Column` types have corresponding `FilterableValue` implementations.
14/// Specifically, these types are:
15/// - Signed and unsigned integers of various widths.
16/// - [`bool`].
17/// - [`String`], which is also filterable with `&str`.
18/// - [`Identity`].
19/// - [`Uuid`].
20/// - [`ConnectionId`].
21/// - [`Hash`](struct@Hash).
22/// - No-payload enums annotated with `#[derive(SpacetimeType)]`.
23/// No-payload enums are sometimes called "plain," "simple" or "C-style."
24/// They are enums where no variant has any payload data.
25///
26/// Because SpacetimeDB indexes are present both on the server
27/// and in clients which use our various SDKs,
28/// implementing `FilterableValue` for a new column type is a significant burden,
29/// and **is not as simple** as adding a new `impl FilterableValue` block to our Rust code.
30/// To implement `FilterableValue` for a new column type, you must also:
31/// - Ensure (with automated tests) that the `spacetimedb-codegen` crate
32/// and accompanying SpacetimeDB client SDK can equality-compare and ordering-compare values of the column type,
33/// and that the resulting ordering is the same as the canonical ordering
34/// implemented by `spacetimedb-sats` for [`spacetimedb_sats::AlgebraicValue`].
35/// This will nearly always require implementing bespoke comparison methods for the type in question,
36/// as most languages do not automatically make product types (structs or classes) sortable.
37/// - Extend our other supported module languages' bindings libraries.
38/// so that they can also define tables with indexes keyed by the new filterable type.
39//
40// General rules for implementors of this type:
41// - See above doc comment for requirements to add implementations for new column types.
42// - It should only be implemented for owned values if those values are `Copy`.
43// Otherwise it should only be implemented for references.
44// This is so that rustc and IDEs will recommend rewriting `x` to `&x` rather than `x.clone()`.
45// - `Arg: FilterableValue<Column = Col>`
46// for any pair of types `(Arg, Col)` which meet the above criteria
47// is desirable if `Arg` and `Col` have the same BSATN layout.
48// E.g. `&str: FilterableValue<Column = String>` is desirable.
49#[diagnostic::on_unimplemented(
50 message = "`{Self}` cannot appear as an argument to an index filtering operation",
51 label = "should be an integer type, `bool`, `String`, `&str`, `Identity`, `Uuid`, `ConnectionId`, `Hash` or a no-payload enum which derives `SpacetimeType`, not `{Self}`",
52 note = "The allowed set of types are limited to integers, bool, strings, `Identity`, `Uuid`, `ConnectionId`, `Hash` and no-payload enums which derive `SpacetimeType`,"
53)]
54pub trait FilterableValue: Serialize + Private {
55 type Column;
56}
57
58/// Hidden supertrait for [`FilterableValue`],
59/// to discourage users from hand-writing implementations.
60///
61/// We want to expose [`FilterableValue`] in the docs, but to prevent users from implementing it.
62/// Normally, we would just make this `Private` trait inaccessible,
63/// but we need to macro-generate implementations, so it must be `pub`.
64/// We mark it `doc(hidden)` to discourage use.
65#[doc(hidden)]
66pub trait Private {}
67
68macro_rules! impl_filterable_value {
69 (@one $arg:ty => $col:ty) => {
70 impl Private for $arg {}
71 impl FilterableValue for $arg {
72 type Column = $col;
73 }
74 };
75 (@one $arg:ty: Copy) => {
76 impl_filterable_value!(@one $arg => $arg);
77 impl_filterable_value!(@one &$arg => $arg);
78 };
79 (@one $arg:ty) => {
80 impl_filterable_value!(@one &$arg => $arg);
81 };
82 ($($arg:ty $(: $copy:ident)? $(=> $col:ty)?),* $(,)?) => {
83 $(impl_filterable_value!(@one $arg $(: $copy)? $(=> $col)?);)*
84 };
85}
86
87impl_filterable_value! {
88 u8: Copy,
89 u16: Copy,
90 u32: Copy,
91 u64: Copy,
92 u128: Copy,
93 u256: Copy,
94
95 i8: Copy,
96 i16: Copy,
97 i32: Copy,
98 i64: Copy,
99 i128: Copy,
100 i256: Copy,
101
102 bool: Copy,
103
104 String,
105 &str => String,
106
107 Identity: Copy,
108 Uuid: Copy,
109 ConnectionId: Copy,
110 Hash: Copy,
111
112 // Some day we will likely also want to support `Vec<u8>` and `[u8]`,
113 // as they have trivial portable equality and ordering,
114 // but @RReverser's proposed filtering rules do not include them.
115 // Vec<u8>,
116 // &[u8] => Vec<u8>,
117}
118
119pub enum TermBound<T> {
120 Single(ops::Bound<T>),
121 Range(ops::Bound<T>, ops::Bound<T>),
122}
123impl<Bound: FilterableValue> TermBound<&Bound> {
124 #[inline]
125 /// If `self` is [`TermBound::Range`], returns the `rend_idx` value for `IndexScanRangeArgs`,
126 /// i.e. the index in `buf` of the first byte in the end range
127 pub fn serialize_into(&self, buf: &mut Vec<u8>) -> Option<usize> {
128 let (start, end) = match self {
129 TermBound::Single(elem) => (elem, None),
130 TermBound::Range(start, end) => (start, Some(end)),
131 };
132 bsatn::to_writer(buf, start).unwrap();
133 end.map(|end| {
134 let rend_idx = buf.len();
135 bsatn::to_writer(buf, end).unwrap();
136 rend_idx
137 })
138 }
139}
140pub trait IndexScanRangeBoundsTerminator {
141 /// Whether this bound terminator is a point.
142 const POINT: bool = false;
143
144 /// The key type of the bound.
145 type Arg;
146
147 /// Returns the point bound, assuming `POINT == true`.
148 fn point(&self) -> &Self::Arg {
149 unimplemented!()
150 }
151
152 /// Returns the terminal bound for the range scan.
153 /// This bound can either be a point, as in most cases, or an actual bound.
154 fn bounds(&self) -> TermBound<&Self::Arg>;
155}
156
157impl<Col, Arg: FilterableValue<Column = Col>> IndexScanRangeBoundsTerminator for Arg {
158 const POINT: bool = true;
159 type Arg = Arg;
160 fn point(&self) -> &Arg {
161 self
162 }
163 fn bounds(&self) -> TermBound<&Arg> {
164 TermBound::Single(ops::Bound::Included(self))
165 }
166}
167
168macro_rules! impl_terminator {
169 ($($range:ty),* $(,)?) => {
170 $(impl<T: FilterableValue> IndexScanRangeBoundsTerminator for $range {
171 type Arg = T;
172 fn bounds(&self) -> TermBound<&T> {
173 TermBound::Range(
174 ops::RangeBounds::start_bound(self),
175 ops::RangeBounds::end_bound(self),
176 )
177 }
178 })*
179 };
180}
181
182impl_terminator!(
183 ops::Range<T>,
184 ops::RangeFrom<T>,
185 ops::RangeInclusive<T>,
186 ops::RangeTo<T>,
187 ops::RangeToInclusive<T>,
188 (ops::Bound<T>, ops::Bound<T>),
189);