1use std::borrow::Cow;
4
5use crate::error::{Db, Error, Result};
6use crate::pg_unstable::Connector;
7use crate::pool::{Conn, Pool, PoolOptions, Pooled};
8use crate::sql::Q;
9use crate::{Binding, Namespace, Store, Version};
10use datumesh_core::lexical::{check_dns_name, check_function_name, check_hex, check_instant};
11use datumesh_core::{
12 json, keys_from_response, sha256_hex, verify, Cell, Descriptor, ErrorCode, Keys, Receipt, Resolver, Scalar,
13};
14
15pub struct PgStore {
23 pool: Pool,
24}
25
26impl std::fmt::Debug for PgStore {
27 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28 f.debug_struct("PgStore").finish_non_exhaustive()
29 }
30}
31
32fn is_hash(s: &str) -> bool {
33 check_hex(s, 64, "$").is_ok()
34}
35
36fn is_instant(s: &str) -> bool {
37 check_instant(s, "$").is_ok()
38}
39
40impl PgStore {
41 pub fn connect(url: &str) -> Result<PgStore> {
44 PgStore::connect_with(url, PoolOptions::new())
45 }
46
47 pub fn connect_with(url: &str, options: PoolOptions) -> Result<PgStore> {
61 let config: postgres::Config = url.parse().db()?;
62 let connector = Connector::new(config, &options)?;
63 PgStore::from_pool(Pool::new(Box::new(move || connector.connect()), options))
64 }
65
66 pub(crate) fn from_pool(pool: Pool) -> Result<PgStore> {
68 let store = PgStore { pool };
69 drop(store.pool.get()?);
70 Ok(store)
71 }
72
73 pub(crate) fn conn(&self) -> Result<Pooled<'_>> {
75 self.pool.get()
76 }
77
78 pub fn check_statements(&self) -> Result<usize> {
82 let mut c = self.pool.get()?;
83 for q in Q::ALL {
84 c.client.prepare_typed(q.sql(), q.types()).db()?;
85 }
86 Ok(Q::ALL.len())
87 }
88
89 fn resolve<T>(&self, op: &'static str, f: impl FnOnce(&mut Conn) -> Result<Option<T>>) -> Option<T> {
91 self.run(op, f).ok().flatten()
92 }
93
94 fn run<T>(&self, op: &'static str, f: impl FnOnce(&mut Conn) -> Result<T>) -> Result<T> {
96 let result = self.pool.get().and_then(|mut c| f(&mut c));
97 if let Err(e @ (Error::Database(_) | Error::PoolTimeout)) = &result {
98 tracing::error!(target: "datumesh_store", op, error = %e, "database failure");
99 }
100 result
101 }
102
103 fn version_rows(rows: &[postgres::Row], cell_id: &str) -> Result<Vec<Version>> {
105 rows.iter().map(|r| version_row(r, cell_id)).collect()
106 }
107
108 fn asof_with_receipt(
111 &self,
112 op: &'static str,
113 q: Q,
114 cell_id: &str,
115 asof: &str,
116 ) -> Result<Option<(Version, Vec<u8>)>> {
117 check_hex(cell_id, 64, "cell_id")?;
118 check_instant(asof, "asof")?;
119 self.run(op, |c| {
120 let st = c.stmt(q)?;
121 let Some(row) = c.client.query_opt(&st, &[&cell_id, &asof]).db()? else { return Ok(None) };
122 Ok(Some((version_row(&row, cell_id)?, row.try_get(3).db()?)))
123 })
124 }
125
126 fn asof_version(&self, op: &'static str, q: Q, cell_id: &str, asof: &str) -> Result<Option<Version>> {
128 check_hex(cell_id, 64, "cell_id")?;
129 check_instant(asof, "asof")?;
130 self.run(op, |c| {
131 let st = c.stmt(q)?;
132 let rows = c.client.query(&st, &[&cell_id, &asof]).db()?;
133 Ok(PgStore::version_rows(&rows, cell_id)?.pop())
134 })
135 }
136}
137
138fn version_row(r: &postgres::Row, cell_id: &str) -> Result<Version> {
139 Ok(Version {
140 cell_id: cell_id.to_string(),
141 tx: r.try_get(0).db()?,
142 receipt_hash: r.try_get(1).db()?,
143 asof: r.try_get(2).db()?,
144 })
145}
146
147fn get_bytes(c: &mut Conn, q: Q, key: &str) -> Result<Option<Vec<u8>>> {
148 let st = c.stmt(q)?;
149 let row = c.client.query_opt(&st, &[&key]).db()?;
150 Ok(match row {
151 Some(r) => Some(r.try_get(0).db()?),
152 None => None,
153 })
154}
155
156fn get_strings(c: &mut Conn, q: Q, key: &str) -> Result<Vec<String>> {
157 let st = c.stmt(q)?;
158 let rows = c.client.query(&st, &[&key]).db()?;
159 rows.iter().map(|r| r.try_get(0).db()).collect()
160}
161
162impl Resolver for PgStore {
163 fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>> {
164 if !is_hash(fn_hash) {
165 return None;
166 }
167 self.resolve("descriptor", |c| get_bytes(c, Q::DescriptorGet, fn_hash)).map(Cow::Owned)
168 }
169
170 fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>> {
171 if !is_hash(hash) {
172 return None;
173 }
174 self.resolve("receipt", |c| get_bytes(c, Q::ReceiptGet, hash)).map(Cow::Owned)
175 }
176
177 fn evaluate(&self, cell: &Cell, asof: &str) -> Option<Scalar> {
185 if !is_instant(asof) {
186 return None;
187 }
188 self.resolve("evaluate", |c| {
189 let Some(desc) = get_bytes(c, Q::DescriptorGet, &cell.fn_hash)? else { return Ok(None) };
190 match Descriptor::from_json(&desc) {
191 Ok(d) if d.lang == "stored" => {}
192 Ok(_) => return Ok(None),
193 Err(e) => {
194 tracing::error!(target: "datumesh_store", fn_hash = %cell.fn_hash, error = %e, "stored descriptor does not parse");
195 return Ok(None);
196 }
197 }
198 let st = c.stmt(Q::ValueAsof)?;
199 let Some(row) = c.client.query_opt(&st, &[&cell.id(), &asof]).db()? else { return Ok(None) };
200 let bytes: Vec<u8> = row.try_get(0).db()?;
201 match Receipt::from_json(&bytes) {
202 Ok(r) => Ok(Some(r.value().clone())),
203 Err(e) => {
204 tracing::error!(target: "datumesh_store", cell_id = %cell.id(), error = %e, "stored receipt does not parse");
205 Ok(None)
206 }
207 }
208 })
209 }
210}
211
212impl Store for PgStore {
213 fn put_descriptor(&self, bytes: &[u8]) -> Result<String> {
214 let d = Descriptor::from_json(bytes)?;
215 let hash = d.hash();
216 let canonical = d.canonical();
217 self.run("put_descriptor", |c| {
218 let st = c.stmt(Q::DescriptorPut)?;
219 c.client.execute(&st, &[&hash, &canonical]).db()?;
220 Ok(())
221 })?;
222 tracing::debug!(target: "datumesh_store", fn_hash = %hash, "put_descriptor");
223 Ok(hash)
224 }
225
226 fn put_receipt(&self, bytes: &[u8]) -> Result<String> {
227 let r = Receipt::from_json(bytes)?;
229 let canonical = json::parse(bytes)?.to_jcs();
231 let hash = r.hash().to_string();
232 let cell_id = r.cell().id();
233 let host = r.signer_host().to_string();
234 let inputs: Vec<String> = r.inputs().to_vec();
235 self.run("put_receipt", |c| {
236 let ns = c.stmt(Q::NamespaceGet)?;
237 let put = c.stmt(Q::ReceiptPut)?;
238 let get = c.stmt(Q::ReceiptGet)?;
239 let edges = c.stmt(Q::EdgesPut)?;
240 let mut t = c.client.transaction().db()?;
241 if let Some(row) = t.query_opt(&get, &[&hash]).db()? {
245 let stored: Vec<u8> = row.try_get(0).db()?;
246 if stored == canonical {
247 return Ok(());
248 }
249 }
250 let mut keys = Keys::new();
253 if let Some(row) = t.query_opt(&ns, &[&host]).db()? {
254 let doc: Vec<u8> = row.try_get(0).db()?;
255 keys.insert_response(&host, 200, Some(&doc));
256 }
257 let out = verify(bytes, &keys, None, None);
258 if !out.ok {
259 return Err(Error::Invalid(out.error().unwrap_or_else(|| {
260 datumesh_core::Error::new(ErrorCode::Signature, "$", "the receipt did not verify")
261 })));
262 }
263 let n = t
264 .execute(&put, &[&hash, &canonical, &cell_id, &r.tx(), &r.asof(), &r.cell().fn_hash, &r.publisher()])
265 .db()?;
266 if n == 0 {
267 let row = t.query_one(&get, &[&hash]).db()?;
268 let stored: Vec<u8> = row.try_get(0).db()?;
269 if stored != canonical {
270 return Err(Error::Conflict { hash: hash.clone() });
271 }
272 }
273 if !inputs.is_empty() {
274 t.execute(&edges, &[&hash, &inputs]).db()?;
275 }
276 t.commit().db()?;
277 Ok(())
278 })?;
279 tracing::debug!(target: "datumesh_store", hash = %hash, cell_id = %cell_id, "put_receipt");
280 Ok(hash)
281 }
282
283 fn put_blob(&self, bytes: &[u8]) -> Result<String> {
284 let sha = sha256_hex(bytes);
285 let length = i64::try_from(bytes.len()).unwrap_or(i64::MAX);
286 self.run("put_blob", |c| {
287 let st = c.stmt(Q::BlobPut)?;
288 c.client.execute(&st, &[&sha, &bytes, &length]).db()?;
289 Ok(())
290 })?;
291 tracing::debug!(target: "datumesh_store", sha256 = %sha, length, "put_blob");
292 Ok(sha)
293 }
294
295 fn blob(&self, sha256: &str) -> Result<Option<Cow<'_, [u8]>>> {
296 check_hex(sha256, 64, "sha256")?;
297 Ok(self.run("blob", |c| get_bytes(c, Q::BlobGet, sha256))?.map(Cow::Owned))
298 }
299
300 fn put_version(&self, cell_id: &str, tx: &str, receipt_hash: &str) -> Result<()> {
301 check_hex(cell_id, 64, "cell_id")?;
302 check_instant(tx, "tx")?;
303 check_hex(receipt_hash, 64, "receipt_hash")?;
304 self.run("put_version", |c| {
305 let lock = c.stmt(Q::Lock)?;
306 let meta = c.stmt(Q::ReceiptMeta)?;
307 let latest = c.stmt(Q::VersionLatestTx)?;
308 let put = c.stmt(Q::VersionPut)?;
309 let mut t = c.client.transaction().db()?;
310 t.execute(&lock, &[&"versions", &cell_id]).db()?;
311 let Some(row) = t.query_opt(&meta, &[&receipt_hash]).db()? else {
312 return Err(Error::NotFound { what: "receipt", key: receipt_hash.to_string() });
313 };
314 let (r_cell, r_tx): (String, String) = (row.try_get(0).db()?, row.try_get(1).db()?);
315 if r_cell != cell_id {
316 return Err(Error::Mismatch { what: "cell_id", expected: cell_id.to_string(), found: r_cell });
317 }
318 if r_tx != tx {
319 return Err(Error::Mismatch { what: "tx", expected: tx.to_string(), found: r_tx });
320 }
321 if let Some(row) = t.query_opt(&latest, &[&cell_id]).db()? {
322 let l: String = row.try_get(0).db()?;
323 if tx <= l.as_str() {
324 return Err(Error::NotLater {
325 what: "version",
326 key: cell_id.to_string(),
327 given: tx.to_string(),
328 latest: l,
329 });
330 }
331 }
332 t.execute(&put, &[&cell_id, &tx, &receipt_hash]).db()?;
333 t.commit().db()?;
334 Ok(())
335 })?;
336 tracing::debug!(target: "datumesh_store", cell_id, tx, receipt_hash, "put_version");
337 Ok(())
338 }
339
340 fn version_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>> {
341 self.asof_version("version_asof", Q::VersionAsof, cell_id, asof)
342 }
343
344 fn versions(&self, cell_id: &str, from: &str, to: &str) -> Result<Vec<Version>> {
345 check_hex(cell_id, 64, "cell_id")?;
346 check_instant(from, "from")?;
347 check_instant(to, "to")?;
348 self.run("versions", |c| {
349 let st = c.stmt(Q::VersionRange)?;
350 let rows = c.client.query(&st, &[&cell_id, &from, &to]).db()?;
351 PgStore::version_rows(&rows, cell_id)
352 })
353 }
354
355 fn latest(&self, cell_id: &str) -> Result<Option<Version>> {
356 check_hex(cell_id, 64, "cell_id")?;
357 self.run("latest", |c| {
358 let st = c.stmt(Q::VersionLatest)?;
359 let rows = c.client.query(&st, &[&cell_id]).db()?;
360 Ok(PgStore::version_rows(&rows, cell_id)?.pop())
361 })
362 }
363
364 fn dependents(&self, receipt_hash: &str) -> Result<Vec<String>> {
365 check_hex(receipt_hash, 64, "receipt_hash")?;
366 self.run("dependents", |c| get_strings(c, Q::Dependents, receipt_hash))
367 }
368
369 fn inputs(&self, receipt_hash: &str) -> Result<Vec<String>> {
370 check_hex(receipt_hash, 64, "receipt_hash")?;
371 self.run("inputs", |c| get_strings(c, Q::Inputs, receipt_hash))
372 }
373
374 fn put_namespace(&self, host: &str, keydoc_bytes: &[u8], validated_at: &str) -> Result<()> {
375 keys_from_response(host, 200, Some(keydoc_bytes))?;
377 check_instant(validated_at, "validated_at")?;
378 let canonical = json::parse(keydoc_bytes)?.to_jcs();
379 self.run("put_namespace", |c| {
380 let lock = c.stmt(Q::Lock)?;
381 let get = c.stmt(Q::NamespaceGet)?;
382 let put = c.stmt(Q::NamespacePut)?;
383 let mut t = c.client.transaction().db()?;
384 t.execute(&lock, &[&"namespaces", &host]).db()?;
385 if let Some(row) = t.query_opt(&get, &[&host]).db()? {
386 let at: String = row.try_get(1).db()?;
387 if validated_at < at.as_str() {
388 return Err(Error::NotLater {
389 what: "namespace",
390 key: host.to_string(),
391 given: validated_at.to_string(),
392 latest: at,
393 });
394 }
395 }
396 t.execute(&put, &[&host, &canonical, &validated_at]).db()?;
397 t.commit().db()?;
398 Ok(())
399 })?;
400 tracing::debug!(target: "datumesh_store", host, validated_at, "put_namespace");
401 Ok(())
402 }
403
404 fn namespace(&self, host: &str) -> Result<Option<Namespace>> {
405 check_dns_name(host, "host")?;
406 self.run("namespace", |c| {
407 let st = c.stmt(Q::NamespaceGet)?;
408 let Some(row) = c.client.query_opt(&st, &[&host]).db()? else { return Ok(None) };
409 Ok(Some(Namespace {
410 host: host.to_string(),
411 keydoc: row.try_get(0).db()?,
412 validated_at: row.try_get(1).db()?,
413 suspended_at: row.try_get(2).db()?,
414 }))
415 })
416 }
417
418 fn bind_name(&self, fn_name: &str, fn_hash: &str, tx: &str) -> Result<()> {
419 check_function_name(fn_name, "fn_name")?;
420 check_hex(fn_hash, 64, "fn_hash")?;
421 check_instant(tx, "tx")?;
422 self.run("bind_name", |c| {
423 let lock = c.stmt(Q::Lock)?;
424 let exists = c.stmt(Q::DescriptorExists)?;
425 let latest = c.stmt(Q::NameLatest)?;
426 let put = c.stmt(Q::NamePut)?;
427 let mut t = c.client.transaction().db()?;
428 t.execute(&lock, &[&"names", &fn_name]).db()?;
429 let found: bool = t.query_one(&exists, &[&fn_hash]).db()?.try_get(0).db()?;
430 if !found {
431 return Err(Error::NotFound { what: "descriptor", key: fn_hash.to_string() });
432 }
433 if let Some(row) = t.query_opt(&latest, &[&fn_name]).db()? {
434 let l: String = row.try_get(0).db()?;
435 if tx <= l.as_str() {
436 return Err(Error::NotLater {
437 what: "name",
438 key: fn_name.to_string(),
439 given: tx.to_string(),
440 latest: l,
441 });
442 }
443 }
444 t.execute(&put, &[&fn_name, &tx, &fn_hash]).db()?;
445 t.commit().db()?;
446 Ok(())
447 })?;
448 tracing::debug!(target: "datumesh_store", fn_name, fn_hash, tx, "bind_name");
449 Ok(())
450 }
451
452 fn put_computed(&self, cell_id: &str, tx: &str, receipt_hash: &str) -> Result<()> {
453 check_hex(cell_id, 64, "cell_id")?;
454 check_instant(tx, "tx")?;
455 check_hex(receipt_hash, 64, "receipt_hash")?;
456 let asof = self.run("put_computed", |c| {
460 let meta = c.stmt(Q::ReceiptMeta)?;
461 let put = c.stmt(Q::ComputedPut)?;
462 let get = c.stmt(Q::ComputedGet)?;
463 let mut t = c.client.transaction().db()?;
464 let Some(row) = t.query_opt(&meta, &[&receipt_hash]).db()? else {
465 return Err(Error::NotFound { what: "receipt", key: receipt_hash.to_string() });
466 };
467 let (r_cell, r_tx, asof): (String, String, String) =
468 (row.try_get(0).db()?, row.try_get(1).db()?, row.try_get(2).db()?);
469 if r_cell != cell_id {
470 return Err(Error::Mismatch { what: "cell_id", expected: cell_id.to_string(), found: r_cell });
471 }
472 if r_tx != tx {
473 return Err(Error::Mismatch { what: "tx", expected: tx.to_string(), found: r_tx });
474 }
475 if t.execute(&put, &[&cell_id, &tx, &asof, &receipt_hash]).db()? == 0 {
476 let stored: String = t.query_one(&get, &[&cell_id, &tx, &asof]).db()?.try_get(0).db()?;
477 if stored != receipt_hash {
478 return Err(Error::Conflict { hash: receipt_hash.to_string() });
479 }
480 }
481 t.commit().db()?;
482 Ok(asof)
483 })?;
484 tracing::debug!(target: "datumesh_store", cell_id, tx, asof, receipt_hash, "put_computed");
485 Ok(())
486 }
487
488 fn computed_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>> {
489 self.asof_version("computed_asof", Q::ComputedAsof, cell_id, asof)
490 }
491
492 fn stored_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>> {
493 self.asof_version("stored_asof", Q::StoredAsof, cell_id, asof)
494 }
495
496 fn cell_dependents(&self, cell_id: &str) -> Result<Vec<String>> {
497 check_hex(cell_id, 64, "cell_id")?;
498 self.run("cell_dependents", |c| get_strings(c, Q::CellDependents, cell_id))
499 }
500
501 fn computed_asof_with_receipt(&self, cell_id: &str, asof: &str) -> Result<Option<(Version, Cow<'_, [u8]>)>> {
503 Ok(self
504 .asof_with_receipt("computed_asof_with_receipt", Q::ComputedAsofReceipt, cell_id, asof)?
505 .map(|(v, b)| (v, Cow::Owned(b))))
506 }
507
508 fn stored_asof_with_receipt(&self, cell_id: &str, asof: &str) -> Result<Option<(Version, Cow<'_, [u8]>)>> {
510 Ok(self
511 .asof_with_receipt("stored_asof_with_receipt", Q::StoredAsofReceipt, cell_id, asof)?
512 .map(|(v, b)| (v, Cow::Owned(b))))
513 }
514
515 fn resolve_binding(&self, fn_name: &str, asof: &str) -> Result<Option<Binding>> {
516 check_function_name(fn_name, "fn_name")?;
517 check_instant(asof, "asof")?;
518 self.run("resolve_binding", |c| {
519 let st = c.stmt(Q::BindingAsof)?;
520 let Some(row) = c.client.query_opt(&st, &[&fn_name, &asof]).db()? else { return Ok(None) };
521 Ok(Some(Binding { fn_hash: row.try_get(0).db()?, tx: row.try_get(1).db()? }))
522 })
523 }
524
525 fn resolve_name(&self, fn_name: &str, asof: &str) -> Result<Option<String>> {
526 check_function_name(fn_name, "fn_name")?;
527 check_instant(asof, "asof")?;
528 self.run("resolve_name", |c| {
529 let st = c.stmt(Q::NameAsof)?;
530 let row = c.client.query_opt(&st, &[&fn_name, &asof]).db()?;
531 Ok(match row {
532 Some(r) => Some(r.try_get(0).db()?),
533 None => None,
534 })
535 })
536 }
537}