corium_client/lib.rs
1//! A fluent, async, Datomic-style client for corium.
2//!
3//! This crate is the ergonomic front door to the peer. It offers one API
4//! surface over two backends:
5//!
6//! - [`LocalPeer`] wraps the [`corium_peer::Connection`] library, so queries
7//! run in-process against immutable database values read directly from
8//! storage — no round trip to the transactor.
9//! - [`RemotePeer`] speaks the peer-server gRPC protocol, presenting the same
10//! surface to processes that reach a hosted peer over the network.
11//!
12//! Both implement the [`Peer`] trait and hand back [`Db`] values that share
13//! the [`Db::query`], [`Db::pull`], [`Db::datoms`], and time-view
14//! ([`Db::as_of`], [`Db::since`], [`Db::history`]) methods.
15//!
16//! Datalog queries and pull specifications are built as typesafe, immutable
17//! values (see the [`query`] and [`pull`] modules) that lower to the boundary
18//! EDN the engine parses, so a malformed query is a compile error rather than
19//! a runtime parse failure.
20//!
21//! ```no_run
22//! use corium_client::{LocalPeer, Peer};
23//! use corium_client::query::{Query, data, var, attr};
24//! use corium_peer::ConnectConfig;
25//!
26//! # async fn demo() -> Result<(), corium_client::ClientError> {
27//! let peer = LocalPeer::connect(ConnectConfig::new("http://127.0.0.1:4334", "people")).await?;
28//! let db = peer.db().await?;
29//! let q = Query::find([var("name")])
30//! .where_(data(var("e"), attr("person/name"), var("name")));
31//! let result = db.query(&q, Default::default()).await?;
32//! for row in result.rows() {
33//! let name: String = row.get(0)?;
34//! println!("{name}");
35//! }
36//! # Ok(())
37//! # }
38//! ```
39
40pub mod pull;
41pub mod query;
42pub mod result;
43pub mod tx;
44pub mod value;
45
46mod local;
47mod remote;
48
49use std::collections::BTreeMap;
50use std::sync::Arc;
51
52use async_trait::async_trait;
53use corium_core::EntityId;
54use corium_query::edn::Edn;
55use thiserror::Error;
56
57pub use crate::local::LocalPeer;
58pub use crate::pull::Pull;
59pub use crate::query::Query;
60pub use crate::remote::RemotePeer;
61pub use crate::result::{QueryResult, ResultShape, Row};
62pub use crate::tx::{TxBuilder, TxData};
63pub use crate::value::{FromEdn, IntoEdn};
64
65/// A failure from the fluent client layer.
66#[derive(Debug, Error)]
67pub enum ClientError {
68 /// A peer-library failure (local backend).
69 #[error(transparent)]
70 Peer(#[from] corium_peer::PeerError),
71 /// A query-engine failure (local execution).
72 #[error(transparent)]
73 Query(#[from] corium_query::QueryError),
74 /// A gRPC status (remote backend).
75 #[error(transparent)]
76 Rpc(#[from] tonic::Status),
77 /// A transport failure establishing a remote connection.
78 #[error(transparent)]
79 Transport(#[from] tonic::transport::Error),
80 /// A wire codec failure.
81 #[error(transparent)]
82 Codec(#[from] corium_protocol::codec::CodecError),
83 /// A result or argument could not be decoded to the requested type.
84 #[error("decode error: {0}")]
85 Decode(String),
86 /// A protocol contract was violated.
87 #[error("protocol error: {0}")]
88 Protocol(String),
89}
90
91/// A database time view.
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum View {
94 /// The latest value.
95 Current,
96 /// The value as of transaction `t` (inclusive).
97 AsOf(u64),
98 /// Only assertions since transaction `t`.
99 Since(u64),
100 /// The full history view, including retractions.
101 History,
102}
103
104/// A covering index, naming a datom-scan order.
105#[derive(Clone, Copy, Debug, Eq, PartialEq)]
106pub enum Index {
107 /// Entity, attribute, value, tx.
108 Eavt,
109 /// Attribute, entity, value, tx.
110 Aevt,
111 /// Attribute, value, entity, tx (the value index).
112 Avet,
113 /// Value, attribute, entity, tx (the reverse-ref index).
114 Vaet,
115}
116
117impl Index {
118 /// The wire name (`"eavt"`, `"aevt"`, `"avet"`, `"vaet"`).
119 #[must_use]
120 pub fn as_str(self) -> &'static str {
121 match self {
122 Self::Eavt => "eavt",
123 Self::Aevt => "aevt",
124 Self::Avet => "avet",
125 Self::Vaet => "vaet",
126 }
127 }
128}
129
130/// One raw datom returned by [`Db::datoms`]. Entity, attribute, and
131/// transaction positions are raw ids; the value is boundary [`Edn`].
132#[derive(Clone, Debug, Eq, PartialEq)]
133pub struct DatomRow {
134 /// Entity id.
135 pub e: u64,
136 /// Attribute id.
137 pub a: u64,
138 /// Value.
139 pub v: Edn,
140 /// Transaction id.
141 pub tx: u64,
142 /// Assertion flag (`false` on history-view retractions).
143 pub added: bool,
144}
145
146/// Coarse database statistics.
147#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
148pub struct DbStats {
149 /// Basis transaction of the view.
150 pub basis_t: u64,
151 /// Datom count.
152 pub datoms: u64,
153 /// Distinct entity count.
154 pub entities: u64,
155 /// Attribute count.
156 pub attributes: u64,
157}
158
159/// The result of a committed transaction.
160#[derive(Clone, Debug)]
161pub struct TxReport {
162 /// Basis before the transaction.
163 pub basis_before: u64,
164 /// The transaction's `t`.
165 pub basis_t: u64,
166 /// Commit timestamp (Unix milliseconds).
167 pub tx_instant: i64,
168 /// Tempid string to allocated entity id.
169 pub tempids: BTreeMap<String, EntityId>,
170 /// A database value pinned to the state right after the transaction.
171 pub db_after: Db,
172}
173
174/// The backend that resolves [`Db`] operations, implemented in-process by the
175/// local peer and over gRPC by the remote peer-server client.
176#[async_trait]
177pub(crate) trait DbBackend: Send + Sync {
178 fn db_name(&self) -> &str;
179
180 async fn query(
181 &self,
182 view: View,
183 query: Edn,
184 args: Vec<Edn>,
185 fuel: Option<u64>,
186 ) -> Result<QueryResult, ClientError>;
187
188 async fn pull(&self, view: View, pattern: Edn, eid: Edn) -> Result<Edn, ClientError>;
189
190 async fn datoms(
191 &self,
192 view: View,
193 index: Index,
194 components: Vec<Edn>,
195 limit: usize,
196 ) -> Result<Vec<DatomRow>, ClientError>;
197
198 async fn stats(&self, view: View) -> Result<DbStats, ClientError>;
199}
200
201/// An immutable database value.
202///
203/// A `Db` names a snapshot to read from; [`Db::as_of`], [`Db::since`], and
204/// [`Db::history`] derive new views cheaply. Every read method is async
205/// because the remote backend may need a round trip; the local backend
206/// resolves in-process.
207#[derive(Clone)]
208pub struct Db {
209 backend: Arc<dyn DbBackend>,
210 view: View,
211}
212
213impl Db {
214 pub(crate) fn new(backend: Arc<dyn DbBackend>, view: View) -> Self {
215 Self { backend, view }
216 }
217
218 /// The database name.
219 #[must_use]
220 pub fn db_name(&self) -> &str {
221 self.backend.db_name()
222 }
223
224 /// This value's time view.
225 #[must_use]
226 pub fn view(&self) -> View {
227 self.view
228 }
229
230 /// The value as of transaction `t` (inclusive).
231 #[must_use]
232 pub fn as_of(&self, t: u64) -> Self {
233 Self {
234 backend: Arc::clone(&self.backend),
235 view: View::AsOf(t),
236 }
237 }
238
239 /// The value including only assertions since transaction `t`.
240 #[must_use]
241 pub fn since(&self, t: u64) -> Self {
242 Self {
243 backend: Arc::clone(&self.backend),
244 view: View::Since(t),
245 }
246 }
247
248 /// The full history view, including retractions.
249 #[must_use]
250 pub fn history(&self) -> Self {
251 Self {
252 backend: Arc::clone(&self.backend),
253 view: View::History,
254 }
255 }
256
257 /// Runs a typed [`Query`] with input arguments.
258 ///
259 /// The receiver binds the query's default `$` source; additional
260 /// non-database `:in` inputs are supplied positionally by `args`.
261 ///
262 /// # Errors
263 /// Returns [`ClientError`] for malformed queries or execution failures.
264 pub async fn query(&self, query: &Query, args: Args) -> Result<QueryResult, ClientError> {
265 self.backend
266 .query(self.view, query.to_edn(), args.arg_forms(), args.fuel)
267 .await
268 }
269
270 /// Runs a raw boundary-[`Edn`] query with positional argument forms — an
271 /// escape hatch for queries not built through [`Query`].
272 ///
273 /// # Errors
274 /// Returns [`ClientError`] for malformed queries or execution failures.
275 pub async fn query_edn(&self, query: Edn, args: Vec<Edn>) -> Result<QueryResult, ClientError> {
276 self.backend.query(self.view, query, args, None).await
277 }
278
279 /// Pulls a typed [`Pull`] specification for one entity.
280 ///
281 /// The entity is named by any [`IntoEdn`] value the boundary accepts: an
282 /// entity id (long), an ident keyword, or a lookup ref
283 /// (`tx::lookup(...)`).
284 ///
285 /// # Errors
286 /// Returns [`ClientError`] for malformed patterns or unknown idents.
287 pub async fn pull(&self, pattern: &Pull, entity: impl IntoEdn) -> Result<Edn, ClientError> {
288 self.backend
289 .pull(self.view, pattern.to_edn(), entity.into_edn())
290 .await
291 }
292
293 /// Scans datoms from a covering index, binding `components` as a leading
294 /// prefix in the index's order.
295 ///
296 /// # Errors
297 /// Returns [`ClientError`] on bad components or transport failure.
298 pub async fn datoms(
299 &self,
300 index: Index,
301 components: Vec<Edn>,
302 limit: usize,
303 ) -> Result<Vec<DatomRow>, ClientError> {
304 self.backend
305 .datoms(self.view, index, components, limit)
306 .await
307 }
308
309 /// Coarse statistics for this view.
310 ///
311 /// # Errors
312 /// Returns [`ClientError`] on transport failure.
313 pub async fn stats(&self) -> Result<DbStats, ClientError> {
314 self.backend.stats(self.view).await
315 }
316
317 /// The basis transaction of this view.
318 ///
319 /// # Errors
320 /// Returns [`ClientError`] on transport failure.
321 pub async fn basis_t(&self) -> Result<u64, ClientError> {
322 Ok(self.stats().await?.basis_t)
323 }
324}
325
326impl std::fmt::Debug for Db {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 f.debug_struct("Db")
329 .field("db_name", &self.backend.db_name())
330 .field("view", &self.view)
331 .finish()
332 }
333}
334
335/// Positional non-database arguments for a [`Query`]'s `:in` inputs, in
336/// declaration order.
337#[derive(Clone, Debug, Default)]
338pub struct Args {
339 values: Vec<Edn>,
340 fuel: Option<u64>,
341}
342
343impl Args {
344 /// An empty argument list.
345 #[must_use]
346 pub fn new() -> Self {
347 Self::default()
348 }
349
350 /// Appends a scalar argument for an `in_scalar` input.
351 #[must_use]
352 pub fn scalar(mut self, value: impl IntoEdn) -> Self {
353 self.values.push(value.into_edn());
354 self
355 }
356
357 /// Appends a tuple argument for an `in_tuple` input.
358 #[must_use]
359 pub fn tuple<V: IntoEdn>(mut self, values: impl IntoIterator<Item = V>) -> Self {
360 self.values.push(Edn::Vector(
361 values.into_iter().map(IntoEdn::into_edn).collect(),
362 ));
363 self
364 }
365
366 /// Appends a collection argument for an `in_coll` input.
367 #[must_use]
368 pub fn coll<V: IntoEdn>(mut self, values: impl IntoIterator<Item = V>) -> Self {
369 self.values.push(Edn::Vector(
370 values.into_iter().map(IntoEdn::into_edn).collect(),
371 ));
372 self
373 }
374
375 /// Appends a relation argument (vector of tuples) for an `in_rel` input.
376 #[must_use]
377 pub fn relation<V: IntoEdn>(mut self, tuples: impl IntoIterator<Item = Vec<V>>) -> Self {
378 let rows = tuples
379 .into_iter()
380 .map(|tuple| Edn::Vector(tuple.into_iter().map(IntoEdn::into_edn).collect()))
381 .collect();
382 self.values.push(Edn::Vector(rows));
383 self
384 }
385
386 /// Appends a raw boundary form as the next argument.
387 #[must_use]
388 pub fn arg(mut self, value: Edn) -> Self {
389 self.values.push(value);
390 self
391 }
392
393 /// Bounds the query's execution fuel (datoms touched).
394 #[must_use]
395 pub fn fuel(mut self, fuel: u64) -> Self {
396 self.fuel = Some(fuel);
397 self
398 }
399
400 fn arg_forms(&self) -> Vec<Edn> {
401 self.values.clone()
402 }
403}
404
405/// A live connection to a database, backed either by the local peer library
406/// or a remote peer server.
407///
408/// Both [`LocalPeer`] and [`RemotePeer`] implement this trait, so code can be
409/// written once against `impl Peer` and run against either backend.
410#[async_trait]
411pub trait Peer: Send + Sync {
412 /// The connected database name.
413 fn db_name(&self) -> &str;
414
415 /// The current database value.
416 ///
417 /// # Errors
418 /// Returns [`ClientError`] on transport failure.
419 async fn db(&self) -> Result<Db, ClientError>;
420
421 /// Submits a transaction and waits until it is applied, returning a
422 /// report whose `db_after` observes it.
423 ///
424 /// # Errors
425 /// Returns [`ClientError`] for rejected transactions or transport
426 /// failure.
427 async fn transact(&self, tx: TxData) -> Result<TxReport, ClientError>;
428
429 /// Waits until the local view reaches the transactor's current basis and
430 /// returns the resulting database value.
431 ///
432 /// # Errors
433 /// Returns [`ClientError`] on transport failure.
434 async fn sync(&self) -> Result<Db, ClientError>;
435}