From 4ef2b74c8a5e0963b14e2442ad978175cee9bed0 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 10:36:12 -0700 Subject: [PATCH 01/10] custom deserializer for resolvedfn --- core-relations/src/base_values/mod.rs | 30 +++++++++++++++++++++ core-relations/src/lib.rs | 5 +++- src/lib.rs | 38 +++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/core-relations/src/base_values/mod.rs b/core-relations/src/base_values/mod.rs index dceb9b7e3..8946e7d6e 100644 --- a/core-relations/src/base_values/mod.rs +++ b/core-relations/src/base_values/mod.rs @@ -8,8 +8,10 @@ use std::{ use hashbrown::HashMap; use num::{rational::Ratio, BigInt, Rational64}; +use once_cell::sync::Lazy; use serde::{de, ser::SerializeStruct, Deserialize, Serialize}; use serde_json::json; +use std::sync::Mutex; use crate::numeric_id::{define_id, DenseIdMap, NumericId}; @@ -200,6 +202,12 @@ trait DynamicInternTable: Any + dyn_clone::DynClone + Send + Sync { // Implements `Clone` for `Box`. dyn_clone::clone_trait_object!(DynamicInternTable); +type DynTableDeserializer = + fn(serde_json::Value) -> Result, Box>; + +static EXTERNAL_BASE_VALUE_DESERIALIZERS: Lazy>> = + Lazy::new(|| Mutex::new(HashMap::new())); + #[derive(Serialize, Deserialize)] struct BaseInternTableErased { table: serde_json::Value, @@ -219,6 +227,20 @@ impl Default for BaseInternTable

{ } } +fn deserialize_table_for( + table: serde_json::Value, +) -> Result, Box> { + let table: InternTable = serde_json::from_value(table)?; + Ok(Box::new(BaseInternTable { table })) +} + +pub fn register_deserializable_base_type() { + EXTERNAL_BASE_VALUE_DESERIALIZERS + .lock() + .unwrap() + .insert(P::type_id_string(), deserialize_table_for::

); +} + impl

DynamicInternTable for BaseInternTable

where P: BaseValue + Serialize + 'static, @@ -415,6 +437,14 @@ fn deserialize_dyn( value: serde_json::Value, ) -> Result, Box> { let erased: BaseInternTableErased = serde_json::from_value(value)?; + if let Some(deserialize) = EXTERNAL_BASE_VALUE_DESERIALIZERS + .lock() + .unwrap() + .get(&erased.base_value_type) + .copied() + { + return deserialize(erased.table); + } match erased.base_value_type.as_str() { "Unit" => { diff --git a/core-relations/src/lib.rs b/core-relations/src/lib.rs index 66fe1248c..462bdc0a3 100644 --- a/core-relations/src/lib.rs +++ b/core-relations/src/lib.rs @@ -25,7 +25,10 @@ pub(crate) mod uf; mod tests; pub use action::{ExecutionState, MergeVal, QueryEntry, WriteVal}; -pub use base_values::{BaseValue, BaseValueId, BaseValuePrinter, BaseValues, Boxed}; +pub use base_values::{ + register_deserializable_base_type, BaseValue, BaseValueId, BaseValuePrinter, BaseValues, + Boxed, +}; pub use common::Value; pub use containers::{ContainerValue, ContainerValueId, ContainerValues}; pub use free_join::{ diff --git a/src/lib.rs b/src/lib.rs index d8ea68a61..56a0ce9d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,6 +94,10 @@ pub const GLOBAL_NAME_PREFIX: &str = "$"; pub type ArcSort = Arc; +fn register_custom_base_value_deserializers() { + core_relations::register_deserializable_base_type::(); +} + impl dyn Sort { fn as_any(&self) -> &dyn Any { self @@ -2417,6 +2421,36 @@ mod tests { assert!(is_send(&egraph) && is_sync(&egraph)); } + #[test] + fn test_unstable_fn_round_trip_deserializes() { + let mut egraph = EGraph::default(); + egraph + .parse_and_run_program( + None, + r#" + (datatype Math (Num i64)) + (sort MathToMath (UnstableFn (Math) Math)) + (constructor square (Math) Math) + (rewrite (square (Num x)) (Num (* x x))) + (let squared (unstable-app (unstable-fn "square") (Num 3))) + (run 1) + (check (= squared (Num 9))) + "#, + ) + .unwrap(); + + let expected_tuples = egraph.num_tuples(); + let mut buf = flexbuffers::FlexbufferSerializer::new(); + Serialize::serialize(&egraph, &mut buf).unwrap(); + + register_custom_base_value_deserializers(); + let r = flexbuffers::Reader::get_root(buf.view()).unwrap(); + let mut restored: EGraph = EGraph::deserialize(r).unwrap(); + restored.restore_deserialized_runtime().unwrap(); + + assert_eq!(restored.num_tuples(), expected_tuples); + } + fn get_function(egraph: &EGraph, name: &str) -> Function { egraph.functions.get(name).unwrap().clone() } @@ -2591,6 +2625,7 @@ impl Default for TimedEgraph { impl TimedEgraph { /// Create a new TimedEgraph with a default EGraph pub fn new() -> Self { + register_custom_base_value_deserializers(); let mut egraph = EGraph::default(); egraph.add_primitive(GetSizePrimitive); @@ -2602,6 +2637,7 @@ impl TimedEgraph { } pub fn new_from_file(path: &Path) -> Self { + register_custom_base_value_deserializers(); let mut file = fs::File::open(path).expect("failed to open file"); let mut buf = Vec::new(); file.read_to_end(&mut buf) @@ -2789,6 +2825,7 @@ impl TimedEgraph { } pub fn from_value(&mut self, value: Vec) -> Result<()> { + register_custom_base_value_deserializers(); let mut timeline = ProgramTimeline::new("(deserialize)"); timeline.evts.push(EgraphEvent { @@ -2864,6 +2901,7 @@ impl TimedEgraph { } pub fn from_file(&mut self, path: &Path) -> Result<()> { + register_custom_base_value_deserializers(); let mut timeline = ProgramTimeline::new("(read)\n(deserialize)"); timeline.evts.push(EgraphEvent { From 450157feb6e8a73ec334757e38b63382c32c4321 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 11:30:12 -0700 Subject: [PATCH 02/10] serialize/deserialize using flexbuffers for interntable --- Cargo.lock | 1 + core-relations/Cargo.toml | 1 + core-relations/src/common.rs | 74 +++++++++++++++++++++++++++--------- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0b9ff2552..47ad77cde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,6 +572,7 @@ dependencies = [ "egglog-reports", "egglog-union-find", "fixedbitset", + "flexbuffers", "hashbrown 0.16.0", "indexmap", "log", diff --git a/core-relations/Cargo.toml b/core-relations/Cargo.toml index 7b217eb23..17bf3c0c8 100644 --- a/core-relations/Cargo.toml +++ b/core-relations/Cargo.toml @@ -39,6 +39,7 @@ typetag = { workspace = true } serde_json = { workspace = true } ordered-float = { workspace = true } num-bigint = { workspace = true } +flexbuffers = { workspace = true } [dev-dependencies] diff --git a/core-relations/src/common.rs b/core-relations/src/common.rs index 6f189b010..3a7282caa 100644 --- a/core-relations/src/common.rs +++ b/core-relations/src/common.rs @@ -7,9 +7,9 @@ use std::{ use crate::numeric_id::{define_id, DenseIdMap, IdVec, NumericId}; use egglog_concurrency::ConcurrentVec; +use flexbuffers::{FlexbufferSerializer, Reader}; use rustc_hash::FxHasher; use serde::{ser::SerializeStruct, Deserialize, Deserializer, Serialize}; -use serde_json::{from_value, to_value}; use crate::{pool::Clear, Subset, TableId, TableVersion, WrappedTable}; @@ -43,24 +43,43 @@ where where S: serde::Serializer, { + fn to_flexbuffer(value: &T) -> Result, Box> { + let mut serializer = FlexbufferSerializer::new(); + value.serialize(&mut serializer)?; + Ok(serializer.take_buffer()) + } + + let vals = self + .vals + .read() + .iter() + .map(|value| to_flexbuffer(value).map_err(serde::ser::Error::custom)) + .collect::, _>>()?; + let mut s = serializer.serialize_struct("InternTable", 3)?; - s.serialize_field("vals", &self.vals)?; + s.serialize_field("vals", &vals)?; - let mut serializable_data: Vec> = Vec::new(); + #[derive(Serialize)] + struct SerializedEntry { + key: Vec, + value: Vec, + } + + let mut serializable_data: Vec> = Vec::new(); for shard in &self.data { let shard = shard .lock() .map_err(|_| serde::ser::Error::custom("mutex poisoned"))?; let mut inner = Vec::new(); for (k, v) in shard.iter() { - inner.push(( - to_value(k).map_err(serde::ser::Error::custom)?, - to_value(v).map_err(serde::ser::Error::custom)?, - )); + inner.push(SerializedEntry { + key: to_flexbuffer(k).map_err(serde::ser::Error::custom)?, + value: to_flexbuffer(v).map_err(serde::ser::Error::custom)?, + }); } - // Sort by the serialized key to guarantee deterministic order - inner.sort_by(|(k1, _), (k2, _)| k1.to_string().cmp(&k2.to_string())); + // Sort by the encoded key bytes to guarantee deterministic order. + inner.sort_by(|e1, e2| e1.key.cmp(&e2.key)); serializable_data.push(inner) } @@ -80,25 +99,44 @@ where where D: Deserializer<'de>, { - // Helper struct matching the serialized shape + fn from_flexbuffer Deserialize<'a>>( + bytes: &[u8], + ) -> Result> { + let reader = Reader::get_root(bytes)?; + Ok(T::deserialize(reader)?) + } + #[derive(Deserialize)] - struct Partial { - vals: Arc>, - data: Vec>, + struct SerializedEntry { + key: Vec, + value: Vec, + } + + #[derive(Deserialize)] + struct Partial { + vals: Vec>, + data: Vec>, shards_log2: u32, } let helper = Partial::deserialize(deserializer)?; + let vals = Arc::new(ConcurrentVec::with_capacity(helper.vals.len().max(128))); + for bytes in helper.vals { + let value = from_flexbuffer(&bytes).unwrap(); + vals.push(value); + } - // Convert each shard from Vec<(Value, Value)> back into HashMap let data: Vec>>> = helper .data .into_iter() .map(|shard_vec| { let mut map = hashbrown::HashMap::new(); - for (k_val, v_val) in shard_vec { - let k: K = from_value(k_val).unwrap(); - let v: V = from_value(v_val).unwrap(); + for entry in shard_vec { + let SerializedEntry { key, value } = entry; + let (k, v) = ( + from_flexbuffer(&key).unwrap(), + from_flexbuffer(&value).unwrap(), + ); map.insert(k, v); } Arc::new(Mutex::new(map)) @@ -106,7 +144,7 @@ where .collect(); Ok(InternTable { - vals: helper.vals, + vals, data, shards_log2: helper.shards_log2, }) From 2f83a6db6abded5c96e46a3225bb1599a1030a0f Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 11:34:20 -0700 Subject: [PATCH 03/10] skip when there are no extracts --- src/poach.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/poach.rs b/src/poach.rs index 39720f4a2..d2149bac6 100644 --- a/src/poach.rs +++ b/src/poach.rs @@ -470,6 +470,11 @@ fn poach( .collect::>() .join("\n"); + if extracts.trim().is_empty() { + timed_egraph.write_timeline(&out_dir.join(format!("{name}-timeline.json")))?; + return Ok(()); + } + let extract_cmds = timed_egraph .egraphs .last_mut() From 86a0022f3f6c28fcf0b593cf1192e9ab4a082062 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 11:38:36 -0700 Subject: [PATCH 04/10] register type --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 56a0ce9d7..7af11a826 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1188,6 +1188,7 @@ impl EGraph { .collect::>(); for builtin in ["Unit", "String", "bool", "i64", "f64", "BigInt", "BigRat"] { if let Some(sort) = sorts.remove(builtin) { + sort.register_type(&mut self.backend); sort.register_primitives(self); } } @@ -1204,6 +1205,7 @@ impl EGraph { if a > b { a } else { b } }); for (_, sort) in sorts { + sort.register_type(&mut self.backend); sort.register_primitives(self); } self.backend.restore_deserialized_runtime(); From bd593dd1f43071f276817fb214167d7dcf775003 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 11:48:54 -0700 Subject: [PATCH 05/10] clear runtime-only caches and trackers after deserialization --- core-relations/src/common.rs | 7 +++++++ core-relations/src/containers/mod.rs | 4 ++++ core-relations/src/free_join/mod.rs | 7 +++++++ core-relations/src/table/mod.rs | 5 +++++ core-relations/src/table_spec.rs | 7 +++++++ egglog-bridge/src/lib.rs | 2 ++ 6 files changed, 32 insertions(+) diff --git a/core-relations/src/common.rs b/core-relations/src/common.rs index 3a7282caa..b131d76cf 100644 --- a/core-relations/src/common.rs +++ b/core-relations/src/common.rs @@ -332,6 +332,13 @@ pub(crate) struct SubsetTracker { } impl SubsetTracker { + /// Deserialized tables may have different row/version state than when this + /// tracker was recorded, so cached "last rebuilt at" entries cannot be + /// trusted across serialization boundaries. + pub(crate) fn restore_deserialized_runtime(&mut self) { + *self = Default::default(); + } + /// Hand back the subset of the table needed to be scanned in order to see all updates since /// the last call to this method. /// diff --git a/core-relations/src/containers/mod.rs b/core-relations/src/containers/mod.rs index 414aabbb5..5638ed785 100644 --- a/core-relations/src/containers/mod.rs +++ b/core-relations/src/containers/mod.rs @@ -65,6 +65,10 @@ impl ContainerValues { Default::default() } + pub fn restore_deserialized_runtime(&mut self) { + self.subset_tracker.restore_deserialized_runtime(); + } + fn get(&self) -> Option<&ContainerEnv> { let id = self .container_ids diff --git a/core-relations/src/free_join/mod.rs b/core-relations/src/free_join/mod.rs index 9bccd0d1f..73a162b74 100644 --- a/core-relations/src/free_join/mod.rs +++ b/core-relations/src/free_join/mod.rs @@ -343,6 +343,13 @@ impl Database { &mut self.container_values } + pub fn restore_deserialized_runtime(&mut self) { + for (_, table) in self.tables.iter_mut() { + table.table.restore_deserialized_runtime(); + } + self.container_values.restore_deserialized_runtime(); + } + pub fn rebuild_containers(&mut self, table_id: TableId) -> bool { let mut containers = mem::take(&mut self.container_values); let table = &self.tables[table_id].table; diff --git a/core-relations/src/table/mod.rs b/core-relations/src/table/mod.rs index 039b20912..45d9b24ef 100644 --- a/core-relations/src/table/mod.rs +++ b/core-relations/src/table/mod.rs @@ -436,6 +436,11 @@ impl Table for SortedWritesTable { fn as_any_mut(&mut self) -> &mut dyn Any { self } + + fn restore_deserialized_runtime(&mut self) { + self.subset_tracker.restore_deserialized_runtime(); + } + fn clear(&mut self) { self.pending_state.clear(); if self.data.data.len() == 0 { diff --git a/core-relations/src/table_spec.rs b/core-relations/src/table_spec.rs index 902c0213e..4c75ef379 100644 --- a/core-relations/src/table_spec.rs +++ b/core-relations/src/table_spec.rs @@ -187,6 +187,13 @@ pub trait Table: Any + Send + Sync { /// A mutable variant of [`Table::as_any`] for downcasting. fn as_any_mut(&mut self) -> &mut dyn Any; + /// Restore runtime-only state after deserialization. + /// + /// Most tables serialize only durable data, so they do not need any extra + /// fixup and can use the default no-op implementation. Tables with + /// incremental caches or trackers should override this and reset them. + fn restore_deserialized_runtime(&mut self) {} + /// The schema of the table. /// /// These are immutable properties of the table; callers can assume they diff --git a/egglog-bridge/src/lib.rs b/egglog-bridge/src/lib.rs index 4b591ef86..b64f67883 100644 --- a/egglog-bridge/src/lib.rs +++ b/egglog-bridge/src/lib.rs @@ -1163,6 +1163,8 @@ impl EGraph { } pub fn restore_deserialized_runtime(&mut self) { + self.db.restore_deserialized_runtime(); + let funcs = self .funcs .iter() From c69eee0ccf501af1a74ad0ca155ce2c5c6008326 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 11:51:52 -0700 Subject: [PATCH 06/10] suffix instead of prefix --- src/poach.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/poach.rs b/src/poach.rs index d2149bac6..6cff9b38c 100644 --- a/src/poach.rs +++ b/src/poach.rs @@ -550,7 +550,7 @@ fn poach( if self.map.contains_key(&name) { panic!("duplicate variable names") } else { - let namespaced = format!("@@{name}"); + let namespaced = format!("{name}@@"); self.map.insert(name.clone(), namespaced.clone()); namespaced } From a38bf8ca977397f5331c94533fa7ea8f4c0b6f5d Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 12:39:37 -0700 Subject: [PATCH 07/10] run rules in mine mode --- src/poach.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/poach.rs b/src/poach.rs index 6cff9b38c..6b73452a6 100644 --- a/src/poach.rs +++ b/src/poach.rs @@ -180,6 +180,7 @@ where .file_stem() .and_then(|s| s.to_str()) .unwrap_or("unknown"); + println!("{}", name); let mut timed_egraph = if let Some(path) = initial_egraph { if path.is_file() { @@ -644,19 +645,16 @@ fn poach( let (filtered_cmds, filtered_sexps): (Vec<_>, Vec<_>) = all_cmds .into_iter() .zip(all_sexps) - .filter(|(c, _)| { - match c { - GenericCommand::Action(GenericAction::Let(..)) => true, - egglog::ast::GenericCommand::Extract(..) => true, - egglog::ast::GenericCommand::MultiExtract(..) => true, - // TODO: Running rules on a deserialized egraph currently does not work - // | egglog::ast::GenericCommand::RunSchedule(_) - egglog::ast::GenericCommand::PrintOverallStatistics(..) => true, - egglog::ast::GenericCommand::Check(..) => true, - egglog::ast::GenericCommand::PrintFunction(..) => true, - egglog::ast::GenericCommand::PrintSize(..) => true, - _ => false, - } + .filter(|(c, _)| match c { + GenericCommand::Action(GenericAction::Let(..)) => true, + egglog::ast::GenericCommand::Extract(..) => true, + egglog::ast::GenericCommand::MultiExtract(..) => true, + egglog::ast::GenericCommand::RunSchedule(..) => true, + egglog::ast::GenericCommand::PrintOverallStatistics(..) => true, + egglog::ast::GenericCommand::Check(..) => true, + egglog::ast::GenericCommand::PrintFunction(..) => true, + egglog::ast::GenericCommand::PrintSize(..) => true, + _ => false, }) .map(|(cmd, sexp)| { ( From 0902664e399484fbff9b26df61751c6ca7e36c65 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 13:57:22 -0700 Subject: [PATCH 08/10] clear stale data and rebuild --- core-relations/src/table/mod.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/core-relations/src/table/mod.rs b/core-relations/src/table/mod.rs index 45d9b24ef..0c596234b 100644 --- a/core-relations/src/table/mod.rs +++ b/core-relations/src/table/mod.rs @@ -439,6 +439,32 @@ impl Table for SortedWritesTable { fn restore_deserialized_runtime(&mut self) { self.subset_tracker.restore_deserialized_runtime(); + self.hash = ShardedHashTable::default(); + self.offsets.clear(); + + for (i, row) in self.data.data.iter().enumerate() { + if row[0].is_stale() { + continue; + } + + let row_id = RowId::from_usize(i); + let (shard, hc) = hash_code(self.hash.shard_data(), row, self.n_keys); + self.hash.mut_shards()[shard.index()].insert_unique( + hc, + TableEntry { + hashcode: hc as _, + row: row_id, + }, + TableEntry::hashcode, + ); + + if let Some(sort_by) = self.sort_by { + let sort_val = row[sort_by.index()]; + if self.offsets.last().is_none_or(|(max, _)| sort_val > *max) { + self.offsets.push((sort_val, row_id)); + } + } + } } fn clear(&mut self) { From 3a163e351fbabaa8183c1b62ccc0b20029d0bf5d Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 14:28:19 -0700 Subject: [PATCH 09/10] handle empty vec --- src/sort/vec.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sort/vec.rs b/src/sort/vec.rs index 2bfac62bd..da36d01ba 100644 --- a/src/sort/vec.rs +++ b/src/sort/vec.rs @@ -102,10 +102,10 @@ impl ContainerSort for VecSort { container_values: &ContainerValues, value: Value, ) -> Vec<(ArcSort, Value)> { - let val = container_values - .get_val::(value) - .unwrap() - .clone(); + let Some(val) = container_values.get_val::(value) else { + return vec![]; + }; + let val = val.clone(); val.data .iter() .map(|e| (self.element.clone(), *e)) From bc89bb5451d36eaf3680022c3b9724ba28925193 Mon Sep 17 00:00:00 2001 From: Anjali Pal Date: Fri, 13 Mar 2026 14:46:12 -0700 Subject: [PATCH 10/10] cargo fmt --- core-relations/src/lib.rs | 3 +-- core-relations/src/table_spec.rs | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core-relations/src/lib.rs b/core-relations/src/lib.rs index 462bdc0a3..3cdfcc638 100644 --- a/core-relations/src/lib.rs +++ b/core-relations/src/lib.rs @@ -26,8 +26,7 @@ mod tests; pub use action::{ExecutionState, MergeVal, QueryEntry, WriteVal}; pub use base_values::{ - register_deserializable_base_type, BaseValue, BaseValueId, BaseValuePrinter, BaseValues, - Boxed, + register_deserializable_base_type, BaseValue, BaseValueId, BaseValuePrinter, BaseValues, Boxed, }; pub use common::Value; pub use containers::{ContainerValue, ContainerValueId, ContainerValues}; diff --git a/core-relations/src/table_spec.rs b/core-relations/src/table_spec.rs index 4c75ef379..f7e7d618d 100644 --- a/core-relations/src/table_spec.rs +++ b/core-relations/src/table_spec.rs @@ -27,8 +27,7 @@ use crate::{ offsets::{RowId, Subset, SubsetRef}, pool::{with_pool_set, PoolSet, Pooled}, row_buffer::{RowBuffer, TaggedRowBuffer}, - DisplacedTable, DisplacedTableWithProvenance, - QueryEntry, TableId, Variable, + DisplacedTable, DisplacedTableWithProvenance, QueryEntry, TableId, Variable, }; define_id!(pub ColumnId, u32, "a particular column in a table"); @@ -561,7 +560,9 @@ impl<'de> Deserialize<'de> for WrappedTable { } else if inner.as_any().is::() { wrapper::() } else { - return Err(serde::de::Error::custom("unknown table type for WrappedTable")); + return Err(serde::de::Error::custom( + "unknown table type for WrappedTable", + )); }; Ok(WrappedTable { inner, wrapper })