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}