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}