Skip to main content

datumesh_store/
pg.rs

1//! `PgStore`: the store on PostgreSQL.
2
3use 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, SliceMember, SliceValue, Store, Version};
10use datumesh_core::json::Value;
11use datumesh_core::lexical::{check_dns_name, check_function_name, check_hex, check_input_entry, check_instant};
12use datumesh_core::{
13    json, keys_from_response, sha256_hex, verify, Cell, ErrorCode, FunctionDescriptor, Keys, Manifest, Receipt,
14    Resolver, Scalar, Slice, Type,
15};
16
17/// The store on PostgreSQL, with a connection pool. `Send + Sync`: share one per process
18/// (behind an `Arc`); each call checks a connection out for its duration.
19///
20/// Every `put_*` is one transaction. [`Store`] methods return errors: a malformed argument
21/// is [`Error::Invalid`] (no query is made), a database failure [`Error::Database`]. The
22/// [`Resolver`] methods answer both with `None`; a database failure is logged with
23/// `tracing` (target `datumesh_store`, level `error`).
24pub struct PgStore {
25    pool: Pool,
26}
27
28impl std::fmt::Debug for PgStore {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        f.debug_struct("PgStore").finish_non_exhaustive()
31    }
32}
33
34fn is_hash(s: &str) -> bool {
35    check_hex(s, 64, "$").is_ok()
36}
37
38fn is_instant(s: &str) -> bool {
39    check_instant(s, "$").is_ok()
40}
41
42/// A slice entry, `slice:<slice id>:<members hash>` (`E_CANON_HEX` otherwise, as SPEC 10.1
43/// step 3 says of a malformed `inputs` entry).
44fn check_entry(entry: &str, path: &str) -> Result<()> {
45    check_input_entry(entry, path)?;
46    if !entry.starts_with("slice:") {
47        return Err(Error::Invalid(datumesh_core::Error::new(ErrorCode::CanonHex, path, "not a slice entry")));
48    }
49    Ok(())
50}
51
52impl PgStore {
53    /// Connects to a `postgres://` URL or `key=value` connection string with the default
54    /// [`PoolOptions`]: [`connect_with(url, PoolOptions::new())`](Self::connect_with).
55    pub fn connect(url: &str) -> Result<PgStore> {
56        PgStore::connect_with(url, PoolOptions::new())
57    }
58
59    /// Connects to a `postgres://` URL or `key=value` connection string, with a pool and TLS
60    /// roots as `options` say. One connection is opened at once, so a wrong URL, an
61    /// unreachable server or a refused certificate fails here; nothing else is done on it
62    /// (no statement is prepared, the schema is not looked at), so an empty database is fine
63    /// (create the schema with [`migrate::apply`](crate::migrate::apply)).
64    ///
65    /// TLS (rustls) is used as the URL's `sslmode` says: `disable` never, `prefer` (the
66    /// default) when the server offers it, `require` always. Whenever it is used, the
67    /// server's certificate and host name are verified, against the Mozilla roots plus the
68    /// roots added with [`PoolOptions::tls_root_certificates_pem`]; there is no way to
69    /// accept any certificate. So a server with `ssl=on` and a self-signed certificate (or
70    /// one from a private CA) fails even under the default `prefer`: add its CA, or connect
71    /// with `sslmode=disable`.
72    pub fn connect_with(url: &str, options: PoolOptions) -> Result<PgStore> {
73        let config: postgres::Config = url.parse().db()?;
74        let connector = Connector::new(config, &options)?;
75        PgStore::from_pool(Pool::new(Box::new(move || connector.connect()), options))
76    }
77
78    /// A store on `pool`, after opening one connection.
79    pub(crate) fn from_pool(pool: Pool) -> Result<PgStore> {
80        let store = PgStore { pool };
81        drop(store.pool.get()?);
82        Ok(store)
83    }
84
85    /// A pooled connection, held until dropped (migrations hold one throughout).
86    pub(crate) fn conn(&self) -> Result<Pooled<'_>> {
87        self.pool.get()
88    }
89
90    /// Prepares every SQL statement the store uses against the live schema, on a fresh
91    /// connection's behalf (no cached statement is reused), and returns how many there are.
92    /// Fails with the first statement the schema cannot serve.
93    pub fn check_statements(&self) -> Result<usize> {
94        let mut c = self.pool.get()?;
95        for q in Q::ALL {
96            c.client.prepare_typed(q.sql(), q.types()).db()?;
97        }
98        Ok(Q::ALL.len())
99    }
100
101    /// A [`Resolver`] lookup: a failure (logged by `run`) becomes `None`.
102    fn resolve<T>(&self, op: &'static str, f: impl FnOnce(&mut Conn) -> Result<Option<T>>) -> Option<T> {
103        self.run(op, f).ok().flatten()
104    }
105
106    /// Runs `f` on a pooled connection; a database or pool failure is logged.
107    fn run<T>(&self, op: &'static str, f: impl FnOnce(&mut Conn) -> Result<T>) -> Result<T> {
108        let result = self.pool.get().and_then(|mut c| f(&mut c));
109        if let Err(e @ (Error::Database(_) | Error::PoolTimeout)) = &result {
110            tracing::error!(target: "datumesh_store", op, error = %e, "database failure");
111        }
112        result
113    }
114
115    /// Versions from rows of (tx, receipt_hash, asof).
116    fn version_rows(rows: &[postgres::Row], cell_id: &str) -> Result<Vec<Version>> {
117        rows.iter().map(|r| version_row(r, cell_id)).collect()
118    }
119
120    /// A version (and its receipt's bytes) from a query answering
121    /// (tx, receipt_hash, asof, bytes) for (cell_id, asof).
122    fn asof_with_receipt(
123        &self,
124        op: &'static str,
125        q: Q,
126        cell_id: &str,
127        asof: &str,
128    ) -> Result<Option<(Version, Vec<u8>)>> {
129        check_hex(cell_id, 64, "cell_id")?;
130        check_instant(asof, "asof")?;
131        self.run(op, |c| {
132            let st = c.stmt(q)?;
133            let Some(row) = c.client.query_opt(&st, &[&cell_id, &asof]).db()? else { return Ok(None) };
134            Ok(Some((version_row(&row, cell_id)?, row.try_get(3).db()?)))
135        })
136    }
137
138    /// A version from a query answering (tx, receipt_hash, asof) for (cell_id, asof).
139    fn asof_version(&self, op: &'static str, q: Q, cell_id: &str, asof: &str) -> Result<Option<Version>> {
140        check_hex(cell_id, 64, "cell_id")?;
141        check_instant(asof, "asof")?;
142        self.run(op, |c| {
143            let st = c.stmt(q)?;
144            let rows = c.client.query(&st, &[&cell_id, &asof]).db()?;
145            Ok(PgStore::version_rows(&rows, cell_id)?.pop())
146        })
147    }
148}
149
150fn version_row(r: &postgres::Row, cell_id: &str) -> Result<Version> {
151    Ok(Version {
152        cell_id: cell_id.to_string(),
153        tx: r.try_get(0).db()?,
154        receipt_hash: r.try_get(1).db()?,
155        asof: r.try_get(2).db()?,
156    })
157}
158
159/// A member from a row of (tx, receipt_hash, asof, bytes, ...).
160fn member_row(r: &postgres::Row, cell_id: &str) -> Result<SliceMember> {
161    Ok(SliceMember { version: version_row(r, cell_id)?, receipt: r.try_get(3).db()? })
162}
163
164/// A member's value and cell from a row of (tx, receipt_hash, asof, value_type, value, ns,
165/// fn, fn_hash, "name:type:value" of each argument, ...): the cell is rebuilt from the
166/// typed-argument index (`cell_args` holds every argument's type and lexical form).
167fn value_row(r: &postgres::Row, cell_id: &str) -> Result<SliceValue> {
168    let bad =
169        |what: &str| Error::Invalid(datumesh_core::Error::new(ErrorCode::CanonType, "$", format!("a stored {what}")));
170    let scalar = |ty: &str, value: &str| -> Result<Scalar> {
171        let ty = Type::from_name(ty).ok_or_else(|| bad("type"))?;
172        Ok(Scalar::from_lexical(ty, value)?)
173    };
174    let (ty, value): (String, String) = (r.try_get(3).db()?, r.try_get(4).db()?);
175    let mut args = datumesh_core::Args::new();
176    for a in r.try_get::<_, Vec<String>>(8).db()? {
177        let mut parts = a.splitn(3, ':');
178        let (Some(name), Some(t), Some(v)) = (parts.next(), parts.next(), parts.next()) else {
179            return Err(bad("argument"));
180        };
181        args.insert(name.to_string(), datumesh_core::Arg::Value(scalar(t, v)?));
182    }
183    let cell = Cell { args, fn_hash: r.try_get(7).db()?, fn_name: r.try_get(6).db()?, ns: r.try_get(5).db()? };
184    Ok(SliceValue { version: version_row(r, cell_id)?, value: scalar(&ty, &value)?, cell })
185}
186
187fn get_bytes(c: &mut Conn, q: Q, key: &str) -> Result<Option<Vec<u8>>> {
188    let st = c.stmt(q)?;
189    let row = c.client.query_opt(&st, &[&key]).db()?;
190    Ok(match row {
191        Some(r) => Some(r.try_get(0).db()?),
192        None => None,
193    })
194}
195
196fn get_strings(c: &mut Conn, q: Q, key: &str) -> Result<Vec<String>> {
197    let st = c.stmt(q)?;
198    let rows = c.client.query(&st, &[&key]).db()?;
199    rows.iter().map(|r| r.try_get(0).db()).collect()
200}
201
202impl Resolver for PgStore {
203    fn descriptor(&self, fn_hash: &str) -> Option<Cow<'_, [u8]>> {
204        if !is_hash(fn_hash) {
205            return None;
206        }
207        self.resolve("descriptor", |c| get_bytes(c, Q::DescriptorGet, fn_hash)).map(Cow::Owned)
208    }
209
210    fn receipt(&self, hash: &str) -> Option<Cow<'_, [u8]>> {
211        if !is_hash(hash) {
212            return None;
213        }
214        self.resolve("receipt", |c| get_bytes(c, Q::ReceiptGet, hash)).map(Cow::Owned)
215    }
216
217    /// The stored manifest of slice entry `entry` ([`Store::put_manifest`]), its canonical
218    /// form; `None` when there is none, or `entry` is not an entry (since 0.3.1).
219    fn manifest(&self, entry: &str) -> Option<Cow<'_, [u8]>> {
220        if check_entry(entry, "$").is_err() {
221            return None;
222        }
223        self.resolve("manifest", |c| get_bytes(c, Q::ManifestGet, entry)).map(Cow::Owned)
224    }
225
226    /// `Some` only for a `stored` function (descriptor `code.lang` is `stored`, and the store
227    /// has the descriptor): the value of the receipt of the version that answers as of
228    /// `asof`, [`stored_asof`](Store::stored_asof)'s: the latest version (by tx) with `tx <=
229    /// asof` whose receipt's asof is also `<= asof` (the core's testkit
230    /// `AnswerRule::TxAndAsof`). `None` for every other function, and when no version
231    /// answers. (Until 0.2.0 it looked at `tx` only, as [`version_asof`](Store::version_asof)
232    /// still does; the same when every receipt's asof is its tx.)
233    fn evaluate(&self, cell: &Cell, asof: &str) -> Option<Scalar> {
234        if !is_instant(asof) {
235            return None;
236        }
237        self.resolve("evaluate", |c| {
238            let Some(desc) = get_bytes(c, Q::DescriptorGet, &cell.fn_hash)? else { return Ok(None) };
239            match FunctionDescriptor::from_json(&desc) {
240                Ok(d) if d.lang == "stored" => {}
241                Ok(_) => return Ok(None),
242                Err(e) => {
243                    tracing::error!(target: "datumesh_store", fn_hash = %cell.fn_hash, error = %e, "stored descriptor does not parse");
244                    return Ok(None);
245                }
246            }
247            let st = c.stmt(Q::ValueAsof)?;
248            let Some(row) = c.client.query_opt(&st, &[&cell.id(), &asof]).db()? else { return Ok(None) };
249            let bytes: Vec<u8> = row.try_get(0).db()?;
250            match Receipt::from_json(&bytes) {
251                Ok(r) => Ok(Some(r.value().clone())),
252                Err(e) => {
253                    tracing::error!(target: "datumesh_store", cell_id = %cell.id(), error = %e, "stored receipt does not parse");
254                    Ok(None)
255                }
256            }
257        })
258    }
259}
260
261impl Store for PgStore {
262    fn put_descriptor(&self, bytes: &[u8]) -> Result<String> {
263        // spec-0.4.0's descriptor: `signature.order` and argument `extent` allowed.
264        let d = FunctionDescriptor::from_json(bytes)?;
265        let hash = d.hash();
266        let canonical = d.canonical();
267        self.run("put_descriptor", |c| {
268            let st = c.stmt(Q::DescriptorPut)?;
269            c.client.execute(&st, &[&hash, &canonical]).db()?;
270            Ok(())
271        })?;
272        tracing::debug!(target: "datumesh_store", fn_hash = %hash, "put_descriptor");
273        Ok(hash)
274    }
275
276    fn put_receipt(&self, bytes: &[u8]) -> Result<String> {
277        // Steps 1-4 first: a receipt that is not one is refused without a query.
278        let r = Receipt::from_json(bytes)?;
279        // Steps 1-4 passed, so the text parses; its JCS is the canonical form.
280        let canonical = json::parse(bytes)?.to_jcs();
281        let hash = r.hash().to_string();
282        let cell_id = r.cell().id();
283        let host = r.signer_host().to_string();
284        // Receipt hashes are edges; slice entries (spec-0.4.0) slice edges.
285        let (entries, inputs): (Vec<String>, Vec<String>) =
286            r.inputs().iter().cloned().partition(|e| e.starts_with("slice:"));
287        self.run("put_receipt", |c| {
288            let ns = c.stmt(Q::NamespaceGet)?;
289            let put = c.stmt(Q::ReceiptPut)?;
290            let get = c.stmt(Q::ReceiptGet)?;
291            let edges = c.stmt(Q::EdgesPut)?;
292            let slice_edges = c.stmt(Q::SliceEdgesPut)?;
293            let mut t = c.client.transaction().db()?;
294            // Idempotent: the very receipt already stored verified when it was stored, and
295            // the stored copy is unchanged, so it is not checked again (even if its key has
296            // since been revoked).
297            if let Some(row) = t.query_opt(&get, &[&hash]).db()? {
298                let stored: Vec<u8> = row.try_get(0).db()?;
299                if stored == canonical {
300                    return Ok(());
301                }
302            }
303            // Steps 5-6 against the stored key document of the signer host; none stored is
304            // a host without keys (E_KEY_UNAVAILABLE).
305            let mut keys = Keys::new();
306            if let Some(row) = t.query_opt(&ns, &[&host]).db()? {
307                let doc: Vec<u8> = row.try_get(0).db()?;
308                keys.insert_response(&host, 200, Some(&doc));
309            }
310            let out = verify(bytes, &keys, None, None);
311            if !out.ok {
312                return Err(Error::Invalid(out.error().unwrap_or_else(|| {
313                    datumesh_core::Error::new(ErrorCode::Signature, "$", "the receipt did not verify")
314                })));
315            }
316            let n = t
317                .execute(&put, &[&hash, &canonical, &cell_id, &r.tx(), &r.asof(), &r.cell().fn_hash, &r.publisher()])
318                .db()?;
319            if n == 0 {
320                let row = t.query_one(&get, &[&hash]).db()?;
321                let stored: Vec<u8> = row.try_get(0).db()?;
322                if stored != canonical {
323                    return Err(Error::Conflict { hash: hash.clone() });
324                }
325            }
326            if !inputs.is_empty() {
327                t.execute(&edges, &[&hash, &inputs]).db()?;
328            }
329            if !entries.is_empty() {
330                t.execute(&slice_edges, &[&hash, &entries]).db()?;
331            }
332            t.commit().db()?;
333            Ok(())
334        })?;
335        tracing::debug!(target: "datumesh_store", hash = %hash, cell_id = %cell_id, "put_receipt");
336        Ok(hash)
337    }
338
339    fn put_blob(&self, bytes: &[u8]) -> Result<String> {
340        let sha = sha256_hex(bytes);
341        let length = i64::try_from(bytes.len()).unwrap_or(i64::MAX);
342        self.run("put_blob", |c| {
343            let st = c.stmt(Q::BlobPut)?;
344            c.client.execute(&st, &[&sha, &bytes, &length]).db()?;
345            Ok(())
346        })?;
347        tracing::debug!(target: "datumesh_store", sha256 = %sha, length, "put_blob");
348        Ok(sha)
349    }
350
351    fn blob(&self, sha256: &str) -> Result<Option<Cow<'_, [u8]>>> {
352        check_hex(sha256, 64, "sha256")?;
353        Ok(self.run("blob", |c| get_bytes(c, Q::BlobGet, sha256))?.map(Cow::Owned))
354    }
355
356    fn put_version(&self, cell_id: &str, tx: &str, receipt_hash: &str) -> Result<()> {
357        check_hex(cell_id, 64, "cell_id")?;
358        check_instant(tx, "tx")?;
359        check_hex(receipt_hash, 64, "receipt_hash")?;
360        self.run("put_version", |c| {
361            let lock = c.stmt(Q::Lock)?;
362            let meta = c.stmt(Q::ReceiptMeta)?;
363            let latest = c.stmt(Q::VersionLatestTx)?;
364            let put = c.stmt(Q::VersionPut)?;
365            let mut t = c.client.transaction().db()?;
366            t.execute(&lock, &[&"versions", &cell_id]).db()?;
367            let Some(row) = t.query_opt(&meta, &[&receipt_hash]).db()? else {
368                return Err(Error::NotFound { what: "receipt", key: receipt_hash.to_string() });
369            };
370            let (r_cell, r_tx): (String, String) = (row.try_get(0).db()?, row.try_get(1).db()?);
371            if r_cell != cell_id {
372                return Err(Error::Mismatch { what: "cell_id", expected: cell_id.to_string(), found: r_cell });
373            }
374            if r_tx != tx {
375                return Err(Error::Mismatch { what: "tx", expected: tx.to_string(), found: r_tx });
376            }
377            if let Some(row) = t.query_opt(&latest, &[&cell_id]).db()? {
378                let l: String = row.try_get(0).db()?;
379                if tx <= l.as_str() {
380                    return Err(Error::NotLater {
381                        what: "version",
382                        key: cell_id.to_string(),
383                        given: tx.to_string(),
384                        latest: l,
385                    });
386                }
387            }
388            t.execute(&put, &[&cell_id, &tx, &receipt_hash]).db()?;
389            t.commit().db()?;
390            Ok(())
391        })?;
392        tracing::debug!(target: "datumesh_store", cell_id, tx, receipt_hash, "put_version");
393        Ok(())
394    }
395
396    fn version_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>> {
397        self.asof_version("version_asof", Q::VersionAsof, cell_id, asof)
398    }
399
400    fn versions(&self, cell_id: &str, from: &str, to: &str) -> Result<Vec<Version>> {
401        check_hex(cell_id, 64, "cell_id")?;
402        check_instant(from, "from")?;
403        check_instant(to, "to")?;
404        self.run("versions", |c| {
405            let st = c.stmt(Q::VersionRange)?;
406            let rows = c.client.query(&st, &[&cell_id, &from, &to]).db()?;
407            PgStore::version_rows(&rows, cell_id)
408        })
409    }
410
411    fn latest(&self, cell_id: &str) -> Result<Option<Version>> {
412        check_hex(cell_id, 64, "cell_id")?;
413        self.run("latest", |c| {
414            let st = c.stmt(Q::VersionLatest)?;
415            let rows = c.client.query(&st, &[&cell_id]).db()?;
416            Ok(PgStore::version_rows(&rows, cell_id)?.pop())
417        })
418    }
419
420    fn dependents(&self, receipt_hash: &str) -> Result<Vec<String>> {
421        check_hex(receipt_hash, 64, "receipt_hash")?;
422        self.run("dependents", |c| get_strings(c, Q::Dependents, receipt_hash))
423    }
424
425    fn inputs(&self, receipt_hash: &str) -> Result<Vec<String>> {
426        check_hex(receipt_hash, 64, "receipt_hash")?;
427        self.run("inputs", |c| get_strings(c, Q::Inputs, receipt_hash))
428    }
429
430    fn put_namespace(&self, host: &str, keydoc_bytes: &[u8], validated_at: &str) -> Result<()> {
431        // SPEC 9.3: a 200 with this body, for this host (a DNS name equal to the document's).
432        keys_from_response(host, 200, Some(keydoc_bytes))?;
433        check_instant(validated_at, "validated_at")?;
434        let canonical = json::parse(keydoc_bytes)?.to_jcs();
435        self.run("put_namespace", |c| {
436            let lock = c.stmt(Q::Lock)?;
437            let get = c.stmt(Q::NamespaceGet)?;
438            let put = c.stmt(Q::NamespacePut)?;
439            let mut t = c.client.transaction().db()?;
440            t.execute(&lock, &[&"namespaces", &host]).db()?;
441            if let Some(row) = t.query_opt(&get, &[&host]).db()? {
442                let at: String = row.try_get(1).db()?;
443                if validated_at < at.as_str() {
444                    return Err(Error::NotLater {
445                        what: "namespace",
446                        key: host.to_string(),
447                        given: validated_at.to_string(),
448                        latest: at,
449                    });
450                }
451            }
452            t.execute(&put, &[&host, &canonical, &validated_at]).db()?;
453            t.commit().db()?;
454            Ok(())
455        })?;
456        tracing::debug!(target: "datumesh_store", host, validated_at, "put_namespace");
457        Ok(())
458    }
459
460    fn namespace(&self, host: &str) -> Result<Option<Namespace>> {
461        check_dns_name(host, "host")?;
462        self.run("namespace", |c| {
463            let st = c.stmt(Q::NamespaceGet)?;
464            let Some(row) = c.client.query_opt(&st, &[&host]).db()? else { return Ok(None) };
465            Ok(Some(Namespace {
466                host: host.to_string(),
467                keydoc: row.try_get(0).db()?,
468                validated_at: row.try_get(1).db()?,
469                suspended_at: row.try_get(2).db()?,
470            }))
471        })
472    }
473
474    fn bind_name(&self, fn_name: &str, fn_hash: &str, tx: &str) -> Result<()> {
475        check_function_name(fn_name, "fn_name")?;
476        check_hex(fn_hash, 64, "fn_hash")?;
477        check_instant(tx, "tx")?;
478        self.run("bind_name", |c| {
479            let lock = c.stmt(Q::Lock)?;
480            let exists = c.stmt(Q::DescriptorExists)?;
481            let latest = c.stmt(Q::NameLatest)?;
482            let put = c.stmt(Q::NamePut)?;
483            let mut t = c.client.transaction().db()?;
484            t.execute(&lock, &[&"names", &fn_name]).db()?;
485            let found: bool = t.query_one(&exists, &[&fn_hash]).db()?.try_get(0).db()?;
486            if !found {
487                return Err(Error::NotFound { what: "descriptor", key: fn_hash.to_string() });
488            }
489            if let Some(row) = t.query_opt(&latest, &[&fn_name]).db()? {
490                let l: String = row.try_get(0).db()?;
491                if tx <= l.as_str() {
492                    return Err(Error::NotLater {
493                        what: "name",
494                        key: fn_name.to_string(),
495                        given: tx.to_string(),
496                        latest: l,
497                    });
498                }
499            }
500            t.execute(&put, &[&fn_name, &tx, &fn_hash]).db()?;
501            t.commit().db()?;
502            Ok(())
503        })?;
504        tracing::debug!(target: "datumesh_store", fn_name, fn_hash, tx, "bind_name");
505        Ok(())
506    }
507
508    fn put_computed(&self, cell_id: &str, tx: &str, receipt_hash: &str) -> Result<()> {
509        check_hex(cell_id, 64, "cell_id")?;
510        check_instant(tx, "tx")?;
511        check_hex(receipt_hash, 64, "receipt_hash")?;
512        // No lock: the primary key serialises writers of one (cell_id, tx, asof). An insert
513        // that meets a concurrent one waits for it; if it committed, nothing is inserted and
514        // the row it wrote (visible to the next statement under READ COMMITTED) decides.
515        let asof = self.run("put_computed", |c| {
516            let meta = c.stmt(Q::ReceiptMeta)?;
517            let put = c.stmt(Q::ComputedPut)?;
518            let get = c.stmt(Q::ComputedGet)?;
519            let mut t = c.client.transaction().db()?;
520            let Some(row) = t.query_opt(&meta, &[&receipt_hash]).db()? else {
521                return Err(Error::NotFound { what: "receipt", key: receipt_hash.to_string() });
522            };
523            let (r_cell, r_tx, asof): (String, String, String) =
524                (row.try_get(0).db()?, row.try_get(1).db()?, row.try_get(2).db()?);
525            if r_cell != cell_id {
526                return Err(Error::Mismatch { what: "cell_id", expected: cell_id.to_string(), found: r_cell });
527            }
528            if r_tx != tx {
529                return Err(Error::Mismatch { what: "tx", expected: tx.to_string(), found: r_tx });
530            }
531            if t.execute(&put, &[&cell_id, &tx, &asof, &receipt_hash]).db()? == 0 {
532                let stored: String = t.query_one(&get, &[&cell_id, &tx, &asof]).db()?.try_get(0).db()?;
533                if stored != receipt_hash {
534                    return Err(Error::Conflict { hash: receipt_hash.to_string() });
535                }
536            }
537            t.commit().db()?;
538            Ok(asof)
539        })?;
540        tracing::debug!(target: "datumesh_store", cell_id, tx, asof, receipt_hash, "put_computed");
541        Ok(())
542    }
543
544    fn computed_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>> {
545        self.asof_version("computed_asof", Q::ComputedAsof, cell_id, asof)
546    }
547
548    fn stored_asof(&self, cell_id: &str, asof: &str) -> Result<Option<Version>> {
549        self.asof_version("stored_asof", Q::StoredAsof, cell_id, asof)
550    }
551
552    fn cell_dependents(&self, cell_id: &str) -> Result<Vec<String>> {
553        check_hex(cell_id, 64, "cell_id")?;
554        self.run("cell_dependents", |c| get_strings(c, Q::CellDependents, cell_id))
555    }
556
557    /// One query: the version and its receipt's bytes.
558    fn computed_asof_with_receipt(&self, cell_id: &str, asof: &str) -> Result<Option<(Version, Cow<'_, [u8]>)>> {
559        Ok(self
560            .asof_with_receipt("computed_asof_with_receipt", Q::ComputedAsofReceipt, cell_id, asof)?
561            .map(|(v, b)| (v, Cow::Owned(b))))
562    }
563
564    /// One query: the version and its receipt's bytes.
565    fn stored_asof_with_receipt(&self, cell_id: &str, asof: &str) -> Result<Option<(Version, Cow<'_, [u8]>)>> {
566        Ok(self
567            .asof_with_receipt("stored_asof_with_receipt", Q::StoredAsofReceipt, cell_id, asof)?
568            .map(|(v, b)| (v, Cow::Owned(b))))
569    }
570
571    fn put_manifest(&self, bytes: &[u8]) -> Result<String> {
572        let m = Manifest::from_json(bytes)?;
573        let entry = m.entry();
574        let canonical = m.canonical();
575        let members = m.members();
576        let n = i32::try_from(members.len()).unwrap_or(i32::MAX);
577        self.run("put_manifest", |c| {
578            let put = c.stmt(Q::ManifestPut)?;
579            let put_members = c.stmt(Q::ManifestMembersPut)?;
580            let mut t = c.client.transaction().db()?;
581            // The entry hashes the content: a stored entry has these very bytes. A concurrent
582            // insert of it waits for the other; if that one committed, nothing is inserted
583            // here, members included (it inserted them).
584            let inserted = t
585                .execute(&put, &[&entry, &m.slice().id(), &m.members_hash(), &m.slice().fn_hash(), &n, &canonical])
586                .db()?;
587            if inserted == 1 && !members.is_empty() {
588                t.execute(&put_members, &[&entry, &members]).db()?;
589            }
590            t.commit().db()?;
591            Ok(())
592        })?;
593        tracing::debug!(target: "datumesh_store", entry = %entry, members = n, "put_manifest");
594        Ok(entry)
595    }
596
597    fn member_manifests(&self, receipt_hash: &str) -> Result<Vec<String>> {
598        check_hex(receipt_hash, 64, "receipt_hash")?;
599        self.run("member_manifests", |c| get_strings(c, Q::MemberManifests, receipt_hash))
600    }
601
602    fn slice_entries(&self, receipt_hash: &str) -> Result<Vec<String>> {
603        check_hex(receipt_hash, 64, "receipt_hash")?;
604        self.run("slice_entries", |c| get_strings(c, Q::SliceEntries, receipt_hash))
605    }
606
607    fn slice_dependents(&self, entry: &str) -> Result<Vec<String>> {
608        check_entry(entry, "entry")?;
609        self.run("slice_dependents", |c| get_strings(c, Q::SliceDependents, entry))
610    }
611
612    fn slice_members(&self, slice: &Slice, asof: &str) -> Result<Vec<SliceMember>> {
613        check_instant(asof, "asof")?;
614        let v = slice.to_value();
615        if let Some((cell_id, from, to)) = version_slice(slice, &v)? {
616            return self.versions_between(&cell_id, &from, &to, asof);
617        }
618        let pattern = Pattern::of(&v)?;
619        self.run("slice_members", |c| pattern.members(c, asof))
620    }
621
622    fn versions_between(&self, cell_id: &str, from: &str, to: &str, asof: &str) -> Result<Vec<SliceMember>> {
623        check_hex(cell_id, 64, "cell_id")?;
624        check_instant(from, "from")?;
625        check_instant(to, "to")?;
626        check_instant(asof, "asof")?;
627        self.run("versions_between", |c| {
628            let st = c.stmt(Q::VersionsBetween)?;
629            let rows = c.client.query(&st, &[&cell_id, &from, &to, &asof]).db()?;
630            rows.iter().map(|r| member_row(r, cell_id)).collect()
631        })
632    }
633
634    fn slice_values(&self, slice: &Slice, asof: &str) -> Result<Vec<SliceValue>> {
635        check_instant(asof, "asof")?;
636        let v = slice.to_value();
637        if let Some((cell_id, from, to)) = version_slice(slice, &v)? {
638            return self.run("slice_values", |c| {
639                let st = c.stmt(Q::VersionsBetweenValues)?;
640                let rows = c.client.query(&st, &[&cell_id, &from, &to, &asof]).db()?;
641                rows.iter().map(|r| value_row(r, &cell_id)).collect()
642            });
643        }
644        let pattern = Pattern::of(&v)?;
645        self.run("slice_values", |c| pattern.values(c, asof))
646    }
647
648    fn slice_changed_after(&self, slice: &Slice, asof: &str) -> Result<bool> {
649        check_instant(asof, "asof")?;
650        let v = slice.to_value();
651        if let Some((cell_id, from, to)) = version_slice(slice, &v)? {
652            return self.run("slice_changed_after", |c| {
653                let st = c.stmt(Q::VersionsChangedAfter)?;
654                c.client.query_one(&st, &[&cell_id, &from, &to, &asof]).db()?.try_get(0).db()
655            });
656        }
657        let pattern = Pattern::of(&v)?;
658        self.run("slice_changed_after", |c| pattern.changed_after(c, asof))
659    }
660
661    fn resolve_binding(&self, fn_name: &str, asof: &str) -> Result<Option<Binding>> {
662        check_function_name(fn_name, "fn_name")?;
663        check_instant(asof, "asof")?;
664        self.run("resolve_binding", |c| {
665            let st = c.stmt(Q::BindingAsof)?;
666            let Some(row) = c.client.query_opt(&st, &[&fn_name, &asof]).db()? else { return Ok(None) };
667            Ok(Some(Binding { fn_hash: row.try_get(0).db()?, tx: row.try_get(1).db()? }))
668        })
669    }
670
671    fn resolve_name(&self, fn_name: &str, asof: &str) -> Result<Option<String>> {
672        check_function_name(fn_name, "fn_name")?;
673        check_instant(asof, "asof")?;
674        self.run("resolve_name", |c| {
675            let st = c.stmt(Q::NameAsof)?;
676            let row = c.client.query_opt(&st, &[&fn_name, &asof]).db()?;
677            Ok(match row {
678                Some(r) => Some(r.try_get(0).db()?),
679                None => None,
680            })
681        })
682    }
683}
684
685/// A version slice's cell id, `from` and `to`; `None` for a slice of cells.
686fn version_slice(slice: &Slice, v: &Value) -> Result<Option<(String, String, String)>> {
687    if !slice.is_version_slice() {
688        return Ok(None);
689    }
690    match (v.get("cell"), v.get("from").and_then(Value::as_str), v.get("to").and_then(Value::as_str)) {
691        (Some(cell), Some(from), Some(to)) => Ok(Some((sha256_hex(&cell.to_jcs()), from.to_string(), to.to_string()))),
692        _ => Err(Error::Invalid(datumesh_core::Error::new(ErrorCode::Schema, "$", "not a version slice"))),
693    }
694}
695
696/// How a slice of cells constrains one argument.
697#[derive(Clone, Copy, Debug, PartialEq, Eq)]
698enum Kind {
699    Open,
700    Value,
701    Range,
702}
703
704impl Kind {
705    fn as_str(self) -> &'static str {
706        match self {
707            Kind::Open => "open",
708            Kind::Value => "value",
709            Kind::Range => "range",
710        }
711    }
712}
713
714/// One argument of a slice of cells: its name, how it is constrained, and for a bound one
715/// its type and lexical form (`lo` = `hi`), for a ranged one the range's type and ends.
716struct Constraint {
717    name: String,
718    kind: Kind,
719    ty: String,
720    lo: String,
721    hi: String,
722}
723
724/// A slice of cells (SPEC 4.4) as the member query takes it.
725struct Pattern {
726    fn_hash: String,
727    ns: String,
728    fn_name: String,
729    args: Vec<Constraint>,
730}
731
732fn malformed(what: &str) -> Error {
733    Error::Invalid(datumesh_core::Error::new(ErrorCode::Schema, "$", format!("not a slice object: {what}")))
734}
735
736impl Pattern {
737    /// From the JSON form of a checked slice object of cells (`Slice::to_value`).
738    fn of(v: &Value) -> Result<Pattern> {
739        let s = |k: &str| v.get(k).and_then(Value::as_str).map(str::to_string).ok_or_else(|| malformed(k));
740        let mut args = Vec::new();
741        for (name, a) in v.get("args").and_then(Value::as_object).ok_or_else(|| malformed("args"))? {
742            let text = |k: &str| match a.get(k) {
743                Some(Value::String(x)) => Ok(x.clone()),
744                Some(Value::Bool(b)) => Ok(b.to_string()),
745                _ => Err(malformed(k)),
746            };
747            let c = match a {
748                Value::String(x) if x == "*" => Constraint {
749                    name: name.clone(),
750                    kind: Kind::Open,
751                    ty: String::new(),
752                    lo: String::new(),
753                    hi: String::new(),
754                },
755                Value::Object(m) if m.iter().any(|(k, _)| k == "lo") => Constraint {
756                    name: name.clone(),
757                    kind: Kind::Range,
758                    ty: text("type")?,
759                    lo: text("lo")?,
760                    hi: text("hi")?,
761                },
762                _ => {
763                    let value = text("value")?;
764                    Constraint {
765                        name: name.clone(),
766                        kind: Kind::Value,
767                        ty: text("type")?,
768                        lo: value.clone(),
769                        hi: value,
770                    }
771                }
772            };
773            args.push(c);
774        }
775        args.sort_by(|a, b| a.name.cmp(&b.name));
776        Ok(Pattern { fn_hash: s("fn_hash")?, ns: s("ns")?, fn_name: s("fn")?, args })
777    }
778
779    /// The members as of `asof`, in key order.
780    fn members(&self, c: &mut Conn, asof: &str) -> Result<Vec<SliceMember>> {
781        let qs = [Q::SliceMembersAll, Q::SliceMembersText, Q::SliceMembersNum];
782        self.rows(c, asof, qs, 4, member_row, |m| &m.version.cell_id)
783    }
784
785    /// The members' values as of `asof`, in key order.
786    fn values(&self, c: &mut Conn, asof: &str) -> Result<Vec<SliceValue>> {
787        let qs = [Q::SliceValuesAll, Q::SliceValuesText, Q::SliceValuesNum];
788        self.rows(c, asof, qs, 9, value_row, |m| &m.version.cell_id)
789    }
790
791    /// The constraint columns of the member and change queries ($6-$10).
792    fn columns(&self) -> [Vec<String>; 5] {
793        let col = |f: fn(&Constraint) -> &str| -> Vec<String> { self.args.iter().map(|a| f(a).to_string()).collect() };
794        [
795            col(|a| a.name.as_str()),
796            col(|a| a.kind.as_str()),
797            col(|a| a.ty.as_str()),
798            col(|a| a.lo.as_str()),
799            col(|a| a.hi.as_str()),
800        ]
801    }
802
803    /// Whether a matching cell has a stored version answering from after `asof`: from
804    /// whichever side is smaller (both probed, up to 10,001), the function's receipts with an
805    /// asof after `asof` (then each one's cell checked against the slice), or the slice's
806    /// candidate cells (then each one's receipts by cell and asof).
807    fn changed_after(&self, c: &mut Conn, asof: &str) -> Result<bool> {
808        let (drive, candidates) = self.drive(c, true)?;
809        let st = c.stmt(Q::ProbeRecent)?;
810        let recent: i64 = c.client.query_one(&st, &[&self.fn_hash, &asof]).db()?.try_get(0).db()?;
811        let qs = if recent <= candidates.unwrap_or(i64::MAX) {
812            [Q::ChangedRecent; 3]
813        } else {
814            [Q::ChangedCandAll, Q::ChangedCandText, Q::ChangedCandNum]
815        };
816        let rows = self.query(c, asof, qs, drive)?;
817        match rows.first() {
818            Some(r) => r.try_get(0).db(),
819            None => Ok(false),
820        }
821    }
822
823    /// The bound or ranged argument holding the fewest cells (probed, up to 10,001, when
824    /// there are several, or when `count` asks), else none (every cell of the function); and
825    /// the number of candidates when probed.
826    fn drive(&self, c: &mut Conn, count: bool) -> Result<(Option<&Constraint>, Option<i64>)> {
827        let mut probe = |a: Option<&Constraint>| -> Result<i64> {
828            match a {
829                None => {
830                    let st = c.stmt(Q::ProbeCells)?;
831                    c.client.query_one(&st, &[&self.fn_hash]).db()?.try_get(0).db()
832                }
833                Some(a) => {
834                    let st = c.stmt(if a.ty == "number" { Q::SliceProbeNum } else { Q::SliceProbeText })?;
835                    c.client.query_one(&st, &[&self.fn_hash, &a.name, &a.ty, &a.lo, &a.hi]).db()?.try_get(0).db()
836                }
837            }
838        };
839        let constrained: Vec<&Constraint> = self.args.iter().filter(|a| a.kind != Kind::Open).collect();
840        Ok(match constrained.as_slice() {
841            [] => (None, if count { Some(probe(None)?) } else { None }),
842            [one] => (Some(*one), if count { Some(probe(Some(one))?) } else { None }),
843            several => {
844                let mut best: Option<(i64, &Constraint)> = None;
845                for a in several {
846                    let n = probe(Some(a))?;
847                    if best.is_none_or(|(m, _)| n < m) {
848                        best = Some((n, a));
849                    }
850                }
851                (best.map(|(_, a)| a), best.map(|(n, _)| n))
852            }
853        })
854    }
855
856    /// Runs one of a family of slice queries `qs` (all cells, a text range, a number range)
857    /// with the slice's parameters and the candidates narrowed by `drive`.
858    fn query(&self, c: &mut Conn, asof: &str, qs: [Q; 3], drive: Option<&Constraint>) -> Result<Vec<postgres::Row>> {
859        let [names, kinds, types, los, his] = self.columns();
860        let keys: Vec<String> = self.args.iter().filter(|a| a.kind != Kind::Value).map(|a| a.name.clone()).collect();
861        let nargs = i32::try_from(self.args.len()).unwrap_or(i32::MAX);
862        let (q, d_name, d_ty, d_lo, d_hi) = match drive {
863            None => (qs[0], "", "", "", ""),
864            Some(a) => {
865                let q = if a.ty == "number" { qs[2] } else { qs[1] };
866                (q, a.name.as_str(), a.ty.as_str(), a.lo.as_str(), a.hi.as_str())
867            }
868        };
869        let st = c.stmt(q)?;
870        c.client
871            .query(
872                &st,
873                &[
874                    &self.fn_hash,
875                    &self.ns,
876                    &self.fn_name,
877                    &nargs,
878                    &asof,
879                    &names,
880                    &kinds,
881                    &types,
882                    &los,
883                    &his,
884                    &keys,
885                    &d_name,
886                    &d_ty,
887                    &d_lo,
888                    &d_hi,
889                ],
890            )
891            .db()
892    }
893
894    /// The rows of one of the member queries `qs` (all cells, a text range, a number range),
895    /// each made into a `T` (the cell id in column `at`, the key after it), in key order.
896    fn rows<T>(
897        &self,
898        c: &mut Conn,
899        asof: &str,
900        qs: [Q; 3],
901        at: usize,
902        make: impl Fn(&postgres::Row, &str) -> Result<T>,
903        cell_of: impl Fn(&T) -> &String,
904    ) -> Result<Vec<T>> {
905        let (drive, _) = self.drive(c, false)?;
906        let rows = self.query(c, asof, qs, drive)?;
907        let mut out: Vec<(Vec<String>, T)> = Vec::with_capacity(rows.len());
908        for r in &rows {
909            let cell_id: String = r.try_get(at).db()?;
910            let key: Vec<String> = r.try_get(at + 1).db()?;
911            out.push((key, make(r, &cell_id)?));
912        }
913        out.sort_by(|(a, ma), (b, mb)| cmp_keys(a, b).then_with(|| cell_of(ma).cmp(cell_of(mb))));
914        Ok(out.into_iter().map(|(_, m)| m).collect())
915    }
916}
917
918/// Key tuples of `type:value` items, compared item by item in each type's order (SPEC
919/// 15.13): numbers numerically, everything else by its lexical form as bytes (dates and
920/// instants are fixed width, so chronologically; strings by code point; `false` before
921/// `true`; blobs as text). Items of different types (cells of one function hash whose
922/// receipts disagree on an argument's type) by type name.
923fn cmp_keys(a: &[String], b: &[String]) -> std::cmp::Ordering {
924    for (x, y) in a.iter().zip(b) {
925        let (tx, vx) = x.split_once(':').unwrap_or(("", x));
926        let (ty, vy) = y.split_once(':').unwrap_or(("", y));
927        let o = tx.cmp(ty).then_with(|| if tx == "number" { cmp_decimal(vx, vy) } else { vx.cmp(vy) });
928        if o.is_ne() {
929            return o;
930        }
931    }
932    a.len().cmp(&b.len())
933}
934
935/// Two numbers in canonical form (SPEC 3.1) compared as decimals.
936pub(crate) fn cmp_decimal(a: &str, b: &str) -> std::cmp::Ordering {
937    let (na, ma) = a.strip_prefix('-').map_or((false, a), |m| (true, m));
938    let (nb, mb) = b.strip_prefix('-').map_or((false, b), |m| (true, m));
939    let magnitude = || {
940        let (ia, fa) = ma.split_once('.').unwrap_or((ma, ""));
941        let (ib, fb) = mb.split_once('.').unwrap_or((mb, ""));
942        // No leading zeros: the longer integer part is the greater; no trailing zeros: the
943        // fractions compare as text.
944        ia.len().cmp(&ib.len()).then_with(|| ia.cmp(ib)).then_with(|| fa.cmp(fb))
945    };
946    match (na, nb) {
947        (false, true) => std::cmp::Ordering::Greater,
948        (true, false) => std::cmp::Ordering::Less,
949        (false, false) => magnitude(),
950        (true, true) => magnitude().reverse(),
951    }
952}
953
954#[cfg(test)]
955mod tests {
956    use super::cmp_decimal;
957    use std::cmp::Ordering::{Equal, Greater, Less};
958
959    #[test]
960    fn decimals_compare_numerically() {
961        for (a, b, o) in [
962            ("9", "10", Less),
963            ("-1.5", "-1", Less),
964            ("-1", "-1.5", Greater),
965            ("0", "-0.001", Greater),
966            ("0.05", "0.5", Less),
967            ("0.5", "0.51", Less),
968            ("10.5", "9.75", Greater),
969            ("-10", "-9", Less),
970            ("123", "123", Equal),
971            ("-0.1", "0", Less),
972            ("1", "0.999999", Greater),
973        ] {
974            assert_eq!(cmp_decimal(a, b), o, "{a} {b}");
975        }
976    }
977}