hyperdb-api 0.1.1

Pure Rust API for Hyper database
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::path::{Path, PathBuf};
use std::time::Duration;

use crate::connection::{Connection, CreateMode};
use crate::error::{Error, Result};
use crate::transport::{detect_transport_type, Transport, TransportType};
use hyperdb_api_core::client::Config;

/// A builder for creating database connections.
///
/// This provides a flexible way to configure a connection with various options
/// like authentication, database creation mode, and transport settings.
///
/// # Transport Auto-Detection
///
/// The transport is automatically detected from the endpoint URL:
/// - `https://` or `http://` → gRPC transport (read-only)
/// - Otherwise → TCP transport (e.g., `localhost:7483`)
///
/// # Example
///
/// ```no_run
/// use hyperdb_api::{ConnectionBuilder, CreateMode, Result};
///
/// fn main() -> Result<()> {
///     // TCP connection
///     let conn = ConnectionBuilder::new("localhost:7483")
///         .database("example.hyper")
///         .create_mode(CreateMode::CreateIfNotExists)
///         .user("myuser")
///         .password("mypassword")
///         .build()?;
///     Ok(())
/// }
/// ```
///
/// ```no_run
/// # use hyperdb_api::{ConnectionBuilder, Result};
/// # fn example() -> Result<()> {
/// // gRPC connection (auto-detected from URL)
/// let conn = ConnectionBuilder::new("https://hyper-server.example.com:443")
///     .database("example.hyper")
///     .build()?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
pub struct ConnectionBuilder {
    endpoint: String,
    database: Option<PathBuf>,
    create_mode: CreateMode,
    user: Option<String>,
    password: Option<String>,
    login_timeout: Option<Duration>,
    /// Query timeout — cancel queries that exceed this duration.
    query_timeout: Option<Duration>,
    /// Application name sent to the server during connection startup.
    application_name: Option<String>,
    /// Transfer mode for gRPC connections (ignored for TCP)
    transfer_mode: Option<hyperdb_api_core::client::grpc::TransferMode>,
}

impl Default for ConnectionBuilder {
    fn default() -> Self {
        Self::new("localhost:7483")
    }
}

impl ConnectionBuilder {
    /// Creates a new builder for the given endpoint.
    ///
    /// # Arguments
    ///
    /// * `endpoint` - The server endpoint (host:port).
    pub fn new(endpoint: impl Into<String>) -> Self {
        Self {
            endpoint: endpoint.into(),
            database: None,
            create_mode: CreateMode::default(),
            user: Some("tableau_internal_user".to_string()),
            password: None,
            login_timeout: None,
            query_timeout: None,
            application_name: None,
            transfer_mode: None, // Use default (Adaptive)
        }
    }

    #[must_use]
    /// Sets the database path.
    pub fn database(mut self, path: impl AsRef<Path>) -> Self {
        self.database = Some(path.as_ref().to_path_buf());
        self
    }

    /// Sets the database creation mode.
    ///
    /// Default is `CreateMode::DoNotCreate`.
    #[must_use]
    pub fn create_mode(mut self, mode: CreateMode) -> Self {
        self.create_mode = mode;
        self
    }

    #[must_use]
    /// Sets the username for authentication.
    ///
    /// Default is "`tableau_internal_user`".
    pub fn user(mut self, user: impl Into<String>) -> Self {
        self.user = Some(user.into());
        self
    }

    #[must_use]
    /// Sets the password for authentication.
    pub fn password(mut self, password: impl Into<String>) -> Self {
        self.password = Some(password.into());
        self
    }

    /// Sets the login timeout.
    #[must_use]
    pub fn login_timeout(mut self, timeout: Duration) -> Self {
        self.login_timeout = Some(timeout);
        self
    }

    /// Sets the query timeout.
    ///
    /// Queries that exceed this duration will be cancelled automatically.
    /// Default is no timeout (queries run until completion).
    #[must_use]
    pub fn query_timeout(mut self, timeout: Duration) -> Self {
        self.query_timeout = Some(timeout);
        self
    }

    #[must_use]
    /// Sets the application name sent to the server.
    ///
    /// This appears in server logs and can be used for monitoring.
    pub fn application_name(mut self, name: impl Into<String>) -> Self {
        self.application_name = Some(name.into());
        self
    }

