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::{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> {
181 if !is_instant(asof) {
182 return None;
183 }
184 self.resolve("evaluate", |c| {
185 let Some(desc) = get_bytes(c, Q::DescriptorGet, &cell.fn_hash)? else { return Ok(None) };
186 match Descriptor::from_json(&desc) {
187 Ok(d) if d.lang == "stored" => {}
188 Ok(_) => return Ok(None),
189 Err(e) => {
190 tracing::error!(target: "datumesh_store", fn_hash = %cell.fn_hash, error = %e, "stored descriptor does not parse");
191 return Ok(None);
192 }
193 }
194 let st = c.stmt(Q::ValueAsof)?;
195 let Some(row) = c.client.query_opt(&st, &[&cell.id(), &asof]).db()? else { return Ok(None) };
196 let bytes: Vec<u8> = row.try_get(0).db()?;
197 match Receipt::from_json(&bytes) {
198 Ok(r) => Ok(Some(r.value().clone())),
199 Err(e) => {
200 tracing::error!(target: "datumesh_store", cell_id = %cell.id(), error = %e, "stored receipt does not parse");
201 Ok(None)
202 }
203 }
204 })
205 }
206}
207
208impl Store for PgStore {
209 fn put_descriptor(&self, bytes: &[u8]) -> Result<String> {
210 let d = Descriptor::from_json(bytes)?;
211 let hash = d.hash();
212 let canonical = d.canonical();
213 self.run("put_descriptor", |c| {
214 let st = c.stmt(Q::DescriptorPut)?;
215 c.client.execute(&st, &[&hash, &canonical]).db()?;
216 Ok(())
217 })?;
218 tracing::debug!(target: "datumesh_store", fn_hash = %hash, "put_descriptor");
219 Ok(hash)
220 }
221
222 fn put_receipt(&self, bytes: &[u8]) -> Result<String> {
223 let r = Receipt::from_json(bytes)?;
225 let canonical = json::parse(bytes)?.to_jcs();
227 let hash = r.hash().to_string();
228 let cell_id = r.cell().id();
229 let host = r.signer_host().to_string();
230 let inputs: Vec<String> = r.inputs().to_vec();
231 self.run("put_receipt", |c| {
232 let ns = c.stmt(Q::NamespaceGet)?;
233 let put = c.stmt(Q::ReceiptPut)?;
234 let get = c.stmt(Q::ReceiptGet)?;
235 let edges = c.stmt(Q::EdgesPut)?;
236 let mut t = c.client.transaction().db()?;
237 if let Some(row) = t.query_opt(&get, &[&hash]).db()? {
241 let stored: Vec<u8> = row.try_get(0).db()?;
242 if stored == canonical {
243 return Ok(());
244 }
245 }
246 let mut keys = Keys::new();
249 if let Some(row) = t.query_opt(&ns, &[&host]).db()? {
250 let doc: Vec<u8> = row.try_get(0).db()?;
251 keys.insert_response(&host, 200, Some(&doc));
252 }
253 let out = verify(bytes, &keys, None, None);
254 if !out.ok {
255 return Err(Error::Invalid(out.error().unwrap_or_else(|| {
256 datumesh_core::Error::new(ErrorCode::Signature, "$", "the receipt did not verify")
257 })));
258 }
259 let n = t
260 .execute(&put, &[&hash, &canonical, &cell_id, &r.tx(), &r.asof(), &r.cell().fn_hash, &r.publisher()])
261 .db()?;
262 if n == 0 {
263 let row = t.query_one(&get, &[&hash]).db()?;
264 let stored: Vec<u8> = row.try_get(0).db()?;
265 if stored != canonical {
266 return Err(Error::Conflict { hash: hash.clone() });
267 }
268 }
269 if !inputs.is_empty() {
270 t.execute(&edges, &[&hash, &inputs]).db()?;
271 }
272 t.commit().db()?;
273 Ok(())
274 })?;
275 tracing::debug!(target: "datumesh_store", hash = %hash, cell_id = %cell_id, "put_receipt");
276 Ok(hash)
277 }
278
279 fn put_blob(&self, bytes: &[u8]) -> Result<String> {
280 let sha = sha256_hex(bytes);
281 let length = i64::try_from(bytes.len()).unwrap_or(i64::MAX);
282 self.run("put_blob", |c| {
283 let st = c.stmt(Q::BlobPut)?;
284 c.client.execute(&st, &[&sha, &bytes, &length]).db()?;
285 Ok(())
286 })?;
287 tracing::debug!(target: "datumesh_store", sha256 = %sha, length, "put_blob");
288 Ok(sha)
289 }
290
291 fn blob(&self, sha256: &str) -> Result<Option<Cow<'_, [u8]>>> {
292 check_hex(sha256, 64, "sha256")?;
293 Ok(self.run("blob", |c| get_bytes(c, Q::BlobGet, sha256))?.map(Cow::Owned))
294 }
295
296 fn put_version(&self, cell_id: &str, tx: &str, receipt_hash: &str) -> Result<()> {
297 check_hex(cell_id, 64, "cell_id")?;
298 check_instant(tx, "tx")?;
299 check_hex(receipt_hash, 64, "receipt_hash")?;
300 self.run("put_version", |c| {
301 let lock = c.stmt(Q::Lock)?;
302 let meta = c.stmt(Q::ReceiptMeta)?;
303 let latest = c.stmt(Q::VersionLatestTx)?;
304 let put = c.stmt(Q::VersionPut)?;
305 let mut t = c.client.transaction().db()?;
306 t.execute(&lock, &[&"versions", &cell_id]).db()?;
307 let Some(row) = t.query_opt(&meta, &[&receipt_hash]).db()? else {
308 return Err(Error::NotFound { what: "receipt", key: receipt_hash.to_string() });
309 };
310 let (r_cell, r_tx): (String, String) = (row.try_get(0).db()?, row.try_get(1).db()?);
311 if r_cell != cell_id {
312 return Err(Error::Mismatch { what: "cell_id", expected: cell_id.to_string(), found: r_cell });
313 }
314 if r_tx != tx {
315 return Err(Error::Mismatch { what: "tx", expected: tx.to_string(), found: r_tx });
316 }
317 if let Some(row) = t.query_opt(&latest, &[&cell_id]).db()? {
318 let l: String = row.try_get(0).db()?;
319 if tx <= l.as_str() {
320 return Err(Error::NotLater {
321 what: "version",
322 key: cell_id.to_string(),
323 given: tx.to_string(),
324 latest: l,
325 });
326 }
327 }
328 t.execute(&put, &[&cell_id, &tx, &receipt_hash]).db()?;
329 t.commit().db()?;
330 Ok(())
331 })?;
332 tracing::debug!(target: "datumesh_store", cell_id, tx, receipt_hash, "put_version");
333 Ok(())
334 }
335
336 fn version_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>> {
337 self.asof_version("version_asof", Q::VersionAsof, cell_id, asof)
338 }
339
340 fn versions(&self, cell_id: &str, from: &str, to: &str) -> Result<Vec<Version>> {
341 check_hex(cell_id, 64, "cell_id")?;
342 check_instant(from, "from")?;
343 check_instant(to, "to")?;
344 self.run("versions", |c| {
345 let st = c.stmt(Q::VersionRange)?;
346 let rows = c.client.query(&st, &[&cell_id, &from, &to]).db()?;
347 PgStore::version_rows(&rows, cell_id)
348 })
349 }
350
351 fn latest(&self, cell_id: &str) -> Result<Option<Version>> {
352 check_hex(cell_id, 64, "cell_id")?;
353 self.run("latest", |c| {
354 let st = c.stmt(Q::VersionLatest)?;
355 let rows = c.client.query(&st, &[&cell_id]).db()?;
356 Ok(PgStore::version_rows(&rows, cell_id)?.pop())
357 })
358 }
359
360 fn dependents(&self, receipt_hash: &str) -> Result<Vec<String>> {
361 check_hex(receipt_hash, 64, "receipt_hash")?;
362 self.run("dependents", |c| get_strings(c, Q::Dependents, receipt_hash))
363 }
364
365 fn inputs(&self, receipt_hash: &str) -> Result<Vec<String>> {
366 check_hex(receipt_hash, 64, "receipt_hash")?;
367 self.run("inputs", |c| get_strings(c, Q::Inputs, receipt_hash))
368 }
369
370 fn put_namespace(&self, host: &str, keydoc_bytes: &[u8], validated_at: &str) -> Result<()> {
371 keys_from_response(host, 200, Some(keydoc_bytes))?;
373 check_instant(validated_at, "validated_at")?;
374 let canonical = json::parse(keydoc_bytes)?.to_jcs();
375 self.run("put_namespace", |c| {
376 let lock = c.stmt(Q::Lock)?;
377 let get = c.stmt(Q::NamespaceGet)?;
378 let put = c.stmt(Q::NamespacePut)?;
379 let mut t = c.client.transaction().db()?;
380 t.execute(&lock, &[&"namespaces", &host]).db()?;
381 if let Some(row) = t.query_opt(&get, &[&host]).db()? {
382 let at: String = row.try_get(1).db()?;
383 if validated_at < at.as_str() {
384 return Err(Error::NotLater {
385 what: "namespace",
386 key: host.to_string(),
387 given: validated_at.to_string(),
388 latest: at,
389 });
390 }
391 }
392 t.execute(&put, &[&host, &canonical, &validated_at]).db()?;
393 t.commit().db()?;
394 Ok(())
395 })?;
396 tracing::debug!(target: "datumesh_store", host, validated_at, "put_namespace");
397 Ok(())
398 }
399
400 fn namespace(&self, host: &str) -> Result<Option<Namespace>> {
401 check_dns_name(host, "host")?;
402 self.run("namespace", |c| {
403 let st = c.stmt(Q::NamespaceGet)?;
404 let Some(row) = c.client.query_opt(&st, &[&host]).db()? else { return Ok(None) };
405 Ok(Some(Namespace {
406 host: host.to_string(),
407 keydoc: row.try_get(0).db()?,
408 validated_at: row.try_get(1).db()?,
409 suspended_at: row.try_get(2).db()?,
410 }))
411 })
412 }
413
414 fn bind_name(&self, fn_name: &str, fn_hash: &str, tx: &str) -> Result<()> {
415 check_function_name(fn_name, "fn_name")?;
416 check_hex(fn_hash, 64, "fn_hash")?;
417 check_instant(tx, "tx")?;
418 self.run("bind_name", |c| {
419 let lock = c.stmt(Q::Lock)?;
420 let exists = c.stmt(Q::DescriptorExists)?;
421 let latest = c.stmt(Q::NameLatest)?;
422 let put = c.stmt(Q::NamePut)?;
423 let mut t = c.client.transaction().db()?;
424 t.execute(&lock, &[&"names", &fn_name]).db()?;
425 let found: bool = t.query_one(&exists, &[&fn_hash]).db()?.try_get(0).db()?;
426 if !found {
427 return Err(Error::NotFound { what: "descriptor", key: fn_hash.to_string() });
428 }
429 if let Some(row) = t.query_opt(&latest, &[&fn_name]).db()? {
430 let l: String = row.try_get(0).db()?;
431 if tx <= l.as_str() {
432 return Err(Error::NotLater {
433 what: "name",
434 key: fn_name.to_string(),
435 given: tx.to_string(),
436 latest: l,
437 });
438 }
439 }
440 t.execute(&put, &[&fn_name, &tx, &fn_hash]).db()?;
441 t.commit().db()?;
442 Ok(())
443 })?;
444 tracing::debug!(target: "datumesh_store", fn_name, fn_hash, tx, "bind_name");
445 Ok(())
446 }
447
448 fn put_computed(&self, cell_id: &str, tx: &str, receipt_hash: &str) -> Result<()> {
449 check_hex(cell_id, 64, "cell_id")?;
450 check_instant(tx, "tx")?;
451 check_hex(receipt_hash, 64, "receipt_hash")?;
452 let asof = self.run("put_computed", |c| {
456 let meta = c.stmt(Q::ReceiptMeta)?;
457 let put = c.stmt(Q::ComputedPut)?;
458 let get = c.stmt(Q::ComputedGet)?;
459 let mut t = c.client.transaction().db()?;
460 let Some(row) = t.query_opt(&meta, &[&receipt_hash]).db()? else {
461 return Err(Error::NotFound { what: "receipt", key: receipt_hash.to_string() });
462 };
463 let (r_cell, r_tx, asof): (String, String, String) =
464 (row.try_get(0).db()?, row.try_get(1).db()?, row.try_get(2).db()?);
465 if r_cell != cell_id {
466 return Err(Error::Mismatch { what: "cell_id", expected: cell_id.to_string(), found: r_cell });
467 }
468 if r_tx != tx {
469 return Err(Error::Mismatch { what: "tx", expected: tx.to_string(), found: r_tx });
470 }
471 if t.execute(&put, &[&cell_id, &tx, &asof, &receipt_hash]).db()? == 0 {
472 let stored: String = t.query_one(&get, &[&cell_id, &tx, &asof]).db()?.try_get(0).db()?;
473 if stored != receipt_hash {
474 return Err(Error::Conflict { hash: receipt_hash.to_string() });
475 }
476 }
477 t.commit().db()?;
478 Ok(asof)
479 })?;
480 tracing::debug!(target: "datumesh_store", cell_id, tx, asof, receipt_hash, "put_computed");
481 Ok(())
482 }
483
484 fn computed_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>> {
485 self.asof_version("computed_asof", Q::ComputedAsof, cell_id, asof)
486 }
487
488 fn stored_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>> {
489 self.asof_version("stored_asof", Q::StoredAsof, cell_id, asof)
490 }
491
492 fn cell_dependents(&self, cell_id: &str) -> Result<Vec<String>> {
493 check_hex(cell_id, 64, "cell_id")?;
494 self.run("cell_dependents", |c| get_strings(c, Q::CellDependents, cell_id))
495 }
496
497 fn computed_asof_with_receipt(&self, cell_id: &str, asof: &str) -> Result<Option<(Version, Cow<'_, [u8]>)>> {
499 Ok(self
500 .asof_with_receipt("computed_asof_with_receipt", Q::ComputedAsofReceipt, cell_id, asof)?
501 .map(|(v, b)| (v, Cow::Owned(b))))
502 }
503
504 fn stored_asof_with_receipt(&self, cell_id: &str, asof: &str) -> Result<Option<(Version, Cow<'_, [u8]>)>> {
506 Ok(self
507 .asof_with_receipt("stored_asof_with_receipt", Q::StoredAsofReceipt, cell_id, asof)?
508 .map(|(v, b)| (v, Cow::Owned(b))))
509 }
510
511 fn resolve_name(&self, fn_name: &str, asof: &str) -> Result<Option<String>> {
512 check_function_name(fn_name, "fn_name")?;
513 check_instant(asof, "asof")?;
514 self.run("resolve_name", |c| {
515 let st = c.stmt(Q::NameAsof)?;
516 let row = c.client.query_opt(&st, &[&fn_name, &asof]).db()?;
517 Ok(match row {
518 Some(r) => Some(r.try_get(0).db()?),
519 None => None,
520 })
521 })
522 }
523}