Skip to main content

runlight/store/
db.rs

1//! The little a store needs from a database driver.
2
3use std::sync::Arc;
4
5use crate::BoxFuture;
6use crate::js::{self, Value};
7
8/// The SQL a database speaks.
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum Dialect {
11    /// SQLite.
12    Sqlite,
13    /// Postgres.
14    Postgres,
15    /// MySQL 8.0.20 or MariaDB 11.4 and later.
16    Mysql,
17}
18
19/// A statement's parameter, in the types a JavaScript driver binds.
20#[derive(Clone, Debug, PartialEq)]
21pub enum Param {
22    /// NULL.
23    Null,
24    /// A whole number.
25    Int(i64),
26    /// A number with a fraction.
27    Float(f64),
28    /// Text.
29    Text(String),
30}
31
32impl From<i64> for Param {
33    fn from(n: i64) -> Param {
34        Param::Int(n)
35    }
36}
37impl From<i32> for Param {
38    fn from(n: i32) -> Param {
39        Param::Int(i64::from(n))
40    }
41}
42impl From<usize> for Param {
43    fn from(n: usize) -> Param {
44        Param::Int(n as i64)
45    }
46}
47impl From<f64> for Param {
48    /// A JavaScript number: whole ones are bound as integers, as the drivers bind them.
49    fn from(n: f64) -> Param {
50        if js::is_integer(n) && n.abs() < 9.007_199_254_740_992e15 { Param::Int(n as i64) } else { Param::Float(n) }
51    }
52}
53impl From<&str> for Param {
54    fn from(s: &str) -> Param {
55        Param::Text(s.to_string())
56    }
57}
58impl From<String> for Param {
59    fn from(s: String) -> Param {
60        Param::Text(s)
61    }
62}
63impl From<&String> for Param {
64    fn from(s: &String) -> Param {
65        Param::Text(s.clone())
66    }
67}
68impl<T: Into<Param>> From<Option<T>> for Param {
69    fn from(v: Option<T>) -> Param {
70        v.map_or(Param::Null, Into::into)
71    }
72}
73
74/// Builds a parameter list: `params![site, from, to]`.
75#[macro_export]
76macro_rules! params {
77    () => { Vec::<$crate::store::Param>::new() };
78    ($($v:expr),+ $(,)?) => { vec![$($crate::store::Param::from($v)),+] };
79}
80
81/// A column's value, as the driver gave it.
82#[derive(Clone, Debug, PartialEq)]
83pub enum Cell {
84    /// NULL.
85    Null,
86    /// A whole number.
87    Int(i64),
88    /// A number with a fraction.
89    Float(f64),
90    /// Text, as which some drivers give big numbers too.
91    Text(String),
92}
93
94impl Cell {
95    /// The value as JavaScript holds what its driver gave it.
96    pub fn to_value(&self) -> Value {
97        match self {
98            Cell::Null => Value::Null,
99            Cell::Int(n) => Value::Number(*n as f64),
100            Cell::Float(f) => Value::Number(*f),
101            Cell::Text(s) => Value::String(s.clone()),
102        }
103    }
104}
105
106/// One row, its columns by name.
107#[derive(Clone, Debug, Default, PartialEq)]
108pub struct Row(pub Vec<(String, Cell)>);
109
110static NULL: Cell = Cell::Null;
111
112impl Row {
113    /// A column, NULL when there is none by that name.
114    pub fn get(&self, name: &str) -> &Cell {
115        self.0.iter().find(|(n, _)| n == name).map_or(&NULL, |(_, c)| c)
116    }
117
118    /// Whether the column is NULL or missing.
119    pub fn is_null(&self, name: &str) -> bool {
120        matches!(self.get(name), Cell::Null)
121    }
122
123    /// `num(row[name])`: `Number(value ?? 0)`, and 0 for anything not finite.
124    pub fn num(&self, name: &str) -> f64 {
125        let n = match self.get(name) {
126            Cell::Null => 0.0,
127            Cell::Int(n) => *n as f64,
128            Cell::Float(f) => *f,
129            Cell::Text(t) => js::text_number(t),
130        };
131        if n.is_finite() { n } else { 0.0 }
132    }
133
134    /// [`Row::num`] as a whole number, for times and counts.
135    pub fn int(&self, name: &str) -> i64 {
136        js::to_i64(self.num(name))
137    }
138
139    /// `Number(row[name])`, NaN for NULL kept as `None`.
140    pub fn opt_int(&self, name: &str) -> Option<i64> {
141        if self.is_null(name) { None } else { Some(self.int(name)) }
142    }
143
144    /// `String(row[name])`: NULL is `"null"`.
145    pub fn text(&self, name: &str) -> String {
146        match self.get(name) {
147            Cell::Null => "null".into(),
148            Cell::Int(n) => n.to_string(),
149            Cell::Float(f) => js::format_number(*f),
150            Cell::Text(t) => t.clone(),
151        }
152    }
153
154    /// `String(row[name] ?? fallback)`.
155    pub fn text_or(&self, name: &str, fallback: &str) -> String {
156        if self.is_null(name) { fallback.to_string() } else { self.text(name) }
157    }
158
159    /// The row as JavaScript holds it, its columns in order.
160    pub fn to_value(&self) -> Value {
161        let mut o = js::Object::new();
162        for (name, cell) in &self.0 {
163            o.set(name.clone(), cell.to_value());
164        }
165        Value::Object(o)
166    }
167}
168
169/// A database that refused a statement, or could not be reached.
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct DbError(pub String);
172
173impl std::fmt::Display for DbError {
174    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
175        f.write_str(&self.0)
176    }
177}
178
179impl std::error::Error for DbError {}
180
181/// What a held connection is for.
182#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183pub enum Hold {
184    /// One transaction: committed by `finish(true)`, rolled back by `finish(false)`.
185    Transaction,
186    /// A database-wide lock, so two processes starting at once do not race to create the same
187    /// tables; let go by `finish`. Waits for the lock at most [`lock_wait`](super::lock_wait), then
188    /// fails with a [`TablesBusyError`](super::TablesBusyError).
189    Exclusive,
190}
191
192/// The little a store needs from a database driver. SQL uses `?` placeholders and "double quotes"
193/// around a name that is a keyword somewhere; a driver numbers the placeholders, or fills them in and
194/// quotes names its own way, as the TypeScript drivers do.
195pub trait Db: Send + Sync {
196    /// The SQL the database speaks.
197    fn dialect(&self) -> Dialect;
198
199    /// Runs a query and gives its rows.
200    fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>>;
201
202    /// Runs a statement and says how many rows it matched.
203    fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>>;
204
205    /// One connection, held for a transaction or a lock until `finish`. Statements on it run in
206    /// turn with nothing else of the store's in between. Holding a transaction on a connection
207    /// already in one joins it.
208    fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>>;
209
210    /// Ends what `hold` began: commits a transaction (`ok`) or rolls it back, or lets go of a lock.
211    /// A connection that was not held answers Ok.
212    fn finish(&self, _ok: bool) -> BoxFuture<'_, Result<(), DbError>> {
213        Box::pin(async { Ok(()) })
214    }
215
216    /// True for a database reached one statement at a time over the network with a cap on
217    /// statements per request (Cloudflare D1), so long jobs send fewer, larger pieces.
218    fn metered(&self) -> bool {
219        false
220    }
221
222    /// Lets the connections go.
223    fn close(&self) -> BoxFuture<'_, ()> {
224        Box::pin(async {})
225    }
226
227    /// The prefix this Db puts on Runlight's table and index names in place of `rl_`, when it puts one
228    /// ([`Prefixed`](super::Prefixed)), so a store over it keeps it.
229    fn prefix(&self) -> Option<&str> {
230        None
231    }
232}
233
234/// SQL written with `?` placeholders and "double quoted" names, with each placeholder outside quotes
235/// filled in by `fill`, as node-postgres and mysql2 send values in the statement's text. For MySQL,
236/// a "quoted" name goes in backticks and a backslash inside 'text' is doubled, since MySQL reads it as
237/// an escape where standard SQL takes it literally.
238pub fn fill_placeholders(
239    sql: &str,
240    params: &[Param],
241    mysql: bool,
242    fill: impl Fn(&Param) -> String,
243) -> Result<String, DbError> {
244    let mut out = String::with_capacity(sql.len() + params.len() * 8);
245    let mut n = 0;
246    let mut quote: Option<char> = None;
247    for ch in sql.chars() {
248        if let Some(q) = quote {
249            if ch == q {
250                quote = None;
251                out.push(if mysql && ch == '"' { '`' } else { ch });
252            } else if mysql && q == '\'' && ch == '\\' {
253                out.push_str("\\\\");
254            } else if mysql && q == '"' && ch == '`' {
255                out.push_str("``");
256            } else {
257                out.push(ch);
258            }
259        } else if ch == '\'' || ch == '"' || (mysql && ch == '`') {
260            quote = Some(ch);
261            out.push(if mysql && ch == '"' { '`' } else { ch });
262        } else if ch == '?' {
263            let p = params
264                .get(n)
265                .ok_or_else(|| DbError("Runlight: a statement has more placeholders than values".into()))?;
266            out.push_str(&fill(p));
267            n += 1;
268        } else {
269            out.push(ch);
270        }
271    }
272    if n != params.len() {
273        return Err(DbError("Runlight: a statement has more values than placeholders".into()));
274    }
275    Ok(out)
276}
277
278/// A value as a Postgres literal of unknown type, which the server reads as it reads a parameter
279/// node-postgres sends untyped: `E'...'` text (backslashes and quotes escaped, whatever
280/// standard_conforming_strings says), numbers too, and NULL.
281pub fn postgres_literal(p: &Param) -> String {
282    match p {
283        Param::Null => "NULL".into(),
284        Param::Int(n) => format!("'{n}'"),
285        Param::Float(f) => format!("'{}'", js::format_number(*f)),
286        Param::Text(s) => {
287            let mut out = String::with_capacity(s.len() + 3);
288            out.push_str("E'");
289            for c in s.chars() {
290                match c {
291                    '\\' => out.push_str("\\\\"),
292                    '\'' => out.push_str("''"),
293                    _ => out.push(c),
294                }
295            }
296            out.push('\'');
297            out
298        }
299    }
300}
301
302/// A value as a MySQL literal, as mysql2's escape() writes the values Runlight binds, except that a
303/// quote is doubled rather than escaped with a backslash, so a value stays inside its quotes even on a
304/// connection whose sql_mode has NO_BACKSLASH_ESCAPES.
305pub fn mysql_literal(p: &Param) -> String {
306    match p {
307        Param::Null => "NULL".into(),
308        Param::Int(n) => n.to_string(),
309        Param::Float(f) => js::format_number(*f),
310        Param::Text(s) => {
311            let mut out = String::with_capacity(s.len() + 2);
312            out.push('\'');
313            for c in s.chars() {
314                match c {
315                    '\0' => out.push_str("\\0"),
316                    '\x08' => out.push_str("\\b"),
317                    '\t' => out.push_str("\\t"),
318                    '\x1a' => out.push_str("\\Z"),
319                    '\n' => out.push_str("\\n"),
320                    '\r' => out.push_str("\\r"),
321                    '"' => out.push_str("\\\""),
322                    '\'' => out.push_str("''"),
323                    '\\' => out.push_str("\\\\"),
324                    _ => out.push(c),
325                }
326            }
327            out.push('\'');
328            out
329        }
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    #[test]
338    fn placeholders_are_filled_outside_quotes() {
339        let sql = r#"SELECT "key", 'a?b\c' FROM t WHERE x = ? AND y LIKE ? ESCAPE '\'"#;
340        let params = vec![Param::Int(3), Param::Text("o'k\\".into())];
341        assert_eq!(
342            fill_placeholders(sql, &params, true, mysql_literal).unwrap(),
343            r#"SELECT `key`, 'a?b\\c' FROM t WHERE x = 3 AND y LIKE 'o''k\\' ESCAPE '\\'"#
344        );
345        assert_eq!(
346            fill_placeholders(sql, &params, false, postgres_literal).unwrap(),
347            r#"SELECT "key", 'a?b\c' FROM t WHERE x = '3' AND y LIKE E'o''k\\' ESCAPE '\'"#
348        );
349        assert!(fill_placeholders("?", &[], false, postgres_literal).is_err());
350    }
351}