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, 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
15/// The store on PostgreSQL, with a connection pool. `Send + Sync`: share one per process
16/// (behind an `Arc`); each call checks a connection out for its duration.
17///
18/// Every `put_*` is one transaction. [`Store`] methods return errors: a malformed argument
19/// is [`Error::Invalid`] (no query is made), a database failure [`Error::Database`]. The
20/// [`Resolver`] methods answer both with `None`; a database failure is logged with
21/// `tracing` (target `datumesh_store`, level `error`).
22pub 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    /// Connects to a `postgres://` URL or `key=value` connection string with the default
42    /// [`PoolOptions`]: [`connect_with(url, PoolOptions::new())`](Self::connect_with).
43    pub fn connect(url: &str) -> Result<PgStore> {
44        PgStore::connect_with(url, PoolOptions::new())
45    }
46
47    /// Connects to a `postgres://` URL or `key=value` connection string, with a pool and TLS
48    /// roots as `options` say. One connection is opened at once, so a wrong URL, an
49    /// unreachable server or a refused certificate fails here; nothing else is done on it
50    /// (no statement is prepared, the schema is not looked at), so an empty database is fine
51    /// (create the schema with [`migrate::apply`](crate::migrate::apply)).
52    ///
53    /// TLS (rustls) is used as the URL's `sslmode` says: `disable` never, `prefer` (the
54    /// default) when the server offers it, `require` always. Whenever it is used, the
55    /// server's certificate and host name are verified, against the Mozilla roots plus the
56    /// roots added with [`PoolOptions::tls_root_certificates_pem`]; there is no way to
57    /// accept any certificate. So a server with `ssl=on` and a self-signed certificate (or
58    /// one from a private CA) fails even under the default `prefer`: add its CA, or connect
59    /// with `sslmode=disable`.
60    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    /// A store on `pool`, after opening one connection.
67    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    /// A pooled connection, held until dropped (migrations hold one throughout).
74    pub(crate) fn conn(&self) -> Result<Pooled<'_>> {
75        self.pool.get()
76    }
77
78    /// Prepares every SQL statement the store uses against the live schema, on a fresh
79    /// connection's behalf (no cached statement is reused), and returns how many there are.
80    /// Fails with the first statement the schema cannot serve.
81    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    /// A [`Resolver`] lookup: a failure (logged by `run`) becomes `None`.
90    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    /// Runs `f` on a pooled connection; a database or pool failure is logged.
95    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    /// Versions from rows of (tx, receipt_hash, asof).
104    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    /// A version (and its receipt's bytes) from a query answering
109    /// (tx, receipt_hash, asof, bytes) for (cell_id, asof).
110    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    /// A version from a query answering (tx, receipt_hash, asof) for (cell_id, asof).
127    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    /// `Some` only for a `stored` function (descriptor `code.lang` is `stored`, and the store
178    /// has the descriptor): the value of the receipt of the version that answers as of
179    /// `asof`, [`stored_asof`](Store::stored_asof)'s: the latest version (by tx) with `tx <=
180    /// asof` whose receipt's asof is also `<= asof` (the core's testkit
181    /// `AnswerRule::TxAndAsof`). `None` for every other function, and when no version
182    /// answers. (Until 0.2.0 it looked at `tx` only, as [`version_asof`](Store::version_asof)
183    /// still does; the same when every receipt's asof is its tx.)
184    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        // Steps 1-4 first: a receipt that is not one is refused without a query.
228        let r = Receipt::from_json(bytes)?;
229        // Steps 1-4 passed, so the text parses; its JCS is the canonical form.
230        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            // Idempotent: the very receipt already stored verified when it was stored, and
242            // the stored copy is unchanged, so it is not checked again (even if its key has
243            // since been revoked).
244            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            // Steps 5-6 against the stored key document of the signer host; none stored is
251            // a host without keys (E_KEY_UNAVAILABLE).
252            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        // SPEC 9.3: a 200 with this body, for this host (a DNS name equal to the document's).
376        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        // No lock: the primary key serialises writers of one (cell_id, tx, asof). An insert
457        // that meets a concurrent one waits for it; if it committed, nothing is inserted and
458        // the row it wrote (visible to the next statement under READ COMMITTED) decides.
459        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    /// One query: the version and its receipt's bytes.
502    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    /// One query: the version and its receipt's bytes.
509    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}