Skip to main content

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    /// The value as of the last transaction committed at or before this
103    /// instant (Unix milliseconds).
104    AsOfInstant(i64),
105    /// Only assertions since the last transaction committed at or before this
106    /// instant (Unix milliseconds).
107    SinceInstant(i64),
108}
109
110/// A covering index, naming a datom-scan order.
111#[derive(Clone, Copy, Debug, Eq, PartialEq)]
112pub enum Index {
113    /// Entity, attribute, value, tx.
114    Eavt,
115    /// Attribute, entity, value, tx.
116    Aevt,
117    /// Attribute, value, entity, tx (the value index).
118    Avet,
119    /// Value, attribute, entity, tx (the reverse-ref index).
120    Vaet,
121}
122
123impl Index {
124    /// The wire name (`"eavt"`, `"aevt"`, `"avet"`, `"vaet"`).
125    #[must_use]
126    pub fn as_str(self) -> &'static str {
127        match self {
128            Self::Eavt => "eavt",
129            Self::Aevt => "aevt",
130            Self::Avet => "avet",
131            Self::Vaet => "vaet",
132        }
133    }
134}
135
136/// One raw datom returned by [`Db::datoms`]. Entity, attribute, and
137/// transaction positions are raw ids; the value is boundary [`Edn`].
138#[derive(Clone, Debug, Eq, PartialEq)]
139pub struct DatomRow {
140    /// Entity id.
141    pub e: u64,
142    /// Attribute id.
143    pub a: u64,
144    /// Value.
145    pub v: Edn,
146    /// Transaction id.
147    pub tx: u64,
148    /// Assertion flag (`false` on history-view retractions).
149    pub added: bool,
150}
151
152/// Coarse database statistics.
153#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
154pub struct DbStats {
155    /// Basis transaction of the view.
156    pub basis_t: u64,
157    /// Datom count.
158    pub datoms: u64,
159    /// Distinct entity count.
160    pub entities: u64,
161    /// Attribute count.
162    pub attributes: u64,
163}
164
165/// The result of a committed transaction.
166#[derive(Clone, Debug)]
167pub struct TxReport {
168    /// Basis before the transaction.
169    pub basis_before: u64,
170    /// The transaction's `t`.
171    pub basis_t: u64,
172    /// Commit timestamp (Unix milliseconds).
173    pub tx_instant: i64,
174    /// Tempid string to allocated entity id.
175    pub tempids: BTreeMap<String, EntityId>,
176    /// A database value pinned to the state right after the transaction.
177    pub db_after: Db,
178}
179
180/// The backend that resolves [`Db`] operations, implemented in-process by the
181/// local peer and over gRPC by the remote peer-server client.
182#[async_trait]
183pub(crate) trait DbBackend: Send + Sync {
184    fn db_name(&self) -> &str;
185
186    async fn query(
187        &self,
188        view: View,
189        query: Edn,
190        args: Vec<Edn>,
191        fuel: Option<u64>,
192    ) -> Result<QueryResult, ClientError>;
193
194    async fn pull(&self, view: View, pattern: Edn, eid: Edn) -> Result<Edn, ClientError>;
195
196    async fn datoms(
197        &self,
198        view: View,
199        index: Index,
200        components: Vec<Edn>,
201        limit: usize,
202    ) -> Result<Vec<DatomRow>, ClientError>;
203
204    async fn stats(&self, view: View) -> Result<DbStats, ClientError>;
205}
206
207/// An immutable database value.
208///
209/// A `Db` names a snapshot to read from; [`Db::as_of`], [`Db::since`],
210/// [`Db::history`], and the wall-clock [`Db::as_of_instant`] /
211/// [`Db::since_instant`] derive new views cheaply. Every read method is async
212/// because the remote backend may need a round trip; the local backend
213/// resolves in-process.
214#[derive(Clone)]
215pub struct Db {
216    backend: Arc<dyn DbBackend>,
217    view: View,
218}
219
220impl Db {
221    pub(crate) fn new(backend: Arc<dyn DbBackend>, view: View) -> Self {
222        Self { backend, view }
223    }
224
225    /// The database name.
226    #[must_use]
227    pub fn db_name(&self) -> &str {
228        self.backend.db_name()
229    }
230
231    /// This value's time view.
232    #[must_use]
233    pub fn view(&self) -> View {
234        self.view
235    }
236
237    /// The value as of transaction `t` (inclusive).
238    #[must_use]
239    pub fn as_of(&self, t: u64) -> Self {
240        Self {
241            backend: Arc::clone(&self.backend),
242            view: View::AsOf(t),
243        }
244    }
245
246    /// The value including only assertions since transaction `t`.
247    #[must_use]
248    pub fn since(&self, t: u64) -> Self {
249        Self {
250            backend: Arc::clone(&self.backend),
251            view: View::Since(t),
252        }
253    }
254
255    /// The full history view, including retractions.
256    #[must_use]
257    pub fn history(&self) -> Self {
258        Self {
259            backend: Arc::clone(&self.backend),
260            view: View::History,
261        }
262    }
263
264    /// The value as of wall-clock `instant` (Unix milliseconds): the last
265    /// transaction committed at or before it.
266    #[must_use]
267    pub fn as_of_instant(&self, instant: i64) -> Self {
268        Self {
269            backend: Arc::clone(&self.backend),
270            view: View::AsOfInstant(instant),
271        }
272    }
273
274    /// The value including only assertions since wall-clock `instant`
275    /// (Unix milliseconds).
276    #[must_use]
277    pub fn since_instant(&self, instant: i64) -> Self {
278        Self {
279            backend: Arc::clone(&self.backend),
280            view: View::SinceInstant(instant),
281        }
282    }
283
284    /// Runs a typed [`Query`] with input arguments.
285    ///
286    /// The receiver binds the query's default `$` source; additional
287    /// non-database `:in` inputs are supplied positionally by `args`.
288    ///
289    /// # Errors
290    /// Returns [`ClientError`] for malformed queries or execution failures.
291    pub async fn query(&self, query: &Query, args: Args) -> Result<QueryResult, ClientError> {
292        self.backend
293            .query(self.view, query.to_edn(), args.arg_forms(), args.fuel)
294            .await
295    }
296
297    /// Runs a raw boundary-[`Edn`] query with positional argument forms — an
298    /// escape hatch for queries not built through [`Query`].
299    ///
300    /// # Errors
301    /// Returns [`ClientError`] for malformed queries or execution failures.
302    pub async fn query_edn(&self, query: Edn, args: Vec<Edn>) -> Result<QueryResult, ClientError> {
303        self.backend.query(self.view, query, args, None).await
304    }
305
306    /// Pulls a typed [`Pull`] specification for one entity.
307    ///
308    /// The entity is named by any [`IntoEdn`] value the boundary accepts: an
309    /// entity id (long), an ident keyword, or a lookup ref
310    /// (`tx::lookup(...)`).
311    ///
312    /// # Errors
313    /// Returns [`ClientError`] for malformed patterns or unknown idents.
314    pub async fn pull(&self, pattern: &Pull, entity: impl IntoEdn) -> Result<Edn, ClientError> {
315        self.backend
316            .pull(self.view, pattern.to_edn(), entity.into_edn())
317            .await
318    }
319
320    /// Scans datoms from a covering index, binding `components` as a leading
321    /// prefix in the index's order.
322    ///
323    /// # Errors
324    /// Returns [`ClientError`] on bad components or transport failure.
325    pub async fn datoms(
326        &self,
327        index: Index,
328        components: Vec<Edn>,
329        limit: usize,
330    ) -> Result<Vec<DatomRow>, ClientError> {
331        self.backend
332            .datoms(self.view, index, components, limit)
333            .await
334    }
335
336    /// Coarse statistics for this view.
337    ///
338    /// # Errors
339    /// Returns [`ClientError`] on transport failure.
340    pub async fn stats(&self) -> Result<DbStats, ClientError> {
341        self.backend.stats(self.view).await
342    }
343
344    /// The basis transaction of this view.
345    ///
346    /// # Errors
347    /// Returns [`ClientError`] on transport failure.
348    pub async fn basis_t(&self) -> Result<u64, ClientError> {
349        Ok(self.stats().await?.basis_t)
350    }
351}
352
353impl std::fmt::Debug for Db {
354    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355        f.debug_struct("Db")
356            .field("db_name", &self.backend.db_name())
357            .field("view", &self.view)
358            .finish()
359    }
360}
361
362/// Positional non-database arguments for a [`Query`]'s `:in` inputs, in
363/// declaration order.
364#[derive(Clone, Debug, Default)]
365pub struct Args {
366    values: Vec<Edn>,
367    fuel: Option<u64>,
368}
369
370impl Args {
371    /// An empty argument list.
372    #[must_use]
373    pub fn new() -> Self {
374        Self::default()
375    }
376
377    /// Appends a scalar argument for an `in_scalar` input.
378    #[must_use]
379    pub fn scalar(mut self, value: impl IntoEdn) -> Self {
380        self.values.push(value.into_edn());
381        self
382    }
383
384    /// Appends a tuple argument for an `in_tuple` input.
385    #[must_use]
386    pub fn tuple<V: IntoEdn>(mut self, values: impl IntoIterator<Item = V>) -> Self {
387        self.values.push(Edn::Vector(
388            values.into_iter().map(IntoEdn::into_edn).collect(),
389        ));
390        self
391    }
392
393    /// Appends a collection argument for an `in_coll` input.
394    #[must_use]
395    pub fn coll<V: IntoEdn>(mut self, values: impl IntoIterator<Item = V>) -> Self {
396        self.values.push(Edn::Vector(
397            values.into_iter().map(IntoEdn::into_edn).collect(),
398        ));
399        self
400    }
401
402    /// Appends a relation argument (vector of tuples) for an `in_rel` input.
403    #[must_use]
404    pub fn relation<V: IntoEdn>(mut self, tuples: impl IntoIterator<Item = Vec<V>>) -> Self {
405        let rows = tuples
406            .into_iter()
407            .map(|tuple| Edn::Vector(tuple.into_iter().map(IntoEdn::into_edn).collect()))
408            .collect();
409        self.values.push(Edn::Vector(rows));
410        self
411    }
412
413    /// Appends a raw boundary form as the next argument.
414    #[must_use]
415    pub fn arg(mut self, value: Edn) -> Self {
416        self.values.push(value);
417        self
418    }
419
420    /// Bounds the query's execution fuel (datoms touched).
421    #[must_use]
422    pub fn fuel(mut self, fuel: u64) -> Self {
423        self.fuel = Some(fuel);
424        self
425    }
426
427    fn arg_forms(&self) -> Vec<Edn> {
428        self.values.clone()
429    }
430}
431
432/// A live connection to a database, backed either by the local peer library
433/// or a remote peer server.
434///
435/// Both [`LocalPeer`] and [`RemotePeer`] implement this trait, so code can be
436/// written once against `impl Peer` and run against either backend.
437#[async_trait]
438pub trait Peer: Send + Sync {
439    /// The connected database name.
440    fn db_name(&self) -> &str;
441
442    /// The current database value.
443    ///
444    /// # Errors
445    /// Returns [`ClientError`] on transport failure.
446    async fn db(&self) -> Result<Db, ClientError>;
447
448    /// Submits a transaction and waits until it is applied, returning a
449    /// report whose `db_after` observes it.
450    ///
451    /// # Errors
452    /// Returns [`ClientError`] for rejected transactions or transport
453    /// failure.
454    async fn transact(&self, tx: TxData) -> Result<TxReport, ClientError>;
455
456    /// Waits until the local view reaches the transactor's current basis and
457    /// returns the resulting database value.
458    ///
459    /// # Errors
460    /// Returns [`ClientError`] on transport failure.
461    async fn sync(&self) -> Result<Db, ClientError>;
462}