Skip to main content

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}