sui_graphql_rpc/
raw_query.rs

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