Skip to main content

rainier_database/
raw.rs

1//! Raw SQL, bound safely — [`RawQuery`].
2//!
3//! ```ignore
4//! let stale = database
5//!     .query("DELETE FROM sessions WHERE last_seen_at < ?")
6//!     .bind(cutoff)
7//!     .execute()
8//!     .await?;
9//!
10//! let names: Vec<String> = database
11//!     .query("SELECT name FROM users WHERE team_id = ? ORDER BY name")
12//!     .bind(team_id)
13//!     .route_by(team_id)
14//!     .column("name")
15//!     .await?;
16//! ```
17//!
18//! Every ORM needs this door: a
19//! recursive CTE, a window function, a `LATERAL` join, an `EXPLAIN`, a
20//! migration's one-off backfill. Without it the escape hatch is
21//! [`Database::statement`], which takes no bindings at all — and a query that
22//! needs a value with no way to bind one is a query somebody is about to build
23//! with `format!`.
24//!
25//! # Placeholders are always `?`
26//!
27//! MySQL and SQLite spell a placeholder `?`; Postgres spells it `$1`, `$2`.
28//! Writing SQL that runs on all three otherwise means writing it twice, which
29//! is the exact madness a DBAL exists to absorb. So `?` is the spelling here
30//! and it is rewritten to `$n` for Postgres, in order, skipping anything
31//! inside a string literal or a quoted identifier.
32//!
33//! The one place that bites is Postgres's JSON `?` operator (`data ? 'key'`)
34//! and its `??`/`?|`/`?&` relatives — genuinely `?` characters that are not
35//! placeholders. Reach for [`raw_placeholders`](RawQuery::raw_placeholders)
36//! there and write `$1` yourself.
37//!
38//! # This is the unsafe door, and it is unsafe in one specific way
39//!
40//! **Values** are always bound — that is what [`bind`](RawQuery::bind) is for,
41//! and a bound value can never be SQL. The **statement** is not: it is
42//! whatever string you passed. Building that string out of anything a request
43//! supplied is an injection, and no amount of binding downstream repairs it.
44//! A table or column name that has to vary belongs in a `match` over a closed
45//! set, never in a `format!`.
46
47use rainier_orm::sea_query::Value;
48use rainier_orm::{Dialect, Entity, ExecOutcome, ShardRoute};
49use rainier_support::{Error, Result};
50
51use crate::connection::Database;
52use crate::row::{ColumnRequest, OwnedRow};
53use crate::statement::{routing_key, Prepared};
54
55/// A statement being built: the SQL, its bindings, and where to run it.
56///
57/// Nothing happens until one of the terminal methods is awaited.
58#[derive(Debug)]
59pub struct RawQuery<'a> {
60    database: &'a Database,
61    sql: String,
62    params: Vec<Value>,
63    route: ShardRoute,
64    rewrite_placeholders: bool,
65}
66
67impl<'a> RawQuery<'a> {
68    /// Start from `sql`. Prefer [`Database::query`].
69    pub fn new(database: &'a Database, sql: impl Into<String>) -> Self {
70        Self {
71            database,
72            sql: sql.into(),
73            params: Vec::new(),
74            route: ShardRoute::Global,
75            rewrite_placeholders: true,
76        }
77    }
78
79    /// Bind the next `?`.
80    ///
81    /// Order matters and nothing checks the count until the driver does — an
82    /// over- or under-bound statement is an error from the database, not from
83    /// here, because only it knows how many placeholders it found.
84    #[must_use = "this returns the query rather than modifying it in place"]
85    pub fn bind(mut self, value: impl Into<Value>) -> Self {
86        self.params.push(value.into());
87        self
88    }
89
90    /// Bind several, in order.
91    #[must_use = "this returns the query rather than modifying it in place"]
92    pub fn bind_all(mut self, values: impl IntoIterator<Item = impl Into<Value>>) -> Self {
93        self.params.extend(values.into_iter().map(Into::into));
94        self
95    }
96
97    /// Send this to the shard that owns `key`.
98    ///
99    /// Only meaningful on a sharded connection; ignored by everything else.
100    /// Without it a query goes to the global route, which for a sharded
101    /// deployment means "the one that is not shard-specific" — so a query
102    /// touching a sharded table **must** say which shard, or it will look in
103    /// the wrong place and quietly find nothing.
104    ///
105    /// Takes the same values the ORM routes by: a shard-encoded id as-is, a
106    /// string key hashed the same stable way, so the same key lands on the
107    /// same shard from any process.
108    #[must_use = "this returns the query rather than modifying it in place"]
109    pub fn route_by(mut self, key: impl Into<Value>) -> Self {
110        let key = key.into();
111        self.route = match routing_key(&key) {
112            Some(key) => ShardRoute::Key(key),
113            // A value with no routing meaning — a float, a null. Global is the
114            // honest answer rather than a hash of something arbitrary.
115            None => ShardRoute::Global,
116        };
117        self
118    }
119
120    /// Send this to the shard holding `key`, already resolved.
121    #[must_use = "this returns the query rather than modifying it in place"]
122    pub fn on_shard_key(mut self, key: u64) -> Self {
123        self.route = ShardRoute::Key(key);
124        self
125    }
126
127    /// Leave the placeholders exactly as written.
128    ///
129    /// For Postgres's JSON `?` operators, or for SQL that is already written
130    /// in `$n` form. The statement is then dialect-specific, which is the
131    /// trade.
132    #[must_use = "this returns the query rather than modifying it in place"]
133    pub fn raw_placeholders(mut self) -> Self {
134        self.rewrite_placeholders = false;
135        self
136    }
137
138    /// The statement as it will be sent — SQL, bindings and route.
139    ///
140    /// For logging it, asserting on it in a test, or handing it to
141    /// [`Database::fetch`] directly.
142    pub fn prepared(self) -> Prepared {
143        let sql = if self.rewrite_placeholders && self.database.dialect() == Dialect::Postgres {
144            to_numbered_placeholders(&self.sql)
145        } else {
146            self.sql
147        };
148
149        Prepared { sql, params: self.params, route: self.route }
150    }
151
152    // --- running it --------------------------------------------------------
153
154    /// Run it as a write, and report what happened.
155    pub async fn execute(self) -> Result<ExecOutcome> {
156        let database = self.database;
157        database.execute(self.prepared()).await
158    }
159
160    /// Run it and read back the named columns.
161    pub async fn fetch(self, columns: Vec<ColumnRequest>) -> Result<Vec<OwnedRow>> {
162        let database = self.database;
163        database.fetch(self.prepared(), columns).await
164    }
165
166    /// Run it and decode every row into `E`.
167    ///
168    /// The statement has to return `E`'s columns by their own names —
169    /// `SELECT *`, or an explicit list with matching aliases.
170    pub async fn fetch_all<E: Entity>(self) -> Result<Vec<E>> {
171        let database = self.database;
172        database.fetch_all::<E>(self.prepared()).await
173    }
174
175    /// Run it and decode the first row into `E`.
176    pub async fn fetch_one<E: Entity>(self) -> Result<Option<E>> {
177        let database = self.database;
178        database.fetch_one::<E>(self.prepared()).await
179    }
180
181    /// Read one integer out of the first row — a `COUNT`, a `SUM`, a `MAX`.
182    ///
183    /// `None` when there was no row, or the value was `NULL`. Those are
184    /// different from `0`, which is why this does not flatten them: `SUM` over
185    /// no rows is `NULL`, not zero, and rounding that to zero is how a total
186    /// silently becomes wrong.
187    pub async fn scalar_i64(self, column: &str) -> Result<Option<i64>> {
188        let requested = vec![ColumnRequest::new(column, rainier_orm::ColumnType::BigInt)];
189        let rows = self.fetch(requested).await?;
190
191        match rows.first() {
192            Some(row) => Ok(rainier_orm::Row::get_i64(row, column).map_err(Error::from)?),
193            None => Ok(None),
194        }
195    }
196
197    /// Read one text column out of the first row.
198    pub async fn scalar_string(self, column: &str) -> Result<Option<String>> {
199        let requested = vec![ColumnRequest::new(column, rainier_orm::ColumnType::Text)];
200        let rows = self.fetch(requested).await?;
201
202        match rows.first() {
203            Some(row) => Ok(rainier_orm::Row::get_string(row, column).map_err(Error::from)?),
204            None => Ok(None),
205        }
206    }
207
208    /// Read one text column out of every row.
209    ///
210    /// A `NULL` is skipped rather than becoming an empty string — the two mean
211    /// different things and only one of them is a name.
212    pub async fn column(self, column: &str) -> Result<Vec<String>> {
213        let requested = vec![ColumnRequest::new(column, rainier_orm::ColumnType::Text)];
214        let rows = self.fetch(requested).await?;
215
216        rows.iter()
217            .map(|row| rainier_orm::Row::get_string(row, column).map_err(Error::from))
218            .filter_map(Result::transpose)
219            .collect()
220    }
221}
222
223/// Rewrite `?` placeholders as `$1`, `$2`, … for Postgres.
224///
225/// A `?` inside `'a string'`, `"a quoted identifier"` or a `$$dollar quote$$`
226/// is left alone — it is data or a name, not a placeholder. Doubled quotes
227/// inside a literal (`'it''s'`) are handled by the state machine falling out
228/// and straight back in, which lands in the right place.
229fn to_numbered_placeholders(sql: &str) -> String {
230    let mut out = String::with_capacity(sql.len() + 8);
231    let mut next = 1;
232    let mut quote: Option<char> = None;
233
234    for c in sql.chars() {
235        match quote {
236            Some(open) => {
237                out.push(c);
238                if c == open {
239                    quote = None;
240                }
241            }
242            None => match c {
243                '\'' | '"' | '`' => {
244                    quote = Some(c);
245                    out.push(c);
246                }
247                '?' => {
248                    out.push('$');
249                    out.push_str(&next.to_string());
250                    next += 1;
251                }
252                _ => out.push(c),
253            },
254        }
255    }
256
257    out
258}
259
260impl Database {
261    /// Start a raw statement.
262    ///
263    /// ```ignore
264    /// database.query("UPDATE posts SET views = views + 1 WHERE id = ?")
265    ///     .bind(post_id)
266    ///     .execute()
267    ///     .await?;
268    /// ```
269    ///
270    /// See the [module docs](crate::raw) for what is and is not safe to put in
271    /// the statement itself.
272    pub fn query(&self, sql: impl Into<String>) -> RawQuery<'_> {
273        RawQuery::new(self, sql)
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::testing::MemoryConnection;
281
282    fn database(dialect: Dialect) -> Database {
283        Database::new(MemoryConnection::new(dialect))
284    }
285
286    #[test]
287    fn sqlite_and_mysql_keep_question_marks() {
288        for dialect in [Dialect::Sqlite, Dialect::MySql] {
289            let prepared = database(dialect)
290                .query("SELECT * FROM users WHERE team_id = ? AND name = ?")
291                .bind(7)
292                .bind("Ada")
293                .prepared();
294
295            assert_eq!(prepared.sql, "SELECT * FROM users WHERE team_id = ? AND name = ?");
296            assert_eq!(prepared.params.len(), 2);
297        }
298    }
299
300    #[test]
301    fn postgres_gets_numbered_placeholders() {
302        let prepared = database(Dialect::Postgres)
303            .query("SELECT * FROM users WHERE team_id = ? AND name = ?")
304            .bind(7)
305            .bind("Ada")
306            .prepared();
307
308        assert_eq!(prepared.sql, "SELECT * FROM users WHERE team_id = $1 AND name = $2");
309    }
310
311    #[test]
312    fn a_question_mark_inside_a_literal_is_not_a_placeholder() {
313        // Otherwise `'why?'` becomes `'why$1'` and the query returns nothing,
314        // which is the kind of bug that survives review.
315        let prepared = database(Dialect::Postgres)
316            .query("SELECT * FROM posts WHERE title = 'why?' AND author_id = ?")
317            .bind(1)
318            .prepared();
319
320        assert_eq!(prepared.sql, "SELECT * FROM posts WHERE title = 'why?' AND author_id = $1");
321    }
322
323    #[test]
324    fn a_question_mark_inside_a_quoted_identifier_is_left_alone() {
325        let prepared = database(Dialect::Postgres)
326            .query(r#"SELECT "odd?column" FROM t WHERE id = ?"#)
327            .prepared();
328
329        assert_eq!(prepared.sql, r#"SELECT "odd?column" FROM t WHERE id = $1"#);
330    }
331
332    #[test]
333    fn raw_placeholders_leaves_postgres_alone() {
334        // For `data ? 'key'`, where the `?` really is an operator.
335        let sql = "SELECT * FROM events WHERE payload ? 'user_id' AND id = $1";
336        let prepared = database(Dialect::Postgres).query(sql).bind(1).raw_placeholders().prepared();
337
338        assert_eq!(prepared.sql, sql);
339    }
340
341    #[test]
342    fn a_query_is_global_until_it_is_routed() {
343        let prepared = database(Dialect::Sqlite).query("SELECT 1").prepared();
344        assert_eq!(prepared.route, ShardRoute::Global);
345
346        let routed = database(Dialect::Sqlite).query("SELECT 1").route_by(4096u64).prepared();
347        assert_eq!(routed.route, ShardRoute::Key(4096));
348    }
349
350    #[test]
351    fn a_string_key_routes_the_same_way_the_orm_routes_it() {
352        let routed = database(Dialect::Sqlite).query("SELECT 1").route_by("tenant-a").prepared();
353
354        assert_eq!(routed.route, ShardRoute::Key(rainier_orm::stable_hash(b"tenant-a")));
355    }
356
357    #[test]
358    fn a_key_with_no_routing_meaning_stays_global() {
359        // Better than hashing something arbitrary and landing on a shard that
360        // has nothing to do with the row.
361        let routed = database(Dialect::Sqlite).query("SELECT 1").route_by(1.5f64).prepared();
362
363        assert_eq!(routed.route, ShardRoute::Global);
364    }
365
366    #[test]
367    fn bind_all_keeps_its_order() {
368        let prepared = database(Dialect::Sqlite)
369            .query("SELECT * FROM users WHERE id IN (?, ?, ?)")
370            .bind_all([1, 2, 3])
371            .prepared();
372
373        assert_eq!(prepared.params.len(), 3);
374        assert_eq!(prepared.params[0], Value::Int(Some(1)));
375        assert_eq!(prepared.params[2], Value::Int(Some(3)));
376    }
377}