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