Skip to main content

hyperdb_api/
connection_builder.rs

1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4use std::path::{Path, PathBuf};
5use std::time::Duration;
6
7use crate::connection::{Connection, CreateMode};
8use crate::error::{Error, Result};
9use crate::transport::{detect_transport_type, Transport, TransportType};
10use hyperdb_api_core::client::Config;
11
12/// A builder for creating database connections.
13///
14/// This provides a flexible way to configure a connection with various options
15/// like authentication, database creation mode, and transport settings.
16///
17/// # Transport Auto-Detection
18///
19/// The transport is automatically detected from the endpoint URL:
20/// - `https://` or `http://` → gRPC transport (read-only)
21/// - Otherwise → TCP transport (e.g., `localhost:7483`)
22///
23/// # Example
24///
25/// ```no_run
26/// use hyperdb_api::{ConnectionBuilder, CreateMode, Result};
27///
28/// fn main() -> Result<()> {
29///     // TCP connection
30///     let conn = ConnectionBuilder::new("localhost:7483")
31///         .database("example.hyper")
32///         .create_mode(CreateMode::CreateIfNotExists)
33///         .user("myuser")
34///         .password("mypassword")
35///         .build()?;
36///     Ok(())
37/// }
38/// ```
39///
40/// ```no_run
41/// # use hyperdb_api::{ConnectionBuilder, Result};
42/// # fn example() -> Result<()> {
43/// // gRPC connection (auto-detected from URL)
44/// let conn = ConnectionBuilder::new("https://hyper-server.example.com:443")
45///     .database("example.hyper")
46///     .build()?;
47/// # Ok(())
48/// # }
49/// ```
50#[derive(Debug, Clone)]
51pub struct ConnectionBuilder {
52    endpoint: String,
53    database: Option<PathBuf>,
54    create_mode: CreateMode,
55    user: Option<String>,
56    password: Option<String>,
57    login_timeout: Option<Duration>,
58    /// Query timeout — cancel queries that exceed this duration.
59    query_timeout: Option<Duration>,
60    /// Application name sent to the server during connection startup.
61    application_name: Option<String>,
62    /// Transfer mode for gRPC connections (ignored for TCP)
63    transfer_mode: Option<hyperdb_api_core::client::grpc::TransferMode>,
64}
65
66impl Default for ConnectionBuilder {
67    fn default() -> Self {
68        Self::new("localhost:7483")
69    }
70}
71
72impl ConnectionBuilder {
73    /// Creates a new builder for the given endpoint.
74    ///
75    /// # Arguments
76    ///
77    /// * `endpoint` - The server endpoint (host:port).
78    pub fn new(endpoint: impl Into<String>) -> Self {
79        Self {
80            endpoint: endpoint.into(),
81            database: None,
82            create_mode: CreateMode::default(),
83            user: Some("tableau_internal_user".to_string()),
84            password: None,
85            login_timeout: None,
86            query_timeout: None,
87            application_name: None,
88            transfer_mode: None, // Use default (Adaptive)
89        }
90    }
91
92    #[must_use]
93    /// Sets the database path.
94    pub fn database(mut self, path: impl AsRef<Path>) -> Self {
95        self.database = Some(path.as_ref().to_path_buf());
96        self
97    }
98
99    /// Sets the database creation mode.
100    ///
101    /// Default is `CreateMode::DoNotCreate`.
102    #[must_use]
103    pub fn create_mode(mut self, mode: CreateMode) -> Self {
104        self.create_mode = mode;
105        self
106    }
107
108    #[must_use]
109    /// Sets the username for authentication.
110    ///
111    /// Default is "`tableau_internal_user`".
112    pub fn user(mut self, user: impl Into<String>) -> Self {
113        self.user = Some(user.into());
114        self
115    }
116
117    #[must_use]
118    /// Sets the password for authentication.
119    pub fn password(mut self, password: impl Into<String>) -> Self {
120        self.password = Some(password.into());
121        self
122    }
123
124    /// Sets the login timeout.
125    #[must_use]
126    pub fn login_timeout(mut self, timeout: Duration) -> Self {
127        self.login_timeout = Some(timeout);
128        self
129    }
130
131    /// Sets the query timeout.
132    ///
133    /// Queries that exceed this duration will be cancelled automatically.
134    /// Default is no timeout (queries run until completion).
135    #[must_use]
136    pub fn query_timeout(mut self, timeout: Duration) -> Self {
137        self.query_timeout = Some(timeout);
138        self
139    }
140
141    #[must_use]
142    /// Sets the application name sent to the server.
143    ///
144    /// This appears in server logs and can be used for monitoring.
145    pub fn application_name(mut self, name: impl Into<String>) -> Self {
146        self.application_name = Some(name.into());
147        self
148    }
149
150    #[must_use]
151    /// Convenience method to set user and password at once.
152    pub fn auth(mut self, user: impl Into<String>, password: impl Into<String>) -> Self {
153        self.user = Some(user.into());
154        self.password = Some(password.into());
155        self
156    }
157
158    #[must_use]
159    /// Convenience method to create a new database.
160    pub fn create_new_database(mut self, database_path: impl AsRef<Path>) -> Self {
161        self.database = Some(database_path.as_ref().to_path_buf());
162        self.create_mode = CreateMode::Create;
163        self
164    }
165
166    #[must_use]
167    /// Convenience method to create database if it doesn't exist.
168    pub fn create_or_open_database(mut self, database_path: impl AsRef<Path>) -> Self {
169        self.database = Some(database_path.as_ref().to_path_buf());
170        self.create_mode = CreateMode::CreateIfNotExists;
171        self
172    }
173
174    #[must_use]
175    /// Convenience method to open an existing database.
176    pub fn open_database(mut self, database_path: impl AsRef<Path>) -> Self {
177        self.database = Some(database_path.as_ref().to_path_buf());
178        self.create_mode = CreateMode::DoNotCreate;
179        self
180    }
181
182    /// Sets the transfer mode for gRPC connections.
183    ///
184    /// This setting is ignored for TCP connections.
185    ///
186    /// - `TransferMode::Sync` - All results in one response (simple, 100s timeout)
187    /// - `TransferMode::Async` - Header only, fetch results via `GetQueryResult`
188    /// - `TransferMode::Adaptive` - First chunk inline, rest streamed (default, recommended)
189    ///
190    /// # Example
191    ///
192    /// ```no_run
193    /// use hyperdb_api::{ConnectionBuilder, CreateMode, Result};
194    /// use hyperdb_api::grpc::TransferMode;
195    ///
196    /// fn example() -> Result<()> {
197    ///     let conn = ConnectionBuilder::new("https://hyper-server:443")
198    ///         .database("example.hyper")
199    ///         .transfer_mode(TransferMode::Adaptive)
200    ///         .build()?;
201    ///     Ok(())
202    /// }
203    /// ```
204    #[must_use]
205    pub fn transfer_mode(mut self, mode: hyperdb_api_core::client::grpc::TransferMode) -> Self {
206        self.transfer_mode = Some(mode);
207        self
208    }
209
210    /// Builds and establishes the connection.
211    ///
212    /// The transport is automatically detected from the endpoint URL:
213    /// - `https://` or `http://` → gRPC transport
214    /// - Otherwise → TCP transport
215    ///
216    /// # Errors
217    ///
218    /// Returns an error if the connection fails or if database creation fails.
219    pub fn build(self) -> Result<Connection> {
220        let transport_type = detect_transport_type(&self.endpoint);
221
222        match transport_type {
223            TransportType::Tcp => self.build_tcp(),
224            #[cfg(unix)]
225            TransportType::UnixSocket => self.build_unix(),
226            #[cfg(windows)]
227            TransportType::NamedPipe => self.build_named_pipe(),
228            TransportType::Grpc => self.build_grpc(),
229        }
230    }
231
232    /// Build a TCP connection.
233    fn build_tcp(self) -> Result<Connection> {
234        let mut config: Config = self
235            .endpoint
236            .parse()
237            .map_err(|e| Error::new(format!("invalid endpoint: {e}")))?;
238
239        if let Some(user) = &self.user {
240            config = config.with_user(user);
241        }
242
243        if let Some(password) = &self.password {
244            config = config.with_password(password);
245        }
246
247        if let Some(ref app_name) = self.application_name {
248            config = config.with_application_name(app_name);
249        }
250
251        if let Some(timeout) = self.login_timeout {
252            config = config.with_connect_timeout(timeout);
253        }
254
255        let db_path_str = self
256            .database
257            .as_ref()
258            .map(|p| p.to_string_lossy().to_string());
259
260        let client = hyperdb_api_core::client::Client::connect(&config)?;
261
262        let conn = Connection::from_client(client, db_path_str.clone());
263
264        // Handle database creation (TCP only - gRPC is read-only)
265        if let Some(db_path) = db_path_str {
266            conn.handle_creation_mode(&db_path, self.create_mode)?;
267            conn.attach_and_set_path(&db_path)?;
268        }
269
270        Ok(conn)
271    }
272
273    /// Build a Unix Domain Socket connection (Unix only).
274    #[cfg(unix)]
275    fn build_unix(self) -> Result<Connection> {
276        use hyperdb_api_core::client::ConnectionEndpoint;
277
278        // Parse the endpoint to get the socket path
279        let socket_path = if self.endpoint.starts_with("tab.domain://") {
280            // Format: tab.domain://<dir>/domain/<name>
281            let endpoint = ConnectionEndpoint::parse(&self.endpoint)
282                .map_err(|e| Error::new(format!("invalid Unix socket endpoint: {e}")))?;
283            match endpoint {
284                ConnectionEndpoint::DomainSocket { directory, name } => directory.join(&name),
285                ConnectionEndpoint::Tcp { .. } => {
286                    return Err(Error::new("expected Unix domain socket endpoint"))
287                }
288            }
289        } else {
290            // Treat as direct socket path
291            std::path::PathBuf::from(&self.endpoint)
292        };
293
294        let mut config = hyperdb_api_core::client::Config::new();
295
296        if let Some(user) = &self.user {
297            config = config.with_user(user);
298        }
299
300        if let Some(password) = &self.password {
301            config = config.with_password(password);
302        }
303
304        let db_path_str = self
305            .database
306            .as_ref()
307            .map(|p| p.to_string_lossy().to_string());
308
309        // Connect via Unix socket
310        let client = hyperdb_api_core::client::Client::connect_unix(&socket_path, &config)?;
311
312        let conn = Connection::from_client(client, db_path_str.clone());
313
314        // Handle database creation
315        if let Some(db_path) = db_path_str {
316            conn.handle_creation_mode(&db_path, self.create_mode)?;
317            conn.attach_and_set_path(&db_path)?;
318        }
319
320        Ok(conn)
321    }
322
323    /// Build a Windows Named Pipe connection (Windows only).
324    #[cfg(windows)]
325    fn build_named_pipe(self) -> Result<Connection> {
326        use hyperdb_api_core::client::ConnectionEndpoint;
327
328        // Parse the endpoint to get the pipe path
329        let pipe_path = if self.endpoint.starts_with("tab.pipe://") {
330            // Format: tab.pipe://<host>/pipe/<name>
331            let endpoint = ConnectionEndpoint::parse(&self.endpoint)
332                .map_err(|e| Error::new(format!("invalid named pipe endpoint: {e}")))?;
333            match endpoint {
334                ConnectionEndpoint::NamedPipe { host, name } => {
335                    format!(r"\\{host}\pipe\{name}")
336                }
337                _ => return Err(Error::new("expected named pipe endpoint")),
338            }
339        } else {
340            // Treat as direct pipe path (e.g., \\.\pipe\hyper-12345)
341            self.endpoint.clone()
342        };
343
344        let mut config = hyperdb_api_core::client::Config::new();
345
346        if let Some(user) = &self.user {
347            config = config.with_user(user);
348        }
349
350        if let Some(password) = &self.password {
351            config = config.with_password(password);
352        }
353
354        let db_path_str = self
355            .database
356            .as_ref()
357            .map(|p| p.to_string_lossy().to_string());
358
359        // Connect via Named Pipe
360        let client = hyperdb_api_core::client::Client::connect_named_pipe(&pipe_path, &config)?;
361
362        let conn = Connection::from_client(client, db_path_str.clone());
363
364        // Handle database creation
365        if let Some(db_path) = db_path_str {
366            conn.handle_creation_mode(&db_path, self.create_mode)?;
367            conn.attach_and_set_path(&db_path)?;
368        }
369
370        Ok(conn)
371    }
372
373    /// Build a gRPC connection.
374    fn build_grpc(self) -> Result<Connection> {
375        // Validate create_mode - gRPC is read-only
376        if self.create_mode != CreateMode::DoNotCreate {
377            return Err(Error::new(
378                "gRPC transport is read-only. Use CreateMode::DoNotCreate for gRPC connections.",
379            ));
380        }
381
382        let db_path_str = self
383            .database
384            .as_ref()
385            .map(|p| p.to_string_lossy().to_string());
386
387        // Build gRPC config
388        let mut grpc_config = hyperdb_api_core::client::grpc::GrpcConfig::new(&self.endpoint);
389
390        if let Some(ref db_path) = db_path_str {
391            grpc_config = grpc_config.database(db_path);
392        }
393
394        // Apply transfer mode if specified
395        if let Some(mode) = self.transfer_mode {
396            grpc_config = grpc_config.transfer_mode(mode);
397        }
398
399        // Connect via gRPC
400        let transport = Transport::connect_grpc(grpc_config)?;
401
402        Ok(Connection::from_transport(transport, db_path_str))
403    }
404}