1use 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#[cfg(not(feature = "embedded-db"))]
80static NO_DEFERRABLE_ON_READ: std::sync::atomic::AtomicBool =
81 std::sync::atomic::AtomicBool::new(true);
82
83#[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#[derive(Clone, Copy, Debug, Default)]
110pub struct Write;
111
112#[derive(Clone, Copy, Debug, Default)]
114pub struct Read;
115
116#[derive(Clone, Copy, Debug, Default)]
121pub struct Prune;
122
123pub 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 #[cfg(feature = "embedded-db")]
164 conn.execute("UPDATE pruned_height SET id = id WHERE false")
165 .await?;
166
167 #[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 #[cfg(feature = "embedded-db")]
186 conn.execute("UPDATE pruned_height SET id = id WHERE false")
187 .await?;
188
189 #[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#[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 #[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 #[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#[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
349pub 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
387pub 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
458impl Transaction<Write> {
460 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 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
533impl Transaction<Prune> {
535 #[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 #[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
634impl Transaction<Write> {
636 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 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 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 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 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 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 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 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 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 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 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 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}