    #[must_use]
    /// Convenience method to set user and password at once.
    pub fn auth(mut self, user: impl Into<String>, password: impl Into<String>) -> Self {
        self.user = Some(user.into());
        self.password = Some(password.into());
        self
    }

    #[must_use]
    /// Convenience method to create a new database.
    pub fn create_new_database(mut self, database_path: impl AsRef<Path>) -> Self {
        self.database = Some(database_path.as_ref().to_path_buf());
        self.create_mode = CreateMode::Create;
        self
    }

    #[must_use]
    /// Convenience method to create database if it doesn't exist.
    pub fn create_or_open_database(mut self, database_path: impl AsRef<Path>) -> Self {
        self.database = Some(database_path.as_ref().to_path_buf());
        self.create_mode = CreateMode::CreateIfNotExists;
        self
    }

    #[must_use]
    /// Convenience method to open an existing database.
    pub fn open_database(mut self, database_path: impl AsRef<Path>) -> Self {
        self.database = Some(database_path.as_ref().to_path_buf());
        self.create_mode = CreateMode::DoNotCreate;
        self
    }

    /// Sets the transfer mode for gRPC connections.
    ///
    /// This setting is ignored for TCP connections.
    ///
    /// - `TransferMode::Sync` - All results in one response (simple, 100s timeout)
    /// - `TransferMode::Async` - Header only, fetch results via `GetQueryResult`
    /// - `TransferMode::Adaptive` - First chunk inline, rest streamed (default, recommended)
    ///
    /// # Example
    ///
    /// ```no_run
    /// use hyperdb_api::{ConnectionBuilder, CreateMode, Result};
    /// use hyperdb_api::grpc::TransferMode;
    ///
    /// fn example() -> Result<()> {
    ///     let conn = ConnectionBuilder::new("https://hyper-server:443")
    ///         .database("example.hyper")
    ///         .transfer_mode(TransferMode::Adaptive)
    ///         .build()?;
    ///     Ok(())
    /// }
    /// ```
    #[must_use]
    pub fn transfer_mode(mut self, mode: hyperdb_api_core::client::grpc::TransferMode) -> Self {
        self.transfer_mode = Some(mode);
        self
    }

    /// Builds and establishes the connection.
    ///
    /// The transport is automatically detected from the endpoint URL:
    /// - `https://` or `http://` → gRPC transport
    /// - Otherwise → TCP transport
    ///
    /// # Errors
    ///
    /// Returns an error if the connection fails or if database creation fails.
    pub fn build(self) -> Result<Connection> {
        let transport_type = detect_transport_type(&self.endpoint);

        match transport_type {
            TransportType::Tcp => self.build_tcp(),
            #[cfg(unix)]
            TransportType::UnixSocket => self.build_unix(),
            #[cfg(windows)]
            TransportType::NamedPipe => self.build_named_pipe(),
            TransportType::Grpc => self.build_grpc(),
        }
    }

    /// Build a TCP connection.
    fn build_tcp(self) -> Result<Connection> {
        let mut config: Config = self
            .endpoint
            .parse()
            .map_err(|e| Error::new(format!("invalid endpoint: {e}")))?;

        if let Some(user) = &self.user {
            config = config.with_user(user);
        }

        if let Some(password) = &self.password {
            config = config.with_password(password);
        }

        if let Some(ref app_name) = self.application_name {
            config = config.with_application_name(app_name);
        }

        if let Some(timeout) = self.login_timeout {
            config = config.with_connect_timeout(timeout);
        }

        let db_path_str = self
            .database
            .as_ref()
            .map(|p| p.to_string_lossy().to_string());

        let client = hyperdb_api_core::client::Client::connect(&config)?;

        let conn = Connection::from_client(client, db_path_str.clone());

        // Handle database creation (TCP only - gRPC is read-only)
        if let Some(db_path) = db_path_str {
            conn.handle_creation_mode(&db_path, self.create_mode)?;
            conn.attach_and_set_path(&db_path)?;
        }

        Ok(conn)
    }

