Skip to main content

hotshot_query_service/data_source/storage/sql/
transaction.rs

1// Copyright (c) 2022 Espresso Systems (espressosys.com)
2// This file is part of the HotShot Query Service library.
3//
4// This program is free software: you can redistribute it and/or modify it under the terms of the GNU
5// General Public License as published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
8// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
9// General Public License for more details.
10// You should have received a copy of the GNU General Public License along with this program. If not,
11// see <https://www.gnu.org/licenses/>.
12
13//! SQL transactions
14//!
15//! A transaction encapsulates all the mutable functionality provided by the SQL database, and
16//! allows for mutable operations to be combined into complex updates that affect the main database
17//! atomically. A transaction also provides all the immutable query functionality of a regular
18//! database connection, so that the updated state of the database can be queried midway through a
19//! transaction.
20
21use std::{
22    collections::HashMap,
23    fmt::{Debug, Display},
24    marker::PhantomData,
25    time::Instant,
26};
27
28use anyhow::{Context, bail};
29use async_trait::async_trait;
30use committable::Committable;
31use derive_more::{Deref, DerefMut};
32use futures::future::Future;
33#[cfg(feature = "embedded-db")]
34use futures::stream::TryStreamExt;
35use hotshot_types::{
36    data::VidShare,
37    simple_certificate::CertificatePair,
38    traits::{
39        EncodeBytes,
40        block_contents::BlockHeader,
41        metrics::{Counter, Gauge, Histogram, Metrics},
42        node_implementation::NodeType,
43    },
44};
45use itertools::Itertools;
46use jf_merkle_tree_compat::prelude::MerkleProof;
47pub use sqlx::Executor;
48use sqlx::{Encode, Execute, FromRow, QueryBuilder, Type, pool::Pool, query_builder::Separated};
49use tracing::instrument;
50
51#[cfg(not(feature = "embedded-db"))]
52use super::queries::state::batch_insert_hashes;
53#[cfg(feature = "embedded-db")]
54use super::queries::state::build_hash_batch_insert;
55use super::{
56    Database, Db,
57    queries::{
58        self,
59        state::{Node, collect_nodes_from_proofs},
60    },
61};
62use crate::{
63    Header, Payload, QueryError, QueryResult,
64    availability::{
65        BlockQueryData, Certificate2, LeafQueryData, QueryableHeader, QueryablePayload,
66        VidCommonQueryData,
67    },
68    data_source::{
69        storage::{NodeStorage, UpdateAvailabilityStorage, pruning::PrunedHeightStorage},
70        update,
71    },
72    merklized_state::{MerklizedState, UpdateStateData},
73    types::HeightIndexed,
74};
75
76/// When set to `true`, read transactions begin with
77/// `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY` (no `DEFERRABLE`),
78/// so they start immediately instead of waiting for a safe serializable snapshot.
79#[cfg(not(feature = "embedded-db"))]
80static NO_DEFERRABLE_ON_READ: std::sync::atomic::AtomicBool =
81    std::sync::atomic::AtomicBool::new(true);
82
83/// Configure whether read transactions on Postgres should omit `DEFERRABLE`.
84///
85/// When `true`, `Read::begin` issues `SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY`
86/// (no `DEFERRABLE`) so the transaction starts immediately rather than waiting for a safe
87/// serializable snapshot. Default: `true`. Call this once at startup based on the operator's
88/// chosen configuration.
89#[cfg(not(feature = "embedded-db"))]
90pub fn set_no_deferrable_on_read(value: bool) {
91    NO_DEFERRABLE_ON_READ.store(value, std::sync::atomic::Ordering::Relaxed);
92}
93
94pub type Query<'q> = sqlx::query::Query<'q, Db, <Db as Database>::Arguments<'q>>;
95pub type QueryAs<'q, T> = sqlx::query::QueryAs<'q, Db, T, <Db as Database>::Arguments<'q>>;
96
97pub fn query(sql: &str) -> Query<'_> {
98    sqlx::query(sql)
99}
100
101pub fn query_as<'q, T>(sql: &'q str) -> QueryAs<'q, T>
102where
103    T: for<'r> FromRow<'r, <Db as Database>::Row>,
104{
105    sqlx::query_as(sql)
106}
107
108/// Marker type indicating a transaction with read-write access to the database.
109#[derive(Clone, Copy, Debug, Default)]
110pub struct Write;
111
112/// Marker type indicating a transaction with read-only access to the database.
113#[derive(Clone, Copy, Debug, Default)]
114pub struct Read;
115
116/// Marker type indicating a transaction used for pruning deletes.
117///
118/// On Postgres this uses READ COMMITTED isolation instead of SERIALIZABLE to avoid predicate lock
119/// conflicts between pruning DELETE and consensus INSERT operations.
120#[derive(Clone, Copy, Debug, Default)]
121pub struct Prune;
122
123/// Trait for marker types indicating what type of access a transaction has to the database.
124pub trait TransactionMode: Send + Sync {
125    fn begin(
126        conn: &mut <Db as Database>::Connection,
127    ) -> impl Future<Output = anyhow::Result<()>> + Send;
128
129    fn display() -> &'static str;
130}
131
132impl TransactionMode for Write {
133    #[allow(unused_variables)]
134    async fn begin(conn: &mut <Db as Database>::Connection) -> anyhow::Result<()> {
135        // SQLite automatically sets the read/write mode of a transactions based on the statements
136        // in it. However, there is still a good reason to explicitly enable write mode right from
137        // the start: if a transaction first executes a read statement and then a write statement,
138        // it will be upgraded from a read transaction to a write transaction. Because this involves
139        // obtaining a different kind of lock while already holding one, it can cause a deadlock,
140        // e.g.:
141        // * Transaction A executes a read statement, obtaining a read lock
142        // * Transaction B executes a write statement and begins waiting for a write lock
143        // * Transaction A executes a write statement and begins waiting for a write lock
144        //
145        // Transaction A can never obtain its write lock because it must first wait for transaction
146        // B to get a write lock, which cannot happen because B is in turn waiting for A to release
147        // its read lock.
148        //
149        // This type of deadlock cannot happen if transaction A immediately starts as a write, since
150        // it will then only ever try to acquire one type of lock (a write lock). By working with
151        // this restriction (transactions are either readers or writers, but never upgradable), we
152        // avoid deadlock, we more closely imitate the concurrency semantics of postgres, and we
153        // take advantage of the SQLite busy timeout, which may allow a transaction to acquire a
154        // lock and succeed (after a small delay), even when there was a conflicting transaction in
155        // progress. Whereas a deadlock is always an automatic rollback.
156        //
157        // The proper way to begin a write transaction in SQLite is with `BEGIN IMMEDIATE`. However,
158        // sqlx does not expose any way to customize the `BEGIN` statement that starts a
159        // transaction. A serviceable workaround is to perform some write statement before performing
160        // any read statement, ensuring that the first lock we acquire is exclusive. A write
161        // statement that has no actual effect on the database is suitable for this purpose, hence
162        // the `WHERE false`.
163        #[cfg(feature = "embedded-db")]
164        conn.execute("UPDATE pruned_height SET id = id WHERE false")
165            .await?;
166
167        // With Postgres things are much more straightforward: just tell Postgres we want a write
168        // transaction immediately after opening it.
169        #[cfg(not(feature = "embedded-db"))]
170        conn.execute("SET TRANSACTION ISOLATION LEVEL SERIALIZABLE")
171            .await?;
172
173        Ok(())
174    }
175
176    fn display() -> &'static str {
177        "write"
178    }
179}
180
181impl TransactionMode for Prune {
182    #[allow(unused_variables)]
183    async fn begin(conn: &mut <Db as Database>::Connection) -> anyhow::Result<()> {
184        // SQLite: same as Write -- acquire an exclusive lock immediately to avoid deadlocks.
185        #[cfg(feature = "embedded-db")]
186        conn.execute("UPDATE pruned_height SET id = id WHERE false")
187            .await?;
188
189        // Postgres: use READ COMMITTED to avoid predicate lock conflicts between pruning
190        // DELETE and concurrent consensus INSERT operations. Pruning does not need SERIALIZABLE
191        // guarantees since it only removes old data that is no longer read by consensus.
192        #[cfg(not(feature = "embedded-db"))]
193        conn.execute("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
194            .await?;
195
196        Ok(())
197    }
198
199    fn display() -> &'static str {
200        "prune"
201    }
202}
203
204/// Marker type indicating a transaction used for deferred-migration batches.
205///
206/// On Postgres this uses READ COMMITTED instead of SERIALIZABLE. Long-running backfill batches
207/// that INSERT into and DELETE from tables consensus is also writing to (e.g. the merkle-tree
208/// tables) trigger Serializable Snapshot Isolation predicate-lock conflicts and abort with
209/// "could not serialize access". Backfill batches are designed to be idempotent (`ON CONFLICT`,
210/// monotonic keyset cursors), so the weaker isolation is acceptable.
211#[derive(Clone, Copy, Debug, Default)]
212pub struct Backfill;
213
214impl TransactionMode for Backfill {
215    #[allow(unused_variables)]
216    async fn begin(conn: &mut <Db as Database>::Connection) -> anyhow::Result<()> {
217        // SQLite: same as Write -- acquire an exclusive lock immediately to avoid deadlocks.
218        #[cfg(feature = "embedded-db")]
219        conn.execute("UPDATE pruned_height SET id = id WHERE false")
220            .await?;
221
222        #[cfg(not(feature = "embedded-db"))]
223        conn.execute("SET TRANSACTION ISOLATION LEVEL READ COMMITTED")
224            .await?;
225
226        Ok(())
227    }
228
229    fn display() -> &'static str {
230        "backfill"
231    }
232}
233
234impl TransactionMode for Read {
235    #[allow(unused_variables)]
236    async fn begin(conn: &mut <Db as Database>::Connection) -> anyhow::Result<()> {
237        // With Postgres, we explicitly set the transaction mode to specify that we want the
238        // strongest possible consistency semantics in case of competing transactions
239        // (SERIALIZABLE), and we want to wait until this is possible rather than failing
240        // (DEFERRABLE).
241        //
242        // Setting `ESPRESSO_NODE_POSTGRES_NO_DEFERRABLE=true` disables the DEFERRABLE
243        // option, so that read transactions start immediately and may instead fail with a
244        // serialization error if they conflict with a concurrent write. This trades start-up
245        // latency for the chance of a retry, and is opt-in.
246        //
247        // With SQLite, there is nothing to be done here, as SQLite automatically starts
248        // transactions in read-only mode, and always has serializable concurrency unless we
249        // explicitly opt in to dirty reads with a pragma.
250        #[cfg(not(feature = "embedded-db"))]
251        {
252            let sql = if NO_DEFERRABLE_ON_READ.load(std::sync::atomic::Ordering::Relaxed) {
253                "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY"
254            } else {
255                "SET TRANSACTION ISOLATION LEVEL SERIALIZABLE, READ ONLY, DEFERRABLE"
256            };
257            conn.execute(sql).await?;
258        }
259
260        Ok(())
261    }
262
263    fn display() -> &'static str {
264        "read-only"
265    }
266}
267
268#[derive(Clone, Copy, Debug)]
269enum CloseType {
270    Commit,
271    Revert,
272    Drop,
273}
274
275#[derive(Debug)]
276struct TransactionMetricsGuard<Mode> {
277    started_at: Instant,
278    metrics: PoolMetrics,
279    close_type: CloseType,
280    _mode: PhantomData<Mode>,
281}
282
283impl<Mode: TransactionMode> TransactionMetricsGuard<Mode> {
284    fn begin(metrics: PoolMetrics) -> Self {
285        let started_at = Instant::now();
286        tracing::trace!(mode = Mode::display(), ?started_at, "begin");
287        metrics.open_transactions.update(1);
288
289        Self {
290            started_at,
291            metrics,
292            close_type: CloseType::Drop,
293            _mode: Default::default(),
294        }
295    }
296
297    fn set_closed(&mut self, t: CloseType) {
298        self.close_type = t;
299    }
300}
301
302impl<Mode> Drop for TransactionMetricsGuard<Mode> {
303    fn drop(&mut self) {
304        self.metrics
305            .transaction_durations
306            .add_point((self.started_at.elapsed().as_millis() as f64) / 1000.);
307        self.metrics.open_transactions.update(-1);
308        match self.close_type {
309            CloseType::Commit => self.metrics.commits.add(1),
310            CloseType::Revert => self.metrics.reverts.add(1),
311            CloseType::Drop => self.metrics.drops.add(1),
312        }
313        tracing::trace!(started_at = ?self.started_at, reason = ?self.close_type, "close");
314    }
315}
316
317/// An atomic SQL transaction.
318#[derive(Debug, Deref, DerefMut)]
319pub struct Transaction<Mode> {
320    #[deref]
321    #[deref_mut]
322    inner: sqlx::Transaction<'static, Db>,
323    metrics: TransactionMetricsGuard<Mode>,
324}
325
326impl<Mode: TransactionMode> Transaction<Mode> {
327    pub(super) async fn new(pool: &Pool<Db>, metrics: PoolMetrics) -> anyhow::Result<Self> {
328        let mut inner = pool.begin().await?;
329        let metrics = TransactionMetricsGuard::begin(metrics);
330        Mode::begin(inner.as_mut()).await?;
331        Ok(Self { inner, metrics })
332    }
333}
334
335impl<Mode: TransactionMode> update::Transaction for Transaction<Mode> {
336    async fn commit(mut self) -> anyhow::Result<()> {
337        self.inner.commit().await?;
338        self.metrics.set_closed(CloseType::Commit);
339        Ok(())
340    }
341    fn revert(mut self) -> impl Future + Send {
342        async move {
343            self.inner.rollback().await.unwrap();
344            self.metrics.set_closed(CloseType::Revert);
345        }
346    }
347}
348
349/// A collection of parameters which can be bound to a SQL query.
350///
351/// This trait allows us to carry around hetergenous lists of parameters (e.g. tuples) and bind them
352/// to a query at the last moment before executing. This means we can manipulate the parameters
353/// independently of the query before executing it. For example, by requiring a trait bound of
354/// `Params<'p> + Clone`, we get a list (or tuple) of parameters which can be cloned and then bound
355/// to a query, which allows us to keep a copy of the parameters around in order to retry the query
356/// if it fails.
357///
358/// # Lifetimes
359///
360/// A SQL [`Query`] with lifetime `'q` borrows from both it's SQL statement (`&'q str`) and its
361/// parameters (bound via `bind<'q>`). Sometimes, though, it is necessary for the statement and its
362/// parameters to have different (but overlapping) lifetimes. For example, the parameters might be
363/// passed in and owned by the caller, while the query string is constructed in the callee and its
364/// lifetime is limited to the callee scope. (See for example the [`upsert`](Transaction::upsert)
365/// function which does exactly this.)
366///
367/// We could rectify this situation with a trait bound like `P: for<'q> Params<'q>`, meaning `P`
368/// must be bindable to a query with a lifetime chosen by the callee. However, when `P` is an
369/// associated type, such as an element of an iterator, as in
370/// `<I as IntoIter>::Item: for<'q> Params<'q>`, [a current limitation](https://blog.rust-lang.org/2022/10/28/gats-stabilization.html#implied-static-requirement-from-higher-ranked-trait-bounds.)
371/// in the Rust compiler then requires `P: 'static`, which we don't necessarily want: the caller
372/// should be able to pass in a reference to avoid expensive cloning.
373///
374/// So, instead, we work around this by making it explicit in the [`Params`] trait that the lifetime
375/// of the query we're binding to (`'q`) may be different than the lifetime of the parameters (`'p`)
376/// as long as the parameters outlive the duration of the query (the `'p: 'q`) bound on the
377/// [`bind`](Self::bind) function.
378pub trait Params<'p> {
379    fn bind<'q, 'r>(
380        self,
381        q: &'q mut Separated<'r, 'p, Db, &'static str>,
382    ) -> &'q mut Separated<'r, 'p, Db, &'static str>
383    where
384        'p: 'r;
385}
386
387/// A collection of parameters with a statically known length.
388///
389/// This is a simple trick for enforcing at compile time that a list of parameters has a certain
390/// length, such as matching the length of a list of column names. This can prevent easy mistakes
391/// like leaving out a parameter. It is implemented for tuples up to length 8.
392pub trait FixedLengthParams<'p, const N: usize>: Params<'p> {}
393
394macro_rules! impl_tuple_params {
395    ($n:literal, ($($t:ident,)+)) => {
396        impl<'p,  $($t),+> Params<'p> for ($($t,)+)
397        where $(
398            $t: 'p +  Encode<'p, Db> + Type<Db>
399        ),+{
400            fn bind<'q, 'r>(self, q: &'q mut Separated<'r, 'p, Db, &'static str>) ->   &'q mut Separated<'r, 'p, Db, &'static str>
401            where 'p: 'r,
402            {
403                #[allow(non_snake_case)]
404                let ($($t,)+) = self;
405                q $(
406                    .push_bind($t)
407                )+
408
409            }
410        }
411
412        impl<'p, $($t),+> FixedLengthParams<'p, $n> for ($($t,)+)
413        where $(
414            $t: 'p + for<'q> Encode<'q, Db> + Type<Db>
415        ),+ {
416        }
417    };
418}
419
420impl_tuple_params!(1, (T,));
421impl_tuple_params!(2, (T1, T2,));
422impl_tuple_params!(3, (T1, T2, T3,));
423impl_tuple_params!(4, (T1, T2, T3, T4,));
424impl_tuple_params!(5, (T1, T2, T3, T4, T5,));
425impl_tuple_params!(6, (T1, T2, T3, T4, T5, T6,));
426impl_tuple_params!(7, (T1, T2, T3, T4, T5, T6, T7,));
427impl_tuple_params!(8, (T1, T2, T3, T4, T5, T6, T7, T8,));
428
429pub fn build_where_in<'a, I>(
430    query: &'a str,
431    column: &'a str,
432    values: I,
433) -> QueryResult<(queries::QueryBuilder<'a>, String)>
434where
435    I: IntoIterator,
436    I::Item: 'a + Encode<'a, Db> + Type<Db>,
437{
438    let mut builder = queries::QueryBuilder::default();
439    let params = values
440        .into_iter()
441        .map(|v| Ok(format!("{} ", builder.bind(v)?)))
442        .collect::<QueryResult<Vec<String>>>()?;
443
444    if params.is_empty() {
445        return Err(QueryError::Error {
446            message: "failed to build WHERE IN query. No parameter found ".to_string(),
447        });
448    }
449
450    let sql = format!(
451        "{query} where {column} IN ({}) ",
452        params.into_iter().join(",")
453    );
454
455    Ok((builder, sql))
456}
457
458/// Low-level, general database queries and mutation.
459impl Transaction<Write> {
460    /// Maximum number of parameters allowed in a single query.
461    ///
462    /// This should be safely under the hard limit imposed by the Postgres client, which is either
463    /// 32,767 or 65,535.
464    const STATEMENT_MAX_PARAMETERS: usize = 30_000;
465
466    pub async fn upsert<'p, const N: usize, R>(
467        &mut self,
468        table: &str,
469        columns: [&str; N],
470        pk: impl IntoIterator<Item = &str>,
471        rows: R,
472    ) -> anyhow::Result<()>
473    where
474        R: IntoIterator,
475        R::Item: 'p + FixedLengthParams<'p, N>,
476    {
477        let set_columns = columns
478            .iter()
479            .map(|col| format!("{col} = excluded.{col}"))
480            .join(",");
481
482        let columns_str = columns.iter().map(|col| format!("\"{col}\"")).join(",");
483
484        let pk = pk.into_iter().join(",");
485
486        let rows: Vec<_> = rows.into_iter().collect();
487        let num_rows = rows.len();
488
489        if num_rows == 0 {
490            tracing::warn!("trying to upsert 0 rows into {table}, this has no effect");
491            return Ok(());
492        }
493
494        // For very large upserts, we might need to proceed in chunks to avoid exceeding the maximum
495        // number of parameters in a single statement.
496        let rows_per_chunk = Self::STATEMENT_MAX_PARAMETERS / N;
497        let mut rows = rows.into_iter();
498        loop {
499            let chunk = rows.by_ref().take(rows_per_chunk).collect::<Vec<_>>();
500            if chunk.is_empty() {
501                break;
502            }
503            let num_rows = chunk.len();
504            tracing::debug!(num_rows, "upsert chunk");
505
506            let mut query_builder =
507                QueryBuilder::new(format!("INSERT INTO \"{table}\" ({columns_str}) "));
508            query_builder.push_values(chunk, |mut b, row| {
509                row.bind(&mut b);
510            });
511            query_builder.push(format!(" ON CONFLICT ({pk}) DO UPDATE SET {set_columns}"));
512
513            let query = query_builder.build();
514            let statement = query.sql();
515
516            let res = self.execute(query).await.inspect_err(|err| {
517                tracing::error!(statement, "error in statement execution: {err:#}");
518            })?;
519            let rows_modified = res.rows_affected() as usize;
520            if rows_modified != num_rows {
521                let error = format!(
522                    "unexpected number of rows modified: expected {num_rows}, got \
523                     {rows_modified}. query: {statement}"
524                );
525                tracing::error!(error);
526                bail!(error);
527            }
528        }
529        Ok(())
530    }
531}
532
533/// Pruning mutations, run under READ COMMITTED isolation on Postgres.
534impl Transaction<Prune> {
535    /// Delete a batch of data for pruning.
536    ///
537    /// Payloads/vid_common are GC'd after header deletion using NOT EXISTS. Under READ
538    /// COMMITTED, if a concurrent insert holds a lock on a payload row, the DELETE waits
539    /// and re-evaluates with a fresh snapshot after the insert commits. If the payload was
540    /// already deleted, the inserting SERIALIZABLE transaction gets a serialization error
541    /// and retries, recreating the payload.
542    #[instrument(skip(self))]
543    pub(super) async fn delete_batch(&mut self, height: u64) -> anyhow::Result<()> {
544        let res = query("DELETE FROM transactions WHERE block_height <= $1")
545            .bind(height as i64)
546            .execute(self.as_mut())
547            .await
548            .context("deleting transactions")?;
549        tracing::debug!(rows_affected = res.rows_affected(), "pruned transactions");
550
551        let res = query("DELETE FROM leaf2 WHERE height <= $1")
552            .bind(height as i64)
553            .execute(self.as_mut())
554            .await
555            .context("deleting leaf2")?;
556        tracing::debug!(rows_affected = res.rows_affected(), "pruned leaf2");
557
558        let res = query("DELETE FROM header WHERE height <= $1")
559            .bind(height as i64)
560            .execute(self.as_mut())
561            .await
562            .context("deleting headers")?;
563        tracing::debug!(rows_affected = res.rows_affected(), "pruned headers");
564
565        let res = query(
566            "DELETE FROM payload AS p
567             WHERE NOT EXISTS (
568                SELECT 1 FROM header AS h
569                WHERE h.payload_hash = p.hash AND h.ns_table = p.ns_table
570             )",
571        )
572        .execute(self.as_mut())
573        .await
574        .context("garbage collecting payloads")?;
575        tracing::debug!(
576            rows_affected = res.rows_affected(),
577            "garbage collected payloads"
578        );
579
580        let res = query(
581            "DELETE FROM vid_common AS v
582             WHERE NOT EXISTS (
583                SELECT 1 FROM header AS h
584                WHERE h.payload_hash = v.hash
585             )",
586        )
587        .execute(self.as_mut())
588        .await
589        .context("garbage collecting VID common")?;
590        tracing::debug!(
591            rows_affected = res.rows_affected(),
592            "garbage collected VID common"
593        );
594
595        Ok(())
596    }
597
598    /// Prune merklized state tables.
599    ///
600    /// Only deletes nodes having `created <= height` that are not the newest node at their position.
601    #[instrument(skip(self))]
602    pub(super) async fn delete_state_batch(
603        &mut self,
604        state_tables: impl Debug + IntoIterator<Item: Display>,
605        height: u64,
606    ) -> anyhow::Result<()> {
607        for state_table in state_tables {
608            self.execute(
609                query(&format!(
610                    "
611                DELETE FROM {state_table}
612                WHERE {state_table}.created <= $1
613                  AND EXISTS (
614                    SELECT 1 FROM {state_table} AS t2
615                    WHERE t2.path = {state_table}.path
616                      AND t2.created > {state_table}.created
617                      AND t2.created <= $1
618                  )"
619                ))
620                .bind(height as i64),
621            )
622            .await?;
623        }
624
625        Ok(())
626    }
627}
628
629impl<Mode> Transaction<Mode> {
630    const PRUNED_HEIGHT_ID: i32 = 1;
631    const STATE_PRUNED_HEIGHT_ID: i32 = 2;
632}
633
634/// Query service specific mutations.
635impl Transaction<Write> {
636    /// Record the height of the latest pruned header.
637    pub(crate) async fn save_pruned_height(&mut self, height: u64) -> anyhow::Result<()> {
638        self.upsert(
639            "pruned_height",
640            ["id", "last_height"],
641            ["id"],
642            [(Self::PRUNED_HEIGHT_ID, height as i64)],
643        )
644        .await
645        .context("updating pruned height")
646    }
647
648    /// Record the height of the latest pruned merklized state.
649    pub(crate) async fn save_state_pruned_height(&mut self, height: u64) -> anyhow::Result<()> {
650        self.upsert(
651            "pruned_height",
652            ["id", "last_height"],
653            ["id"],
654            [(Self::STATE_PRUNED_HEIGHT_ID, height as i64)],
655        )
656        .await
657        .context("updating state pruned height")
658    }
659}
660
661impl<Types> UpdateAvailabilityStorage<Types> for Transaction<Write>
662where
663    Types: NodeType,
664    Payload<Types>: QueryablePayload<Types>,
665    Header<Types>: QueryableHeader<Types>,
666{
667    async fn insert_qc_chain(
668        &mut self,
669        height: u64,
670        qc_chain: Option<[CertificatePair<Types>; 2]>,
671    ) -> anyhow::Result<()> {
672        let block_height = NodeStorage::<Types>::block_height(self).await? as u64;
673        if height + 1 >= block_height {
674            // If this QC chain is for the latest leaf we know about, store it so that we can prove
675            // to clients that the corresponding leaf is finalized. (If it is not the latest leaf,
676            // this is unnecessary, since we can prove it is an ancestor of some later, finalized
677            // leaf.)
678            let qcs = serde_json::to_value(&qc_chain)?;
679            self.upsert("latest_qc_chain", ["id", "qcs"], ["id"], [(1i32, qcs)])
680                .await
681                .context("inserting QC chain")?;
682        }
683
684        Ok(())
685    }
686
687    async fn insert_cert2(
688        &mut self,
689        height: u64,
690        cert2: Certificate2<Types>,
691    ) -> anyhow::Result<()> {
692        let cert2_json = serde_json::to_value(&cert2)?;
693        self.upsert(
694            "cert2",
695            ["height", "data"],
696            ["height"],
697            [(height as i64, cert2_json)],
698        )
699        .await
700        .context("inserting cert2")?;
701        Ok(())
702    }
703
704    async fn insert_leaf_range<'a>(
705        &mut self,
706        leaves: impl Send + IntoIterator<IntoIter: Send, Item = &'a LeafQueryData<Types>>,
707    ) -> anyhow::Result<()> {
708        let leaves = leaves.into_iter();
709
710        // Ignore leaves below the pruned height.
711        let pruned_height = self.load_pruned_height().await?;
712        let leaves = leaves.skip_while(|leaf| pruned_height.is_some_and(|h| leaf.height() <= h));
713
714        // While we don't necessarily have the full block for these leaves yet, we can initialize
715        // the header and leaf tables with block metadata taken from the leaves.
716        let (header_rows, leaf_rows): (Vec<_>, Vec<_>) = leaves
717            .map(|leaf| {
718                let header_json = serde_json::to_value(leaf.leaf().block_header())
719                    .context("failed to serialize header")?;
720                let header_row = (
721                    leaf.height() as i64,
722                    leaf.block_hash().to_string(),
723                    leaf.leaf().block_header().payload_commitment().to_string(),
724                    leaf.leaf().block_header().ns_table(),
725                    header_json,
726                    leaf.leaf().block_header().timestamp() as i64,
727                );
728
729                let leaf_json =
730                    serde_json::to_value(leaf.leaf()).context("failed to serialize leaf")?;
731                let qc_json = serde_json::to_value(leaf.qc()).context("failed to serialize QC")?;
732                let leaf_row = (
733                    leaf.height() as i64,
734                    leaf.hash().to_string(),
735                    leaf.block_hash().to_string(),
736                    leaf_json,
737                    qc_json,
738                );
739
740                anyhow::Ok((header_row, leaf_row))
741            })
742            .process_results(|iter| iter.unzip())?;
743
744        self.upsert(
745            "header",
746            [
747                "height",
748                "hash",
749                "payload_hash",
750                "ns_table",
751                "data",
752                "timestamp",
753            ],
754            ["height"],
755            header_rows,
756        )
757        .await
758        .context("inserting headers")?;
759
760        // Insert the leaves themselves, which reference the header rows we created.
761        self.upsert(
762            "leaf2",
763            ["height", "hash", "block_hash", "leaf", "qc"],
764            ["height"],
765            leaf_rows,
766        )
767        .await
768        .context("inserting leaves")?;
769
770        Ok(())
771    }
772
773    async fn insert_block_range<'a>(
774        &mut self,
775        blocks: impl Send + IntoIterator<IntoIter: Send, Item = &'a BlockQueryData<Types>>,
776    ) -> anyhow::Result<()> {
777        let blocks = blocks.into_iter();
778
779        // Ignore blocks below the pruned height.
780        let pruned_height = self.load_pruned_height().await?;
781        let blocks = blocks.skip_while(|block| pruned_height.is_some_and(|h| block.height() <= h));
782
783        let (payload_rows, tx_rows): (Vec<_>, Vec<_>) = blocks
784            .map(|block| {
785                let payload_row = (
786                    block.payload_hash().to_string(),
787                    block.header().ns_table(),
788                    block.size() as i32,
789                    block.num_transactions() as i32,
790                    block.payload.encode().as_ref().to_vec(),
791                );
792
793                let tx_rows = block.enumerate().map(|(txn_ix, txn)| {
794                    let ns_id = block.header().namespace_id(&txn_ix.ns_index).unwrap();
795                    (
796                        txn.commit().to_string(),
797                        block.height() as i64,
798                        txn_ix.ns_index.into(),
799                        ns_id.into(),
800                        txn_ix.position as i64,
801                    )
802                });
803
804                (payload_row, tx_rows)
805            })
806            .unzip();
807        let tx_rows = tx_rows.into_iter().flatten().collect::<Vec<_>>();
808
809        // Multiple blocks in the range might have the same payload. We must filter out such
810        // duplicates, because SQL does not allow conflicting rows in a single upsert statement.
811        let payload_rows = payload_rows
812            .into_iter()
813            .unique_by(|(hash, ns_table, ..)| (hash.clone(), ns_table.clone()));
814
815        self.upsert(
816            "payload",
817            ["hash", "ns_table", "size", "num_transactions", "data"],
818            ["hash", "ns_table"],
819            payload_rows,
820        )
821        .await
822        .context("inserting payloads")?;
823
824        // Index the transactions and namespaces in the block.
825        if !tx_rows.is_empty() {
826            self.upsert(
827                "transactions",
828                ["hash", "block_height", "ns_index", "ns_id", "position"],
829                ["block_height", "ns_id", "position"],
830                tx_rows,
831            )
832            .await
833            .context("inserting transactions")?;
834        }
835
836        Ok(())
837    }
838
839    async fn insert_vid_range<'a>(
840        &mut self,
841        vid: impl Send
842        + IntoIterator<
843            IntoIter: Send,
844            Item = (&'a VidCommonQueryData<Types>, Option<&'a VidShare>),
845        >,
846    ) -> anyhow::Result<()> {
847        let vid = vid.into_iter();
848
849        // Ignore objects below the pruned height.
850        let pruned_height = self.load_pruned_height().await?;
851        let vid = vid.skip_while(|(common, _)| pruned_height.is_some_and(|h| common.height() <= h));
852
853        let (common_rows, share_rows): (Vec<_>, Vec<_>) = vid
854            .map(|(common, share)| {
855                let common_data = bincode::serialize(common.common())
856                    .context("failed to serialize VID common data")?;
857                let common_row = (common.payload_hash().to_string(), common_data);
858
859                let share_row = if let Some(share) = share {
860                    let share_data =
861                        bincode::serialize(&share).context("failed to serialize VID share")?;
862                    Some((common.height() as i64, share_data))
863                } else {
864                    None
865                };
866
867                anyhow::Ok((common_row, share_row))
868            })
869            .process_results(|iter| iter.unzip())?;
870        let share_rows = share_rows.into_iter().flatten().collect::<Vec<_>>();
871
872        // Multiple blocks in the range might have the same VID common. We must filter out such
873        // duplicates, because SQL does not allow conflicting rows in a single upsert statement.
874        let common_rows = common_rows.into_iter().unique_by(|(hash, ..)| hash.clone());
875
876        self.upsert("vid_common", ["hash", "data"], ["hash"], common_rows)
877            .await
878            .context("inserting VID common")?;
879
880        if !share_rows.is_empty() {
881            let mut q = QueryBuilder::new("WITH rows (height, share) AS (");
882            q.push_values(share_rows, |mut q, (height, share)| {
883                q.push_bind(height).push_bind(share);
884            });
885            q.push(
886                ") UPDATE header SET vid_share = rows.share
887                FROM rows
888                WHERE header.height = rows.height",
889            );
890            q.build()
891                .execute(self.as_mut())
892                .await
893                .context("inserting VID shares")?;
894        }
895
896        Ok(())
897    }
898}
899
900#[async_trait]
901impl<Types: NodeType, State: MerklizedState<Types, ARITY>, const ARITY: usize>
902    UpdateStateData<Types, State, ARITY> for Transaction<Write>
903{
904    async fn set_last_state_height(&mut self, height: usize) -> anyhow::Result<()> {
905        self.upsert(
906            "last_merklized_state_height",
907            ["id", "height"],
908            ["id"],
909            [(1i32, height as i64)],
910        )
911        .await?;
912
913        Ok(())
914    }
915
916    async fn insert_merkle_nodes(
917        &mut self,
918        proof: MerkleProof<State::Entry, State::Key, State::T, ARITY>,
919        traversal_path: Vec<usize>,
920        block_number: u64,
921    ) -> anyhow::Result<()> {
922        let proofs = vec![(proof, traversal_path)];
923        UpdateStateData::<Types, State, ARITY>::insert_merkle_nodes_batch(
924            self,
925            proofs,
926            block_number,
927        )
928        .await
929    }
930
931    async fn insert_merkle_nodes_batch(
932        &mut self,
933        proofs: Vec<(
934            MerkleProof<State::Entry, State::Key, State::T, ARITY>,
935            Vec<usize>,
936        )>,
937        block_number: u64,
938    ) -> anyhow::Result<()> {
939        if proofs.is_empty() {
940            return Ok(());
941        }
942
943        let name = State::state_type();
944        let block_number = block_number as i64;
945
946        let (mut all_nodes, all_hashes) = collect_nodes_from_proofs(&proofs)?;
947        let hashes: Vec<Vec<u8>> = all_hashes.into_iter().collect();
948
949        #[cfg(not(feature = "embedded-db"))]
950        let nodes_hash_ids: HashMap<Vec<u8>, i64> = batch_insert_hashes(hashes, self).await?;
951
952        #[cfg(feature = "embedded-db")]
953        let nodes_hash_ids: HashMap<Vec<u8>, i64> = {
954            let mut hash_ids: HashMap<Vec<u8>, i64> = HashMap::with_capacity(hashes.len());
955            for hash_chunk in hashes.chunks(20) {
956                let (query, sql) = build_hash_batch_insert(hash_chunk)?;
957                let chunk_ids: HashMap<Vec<u8>, i64> = query
958                    .query_as(&sql)
959                    .fetch(self.as_mut())
960                    .try_collect()
961                    .await?;
962                hash_ids.extend(chunk_ids);
963            }
964            hash_ids
965        };
966
967        for (node, children, hash) in &mut all_nodes {
968            node.created = block_number;
969            node.hash_id = *nodes_hash_ids.get(&*hash).ok_or(QueryError::Error {
970                message: "Missing node hash".to_string(),
971            })?;
972
973            if let Some(children) = children {
974                let children_hashes = children
975                    .iter()
976                    .map(|c| nodes_hash_ids.get(c).copied())
977                    .collect::<Option<Vec<i64>>>()
978                    .ok_or(QueryError::Error {
979                        message: "Missing child hash".to_string(),
980                    })?;
981
982                node.children = Some(children_hashes.into());
983            }
984        }
985
986        Node::upsert(name, all_nodes.into_iter().map(|(n, ..)| n), self).await?;
987
988        Ok(())
989    }
990}
991
992#[async_trait]
993impl<Mode: TransactionMode> PrunedHeightStorage for Transaction<Mode> {
994    async fn load_pruned_height(&mut self) -> anyhow::Result<Option<u64>> {
995        let Some((height,)) =
996            query_as::<(i64,)>("SELECT last_height FROM pruned_height WHERE id = $1 LIMIT 1")
997                .bind(Self::PRUNED_HEIGHT_ID)
998                .fetch_optional(self.as_mut())
999                .await
1000                .context("loading pruned height")?
1001        else {
1002            return Ok(None);
1003        };
1004        Ok(Some(height as u64))
1005    }
1006
1007    async fn load_state_pruned_height(&mut self) -> anyhow::Result<Option<u64>> {
1008        let Some((height,)) =
1009            query_as::<(i64,)>("SELECT last_height FROM pruned_height WHERE id = $1 LIMIT 1")
1010                .bind(Self::STATE_PRUNED_HEIGHT_ID)
1011                .fetch_optional(self.as_mut())
1012                .await
1013                .context("loading state pruned height")?
1014        else {
1015            return Ok(None);
1016        };
1017        Ok(Some(height as u64))
1018    }
1019}
1020
1021#[derive(Clone, Debug)]
1022pub(super) struct PoolMetrics {
1023    open_transactions: Box<dyn Gauge>,
1024    transaction_durations: Box<dyn Histogram>,
1025    commits: Box<dyn Counter>,
1026    reverts: Box<dyn Counter>,
1027    drops: Box<dyn Counter>,
1028}
1029
1030impl PoolMetrics {
1031    pub(super) fn new(metrics: &(impl Metrics + ?Sized)) -> Self {
1032        Self {
1033            open_transactions: metrics.create_gauge("open_transactions".into(), None),
1034            transaction_durations: metrics
1035                .create_histogram("transaction_duration".into(), Some("s".into())),
1036            commits: metrics.create_counter("committed_transactions".into(), None),
1037            reverts: metrics.create_counter("reverted_transactions".into(), None),
1038            drops: metrics.create_counter("dropped_transactions".into(), None),
1039        }
1040    }
1041}
1042
1043#[cfg(test)]
1044mod test {
1045    use super::*;
1046    use crate::data_source::{
1047        Transaction as _, VersionedDataSource,
1048        sql::testing::TmpDb,
1049        storage::{SqlStorage, StorageConnectionType},
1050    };
1051
1052    #[tokio::test]
1053    #[test_log::test]
1054    async fn test_upsert_many_rows() {
1055        let db = TmpDb::init().await;
1056        let storage = SqlStorage::connect(db.config(), StorageConnectionType::Sequencer)
1057            .await
1058            .unwrap();
1059
1060        let mut tx = storage.write().await.unwrap();
1061        query(
1062            "CREATE TABLE test (
1063                a INT PRIMARY KEY,
1064                b INT,
1065                c INT
1066            )",
1067        )
1068        .execute(tx.as_mut())
1069        .await
1070        .unwrap();
1071        tx.commit().await.unwrap();
1072
1073        // use a non-integer number of chunks.
1074        let n = (2 * Transaction::STATEMENT_MAX_PARAMETERS
1075            + (Transaction::STATEMENT_MAX_PARAMETERS / 2)) as i32;
1076        let rows = (0..n).map(|i| (i, i, i)).collect::<Vec<_>>();
1077
1078        let mut tx = storage.write().await.unwrap();
1079        tx.upsert("test", ["a", "b", "c"], ["a"], rows.clone())
1080            .await
1081            .unwrap();
1082        tx.commit().await.unwrap();
1083
1084        let mut tx = storage.read().await.unwrap();
1085        assert_eq!(
1086            rows,
1087            query_as("SELECT * FROM test ORDER BY a")
1088                .fetch_all(tx.as_mut())
1089                .await
1090                .unwrap()
1091        );
1092    }
1093}