Skip to main content

oliphaunt_wasix/oliphaunt/
builder.rs

1use std::path::PathBuf;
2
3use anyhow::{Result, bail};
4
5use crate::oliphaunt::base::{PreparedRoot, RootPlan, RootSource, RootTarget, prepare_root};
6use crate::oliphaunt::client::Oliphaunt;
7use crate::oliphaunt::config::{PostgresConfig, StartupConfig};
8#[cfg(feature = "extensions")]
9use crate::oliphaunt::extensions::{Extension, resolve_extension_set};
10use crate::oliphaunt::interface::DebugLevel;
11
12/// Builder for opening persistent or temporary [`Oliphaunt`] databases.
13#[derive(Debug, Clone)]
14pub struct OliphauntBuilder {
15    target: Option<OliphauntTarget>,
16    template_cache: bool,
17    postgres_config: PostgresConfig,
18    startup_config: StartupConfig,
19    load_data_dir_archive: Option<Vec<u8>>,
20    #[cfg(feature = "extensions")]
21    extensions: Vec<Extension>,
22}
23
24#[derive(Debug, Clone)]
25enum OliphauntTarget {
26    Path(PathBuf),
27    AppId {
28        qualifier: String,
29        organization: String,
30        application: String,
31    },
32    Temporary,
33}
34
35impl Default for OliphauntBuilder {
36    fn default() -> Self {
37        Self {
38            target: None,
39            template_cache: true,
40            postgres_config: PostgresConfig::default(),
41            startup_config: StartupConfig::default(),
42            load_data_dir_archive: None,
43            #[cfg(feature = "extensions")]
44            extensions: Vec::new(),
45        }
46    }
47}
48
49impl OliphauntBuilder {
50    /// Create a builder. Call [`path`](Self::path), [`app_id`](Self::app_id),
51    /// or [`temporary`](Self::temporary) before [`open`](Self::open).
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Open a persistent database rooted at `root`.
57    pub fn path(mut self, root: impl Into<PathBuf>) -> Self {
58        self.target = Some(OliphauntTarget::Path(root.into()));
59        self
60    }
61
62    /// Open a persistent database under the platform data directory.
63    pub fn app(
64        mut self,
65        qualifier: impl Into<String>,
66        organization: impl Into<String>,
67        application: impl Into<String>,
68    ) -> Self {
69        self.target = Some(OliphauntTarget::AppId {
70            qualifier: qualifier.into(),
71            organization: organization.into(),
72            application: application.into(),
73        });
74        self
75    }
76
77    /// Open a persistent database under the platform data directory.
78    pub fn app_id(self, app_id: (&str, &str, &str)) -> Self {
79        self.app(app_id.0, app_id.1, app_id.2)
80    }
81
82    /// Open an ephemeral database removed when the instance is dropped.
83    ///
84    /// Temporary databases use the process-local template cluster cache by
85    /// default, avoiding repeated `initdb` work in test suites.
86    pub fn temporary(mut self) -> Self {
87        self.target = Some(OliphauntTarget::Temporary);
88        self
89    }
90
91    /// Control whether new databases are cloned from the process-local or
92    /// embedded PGDATA template cache.
93    pub fn template_cache(mut self, enabled: bool) -> Self {
94        self.template_cache = enabled;
95        self
96    }
97
98    /// Open an ephemeral database with a fresh `initdb`.
99    ///
100    /// This is a compatibility alias for
101    /// `temporary().template_cache(false)`. Fresh initdb uses the bundled split
102    /// WASIX `initdb` module; cached temporary databases remain the production
103    /// fast path.
104    pub fn fresh_temporary(self) -> Self {
105        self.temporary().template_cache(false)
106    }
107
108    /// Set a PostgreSQL startup GUC for this embedded backend.
109    pub fn postgres_config(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
110        self.postgres_config.insert(name, value);
111        self
112    }
113
114    /// Set multiple PostgreSQL startup GUCs for this embedded backend.
115    pub fn postgres_configs<K, V>(mut self, settings: impl IntoIterator<Item = (K, V)>) -> Self
116    where
117        K: Into<String>,
118        V: Into<String>,
119    {
120        for (name, value) in settings {
121            self.postgres_config.insert(name, value);
122        }
123        self
124    }
125
126    /// Connect as a PostgreSQL role. The role must already exist in the
127    /// cluster.
128    pub fn username(mut self, username: impl Into<String>) -> Self {
129        self.startup_config.username = username.into();
130        self
131    }
132
133    /// Connect to a PostgreSQL database. The database must already exist in the
134    /// cluster.
135    pub fn database(mut self, database: impl Into<String>) -> Self {
136        self.startup_config.database = database.into();
137        self
138    }
139
140    /// Enable PostgreSQL debug logging level `0..=5` for the embedded backend.
141    pub fn debug_level(mut self, level: DebugLevel) -> Self {
142        self.startup_config.debug_level = Some(level);
143        self
144    }
145
146    /// Use lower durability settings for ephemeral or cacheable local
147    /// workloads.
148    pub fn relaxed_durability(mut self, enabled: bool) -> Self {
149        self.startup_config.relaxed_durability = enabled;
150        self
151    }
152
153    /// Append an advanced PostgreSQL startup argument. Prefer
154    /// [`postgres_config`](Self::postgres_config) for GUCs.
155    pub fn startup_arg(mut self, arg: impl Into<String>) -> Self {
156        self.startup_config.extra_args.push(arg.into());
157        self
158    }
159
160    /// Append advanced PostgreSQL startup arguments.
161    pub fn startup_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
162        self.startup_config
163            .extra_args
164            .extend(args.into_iter().map(Into::into));
165        self
166    }
167
168    /// Load a previously dumped PGDATA tar archive before opening the database.
169    pub fn load_data_dir_archive(mut self, archive: impl Into<Vec<u8>>) -> Self {
170        self.load_data_dir_archive = Some(archive.into());
171        self
172    }
173
174    /// Enable a bundled Postgres extension before returning the database.
175    #[cfg(feature = "extensions")]
176    pub fn extension(mut self, extension: Extension) -> Self {
177        self.extensions.push(extension);
178        self
179    }
180
181    /// Enable bundled Postgres extensions before returning the database.
182    #[cfg(feature = "extensions")]
183    pub fn extensions(mut self, extensions: impl IntoIterator<Item = Extension>) -> Self {
184        self.extensions.extend(extensions);
185        self
186    }
187
188    /// Install, initialize, and start the selected database.
189    pub fn open(self) -> Result<Oliphaunt> {
190        self.postgres_config.validate()?;
191        self.startup_config.validate()?;
192        let target = match self.target.clone() {
193            Some(OliphauntTarget::Path(root)) => RootTarget::Path(root),
194            Some(OliphauntTarget::AppId {
195                qualifier,
196                organization,
197                application,
198            }) => RootTarget::AppId {
199                qualifier,
200                organization,
201                application,
202            },
203            Some(OliphauntTarget::Temporary) => RootTarget::Temporary,
204            None => {
205                bail!(
206                    "OliphauntBuilder target is not set; call path, app_id, or temporary before open"
207                )
208            }
209        };
210        let source = if let Some(archive) = self.load_data_dir_archive.clone() {
211            RootSource::DataDirArchive(archive)
212        } else if self.template_cache {
213            RootSource::Template
214        } else {
215            RootSource::FreshInitdb
216        };
217        #[cfg(feature = "extensions")]
218        let extensions = resolve_extension_set(&self.extensions)?;
219        let plan = RootPlan::new(target, source);
220        #[cfg(feature = "extensions")]
221        let plan = plan.with_extensions(extensions.clone(), self.postgres_config.clone());
222        let prepared = prepare_root(plan)?;
223        #[cfg(feature = "extensions")]
224        {
225            self.open_prepared_root(prepared, extensions)
226        }
227        #[cfg(not(feature = "extensions"))]
228        {
229            self.open_prepared_root(prepared)
230        }
231    }
232
233    fn open_prepared_root(
234        self,
235        prepared: PreparedRoot,
236        #[cfg(feature = "extensions")] extensions: Vec<Extension>,
237    ) -> Result<Oliphaunt> {
238        let PreparedRoot {
239            temp_dir,
240            root_lock,
241            outcome,
242            ..
243        } = prepared;
244        #[cfg(feature = "extensions")]
245        let preinstalled_extensions = outcome.preinstalled_extensions.clone();
246        let mut instance = Oliphaunt::new_prepared_with_config(
247            outcome,
248            self.postgres_config,
249            self.startup_config,
250        )?;
251        if let Some(lock) = root_lock {
252            instance.attach_root_lock(lock);
253        }
254        if let Some(temp_dir) = temp_dir {
255            instance.attach_temp_dir(temp_dir);
256        }
257        #[cfg(feature = "extensions")]
258        let mut instance = instance;
259        #[cfg(feature = "extensions")]
260        for extension in extensions {
261            if preinstalled_extensions
262                .iter()
263                .any(|sql_name| sql_name == extension.sql_name())
264            {
265                instance.enable_preinstalled_extension(extension)?;
266            } else {
267                instance.enable_extension(extension)?;
268            }
269        }
270        Ok(instance)
271    }
272}