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}
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}