spacetimedb_lib/
lib.rs

1use crate::db::raw_def::v9::RawModuleDefV9Builder;
2use crate::db::raw_def::RawTableDefV8;
3use anyhow::Context;
4use sats::typespace::TypespaceBuilder;
5use spacetimedb_sats::{impl_serialize, WithTypespace};
6use std::any::TypeId;
7use std::collections::{btree_map, BTreeMap};
8
9pub mod connection_id;
10pub mod db;
11mod direct_index_key;
12pub mod error;
13mod filterable_value;
14pub mod identity;
15pub mod metrics;
16pub mod operator;
17pub mod query;
18pub mod scheduler;
19pub mod st_var;
20pub mod version;
21
22pub mod type_def {
23    pub use spacetimedb_sats::{AlgebraicType, ProductType, ProductTypeElement, SumType};
24}
25pub mod type_value {
26    pub use spacetimedb_sats::{AlgebraicValue, ProductValue};
27}
28
29pub use connection_id::ConnectionId;
30pub use direct_index_key::{assert_column_type_valid_for_direct_index, DirectIndexKey};
31#[doc(hidden)]
32pub use filterable_value::Private;
33pub use filterable_value::{FilterableValue, IndexScanRangeBoundsTerminator, TermBound};
34pub use identity::Identity;
35pub use scheduler::ScheduleAt;
36pub use spacetimedb_sats::hash::{self, hash_bytes, Hash};
37pub use spacetimedb_sats::time_duration::TimeDuration;
38pub use spacetimedb_sats::timestamp::Timestamp;
39pub use spacetimedb_sats::SpacetimeType;
40pub use spacetimedb_sats::__make_register_reftype;
41pub use spacetimedb_sats::{self as sats, bsatn, buffer, de, ser};
42pub use spacetimedb_sats::{AlgebraicType, ProductType, ProductTypeElement, SumType};
43pub use spacetimedb_sats::{AlgebraicValue, ProductValue};
44
45pub const MODULE_ABI_MAJOR_VERSION: u16 = 10;
46
47// if it ends up we need more fields in the future, we can split one of them in two
48#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
49pub struct VersionTuple {
50    /// Breaking change; different major versions are not at all compatible with each other.
51    pub major: u16,
52    /// Non-breaking change; a host can run a module that requests an older minor version than the
53    /// host implements, but not the other way around
54    pub minor: u16,
55}
56
57impl VersionTuple {
58    pub const fn new(major: u16, minor: u16) -> Self {
59        Self { major, minor }
60    }
61
62    #[inline]
63    pub const fn eq(self, other: Self) -> bool {
64        self.major == other.major && self.minor == other.minor
65    }
66
67    /// Checks if a host implementing this version can run a module that expects `module_version`
68    #[inline]
69    pub const fn supports(self, module_version: VersionTuple) -> bool {
70        self.major == module_version.major && self.minor >= module_version.minor
71    }
72
73    #[inline]
74    pub const fn from_u32(v: u32) -> Self {
75        let major = (v >> 16) as u16;
76        let minor = (v & 0xFF) as u16;
77        Self { major, minor }
78    }
79
80    #[inline]
81    pub const fn to_u32(self) -> u32 {
82        (self.major as u32) << 16 | self.minor as u32
83    }
84}
85
86impl std::fmt::Display for VersionTuple {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        let Self { major, minor } = *self;
89        write!(f, "{major}.{minor}")
90    }
91}
92
93extern crate self as spacetimedb_lib;
94
95//WARNING: Change this structure(or any of their members) is an ABI change.
96#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, SpacetimeType)]
97#[sats(crate = crate)]
98pub struct TableDesc {
99    pub schema: RawTableDefV8,
100    /// data should always point to a ProductType in the typespace
101    pub data: sats::AlgebraicTypeRef,
102}
103
104impl TableDesc {
105    pub fn into_table_def(table: WithTypespace<'_, TableDesc>) -> anyhow::Result<RawTableDefV8> {
106        let schema = table
107            .map(|t| &t.data)
108            .resolve_refs()
109            .context("recursive types not yet supported")?;
110        let schema = schema.into_product().ok().context("table not a product type?")?;
111        let table = table.ty();
112        anyhow::ensure!(
113            table.schema.columns.len() == schema.elements.len(),
114            "mismatched number of columns"
115        );
116
117        Ok(table.schema.clone())
118    }
119}
120
121#[derive(Debug, Clone, SpacetimeType)]
122#[sats(crate = crate)]
123pub struct ReducerDef {
124    pub name: Box<str>,
125    pub args: Vec<ProductTypeElement>,
126}
127
128impl ReducerDef {
129    pub fn encode(&self, writer: &mut impl buffer::BufWriter) {
130        bsatn::to_writer(writer, self).unwrap()
131    }
132
133    pub fn serialize_args<'a>(ty: sats::WithTypespace<'a, Self>, value: &'a ProductValue) -> impl ser::Serialize + 'a {
134        ReducerArgsWithSchema { value, ty }
135    }
136
137    pub fn deserialize(
138        ty: sats::WithTypespace<'_, Self>,
139    ) -> impl for<'de> de::DeserializeSeed<'de, Output = ProductValue> + '_ {
140        ReducerDeserialize(ty)
141    }
142}
143
144struct ReducerDeserialize<'a>(sats::WithTypespace<'a, ReducerDef>);
145
146impl<'de> de::DeserializeSeed<'de> for ReducerDeserialize<'_> {
147    type Output = ProductValue;
148
149    fn deserialize<D: de::Deserializer<'de>>(self, deserializer: D) -> Result<Self::Output, D::Error> {
150        deserializer.deserialize_product(self)
151    }
152}
153
154impl<'de> de::ProductVisitor<'de> for ReducerDeserialize<'_> {
155    type Output = ProductValue;
156
157    fn product_name(&self) -> Option<&str> {
158        Some(&self.0.ty().name)
159    }
160    fn product_len(&self) -> usize {
161        self.0.ty().args.len()
162    }
163    fn product_kind(&self) -> de::ProductKind {
164        de::ProductKind::ReducerArgs
165    }
166
167    fn visit_seq_product<A: de::SeqProductAccess<'de>>(self, tup: A) -> Result<Self::Output, A::Error> {
168        de::visit_seq_product(self.0.map(|r| &*r.args), &self, tup)
169    }
170
171    fn visit_named_product<A: de::NamedProductAccess<'de>>(self, tup: A) -> Result<Self::Output, A::Error> {
172        de::visit_named_product(self.0.map(|r| &*r.args), &self, tup)
173    }
174}
175
176struct ReducerArgsWithSchema<'a> {
177    value: &'a ProductValue,
178    ty: sats::WithTypespace<'a, ReducerDef>,
179}
180impl_serialize!([] ReducerArgsWithSchema<'_>, (self, ser) => {
181    use itertools::Itertools;
182    use ser::SerializeSeqProduct;
183    let mut seq = ser.serialize_seq_product(self.value.elements.len())?;
184    for (value, elem) in self.value.elements.iter().zip_eq(&self.ty.ty().args) {
185        seq.serialize_element(&self.ty.with(&elem.algebraic_type).with_value(value))?;
186    }
187    seq.end()
188});
189
190//WARNING: Change this structure (or any of their members) is an ABI change.
191#[derive(Debug, Clone, Default, SpacetimeType)]
192#[sats(crate = crate)]
193pub struct RawModuleDefV8 {
194    pub typespace: sats::Typespace,
195    pub tables: Vec<TableDesc>,
196    pub reducers: Vec<ReducerDef>,
197    pub misc_exports: Vec<MiscModuleExport>,
198}
199
200impl RawModuleDefV8 {
201    pub fn builder() -> ModuleDefBuilder {
202        ModuleDefBuilder::default()
203    }
204
205    pub fn with_builder(f: impl FnOnce(&mut ModuleDefBuilder)) -> Self {
206        let mut builder = Self::builder();
207        f(&mut builder);
208        builder.finish()
209    }
210}
211
212/// A versioned raw module definition.
213///
214/// This is what is actually returned by the module when `__describe_module__` is called, serialized to BSATN.
215#[derive(Debug, Clone, SpacetimeType)]
216#[sats(crate = crate)]
217#[non_exhaustive]
218pub enum RawModuleDef {
219    V8BackCompat(RawModuleDefV8),
220    V9(db::raw_def::v9::RawModuleDefV9),
221    // TODO(jgilles): It would be nice to have a custom error message if this fails with an unknown variant,
222    // but I'm not sure if that can be done via the Deserialize trait.
223}
224
225/// A builder for a [`RawModuleDefV8`].
226/// Deprecated.
227#[derive(Default)]
228pub struct ModuleDefBuilder {
229    /// The module definition.
230    module: RawModuleDefV8,
231    /// The type map from `T: 'static` Rust types to sats types.
232    type_map: BTreeMap<TypeId, sats::AlgebraicTypeRef>,
233}
234
235impl ModuleDefBuilder {
236    pub fn add_type<T: SpacetimeType>(&mut self) -> AlgebraicType {
237        TypespaceBuilder::add_type::<T>(self)
238    }
239
240    /// Add a type that may not correspond to a Rust type.
241    /// Used only in tests.
242    #[cfg(feature = "test")]
243    pub fn add_type_for_tests(&mut self, name: &str, ty: AlgebraicType) -> spacetimedb_sats::AlgebraicTypeRef {
244        let slot_ref = self.module.typespace.add(ty);
245        self.module.misc_exports.push(MiscModuleExport::TypeAlias(TypeAlias {
246            name: name.to_owned(),
247            ty: slot_ref,
248        }));
249        slot_ref
250    }
251
252    /// Add a table that may not correspond to a Rust type.
253    /// Wraps it in a `TableDesc` and generates a corresponding `ProductType` in the typespace.
254    /// Used only in tests.
255    /// Returns the `AlgebraicTypeRef` of the generated `ProductType`.
256    #[cfg(feature = "test")]
257    pub fn add_table_for_tests(&mut self, schema: RawTableDefV8) -> spacetimedb_sats::AlgebraicTypeRef {
258        let ty: ProductType = schema
259            .columns
260            .iter()
261            .map(|c| ProductTypeElement {
262                name: Some(c.col_name.clone()),
263                algebraic_type: c.col_type.clone(),
264            })
265            .collect();
266        let data = self.module.typespace.add(ty.into());
267        self.add_type_alias(TypeAlias {
268            name: schema.table_name.clone().into(),
269            ty: data,
270        });
271        self.add_table(TableDesc { schema, data });
272        data
273    }
274
275    pub fn add_table(&mut self, table: TableDesc) {
276        self.module.tables.push(table)
277    }
278
279    pub fn add_reducer(&mut self, reducer: ReducerDef) {
280        self.module.reducers.push(reducer)
281    }
282
283    #[cfg(feature = "test")]
284    pub fn add_reducer_for_tests(&mut self, name: impl Into<Box<str>>, args: ProductType) {
285        self.add_reducer(ReducerDef {
286            name: name.into(),
287            args: args.elements.to_vec(),
288        });
289    }
290
291    pub fn add_misc_export(&mut self, misc_export: MiscModuleExport) {
292        self.module.misc_exports.push(misc_export)
293    }
294
295    pub fn add_type_alias(&mut self, type_alias: TypeAlias) {
296        self.add_misc_export(MiscModuleExport::TypeAlias(type_alias))
297    }
298
299    pub fn typespace(&self) -> &sats::Typespace {
300        &self.module.typespace
301    }
302
303    pub fn finish(self) -> RawModuleDefV8 {
304        self.module
305    }
306}
307
308impl TypespaceBuilder for ModuleDefBuilder {
309    fn add(
310        &mut self,
311        typeid: TypeId,
312        name: Option<&'static str>,
313        make_ty: impl FnOnce(&mut Self) -> AlgebraicType,
314    ) -> AlgebraicType {
315        let r = match self.type_map.entry(typeid) {
316            btree_map::Entry::Occupied(o) => *o.get(),
317            btree_map::Entry::Vacant(v) => {
318                // Bind a fresh alias to the unit type.
319                let slot_ref = self.module.typespace.add(AlgebraicType::unit());
320                // Relate `typeid -> fresh alias`.
321                v.insert(slot_ref);
322
323                // Alias provided? Relate `name -> slot_ref`.
324                if let Some(name) = name {
325                    self.module.misc_exports.push(MiscModuleExport::TypeAlias(TypeAlias {
326                        name: name.to_owned(),
327                        ty: slot_ref,
328                    }));
329                }
330
331                // Borrow of `v` has ended here, so we can now convince the borrow checker.
332                let ty = make_ty(self);
333                self.module.typespace[slot_ref] = ty;
334                slot_ref
335            }
336        };
337        AlgebraicType::Ref(r)
338    }
339}
340
341// an enum to keep it extensible without breaking abi
342#[derive(Debug, Clone, SpacetimeType)]
343#[sats(crate = crate)]
344pub enum MiscModuleExport {
345    TypeAlias(TypeAlias),
346}
347
348#[derive(Debug, Clone, SpacetimeType)]
349#[sats(crate = crate)]
350pub struct TypeAlias {
351    pub name: String,
352    pub ty: sats::AlgebraicTypeRef,
353}
354
355/// Converts a hexadecimal string reference to a byte array.
356///
357/// This function takes a reference to a hexadecimal string and attempts to convert it into a byte array.
358///
359/// If the hexadecimal string starts with "0x", these characters are ignored.
360pub fn from_hex_pad<R: hex::FromHex<Error = hex::FromHexError>, T: AsRef<[u8]>>(
361    hex: T,
362) -> Result<R, hex::FromHexError> {
363    let hex = hex.as_ref();
364    let hex = if hex.starts_with(b"0x") {
365        &hex[2..]
366    } else if hex.starts_with(b"X'") {
367        &hex[2..hex.len()]
368    } else {
369        hex
370    };
371    hex::FromHex::from_hex(hex)
372}
373
374/// Returns a resolved `AlgebraicType` (containing no `AlgebraicTypeRefs`) for a given `SpacetimeType`,
375/// using the v9 moduledef infrastructure.
376/// Panics if the type is recursive.
377///
378/// TODO: we could implement something like this in `sats` itself, but would need a lightweight `TypespaceBuilder` implementation there.
379pub fn resolved_type_via_v9<T: SpacetimeType>() -> AlgebraicType {
380    let mut builder = RawModuleDefV9Builder::new();
381    let ty = T::make_type(&mut builder);
382    let module = builder.finish();
383
384    WithTypespace::new(&module.typespace, &ty)
385        .resolve_refs()
386        .expect("recursive types not supported")
387}