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 facade 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    async fn ensure_schemaless_table(&self, table: &str) -> Result<()> {
112        let _ = table;
113        Ok(())
114    }
115
116    /// Fetch one record by primary key.
117    ///
118    /// **Contract:** returns `Ok(None)` when the row does not exist.
119    async fn get_record(&self, table: &str, id: &str) -> Result<Option<serde_json::Value>>;
120
121    /// Insert a new record; content must include any required fields.
122    ///
123    /// **Contract:** returns the persisted row (including server-assigned fields when applicable).
124    async fn create_record(
125        &self,
126        table: &str,
127        content: serde_json::Value,
128    ) -> Result<serde_json::Value>;
129
130    /// Replace an existing record by id.
131    ///
132    /// **Contract:** returns the updated row; errors when the id is missing unless the adapter
133    /// supports upsert semantics via [`Self::upsert_record`].
134    async fn update_record(
135        &self,
136        table: &str,
137        id: &str,
138        content: serde_json::Value,
139    ) -> Result<serde_json::Value>;
140
141    /// Patch an existing record with a partial JSON object.
142    ///
143    /// **Contract:** default returns `Error::Internal` — override when [`BackendCapabilities::supports_merge`]
144    /// is `true`.
145    async fn merge_record(
146        &self,
147        table: &str,
148        id: &str,
149        patch: serde_json::Value,
150    ) -> Result<serde_json::Value> {
151        let _ = (table, id, patch);
152        Err(Error::Internal(
153            "merge_record is not supported by this database backend".into(),
154        ))
155    }
156
157    /// Create or replace a record by explicit id.
158    async fn upsert_record(
159        &self,
160        table: &str,
161        id: &str,
162        content: serde_json::Value,
163    ) -> Result<serde_json::Value>;
164
165    /// Delete one record by primary key.
166    ///
167    /// **Contract:** succeeds when the row is already absent (idempotent delete).
168    async fn delete_record(&self, table: &str, id: &str) -> Result<()>;
169
170    /// Create a directed graph edge from `from` to `to` through `edge_table`.
171    async fn relate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()>;
172
173    /// Remove a directed graph edge.
174    ///
175    /// **Contract:** idempotent when the edge does not exist.
176    async fn unrelate_edge(&self, from: &RecordId, edge_table: &str, to: &RecordId) -> Result<()>;
177
178    /// List target record ids reachable via outgoing edges in `edge_table`.
179    async fn get_edge_targets(&self, from: &RecordId, edge_table: &str) -> Result<Vec<RecordId>>;
180
181    /// Define a unique index on `table.field` when the engine supports DDL.
182    ///
183    /// **Contract:** default returns `Error::Internal`.
184    async fn define_unique_index(&self, table: &str, field: &str) -> Result<()> {
185        let _ = (table, field);
186        Err(Error::Internal(format!(
187            "define_unique_index not supported for {}",
188            self.engine_id()
189        )))
190    }
191
192    /// Whether this adapter can apply schema TTL policies natively.
193    fn ttl_capability(&self) -> BackendTtlCapability {
194        BackendTtlCapability::Unsupported
195    }
196
197    /// Apply a schema TTL policy to `table` when supported.
198    ///
199    /// **Contract:** default is a no-op.
200    async fn apply_ttl_policy(&self, _table: &str, _policy: &SchemaTtlPolicy) -> Result<()> {
201        Ok(())
202    }
203}