Skip to main content

valence_core/backend/
port.rs

1//! [`DatabaseBackend`] port and capability metadata.
2
3use crate::compiled_query::CompiledQuery;
4use crate::error::{Error, Result};
5use crate::record_id::RecordId;
6use crate::ttl::{BackendTtlCapability, SchemaTtlPolicy};
7use std::any::Any;
8
9/// Capabilities advertised by a storage adapter (telemetry labels and contract tests).
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct BackendCapabilities {
12    /// Whether [`DatabaseBackend::merge_record`] is supported (not the default error stub).
13    pub supports_merge: bool,
14    /// Whether graph edge methods (`relate_edge`, `unrelate_edge`, `get_edge_targets`) are supported.
15    pub supports_graph_edges: bool,
16    /// Short label attached to instrumentation counters (e.g. `"mem"`, `"surrealdb"`).
17    pub telemetry_label: &'static str,
18}
19
20impl BackendCapabilities {
21    /// Capabilities for the in-memory reference adapter.
22    #[must_use]
23    pub const fn mem() -> Self {
24        Self {
25            supports_merge: true,
26            supports_graph_edges: true,
27            telemetry_label: "mem",
28        }
29    }
30}
31
32/// Storage engine behind Valence: CRUD, compiled queries, and graph edges.
33///
34/// `valence-core` defines this trait only — no engine SDKs. Third-party adapters
35/// implement it in separate crates and register instances on [`crate::ValenceBuilder`].
36///
37/// | Concern | Contract |
38/// |---------|----------|
39/// | [`engine_id`](Self::engine_id) | **Open** slug (not a closed enum) |
40/// | [`capabilities`](Self::capabilities) | Merge/graph support + telemetry label |
41/// | CRUD / queries / edges / TTL | Per-method contracts below |
42///
43/// Router keys combine a logical name with [`engine_id`](Self::engine_id) via
44/// [`crate::router_key()`].
45///
46/// # Implementing a published adapter
47///
48/// 1. Depend on `valence-core` only (+ `async-trait`, etc.).
49/// 2. `impl DatabaseBackend` and export `pub const ENGINE_ID: &str`.
50/// 3. Export a schema evaluator, e.g. `pub const PRIMARY: DatabaseFromEngine =
51///    Database::from_engine("primary", ENGINE_ID)`.
52/// 4. Optional: `XBackend::builder()` with explicit setters; `from_env_defaults()` fills
53///    **unset** fields only.
54/// 5. Host wires with `.add_backend("primary", Arc::new(adapter))` — no public crate feature.
55///
56/// Reference: `examples/acme-valence-backend-stub`.
57///
58/// # Examples
59///
60/// ```
61/// use std::sync::Arc;
62/// use valence_backend_mem::{InMemoryBackend, ENGINE_ID};
63/// use valence_core::{DatabaseBackend, Valence};
64///
65/// let backend = Arc::new(InMemoryBackend::new());
66/// assert_eq!(backend.engine_id(), ENGINE_ID);
67/// let valence = Valence::builder()
68///     .add_backend("default", backend)
69///     .build()
70///     .expect("build");
71/// assert_eq!(
72///     valence.active_backend().unwrap().engine_id(),
73///     ENGINE_ID
74/// );
75/// ```
76#[async_trait::async_trait]
77pub trait DatabaseBackend: Send + Sync + std::fmt::Debug + 'static {
78    /// Stable **open** engine slug for router keys (see [`crate::router_key()`]).
79    ///
80    /// First-party constants live in [`crate::KnownEngines`] for ergonomics — that is not
81    /// a closed set; third-party crates define their own slugs.
82    fn engine_id(&self) -> &'static str;
83
84    /// Adapter capabilities for contract tests and telemetry.
85    fn capabilities(&self) -> BackendCapabilities;
86
87    #[doc(hidden)]
88    fn as_any_local(&self) -> Option<&dyn Any> {
89        None
90    }
91
92    /// Select namespace/database on engines that support multi-tenant routing.
93    ///
94    /// **Contract:** default implementation is a no-op; remote adapters may override.
95    async fn use_namespace(&self, ns: &str, db_name: &str) -> Result<()> {
96        let _ = (ns, db_name);
97        Ok(())
98    }
99
100    /// Execute a compiled admin/query statement and return JSON rows.
101    ///
102    /// **Contract:** must honor parameter binding in `compiled`; empty result sets return `Ok(vec![])`.
103    async fn execute_compiled_query(
104        &self,
105        compiled: &CompiledQuery,
106    ) -> Result<Vec<serde_json::Value>>;
107
108    /// Ensure a schemaless table exists before first write.
109    ///
110    /// **Contract:** default implementation is a no-op; adapters may create tables lazily elsewhere.
111    /// Prefer [`Self::ensure_typed_table`] for schema-backed models.
112    async fn ensure_schemaless_table(&self, table: &str) -> Result<()> {
113        let _ = table;
114        Ok(())
115    }
116
117    /// Inspect physical columns/fields currently present for `table`.
118    ///
119    /// **Contract:** default returns `Ok(None)` (unknown / unsupported). Empty table →
120    /// `Ok(Some(layout))` with only discovered fields (may be empty aside from conventions).
121    async fn inspect_typed_layout(
122        &self,
123        table: &str,
124    ) -> Result<Option<crate::storage_layout::StorageLayout>> {
125        let _ = table;
126        Ok(None)
127    }
128
129    /// Create a typed table/collection from [`crate::storage_layout::StorageLayout`] when missing.
130    ///
131    /// **Contract:** default falls back to [`Self::ensure_schemaless_table`] using `layout.table`
132    /// (deprecated path). SQL / Surreal / Redis adapters override.
133    async fn ensure_typed_table(
134        &self,
135        layout: &crate::storage_layout::StorageLayout,
136    ) -> Result<()> {
137        self.ensure_schemaless_table(&layout.table).await
138    }
139
140    /// Additive sync: add missing fields/indexes from `layout`; refuse drops/renames/type changes.
141    ///
142    /// **Contract:** default calls [`Self::ensure_typed_table`] (create-only).
143    async fn sync_typed_table(&self, layout: &crate::storage_layout::StorageLayout) -> Result<()> {
144        self.ensure_typed_table(layout).await
145    }
146
147    /// Read last-applied DSL schema version stamp for `table`.
148    ///
149    /// **Contract:** default returns `Ok(None)` (unsupported / no stamp store).
150    async fn read_schema_version(&self, table: &str) -> Result<Option<String>> {
151        let _ = table;
152        Ok(None)
153    }
154
155    /// Persist last-applied DSL schema version stamp for `table`.
156    ///
157    /// **Contract:** default is a no-op success (unsupported engines).
158    async fn write_schema_version(&self, table: &str, version: &str) -> Result<()> {
159        let _ = (table, version);
160        Ok(())
161    }
162
163    /// Fetch one record by primary key.
164    ///
165    /// **Contract:** returns `Ok(None)` when the row does not exist.
166    async fn get_record(&self, table: &str, id: &str) -> Result<Option<serde_json::Value>>;
167
168    /// Insert a new record; content must include any required fields.
169    ///
170    /// **Contract:** returns the persisted row (including server-assigned fields when applicable).
171    async fn create_record(
172        &self,
173        table: &str,
174        content: serde_json::Value,
175    ) -> Result<serde_json::Value>;
176
177    /// Replace an existing record by id.
178    ///
179    /// **Contract:** returns the updated row; errors when the id is missing unless the adapter
180    /// supports upsert semantics via [`Self::upsert_record`].
181    async fn update_record(
182        &self,
183        table: &str,
184        id: &str,
185        content: serde_json::Value,
186    ) -> Result<serde_json::Value>;
187
188    /// Patch an existing record with a partial JSON object.
189    ///
190    /// **Contract:** default returns `Error::Internal` — override when [`BackendCapabilities::supports_merge`]
191    /// is `true`.
192    async fn merge_record(
193        &self,
194        table: &str,
195        id: &str,
196        patch: serde_json::Value,
197    ) -> Result<serde_json::Value> {
198        let _ = (table, id, patch);
199        Err(Error::Internal(
200            "merge_record is not supported by this database backend".into(),
201        ))
202    }
203
204    /// Create or replace a record by explicit id.
205    async fn upsert_record(
206        &self,
207        table: &str,
208        id: &str,
209        content: serde_json::Value,
210    ) -> Result<serde_json::Value>;
211
212    /// Delete one record by primary key.
213    ///
214    /// **Contract:** succeeds when the row is already absent (idempotent delete).
215    async fn delete_record(&self, table: &str, id: &str) -> Result<()>;
216
217    /// Create a directed graph edge from `from` to `to` through `edge_table`.
218    async fn relate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()>;
219
220    /// Remove a directed graph edge.
221    ///
222    /// **Contract:** idempotent when the edge does not exist.
223    async fn unrelate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()>;
224
225    /// List target record ids reachable via outgoing edges in `edge_table`.
226    async fn get_edge_targets(&self, from: &RecordId, edge_table: &str) -> Result<Vec<RecordId>>;
227
228    /// List source record ids with an outgoing edge to `to` in `edge_table`.
229    ///
230    /// **Contract:** default returns an empty list (adapters that store edges should override).
231    async fn get_edge_sources(&self, to: &RecordId, edge_table: &str) -> Result<Vec<RecordId>> {
232        let _ = (to, edge_table);
233        Ok(Vec::new())
234    }
235
236    /// Define a unique index on `table.field` when the engine supports DDL.
237    ///
238    /// **Contract:** default returns `Error::Internal`.
239    async fn define_unique_index(&self, table: &str, field: &str) -> Result<()> {
240        let _ = (table, field);
241        Err(Error::Internal(format!(
242            "define_unique_index not supported for {}",
243            self.engine_id()
244        )))
245    }
246
247    /// Whether this adapter can apply schema TTL policies natively.
248    fn ttl_capability(&self) -> BackendTtlCapability {
249        BackendTtlCapability::Unsupported
250    }
251
252    /// Apply a schema TTL policy to `table` when supported.
253    ///
254    /// **Contract:** default is a no-op.
255    async fn apply_ttl_policy(&self, _table: &str, _policy: &SchemaTtlPolicy) -> Result<()> {
256        Ok(())
257    }
258}