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}