hotshot_query_service/data_source/storage/sql/queries.rs
1#![allow(clippy::needless_lifetimes)]
2// Copyright (c) 2022 Espresso Systems (espressosys.com)
3// This file is part of the HotShot Query Service library.
4//
5// This program is free software: you can redistribute it and/or modify it under the terms of the GNU
6// General Public License as published by the Free Software Foundation, either version 3 of the
7// License, or (at your option) any later version.
8// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
9// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
10// General Public License for more details.
11// You should have received a copy of the GNU General Public License along with this program. If not,
12// see <https://www.gnu.org/licenses/>.
13
14//! Immutable query functionality of a SQL database.
15
16use std::ops::{Bound, RangeBounds};
17
18use derivative::Derivative;
19pub(super) use hotshot_query_service_types::availability::sql::DecodeError;
20use hotshot_query_service_types::availability::sql::{
21 BLOCK_COLUMNS, HEADER_COLUMNS, LEAF_COLUMNS, PAYLOAD_COLUMNS, PAYLOAD_METADATA_COLUMNS,
22 VID_COMMON_COLUMNS, VID_COMMON_METADATA_COLUMNS, parse_header,
23};
24use hotshot_types::traits::node_implementation::NodeType;
25use sqlx::{Arguments, FromRow};
26
27use super::{Database, Db, Query, QueryAs, Transaction};
28use crate::{Header, QueryError, QueryResult, availability::BlockId};
29
30pub(super) mod availability;
31pub(super) mod explorer;
32pub(super) mod node;
33pub(super) mod state;
34
35/// Helper type for programmatically constructing queries.
36///
37/// This type can be used to bind arguments of various types, similar to [`Query`] or [`QueryAs`].
38/// With [`QueryBuilder`], though, the arguments are bound *first* and the SQL statement is given
39/// last. Each time an argument is bound, a SQL fragment is returned as a string which can be used
40/// to represent that argument in the statement (e.g. `$1` for the first argument bound). This makes
41/// it easier to programmatically construct queries where the statement is not a compile time
42/// constant.
43///
44/// # Example
45///
46/// ```
47/// # use hotshot_query_service::{
48/// # data_source::storage::sql::{
49/// # Database, Db, QueryBuilder, Transaction,
50/// # },
51/// # QueryResult,
52/// # };
53/// # use sqlx::FromRow;
54/// async fn search_and_maybe_filter<T, Mode>(
55/// tx: &mut Transaction<Mode>,
56/// id: Option<i64>,
57/// ) -> QueryResult<Vec<T>>
58/// where
59/// for<'r> T: FromRow<'r, <Db as Database>::Row> + Send + Unpin,
60/// {
61/// let mut query = QueryBuilder::default();
62/// let mut sql = "SELECT * FROM table".into();
63/// if let Some(id) = id {
64/// sql = format!("{sql} WHERE id = {}", query.bind(id)?);
65/// }
66/// let results = query
67/// .query_as(&sql)
68/// .fetch_all(tx.as_mut())
69/// .await?;
70/// Ok(results)
71/// }
72/// ```
73#[derive(Derivative, Default)]
74#[derivative(Debug)]
75pub struct QueryBuilder<'a> {
76 #[derivative(Debug = "ignore")]
77 arguments: <Db as Database>::Arguments<'a>,
78}
79
80impl<'q> QueryBuilder<'q> {
81 /// Add an argument and return its name as a formal parameter in a SQL prepared statement.
82 pub fn bind<T>(&mut self, arg: T) -> QueryResult<String>
83 where
84 T: 'q + sqlx::Encode<'q, Db> + sqlx::Type<Db>,
85 {
86 self.arguments.add(arg).map_err(|err| QueryError::Error {
87 message: format!("{err:#}"),
88 })?;
89
90 Ok(format!("${}", self.arguments.len()))
91 }
92
93 /// Finalize the query with a constructed SQL statement.
94 pub fn query(self, sql: &'q str) -> Query<'q> {
95 sqlx::query_with(sql, self.arguments)
96 }
97
98 /// Finalize the query with a constructed SQL statement and a specified output type.
99 pub fn query_as<T>(self, sql: &'q str) -> QueryAs<'q, T>
100 where
101 T: for<'r> FromRow<'r, <Db as Database>::Row>,
102 {
103 sqlx::query_as_with(sql, self.arguments)
104 }
105}
106
107impl QueryBuilder<'_> {
108 /// Construct a SQL `WHERE` clause which filters for a header exactly matching `id`.
109 pub fn header_where_clause<Types: NodeType>(
110 &mut self,
111 id: BlockId<Types>,
112 ) -> QueryResult<String> {
113 let clause = match id {
114 BlockId::Number(n) => format!("h.height = {}", self.bind(n as i64)?),
115 BlockId::Hash(h) => format!("h.hash = {}", self.bind(h.to_string())?),
116 BlockId::PayloadHash(h) => format!("h.payload_hash = {}", self.bind(h.to_string())?),
117 };
118 Ok(clause)
119 }
120
121 /// Convert range bounds to a SQL `WHERE` clause constraining a given column.
122 pub fn bounds_to_where_clause<R>(&mut self, range: R, column: &str) -> QueryResult<String>
123 where
124 R: RangeBounds<usize>,
125 {
126 let mut bounds = vec![];
127
128 match range.start_bound() {
129 Bound::Included(n) => {
130 bounds.push(format!("{column} >= {}", self.bind(*n as i64)?));
131 },
132 Bound::Excluded(n) => {
133 bounds.push(format!("{column} > {}", self.bind(*n as i64)?));
134 },
135 Bound::Unbounded => {},
136 }
137 match range.end_bound() {
138 Bound::Included(n) => {
139 bounds.push(format!("{column} <= {}", self.bind(*n as i64)?));
140 },
141 Bound::Excluded(n) => {
142 bounds.push(format!("{column} < {}", self.bind(*n as i64)?));
143 },
144 Bound::Unbounded => {},
145 }
146
147 let mut where_clause = bounds.join(" AND ");
148 if !where_clause.is_empty() {
149 where_clause = format!(" WHERE {where_clause}");
150 }
151
152 Ok(where_clause)
153 }
154}
155
156impl<Mode> Transaction<Mode> {
157 /// Load a header from storage.
158 ///
159 /// This function is similar to `AvailabilityStorage::get_header`, but
160 /// * does not require the `QueryablePayload<Types>` bound that that trait impl does
161 /// * makes it easier to specify types since the type parameter is on the function and not on a
162 /// trait impl
163 /// * allows type conversions for the `id` parameter
164 ///
165 /// This more ergonomic interface is useful as loading headers is important for many SQL storage
166 /// functions, not just the `AvailabilityStorage` interface.
167 pub async fn load_header<Types: NodeType>(
168 &mut self,
169 id: impl Into<BlockId<Types>> + Send,
170 ) -> QueryResult<Header<Types>> {
171 let mut query = QueryBuilder::default();
172 let where_clause = query.header_where_clause(id.into())?;
173 let sql = format!(
174 "SELECT {HEADER_COLUMNS}
175 FROM header AS h
176 WHERE {where_clause}
177 LIMIT 1"
178 );
179
180 let row = query.query(&sql).fetch_one(self.as_mut()).await?;
181 let header = parse_header::<Types, <Db as Database>::Row>(row)?;
182
183 Ok(header)
184 }
185}