database_replicator/postgres/
extensions.rs

1// ABOUTME: Extension compatibility checking for PostgreSQL databases
2// ABOUTME: Validates that target has all required extensions and they are properly configured
3
4use anyhow::{Context, Result};
5use tokio_postgres::Client;
6
7#[derive(Debug, Clone)]
8pub struct Extension {
9    pub name: String,
10    pub version: String,
11}
12
13#[derive(Debug, Clone)]
14pub struct AvailableExtension {
15    pub name: String,
16    pub default_version: Option<String>,
17    pub installed_version: Option<String>,
18}
19
20/// Get list of installed extensions on a database
21pub async fn get_installed_extensions(client: &Client) -> Result<Vec<Extension>> {
22    let rows = client
23        .query(
24            "SELECT extname, extversion FROM pg_extension WHERE extname != 'plpgsql' ORDER BY extname",
25            &[],
26        )
27        .await
28        .context("Failed to query installed extensions")?;
29
30    let extensions = rows
31        .iter()
32        .map(|row| Extension {
33            name: row.get(0),
34            version: row.get(1),
35        })
36        .collect();
37
38    Ok(extensions)
39}
40
41/// Get list of available extensions on a database
42pub async fn get_available_extensions(client: &Client) -> Result<Vec<AvailableExtension>> {
43    let rows = client
44        .query(
45            "SELECT name, default_version, installed_version FROM pg_available_extensions ORDER BY name",
46            &[],
47        )
48        .await
49        .context("Failed to query available extensions")?;
50
51    let extensions = rows
52        .iter()
53        .map(|row| AvailableExtension {
54            name: row.get(0),
55            default_version: row.get(1),
56            installed_version: row.get(2),
57        })
58        .collect();
59
60    Ok(extensions)
61}
62
63/// Get list of preloaded libraries from shared_preload_libraries setting
64pub async fn get_preloaded_libraries(client: &Client) -> Result<Vec<String>> {
65    let row = client
66        .query_one("SHOW shared_preload_libraries", &[])
67        .await
68        .context("Failed to query shared_preload_libraries")?;
69
70    let libs_str: String = row.get(0);
71
72    // Parse comma-separated list, handling spaces and empty strings
73    let libraries = libs_str
74        .split(',')
75        .map(|s| s.trim().to_string())
76        .filter(|s| !s.is_empty())
77        .collect();
78
79    Ok(libraries)
80}
81
82/// Extensions that require preloading via shared_preload_libraries
83const PRELOAD_REQUIRED_EXTENSIONS: &[&str] = &[
84    "timescaledb",
85    "citus",
86    "pg_stat_statements",
87    "pg_cron",
88    "auto_explain",
89    "pg_partman_bgw",
90];
91
92/// Check if an extension requires preloading
93pub fn requires_preload(extension_name: &str) -> bool {
94    PRELOAD_REQUIRED_EXTENSIONS.contains(&extension_name)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn test_requires_preload() {
103        assert!(requires_preload("timescaledb"));
104        assert!(requires_preload("citus"));
105        assert!(requires_preload("pg_stat_statements"));
106        assert!(!requires_preload("pg_trgm"));
107        assert!(!requires_preload("uuid-ossp"));
108    }
109}