Skip to main content

vantage_table/
any.rs

1//! Type-erased table wrapper with downcasting support
2//!
3//! `AnyTable` provides a way to store tables of different types uniformly
4//! while preserving the ability to recover the concrete type through downcasting.
5//!
6//! Tables whose value/id types differ from `ciborium::Value`/`String` can still
7//! be wrapped via [`AnyTable::from_table`] as long as the value type implements
8//! `Into<CborValue> + From<CborValue>` and the id type implements
9//! `Display + From<String>`.
10//!
11//! Each backend brings its own native value type (`AnySurrealType`,
12//! `AnyMongoType`, `AnyPostgresType`, etc.); CBOR is just the carrier across
13//! the type-erased boundary, chosen because it preserves type fidelity that
14//! JSON loses (integer vs float, binary blobs, NaN/Infinity, precise
15//! decimals).
16
17use std::any::TypeId;
18use std::fmt::Display;
19
20use async_trait::async_trait;
21use ciborium::Value as CborValue;
22use indexmap::IndexMap;
23use vantage_core::{Result, error};
24use vantage_dataset::traits::{ReadableValueSet, ValueSet, WritableValueSet};
25use vantage_types::{Entity, Record};
26
27// Type alias for cleaner code
28pub type AnyRecord = Record<CborValue>;
29
30use crate::{
31    conditions::ConditionHandle,
32    pagination::Pagination,
33    table::Table,
34    traits::{table_like::TableLike, table_source::TableSource},
35};
36
37/// Type-erased table that can be downcast to concrete `Table<T, E>`
38/// Works with AnyRecord (which uses ciborium::Value)
39pub struct AnyTable {
40    inner: Box<dyn TableLike<Value = CborValue, Id = String>>,
41    datasource_type_id: TypeId,
42    entity_type_id: TypeId,
43    datasource_name: &'static str,
44    entity_name: &'static str,
45}
46
47impl AnyTable {
48    /// Create a new AnyTable from a concrete table.
49    /// Only works with tables that use `ciborium::Value` as their Value type.
50    pub fn new<
51        T: TableSource<Value = CborValue, Id = String> + 'static,
52        E: Entity<CborValue> + 'static,
53    >(
54        table: Table<T, E>,
55    ) -> Self {
56        Self {
57            inner: Box::new(table),
58            datasource_type_id: TypeId::of::<T>(),
59            entity_type_id: TypeId::of::<E>(),
60            datasource_name: std::any::type_name::<T>(),
61            entity_name: std::any::type_name::<E>(),
62        }
63    }
64
65    /// Create an AnyTable from a table with any value/id types that convert
66    /// to/from CBOR.
67    ///
68    /// This wraps the table in an internal adapter that converts values on the
69    /// fly, so any `TableSource` can be used with the unified `AnyTable`
70    /// interface.
71    pub fn from_table<T, E>(table: Table<T, E>) -> Self
72    where
73        T: TableSource + 'static,
74        T::Value: Into<CborValue> + From<CborValue>,
75        T::Id: Display + From<String>,
76        E: Entity<T::Value> + 'static,
77    {
78        Self {
79            inner: Box::new(CborAdapter { inner: table }),
80            datasource_type_id: TypeId::of::<T>(),
81            entity_type_id: TypeId::of::<E>(),
82            datasource_name: std::any::type_name::<T>(),
83            entity_name: std::any::type_name::<E>(),
84        }
85    }
86
87    /// Wrap any `TableLike<Value = CborValue, Id = String>` directly. Use
88    /// this for table-like wrappers (e.g. `vantage_live::LiveTable`) that
89    /// aren't a `Table<T, E>` instance themselves but already satisfy the
90    /// `AnyTable`-facing trait surface. The entity type is recorded as
91    /// `()` since wrappers don't carry one.
92    pub fn from_table_like<T>(table: T) -> Self
93    where
94        T: TableLike<Value = CborValue, Id = String> + 'static,
95    {
96        Self {
97            inner: Box::new(table),
98            datasource_type_id: TypeId::of::<T>(),
99            entity_type_id: TypeId::of::<()>(),
100            datasource_name: std::any::type_name::<T>(),
101            entity_name: "<wrapped>",
102        }
103    }
104
105    /// Attempt to downcast to a concrete `Table<T, E>`
106    ///
107    /// Returns `Err(self)` if the type doesn't match, allowing recovery
108    pub fn downcast<
109        T: TableSource<Value = CborValue, Id = String> + 'static,
110        E: Entity<CborValue> + 'static,
111    >(
112        self,
113    ) -> Result<Table<T, E>> {
114        // Check TypeIds for better error messages
115        if self.datasource_type_id != TypeId::of::<T>() {
116            let expected = std::any::type_name::<T>();
117            return Err(error!(
118                "DataSource type mismatch",
119                expected = expected,
120                actual = self.datasource_name
121            ));
122        }
123        if self.entity_type_id != TypeId::of::<E>() {
124            let expected = std::any::type_name::<E>();
125            return Err(error!(
126                "Entity type mismatch",
127                expected = expected,
128                actual = self.entity_name
129            ));
130        }
131
132        // Perform the actual downcast
133        self.inner
134            .into_any()
135            .downcast::<Table<T, E>>()
136            .map(|boxed| *boxed)
137            .map_err(|_| error!("Failed to downcast table"))
138    }
139
140    /// Get the datasource type name for debugging
141    pub fn datasource_name(&self) -> &str {
142        self.datasource_name
143    }
144
145    /// Get the entity type name for debugging
146    pub fn entity_name(&self) -> &str {
147        self.entity_name
148    }
149
150    /// Get the datasource TypeId
151    pub fn datasource_type_id(&self) -> TypeId {
152        self.datasource_type_id
153    }
154
155    /// Get the entity TypeId
156    pub fn entity_type_id(&self) -> TypeId {
157        self.entity_type_id
158    }
159
160    /// Check if this table matches the given types
161    pub fn is_type<T: TableSource + 'static, E: Entity<CborValue> + 'static>(&self) -> bool {
162        self.datasource_type_id == TypeId::of::<T>() && self.entity_type_id == TypeId::of::<E>()
163    }
164}
165
166impl Clone for AnyTable {
167    fn clone(&self) -> Self {
168        Self {
169            inner: self.inner.clone_box(),
170            datasource_type_id: self.datasource_type_id,
171            entity_type_id: self.entity_type_id,
172            datasource_name: self.datasource_name,
173            entity_name: self.entity_name,
174        }
175    }
176}
177
178impl std::fmt::Debug for AnyTable {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        f.debug_struct("AnyTable")
181            .field("datasource", &self.datasource_name)
182            .field("entity", &self.entity_name)
183            .finish()
184    }
185}
186
187// Implement ValueSet first
188impl ValueSet for AnyTable {
189    type Id = String;
190    type Value = CborValue;
191}
192
193// Implement ReadableValueSet by delegating to inner TableLike
194#[async_trait]
195impl ReadableValueSet for AnyTable {
196    async fn list_values(&self) -> Result<IndexMap<Self::Id, Record<Self::Value>>> {
197        self.inner.list_values().await
198    }
199
200    async fn get_value(&self, id: &Self::Id) -> Result<Option<Record<Self::Value>>> {
201        self.inner.get_value(id).await
202    }
203
204    async fn get_some_value(&self) -> Result<Option<(Self::Id, Record<Self::Value>)>> {
205        self.inner.get_some_value().await
206    }
207}
208
209// Implement WritableValueSet by delegating to inner TableLike
210#[async_trait]
211impl WritableValueSet for AnyTable {
212    async fn insert_value(
213        &self,
214        id: &Self::Id,
215        record: &Record<Self::Value>,
216    ) -> Result<Record<Self::Value>> {
217        self.inner.insert_value(id, record).await
218    }
219
220    async fn replace_value(
221        &self,
222        id: &Self::Id,
223        record: &Record<Self::Value>,
224    ) -> Result<Record<Self::Value>> {
225        self.inner.replace_value(id, record).await
226    }
227
228    async fn patch_value(
229        &self,
230        id: &Self::Id,
231        partial: &Record<Self::Value>,
232    ) -> Result<Record<Self::Value>> {
233        self.inner.patch_value(id, partial).await
234    }
235
236    async fn delete(&self, id: &Self::Id) -> Result<()> {
237        self.inner.delete(id).await
238    }
239
240    async fn delete_all(&self) -> Result<()> {
241        self.inner.delete_all().await
242    }
243}
244
245// Implement TableLike by delegating to inner
246#[async_trait]
247impl TableLike for AnyTable {
248    fn table_name(&self) -> &str {
249        self.inner.table_name()
250    }
251
252    fn table_alias(&self) -> &str {
253        self.inner.table_alias()
254    }
255
256    fn column_names(&self) -> Vec<String> {
257        self.inner.column_names()
258    }
259
260    fn add_condition(&mut self, condition: Box<dyn std::any::Any + Send + Sync>) -> Result<()> {
261        self.inner.add_condition(condition)
262    }
263
264    fn temp_add_condition(
265        &mut self,
266        condition: vantage_expressions::AnyExpression,
267    ) -> Result<ConditionHandle> {
268        self.inner.temp_add_condition(condition)
269    }
270
271    fn temp_remove_condition(&mut self, handle: ConditionHandle) -> Result<()> {
272        self.inner.temp_remove_condition(handle)
273    }
274
275    fn search_expression(&self, search_value: &str) -> Result<vantage_expressions::AnyExpression> {
276        self.inner.search_expression(search_value)
277    }
278
279    fn clone_box(&self) -> Box<dyn TableLike<Value = Self::Value, Id = Self::Id>> {
280        Box::new(self.clone())
281    }
282
283    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
284        self
285    }
286
287    fn as_any_ref(&self) -> &dyn std::any::Any {
288        self
289    }
290
291    fn set_pagination(&mut self, pagination: Option<Pagination>) {
292        self.inner.set_pagination(pagination)
293    }
294
295    fn get_pagination(&self) -> Option<&Pagination> {
296        self.inner.get_pagination()
297    }
298
299    async fn get_count(&self) -> vantage_core::Result<i64> {
300        self.inner.get_count().await
301    }
302
303    fn get_ref(&self, relation: &str) -> Result<AnyTable> {
304        self.inner.get_ref(relation)
305    }
306}
307
308impl AnyTable {
309    /// Traverse a named reference and return the related table as `AnyTable`.
310    ///
311    /// Inherent wrapper so callers don't need `use TableLike` in scope.
312    pub fn get_ref(&self, relation: &str) -> Result<AnyTable> {
313        TableLike::get_ref(self, relation)
314    }
315
316    /// Configure pagination using a callback
317    pub fn with_pagination<F>(&mut self, func: F)
318    where
319        F: FnOnce(&mut Pagination),
320    {
321        let mut pagination = self.inner.get_pagination().copied().unwrap_or_default();
322        func(&mut pagination);
323        self.inner.set_pagination(Some(pagination));
324    }
325}
326
327// ── CborAdapter: blanket bridge for non-CBOR table types ────────────────
328
329/// Wraps a `Table<T, E>` whose value/id types are not `ciborium::Value`/`String`,
330/// converting on the fly so it can satisfy `TableLike<Value = CborValue, Id = String>`.
331struct CborAdapter<T: TableSource, E: Entity<T::Value>> {
332    inner: Table<T, E>,
333}
334
335impl<T: TableSource, E: Entity<T::Value>> Clone for CborAdapter<T, E> {
336    fn clone(&self) -> Self {
337        Self {
338            inner: self.inner.clone(),
339        }
340    }
341}
342
343impl<T, E> CborAdapter<T, E>
344where
345    T: TableSource,
346    T::Value: Into<CborValue>,
347    T::Id: Display,
348    E: Entity<T::Value>,
349{
350    fn convert_record(record: Record<T::Value>) -> Record<CborValue> {
351        record.into_iter().map(|(k, v)| (k, v.into())).collect()
352    }
353
354    fn convert_record_back(record: &Record<CborValue>) -> Record<T::Value>
355    where
356        T::Value: From<CborValue>,
357    {
358        record
359            .iter()
360            .map(|(k, v)| (k.clone(), T::Value::from(v.clone())))
361            .collect()
362    }
363}
364
365impl<T, E> ValueSet for CborAdapter<T, E>
366where
367    T: TableSource,
368    E: Entity<T::Value>,
369{
370    type Id = String;
371    type Value = CborValue;
372}
373
374#[async_trait]
375impl<T, E> ReadableValueSet for CborAdapter<T, E>
376where
377    T: TableSource,
378    T::Value: Into<CborValue>,
379    T::Id: Display + From<String>,
380    E: Entity<T::Value>,
381{
382    async fn list_values(&self) -> Result<IndexMap<String, Record<CborValue>>> {
383        let raw = self
384            .inner
385            .data_source()
386            .list_table_values(&self.inner)
387            .await?;
388        Ok(raw
389            .into_iter()
390            .map(|(id, rec)| (id.to_string(), Self::convert_record(rec)))
391            .collect())
392    }
393
394    async fn get_value(&self, id: &String) -> Result<Option<Record<CborValue>>> {
395        let native_id: T::Id = id.clone().into();
396        let rec = self
397            .inner
398            .data_source()
399            .get_table_value(&self.inner, &native_id)
400            .await?;
401        Ok(rec.map(Self::convert_record))
402    }
403
404    async fn get_some_value(&self) -> Result<Option<(String, Record<CborValue>)>> {
405        let result = self
406            .inner
407            .data_source()
408            .get_table_some_value(&self.inner)
409            .await?;
410        Ok(result.map(|(id, rec)| (id.to_string(), Self::convert_record(rec))))
411    }
412}
413
414#[async_trait]
415impl<T, E> WritableValueSet for CborAdapter<T, E>
416where
417    T: TableSource,
418    T::Value: Into<CborValue> + From<CborValue>,
419    T::Id: Display + From<String>,
420    E: Entity<T::Value>,
421{
422    async fn insert_value(
423        &self,
424        id: &String,
425        record: &Record<CborValue>,
426    ) -> Result<Record<CborValue>> {
427        let native_id: T::Id = id.clone().into();
428        let native_rec = Self::convert_record_back(record);
429        let returned = self
430            .inner
431            .data_source()
432            .insert_table_value(&self.inner, &native_id, &native_rec)
433            .await?;
434        Ok(Self::convert_record(returned))
435    }
436
437    async fn replace_value(
438        &self,
439        id: &String,
440        record: &Record<CborValue>,
441    ) -> Result<Record<CborValue>> {
442        let native_id: T::Id = id.clone().into();
443        let native_rec = Self::convert_record_back(record);
444        let returned = self
445            .inner
446            .data_source()
447            .replace_table_value(&self.inner, &native_id, &native_rec)
448            .await?;
449        Ok(Self::convert_record(returned))
450    }
451
452    async fn patch_value(
453        &self,
454        id: &String,
455        partial: &Record<CborValue>,
456    ) -> Result<Record<CborValue>> {
457        let native_id: T::Id = id.clone().into();
458        let native_rec = Self::convert_record_back(partial);
459        let returned = self
460            .inner
461            .data_source()
462            .patch_table_value(&self.inner, &native_id, &native_rec)
463            .await?;
464        Ok(Self::convert_record(returned))
465    }
466
467    async fn delete(&self, id: &String) -> Result<()> {
468        let native_id: T::Id = id.clone().into();
469        self.inner
470            .data_source()
471            .delete_table_value(&self.inner, &native_id)
472            .await
473    }
474
475    async fn delete_all(&self) -> Result<()> {
476        self.inner
477            .data_source()
478            .delete_table_all_values(&self.inner)
479            .await
480    }
481}
482
483#[async_trait]
484impl<T, E> TableLike for CborAdapter<T, E>
485where
486    T: TableSource + 'static,
487    T::Value: Into<CborValue> + From<CborValue>,
488    T::Id: Display + From<String>,
489    E: Entity<T::Value> + 'static,
490{
491    fn table_name(&self) -> &str {
492        self.inner.table_name()
493    }
494
495    fn table_alias(&self) -> &str {
496        self.inner.table_name()
497    }
498
499    fn column_names(&self) -> Vec<String> {
500        self.inner.columns().keys().cloned().collect()
501    }
502
503    fn add_condition(&mut self, _condition: Box<dyn std::any::Any + Send + Sync>) -> Result<()> {
504        Err(error!("add_condition not supported through CborAdapter"))
505    }
506
507    fn temp_add_condition(
508        &mut self,
509        _condition: vantage_expressions::AnyExpression,
510    ) -> Result<ConditionHandle> {
511        Err(error!(
512            "temp_add_condition not supported through CborAdapter"
513        ))
514    }
515
516    fn temp_remove_condition(&mut self, _handle: ConditionHandle) -> Result<()> {
517        Err(error!(
518            "temp_remove_condition not supported through CborAdapter"
519        ))
520    }
521
522    fn search_expression(&self, _search_value: &str) -> Result<vantage_expressions::AnyExpression> {
523        Err(error!(
524            "search_expression not supported through CborAdapter"
525        ))
526    }
527
528    fn clone_box(&self) -> Box<dyn TableLike<Value = CborValue, Id = String>> {
529        Box::new(self.clone())
530    }
531
532    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
533        self
534    }
535
536    fn as_any_ref(&self) -> &dyn std::any::Any {
537        self
538    }
539
540    fn set_pagination(&mut self, pagination: Option<Pagination>) {
541        self.inner.set_pagination(pagination);
542    }
543
544    fn get_pagination(&self) -> Option<&Pagination> {
545        self.inner.pagination()
546    }
547
548    async fn get_count(&self) -> Result<i64> {
549        self.inner.data_source().get_table_count(&self.inner).await
550    }
551
552    fn get_ref(&self, relation: &str) -> Result<AnyTable> {
553        self.inner.get_ref(relation)
554    }
555}
556
557// Inline AnyTable tests using `MockTableSource` were dropped as part of the
558// CBOR swap. `MockTableSource` still uses `serde_json::Value`, but `AnyTable`
559// now requires `Value = CborValue`. A new CBOR-flavoured mock + restored
560// integration tests will live in `tests/table_like.rs` once `MockTableSource`
561// (or a sibling mock) gains CBOR support — see TODO.md "Architecture: Make
562// ImTable / ImDataSource generic over Value".