Skip to main content

uni_plugin/traits/
storage.rs

1//! Per-label plugin storage — `MATCH` / `CREATE` against a pluggable store.
2//!
3//! M5a (2026-05-24): the [`Storage`] trait is `#[async_trait]`. A plugin
4//! registers a [`Storage`] instance for a native label via
5//! [`crate::PluginRegistrar::label_storage`]; native-schema scans for that
6//! label are routed through the plugin's store instead of the host backend.
7
8use arrow_schema::SchemaRef;
9use async_trait::async_trait;
10use datafusion::arrow::record_batch::RecordBatch;
11use datafusion::execution::SendableRecordBatchStream;
12use datafusion::logical_expr::Expr;
13
14use crate::errors::FnError;
15
16/// Opaque write handle returned by [`Storage::write_batch`].
17#[derive(Clone, Debug)]
18pub struct WriteHandle {
19    /// Backend-specific identifier (LSN, transaction id, …).
20    pub id: u64,
21}
22
23/// Metadata returned by [`Storage::fork`] describing the newly-created branch.
24///
25/// `parent_version` is the backend version pinned as the fork-point, so
26/// callers orchestrating nested forks can chain `create_branch_from`-style
27/// calls without re-querying the backend. `branch_name` echoes the
28/// `dst_branch` argument, surfaced explicitly so backends with name
29/// canonicalization can return the resolved form.
30#[derive(Clone, Debug)]
31pub struct BranchMetadata {
32    /// Backend version pinned as the new branch's fork-point.
33    pub parent_version: u64,
34    /// Branch identifier as registered on the backend.
35    pub branch_name: String,
36}
37
38/// Per-instance storage interface.
39#[async_trait]
40pub trait Storage: Send + Sync {
41    /// Stream batches from `table` matching `predicate`.
42    ///
43    /// `predicate = None` means a full scan.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`FnError`] if the read cannot start.
48    async fn read_batch(
49        &self,
50        table: &str,
51        predicate: Option<&Expr>,
52    ) -> Result<SendableRecordBatchStream, FnError>;
53
54    /// Write a single batch to `table`.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`FnError`] on write failure.
59    async fn write_batch(&self, table: &str, batch: &RecordBatch) -> Result<WriteHandle, FnError>;
60
61    /// List tables known to this backend.
62    ///
63    /// # Errors
64    ///
65    /// Returns [`FnError`] if the listing cannot complete.
66    async fn list_tables(&self) -> Result<Vec<String>, FnError>;
67
68    /// Delete rows in `table` matching `predicate`. Returns the number of
69    /// rows actually deleted.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`FnError`] on delete failure.
74    async fn delete(&self, table: &str, predicate: &Expr) -> Result<u64, FnError>;
75
76    /// Whether this backend supports branched / forked state.
77    fn supports_branching(&self) -> bool {
78        false
79    }
80
81    /// Fork `src_branch` of `table` into `dst_branch`. Default: unsupported.
82    ///
83    /// Granularity is per-dataset (`table`) because real branching backends
84    /// (Lance) track branches and versions independently per dataset.
85    /// Multi-dataset orchestration (atomic across all tables of a logical
86    /// fork) is the caller's responsibility.
87    ///
88    /// # Errors
89    ///
90    /// Returns [`FnError`] if branching is not supported or the fork
91    /// operation fails (missing source branch, name collision, I/O).
92    async fn fork(
93        &self,
94        _table: &str,
95        _src_branch: &str,
96        _dst_branch: &str,
97    ) -> Result<BranchMetadata, FnError> {
98        Err(FnError::new(
99            0x10,
100            "storage backend does not support branching",
101        ))
102    }
103
104    /// Backend-declared schema for `table`, if known.
105    async fn schema(&self, _table: &str) -> Option<SchemaRef> {
106        None
107    }
108}