Skip to main content

fraiseql_wire/client/
fraise_client.rs

1//! `FraiseClient` implementation
2
3use super::connection_string::{ConnectionInfo, TransportType};
4use super::query_builder::QueryBuilder;
5use crate::connection::{Connection, ConnectionConfig, Transport};
6use crate::stream::JsonStream;
7use crate::Result;
8use serde::de::DeserializeOwned;
9
10/// FraiseQL wire protocol client
11pub struct FraiseClient {
12    conn: Connection,
13}
14
15impl FraiseClient {
16    /// Connect to Postgres using connection string
17    ///
18    /// # Examples
19    ///
20    /// ```no_run
21    /// // Requires: live Postgres server.
22    /// # async fn example() -> fraiseql_wire::Result<()> {
23    /// use fraiseql_wire::FraiseClient;
24    ///
25    /// // TCP connection
26    /// let client = FraiseClient::connect("postgres://localhost/mydb").await?;
27    ///
28    /// // Unix socket
29    /// let client = FraiseClient::connect("postgres:///mydb").await?;
30    /// # Ok(())
31    /// # }
32    /// ```
33    ///
34    /// # Errors
35    ///
36    /// Returns `Error::Config` if the connection string is invalid or missing required fields.
37    /// Returns `Error::Io` if the TCP/Unix connection fails.
38    /// Returns `Error::Authentication` if startup authentication fails.
39    pub async fn connect(connection_string: &str) -> Result<Self> {
40        let info = ConnectionInfo::parse(connection_string)?;
41
42        let transport = match info.transport {
43            TransportType::Tcp => {
44                let host = info
45                    .host
46                    .as_ref()
47                    .ok_or_else(|| crate::Error::Config("TCP transport requires a host".into()))?;
48                let port = info
49                    .port
50                    .ok_or_else(|| crate::Error::Config("TCP transport requires a port".into()))?;
51                Transport::connect_tcp(host, port).await?
52            }
53            TransportType::Unix => {
54                let path = info.unix_socket.as_ref().ok_or_else(|| {
55                    crate::Error::Config("Unix transport requires a socket path".into())
56                })?;
57                Transport::connect_unix(path).await?
58            }
59        };
60
61        let mut conn = Connection::new(transport);
62        let config = info.to_config();
63        conn.startup(&config).await?;
64
65        Ok(Self { conn })
66    }
67
68    /// Connect to Postgres with TLS encryption
69    ///
70    /// TLS is configured independently from the connection string. The connection string
71    /// should contain the hostname and credentials (user/password), while TLS configuration
72    /// is provided separately via `TlsConfig`.
73    ///
74    /// # Examples
75    ///
76    /// ```no_run
77    /// // Requires: live Postgres server with TLS.
78    /// # async fn example() -> fraiseql_wire::Result<()> {
79    /// use fraiseql_wire::{FraiseClient, connection::TlsConfig};
80    ///
81    /// // Configure TLS with system root certificates
82    /// let tls = TlsConfig::builder()
83    ///     .verify_hostname(true)
84    ///     .build()?;
85    ///
86    /// // Connect with TLS
87    /// let client = FraiseClient::connect_tls("postgres://secure.db.example.com/mydb", tls).await?;
88    /// # Ok(())
89    /// # }
90    /// ```
91    ///
92    /// # Errors
93    ///
94    /// Returns `Error::Config` if the connection string is invalid, TLS is attempted over Unix socket,
95    /// or the server rejects the SSL request or TLS handshake fails.
96    /// Returns `Error::Io` if the TCP connection fails.
97    /// Returns `Error::Authentication` if startup authentication fails.
98    pub async fn connect_tls(
99        connection_string: &str,
100        tls_config: crate::connection::TlsConfig,
101    ) -> Result<Self> {
102        let info = ConnectionInfo::parse(connection_string)?;
103
104        let transport = match info.transport {
105            TransportType::Tcp => {
106                let host = info
107                    .host
108                    .as_ref()
109                    .ok_or_else(|| crate::Error::Config("TCP transport requires a host".into()))?;
110                let port = info
111                    .port
112                    .ok_or_else(|| crate::Error::Config("TCP transport requires a port".into()))?;
113                Transport::connect_tcp_tls(host, port, &tls_config).await?
114            }
115            TransportType::Unix => {
116                return Err(crate::Error::Config(
117                    "TLS is only supported for TCP connections".into(),
118                ));
119            }
120        };
121
122        let mut conn = Connection::new(transport);
123        let config = info.to_config();
124        conn.startup(&config).await?;
125
126        Ok(Self { conn })
127    }
128
129    /// Connect to Postgres with custom connection configuration
130    ///
131    /// This method allows you to configure timeouts, keepalive intervals, and other
132    /// connection options. The connection configuration is merged with parameters from
133    /// the connection string.
134    ///
135    /// # Examples
136    ///
137    /// ```no_run
138    /// // Requires: live Postgres server.
139    /// # async fn example() -> fraiseql_wire::Result<()> {
140    /// use fraiseql_wire::{FraiseClient, connection::ConnectionConfig};
141    /// use std::time::Duration;
142    ///
143    /// // Build connection configuration with timeouts
144    /// let config = ConnectionConfig::builder("localhost", "mydb")
145    ///     .password("secret")
146    ///     .statement_timeout(Duration::from_secs(30))
147    ///     .keepalive_idle(Duration::from_secs(300))
148    ///     .application_name("my_app")
149    ///     .build();
150    ///
151    /// // Connect with configuration
152    /// let client = FraiseClient::connect_with_config("postgres://localhost:5432/mydb", config).await?;
153    /// # Ok(())
154    /// # }
155    /// ```
156    ///
157    /// # Errors
158    ///
159    /// Returns `Error::Config` if the connection string is invalid or missing required fields.
160    /// Returns `Error::Io` if the TCP/Unix connection fails.
161    /// Returns `Error::Authentication` if startup authentication fails.
162    pub async fn connect_with_config(
163        connection_string: &str,
164        config: ConnectionConfig,
165    ) -> Result<Self> {
166        let info = ConnectionInfo::parse(connection_string)?;
167
168        let transport = match info.transport {
169            TransportType::Tcp => {
170                let host = info
171                    .host
172                    .as_ref()
173                    .ok_or_else(|| crate::Error::Config("TCP transport requires a host".into()))?;
174                let port = info
175                    .port
176                    .ok_or_else(|| crate::Error::Config("TCP transport requires a port".into()))?;
177                Transport::connect_tcp(host, port).await?
178            }
179            TransportType::Unix => {
180                let path = info.unix_socket.as_ref().ok_or_else(|| {
181                    crate::Error::Config("Unix transport requires a socket path".into())
182                })?;
183                Transport::connect_unix(path).await?
184            }
185        };
186
187        // Apply TCP keepalive when configured.
188        if let Some(idle) = config.keepalive_idle {
189            if let Err(e) = transport.apply_keepalive(idle) {
190                tracing::warn!("Failed to apply TCP keepalive (idle={idle:?}): {e}");
191            }
192        }
193
194        let mut conn = Connection::new(transport);
195        conn.startup(&config).await?;
196
197        Ok(Self { conn })
198    }
199
200    /// Connect to Postgres with both custom configuration and TLS encryption
201    ///
202    /// This method combines connection configuration (timeouts, keepalive, etc.)
203    /// with TLS encryption for secure connections with advanced options.
204    ///
205    /// # Examples
206    ///
207    /// ```no_run
208    /// // Requires: live Postgres server with TLS.
209    /// # async fn example() -> fraiseql_wire::Result<()> {
210    /// use fraiseql_wire::{FraiseClient, connection::{ConnectionConfig, TlsConfig}};
211    /// use std::time::Duration;
212    ///
213    /// // Configure connection with timeouts
214    /// let config = ConnectionConfig::builder("localhost", "mydb")
215    ///     .password("secret")
216    ///     .statement_timeout(Duration::from_secs(30))
217    ///     .build();
218    ///
219    /// // Configure TLS
220    /// let tls = TlsConfig::builder()
221    ///     .verify_hostname(true)
222    ///     .build()?;
223    ///
224    /// // Connect with both configuration and TLS
225    /// let client = FraiseClient::connect_with_config_and_tls(
226    ///     "postgres://secure.db.example.com/mydb",
227    ///     config,
228    ///     tls
229    /// ).await?;
230    /// # Ok(())
231    /// # }
232    /// ```
233    ///
234    /// # Errors
235    ///
236    /// Returns `Error::Config` if the connection string is invalid, TLS is attempted over Unix socket,
237    /// or the server rejects the SSL request or TLS handshake fails.
238    /// Returns `Error::Io` if the TCP connection fails.
239    /// Returns `Error::Authentication` if startup authentication fails.
240    pub async fn connect_with_config_and_tls(
241        connection_string: &str,
242        config: ConnectionConfig,
243        tls_config: crate::connection::TlsConfig,
244    ) -> Result<Self> {
245        let info = ConnectionInfo::parse(connection_string)?;
246
247        let transport = match info.transport {
248            TransportType::Tcp => {
249                let host = info
250                    .host
251                    .as_ref()
252                    .ok_or_else(|| crate::Error::Config("TCP transport requires a host".into()))?;
253                let port = info
254                    .port
255                    .ok_or_else(|| crate::Error::Config("TCP transport requires a port".into()))?;
256                Transport::connect_tcp_tls(host, port, &tls_config).await?
257            }
258            TransportType::Unix => {
259                return Err(crate::Error::Config(
260                    "TLS is only supported for TCP connections".into(),
261                ));
262            }
263        };
264
265        // Apply TCP keepalive when configured.
266        if let Some(idle) = config.keepalive_idle {
267            if let Err(e) = transport.apply_keepalive(idle) {
268                tracing::warn!("Failed to apply TCP keepalive (idle={idle:?}): {e}");
269            }
270        }
271
272        let mut conn = Connection::new(transport);
273        conn.startup(&config).await?;
274
275        Ok(Self { conn })
276    }
277
278    /// Start building a query for an entity with automatic deserialization
279    ///
280    /// The type parameter T controls consumer-side deserialization only.
281    /// Type T does NOT affect SQL generation, filtering, ordering, or wire protocol.
282    ///
283    /// # Examples
284    ///
285    /// Type-safe query (recommended):
286    /// ```no_run
287    /// // Requires: live Postgres server.
288    /// # async fn example(client: fraiseql_wire::FraiseClient) -> fraiseql_wire::Result<()> {
289    /// use serde::Deserialize;
290    /// use futures::stream::StreamExt;
291    ///
292    /// #[derive(Deserialize)]
293    /// struct User {
294    ///     id: String,
295    ///     name: String,
296    /// }
297    ///
298    /// let mut stream = client
299    ///     .query::<User>("user")
300    ///     .where_sql("data->>'type' = 'customer'")  // SQL predicate
301    ///     .where_rust(|json| {
302    ///         // Rust predicate (applied client-side, on JSON)
303    ///         json["estimated_value"].as_f64().unwrap_or(0.0) > 1000.0
304    ///     })
305    ///     .order_by("data->>'name' ASC")
306    ///     .execute()
307    ///     .await?;
308    ///
309    /// while let Some(result) = stream.next().await {
310    ///     let user: User = result?;
311    ///     println!("User: {}", user.name);
312    /// }
313    /// # Ok(())
314    /// # }
315    /// ```
316    ///
317    /// Raw JSON query (debugging, forward compatibility):
318    /// ```no_run
319    /// // Requires: live Postgres server.
320    /// # async fn example(client: fraiseql_wire::FraiseClient) -> fraiseql_wire::Result<()> {
321    /// use futures::stream::StreamExt;
322    ///
323    /// let mut stream = client
324    ///     .query::<serde_json::Value>("user")  // Escape hatch
325    ///     .execute()
326    ///     .await?;
327    ///
328    /// while let Some(result) = stream.next().await {
329    ///     let json = result?;
330    ///     println!("JSON: {:?}", json);
331    /// }
332    /// # Ok(())
333    /// # }
334    /// ```
335    pub fn query<T: DeserializeOwned + std::marker::Unpin + 'static>(
336        self,
337        entity: impl Into<String>,
338    ) -> QueryBuilder<T> {
339        QueryBuilder::new(self, entity)
340    }
341
342    /// Execute a raw SQL query (must match fraiseql-wire constraints)
343    pub(crate) async fn execute_query(
344        self,
345        sql: &str,
346        chunk_size: usize,
347        max_memory: Option<usize>,
348        soft_limit_warn_threshold: Option<f32>,
349        soft_limit_fail_threshold: Option<f32>,
350    ) -> Result<JsonStream> {
351        self.conn
352            .streaming_query(
353                sql,
354                chunk_size,
355                max_memory,
356                soft_limit_warn_threshold,
357                soft_limit_fail_threshold,
358                false, // enable_adaptive_chunking: disabled by default for backward compatibility
359                None,  // adaptive_min_chunk_size
360                None,  // adaptive_max_chunk_size
361            )
362            .await
363    }
364}