    /// Build a Unix Domain Socket connection (Unix only).
    #[cfg(unix)]
    fn build_unix(self) -> Result<Connection> {
        use hyperdb_api_core::client::ConnectionEndpoint;

        // Parse the endpoint to get the socket path
        let socket_path = if self.endpoint.starts_with("tab.domain://") {
            // Format: tab.domain://<dir>/domain/<name>
            let endpoint = ConnectionEndpoint::parse(&self.endpoint)
                .map_err(|e| Error::new(format!("invalid Unix socket endpoint: {e}")))?;
            match endpoint {
                ConnectionEndpoint::DomainSocket { directory, name } => directory.join(&name),
                ConnectionEndpoint::Tcp { .. } => {
                    return Err(Error::new("expected Unix domain socket endpoint"))
                }
            }
        } else {
            // Treat as direct socket path
            std::path::PathBuf::from(&self.endpoint)
        };

        let mut config = hyperdb_api_core::client::Config::new();

        if let Some(user) = &self.user {
            config = config.with_user(user);
        }

        if let Some(password) = &self.password {
            config = config.with_password(password);
        }

        let db_path_str = self
            .database
            .as_ref()
            .map(|p| p.to_string_lossy().to_string());

        // Connect via Unix socket
        let client = hyperdb_api_core::client::Client::connect_unix(&socket_path, &config)?;

        let conn = Connection::from_client(client, db_path_str.clone());

        // Handle database creation
        if let Some(db_path) = db_path_str {
            conn.handle_creation_mode(&db_path, self.create_mode)?;
            conn.attach_and_set_path(&db_path)?;
        }

        Ok(conn)
    }

    /// Build a Windows Named Pipe connection (Windows only).
    #[cfg(windows)]
    fn build_named_pipe(self) -> Result<Connection> {
        use hyperdb_api_core::client::ConnectionEndpoint;

        // Parse the endpoint to get the pipe path
        let pipe_path = if self.endpoint.starts_with("tab.pipe://") {
            // Format: tab.pipe://<host>/pipe/<name>
            let endpoint = ConnectionEndpoint::parse(&self.endpoint)
                .map_err(|e| Error::new(format!("invalid named pipe endpoint: {e}")))?;
            match endpoint {
                ConnectionEndpoint::NamedPipe { host, name } => {
                    format!(r"\\{host}\pipe\{name}")
                }
                _ => return Err(Error::new("expected named pipe endpoint")),
            }
        } else {
            // Treat as direct pipe path (e.g., \\.\pipe\hyper-12345)
            self.endpoint.clone()
        };

        let mut config = hyperdb_api_core::client::Config::new();

        if let Some(user) = &self.user {
            config = config.with_user(user);
        }

        if let Some(password) = &self.password {
            config = config.with_password(password);
        }

        let db_path_str = self
            .database
            .as_ref()
            .map(|p| p.to_string_lossy().to_string());

        // Connect via Named Pipe
        let client = hyperdb_api_core::client::Client::connect_named_pipe(&pipe_path, &config)?;

        let conn = Connection::from_client(client, db_path_str.clone());

        // Handle database creation
        if let Some(db_path) = db_path_str {
            conn.handle_creation_mode(&db_path, self.create_mode)?;
            conn.attach_and_set_path(&db_path)?;
        }

        Ok(conn)
    }

    /// Build a gRPC connection.
    fn build_grpc(self) -> Result<Connection> {
        // Validate create_mode - gRPC is read-only
        if self.create_mode != CreateMode::DoNotCreate {
            return Err(Error::new(
                "gRPC transport is read-only. Use CreateMode::DoNotCreate for gRPC connections.",
            ));
        }

        let db_path_str = self
            .database
            .as_ref()
            .map(|p| p.to_string_lossy().to_string());

        // Build gRPC config
        let mut grpc_config = hyperdb_api_core::client::grpc::GrpcConfig::new(&self.endpoint);

        if let Some(ref db_path) = db_path_str {
            grpc_config = grpc_config.database(db_path);
        }

        // Apply transfer mode if specified
        if let Some(mode) = self.transfer_mode {
            grpc_config = grpc_config.transfer_mode(mode);
        }

        // Connect via gRPC
        let transport = Transport::connect_grpc(grpc_config)?;

        Ok(Connection::from_transport(transport, db_path_str))
    }
}