From b94fdc13c496530c0f7448913b7fc83daa716bb1 Mon Sep 17 00:00:00 2001 From: Egor Vorontsov Date: Sat, 19 Jul 2025 06:48:02 +0300 Subject: [PATCH 1/3] Implemented newtype pattern support for tuples. --- crates/bindings-macro/src/sats.rs | 17 +++++++++-------- crates/sats/src/de/impls.rs | 30 ++++++++++++++++++++++++++++++ crates/sats/src/ser/impls.rs | 10 ++++++++-- crates/sats/src/typespace.rs | 6 +++++- 4 files changed, 52 insertions(+), 11 deletions(-) diff --git a/crates/bindings-macro/src/sats.rs b/crates/bindings-macro/src/sats.rs index 8bad39da837..8f5553eb57b 100644 --- a/crates/bindings-macro/src/sats.rs +++ b/crates/bindings-macro/src/sats.rs @@ -380,8 +380,9 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream { let n_fields = fields.len(); - let field_names = fields.iter().map(|f| f.ident.unwrap()).collect::>(); - let field_strings = fields.iter().map(|f| f.name.as_deref().unwrap()).collect::>(); + let field_members = fields.iter().enumerate().map(|(ii, f)| f.ident.map_or_else(|| ii.into(), |v| v.clone().into())).collect::>(); + let field_names = fields.iter().enumerate().map(|(ii, f)| f.ident.map_or_else(|| syn::Ident::new(("_".to_owned() + &ii.to_string()).as_ref(), Span::call_site()), |v| v.clone())).collect::>(); + let field_strings = fields.iter().enumerate().map(|(ii, f)| f.name.clone().unwrap_or_else(|| ii.to_string())).collect::>(); let field_types = fields.iter().map(|f| &f.ty); let field_types2 = field_types.clone(); quote! { @@ -414,7 +415,7 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream { fn visit_seq_product>(self, mut tup: A) -> Result { Ok(#name { - #(#field_names: + #(#field_members: tup.next_element::<#field_types>()? .ok_or_else(|| #spacetimedb_lib::de::Error::invalid_product_length(#iter_n, &self))?,)* }) @@ -434,7 +435,7 @@ pub(crate) fn derive_deserialize(ty: &SatsType<'_>) -> TokenStream { } } Ok(#name { - #(#field_names: + #(#field_members: #field_names.ok_or_else(|| #spacetimedb_lib::de::Error::missing_field(#iter_n3, Some(#field_strings), &self))?,)* }) } @@ -595,13 +596,13 @@ pub(crate) fn derive_serialize(ty: &SatsType) -> TokenStream { }); } - let fieldnames = fields.iter().map(|field| field.ident.unwrap()); - let tys = fields.iter().map(|f| &f.ty); - let fieldnamestrings = fields.iter().map(|field| field.name.as_ref().unwrap()); + let field_members = fields.iter().enumerate().map(|(ii, f)| f.ident.map_or_else(|| ii.into(), |v| v.clone().into())).collect::>(); + let field_strings = fields.iter().enumerate().map(|(ii, f)| f.name.clone().unwrap_or_else(|| ii.to_string())).collect::>(); + let field_types = fields.iter().map(|f| &f.ty); let nfields = fields.len(); quote! { let mut __prod = __serializer.serialize_named_product(#nfields)?; - #(#spacetimedb_lib::ser::SerializeNamedProduct::serialize_element::<#tys>(&mut __prod, Some(#fieldnamestrings), &self.#fieldnames)?;)* + #(#spacetimedb_lib::ser::SerializeNamedProduct::serialize_element::<#field_types>(&mut __prod, Some(#field_strings), &self.#field_members)?;)* #spacetimedb_lib::ser::SerializeNamedProduct::end(__prod) } } diff --git a/crates/sats/src/de/impls.rs b/crates/sats/src/de/impls.rs index d0c9a92c23d..433ad7f0f33 100644 --- a/crates/sats/src/de/impls.rs +++ b/crates/sats/src/de/impls.rs @@ -178,6 +178,36 @@ impl_deserialize!([T: Deserialize<'de>] Box<[T]>, de => Vec::deserialize(de).map impl_deserialize!([T: Deserialize<'de>] Rc<[T]>, de => Vec::deserialize(de).map(|s| s.into())); impl_deserialize!([T: Deserialize<'de>] Arc<[T]>, de => Vec::deserialize(de).map(|s| s.into())); +impl_deserialize!([U: Deserialize<'de>, V: Deserialize<'de>] (U, V), de => de.deserialize_product(TupleVisitor:: {u: PhantomData::, v: PhantomData::})); + +struct TupleVisitor { + u: PhantomData, + v: PhantomData, +} + +impl<'de, U: Deserialize<'de>, V: Deserialize<'de>> ProductVisitor<'de> for TupleVisitor { + type Output = (U, V); + + fn product_name(&self) -> Option<&str> { + None + } + + fn product_len(&self) -> usize { + 2 + } + + fn visit_seq_product>(self, mut tup: A) -> Result { + Ok(( + tup.next_element().transpose().unwrap_or(Err(Error::missing_field(0, None, &self)))?, + tup.next_element().transpose().unwrap_or(Err(Error::missing_field(1, None, &self)))?, + )) + } + + fn visit_named_product>(self, _tup: A) -> Result { + Err(Error::custom("Unnamed tuple")) + } +} + /// The visitor converts the slice to its owned version. struct OwnedSliceVisitor; diff --git a/crates/sats/src/ser/impls.rs b/crates/sats/src/ser/impls.rs index 9baac393dff..4025101fd9a 100644 --- a/crates/sats/src/ser/impls.rs +++ b/crates/sats/src/ser/impls.rs @@ -102,8 +102,8 @@ impl Serialize for u8 { impl_serialize!([] F32, (self, ser) => f32::from(*self).serialize(ser)); impl_serialize!([] F64, (self, ser) => f64::from(*self).serialize(ser)); -impl_serialize!([T: Serialize] Vec, (self, ser) => (**self).serialize(ser)); -impl_serialize!([T: Serialize, const N: usize] SmallVec<[T; N]>, (self, ser) => (**self).serialize(ser)); +impl_serialize!([T: Serialize] Vec, (self, ser) => (**self).serialize(ser)); +impl_serialize!([T: Serialize, const N: usize] SmallVec<[T; N]>, (self, ser) => (**self).serialize(ser)); impl_serialize!([T: Serialize] [T], (self, ser) => T::__serialize_array(self, ser)); impl_serialize!([T: Serialize, const N: usize] [T; N], (self, ser) => T::__serialize_array(self, ser)); impl_serialize!([T: Serialize + ?Sized] Box, (self, ser) => (**self).serialize(ser)); @@ -154,6 +154,12 @@ impl_serialize!([] ProductValue, (self, ser) => { } tup.end() }); +impl_serialize!([U: Serialize, V: Serialize] (U, V), (self, ser) => { + let mut tup = ser.serialize_seq_product(2)?; + tup.serialize_element(&self.0)?; + tup.serialize_element(&self.1)?; + tup.end() +}); impl_serialize!([] SumValue, (self, ser) => ser.serialize_variant(self.tag, None, &*self.value)); impl_serialize!([] ArrayValue, (self, ser) => match self { Self::Sum(v) => v.serialize(ser), diff --git a/crates/sats/src/typespace.rs b/crates/sats/src/typespace.rs index f7d0978d660..b879da60df1 100644 --- a/crates/sats/src/typespace.rs +++ b/crates/sats/src/typespace.rs @@ -399,9 +399,10 @@ impl_primitives! { String => String, } -impl_st!([](), AlgebraicType::unit()); +impl_st!([] (), AlgebraicType::unit()); impl_st!([] str, AlgebraicType::String); impl_st!([T] [T], ts => AlgebraicType::array(T::make_type(ts))); +impl_st!([T, const N: usize] [T; N], ts => AlgebraicType::array(T::make_type(ts))); impl_st!([T: ?Sized] &T, ts => T::make_type(ts)); impl_st!([T: ?Sized] Box, ts => T::make_type(ts)); impl_st!([T: ?Sized] Rc, ts => T::make_type(ts)); @@ -410,6 +411,9 @@ impl_st!([T] Vec, ts => <[T]>::make_type(ts)); impl_st!([T, const N: usize] SmallVec<[T; N]>, ts => <[T]>::make_type(ts)); impl_st!([T] Option, ts => AlgebraicType::option(T::make_type(ts))); +impl_st!([U] (U,), ts => AlgebraicType::product([U::make_type(ts)])); +impl_st!([U, V: SpacetimeType] (U, V), ts => AlgebraicType::product([U::make_type(ts), V::make_type(ts)])); + impl_st!([] spacetimedb_primitives::ArgId, AlgebraicType::U64); impl_st!([] spacetimedb_primitives::ColId, AlgebraicType::U16); impl_st!([] spacetimedb_primitives::TableId, AlgebraicType::U32); From 4cd5e6dee0b4535d3f6fa3cc0e0eb4f55f32f691 Mon Sep 17 00:00:00 2001 From: Egor Vorontsov Date: Thu, 23 Oct 2025 02:02:59 +0300 Subject: [PATCH 2/3] Removed duplicate/outdated code. --- crates/sats/src/de/impls.rs | 30 ------------------------------ crates/sats/src/ser/impls.rs | 6 ------ 2 files changed, 36 deletions(-) diff --git a/crates/sats/src/de/impls.rs b/crates/sats/src/de/impls.rs index 433ad7f0f33..d0c9a92c23d 100644 --- a/crates/sats/src/de/impls.rs +++ b/crates/sats/src/de/impls.rs @@ -178,36 +178,6 @@ impl_deserialize!([T: Deserialize<'de>] Box<[T]>, de => Vec::deserialize(de).map impl_deserialize!([T: Deserialize<'de>] Rc<[T]>, de => Vec::deserialize(de).map(|s| s.into())); impl_deserialize!([T: Deserialize<'de>] Arc<[T]>, de => Vec::deserialize(de).map(|s| s.into())); -impl_deserialize!([U: Deserialize<'de>, V: Deserialize<'de>] (U, V), de => de.deserialize_product(TupleVisitor:: {u: PhantomData::, v: PhantomData::})); - -struct TupleVisitor { - u: PhantomData, - v: PhantomData, -} - -impl<'de, U: Deserialize<'de>, V: Deserialize<'de>> ProductVisitor<'de> for TupleVisitor { - type Output = (U, V); - - fn product_name(&self) -> Option<&str> { - None - } - - fn product_len(&self) -> usize { - 2 - } - - fn visit_seq_product>(self, mut tup: A) -> Result { - Ok(( - tup.next_element().transpose().unwrap_or(Err(Error::missing_field(0, None, &self)))?, - tup.next_element().transpose().unwrap_or(Err(Error::missing_field(1, None, &self)))?, - )) - } - - fn visit_named_product>(self, _tup: A) -> Result { - Err(Error::custom("Unnamed tuple")) - } -} - /// The visitor converts the slice to its owned version. struct OwnedSliceVisitor; diff --git a/crates/sats/src/ser/impls.rs b/crates/sats/src/ser/impls.rs index 4025101fd9a..49851e5ba31 100644 --- a/crates/sats/src/ser/impls.rs +++ b/crates/sats/src/ser/impls.rs @@ -154,12 +154,6 @@ impl_serialize!([] ProductValue, (self, ser) => { } tup.end() }); -impl_serialize!([U: Serialize, V: Serialize] (U, V), (self, ser) => { - let mut tup = ser.serialize_seq_product(2)?; - tup.serialize_element(&self.0)?; - tup.serialize_element(&self.1)?; - tup.end() -}); impl_serialize!([] SumValue, (self, ser) => ser.serialize_variant(self.tag, None, &*self.value)); impl_serialize!([] ArrayValue, (self, ser) => match self { Self::Sum(v) => v.serialize(ser), From ab2ec583010ce97fe3267d6038609d1397578b0b Mon Sep 17 00:00:00 2001 From: Egor Vorontsov Date: Fri, 24 Oct 2025 01:55:32 +0300 Subject: [PATCH 3/3] Implemented type-safe primary keys. --- Cargo.lock | 10 ++++++++++ crates/bindings-macro/Cargo.toml | 1 + crates/bindings-macro/src/table.rs | 17 +++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index c2d03fb6b56..0f114e2b518 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5251,6 +5251,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-type-name" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f94190a20388d5e7477532030a7b3d0077b473431d785b20bef0edadca55a5f" +dependencies = [ + "proc-macro2", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -7081,6 +7090,7 @@ checksum = "9a930242493f5c875ab96903eb40fb6a90c4d3ae99597fd51da569ff22769a03" dependencies = [ "heck 0.4.1", "humantime", + "proc-macro-type-name", "proc-macro2", "quote", "spacetimedb-primitives 1.6.0", diff --git a/crates/bindings-macro/Cargo.toml b/crates/bindings-macro/Cargo.toml index 9e0094df23a..d17d1280783 100644 --- a/crates/bindings-macro/Cargo.toml +++ b/crates/bindings-macro/Cargo.toml @@ -19,6 +19,7 @@ proc-macro2.workspace = true quote.workspace = true syn.workspace = true heck.workspace = true +proc-macro-type-name = "0.1.0" [lints] workspace = true diff --git a/crates/bindings-macro/src/table.rs b/crates/bindings-macro/src/table.rs index 2748f195e64..013c6545918 100644 --- a/crates/bindings-macro/src/table.rs +++ b/crates/bindings-macro/src/table.rs @@ -3,6 +3,7 @@ use crate::sym; use crate::util::{check_duplicate, check_duplicate_msg, ident_to_litstr, match_meta}; use core::slice; use heck::ToSnakeCase; +use proc_macro_type_name::ToTypeName; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use std::borrow::Cow; @@ -794,6 +795,9 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R } }; + let pk_type = primary_key_column.clone().map(|x| x.ty); + let pk_type_ident = primary_key_column.clone().map(|x| Ident::new(&(original_struct_ident.to_string() + &(&x.ident).to_type_ident(x.ident.span()).to_string()), x.ident.span())); + let (schedule, schedule_typecheck) = args .scheduled .as_ref() @@ -900,6 +904,18 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R }; // Output all macro data + + let trait_def_pk = pk_type.map(|pk_type| quote_spanned! {table_ident.span()=> + #[allow(dead_code)] + #[derive(SpacetimeType)] + struct #pk_type_ident(#pk_type); + impl std::borrow::Borrow<#pk_type> for #pk_type_ident { + fn borrow(&self) -> &#pk_type { + &self.0 + } + } + }); + let trait_def = quote_spanned! {table_ident.span()=> #[allow(non_camel_case_types, dead_code)] #vis trait #table_ident { @@ -944,6 +960,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R #default_type_check }; + #trait_def_pk #trait_def #trait_def_view