pgdo/cluster/
config.rs

1use std::{borrow::Cow, fmt, str::FromStr};
2
3use postgres_protocol::escape::{escape_identifier, escape_literal};
4
5use super::sqlx;
6
7trait AsSql {
8    fn as_sql(&self) -> Cow<'_, str>;
9}
10
11/// Reload configuration using `pg_reload_conf`. Equivalent to `SIGHUP` or
12/// `pg_ctl reload`.
13pub async fn reload(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
14    sqlx::query("SELECT pg_reload_conf()").execute(pool).await?;
15    Ok(())
16}
17
18pub enum AlterSystem<'a> {
19    Set(&'a Parameter<'a>, &'a Value),
20    Reset(&'a Parameter<'a>),
21    ResetAll,
22}
23
24impl AlterSystem<'_> {
25    /// Alter the system. Changes made by `ALTER SYSTEM` may require a reload or
26    /// even a full restart to take effect.
27    pub async fn apply(&self, pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
28        sqlx::query(&self.as_sql()).execute(pool).await?;
29        Ok(())
30    }
31}
32
33impl AsSql for AlterSystem<'_> {
34    /// Return the SQL to apply this change.
35    fn as_sql(&self) -> Cow<'_, str> {
36        use AlterSystem::*;
37        match self {
38            Set(p, v) => format!("ALTER SYSTEM SET {} TO {}", p.as_sql(), v.as_sql()).into(),
39            Reset(p) => format!("ALTER SYSTEM RESET {}", p.as_sql()).into(),
40            ResetAll => "ALTER SYSTEM RESET ALL".into(),
41        }
42    }
43}
44
45/// A setting as defined in `pg_catalog.pg_settings`.
46///
47/// This is fairly stringly-typed and mostly informational. For getting and
48/// setting values, [`Parameter`] and [`Value`] may be more convenient.
49///
50/// **Note** that this does not work on PostgreSQL 9.4 and earlier because the
51/// `pending_restart` column does not exist. PostgreSQL 9.4 has long been
52/// obsolete so a workaround is not provided.
53///
54/// See the [documentation for
55/// `pg_settings`](https://www.postgresql.org/docs/current/view-pg-settings.html).
56#[derive(Debug, Clone, sqlx::FromRow)]
57pub struct Setting {
58    pub name: String,
59    pub setting: String,
60    pub unit: Option<String>,
61    pub category: String,
62    pub short_desc: String,
63    pub extra_desc: Option<String>,
64    pub context: String,
65    pub vartype: String,
66    pub source: String,
67    pub min_val: Option<String>,
68    pub max_val: Option<String>,
69    pub enumvals: Option<Vec<String>>,
70    pub boot_val: Option<String>,
71    pub reset_val: Option<String>,
72    pub sourcefile: Option<String>,
73    pub sourceline: Option<i32>,
74    pub pending_restart: bool,
75}
76
77impl Setting {
78    pub async fn list(pool: &sqlx::PgPool) -> Result<Vec<Self>, sqlx::Error> {
79        sqlx::query_as(
80            r"
81            SELECT
82                name,
83                setting,
84                unit,
85                category,
86                short_desc,
87                extra_desc,
88                context,
89                vartype,
90                source,
91                min_val,
92                max_val,
93                enumvals,
94                boot_val,
95                reset_val,
96                sourcefile,
97                sourceline,
98                pending_restart
99            FROM
100                pg_catalog.pg_settings
101            ",
102        )
103        .fetch_all(pool)
104        .await
105    }
106
107    pub async fn get<N: AsRef<str>>(
108        name: N,
109        pool: &sqlx::PgPool,
110    ) -> Result<Option<Self>, sqlx::Error> {
111        sqlx::query_as(
112            r"
113            SELECT
114                name,
115                setting,
116                unit,
117                category,
118                short_desc,
119                extra_desc,
120                context,
121                vartype,
122                source,
123                min_val,
124                max_val,
125                enumvals,
126                boot_val,
127                reset_val,
128                sourcefile,
129                sourceline,
130                pending_restart
131            FROM
132                pg_catalog.pg_settings
133            WHERE
134                name = $1
135            ",
136        )
137        .bind(name.as_ref())
138        .fetch_optional(pool)
139        .await
140    }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub struct Parameter<'a>(pub &'a str);
145
146impl Parameter<'_> {
147    /// Get the current [`Value`] for this parameter.
148    ///
149    /// If you want the full/raw [`Setting`], use [`Setting::get`] instead.
150    pub async fn get(&self, pool: &sqlx::PgPool) -> Result<Option<Value>, sqlx::Error> {
151        Setting::get(self.0, pool)
152            .await?
153            .map(|setting| {
154                Value::try_from(&setting)
155                    .map_err(Into::into)
156                    .map_err(sqlx::Error::Decode)
157            })
158            .transpose()
159    }
160
161    /// Set the current value for this parameter.
162    pub async fn set<V: Into<Value>>(
163        &self,
164        pool: &sqlx::PgPool,
165        value: V,
166    ) -> Result<(), sqlx::Error> {
167        let value = value.into();
168        AlterSystem::Set(self, &value).apply(pool).await?;
169        Ok(())
170    }
171
172    /// Reset the value for this parameter.
173    pub async fn reset(&self, pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
174        AlterSystem::Reset(self).apply(pool).await?;
175        Ok(())
176    }
177}
178
179impl AsSql for Parameter<'_> {
180    /// Return this parameter name escaped as an SQL identifier.
181    fn as_sql(&self) -> Cow<'_, str> {
182        escape_identifier(self.0).into()
183    }
184}
185
186impl fmt::Display for Parameter<'_> {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        write!(f, "{}", self.0)
189    }
190}
191
192impl AsRef<str> for Parameter<'_> {
193    fn as_ref(&self) -> &str {
194        self.0
195    }
196}
197
198impl<'a> From<&'a str> for Parameter<'a> {
199    fn from(name: &'a str) -> Self {
200        Self(name)
201    }
202}
203
204impl<'a> From<&'a Setting> for Parameter<'a> {
205    fn from(setting: &'a Setting) -> Self {
206        Self(&setting.name)
207    }
208}
209
210#[derive(Debug, PartialEq)]
211pub enum Value {
212    Boolean(bool),
213    String(String), // Or enumerated.
214    Number(String),
215    Memory(String, MemoryUnit),
216    Time(String, TimeUnit),
217}
218
219impl AsSql for Value {
220    /// Return this parameter value escaped as an SQL literal.
221    fn as_sql(&self) -> Cow<'_, str> {
222        match self {
223            Value::Boolean(true) => "true".into(),
224            Value::Boolean(false) => "false".into(),
225            Value::String(value) => escape_literal(value).into(),
226            Value::Number(value) => value.into(),
227            Value::Memory(value, unit) => escape_literal(&format!("{value}{unit}")).into(),
228            Value::Time(value, unit) => escape_literal(&format!("{value}{unit}")).into(),
229        }
230    }
231}
232
233impl fmt::Display for Value {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        match self {
236            Value::Boolean(value) => write!(f, "{value}"),
237            Value::String(value) => write!(f, "{value}"),
238            Value::Number(value) => write!(f, "{value}"),
239            Value::Memory(value, unit) => write!(f, "{value}{unit}"),
240            Value::Time(value, unit) => write!(f, "{value}{unit}"),
241        }
242    }
243}
244
245impl From<bool> for Value {
246    fn from(value: bool) -> Self {
247        Value::Boolean(value)
248    }
249}
250
251impl From<&str> for Value {
252    fn from(value: &str) -> Self {
253        Value::String(value.to_owned())
254    }
255}
256
257impl From<String> for Value {
258    fn from(value: String) -> Self {
259        Value::String(value)
260    }
261}
262
263impl From<&String> for Value {
264    fn from(value: &String) -> Self {
265        Value::String(value.clone())
266    }
267}
268
269macro_rules! value_number_from {
270    ($($from_type:ty),*) => {
271        $(
272            impl From<$from_type> for Value {
273                fn from(number: $from_type) -> Self {
274                    Value::Number(number.to_string())
275                }
276            }
277        )*
278    }
279}
280
281value_number_from!(i8, i16, i32, i64, i128);
282value_number_from!(u8, u16, u32, u64, u128);
283value_number_from!(f32, f64);
284value_number_from!(usize, isize);
285
286macro_rules! value_memory_from {
287    ($($from_type:ty),*) => {
288        $(
289            impl From<($from_type, MemoryUnit)> for Value {
290                fn from((number, unit): ($from_type, MemoryUnit)) -> Self {
291                    Value::Memory(number.to_string(), unit)
292                }
293            }
294        )*
295    }
296}
297
298value_memory_from!(i8, i16, i32, i64, i128);
299value_memory_from!(u8, u16, u32, u64, u128);
300value_memory_from!(f32, f64);
301value_memory_from!(usize, isize);
302
303macro_rules! value_time_from {
304    ($($from_type:ty),*) => {
305        $(
306            impl From<($from_type, TimeUnit)> for Value {
307                fn from((number, unit): ($from_type, TimeUnit)) -> Self {
308                    Value::Time(number.to_string(), unit)
309                }
310            }
311        )*
312    }
313}
314
315value_time_from!(i8, i16, i32, i64, i128);
316value_time_from!(u8, u16, u32, u64, u128);
317value_time_from!(f32, f64);
318value_time_from!(usize, isize);
319
320impl TryFrom<&Setting> for Value {
321    type Error = String;
322
323    fn try_from(setting: &Setting) -> Result<Self, Self::Error> {
324        Ok(match setting.vartype.as_ref() {
325            "bool" => match setting.setting.as_ref() {
326                "on" | "true" | "tru" | "tr" | "t" => Self::Boolean(true),
327                "yes" | "ye" | "y" | "1" => Self::Boolean(true),
328                "off" | "of" | "false" | "fals" | "fal" | "fa" | "f" => Self::Boolean(false),
329                "no" | "n" | "0" => Self::Boolean(false),
330                _ => return Err(format!("invalid boolean value: {setting:?}")),
331            },
332            "integer" | "real" => match setting.unit.as_deref() {
333                None => Self::Number(setting.setting.clone()),
334                Some("8kB" | "16MB") => Self::Number(setting.setting.clone()), // Special cases 🤷
335                Some(unit) => {
336                    if let Ok(unit) = unit.parse::<MemoryUnit>() {
337                        Self::Memory(setting.setting.clone(), unit)
338                    } else if let Ok(unit) = unit.parse::<TimeUnit>() {
339                        Self::Time(setting.setting.clone(), unit)
340                    } else {
341                        return Err(format!("invalid numeric value: {setting:?}"));
342                    }
343                }
344            },
345            "string" => Self::String(setting.setting.clone()),
346            "enum" => Self::String(setting.setting.clone()),
347            _ => return Err(format!("unrecognised value type: {setting:?}")),
348        })
349    }
350}
351
352/// Memory units recognised in PostgreSQL parameter values.
353/// <https://www.postgresql.org/docs/16/config-setting.html#CONFIG-SETTING-NAMES-VALUES>
354#[derive(Debug, Clone, Copy, PartialEq)]
355pub enum MemoryUnit {
356    Bytes,
357    Kibibytes,
358    Mebibytes,
359    Gibibytes,
360    Tebibytes,
361}
362
363impl fmt::Display for MemoryUnit {
364    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365        match self {
366            MemoryUnit::Bytes => write!(f, "B"),
367            MemoryUnit::Kibibytes => write!(f, "kB"),
368            MemoryUnit::Mebibytes => write!(f, "MB"),
369            MemoryUnit::Gibibytes => write!(f, "GB"),
370            MemoryUnit::Tebibytes => write!(f, "TB"),
371        }
372    }
373}
374
375impl FromStr for MemoryUnit {
376    type Err = String;
377
378    fn from_str(s: &str) -> Result<Self, Self::Err> {
379        match s {
380            "B" => Ok(MemoryUnit::Bytes),
381            "kB" => Ok(MemoryUnit::Kibibytes),
382            "MB" => Ok(MemoryUnit::Mebibytes),
383            "GB" => Ok(MemoryUnit::Gibibytes),
384            "TB" => Ok(MemoryUnit::Tebibytes),
385            _ => Err(format!("invalid memory unit: {s:?}")),
386        }
387    }
388}
389
390/// Time units recognised in PostgreSQL parameter values.
391/// <https://www.postgresql.org/docs/16/config-setting.html#CONFIG-SETTING-NAMES-VALUES>
392#[derive(Debug, Clone, Copy, PartialEq)]
393pub enum TimeUnit {
394    Microseconds,
395    Milliseconds,
396    Seconds,
397    Minutes,
398    Hours,
399    Days,
400}
401
402impl fmt::Display for TimeUnit {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        match self {
405            TimeUnit::Microseconds => write!(f, "us"),
406            TimeUnit::Milliseconds => write!(f, "ms"),
407            TimeUnit::Seconds => write!(f, "s"),
408            TimeUnit::Minutes => write!(f, "min"),
409            TimeUnit::Hours => write!(f, "h"),
410            TimeUnit::Days => write!(f, "d"),
411        }
412    }
413}
414
415impl FromStr for TimeUnit {
416    type Err = String;
417
418    fn from_str(s: &str) -> Result<Self, Self::Err> {
419        match s {
420            "us" => Ok(TimeUnit::Microseconds),
421            "ms" => Ok(TimeUnit::Milliseconds),
422            "s" => Ok(TimeUnit::Seconds),
423            "min" => Ok(TimeUnit::Minutes),
424            "h" => Ok(TimeUnit::Hours),
425            "d" => Ok(TimeUnit::Days),
426            _ => Err(format!("invalid time unit: {s:?}")),
427        }
428    }
429}
430
431#[cfg(test)]
432mod tests {
433    use paste::paste;
434
435    use super::{
436        AsSql,
437        MemoryUnit::{self, *},
438        Parameter,
439        TimeUnit::{self, *},
440        Value,
441    };
442
443    #[test]
444    fn test_parameter_as_sql() {
445        assert_eq!(Parameter("foo").as_sql(), "\"foo\"");
446        assert_eq!(Parameter("foo \\bar").as_sql(), "\"foo \\bar\"");
447        assert_eq!(Parameter("foo\"bar").as_sql(), "\"foo\"\"bar\"");
448    }
449
450    #[test]
451    fn test_value_as_sql_bool() {
452        assert_eq!(Value::Boolean(false).as_sql(), "false");
453        assert_eq!(Value::Boolean(true).as_sql(), "true");
454    }
455
456    #[test]
457    fn test_value_as_sql_string() {
458        assert_eq!(Value::from("foo").as_sql(), "'foo'");
459        assert_eq!(Value::from("foo \\bar").as_sql(), " E'foo \\\\bar'");
460        assert_eq!(Value::from("foo'\"'bar").as_sql(), "'foo''\"''bar'");
461    }
462
463    #[test]
464    fn test_value_as_sql_number() {
465        // Numbers are represented as strings, and displayed verbatim, with no
466        // escaping. Not ideal. An alternative would be to have signed/unsigned
467        // integers (as i128/u128) and floating points (as f64) separately. But
468        // PostgreSQL also has arbitrary precision numbers. For now, we'll live
469        // with this.
470        assert_eq!(Value::Number("123".into()).as_sql(), "123");
471        assert_eq!(Value::Number("123.456".into()).as_sql(), "123.456");
472    }
473
474    #[test]
475    fn test_value_as_sql_memory() {
476        assert_eq!(
477            Value::Memory("123.4".into(), Gibibytes).as_sql(),
478            "'123.4GB'",
479        );
480    }
481
482    #[test]
483    fn test_value_as_sql_time() {
484        assert_eq!(Value::Time("123.4".into(), Hours).as_sql(), "'123.4h'",);
485    }
486
487    macro_rules! test_value_number_from {
488        ($($from_type:ty),*) => {
489            $(
490                paste! {
491                    #[test]
492                    #[allow(clippy::cast_precision_loss, clippy::cast_lossless)]
493                    fn [< test_value_number_from_ $from_type >]() {
494                        assert_eq!(Value::from(42 as $from_type), Value::Number("42".into()));
495                    }
496                }
497            )*
498        }
499    }
500
501    test_value_number_from!(i8, i16, i32, i64, i128);
502    test_value_number_from!(u8, u16, u32, u64, u128);
503    test_value_number_from!(f32, f64);
504    test_value_number_from!(usize, isize);
505
506    #[test]
507    fn test_memory_unit_roundtrip() {
508        let units = &[Bytes, Kibibytes, Mebibytes, Gibibytes, Tebibytes];
509        for unit in units {
510            assert_eq!(format!("{unit}").parse::<MemoryUnit>(), Ok(*unit));
511        }
512    }
513
514    #[test]
515    fn test_time_unit_roundtrip() {
516        let units = &[Microseconds, Milliseconds, Seconds, Minutes, Hours, Days];
517        for unit in units {
518            assert_eq!(format!("{unit}").parse::<TimeUnit>(), Ok(*unit));
519        }
520    }
521}