Skip to main content

musq/
expr.rs

1//! Helpers for building safe SQL expressions for use with `Values`.
2//!
3//! Expressions are SQL fragments that can be embedded into `{set:...}`, `{insert:...}`, and
4//! `{where:...}` placeholders. Unlike regular bound values, expressions may expand to arbitrary SQL
5//! (optionally containing their own bind parameters).
6//!
7//! Expression fragments support anonymous `?` and named bind parameters. Numeric positional
8//! placeholders such as `?1` and numeric `$1` are rejected when an expression is embedded because
9//! their absolute SQLite indices cannot be safely rebased inside a larger statement.
10//!
11//! Use curated helpers (like [`crate::expr::now_rfc3339_utc`]) whenever possible. If you must embed
12//! ad-hoc SQL, use [`crate::expr::raw`], which will taint the resulting query.
13
14use either::Either;
15
16use crate::{Arguments, Query, Value, error::EncodeError};
17
18/// A SQL expression fragment with optional bound parameters.
19///
20/// This type is intentionally not `Encode`; it is meant to be embedded into composed SQL, not
21/// bound as a single SQLite value.
22#[derive(Debug, Clone)]
23pub struct Expr {
24    /// SQL for the expression fragment.
25    pub(crate) sql: String,
26    /// Arguments referenced by the expression fragment.
27    pub(crate) arguments: Arguments,
28    /// Whether the expression is tainted with raw SQL.
29    pub(crate) tainted: bool,
30}
31
32impl Expr {
33    /// Returns the SQL text for this expression.
34    pub fn sql(&self) -> &str {
35        &self.sql
36    }
37
38    /// Returns `true` if this expression includes raw SQL and should be treated as tainted.
39    pub fn tainted(&self) -> bool {
40        self.tainted
41    }
42}
43
44impl From<Query> for Expr {
45    fn from(query: Query) -> Self {
46        let sql = match query.statement {
47            Either::Left(sql) => sql,
48            Either::Right(statement) => statement.sql,
49        };
50        let arguments = query.arguments.unwrap_or_default();
51        Self {
52            sql,
53            arguments,
54            tainted: query.tainted,
55        }
56    }
57}
58
59/// SQLite expression for the current UTC time, formatted as RFC3339.
60///
61/// This is intended to match hb's documented storage format for timestamps.
62pub fn now_rfc3339_utc() -> Expr {
63    Expr {
64        sql: "STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')".to_string(),
65        arguments: Arguments::default(),
66        tainted: false,
67    }
68}
69
70/// Wrap a JSON text value with SQLite's `jsonb(...)` function.
71///
72/// The JSON text is bound as a parameter; SQLite will store it using its internal JSONB encoding.
73pub fn jsonb(json: &str) -> Expr {
74    let mut arguments = Arguments::default();
75    arguments.values.push(Value::Text {
76        value: json.to_string().into(),
77        type_info: None,
78    });
79    Expr {
80        sql: "jsonb(?)".to_string(),
81        arguments,
82        tainted: false,
83    }
84}
85
86/// Wrap a JSON text value with SQLite's `jsonb(...)` function.
87///
88/// Alias for [`jsonb`].
89pub fn jsonb_text(json: &str) -> Expr {
90    jsonb(json)
91}
92
93/// Serialize a value to JSON and wrap it with SQLite's `jsonb(...)` function.
94pub fn jsonb_serde<T>(value: &T) -> crate::Result<Expr>
95where
96    T: serde::Serialize + ?Sized,
97{
98    let json = serde_json::to_string(value).map_err(|e| {
99        crate::Error::Encode(EncodeError::Conversion(format!(
100            "failed to encode value as JSON: {e}"
101        )))
102    })?;
103
104    let mut arguments = Arguments::default();
105    arguments.values.push(Value::Text {
106        value: json.into(),
107        type_info: None,
108    });
109    Ok(Expr {
110        sql: "jsonb(?)".to_string(),
111        arguments,
112        tainted: false,
113    })
114}
115
116/// Embed raw SQL as an expression and taint the resulting query.
117pub fn raw(sql: &str) -> Expr {
118    Expr {
119        sql: sql.to_string(),
120        arguments: Arguments::default(),
121        tainted: true,
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn raw_expr_is_tainted() {
131        assert!(raw("1").tainted());
132    }
133
134    #[test]
135    fn now_expr_is_not_tainted() {
136        assert!(!now_rfc3339_utc().tainted());
137    }
138
139    #[test]
140    fn jsonb_serde_roundtrips() {
141        #[derive(serde::Serialize)]
142        struct Payload {
143            a: i32,
144        }
145
146        let expr = jsonb_serde(&Payload { a: 1 }).unwrap();
147        assert_eq!(expr.sql(), "jsonb(?)");
148        assert!(!expr.tainted());
149    }
150}