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}