oliphaunt_wasix/oliphaunt/
builder.rs1use std::path::PathBuf;
2
3use anyhow::{Result, bail};
4
5#[cfg(feature = "extensions")]
6use crate::oliphaunt::base::install_missing_extension_archives;
7use crate::oliphaunt::base::{PreparedRoot, RootPlan, RootSource, RootTarget, prepare_root};
8use crate::oliphaunt::client::Oliphaunt;
9use crate::oliphaunt::config::{PostgresConfig, StartupConfig};
10#[cfg(feature = "extensions")]
11use crate::oliphaunt::extensions::{
12 Extension, postgres_config_with_extension_startup, resolve_extension_set,
13};
14use crate::oliphaunt::interface::DebugLevel;
15
16#[derive(Debug, Clone)]
18pub struct OliphauntBuilder {
19 target: Option<OliphauntTarget>,
20 template_cache: bool,
21 postgres_config: PostgresConfig,
22 startup_config: StartupConfig,
23 load_data_dir_archive: Option<Vec<u8>>,
24 #[cfg(feature = "extensions")]
25 extensions: Vec<Extension>,
26}
27
28#[derive(Debug, Clone)]
29enum OliphauntTarget {
30 Path(PathBuf),
31 AppId {
32 qualifier: String,
33 organization: String,
34 application: String,
35 },
36 Temporary,
37}
38
39impl Default for OliphauntBuilder {
40 fn default() -> Self {
41 Self {
42 target: None,
43 template_cache: true,
44 postgres_config: PostgresConfig::default(),
45 startup_config: StartupConfig::default(),
46 load_data_dir_archive: None,
47 #[cfg(feature = "extensions")]
48 extensions: Vec::new(),
49 }
50 }
51}
52
53impl OliphauntBuilder {
54 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn path(mut self, root: impl Into<PathBuf>) -> Self {
62 self.target = Some(OliphauntTarget::Path(root.into()));
63 self
64 }
65
66 pub fn app(
68 mut self,
69 qualifier: impl Into<String>,
70 organization: impl Into<String>,
71 application: impl Into<String>,
72 ) -> Self {
73 self.target = Some(OliphauntTarget::AppId {
74 qualifier: qualifier.into(),
75 organization: organization.into(),
76 application: application.into(),
77 });
78 self
79 }
80
81 pub fn app_id(self, app_id: (&str, &str, &str)) -> Self {
83 self.app(app_id.0, app_id.1, app_id.2)
84 }
85
86 pub fn temporary(mut self) -> Self {
91 self.target = Some(OliphauntTarget::Temporary);
92 self
93 }
94
95 pub fn template_cache(mut self, enabled: bool) -> Self {
98 self.template_cache = enabled;
99 self
100 }
101
102 pub fn fresh_temporary(self) -> Self {
109 self.temporary().template_cache(false)
110 }
111
112 pub fn postgres_config(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
114 self.postgres_config.insert(name, value);
115 self
116 }
117
118 pub fn postgres_configs<K, V>(mut self, settings: impl IntoIterator<Item = (K, V)>) -> Self
120 where
121 K: Into<String>,
122 V: Into<String>,
123 {
124 for (name, value) in settings {
125 self.postgres_config.insert(name, value);
126 }
127 self
128 }
129
130 pub fn username(mut self, username: impl Into<String>) -> Self {
133 self.startup_config.username = username.into();
134 self
135 }
136
137 pub fn database(mut self, database: impl Into<String>) -> Self {
140 self.startup_config.database = database.into();
141 self
142 }
143
144 pub fn debug_level(mut self, level: DebugLevel) -> Self {
146 self.startup_config.debug_level = Some(level);
147 self
148 }
149
150 pub fn relaxed_durability(mut self, enabled: bool) -> Self {
153 self.startup_config.relaxed_durability = enabled;
154 self
155 }
156
157 pub fn startup_arg(mut self, arg: impl Into<String>) -> Self {
160 self.startup_config.extra_args.push(arg.into());
161 self
162 }
163
164 pub fn startup_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
166 self.startup_config
167 .extra_args
168 .extend(args.into_iter().map(Into::into));
169 self
170 }
171
172 pub fn load_data_dir_archive(mut self, archive: impl Into<Vec<u8>>) -> Self {
174 self.load_data_dir_archive = Some(archive.into());
175 self
176 }
177
178 #[cfg(feature = "extensions")]
180 pub fn extension(mut self, extension: Extension) -> Self {
181 self.extensions.push(extension);
182 self
183 }
184
185 #[cfg(feature = "extensions")]
187 pub fn extensions(mut self, extensions: impl IntoIterator<Item = Extension>) -> Self {
188 self.extensions.extend(extensions);
189 self
190 }
191
192 pub fn open(self) -> Result<Oliphaunt> {
194 #[cfg(feature = "extensions")]
195 let (extensions, postgres_config) = self.resolved_extension_startup()?;
196 #[cfg(not(feature = "extensions"))]
197 let postgres_config = self.postgres_config.clone();
198 postgres_config.validate()?;
199 self.startup_config.validate()?;
200 let target = match self.target.clone() {
201 Some(OliphauntTarget::Path(root)) => RootTarget::Path(root),
202 Some(OliphauntTarget::AppId {
203 qualifier,
204 organization,
205 application,
206 }) => RootTarget::AppId {
207 qualifier,
208 organization,
209 application,
210 },
211 Some(OliphauntTarget::Temporary) => RootTarget::Temporary,
212 None => {
213 bail!(
214 "OliphauntBuilder target is not set; call path, app_id, or temporary before open"
215 )
216 }
217 };
218 let source = if let Some(archive) = self.load_data_dir_archive.clone() {
219 RootSource::DataDirArchive(archive)
220 } else if self.template_cache {
221 RootSource::Template
222 } else {
223 RootSource::FreshInitdb
224 };
225 let plan = RootPlan::new(target, source);
226 #[cfg(feature = "extensions")]
227 let plan = plan.with_extensions(extensions.clone(), postgres_config.clone());
228 let prepared = prepare_root(plan)?;
229 #[cfg(feature = "extensions")]
230 {
231 self.open_prepared_root(prepared, extensions, postgres_config)
232 }
233 #[cfg(not(feature = "extensions"))]
234 {
235 self.open_prepared_root(prepared, postgres_config)
236 }
237 }
238
239 #[cfg(feature = "extensions")]
240 fn resolved_extension_startup(&self) -> Result<(Vec<Extension>, PostgresConfig)> {
241 let extensions = resolve_extension_set(&self.extensions)?;
242 let postgres_config =
243 postgres_config_with_extension_startup(self.postgres_config.clone(), &extensions)?;
244 Ok((extensions, postgres_config))
245 }
246
247 fn open_prepared_root(
248 self,
249 prepared: PreparedRoot,
250 #[cfg(feature = "extensions")] extensions: Vec<Extension>,
251 postgres_config: PostgresConfig,
252 ) -> Result<Oliphaunt> {
253 let PreparedRoot {
254 temp_dir,
255 root_lock,
256 outcome,
257 ..
258 } = prepared;
259 #[cfg(feature = "extensions")]
260 install_missing_extension_archives(&outcome, &extensions)?;
261 #[cfg(feature = "extensions")]
262 let mut instance = Oliphaunt::new_prepared_with_config_and_extension_preload(
263 outcome,
264 postgres_config,
265 self.startup_config,
266 &extensions,
267 )?;
268 #[cfg(not(feature = "extensions"))]
269 let mut instance =
270 Oliphaunt::new_prepared_with_config(outcome, postgres_config, self.startup_config)?;
271 if let Some(lock) = root_lock {
272 instance.attach_root_lock(lock);
273 }
274 if let Some(temp_dir) = temp_dir {
275 instance.attach_temp_dir(temp_dir);
276 }
277 #[cfg(feature = "extensions")]
278 instance.enable_startup_extensions(&extensions)?;
279 Ok(instance)
280 }
281}
282
283#[cfg(all(test, feature = "extensions"))]
284mod tests {
285 use super::*;
286 use crate::oliphaunt::extensions::PG_TEXTSEARCH;
287
288 #[test]
289 fn direct_path_merges_pg_textsearch_preload_once_before_open() {
290 let builder = OliphauntBuilder::new()
291 .postgres_config("shared_preload_libraries", "auto_explain")
292 .postgres_config("work_mem", "16MB")
293 .extensions([PG_TEXTSEARCH, PG_TEXTSEARCH]);
294
295 let (_, postgres_config) = builder.resolved_extension_startup().unwrap();
296
297 assert_eq!(
298 postgres_config.get("shared_preload_libraries"),
299 Some("auto_explain,pg_textsearch")
300 );
301 assert_eq!(postgres_config.get("work_mem"), Some("16MB"));
302 assert_eq!(
303 postgres_config
304 .get("shared_preload_libraries")
305 .unwrap()
306 .split(',')
307 .filter(|library| *library == "pg_textsearch")
308 .count(),
309 1
310 );
311 }
312}