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.query_edn_with_fuel(query, args, None).await
304    }
305
306    /// Runs a raw boundary-[`Edn`] query with positional argument forms and
307    /// an optional execution-fuel limit.
308    ///
309    /// # Errors
310    /// Returns [`ClientError`] for malformed queries or execution failures.
311    pub async fn query_edn_with_fuel(
312        &self,
313        query: Edn,
314        args: Vec<Edn>,
315        fuel: Option<u64>,
316    ) -> Result<QueryResult, ClientError> {
317        self.backend.query(self.view, query, args, fuel).await
318    }
319
320    /// Pulls a typed [`Pull`] specification for one entity.
321    ///
322    /// The entity is named by any [`IntoEdn`] value the boundary accepts: an
323    /// entity id (long), an ident keyword, or a lookup ref
324    /// (`tx::lookup(...)`).
325    ///
326    /// # Errors
327    /// Returns [`ClientError`] for malformed patterns or unknown idents.
328    pub async fn pull(&self, pattern: &Pull, entity: impl IntoEdn) -> Result<Edn, ClientError> {
329        self.pull_edn(pattern.to_edn(), entity.into_edn()).await
330    }
331
332    /// Pulls a raw boundary-[`Edn`] pattern for one entity.
333    ///
334    /// # Errors
335    /// Returns [`ClientError`] for malformed patterns or unknown idents.
336    pub async fn pull_edn(&self, pattern: Edn, entity: Edn) -> Result<Edn, ClientError> {
337        self.backend.pull(self.view, pattern, entity).await
338    }
339
340    /// Scans datoms from a covering index, binding `components` as a leading
341    /// prefix in the index's order.
342    ///
343    /// # Errors
344    /// Returns [`ClientError`] on bad components or transport failure.
345    pub async fn datoms(
346        &self,
347        index: Index,
348        components: Vec<Edn>,
349        limit: usize,
350    ) -> Result<Vec<DatomRow>, ClientError> {
351        self.backend
352            .datoms(self.view, index, components, limit)
353            .await
354    }
355
356    /// Coarse statistics for this view.
357    ///
358    /// # Errors
359    /// Returns [`ClientError`] on transport failure.
360    pub async fn stats(&self) -> Result<DbStats, ClientError> {
361        self.backend.stats(self.view).await
362    }
363
364    /// The basis transaction of this view.
365    ///
366    /// # Errors
367    /// Returns [`ClientError`] on transport failure.
368    pub async fn basis_t(&self) -> Result<u64, ClientError> {
369        Ok(self.stats().await?.basis_t)
370    }
371}
372
373impl std::fmt::Debug for Db {
374    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
375        f.debug_struct("Db")
376            .field("db_name", &self.backend.db_name())
377            .field("view", &self.view)
378            .finish()
379    }
380}
381
382/// Positional non-database arguments for a [`Query`]'s `:in` inputs, in
383/// declaration order.
384#[derive(Clone, Debug, Default)]
385pub struct Args {
386    values: Vec<Edn>,
387    fuel: Option<u64>,
388}
389
390impl Args {
391    /// An empty argument list.
392    #[must_use]
393    pub fn new() -> Self {
394        Self::default()
395    }
396
397    /// Appends a scalar argument for an `in_scalar` input.
398    #[must_use]
399    pub fn scalar(mut self, value: impl IntoEdn) -> Self {
400        self.values.push(value.into_edn());
401        self
402    }
403
404    /// Appends a tuple argument for an `in_tuple` input.
405    #[must_use]
406    pub fn tuple<V: IntoEdn>(mut self, values: impl IntoIterator<Item = V>) -> Self {
407        self.values.push(Edn::Vector(
408            values.into_iter().map(IntoEdn::into_edn).collect(),
409        ));
410        self
411    }
412
413    /// Appends a collection argument for an `in_coll` input.
414    #[must_use]
415    pub fn coll<V: IntoEdn>(mut self, values: impl IntoIterator<Item = V>) -> Self {
416        self.values.push(Edn::Vector(
417            values.into_iter().map(IntoEdn::into_edn).collect(),
418        ));
419        self
420    }
421
422    /// Appends a relation argument (vector of tuples) for an `in_rel` input.
423    #[must_use]
424    pub fn relation<V: IntoEdn>(mut self, tuples: impl IntoIterator<Item = Vec<V>>) -> Self {
425        let rows = tuples
426            .into_iter()
427            .map(|tuple| Edn::Vector(tuple.into_iter().map(IntoEdn::into_edn).collect()))
428            .collect();
429        self.values.push(Edn::Vector(rows));
430        self
431    }
432
433    /// Appends a raw boundary form as the next argument.
434    #[must_use]
435    pub fn arg(mut self, value: Edn) -> Self {
436        self.values.push(value);
437        self
438    }
439
440    /// Bounds the query's execution fuel (datoms touched).
441    #[must_use]
442    pub fn fuel(mut self, fuel: u64) -> Self {
443        self.fuel = Some(fuel);
444        self
445    }
446
447    fn arg_forms(&self) -> Vec<Edn> {
448        self.values.clone()
449    }
450}
451
452/// A live connection to a database, backed either by the local peer library
453/// or a remote peer server.
454///
455/// Both [`LocalPeer`] and [`RemotePeer`] implement this trait, so code can be
456/// written once against `impl Peer` and run against either backend.
457#[async_trait]
458pub trait Peer: Send + Sync {
459    /// The connected database name.
460    fn db_name(&self) -> &str;
461
462    /// The current database value.
463    ///
464    /// # Errors
465    /// Returns [`ClientError`] on transport failure.
466    async fn db(&self) -> Result<Db, ClientError>;
467
468    /// Submits a transaction and waits until it is applied, returning a
469    /// report whose `db_after` observes it.
470    ///
471    /// # Errors
472    /// Returns [`ClientError`] for rejected transactions or transport
473    /// failure.
474    async fn transact(&self, tx: TxData) -> Result<TxReport, ClientError>;
475
476    /// Waits until the local view reaches the transactor's current basis and
477    /// returns the resulting database value.
478    ///
479    /// # Errors
480    /// Returns [`ClientError`] on transport failure.
481    async fn sync(&self) -> Result<Db, ClientError>;
482}