1use std::sync::Arc;
6
7use async_trait::async_trait;
8use corium_core::{EntityId, IndexOrder};
9use corium_db::{Db as DbValue, key_prefix};
10use corium_peer::{ConnectConfig, Connection};
11use corium_query::edn::Edn;
12use corium_query::{ExecOptions, QInput, ast, boundary, exec};
13
14use crate::result::{QueryResult, ResultShape};
15use crate::{ClientError, DatomRow, Db, DbBackend, DbStats, Index, Peer, TxData, TxReport, View};
16
17pub struct LocalPeer {
19 connection: Arc<Connection>,
20}
21
22impl LocalPeer {
23 pub async fn connect(config: ConnectConfig) -> Result<Self, ClientError> {
28 Ok(Self {
29 connection: Arc::new(Connection::connect(config).await?),
30 })
31 }
32
33 #[must_use]
35 pub fn from_connection(connection: Arc<Connection>) -> Self {
36 Self { connection }
37 }
38
39 #[must_use]
42 pub fn connection(&self) -> &Arc<Connection> {
43 &self.connection
44 }
45
46 fn snapshot_db(&self, snapshot: DbValue) -> Db {
47 Db::new(
48 Arc::new(LocalDbBackend {
49 snapshot,
50 db_name: self.connection.db_name().to_owned(),
51 }),
52 View::Current,
53 )
54 }
55}
56
57#[async_trait]
58impl Peer for LocalPeer {
59 fn db_name(&self) -> &str {
60 self.connection.db_name()
61 }
62
63 async fn db(&self) -> Result<Db, ClientError> {
64 Ok(self.snapshot_db(self.connection.db()))
65 }
66
67 async fn transact(&self, tx: TxData) -> Result<TxReport, ClientError> {
68 let result = self.connection.transact(tx.into_forms()).await?;
69 Ok(TxReport {
70 basis_before: result.basis_before,
71 basis_t: result.basis_t,
72 tx_instant: result.tx_instant,
73 tempids: result.tempids,
74 db_after: self.snapshot_db(result.db_after),
75 })
76 }
77
78 async fn sync(&self) -> Result<Db, ClientError> {
79 Ok(self.snapshot_db(self.connection.sync().await?))
80 }
81}
82
83struct LocalDbBackend {
85 snapshot: DbValue,
86 db_name: String,
87}
88
89impl LocalDbBackend {
90 fn resolve(&self, view: View) -> DbValue {
91 match view {
92 View::Current => self.snapshot.clone(),
93 View::AsOf(t) => self.snapshot.as_of(t),
94 View::Since(t) => self.snapshot.since(t),
95 View::History => self.snapshot.history(),
96 }
97 }
98}
99
100#[async_trait]
101impl DbBackend for LocalDbBackend {
102 fn db_name(&self) -> &str {
103 &self.db_name
104 }
105
106 async fn query(
107 &self,
108 view: View,
109 query: Edn,
110 args: Vec<Edn>,
111 fuel: Option<u64>,
112 ) -> Result<QueryResult, ClientError> {
113 let db = self.resolve(view);
114 let parsed = ast::parse_query(&query)?;
115 let mut inputs: Vec<QInput<'_>> = Vec::with_capacity(parsed.inputs.len());
118 let mut bound_db = false;
119 let mut next_arg = args.iter();
120 for spec in &parsed.inputs {
121 match spec {
122 ast::InSpec::Db(_) if !bound_db => {
123 inputs.push(QInput::Db(&db));
124 bound_db = true;
125 }
126 ast::InSpec::Db(_) => {
127 return Err(ClientError::Protocol(
128 "the local fluent API binds a single database source".into(),
129 ));
130 }
131 _ => {
132 let arg = next_arg.next().ok_or_else(|| {
133 ClientError::Protocol("query needs more arguments".into())
134 })?;
135 inputs.push(QInput::Edn(arg.clone()));
136 }
137 }
138 }
139 if parsed.inputs.is_empty() {
140 inputs.push(QInput::Db(&db));
142 }
143 let (value, _report) = corium_query::run(
144 &parsed,
145 &inputs,
146 ExecOptions {
147 fuel,
148 ..ExecOptions::default()
149 },
150 )?;
151 Ok(QueryResult::new(shape_of(&parsed.find), value))
152 }
153
154 async fn pull(&self, view: View, pattern: Edn, eid: Edn) -> Result<Edn, ClientError> {
155 let db = self.resolve(view);
156 let entity = resolve_eid(&db, &eid)?;
157 Ok(corium_query::pull(&db, &pattern, entity)?)
158 }
159
160 async fn datoms(
161 &self,
162 view: View,
163 index: Index,
164 components: Vec<Edn>,
165 limit: usize,
166 ) -> Result<Vec<DatomRow>, ClientError> {
167 let db = self.resolve(view);
168 let order = index_order(index);
169 let (e, a, v) = resolve_components(&db, order, &components)?;
170 let prefix = key_prefix(order, e, a, v.as_ref());
171 let limit = if limit == 0 { usize::MAX } else { limit };
172 Ok(db
173 .datoms_prefix(order, &prefix)
174 .take(limit)
175 .map(|datom| DatomRow {
176 e: datom.e.raw(),
177 a: datom.a.raw(),
178 v: boundary::value_to_edn(&db, &datom.v),
179 tx: datom.tx.raw(),
180 added: datom.added,
181 })
182 .collect())
183 }
184
185 async fn stats(&self, view: View) -> Result<DbStats, ClientError> {
186 let db = self.resolve(view);
187 let stats = db.stats();
188 Ok(DbStats {
189 basis_t: db.basis_t(),
190 datoms: stats.datoms as u64,
191 entities: stats.entities as u64,
192 attributes: stats.attributes as u64,
193 })
194 }
195}
196
197fn shape_of(find: &ast::FindSpec) -> ResultShape {
198 match find {
199 ast::FindSpec::Rel(_) => ResultShape::Relation,
200 ast::FindSpec::Coll(_) => ResultShape::Collection,
201 ast::FindSpec::Tuple(_) => ResultShape::Tuple,
202 ast::FindSpec::Scalar(_) => ResultShape::Scalar,
203 }
204}
205
206fn index_order(index: Index) -> IndexOrder {
207 match index {
208 Index::Eavt => IndexOrder::Eavt,
209 Index::Aevt => IndexOrder::Aevt,
210 Index::Avet => IndexOrder::Avet,
211 Index::Vaet => IndexOrder::Vaet,
212 }
213}
214
215fn resolve_eid(db: &DbValue, form: &Edn) -> Result<EntityId, ClientError> {
218 match form {
219 Edn::Long(n) => u64::try_from(*n)
220 .map(EntityId::from_raw)
221 .map_err(|_| ClientError::Decode("negative entity id".into())),
222 Edn::Tagged(tag, value) if tag == "eid" => match value.as_ref() {
223 Edn::Long(n) => u64::try_from(*n)
224 .map(EntityId::from_raw)
225 .map_err(|_| ClientError::Decode("negative entity id".into())),
226 _ => Err(ClientError::Decode("#eid requires a long".into())),
227 },
228 Edn::Keyword(keyword) => db
229 .idents()
230 .entid(keyword)
231 .ok_or_else(|| ClientError::Decode(format!("unknown ident {keyword}"))),
232 Edn::Vector(items) => {
233 let [attr_form, value_form] = items.as_slice() else {
234 return Err(ClientError::Decode(
235 "lookup ref requires [attr value]".into(),
236 ));
237 };
238 let attr = attr_form
239 .as_keyword()
240 .and_then(|keyword| db.idents().entid(keyword))
241 .ok_or_else(|| ClientError::Decode("unknown lookup attribute".into()))?;
242 let value = boundary::edn_to_value(Some(db), value_form)
243 .ok_or_else(|| ClientError::Decode("bad lookup value".into()))?;
244 let value = db.schema().get(attr).map_or(value.clone(), |meta| {
245 exec::coerce_for_type(value, meta.value_type)
246 });
247 db.lookup(attr, &value)
248 .ok_or_else(|| ClientError::Decode(format!("lookup ref {form} did not resolve")))
249 }
250 other => Err(ClientError::Decode(format!("bad entity position {other}"))),
251 }
252}
253
254enum Slot {
255 E,
256 A,
257 V,
258}
259
260type PrefixParts = (
262 Option<EntityId>,
263 Option<EntityId>,
264 Option<corium_core::Value>,
265);
266
267fn resolve_components(
270 db: &DbValue,
271 order: IndexOrder,
272 components: &[Edn],
273) -> Result<PrefixParts, ClientError> {
274 if components.len() > 3 {
275 return Err(ClientError::Decode("at most three components".into()));
276 }
277 let positions = match order {
278 IndexOrder::Eavt => [Slot::E, Slot::A, Slot::V],
279 IndexOrder::Aevt => [Slot::A, Slot::E, Slot::V],
280 IndexOrder::Avet => [Slot::A, Slot::V, Slot::E],
281 IndexOrder::Vaet => [Slot::V, Slot::A, Slot::E],
282 };
283 let mut e = None;
284 let mut a = None;
285 let mut v = None;
286 for (slot, form) in positions.iter().zip(components) {
287 match slot {
288 Slot::E => e = Some(resolve_eid(db, form)?),
289 Slot::A => {
290 a = Some(match form {
291 Edn::Keyword(keyword) => db.idents().entid(keyword).ok_or_else(|| {
292 ClientError::Decode(format!("unknown attribute {keyword}"))
293 })?,
294 other => resolve_eid(db, other)?,
295 });
296 }
297 Slot::V => {
298 let value = boundary::edn_to_value(Some(db), form)
299 .ok_or_else(|| ClientError::Decode(format!("bad value {form}")))?;
300 let value = a
301 .and_then(|a| db.schema().get(a))
302 .map_or(value.clone(), |meta| {
303 exec::coerce_for_type(value, meta.value_type)
304 });
305 v = Some(value);
306 }
307 }
308 }
309 Ok((e, a, v))
310}