iota_graphql_rpc/
raw_query.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use diesel::{
6    query_builder::{BoxedSqlQuery, SqlQuery},
7    sql_query,
8};
9
10use crate::data::DieselBackend;
11
12pub(crate) type RawSqlQuery = BoxedSqlQuery<'static, DieselBackend, SqlQuery>;
13
14/// `RawQuery` is a utility for building and managing
15/// `diesel::query_builder::BoxedSqlQuery` queries dynamically.
16///
17/// 1. **Dynamic Value Binding**: Allows binding string values dynamically to
18///    the query, bypassing the need to specify types explicitly, as is
19///    typically required with Diesel's `sql_query.bind`.
20///
21/// 2. **Query String Merging**: Can be used to represent and merge query
22///    strings and their associated bindings. Placeholder strings and bindings
23///    are applied in sequential order.
24///
25/// Note: `RawQuery` only supports binding string values, as interpolating raw
26/// strings directly increases exposure to SQL injection attacks.
27#[derive(Clone)]
28pub(crate) struct RawQuery {
29    /// The `SELECT` and `FROM` clauses of the query.
30    select: String,
31    /// The `WHERE` clause of the query.
32    where_: Option<String>,
33    /// The `ORDER BY` clause of the query.
34    order_by: Vec<String>,
35    /// The `GROUP BY` clause of the query.
36    group_by: Vec<String>,
37    /// The `LIMIT` clause of the query.
38    limit: Option<i64>,
39    /// The list of string binds for this query.
40    binds: Vec<String>,
41}
42
43impl RawQuery {
44    /// Constructs a new `RawQuery` with the given `SELECT` clause and binds.
45    pub(crate) fn new(select: impl Into<String>, binds: Vec<String>) -> Self {
46        Self {
47            select: select.into(),
48            where_: None,
49            order_by: Vec::new(),
50            group_by: Vec::new(),
51            limit: None,
52            binds,
53        }
54    }
55
56    /// Adds a `WHERE` condition to the query, combining it with existing
57    /// conditions using `AND`.
58    pub(crate) fn filter<T: std::fmt::Display>(mut self, condition: T) -> Self {
59        self.where_ = match self.where_ {
60            Some(where_) => Some(format!("({}) AND {}", where_, condition)),
61            None => Some(condition.to_string()),
62        };
63
64        self
65    }
66
67    /// Adds a `WHERE` condition to the query, combining it with existing
68    /// conditions using `OR`.
69    pub(crate) fn or_filter<T: std::fmt::Display>(mut self, condition: T) -> Self {
70        self.where_ = match self.where_ {
71            Some(where_) => Some(format!("({}) OR {}", where_, condition)),
72            None => Some(condition.to_string()),
73        };
74
75        self
76    }
77
78    /// Adds an `ORDER BY` clause to the query.
79    pub(crate) fn order_by<T: ToString>(mut self, order: T) -> Self {
80        self.order_by.push(order.to_string());
81        self
82    }
83
84    /// Adds a `GROUP BY` clause to the query.
85    pub(crate) fn group_by<T: ToString>(mut self, group: T) -> Self {
86        self.group_by.push(group.to_string());
87        self
88    }
89
90    /// Adds a `LIMIT` clause to the query.
91    pub(crate) fn limit(mut self, limit: i64) -> Self {
92        self.limit = Some(limit);
93        self
94    }
95
96    /// Adds the `String` value to the list of binds for this query.
97    pub(crate) fn bind_value(&mut self, condition: String) {
98        self.binds.push(condition);
99    }
100
101    /// Constructs the query string and returns it along with the list of binds
102    /// for this query. This function is not intended to be called directly,
103    /// and instead should be used through the `query!` macro.
104    pub(crate) fn finish(self) -> (String, Vec<String>) {
105        let mut select = self.select;
106
107        if let Some(where_) = self.where_ {
108            select.push_str(" WHERE ");
109            select.push_str(&where_);
110        }
111
112        let mut prefix = " GROUP BY ";
113        for group in self.group_by.iter() {
114            select.push_str(prefix);
115            select.push_str(group);
116            prefix = ", ";
117        }
118
119        let mut prefix = " ORDER BY ";
120        for order in self.order_by.iter() {
121            select.push_str(prefix);
122            select.push_str(order);
123            prefix = ", ";
124        }
125
126        if let Some(limit) = self.limit {
127            select.push_str(" LIMIT ");
128            select.push_str(&limit.to_string());
129        }
130
131        (select, self.binds)
132    }
133
134    /// Converts this `RawQuery` into a `diesel::query_builder::BoxedSqlQuery`.
135    /// Consumes `self` into a raw sql string and bindings, if any. A
136    /// `BoxedSqlQuery` is constructed from the raw sql string, and bindings
137    /// are added using `sql_query.bind()`.
138    pub(crate) fn into_boxed(self) -> RawSqlQuery {
139        let (raw_sql_string, binds) = self.finish();
140
141        let mut result = String::with_capacity(raw_sql_string.len());
142
143        let mut sql_components = raw_sql_string.split("{}").enumerate();
144
145        if let Some((_, first)) = sql_components.next() {
146            result.push_str(first);
147        }
148
149        for (i, sql) in sql_components {
150            result.push_str(&format!("${}", i));
151            result.push_str(sql);
152        }
153
154        let mut diesel_query = sql_query(result).into_boxed();
155
156        for bind in binds {
157            diesel_query = diesel_query.bind::<diesel::sql_types::Text, _>(bind);
158        }
159
160        diesel_query
161    }
162}
163
164/// Applies the `AND` condition to the given `RawQuery` and binds input string
165/// values, if any.
166#[macro_export]
167macro_rules! filter {
168    ($query:expr, $condition:expr $(,$binds:expr)*) => {{
169        let mut query = $query;
170        query = query.filter($condition);
171        $(query.bind_value($binds.to_string());)*
172        query
173    }};
174}
175
176/// Applies the `OR` condition to the given `RawQuery` and binds input string
177/// values, if any.
178#[macro_export]
179macro_rules! or_filter {
180    ($query:expr, $condition:expr $(,$binds:expr)*) => {{
181        let mut query = $query;
182        query = query.or_filter($condition);
183        $(query.bind_value($binds.to_string());)*
184        query
185    }};
186}
187
188/// Accepts two `RawQuery` instances and a third expression consisting of which
189/// columns to join on.
190#[macro_export]
191macro_rules! inner_join {
192    ($lhs:expr, $alias:expr => $rhs_query:expr, using: [$using:expr $(, $more_using:expr)*]) => {{
193        use $crate::raw_query::RawQuery;
194
195        let (lhs_sql, mut binds) = $lhs.finish();
196        let (rhs_sql, rhs_binds) = $rhs_query.finish();
197
198        binds.extend(rhs_binds);
199
200        let sql = format!(
201            "{lhs_sql} INNER JOIN ({rhs_sql}) AS {} USING ({})",
202            $alias,
203            stringify!($using $(, $more_using)*),
204        );
205
206        RawQuery::new(sql, binds)
207    }};
208}
209
210/// Accepts a `SELECT FROM` format string and optional subqueries. If subqueries
211/// are provided, there should be curly braces `{}` in the format string to
212/// interpolate each subquery's sql string into. Concatenates subqueries to the
213/// `SELECT FROM` clause, and creates a new `RawQuery` from the concatenated sql
214/// string. The binds from each subquery are added in the order they appear in
215/// the macro parameter. Subqueries are consumed into the new `RawQuery`.
216#[macro_export]
217macro_rules! query {
218    // Matches the case where no subqueries are provided. A `RawQuery` is constructed from the given
219    // select clause.
220    ($select:expr) => {
221        $crate::raw_query::RawQuery::new($select, vec![])
222    };
223
224    // Expects a select clause and one or more subqueries. The select clause should contain curly
225    // braces for subqueries to be interpolated into. Use when the subqueries can be aliased
226    // directly in the select statement.
227    ($select:expr $(,$subquery:expr)+) => {{
228        use $crate::raw_query::RawQuery;
229        let mut binds = vec![];
230
231        let select = format!(
232            $select,
233            $({
234                let (sub_sql, sub_binds) = $subquery.finish();
235                binds.extend(sub_binds);
236                sub_sql
237            }),*
238        );
239
240        RawQuery::new(select, binds)
241    }};
242}